불을 살아있는 것처럼 느끼게 만들기: Ignitement의 실시간 유체 시뮬레이션

오늘의 게스트 포스트에서, 솔로 개발자 Sørb는 그의 다가오는 액션 로그라이트 Ignitement의 인상적인 불과 용암 VFX 뒤에 숨겨진 기술적 예술성을 분석합니다.
Ignitement을 볼 때 가장 먼저 눈에 띄는 것은 VFX입니다. 특히 불에 대해 즉각적으로 다른 점이 있습니다. 그것은 단순히 애니메이션처럼 보이지 않을 뿐만 아니라, 살아있고, 반응하며, 세계에 깊이 통합된 느낌을 줍니다.
그렇다면 내부에서 무슨 일이 일어나고 있을까요?
불 VFX에 대해 신경 써야 하는 이유
불과 유체 같은 효과는 일반적으로 게임에서 제대로 구현하기 매우 어렵습니다. 전통적인 파티클 시스템은 멋지게 보일 수 있지만, 종종 세계와의 진정한 상호작용이 부족합니다. 스펙트럼의 반대편에서는, 완전한 3D 시뮬레이션이 실시간 게임플레이에 비해 너무 비쌉니다.

몇몇 게임은 유체 시뮬레이션을 핵심 메커니즘으로 탐구했습니다. Tomorrow Corporation의 Little Inferno에서, 불의 동작은 경험의 중심이며, Steve Mason의 Plasma Pong은 반응적이고 흐르는 움직임을 중심으로 전체 게임플레이를 구성합니다. 이러한 예시는 유체 기반 시스템이 게임플레이에 직접적으로 영향을 미칠 때 얼마나 강력할 수 있는지를 강조합니다.

