スクリーン後処理効果 - シェーダー入門

スクリーン後処理効果

1 基本的なスクリーン後処理スクリプトシステムの構築

スクリーン後処理効果とは、通常、シーン全体のレンダリングが完了した後に得られるスクリーン画像に対して一連の操作を行い、各種スクリーン特殊効果を実現することを指します。

Unityはスクリーンキャプチャのための便利なインターフェースとしてOnRenderImage関数を提供しています。その宣言は以下の通りです。

MonoBehaviour.OnRenderImage(RenderTexture src, RenderTexture dest)

OnRenderImageはレンダリングパイプラインの最終段階であり、このメソッドを呼び出さない場合、レンダリングされた画像は直接スクリーンに表示されます。このメソッドを呼び出すと、

  • src:取得されたレンダリング画像
  • dest:描画対象のテクスチャ、つまり最終的にスクリーンに表示されるテクスチャ

となります。

注意:このメソッドを呼び出すスクリプトは、Cameraコンポーネントがアタッチされたオブジェクトにアタッチする必要があります。

OnRenderImage内では通常、Graphics.Blit関数を使用してテクスチャレンダリング処理を完了します。この関数は1枚のテクスチャを別のテクスチャに描画することができ、特殊な効果を実現するために特定のマテリアル(マテリアルにはShaderがアタッチされています)を指定することができます。3つの宣言があります。

public static void Blit(Texture src, RenderTexture dest);
public static void Blit(Texture src, RenderTexture dest, Material mat, int pass = -1);
public static void Blit(Texture src, RenderTexture dest, int pass = -1);
  • src:ソーステクスチャ。このパラメータは通常、現在のスクリーンのレンダリングテクスチャまたは前の処理ステップで得られたレンダリングテクスチャであり、Shader内で**_MainTex**という名前のテクスチャプロパティに渡されます。
  • dest:対象レンダリングテクスチャ。その値がnullの場合、結果は直接スクリーンに表示されます。
  • mat:使用するマテリアル。このマテリアルが使用するUnity Shaderが各種スクリーン後処理操作を実行します。
  • pass:デフォルト値は-1で、Shader内のすべてのPassが順次呼び出されることを意味します。それ以外の場合は、指定されたインデックスのPassのみが呼び出されます。

デフォルトでは、OnRenderImage関数は不透明物体と透明物体の両方が完了した直後に呼び出され、シーン内のすべてのゲームオブジェクトに影響を与えます。しかし、時には不透明なPassが完了した直後にOnRenderImage関数を呼び出し、透明な物体には何の影響も与えたくない場合があります。このような目的のために、OnRenderImage関数の前にImageEffectOpaque属性を追加することで実現できます。

Unityでのスクリーン後処理の実装プロセスは通常、以下の通りです。

  1. カメラにスクリーン後処理用のスクリプトを追加し、OnRenderImage関数を実装して現在のスクリーンのレンダリングテクスチャを取得します。
  2. 特定のUnity Shaderを使用してGraphics.Blit関数を呼び出し、現在の画像を処理し、その後のレンダリングテクスチャをスクリーンに表示します。複雑なスクリーン特殊効果の場合、Graphics.Blit関数を複数回呼び出して、前のステップの出力結果を次の処理に渡す必要があるかもしれません。

スクリーン後処理を行う前に、現在のプラットフォームがレンダリングテクスチャとスクリーン特殊効果をサポートしているか、現在使用しているUnity Shaderがサポートされているかなど、一連の条件が満たされているかを確認する必要があります。この目的のために、スクリーン後処理効果用の基底クラスを作成しました。各種スクリーン特殊効果を実装する際には、この基底クラスを継承し、派生クラスで異なる操作を実装するだけで済みます。

PostEffectsBase.csの主なコードは以下の通りです。

using UnityEngine;

[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]  // まず、すべてのスクリーン後処理効果は特定のカメラにバインドする必要があり、エディターモードでも効果を確認するためにスクリプトを実行したい
public class PostEffectsBase : MonoBehaviour
{
    private void CheckResources()
    {
        bool isSupported = CheckSupported();

        if (isSupported == false)
        {
            NotSupported();
        }
    }
    
    /// <summary>
    /// リソースと条件が満たされているか事前に検測するために、Start関数内でCheckResources関数を呼び出します
    /// </summary>
    protected bool CheckSupported()
    {
        if (SystemInfo.supportsImageEffects == false || SystemInfo.supportsRenderTextures == false)
        {
            Debug.LogWarning("このプラットフォームはイメージエフェクトまたはレンダリングテクスチャをサポートしていません");
            return false;
        }

        return true;
    }

    protected void NotSupported()
    {
    }
    
    /// <summary>
    /// 特定のShaderを指定して、レンダリングテクスチャ処理用のマテリアルを作成します
    /// </summary>
    /// <param name="shader">この特殊効果で使用するShaderを指定</param>
    /// <param name="material">後処理に使用するマテリアル</param>
    /// <returns></returns>
    protected Material CheckShaderAndCreateMaterial(Shader shader, Material material)
    {
        if (shader == null)
        {
            return null;
        }
        if (shader.isSupported && material && material.shader == shader)
        {
            return material;
        }
        if (shader.isSupported == false)
        {
            return null;
        }
        else
        {
            material = new Material(shader);
            material.hideFlags = HideFlags.DontSave;
            if (material) return material;
            else return null;
        }
    }
    
    
    #region Unity CallBack

    void Start()
    {
        CheckResources();
    }
    
    void Update()
    {
        
    }
    
    #endregion
}

2 スクリーンの明るさ、彩度、コントラストの調整

