2026.03.24삽질 일지
Claude CodeBabylon.js삽질기그림자InstancedMeshreceiveShadows3D게임개발

Babylon.js instanced mesh와 그림자의 함정 — receiveShadows가 안 먹는 이유

"안나와. 대체 뭐야?"

myDGC 프로젝트에서 전투 씬을 만들고 있었습니다. 몬스터, 캐릭터, 나무… 3D 게임이라면 당연히 그림자가 있어야 분위기가 사는데, 그림자 하나 넣겠다고 시작한 작업이 반나절짜리 삽질로 이어질 줄은 몰랐습니다.

결론부터 말하면, Babylon.js의 InstancedMesh에는 receiveShadows를 설정해도 아무 효과가 없습니다. 원본(source) 메시에 설정해야 합니다. 이 한 줄짜리 사실을 깨닫기까지의 여정을 공유합니다.


1단계: 그림자가 아예 안 나온다

Claude에게 그림자 관련 코드를 분석해달라고 요청했더니, 곧바로 세 가지 문제를 짚어냈습니다.

문제를 파악했습니다. 3가지 원인이 있습니다:

  1. BattleScene.ts:279shadowsEnabled: false로 그림자 자체가 꺼져 있음
  2. createUnitMesh — 유닛 메시를 shadow caster로 등록하지 않음
  3. BlockAssetManager.ts — 지형 메시에 receiveShadows = true가 없음

"아, 그냥 세 군데 고치면 되는 거구나." 라고 생각했습니다. 순진했습니다.

typescript
// BattleScene.ts — 그림자 활성화
shadowsEnabled: true,
shadowQuality: 'medium'

// createUnitMesh — shadow caster 등록
lighting.addShadowCaster(mesh);

// BlockAssetManager.ts — 지형이 그림자를 받도록
instance.receiveShadows = true;

수정 후 확인. 안 나옵니다. 왜?


2단계: 맵 JSON의 반란

원인 찾았습니다. loadMap()applyMapConfig(mapData.lighting) 호출 시 맵 데이터의 shadowsEnabled: false가 다시 그림자를 끕니다.

BattleScene.ts에서 아무리 true로 바꿔도, 맵 JSON 파일(battle_test.json)에 "shadowsEnabled": false가 박혀 있어서 로드 시점에 다시 꺼지고 있었던 겁니다. 설정이 두 곳에 있으면 반드시 이런 일이 생깁니다.

맵 JSON을 수정했습니다. 그런데…

맵 JSON이 아직 false입니다! 이전 편집이 적용 안 됐네요.

그림자가 없는 3D 게임 화면의 허전함

dev 서버가 파일을 덮어쓴 건지, 편집이 다른 위치에 적용된 건지 — 어쨌든 파일이 안 바뀌어 있었습니다. Claude도 이 상황을 인지하고 솔직하게 인정했습니다.

죄송합니다. 첫 편집이 성공으로 보고됐지만 실제로 파일이 외부 프로세스에 의해 덮어쓰였거나 파일 내 다른 위치에 적용된 것 같습니다. 제가 편집 후 결과를 바로 검증하지 않은 실수입니다.

다시 수정하고, 이번엔 코드 레벨에서도 강제로 그림자를 보장하는 방어 로직을 넣었습니다.


3단계: 그래도 안 나온다 — frustum 설정 누락

맵 JSON 문제를 해결했는데도 그림자가 보이지 않습니다. 시크릿 탭에서도 마찬가지. 캐시 문제가 아니라 근본적인 원인이 또 있다는 뜻입니다.

Babylon.js 방향광 그림자는 frustum 설정이 필요합니다.

DirectionalLight의 shadow projection은 frustum(절두체) 범위를 명시적으로 잡아줘야 합니다. 위치(position)와 shadow Z bounds가 빠져 있어서 shadow map이 아무것도 캡처하지 못하고 있었습니다.

typescript
// LightingManager.ts — frustum 설정
shadowGenerator.autoUpdateExtends = false;
shadowGenerator.shadowMinZ = 0;
shadowGenerator.shadowMaxZ = 100;
// PCF 방식으로 변경 (로우폴리 모델에서 더 안정적)
shadowGenerator.usePercentageCloserFiltering = true;
// forceBackFacesOnly 제거 — 로우폴리 모델에서 문제 유발

4단계: 콘솔이 너무 시끄럽다

디버깅을 위해 [Shadow] 태그로 로그를 추가했는데, 기존 로그가 너무 많아서 필요한 정보를 찾기 어려웠습니다. 그래서 태그 기반 로그 필터링 시스템을 만들었습니다.

