知識0からのUnityShader勉強

知識0からのUnityShader勉強

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

【UnityShader】Imageにアウトラインをつける #49

前回の成果

ノイズで背景を歪めました。

soramamenatan.hatenablog.com


今回やること

UGUIのImageの画像にアウトラインをつけます。

github.com


事前準備

Scene上にImageを配置します。
Imageに画像と今回制作するマテリアルをアタッチします。

Sceneのキャプチャ

f:id:soramamenatan:20200419144809p:plain

使用した画像

f:id:soramamenatan:20200419144822p:plain

コタツでくつろぐぴょこのイラスト | かわいいフリー素材集 いらすとや:より引用


ソースコード

Shader "Unlit/outlineImage" {
    Properties {
        _MainTex ("Base (RGB), Alpha (A)", 2D) = "white" {}
        _BlurColor ("Blur Color", Color) = (1, 1, 1, 1)
        _BlurSize ("Blur Size", float) = 1
    }

    CGINCLUDE
    struct appdata {
        float4 vertex   : POSITION;
        float2 texcoord : TEXCOORD0;
    };

    struct v2f {
        float4 vertex   : SV_POSITION;
        half2 texcoord  : TEXCOORD0;
        float4 worldPosition : TEXCOORD1;
    };

    #include "UnityCG.cginc"

    sampler2D _MainTex;
    float4 _MainTex_TexelSize;
    fixed4 _BlurColor;
    float _BlurSize;

    v2f vert (appdata v) {
        v2f o;
        o.worldPosition = v.vertex;
        o.vertex = UnityObjectToClipPos(o.worldPosition);
        o.texcoord = v.texcoord;
        return o;
    }

    fixed4 frag(v2f v) : SV_Target {
        half4 color = (tex2D(_MainTex, v.texcoord));
        return color;
    }

    fixed4 frag_blur (v2f v) : SV_Target {
        int k = 1;
        float2 blurSize = _BlurSize * _MainTex_TexelSize.xy;
        float blurAlpha = 0;
        float2 tempCoord = float2(0,0);
        float tempAlpha;
        for (int px = -k; px <= k; px++) {
            for (int py = -k; py <= k; py++) {
                tempCoord = v.texcoord;
                tempCoord.x += px * blurSize.x;
                tempCoord.y += py * blurSize.y;
                tempAlpha = tex2D(_MainTex, tempCoord).a;
                blurAlpha += tempAlpha;
            }
        }

        half4 blurColor = _BlurColor;
        blurColor.a *= blurAlpha;
        return blurColor;
    }
    ENDCG

    SubShader {
        Tags {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Cull Off
        Lighting Off
        ZWrite Off
        ZTest [unity_GUIZTestMode]
        Blend SrcAlpha OneMinusSrcAlpha

        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag_blur
            ENDCG
        }

        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            ENDCG
        }
    }
}

2つのパスがありますが、いつもと異なる書き方をしています。


複数のパスでの共通処理

CGINCLUDEを使用すると、CGINCLUDE~ENDCGまでの間を各パスでの共通処理とすることができます。


実例

複数パスで色を変える

色を変えるだけの複数パスのシェーダーを用意します。

Shader "Unlit/cgincludePass" {
    Properties {
        _Color ("Color", Color) = (0, 0, 0, 0)
    }
    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata {
                float4 vertex : POSITION;
            };

            struct v2f {
                float4 vertex : SV_POSITION;
            };

            float4 _Color;

            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                return _Color;
            }
            ENDCG
        }
        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata {
                float4 vertex : POSITION;
            };

            struct v2f {
                float4 vertex : SV_POSITION;
            };

            float4 _Color;

            v2f vert (appdata v) {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex - 1);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                return _Color + 1;
            }
            ENDCG
        }
    }
}

結果としては、以下の画像のようになります。

f:id:soramamenatan:20200419174734p:plain

しかし変数や構造体で同じことを書いてしまい冗長です。


CGINCLUDEを使う

そこで、CGINCLUDEを使用します。