BrightnessSaturationAndContrast.csという新しいスクリプトを作成し、以下のコードを追加します。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public class BrightnessSaturationAndContrast : PostEffectsBase
{
    public Shader briSatConShader;
    private Material _briSatConMaterial;
    
    public Material material
    {
        get
        {
            // briSatConShaderは指定したShaderで、後で実装するBrightnessSaturationAndContrast.shaderに対応します。
            // _briSatConMaterialは作成したマテリアルで、Materialという名前のプロパティを通じてアクセスします
            _briSatConMaterial = CheckShaderAndCreateMaterial(briSatConShader, _briSatConMaterial);
            return _briSatConMaterial;
        }
    }

    [Range(0.0f, 3.0f)] public float brightness = 1.0f;
    [Range(0.0f, 3.0f)] public float saturation = 1.0f;
    [Range(0.0f, 3.0f)] public float contrast = 1.0f;
    
    /// <summary>
    /// 呼び出されたときにマテリアルが使用可能かチェックし、使用可能であればパラメータをマテリアルに渡し、Graphics.Blitで処理を行います。
    /// 使用不可能であれば、元の画像を直接スクリーンに表示し、何の処理も行いません
    /// </summary>
    /// <param name="src"></param>
    /// <param name="dest"></param>
    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (material != null)
        {
            material.SetFloat("_Brightness", brightness);
            material.SetFloat("_Saturation", saturation);
            material.SetFloat("_Contrast", contrast);
            
            Graphics.Blit(src, dest, material);
        }
        else
        {
            Graphics.Blit(src, dest);
        }
    }
}

このスクリプトをカメラにアタッチし、以下のような新しいShaderを作成します。

Shader "Custom/PostProcessing/BrightnessSaturationAndContrast"
{
    Properties
    {
        _MainTex ("Base(RGB)", 2D) = "white" {}
        _Brightness ("Brightness", Float) = 1
        _Saturation ("Saturation", Float) = 1
        _Contrast ("Contrast", Float) = 1
    }
    SubShader
    {
        Pass
        {
            ZTest Always Cull Off ZWrite Off
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

            sampler2D _MainTex;
            half _Brightness;
            half _Saturation;
            half _Contrast;

            struct v2f
            {
                float4 pos : SV_POSITION;
                half2 uv : TEXCOORD0;
            };

            // 頂点変換を行い、フラグメントシェーダーに正しいテクスチャ座標を渡します
            // UnityCG.cgincで宣言されているappdata_img構造体を頂点シェーダーの入力として使用できます。これは画像処理に必要な頂点座標とテクスチャ座標などの変数のみを含んでいます
            v2f vert (appdata_img v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.texcoord;
                return  o;
            }

            // 明るさ調整を実装
            fixed4 frag (v2f i) : SV_Target
            {
                // 元のスクリーンをサンプリング
                fixed4 renderTex = tex2D(_MainTex, i.uv);

                // 明るさを適用:元の色に明るさ係数を掛けることで明るさを得ます
                fixed3 finalColor = renderTex.rgb * _Brightness;

                // 彩度を適用:ピクセルに対応する輝度値luminanceを計算し、その輝度値を使用して彩度が0の色値を作成し、_Saturationを使用して補間して、希望する彩度の色を得ます
                fixed luminance = 0.2125 * renderTex.r + 0.7154 * renderTex.g + 0.0721 * renderTex.b;
                fixed3 luminaceColor = fixed3(luminance, luminance, luminance);
                finalColor = lerp(luminaceColor, finalColor, _Saturation);

                // コントラストを適用:コントラストが0の色値(各成分が0.5)を作成し、_Contrastを使用して補間します
                fixed3 avgColor = fixed3(0.5, 0.5, 0.5);
                finalColor = lerp(avgColor, finalColor, _Contrast);

                return  fixed4(finalColor, renderTex.a);
            }
            ENDCG
        }
    }
    Fallback Off
}

このShaderをカメラのBrightnessSaturationAndContrastコンポーネントのbriSatConShaderにドラッグし、brightness、saturation、contrastの値を調整すると効果が見られます。

3 エッジ検出

エッジ検出はアウトライン効果の一つの実現方法で、いくつかのエッジ検出演算子を使用して画像に**畳み込み(convolution)**操作を行います。

3.1 畳み込み

畳み込み操作とは、畳み込みカーネルを使用して画像内の各ピクセルに対して一連の操作を行うことを指します。畳み込みカーネルは通常、四角形のグリッド構造(例えば2×2、3×3、5×5の正方形領域)で、その領域内の各マスには重み値があります。画像内の特定のピクセルに対して畳み込みを行う際には、そのピクセルの上に畳み込みカーネルの中心を配置し、カーネルを反転した後、カーネル内の各要素がカバーする画像ピクセル値との積を順次計算し合計を求め、その結果がその位置の新しいピクセル値となります。

このような計算は単純ですが、画像ぼかし、エッジ検出など、多くの一般的な画像処理効果を実現できます。例えば、画像に平均ぼかしを適用するには、3×3の畳み込みカーネルを使用し、カーネル内の各要素の値を1/9に設定します。

3.2 一般的なエッジ検出演算子

畳み込み操作の鍵は畳み込みカーネルにあります。エッジ検出に使用する畳み込みカーネルはどのようなものであるべきでしょうか。

隣接するピクセル間に色、輝度、テクスチャなどの属性に明らかな差がある場合、それらの間に境界があると考えます。隣接ピクセル間の差値は**勾配(gradient)**で表すことができます。エッジ部分では勾配の絶対値が比較的大きくなります。このような理解に基づき、いくつかの異なるエッジ検出演算子が提案されてきました。

一般的なエッジ検出演算子は上記の通りで、これらは水平方向と垂直方向のエッジ情報を検出するための2つの方向の畳み込みカーネルを含んでいます。エッジ検出を行う際には、各ピクセルに対してそれぞれ1回ずつ畳み込み計算を行い、2つの方向の勾配値GxとGyを得ます。全体の勾配は以下の式で計算できます。

G = √(Gx² + Gy²)

上記の計算には平方根操作が含まれるため、パフォーマンスの考慮から、時には絶対値操作で平方根操作を置き換えることがあります。

G = |Gx| + |Gy|

勾配Gが得られた後、それに基づいてどのピクセルがエッジに対応しているか判断できます(勾配値が大きいほど、エッジである可能性が高い)。

3.3 実装

Sobel演算子を使用してエッジ検出を実装し、アウトライン効果を実現します。

EdgeDetection.csという新しいスクリプトを作成し、以下のコードを追加します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;

public class EdgeDetection : PostEffectsBase
{
    public Shader edgeDetectShader;
    private Material _edgeDetectMaterial;
    
    public Material material
    {
        get
        {
            // edgeDetectShaderは指定したShaderで、後で実装するEdgeDetection.shaderに対応します。
            // _edgeDetectMaterialは作成したマテリアルで、Materialという名前のプロパティを通じてアクセスします
            _edgeDetectMaterial = CheckShaderAndCreateMaterial(edgeDetectShader, _edgeDetectMaterial);
            return _edgeDetectMaterial;
        }
    }

