์นดํ…Œ๊ณ ๋ฆฌ ์—†์Œ

Unity URP ์ปค์Šคํ…€ ๋ Œ๋” ํ”ผ์ฒ˜ VRAM ๋ˆ„์ˆ˜ ๋ฐ ํ”„๋ ˆ์ž„ ๋“œ๋กญ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…: RenderTargetIdentifier ๋ผ์ดํ”„์‚ฌ์ดํด ๊ด€๋ฆฌ

๊ฒŒ์ž„๊ต์ˆ˜ 2026. 9. 5. 04:01
๋ฐ˜์‘ํ˜•

๐ŸŒ English Abstract

This article provides an in-depth technical analysis and troubleshooting guide for GPU Video RAM (VRAM) leaks and frame rate drops caused by mismanaged RenderTargetIdentifier and RTHandle lifecycles in Unity Universal Render Pipeline (URP) Custom Render Features. We explore the inner execution flow of ScriptableRenderPass, pinpoint common memory allocation mistakes made inside rendering loops, and demonstrate best-practice code patterns for optimal GPU memory lifecycle management.

์œ ๋‹ˆํ‹ฐ(Unity)์˜ URP(Universal Render Pipeline)๋Š” ๊ฐ€๋ณ๊ณ  ์œ ์—ฐํ•œ ๋ Œ๋”๋ง ํŒŒ์ดํ”„๋ผ์ธ์„ ์ œ๊ณตํ•˜์—ฌ ๋‹ค์–‘ํ•œ ํ”Œ๋žซํผ์—์„œ ๊ณ ์„ฑ๋Šฅ ๊ทธ๋ž˜ํ”ฝ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋•์Šต๋‹ˆ๋‹ค. ํŠนํžˆ ์ปค์Šคํ…€ ๋ Œ๋” ํ”ผ์ฒ˜(Custom Render Feature)์™€ ์Šคํฌ๋ฆฝํ„ฐ๋ธ” ๋ Œ๋” ํŒจ์Šค(ScriptableRenderPass)๋ฅผ ํ™œ์šฉํ•˜๋ฉด ํŒŒ์ดํ”„๋ผ์ธ ๋‚ด๋ถ€ ๋ Œ๋”๋ง ์ˆœ์„œ์— ์ง์ ‘ ๊ฐœ์ž…ํ•˜์—ฌ ์ปค์Šคํ…€ ํฌ์ŠคํŠธ ํ”„๋กœ์„ธ์‹ฑ(Post-Processing), ์™ธ๊ณฝ์„  ํšจ๊ณผ(Outline Effects), ๊นŠ์ด ๊ธฐ๋ฐ˜ ์Šคํฌ๋ฆฐ ๊ณต๊ฐ„ ํšจ๊ณผ ๋“ฑ์„ ์†์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด URP ์ปค์Šคํ…€ ๋ Œ๋” ํŒจ์Šค๋ฅผ ๊ฐœ๋ฐœํ•˜๋Š” ๊ณผ์ •์—์„œ ๋ Œ๋” ํƒ€๊ฒŸ ์‹๋ณ„์ž(RenderTargetIdentifier) ๋ฐ RTHandle์˜ ๋ผ์ดํ”„์‚ฌ์ดํด(Lifecycle)์„ ๋ช…ํ™•ํ•˜๊ฒŒ ์ดํ•ดํ•˜์ง€ ๋ชปํ•ด ์‹ฌ๊ฐํ•œ GPU ๋น„๋””์˜ค ๋žจ ๋ˆ„์ˆ˜(VRAM Leak)์™€ ์ง€์†์ ์ธ ํ”„๋ ˆ์ž„ ๋“œ๋กญ(Frame Drop) ํ˜„์ƒ์„ ๊ฒช๊ณค ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ธ€์—์„œ๋Š” ์ด๋Ÿฌํ•œ ์„ฑ๋Šฅ ์ €ํ•˜์˜ ๊ทผ๋ณธ์ ์ธ ์›์ธ์„ ๊ธฐ์ˆ ์ ์œผ๋กœ ๋ถ„์„ํ•˜๊ณ , ์œ ๋‹ˆํ‹ฐ ํ”„๋กœํŒŒ์ผ๋Ÿฌ(Unity Profiler)๋ฅผ ํ™œ์šฉํ•œ ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ๊ณผ์ •๊ณผ ์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ ์ตœ์ ์˜ ์ฝ”๋“œ ์ž‘์„ฑ ํŒจ๋Ÿฌ๋‹ค์ž„์„ ์ƒ์„ธํžˆ ๋‹ค๋ฃน๋‹ˆ๋‹ค.



