개요


URP 환경에서는 빌트인에서 사용하던 OnRenderImage()가 작동하지 않아 기존 처럼 카메라에 포스트 프로세스 컴포넌트를 추가하는 방식이 안됩니다.

URP에서는 별도 렌더피쳐를 만들고 Post-process Volume을 사용해야합니다.

이번 포스팅은 URP환경에서 커스텀 포스트 프로세싱을 구현 & 적용 방법에 대해 포스팅하겠습니다.

 

 

프리뷰


 

플 프로젝트

Unity 2021.2.4f1 URP에서 구현 V1

https://github.com/CatDarkGame/URP_CustomPostProcess/releases/tag/V1

 

Release V1 · CatDarkGame/URP_CustomPostProcess

 

github.com

 

 

 

 

 

포스트프로세싱 전용 쉐이더 제작


Shader "Hidden/Postprocess/Grayscale"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
	}
	SubShader
	{
	    Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalPipeline"}
		ZTest Always ZWrite Off Cull Off

        Pass
        {
            HLSLPROGRAM

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.universal/Shaders/PostProcessing/Common.hlsl"

            #pragma prefer_hlslcc gles   
            #pragma exclude_renderers d3d11_9x  
            
            #pragma vertex FullscreenVert
            #pragma fragment frag

            sampler2D _MainTex;
            float4 _MainTex_ST;
            
            float _Amount;
             
       
            float4 frag(Varyings i) : SV_Target
            {
                float2 mainTexUV = i.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw;
                float4 col = tex2D(_MainTex, mainTexUV);
                float3 grayscale = (col.r + col.g + col.b) * 0.3333f;
                
               
                col.rgb = lerp(col.rgb,  grayscale, _Amount);
                
                return col;
            }
            
            ENDHLSL
        }
    }
}

 

먼저 HLSL기반으로 포스트프로세싱 전용 쉐이더를 제작합니다.

위 예시 코드로는 간단히 흑백 쉐이더를 만들었습니다.

URP에 내장되어 있는 다른 포스트프로세싱 쉐이더 코드를 참고했습니다.

 

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/Shaders/PostProcessing/Common.hlsl"

#pragma vertex FullscreenVert
#pragma fragment frag

