유니티

유니티 커스텀 렌더 피쳐 다루기 / Unity Custom Render Feature

고라니TV 2025. 11. 12. 22:01

개념

(해당 내용은 그래픽적인 부분을 다루므로 난이도가 어려울 수 있습니다.)

유니티는 URP, HDRP가 등장함으로 커스텀하게 렌더 피쳐를 만들 수 있게 되었습니다.

 

 

Edit - Project Settings - Graphics - Default Render Pipeline - Render Pipeline Asset의

Renderer List - Renderer Data - Add Renderer Feature를 누르면

유니티가 범용적으로 쓰도록 만들어 놓은 렌더 피쳐들이 있습니다.

 

ex) 렌더 오브젝트, 전체화면, 데칼, 화면기반 환경 차폐, 화면기반 그림자

 

 

렌더 파이프라인 에셋(Render Pipeline Asset)은 렌더링 설정 방식입니다.

렌더러 데이터(Renderer Data)는 세부 렌더링 설정 방식입니다.

그 하위에 바로 렌더 피쳐(Render Feature)가 있습니다.

 

유니티의 커스텀 렌더 피쳐를 직접 만들면

특정 레이어에서 특정 카메라특정 렌더 순서에서 특정 오브젝트만 처리할 수 있는 장점이 있습니다.

셰이더 그래프로 불가능한 머테리얼이 적용된 텍스쳐를 출력하는 Blit 효과도 다룰 수 있습니다.

텍스쳐를 다운 샘플링하여 해상도를 낮추는 효과도 가능합니다.

 

그래서 Outline, Blur, Bloom, Edge Detect, Toon Light, SSAO 등에서 사용됩니다.

 

 

 

기본형 렌더 피쳐 만들기

6000.2 버전 URP 기준으로 생성하는 법을 알려드리겠습니다.

2023 이전 같은 구 버전에서는 다른 형태로 나올 수 있는데 대부분 더이상 사용되지 않는 코드로 표시될 것입니다.

Project 우클릭 - Create - Scripting - URP Renderer Feature Script로 생성합니다.

이름을 TestRenderFeature로 하겠습니다.

 

 

TestRenderFeature.cs

using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering.RenderGraphModule;

public class TestRenderFeature : ScriptableRendererFeature
{
    [SerializeField] TestRenderFeatureSettings settings;
    TestRenderFeaturePass m_ScriptablePass;

    public override void Create()
    {
        m_ScriptablePass = new TestRenderFeaturePass(settings);

        // 렌더 패스를 어느 시점에 삽입할지 설정합니다.
        m_ScriptablePass.renderPassEvent = RenderPassEvent.AfterRenderingOpaques;

        // 아래 줄의 주석을 해제하면 URP 컬러 텍스처와 깊이 버퍼를 입력으로 요청할 수 있습니다.
        // URP는 렌더 패스를 실행하기 전에 이러한 리소스의 복사본이 샘플링 가능하도록 보장합니다.
        // 성능에 영향을 주므로 꼭 필요할 때만 사용하세요. 특히 모바일 및 다른 TBDR GPU에서는 렌더 패스를 깨뜨릴 수 있습니다.
        //m_ScriptablePass.ConfigureInput(ScriptableRenderPassInput.Color | ScriptableRenderPassInput.Depth);

        // 아래 줄의 주석을 해제하면 URP가 중간 텍스처로 렌더하도록 요청할 수 있습니다.
        // 백버퍼에 직접 렌더링을 지원하지 않는 패스를 위해 이 옵션을 사용하세요.
        // 역시 성능에 영향을 주므로 꼭 필요할 때만 사용하세요. 특히 모바일 및 다른 TBDR GPU에서는 렌더 패스를 깨뜨릴 수 있습니다.
        //m_ScriptablePass.requiresIntermediateTexture = true;
    }