1. ๋ฌธ์ œ์˜ ๊ทผ๋ณธ ์›์ธ: RenderTargetIdentifier์™€ GPU ๋ฉ”๋ชจ๋ฆฌ ๋ผ์ดํ”„์‚ฌ์ดํด์˜ ์˜คํ•ด

URP ์ปค์Šคํ…€ ๋ Œ๋” ํŒจ์Šค ์ž‘์„ฑ ์‹œ ๊ฐ€์žฅ ํ”ํ•˜๊ฒŒ ๋ฐœ์ƒํ•˜๋Š” ์‹ค์ˆ˜๋Š” ๋งค ํ”„๋ ˆ์ž„๋งˆ๋‹ค ์ž„์‹œ ๋ Œ๋” ํƒ€๊ฒŸ(Temporary Render Target)์„ ํ• ๋‹นํ•˜๋ฉด์„œ, ํ•ด๋‹น ํƒ€๊ฒŸ์˜ ํ•ด์ œ(Release) ์‹œ์ ์„ ๋ˆ„๋ฝํ•˜๊ฑฐ๋‚˜ ๋งค์นญ์‹œํ‚ค์ง€ ๋ชปํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

RenderTargetIdentifier๋Š” ์ฐธ์กฐ์šฉ ๊ตฌ์กฐ์ฒด์ผ ๋ฟ์ด๋‹ค

๋งŽ์€ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ฐฉ๊ฐํ•˜๋Š” ๊ฐœ๋… ์ค‘ ํ•˜๋‚˜๋Š” RenderTargetIdentifier ์ž์ฒด๊ฐ€ ์‹ค์ œ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์ ์œ ํ•˜๋Š” ๊ฐ์ฒด๋ผ๊ณ  ์ƒ๊ฐํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ RenderTargetIdentifier๋Š” C#์˜ ๊ตฌ์กฐ์ฒด(struct)๋กœ, GPU์˜ ์‹ค์ œ ๋ Œ๋” ํƒ€๊ฒŸ ํ…์Šค์ฒ˜(RenderTexture) ๋ฒ„ํผ๋‚˜ ๋ Œ๋” ๋ฒ„ํผ ์‹๋ณ„์šฉ ID(์ธ์ˆ˜ ๊ฐ’ ๋˜๋Š” ๋„ค์ž„๋“œ ID)๋ฅผ ๊ฐ€๋ฆฌํ‚ค๋Š” 'ํ•ธ๋“ค(Handle)' ์—ญํ• ๋งŒ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค.

์ง„์งœ ๋ฌธ์ œ๋Š” ์ปค๋งจ๋“œ ๋ฒ„ํผ(CommandBuffer)๋ฅผ ํ†ตํ•ด GetTemporaryRT ๋ช…๋ น์„ ์‹คํ–‰ํ•˜๊ฑฐ๋‚˜, RTHandles.Alloc์„ ํ˜ธ์ถœํ•˜์—ฌ GPU VRAM ์ƒ์— ์‹ค์ œ ๋ฌผ๋ฆฌ์  ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ํ• ๋‹นํ•  ๋•Œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ์ด ํ• ๋‹น ๊ณผ์ •์ด ๋ Œ๋”๋ง ๋ฃจํ”„ ๋‚ด๋ถ€์—์„œ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํ•ด์ œ๋˜์ง€ ์•Š์œผ๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์น˜๋ช…์ ์ธ ๊ทธ๋ž˜ํ”ฝ์Šค ์ด์Šˆ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

  • ์ง€์†์ ์ธ VRAM ๋ฉ”๊ฐ€๋ฐ”์ดํŠธ(MB) ๋ˆ„์ˆ˜: ๋งค ํ”„๋ ˆ์ž„ ์ƒˆ๋กœ์šด GPU ๋ฉ”๋ชจ๋ฆฌ ๋ธ”๋ก์ด ํ• ๋‹น๋˜์–ด ๋น„๋””์˜ค ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์ด ๊ณ„์† ์ƒ์Šนํ•ฉ๋‹ˆ๋‹ค.
  • GPU ๋“œ๋ผ์ด๋ฒ„ ๋ฐ ์˜ค๋ฒ„ํ—ค๋“œ ํญ์ฆ: GPU ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด OS ๋ฐ ๊ทธ๋ž˜ํ”ฝ์Šค ๋“œ๋ผ์ด๋ฒ„ ๋ ˆ๋ฒจ์—์„œ ๋นˆ๋ฒˆํ•œ ๋ฉ”๋ชจ๋ฆฌ ํ• ๋‹น/ํ•ด์ œ ์š”์ฒญ์ด ๋ฐœ์ƒํ•˜๋ฉด์„œ CPU-GPU ๊ฐ„ ๋™๊ธฐํ™” ๋ณ‘๋ชฉ์ด ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.
  • ๊ฐ€๋น„์ง€ ์ปฌ๋ ‰์…˜(GC) ์ŠคํŒŒ์ดํฌ ๋ฐ ํ”„๋ ˆ์ž„ ๋“œ๋กญ: ๋ Œ๋”๋ง ๋ฃจํ”„(Execute) ๋‚ด์—์„œ C# ๋ฉ”๋ชจ๋ฆฌ ํ• ๋‹น(Allocations)์ด ์ˆ˜๋ฐ˜๋  ๊ฒฝ์šฐ GC Spike๋กœ ์ธํ•œ ํ™”๋ฉด ๋ฉˆ์ถค ํ˜„์ƒ์ด ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.

