知識0からのUnityShader勉強

知識0からのUnityShader勉強

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

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

前回の成果

Imageにアウトラインをつけた。

soramamenatan.hatenablog.com


今回やること

前回つけたアウトラインを動かそうと思います。


事前準備

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

Sceneのキャプチャ

f:id:soramamenatan:20200419144809p:plain

使用した画像

f:id:soramamenatan:20200419144822p: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
        _OffSet("xy : offset, zw : notUseing", Vector) = (0.5,0.5,0,0)
    }

    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;
    half _Speed;
    half _Angle;
    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;
        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;
        half timeAngle = _Time.y * _Speed;
        half2x2 rotate = half2x2(cos(timeAngle), -sin(timeAngle), sin(timeAngle), cos(timeAngle));
        float2 offsetUv = v.texcoord - _OffSet.xy;
        offsetUv = mul(rotate, offsetUv);
        offsetUv = step(offsetUv, 0);
        blurColor.a *= offsetUv;
        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
        }
    }
}


回転させる

half2x2 rotate = half2x2(cos(timeAngle), -sin(timeAngle), sin(timeAngle), cos(timeAngle));
float2 offsetUv = v.texcoord - _OffSet.xy;
offsetUv = mul(rotate, offsetUv);

まずはImageを考慮せずに、回転させていきたいと思います。


回転行列

回転行列(かいてんぎょうれつ、英: rotation matrix)とは、ユークリッド空間内における原点中心の回転変換の表現行列のことである。

回転行列 - Wikipedia:より引用

ベクトルと乗算することにより原点を中心として \displaystyle \theta回転させることができる行列の認識で問題ないです。

公式は以下になります。

回転行列の公式  \displaystyle
R(\theta) = \begin{pmatrix} cos\theta&-sin\theta \\ sin\theta&cos\theta \end{pmatrix}

どうしてこの式で求めることが出来るのか解説します。


極座標

行いたいこと

f:id:soramamenatan:20200425152735p:plain

座標平面上における回転の公式 - 具体例で学ぶ数学:より引用

今回やりたいことは、上記の図のように
 \displaystyle P(x,y) \displaystyle \theta回転させて点\displaystyle (x',y') (画像では\displaystyle(X,Y))にすることです。
そのためには、加法定理を知る必要があるのですがあります。
しかし、その加法定理を理解するには極座標を知る必要があります。


極座標とは

極座標とは、原点からの距離である\displaystyle rと角度である\displaystyle \thetaを使い点を表すものです。

f:id:soramamenatan:20200425153442p:plain

直交座標と極座標(2次元)の変換とメリットの比較 | 高校数学の美しい物語:より引用

上記の図の場合、点\displaystyle Aは普段\displaystyle (x,y)と表します。
これを直交座標と呼びます。
極座標の場合、\displaystyle A\displaystyle (r, \theta)と表します。

直交座標 点A = \displaystyle (x,y)
極座標 点A = \displaystyle (r, \theta)


変換

同じ点が直交座標では\displaystyle (x,y)極座標では\displaystyle (r, \theta)と表されている場合、三角関数より
x= \displaystyle
 r \cos \theta,y=r \sin \thetaが成り立ちます。

極座標を直交座標に変換 x= \displaystyle 
 r \cos \theta,y=r \sin \theta


加法定理

極座標を理解したので、加法定理に移ります。

加法定理とは

加法定理(かほうていり、英: addition theorem)、加法法則(かほうほうそく、英: addition law/rule)あるいは加法公式(かほうこうしき、英: addition formula)とは、ある関数や対応・写像について、2 つ以上の変数の和として記される変数における値を、それぞれの変数における値によって書き表したもの。

加法定理 - Wikipedia:より引用

とのことです。


加法定理の公式

加法定理は以下の6つがあります。