    // 여기에서 하나 이상의 렌더 패스를 렌더러에 주입할 수 있습니다.
    // 이 메서드는 카메라당 한 번, 렌더러를 설정할 때 호출됩니다.
    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        renderer.EnqueuePass(m_ScriptablePass);
    }

    // 기능에서 패스로 설정을 전달할 때 사용하는 클래스입니다.
    [Serializable]
    public class TestRenderFeatureSettings
    {
        
    }

    class TestRenderFeaturePass : ScriptableRenderPass
    {
        readonly TestRenderFeatureSettings settings;

        public TestRenderFeaturePass(TestRenderFeatureSettings settings)
        {
            this.settings = settings;
        }

        // 이 클래스는 RenderGraph 패스에 필요한 데이터를 저장합니다.
        // RenderGraph 패스를 실행하는 대리자 함수에 매개변수로 전달됩니다.
        private class PassData
        {
            
        }

        // 이 정적 메서드는 RenderGraph 렌더 패스에 RenderFunc 대리자로 전달됩니다.
        // 드로우 명령을 실행하는 데 사용됩니다.
        static void ExecutePass(PassData data, RasterGraphContext context)
        {
            
        }

        // RecordRenderGraph는 RenderGraph 핸들에 접근하여 그래프에 렌더 패스를 추가할 수 있는 곳입니다.
        // FrameData는 URP 리소스에 접근하고 관리할 수 있는 컨텍스트 컨테이너입니다.
        public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
        {
            const string passName = "Render Custom Pass";

            // 이름과 ExecutePass 함수에 전달될 데이터 타입을 지정하여 래스터 렌더 패스를 그래프에 추가합니다.
            using (var builder = renderGraph.AddRasterRenderPass<PassData>(passName, out var passData))
            {
                // 이 영역을 사용하여 패스에 필요한 입력과 출력 설정, 실행 시 필요한 passData의 속성을 구성하세요.

                // frameData를 활용해 전용 컨테이너를 통해 리소스와 카메라 데이터를 접근할 수 있습니다.
                // 예:
                // UniversalCameraData cameraData = frameData.Get<UniversalCameraData>();
                UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();

                // builder 인터페이스를 통해 패스의 입력과 출력을 설정합니다.
                // 예:
                // builder.UseTexture(sourceTexture);
                // TextureHandle destination = UniversalRenderer.CreateRenderGraphTexture(renderGraph, cameraData.cameraTargetDescriptor, "Destination Texture", false);

                // 패스의 렌더 타겟을 활성 컬러 텍스처로 설정합니다. 필요하면 자신의 렌더 타겟으로 변경하세요.
                builder.SetRenderAttachment(resourceData.activeColorTexture, 0);

                // ExecutePass 함수를 렌더 패스 대리자로 지정합니다. RenderGraph가 패스를 실행할 때 호출됩니다.
                builder.SetRenderFunc((PassData data, RasterGraphContext context) => ExecutePass(data, context));
            }
        }
    }
}

 

클래스로 보면 TestRenderFeature 내부에 TestRenderFeaturePass가 있으며 패스는 여러개를 돌릴 수 있습니다.

 

 

TestRenderFeature 클래스부터 살펴보겠습니다.

 

Create 함수는 씬 로드시 한번 호출되며 MonoBehaviour의 Start와 같은 기능입니다.

 

AddRenderPasses 함수는 매 프레임마다 호출되며 MonoBehaviour의 Update와 같은 기능입니다.

 

 

  • m_ScriptablePass = new TestRenderFeaturePass(settings);
    패스 클래스의 인스턴스를 셋팅과 함께 생성합니다.
    셋팅은 인스펙터에서 값을 조절할 때 사용합니다.


  • m_ScriptablePass.renderPassEvent = RenderPassEvent.AfterRenderingOpaques;
    렌더링의 어느시점에 실행할 지 설정합니다.
    해당 시점에서 매 프레임마다 AddRenderPasses가 돌아갑니다.

렌더링 파이프라인의 패스

