知識0からのUnityShader勉強

知識0からのUnityShader勉強

UnityのShaderをメインとして、0から学んでいくブログです。

【UnityShader】ソーベルフィルタ #83

前回の成果

ロドリゲスの回転公式を理解した。

soramamenatan.hatenablog.com


今回やること

ソーベルフィルタを使用して、鉛筆で書いたようなShaderを制作します。


事前準備

Scene上にオブジェクトを配置します。
自分はアニメーションするものを配置したかったのでUnityちゃんを配置しました。

f:id:soramamenatan:20201202152542p:plain

カメラに今回制作するScriptをアタッチします。
そのScriptのInspectorに今回制作したShaderのマテリアルをアタッチします。

f:id:soramamenatan:20201202152538p:plain


ソースコード

Shader側

Shader "Unlit/PencilNoiseShader" {
    Properties {
        _MainTex ("Texture", 2D) = "white" {}
        _Brightness ("Brightness", Range(-1.0, 1.0)) = 0.2
        _OutlineThick ("_OutlineThick", Range(0.0, 1.0)) = 0.1
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 100

        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;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _MainTex_TexelSize;
            float _Brightness;
            float _OutlineThick;

            float rand(float3 value) {
                return frac(sin(dot(value.xyz, float3(12.9898, 78.233, 56.787))) * 43758.5453);
            }

            float noise(float3 pos) {
                float3 floorPos = floor(pos);
                float3 stepPos = smoothstep(0, 1, frac(pos));
                float4 a = float4(
                    rand(floorPos + float3(0, 0, 0)),
                    rand(floorPos + float3(1, 0, 0)),
                    rand(floorPos + float3(0, 1, 0)),
                    rand(floorPos + float3(1, 1, 0)));
                float4 b = float4(
                    rand(floorPos + float3(0, 0, 1)),
                    rand(floorPos + float3(1, 0, 1)),
                    rand(floorPos + float3(0, 1, 1)),
                    rand(floorPos + float3(1, 1, 1)));
                a = lerp(a, b, stepPos.z);
                a.xy = lerp(a.xy, a.zw, stepPos.y);
                return lerp(a.x, a.y, stepPos.x);
            }

            float perlin(float3 pos) {
                return
                    (noise(pos) * 32 +
                    noise(pos * 2 ) * 16 +
                    noise(pos * 4) * 8 +
                    noise(pos * 8) * 4 +
                    noise(pos * 16) * 2 +
                    noise(pos * 32) ) / 63;
            }

            float monochrome(float3 col) {
                return 0.299 * col.r + 0.587 * col.g + 0.114 * col.b;
            }

            half3 Sobel(float2 uv) {
                float diffU = _MainTex_TexelSize.x * _OutlineThick;
                float diffV = _MainTex_TexelSize.y * _OutlineThick;
                half3 col00 = tex2D(_MainTex, uv + half2(-diffU, -diffV));
                half3 col01 = tex2D(_MainTex, uv + half2(-diffU, 0.0));
                half3 col02 = tex2D(_MainTex, uv + half2(-diffU, diffV));
                half3 col10 = tex2D(_MainTex, uv + half2(0.0, -diffV));
                half3 col12 = tex2D(_MainTex, uv + half2(0.0, diffV));
                half3 col20 = tex2D(_MainTex, uv + half2(diffU, -diffV));
                half3 col21 = tex2D(_MainTex, uv + half2(diffU, 0.0));
                half3 col22 = tex2D(_MainTex, uv + half2(diffU, diffV));

                half3 horizontalColor = 0;
                horizontalColor += col00 * -1.0;
                horizontalColor += col01 * -2.0;
                horizontalColor += col02 * -1.0;
                horizontalColor += col20;
                horizontalColor += col21 * 2.0;
                horizontalColor += col22;

                half3 verticalColor = 0;
                verticalColor += col00;
                verticalColor += col10 * 2.0;
                verticalColor += col20;
                verticalColor += col02 * -1.0;
                verticalColor += col12 * -2.0;
                verticalColor += col22 * -1.0;

                half3 outline = sqrt(horizontalColor * horizontalColor + verticalColor * verticalColor);
                return outline;
            }


            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                float2 screenUV = i.uv * _ScreenParams.xy;
                i.uv += (float2(perlin(float3(screenUV, _Time.y)), perlin(float3(screenUV, _Time.y)))) * 0.01;
                float col = monochrome(tex2D(_MainTex, i.uv)) + _Brightness;
                col -= Sobel(i.uv);
                col *= perlin(float3(screenUV, _Time.y * 10)) * 0.5f + 0.8f;
                return float4(col, col, col, 1);
            }
            ENDCG
        }
    }
}

