본문 바로가기
게임개발뜯어보기

눈에 발자국이 남는 게임, 저 눈은 땅이 아니라 그림입니다 - 높이 맵과 테셀레이션

by 조생 2026. 9. 14.

눈밭을 걸으면 발자국이 파이는 게임이 있습니다. 걸어온 길이 그대로 남고, 구르면 몸 자국까지 납니다. 그런데 한참 돌아다니다 아까 그 자리에 오면 발자국이 없습니다. 지형이 진짜로 파였다면 남아 있어야 할 텐데요.

눈을 지형이 아니라 높이 값이 적힌 그림 한 장으로 두고 그 그림을 캐릭터 주변에만 들고 다니면, 이 현상이 그대로 나옵니다. 파이썬으로 짜 보고, 언리얼에서 그 그림으로 바닥을 실제로 눌러 촬영했습니다.

영상판 — 눈밭은 언리얼에서 512² 높이 그림 한 장의 값으로 바닥 메시의 정점을 매 프레임 내려 그린 것입니다. 한 캐릭터의 겉모습은 앞선 편에서 직접 그린 것이고, 화면은 엔진이 렌더링했고, 눈과 도장과 측정은 우리 코드입니다.

왜 이런 게 생겼나

지형을 진짜로 파려면 지형 메시의 정점을 바꿔야 합니다. 눈밭 전체를 잘게 나눠 두고, 발이 닿을 때마다 근처 정점을 찾아 내린 뒤 그래픽카드에 다시 올려야 합니다. 맵이 몇 km 면 정점 수가 감당이 안 됩니다.

그래서 개발자들은 눈을 지형과 분리했습니다. 눈은 평평한 메시 그대로 두고, 얼마나 파였는지는 따로 된 그림 한 장에 적습니다. 2014년 GDC 에서 Colin Barré-Brisebois 는 이 방식을 "Generate displacement heightmaps at runtime" 이라고 소개하며 콘솔에서 쓴 메모리를 "2 MB" 로 적었고, 다른 개발사의 기술 아트 디렉터 Mike Oliver 는 2015년 개발 블로그에서 자기들 눈을 "dynamically generated height and normal map, which displaced vertices" 라고 설명했습니다. 이름은 달라도 눈은 그림이라는 뼈대는 같습니다. 그림은 캐릭터 주변 몇 m 만 덮고, 캐릭터가 움직이면 같이 따라갑니다.

눈은 높이 그림 한 장이다

파이썬으로 짠 눈밭(이하 랩)은 512×512 칸짜리 배열입니다. 칸마다 숫자 하나, 평평한 눈이 0 이고 파이면 음수입니다. 10 m 를 덮으니 한 칸이 1.95 cm, 16비트로 저장하면 그림 한 장이 512 KB 입니다.

발이 닿으면 이 그림에 도장을 찍습니다. 도장은 신발 모양(길이 30 cm, 폭 13 cm)의 깊이 값입니다. 아래 그림 오른쪽 단면처럼 뒤꿈치 쪽이 더 깊고, 바깥 테두리는 밀려난 눈으로 살짝 볼록합니다. 도장은 최솟값으로 찍어서, 이미 파인 칸을 또 밟아도 더 얕아지지 않게 합니다. 하나 찍는 데 배열 연산으로 0.004 ms 면 끝납니다.

그림 1 — 발 도장: 왼쪽은 위에서 본 모양, 오른쪽 앞뒤 단면은 뒤꿈치 쪽이 더 깊고 테두리가 볼록하다

화면은 그 그림으로 바닥을 누른다

그림에는 높이만 적혀 있습니다. 눈 표면 메시의 정점마다 그림에서 자기 자리의 높이를 읽어 그만큼 내리고, 옆 칸과의 높이 차이로 법선(면이 향하는 방향)을 만들어 음영을 줍니다. 파인 벽면이 빛을 다르게 받아서 발자국이 보입니다. 아래 그림 왼쪽의 어두운 칸이 오른쪽에서 파인 자리입니다.

그림 2 — 같은 눈밭: 왼쪽은 그림에 적힌 높이(검을수록 깊다), 오른쪽은 그만큼 누른 바닥(아래 적힌 정점 수는 언리얼 리그 값)