핵심 아이디어
입자 시스템에 의존하는 대신, Ignitement은 완전 동적이며 실시간 유체 시뮬레이션을 사용합니다.
첫눈에 보기에는, 이것이 비쌀 것처럼 들릴 수 있습니다.
“하지만 그게 당신의 PC를 불태우고 성능을 떨어뜨리게 하지 않나요?”
적어도 하드웨어는 아닙니다.
시뮬레이션은 전적으로 2D이며 Graphics.Blit을 통해 실행되며, 소규모 텍스처 세트를 업데이트합니다(주로 1024×1024 및 512×512). 실제로, 이는 몇 가지 후처리 효과와 비용이 비슷하게 만듭니다.
또 다른 의도적인 선택은 컴퓨트 셰이더 대신 프래그먼트 셰이더를 고수하는 것이었습니다. 이것은 시스템이 내장된 텍스처 필터링 및 보간을 활용할 수 있게 하며, 구형 하드웨어나 잠재적인 콘솔 대상에서도 호환성을 높게 유지합니다.
이해하기 쉽게 만들기 위해, 시스템을 세 부분으로 나눌 수 있습니다:
- 시뮬레이션
- 렌더링
- 조명
유체 시뮬레이션 요약
시스템의 핵심은 전적으로 Graphics.Blit 패스를 통해 구현된 표준 유체 시뮬레이션입니다. 시뮬레이션은 각각 다른 물리적 속성을 나타내는 여러 텍스처에서 작동합니다:
- 밀도 (1024×1024, RGBA 반) 이것은 당신의 연기이며, 공기를 두껍고 가시적으로 만드는 모든 것입니다.
- 속도 (512×512, RG 반) 이것은 물체가 어떻게 움직이는지를 제어합니다. 무언가가 흐르거나 떠다니거나 소용돌이친다면, 이것이 그 이유입니다.
- 온도 (1024×1024, 단일 채널 반) 서로 다른 지역이 얼마나 뜨거운지를 결정합니다.
- 반응 (1024×1024, RGBA 반) 이것은 실제 불이 존재하는 곳이며, 그 강도, 확산 및 동작입니다.
이 구조를 염두에 두고, 유체 솔버는 이 의사 코드로 개요를 작성할 수 있습니다:
void Simulate(float dt)
{
//feed data to simulation
RenderEmittersAndObstaclesToTexture();
ApplyDiffusion(dt); //fluid spreading
ApplyAdvection(dt); //moving data along the velocity field
ApplyExtinguishmentImpulse(dt); //fire producing smoke (reaction>density)
ComputeVorticityConfinement(dt); //increase swirlyness of fluid
//calculate divergence free velocity field
ComputeDivergence();
ComputePressure();
ComputeProjection();
AdvectParticles(dt); //move particles along velocity field
}
반응 데이터는 때때로 GPU를 벗어납니다. CPU에서 다운샘플링되고 다시 읽혀져 대미지와 같은 게임플레이 효과를 유도합니다. 그래서 불은 단순히 위험해 보이는 것이 아니라 실제로 위험합니다!
시뮬레이션 도메인 자체는 세계에 고정되어 있지 않습니다. 대신, 카메라를 따라 이동하며 플레이어가 움직일 때 함께 이동합니다. 이것은 실제로는 상대적으로 작은 영역만 계산되고 있지만 끝없는 연속 시뮬레이션의 환상을 만듭니다. 어디에나 연기가 있고, 비용은 없습니다.
렌더링
모든 데이터가 준비되면, 다음 단계는 실제로 바라보고 싶어지는 것처럼 보이게 만드는 것입니다.
Fire
불은 반응 텍스처에서 렌더링되며, 이는 하이트맵처럼 다루어집니다. 프래그먼트 셰이더의 패럴랙스 스타일 트릭이 pseudo-3D 모양을 부여하여 진정한 볼류메트릭 효과의 비용 없이 깊이를 추가합니다.
연기
연기와 안개는 주로 온도 정보에서 발생합니다. 이들은 셰이더에서 해석되어 부드럽고 진화하는 형태를 생성하며, 기술적으로는 몇 개의 텍스처가 이동하는 것일 뿐인데도 놀랍도록 볼류메트릭하게 느껴집니다.
불씨
물론, 불은 불씨 없이는 완전하지 않습니다.
이들은 GPU 기반의 입자로 속도 필드를 샘플링하며, 시뮬레이션의 흐름을 자연스럽게 따릅니다. 추가적인 논리가 필요하지 않으며, 그저 흐름을 따라갑니다(문자 그대로).
이 불꽃 입자들은 커스텀 GPU 구현을 사용하여 업데이트되고 전파됩니다 (슈리켄 없음, VFX 그래프 없음). 모든 입자 데이터에 대한 ComputeBuffer와 업데이트를 위한 ComputeShader.Dispatch 호출, 그리고 화면에 렌더링하기 위한 Graphics.DrawProcedural 호출이 필요합니다.
조명
조명 맵 계산 중
조명은 많은 무거운 작업을 처리하는 간단한 트릭을 사용하여 처리됩니다.
반응 텍스처는 다운샘플링되고 흐려져 동적 조명 맵으로 변환됩니다. 물리적으로 정확하지는 않지만, 그럴 필요는 없습니다. 그냥 올바르게 보이면 됩니다!
환경에 조명 적용 중
객체를 렌더링할 때, 조명은 커스텀 셰이더에서 단일 텍스처 룩업으로 귀결됩니다.
표면에서 직접 샘플링하는 대신, 룩업은 표면 노멀을 따라 약간 이동합니다:
worldPosition + worldNormal * c