Shader "Unlit/cgincludePass" {
    Properties {
        _Color ("Color", Color) = (0, 0, 0, 0)
    }
    CGINCLUDE
    #include "UnityCG.cginc"
    struct appdata {
        float4 vertex : POSITION;
    };

    struct v2f {
        float4 vertex : SV_POSITION;
    };

    float4 _Color;

    v2f vertFirst (appdata v) {
        v2f o;
        o.vertex = UnityObjectToClipPos(v.vertex);
        return o;
    }

    fixed4 fragFirst (v2f i) : SV_Target {
        return _Color;
    }

    v2f vertSecond (appdata v) {
        v2f o;
        o.vertex = UnityObjectToClipPos(v.vertex - 1);
        return o;
    }

    fixed4 fragSecond (v2f i) : SV_Target {
        return _Color + 1;
    }
    ENDCG

    SubShader {
        Tags { "RenderType"="Opaque" }
        LOD 100
        Pass {
            CGPROGRAM
            #pragma vertex vertFirst
            #pragma fragment fragFirst
            ENDCG
        }
        Pass {
            CGPROGRAM
            #pragma vertex vertSecond
            #pragma fragment fragSecond
            ENDCG
        }
    }
}

CGINCLUDEを使用し2つのパスで共通の処理であったものをまとめることソースコードを綺麗にすることができます。
もちろん、描画結果は同じとなります。

f:id:soramamenatan:20200419180046p:plain

light11.hatenadiary.com


各用語の説明

_MainTex_TexelSize

テクスチャのサイズを扱う変数です。

テクスチャ名_TexelSize

で定義することができます。

xyzw 意味
x 1.0 / width
y 1.0 / height
z width
w height


IgnoreProjector

値がTrueの場合、シェーダーを使用しているオブジェクトはプロジェクターの影響を受けません
部分的に透過なオブジェクトに使用します。


PreviewType

マテリアルのインスペクターでどのようにプレビューされるかを変更します。
デフォルトはsphereです。

デフォルト

f:id:soramamenatan:20200419181133p:plain

"PreviewType"="Plane"f:id:soramamenatan:20200419181130p:plain


CanUseSpriteAtlas

シェーダーがスプライトに用いられる場合にで、何か不具合があるときにFalseに設定されます。


ZTest [unity_GUIZTestMode]

描画されるマテリアルのCanvasのRenderModeの設定によって自動的に変化するものです。

RenderMode unity_GUIZTestModeの値
Screen Space - Overlay Always
Screen Space - Camera LEqual
World Space LEqual

docs.unity3d.com

zenigane138.hateblo.jp


frag_blur関数の説明

今回のシェーダーの肝はfrag_blur関数なので解説していこうと思います。


変数の定義

主に前半部分です。

int k = 1;
float2 blurSize = _BlurSize * _MainTex_TexelSize.xy;
float blurAlpha = 0;
float2 tempCoord = float2(0,0);
float tempAlpha;
int k = 1;

kは後のfor文で使用するループの回数(-k~k)になります。
今回の場合、-1~1でループさせたいので1としています。

float2 blurSize = _BlurSize * _MainTex_TexelSize.xy;

ブラーの大きさを決めてます。
_MainTex_TexelSize.xyは1.0/画像の大きさなので気をつけてください。
僕はここでハマりました。


アウトラインをつける

今回の肝の部分となります。

for (int px = -k; px <= k; px++) {
    for (int py = -k; py <= k; py++) {
        tempCoord = v.texcoord;
        tempCoord.x += px * blurSize.x;
        tempCoord.y += py * blurSize.y;
        tempAlpha = tex2D(_MainTex, tempCoord).a;
        blurAlpha += tempAlpha;
    }
}
half4 blurColor = _BlurColor;
blurColor.a *= blurAlpha;
return blurColor;
for (int px = -k; px <= k; px++) {
    for (int py = -k; py <= k; py++) {
        tempCoord = v.texcoord;
        tempCoord.x += px * blurSize.x;
        tempCoord.y += py * blurSize.y;
        tempAlpha = tex2D(_MainTex, tempCoord).a;
        blurAlpha += tempAlpha;
    }
}

for文では、テクスチャをblurSize分ズラしたときのアルファ値を求めています。
イメージは以下となります。