加法定理
  • \displaystyle \sin(\alpha +  \beta)=\sin 
 \alpha \cos \beta + \cos \alpha \sin \beta
  • \displaystyle 
 \sin (\alpha − \beta)= \sin \alpha \cos \beta− \cos \alpha \sin \beta
  • \displaystyle \cos (\alpha + \beta)=\cos \alpha \cos \beta − \sin \alpha \sin \beta
  • \displaystyle 
 \cos (\alpha − \beta)= \cos \alpha \cos \beta + \sin \alpha \sin \beta
  • \displaystyle \tan (\alpha + \beta)= \frac{ \tan \alpha + \tan \beta}{1 − \tan \alpha \tan \beta}
  • \displaystyle \tan (\alpha − \beta)= \frac{\tan \alpha − \tan \beta}{1 + \tan \alpha \tan \beta}


回転行列で求めることが出来る理由

f:id:soramamenatan:20200425162454p:plain

回転行列 : 数学 – FindxFine:より引用

まず、点P\displaystyle (x,y)と点Q\displaystyle (x,'y')極座標に置き換えます。

点P\displaystyle (x,y)極座標
  • \displaystyle x = r \cos \alpha
  • \displaystyle y = r \sin \alpha
点Q\displaystyle (x,'y')極座標
  • \displaystyle x' = r \cos (\alpha + \beta)
  • \displaystyle y' = r \sin (\alpha + \beta)

そして、点Q\displaystyle (x,'y')極座標を加法定理を使用して置換します。

極座標を置換
  • \displaystyle x' = r \cos (\alpha + \beta) = r (\cos \alpha \cos \beta - \sin \alpha \sin \beta)
  • \displaystyle y' = r \sin (\alpha + \beta) = r (\sin \alpha \cos \beta + \cos \alpha \sin \beta)

加法定理を使用したら、極座標で求めた点P\displaystyle (x,y)を代入します。

点P\displaystyle (x,y)極座標
  • \displaystyle x = r \cos \alpha
  • \displaystyle y = r \sin \alpha
代入
  • \displaystyle x' = x \cos \beta - y \sin \beta
  • \displaystyle y' = x \sin \beta + y \cos \beta

最後にこれを行列にします。

行列に変換  \displaystyle
\begin{pmatrix}
x'
\\
y'
\end{pmatrix}

=

\begin{pmatrix}
cos \beta && -sin \beta
\\
sin \beta && cos \beta
\end{pmatrix}

\begin{pmatrix}
x
\\
y
\end{pmatrix}

回転行列の公式は以下ですので、

 \displaystyle
R(\theta) = \begin{pmatrix} cos\theta&-sin\theta \\ sin\theta&cos\theta \end{pmatrix}

回転後の座標を求めることができます。

half2x2 rotate = half2x2(cos(timeAngle), -sin(timeAngle), sin(timeAngle), cos(timeAngle));
float2 offsetUv = v.texcoord - _OffSet.xy;
offsetUv = mul(rotate, offsetUv);

以上のことからrotate(回転座標)とoffsetUv(点P)を乗算することにより回転座標を求めることができます。


結果

以下のように回転すれば成功です。

f:id:soramamenatan:20200425164845g: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
        _OffSet("xy : offset, zw : notUseing", Vector) = (0.5,0.5,0,0)
    }

    CGINCLUDE
    // passで共通処理をまとめる
    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;
    half _Speed;
    half _Angle;
    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;
        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;
        half timeAngle = _Time.y * _Speed;
        // 回転座標
        half2x2 rotate = half2x2(cos(timeAngle), -sin(timeAngle), sin(timeAngle), cos(timeAngle));
        // 回転の中心座標
        float2 offsetUv = v.texcoord - _OffSet.xy;
        // timeAngle回転させたときの座標
        offsetUv = mul(rotate, offsetUv);
        // アルファ値を0or1に
        offsetUv = step(offsetUv, 0);
        blurColor.a *= offsetUv;
        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
        }
    }
}

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