[PrePasses] ➝ [GBuffer] ➝ [DeferredLights] ➝ [Opaques] ➝
[Skybox] ➝ [Transparents] ➝ [PostProcessing] ➝ [AfterRendering]

PrePasses: 렌더링 전
GBuffer: (Deferred 렌더링 전용) 색상, 노말 등을 각각 렌더 텍스쳐에 분리해 저장
DeferredLights: (Deferred 렌더링 전용) 조명을 한번에 연산
Opaques: 불투명 오브젝트
Skybox: 스카이박스
Transparents: 반투명 오브젝트
PostProcessing: 후처리
AfterRendering: 렌더링 후

 

 

  • m_ScriptablePass.ConfigureInput(ScriptableRenderPassInput.Color | ScriptableRenderPassInput.Depth);
    텍스쳐를 읽고 쓰기 위해 어떤 렌더링 리소스 권한이 필요한지 미리 URP에게 알려줍니다.
    | 로 여러 개의 권한을 동시에 받을 수 있으며, 성능에 영향을 주므로 꼭 필요할 때 사용해야 합니다.

    None : 아무 입력도 필요 없음
    Color : 카메라 컬러 텍스쳐 접근
    Depth : 카메라 깊이 텍스쳐 접근
    Normal : 카메라 월드 노말 텍스쳐 접근
    Motion : 모션 벡터 텍스쳐 접근


  • m_ScriptablePass.requiresIntermediateTexture = true;
    유니티 6000 이상부터는 중간 텍스쳐가 없이 주로 끄기 때문에 잘 안씁니다.


  • renderer.EnqueuePass(m_ScriptablePass);
    렌더링 파이프라인의 패스에서 내가 설정한 시점(RenderPassEvent.AfterRenderingOpaques 같은)에 패스를 끼워넣습니다.

 

 

TestRenderFeaturePass 내부 클래스를 살펴보겠습니다.

 

RecordRenderGraph 함수는 CPU에서 사용할 리소스를 지정하는 계획 단계입니다.

RenderGraph 매개변수는 필요한 리소스만 GPU 메모리에 유지하도록 기록하는 역할입니다.

ContextContainer 매개변수는 카메라 프레임에 관련된 모든 데이터를 담는 컨테이너 입니다.

 

PassData 클래스는 CPU에서 만들어져 GPU에게 전달할 머테리얼, 텍스쳐 등을 담는 컨테이너입니다.

 

ExecutePass 함수는 GPU에서 DrawCall, Blit 머테리얼 적용을 합니다.
static인 이유는 반드시 PassData로 전달된 데이터만 사용가능해야하며

외부 인스턴스(this)를 참조하지 못하도록 안전해야 하기 때문입니다.

 

 

  • using (var builder = renderGraph.AddRasterRenderPass<PassData>(passName, out var passData))
    렌더 그래프에 GPU 렌더 패스 하나를 추가하는데 passName 이름이고, PassData 데이터를 사용하도록 합니다.
    builder 인터페이스를 통해 패스의 입력과 출력을 설정합니다.


  • UniversalCameraData cameraData = frameData.Get<UniversalCameraData>();
    ContextContainer에 담겨있는 카메라 관련 데이터를 가져옵니다.


  • UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();
    ContextContainer에 담겨있는 색상, 깊이 등 모든 텍스쳐 리소스를 가져옵니다.


  • builder.UseTexture(sourceTexture);
    TextureHandle을 읽기 전용으로 사용합니다.
    TextureHandle은 텍스쳐와 텍스쳐와 관련된 정보를 담는 구조체입니다.


  • TextureHandle destination = UniversalRenderer.CreateRenderGraphTexture(renderGraph, cameraData.cameraTargetDescriptor, "Destination Texture", false);
    텍스쳐를 생성합니다.
    렌더 그래프에 대해서 카메라 설정값으로 이름을 정해주고 텍스쳐 생성시 초기화를 대부분 false
    현재는 빈 검정색 텍스쳐가 생성됨


  • builder.SetRenderAttachment(resourceData.activeColorTexture, 0);
    TextureHandle을 출력으로 사용합니다.


  • builder.SetRenderFunc((PassData data, RasterGraphContext context) => ExecutePass(data, context));
    CPU에서 작성된 정보를 토대로 GPU에게 패스를 실행하라고 명령합니다.
    GPU에서 사용할 데이터인 PassData와 함께 GPU 실행환경을 매개변수로 넘겨줍니다.

 

 

 

