유체 효과 구현
Grid 3D·Vector Field·Noise Force로 흐름과 난류를 만들고 밀도·부력·렌더링을 조정해 연기 기둥을 구현합니다.
피어오르는 연기나 소용돌이치는 먼지는 흐름이 있어야 자연스럽습니다. 이런 흐름은 두 가지 방법으로 만들 수 있습니다. 힘을 받는 스프라이트 입자로 흐름을 흉내 낼 수도 있고, 나이아가라 유체(Niagara Fluids)의 격자 시뮬레이션으로 실제로 계산할 수도 있습니다. 어느 쪽을 고를지는 필요한 상호작용과 화면의 표현에 달려 있습니다. 이 글에서는 두 방법의 차이와 격자의 비용을 계산해 보고, 흐름을 만드는 힘과 반투명 연기가 벽에 닿는 모습을 다듬는 방법을 살펴본 뒤, 스프라이트로 연기 기둥을 만들어 봅니다.
스프라이트 흐름과 격자 시뮬레이션
먼저 흔한 오해를 하나 짚습니다. 일반 Collision 모듈에서 유체 응답을 켜고 Fluid Density Scale이나 Fluid Resistance 같은 값을 정하면 유체 계산이 완성된다는 식의 절차는 쓰지 않습니다. 단순한 입자의 충돌과 저항은, 격자에 밀도와 온도, 속도를 저장해 계산하는 유체 시뮬레이션과 구조가 다르기 때문입니다.
나이아가라 유체 플러그인은 2D와 3D의 기체(Gas)와 액체(Liquid) 같은 템플릿을 제공합니다. 예를 들어 Grid 3D Gas에서는 격자의 칸(셀)마다 상태를 저장해 시뮬레이션하고, 그 결과를 볼륨으로 그립니다. 그래서 바운드와 셀의 크기, 계산 설정, 렌더링의 샘플링을 함께 다뤄야 합니다.
격자는 해상도를 올리면 비용이 빠르게 늘어납니다. 32×32×16 격자의 셀 수는 32 × 32 × 16 = 16,384개인데, 세 축을 모두 두 배로 한 64×64×32는 131,072개로 8배입니다. 계산 연습을 위해 셀마다 상태를 16바이트로 가정하면, 상태 한 벌의 크기는 각각 256KiB와 2MiB입니다. 같은 공간 범위에서 각 축의 해상도를 두 배로 올리면 셀 수가 약 8배가 되므로 메모리와 GPU 비용을 꼭 확인해야 합니다. 다만 이 수치는 실제 구현의 추가 버퍼나 여백, 중간 결과, 렌더링 샘플링까지 포함해 잰 GPU 메모리가 아닙니다.
이 글의 실습은 격자 계산을 직접 구현하는 것이 아니라 스프라이트로 만드는 연기 기둥입니다. 실제의 압력이나 부력, 벽과의 상호작용이 필요하다면 나이아가라 유체 템플릿에서 따로 시작합니다.
더 깊이: 유체 템플릿을 게임에 쓸 때 고를 것
나이아가라 유체 플러그인을 켜면 불, 연기, 기체 같은 효과의 표준 템플릿을 쓸 수 있습니다. Niagara Fluids 문서는 이 가운데 2D 템플릿은 게임에 맞게 최적화되어 있고, 3D 템플릿은 시네마틱을 위해 설계되었다고 구분합니다. 앞에서 계산한 것처럼 3D 격자는 세 축의 해상도를 함께 올리면 셀 수가 빠르게 늘어나므로, 실시간 게임 장면이라면 2D 템플릿부터 검토하는 것이 이 구분과 맞습니다.
문서는 유체 장면의 그래픽 부하가 커서 Windows에서 GPU 크래시가 날 수 있다고도 경고하며, 그 해결 방법은 별도의 안내 문서로 연결합니다. 또 유체 시뮬레이션은 무거울 수 있으므로, 유체 이미터로 결과를 만든 뒤 플립북(flipbook)으로 구워 어떤 머티리얼에서든 쓰는 방법을 제안합니다. 플레이어와 상호작용할 필요가 없는 배경 연기라면, 이렇게 구운 플립북을 스프라이트 입자의 SubUV로 재생하는 방식과 실시간 유체를 같은 조건에서 비교해 볼 수 있습니다.
흐름을 만드는 힘
스프라이트 연기의 흐름은 여러 힘을 조합해 만듭니다. 위로 올라가는 큰 흐름은 Particle Spawn의 초기 Z 속도와 Particle Update의 위쪽 가속도로 정합니다. 아래로 끄는 중력을 줄이는 것만으로 상승이 계속 유지되지는 않습니다. Drag는 속도를 줄여 연기가 지나치게 퍼지지 않게 하므로, 상승이나 난류를 조정하기 전에 기본 궤적부터 확인합니다.
불규칙한 흔들림은 Curl Noise Force 같은 노이즈 힘 모듈로 더합니다. 공간에 깔린 노이즈로 불규칙한 힘을 주는데, 세기(Strength)는 힘의 크기를, 주파수(Frequency)는 공간 패턴의 크기를 정합니다. 시간에 따라 패턴이 바뀌는 입력과 공간의 주파수를 헷갈리지 않으며, 모든 버전의 노이즈 힘 모듈에 같은 Noise Speed 항목이 있다고 가정하지 않습니다.
미리 만든 3D 벡터 데이터를 샘플링하는 벡터 필드(Vector Field)로 큰 방향과 소용돌이를 만들 수도 있습니다. 이때는 실제 프로젝트에 있는 에셋과 모듈, 데이터 인터페이스의 지원, 필드의 좌표 공간을 확인합니다. VF_Turbulence 같은 특정 에셋이 모든 설치에 들어 있는 것은 아닙니다.
반투명 연기가 벽에 닿는 모습
연기용 Translucent 머티리얼에서는 텍스처의 Alpha와 Particle Color의 Alpha를 곱해 Opacity로 넘깁니다. 그런데 연기 스프라이트가 벽이나 바닥을 지나가면, 평평한 면이 표면과 만나는 자리에 딱딱한 선이 보입니다. 이 선은 머티리얼의 DepthFade로 부드럽게 만들 수 있습니다. DepthFade의 Opacity 입력에 기존 알파를 넣고, 그 결과를 머티리얼의 Opacity에 연결한 뒤, Fade Distance를 월드 단위로 조절합니다. 렌더러에 있는 공통 Soft Particle 체크박스를 켜는 방식과는 다른 절차입니다.
DepthFade는 화면에서 만나는 자리를 흐리게 할 뿐, 입자가 표면을 뚫는 것을 물리적으로 막지는 않습니다. 실제로 부딪혀야 한다면 따로 Collision이 필요합니다. 볼류메트릭 안개(Volumetric Fog)나 구름과 함께 쓸 때도 스프라이트와 볼륨의 비용을 따로 측정합니다.
연기 기둥 만들기
먼저 기본 스프라이트 시스템을 복제해 NS_SimpleSmoke로 저장합니다. Emitter State가 계속 재생되게 하고, Emitter Update의 Spawn Rate는 초당 20개 정도에서 시작합니다. Particle Spawn의 Initialize Particle에서 Lifetime은 3초에서 5초 사이, 스프라이트 크기는 50cm에서 100cm 사이, 색은 (0.1, 0.1, 0.1, 1)로 두고, Shape Location으로 입자가 생기는 작은 영역을 정합니다. 같은 그룹의 Add Velocity로는 Z를 50cm/s에서 100cm/s 사이, X와 Y를 -10cm/s에서 10cm/s 사이의 값으로 초기 속도를 줍니다. 아래로 끄는 Gravity Force는 우선 꺼 두고 기본 상승을 확인합니다.
다음으로 Particle Update에 Particle State와 Drag 0.5, Solve Forces and Velocity를 둡니다. 계속 올라가야 한다면 Solve 앞에 Acceleration Force를 두고 Z를 20cm/s² 정도의 약한 상승 가속도로 시험합니다. 이것은 부력을 눈에 보이게 흉내 낸 것이지, 실제 밀도의 차이를 계산하는 것은 아닙니다.
그다음 Solve 앞에 Curl Noise Force를 추가합니다. Noise Strength 50, Noise Frequency 0.02처럼 작은 값에서 시작해 한 항목씩 조절하고, 세기를 올릴 때 위로 올라가는 흐름이 난류에 묻히지 않는지 확인합니다. 입자마다 고정된 난수가 필요하다면 Spawn에서 한 번 정해 속성에 보관합니다.
이어서 Sprite Renderer에 프로젝트의 한 장짜리 연기 텍스처를 쓰는 Translucent 머티리얼을 지정합니다. 텍스처가 스프라이트 시트라면 Sub Image Size와 Index, UV도 함께 맞춥니다. 머티리얼의 DepthFade로 불투명한 표면과 만나는 자리를 부드럽게 다듬습니다.
크기와 색의 변화도 줍니다. Particle Update의 Scale Sprite Size에 NormalizedAge를 기준으로 배율이 1에서 2로 커지는 커브를 연결해, 초기 크기에서 점점 부풀게 합니다. Scale Color의 RGB 배율은 1에서 3으로, Alpha 배율은 1에서 0으로 둡니다. 초기 RGB 0.1에 배율 3을 곱하면 0.3이 되므로, 연기가 올라가며 조금 밝아집니다. 이것은 색 자체를 커브로 쓰는 것과는 다른 방식입니다.
마지막으로 수명이 다하면 입자가 사라지게 하고, 같은 카메라에서 연기 밑동의 폭과 상승, 퍼짐, 알파가 사라지는 모습을 비교합니다. 연기가 아래로 떨어지면 아래쪽 가속도를, 너무 산만하면 Noise Strength를, 벽에서 잘려 보이면 DepthFade와 실제 충돌이 필요한지를 나누어 확인합니다.
이 설정의 목표는 위로 올라가며 불규칙하게 퍼지다가 사라지는 연기입니다. 정상 상태에서 동시에 살아 있는 입자는 대략 Spawn Rate × 평균 Lifetime으로 어림할 수 있으며, 초당 20개와 평균 4초라면 약 80개입니다. 다만 반투명이 덮는 면적과 겹침의 비용은 따로 측정해야 합니다.
정리
- 연기나 먼지의 흐름은 힘을 받는 스프라이트로 흉내 낼 수도 있고, 나이아가라 유체의 격자 시뮬레이션으로 계산할 수도 있습니다.
Collision모듈의 값을 바꾸는 것으로 유체 계산이 생기지 않으며, 실제 압력이나 부력이 필요하면 유체 템플릿에서 시작합니다.- 격자는 세 축의 해상도를 두 배로 올리면 셀 수가 8배가 되므로 메모리와 GPU 비용을 확인합니다.
- 스프라이트 연기의 흐름은 초기 속도와 상승 가속도,
Drag,Curl Noise Force같은 노이즈 힘을 조합해 만듭니다. - 반투명 연기가 표면과 만나는 자리는 머티리얼의
DepthFade로 부드럽게 하며, 이것은 표면을 뚫는 것을 막지 않습니다.
연습 문제
1. 한 축만 두 배로
32×32×16 격자에서 첫 축만 두 배로 바꾸면 셀 수와, 셀마다 16바이트로 가정한 상태의 크기는 어떻게 달라질까요? 이 변경을 스프라이트 실습의 Spawn Rate를 두 배로 바꾼 것과 같다고 설명해도 될까요?
해설 보기
64×32×16은 64 × 32 × 16 = 32,768셀이고, 상태의 크기는 32,768 × 16 = 524,288바이트, 즉 512KiB로 원래의 2배입니다.
이것을 Spawn Rate를 두 배로 바꾼 것과 같다고 볼 수는 없습니다. 격자에 저장하는 상태의 수와 스프라이트의 살아 있는 입자 수는 서로 다른 구조입니다. 스프라이트 실습의 생성률과 수명은 따로 유지되며, 셀 수만 보고 프레임 시간이 정확히 2배나 8배가 된다고 말하지도 않습니다.
2. 떠오르다 가라앉는 연기
NS_SimpleSmoke의 연기가 처음에는 올라가다가 잠시 뒤 아래로 가라앉습니다. 어디를 확인해야 할까요?
해설 보기
먼저 Gravity Force가 켜져 있는지 확인합니다. 초기 Z 속도는 50cm/s에서 100cm/s 정도로 작아서, 중력이 켜져 있으면 금방 속도가 0을 지나 아래로 바뀝니다. Drag도 위로 향하는 속도를 계속 줄이므로, 상승을 유지하는 힘이 없으면 연기는 멈추거나 가라앉습니다.
계속 떠오르게 하려면 중력을 끄거나 약하게 하고, Solve 앞의 Acceleration Force로 약한 상승 가속도를 줍니다. 노이즈의 세기가 너무 커서 아래쪽 성분이 섞이고 있지 않은지도 함께 봅니다.
3. 벽을 지나가는 연기
DepthFade를 적용했더니 벽과 만나는 자리의 딱딱한 선은 사라졌는데, 연기가 여전히 벽 너머로 넘어가 보입니다. 무엇이 빠졌을까요?
해설 보기
DepthFade는 화면에서 연기 스프라이트와 표면이 만나는 자리를 흐리게 할 뿐, 입자가 벽을 뚫고 지나가는 것을 막지 않습니다. 입자의 위치는 여전히 벽 너머로 움직이고 있습니다.
연기가 벽에서 멈추거나 비켜 가야 한다면 Collision을 따로 더하거나, 연기가 벽 쪽으로 가지 않도록 초기 속도와 노이즈의 범위를 조절합니다. 충돌을 더한다면 그 비용도 함께 측정합니다.
나이아가라 유체의 템플릿과 설정은 Epic의 나이아가라 유체 레퍼런스에서, DepthFade의 입력은 깊이 머티리얼 표현식 문서에서 확인할 수 있습니다.
다음 글 파티클 시스템의 최적화 기법에서는 충돌과 물리, 유체처럼 비용이 큰 효과를 예산 안에 맞추는 방법을 정리합니다.