개요


https://darkcatgame.tistory.com/121

 

URP 커스텀 포스트프로세싱 - 1

개요 URP 환경에서는 빌트인에서 사용하던 OnRenderImage()가 작동하지 않아 기존 처럼 카메라에 포스트 프로세스 컴포넌트를 추가하는 방식이 안됩니다. URP에서는 별도 렌더피쳐를 만들고 Post-process

darkcatgame.tistory.com

이전 포스팅에서 URP환경에서 RenderFeature기반으로 포스트프로세싱 제작 방법에 대해 작성했습니다.

하지만 아래와 같은 이슈와 개선 필요점이 있었습니다.

 

   1. PostProcess Volume과 On/Off옵션이 기본 PostProcess와 다르게 동작하는 버그.

   2. 새로운 효과를 추가할때마다 RenderFeather에 새로운 RenderPass 추가해야함.

 

이번 포스팅은 이슈에 대한 자세한 원인과 해결 방법에 대해 작성하겠습니다.

 

 

 

프리뷰


 

 

 

플 프로젝트

Unity 2021.2.4f1 URP에서 구현 V2

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

 

Release URP_CustomPostProcess V2 · CatDarkGame/URP_CustomPostProcess

 

github.com

 

 

 

 

 

 

 

PostProcess OnOff 기능 지원


이슈 상황

위 예시처럼 RendererData나 Camera오브젝트의 PostProcess Enable옵션을 끄고키는 기능이 버그가 있고

유니티 기본 PostProcess와 동일하지 않게 동작하는 상황입니다.

 

또한 Volume오브젝트의 Local모드도 SceneView에서 동작하지 않은 이슈도 있습니다.

 

 

원인

현재는 RenderFeature의 AddRenderPasses에서 참조한 renderingData에서 위 변수를 체크하고 있습니다.

하지만 해당 코드는 현재 함수를 호출한 카메라 오브젝트를 참조하는 것이며, GameView외에 다른 뷰에서는 동작하지 않습니다.

 

 

해결 방법

위 코드 외에 추가로 RendererData에 접근해서 Enable상태를 체크하는 코드가 필요합니다.

 

public static class RendererUtill
{
    // 렌더파이프라인 에셋 참조
    public static RenderPipelineAsset GetRenderPipelineAsset()
    {
        return GraphicsSettings.renderPipelineAsset;
    }

    // 렌더데이터 에셋 리스트 참조
    public static ScriptableRendererData[] GetScriptableRendererDatas()
    {
        RenderPipelineAsset pipelineAsset = GetRenderPipelineAsset();
        if (!pipelineAsset) return null;
        
        FieldInfo propertyInfo = pipelineAsset.GetType().GetField("m_RendererDataList", BindingFlags.Instance | BindingFlags.NonPublic);
        ScriptableRendererData[] scriptableRendererDatas = propertyInfo.GetValue(pipelineAsset) as ScriptableRendererData[];
        return scriptableRendererDatas;
    }

    // Universal렌더 데이터 참조(기본 세팅)
    public static UniversalRendererData GetUniversalRendererData(int rendererListIndex = 0)
    {
        ScriptableRendererData[] scriptableRendererDatas = GetScriptableRendererDatas();
        if (scriptableRendererDatas == null || scriptableRendererDatas.Length <= 0) return null;

        UniversalRendererData universalRendererData =
            scriptableRendererDatas[rendererListIndex] as UniversalRendererData;
        return universalRendererData;
    }
}

우선 렌더파이프라인과 렌더 데이터 에셋을 참조할 수 있는 함수를 만들었습니다.

이 기능은 다양한 곳에서도 사용할 가능성이 높다고 판단하여 static클래스 함수로 제작했습니다.

 

 

   // CustomPostprocessRenderFeature.cs
   
   public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        if(!_universalRendererData) _universalRendererData = RendererUtill.GetUniversalRendererData();
        if(!IsPostProcessEnabled(_universalRendererData, ref renderingData)) return;
       
        _renderPass.Setup(renderer.cameraColorTarget);
        renderer.EnqueuePass(_renderPass);
    }

    // Postprocess 옵션 활성화 체크
    private bool IsPostProcessEnabled(UniversalRendererData universalRendererData, ref RenderingData renderingData)
    {
        // 카메라 오브젝트의 Post Processing 활성화 체크
        if (!renderingData.cameraData.postProcessEnabled) return false;
        // RendererData 에셋의 Post-processing 활성화 체크
        if(!_universalRendererData ||
           !_universalRendererData.postProcessData) return false;

        return true;
    }