풀스크린 렌더 피쳐 만들기

 

위에서 안 내용을 기반으로 전체화면에 머테리얼을 적용하는 풀스크린 렌더 피쳐를 만들어 보겠습니다.

 

 

TestRenderFeature.cs

using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.RenderGraphModule.Util;

public class TestRenderFeature : ScriptableRendererFeature
{
    [SerializeField] TestRenderFeatureSettings settings;
    TestRenderFeaturePass m_ScriptablePass;

    public override void Create()
    {
        m_ScriptablePass = new TestRenderFeaturePass(settings);
        m_ScriptablePass.renderPassEvent = RenderPassEvent.BeforeRenderingPostProcessing;
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        renderer.EnqueuePass(m_ScriptablePass);
    }

    [Serializable]
    public class TestRenderFeatureSettings
    {
        public Material material;
    }

    class TestRenderFeaturePass : ScriptableRenderPass
    {
        readonly TestRenderFeatureSettings settings;

        public TestRenderFeaturePass(TestRenderFeatureSettings settings)
        {
            this.settings = settings;
        }

        public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
        {
            UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();

            if (resourceData.isActiveTargetBackBuffer) return;

            TextureHandle source = resourceData.activeColorTexture;
            TextureDesc destinationDesc = renderGraph.GetTextureDesc(source);
            destinationDesc.name = "DestinationTexture";
            destinationDesc.clearBuffer = false;
            TextureHandle destination = renderGraph.CreateTexture(destinationDesc);

            RenderGraphUtils.BlitMaterialParameters blitParameters = new(source, destination, settings.material, 0);
            renderGraph.AddBlitPass(blitParameters, "TestRenderFeaturePass");
            
            resourceData.cameraColor = destination;
        }
    }
}

 

 

셋팅에 풀스크린에 적용할 머테리얼을 인스펙터에 보이도록 합니다.

 

Create 함수에서 포스트 프로세싱 이전 단계에서 패스가 돌도록 합니다.

 

RecordRenderGraph 함수를 봅시다.

  • UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();
    리소스 데이터를 가져옵니다.


  • if (resourceData.isActiveTargetBackBuffer) return;
    만약 리소스 데이터가 Back Buffer라면 실제 스크린에 그려지는 버퍼이므로 리턴합니다.  


  • TextureHandle source = resourceData.activeColorTexture;
    현재 텍스쳐를 source로 합니다.


  • TextureDesc destinationDesc = renderGraph.GetTextureDesc(source);
    source의 텍스쳐 정보를 destinationDesc에 담습니다.


  • destinationDesc.name = "DestinationTexture";
    destinationDesc.clearBuffer = false;
    이름도 정해주고, 클리어 버퍼를 false로 해줍니다. (기본값이 true인데 true라면 초기화를 하니 퍼포먼스가 좀더 듬)


  • TextureHandle destination = renderGraph.CreateTexture(destinationDesc);
    destination 텍스쳐를 생성합니다.


  • RenderGraphUtils.BlitMaterialParameters blitParameters = new(source, destination, settings.material, 0);
    Blit(머테리얼을 적용한 텍스쳐를 출력하는 GPU 처리과정)을 CPU 코드에서 쉽게 하는 방법입니다.
    생성자에서 원본 텍스쳐, 대상 텍스쳐, 적용할 머테리얼, 0번을 넣는 파라미터를 만듭니다.


  • renderGraph.AddBlitPass(blitParameters, "TestRenderFeaturePass");
    Blit하는 패스를 파라미터와 이름과 함께 추가합니다.


  • resourceData.cameraColor = destination;
    Blit된 destination 텍스쳐를 카메라에 표시되도록 합니다.

 

 

 

