ゲーム開発
Unity
UnrealEngine
C++
Blender
Houdini
ゲーム数学
ゲームAI
グラフィックス
サウンド
アニメーション
GBDK
制作日記
IT関連
ツール開発
フロントエンド関連
サーバサイド関連
WordPress関連
ソフトウェア設計
おすすめ技術書
音楽
DTM
楽器・機材
ピアノ
ラーメン日記
四コマ漫画
その他
おすすめアイテム
おもしろコラム
  • ゲーム開発
    • Unity
    • UnrealEngine
    • C++
    • Blender
    • Houdini
    • ゲーム数学
    • ゲームAI
    • グラフィックス
    • サウンド
    • アニメーション
    • GBDK
    • 制作日記
  • IT関連
    • ツール開発
    • フロントエンド関連
    • サーバサイド関連
    • WordPress関連
    • ソフトウェア設計
    • おすすめ技術書
  • 音楽
    • DTM
    • 楽器・機材
    • ピアノ
  • ラーメン日記
    • 四コマ漫画
      • その他
        • おすすめアイテム
        • おもしろコラム
      1. ホーム
      2. 20230127_01

      【Unity】ポストエフェクトをシェーダーで実装する(基礎、複数エフェクト適用、描画パス切替)【シェーダー】

      Unityグラフィックスシェーダー
      2023-01-28

      マイケル
      マイケル
      みなさんこんにちは!
      マイケルです!
      エレキベア
      エレキベア
      こんにちクマ〜〜〜
      マイケル
      マイケル
      今日もシェーダーを触っていきます!
      今回はポストエフェクトの実装だ!
      エレキベア
      エレキベア
      ポストエフェクトってUnityだとパッケージでかけれなかったクマ??
      マイケル
      マイケル
      そう、UnityだとPost-Processing Version 2パッケージを使ってポストエフェクトをかけることが多いと思うけど、自分でシェーダーを実装してかけることもできるんだ!
      エレキベア
      エレキベア
      自分で実装できたら思うようにできそうクマね
      マイケル
      マイケル
      早速触っていこう!!

      ポストエフェクトの実装方法

      マイケル
      マイケル
      ポストエフェクト用(2D用)のシェーダーの雛形は「Shader > Image Effect Shader」から作成することができます。
      ↑ポストエフェクト用のシェーダーを作成
      マイケル
      マイケル
      作成直後は下記のような状態になっていて、
      デフォルトでネガポジ反転(RGB値を反転させたもの)が実装されています。
      Shader "Hidden/NegaPostEffect"
      {
          Properties
          {
              _MainTex ("Texture", 2D) = "white" {}
          }
          SubShader
          {
              Cull Off ZWrite Off ZTest Always
              Pass
              {
                  CGPROGRAM
                  #pragma vertex vert
                  #pragma fragment frag
                  #include "UnityCG.cginc"
                  struct appdata
                  {
                      float4 vertex : POSITION;
                      float2 uv : TEXCOORD0;
                  };
                  struct v2f
                  {
                      float2 uv : TEXCOORD0;
                      float4 vertex : SV_POSITION;
                  };
                  v2f vert (appdata v)
                  {
                      v2f o;
                      o.vertex = UnityObjectToClipPos(v.vertex);
                      o.uv = v.uv;
                      return o;
                  }
                  sampler2D _MainTex;
                  fixed4 frag (v2f i) : SV_Target
                  {
                      fixed4 col = tex2D(_MainTex, i.uv);
                      col.rgb = 1 - col.rgb; // RGBを反転
                      return col;
                  }
                  ENDCG
              }
          }
      }
      
      ↑作成直後の状態(ネガポジ反転)
      マイケル
      マイケル
      あとは下記スクリプトをカメラオブジェクトにアタッチして、このシェーダーを適用したマテリアルをアタッチすれば完了です!
      using UnityEngine;
      [ExecuteInEditMode]
      public class SimplePostEffect : MonoBehaviour
      {
          [SerializeField] private Material effectMaterial;
          private void OnRenderImage(RenderTexture src, RenderTexture dest)
          {
              // ポストエフェクトをかけない場合
              if (effectMaterial == null)
              {
                  Graphics.Blit(src, dest);
                  return;
              }
              // ポストエフェクトをかける場合
              Graphics.Blit(src, dest, effectMaterial);
          }
      }
      
      ↑ポストエフェクトをかけるスクリプト
      エレキベア
      エレキベア
      これだけなのクマね
      マイケル
      マイケル
      OnRenderImage関数ではそれぞれ
      src: 最終的に描画される画像(入力)
      dest: 最終的に描画される画像(出力)

      として渡されてくるため、Graphics.Blit関数でsrcからdestへコピーする際にマテリアルを適用することで最終的な画像にエフェクトをかける、といった流れになっています。

      OnRenderImage | Unityスクリプトリファレンス

      Graphics-Blit | Unityスクリプトリファレンス

      エレキベア
      エレキベア
      なるほどクマ
      最終的な描画の処理になるクマね
      マイケル
      マイケル
      ポストエフェクトをかける前後では下記のように変化することが確認できます!
      ↑ポストエフェクト無し
      ↑ポストエフェクト有り
      エレキベア
      エレキベア
      おお〜〜〜
      ちゃんとエフェクトがかかっているクマ〜〜〜

      いくつかポストエフェクトを作ってみる

      マイケル
      マイケル
      ポストエフェクトのかけ方が分かったところで、いくつか実装してみましょう!

      グレースケール

      マイケル
      マイケル
      まずはグレースケール!
      こちらはNTSC加重平均法というものを用いて、RGBの輝度として(0.298, 0.586, 0.114)を重みとして乗算してモノクロ化します。

      参考:
      より自然なグレースケール変換 | ゆるゆるプログラミング

      エレキベア
      エレキベア
      なんという絶妙な数値クマ・・・
      マイケル
      マイケル
      今回は数値はざっくりとしたものを指定して下記のように実装してみます。
      Frag関数の処理を少し修正しただけですね
      Shader "Hidden/GrayPostEffect"
      {
          Properties
          {
              _MainTex ("Texture", 2D) = "white" {}
          }
          SubShader
          {
              Cull Off ZWrite Off ZTest Always
              Pass
              {
                  CGPROGRAM
                  #pragma vertex vert
                  #pragma fragment frag
                  #include "UnityCG.cginc"
                  struct appdata
                  {
                      float4 vertex : POSITION;
                      float2 uv : TEXCOORD0;
                  };
                  struct v2f
                  {
                      float2 uv : TEXCOORD0;
                      float4 vertex : SV_POSITION;
                  };
                  v2f vert (appdata v)
                  {
                      v2f o;
                      o.vertex = UnityObjectToClipPos(v.vertex);
                      o.uv = v.uv;
                      return o;
                  }
                  sampler2D _MainTex;
                  fixed4 frag (v2f i) : SV_Target
                  {
                      fixed4 col = tex2D(_MainTex, i.uv);
                      float monochrome = 0.3 * col.r + 0.6 * col.g + 0.1 * col.b;
                      return float4(monochrome.xxx, 1);
                  }
                  ENDCG
              }
          }
      }
      
      ↑グレースケールの実装
      マイケル
      マイケル
      こちらを適応した結果は下記のようになります。
      ↑グレースケール変換した結果
      エレキベア
      エレキベア
      ちゃんとグレースケールになってるクマ〜〜〜

      放射状ブラー

      マイケル
      マイケル
      次は少し難易度を上げて放射状ブラーを実装してみます。
      こちらはLIGHT11さんの下記記事を参考にさせていただきました!

      参考:
      【Unity】【シェーダ】Radial Blur(放射状ブラー)のポストエフェクトを実装する | LIGHT11

      マイケル
      マイケル
      こちらの実装は下記のようになります。
      Shader "Hidden/RadialBlurPostEffect"
      {
          Properties
          {
              _MainTex ("Texture", 2D) = "white" {}
              _SampleCount ("Sample Count", Range(4, 16)) = 8
              _Strength ("Strength", Range(0.0, 1.0)) = 0.5
          }
          SubShader
          {
              Cull Off ZWrite Off ZTest Always
      
              Pass
              {
                  CGPROGRAM
                  #pragma vertex vert
                  #pragma fragment frag
      
                  #include "UnityCG.cginc"
      
                  struct appdata
                  {
                      float4 vertex : POSITION;
                      float2 uv : TEXCOORD0;
                  };
      
                  struct v2f
                  {
                      float2 uv : TEXCOORD0;
                      float4 vertex : SV_POSITION;
                  };
      
                  v2f vert (appdata v)
                  {
                      v2f o;
                      o.vertex = UnityObjectToClipPos(v.vertex);
                      o.uv = v.uv;
                      return o;
                  }
      
                  sampler2D _MainTex;
                  half _SampleCount;
                  half _Strength;
      
                  fixed4 frag (v2f i) : SV_Target
                  {
                      fixed4 col = 0;
                      // 中心からの距離
                      const half2 symmetryUv = i.uv - 0.5;
                      const half distance = length(symmetryUv);
                      // 繰り返すごとにより内側のピクセルをサンプリングする
                      for (int j = 0; j < _SampleCount; j++)
                      {
                          const float uvOffset = 1 - _Strength * j / _SampleCount * distance;
                          col += tex2D(_MainTex, symmetryUv * uvOffset + 0.5);
                      }
                      col /= _SampleCount;
                      return col;
                  }
                  ENDCG
              }
          }
      }
      
      マイケル
      マイケル
      frag関数内の処理を見れば分かる通り、指定回数ループするごとに内側のピクセルをサンプリングして加算、最終的に平均値を求めることでブラーを表現しています。
      パラメータはとりあえず下記のようにサンプル数:12、強さ:0.7を指定してみると、下記のように描画されることが確認できます。
      ↑パラメータの指定
      ↑ブラーをかけた結果
      エレキベア
      エレキベア
      ちゃんと外側に引き伸ばされたようなブラーになったクマ〜〜
      マイケル
      マイケル
      こんな感じで繰り返しながらサンプリングしていく手法はいろいろと応用が効きそうだね!

      複数エフェクトの適用

      マイケル
      マイケル
      次にこれらのエフェクトを複数かけたい場合にどうするか?についてですが、
      これはエフェクトをかける用のRenderTextureを自前で用意して最終的にdestにかける方法で適用することができます。
      実装は下記のようになります。
      using UnityEngine;
      
      [ExecuteInEditMode]
      public class SomePostEffect : MonoBehaviour
      {
          [SerializeField] private Material[] effectMaterials;
          private readonly RenderTexture[] _rts = new RenderTexture[2];
      
          private void OnRenderImage(RenderTexture src, RenderTexture dest)
          {
              if (effectMaterials == null || effectMaterials.Length == 0)
              {
                  Graphics.Blit(src, dest);
                  return;
              }
      
              // RenderTextureが未生成であれば生成する
              CheckAndCreateRTs(src);
      
              // レンダーテクスチャをスワップしながらエフェクトをかける
              Graphics.Blit(src, _rts[0]);
              foreach (var effectMaterial in effectMaterials)
              {
                  Graphics.Blit(_rts[0], _rts[1], effectMaterial);
                  (_rts[0], _rts[1]) = (_rts[1], _rts[0]); // Swap
              }
              Graphics.Blit(_rts[0], dest);
          }
      
          private void CheckAndCreateRTs(RenderTexture src)
          {
              if (_rts[0] == null
                  || _rts[0].width != src.width
                  || _rts[0].height != src.height)
              {
                  for (var i = 0; i < _rts.Length; i++)
                  {
                      var rt = new RenderTexture(src.width, src.height, 16, RenderTextureFormat.ARGB32);
                      rt.enableRandomWrite = true;
                      rt.wrapMode = TextureWrapMode.Repeat;
                      rt.filterMode = FilterMode.Bilinear;
                      rt.Create();
                      _rts[i] = rt;
                  }
              }
          }
      }
      
      エレキベア
      エレキベア
      なるほどクマ
      エフェクトをかけている間は自作のRenderTextureで退避しておくクマね
      マイケル
      マイケル
      ちょっと面倒くさいけど、下記のように複数かけれることが確認できました!
      ↑グレースケール + 放射状ブラー
      エレキベア
      エレキベア
      段々使いこなしてきたクマね

      描画パスの切替とワイプ処理

      マイケル
      マイケル
      最後に、シェーダーコード内に複数パスを記述して切り替える方法についても紹介します!
      描画するパスに関しては下記のようにGraphics.Blitの第4引数に渡すことで指定することができます。
      using UnityEngine;
      [ExecuteInEditMode]
      public class SelectPostEffect : MonoBehaviour
      {
          [SerializeField] private Material selectEffectMaterial;
          private enum UsePass
          {
              UsePass1,
              UsePass2
          }
          [SerializeField] private UsePass usePass;
          private void OnRenderImage(RenderTexture src, RenderTexture dest)
          {
              Graphics.Blit(src, dest, selectEffectMaterial, (int) usePass);
          }
      }
      
      ↑描画パスの指定
      エレキベア
      エレキベア
      なるほどクマ
      あとは渡すパスを切り替えればいいクマね
      マイケル
      マイケル
      今回は3種類のワイプエフェクトを切り替える処理として、下記のように実装してみました!
      合わせて_WipeSizeパラメータにワイプ量を渡すようにも実装しています。
      using System.Collections;
      using UnityEngine;
      [ExecuteInEditMode]
      public class WipePostEffect : MonoBehaviour
      {
          [SerializeField] private Material effectMaterial;
          [SerializeField] private bool isStartWipe;
          private enum UsePass
          {
              WipeX,         // X方向のワイプ
              WipeDirection, // 方向指定のワイプ
              WipeChecker,   // チェッカーボードのワイプ
          }
          [SerializeField] private UsePass usePass;
          private void OnRenderImage(RenderTexture src, RenderTexture dest)
          {
              Graphics.Blit(src, dest, effectMaterial, (int) usePass);
          }
          private static readonly int WipeSize = Shader.PropertyToID("_WipeSize");
          private float _wipeSize = 0.0f;
          private void Start()
          {
              _wipeSize = 0.0f;
              effectMaterial.SetFloat(WipeSize, _wipeSize);
          }
          private void Update()
          {
              // フラグが立ったらワイプ開始
              if (!isStartWipe) return;
              isStartWipe = false;;
              StartCoroutine(StartWipeCoroutine());
          }
          private bool _isDoWipe = false;
          private IEnumerator StartWipeCoroutine()
          {
              if (_isDoWipe) yield break;
              _isDoWipe = true;
              // 一定間隔でワイプ量を変化させる
              _wipeSize = 0.0f;
              while (_wipeSize < 1.0f)
              {
                  _wipeSize += 0.8f * Time.deltaTime;
                  effectMaterial.SetFloat(WipeSize, _wipeSize);
                  yield return null;
              }
              _isDoWipe = false;
          }
      }
      
      ↑3種類のワイプエフェクト適用処理
      マイケル
      マイケル
      シェーダー側のワイプエフェクトのコードは下記のように実装してみました!
      3種類のエフェクトをPassとして分けて実装しています。
      Shader "Hidden/WipePostEffect"
      {
          Properties
          {
              _MainTex ("Texture", 2D) = "white" {}
              _WipeSize ("Wipe Size", Range(0.0, 1.0)) = 0.0
              [IntRange] _WipeDirectionX ("Wipe Direction X", Range(-1, 1)) = 1
              [IntRange] _WipeDirectionY ("Wipe Direction Y", Range(-1, 1)) = 1
          }
          SubShader
          {
              Tags { "Queue"="AlphaTest" "RenderType"="TransparentCutout" }
              Cull Off ZWrite Off ZTest Always
              // 共通部分
              CGINCLUDE
              #pragma vertex vert
              #pragma fragment frag
              #include "UnityCG.cginc"
              struct appdata
              {
                  float4 vertex : POSITION;
                  float2 uv : TEXCOORD0;
              };
              struct v2f
              {
                  float2 uv : TEXCOORD0;
                  float4 vertex : SV_POSITION;
              };
              v2f vert (appdata v)
              {
                  v2f o;
                  o.vertex = UnityObjectToClipPos(v.vertex);
                  o.uv = v.uv;
                  return o;
              }
              sampler2D _MainTex;
              float _WipeSize;
              int _WipeDirectionX;
              int _WipeDirectionY;
              ENDCG
              // X方向
              Pass
              {
                  CGPROGRAM
                  fixed4 frag (v2f i) : SV_Target
                  {
                      // ワイプサイズを超えた場合、クリップする
                      if (i.uv.x - _WipeSize < 0)
                      {
                          return float4(0, 0, 0, 0);
                      }
                      return tex2D(_MainTex, i.uv);
                  }
                  ENDCG
              }
              // 方向指定
              Pass
              {
                  CGPROGRAM
                  fixed4 frag (v2f i) : SV_Target
                  {
                      float2 pos = i.uv.xy;
                      // 指定された向きによって反転させる
                      if (_WipeDirectionX < 0) pos.x = 1.0 - pos.x;
                      if (_WipeDirectionY < 0) pos.y = 1.0 - pos.y;
                      // 斜め方向の場合、ワイプサイズを倍にする
                      if (_WipeDirectionX != 0 && _WipeDirectionY != 0) _WipeSize *= 2.0;
                      // 向きと位置の内積からクリップするかを求める
                      const float2 wipeDirection = float2(abs(_WipeDirectionX), abs(_WipeDirectionY));
                      const float t = dot(wipeDirection, pos);
                      if (t - _WipeSize < 0)
                      {
                          return float4(0, 0, 0, 0);
                      }
                      return tex2D(_MainTex, i.uv);
                  }
                  ENDCG
              }
              // チェッカーボード
              Pass
              {
                  CGPROGRAM
                  fixed4 frag (v2f i) : SV_Target
                  {
                      // セルのサイズ
                      const float cellSizeX = 0.2f;
                      const float cellSizeY = 0.2f;
                      _WipeSize *= cellSizeX;
                      // 行番号を求めて0or1にする
                      float t = floor(i.uv.y / cellSizeY);
                      t = fmod(t, 2.0f);
                      // 奇数行ならX座標をシフトさせて縦じまワイプをかける
                      t = fmod(i.uv.x + cellSizeX/2.0 * t, cellSizeX);
                      // ワイプサイズを超えた場合、クリップする
                      if (t - _WipeSize < 0)
                      {
                          return float4(0, 0, 0, 0);
                      }
                      return tex2D(_MainTex, i.uv);
                  }
                  ENDCG
              }
          }
      }
      
      ↑3種類のワイプエフェクト
      エレキベア
      エレキベア
      共通部分は外に出してるクマね
      マイケル
      マイケル
      なお、ワイプエフェクトに関しては下記の書籍を参考にさせていただきました!
      内容はC++ですが、Unityにも応用することができるので一読して損無しです!

      HLSL シェーダーの魔導書 シェーディングの基礎からレイトレーシングまで

      エレキベア
      エレキベア
      シェーダーの日本語書籍はありがたいクマね〜〜〜
      マイケル
      マイケル
      それぞれのワイプエフェクトの実装と適用結果は下記のようになっています。

      X方向のワイプ

      マイケル
      マイケル
      まず単純にX方向にワイプする処理についてですが、これは単純にワイプ量をuvのx座標から引いてマイナスなら黒を返却、という形で実装しています。
              // X方向
              Pass
              {
                  CGPROGRAM
                  fixed4 frag (v2f i) : SV_Target
                  {
                      // ワイプサイズを超えた場合、クリップする
                      if (i.uv.x - _WipeSize < 0)
                      {
                          return float4(0, 0, 0, 0);
                      }
                      return tex2D(_MainTex, i.uv);
                  }
                  ENDCG
              }
      
      エレキベア
      エレキベア
      おお〜〜ちゃんといい感じにワイプしてるクマね

      方向指定のワイプ

      マイケル
      マイケル
      そして次に応用範囲を広げてワイプの方向を指定できるようにしたものが下記になります。
      指定した方向とUV座標の内積結果を見てクリップするかどうかを判定していますが、指定した向きによって座標位置を反転させないといけない点には注意しましょう。
              // 方向指定
              Pass
              {
                  CGPROGRAM
                  fixed4 frag (v2f i) : SV_Target
                  {
                      float2 pos = i.uv.xy;
                      // 指定された向きによって反転させる
                      if (_WipeDirectionX < 0) pos.x = 1.0 - pos.x;
                      if (_WipeDirectionY < 0) pos.y = 1.0 - pos.y;
                      // 斜め方向の場合、ワイプサイズを倍にする
                      if (_WipeDirectionX != 0 && _WipeDirectionY != 0) _WipeSize *= 2.0;
                      // 向きと位置の内積からクリップするかを求める
                      const float2 wipeDirection = float2(abs(_WipeDirectionX), abs(_WipeDirectionY));
                      const float t = dot(wipeDirection, pos);
                      if (t - _WipeSize < 0)
                      {
                          return float4(0, 0, 0, 0);
                      }
                      return tex2D(_MainTex, i.uv);
                  }
                  ENDCG
              }
      
      エレキベア
      エレキベア
      だいぶ実用的になったクマね

      チェッカーボードのワイプ

      マイケル
      マイケル
      そして最後はチェッカーボード模様のワイプ処理です!
      これは行番号を求めて奇数行の場合にはX座標をシフトさせて縦じまワイプをかけることで実装しています。
      少しややこしいですが一つ一つ見ていくと理解できてくるはずです!
               // チェッカーボード
              Pass
              {
                  CGPROGRAM
                  fixed4 frag (v2f i) : SV_Target
                  {
                      // セルのサイズ
                      const float cellSizeX = 0.2f;
                      const float cellSizeY = 0.2f;
                      _WipeSize *= cellSizeX;
                      // 行番号を求めて0or1にする
                      float t = floor(i.uv.y / cellSizeY);
                      t = fmod(t, 2.0f);
                      // 奇数行ならX座標をシフトさせて縦じまワイプをかける
                      t = fmod(i.uv.x + cellSizeX/2.0 * t, cellSizeX);
                      // ワイプサイズを超えた場合、クリップする
                      if (t - _WipeSize < 0)
                      {
                          return float4(0, 0, 0, 0);
                      }
                      return tex2D(_MainTex, i.uv);
                  }
                  ENDCG
              }
          }
      
      エレキベア
      エレキベア
      しゃれおつクマ〜〜〜〜
      行ごとにオフセットさせるやり方は応用が効きそうクマね

      おわりに

      マイケル
      マイケル
      というわけで今回はポストエフェクトの実装でした!
      どうだったかな??
      エレキベア
      エレキベア
      かけるの自体は意外と簡単だったクマが、いろいろクセもあるから注意クマね
      マイケル
      マイケル
      一回覚えてしまえばシェーダーで自由にいじれるから覚えておいて損はないね!
      これからガンガン活用していこう!!
      マイケル
      マイケル
      それでは今日はこの辺で!
      アデューー!!
      エレキベア
      エレキベア
      クマ〜〜〜〜〜

      【Unity】ポストエフェクトをシェーダーで実装する(基礎、複数エフェクト適用、描画パス切替)【シェーダー】〜完〜


      Unityグラフィックスシェーダー
      2023-01-28

      関連記事
      【Unity】Timeline × Excelでスライドショーを効率よく制作する
      2024-10-31
      【書籍紹介】「コンピュータグラフィックス」に出てくる用語をまとめる【CGエンジニア検定】
      2024-07-13
      【UE5】Niagara SimulationStageによるシミュレーション環境構築
      2024-05-30
      【Unity】Boidsアルゴリズムを用いて魚の群集シミュレーションを実装する
      2024-05-28
      【Unity】GoでのランキングAPI実装とVPSへのデプロイ方法についてまとめる【Go言語】
      2024-04-14
      【Unity】第二回 Wwiseを使用したサウンド制御 〜インタラクティブミュージック編〜
      2024-03-30
      【Unity】第一回 Wwiseを使用したサウンド制御 〜基本動作編〜
      2024-03-30
      【Unity】第二回 CRI ADXを使用したサウンド制御 〜インタラクティブミュージック編〜
      2024-03-28