불꽃에 생명력을 불어넣는 방법: Ignitement의 실시간 유체 시뮬레이션 (Real-time fluid simulation)
요약
1인 개발자 Sørb의 게임 'Ignitement'에서 사용된 실시간 유체 시뮬레이션 기술을 분석합니다. 고비용의 3D 시뮬레이션 대신 2D Graphics.Blit과 프래그먼트 셰이더를 활용하여 성능과 호환성을 동시에 확보한 VFX 구현 방식을 다룹니다.
핵심 포인트
- 전통적인 파티클 시스템 대신 동적인 2D 실시간 유체 시뮬레이션 채택
- Compute Shader 대신 Fragment Shader를 사용하여 하드웨어 호환성 및 텍스처 필터링 활용 극대화
- 밀도, 속도, 온도, 반응을 나타내는 별도의 텍스처 세트를 통한 물리 속성 제어
- Graphics.Blit을 활용하여 후처리 효과 수준의 낮은 연산 비용으로 구현
오늘의 게스트 포스트에서는 1인 개발자 Sørb가 자신의 차기 액션 로그라이트 (roguelite) 게임인 Ignitement에 담긴 인상적인 불과 용암 VFX (Visual Effects) 뒤의 기술적 예술성을 분석합니다.
Ignitement를 볼 때 가장 먼저 눈에 띄는 것은 VFX입니다. 그중에서도 특히 불꽃은 즉각적으로 무언가 다르다는 느낌을 줍니다. 단순히 애니메이션처럼 보이는 것이 아니라, 살아있고 반응하며 세계관 속에 깊이 통합되어 있는 것처럼 느껴집니다.
그렇다면 내부적으로 어떤 일이 일어나고 있는 걸까요?
불꽃 VFX에 주목해야 하는 이유
불꽃, 그리고 일반적으로 유체와 같은 효과들은 게임에서 제대로 구현하기 까다롭기로 유명합니다. 전통적인 파티클 시스템 (Particle systems)은 훌륭해 보일 수 있지만, 세계와의 진정한 상호작용이 부족한 경우가 많습니다. 반대로 스펙트럼의 다른 한편에 있는 완전한 3D 시뮬레이션 (3D simulations)은 대개 실시간 게임플레이를 수행하기에는 비용이 너무 많이 듭니다.
몇몇 게임들은 유체 시뮬레이션 (Fluid simulation)을 핵심 메커니즘으로 탐구해 왔습니다. Tomorrow Corporation의 Little Inferno (위 사진)에서는 불의 움직임이 경험의 중심이며, Steve Mason의 Plasma Pong (아래 사진)은 반응하고 흐르는 움직임을 중심으로 전체 게임플레이를 구축합니다. 이러한 사례들은 유체 기반 시스템이 게임플레이에 직접적인 영향을 미칠 때 얼마나 강력할 수 있는지를 보여줍니다.
핵심 아이디어
Ignitement는 파티클 시스템 (Particle systems)에 의존하는 대신, 완전히 동적인 실시간 유체 시뮬레이션 (Real-time fluid simulation)을 사용합니다.
언뜻 보기에 이는 비용이 많이 드는 작업처럼 들릴 수 있습니다.
"하지만 그러면 PC가 과열되고 성능이 급락하지 않나요?"
적어도 하드웨어는 그렇지 않습니다.
시뮬레이션은 완전히 2D이며 Graphics.Blit을 통해 실행되며, 작은 크기의 텍스처 세트(주로 1024×1024 및 512×512)를 업데이트합니다. 실제로 이는 몇 가지 후처리 효과 (Post-processing effects)와 맞먹는 비용 수준으로 만듭니다.
또 다른 의도적인 선택은 컴퓨트 셰이더 (Compute shaders) 대신 프래그먼트 셰이더 (Fragment shaders)를 고수하는 것이었습니다.
이를 통해 시스템은 내장된 텍스처 필터링 (Texture filtering) 및 보간 (Interpolation) 기능을 활용할 수 있으며, 오래된 하드웨어나 잠재적인 콘솔 타겟에서도 높은 호환성을 유지할 수 있습니다. 이해를 돕기 위해 시스템을 세 부분으로 나눌 수 있습니다:
시뮬레이션 (Simulation)
렌더링 (Rendering)
라이팅 (Lighting)
유체 시뮬레이션 (Fluid simulation) 상세 분석
이 시스템의 핵심은 전적으로 Graphics.Blit 패스 (Passes)를 통해 구현된 표준 유체 시뮬레이션입니다. 시뮬레이션은 각각 다른 물리적 속성을 나타내는 여러 텍스처 상에서 작동합니다:
밀도 (Density) (1024×1024, RGBA half): 이것은 연기이며, 공기를 걸쭉하고 눈에 보이게 만드는 모든 요소입니다.
속도 (Velocity) (512×512, RG half): 이것은 사물이 어떻게 움직이는지를 제어합니다. 무언가가 흐르거나, 떠다니거나, 소용돌이친다면 바로 이 때문입니다.
온도 (Temperature) (1024×1024, single-channel half): 각 영역이 얼마나 뜨거운지를 결정합니다.
반응 (Reaction) (1024×1024, RGBA half): 실제 불이 존재하는 곳이며, 불의 강도, 확산 및 동작을 나타냅니다.
이 구조를 염두에 두면, 유체 솔버 (Fluid solver)는 다음과 같은 의사 코드 (Pseudo-code)로 개략적으로 설명할 수 있습니다:
반응 데이터는 때때로 GPU를 벗어나기도 합니다. 데이터는 다운샘플링 (Downsampled)되어 CPU로 다시 읽혀져 데미지와 같은 게임플레이 효과를 구동합니다. 그러니 네, 불은 단순히 위험해 보이기만 하는 것이 아니라 실제로도 위험합니다!
시뮬레이션 도메인 (Simulation domain) 자체는 월드에 고정되어 있지 않습니다. 대신 카메라를 따라다니며 플레이어가 이동함에 따라 이동합니다. 이는 실제로는 매 순간 비교적 작은 영역만 계산되고 있음에도 불구하고, 끝없이 연속되는 시뮬레이션이라는 환상을 만들어냅니다. 어디에나 연기가 있지만, 비용은 들지 않습니다.
렌더링 (Rendering)
모든 데이터가 준비되면, 다음 단계는 그것을 실제로 계속 바라보고 싶을 만한 무언가로 만드는 것입니다.
불 (Fire)
불은 하이트맵 (Heightmap)과 유사하게 취급되는 반응 텍스처 (Reaction texture)로부터 렌더링됩니다. 프래그먼트 셰이더 (Fragment shader)에서의 시차 (Parallax) 스타일 트릭은 실제 볼륨메트릭 (Volumetrics)의 비용 없이 깊이감을 더해주는 의사 3D (Pseudo-3D) 효과를 제공합니다.
연기 (Smoke)
연기와 안개는 주로 온도 정보로부터 생성됩니다.
이러한 정보들은 셰이더 (Shader)에서 해석되어, 기술적으로는 단순히 몇 개의 텍스처 (Texture)를 움직이는 것에 불과함에도 불구하고 놀라울 정도로 부피감이 느껴지는 부드럽고 진화하는 형태를 만들어냅니다.
불꽃 (Embers)
물론, 불꽃 (Embers) 없이는 불이 완성되지 않습니다. 이것들은 속도장 (Velocity field)을 샘플링하는 GPU 기반 파티클 (GPU-driven particles)로, 시뮬레이션의 흐름을 자연스럽게 따릅니다. 별도의 추가 로직은 필요하지 않으며, 말 그대로 흐름을 따라갈 뿐입니다. 이 불꽃 파티클들은 커스텀 GPU 구현을 통해 업데이트되고 이류 (Advected)됩니다 (Shuriken이나 VFX Graph를 사용하지 않음). 즉, 모든 파티클 데이터를 위한 ComputeBuffer 하나와, 이를 업데이트하기 위한 ComputeShader.Dispatch 호출, 그리고 화면에 렌더링하기 위한 Graphics.DrawProcedural 호출만 있으면 됩니다.
조명 (Lighting)
라이트 맵 (Light map) 계산하기
조명은 많은 작업을 대신 수행해 주는 간단한 트릭을 사용하여 처리됩니다. 반응 텍스처 (Reaction texture)를 다운샘플링 (Downsampled)하고 블러 (Blurred) 처리하여 동적인 라이트 맵으로 변환합니다. 이것이 물리적으로 정확하지는 않지만, 그럴 필요도 없습니다. 그저 보기 좋으면 됩니다!
환경에 조명 적용하기
객체를 렌더링할 때, 조명은 커스텀 셰이더 내의 단일 텍스처 룩업 (Texture lookup)으로 귀결됩니다. 표면에서 직접 샘플링하는 대신, 룩업 위치를 표면 법선 (Surface normal)을 따라 약간 이동시킵니다: worldPosition + worldNormal * c. 이 아주 작은 오프셋이 큰 차이를 만듭니다. 이는 빛이 환경으로부터 오는 듯한 인상을 주어, 표면에 설득력 있는 깊이감과 방향성을 부여합니다. 이 모든 것이 단 하나의 텍스처 샘플링으로 이루어집니다. 나쁘지 않습니다.
상세한 내용이 궁금하다면, 제가 사용하는 셰이더 함수는 다음과 같습니다:
저는 이 함수와 필요한 모든 유니폼 변수 (Uniform variables)를 .cginc 파일에 넣고, 라이트 맵을 읽고자 하는 모든 셰이더에서 편리하게 사용합니다.
조명을 넘어 라이트 맵 확장하기
이 설정의 가장 멋진 부수 효과 중 하나는 라이트 맵이 조명만을 위한 것이 아니라는 점입니다. Ignitement에서는 UI의 일부에서도 실제로 이를 사용합니다. 노멀 맵 (Normal maps)이 적용된 요소들은 라이트 맵을 샘플링하여 반사 (Reflections)를 흉내 냅니다.
예를 들어, 체력 용기 (health container)의 유리는 주변의 불꽃을 반영하여, 단순히 그 위에 놓여 있는 것이 아니라 세계와 연결되어 있다는 느낌을 줍니다. 이는 또한 더 이색적인 효과를 구현할 수 있는 가능성을 열어줍니다. 한 구역에서는 환경이 "살점 벽 (flesh walls)"으로 구성되어 있습니다 (이유는 굳이 따질 필요 없겠죠?). 이 벽들은 라이트 맵 (light map)을 사용하여 얼마나 강하게 흔들릴지를 제어합니다. 근처의 불꽃이 강렬할수록 벽은 더 많이 반응하며, 이는 환경 자체가 살아있으며 불이 붙은 상황을 별로 좋아하지 않는다는 인상을 줍니다. 더욱 고무적인 점은, 이 모든 과정이 버텍스 셰이더 (vertex shader)에서 수행되므로, 이토록 역동적으로 보이는 결과물에 비해 비용이 매우 저렴하다는 것입니다.
불꽃 VFX가 게임플레이에 어떤 영향을 미칠까요? 시각적인 요소도 좋지만, 훌륭한 VFX 시스템만으로는 좋은 게임을 만들 수 없습니다. Ignitement에서 불꽃은 게임플레이에 직접적인 영향을 미칩니다. 불꽃에 닿는 모든 적은 지속적인 피해를 입히는 화상 디버프 (burning debuff)를 받게 됩니다. 이를 구현하기 위해서는 시뮬레이션 데이터가 CPU에서 사용 가능해야 합니다. 매 프레임마다 반응 텍스처 (reaction texture)는 다운샘플링 (downsampled)되며, AsyncGPUReadback.RequestIntoNativeArray를 통해 다시 읽어 들여집니다. GPU에서 객체별로 비용이 많이 드는 쿼리 (queries)를 수행하는 대신, 시스템은 텍스처를 한 번만 읽고 CPU에서 모든 적에 대해 저렴한 룩업 (lookups)을 수행합니다. 단순한 임계값 (threshold)을 사용함으로써, 이는 특정 순간의 불꽃 모양과 완벽하게 일치하는 단일하고 매우 역동적인 콜라이더 (collider)처럼 효과적으로 작동합니다.
한계 및 트레이드오프 (Limitations and trade-offs)
물론 이 접근 방식이 완벽한 것은 아닙니다. 시뮬레이션이 2D이기 때문에, 수직적으로 발생하는 모든 현상은 물리적으로 정확한 솔루션이라기보다 근사치 (approximation)에 가깝습니다. 또한, 시뮬레이션 도메인 (simulation domain)을 이동할 때는 눈에 보이는 이음새 (seams)나 팝핑 (popping) 현상을 피하기 위해 약간의 주의가 필요합니다. 그럼에도 불구하고, 이러한 트레이드오프는 매우 의도적인 것입니다.
이러한 방식은 풍부하고 반응성이 뛰어난 결과물을 제공하면서도, 시스템을 빠르고 확장 가능하며 폭넓은 호환성을 갖도록 유지해 줍니다.
핵심 요약 (Key takeaways)
- 2D 유체 시뮬레이션 (2D fluid simulations)은 예상보다 훨씬 더 많은 것을 해낼 수 있습니다.
- 시뮬레이션 데이터의 재사용이 마법이 일어나는 지점입니다.
- "올바르게 보이는 것"이 종종 "물리적으로 정확한 것"보다 더 중요합니다.
- GPU-to-CPU 읽기 (GPU-to-CPU readback)는 데이터 양을 작게 유지한다면 충분히 실행 가능합니다.
- 잘 설계된 하나의 시스템이 비주얼, 게임플레이, 그리고 UI를 동시에 구동할 수 있습니다.
모든 것을 공유된 유체 시뮬레이션 (shared fluid simulation) 위에 구축함으로써, Ignitement는 불, 조명, UI, 그리고 환경의 일부까지 모두 동일한 언어로 소통하는 응집력 있는 비주얼 스타일을 완성하게 됩니다.
그 결과는 단순히 더 나은 비주얼에 그치지 않고, 더욱 연결되어 있고, 반응성이 높으며, 살아있는 듯한 느낌을 주는 세계를 만들어냅니다.
그리고 이 모든 것은 몇 개의 텍스처, 몇 개의 셰이더 (shaders)… 그리고 모든 것에 불을 붙이는 것에서 시작됩니다.
유체 시뮬레이션과 서바이버라이크 (survivor-likes) / 로그라이크 (roguelikes) 장르를 좋아하신다면, Steam에서 Ignitement를 찜 목록에 추가하고 Discord에 참여해 주세요. 저희의 Steam 큐레이터 페이지에서 Unity로 제작된 더 많은 게임을 탐색해 보시고, Unity 블로그와 리소스 허브 (Resource Hub)에서 Unity 개발자들의 더 많은 이야기를 확인해 보세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Unity Blog의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기