섀도 매핑

Shadow mapping

광원 시점에서 한 번 더 렌더링해 "빛이 닿지 않는 곳"을 미리 기록해두고, 그걸로 그림자를 그리는 표준 기법.

다른 이름: castShadowreceiveShadowPCFSoftShadowMap
···
js
import * as THREE from 'three';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x14141f);
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
camera.position.set(3.4, 2.6, 4.2);
camera.lookAt(0, 0.4, 0);

scene.add(new THREE.AmbientLight(0x404060, 0.8));
const light = new THREE.DirectionalLight(0xffffff, 1.6);
light.position.set(3, 5, 2);
light.castShadow = true;
light.shadow.mapSize.set(1024, 1024);
light.shadow.camera.left = -4;
light.shadow.camera.right = 4;
light.shadow.camera.top = 4;
light.shadow.camera.bottom = -4;
light.shadow.camera.near = 0.5;
light.shadow.camera.far = 12;
light.shadow.radius = 4;
scene.add(light);

const ground = new THREE.Mesh(new THREE.PlaneGeometry(9, 9), new THREE.MeshStandardMaterial({ color: 0x2a2a3c, roughness: 0.9 }));
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);

const ball = new THREE.Mesh(new THREE.SphereGeometry(0.7, 32, 32), new THREE.MeshStandardMaterial({ color: 0xff8a4d, roughness: 0.4 }));
ball.castShadow = true;
ball.receiveShadow = true;
scene.add(ball);

const box = new THREE.Mesh(new THREE.BoxGeometry(0.9, 0.9, 0.9), new THREE.MeshStandardMaterial({ color: 0x4dc3ff, roughness: 0.4 }));
box.position.set(-1.8, 0.45, 0);
box.castShadow = true;
box.receiveShadow = true;
scene.add(box);

function resize() { renderer.setSize(innerWidth, innerHeight); camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); }
addEventListener('resize', resize); resize();
renderer.setAnimationLoop((t) => {
  const time = t * 0.001;
  ball.position.y = 0.7 + Math.abs(Math.sin(time * 1.4)) * 1.1;
  box.rotation.y = time * 0.7;
  renderer.render(scene, camera);
});

그림자는 "광원 입장에서 가려서 안 보이는 지점"입니다. 섀도 매핑은 이걸 그대로 구현합니다: 먼저 카메라가 아니라 광원의 시점에서 장면을 한 번 렌더링해 각 지점까지의 깊이를 텍스처(shadow map)에 저장하고, 본래 카메라로 렌더링할 때 각 픽셀이 "광원에서 본 깊이"보다 더 먼 곳에 있으면 그림자로 칠합니다.

설정은 세 군데에 걸쳐 있습니다: renderer.shadowMap.enabled = true로 기능을 켜고, 그림자를 드리울 물체엔 mesh.castShadow = true를, 그림자를 받을 바닥 등엔 mesh.receiveShadow = true를 켭니다. 그리고 광원에는 light.castShadow = true와 함께 light.shadow.camera(그림자를 계산할 범위)를 장면 크기에 맞게 설정해야 합니다 — 이 범위가 너무 넓으면 그림자가 계단처럼 각지고(해상도 부족), 너무 좁으면 물체가 범위 밖으로 나가 그림자가 잘립니다.

renderer.shadowMap.type을 PCFSoftShadowMap으로 두면 그림자 가장자리가 살짝 부드러워집니다. 데모는 공이 튀어 오르내리며 바닥에 드리우는 그림자가 공의 높이에 따라 옅어지고 진해지는 걸 보여줍니다.

언제 쓰나

오브젝트가 공중에 떠 있는지, 바닥에 닿아 있는지를 시각적으로 알려줘야 할 때 (거의 모든 3D 장면).