Unity Render Texture & Custom Post-Processing
Render Texture는 카메라 출력 결과를 일반 텍스처처럼 다룰 수 있게 해 주는 Unity의 핵심 렌더링 기능입니다. 미니맵, 감시 카메라, 포털, 리플렉션, 커스텀 후처리 파이프라인 등에 광범위하게 쓰입니다. URP(Universal Render Pipeline)에서는 ScriptableRendererFeature API를 통해 렌더 패스를 직접 삽입해 완전한 커스텀 후처리를 구현할 수 있습니다.
1. Render Texture 기초
섹션 제목: “1. Render Texture 기초”1.1 생성과 연결
섹션 제목: “1.1 생성과 연결”using UnityEngine;
public class MiniMapSetup : MonoBehaviour{ [SerializeField] private Camera minimapCamera; [SerializeField] private RawImage minimapDisplay; // UI에 표시
private RenderTexture _rt;
void Awake() { // 해상도, 비트 심도, 스텐실 포맷 지정 _rt = new RenderTexture(512, 512, 24, RenderTextureFormat.ARGB32); _rt.antiAliasing = 1; _rt.filterMode = FilterMode.Bilinear; _rt.Create();
minimapCamera.targetTexture = _rt; minimapDisplay.texture = _rt; }
void OnDestroy() { if (_rt != null) { minimapCamera.targetTexture = null; _rt.Release(); Destroy(_rt); } }}RenderTexture.Create()를 명시적으로 호출해야 GPU 메모리가 실제로 할당됩니다. Release()와 Destroy()를 함께 호출해야 메모리 누수를 막을 수 있습니다.
1.2 임시 Render Texture (RenderTexture.GetTemporary)
섹션 제목: “1.2 임시 Render Texture (RenderTexture.GetTemporary)”후처리 패스에서 중간 버퍼가 필요할 때는 GetTemporary를 사용합니다. Unity 내부 풀에서 재사용하므로 할당 비용이 없습니다.
void OnRenderImage(RenderTexture src, RenderTexture dest){ RenderTexture temp = RenderTexture.GetTemporary( src.width, src.height, 0, src.format);
Graphics.Blit(src, temp, blurMaterial, 0); // 가로 블러 Graphics.Blit(temp, dest, blurMaterial, 1); // 세로 블러
RenderTexture.ReleaseTemporary(temp);}2. URP ScriptableRendererFeature
섹션 제목: “2. URP ScriptableRendererFeature”URP에서는 OnRenderImage가 동작하지 않습니다. 대신 ScriptableRendererFeature + ScriptableRenderPass 조합을 사용합니다.
2.1 렌더 패스 구현
섹션 제목: “2.1 렌더 패스 구현”using UnityEngine;using UnityEngine.Rendering;using UnityEngine.Rendering.Universal;
public class GrayscaleRenderPass : ScriptableRenderPass{ private readonly Material _material; private RTHandle _tempHandle;
public GrayscaleRenderPass(Material mat) { _material = mat; renderPassEvent = RenderPassEvent.AfterRenderingPostProcessing; }
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData) { var descriptor = renderingData.cameraData.cameraTargetDescriptor; descriptor.depthBufferBits = 0; RenderingUtils.ReAllocateIfNeeded(ref _tempHandle, descriptor, name: "_TempGrayscale"); }
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { if (_material == null) return;
CommandBuffer cmd = CommandBufferPool.Get("GrayscalePass"); RTHandle cameraTarget = renderingData.cameraData.renderer.cameraColorTargetHandle;
Blitter.BlitCameraTexture(cmd, cameraTarget, _tempHandle, _material, 0); Blitter.BlitCameraTexture(cmd, _tempHandle, cameraTarget);
context.ExecuteCommandBuffer(cmd); CommandBufferPool.Release(cmd); }
public override void OnCameraCleanup(CommandBuffer cmd) { // RTHandle은 OnCameraSetup에서 ReAllocateIfNeeded로 관리하므로 // 여기서는 별도 해제 불필요 }}2.2 렌더러 피처 등록
섹션 제목: “2.2 렌더러 피처 등록”using UnityEngine;using UnityEngine.Rendering.Universal;
[CreateAssetMenu(menuName = "Rendering/Grayscale Feature")]public class GrayscaleFeature : ScriptableRendererFeature{ [SerializeField] private Material grayscaleMaterial;
private GrayscaleRenderPass _pass;
public override void Create() { _pass = new GrayscaleRenderPass(grayscaleMaterial); }
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { if (grayscaleMaterial == null) return; renderer.EnqueuePass(_pass); }
protected override void Dispose(bool disposing) { // 패스 내부 RTHandle 해제 _pass = null; }}URP Renderer Asset의 Renderer Features 섹션에 이 컴포넌트를 추가하면 모든 카메라에 자동으로 적용됩니다.
3. 성능 고려 사항
섹션 제목: “3. 성능 고려 사항”| 항목 | 권장 |
|---|---|
| 해상도 | 목적에 맞는 최소 해상도 사용 (미니맵 256~512) |
| 포맷 | HDR 불필요 시 RenderTextureFormat.Default |
| 안티앨리어싱 | Render Texture에서는 보통 1 (MSAA 비활성) |
| Mipmap | 정적 용도라면 rt.useMipMap = false |
| 임시 버퍼 | 중간 패스는 반드시 GetTemporary / ReAllocateIfNeeded 활용 |
ReadPixels 비용 주의
섹션 제목: “ReadPixels 비용 주의”// 나쁜 예: 매 프레임 CPU-GPU 동기화 발생void Update(){ Texture2D tex = new Texture2D(rt.width, rt.height); RenderTexture.active = rt; tex.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0); tex.Apply();}
// 좋은 예: 필요할 때만, AsyncGPUReadback 사용void RequestCapture(){ AsyncGPUReadback.Request(_rt, 0, TextureFormat.RGBA32, OnReadbackComplete);}
void OnReadbackComplete(AsyncGPUReadbackRequest request){ if (request.hasError) return; var data = request.GetData<Color32>(); // 데이터 처리}4. 실전 패턴 — 포털 렌더링
섹션 제목: “4. 실전 패턴 — 포털 렌더링”포털 효과는 두 카메라의 Render Texture를 상대방 포털 메시의 머티리얼에 할당하는 방식으로 구현합니다.
public class PortalRenderer : MonoBehaviour{ [SerializeField] private Camera portalCamera; [SerializeField] private Renderer portalMesh;
private RenderTexture _rt; private static readonly int MainTex = Shader.PropertyToID("_MainTex");
void Start() { _rt = new RenderTexture(Screen.width / 2, Screen.height / 2, 24); portalCamera.targetTexture = _rt; portalMesh.material.SetTexture(MainTex, _rt); }
void LateUpdate() { // 플레이어 카메라의 상대 위치를 포털 카메라에 반영 Transform playerCam = Camera.main.transform; Transform linkedPortal = /* 반대쪽 포털 */ transform;
portalCamera.transform.SetPositionAndRotation( linkedPortal.TransformPoint( transform.InverseTransformPoint(playerCam.position)), linkedPortal.rotation * Quaternion.Inverse(transform.rotation) * playerCam.rotation ); }}5. Ping-Pong 더블 버퍼링
섹션 제목: “5. Ping-Pong 더블 버퍼링”반복적인 블러, 파문, 시뮬레이션처럼 이전 프레임 결과를 입력으로 쓸 때 두 RT를 번갈아 사용합니다.
public class PingPongEffect : MonoBehaviour{ [SerializeField] private Material _effectMaterial;
private RenderTexture _bufferA; private RenderTexture _bufferB; private bool _pingPong;
void Awake() { var desc = new RenderTextureDescriptor( Screen.width / 2, Screen.height / 2, RenderTextureFormat.ARGBHalf, 0);
_bufferA = new RenderTexture(desc); _bufferB = new RenderTexture(desc); _bufferA.Create(); _bufferB.Create(); }
void OnRenderImage(RenderTexture src, RenderTexture dest) { RenderTexture current = _pingPong ? _bufferA : _bufferB; RenderTexture previous = _pingPong ? _bufferB : _bufferA;
// 이전 버퍼를 이펙트 입력으로 _effectMaterial.SetTexture("_PrevFrame", previous); Graphics.Blit(src, current, _effectMaterial);
// 최종 결과를 화면에 출력 Graphics.Blit(current, dest);
_pingPong = !_pingPong; }
void OnDestroy() { _bufferA?.Release(); Destroy(_bufferA); _bufferB?.Release(); Destroy(_bufferB); }}6. Stencil 버퍼 활용
섹션 제목: “6. Stencil 버퍼 활용”Render Texture에 스텐실 버퍼를 포함하면 마스킹 효과(포털, 거울, 아웃라인)를 구현할 수 있습니다.
// 깊이+스텐실 포함 RT — depthBits = 24 (16은 스텐실 없음)var rt = new RenderTexture(512, 512, 24, RenderTextureFormat.ARGB32);rt.Create();
// 셰이더에서 스텐실 쓰기 (마스크 오브젝트)// Stencil { Ref 1 Comp Always Pass Replace }
// 셰이더에서 스텐실 읽기 (마스크 영역만 렌더)// Stencil { Ref 1 Comp Equal }RenderTexture는 GPU 메모리에 직접 할당되므로Release()+Destroy()쌍을 반드시 호출한다.- URP에서는
ScriptableRendererFeature+ScriptableRenderPass로 커스텀 렌더 패스를 주입한다. - 중간 버퍼는
RenderTexture.GetTemporary또는RenderingUtils.ReAllocateIfNeeded를 사용해 풀링한다. - CPU 픽셀 읽기가 필요하면
AsyncGPUReadback으로 비동기 처리해 프레임 히치를 방지한다.