【Unity】【URP】Radial Blur(放射状ブラー)のポストエフェクトを実装する(公式未対応バージョン)

UnityのURPでRadial Blur(放射状ブラー)のポストエフェクトを実装する方法についてまとめました。

Unity2020.3.22f1

はじめに

この記事ではUnityのURPでRadial Blur(放射状ブラー)のポストエフェクトを実装する方法について紹介します。

執筆時点ではURPのカスタムポストエフェクトの公式実装がされていない状況なので、
以下の記事のように自身でRendererFeatureを追加する方法で実装を行います。

light11.hatenadiary.com

本記事は上記の記事を前提知識として書きますので、不明点があったらこちらを参照してください。

なおRadial Blurについては以下の記事を参照してください。

light11.hatenadiary.com

シェーダを書く

それではまずシェーダを書いていきます。

Shader "Hidden/Radial Blur"
{
    SubShader
    {
        Pass
        {
            HLSLPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            TEXTURE2D(_MainTex);
            SAMPLER(sampler_MainTex);
            half _SampleCount;
            half _Strength;

            struct Attributes
            {
                float4 positionOS : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct Varyings
            {
                float4 positionHCS : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            Varyings vert(Attributes IN)
            {
                Varyings OUT;
                OUT.positionHCS = TransformObjectToHClip(IN.positionOS.xyz);
                OUT.uv = IN.uv;
                return OUT;
            }

            half4 frag(Varyings IN) : SV_Target
            {
                half4 color = 0;
                half2 symmetryUv = IN.uv - 0.5;
                half distance = length(symmetryUv);
                half factor = _Strength / _SampleCount * distance;
                for (int i = 0; i < _SampleCount; i++)
                {
                    half uvOffset = 1 - factor * i;
                    color += SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, symmetryUv * uvOffset + 0.5);
                }
                color /= _SampleCount;
                return color;
            }
            ENDHLSL
        }
    }
}

以下の記事のシェーダをURP用に書き直しただけです。

light11.hatenadiary.com

ビルトインパイプラインとURP用のシェーダの書き方の違いについては以下の記事を参照してください。

light11.hatenadiary.com

Volumeスクリプト作成する

次にVolumeスクリプトを作成します。

using System;
using UnityEngine.Rendering;

[Serializable]
[VolumeComponentMenu("Radial Blur")]
public class RadialBlurVolume : VolumeComponent
{
    public bool IsActive() => strength.value != 0;
    
    public ClampedIntParameter sampleCount = new ClampedIntParameter(8, 4, 16);
    public FloatParameter strength = new ClampedFloatParameter(0.0f, 0.0f, 1.0f);
}

ScriptableRenderPassを作成する

次にScriptableRenderPassを作成します。

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

public enum PostprocessTiming
{
    BeforePostprocess,
    AfterPostprocess
}

public class RadialBlurRenderPass : ScriptableRenderPass
{
    private const string RenderPassName = nameof(RadialBlurRenderPass);
    private const string ProfilingSamplerName = "SrcToDest";

    private readonly bool _applyToSceneView;
    private readonly int _mainTexPropertyId = Shader.PropertyToID("_MainTex");
    private readonly Material _material;
    private readonly ProfilingSampler _profilingSampler;
    private readonly int _sampleCountPropertyId = Shader.PropertyToID("_SampleCount");
    private readonly int _strengthPropertyId = Shader.PropertyToID("_Strength");

    private RenderTargetHandle _afterPostProcessTexture;
    private RenderTargetIdentifier _cameraColorTarget;
    private RenderTargetHandle _tempRenderTargetHandle;
    private RadialBlurVolume _volume;

    public RadialBlurRenderPass(bool applyToSceneView, Shader shader)
    {
        if (shader == null)
        {
            return;
        }

        _applyToSceneView = applyToSceneView;
        _profilingSampler = new ProfilingSampler(ProfilingSamplerName);
        _tempRenderTargetHandle.Init("_TempRT");
        _material = CoreUtils.CreateEngineMaterial(shader);
        _afterPostProcessTexture.Init("_AfterPostProcessTexture");
    }

    public void Setup(RenderTargetIdentifier cameraColorTarget, PostprocessTiming timing)
    {
        _cameraColorTarget = cameraColorTarget;
        renderPassEvent = GetRenderPassEvent(timing);
        var volumeStack = VolumeManager.instance.stack;
        _volume = volumeStack.GetComponent<RadialBlurVolume>();
    }

    public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
    {
        if (_material == null)
        {
            return;
        }

        if (!renderingData.cameraData.postProcessEnabled)
        {
            return;
        }

        if (!_applyToSceneView && renderingData.cameraData.cameraType == CameraType.SceneView)
        {
            return;
        }

        if (!_volume.IsActive())
        {
            return;
        }
        
        var source = renderPassEvent == RenderPassEvent.AfterRendering && renderingData.cameraData.resolveFinalTarget
            ? _afterPostProcessTexture.Identifier()
            : _cameraColorTarget;

        var cmd = CommandBufferPool.Get(RenderPassName);
        cmd.Clear();
        var tempTargetDescriptor = renderingData.cameraData.cameraTargetDescriptor;
        tempTargetDescriptor.depthBufferBits = 0;
        cmd.GetTemporaryRT(_tempRenderTargetHandle.id, tempTargetDescriptor);

        using (new ProfilingScope(cmd, _profilingSampler))
        {
            _material.SetInt(_sampleCountPropertyId, _volume.sampleCount.value);
            _material.SetFloat(_strengthPropertyId, _volume.strength.value);
            cmd.SetGlobalTexture(_mainTexPropertyId, source);
            Blit(cmd, source, _tempRenderTargetHandle.Identifier(), _material);
        }

        Blit(cmd, _tempRenderTargetHandle.Identifier(), source);

        cmd.ReleaseTemporaryRT(_tempRenderTargetHandle.id);

        context.ExecuteCommandBuffer(cmd);
        CommandBufferPool.Release(cmd);
    }

    private static RenderPassEvent GetRenderPassEvent(PostprocessTiming postprocessTiming)
    {
        switch (postprocessTiming)
        {
            case PostprocessTiming.BeforePostprocess:
                return RenderPassEvent.BeforeRenderingPostProcessing;
            case PostprocessTiming.AfterPostprocess:
                return RenderPassEvent.AfterRendering;
            default:
                throw new ArgumentOutOfRangeException(nameof(postprocessTiming), postprocessTiming, null);
        }
    }
}

Radial Blurは他のポストエフェクトよりも先にかけるか後にかけるかで見た目が変わってくるので、
適用タイミングは選択できるようにしてあります。

ScriptableRendererFeatureを作成する

最後にScriptableRendererFeatureを作成します。

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

[Serializable]
public class RadialBlurRenderFeature : ScriptableRendererFeature
{
    [SerializeField] private PostprocessTiming _timing = PostprocessTiming.AfterPostprocess;
    [SerializeField] private bool _applyToSceneView = true;

    private RadialBlurRenderPass _postProcessPass;
    private Shader _shader;

    public override void Create()
    {
        _shader = Shader.Find("Hidden/Radial Blur");
        _postProcessPass = new RadialBlurRenderPass(_applyToSceneView, _shader);
    }

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

シーンに適用する

あとはこれをシーンに適用するだけです。
適用方法は以下の記事の「使う」節を参照してください。

light11.hatenadiary.com

適用結果は以下の通りとなります。

f:id:halya_11:20211226184921p:plain
適用結果

正常にRadial Blurが掛かっていることが確認できました。

関連

light11.hatenadiary.com

light11.hatenadiary.com

light11.hatenadiary.com