知識0からのUnityShader勉強

知識0からのUnityShader勉強

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

【UnityShader】Imageのアウトラインを動かす 【3】#52

前回の成果

Imageに添ってアウトラインを回転させた。

soramamenatan.hatenablog.com


今回やること

アウトラインの数を指定します。


事前準備

Scene上にImageを配置します。
Imageに画像と今回制作するマテリアルをアタッチします。
画像はなるべく単純な形のもののほうが綺麗にアウトラインができます。

Sceneのキャプチャ

f:id:soramamenatan:20200505082926p:plain

使用した画像

f:id:soramamenatan:20200505082830p:plain

一円玉のイラスト(お金・硬貨) | かわいいフリー素材集 いらすとや:より引用


ソースコード

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

        _Speed ("Speed", float) = 1
        _Angle ("Angle", Range(0, 1)) = 1
        _OutlineNum ("Outline Num", Range(1, 10)) = 1
        _OffSet("xy : offset, zw : notUseing", Vector) = (0.5,0.5,0,0)
    }

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

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

    #include "UnityCG.cginc"

    sampler2D _MainTex;
    float4 _MainTex_TexelSize;
    fixed4 _BlurColor;
    float _BlurSize;
    half _Speed;
    half _Angle;
    int _OutlineNum;
    fixed4 _OffSet;

    static const float PI = 3.14159265;

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

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

    int RotateStep(half rad, float2 offsetUv) {
        half2x2 rotate = half2x2(cos(rad), -sin(rad), sin(rad), cos(rad));
        offsetUv = mul(rotate, offsetUv);
        offsetUv = mul(rotate, offsetUv);
        half uvAngle = atan2(offsetUv.y, offsetUv.x);
        half tolerance = (-_Angle + 0.5) * 2 * PI;
        int angleStep =  step(uvAngle, tolerance);
        return angleStep;
    }

    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;
        float2 offsetUv = v.texcoord - _OffSet.xy;
        int addStep = -_OutlineNum + 1;
        for (int i = 0; i < _OutlineNum; i++) {
            half rotateRad = (PI / _OutlineNum) * i + _Time.y * _Speed;
            addStep += RotateStep(rotateRad, offsetUv);
        }
        offsetUv *= (offsetUv * addStep);
        offsetUv = step(offsetUv, 0);
        half timeAlpha = (sin(_Time.y) + 1) / 2;
        offsetUv *= timeAlpha;
        blurColor.a *= offsetUv;
        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
        }
    }
}

前回と大きな変化はありません。


アウトラインの数を指定する

以前のやり方ですと、アウトラインが1本しかなかったので等間隔で複数本出るようにします。
以下の部分で行っています。

int addStep = -_OutlineNum + 1;
for (int i = 0; i < _OutlineNum; i++) {
    half rotateRad = (PI / _OutlineNum) * i + _Time.y * _Speed;
    addStep += RotateStep(rotateRad, offsetUv);
}
offsetUv *= (offsetUv * addStep);


考え方

前提として描画されない部分はaddStepの値が1以上描画される部分はaddStepの値が0となっています。
以下の図がイメージとなります。
わかりやすいようにaddStepが1以上の場合の箇所を白くしています。

f:id:soramamenatan:20200510111756p:plain

これを以下のソースのように、for文で角度を変えながら繰り返し行います。

for (int i = 0; i < _OutlineNum; i++) {
    half rotateRad = (PI / _OutlineNum) * i + _Time.y * _Speed;
    addStep += RotateStep(rotateRad, offsetUv);
}

まずアウトラインの数分PIを除算することにより、角度を決めます。

アウトラインの数 角度
1 0(360)
2 0(360), 180
3 0(360), 120, 240
4 0(360), 90, 180, 270

そして、RotateStep関数に角度とuv値を引数として渡します。
RotateStep関数は、前回の一定の角度を描画するの部分で詳しく解説しています。

soramamenatan.hatenablog.com

簡単に説明すると、描画されない部分の場合戻り値が1描画される部分の場合戻り値が0で返ってくる関数です。
これをaddStepに加算することにより、前提で説明したとおり描画されない部分はaddStepの値が1以上描画される部分はaddStepの値が0となるのでアウトラインの数を決めることが出来ます。

アウトラインの数が1

f:id:soramamenatan:20200510114019p:plain

アウトラインの数が2

f:id:soramamenatan:20200510114022p:plain

アウトラインの数が5

f:id:soramamenatan:20200510114027p:plain


時間でアルファ値を変化

sinカーブでアルファ値を変動させます。
特に難しいことはしていないので、説明は割愛します。

half timeAlpha = (sin(_Time.y) + 1) / 2;
offsetUv *= timeAlpha;

f:id:soramamenatan:20200510120212g:plain


結果

アウトラインが複数本出れば成功です。

f:id:soramamenatan:20200510122257g:plain

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

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

        _Speed ("Speed", float) = 1
        _Angle ("Angle", Range(0, 1)) = 1
        _OutlineNum ("Outline Num", Range(1, 10)) = 1
        _OffSet("xy : offset, zw : notUseing", Vector) = (0.5,0.5,0,0)
    }

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

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

    #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;
    half _Speed;
    half _Angle;
    int _OutlineNum;
    fixed4 _OffSet;

    static const float PI = 3.14159265;

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

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

    // 回転したときのstep
    int RotateStep(half rad, float2 offsetUv) {
        half2x2 rotate = half2x2(cos(rad), -sin(rad), sin(rad), cos(rad));
        // timeAngle回転させたときの座標
        offsetUv = mul(rotate, offsetUv);
        // timeAngle回転させたときの座標
        offsetUv = mul(rotate, offsetUv);
        // 自身の角度
        half uvAngle = atan2(offsetUv.y, offsetUv.x);
        // 許容する角度(0~1)
        half tolerance = (-_Angle + 0.5) * 2 * PI;
        int angleStep =  step(uvAngle, tolerance);
        return angleStep;
    }

    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;
        float2 offsetUv = v.texcoord - _OffSet.xy;
        // アウトラインの本数
        int addStep = -_OutlineNum + 1;
        for (int i = 0; i < _OutlineNum; i++) {
            half rotateRad = (PI / _OutlineNum) * i + _Time.y * _Speed;
            addStep += RotateStep(rotateRad, offsetUv);
        }
        offsetUv *= (offsetUv * addStep);
        // アルファ値を0or1に
        offsetUv = step(offsetUv, 0);
        // sinカーブでα値変動
        half timeAlpha = (sin(_Time.y) + 1) / 2;
        offsetUv *= timeAlpha;
        blurColor.a *= offsetUv;
        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
        }
    }
}

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