지형 위 물 시뮬레이션
(lisyarus.github.io)- 지형을 파고 메울 수 있는 게임에서는 호수·강·웅덩이가 새 경계로 흘러나가야 하므로, 빠르고 안정적인 격자 기반 물 시뮬레이션이 필요함
- 목표는 약 1m 스케일에서 지형과 같은 격자를 쓰고, 물 보존·제어 가능한 안정성·선형 업데이트 비용을 만족하는 2D 높이장 모델임
- Smoothed Particle Hydrodynamics와 Stable Fluids는 각각 고해상도 입자 유체와 닫힌 유체 부피에 맞춰져 있어, 지형 위 자유 수면을 빠르게 처리하려는 조건과 어긋남
- 채택한 virtual pipes 방식은 staggered grid에 물 높이와 셀 사이 흐름을 저장하고, 흐름 가속·유출 스케일링·물기둥 업데이트를 몇 개의 2D 배열 루프로 처리함
- 적절한
dt와g에서는 물처럼 보이는 결과를 내지만, 관성과 속도 확산이 없어 빠른 물줄기가 호수 안으로 이어져 전파되지 않는 한계가 남음
지형 수정 게임에서 물이 어려운 이유
- 전략 게임이나 도시·마을 시뮬레이션에서 물은 자연스러운 경계, 항해·낚시·교역·해전, 식수·운송·미관 같은 역할을 맡을 수 있음
- 지형을 직접 바꿀 수 있으면 물 처리 난도가 크게 올라감
- 흙·모래·점토 같은 자원을 땅에서 파내면 지형도 함께 제거되는 편이 자연스러움
- 돌·금속 광석도 지표 위 오브젝트보다 땅을 파내며 채굴되는 방식이 더 어울림
- 경사면에 건물을 지을 수 없다면 건설 전에 지형을 평탄화해야 함
- 창의적 표현 도구로 지형 수정 자체를 제공할 수 있음
- 핵심 상황은 호수나 웅덩이의 경계를 파서 물길이 열렸을 때, 물이 어디로 얼마나 흘러가느냐임
단순 해법이 부족한 지점
- 가능한 우회책으로는 물을 초기 위치에 고정하거나, 특정 높이 이하를 모두 물로 간주하거나, 너무 깊게 팔 수 없게 제한하거나, Minecraft·Dwarf Fortress식 단순 흐름 모델을 쓰는 방법이 있음
- 이런 방식들은 대체안으로는 가능하지만, 주 모델로 쓰기에는 너무 단순하거나 블록감이 강함
- Dwarf Fortress 모델은 다른 예보다 가깝지만 3D 전제로 설계되어 있고, 필요한 문제는 주로 2D 지형 위 물임
- Timberborn은 여기서 다루는 것과 같은 모델을 사용함
원하는 시뮬레이션 조건
- 목표로 삼은 물 모델은 다음 조건을 만족해야 함
- 지형과 같은 격자에서 동작하는 편이 바람직함
- 평균 스케일은 약 1m 정도이며, 작은 물튀김까지 시뮬레이션할 필요는 없음
- 물은 지형 위 높이장으로 보고, 수직 흐름이나 수직 단면의 빈틈은 고려하지 않음
- 물은 흘러야 하며, 시뮬레이션 오차 때문에 사라지면 안 됨
- 안정성을 제어할 수 있어야 함
- 한 스텝 비용은 시뮬레이션 크기에 선형이어야 하며, 몇 번의 루프로 끝나는 형태가 이상적임
기존 유체 시뮬레이션과의 불일치
- Smoothed Particle Hydrodynamics는 인상적인 고해상도 유체 결과를 만들지만, 여기서 필요한 문제와 다름
- 1m 크기의 물 입자는 물풍선처럼 보일 수 있음
- 입자를 더 작게 만들면 성능 비용이 커짐
- 목표는 고해상도 사실성이 아니라 빠르고 그럴듯한 모델임
- Jos Stam의 Stable Fluids는 닫힌 탱크처럼 유체로 가득 찬 부피를 다루는 모델에 가까움
- 지형 위 자유 수면을 직접 다루는 문제와 다름
- 일부 단계에서 희소 선형 시스템을 반복적으로 풀어야 해서 비용이 큼
- 전체 Navier-Stokes를 푸는 접근이며, 여기서 필요한 것은 shallow water equations임
Shallow water equations와 격자 선택
- Shallow water equations는 지형 위 물층을 수직 방향으로 평균화해 2D 방정식으로 다루는 접근임
- “Shallow”는 물기둥의 수직 크기가 관심 있는 수평 규모보다 훨씬 작다는 가정을 뜻함
- 강의 수심이 수 m~수십 m이고 관심 거리가 km 단위인 경우처럼 성립할 수 있음
- 일반적인 collocated grid는 물 높이와 속도를 같은 셀에 저장하지만, 유체 동역학에서는 문제가 생길 수 있음
- 1차 미분을 순진하게 이산화하면 방향 편향이나 불안정성이 생길 수 있음
- 좌우에서 들어오고 위아래로 나가는 흐름이 같은 셀에 있으면 전체 속도가 0처럼 보이는 모순이 생김
- Staggered grid는 물 높이·밀도 같은 값은 셀에, 속도나 흐름은 셀 사이 엣지에 저장함
N x N물 높이 배열(N+1) x NX 방향 흐름 배열N x (N+1)Y 방향 흐름 배열
Virtual pipes 방식
- Virtual pipes는 물 셀들이 가상의 파이프로 연결되어 있다고 보고 흐름을 계산하는 방법임
- 참조한 논문 중 하나는 다중 레벨 물기둥과 수직 연결도 다루고, 다른 하나는 주로 수리 침식(hydraulic erosion)을 다루지만, 여기서는 해당 목표를 제외함
- 저장하는 값은 3개임
water: 각 셀의 물기둥 높이flowX: 수평 인접 셀 사이의 총 물 흐름flowY: 수직 인접 셀 사이의 총 물 흐름
- 속도 대신 흐름(flow, flux) 을 저장함
- 흐름은 단위 시간당 통과하는 물 부피로 볼 수 있음
- 빈 셀 사이 흐름은 0으로 자연스럽게 정의됨
- 속도는 흐름을 단면적으로 나눈 값이라 물이 거의 없을 때 0/0이나 임계값 문제가 생길 수 있음
한 스텝을 이루는 3단계
- 시뮬레이션 한 스텝은 세 단계로 나뉨
- 흐름 가속: 인접 셀의 물 표면 높이 차이에 따라 셀 사이 흐름을 증가시킴
- 유출 스케일링: 한 셀에서 빠져나가는 물이 실제 보유량보다 많으면 유출 흐름을 줄임
- 물기둥 업데이트: 인접 흐름에 따라 각 셀의 물 높이를 더하거나 뺌
-
흐름 가속
- 인접한 두 셀의 물 높이가 다르면 높은 쪽에서 낮은 쪽으로 흐름이 가속됨
- X·Y 방향 내부 엣지에 대해 흐름을 업데이트하며,
g,dt,dx,dy가 사용됨 - 가상 파이프 단면적
A는g와 곱으로만 쓰이므로 단순한 용도에서는g에 합친 것처럼 다룰 수 있음 - 마찰은 매 스텝 흐름을 줄이는 방식으로 추가됨
- 논문은
pow(friction, dt)계수를 권장함 - 더 직관적인 값으로 쓰기 위해
pow(1-friction, dt)를 사용할 수 있음 friction=0은 이전 흐름을 완전히 없애는 최대 마찰,friction=1은 마찰 없음으로 볼 수 있음dt가 커질수록 시뮬레이션은 빠르지만 불안정해질 수 있음- 유체 시뮬레이션에는 Courant-Friedrichs-Lewy condition이 중요함
- 실제로는 안정될 때까지
dt를 줄여야 하며, 사용한 값은 대략0.001~0.01임
-
물기둥 업데이트
- 각 셀은 인접한 네 흐름을 보고 물을 더하거나 뺌
- 왼쪽·아래쪽에서 들어오는
flowX(x,y),flowY(x,y)는 더함 - 오른쪽·위쪽으로 나가는
flowX(x+1,y),flowY(x,y+1)는 뺌 - 계산된 흐름에 따라 셀 사이 물을 실제로 이동시키는 단계임
-
유출 스케일링
- 흐름이 너무 크면 업데이트 후 어떤 셀의 물 높이가 음수가 될 수 있음
- 각 셀의 나가는 흐름만 합산해, 한 스텝에서 제거될 물이 실제 물 보유량을 넘는지 검사함
- 제거량이 너무 크면 나가는 흐름을 같은 비율로 줄여 물 높이가 0 이상을 유지함
- 이 단계가 음수 물량을 막는 핵심 안정화 장치임
지형, 경계 조건, 점성 처리
- 지형은 흐름 가속 단계에서 물기둥 높이 대신 물 표면 높이를 쓰는 방식으로 반영함
- 물 표면 높이 =
terrain(x,y) + water(x,y) - 지형이 높은 셀은 같은 물기둥 높이를 가져도 표면이 더 높아 물이 이동할 수 있음
- 물 표면 높이 =
- 경계 조건은 경계 흐름 값으로 암묵적으로 정해짐
flowX(0,y),flowX(N,y),flowY(x,0),flowY(x,N)이 경계에 해당함- 0으로 두면 벽처럼 작동함
- 유입 값은 물을 추가하고, 유출 값은 물을 제거함
- 지형 위 물에서는 지도 가장자리 물이 사라지는 유출 경계가 자연스러울 수 있음
- 강이 경계를 가로지르는 부분은 유입 경계로 두어 강물이 흐르게 할 수 있음
- 경계 흐름은 매 시뮬레이션 스텝 시작 시 다시 설정해야 함
- 유출 스케일링이 경계 흐름을 바꿔 유출 경계가 벽처럼 변할 수 있기 때문임
- 논문에는 물 높이에 따라 흐름을 줄이는 점성 항도 있음
- 작은 물층은 내부 힘 때문에 움직이기 어렵고, 큰 물층은 더 자유롭게 움직인다는 아이디어임
- 마그마 흐름 같은 곳에는 유용할 수 있음
- 물에서는 사용하지 않았고, 큰 지형 스케일에서 점성 영향은 거의 없음
구현 흐름과 성능 형태
- 전체 코드는 다음 순서로 구성됨
- 경계 흐름 초기화
pow(1-friction, dt)마찰 계수 사전 계산- X 흐름 가속
- Y 흐름 가속
- 음수 물량 방지를 위한 유출 스케일링
- 물기둥 업데이트
- 시뮬레이션 대부분은 몇 개의 2D 배열을 순회하는 4개 루프와 단순한 수식으로 끝남
- 전체 C++ 업데이트 코드는 water_2d.cpp에서 볼 수 있음
- 영상 예시는 며칠 전 공개한 WebGPU water simulator에서 나온 것이며, 영상의 입자는 시각화용이고 시뮬레이션에는 참여하지 않음
- 적절한
dt와g값을 찾으면 안정적으로 보이고, 요구 조건을 만족하며 물처럼 보이는 결과를 냄
남는 한계
- 이 모델에는 관성과 속도 확산이 없음
- 빠른 물줄기가 호수로 들어가도 호수 안쪽으로 계속 전파되지 않고 모든 방향으로 퍼짐
- 물 높이가 같다면 반대 방향의 평행한 두 물줄기가 서로 상호작용하지 않고 존재할 수 있음
- 물이 어떤 영역에 처음 들어갈 때 물결이 생기며, 다소 이상해 보일 수 있음
육각형·삼각형 격자로 확장하기
- 대상 게임은 정사각형 격자가 아니라 정규 삼각형 격자를 사용함
- 삼각형 격자는 육각형 격자의 쌍대(dual)로 볼 수 있음
- 인접한 육각형 중심을 선으로 연결하면 정규 삼각형 격자가 됨
- Red Blob Games의 hexagonal grids 글에서 axial coordinate system을 pointy-top hex 방향의 쌍대 격자에 쓰는 형태와 비슷함
- 삼각형 격자도 약간 기울어진 일반 2D 배열에 저장할 수 있음
- 물기둥 높이는 격자 정점에 저장해 수면 렌더링을 쉽게 함
- 흐름은 세 방향으로 나뉨
- X 방향 흐름
- Y 방향 흐름
- Z 방향 흐름
N x N정점 격자에서는 다음 배열을 사용함(N+1) x NX 흐름 배열N x (N+1)Y 흐름 배열(N+1) x (N+1)Z 흐름 배열, 단 bottom-left와 top-right 값은 사용하지 않음
- 정사각형 격자와 비교하면 경계 조건, 가속, 유출 스케일링, 물 업데이트에 Z 흐름을 추가하면 됨
- 가장 어려운 부분은 인덱싱을 틀리지 않는 것임
- 삼각형·육각형 격자용 C++ 코드는 water_2d_hex.cpp에서 볼 수 있음
- 이 방식은 정사각형 격자보다 조금 더 등방적일 수 있음
댓글과 토론
Hacker News 의견들
-
유체 시뮬레이션을 다룬 다른 접근으로 Coding Adventure 영상들이 있음
Rendering Fluids: https://www.youtube.com/watch?v=kOkfC5fLfgE
I Tried Putting my Fluid Simulation on a Planet: https://www.youtube.com/watch?v=8nIB7e_eds4&t=817s
GitHub: https://github.com/SebLague/Fluid-Sim?tab=readme-ov-file- 두 번째 영상은 보는 내내 즐거웠고, 이런 식의 상위 수준 탐험형 영상을 정말 좋아함
- Coding Adventure를 정말 좋아함. 작은 게임 Geographical Adventures도 있는데 음악이 좋고 꽤 편안함
[0] https://www.youtube.com/playlist?list=PLFt_AvWsXl0dT82XMtKAT...
[1] https://github.com/SebLague/Geographical-Adventures
-
절차적 생성 게임에서 수문 시뮬레이션이 어려운 이유 중 하나는 물이 쌓이면 주변 칸에 영향을 주고, 그 영향이 다시 다른 주변 칸으로 계속 퍼진다는 점임
절차적 생성은 병렬화에 잘 맞는 경우가 많지만, 정작 병렬화가 가장 필요해 보이는 무한 영역에서는 이런 계산을 제대로 병렬화하기 어렵다
이 주제는 많이 탐구된 걸 못 봤고, 관련 작업을 하는 사람 중 https://nickmcd.me를 특히 좋아함. 지금까지 본 절차적 지형 중 손꼽히게 좋음
다만 그 작업도 시뮬레이션 설계 때문에 영역이 제한되어 있음. 가능한 해법으로는 깨질 수 없는 유역 경계를 절차적으로 생성한 뒤, 유역 전체를 한 번에 병렬 시뮬레이션하는 방식이 가장 좋아 보임
매우 흥미로운 문제지만 내 지식 범위 밖이라 주로 관찰자 입장임- 궁금하다면 편미분방정식을 푸는 분야에서 말하는 “영향 영역”과 “의존 영역”을 찾아보면 좋음
특정 지점의 값에 영향을 줄 수 있는 영역과, 그 지점의 값이 영향을 줄 수 있는 영역을 뜻하며, 바로 위에서 말한 내용과 맞닿아 있음. 어떤 경우에는 그 영역을 미리 알 수 있음 - 진심으로 https://nickmcd.me는 꼭 확인해볼 만함. 정말 대단함
- 흥미로운 질문임. 아마 각 영역 둘레에 경계를 두고, 효과가 전파되는 최대 속도, 즉 인과성을 가정하면 가능해 보임
예를 들어 10시간 단계를 시뮬레이션하려면 10개 격자 칸짜리 경계를 둠. 각 영역에서 10단계를 계산한 뒤, 병렬로 계산된 다른 경계 시뮬레이션과 경계 상태를 동기화하고 반복하면 됨 - Nick이 겨우 25세라는 걸 알고 놀랐음
- 궁금하다면 편미분방정식을 푸는 분야에서 말하는 “영향 영역”과 “의존 영역”을 찾아보면 좋음
-
약간 주제에서 벗어나지만, 글에서 자원 채집을 위해 지형 조작이 필요하다고 했던 부분이 떠오름
Animal Crossing은 지형 조작 없이도 꽤 영리하고 효율적으로 처리했다고 늘 생각했음. 나무를 베면 통나무가 나오지만 일정량만 나오고 사실상 재사용 대기 시간이 생김
비싼 지형 조작 없이도 피드백과 유한한 자원 느낌을 줄 수 있음. 물론 모든 게임에 맞지는 않고 작은 지도에서 더 잘 맞지만, 고려해볼 만함. 게임에 꼭 필요하지 않다면 지형 조작은 하지 않는 편이 나을 때가 많음- 글에서도 주변에 놓인 금광석 바위 형태로 그런 전략을 다뤘음
자원 지급 방식으로는 표준적이지만, 재사용 대기 시간이 무한 자원 문제를 없애지는 못하고 속도만 늦춤. 그리고 좀 지루하고 영향력이 약함
- 글에서도 주변에 놓인 금광석 바위 형태로 그런 전략을 다뤘음
-
이 주제를 깔끔하게 파고든 글이고, Timberborn을 짚어줘서 반가움
요즘 그 게임에 완전히 빠져 있어서 아직 안 해봤다면 강력 추천함. 물리 기반 물 흐름이 게임 속 또 다른 캐릭터 같고, 물을 막아 엔진에 쓰고 밭에 공급하는 방법을 알아내는 게 핵심 게임 루프임 -
재미있고 실행도 정말 잘 됐음. 이런 걸 개발할 때 가장 큰 위험은 예쁜 결과를 보며 매개변수만 만지다가 몇 시간을 날리는 것임
2011년에 논문 작업으로 직접 GPU 기반 유체역학을 구현했던 기억이 떠올랐음. 표면, 즉 조직 위를 흐르는 유체인 피를 다뤘고, 2D로 시뮬레이션한 뒤 중력과 표면 기울기를 고려해 메시 위에 투영했음
짧은 영상도 YouTube에 올렸음: https://youtu.be/4vGrNc-GGW8 -
정말 멋짐
최근 o3-mini-high의 도움을 받아 비슷한 아이디어를 실험했음. 알고리즘 아이디어를 설명해줬더니 수동 개입 없이 3D로 구현하고 렌더링했음. 다만 프롬프트는 여러 번 넣었음
https://3d-water-sim.netlify.app/
아직 완벽하지는 않은데, 만지다 멈췄기 때문이고 반복할 때마다 꽤 크게 좋아지고 있었음. 재미있는 점은 지형 생성을 위해 CDN 같은 데서 가져오지 않고, Perlin 잡음의 동작 버전을 처음부터 제대로 구현했다는 것임- 이런 실험을 할 때 목적이 재미인지 학습인지 궁금함. 학습이 목적이라면 직접 구현하지 않았더라도 이 글을 읽는 게 여전히 가치 있다고 느끼는지도 궁금함
여정과 목적지의 차이에 관한 질문임
- 이런 실험을 할 때 목적이 재미인지 학습인지 궁금함. 학습이 목적이라면 직접 구현하지 않았더라도 이 글을 읽는 게 여전히 가치 있다고 느끼는지도 궁금함
-
글에서 “이 모델은 관성과 속도 확산이 없다. 빠른 물줄기가 호수로 들어가도 호수 안쪽으로 더 전파되지 않고 누적된 관성을 무시한 채 모든 방향으로 퍼진다. 수위가 같다면 반대 방향으로 흐르는 두 평행 물줄기도 서로 상호작용하지 않을 수 있다”고 한 부분은, 같은 방향의 주변 6개 흐름 화살표와 평균을 내면 해결할 수 있을 것 같음
앞뒤 화살표에는 큰 가중치를, 옆쪽 화살표에는 작은 가중치를 주는 식임. 예를 들어 이런 화살표가 있을 때
-a-> -b->
-c-> -d-> -e->
-f-> -g->
New_d = d * (1 - 2*.1 - 4*.01) + (c+e).1 + (a+b+f+b).01
여기서 .1과 .01은 임의로 잡은 가중치라 조정이 필요하고, 진동을 줄이는 데 쓰는 것처럼 거듭제곱을 넣을 수도 있음. 그 계수까지 포함하면 다음처럼 될 수 있음
New_d = d * (1 - 2*.1 - 4*.01 - .001) + (c+e).1 + (a+b+f+b).01- 제대로 된 해법은 원래 격자와 맞는 2차 미분용 격자를 하나 더 추가하는 것에 가까워 보임
grid 0: 각 칸의 물 높이
grid 1: 각 모서리의 물 흐름, 즉 1차 미분
grid 2: 각 칸의 물 가속도, 즉 2차 미분
각 격자가 이전 격자의 쌍대 격자이고 그 미분값을 저장하는 구조임. 사실 모서리 데이터를 특별 취급할 필요 없이 꼭짓점 데이터만 두고 순수하게 쌍대 격자로 처리해도 될 것 같음. 모서리 흐름은 그 모서리 양끝 꼭짓점 흐름의 합으로 유도할 수 있음
따라서 흐름으로 유체 높이를 갱신하고, 어떤 속도로 얼마나 많은 유체 질량이 칸에 들어왔는지에 따라 가속도를 갱신한 뒤, 가속도와 현재 유체 높이로 흐름을 갱신함. 유체역학은 잘 모르지만 수치 시뮬레이션 관점에서는 맞아 보이고, 대각선 흐름도 가능해짐 - 그 방식은 운동량 보존을 깨뜨림. 현실적인 흐름을 얻으려면 연속 방정식을 써서 흐름의 에너지를 보존하고, 전단이 소용돌이로 소산·확산되게 해야 함
글에서 말했듯 계산량이 훨씬 늘어나므로, 그 정도 현실성이 실제 사용 사례에 필요한지 먼저 따져봐야 함
- 제대로 된 해법은 원래 격자와 맞는 2차 미분용 격자를 하나 더 추가하는 것에 가까워 보임
-
몇 년 전에 궁금해서 만든 조잡한 결과물임: https://aperocky.com/hydrosim/
이 개인 프로젝트가 차가운 보관 선반으로 들어가기 전까지 침식을 어떻게 처리할지 알아내지 못했음. 글쓴이가 이 부분을 언급하고 방정식까지 붙인 점이 좋았음 -
최근 비슷한 걸 공개했음. 무작위 heightfield 생성, 퇴적물 이동, 침식까지 들어 있음: https://github.com/Ono-Sendai/terraingen
-
우리 회사의 훌륭한 개발자가 연구 프로젝트의 일부로 만든 교육용 홍수 시뮬레이션을 직접 만져볼 수 있음
https://flood.concord.org/
큰 효과를 보려면 아래 도구막대에서 모델 값을 바꿔야 함
인접 칸을 바탕으로 WebGL에서 칸 값을 계산하는 셀 기반 시뮬레이션임. 그 계산을 하는 셰이더는 여기 있음
https://github.com/concord-consortium/flooding-model/blob/ma...