회색 플라스틱을 벗기는 툰 셰이딩
토끼와 터널을 처음 MeshStandardMaterial 기본값으로 띄웠을 때 인상은 한마디로 "회색 플라스틱"이었다.
PBR 머티리얼은 사실적인 빛 반응을 주지만, 사실적인 게 곧 귀여운 건 아니다.
동화책 톤에는 명암이 매끄럽게 굴러떨어지는 게 오히려 방해였다.
명암을 세 칸으로 끊기
그래서 툰 셰이딩으로 갈아탔다.
핵심은 명암을 연속이 아니라 몇 단계로 끊는 것이다.
3단 그라데이션 램프를 DataTexture로 만들고, 모든 머티리얼이 이 한 장을 gradientMap으로 공유하게 했다. MeshToonMaterial은 이 램프를 보고 빛을 세 칸으로 양자화한다.
// components/game/materials.ts
cached = new THREE.DataTexture(
new Uint8Array([110, 190, 255]), // 3단 명암 — 어두움/중간/밝음
3, 1, THREE.RedFormat,
);
cached.minFilter = THREE.NearestFilter;
cached.magFilter = THREE.NearestFilter;
NearestFilter가 중요하다.
보간을 끄지 않으면 세 값이 부드럽게 섞여 다시 그라데이션으로 돌아가버린다.
칸을 또렷이 끊어야 툰 특유의 만화 같은 면이 나온다.
램프를 한 장 만들어 캐시해두고 전부가 같은 걸 쓰게 한 것도 의도한 부분이다.
텍스처가 하나뿐이니 GPU에 올라가는 것도 하나고, 머티리얼마다 램프를 새로 만들었을 때 생기는 미묘한 톤 차이도 없다.
온 화면이 같은 세 칸을 공유하니 그림이 한 덩어리로 읽힌다.
굴마다 조명을 따로 준다
머티리얼만으로는 부족했다.
그라데이션 안개로 터널 끝을 앰버빛으로 녹이고, 블룸을 얹어 앰버 크리스탈이 은은히 빛나게 했다.
셋이 합쳐지자 비로소 "아늑한 황혼 굴" 무드가 완성됐다.

나중에 굴을 세 종류로 늘리면서 조명도 굴마다 분리했다.
하늘색·바닥색·반구광 세기·직사광 색과 세기를 테마마다 따로 잡는데, 여기에 항목을 하나 더 뒀다.
토끼 전용 필라이트다.
값을 보면 의도가 드러난다.
밝은 흙굴은 1.0인데, 수정 동굴은 3.4, 밤하늘 달빛 굴은 4.0이다.
어두운 굴에서는 토끼의 실루엣과 털색, 볼터치가 배경에 그대로 매몰됐다.
전체 조명을 올리면 굴의 어두운 분위기가 같이 날아가버리니, 토끼 주변만 국소적으로 들어올리는 수밖에 없었다.
흙굴이 1.0으로 낮은 건 이미 밝아서 더 얹으면 과노출이 되기 때문이다.
벽이 통째로 사라져 보이던 문제
가장 오래 헤맨 건 따로 있었다.
특정 각도에서 터널 벽이 아예 사라진 것처럼 보였다.
정확히는 시선과 벽면이 거의 나란해지는 구간에서, 툰의 가장 어두운 칸이 배경색·안개색과 똑같은 어두운 갈색으로 붕괴했다.
벽과 배경의 경계가 없어지니 벽에 붙어 있던 장식들이 허공에 떠 보이고, 토끼 하반신은 벽에 묻힌 것처럼 보였다.
툰 셰이딩이라 생기는 문제다.
명암이 세 칸뿐이니 가장 어두운 칸에 한번 떨어지면 그 아래로는 아무 정보가 없다.
PBR이었다면 미세한 밝기 차로라도 면이 남았을 텐데, 양자화가 그걸 지워버린 것이다.
해결은 벽에 아주 약한 자체발광을 깔아주는 것이었다.
팔레트의 중간 톤 색으로 emissive를 주고 세기를 0.25 정도로 낮게 유지한다.
그러면 가장 어두운 칸에도 바닥값이 생겨서, 어떤 각도에서도 벽이 "표면"으로 읽힌다.
세기를 낮게 잡은 건 굴이 밝아지면 안 되기 때문이고, 색을 팔레트에서 가져온 건 굴마다 고유한 색감을 유지하기 위해서다.
<meshToonMaterial
color={pal.tunnelDirt}
gradientMap={grad}
emissive={pal.tunnelMid}
emissiveIntensity={0.25}
side={THREE.BackSide}
/>
결국 칠하는 방식의 문제였다
같은 형상이라도 칠하는 방식이 무드를 통째로 바꾼다.
회색 플라스틱과 황혼 굴의 차이는 폴리곤이 아니라 셰이딩이었다.
다만 스타일을 고르는 건 곧 그 스타일의 약점까지 떠안는 일이기도 했다.
툰의 또렷한 면을 얻은 대신 어두운 쪽 정보가 사라지는 문제를 따로 메워야 했으니까.
다음 편은 비주얼이 다 잡힌 줄 알았는데 화면이 통째로 죽어버린 React 19 삽질이다.