Script側

using UnityEngine;

public class PencilNoise : MonoBehaviour {

    [SerializeField]
    private Material _material;

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

こちらはポストエフェクトの処理になります。
OnRenderImage関数に関しては、こちらを参照してください。

soramamenatan.hatenablog.com


ソーベルフィルタとは

ソーベルフィルタとは、アウトラインを検出する際に用いられる空間フィルタの1種です。
ソーベルフィルタは、輝度差が少ないエッジも強調される特徴があります。
ですので、人間が目で見た時にエッジに見える場所が強調されやすいです。
以下の画像を見て頂けると分かりやすいと思います。

f:id:soramamenatan:20201202153513p:plain

1次微分フィルタ Sobel(ソーベル)フィルタ - 画像のエッジ抽出|MiVLog(ミブログ):より引用


ソーベルフィルタの仕組み

ソーベルフィルタは、今から処理するピクセルを中心に8方向の色情報を取り出します。
その情報に対して、以下のカーネル(テーブル)を使用することで値を補正します。

ソーベルフィルタのカーネル  \displaystyle

横方向

K_{x} = 
\begin{pmatrix}
1 & 0 & -1 \\
2 & 0 & -2 \\
1 & 0 & -1 
\end{pmatrix}


, 縦方向

K_{y} = 
\begin{pmatrix}
1 & 2 & 1 \\
0 & 0 & 0 \\
-1 & -2 & -1 
\end{pmatrix}

本来処理しようとしている中心のピクセルには0が乗算されます。
その代わり、横方向であれば左右1ピクセルずつズレた座標、縦方向であれば上下1ピクセルずつズレた座標に、係数が乗算されます。
適応する範囲の上下左右中心の9ピクセル全て同じ色であれば、係数を乗算しても結果は0となります。
色の差が大きいと値も大きくなり、よって色の差が激しいピクセルを強調することができます。


実際の処理

half3 Sobel(float2 uv) {
    float diffU = _MainTex_TexelSize.x * _OutlineThick;
    float diffV = _MainTex_TexelSize.y * _OutlineThick;
    half3 col00 = tex2D(_MainTex, uv + half2(-diffU, -diffV));
    half3 col01 = tex2D(_MainTex, uv + half2(-diffU, 0.0));
    half3 col02 = tex2D(_MainTex, uv + half2(-diffU, diffV));
    half3 col10 = tex2D(_MainTex, uv + half2(0.0, -diffV));
    half3 col12 = tex2D(_MainTex, uv + half2(0.0, diffV));
    half3 col20 = tex2D(_MainTex, uv + half2(diffU, -diffV));
    half3 col21 = tex2D(_MainTex, uv + half2(diffU, 0.0));
    half3 col22 = tex2D(_MainTex, uv + half2(diffU, diffV));

    half3 horizontalColor = 0;
    horizontalColor += col00 * -1.0;
    horizontalColor += col01 * -2.0;
    horizontalColor += col02 * -1.0;
    horizontalColor += col20;
    horizontalColor += col21 * 2.0;
    horizontalColor += col22;

    half3 verticalColor = 0;
    verticalColor += col00;
    verticalColor += col10 * 2.0;
    verticalColor += col20;
    verticalColor += col02 * -1.0;
    verticalColor += col12 * -2.0;
    verticalColor += col22 * -1.0;

    half3 outline = sqrt(horizontalColor * horizontalColor + verticalColor * verticalColor);
    return outline;
}

こちらがソーベルフィルタの処理の部分となります。
_MainTex_TexelSize.xyは縦横の解像度を取得できるので、それを元にカーネルを制作しています。
次に、制作したカーネルを元に周りの色の縦方向と横方向の色を求めます。
最後に、縦方向と横方向の値をそれぞれ二乗してから平方根を求めます。
これは値のプラスマイナスを考慮することなく、値の純粋な大きさのみを見るためになります。


パーリンノイズ

float noise(float3 pos) {
    float3 floorPos = floor(pos);
    float3 stepPos = smoothstep(0, 1, frac(pos));
    float4 a = float4(
        rand(floorPos + float3(0, 0, 0)),
        rand(floorPos + float3(1, 0, 0)),
        rand(floorPos + float3(0, 1, 0)),
        rand(floorPos + float3(1, 1, 0)));
    float4 b = float4(
        rand(floorPos + float3(0, 0, 1)),
        rand(floorPos + float3(1, 0, 1)),
        rand(floorPos + float3(0, 1, 1)),
        rand(floorPos + float3(1, 1, 1)));
    a = lerp(a, b, stepPos.z);
    a.xy = lerp(a.xy, a.zw, stepPos.y);
    return lerp(a.x, a.y, stepPos.x);
}

float perlin(float3 pos) {
    return
        (noise(pos) * 32 +
        noise(pos * 2 ) * 16 +
        noise(pos * 4) * 8 +
        noise(pos * 8) * 4 +
        noise(pos * 16) * 2 +
        noise(pos * 32) ) / 63;
}

パーリンノイズとは、テクスチャ作成技法の1つです。
特徴として、疑似乱数的な見た目ですが、細部のスケール感が一定です。

ソースコードやパーリンノイズの詳しい説明に関しては、以前の記事で説明しているのでそちらを参考にしてください。

soramamenatan.hatenablog.com


fragment

ソーベルフィルタとパーリンノイズの理解は出来たので、fragment shaderに対応していきます。

モノクロ(グレースケール)

モノクロ関数を使用して、カメラに映るものをグレースケールにさせます。

float col = monochrome(tex2D(_MainTex, i.uv)) + _Brightness;

f:id:soramamenatan:20201203142856g:plain


手ぶれっぽくする

パーリンノイズを使用し、UV値をズラします。
そうすることにより、輪郭がズレて手ぶれのようになります。

float2 screenUV = i.uv * _ScreenParams.xy;
i.uv += (float2(perlin(float3(screenUV, _Time.y)), perlin(float3(screenUV, _Time.y)))) * 0.01;

f:id:soramamenatan:20201203151135g:plain


ソーベルフィルタ

今回の本題であるソーベルフィルタをかけます。
ソーベルフィルタをかけることにより、エッジを検出してアウトラインを強調させます。

col -= Sobel(i.uv);

f:id:soramamenatan:20201203151440g:plain

ソーベルフィルタのみにすると以下のようになります。

f:id:soramamenatan:20201203151634g:plain


ブラーをかける

最後にパーリンノイズをかけます。
より鉛筆っぽさが出ると思います。

col *= perlin(float3(screenUV, _Time.y * 10)) * 0.5f + 0.8f;

f:id:soramamenatan:20201203151854g:plain


結果

鉛筆風になれば成功です。

f:id:soramamenatan:20201203151854g:plain


パラメータを変化させることで見た目を変化させることができます。

Brightnessを-1

f:id:soramamenatan:20201203154336g:plain

Brightnessを1

f:id:soramamenatan:20201203154342g:plain

OutlineThickを0

f:id:soramamenatan:20201203154351g:plain

OutlineThickを1

f:id:soramamenatan:20201203154359g:plain


ソースコードにコメントを付与

Shader "Unlit/PencilNoiseShader" {
    Properties {
        _MainTex ("Texture", 2D) = "white" {}
        _Brightness ("Brightness", Range(-1.0, 1.0)) = 0.2
        _OutlineThick ("OutlineThick", Range(0.0, 1.0)) = 0.1
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 100

        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;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _MainTex_TexelSize;
            float _Brightness;
            float _OutlineThick;

            // 乱数生成
            float rand(float3 value) {
                return frac(sin(dot(value.xyz, float3(12.9898, 78.233, 56.787))) * 43758.5453);
            }

            // ノイズ
            float noise(float3 pos) {
                float3 floorPos = floor(pos);
                float3 stepPos = smoothstep(0, 1, frac(pos));
                float4 a = float4(
                    rand(floorPos + float3(0, 0, 0)),
                    rand(floorPos + float3(1, 0, 0)),
                    rand(floorPos + float3(0, 1, 0)),
                    rand(floorPos + float3(1, 1, 0)));
                float4 b = float4(
                    rand(floorPos + float3(0, 0, 1)),
                    rand(floorPos + float3(1, 0, 1)),
                    rand(floorPos + float3(0, 1, 1)),
                    rand(floorPos + float3(1, 1, 1)));
                a = lerp(a, b, stepPos.z);
                a.xy = lerp(a.xy, a.zw, stepPos.y);
                return lerp(a.x, a.y, stepPos.x);
            }

            // パーリンノイズ
            float perlin(float3 pos) {
                return
                    (noise(pos) * 32 +
                    noise(pos * 2 ) * 16 +
                    noise(pos * 4) * 8 +
                    noise(pos * 8) * 4 +
                    noise(pos * 16) * 2 +
                    noise(pos * 32) ) / 63;
            }

            // モノクロ
            float monochrome(float3 col) {
                return 0.299 * col.r + 0.587 * col.g + 0.114 * col.b;
            }

            // ソーベルフィルタ
            half3 Sobel(float2 uv) {
                // 自身を中心とした上下左右8方向のピクセルをサンプリング
                float diffU = _MainTex_TexelSize.x * _OutlineThick;
                float diffV = _MainTex_TexelSize.y * _OutlineThick;
                half3 col00 = tex2D(_MainTex, uv + half2(-diffU, -diffV));
                half3 col01 = tex2D(_MainTex, uv + half2(-diffU, 0.0));
                half3 col02 = tex2D(_MainTex, uv + half2(-diffU, diffV));
                half3 col10 = tex2D(_MainTex, uv + half2(0.0, -diffV));
                half3 col12 = tex2D(_MainTex, uv + half2(0.0, diffV));
                half3 col20 = tex2D(_MainTex, uv + half2(diffU, -diffV));
                half3 col21 = tex2D(_MainTex, uv + half2(diffU, 0.0));
                half3 col22 = tex2D(_MainTex, uv + half2(diffU, diffV));

                // 横方向カーネル
                half3 horizontalColor = 0;
                horizontalColor += col00 * -1.0;
                horizontalColor += col01 * -2.0;
                horizontalColor += col02 * -1.0;
                horizontalColor += col20;
                horizontalColor += col21 * 2.0;
                horizontalColor += col22;

                // 縦方向カーネル
                half3 verticalColor = 0;
                verticalColor += col00;
                verticalColor += col10 * 2.0;
                verticalColor += col20;
                verticalColor += col02 * -1.0;
                verticalColor += col12 * -2.0;
                verticalColor += col22 * -1.0;

                // 色の差を求める
                half3 outline = sqrt(horizontalColor * horizontalColor + verticalColor * verticalColor);
                return outline;
            }


            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                // 手ぶれのようにする
                float2 screenUV = i.uv * _ScreenParams.xy;
                i.uv += (float2(perlin(float3(screenUV, _Time.y)), perlin(float3(screenUV, _Time.y)))) * 0.01;
                // モノクロ
                float col = monochrome(tex2D(_MainTex, i.uv)) + _Brightness;
                // ソーベルフィルタ
                col -= Sobel(i.uv);
                // パーリンノイズで鉛筆風を強める
                col *= perlin(float3(screenUV, _Time.y * 10)) * 0.5f + 0.8f;
                return float4(col, col, col, 1);
            }
            ENDCG
        }
    }
}

今回は以上となります。
ここまでご視聴ありがとうございました。


ユニティちゃんライセンス

この作品はユニティちゃんライセンス条項の元に提供されています

参考サイト様

light11.hatenadiary.com

wgld.org

wordpress.notargs.com