ScriptableRenderPass์˜ ๋‚ด๋ถ€ ์‹คํ–‰ ์ฃผ๊ธฐ(Execution Flow)

URP์—์„œ ๋ Œ๋” ํŒจ์Šค๊ฐ€ ์‹คํ–‰๋  ๋•Œ ํ˜ธ์ถœ๋˜๋Š” ์ฃผ์š” ๋ฉ”์„œ๋“œ์˜ ๋ผ์ดํ”„์‚ฌ์ดํด ์ˆœ์„œ๋ฅผ ์ดํ•ดํ•˜๋Š” ๊ฒƒ์ด ์ตœ์ ํ™”์˜ ์ฒซ๊ฑธ์Œ์ž…๋‹ˆ๋‹ค.

  1. ScriptableRendererFeature.Create(): ๋ Œ๋” ํ”ผ์ฒ˜๊ฐ€ ์ดˆ๊ธฐํ™”๋  ๋•Œ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. ์˜๊ตฌ ์ž์› ์ƒ์„ฑ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.
  2. ScriptableRendererFeature.AddRenderPasses(): ๋งค ํ”„๋ ˆ์ž„ ์นด๋ฉ”๋ผ๋งˆ๋‹ค ํŒจ์Šค๋ฅผ ๋ Œ๋”๋Ÿฌ ํ์— ์ถ”๊ฐ€ํ•  ๋•Œ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.
  3. ScriptableRenderPass.OnCameraSetup(): ์นด๋ฉ”๋ผ๊ฐ€ ๋ Œ๋”๋ง์„ ์‹œ์ž‘ํ•˜๊ธฐ ์ง์ „ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. ๋ Œ๋” ํƒ€๊ฒŸ์˜ ํฌ๊ธฐ(Resolution)๋ฅผ ์นด๋ฉ”๋ผ ๋””์Šคํฌ๋ฆฝํ„ฐ(RenderTextureDescriptor)์— ๋งž์ถ”์–ด ์ž„์‹œ ํƒ€๊ฒŸ์„ ์ƒ์„ฑํ•˜๊ธฐ ๊ฐ€์žฅ ์ข‹์€ ์œ„์น˜์ž…๋‹ˆ๋‹ค.
  4. ScriptableRenderPass.Execute(): ์‹ค์ œ GPU ์ปค๋งจ๋“œ ๋ฒ„ํผ(CommandBuffer) ๋ช…๋ น์„ ๊ธฐ๋กํ•˜๊ณ  ์ œ์ถœํ•˜๋Š” ํ•ต์‹ฌ ๋ฃจํ”„์ž…๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์„œ ๋งค ํ”„๋ ˆ์ž„ ์ž์›์„ ๋™์  ์‹ ๊ทœ ํ• ๋‹นํ•˜๋Š” ๊ฒƒ์€ ๊ธˆ๋ฌผ์ž…๋‹ˆ๋‹ค.
  5. ScriptableRenderPass.OnCameraCleanup(): ์นด๋ฉ”๋ผ ๋ Œ๋”๋ง์ด ์™„๋ฃŒ๋œ ํ›„ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. OnCameraSetup์—์„œ ํ• ๋‹นํ•œ ์ž„์‹œ ๋ Œ๋” ํƒ€๊ฒŸ์„ ๋ฐ˜๋“œ์‹œ ํ•ด์ œ(Release)ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  6. ScriptableRendererFeature.Dispose(): ๋ Œ๋” ํ”ผ์ฒ˜๊ฐ€ ํŒŒ๊ดด๋˜๊ฑฐ๋‚˜ ๋น„ํ™œ์„ฑํ™”๋  ๋•Œ ํ˜ธ์ถœ๋˜๋ฉฐ, ์ „์—ญ์ ์œผ๋กœ ํ• ๋‹น๋œ RTHandle ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์ตœ์ข… ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค.


2. ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ๋ฐ ํ”„๋กœํŒŒ์ผ๋ง(Troubleshooting & Profiling) ๊ณผ์ •

VRAM ๋ˆ„์ˆ˜์™€ ํ”„๋ ˆ์ž„ ๋“œ๋กญ ํ˜„์ƒ์„ ๊ตฌ์ฒด์ ์œผ๋กœ ์ง„๋‹จํ•˜๊ธฐ ์œ„ํ•ด ์œ ๋‹ˆํ‹ฐ ๋‚ด๋ถ€ ๋„๊ตฌ๋ฅผ ํ™œ์šฉํ•˜๋Š” ์‹ค๋ฌด ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ… ์ ˆ์ฐจ๋ฅผ ์‚ดํŽด๋ด…๋‹ˆ๋‹ค.

๋‹จ๊ณ„ 1: Unity Profiler์˜ GPU Memory ๋ฐ Detail ๋ถ„์„

์œ ๋‹ˆํ‹ฐ ํ”„๋กœํŒŒ์ผ๋Ÿฌ์˜ Memory Profiler ํƒญ ๋˜๋Š” Profiler - GPU ๋ชจ๋“ˆ์„ ์—ฝ๋‹ˆ๋‹ค.

  • ๊ฐ€์ƒ ๋ฉ”๋ชจ๋ฆฌ ์ถ”์ด ํ™•์ธ: ๊ฒŒ์ž„์„ ์‹คํ–‰ํ•˜๊ณ  ์ˆ˜๋ถ„ ๋™์•ˆ ๊ด€์ฐฐํ–ˆ์„ ๋•Œ RenderTexture ํ•ญ๋ชฉ์˜ Total Used Memory์™€ Count๊ฐ€ ๊ณ„์†ํ•ด์„œ ์šฐ์ƒํ–ฅํ•˜๋ฉฐ ๋Š˜์–ด๋‚˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
  • Take Sample ๋ถ„์„: ๋ Œ๋”๋ง ๋„์ค‘ ์บก์ฒ˜(Take Sample)๋ฅผ ์‹คํ–‰ํ•˜์—ฌ Objects ๋ชฉ๋ก์—์„œ ์ด๋ฆ„์ด ์—†๋Š” RenderTexture(์˜ˆ: _TemporaryBuffer)๊ฐ€ ์ˆ˜๋ฐฑ, ์ˆ˜์ฒœ ๊ฐœ ๋ˆ„์ ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.

๋‹จ๊ณ„ 2: Frame Debugger๋ฅผ ํ†ตํ•œ Render Target Bind ํšŸ์ˆ˜ ์ถ”์ 

