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대신AddComputePassRasterGraphContext대신ComputeGraphContext
개념 코드:
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) 만들기
개념:
GraphicsBuffer를 생성한다(Structured)- RenderGraph 패스 데이터에
BufferHandle로 들고 간다 - 패스에서 compute로 write
주의
CPU로 읽어오려면AsyncGPUReadback같은 경로를 고려해야 하고, 동기 readback은 스톨을 유발합니다.
17.4.2 버퍼를 쓸 때의 설계 질문 5개
- 이 버퍼는 “프레임 단위 임시”인가, “카메라 히스토리”인가?
- 요소 수는 고정인가? (SSR 타일 리스트처럼 가변이면 카운터/프리픽스 합이 필요)
- Structured/Raw/Append/Consume 중 무엇이 필요한가?
- CPU가 읽어야 하는가? (읽어야 한다면 읽기 타이밍/주기/지연을 설계)
- XR(눈별)과 멀티카메라에서 버퍼를 분리해야 하는가?
17.5 패턴 1: SSAO(반해상도) Compute 설계 스텝
SSAO를 “RenderGraph Compute”로 구현할 때, 최소 설계는 보통 이렇습니다.
입력(필수)
- Depth(씬 뎁스)
- Normals(또는 노말 재구성)
- 랜덤/노이즈(블루노이즈/회전 벡터)
- 카메라 파라미터(프로젝션/근평면 등)
출력
- AO 텍스처(반해상도)
- 필요 시 업샘플 결과(전해상도) 또는 블러 결과
RenderGraph 패스 구성(추천)
- AO 생성(Compute, half-res UAV write)
- AO 블러(Compute 또는 Raster)
- 업샘플+합성(보통 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류는 보통 다음 중 일부를 조합합니다.
- Ray march(또는 Hierarchical Z): 후보 히트 포인트 탐색(Compute)
- Resolve: 히트 포인트에서 컬러 샘플링/페이드(Compute 또는 Raster)
- Temporal Accumulation: 히스토리 누적(Compute)
- Denoise/Blur: 노이즈 제거(Compute 또는 Raster)
이 중 (3) 때문에 History 텍스처/모션 벡터 설계가 필수가 됩니다.
17.7 실전 코드 스켈레톤: URP RendererFeature + Compute Pass
이 코드는 “구조”를 보여주는 스켈레톤입니다. 실제 API 시그니처는 URP/RenderGraph 버전에 따라 다를 수 있으니,
반드시 Unity 6.3 프로젝트에서 컴파일로 확인하고 조정하세요.
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) 예시
// 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)은 흔한 기본값이지만 정답이 아닙니다.- 메모리 접근(연속성), 캐시, 공유 메모리 사용 여부에 따라 최적점이 달라집니다.
실무 루틴:
- 먼저 8x8로 동작/정확도 확보
- 16x16 등으로 바꿔 성능 측정(플랫폼별)
- 대역폭 병목인지, 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 공식 문서(권장)
- Compute shader를 Render Pass에서 실행(URP, RenderGraph): https://docs.unity3d.com/Manual/urp/render-graph-compute-shader-run.html
17.9 다음 읽기
- Lit include 체인/함수 지도: 16. URP Lit 실제 맵
- “완전 호환” Pass 템플릿: 18. URP Lit 호환 Pass 템플릿