동적 혈흔과 사체 흔적 연출: 메모리 효율적인 데칼(Decals)과 랜더 타깃 합성

동적 혈흔과 사체 흔적 연출: 메모리 효율적인 데칼(Decals)과 랜더 타깃 합성

대량의 피자국과 총탄 흔적을 메쉬 생성 없이 렌더 타깃 텍스처 베이킹 및 스크린 공간 데칼로 처리하여 프레임 drop과 메모리 폭증을 방지하는 실무 최적화 기법을 소개합니다.

동적 연출과 프레임 저하의 트레이드오프

하드코어 액션 게임이나 FPS에서는 캐릭터의 타격, 총격, 사체 처리 시 발생하는 피자국(Bloodstains)과 탄흔, 발자국 등 동적 흔적 연출이 시각적 몰입감에 중요한 역할을 합니다. 하지만 연출에 집중해 흔적 오브젝트를 단순 생성(Instantiate/Spawn)하는 방식으로 구현하면 시간이 지날수록 심각한 성능 저하가 발생합니다.

오브젝트 수가 증가하면 다음과 같은 성능 병목이 나타납니다.

  • 드로우 콜(Draw Call / Pass) 폭증: 생성된 데칼 개수만큼 메쉬 컴포넌트가 늘어나 렌더링 호출 수가 비례하여 증가합니다.
  • 메모리 파편화 및 Garbage Collection 오버헤드: 계속해서 새로운 데칼 인스턴스를 동적 할당하고 파괴하는 과정에서 힙 메모리 파편화가 일어나며 GC 일시 정지 현상을 유발합니다.
  • 오버드로우(Overdraw): 동일한 지점에 피자국이 누적되면 동일 픽셀을 여러 번 그리는 오버드로우가 발생하여 GPU 픽셀 셰이더 부하를 급증시킵니다.

이러한 문제를 해결하려면 데칼 렌더링을 지능적으로 처리해야 합니다. 실무에서 널리 쓰이는 스크린 공간 데칼(Screen-Space Deferred Decal) 방식과 랜더 타깃 텍스처 베이킹(Render Target Texture Baking) 방식을 조합하는 메커니즘을 상세히 다룹니다.

데칼 처리 방식 비교

흔적을 표현하는 대표적 렌더링 기법은 크게 세 가지로 구분됩니다.

  1. 메쉬 기반 데칼(Mesh Decal): 투영 타깃 지형의 메쉬 피스(Piece)를 잘라내어 신규 메쉬를 생성하는 방식입니다. 정교하지만 CPU 부담이 큽니다.
  2. 스크린 공간 데칼(Screen-Space Decal): 투영 박스(Decal Box Volume)를 두고 뎁스 버퍼(Depth Buffer) 정보를 읽어와 픽셀 셰이더에서 좌표를 역산하는 방식입니다. GPU 중심 연산으로 유연성이 높습니다.
  3. 랜더 타깃 베이킹(Render Target Baking): 고정된 텍스처 영역이나 지형 UV 위에 동적으로 데칼 텍스처를 덧그리는(Splatting) 방식입니다. 드로우 콜을 일정하게 유지할 수 있습니다.
flowchart TD
    A[동적 흔적 발생] --> B{처리 방식 선택}
    B -->|단기/소량| C[Screen-Space Decal]
    B -->|장기/대량| D[Render Target Baking]
    C --> E[Decal Volume GPU 렌더링]
    D --> F[지형 UV에 텍스처 합성]
    E --> G[결과 출력]
    F --> G

프로젝트 성격에 맞춰 단기적인 총탄 자국은 스크린 공간 데칼로 표현하고 사체 주변에 대량으로 쌓이는 피자국은 랜더 타깃 베이킹 기법을 적용하는 것이 이상적입니다.

Screen-Space Decal 투영 원리와 수학적 계산

스크린 공간 데칼은 3D 공간에 위치한 데칼 큐브(Decal Box) 내부에 포함된 지형 픽셀을 탐색하여 데칼 텍스처를 입히는 방식입니다.

데칼 박스 투영과 뎁스 버퍼 재구성을 통한 월드 좌표 복원 과정

픽셀 셰이더 연산 흐름은 다음과 같습니다.

  1. 현재 스크린 픽셀의 Depth 버퍼 값을 가져옵니다.
  2. 화면 UV 좌표와 Depth 값을 이용하여 월드 공간 좌표 PworldP_{world}를 복원합니다.
  3. 복원된 월드 좌표를 데칼 변환 행렬의 역행렬 Mdecal1M_{decal}^{-1}을 곱해 데칼의 로컬 공간 좌표 PdecalP_{decal}로 변환합니다.

수학적으로 다음과 같이 표현됩니다.

Pdecal=Mdecal1MworldPdepthP_{decal} = M_{decal}^{-1} \cdot M_{world} \cdot P_{depth}

변환된 로컬 좌표가 큐브 경계 내부인지 검증합니다.

0.5Pdecal.x,Pdecal.y,Pdecal.z0.5-0.5 \le P_{decal.x}, P_{decal.y}, P_{decal.z} \le 0.5

범위를 만족하는 경우 로컬 좌표 (Pdecal.x+0.5,Pdecal.y+0.5)(P_{decal.x} + 0.5, P_{decal.y} + 0.5)를 데칼 텍스처 UV 좌표로 사용해 샘플링합니다.

다음은 셰이더 코드(HLSL 기반) 예시입니다.