Frame Debugger (Window > Analysis > Frame Debugger)๋ฅผ ํ™œ์„ฑํ™”ํ•ฉ๋‹ˆ๋‹ค.

  • ์ปค์Šคํ…€ ๋ Œ๋” ํŒจ์Šค๊ฐ€ ์‹คํ–‰๋˜๋Š” ๋‹จ๊ณ„๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค.
  • SetRenderTarget ๋ฐ DrawMesh / Blit ์ด๋ฒคํŠธ๊ฐ€ ๋งค ํ”„๋ ˆ์ž„ ๋น„์ •์ƒ์ ์œผ๋กœ ์ƒˆ๋กœ์šด RT ID๋ฅผ ์ง€์นญํ•˜๋ฉฐ ์Šค์œ„์นญ๋˜๋Š”์ง€ ๊ด€์ฐฐํ•ฉ๋‹ˆ๋‹ค.
  • ๋™์ผํ•œ ํƒ€๊ฒŸ์„ ์žฌ์‚ฌ์šฉํ•˜์ง€ ๋ชปํ•˜๊ณ  ๋งค๋ฒˆ ์ƒˆ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์ฐธ์กฐํ•˜๋Š” ๊ฒฝ์šฐ, GetTemporaryRT์˜ ํ•ด์ œ ๋ˆ„๋ฝ์ด๋‚˜ RTHandle ๋™์  ์ƒ์„ฑ ์ฝ”๋“œ ๋ฌธ์ œ๋ฅผ ์˜์‹ฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

3. ๋ฌธ์ œ ์ฝ”๋“œ ๋ถ„์„ vs ์ตœ์ ํ™”๋œ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ

๊ฐ€์žฅ ํ”ํžˆ ๋ฒ”ํ•˜๋Š” ์•ˆํ‹ฐ ํŒจํ„ด(Anti-Pattern) ์ฝ”๋“œ์™€, URP 12 ๋ฐ ์ดํ›„ ๋ฒ„์ „(URP 14/17 ํฌํ•จ)์—์„œ ๊ถŒ์žฅ๋˜๋Š” ํ‘œ์ค€ RTHandle ์‹œ์Šคํ…œ ๊ธฐ๋ฐ˜ ์ตœ์ ํ™” ์ฝ”๋“œ๋ฅผ ๋น„๊ตํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

โŒ ๋ฌธ์ œ ์ฝ”๋“œ (์•ˆํ‹ฐ ํŒจํ„ด: VRAM ๋ˆ„์ˆ˜ ๋ฐ GC ์ŠคํŒŒ์ดํฌ ์œ ๋ฐœ)

์•„๋ž˜ ์ฝ”๋“œ๋Š” Execute() ์‹คํ–‰ ์‹œ๋งˆ๋‹ค GetTemporaryRT๋ฅผ ํ˜ธ์ถœํ•˜๋ฉด์„œ, ๋ Œ๋” ํƒ€๊ฒŸ์„ ํ•ด์ œํ•˜๋Š” ReleaseTemporaryRT๋ฅผ ๋ˆ„๋ฝํ•˜๊ฑฐ๋‚˜ ์ž˜๋ชป๋œ ์Šค์ฝ”ํ”„์—์„œ ์ฒ˜๋ฆฌํ•˜๋Š” ๋Œ€ํ‘œ์ ์ธ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

// [ ์ž˜๋ชป๋œ ์˜ˆ์‹œ ] ๋งค ํ”„๋ ˆ์ž„ Execute์—์„œ ํ• ๋‹น ๋ฐ ํ•ด์ œ ๋ฏธํก
public class BadCustomRenderPass : ScriptableRenderPass
{
    private RenderTargetIdentifier source;
    private int tempTextureID = Shader.PropertyToID("_TempFrameBuffer");

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        CommandBuffer cmd = CommandBufferPool.Get("BadPass");
        
        // ๋ฌธ์ œ 1: ๋งค ํ”„๋ ˆ์ž„ Execute ์•ˆ์—์„œ ์ž„์‹œ RT ๋””์Šคํฌ๋ฆฝํ„ฐ ๊ณ„์‚ฐ ๋ฐ ํ• ๋‹น
        RenderTextureDescriptor descriptor = renderingData.cameraData.cameraTargetDescriptor;
        cmd.GetTemporaryRT(tempTextureID, descriptor); // VRAM ํ• ๋‹น ๋ฐœ์ƒ!

        RenderTargetIdentifier tempTarget = new RenderTargetIdentifier(tempTextureID);

        // Blit ์ž‘์—… ์ˆ˜ํ–‰
        Blit(cmd, renderingData.cameraData.renderer.cameraColorTargetHandle, tempTarget);
        Blit(cmd, tempTarget, renderingData.cameraData.renderer.cameraColorTargetHandle);

        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);

        // ๋ฌธ์ œ 2: cmd.ReleaseTemporaryRT(tempTextureID)๊ฐ€ ๋ˆ„๋ฝ๋จ!
        // ๊ฒฐ๊ณผ: VRAM์ด ์ง€์†์ ์œผ๋กœ ๋ˆ„์ˆ˜๋˜๊ณ  GPU ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ๊ฐ€๋“ ์ฐธ.
    }
}