    [Range(0.0f, 1.0f)] public float edgesOnly = 0.0f;   // エッジ線の強度
    public Color edgeColor = Color.black;                // アウトラインの色
    public Color backgroundColor = Color.white;          // 背景色
    
    /// <summary>
    /// 呼び出されたときにマテリアルが使用可能かチェックし、使用可能であればパラメータをマテリアルに渡し、Graphics.Blitで処理を行います。
    /// 使用不可能であれば、元の画像を直接スクリーンに表示し、何の処理も行いません
    /// </summary>
    /// <param name="src"></param>
    /// <param name="dest"></param>
    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (material != null)
        {
            material.SetFloat("_EdgeOnly", edgesOnly);
            material.SetColor("_EdgeColor", edgeColor);
            material.SetColor("_BackgroundColor", backgroundColor);
            
            Graphics.Blit(src, dest, material);
        }
        else
        {
            Graphics.Blit(src, dest);
        }
    }
}

このスクリプトをカメラにアタッチし、以下のようなShaderを作成します。

Shader "Custom/PostProcessing/EdgeDetection"
{
    Properties
    {
        _MainTex ("Base(RGB)", 2D) = "white" {}
        _EdgeOnly ("Edge Only", Float) = 1.0
        _EdgeColor ("Edge Color", Color) = (0, 0, 0, 1)
        _BackgroundColor ("Background Color", Color) = (1, 1, 1, 1)
    }
    SubShader
    {
        Pass
        {
            ZTest Always Cull Off ZWrite Off
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment fragSobel
            #include "UnityCG.cginc"

            sampler2D _MainTex;
            half4 _MainTex_TexelSize;   // xxx_TexelSizeはUnityが提供するxxxのピクセルサイズで、値はVector4(1/width, 1/height, width, height)です
            // 例えば512x512サイズのテクスチャの場合、この値は約0.001953(1/512)です
            // 畳み込みでは隣接領域内のテクスチャをサンプリングする必要があるため、_MainTex_TexelSizeを使用して各隣接領域のテクスチャ座標を計算する必要があります
            fixed _EdgeOnly;
            fixed4 _EdgeColor;
            fixed4 _BackgroundColor;

            struct v2f
            {
                float4 pos : SV_POSITION;
                half2 uv[9] : TEXCOORD0;
            };

            v2f vert (appdata_img v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                half2 uv = v.texcoord;

                // 頂点シェーダーでエッジ検出に必要な隣接テクスチャ座標を計算します。つまり、ピクセルとその周囲8つのピクセルのテクスチャ座標を計算し、Sobel演算子を使用してサンプリングするのに必要な9つの隣接テクスチャ座標に対応します。
                // テクスチャ座標を計算するコードをフラグメントシェーダーから頂点シェーダーに移動させることで、演算を減らし、パフォーマンスを向上させることができます
                o.uv[0] = uv + _MainTex_TexelSize.xy * half2(-1, -1);
                o.uv[1] = uv + _MainTex_TexelSize.xy * half2(0, -1);
                o.uv[2] = uv + _MainTex_TexelSize.xy * half2(1, -1);
                o.uv[3] = uv + _MainTex_TexelSize.xy * half2(-1, 0);
                o.uv[4] = uv + _MainTex_TexelSize.xy * half2(0, 0);
                o.uv[5] = uv + _MainTex_TexelSize.xy * half2(1, 0);
                o.uv[6] = uv + _MainTex_TexelSize.xy * half2(-1, 1);
                o.uv[7] = uv + _MainTex_TexelSize.xy * half2(0, 1);
                o.uv[8] = uv + _MainTex_TexelSize.xy * half2(1, 1);
                
                return  o;
            }

            fixed4 luminance(fixed4 color)
            {
                return  0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
            }

            half Sobel(v2f i)
            {
                const half Gx[9] = {-1, -2, -1, 0, 0, 0, 1, 2, 1};
                const half Gy[9] = {-1, 0, 1, -2, 0, 2, -1, 0, 1};
                half texColor;
                half edgeX = 0;
                half edgeY = 0;
                for(int it = 0; it < 9; it ++)
                {
                    texColor = luminance(tex2D(_MainTex, i.uv[it]));   // 対応する位置の輝度値を取得し、勾配を計算して、その点がエッジ点かどうかを判断します
                    edgeX += texColor * Gx[it];                              // 水平方向の畳み込みを行います
                    edgeY += texColor * Gy[it];                              // 垂直方向の畳み込みを行います
                } 

                half edge = 1 - abs(edgeX) - abs(edgeY);                     // edgeが小さいほど、勾配が大きく、その位置はエッジ点である可能性が高いことを示します
                return  edge;
            }

            fixed4 fragSobel (v2f i) : SV_Target
            {
                half edge = Sobel(i);                                                      // edgeが小さいほど、その位置はエッジ点である可能性が高く、補間する際にこの値は_EdgeColorに近づきます

                fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv[4]), edge);   // 背景が元の画像の場合の色値を計算します
                fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);           // 純色の場合の色値を計算します(_BackgroundColorは全体のトーンに相当します)
                return  lerp(withEdgeColor, onlyEdgeColor, _EdgeOnly);
            }
            ENDCG
        }
    }
    Fallback Off
}

このShaderをEdgeDetectionコンポーネントのedgeDetectShaderにドラッグし、パラメータを調整してアウトライン効果を実現します。

4 ガウシアンブラー

ぼかしの実現には多くの方法があります。例えば、平均ぼかしと中央値ぼかしがあります。

  • 平均ぼかし:畳み込み操作を使用し、使用する畳み込みカーネル内の各要素の値は等しく、合計が1になるように設定されています。つまり、畳み込み後のピクセル値はその近傍内の各ピクセルの平均値です
  • 中央値ぼかし:近傍内のすべてのピクセルをソートした中央値を元の色に置き換えます
  • ガウシアンぼかし:この節で紹介する方法です

4.1 ガウシアンフィルタリング

ガウシアンフィルタリングも同様に畳み込み計算を使用しており、使用する畳み込みカーネルはガウシアンカーネルと呼ばれます。ガウシアンカーネルは正方形サイズのフィルタカーネルで、その中の各要素の計算は以下のガウシアン方程式に基づいています。

