Learning
레슨 7 / 8·25분

실전: 3D 갤러리

3D 갤러리 프로젝트

지금까지 배운 Three.js의 핵심 개념들을 조합하여 인터랙티브 3D 갤러리를 만듭니다. 씬 구성, 텍스처 로딩, 카메라 컨트롤, 레이캐스팅을 모두 활용하여 가상 갤러리 공간에 이미지 프레임을 배치하고 클릭 인터랙션을 구현합니다.

javascript
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

// 기본 씬 설정
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
scene.fog = new THREE.Fog(0x1a1a2e, 10, 30);

const camera = new THREE.PerspectiveCamera(
  60, window.innerWidth / window.innerHeight, 0.1, 100
);
camera.position.set(0, 2, 8);

const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);

// 조명
const ambient = new THREE.AmbientLight(0xffffff, 0.3);
scene.add(ambient);
const spotLight = new THREE.SpotLight(0xffffff, 1);
spotLight.position.set(0, 8, 0);
spotLight.castShadow = true;
scene.add(spotLight);

갤러리 공간과 이미지 프레임

javascript
// 바닥
const floor = new THREE.Mesh(
  new THREE.PlaneGeometry(20, 20),
  new THREE.MeshStandardMaterial({ color: 0x222233, roughness: 0.8 })
);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);

// 벽 생성
function createWall(w, h, pos, rot) {
  const wall = new THREE.Mesh(
    new THREE.PlaneGeometry(w, h),
    new THREE.MeshStandardMaterial({ color: 0x2a2a3e, side: THREE.DoubleSide })
  );
  wall.position.copy(pos);
  if (rot) wall.rotation.copy(rot);
  wall.receiveShadow = true;
  scene.add(wall);
}

createWall(20, 5, new THREE.Vector3(0, 2.5, -5));
createWall(10, 5, new THREE.Vector3(-10, 2.5, 0), new THREE.Euler(0, Math.PI / 2, 0));
createWall(10, 5, new THREE.Vector3(10, 2.5, 0), new THREE.Euler(0, -Math.PI / 2, 0));
javascript
// 이미지 프레임 생성
const loader = new THREE.TextureLoader();
const frames = [];

function createFrame(imageUrl, position) {
  const group = new THREE.Group();
  const frameMesh = new THREE.Mesh(
    new THREE.BoxGeometry(2.2, 1.7, 0.1),
    new THREE.MeshStandardMaterial({ color: 0xc0a060, metalness: 0.8, roughness: 0.3 })
  );
  frameMesh.castShadow = true;
  group.add(frameMesh);

  const imgMesh = new THREE.Mesh(
    new THREE.PlaneGeometry(1.9, 1.4),
    new THREE.MeshBasicMaterial({ map: loader.load(imageUrl) })
  );
  imgMesh.position.z = 0.06;
  imgMesh.userData.frameName = imageUrl;
  group.add(imgMesh);

  group.position.copy(position);
  scene.add(group);
  frames.push(imgMesh);
}

createFrame('/img/art1.jpg', new THREE.Vector3(-4, 2, -4.9));
createFrame('/img/art2.jpg', new THREE.Vector3(0, 2, -4.9));
createFrame('/img/art3.jpg', new THREE.Vector3(4, 2, -4.9));

// 클릭 감지
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
renderer.domElement.addEventListener('click', (e) => {
  mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
  raycaster.setFromCamera(mouse, camera);
  const hits = raycaster.intersectObjects(frames);
  if (hits.length > 0) {
    console.log('Selected:', hits[0].object.userData.frameName);
  }
});

// 렌더 루프
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.set(0, 2, 0);

function animate() {
  requestAnimationFrame(animate);
  controls.update();
  renderer.render(scene, camera);
}
animate();
  • Fog — 거리감을 주어 공간의 깊이감 표현
  • THREE.Group — 여러 메시를 하나로 묶어 함께 이동/회전
  • receiveShadow / castShadow — 그림자 설정
  • userData — 메시에 사용자 정의 데이터 저장
  • Raycaster + frames 배열로 특정 객체만 클릭 감지
💡

실전 프로젝트에서는 성능 최적화가 중요합니다. 동일한 머티리얼은 재사용하고, 화면에 보이지 않는 객체는 visible = false로 설정하세요. renderer.info.render에서 드로우콜 수를 확인할 수 있습니다.