Skip to content

브라우저에서 오픈 월드 만들기, 8부: 기준선을 잃지 않고 통합하기

글쓴이 Oleg Sidorkin, Cinevva CTO 겸 공동 창업자

처음 보시나요? 시리즈 가이드를 보세요. spike가 무엇인지 설명하고 모든 부분으로 연결해 줍니다.

통합은 프로젝트가 엉망이 되는 지점입니다. 따로 떼어 놓으면 잘 도는 부분들이 있습니다. 그것들을 연결하면 갑자기 모든 버그가 어디에나 있을 수 있는 것처럼 보입니다.

Spike 13과 14는 그 함정에 대한 우리의 답이었습니다. Spike 13은 깨끗한 Three.js WebGPU 기준선을 세웠습니다. renderer 하나, scene 하나, camera 하나, 그리고 단순한 mesh 하나뿐이었습니다. 지형도, compute도, 이펙트도 없었습니다. 우리는 Three.js의 WebGPU 백엔드가 제대로 초기화되는지, 렌더 루프가 안정적인지, TSL(Three.js Shading Language) 노드 머티리얼이 기대대로 동작하는지 확인했습니다. 그 체크포인트를 통과한 뒤에야 계층을 추가하기 시작했습니다.

새 탭에서 Spike 13 열기 ↗ · 소스 보기

Spike 14는 점진적 강화였습니다. 한 번에 하나의 기능씩 추가했습니다. 먼저 카메라 컨트롤, 그다음 조명, 그다음 marching cubes 파이프라인에서 compute로 생성한 mesh, 그다음 GPU 출력을 Three.js 지오메트리 속성에 직접 연결하는 buffer 배선. 추가할 때마다 이전 계층이 여전히 제대로 동작하는지 확인했습니다.

새 탭에서 Spike 14 열기 ↗ · 소스 보기

느려 보입니다. 딱 하루 느렸고, 곧이어 seam 로직과 정책 전환이 복잡해졌을 때 며칠을 아껴 줬습니다.

이 규율이 정당했음을 증명한 특정 버그 부류는 머리카락처럼 가는 아티팩트였습니다. 지오메트리 손상처럼 보이는 얇은 조각이었지만 사실은 과거 데이터였습니다. compute shader는 buffer에 N개의 정점을 쓰는데, draw call은 여전히 이전 프레임의 N+M개 정점을 렌더링하도록 설정되어 있었습니다. 그 여분의 정점에는 이전 dispatch에서 남은 쓰레기 값이 들어 있었습니다. 시각적 결과는 예측할 수 없이 나타났다 사라지는, 깜빡이는 면도날처럼 얇은 삼각형이었습니다.

이런 부류의 버그는 직관으로 이기는 게 아닙니다. 마지막으로 잘 돌던 상태와 지금 망가진 상태 사이에 정확히 무엇이 바뀌었는지 아는, 통제된 델타로 이깁니다.

WebGPU 통합은 buffer 생명주기에 대해서도 가르쳐 줬습니다. WebGPU의 GPU buffer는 특정 usage로 매핑되고 나면 불변입니다. marching cubes 출력이 커져서 vertex buffer를 키워야 한다면, 새 buffer를 만들고 바인딩을 갱신해야 합니다. realloc은 없습니다. 그 생명주기를 제대로 하는 것, 즉 진행 중인 GPU 작업과 경쟁하지 않으면서 옛 buffer를 파괴하는 것은 WebGL에 없는 명시적 fence 관리를 필요로 했습니다.

9부에서는 Transvoxel seam 작업으로 들어갑니다. 그 장은 의도적으로 스캐폴드에서 시작합니다. 이 시점에 우리는 통합을 서두르면 미스터리가 생기고, 통제된 셋업은 디버깅 가능한 문제를 만든다는 교훈을 완전히 체화한 상태였습니다.

이 장에서 다룬 기술

WebGPU. WebGL의 후속으로, 브라우저에서 compute shader와 indirect rendering을 갖춘 저수준 GPU 접근을 제공합니다. 오픈 월드에 중요한 WebGPU의 두 가지 핵심 기능은 이렇습니다. compute shader는 GPU 측 지형 생성, 식생 배치, 컬링을 가능하게 하고, indirect rendering은 GPU가 compute 출력을 바탕으로 무엇을 그릴지 결정하게 해서 밀집된 장면에서 CPU 병목을 없앱니다. 데스크톱의 Chrome, Edge, Firefox에서 쓸 수 있습니다. 성능 면에서의 WebGPU를 보세요.

Three.js Shading Language (TSL). Three.js의 노드 기반 셰이더 시스템으로, 날것의 GLSL/WGSL을 조합 가능한 JavaScript 표현식으로 대체합니다. texture(), positionWorld, smoothstep(), fog() 같은 TSL 노드는 런타임에 셰이더 그래프를 만들고 적절한 백엔드(WebGL의 GLSL이나 WebGPU의 WGSL)로 컴파일됩니다. TSL 덕분에 머티리얼 로직을 한 번 작성하고 두 렌더러 모두를 대상으로 삼을 수 있습니다. 노드 그래프는 프레임마다 평가되므로 동적 uniform과 조건 분기가 자연스럽게 동작합니다.

WebGPU의 GPU buffer 생명주기. WebGPU buffer는 특정 usage 플래그(VERTEX, STORAGE, COPY_DST 등)와 함께 생성되며 생성 후에는 크기를 바꿀 수 없습니다. 한 번의 marching cubes dispatch가 buffer가 담을 수 있는 것보다 많은 정점을 만들어 내면, 새 buffer를 만들고 바인딩을 갱신하고 옛것을 파괴해야 합니다. 진행 중인 GPU 명령이 아직 참조하는 buffer를 파괴하면 오류가 납니다. 명시적 fence 관리(device.queue.onSubmittedWorkDone()를 통해)는 GPU가 다 쓸 때까지 옛 buffer가 파괴되지 않도록 보장합니다. 이런 생명주기 규율은 드라이버가 메모리를 암묵적으로 관리하는 WebGL에는 없습니다.

점진적 강화. 통합을 위한 프로세스 규율입니다. 검증된 기준선을 세우고, 한 번에 한 기능씩 추가하고, 추가할 때마다 이전 계층이 여전히 동작하는지 확인합니다. 이 접근은 하루는 더 느리지만 나중에 디버깅할 때 며칠을 아껴 줍니다. 각 회귀를 구체적이고 통제된 변경 하나로 추적할 수 있기 때문입니다. 기준선을 먼저 세우고 점진적으로 늘려 가는 패턴은 AAA 오픈 월드 개발에서 흔하며, 거기서는 위험을 관리하기 위해 시스템을 특정 순서로 통합합니다.


12부 중 8부.
이전: 7부 - Marching cubes와 첫 진짜 동굴
다음: 9부 - Transvoxel은 스캐폴드에서 시작했다
시리즈 가이드: /ko/blog/2026-02-25-open-world-browser-series-guide