Rederer Data - Add Renderer Feature - Test Render Feature를 선택하면 위와같이 됩니다.

 

 

Project 우클릭 - Create - Shader Graph - URP - Fullscreen Shader Graph로 셰이더 그래프를 만듭니다.

이때 URP Sample Buffer의 SourceBuffer를 Blit Source로 하면

RenderGraphUtils.BlitMaterialParameters 내부에 선언되어 있는 _BlitTexture를 가져오는 것입니다.

 

 

해당 셰이더를 참조하는 머테리얼을 만들어 넣어주었더니 전체화면이 네거티브 효과가 나타나게 되었습니다.

 

 

 

중간 정리

핵심은 RecordRenderGraph (CPU 로직) ~ ExecutePass (GPU 로직) 입니다.

ResouceData나 CameraData를 입력받아

렌더 텍스쳐를 지지고 볶아 

GPU에서 실행할 것을 합니다.

 

 

 

다운샘플링과 글로벌 텍스쳐 렌더 피쳐 만들기

다운샘플링을 하여 셰이더그래프에서 텍스쳐 이름으로 다운샘플링된 텍스쳐를 가져와보겠습니다.

 

 

TestRenderFeature.cs

using System;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.RenderGraphModule.Util;

public class TestRenderFeature : ScriptableRendererFeature
{
    [SerializeField] TestRenderFeatureSettings settings;
    TestRenderFeaturePass m_ScriptablePass;

    public override void Create()
    {
        m_ScriptablePass = new TestRenderFeaturePass(settings);
        m_ScriptablePass.renderPassEvent = RenderPassEvent.BeforeRenderingTransparents;
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        renderer.EnqueuePass(m_ScriptablePass);
    }

    [Serializable]
    public class TestRenderFeatureSettings
    {
        public Material blitMaterial;
        [Range(1, 16)] public int downsample = 16;
    }

    class TestRenderFeaturePass : ScriptableRenderPass
    {
        readonly TestRenderFeatureSettings settings;
        readonly int globalTextureID = Shader.PropertyToID("_MyGlobalTexture");

        class PassData { }

        public TestRenderFeaturePass(TestRenderFeatureSettings settings)
        {
            this.settings = settings;
        }

        public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
        {
            UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();

            if (resourceData.isActiveTargetBackBuffer) return;

            TextureHandle source = resourceData.activeColorTexture;
            TextureDesc destinationDesc = renderGraph.GetTextureDesc(source);
            destinationDesc.width /= settings.downsample;
            destinationDesc.height /= settings.downsample;
            destinationDesc.clearBuffer = false;
            TextureHandle destination = renderGraph.CreateTexture(destinationDesc);

            RenderGraphUtils.BlitMaterialParameters blitParams = new(source, destination, settings.blitMaterial, 0);
            renderGraph.AddBlitPass(blitParams, "SimpleBlitPass");

            using (var builder = renderGraph.AddUnsafePass<PassData>("DownsampleGlobalPass", out var passData))
            {
                builder.AllowGlobalStateModification(true);
                builder.UseTexture(destination, AccessFlags.Read);
                builder.SetGlobalTextureAfterPass(destination, globalTextureID);
                builder.SetRenderFunc((PassData data, UnsafeGraphContext context) => { });
            }
        }
    }
}

 

Create 함수에서 Transparent 이전에 패스를 끼워넣게 합니다.

 

설정에서 블릿할 머테리얼과 다운샘플링 1~16까지 범위의 숫자를 넣습니다.

 

