고도 엔진으로 배우는 유체 역학 시뮬레이션
Godot(고도) 게임 엔진을 활용해 복잡한 Navier-Stokes 방정식 기반의 유체 시뮬레이션을 구현하는 과정을 다룬 실전 중심의 튜토리얼입니다. 성능 최적화보다는 코드의 가독성과 학습의 편의성에 초점을 맞추어, 게임 개발자들이 유체 역학의 원리를 쉽게 이해하고 적용할 수 있도록 돕습니다.
2026년 5월 19일
고도 게임 엔진으로 설명하는 나비에-스토크스(Navier-Stokes) 유체 시뮬레이션
게임 개발을 하다 처음 유체 시뮬레이션을 접했을 때, 그 효과가 얼마나 훌륭한지에 감탄했습니다. 이것이 어떻게 작동하는지 정말 배우고 싶었지만, 놀랍게도 이 주제에 대한 학습 자료는 매우 부족했고 제가 찾은 자료들도 이해하기가 꽤 어려웠습니다. 그럼에도 불구하고 나는 직접 시도해보기로 결심했습니다. 그리고 하는 김에 블로그 포스트를 작성해서 다음에 도전하는 사람들에게 조금이나마 도움이 되길 바랐습니다.
시작하기 전에 몇 가지 강조하고 싶은 점이 있습니다.
- 저는 수학자가 아닙니다. 제 설명에서 오류를 발견하시면 Bluesky에서 DM을 보내주시거나 이메일을 보내주세요. 수정하겠습니다.
- 이 구현은 오직 학습 목적입니다. 이러한 이유로 성능면에서 최고가 아닌 방식으로 구현되었습니다. 계산은 전적으로 CPU에서 이루어집니다. 읽고 배우기 쉽게 하기 위해 너무 많은 변수를 도입했지, 최대의 FPS를 끌어내기 위함이 아니었습니다.
제가 참고한 학습 자료는 다음과 같습니다:
- Jos Stam의 "Real-Time Fluid Dynamic for Games" (PDF)
- Mike Ash의 "Fluid Simulation for Dummies"
이 저장소(github.com/rskupnik/godot-fluid-simulation-demo)에서 모든 코드를 찾을 수 있습니다. 저는 이 블로그 포스트의 챕터와 일치하는 코드 체크포인트를 표시하기 위해 git 커밋을 사용했습니다. 따라서 글을 읽으면서 코드를 직접 작성하고 싶지 않다면, 커밋 뷰를 통해 따라 가실 수 있습니다. 편의를 위해 각 챕터에 '프로젝트 스냅샷'과 'diff(차이점)' 링크를 포함시켰습니다. 이는 각각 논의된 시점의 코드베이스와 커밋 diff 보기로 이어집니다.
AI 고지: 이 블로그 포스트의 모든 단어와 이 코드베이스의 모든 코드 줄은 제가 직접 작성했습니다. 모든 다이어그램과 비디오는 제가 만들었습니다. AI는 연구 목적으로만 사용되었습니다.
마지막으로, 이런 작업이 마음에 드신다면 저에게 커피 한 잔 사주시는 것을 고려해 주세요 :)
기초
우리가 사용할 알고리즘은 유체 흐름에 대한 물리 방정식인 Navier-Stokes 방정식을 기반으로 합니다. 우리의 사용 사례는 게임 개발이므로 계산의 정밀도를 희생하여 속도를 선호합니다. 효과는 충분히 좋아야 하지만 지나치게 비용이 많이 들어서는 안 됩니다. 우리는 이것을 세 가지 방법으로 달성합니다. 첫째로, 우리는 큰 셀을 가진 비교적 작은 격자(grid)를 사용합니다. 둘째로, 우리는 임의의 시간 간격으로 시뮬레이션을 진행합니다. 마지막으로, 우리는 몇 가지 방정식에 대한 충분히 좋은 해답을 얻기 위해 근사 방정식(예: Gauss-Seidel 완화법)을 사용합니다.
이 블로그 포스트에서 우리가 할 일에 대한 수학적 설명으로 시작하겠습니다. 이 설명은 겁먹게 들릴 수 있지만, 걱정하지 마세요 - 진행하면서 모든 것을 설명할 것입니다. 자, 여기 있습니다: 우리는 벡터 속도 장(velocity field)을 통해 스칼라 밀도 장(density field)을 이동시켜 유체 흐름을 시뮬레이션할 것입니다. 우리는 속도 확산(diffusion)과 이류(advection)뿐만 아니라 밀도 확산과 이류도 시뮬레이션할 것입니다. 그런 다음 유체가 질량 보존 법칙을 따르도록 하기 위해 속도 투영(projection)을 추가할 것입니다 - 이는 발산(divergence)을 압력 장(pressure field)과 균형을 맞춤으로써 발생할 것입니다. 우리는 필요한 곳의 값을 근사하기 위해 이중 선형 보간법(bilinear interpolation)과 Gauss-Seidel 완화법을 사용할 것입니다.
자, 모든 준비가 끝났으니 시작합시다!
여정은 격자(Grid)에서 시작됩니다
프로젝트 스냅샷
새로운 Godot 프로젝트를 만들고 Node2D를 추가합니다. 저는 mine을 "FluidGrid"라고 불렀습니다. 여기에 스크립트를 연결합니다. 모든 코드는 그 스크립트 안에 들어갑니다.
첫째로, 격자를 정의해 봅시다:
@export var N := 16 @export var cell_size := 32 var size := 0
여기서 N은 행과 열의 셀 개수, 즉 기본적으로 격자의 크기이며, cell_size는 픽셀 단위의 단일 셀 크기입니다. 우리는 또한 곧 초기화할 size를 정의합니다. 우리는 테두리도 원하기 때문에 이것을 N+2로 설정할 것입니다.
다음으로, 실제 데이터를 저장하기 위한 배열을 추가합시다.
density는 '이 셀이 얼마나 많은 물질을 포함하고 있는가'를 의미합니다.
var density : PackedFloat32Array var density_prev : PackedFloat32Array
"u"는 수평 속도(x 방향)를 저장합니다.
var u : PackedFloat32Array var u_prev : PackedFloat32Array
"v"는 수직 속도(y 방향)를 저장합니다.
var v : PackedFloat32Array var v_prev : PackedFloat32Array