G(x,y) = 1/(2πσ²) * e^(-(x² + y²)/(2σ²))

ここで、σは標準偏差(通常値は1)で、xとyはそれぞれ現在の位置から畳み込みカーネル中心までの整数距離に対応します。ガウシアンカーネルを構築するには、ガウシアンカーネル内の各位置に対応するガウシアン値を計算するだけです。フィルタリング後の画像が暗くならないように、ガウシアンカーネル内の重みを正規化する必要があります。つまり、各重みをすべての重みの合計で割ることで、すべての重みの合計が1になるようにします。したがって、ガウシアン関数内のeの前の係数は実際には結果に何の影響も与えません。

ガウシアン方程式は、近傍の各ピクセルが現在処理中のピクセルに与える影響の度合いをうまく模倣しています。距離が近いほど、影響は大きくなります。ガウシアンカーネルの次元が高いほど、ぼかしの度合いは大きくなります。N×Nのガウシアンカーネルを使用して画像に畳み込みフィルタリングを行う場合、N×N×W×H(WとHはそれぞれ画像の幅と高さ)回のテクスチャサンプリングが必要です。Nのサイズが増加するにつれて、サンプリング回数は非常に大きくなります。

ガウシアンカーネルを観察すると、それは対称行列であることがわかります。したがって、この2次元ガウシアン関数を2つの1次元関数に分解できます。つまり、2つの1次元ガウシアンカーネルを順次使用して画像にフィルタリングすることで、直接2次元ガウシアンカーネルを使用するのと同じ結果が得られますが、サンプリング回数は2×N×W×Hになります。さらに観察すると、2つの1次元ガウシアンカーネルには多くの重みが繰り返し含まれていることがわかります。サイズ5の1次元ガウシアンカーネルの場合、実際には3つの重み値のみを記録すれば十分です。

以下に示す5×5のガウシアンカーネルを使用して元の画像にガウシアンぼかしを適用します。2つのPassを順次呼び出します。最初のPassは垂直方向の1次元ガウシアンカーネルを使用して画像にフィルタリングし、2番目のPassは水平方向の1次元ガウシアンカーネルを使用して最終画像を取得します。実装では画像スケーリングを使用してさらにパフォーマンスを向上させ、ガウシアンフィルタリングの適用回数を調整してぼかしの度合いを制御します(回数が多いほどぼかしは強くなります)。

4.2 実装

GaussianBlur.csという新しいスクリプトを作成し、以下のコードを追加します。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class GaussianBlur : PostEffectsBase
{
    public Shader gaussianBlurShader;
    private Material _gaussianBlurMaterial;
    
    public Material material
    {
        get
        {
            // gaussianBlurShaderは指定したShaderで、後で実装するGaussianBlur.shaderに対応します。
            // _gaussianBlurMaterialは作成したマテリアルで、Materialという名前のプロパティを通じてアクセスします
            _gaussianBlurMaterial = CheckShaderAndCreateMaterial(gaussianBlurShader, _gaussianBlurMaterial);
            return _gaussianBlurMaterial;
        }
    }

    [Range(0, 4)] public int iterations = 3;              // ガウシアンぼかしの反復回数
    [Range(0.2f, 3.0f)] public float blurSpread = 0.6f;   // ぼかしの範囲、GaussianBlur.shader内の_BlurSizeに対応し、大きいほどサンプリング点の間隔が大きくなります
    [Range(1, 8)] public int downSample = 2;              // スケーリング係数

    #region Edition1: ぼかしを適用するだけ

    // /// <summary>
    // /// Edition1: ぼかしを適用するだけ
    // /// 呼び出されたときにマテリアルが使用可能かチェックし、使用可能であればパラメータをマテリアルに渡し、Graphics.Blitで処理を行います。
    // /// 使用不可能であれば、元の画像を直接スクリーンに表示し、何の処理も行いません
    // /// </summary>
    // /// <param name="src"></param>
    // /// <param name="dest"></param>
    // private void OnRenderImage(RenderTexture src, RenderTexture dest)
    // {
    //     if (material != null)
    //     {
    //         int rtW = src.width;
    //         int rtH = src.height;
    //         // ガウシアンぼかしには2つのPassを呼び出す必要があるため、最初のPass完了後のぼかし結果を保存するために中間バッファを使用する必要があります
    //         RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);   // スクリーン画像と同じサイズのバッファを割り当てます
    //
    //         // 垂直方向のPassをレンダリング
    //         Graphics.Blit(src, buffer, material, 0);                                            // Shaderの最初のPassを使用してsrcを処理し、結果をbufferに保存します
    //         // 水平方向のPassをレンダリング
    //         Graphics.Blit(buffer, dest, material, 1);                                           // Shaderの2番目のPassを使用して前回の処理結果bufferを処理し、結果をdestに保存し、最終的なスクリーン画像を返します
    //         
    //         RenderTexture.ReleaseTemporary(buffer);                                                   // 以前に割り当てたメモリを解放します
    //     }
    //     else
    //     {
    //         Graphics.Blit(src, dest);
    //     }
    // }

    #endregion

    #region Edition2: レンダリングテクスチャをスケーリングする

    // /// <summary>
    // /// Edition2: レンダリングテクスチャをスケーリングする
    // /// 呼び出されたときにマテリアルが使用可能かチェックし、使用可能であればパラメータをマテリアルに渡し、Graphics.Blitで処理を行います。
    // /// 使用不可能であれば、元の画像を直接スクリーンに表示し、何の処理も行いません
    // /// </summary>
    // /// <param name="src"></param>
    // /// <param name="dest"></param>
    // private void OnRenderImage(RenderTexture src, RenderTexture dest)
    // {
    //     // スケーリングを使用して画像をダウンサンプリングし、処理するピクセルの数を減らしてパフォーマンスを向上させます
    //     if (material != null)
    //     {
    //         // バッファサイズを宣言する際に、元のスクリーン解像率より小さいサイズを使用し、その一時レンダリングテクスチャのフィルタリングモードをバイリニアに設定します
    //         // これにより、最初のpassを呼び出すときに処理するピクセルの数は元の数分の1になります。適切なダウンサンプリングはパフォーマンスを向上させるだけでなく、より良いぼかし効果も得られます
    //         int rtW = src.width/downSample;
    //         int rtH = src.height/downSample;
    //         // ガウシアンぼかしには2つのPassを呼び出す必要があるため、最初のPass完了後のぼかし結果を保存するために中間バッファを使用する必要があります
    //         RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);   // バッファを割り当てます
    //         buffer.filterMode = FilterMode.Bilinear;
    //         
    //         // 垂直方向のPassをレンダリング
    //         Graphics.Blit(src, buffer, material, 0);                                            // Shaderの最初のPassを使用してsrcを処理し、結果をbufferに保存します
    //         // 水平方向のPassをレンダリング
    //         Graphics.Blit(buffer, dest, material, 1);                                           // Shaderの2番目のPassを使用して前回の処理結果bufferを処理し、結果をdestに保存し、最終的なスクリーン画像を返します
    //         
    //         RenderTexture.ReleaseTemporary(buffer);                                                   // 以前に割り当てたメモリを解放します
    //     }
    //     else
    //     {
    //         Graphics.Blit(src, dest);
    //     }
    // }

    #endregion

    #region Edition3: 反復を使用してより大きなぼかしを実現

    /// <summary>
    /// Edition3: 反復を使用してより大きなぼかしを実現
    /// 呼び出されたときにマテリアルが使用可能かチェックし、使用可能であればパラメータをマテリアルに渡し、Graphics.Blitで処理を行います。
    /// 使用不可能であれば、元の画像を直接スクリーンに表示し、何の処理も行いません
    /// </summary>
    /// <param name="src"></param>
    /// <param name="dest"></param>
    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        // スケーリングを使用して画像をダウンサンプリングし、処理するピクセルの数を減らしてパフォーマンスを向上させます
        if (material != null)
        {
            // バッファサイズを宣言する際に、元のスクリーン解像率より小さいサイズを使用し、その一時レンダリングテクスチャのフィルタリングモードをバイリニアに設定します
            // これにより、最初のpassを呼び出すときに処理するピクセルの数は元の数分の1になります。適切なダウンサンプリングはパフォーマンスを向上させるだけでなく、より良いぼかし効果も得られます
            int rtW = src.width/downSample;
            int rtH = src.height/downSample;
            // ガウシアンぼかしには2つのPassを呼び出す必要があるため、最初のPass完了後のぼかし結果を保存するために中間バッファを使用する必要があります
            RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);   // バッファを割り当てます
            buffer0.filterMode = FilterMode.Bilinear;
            Graphics.Blit(src, buffer0);   // ソーステクスチャをbuffer0にレンダリングします

            for (int i = 0; i < iterations; i++)
            {
                // buffer0を最終レンダリングターゲットにします
                // 1.buffer0をソーステクスチャとして使用し、最初のpassをbuffer1にレンダリングし、buffer0をクリアし、buffer1をbuffer0に割り当てます(buffer0には前回のレンダリング結果が保存されます)
                // 2.buffer0を前回のレンダリング結果のソーステクスチャとして使用し、再び2番目のpassをbuffer1にレンダリングし、buffer0を再度クリアし、buffer1をbuffer0に割り当てます(buffer0には前回のレンダリング結果が保存されます)
                material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
                RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
                Graphics.Blit(buffer0, buffer1, material, 0);
                RenderTexture.ReleaseTemporary(buffer0);
                buffer0 = buffer1;
                buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
                Graphics.Blit(buffer0, buffer1, material, 1);
                RenderTexture.ReleaseTemporary(buffer0);
                buffer0 = buffer1;
            }
            
            Graphics.Blit(buffer0, dest);
            RenderTexture.ReleaseTemporary(buffer0);
        }
        else
        {
            Graphics.Blit(src, dest);
        }
    }

    #endregion
}