다음 RenderFeature클래스의 AddRenderPasses함수 앞에 PostProcess 활성화 체크하는 코드를 작성했습니다.

 

 

 

결과

On/OFF옵션이 기본 PostProcessing과 동일하게 동작하게 만들었습니다.

 

또한 Volume오브젝트의 Local모드도 정상적으로 작동합니다.

 

 

 

이 이슈를 해결하면서 참고한 UniversalRenderer.cs 코드에는 카메라 타입 검사 등 다른 예외 처리 코드가 있지만 당장은 필요 없어보입니다.

 

 

 

 

 

 

 

1개의 RenderPass에서 모두 처리하기 (렌더 피쳐 코드 최적화 & 확장성 개선)


이슈 상황

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);
    }
}

기존에는 새로운 효과를 추가하기 위해서 RenderFeature클래스에 새로운 RenderPass 객체를 생성하는 코드를 추가해야 했습니다.

따라서 RenderFeature클래스를 매번 수정해야하는 휴먼에러 위험성이 있고 Volume Component가 존재하지 않아도 Pass를 호출하는 문제가 있습니다.

 

 

개선 방향

기존

- RenderFeature에서 제너릭 타입으로 RenderPass객체 생성.

- 각 RenderPass에서 제너릭 타입에 맞는 CustomVolumeComponent를 참조해 렌더링.

 

개선

- RenderFeature의 Create()함수에서 현재 활성화된 모든 CustomVolumeComponent를 참조해 리스트 저장.

- RenderFeature에서 1개의 RenderPass를 생성.

- RenderPass에서 CustomVolumeComponent리스트에 접근해서 렌더링 호출 반복.

 

 

 

1. 모든 CustomVolumeComponent 참조 & 리스트 저장

// CustomPostprocessRenderFeature.cs

public bool InitVolumeComponents(ref List<CustomVolumeComponent> refComponents)
{
    List<CustomVolumeComponent> components = new List<CustomVolumeComponent>();
    VolumeStack stack = VolumeManager.instance.stack;

    // 프로젝트 내의 모든 VolumeComponent 클래스 참조
    System.Type[] types = VolumeManager.instance.baseComponentTypeArray;

    for (int i = 0; i < types.Length; i++)
    {
        System.Type type = types[i];

        // 타입 검사
        if(!type.IsSubclassOf(typeof(CustomVolumeComponent))) continue;

        // VolumeStack에서 CustomVolumeComponent 참조
        CustomVolumeComponent component = stack.GetComponent(type) as CustomVolumeComponent;
        if(!component) continue;
        component.Setup();
        components.Add(component);
    }
    
  // CustomVolumeComponent참조하지 못했을때 리턴
    if (components.Count <= 0) return false;

    refComponents = components;
    return true;
}

위 함수는 프로젝트의 모든 CustomVolumeComponent를 리스트에 추가하는 기능을 합니다.

 

System.Type[] types = VolumeManager.instance.baseComponentTypeArray;
        
for (int i = 0; i < types.Length; i++)
{
    System.Type type = types[i];

    // 타입 검사
    if(!type.IsSubclassOf(typeof(CustomVolumeComponent))) continue;

    // VolumeStack에서 CustomVolumeComponent 참조
    CustomVolumeComponent component = stack.GetComponent(type) as CustomVolumeComponent;
    if(!component) continue;
    component.Setup();
    components.Add(component);
}

먼저 VolumeManager.instance.baseComponentTypeArray; 코드로 프로젝트 내의 모든 VolumeComponent 배열을 참조합니다.

 

다음 타입 검사를 통해 현재 volume.stack에 CustomVolumeComponent 객체만 참조합니다.

타입을 먼저 참조하는 이유는 stack.GetComponent할때 캐스팅 형변환하면 참조되지 않기 때문입니다.

 

 

// CustomVolumeComponent참조하지 못했을때 리턴
if (components.Count <= 0) return false;

refComponents = components;

다음 참조하지 못했을때 처리입니다.

 

게임이 종료될때 RenderFeature의 Create()함수가 호출되는데 이때 volumeComponent가 참조되지 않기 때문에 

참조 갯수가 0일때 리스트를 갱신하지 않게 했습니다.

 

 

 public void ClearVolumeComponents()
{
    if (_customVolumeComponents == null) return;
    for (int i = 0; i < _customVolumeComponents.Count; i++)
    {
        _customVolumeComponents[i].Destroy();
    }
}

소멸 함수입니다.

현재 참조된 customvolumeComponent들의 소멸 함수를 호출합니다.

 

 