float4 frag(Varyings i) : SV_Target
{
	...

Common.hlsl코드를 include하고 Vertex와 Fragment 함수를 위와 같이 설정합니다.

 

sampler2D _MainTex;
float4 _MainTex_ST;

float4 frag(Varyings i) : SV_Target
{
	float2 mainTexUV = i.uv.xy * _MainTex_ST.xy + _MainTex_ST.zw;
	float4 col = tex2D(_MainTex, mainTexUV);
    ...

화면 텍스처를 기존과 동일하게 _MainTex로 가져올 수 있습니다.

 

 

이슈

HLSL은 구현했을때 작동 되지만, Shader Graph로는 작동하지 않습니다, 원인과 방법은 아직 못 찾았습니다.

 

 

 

 

 

Volume Component 제작


Volume Component는 Post Process Volume에 추가되는 구성 요소들을 뜻합니다.

Volume Component 스크립트를 제작하겠습니다.

 

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

[Serializable, VolumeComponentMenuForRenderPipeline("Custom Post-processing/Grayscale", typeof(UniversalRenderPipeline))]
public class GrayScale : VolumeComponent, IPostProcessComponent
{
    private const string SHADER_NAME = "Hidden/Postprocess/Grayscale";
    private const string PROPERTY_AMOUNT = "_Amount";
    
    private Material _material;

    public BoolParameter IsEnable = new BoolParameter(false);
    public ClampedFloatParameter amount = new ClampedFloatParameter(0f, 0f, 1f);
    
     public bool IsActive()
    {
        if (IsEnable.value == false) return false;
        if (!active || 
            !_material || 
            amount.value <= 0.0f) return false;
        return true;
    }
    public virtual bool IsTileCompatible() => false;

    public void Setup()
    {
        if (!_material)
        {
            Shader shader = Shader.Find(SHADER_NAME);
            _material = CoreUtils.CreateEngineMaterial(shader);
        }
    }

    public void Destroy()
    {
        if (_material)
        {
            CoreUtils.Destroy(_material);
            _material = null;
        }
    }

    public void Render(CommandBuffer commandBuffer, ref RenderingData renderingData, RenderTargetIdentifier source, RenderTargetIdentifier destination)
    {
        if (!_material) return;
        
        _material.SetFloat(PROPERTY_AMOUNT, amount.value);
        
        commandBuffer.Blit(source, destination, _material);
    }
}

해당 스크립트도 마찬가지로, URP 내장 코드를 참고 했습니다.

 

 

[Serializable, VolumeComponentMenuForRenderPipeline("Custom Post-processing/Grayscale", typeof(UniversalRenderPipeline))]

맨 처음 VolumeComponentMenuForRenderPipeline Attribute를 추가하면 Post process Volume에서 추가할 수 있는

메뉴가 생성됩니다.

 

public BoolParameter IsEnable = new BoolParameter(false);
public ClampedFloatParameter amount = new ClampedFloatParameter(0f, 0f, 1f);

다음 코드는 Post process Volume의 Inspector에 보이는 변수입니다, 일반적인 변수는 Inspector에 보이지 않고 VolumeParameter클래스에 포함된 변수만 작동합니다.

 

 

 public void Setup()
{
    if (!_material)
    {
        Shader shader = Shader.Find(SHADER_NAME);
        _material = CoreUtils.CreateEngineMaterial(shader);
    }
}

public void Destroy()
{
    if (_material)
    {
        CoreUtils.Destroy(_material);
        _material = null;
    }
}

public void Render(CommandBuffer commandBuffer, ref RenderingData renderingData, RenderTargetIdentifier source, RenderTargetIdentifier destination)
{
    if (!_material) return;

    _material.SetFloat(PROPERTY_AMOUNT, amount.value);

    commandBuffer.Blit(source, destination, _material);
}

다음 함수들은 아래에서 설명할 RenderFeature & Pass에서 호출할 예정입니다.

 

 

 

 

 

RenderPass 제작


using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;


public class GrayScale_RenderPass : ScriptableRenderPass
{
    protected const string TEMP_BUFFER_NAME = "_TempColorBuffer";
   
    protected GrayScale Component;
    protected string RenderTag { get; }
    
    private RenderTargetIdentifier _source;
    private RenderTargetHandle _tempTexture;
    
    public GrayScale_RenderPass(string renderTag, RenderPassEvent passEvent) 
    {
        renderPassEvent = passEvent;
        RenderTag = renderTag;
    }

    public virtual void Setup(in RenderTargetIdentifier source)
    {
        _source = source;
        _tempTexture.Init(TEMP_BUFFER_NAME);
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (!renderingData.cameraData.postProcessEnabled) return;

        VolumeStack volumeStack = VolumeManager.instance.stack;
        Component = volumeStack.GetComponent<GrayScale>();
        if(Component) Component.Setup();
        if (!Component || !Component.IsActive())  return;
        
        CommandBuffer commandBuffer = CommandBufferPool.Get(RenderTag);
        RenderTargetIdentifier destination = _tempTexture.Identifier();
        
        // 렌더 텍스처 생성
        CameraData cameraData = renderingData.cameraData;
        RenderTextureDescriptor descriptor = new RenderTextureDescriptor(cameraData.camera.scaledPixelWidth, cameraData.camera.scaledPixelHeight);
        descriptor.colorFormat = cameraData.isHdrEnabled ? RenderTextureFormat.DefaultHDR : RenderTextureFormat.Default;
        commandBuffer.GetTemporaryRT(_tempTexture.id, descriptor);
        
        // 임시 버퍼 생성
        commandBuffer.Blit(_source, destination);
        // Pass 렌더링
        Component.Render(commandBuffer, ref renderingData, destination, _source);
        commandBuffer.ReleaseTemporaryRT(_tempTexture.id);
        
        context.ExecuteCommandBuffer(commandBuffer);
        CommandBufferPool.Release(commandBuffer);
    }
}

다음 렌더패스 스크립트입니다.

 

VolumeStack volumeStack = VolumeManager.instance.stack;
Component = volumeStack.GetComponent<GrayScale>();
if(Component) Component.Setup();
if (!Component || !Component.IsActive())  return;

먼저 위 코드를 통해 이전에 제작한 GrayScale VolumComponent를 참조합니다.

VolumeManager.instance.stack은 프로젝트에 있는 모든 VolumeComponent를 참조할 수 있습니다.

 

이슈

Execute함수에서 GetComponent를 호출하고 있는데 퍼포먼스 이슈가 우려됩니다.

해당 코드를 초기화부분에서 해봤지만, Postprocess Volume에서 추가 & 제거되는 것에 대한 업데이트 체크가 불가능한 이슈가 있습니다.

내장 코드와 다른 참고 자료를 찾아봤지만 다른 방법은 못찾았습니다.

 

 

 

 CommandBuffer commandBuffer = CommandBufferPool.Get(RenderTag);
RenderTargetIdentifier destination = _tempTexture.Identifier();

// 렌더 텍스처 생성
CameraData cameraData = renderingData.cameraData;
RenderTextureDescriptor descriptor = new RenderTextureDescriptor(cameraData.camera.scaledPixelWidth, cameraData.camera.scaledPixelHeight);
descriptor.colorFormat = cameraData.isHdrEnabled ? RenderTextureFormat.DefaultHDR : RenderTextureFormat.Default;
commandBuffer.GetTemporaryRT(_tempTexture.id, descriptor);

// 임시 버퍼 생성
commandBuffer.Blit(_source, destination);
// Pass 렌더링
Component.Render(commandBuffer, ref renderingData, destination, _source);
commandBuffer.ReleaseTemporaryRT(_tempTexture.id);

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

다음 작업으로는 커맨드버퍼 & 렌더 텍스처를 생성하고 VolumComponent에 접근해 Render함수를 호출합니다.

 

 

 

 

Render Feature 제작


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;


public class GrayScale_RenderFeature : ScriptableRendererFeature
{
    private GrayScale_RenderPass _renderPass = null;

    public override void Create()
    {
        _renderPass = new GrayScale_RenderPass("GrayscalePass", RenderPassEvent.AfterRenderingPostProcessing);
    }

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

마지막으로 RenderPass 스크립트를 제작합니다.

 

 

 

씬 세팅


RendererData에서 렌더피쳐를 추가하고 Intermediate Texture를 Always로 세팅하면 결과확인 가능합니다.

(Postprocess Volume에 Volume컴포넌트가 추가되어 있어야합니다.)

 

 

 

 

 

 

코드 확장성 개선


이제 커스텀포스트프로세싱 구현이 됬지만, 1개의 효과마다 3개의 스크립트 제작이 필요합니다.

그래서 이번엔 코드를 개선해서 1개의 효과당 1개의 스크립트 제작이 가능하도록 하겠습니다.

 

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public abstract class CustomVolumeComponent : VolumeComponent, IPostProcessComponent
{
    public BoolParameter IsEnable = new BoolParameter(false);
    
    public virtual bool IsTileCompatible() => false;
    public abstract bool IsActive();
    public abstract void Setup();
    public abstract void Destroy();
    public abstract void Render(CommandBuffer commandBuffer, ref RenderingData renderingData, RenderTargetIdentifier source, RenderTargetIdentifier destination);
    
    protected override void OnDestroy()
    {
        Destroy();
    }
}

먼저 VolumeComponent의 부모 클래스를 제작합니다.

렌더 패스 클래스에서 이 클래스를 참조하여 추가 렌더 패스를 제작할 필요 없게 하기 위해서 입니다.

 

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

[Serializable, VolumeComponentMenuForRenderPipeline("Custom Post-processing/Grayscale", typeof(UniversalRenderPipeline))]
public class GrayScale : CustomVolumeComponent
{
    private const string SHADER_NAME = "Hidden/Postprocess/Grayscale";
    private const string PROPERTY_AMOUNT = "_Amount";
    
    private Material _material;

    public BoolParameter IsEnable = new BoolParameter(false);
    public ClampedFloatParameter amount = new ClampedFloatParameter(0f, 0f, 1f);
    
    public override bool IsActive()
    {
        if (IsEnable.value == false) return false;
        if (!active || 
            !_material || 
            amount.value <= 0.0f) return false;
        return true;
    }

    public override void Setup()
    {
        if (!_material)
        {
            Shader shader = Shader.Find(SHADER_NAME);
            _material = CoreUtils.CreateEngineMaterial(shader);
        }
    }

    public override void Destroy()
    {
        if (_material)
        {
            CoreUtils.Destroy(_material);
            _material = null;
        }
    }

    public override void Render(CommandBuffer commandBuffer, ref RenderingData renderingData, RenderTargetIdentifier source, RenderTargetIdentifier destination)
    {
        if (!_material) return;
        
        _material.SetFloat(PROPERTY_AMOUNT, amount.value);
        
        commandBuffer.Blit(source, destination, _material);
    }
}

다음 기존에 제작했던 GrayScale코드를 위와 같이 수정합니다.

 

 

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;


public class CustomPostprocessRenderPass<T> : ScriptableRenderPass where T : CustomVolumeComponent
{
    protected const string TEMP_BUFFER_NAME = "_TempColorBuffer";
   
    protected T Component;
    protected string RenderTag { get; }
    
    private RenderTargetIdentifier _source;
    private RenderTargetHandle _tempTexture;
    
    public CustomPostprocessRenderPass(string renderTag, RenderPassEvent passEvent) 
    {
        renderPassEvent = passEvent;
        RenderTag = renderTag;
    }

    public virtual void Setup(in RenderTargetIdentifier source)
    {
        _source = source;
        _tempTexture.Init(TEMP_BUFFER_NAME);
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (!renderingData.cameraData.postProcessEnabled) return;

        VolumeStack volumeStack = VolumeManager.instance.stack;
        Component = volumeStack.GetComponent<T>();
        if(Component) Component.Setup();
        if (!Component || !Component.IsActive())  return;
        
        CommandBuffer commandBuffer = CommandBufferPool.Get(RenderTag);
        RenderTargetIdentifier destination = _tempTexture.Identifier();
        
        // 렌더 텍스처 생성
        CameraData cameraData = renderingData.cameraData;
        RenderTextureDescriptor descriptor = new RenderTextureDescriptor(cameraData.camera.scaledPixelWidth, cameraData.camera.scaledPixelHeight);
        descriptor.colorFormat = cameraData.isHdrEnabled ? RenderTextureFormat.DefaultHDR : RenderTextureFormat.Default;
        commandBuffer.GetTemporaryRT(_tempTexture.id, descriptor);
        
        // 임시 버퍼 생성
        commandBuffer.Blit(_source, destination);
        // Pass 렌더링
        Component.Render(commandBuffer, ref renderingData, destination, _source);
        commandBuffer.ReleaseTemporaryRT(_tempTexture.id);
        
        context.ExecuteCommandBuffer(commandBuffer);
        CommandBufferPool.Release(commandBuffer);
    }
}

다음 렌더패스 스크립트를 새로 생성합니다.

 

 

public class CustomPostprocessRenderPass<T> : ScriptableRenderPass where T : CustomVolumeComponent

...
protected T Component;
...
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
...
VolumeStack volumeStack = VolumeManager.instance.stack;
Component = volumeStack.GetComponent<T>();
if(Component) Component.Setup();
if (!Component || !Component.IsActive())  return;

 

렌더 패스 클래스를 제너릭 타입으로 모든 CustomVolumeComponent를 대응할 수 있게 하는것 이 핵심입니다.

 

제너릭을 사용하지 않고 일반 형변환을 사용하면 GetComponent로 참조되지 않습니다.

 

 

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;


public class CustomPostprocessRenderFeature : ScriptableRendererFeature
{
    private CustomPostprocessRenderPass<InverseGrayscale> _renderPass = null;
    private CustomPostprocessRenderPass<RadialBlur> _renderPass2 = null;
    
    public override void Create()
    {
        _renderPass = new CustomPostprocessRenderPass<InverseGrayscale>("InverseGrayscalePass", RenderPassEvent.AfterRenderingPostProcessing);
        _renderPass2 = new CustomPostprocessRenderPass<RadialBlur>("RadialBlur", RenderPassEvent.AfterRenderingPostProcessing);
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        _renderPass.Setup(renderer.cameraColorTarget);
        renderer.EnqueuePass(_renderPass);
        
        _renderPass2.Setup(renderer.cameraColorTarget);
        renderer.EnqueuePass(_renderPass2);
    }
}

마지막으로 렌더피쳐 클래스를 생성합니다.

렌더패스 클래스 생성시 제너릭타입을 넣어 추가 클래스 생성 필요없습니다.

 

 

이제 추가 스크립트 생성 없이 여러 효과를 겹칠 수 있게 되었습니다.

 

 

 

추가 개선 방향

현재 새로운 효과를 추가할때 신규 스크립트 생성을 필요없지만, 렌더피쳐 스크립트 코드를 수정해야하는 작업이 필요합니다.

추후 VolumeComponent를 자동으로 참조하는 방식으로 더 개선이 가능할 것 같습니다.

 

 

 

 

참고자료

https://developpaper.com/unity-post-processing-for-better-use-expanding-urp-post-processing-pit-record/

https://github.com/togucchi/urp-postprocessing-examples

https://blog.csdn.net/zakerhero/article/details/108171533

 

 

 

 


WRITTEN BY
CatDarkGame
Technical Artist dhwlgn12@gmail.com

,