실제 게임은 이 정점 누르기를 GPU 쪽에서 합니다. 정점 셰이더가 그림을 직접 읽어 자리를 옮기거나, 테셀레이션으로 가까운 곳만 잘게 쪼개 누릅니다. 2014년 발표는 "Accurate displacement based on depth" 를 DirectX 11 테셀레이션으로 한다고 적었습니다.

우리 언리얼 촬영은 머티리얼 변위나 테셀레이션 없이, 매 프레임 코드로 평면 메시의 정점 높이와 법선을 다시 계산했습니다. 도장은 발 뼈 높이가 내려오다 다시 올라가는 극소점, 곧 발이 닿는 순간에 찍었습니다. 아래 촬영에서 눈 위의 자국 하나하나가 그렇게 정점을 내린 자리입니다.

그림 3 — 언리얼 촬영: 왼쪽은 다섯 캐릭터가 지나간 눈밭, 오른쪽은 캐릭터를 따라간 화면(아래 적힌 높이 그림의 넓이·칸 크기는 랩과 다른 리그 값)

그림은 캐릭터를 따라다닌다

캐릭터를 따라다니는 이 그림을 창이라 부르겠습니다. 랩의 창이 덮는 건 10 m 뿐입니다. 캐릭터가 중심에서 2 m 이상 벗어나면 창을 그만큼 밀고, 새로 들어온 띠는 새 눈(0)으로 채웁니다. 밀려나간 쪽에 있던 자국은 그대로 버려집니다.

60 m 를 걸어갔다 돌아오며 도장 332개를 찍었더니, 창 안에 남은 건 18개였습니다. 멀리 갔다 온 자리에 발자국이 없는 건 게임이 저장을 안 한 게 아니라, 그 자리가 그림 밖이었기 때문입니다.

그림 4 — 노란 테두리가 창, 회색은 밀려나 버려진 자국: 60 m 를 걸어갔다 돌아오면 332개 중 18개만 남는다

왜 맵 전체를 덮지 않나

같은 해상도(1.95 cm)로 맵 전체를 덮으면, 창 한 장에 512 KB 이던 그림이 가로세로 8 km 맵에서는 312 GB 가 됩니다. 눈 하나 때문에 쓸 수 있는 메모리가 아닙니다. 아래 그림의 막대는 로그 눈금이라, 길이 차이가 작아 보여도 실제로는 수십만 배입니다.

그림 5 — 같은 해상도로 덮는 넓이에 따른 그림 크기(로그 눈금)

넓은 눈밭을 다루는 방법은 몇 가지입니다. 해상도를 낮춰 멀리 있는 자국은 대충 남기거나, 창 밖으로 나간 자국을 더 작은 그림에 압축해 두거나, 아예 잊습니다. 게임마다 다른 선택이고, 이 글은 어느 게임이 어느 쪽인지 말하지 않습니다.

시간이 지나면 메워진다

발자국이 영원히 남으면 눈밭이 금세 다 파입니다. 그래서 매 프레임 높이를 0 쪽으로 조금씩 되돌립니다. 우리 값으로는 7 cm 자국이 절반이 되는 데 17.3초입니다. 지수 감쇠라 처음엔 빨리, 나중엔 천천히 메워집니다. 2014년 발표는 높이 그림을 쌓는 단계에서 "subtract a small value to the heightmap to make snow gradually replenish" 라고 적었고, 2015년 개발 블로그는 "add a reset timer, allowing the deformed terrain to naturally 'fill in' over time" 이라고 썼습니다.

그림 6 — 메워짐: 7 cm 자국의 깊이가 시간에 따라 0 으로 돌아오는 곡선

정리

눈밭은 지형이 아니라 높이 그림 한 장입니다. 발이 닿으면 그림에 도장을 찍고, 화면은 그 그림으로 정점을 누르고, 그림은 캐릭터 주변만 따라다니며 밖으로 나간 자국을 버리고, 남은 자국은 시간이 지나면 메워집니다. 발자국은 이 네 가지가 전부입니다.

참고 자료

이 글의 높이 그림·도장·창 이동·메워짐 시뮬레이션과 크기 계산·도판은 전부 직접 짠 것이고(파이썬 랩), 영상의 눈밭은 같은 규칙을 언리얼에 짠 리그가 프로시저럴 메시로 눌러 그린 것입니다. 인용한 발표와 블로그는 원문을 직접 읽었고, 각 게임이 그 뒤로 어떻게 바뀌었는지는 확인하지 않았습니다.