8
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

Organization

Unity shaderで自作ポストプロセスを作ろう

はじめに

こんにちは、アドベントカレンダー13日目担当の避雷です。
UnityにはGraphics.blitと呼ばれる、マテリアルを媒介にしたテクスチャの変換処理を行う関数があります。これとカメラの描画プロセスに関わるコールバック、を利用することによってディスプレイに表示される画像を通常のテクスチャと同じようにシェーダーを用いて加工することができます。
実際にやってみましょう。

実装

Shader側の実装

サンプル用のシェーダーを作る

右クリックから生成できるShaderの中には、Standard,Unlit,Computeの他にImage Effect Shaderというのがあります。これを使うとそのままPostProcess用のシェーダーの下地を用意することができます。試しにこれを生成してみると中身はこんな風になっています。

Shader "Hidden/Test"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        // No culling or depth
        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);
                // just invert the colors
                col.rgb = 1 - col.rgb;
                return col;
            }
            ENDCG
        }
    }
}

ネガポジ変換をする機能が入っているみたいですね。今回はこれをカメラに反映させてみましょう。

マテリアルを作る

名前空間がHidden/hogehogeだとマテリアル作成時に対応シェーダー一覧に表示されないのでまずはコレを変更します。
適当な命名で構いません。今回はPostEffect/Negaとでもしましょう。

先ほど生成したシェーダーを右クリックして、マテリアルを生成すると元のシェーダーをベースにしたマテリアルがProject内にできると思います。シェーダー側の準備はこれで完了です。
image.png

次はC#側でカメラへ反映する部分を実装して行きたいと思います。

C#側の実装

クラスの実装

以下のC#を実装します。

using UnityEngine;

public class CameraFilter : MonoBehaviour
{
    [SerializeField] private Material filter;

    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        Graphics.Blit(src,dest,filter);
    }
}

OnRenderImageはカメラコンポネントが取得した画像をディスプレイに表示するタイミングで呼ばれるコールバックで、srcが元画像、destが出力される画像です。

Graphics.Blitはマテリアルを用いてRenderTextureを変換する関数です。

これをCameraコンポネントを持つオブジェクトにアタッチして、インスペクタのFilterに先ほど作成した
image.png
PostEffect_Negaを入れます。実行してみると
image.png

image.png
ネガポジが反転していることが確認できました。
このままだと実行時しか反映されないのでちょっと不便です。AttributeをつけてPlaymodeに関わらず実行していてもらいましょう。


[ExecuteInEditMode]
public class CameraFilter : MonoBehaviour
{

これでも反映されるのはGameウィンドウのカメラのみでシーンの側には反映されません。これも不便なのでさらにAttributeを追加してSceneビューでもポストエフェクトが反映されるようにしましょう。

複数のエフェクトをかける

コンポネントを複数付ければポストエフェクトも同時につけることが出来ます。
image.png
image.png
ネガポジ反転とピクセル化(後述)のエフェクトが同時にかかっているのを確認できます。
これによって実装されたポストエフェクトは通常のPPSv2などとも共用できるので大変便利です。

実装例

pixel

UnityにもPixelPerfectを実装する機能がデフォルトでありますが、ポストプロセスでピクセル化することも出来ます。
https://blogs.unity3d.com/jp/2019/03/13/2d-pixel-perfect-how-to-set-up-your-unity-project-for-retro-8-bits-games/

Shader "PostEffect/Pixel"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Width("Width", Int) = 160
        _Height("Height",Int) = 90
    }
    SubShader
    {
        // No culling or depth
        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;
            float _Width;
            float _Height;

            fixed4 frag (v2f i) : SV_Target
            {
                float2 grid;
                grid.x = floor(i.uv.x * _Width) / _Width;
                grid.y = floor(i.uv.y * _Height) / _Height;
                fixed4 col = tex2D(_MainTex, grid);
                return col;
            }
            ENDCG
        }
    }
}

モノクロ

グレイスケール・ベクトルとの内積を取ることによってグレイスケールな表現をすることが出来ます。
https://ja.wikipedia.org/wiki/%E3%82%B0%E3%83%AC%E3%83%BC%E3%82%B9%E3%82%B1%E3%83%BC%E3%83%AB

Shader "PostEffect/GrayScale"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        // No culling or depth
        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
            {
                float3 col = tex2D(_MainTex, i.uv).rgb;
                float3 grayVec = float3(0.2126,0.7152,0.0722);
                float g = dot(col,grayVec);
                fixed4 result = fixed4(g,g,g,1.);
                return result;
            }
            ENDCG
        }
    }
}

image.png

歪み

ゲームの状態異常表現でよくある「グニャア…」ってやつも実装できます。

Shader "PostEffect/Nausea"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Amp ("Amp", Float) = 0.1
        _T ("T", Float) = 0.25
    }
    SubShader
    {
        // No culling or depth
        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;
            float _Amp;
            float _T;

            fixed4 frag (v2f i) : SV_Target
            {
                float2 uv = i.uv;
                uv.x += sin((i.uv.y + _Time.y) * 3.14 / _T) * _Amp;
                uv.y += sin((i.uv.x + _Time.y) * 3.14 / _T) * _Amp;
                fixed4 col = tex2D(_MainTex, uv);
                return col;
            }
            ENDCG
        }
    }
}

c3bf1f6ef77c3e4e852b6e59bc1effeb.gif

所見

ポストエフェクトを自前実装することが出来るのが大変便利です。PPSv2と組み合わせることが出来るので独自表現の実装に挑戦するのもいいかもしれません。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
8
Help us understand the problem. What are the problem?