このスクリプトをカメラにアタッチし、以下のようなShaderを作成します。

Shader "Custom/PostProcessing/GaussianBlur"
{
    Properties
    {
        _MainTex ("Base(RGB)", 2D) = "white" {}
        _BlurSize ("Blur Size", Float) = 1.0
    }
    SubShader
    {
        // CGINCLUDEはC++のヘッダーファイルのような機能です。ガウシアンぼかしには2つのPassを定義する必要がありますが、それらが使用するフラグメントシェーダーコードは完全に同じであるため、CGINCLUDEを使用することで2つの同じfrag関数を記述する必要がなくなります
        CGINCLUDE
        #include "UnityCG.cginc"
        
        sampler2D _MainTex;
        half4 _MainTex_TexelSize;
        float _BlurSize;

        struct v2f
        {
            float4 pos : SV_POSITION;
            half2 uv[5] : TEXCOORD0;
        };

        v2f vertBlurVertical(appdata_img v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            half2 uv = v.texcoord;

            // 垂直方向のオフセットのみを行い、一組のガウシアンサンプリングテクスチャ座標を定義し、_BlurSizeとの乗算を使用してサンプリング距離を制御します。
            // _BlurSizeが大きいほど、ぼかしの度合いは高くなりますが、サンプリング数は影響を受けませんが、同時に過度に大きな_BlurSizeはゴーストを引き起こします。
            // テクスチャ座標を計算するコードを頂点シェーダーに移動させることで、演算を減らし、パフォーマンスを向上させることができます
            o.uv[0] = uv;
            o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
            o.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
            o.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
            o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;

            return o;
        }

        v2f vertBlurHorizontal(appdata_img v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            half2 uv = v.texcoord;

            // 水平方向のオフセットのみを行い、一組のガウシアンサンプリングテクスチャ座標を定義し、_BlurSizeとの乗算を使用してサンプリング距離を制御します。
            // _BlurSizeが大きいほど、ぼかしの度合いは高くなりますが、サンプリング数は影響を受けませんが、同時に過度に大きな_BlurSizeはゴーストを引き起こします。
            // テクスチャ座標を計算するコードを頂点シェーダーに移動させることで、演算を減らし、パフォーマンスを向上させることができます
            o.uv[0] = uv;
            o.uv[1] = uv + float2(_MainTex_TexelSize.y * 1.0, 0.0) * _BlurSize;
            o.uv[2] = uv - float2(_MainTex_TexelSize.y * 1.0, 0.0) * _BlurSize;
            o.uv[3] = uv + float2(_MainTex_TexelSize.y * 2.0, 0.0) * _BlurSize;
            o.uv[4] = uv - float2(_MainTex_TexelSize.y * 1.0, 0.0) * _BlurSize;

            return o;
        }

        fixed4 fragBlur(v2f i) : SV_Target
        {
            // 3つの記録されたガウシアン重み
            float weight[3] = {0.4026, 0.2442, 0.0545};
            fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
            for(int it = 1; it < 3; it++)
            {
                // 各近傍ピクセルに対応するweightを宣言し、結果値sumを現在のピクセル値にその重み値を掛けた値で初期化します
                // 対称性に基づいて2回反復を行い、各反復には2回のテクスチャサンプリングが含まれ、ピクセル値と重み値の乗算結果をsumに加算し、最後にフィルタリング結果sumを返します
                sum += tex2D(_MainTex, i.uv[it * 2 - 1]).rgb * weight[it];
                sum += tex2D(_MainTex, i.uv[it * 2]).rgb * weight[it];
            }
            // 上記の操作は1x5行列が5x3行列を掛ける操作に相当します
            // 1x5行列:[0.0545, 0.2442, 0.4026, 0.2442, 0.0545]
            // 5x3行列:各行は各点に対応する色値です
            // 最後に加算して1x3行列のrgb値を得ます
            return  fixed4(sum, 1.0);
        }
        
        ENDCG
        
        ZTest Always Cull Off ZWrite Off
        
        Pass 
        {
            // PassにNAMEセマンティクスを使用して名前を定義します。ガウシアンぼかしは一般的な画像処理操作であり、多くのスクリーン特殊効果はその基礎に構築されています。
            // Passに名前を定義することで、他のShader内で直接その名前を使用してこのPassを使用でき、コードを再記述する必要がなくなります
            NAME "GAUSSIAN_BLUR_VERTICAL"
            
            CGPROGRAM
            
            #pragma vertex vertBlurVertical
            #pragma fragment fragBlur
            
            ENDCG
        }
        
        Pass 
        {
            NAME "GAUSSIAN_BLUR_HORIZONTAL"
            
            CGPROGRAM
            
            #pragma vertex vertBlurHorizontal
            #pragma fragment fragBlur
            
            ENDCG
        }
    }
    Fallback Off
}

