툰 셰이딩

Toon shading

부드러운 명암 대신 몇 단계의 뚜렷한 색 밴드로 음영을 표현해 만화·애니메이션처럼 보이게 하는 셰이딩.

다른 이름: Cel shadingMeshToonMaterialGradient mapInverted hull outline
···
js
import * as THREE from 'three';
function makeToonGradient() {
  const size = 4;
  const c = document.createElement('canvas');
  c.width = size; c.height = 1;
  const ctx = c.getContext('2d');
  const shades = ['#241d33', '#5b4a8a', '#9c86d6', '#efe6ff'];
  shades.forEach((color, i) => { ctx.fillStyle = color; ctx.fillRect(i, 0, 1, 1); });
  const tex = new THREE.CanvasTexture(c);
  tex.minFilter = THREE.NearestFilter;
  tex.magFilter = THREE.NearestFilter;
  tex.needsUpdate = true;
  return tex;
}
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0c0c16);
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
camera.position.set(0, 0.6, 5.2);

const dLight = new THREE.DirectionalLight(0xffffff, 1.6);
dLight.position.set(3, 4, 4);
scene.add(dLight);
scene.add(new THREE.AmbientLight(0x1a1a2c, 1));

const gradientMap = makeToonGradient();
const geo = new THREE.SphereGeometry(1.3, 64, 64);
const mesh = new THREE.Mesh(geo, new THREE.MeshToonMaterial({ color: 0xff8ab8, gradientMap }));
scene.add(mesh);
const outline = new THREE.Mesh(geo, new THREE.MeshBasicMaterial({ color: 0x0c0c16, side: THREE.BackSide }));
outline.scale.setScalar(1.06);
scene.add(outline);

function resize() { renderer.setSize(innerWidth, innerHeight); camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); }
addEventListener('resize', resize); resize();
renderer.setAnimationLoop((t) => {
  mesh.rotation.y = t * 0.0004;
  mesh.rotation.x = Math.sin(t * 0.00025) * 0.5;
  outline.rotation.copy(mesh.rotation);
  renderer.render(scene, camera);
});

보통 조명 계산(diffuse = max(dot(N,L), 0))은 각도에 따라 부드럽게 이어지는 값을 냅니다. MeshToonMaterial은 이 값을 gradientMap이라는 작은 1D 텍스처에서 다시 찾아 읽습니다 — 그 텍스처가 계단식(예: 4단계 회색조)이면 조명값도 계단식으로 뚝뚝 끊기면서 셀 애니메이션 특유의 밴딩이 생깁니다. 텍스처의 minFilter/magFilter를 NearestFilter로 두는 게 핵심인데, 안 그러면 단계 사이가 보간되어 밴드 경계가 흐려집니다.

윤곽선(외곽선)은 별도 기법이 필요합니다. 가장 간단한 방법은 "인버티드 헐(inverted hull)"입니다: 같은 지오메트리를 살짝(1.03~1.08배) 키우고, side: THREE.BackSide인 단색 머티리얼로 뒤집어서 원본 메시 뒤에 겹쳐 그립니다. 뒷면만 보이는 이 확대된 껍질이 원본 메시의 실루엣 바깥으로 살짝 삐져나와 검은 테두리처럼 보입니다.

데모는 구를 MeshToonMaterial + 4단계 그라데이션으로 셰이딩하고, 그 뒤에 검은 인버티드 헐을 겹쳐 만화 캐릭터 같은 룩을 만듭니다.

언제 쓰나

만화·애니메이션풍 캐릭터, 논포토리얼(non-photorealistic) 게임 아트에.