2P by GN⁺ | ★ favorite | 댓글 1개
  • 지형을 파고 메울 수 있는 게임에서는 호수·강·웅덩이가 새 경계로 흘러나가야 하므로, 빠르고 안정적인 격자 기반 물 시뮬레이션이 필요함
  • 목표는 약 1m 스케일에서 지형과 같은 격자를 쓰고, 물 보존·제어 가능한 안정성·선형 업데이트 비용을 만족하는 2D 높이장 모델임
  • Smoothed Particle Hydrodynamics와 Stable Fluids는 각각 고해상도 입자 유체와 닫힌 유체 부피에 맞춰져 있어, 지형 위 자유 수면을 빠르게 처리하려는 조건과 어긋남
  • 채택한 virtual pipes 방식은 staggered grid에 물 높이와 셀 사이 흐름을 저장하고, 흐름 가속·유출 스케일링·물기둥 업데이트를 몇 개의 2D 배열 루프로 처리함
  • 적절한 dtg에서는 물처럼 보이는 결과를 내지만, 관성과 속도 확산이 없어 빠른 물줄기가 호수 안으로 이어져 전파되지 않는 한계가 남음

지형 수정 게임에서 물이 어려운 이유

  • 전략 게임이나 도시·마을 시뮬레이션에서 물은 자연스러운 경계, 항해·낚시·교역·해전, 식수·운송·미관 같은 역할을 맡을 수 있음
  • 지형을 직접 바꿀 수 있으면 물 처리 난도가 크게 올라감
    • 흙·모래·점토 같은 자원을 땅에서 파내면 지형도 함께 제거되는 편이 자연스러움
    • 돌·금속 광석도 지표 위 오브젝트보다 땅을 파내며 채굴되는 방식이 더 어울림
    • 경사면에 건물을 지을 수 없다면 건설 전에 지형을 평탄화해야 함
    • 창의적 표현 도구로 지형 수정 자체를 제공할 수 있음
  • 핵심 상황은 호수나 웅덩이의 경계를 파서 물길이 열렸을 때, 물이 어디로 얼마나 흘러가느냐임

단순 해법이 부족한 지점

  • 가능한 우회책으로는 물을 초기 위치에 고정하거나, 특정 높이 이하를 모두 물로 간주하거나, 너무 깊게 팔 수 없게 제한하거나, 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 N X 방향 흐름 배열
    • 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가 사용됨
    • 가상 파이프 단면적 Ag와 곱으로만 쓰이므로 단순한 용도에서는 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에서 나온 것이며, 영상의 입자는 시각화용이고 시뮬레이션에는 참여하지 않음
  • 적절한 dtg 값을 찾으면 안정적으로 보이고, 요구 조건을 만족하며 물처럼 보이는 결과를 냄

남는 한계

  • 이 모델에는 관성과 속도 확산이 없음
    • 빠른 물줄기가 호수로 들어가도 호수 안쪽으로 계속 전파되지 않고 모든 방향으로 퍼짐
    • 물 높이가 같다면 반대 방향의 평행한 두 물줄기가 서로 상호작용하지 않고 존재할 수 있음
  • 물이 어떤 영역에 처음 들어갈 때 물결이 생기며, 다소 이상해 보일 수 있음

육각형·삼각형 격자로 확장하기

  • 대상 게임은 정사각형 격자가 아니라 정규 삼각형 격자를 사용함
  • 삼각형 격자는 육각형 격자의 쌍대(dual)로 볼 수 있음
    • 인접한 육각형 중심을 선으로 연결하면 정규 삼각형 격자가 됨
    • Red Blob Games의 hexagonal grids 글에서 axial coordinate system을 pointy-top hex 방향의 쌍대 격자에 쓰는 형태와 비슷함
  • 삼각형 격자도 약간 기울어진 일반 2D 배열에 저장할 수 있음
  • 물기둥 높이는 격자 정점에 저장해 수면 렌더링을 쉽게 함
  • 흐름은 세 방향으로 나뉨
    • X 방향 흐름
    • Y 방향 흐름
    • Z 방향 흐름
  • N x N 정점 격자에서는 다음 배열을 사용함
    • (N+1) x N X 흐름 배열
    • 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

  • 절차적 생성 게임에서 수문 시뮬레이션이 어려운 이유 중 하나는 물이 쌓이면 주변 칸에 영향을 주고, 그 영향이 다시 다른 주변 칸으로 계속 퍼진다는 점임
    절차적 생성은 병렬화에 잘 맞는 경우가 많지만, 정작 병렬화가 가장 필요해 보이는 무한 영역에서는 이런 계산을 제대로 병렬화하기 어렵다
    이 주제는 많이 탐구된 걸 못 봤고, 관련 작업을 하는 사람 중 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차 미분
      각 격자가 이전 격자의 쌍대 격자이고 그 미분값을 저장하는 구조임. 사실 모서리 데이터를 특별 취급할 필요 없이 꼭짓점 데이터만 두고 순수하게 쌍대 격자로 처리해도 될 것 같음. 모서리 흐름은 그 모서리 양끝 꼭짓점 흐름의 합으로 유도할 수 있음
      따라서 흐름으로 유체 높이를 갱신하고, 어떤 속도로 얼마나 많은 유체 질량이 칸에 들어왔는지에 따라 가속도를 갱신한 뒤, 가속도와 현재 유체 높이로 흐름을 갱신함. 유체역학은 잘 모르지만 수치 시뮬레이션 관점에서는 맞아 보이고, 대각선 흐름도 가능해짐
    • 그 방식은 운동량 보존을 깨뜨림. 현실적인 흐름을 얻으려면 연속 방정식을 써서 흐름의 에너지를 보존하고, 전단이 소용돌이로 소산·확산되게 해야 함
      글에서 말했듯 계산량이 훨씬 늘어나므로, 그 정도 현실성이 실제 사용 사례에 필요한지 먼저 따져봐야 함
  • 몇 년 전에 궁금해서 만든 조잡한 결과물임: 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...