float4 FragShader(Varyings input) : SV_Target
{
    // 1. Screen UV 계산
    float2 screenUV = input.positionCS.xy / _ScreenParams.xy;
    
    // 2. Depth Buffer 샘플링
    float rawDepth = SampleSceneDepth(screenUV);
    
    // 3. World Position 복원
    float3 worldPos = ReconstructWorldPosition(screenUV, rawDepth);
    
    // 4. Decal Local Space로 변환 (역행렬 곱연산)
    float4 localPos = mul(_WorldToDecalMatrix, float4(worldPos, 1.0));
    
    // 5. Decal Box 내부에 위치하는지 Clip 검사 (-0.5 ~ 0.5)
    clip(0.5 - abs(localPos.xyz));
    
    // 6. Local 좌표를 UV로 전환하여 텍스처 샘플링
    float2 decalUV = localPos.xz + 0.5;
    float4 decalColor = tex2D(_DecalTex, decalUV);
    
    return decalColor;
}

이 연산은 지형의 정점 수에 영향받지 않으며 오직 데칼 박스가 차지하는 스크린 영역 픽셀 수에 의해서만 비용이 결정되는 장점이 있습니다.

영구 흔적을 위한 Render Target Texture Baking

스크린 공간 데칼도 수십 개 이상 지형에 중첩되면 픽셀 셰이더 오버드로우로 인해 프레임 drop이 발생할 수 있습니다. 수백 개의 혈흔과 사체 흔적을 영구적으로 유지하기 위한 가장 효율적인 솔루션은 랜더 타깃 합성(Dynamic Texture Splatting)입니다.

동작 알고리즘

  1. 지형 메쉬(또는 특정 스플래시 맵)에 대응하는 RenderTarget2D 텍스처를 하나 할당합니다.
  2. 혈흔이 생길 때 데칼 볼륨을 매 프레임 그리지 않고 직교(Orthographic) 투영을 사용해 지형 RenderTarget 텍스처 상 해당 위치 좌표에 데칼 텍스처를 1회 블렌딩하여 합성합니다.
  3. 렌더 타깃에 구워진 결과 텍스처를 메인 지형 마이크로 셰이더에서 기본 디퓨즈/노멀 마스크로 참조하여 렌더링합니다.

동적 텍스처 베이킹 방식을 적용하여 드로우 콜을 낮추고 대량의 혈흔을 표현한 결과

아래 C# 스크립트는 영구 지형 랜더 타깃에 데칼을 덧그리는 핵심 로직 개념 예시입니다.

public class BloodTextureBaker : MonoBehaviour
{
    [SerializeField] private RenderTexture bloodRenderTarget;
    [SerializeField] private Material stampMaterial;

    public void StampBlood(Vector3 hitPoint, Texture2D bloodTexture, float radius)
    {
        // 1. 월드 좌표를 지형 UV 좌표로 변환
        Vector2 uvPos = WorldToTerrainUV(hitPoint);

        // 2. 스탬프 머티리얼에 위치와 스케일 전달
        stampMaterial.SetTexture("_BloodTex", bloodTexture);
        stampMaterial.SetVector("_OffsetAndScale", new Vector4(uvPos.x, uvPos.y, radius, radius));

        // 3. RenderTarget에 추가 렌더링 (Graphics.Blit 또는 CommandBuffer)
        RenderTexture tempRT = RenderTexture.GetTemporary(bloodRenderTarget.width, bloodRenderTarget.height);
        Graphics.Blit(bloodRenderTarget, tempRT);
        Graphics.Blit(tempRT, bloodRenderTarget, stampMaterial);
        RenderTexture.ReleaseTemporary(tempRT);
    }
}

이 방식을 적용하면 필드 상에 1,000개 이상의 혈흔이나 사체 자국이 누적되더라도 드로우 콜은 지형 개수만큼(1~2개) 그대로 유지됩니다.

추가 최적화 전략: 오브젝트 풀링과 채널 패킹

메모리와 VRAM을 더욱 아끼기 위해 실무에서 활용하는 팁입니다.

1. 오브젝트 풀링과 Ring Buffer

단기 스크린 공간 데칼(탄흔 등)은 Queue 또는 Ring Buffer 형태의 Object Pool을 구성하여 최대 개수(예: 50개)를 넘어가면 가장 오래된 데칼의 Alpha 값을 낮춰 자연스럽게 Fade Out 시킨 뒤 재활용합니다.

2. RGBA 채널 패킹(Channel Packing)

데칼 RGBA 채널 하나에 피자국 형태(R), 법선 높이 정보(G), Wetness/Roughness 글로스 마스크(B), 디테일 알파(A)를 각 채널별로 다르게 분할 수용하여 텍스처 메모리 사용량을 70% 이상 단축할 수 있습니다.

3. Culling 및 Distance LOD

카메라와의 거리에 따라 멀어진 데칼의 렌더링을 완전히 생략하는 Distance Culling을 적용하면 무의미한 GPU 셰이더 연산을 배제할 수 있습니다.

요약

동적 혈흔 연출 최적화의 핵심은 연출의 지속 시간에 맞춰 렌더링 기법을 이원화하는 것입니다.

  • 일시적 흔적: Screen-Space Deferred Decal + Object Pooling
  • 지속적/대량 흔적: Dynamic Render Target Baking

이 구조를 구축하면 화려한 액션 연출 속에서도 안정적인 60 FPS 프레임을 유지할 수 있습니다.

#Unity#Unreal Engine#Shader#Decal#Optimization#Graphics

계속 읽어보기

이런 글은 어떠세요?

< Back to Logs