book/17-rendergraph-compute-uav-patterns.md

17. RenderGraph Compute/UAV 패턴 (SSAO/SSR류)

URP RenderGraph에서 AddComputePass/ComputeGraphContext로 컴퓨트 패스를 구성하고, UAV(RandomWrite) 텍스처/버퍼를 안전하게 읽고/쓰는 설계 패턴을 정리한다

17. RenderGraph Compute/UAV 패턴 (SSAO/SSR류)

이 챕터는 URP RenderGraph에서 컴퓨트 패스(Compute Pass) 를 “실전 패턴”으로 익히는 것을 목표로 합니다.

  • SSAO처럼 “화면 기반 데이터(Depth/Normals) → 출력 텍스처” 구조
  • SSR처럼 “시간(History) + 모션/깊이/노말/컬러”를 결합하는 구조

선행:

17.1 Raster vs Compute: 언제 컴퓨트가 유리한가

컴퓨트가 유리해지는 대표 케이스:

  • UAV(RandomWrite) 가 필요할 때(예: 히스토그램/리덕션/타일 분류)
  • 화면을 타일/클러스터로 쪼개는 전처리(Forward+ 라이트 목록, SSR 타일 등)
  • 다중 출력/비정형 메모리 접근(예: 버퍼에 스캐터/리스트 작성)

반대로 Raster(풀스크린 패스)가 유리한 케이스:

  • 단순한 per-pixel 필터(블러/색 보정)처럼 텍스처 샘플링 중심
  • 타일 기반 GPU에서 로드/스토어를 최소화할 수 있는 경우

실무 결론
“Compute를 쓴다”는 건 대개 리소스 설계(버퍼/UAV/배리어) 를 복잡하게 만드는 대가를 지불하는 선택입니다.
따라서 정말 compute가 필요한 이유를 먼저 명확히 하세요.

17.2 RenderGraph에서 Compute Pass의 기본 뼈대

URP 문서 흐름 기준으로, Compute Pass는 다음이 핵심입니다.

  • AddRasterRenderPass 대신 AddComputePass
  • RasterGraphContext 대신 ComputeGraphContext

개념 코드:

C#
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
    using (var builder = renderGraph.AddComputePass<PassData>("MyComputePass", out var passData))
    {
        builder.SetRenderFunc((PassData data, ComputeGraphContext ctx) =>
        {
            // ctx.cmd로 compute 커맨드 기록
        });
    }
}

중요한 차이
Raster에서는 SetRenderAttachment로 “렌더 타겟”을 설정하지만,
Compute에서는 보통 SetComputeTextureParam/SetComputeBufferParam으로 UAV/리소스 바인딩이 중심이 됩니다.

17.2.1 Compute 패스도 “계약(Contract)”이다

Raster와 마찬가지로 Compute도 RenderGraph의 규칙을 따라야 합니다.

  • 읽을 텍스처/버퍼는 Read로 선언
  • 쓸 텍스처/버퍼는 Write로 선언

이 선언을 기반으로 RenderGraph는:

  • 리소스 수명(언제 만들고/버릴지)
  • 배리어(동기화)
  • 실행 순서

를 구성합니다.

실무에서 compute가 자주 깨지는 이유는, compute는 “렌더 타겟 바인딩 실수” 대신 “UAV 선언/바인딩 실수”가 더 흔하기 때문입니다.

17.3 UAV(RandomWrite) 텍스처 설계: enableRandomWrite

Compute에서 텍스처를 “쓰기 대상(UAV)”로 쓰려면, 텍스처 생성 단계에서 RandomWrite를 허용해야 합니다.

RenderGraph 텍스처 desc(개념):

  • desc.enableRandomWrite = true

또한 UAV로 쓸 텍스처는 포맷 제약(플랫폼별 지원)이 있으므로, 가급적 URP/플랫폼에서 검증된 포맷을 선택하세요.

실무 패턴:

  • SSAO: R8, R16, RHalf류(정밀도/대역폭 균형)
  • SSR: RGBAHalf류(중간 결과/히스토리)

17.4 BufferHandle / GraphicsBuffer: 입력·출력 버퍼 패턴

URP 문서의 예시처럼, compute는 종종 “버퍼에 출력”하고 CPU로 읽어오거나, 다른 패스가 그 버퍼를 소비합니다.

17.4.1 출력 버퍼(Structured Buffer) 만들기