イメージ1

元画像のUV値をもらいます。
f:id:soramamenatan:20200419184355p:plain

イメージ2

テクスチャをズラします。
この時、pxは-1でpyは-1~1になります。
f:id:soramamenatan:20200419184359p:plain

イメージ2のズラす値

それぞれblurSize分ズラします。 f:id:soramamenatan:20200419184404p:plain

これを繰り返すことによって、アウトラインが表示できます。
今回のやり方はテクスチャの大きさを拡大しているので、

Pass {
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    ENDCG
}

コメントアウトすると以下のようになります。

f:id:soramamenatan:20200419190019p:plain

この方法はズラして拡大しているので、blurSizeが大きいと表示がおかしくなってしまうので気をつけてください。

blurSizeが20の場合

ズラす値が大きくなっているので、うさぎの耳のアウトラインが3つになってしまっている。
f:id:soramamenatan:20200419190224p:plain

half4 blurColor = _BlurColor;
blurColor.a *= blurAlpha;
return blurColor;

最後にアルファ値を計算して終了となります。


結果

Imageにアウトラインがつけば成功です。

f:id:soramamenatan:20200419191136p:plain


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

Shader "Unlit/outlineImage" {
    Properties {
        _MainTex ("Base (RGB), Alpha (A)", 2D) = "white" {}
        _BlurColor ("Blur Color", Color) = (1, 1, 1, 1)
        _BlurSize ("Blur Size", float) = 1
    }

    // passで共通処理をまとめる
    CGINCLUDE
    struct appdata {
        float4 vertex   : POSITION;
        float2 texcoord : TEXCOORD0;
    };

    struct v2f {
        float4 vertex   : SV_POSITION;
        half2 texcoord  : TEXCOORD0;
        float4 worldPosition : TEXCOORD1;
    };

    #include "UnityCG.cginc"

    sampler2D _MainTex;
    // MainTexのサイズを扱う
    // x : 1.0 / width
    // y : 1.0 / height
    // z : width
    // w : height
    float4 _MainTex_TexelSize;
    fixed4 _BlurColor;
    float _BlurSize;

    v2f vert (appdata v) {
        v2f o;
        o.worldPosition = v.vertex;
        o.vertex = UnityObjectToClipPos(o.worldPosition);
        o.texcoord = v.texcoord;
        return o;
    }

    fixed4 frag(v2f v) : SV_Target {
        half4 color = (tex2D(_MainTex, v.texcoord));
        return color;
    }

    fixed4 frag_blur (v2f v) : SV_Target {
        // -1 ~ 1でforを回す
        int k = 1;
        float2 blurSize = _BlurSize * _MainTex_TexelSize.xy;
        float blurAlpha = 0;
        float2 tempCoord = float2(0,0);
        float tempAlpha;
        // xy方向にblurSize分ズラし、アルファ値を計算
        for (int px = -k; px <= k; px++) {
            for (int py = -k; py <= k; py++) {
                tempCoord = v.texcoord;
                tempCoord.x += px * blurSize.x;
                tempCoord.y += py * blurSize.y;
                tempAlpha = tex2D(_MainTex, tempCoord).a;
                blurAlpha += tempAlpha;
            }
        }

        half4 blurColor = _BlurColor;
        blurColor.a *= blurAlpha;
        return blurColor;
    }
    ENDCG

    SubShader {
        Tags {
            "Queue"="Transparent"
            // プロジェクターの影響を受けないように
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            // マテリアルのincpecterでの表示をplaneに
            "PreviewType"="Plane"
            // spriteに不具合がある場合にfalseになる
            "CanUseSpriteAtlas"="True"
        }

        Cull Off
        Lighting Off
        ZWrite Off
        // CanvasのRenderModeによって動的に変化
        // Screen Space - Overlay : Always
        // Screen Space - Camera : LEqual
        // World Space : LEqual
        ZTest [unity_GUIZTestMode]
        Blend SrcAlpha OneMinusSrcAlpha

        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag_blur
            ENDCG
        }

        Pass {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            ENDCG
        }
    }
}

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