このShaderをGaussianBlurコンポーネントのgaussianBlurShaderにドラッグし、パラメータを調整してぼかし効果を実現します。

5 ブルーム効果

ブルーム効果はゲームで一般的なスクリーン効果の一種です。この効果は、現実のカメラの一つの画像効果を模倣し、画面の明るい領域を周囲の領域に「拡散」させて、夢のような効果を作り出します。

ブルームの実装原理は非常に簡単です:

  1. 特定のしきい値に基づいて画像内の明るい領域を抽出し、それらを1枚のレンダリングテクスチャに保存します
  2. ガウシアンぼかしを使用してこのレンダリングテクスチャをぼかし処理し、光の拡散効果を模倣します
  3. 処理結果を元の画像と混合して、最終効果を得ます。

Bloom.csという新しいスクリプトを作成し、以下のコードを追加します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Bloom : PostEffectsBase
{
    public Shader bloomShader;
    private Material _bloomMaterial;
    
    public Material material
    {
        get
        {
            // bloomShaderは指定したShaderで、後で実装するBloom.shaderに対応します。
            // _bloomMaterialは作成したマテリアルで、Materialという名前のプロパティを通じてアクセスします
            _bloomMaterial = CheckShaderAndCreateMaterial(bloomShader, _bloomMaterial);
            return _bloomMaterial;
        }
    }

    [Range(0, 4)] public int iterations = 3;
    [Range(0.2f, 3.0f)] public float blurSpread = 0.6f;
    [Range(1, 8)] public int downSample = 2;
    [Range(0.0f, 4.0f)] public float luminanceThreshold = 0.6f;

    /// <summary>
    /// ガウシアンぼかしのコードと基本的に同じ
    /// 呼び出されたときにマテリアルが使用可能かチェックし、使用可能であればパラメータをマテリアルに渡し、Graphics.Blitで処理を行います。
    /// 使用不可能であれば、元の画像を直接スクリーンに表示し、何の処理も行いません
    /// </summary>
    /// <param name="src"></param>
    /// <param name="dest"></param>
    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        // スケーリングを使用して画像をダウンサンプリングし、処理するピクセルの数を減らしてパフォーマンスを向上させます
        if (material != null)
        {
            material.SetFloat("_LuminanceThreshold", luminanceThreshold);
            // バッファサイズを宣言する際に、元のスクリーン解像率より小さいサイズを使用し、その一時レンダリングテクスチャのフィルタリングモードをバイリニアに設定します
            // これにより、最初のpassを呼び出すときに処理するピクセルの数は元の数分の1になります。適切なダウンサンプリングはパフォーマンスを向上させるだけでなく、より良いぼかし効果も得られます
            int rtW = src.width/downSample;
            int rtH = src.height/downSample;
            // ガウシアンぼかしには2つのPassを呼び出す必要があるため、最初のPass完了後のぼかし結果を保存するために中間バッファを使用する必要があります
            RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);   // バッファを割り当てます
            buffer0.filterMode = FilterMode.Bilinear;
            
            // 1.pass0で画像内の明るい領域を抽出し、buffer0に保存します
            Graphics.Blit(src, buffer0, material, 0);  // 0番目のpassを呼び出し、ソーステクスチャをmaterialで処理した結果をbuffer0にレンダリングします
            
            // 2.pass1とpass2でそれぞれ縦方向と横方向のガウシアンぼかしを行い、ぼかし処理された明るい領域はbuffer0に保存されます
            for (int i = 0; i < iterations; i++)
            {
                material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
                RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
                Graphics.Blit(buffer0, buffer1, material, 1);  // 1番目のpassを呼び出し、buffer0をmaterialで処理した結果をbuffer1にレンダリングします
                RenderTexture.ReleaseTemporary(buffer0);
                buffer0 = buffer1;
                buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
                Graphics.Blit(buffer0, buffer1, material, 2);  // 再びbuffer0をmaterialで処理した結果をbuffer1にレンダリングします
                RenderTexture.ReleaseTemporary(buffer0);
                buffer0 = buffer1;
            }
            
            // 3.buffer0をマテリアル内の_Bloomテクスチャプロパティに渡し、最後の混合を行うために最後のpassを使用し、結果を対象レンダリングテクスチャdestに保存します
            material.SetTexture("_Bloom", buffer0);    // buffer0を_Bloomテクスチャとして設定します
            Graphics.Blit(src, dest, material, 3);      // ソーステクスチャを3番目のpassでdestにレンダリングします
            RenderTexture.ReleaseTemporary(buffer0);
        }
        else
        {
            Graphics.Blit(src, dest);
        }
    }
}