개념:

  1. GraphicsBuffer를 생성한다(Structured)
  2. RenderGraph 패스 데이터에 BufferHandle로 들고 간다
  3. 패스에서 compute로 write

주의
CPU로 읽어오려면 AsyncGPUReadback 같은 경로를 고려해야 하고, 동기 readback은 스톨을 유발합니다.

17.4.2 버퍼를 쓸 때의 설계 질문 5개

  1. 이 버퍼는 “프레임 단위 임시”인가, “카메라 히스토리”인가?
  2. 요소 수는 고정인가? (SSR 타일 리스트처럼 가변이면 카운터/프리픽스 합이 필요)
  3. Structured/Raw/Append/Consume 중 무엇이 필요한가?
  4. CPU가 읽어야 하는가? (읽어야 한다면 읽기 타이밍/주기/지연을 설계)
  5. XR(눈별)과 멀티카메라에서 버퍼를 분리해야 하는가?

17.5 패턴 1: SSAO(반해상도) Compute 설계 스텝

SSAO를 “RenderGraph Compute”로 구현할 때, 최소 설계는 보통 이렇습니다.

입력(필수)

  • Depth(씬 뎁스)
  • Normals(또는 노말 재구성)
  • 랜덤/노이즈(블루노이즈/회전 벡터)
  • 카메라 파라미터(프로젝션/근평면 등)

출력

  • AO 텍스처(반해상도)
  • 필요 시 업샘플 결과(전해상도) 또는 블러 결과

RenderGraph 패스 구성(추천)

  1. AO 생성(Compute, half-res UAV write)
  2. AO 블러(Compute 또는 Raster)
  3. 업샘플+합성(보통 Raster)

실무 팁
AO 생성까지 compute로 가고, 블러/합성은 풀스크린(raster)로 처리하는 하이브리드가 자주 쓰입니다.

17.6 패턴 2: SSR(시간/히스토리 포함) Compute 설계 스텝

SSR(스크린 스페이스 반사)은 “기하학적 제약 + 시간 누적” 때문에 리소스 요구가 급격히 커집니다.

입력(대표)

  • Color(현재 프레임)
  • Depth
  • Normals
  • Roughness/Metallic(재질 파라미터)
  • Motion Vectors
  • History Color(이전 프레임)

출력(대표)

  • Reflection Color(현재)
  • Temporal Accumulation 결과(History 갱신)

RenderGraph 설계 포인트

  • 히스토리 텍스처는 카메라별이며, 컷/해상도 변경에 대한 리셋이 필요합니다.
    관련: 04.8 History Render Textures

17.6.1 SSR의 “최소 패스 분해”(실무 관점)

SSR류는 보통 다음 중 일부를 조합합니다.

  1. Ray march(또는 Hierarchical Z): 후보 히트 포인트 탐색(Compute)
  2. Resolve: 히트 포인트에서 컬러 샘플링/페이드(Compute 또는 Raster)
  3. Temporal Accumulation: 히스토리 누적(Compute)
  4. Denoise/Blur: 노이즈 제거(Compute 또는 Raster)

이 중 (3) 때문에 History 텍스처/모션 벡터 설계가 필수가 됩니다.

17.7 실전 코드 스켈레톤: URP RendererFeature + Compute Pass

이 코드는 “구조”를 보여주는 스켈레톤입니다. 실제 API 시그니처는 URP/RenderGraph 버전에 따라 다를 수 있으니,
반드시 Unity 6.3 프로젝트에서 컴파일로 확인하고 조정하세요.

C#
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering.RenderGraphModule;

public sealed class ComputeAoFeature : ScriptableRendererFeature
{
    [System.Serializable]
    public sealed class Settings
    {
        public ComputeShader computeShader;
        public int kernelIndex = 0;
    }

    public Settings settings = new();

    sealed class ComputeAoPass : ScriptableRenderPass
    {
        readonly ComputeShader _cs;
        readonly int _kernel;

        class PassData
        {
            public ComputeShader cs;
            public int kernel;
            public TextureHandle depth;
            public TextureHandle normals;
            public TextureHandle aoUav;
            public Vector4 dispatch; // (gx, gy, gz, unused)
        }

        public ComputeAoPass(ComputeShader cs, int kernel)
        {
            _cs = cs;
            _kernel = kernel;
        }

