레슨 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에서 드로우콜 수를 확인할 수 있습니다.