このスクリプトをカメラにアタッチし、以下のようなShaderを作成します。

Shader "Custom/PostProcessing/Bloom"
{
    Properties
    {
        _MainTex ("Base(RGB)", 2D) = "white" {}
        _BlurSize ("Blur Size", Float) = 1.0
        _LuminanceThreshold ("Luminance Threshold", Float) = 0.6
    }
    SubShader
    {
        // CGINCLUDEはC++のヘッダーファイルのような機能です。ガウシアンぼかしには2つのPassを定義する必要がありますが、それらが使用するフラグメントシェーダーコードは完全に同じであるため、CGINCLUDEを使用することで2つの同じfrag関数を記述する必要がなくなります
        CGINCLUDE
        
        #include "UnityCG.cginc"
        sampler2D _MainTex;
        half4 _MainTex_TexelSize;
        float _BlurSize;
        float _LuminanceThreshold;

        struct v2f
        {
            float4 pos : SV_POSITION;
            half2 uv[5] : TEXCOORD0;
        };

        v2f vertBlurVertical(appdata_img v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            half2 uv = v.texcoord;

            // 垂直方向のオフセットのみを行い、一組のガウシアンサンプリングテクスチャ座標を定義し、_BlurSizeとの乗算を使用してサンプリング距離を制御します。
            // _BlurSizeが大きいほど、ぼかしの度合いは高くなりますが、サンプリング数は影響を受けませんが、同時に過度に大きな_BlurSizeはゴーストを引き起こします。
            // テクスチャ座標を計算するコードを頂点シェーダーに移動させることで、演算を減らし、パフォーマンスを向上させることができます
            o.uv[0] = uv;
            o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
            o.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
            o.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
            o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;

            return o;
        }

        v2f vertBlurHorizontal(appdata_img v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            half2 uv = v.texcoord;

            // 水平方向のオフセットのみを行い、一組のガウシアンサンプリングテクスチャ座標を定義し、_BlurSizeとの乗算を使用してサンプリング距離を制御します。
            // _BlurSizeが大きいほど、ぼかしの度合いは高くなりますが、サンプリング数は影響を受けませんが、同時に過度に大きな_BlurSizeはゴーストを引き起こします。
            // テクスチャ座標を計算するコードを頂点シェーダーに移動させることで、演算を減らし、パフォーマンスを向上させることができます
            o.uv[0] = uv;
            o.uv[1] = uv + float2(_MainTex_TexelSize.y * 1.0, 0.0) * _BlurSize;
            o.uv[2] = uv - float2(_MainTex_TexelSize.y * 1.0, 0.0) * _BlurSize;
            o.uv[3] = uv + float2(_MainTex_TexelSize.y * 2.0, 0.0) * _BlurSize;
            o.uv[4] = uv - float2(_MainTex_TexelSize.y * 1.0, 0.0) * _BlurSize;

            return o;
        }

        fixed4 fragBlur(v2f i) : SV_Target
        {
            // 3つの記録されたガウシアン重み
            float weight[3] = {0.4026, 0.2442, 0.0545};
            fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
            for(int it = 1; it < 3; it++)
            {
                // 各近傍ピクセルに対応するweightを宣言し、結果値sumを現在のピクセル値にその重み値を掛けた値で初期化します
                // 対称性に基づいて2回反復を行い、各反復には2回のテクスチャサンプリングが含まれ、ピクセル値と重み値の乗算結果をsumに加算し、最後にフィルタリング結果sumを返します
                sum += tex2D(_MainTex, i.uv[it * 2 - 1]).rgb * weight[it];
                sum += tex2D(_MainTex, i.uv[it * 2]).rgb * weight[it];
            }
            // 上記の操作は1x5行列が5x3行列を掛ける操作に相当します
            // 1x5行列:[0.0545, 0.2442, 0.4026, 0.2442, 0.0545]
            // 5x3行列:各行は各点に対応する色値です
            // 最後に加算して1x3行列のrgb値を得ます
            return  fixed4(sum, 1.0);
        }
        
        fixed4 fragExtractBright(v2f i) : SV_Target
        {
            fixed4 color = tex2D(_MainTex, i.uv[0]);
            float luminance = 0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b;
            return luminance > _LuminanceThreshold ? color : fixed4(0, 0, 0, 1);
        }
        
        fixed4 fragBloom(v2f i) : SV_Target
        {
            fixed4 original = tex2D(_MainTex, i.uv[0]);
            fixed4 bloom = tex2D(_Bloom, i.uv[0]);
            return original + bloom;
        }
        
        ENDCG
        
        ZTest Always Cull Off ZWrite Off
        
        Pass 
        {
            NAME "EXTRACT_BRIGHT"
            
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment fragExtractBright
            
            ENDCG
        }
        
        Pass 
        {
            NAME "GAUSSIAN_BLUR_VERTICAL"
            
            CGPROGRAM
            
            #pragma vertex vertBlurVertical
            #pragma fragment fragBlur
            
            ENDCG
        }
        
        Pass 
        {
            NAME "GAUSSIAN_BLUR_HORIZONTAL"
            
            CGPROGRAM
            
            #pragma vertex vertBlurHorizontal
            #pragma fragment fragBlur
            
            ENDCG
        }
        
        Pass 
        {
            NAME "BLOOM"
            
            CGPROGRAM
            
            #pragma vertex vert
            #pragma fragment fragBloom
            
            ENDCG
        }
    }
    Fallback Off
}

このShaderをBloomコンポーネントのbloomShaderにドラッグし、パラメータを調整してブルーム効果を実現します。

6 モーションブラー

モーションブラーは現実世界のカメラの一つの効果です。カメラの露出時にシーンが変化すると、ぼやけた画像が生成されます。コンピュータ生成の画像には露出という物理現象が存在しないため、レンダリングされた画像は往々にして角ばっており、モーションブラーに欠けています。