        public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
        {
            var resources = frameData.Get<UniversalResourceData>();

            // 입력(프로젝트/설정에 따라 존재 여부가 달라질 수 있음)
            TextureHandle depth = resources.activeDepthTexture;
            TextureHandle normals = resources.cameraNormalsTexture;

            // 출력 UAV 텍스처(desc는 카메라 컬러 기반으로 잡는 것을 권장)
            var aoDesc = renderGraph.GetTextureDesc(resources.activeColorTexture);
            aoDesc.name = "AO_UAV";
            aoDesc.enableRandomWrite = true;
            aoDesc.width /= 2;
            aoDesc.height /= 2;
            var ao = renderGraph.CreateTexture(aoDesc);

            using (var builder = renderGraph.AddComputePass<PassData>("Compute AO", out var passData))
            {
                passData.cs = _cs;
                passData.kernel = _kernel;
                passData.depth = depth;
                passData.normals = normals;
                passData.aoUav = ao;
                passData.dispatch = new Vector4(
                    Mathf.CeilToInt(aoDesc.width / 8.0f),
                    Mathf.CeilToInt(aoDesc.height / 8.0f),
                    1, 0);

                builder.UseTexture(passData.depth, AccessFlags.Read);
                builder.UseTexture(passData.normals, AccessFlags.Read);
                builder.UseTexture(passData.aoUav, AccessFlags.Write);

                builder.SetRenderFunc((PassData data, ComputeGraphContext ctx) =>
                {
                    var cmd = ctx.cmd;
                    cmd.SetComputeTextureParam(data.cs, data.kernel, "_CameraDepthTexture", data.depth);
                    cmd.SetComputeTextureParam(data.cs, data.kernel, "_CameraNormalsTexture", data.normals);
                    cmd.SetComputeTextureParam(data.cs, data.kernel, "_AOTexture", data.aoUav);
                    cmd.DispatchCompute(data.cs, data.kernel, (int)data.dispatch.x, (int)data.dispatch.y, (int)data.dispatch.z);
                });
            }

            // 다음 패스가 접근할 수 있도록 전역 슬롯으로 노출(선택)
            // (프로젝트 사정에 따라 필요)
            // resources.xyz = ao; 또는 cmd.SetGlobalTexture(...)
        }
    }

    ComputeAoPass _pass;

    public override void Create()
    {
        if (settings.computeShader != null)
            _pass = new ComputeAoPass(settings.computeShader, settings.kernelIndex);
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        if (_pass == null)
            return;
        renderer.EnqueuePass(_pass);
    }
}

17.7.1 UAV 텍스처를 셰이더에서 쓰는 HLSL(Compute) 예시

HLSL
// Compute shader snippet
RWTexture2D<float> _AOTexture;
Texture2D<float> _CameraDepthTexture;

[numthreads(8,8,1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
    float d = _CameraDepthTexture[id.xy];
    _AOTexture[id.xy] = saturate(d); // 예시: depth를 그대로 써보기
}

17.8 성능 설계 포인트(Compute)

17.8.1 타일/스레드 그룹 크기

  • numthreads(8,8,1)은 흔한 기본값이지만 정답이 아닙니다.
  • 메모리 접근(연속성), 캐시, 공유 메모리 사용 여부에 따라 최적점이 달라집니다.

실무 루틴:

  1. 먼저 8x8로 동작/정확도 확보
  2. 16x16 등으로 바꿔 성능 측정(플랫폼별)
  3. 대역폭 병목인지, ALU 병목인지(Profiler/GPU capture) 확인

17.8.2 대역폭(텍스처 read/write) 줄이기

Compute는 잘못 설계하면 “읽고 쓰는 텍스처”가 많아져 대역폭 병목이 생깁니다.

  • half-res를 적극 활용(SSAO/블러/일부 SSR 중간 결과)
  • 포맷을 줄이기(가능한 경우 R8/R16 등)
  • 불필요한 중간 텍스처를 줄이기(RenderGraph가 재사용할 수 있게 desc를 통일)

관련: 04.9 Blit 최적화

17.8 디버깅 체크리스트(Compute)

  • 텍스처 desc에 enableRandomWrite=true가 되어 있는가?
  • builder에서 write 선언을 했는가? (UseTexture(..., Write))
  • Dispatch 그룹 크기(numthreads)와 dispatch 계산이 일치하는가?
  • 입력 텍스처가 실제로 존재하는가? (Requirements/RenderGraph Viewer로 확인)
  • 플랫폼이 compute를 지원하는가? (SystemInfo.supportsComputeShaders)

17.9 공식 문서(권장)

17.9 다음 읽기