typescript
// 브라우저 콘솔에서 사용
window.DEBUG.all = true;        // 모든 로그 표시
window.DEBUG.Shadow = true;     // [Shadow] 로그만 표시
window.DEBUG.ItemManager = true; // [ItemManager] 로그만 표시
// console.error는 항상 표시 (절대 안 꺼짐)

이건 삽질의 부산물이지만, 이후 디버깅에서 정말 유용하게 쓰이게 됩니다. 삽질에도 가치가 있습니다.


5단계: 진짜 원인 — InstancedMesh의 함정

로그를 켜고 새로고침하니 콘솔에 경고가 쏟아졌습니다.

code
receiveShadows on instanced mesh has no effect
receiveShadows on instanced mesh has no effect
receiveShadows on instanced mesh has no effect
...

대량으로. 지형 블록 하나하나마다.

핵심 원인 찾았습니다! InstancedMesh에는 receiveShadows가 안 먹힙니다. source mesh에 설정해야 합니다.

코드 디버깅 과정의 시행착오

Babylon.js에서 InstancedMesh는 성능 최적화를 위해 원본 메시의 속성을 공유합니다. receiveShadows도 마찬가지입니다. 인스턴스에 아무리 설정해봐야 무시됩니다. 원본(source) 메시에 한 번만 설정하면 모든 인스턴스에 적용됩니다.

typescript
// ❌ 이렇게 하면 안 됨 — 경고만 대량 발생
const instance = sourceMesh.createInstance("block");
instance.receiveShadows = true; // has no effect!

// ✅ 이렇게 해야 함 — source mesh에 설정
sourceMesh.receiveShadows = true;
const instance = sourceMesh.createInstance("block"); // 자동으로 적용

수정은 모델 로드 시점에 source mesh의 receiveShadowstrue로 설정하고, createBlockInstance에서 인스턴스마다 중복 설정하던 코드를 제거하는 것으로 마무리했습니다.


삽질 타임라인 정리

순서문제증상
1shadowsEnabled: false그림자 시스템 자체가 꺼져 있음
2맵 JSON이 설정을 덮어씀코드 수정해도 로드 시 다시 꺼짐
3파일 편집이 실제 반영 안 됨dev 서버가 파일을 덮어쓴 것으로 추정
4DirectionalLight frustum 미설정shadow map이 아무것도 캡처 못함
5InstancedMesh에 receiveShadows 설정경고만 대량 발생, 실제 효과 없음

5개의 문제가 겹쳐 있었습니다. 하나를 고쳐도 다음 문제가 기다리고 있었기에, "왜 적용이 한 번에 안돼?"라는 좌절이 계속될 수밖에 없었습니다.


교훈

1. InstancedMesh의 속성은 source mesh에서 관리한다

Babylon.js의 InstancedMesh는 렌더링 속성 대부분을 원본 메시에서 상속합니다. receiveShadows, material, visibility 등을 인스턴스에 개별 설정하려고 하면 무시되거나 경고가 발생합니다. 공식 문서에도 있지만, 직접 겪기 전까진 눈에 안 들어오는 종류의 정보입니다.

2. 설정이 여러 곳에 있으면 반드시 충돌한다

BattleScene.ts의 초기값과 맵 JSON의 값이 서로 다른 건 시한폭탄입니다. Single Source of Truth 원칙은 게임 설정에도 적용됩니다.

3. 편집 후 반드시 검증하라

"파일을 수정했다"와 "파일이 실제로 바뀌었다"는 다른 이야기입니다. 특히 dev 서버, 핫 리로드, 파일 워처가 돌아가는 환경에서는 편집한 파일이 즉시 덮어쓰일 수 있습니다.

4. 문제는 겹쳐서 온다

그림자가 안 나오는 원인이 하나였다면 금방 찾았을 겁니다. 5개가 겹치니까 하나를 고쳐도 변화가 없고, "내가 뭘 잘못한 거지?"라는 의심의 늪에 빠지게 됩니다. 이럴 때는 가장 기본적인 것(테스트 박스 + 바닥)부터 동작을 확인하고 하나씩 쌓아올리는 게 가장 빠른 길입니다.


그림자 하나 넣겠다고 시작한 일이 반나절 삽질이 됐지만, 덕분에 Babylon.js의 그림자 파이프라인을 속속들이 이해하게 됐습니다. 삽질은 배신하지 않습니다. 가끔 배신하긴 하는데, 이번엔 안 했습니다.