Three.js는 WebGL을 추상화한 JavaScript 3D 그래픽 라이브러리다. 복잡한 WebGL API를 감싸 Scene, Camera, Renderer, Geometry, Material, Light 등 직관적인 API로 3D 씬을 구성한다. r0 (2010)부터 시작해 현재 WebGPU 렌더러까지 지원한다.
핵심 구조
씬 그래프(Scene Graph) 구조로 모든 3D 객체를 트리 형태로 관리한다.
Scene
├── Camera (PerspectiveCamera / OrthographicCamera)
├── Light (Directional / Point / Spot / Ambient / Hemisphere)
└── Mesh
├── Geometry (BufferGeometry)
└── Material (MeshStandardMaterial / ...)
- •Scene: 3D 객체들의 루트 컨테이너.
scene.add(object) / scene.remove(object) - •Renderer: GPU에 씬을 그리는 주체. WebGLRenderer가 기본, r163부터 WebGPURenderer 안정화
- •Mesh: Geometry + Material 조합. 실제로 화면에 보이는 단위
- •Object3D: Scene, Mesh, Group, Light 모두 Object3D를 상속. position / rotation / scale / matrix 공유
렌더러
javascript
const renderer = new THREE.WebGLRenderer({
antialias: true, // MSAA 활성화
alpha: false, // 투명 배경 여부
powerPreference: 'high-performance',
});
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // 고DPI 대응
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 부드러운 그림자
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
document.body.appendChild(renderer.domElement);
WebGPU 렌더러로 전환 시 import 경로만 변경:
javascript
import WebGPURenderer from 'three/addons/renderers/common/WebGPURenderer.js';
const renderer = new WebGPURenderer({ antialias: true });
카메라
PerspectiveCamera (원근 투영)
javascript
new THREE.PerspectiveCamera(fov, aspect, near, far)
// fov: 수직 시야각(도), aspect: 종횡비, near/far: 클리핑 거리
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 1000);
camera.position.set(0, 2, 5);
camera.lookAt(0, 0, 0);
| 파라미터 | 설명 | 권장값 |
|---|
| fov | 수직 시야각 | 45–90 |
| near | 근거리 클리핑 | 0.01–0.1 |
| far | 원거리 클리핑 | near의 10,000배 이하 |
near/far 비율이 너무 크면 Z-fighting 발생. near는 최대한 크게, far는 최대한 작게 유지.
OrthographicCamera (직교 투영)
원근감 없이 렌더링. UI, 2D 게임, 기술 도면에 사용.
javascript
new THREE.OrthographicCamera(left, right, top, bottom, near, far)
const aspect = innerWidth / innerHeight;
const camera = new THREE.OrthographicCamera(-aspect, aspect, 1, -1, 0.1, 100);
지오메트리
모든 지오메트리는 BufferGeometry를 기반으로 한다.
내장 지오메트리
javascript
new THREE.BoxGeometry(w, h, d, wSeg, hSeg, dSeg)
new THREE.SphereGeometry(radius, widthSeg, heightSeg)
new THREE.PlaneGeometry(w, h, wSeg, hSeg)
new THREE.CylinderGeometry(radiusTop, radiusBottom, height, radialSeg)
new THREE.TorusGeometry(radius, tube, radialSeg, tubularSeg)
new THREE.TorusKnotGeometry(radius, tube, tubularSeg, radialSeg, p, q)
new THREE.ConeGeometry(radius, height, radialSeg)
new THREE.IcosahedronGeometry(radius, detail) // 구체 근사
new THREE.LatheGeometry(points, segments) // 회전체
new THREE.ExtrudeGeometry(shape, options) // 2D → 3D 돌출
커스텀 BufferGeometry
정점 데이터를 TypedArray로 직접 지정. GPU에 최적화된 형태.
javascript
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array([
0, 1, 0, // 정점 0
-1, -1, 0, // 정점 1
1, -1, 0, // 정점 2
]);
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.computeVertexNormals(); // 법선 자동 계산
// 동적 업데이트
const attr = geometry.getAttribute('position');
attr.array[0] = Math.sin(t);
attr.needsUpdate = true; // GPU 재업로드 신호
머티리얼
라이팅 반응 여부와 연산 비용에 따라 선택.
| 머티리얼 | 조명 반응 | 특징 |
|---|
| MeshBasicMaterial | ✗ | 가장 가볍. 색상/텍스처만 |
| MeshLambertMaterial | ✓ | Gouraud 셰이딩. 저비용 |
| MeshPhongMaterial | ✓ | Phong 셰이딩. 스펙큘러 지원 |
| MeshStandardMaterial | ✓ | PBR. metalness/roughness |
| MeshPhysicalMaterial | ✓ | PBR 확장. clearcoat/transmission/IOR |
| MeshToonMaterial | ✓ | 셀 셰이딩 |
| ShaderMaterial | 커스텀 | GLSL 직접 작성 |
MeshStandardMaterial (PBR)
javascript
const material = new THREE.MeshStandardMaterial({
color: 0xffffff,
metalness: 0.8, // 0(비금속) ~ 1(금속)
roughness: 0.2, // 0(매끈) ~ 1(거칠음)
map: colorTexture, // 베이스 컬러
normalMap: normalTexture, // 표면 요철 (법선)
roughnessMap: roughTex,
metalnessMap: metalTex,
aoMap: aoTexture, // 앰비언트 오클루전
emissive: 0x000000,
emissiveIntensity: 1.0,
envMapIntensity: 1.0, // 환경 반사 강도
transparent: false,
opacity: 1.0,
side: THREE.FrontSide, // FrontSide / BackSide / DoubleSide
wireframe: false,
});
ShaderMaterial (커스텀 GLSL)
javascript
const material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uColor: { value: new THREE.Color(0xff0000) },
},
vertexShader: `
uniform float uTime;
void main() {
vec3 pos = position;
pos.y += sin(pos.x * 2.0 + uTime) * 0.2;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
`,
fragmentShader: `
uniform vec3 uColor;
void main() {
gl_FragColor = vec4(uColor, 1.0);
}
`,
});
// 애니메이션 루프에서 uniform 업데이트
material.uniforms.uTime.value = clock.getElapsedTime();
조명
javascript
// 전방향 환경광. 그림자 없음
const ambient = new THREE.AmbientLight(0xffffff, 0.4);
// 태양광. 평행광선. 그림자 지원
const dirLight = new THREE.DirectionalLight(0xffffff, 1.0);
dirLight.position.set(5, 10, 5);
dirLight.castShadow = true;
dirLight.shadow.mapSize.set(2048, 2048); // 그림자 해상도
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 50;
dirLight.shadow.camera.left = -10;
dirLight.shadow.camera.right = 10;
// 점광원. 사방으로 발산. 그림자 지원 (비용 높음)
const pointLight = new THREE.PointLight(0xff8800, 1.0, 20, 2); // color, intensity, distance, decay
// 원뿔형 조명. 스팟라이트
const spotLight = new THREE.SpotLight(0xffffff, 1.0);
spotLight.angle = Math.PI / 6;
spotLight.penumbra = 0.2; // 가장자리 부드러움
spotLight.castShadow = true;
// 하늘+땅 색상 그래디언트. 자연광 시뮬레이션
const hemiLight = new THREE.HemisphereLight(0x87ceeb, 0x3d2b1f, 0.6);
그림자를 받으려면 Mesh에 castShadow / receiveShadow 설정 필요:
javascript
mesh.castShadow = true;
floor.receiveShadow = true;
텍스처
javascript
const loader = new THREE.TextureLoader();
const texture = loader.load('/textures/color.jpg', (tex) => {
tex.wrapS = THREE.RepeatWrapping;
tex.wrapT = THREE.RepeatWrapping;
tex.repeat.set(4, 4);
tex.colorSpace = THREE.SRGBColorSpace; // 베이스 컬러 텍스처는 반드시 설정
});
// 큐브맵 (환경 반사)
const cubeLoader = new THREE.CubeTextureLoader();
const envMap = cubeLoader.load(['+x', '-x', '+y', '-y', '+z', '-z'].map(f => `/env/${f}.jpg`));
scene.environment = envMap; // 모든 PBR 머티리얼에 자동 적용
scene.background = envMap;
// HDRI (RGBELoader 사용)
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
new RGBELoader().load('/hdr/studio.hdr', (hdr) => {
hdr.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = hdr;
});
로더
| 로더 | 용도 |
|---|
| GLTFLoader | .gltf / .glb (권장 포맷. 애니메이션/머티리얼 포함) |
| FBXLoader | .fbx (모션 캡처 데이터에 주로 사용) |
| OBJLoader | .obj (단순 메시. 머티리얼 분리 파일) |
| DRACOLoader | GLTF의 메시 압축 해제 (GLTFLoader와 병용) |
| KTX2Loader | 압축 텍스처 (.ktx2) |
| RGBELoader | HDRI 환경맵 (.hdr) |
| SVGLoader | SVG → 2D 경로 |
GLTFLoader + Draco
javascript
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
const draco = new DRACOLoader();
draco.setDecoderPath('/draco/'); // 디코더 WASM 경로
const loader = new GLTFLoader();
loader.setDRACOLoader(draco);
loader.load('/models/character.glb', ({ scene: model, animations }) => {
scene.add(model);
// 모든 메시에 그림자 적용
model.traverse(obj => {
if (obj.isMesh) {
obj.castShadow = true;
obj.receiveShadow = true;
}
});
// 애니메이션 재생
const mixer = new THREE.AnimationMixer(model);
const action = mixer.clipAction(animations[0]);
action.play();
});
애니메이션 시스템
javascript
const mixer = new THREE.AnimationMixer(model);
const clock = new THREE.Clock();
// 클립 제어
const idleAction = mixer.clipAction(AnimationClip.findByName(animations, 'Idle'));
const walkAction = mixer.clipAction(AnimationClip.findByName(animations, 'Walk'));
idleAction.play();
// 블렌딩 전환
idleAction.fadeOut(0.5);
walkAction.reset().fadeIn(0.5).play();
// 루프 설정
action.loop = THREE.LoopRepeat; // 반복
action.loop = THREE.LoopOnce; // 1회
action.clampWhenFinished = true; // 마지막 프레임 유지
// 애니메이션 루프에서 반드시 업데이트
function animate() {
const delta = clock.getDelta();
mixer.update(delta);
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
Raycasting (마우스 피킹)
마우스 좌표 → 3D 광선 → 교점 감지.
javascript
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
window.addEventListener('click', (e) => {
// 정규화 좌표 (-1 ~ +1)
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const hits = raycaster.intersectObjects(scene.children, true); // true = 재귀
if (hits.length > 0) {
const { object, point, distance, face } = hits[0];
object.material.color.set(0xff0000);
}
});
성능 최적화
InstancedMesh — 동일 메시 대량 렌더링
Draw Call을 1번으로 줄인다.
javascript
const count = 10000;
const mesh = new THREE.InstancedMesh(geometry, material, count);
const matrix = new THREE.Matrix4();
for (let i = 0; i < count; i++) {
matrix.setPosition(
(Math.random() - 0.5) * 100,
(Math.random() - 0.5) * 100,
(Math.random() - 0.5) * 100
);
mesh.setMatrixAt(i, matrix);
}
mesh.instanceMatrix.needsUpdate = true;
scene.add(mesh);
메모리 해제
javascript
// 씬에서 제거 후 반드시 dispose
geometry.dispose();
material.dispose();
texture.dispose();
renderer.dispose();
기타 최적화 포인트
- •
renderer.setPixelRatio() — DPR 2 이상은 성능 저하 큼 - •
mesh.frustumCulled = true — 카메라 외 객체 자동 제외 (기본값) - •
geometry.index 사용으로 정점 재사용 - •텍스처 해상도를 2의 거듭제곱으로 유지 (512, 1024, 2048)
- •LOD (Level of Detail) — 거리에 따라 저해상도 메시 전환
기본 씬 설정
javascript
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 1000);
camera.position.set(0, 2, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// 메시
const cube = new THREE.Mesh(
new THREE.BoxGeometry(),
new THREE.MeshStandardMaterial({ color: 0x0088ff, roughness: 0.4, metalness: 0.6 })
);
cube.castShadow = true;
scene.add(cube);
// 바닥
const floor = new THREE.Mesh(
new THREE.PlaneGeometry(10, 10),
new THREE.MeshStandardMaterial({ color: 0x444444 })
);
floor.rotation.x = -Math.PI / 2;
floor.receiveShadow = true;
scene.add(floor);
// 조명
scene.add(new THREE.AmbientLight(0xffffff, 0.4));
const dirLight = new THREE.DirectionalLight(0xffffff, 1.0);
dirLight.position.set(5, 10, 5);
dirLight.castShadow = true;
scene.add(dirLight);
// 반응형
window.addEventListener('resize', () => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
cube.rotation.y += 0.01;
controls.update();
renderer.render(scene, camera);
}
animate();
관련 문서