โœ… ํ•ด๊ฒฐ ์ฝ”๋“œ (์ตœ์‹  URP RTHandle API ๊ธฐ๋ฐ˜ ์ตœ์ ํ™” ํŒจํ„ด)

URP ์ตœ์‹  ๋ฒ„์ „์—์„œ๋Š” ๋‹จ์ˆœ RenderTargetIdentifier ๋Œ€์‹  RTHandle ์‹œ์Šคํ…œ์„ ์‚ฌ์šฉํ•˜์—ฌ ์•ˆ์ „ํ•œ ๋ผ์ดํ”„์‚ฌ์ดํด ๊ด€๋ฆฌ๋ฅผ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. OnCameraSetup๊ณผ OnCameraCleanup์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์Œ(Pair)์œผ๋กœ ๊ตฌํ˜„ํ•˜๊ฑฐ๋‚˜ ScriptableRendererFeature.Dispose๋ฅผ ํ†ตํ•ด ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class OptimizedCustomRenderPass : ScriptableRenderPass
{
    private RTHandle tempRTHandle;
    private readonly Material customMaterial;
    private const string ProfilerTag = "OptimizedCustomPass";

    public OptimizedCustomRenderPass(Material material)
    {
        this.customMaterial = material;
        renderPassEvent = RenderPassEvent.AfterRenderingTransparents;
    }

    public override void OnCameraSetup(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        // 1. ์นด๋ฉ”๋ผ ๋””์Šคํฌ๋ฆฝํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
        RenderTextureDescriptor descriptor = renderingData.cameraData.cameraTargetDescriptor;
        descriptor.depthBufferBits = 0; // ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ ๊นŠ์ด ๋ฒ„ํผ ์‚ฌ์šฉ

        // 2. RTHandle ํ• ๋‹น ๋˜๋Š” ์žฌํ• ๋‹น (ํ•ด๋‹น ํŒจ์Šค ์‹คํ–‰ ์ „ ์•ˆ์ „ํ•˜๊ฒŒ ์ค€๋น„)
        RenderingUtils.ReAllocateIfNeeded(
            ref tempRTHandle, 
            descriptor, 
            FilterMode.Bilinear, 
            TextureWrapMode.Clamp, 
            name: "_CustomTempTexture"
        );
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (customMaterial == null) return;

        CommandBuffer cmd = CommandBufferPool.Get(ProfilerTag);

        using (new ProfilingScope(cmd, new ProfilingSampler(ProfilerTag)))
        {
            // ํ˜„์žฌ ์นด๋ฉ”๋ผ์˜ ์ปฌ๋Ÿฌ ํƒ€๊ฒŸ ๊ฐ€์ ธ์˜ค๊ธฐ (URP ์ตœ์‹  RTHandle ๋ฐฉ์‹)
            RTHandle cameraColorTarget = renderingData.cameraData.renderer.cameraColorTargetHandle;

            // Blit ์ฒ˜๋ฆฌ: Source -> Temp -> Source
            Blit(cmd, cameraColorTarget, tempRTHandle, customMaterial, 0);
            Blit(cmd, tempRTHandle, cameraColorTarget);
        }

        // ๋ช…๋ น์–ด ์ œ์ถœ
        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
    }

    public override void OnCameraCleanup(CommandBuffer cmd)
    {
        // FrameCleanup ์‹œ์ ์— ํ•„์š”์— ๋”ฐ๋ผ ์ •๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Œ.
        // ReAllocateIfNeeded๋ฅผ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ ์นด๋ฉ”๋ผ ํ•ด์ƒ๋„ ๋ณ€๊ฒฝ ์‹œ ์ž๋™ ๋Œ€์‘๋จ.
    }

    public void Dispose()
    {
        // 3. RenderFeature๊ฐ€ ํŒŒ๊ดด๋  ๋•Œ RTHandle ์ตœ์ข… ํ•ด์ œ (VRAM ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ ํ•ต์‹ฌ)
        tempRTHandle?.Release();
        tempRTHandle = null;
    }
}


