
マイケル
みなさんこんにちは!
マイケルです!
マイケルです!

エレキベア
こんにちクマ〜〜〜

マイケル
今日もシェーダーを触っていきます!
今回はポストエフェクトの実装だ!
今回はポストエフェクトの実装だ!

エレキベア
ポストエフェクトってUnityだとパッケージでかけれなかったクマ??

マイケル
そう、UnityだとPost-Processing Version 2パッケージを使ってポストエフェクトをかけることが多いと思うけど、自分でシェーダーを実装してかけることもできるんだ!

エレキベア
自分で実装できたら思うようにできそうクマね

マイケル
早速触っていこう!!
ポストエフェクトの実装方法

マイケル
ポストエフェクト用(2D用)のシェーダーの雛形は「Shader > Image Effect Shader」から作成することができます。


マイケル
作成直後は下記のような状態になっていて、
デフォルトでネガポジ反転(RGB値を反転させたもの)が実装されています。
デフォルトでネガポジ反転(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へコピーする際にマテリアルを適用することで最終的な画像にエフェクトをかける、といった流れになっています。
src: 最終的に描画される画像(入力)
dest: 最終的に描画される画像(出力)
として渡されてくるため、Graphics.Blit関数でsrcからdestへコピーする際にマテリアルを適用することで最終的な画像にエフェクトをかける、といった流れになっています。
OnRenderImage | Unityスクリプトリファレンス
Graphics-Blit | Unityスクリプトリファレンス

エレキベア
なるほどクマ
最終的な描画の処理になるクマね
最終的な描画の処理になるクマね

マイケル
ポストエフェクトをかける前後では下記のように変化することが確認できます!



エレキベア
おお〜〜〜
ちゃんとエフェクトがかかっているクマ〜〜〜
ちゃんとエフェクトがかかっているクマ〜〜〜
いくつかポストエフェクトを作ってみる

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

マイケル
まずはグレースケール!
こちらはNTSC加重平均法というものを用いて、RGBの輝度として(0.298, 0.586, 0.114)を重みとして乗算してモノクロ化します。
こちらはNTSC加重平均法というものを用いて、RGBの輝度として(0.298, 0.586, 0.114)を重みとして乗算してモノクロ化します。
参考:
より自然なグレースケール変換 | ゆるゆるプログラミング

エレキベア
なんという絶妙な数値クマ・・・

マイケル
今回は数値はざっくりとしたものを指定して下記のように実装してみます。
Frag関数の処理を少し修正しただけですね
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さんの下記記事を参考にさせていただきました!
こちらは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を指定してみると、下記のように描画されることが確認できます。
パラメータはとりあえず下記のようにサンプル数:12、強さ:0.7を指定してみると、下記のように描画されることが確認できます。



エレキベア
ちゃんと外側に引き伸ばされたようなブラーになったクマ〜〜

マイケル
こんな感じで繰り返しながらサンプリングしていく手法はいろいろと応用が効きそうだね!
複数エフェクトの適用

マイケル
次にこれらのエフェクトを複数かけたい場合にどうするか?についてですが、
これはエフェクトをかける用のRenderTextureを自前で用意して最終的にdestにかける方法で適用することができます。
実装は下記のようになります。
これはエフェクトをかける用の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で退避しておくクマね
エフェクトをかけている間は自作のRenderTextureで退避しておくクマね

マイケル
ちょっと面倒くさいけど、下記のように複数かけれることが確認できました!


エレキベア
段々使いこなしてきたクマね
描画パスの切替とワイプ処理

マイケル
最後に、シェーダーコード内に複数パスを記述して切り替える方法についても紹介します!
描画するパスに関しては下記のようにGraphics.Blitの第4引数に渡すことで指定することができます。
描画するパスに関しては下記のように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パラメータにワイプ量を渡すようにも実装しています。
合わせて_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として分けて実装しています。
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にも応用することができるので一読して損無しです!
内容は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座標の内積結果を見てクリップするかどうかを判定していますが、指定した向きによって座標位置を反転させないといけない点には注意しましょう。
指定した方向と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座標をシフトさせて縦じまワイプをかけることで実装しています。
少しややこしいですが一つ一つ見ていくと理解できてくるはずです!
これは行番号を求めて奇数行の場合には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】ポストエフェクトをシェーダーで実装する(基礎、複数エフェクト適用、描画パス切替)【シェーダー】〜完〜
コメント