RecordRenderGraph 함수를 봅시다.

 

  • TextureHandle source = resourceData.activeColorTexture;
    TextureDesc destinationDesc = renderGraph.GetTextureDesc(source);
    destinationDesc.width /= settings.downsample;
    destinationDesc.height /= settings.downsample;
    destinationDesc.clearBuffer = false;
    TextureHandle destination = renderGraph.CreateTexture(destinationDesc);
    현재 컬러 텍스쳐에서 대상 텍스쳐로 다운샘플만큼 나눈 너비와 높이로 복사합니다.
    destination 텍스쳐에는 너비와 높이가 작아진 초기화되지 않은 검정색 텍스쳐가 생성됩니다.

  • RenderGraphUtils.BlitMaterialParameters blitParams = new(source, destination, settings.blitMaterial, 0);
    renderGraph.AddBlitPass(blitParams, "SimpleBlitPass");
    원본에서 대상 텍스쳐로 블릿패스를 실행하여 대상 텍스쳐가 다운샘플링된 텍스쳐로 그려집니다.

    이때 해상도도 변경이 됩니다.


  • using (var builder = renderGraph.AddUnsafePass<PassData>("DownsampleGlobalPass", out var passData))
    안전하지 않은 패스(UnsafePass)를 실행합니다.
    RasterRenderPass는 최적화 시 재배치 대상이 될 수 있는데,
    글로벌 텍스쳐는 재배치 대상이 아니므로 UnsafePass에서 돌라고 명시해 줍니다.


  • builder.AllowGlobalStateModification(true);
    글로벌 텍스쳐로 동작하는 것을 허용합니다.


  • builder.UseTexture(destination, AccessFlags.Read);
    대상 텍스쳐를 읽기 전용으로 합니다.


  • builder.SetGlobalTextureAfterPass(destination, globalTextureID);
    globalTextureID는 Shader.PropertyToID("_MyGlobalTexture")의 int값입니다.
    셰이더 그래프에서 사용할 글로벌 텍스쳐 이름으로 지정합니다.


  • builder.SetRenderFunc((PassData data, UnsafeGraphContext context) => { });
    빈 GPU 렌더링을 돌립니다.

 

 

 

프로젝트 우클릭 - Create - Shader Graph - URP - Fullscreen Shader Graph으로 TestBlitter라는 이름으로 만듭니다.

URP Sample Buffer노드의 Blit Source가 블릿할 때 _BlitTexture를 가져오기 때문에 원본 색상이 들어옵니다.

지금은 풀스크린 이펙트를 넣는 게 아니고 단순 다운 샘플링이기에 Base Color에 아무 작업없이 연결합니다.

 

 

 

프로젝트에 있는 셰이더 그래프 하위에는 자동으로 머테리얼이 생성되어 있습니다.

이것을 Test Render Feature의 Blit Material에 넣어줍니다.

이렇게 됨으로써 블릿을 수행하는 머테리얼이 대입되었습니다.

 

 

글로벌 텍스쳐를 셰이더 그래프에서 사용해보겠습니다.

 

 

이름이 Downsample인 Material은 Unlit, Surface Type은 Transparent로 셰이더 그래프를 만듭니다.

 

 

 

Texture2D 타입의 프로퍼티를 만드는데 Reference 이름을 "_MyGlobalTexture"로 맞춰줍니다.

노드 설정의 Scope를 Global로 바꿔주시면 이제 글로벌 텍스쳐를 불러올 수 있게 됩니다.

 

 

 

텍스쳐를 출력합니다.

 

 

 

TestRenderFeature의 DownSample을 조정해보면 다운샘플링 된 텍스쳐가 출력된 것을 알 수 있습니다.

 

 

 

마무리

축하드립니다. 렌더 피쳐의 기본을 배웠습니다.

별 기능이 없는데도 복잡하죠?

타이핑 하면서 익히시면 또 도움이 되실거예요.

다음 시간에는 에셋스토어 유료에셋 부럽지 않은 풀스크린 아웃라인과 카와세 블러에 대해 심도있게 정리하겠습니다.