4. ์—ฃ์ง€ ์ผ€์ด์Šค(Edge Cases) ๋Œ€์‘ ๋ฐ ์ถ”๊ฐ€ ์„ฑ๋Šฅ ์ตœ์ ํ™” ํŒ

์‹ค๋ฌด ํ™˜๊ฒฝ์—์„œ๋Š” ๋‹จ์ˆœ ๋‹จ์ผ ์นด๋ฉ”๋ผ ๋ Œ๋”๋ง ์™ธ์—๋„ ํ•ด์ƒ๋„ ๋ณ€๊ฒฝ, XR(VR/AR) ๋ฉ€ํ‹ฐํŒจ์Šค ๋ Œ๋”๋ง, ์”ฌ ๋ณ€๊ฒฝ ๋“ฑ ๋‹ค์–‘ํ•œ ๋ณ€์ˆ˜๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ์ž์› ๊ด€๋ฆฌ ์‹œ ๊ณ ๋ คํ•ด์•ผ ํ•  ์ค‘์š” ์—ฃ์ง€ ์ผ€์ด์Šค๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

1) ๋™์  ํ•ด์ƒ๋„ ๋ฐ ์œˆ๋„์šฐ ๋ฆฌ์‚ฌ์ด์ฆˆ(Dynamic Resolution & Window Resizing)

์‚ฌ์šฉ์ž๊ฐ€ ์ฐฝ ๋ชจ๋“œ ํฌ๊ธฐ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜ ๋ชจ๋ฐ”์ผ ๋””๋ฐ”์ด์Šค์—์„œ ๋™์  ํ•ด์ƒ๋„๊ฐ€ ์ ์šฉ๋˜๋ฉด, cameraTargetDescriptor์˜ ํญ(Width)๊ณผ ๋†’์ด(Height)๊ฐ€ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค.

  • ๊ธฐ์กด GetTemporaryRT ๋ฐฉ์‹์€ ์ŠคํŽ™ ๋ณ€๊ฒฝ ์‹œ ์ด์ „ ํฌ๊ธฐ์˜ ๋ฒ„ํผ๊ฐ€ VRAM์— ๋‚จ๋Š” ๋ฌธ์ œ๊ฐ€ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
  • ์œ ๋‹ˆํ‹ฐ RenderingUtils.ReAllocateIfNeeded API๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํ…์Šค์ฒ˜ ํŒŒ๋ผ๋ฏธํ„ฐ(Width, Height, Format ๋“ฑ)๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ๋งŒ ๊ธฐ์กด GPU ๋ฒ„ํผ๋ฅผ ํ•ด์ œํ•˜๊ณ  ์ƒˆ๋กœ ํ• ๋‹นํ•˜๋ฏ€๋กœ VRAM ์˜ค๋ฒ„ํ—ค๋“œ๋ฅผ ๋Œ€ํญ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

2) XR/VR ์‹ฑ๊ธ€ ํŒจ์Šค ์ธ์Šคํ„ด์‹ฑ(Single-Pass Instanced Rendering)

VR ํ™˜๊ฒฝ์—์„œ๋Š” ์ขŒ์šฐ ์–‘์•ˆ ๋ Œ๋”๋ง์„ ์œ„ํ•ด ํ…์Šค์ฒ˜ ์–ด๋ ˆ์ด(Texture2DArray) ํ˜•ํƒœ์˜ ๋ Œ๋” ํƒ€๊ฒŸ์ด ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

  • ์ž„์‹œ RT๋ฅผ ๋งŒ๋‹ฌ ๋•Œ descriptor.msaaSamples = 1; ๋ฐ descriptor.vrUsage ์†์„ฑ์„ ๊ธฐ๋ณธ ์นด๋ฉ”๋ผ ์„ค์ •๊ณผ ์ผ์น˜์‹œ์ผœ์•ผ ์›์ธ ๋ชจ๋ฅผ ๋ Œ๋”๋ง ๊นจ์ง ํ˜„์ƒ๊ณผ ๋ถˆํ•„์š”ํ•œ VRAM ์žฌํ• ๋‹น์„ ๋ง‰์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

3) GC Alloc ์ตœ์†Œํ™”๋ฅผ ์œ„ํ•œ CommandBufferPool ํ™œ์šฉ