モーションブラーの実装方法には以下の2つがあります:

  • 蓄積バッファ(accumulation buffer):複数の連続した画像を混合します。物体が高速に移動して複数の画像が生成された後、それらの間の平均値を最終的なモーションブラー画像として取ります。この暴力的な方法はパフォーマンスの消費が大きく、複数のフレーム画像を取得するには同じフレーム内でシーンを複数回レンダリングする必要があります。
  • 速度バッファ(velocity buffer):このバッファには各ピクセルの現在の運動速度が保存されており、その値に基づいてぼかしの方向と大きさを決定します。

私たちは**蓄積バッファ(accumulation buffer)**の方法を使用してモーションブラーを実装します。私たちは同じフレーム内でシーンを複数回レンダリングする必要はありませんが、以前のレンダリング結果を保存し、現在のレンダリング画像を継続的にそれに重ね合わせることで、運動軌跡の視覚効果を生成します。この方法は元の蓄積バッファを使用する方法と比較してパフォーマンスは良いですが、ぼかし効果は若干影響を受ける可能性があります。

MotionBlur.csというスクリプトを作成し、コードは以下の通りです。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MotionBlur : PostEffectsBase
{
    public Shader motionBlurShader;
    private Material _motionBlurMaterial;
    
    public Material material
    {
        get
        {
            // motionBlurShaderは指定したShaderで、後で実装するMotionBlur.shaderに対応します。
            // _motionBlurMaterialは作成したマテリアルで、Materialという名前のプロパティを通じてアクセスします
            _motionBlurMaterial = CheckShaderAndCreateMaterial(motionBlurShader, _motionBlurMaterial);
            return _motionBlurMaterial;
        }
    }

    [Range(0.0f, 0.9f)] public float blurAmount = 0.5f;  // blurAmountの値が大きいほど、運動のトレイル効果はより顕著になります。トレイル効果が現在のフレームのレンダリング結果を完全に置き換えないように、その値を0.0-0.9の範囲に制限します
    private RenderTexture _accumulationTexture;          // 以前の画像の重ね合わせ結果を保存するRenderTextureタイプの変数を定義します

    private void OnDisable()
    {
        DestroyImmediate(_accumulationTexture);
    }

    /// <summary>
    /// 呼び出されたときにマテリアルが使用可能かチェックし、使用可能であればパラメータをマテリアルに渡し、Graphics.Blitで処理を行います。
    /// 使用不可能であれば、元の画像を直接スクリーンに表示し、何の処理も行いません
    /// </summary>
    /// <param name="src"></param>
    /// <param name="dest"></param>
    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (material != null)
        {
            // _accumulationTextureを作成
            if (_accumulationTexture == null || _accumulationTexture.width != src.width || _accumulationTexture.height != src.height)
            {
                // _accumulationTextureが条件を満たしているか、nullか、解像度が満たしているかを判断し、満たしていない場合は再生成し、ソーステクスチャを_accumulationTextureにレンダリングします
                DestroyImmediate(_accumulationTexture);
                _accumulationTexture = new RenderTexture(src.width, src.height, 0);
                // 私たちは自分でこの変数の破棄を制御できるため、hideFlagsをHideAndDontSaveに設定できます。これはこの変数がHierarchyに表示されず、シーンにも保存されないことを意味します
                _accumulationTexture.hideFlags = HideFlags.HideAndDontSave;
                Graphics.Blit(src, _accumulationTexture);
            }
            // MarkRestoreExpectedはレンダリングテクスチャの復元操作が必要であることを示します。復元操作(restore operation)は、レンダリングテクスチャにレンダリングされたが、そのテクスチャが事前にクリアまたは破棄されていない場合に発生します
            // 私たちは各フレームでOnRenderImageを呼び出すときに現在のフレーム画像と_accumulationTexture内の画像を混合する必要があります。_accumulationTextureテクスチャは事前にクリアする必要はありませんなぜなら、それは以前の混合結果を保存しているからです
            // _accumulationTexture.MarkRestoreExpected();
            material.SetFloat("_BlurAmount", 1.0f - blurAmount);
            Graphics.Blit(src, _accumulationTexture, material);
            Graphics.Blit(_accumulationTexture, dest);
        }
        else
        {
            Graphics.Blit(src, dest);
        }
    }
}

このスクリプトをカメラにアタッチし、以下のようなShaderを作成します。

Shader "Custom/PostProcessing/MotionBlur"
{
    Properties
    {
        _MainTex ("Base(RGB)", 2D) = "white" {}
        _BlurAmount ("Blur Amount", Float) = 1.0
    }
    SubShader
    {
        CGINCLUDE
        #include "UnityCG.cginc"
        sampler2D _MainTex;
        fixed _BlurAmount;

        struct v2f
        {
            float4 pos : SV_POSITION;
            half2 uv : TEXCOORD0;
        };

        v2f vert(appdata_img v)
        {
            v2f o;
            o.pos = UnityObjectToClipPos(v.vertex);
            o.uv = v.texcoord;
            return o;
        }

        fixed4 fragRGB(v2f i) : SV_Target
        {
            // RGBバージョンのShaderは現在の画像をサンプリングし、そのAチャンネルの値を_BlurAmountに設定して、後の混合でその透明度を使用できるようにします
            return fixed4(tex2D(_MainTex, i.uv).rgb, _BlurAmount);
        }

        half4 fragA(v2f i) : SV_Target
        {
            // サンプリング結果を直接返します
            // レンダリングテクスチャの透明チャンネル値を維持し、混合時に使用する透明度値の影響を受けさせません
            return  tex2D(_MainTex, i.uv);
        }
        
        ENDCG
        
        ZTest Always Cull Off Zwrite Off
        
        Pass 
        {
            Blend SrcAlpha OneMinusSrcAlpha
            ColorMask RGB
            CGPROGRAM

            #pragma  vertex vert
            #pragma  fragment fragRGB
            
            ENDCG
        }
        
        Pass 
        {
            Blend One Zero
            ColorMask A
            CGPROGRAM

            #pragma  vertex vert
            #pragma  fragment fragA
            
            ENDCG
        }
    }
    Fallback Off
}

このShaderをMotionBlurコンポーネントのmotionBlurShaderにドラッグし、パラメータを調整してモーションブラー効果を実現します。

タグ: Unity シェーダー ポストプロセッシング グラフィックスプログラミング

8月2日 12:10 投稿