브라우저에서 오픈 월드 만들기, 14부: 세계가 살아나다
글 Oleg Sidorkin, Cinevva CTO 겸 공동 창업자
처음 오셨나요? 시리즈 가이드를 보세요. spike가 무엇인지 설명하고 모든 파트로 연결됩니다.
13부 이후 우리에겐 조각 기능이 생겼습니다. 지형을 올리고, 동굴을 파고, 절벽을 매끄럽게 다듬는 일을 전부 실시간으로, 이음새가 멀쩡한 채로 할 수 있었죠. 하지만 세계는 기술 데모처럼 보였습니다. 디버그 색으로 칠해진 플랫 셰이딩, 와이어프레임 오버레이, 청크를 구분하려고 LOD 색을 입힌 회색 지오메트리. 편집은 할 수 있었습니다. 느낄 수는 없었죠.
세 개의 spike가 그것을 바꿨습니다. 아키텍처가 바뀐 게 아닙니다. 13부와 똑같은 파이프라인, 버퍼, 이음새 스티칭을 그대로 썼습니다. 우리는 그저 지형이 하나의 장소처럼 느껴지게 하는 레이어를 더했을 뿐입니다. 형태에 반응하는 표면, 그 표면 위에서 자라나는 생명, 그리고 그 위를 걷는 몸 말입니다.
"기술적으로 작동한다"와 "여기 계속 있고 싶다" 사이의 차이는 놀라울 만큼 작았습니다.
절벽을 조각하면 바위로 변하는 걸 지켜보세요
Spike 28은 좁은 질문 하나를 던졌습니다. 트라이플래너 매핑을 쓰는 4레이어 머티리얼이 컴퓨트로 생성한 지형 위에서 프레임 예산을 잡아먹지 않고 돌아갈 수 있을까? 답은 그렇다였지만, 흥미로운 부분은 그다음에 벌어진 일이었습니다.
네 개의 절차적 텍스처(풀, 바위, 모래, 눈)를 시작 시점에 FBM 노이즈로 생성합니다. 외부 파일도, 에셋 파이프라인도 없이 그냥 수학과 DataTexture 하나만 씁니다. 머티리얼 가중치는 표면 자체에서 나옵니다. 경사와 고도죠. 수목한계선 아래의 평평한 지역은 풀이 됩니다. 가파른 면은 바위가 됩니다. 낮은 땅은 모래가 됩니다. 높은 봉우리는 눈이 됩니다. 각 원시 가중치는 경사와 고도에 smoothstep을 적용해 나오고, 그다음 프래그먼트마다 정규화되어
확신을 준 순간은 이랬습니다. 브러시로 지형을 올려 가파른 절벽을 만들면, 같은 프레임 안에 새 면 위로 바위 텍스처가 나타납니다. 다시 평평하게 누르면 풀이 그 표면을 되찾습니다. 머티리얼은 브러시에 대해 알지 못합니다. 그저 월드 좌표와 표면 법선을 읽을 뿐이고, 그건 지오메트리가 만들어질 때 쓴 것과 같은 데이터입니다. 조각에서 시각으로 이어지는 피드백 루프는 즉각적이고 스크립트로 짠 게 아닙니다.
Spike 8은 한 달 반 전에 WebGL 위의 정적 메시로 지형 머티리얼 비용을 테스트했습니다. Spike 28은 그것이 전체 조각 파이프라인을 깔고 있는 동적 컴퓨트 생성 지형 위에서도 작동한다는 걸 증명합니다. 우리는 이걸 예산에 잡아뒀지만, 모든 게 돌아가는 상태에서 60fps를 유지하는 걸 보는 건 그래도 안심이 됐습니다.
풀 더미 8만 개와 동굴 천장 하나
Spike 29는 게임 개발 본능이 발동한 지점입니다.
우리는 젤다의 전설 야생의 숨결 같은 풀을 원했습니다. 폴리곤 수가 아니라 그 느낌이요. 적절한 곳을 덮고, 바람에 흔들리고, 그 사이를 뛰어가고 싶게 만드는 풀 말입니다.
풀 더미 하나는 60도 각도로 교차하는 세 개의 쿼드로 이루어지고, 휘어짐을 위해 수직 세그먼트가 네 개 들어갑니다. 어느 각도에서 봐도 입체적으로 보이는 십자 모양이고 빌보드 트릭은 쓰지 않습니다. 청크당 InstancedMesh 하나, 세계 전체에 8만 더미, 통틀어 대략 240만 개의 풀 정점입니다.
중요한 건 배치입니다. CPU가 각 청크에 걸쳐 흔들린 격자(jittered grid)를 훑으면서 GPU 머티리얼 셰이더가 쓰는 것과 똑같은 경사/고도 로직을 평가합니다. 머티리얼 시스템이 "풀"이라고 하는 곳에 풀이 자랍니다. 바위나 모래가 우세한 곳에서는 밀도가 0으로 떨어집니다. 바닥에 깔린 smoothstep 가중치가 바이옴 경계에서 연속적인 그라데이션을 만들기 때문에 감쇠가 매끈합니다. 경계가 없으니 경계를 알아채지 못합니다.
그다음 작동할지 확신하지 못했던 일을 했습니다. SDF 표면 위의 풀이요. 스캐터 함수는 SDF 볼륨의 각 컬럼을 훑으면서 인접 복셀 사이의 영점 교차를 찾습니다. SDF가 음수에서 양수로 넘어가는 곳에 표면이 있습니다. 표면 법선은 필드의 정규화된 그래디언트
Spike 27에서 SDF 브러시로 동굴을 팝니다. Spike 29로 돌아오면 동굴 천장 위에 풀이 자라고 있습니다. 스캐터 코드는 동굴이 뭔지 모릅니다. 그저 적절한 고도에서 적절한 경사를 가진 표면을 볼 뿐입니다. 그게 오픈 월드 시스템을 만드는 일을 만족스럽게 하는 종류의 창발적 동작입니다.
바람은 TSL 버텍스 셰이더 안의 사인파이고, 잎의 V 좌표로 변조되어 끝은 흔들리고 뿌리는 박혀 있습니다. V를 토글하면 약하게, 강하게, 끄기를 순환합니다. 바람은 전적으로 버텍스 셰이더 안에서 돌기 때문에 8만 더미 전부에서 동시에, CPU 비용 없이 작동합니다.
Spike 7은 5만 개의 풀 인스턴스를 테스트했고 우리는 한계에 부딪힐까 봐 긴장했습니다. Spike 29는 컴퓨트 지형, 이음새 스티칭, 멀티 머티리얼 텍스처링, 브러시 위에 8만 개를 돌립니다. 잎당 정점 수를 줄이는 것보다 InstancedMesh 드로우를 더 적게 묶는 게 여전히 더 중요합니다. Spike 7의 교훈은 그대로 통했습니다.
미뤄둔 게 하나 있습니다. 풀 아래를 조각해도 풀은 갱신되지 않습니다. 인스턴스 행렬은 스캐터 시점에 설정됩니다. 언덕을 골짜기로 조각하면 G를 눌러 다시 스캐터할 때까지 풀이 공중에 떠 있습니다. spike로는 충분합니다. 프로덕션에서는 더티 청크 재스캐터가 필요할 겁니다.
첫 발자국
Spike 30은 제가 계속 다시 찾게 되는 것입니다.
물리 라이브러리는 쓰지 않습니다. 120Hz 고정 타임스텝의 커스텀 캡슐입니다. 프로덕션 코드베이스는 웹 워커에서 Rapier를 씁니다(Spike 2가 지연 시간이 괜찮다는 걸 증명했죠). 하지만 이 spike는 충돌 쿼리 자체를 증명해야 했습니다. 캐릭터가 하이트맵 지형을 가로질러 걷고, 조각한 SDF 지형으로 올라서고, 바닥을 뚫고 떨어지지 않을 수 있을까?
핵심은 terrainQuery(x, y, z) 라는 함수입니다. 그 위치가 MC로 잠긴 청크 안에 들어가는지 확인합니다. 그렇다면 CPU SDF 미러를 삼선형 보간하고 그래디언트를 표면 법선으로 반환합니다. 아니라면 중앙 차분 법선을 쓰는 하이트맵 조회로 갑니다. 캐릭터는 자기가 어떤 지형 시스템 위에 서 있는지 모릅니다. 그저 바닥을 묻고 답을 받을 뿐입니다.
SDF 충돌은 어려울 거라 예상했던 부분입니다. 캡슐 둘레의 일곱 개 프로브 포인트(바닥, 중심, 위, 네 개의 기본 방향 오프셋). 위치
이렇게 하면 표면을 따라 미끄러지는 성분만 남습니다. 물리 엔진이 아닙니다. 지오메트리 쿼리와 단순한 반응입니다. 하지만 형태마다 특수 케이스 코드를 하나도 안 쓰고 동굴, 돌출부, 조각한 터널을 다 처리합니다. 10초 전에 판 동굴로 걸어 들어가면 캡슐이 천장 윤곽을 정확히 따라갑니다.
이동 모델은 실용적으로 시작했다가 재미있어졌습니다. 걷기, 달리기, 전력 질주, 점프, 45도 이상에서의 경사 미끄러짐. 그다음 야생의 숨결식 패러글라이더를 넣었더니 spike가 닫고 싶지 않은 무언가로 변했습니다.
공중에서 스페이스를 누릅니다. 중력이 -30에서 -4로 떨어집니다. 낙하 속도는 -3에서 한계가 잡힙니다. 캡슐에서 델타윙 메시가 스케일 보간으로 펼쳐집니다. 왼쪽으로 기울이면 날개가 기웁니다. 캡슐이 향하는 방향이 속도 벡터 쪽으로 자동 정렬되어 항상 가는 곳을 바라봅니다. 스페이스를 놓으면 떨어집니다. 땅에 닿으면 다시 달리고 있습니다.
카메라는 3인칭으로 당겨집니다(P로 토글). 요(yaw) 스무딩과 함께 플레이어 뒤를 따라옵니다. 플레이어에서 카메라까지 레이마치가 20스텝으로 지형을 테스트합니다. 동굴로 날아 들어가면 카메라 암이 바위를 뚫고 들어가는 대신 매끄럽게 짧아집니다. 반대편으로 나오면 다시 늘어납니다.
이 spike를 가치 있게 만든 순간이 여기 있습니다. 하이트맵 브러시로 높은 절벽을 조각합니다. 3인칭 카메라로 전환합니다. 가장자리까지 달립니다. 점프합니다. 글라이더를 펼칩니다. 방금 조각한 지형 위로 왼쪽으로 기울이는데, 아래에는 Spike 29에서 키운 풀이 흔들리고, 절벽 면에는 Spike 28의 바위 텍스처가 깔려 있고, 모든 청크 경계에서 Transvoxel 이음새가 버티고 있습니다. 반대편에 착지합니다. 하나의 브라우저 탭 안에서 모든 게 함께 작동합니다.
내 발밑을 조각하기
확신하지 못했던 게 하나 있습니다. 캐릭터가 서 있는 지형을 조각하면 어떻게 될까? 하이트맵 변경은 terrainQuery()가 CPU 버퍼를 직접 읽기 때문에 즉시 전파됩니다. SDF 변경은 CPU SDF 미러를 통해 전파됩니다. 캡슐은 다음 물리 틱에 침투를 해소하는데, 120Hz에서 그건
그냥 작동합니다. 플레이어 아래 땅을 올리면 함께 올라갑니다. 땅을 파내면 떨어집니다. 특별한 처리는 없습니다. 물리가 충분히 빠르게 돌아서 단일 프레임의 지형 변경이 큰 침투를 만들지 않습니다. 이건 타임스텝을 120Hz로 고른 데서 온 행복한 우연이었습니다. 우리는 매끄러운 이동을 위해 그걸 골랐는데, 덤으로 지형 편집이 안전해졌습니다.
spike 30개를 지나
우리는 이 시리즈를 평평한 하이트맵과 큐브 500개로 시작했습니다. 이제는 입체적인 동굴이 있는 조각된 지형, Transvoxel 이음새 스티칭, 표면 형태에 반응하는 멀티 머티리얼 텍스처링, 바람에 흔들리는 풀 더미 8만 개, 그리고 그 위를 걷고 전력 질주하고 점프하고 활공하는 캐릭터를 갖췄습니다.
이 중 무엇도 아직 프로덕션에 들어가지 않았습니다. world/client/ 코드베이스는 여전히 단순한 하이트맵 청크로 WebGL을 돌립니다. 이 30개 spike의 모든 것은 독립적인 HTML 페이지 안에 살고 있습니다. 다음은 통합 작업입니다. WebGPURenderer로 마이그레이션하고, 하이브리드 HM/MC 정책을 청크 매니저에 연결하고, 브러시와 머티리얼 시스템을 멀티플레이어에 잇는 일이요.
하지만 이제 렌더링 시스템은 위험이 아닙니다. 열린 질문은 데이터 흐름에 관한 것입니다. 편집 영속성, 브러시 스트로크의 네트워크 동기화, 협업 조각. 삼각형 사이가 아니라 플레이어 사이에서 벌어지는 것들이죠.
1부부터 이 시리즈를 따라오셨다면, 지저분한 부분까지 함께해 주셔서 고맙습니다. 방금 발견하셨다면 1부로 돌아가세요. 잘못 든 길에 교훈이 있습니다.
이 챕터에서 언급한 기술
TSL (Three Shading Language). WebGPU 렌더러를 위한 Three.js의 노드 기반 셰이더 시스템입니다. 머티리얼은 자바스크립트의 함수 합성을 써서 노드(positionWorld, normalWorld, smoothstep, triplanarTexture)로 구성됩니다. 셰이더 그래프는 런타임에 WGSL로 컴파일됩니다. TSL은 WebGPU 타깃에서 원시 GLSL ShaderMaterial을 대체하고, 표준 Three.js 머티리얼 기능(라이트, 그림자, 안개)과 커스텀 프래그먼트별 로직 사이의 상호 운용을 제공합니다.
트라이플래너 매핑. 텍스처를 세 번(XY, XZ, YZ 평면) 샘플링하고 표면 법선 방향에 따라 혼합하는 텍스처 투영 기법입니다. 임의의 메시 지오메트리에서 UV 늘어짐을 없애는데, 이는 삼각형에 의미 있는 UV 좌표가 없는 마칭 큐브 출력에 핵심적입니다. TSL은 triplanarTexture()를 내장 노드로 제공합니다.
교차 쿼드 잎으로 만든 인스턴스 식생. 풀 더미 하나는 60도 각도로 교차하는 쿼드 3개로, 어느 시점에서 봐도 입체적으로 보이게 합니다. 쿼드당 수직 세그먼트 4개가 바람 애니메이션을 위한 매끄러운 휘어짐을 가능하게 합니다. 전체 필드는 청크당 단일 InstancedMesh로 렌더링됩니다. 용량은 25% 초과 할당되어, 지형을 편집할 때 GPU 버퍼를 재할당하지 않고도 새 인스턴스가 예약된 슬롯을 채울 수 있습니다. 식생에 관한 우리 랜드스케이프 생성 가이드를 보세요.
캡슐 대 SDF 충돌. 물리 엔진 없이 입체 지형에 대한 캐릭터 충돌입니다. 캡슐을 SDF에 대해 여러 지점에서 프로브합니다. 필드 값이 캡슐 반지름보다 작은 곳에서, 그래디언트가 바깥쪽 법선을 주고 그 차이가 침투 깊이를 줍니다. 이는 형태별 코드 없이 동굴, 돌출부, 터널을 처리합니다. SDF 지형 충돌을 보세요.
고정 타임스텝 캐릭터 컨트롤러. 물리는 프레임 레이트와 무관하게 120Hz로 스텝을 밟으며, 실제 시간을 누적해 고정 크기 스텝으로 소비합니다. 최대 서브스텝 횟수가 느린 프레임에서 죽음의 나선(spiral-of-death)을 막습니다. 그라운드 스내핑이 경사 이동 중에 캡슐을 붙여둡니다. 고정 타임스텝은 향후 멀티플레이어 리플레이를 위한 결정론적 동작을 보장합니다.
29부 중 14부. 이전: 13부 - 지형 조각과 수학 함수의 죽음 다음: 15부 - 베이스라인을 교체한 다음 동기화하기 시리즈 가이드: /blog/2026-02-25-open-world-browser-series-guide