이 작은 오프셋이 큰 효과를 줍니다. 이것은 빛이 환경에서 오는 인상을 만들어내어 표면에 깊이와 방향성을 부여합니다.
모든 것이 하나의 텍스처 샘플에서 나옵니다. 나쁘지 않네요!
자세한 내용을 알고 싶다면, 제가 사용하는 셰이더-함수는 다음과 같습니다:
void Simulate(float dt)
{
//feed data to simulation
RenderEmittersAndObstaclesToTexture();
ApplyDiffusion(dt); //fluid spreading
ApplyAdvection(dt); //moving data along the velocity field
ApplyExtinguishmentImpulse(dt); //fire producing smoke (reaction>density)
ComputeVorticityConfinement(dt); //increase swirlyness of fluid
//calculate divergence free velocity field
ComputeDivergence();
ComputePressure();
ComputeProjection();
AdvectParticles(dt); //move particles along velocity field
}
저는 이 함수를 .cginc 파일에 넣고 필요한 모든 유니폼 변수를 설정하여, 광원 맵을 읽고자 하는 모든 셰이더에서 편리하게 사용합니다.
조명을 넘어 광원 맵 확장하기
이 설정의 가장 멋진 부작용 중 하나는 광원 맵이 단순히 조명용이 아니라는 것입니다.
Ignitement에서는 UI의 일부가 실제로 이를 사용합니다. 노멀 맵이 있는 요소들은 광원 맵을 샘플링하여 반사를 가짜로 만듭니다. 예를 들어, 건강 용기의 유리는 주변의 불꽃을 반사하여, 단순히 위에 놓여 있는 것이 아니라 세계와 연결되어 있는 느낌을 줍니다.
이것은 또한 더 이례적인 효과를 위한 문을 엽니다.
어떤 영역에서는 환경이 "살아있는 벽"으로 구성되어 있습니다 (왜 안 되겠습니까?). 이들은 광원 맵을 사용하여 얼마나 강하게 흔들리는지를 제어합니다. 주변의 불꽃이 강할수록 벽이 더 반응하여, 환경 자체가 살아있고 불에 타는 것에 대해 그리 행복하지 않은 인상을 줍니다.
더욱이, 이 모든 것은 버텍스 셰이더에서 처리되어, 이렇게 역동적으로 보이는 것에 비해 매우 저렴합니다.
불꽃 VFX가 게임플레이에 어떻게 영향을 미칩니까?
비주얼은 좋지만, 좋은 VFX 시스템만으로는 좋은 게임이 되지 않습니다. Ignitement에서는 불꽃이 게임플레이에 직접적인 영향을 미칩니다: 불꽃에 닿는 적은 시간이 지남에 따라 대미지를 주는 화상 디버프를 받습니다.
이것이 작동하려면, 시뮬레이션 데이터가 CPU에서 사용 가능해야 합니다. 각 프레임마다 반응 텍스처가 다운샘플링되고 AsyncGPUReadback.RequestIntoNativeArray를 사용하여 리드백됩니다. GPU에서 비싼 오브젝트별 쿼리를 수행하는 대신, 시스템은 텍스처를 한 번 읽고 CPU에서 모든 적에 대해 저렴한 조회를 수행합니다. 간단한 임계값을 사용하여, 이는 효과적으로 순간마다 불의 형태와 완벽하게 일치하는 단일의 매우 동적인 콜라이더처럼 작동합니다.
제한 사항 및 트레이드오프
물론, 이 접근 방식은 완벽하지 않습니다.
시뮬레이션이 2D이기 때문에, 수직으로 발생하는 모든 것은 물리적으로 정확한 해결책보다 근사치에 가깝습니다. 또한, 시뮬레이션 도메인을 이동하는 것은 눈에 띄는 이음새나 튀는 것을 피하기 위해 약간의 주의가 필요합니다.
그렇긴 하지만, 이러한 트레이드오프는 매우 의도적입니다. 이들은 시스템을 빠르고, 확장 가능하며, 널리 호환되도록 유지하면서도 풍부하고 반응적인 결과를 제공합니다.
주요 내용 요약
- 2D 유체 시뮬레이션은 예상보다 훨씬 더 멀리 나아갈 수 있습니다.
- 시뮬레이션 데이터를 재사용하는 것이 많은 마법이 일어나는 곳입니다.
- “보기에 맞는 것”이 종종 “물리적으로 정확한 것”보다 우선합니다.
- GPU에서 CPU로의 리드백은 작게 유지할 경우 완벽하게 실행 가능합니다.
- 잘 설계된 하나의 시스템이 시각, 게임플레이 및 UI를 동시에 구동할 수 있습니다.
공유된 유체 시뮬레이션 위에 모든 것을 구축함으로써, Ignitement은 불, 조명, UI 및 환경의 일부가 모두 같은 언어를 사용하는 응집력 있는 시각 스타일을 갖게 됩니다.
결과는 단순히 더 나은 시각이 아니라, 더 연결되고, 더 반응적이며, 더 생동감 있는 세계입니다.
그리고 이 모든 것은 몇 개의 텍스처, 몇 개의 셰이더로 시작됩니다… 그리고 모든 것을 불태우는 것입니다.
유체 시뮬레이션과 생존형 게임/로그라이크를 좋아하신다면 위시리스트에 추가하세요 Ignitement를 Steam에서하고 Discord에 참여하세요. Steam 큐레이터 페이지에서 Unity로 제작된 게임을 더 많이 탐색하고, Unity 블로그와 리소스 허브에서 Unity 개발자들의 더 많은 이야기를 확인하세요..
