知識0からのUnityShader勉強

知識0からのUnityShader勉強

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

【UnityShader】uGUIのImageをズラす 【3】#72

前回の成果

uGUIのImageをズラすScriptのUpdate関数まで学んだ。

soramamenatan.hatenablog.com


今回やること

前回、前々回に引き続き、uGUIのImageをズラすことをしていこうと思います。


Shader

Shader "Unlit/OnMouseMoveImage" {
    Properties {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)
        _StencilComp ("Stencil Comparison", Float) = 8
        _Stencil ("Stencil ID", Float) = 0
        _StencilOp ("Stencil Operation", Float) = 0
        _StencilWriteMask ("Stencil Write Mask", Float) = 255
        _StencilReadMask ("Stencil Read Mask", Float) = 255
        _ColorMask ("Color Mask", Float) = 15
        [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
    }

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

        Stencil {
            Ref [_Stencil]
            Comp [_StencilComp]
            Pass [_StencilOp]
            ReadMask [_StencilReadMask]
            WriteMask [_StencilWriteMask]
        }

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

        Pass {
            Name "Default"
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0

            #include "UnityCG.cginc"
            #include "UnityUI.cginc"

            #pragma multi_compile_local _ UNITY_UI_CLIP_RECT
            #pragma multi_compile_local _ UNITY_UI_ALPHACLIP

            struct appdata_t {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                float2 texcoord  : TEXCOORD0;
                float4 worldPosition : TEXCOORD1;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            sampler2D _MainTex;
            sampler2D _TouchMap;
            fixed4 _Color;
            fixed4 _TextureSampleAdd;
            float4 _ClipRect;
            float4 _MainTex_ST;

            v2f vert(appdata_t v) {
                v2f o;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
                o.worldPosition = v.vertex;
                o.vertex = UnityObjectToClipPos(o.worldPosition);
                o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.color = v.color * _Color;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target {
                float adj = 0.1;
                float moveX = 0.0;
                float moveY = 0.0;
                half4 touchC = tex2D(_TouchMap, i.texcoord);
                moveX += adj * ((touchC.r - 0.5));
                moveY += adj * ((touchC.g - 0.5));
                float2 move = float2(-moveX, -moveY);
                half4 color = (tex2D(_MainTex, i.texcoord + move + _TextureSampleAdd)) * i.color;
                #ifdef UNITY_UI_CLIP_RECT
                color.a *= UnityGet2DClipping(i.worldPosition.xy, _ClipRect);
                #endif
                #ifdef UNITY_UI_ALPHACLIP
                clip (color.a - 0.001);
                #endif
                return color;
            }
        ENDCG
        }
    }
}

ShaderはUnityのビルドインシェーダーのUI-Default.shaderを元に作成しています。
ビルドインシェーダーは、以下のUnity公式サイトから取得することができます。

unity3d.com

対象のUnityバージョンを選択して、以下画像のようにダウンロードしてください。

f:id:soramamenatan:20200814150453p:plain


ピクセルの移動

ビルドインシェーダーから、フラグメントシェーダーのみを変えています。

fixed4 frag(v2f i) : SV_Target {
    float adj = 0.1;
    float moveX = 0.0;
    float moveY = 0.0;
    // Scriptで定義した_touchTexを元にtex2D
    half4 touchC = tex2D(_TouchMap, i.texcoord);
    // 0.5が移動量0として扱っているので減算
    moveX += adj * ((touchC.r - 0.5));
    moveY += adj * ((touchC.g - 0.5));
    // 反転
    float2 move = float2(-moveX, -moveY);
    half4 color = (tex2D(_MainTex, i.texcoord + move + _TextureSampleAdd)) * i.color;
    #ifdef UNITY_UI_CLIP_RECT
    color.a *= UnityGet2DClipping(i.worldPosition.xy, _ClipRect);
    #endif
    #ifdef UNITY_UI_ALPHACLIP
    clip (color.a - 0.001);
    #endif
    return color;
}

特に難しいことはしていないので、コメントで簡易的に解説しました。


結果

マウスの移動に応じて、uGUIが移動すれば成功です。

f:id:soramamenatan:20200924145633g:plain


隣接しているピクセルも移動させる

現状ですと動きが単調ですので、隣接しているピクセルも移動させてみようと思います。
下記のScriptの追加の部分を加えてみてください。

if (distance2 < radius2) {
    float strength = 1 - Mathf.Sqrt(distance2) / radius;
    r += v.x * 1f / maxR * strength;
    g += v.y * 1f / maxR * strength;
}
// 追加
// 隣接しているピクセルに影響を与える
float effect = 0.01f;
for (int xx = Mathf.Max(0, x - 1); xx <= Mathf.Min(_touchTex.width, x + 1); ++xx) {
    for (int yy = Mathf.Max(0, y - 1); yy <= Mathf.Min(_touchTex.height, y + 1); ++yy) {
        if (xx == x && yy == y) {
            continue;
        }
        // 隣接しているピクセルとの距離
        int distance = Mathf.Abs(xx - x) + Mathf.Abs(yy - y);
        r += (_touchTex.GetPixel(xx, yy).r - 0.5f) * (distance == 1 ? effect : effect * 0.7f);
    }
}
// ここまで
_touchTex.SetPixel(x, y, new Color(Mathf.Clamp01(r), Mathf.Clamp01(g), 0f));

やっていることは、隣接しているピクセルの色を距離に応じて変化させています。


結果

先程のものより滑らかに移動していれば成功です。
ただし、かなり重い処理となります。

f:id:soramamenatan:20200924150505g:plain


明暗を付ける

移動した箇所がより分かりやすくなるように明暗をつけてみます。
フラグメントシェーダーを以下のように置き換えてください。

fixed4 frag(v2f i) : SV_Target {
    float adj = 0.1;
    float moveX = 0.0;
    float moveY = 0.0;
    half4 touchC = tex2D(_TouchMap, i.texcoord);
    moveX += adj * ((touchC.r - 0.5));
    moveY += adj * ((touchC.g - 0.5));
    float2 move = float2(-moveX, -moveY);
    float moveTotal = abs(moveX) + abs(moveY);
    float ripples = 80;
    float rightCoefficient = 1.5;
    float brightGap = rightCoefficient * moveTotal * (sin(ripples * moveTotal * UNITY_PI));
    half4 color = (tex2D(_MainTex, i.texcoord + move + _TextureSampleAdd)) * i.color;
    #ifdef UNITY_UI_CLIP_RECT
    color.a *= UnityGet2DClipping(i.worldPosition.xy, _ClipRect);
    #endif
    color.rgb += brightGap;
    #ifdef UNITY_UI_ALPHACLIP
    clip (color.a - 0.001);
    #endif
    return color;
}

追加した部分は以下の箇所で、

float moveTotal = abs(moveX) + abs(moveY);
float ripples = 80;
float rightCoefficient = 1.5;
float brightGap = rightCoefficient * moveTotal * (sin(ripples * moveTotal * UNITY_PI));

sin関数を使用し、移動量を波紋となるように扱います。
グラフを見ると分かりやすいかと思います。
ripplesは波紋量、rightCoefficientはライトの係数となります。

ripples = 80, rightCoefficient = 1.5

f:id:soramamenatan:20200924155907p:plain

ripples = 80, rightCoefficient = 0.1

f:id:soramamenatan:20200924155912p:plain

ripples = 10, rightCoefficient = 1.5

f:id:soramamenatan:20200924155917p:plain

結果

マウスが移動した箇所に明暗が付けば成功です。

f:id:soramamenatan:20200924163029g:plain


明暗の調整

最後に、暗すぎる部分があったので調整します。
下記の部分をフラグメントシェーダーに追加してください。
if文を使っているのには目をつぶってください。

// 8に変更
float rightCoefficient = 8;
float brightGap = rightCoefficient * moveTotal * (sin(ripples * moveTotal * UNITY_PI));
// 追加
brightGap = clamp(-1.0, 1.0, brightGap);
if (brightGap <= 0.5) {
    brightGap = sign(brightGap) * 2 * brightGap * brightGap;
} else {
    brightGap = sign(brightGap) -2 * (brightGap - 1) * (brightGap - 1) + 1;
}
if (brightGap < 0) {
    brightGap *= 0.2;
}
// ここまで
half4 color = (tex2D(_MainTex, i.texcoord + move + _TextureSampleAdd)) * i.color;

ここでは、easingのeaseInOutExpoを使用しています。
移動に関してはこちらのサイト様が分かりやすいです。

easings.net

また、式は以下の画像のようになっています。

f:id:soramamenatan:20200924163616p:plain

naochang | ShaderでImageをかき混ぜる:より引用

下記の部分が式に当てはめたものとなります。

if (brightGap <= 0.5) {
    brightGap = sign(brightGap) * 2 * brightGap * brightGap;
} else {
    brightGap = sign(brightGap) -2 * (brightGap - 1) * (brightGap - 1) + 1;
}

結果

暗い部分がなくなれば完成です。

f:id:soramamenatan:20200924164247g:plain


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

最後にソースコードを添付します。

Script

using UnityEngine;
using UnityEngine.UI;

namespace onMouseMove {
    public class OnMouseMoveImage : MonoBehaviour {
        [SerializeField]
        private Image _previewImage;
        [SerializeField]
        private Shader _shader;
        // もとのテクスチャに戻る時間,1が最大
        [SerializeField]
        private float easing = 0.1f;
        // 移動量に応じた円の大きさ
        // 移動量が大きいほど円は大きくなる
        [SerializeField]
        private float maxR = 100f;
        [SerializeField]
        private bool isUseImage;
        private Texture2D _touchTex;
        private Material _material;
        private Image _image;
        private RectTransform _rectTrans;
        private Vector2 _ratio;
        private Vector2 _inverseRatio;
        private Vector2 _prevPos;

        /// <summary>
        /// 初期化
        /// </summary>
        void Start () {
            _image = GetComponent<Image>();
            _rectTrans = GetComponent<RectTransform>();
            _material = new Material(_shader);
            _previewImage.material = _material;
            // 2のべき乗でテクスチャ制作
            _touchTex = new Texture2D(128, 128);
            // テクスチャの繰り返し設定をoff
            _touchTex.wrapMode = TextureWrapMode.Clamp;
            // ワールド空間からローカル空間へマウス座標を変換
            _prevPos = _rectTrans.InverseTransformPoint(Input.mousePosition);
            // Imageに対して、制作したテクスチャの割合を取得
            _ratio = new Vector3(_touchTex.width / _rectTrans.sizeDelta.x, _touchTex.height / _rectTrans.sizeDelta.y);
            _inverseRatio = new Vector2(1f / _ratio.x, 1f / _ratio.y);;
            // xベクトルをred、yベクトルをgreenに設定する
            // 移動していない状態を0.5とする
            for (int y = 0; y < _touchTex.height; ++y) {
                Color[] colors = new Color[_touchTex.width];
                for (int i = 0; i < colors.Length; ++i) {
                    // blueは移動量として使用しないので、0にする
                    colors[i] = new Color(0.5f, 0.5f, 0f);
                }
                // x : フェッチするピクセル配列のx位置
                // y : フェッチするピクセル配列のy位置
                // blockWidth : フェッチするピクセル配列の幅の長さ
                // blockHeight : フェッチするピクセル配列の高さ
                // for文節約
                _touchTex.SetPixels(y, 0, 1, _touchTex.width, colors);
            }
            _touchTex.Apply ();
            if (isUseImage == false) {
                _previewImage.sprite = Sprite.Create (_touchTex, new Rect (0, 0, _touchTex.width, _touchTex.height), Vector2.zero);
                _previewImage.GetComponent<RectTransform> ().sizeDelta = _rectTrans.sizeDelta;
            }
        }

        /// <summary>
        /// 更新
        /// </summary>
        void Update () {
            CalcColor();
        }

        /// <summary>
        /// 色の計算
        /// </summary>
        private void CalcColor() {
            // 元のテクスチャに戻る時間、最大1
            float easing = 0.1f;
            // 移動量に応じた円の大きさの許容値
            float maxR = 100f;
            // ワールド空間からローカル空間へマウス座標を変換
            Vector2 localPos = _rectTrans.InverseTransformPoint(Input.mousePosition);
            // _touchTexでの位置
            Vector2 drawPos = new Vector2(Mathf.Round(localPos.x * _ratio.x), Mathf.Round(localPos.y * _ratio.y));
            // 前フレームとの移動量
            Vector2 v = localPos - _prevPos;
            float radius = v.magnitude;
            // 円の半径が許容値を超えないように
            if (radius > maxR) {
                radius = maxR;
                v = v.normalized * maxR;
            }
            float radius2 = radius * radius;
            // 各ピクセルを総舐め
            for (int x = 0; x < _touchTex.width; ++x) {
                for (int y = 0; y < _touchTex.height; ++y) {
                    // 移動時の軌跡を元に戻す
                    Color c = _touchTex.GetPixel(x, y);
                    float r = c.r;
                    float g = c.g;
                    // 移動している時
                    if (r != 0.5f && g != 0.5f) {
                        // easingの値に応じて0.5に戻す
                        r += easing * (0.5f - r);
                        g += easing * (0.5f - g);
                        // 軽微なズレを元に戻す
                        if (Mathf.Abs(r - 0.5f) < 0.05f) {
                            r = 0.5f;
                        }
                        if (Mathf.Abs(g - 0.5f) < 0.05f) {
                            g = 0.5f;
                        }
                    }
                    // 移動量に応じて軌跡を大きくする
                    // 各ピクセルのローカルでの位置
                    Vector2 pixelPos = new Vector2(x * _inverseRatio.x - _rectTrans.sizeDelta.x / 2f, y * _inverseRatio.y - _rectTrans.sizeDelta.y / 2f);
                    float distance2 = (localPos - pixelPos).sqrMagnitude;
                    // 影響範囲内か
                    if (distance2 < radius2) {
                        // 影響力を計算
                        // 円の中心の方が影響力を高くするように
                        float strength = 1 - Mathf.Sqrt(distance2) / radius;
                        r += v.x * 1f / maxR * strength;
                        g += v.y * 1f / maxR * strength;
                    }
                    // 重いので注意
                    // 隣接しているピクセルに影響を与える
                    float effect = 0.01f;
                    for (int xx = Mathf.Max(0, x - 1); xx <= Mathf.Min(_touchTex.width, x + 1); ++xx) {
                        for (int yy = Mathf.Max(0, y - 1); yy <= Mathf.Min(_touchTex.height, y + 1); ++yy) {
                            if (xx == x && yy == y) {
                                continue;
                            }
                            // 隣接しているピクセルとの距離
                            int distance = Mathf.Abs(xx - x) + Mathf.Abs(yy - y);
                            r += (_touchTex.GetPixel(xx, yy).r - 0.5f) * (distance == 1 ? effect : effect * 0.7f);
                        }
                    }
                    // 色を設定
                    _touchTex.SetPixel(x, y, new Color(Mathf.Clamp01(r), Mathf.Clamp01(g), 0f));
                }
            }
            // 色を更新
            _touchTex.Apply();
            // shaderに設定
            _image.material.SetTexture("_TouchMap", _touchTex);
            // 現在のマウス座標を前フレームの座標として保持
            _prevPos = localPos;
        }
    }
}

Shader

Shader "Unlit/OnMouseMoveImage" {
    Properties {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)
        _StencilComp ("Stencil Comparison", Float) = 8
        _Stencil ("Stencil ID", Float) = 0
        _StencilOp ("Stencil Operation", Float) = 0
        _StencilWriteMask ("Stencil Write Mask", Float) = 255
        _StencilReadMask ("Stencil Read Mask", Float) = 255
        _ColorMask ("Color Mask", Float) = 15
        [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
    }

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

        Stencil {
            Ref [_Stencil]
            Comp [_StencilComp]
            Pass [_StencilOp]
            ReadMask [_StencilReadMask]
            WriteMask [_StencilWriteMask]
        }

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

        Pass {
            Name "Default"
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0

            #include "UnityCG.cginc"
            #include "UnityUI.cginc"

            #pragma multi_compile_local _ UNITY_UI_CLIP_RECT
            #pragma multi_compile_local _ UNITY_UI_ALPHACLIP

            struct appdata_t {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                float2 texcoord  : TEXCOORD0;
                float4 worldPosition : TEXCOORD1;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            sampler2D _MainTex;
            sampler2D _TouchMap;
            fixed4 _Color;
            fixed4 _TextureSampleAdd;
            float4 _ClipRect;
            float4 _MainTex_ST;

            v2f vert(appdata_t v) {
                v2f o;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o);
                o.worldPosition = v.vertex;
                o.vertex = UnityObjectToClipPos(o.worldPosition);
                o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.color = v.color * _Color;
                return o;
            }

            fixed4 frag(v2f i) : SV_Target {
                // 調整係数
                float adj = 0.1;
                float moveX = 0.0;
                float moveY = 0.0;
                // Scriptで制作したタッチ画像
                half4 touchC = tex2D(_TouchMap, i.texcoord);
                // 0.5が移動していない状態なので-0.5
                moveX += adj * ((touchC.r - 0.5));
                moveY += adj * ((touchC.g - 0.5));
                // 反転
                float2 move = float2(-moveX, -moveY);
                float moveTotal = abs(moveX) + abs(moveY);
                // 波紋数
                float ripples = 80;
                // ライト係数
                float rightCoefficient = 8;
                // 波紋状にする
                float brightGap = rightCoefficient * moveTotal * (sin(ripples * moveTotal * UNITY_PI));
                brightGap = clamp(-1.0, 1.0, brightGap);
                // easeInOutExpoの式に当てはめて計算
                if (brightGap <= 0.5) {
                    brightGap = sign(brightGap) * 2 * brightGap * brightGap;
                } else {
                    brightGap = sign(brightGap) -2 * (brightGap - 1) * (brightGap - 1) + 1;
                }
                if (brightGap < 0) {
                    brightGap *= 0.2;
                }
                half4 color = (tex2D(_MainTex, i.texcoord + move + _TextureSampleAdd)) * i.color;
                #ifdef UNITY_UI_CLIP_RECT
                color.a *= UnityGet2DClipping(i.worldPosition.xy, _ClipRect);
                #endif
                color.rgb += brightGap;
                #ifdef UNITY_UI_ALPHACLIP
                clip (color.a - 0.001);
                #endif
                return color;
            }
        ENDCG
        }
    }
}

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

参考サイト様

naochang.me

www.iquilezles.org

easings.net