Execute() ๋‚ด๋ถ€์—์„œ new CommandBuffer()๋ฅผ ์ง๋ ฌ๋กœ ์ƒ์„ฑํ•˜๋Š” ํ–‰์œ„๋Š” C# ํž™(Heap) ๋ฉ”๋ชจ๋ฆฌ ํ• ๋‹น์„ ์œ ๋ฐœํ•˜์—ฌ ์ฃผํŒŒ์ˆ˜ ๋†’์€ ๊ฐ€๋น„์ง€ ์ปฌ๋ ‰์…˜์„ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. ๋ฐ˜๋“œ์‹œ CommandBufferPool.Get()๊ณผ CommandBufferPool.Release()๋ฅผ ์‚ฌ์šฉํ•ด ์ปค๋งจ๋“œ ๋ฒ„ํผ ๊ฐ์ฒด๋ฅผ ์žฌ์‚ฌ์šฉํ•ด์•ผ ํ”„๋ ˆ์ž„ ๋“œ๋กญ์„ ์™„๋ฒฝํžˆ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

5. ๊ฒฐ๋ก : ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ํ†ตํ•œ ๊ด€๋ฆฌ

์œ ๋‹ˆํ‹ฐ URP ์ปค์Šคํ…€ ๋ Œ๋” ํ”ผ์ฒ˜ ๊ฐœ๋ฐœ ๊ณผ์ •์—์„œ GPU VRAM ๋ˆ„์ˆ˜์™€ ํ”„๋ ˆ์ž„ ๋“œ๋กญ์„ ์˜ˆ๋ฐฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋‹ค์Œ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ์ค€์ˆ˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ ๊ฒ€ ํ•ญ๋ชฉ ๊ถŒ์žฅ ์‚ฌํ•ญ ๋ฐ ํ•ด๊ฒฐ์ฑ…
Execute() ๋‚ด ์‹ ๊ทœ ํ• ๋‹น ๋งค ํ”„๋ ˆ์ž„ GetTemporaryRT ๋˜๋Š” new ํ‚ค์›Œ๋“œ ํ˜ธ์ถœ์„ ์ฒ ์ €ํžˆ ๊ธˆ์ง€ํ•  ๊ฒƒ.
RTHandle ์‚ฌ์šฉ RenderTargetIdentifier ๋‹จ๋… ์‚ฌ์šฉ์„ ์ง€์–‘ํ•˜๊ณ , URP ์ตœ์‹  RTHandle ๋ฐ ReAllocateIfNeeded ํ™œ์šฉ.
์ž์› ํ•ด์ œ(Dispose) ScriptableRendererFeature.Dispose() ๋ฐ ํŒจ์Šค์˜ ํ•ด์ œ ๋ฉ”์„œ๋“œ์—์„œ RTHandle.Release()๋ฅผ ๋ฐ˜๋“œ์‹œ ํ˜ธ์ถœ.
ํ”„๋กœํŒŒ์ผ๋ง ๊ฒ€์ฆ Frame Debugger์™€ Memory Profiler๋กœ RenderTexture ๊ฐœ์ˆ˜ ์ƒํ•œ์„  ์œ ์ง€ ์—ฌ๋ถ€ ๊ฒ€์ฆ.

์˜ฌ๋ฐ”๋ฅธ ๋ผ์ดํ”„์‚ฌ์ดํด ๊ด€๋ฆฌ๋ฅผ ์ ์šฉํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ๋„ GPU VRAM ๋ˆ„์ˆ˜ ์œ„ํ—˜์„ ์™„์ „ํžˆ ์ œ๊ฑฐํ•˜๊ณ , ๋ชจ๋ฐ”์ผ ๋ฐ ์ €์‚ฌ์–‘ ๊ธฐ๊ธฐ์—์„œ๋„ ์•ˆ์ •์ ์ธ 60FPS ์ด์ƒ์˜ ํ”„๋ ˆ์ž„ ํƒ€์ž„์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ Œ๋” ํŒŒ์ดํ”„๋ผ์ธ์˜ ๊ตฌ์กฐ์  ํŠน์„ฑ์„ ์ดํ•ดํ•˜๊ณ  ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ์Šต๊ด€์ด ๊ณ ์„ฑ๋Šฅ ๊ฒŒ์ž„ ๊ทธ๋ž˜ํ”ฝ ๊ตฌํ˜„์˜ ์ถœ๋ฐœ์ ์ž…๋‹ˆ๋‹ค.

๋ฐ˜์‘ํ˜•