// CustomPostprocessRenderFeature.cs

private List<CustomVolumeComponent> _customVolumeComponents;


public override void Create()
{
    InitVolumeComponents(ref _customVolumeComponents);

	 _renderPass = new CustomPostprocessRenderPass(_settings.renderTag, _settings.passEvent, _customVolumeComponents);
    ...
}

protected override void Dispose(bool disposing)
{
    if (!disposing) return;
    ClearVolumeComponents();
}

다음 RenderFeature클래스에서 함수 활용입니다.

 

Create() 함수는 RenderFeature클래스 생성시 호출됩니다.

customVolumeComponent 리스트를 참조하고 RenderPass에 전달합니다.

 

Disose()함수는 RenderFeature클래스 소멸시 호출됩니다.

 

 

 

2. RenderPass에서 활성화된 customVolumeComponent 체크

public class CustomPostprocessRenderPass : ScriptableRenderPass
{
	private List<CustomVolumeComponent> _customVolumeComponents;
    private List<CustomVolumeComponent> _activeCustomVolumeComponents;
 ...

	public CustomPostprocessRenderPass(string renderTag, RenderPassEvent passEvent, List<CustomVolumeComponent> customVolumeComponents)
    {
        _activeCustomVolumeComponents = new List<CustomVolumeComponent>();
        _customVolumeComponents = customVolumeComponents;
    }

	public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        // 렌더링 가능한 상태인지 체크
        if (!renderingData.cameraData.postProcessEnabled ||
            _customVolumeComponents==null ||
            _customVolumeComponents.Count<=0) return;
        
        // 활성화된 customVolumeComponent 리스트 참조
        int stackCount = 0;
        bool isDoubleBuffering = false;
        _activeCustomVolumeComponents.Clear();
        VolumeStack volumeStack = VolumeManager.instance.stack;
        for (int i = 0; i < _customVolumeComponents.Count; i++)
        {
            CustomVolumeComponent component = volumeStack.GetComponent(_customVolumeComponents[i].GetType()) as CustomVolumeComponent;
            if(component) component.Setup();
            if(!component || !component.IsActive()) continue;
            _activeCustomVolumeComponents.Add(component);
            stackCount++;
        }
        if (stackCount == 0) return;
        if (stackCount > 1) isDoubleBuffering = true;
        
        ...

 

 

이전 RenderFeature에서 참조한 모든 customVolumeComponent를 RenderPass의 Execute함수에서 한번더 참조 & 검사하여 활성화된 리스트만 따로 저장합니다.

 

매번 렌더링마다 stack.getcomponent하는 이유는 각 카메라에 적용된 postprocessVolume을 계속 갱신해야하기 때문입니다.

여기서 설명한 카메라는 카메라 오브젝트 뿐만아니라 각 View 카메라도 의미합니다.

 

마지막으로 여러 customvolumeComponent를 대응하기 위해 stackCount를 저장합니다.

 

 

RenderTargetIdentifier tempBuffer1 = _tempTexture_1.id;
RenderTargetIdentifier tempBuffer2;

if (!isDoubleBuffering && stackCount==1)
{
    // CustomVolumeComponent 1개 렌더링
    _activeCustomVolumeComponents[0].Render(commandBuffer, ref renderingData, _source.Identifier(), tempBuffer1);
}
else
{
    // CustomVolumeComponent 2개 이상 렌더링
    // 2번째 임시 렌더버퍼 생성 후 더블 버퍼링 방식으로 Volume을 교차하며 렌더링
    commandBuffer.GetTemporaryRT(_tempTexture_2.id, descriptor);
    tempBuffer2 = _tempTexture_2.id;
    commandBuffer.Blit(_source.Identifier(), tempBuffer1);

    for (int i = 0; i < stackCount; i++)
    {
        CustomVolumeComponent component = _activeCustomVolumeComponents[i];
        if (!component) continue;
        component.Render(commandBuffer, ref renderingData, tempBuffer1, tempBuffer2);
        CoreUtils.Swap(ref tempBuffer1, ref tempBuffer2);
    }
}

commandBuffer.Blit(tempBuffer1, _source.Identifier());

commandBuffer.ReleaseTemporaryRT(_tempTexture_1.id);
if (isDoubleBuffering) commandBuffer.ReleaseTemporaryRT(_tempTexture_2.id);
context.ExecuteCommandBuffer(commandBuffer);
CommandBufferPool.Release(commandBuffer);

다음은 렌더링 코드입니다.

2개 이상의 volumeComponent를 렌더링할때 렌더 텍스처를 겹치기 위해 더블 버퍼링 방식으로 임시 렌더 버퍼를 교체하면서 반복문을 돌립니다.

 

 

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

public class CustomPostprocessRenderPass : ScriptableRenderPass
{
    private const string TEMP_BUFFER_NAME_1 = "_TempColorBuffer_1";
    private const string TEMP_BUFFER_NAME_2 = "_TempColorBuffer_2";
    private string RenderTag { get; }
    
