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 ); }}RenderTexture는 GPU 메모리에 직접 할당되므로Release()+Destroy()쌍을 반드시 호출한다.- URP에서는
ScriptableRendererFeature+ScriptableRenderPass로 커스텀 렌더 패스를 주입한다. - 중간 버퍼는
RenderTexture.GetTemporary또는RenderingUtils.ReAllocateIfNeeded를 사용해 풀링한다. - CPU 픽셀 읽기가 필요하면
AsyncGPUReadback으로 비동기 처리해 프레임 히치를 방지한다.