이 챕터는 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 snippetRWTexture2D<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 템플릿