    private List<CustomVolumeComponent> _customVolumeComponents;
    private List<CustomVolumeComponent> _activeCustomVolumeComponents;
    
    private RenderTargetHandle _source;
    private RenderTargetHandle _tempTexture_1;
    private RenderTargetHandle _tempTexture_2;
    
    public CustomPostprocessRenderPass(string renderTag, RenderPassEvent passEvent, List<CustomVolumeComponent> customVolumeComponents)
    {
        _activeCustomVolumeComponents = new List<CustomVolumeComponent>();
        
        RenderTag = renderTag;
        renderPassEvent = passEvent;
        _customVolumeComponents = customVolumeComponents;
    }

    public virtual void Setup(in RenderTargetIdentifier source)
    {
        _source = new RenderTargetHandle(source);
        _tempTexture_1.Init(TEMP_BUFFER_NAME_1);
        _tempTexture_2.Init(TEMP_BUFFER_NAME_2);
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        // 렌더링 가능한 상태인지 체크
        if (!renderingData.cameraData.postProcessEnabled ||
            _customVolumeComponents==null ||
            _customVolumeComponents.Count<=0) return;
        
        // 활성화된 customVolumeComponent 리스트 참조
        int stackCount = 0;
        bool isDoubleBuffering = false;
        _activeCustomVolumeComponents.Clear();
        VolumeStack volumeStack = VolumeManager.instance.stack;
        for (int i = 0; i < _customVolumeComponents.Count; i++)
        {
            CustomVolumeComponent component = volumeStack.GetComponent(_customVolumeComponents[i].GetType()) as CustomVolumeComponent;
            if(component) component.Setup();
            if(!component || !component.IsActive()) continue;
            _activeCustomVolumeComponents.Add(component);
            stackCount++;
        }
        if (stackCount == 0) return;
        if (stackCount > 1) isDoubleBuffering = true;
        
        
        CommandBuffer commandBuffer = CommandBufferPool.Get(RenderTag);
        context.ExecuteCommandBuffer(commandBuffer);
        commandBuffer.Clear();

        // 렌더 텍스처 생성
        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_1.id, descriptor);
        // 카메라 텍스처가 없을때 빈 RT 생성
        if (_source != RenderTargetHandle.CameraTarget && !_source.HasInternalRenderTargetId())
        {
            commandBuffer.GetTemporaryRT(_source.id, descriptor);
        }
        
        RenderTargetIdentifier tempBuffer1 = _tempTexture_1.id;
        RenderTargetIdentifier tempBuffer2;
        
        if (!isDoubleBuffering && stackCount==1)
        {
            // CustomVolumeComponent 1개 렌더링
            _activeCustomVolumeComponents[0].Render(commandBuffer, ref renderingData, _source.Identifier(), tempBuffer1);
        }
        else
        {
            // CustomVolumeComponent 2개 이상 렌더링
            // 2번째 임시 렌더버퍼 생성 후 더블 버퍼링 방식으로 Volume을 교차하며 렌더링
            commandBuffer.GetTemporaryRT(_tempTexture_2.id, descriptor);
            tempBuffer2 = _tempTexture_2.id;
            commandBuffer.Blit(_source.Identifier(), tempBuffer1);

            for (int i = 0; i < stackCount; i++)
            {
                CustomVolumeComponent component = _activeCustomVolumeComponents[i];
                if (!component) continue;
                component.Render(commandBuffer, ref renderingData, tempBuffer1, tempBuffer2);
                CoreUtils.Swap(ref tempBuffer1, ref tempBuffer2);
            }
        }
        
        commandBuffer.Blit(tempBuffer1, _source.Identifier());
        
        commandBuffer.ReleaseTemporaryRT(_tempTexture_1.id);
        if (isDoubleBuffering) commandBuffer.ReleaseTemporaryRT(_tempTexture_2.id);
        context.ExecuteCommandBuffer(commandBuffer);
        CommandBufferPool.Release(commandBuffer);
    }
}

RenderPass전체 코드

 

 

 

 

 

참고 자료

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

 

 

 

 

 

 

 


WRITTEN BY
CatDarkGame
Technical Artist dhwlgn12@gmail.com

,