知識0からのUnityShader勉強

知識0からのUnityShader勉強

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

【UnityShader】uGUIで波紋を出す #73

前回の成果

uGUIのImageをズラした。

soramamenatan.hatenablog.com


今回やること

uGUIに波紋を出します。


事前準備

まず、Scene上にImageを配置します。

f:id:soramamenatan:20200912150415p:plain

Imageに

  • 適当な画像
  • 今回制作したScript

上記2点をアタッチして準備完了となります。

f:id:soramamenatan:20200912150408p:plain

f:id:soramamenatan:20200912150404p:plain

UnityのShader勉強4 テクスチャを貼る - はるのゲーム開発メモ:より引用


ソースコード

Script

using UnityEngine;
using UnityEngine.UI;

namespace onMouseMove {
    [RequireComponent(typeof(Image))]
    public class RipplesImage : MonoBehaviour {
        [SerializeField]
        private Shader _shader;
        [SerializeField]
        private float _speed;
        [SerializeField, Range(0, 1)]
        private float _strength;
        [SerializeField]
        private float _effectRadius;

        private Material _material;
        private Image _image;
        private RectTransform _rectTrans;
        private int _touchPosPropertyId;
        private int _speedPropertyId;
        private int _strengthPropertyId;
        private int _radiusPropertyId;
        private bool _isInitialized;

        void Start () {
            _image = GetComponent<Image>();
            _rectTrans = GetComponent<RectTransform>();
            _material = new Material(_shader);
            _image.material = _material;
            _touchPosPropertyId = Shader.PropertyToID("_TouchPos");
            _speedPropertyId = Shader.PropertyToID("_Speed");
            _strengthPropertyId = Shader.PropertyToID("_Strength");
            _radiusPropertyId = Shader.PropertyToID("_EffectRadius");
            _isInitialized = true;
            UpdateRipplesValue();
        }

        void Update () {
            Vector2 localPos = _rectTrans.InverseTransformPoint(Input.mousePosition);
            _image.material.SetVector(_touchPosPropertyId, localPos);
        }

        void OnValidate() {
            UpdateRipplesValue();
        }

        private void UpdateRipplesValue() {
            if (_isInitialized == false) {
                return;
            }
            _image.material.SetFloat(_speedPropertyId, _speed);
            _image.material.SetFloat(_strengthPropertyId, _strength);
            _image.material.SetFloat(_radiusPropertyId, _effectRadius);
        }
    }
}

主に、マウスの座標をShaderに渡しています。


Shader

Shader "Unlit/RipplesImage" {
    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

        _TouchPos ("Touch Pos", Vector) = (0,0,0,0)
        _EffectRadius ("Radius", float) = 0
        _Strength ("Strength", float) = 0
        _Speed ("Speed", 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;
            fixed4 _Color;
            fixed4 _TextureSampleAdd;
            float4 _ClipRect;
            float4 _MainTex_ST;

            float2 _TouchPos;
            float _EffectRadius;
            float _Strength;
            float _Speed;

            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 {
                float2 gap = i.worldPosition - _TouchPos;
                float moveX = 0;
                float len = length(gap);

                if (len <= _EffectRadius) {
                    float p = sqrt(pow(_EffectRadius, 2) - pow(gap.x, 2) - pow(gap.y, 2));
                    moveX = p * _Strength * 0.0002 * (cos((len - _Time.w * _Speed) + 0.5));
                }
                half4 color = (tex2D(_MainTex, i.texcoord + float2(-moveX, 0)) + _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


波紋の生成

先に波紋のイメージを起こして、そこからソースコードに落とし込んでいきます。


基礎となる波紋

今回使用する波紋の計算式は以下となります。

 \displaystyle
z = \cos(\sqrt{x^2 + y^2})

イメージはこのような感じです。

基礎となる波紋のイメージ

f:id:soramamenatan:20200930144504p:plain


スケール用の式

次に、スケールさせるための式を用意します。
計算式は以下となります。

 \displaystyle
z = \sqrt{100 - x^2 - y^2}
スケールのイメージ

f:id:soramamenatan:20200930144510p:plain


乗算する

基礎となる波紋と、スケールを乗算していきます。
計算式は以下となります。

 \displaystyle
z = \cos(\sqrt{x^2 + y^2}) * \sqrt{100 - x^2 - y^2}
乗算したイメージ

f:id:soramamenatan:20200930144515p:plain


移動させる

このままですと、動かないのでtimeを使用して波紋のように動かすようにします。
また、移動幅が大きすぎるため最後に0.1を乗算して調節しています。

計算式は以下となります。

 \displaystyle
z = (\cos(\sqrt{x^2 + y^2} - t)) * \sqrt{100 - x^2 - y^2} * 0.1
静止時のイメージ

f:id:soramamenatan:20200930144520p:plain

移動させたときのイメージ

f:id:soramamenatan:20200930144526g:plain

これで波紋の計算式が完成したのでソースコードに落とし込みます。


Shader側の解説

Unityのビルドインシェーダーを使用したため、フラグメントシェーダーのみの解説となります。

fixed4 frag(v2f i) : SV_Target {
    float2 gap = i.worldPosition - _TouchPos;
    float moveX = 0;
    float len = length(gap);
    // 波紋の範囲内
    if (len <= _EffectRadius) {
        // p = √100-x^2-y^2, スケール用
        float p = sqrt(pow(_EffectRadius, 2) - pow(gap.x, 2) - pow(gap.y, 2));
        // 波紋の式である√x^2+y^2を元に調整
        moveX = p * _Strength * 0.0002 * (cos((len - _Time.w * _Speed) + 0.5));
    }
    half4 color = (tex2D(_MainTex, i.texcoord + float2(-moveX, 0)) + _TextureSampleAdd) * i.color;
    // 以下はビルドインシェーダーのままのため省略

特に難しい部分はないため、コメントでの解説となります。

結果

マウスの位置を中心として波紋が出れば成功です。

f:id:soramamenatan:20200930152339g:plain

inspectorの値

f:id:soramamenatan:20200930152540p:plain


+αで修正していこうと思います。

まずは、波紋の数が多かったので少し減らします。

// +0.5 -> *0.2に変更
moveX = p * _Strength * 0.0002 * (cos((len - _Time.w * _Speed) * 0.2));
結果

緩やかな波紋となりました。

f:id:soramamenatan:20200930152354g:plain

最後に明るさを調整します。

// 上記省略
if (len <= _EffectRadius) {
    float p = sqrt(pow(_EffectRadius, 2) - pow(gap.x, 2) - pow(gap.y, 2));
    moveX = p * _Strength * 0.0002 * (cos((len - _Time.w * _Speed) * 0.2));
    // 追加
    brightGap = _Strength * 20 * moveX * cos(45);
}
half4 color = (tex2D(_MainTex, i.texcoord + float2(-moveX, 0)) + _TextureSampleAdd) * i.color;
// 追加
color.rgb += brightGap;
#ifdef UNITY_UI_CLIP_RECT
// 下記省略
結果

明暗ができれば成功です。

f:id:soramamenatan:20200930152408g:plain


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

Script

using UnityEngine;
using UnityEngine.UI;

namespace onMouseMove {
    [RequireComponent(typeof(Image))]
    public class RipplesImage : MonoBehaviour {
        [SerializeField]
        private Shader _shader;
        [SerializeField]
        private float _speed;
        [SerializeField, Range(0, 1)]
        private float _strength;
        [SerializeField]
        private float _effectRadius;

        private Material _material;
        private Image _image;
        private RectTransform _rectTrans;
        private int _touchPosPropertyId;
        private int _speedPropertyId;
        private int _strengthPropertyId;
        private int _radiusPropertyId;
        private bool _isInitialized;

        /// <summary>
        /// 初期化
        /// </summary>
        void Start () {
            _image = GetComponent<Image>();
            _rectTrans = GetComponent<RectTransform>();
            _material = new Material(_shader);
            _image.material = _material;
            _touchPosPropertyId = Shader.PropertyToID("_TouchPos");
            _speedPropertyId = Shader.PropertyToID("_Speed");
            _strengthPropertyId = Shader.PropertyToID("_Strength");
            _radiusPropertyId = Shader.PropertyToID("_EffectRadius");
            _isInitialized = true;
            UpdateRipplesValue();
        }

        /// <summary>
        /// 更新
        /// </summary>
        void Update () {
            // ワールド空間からローカル空間へマウス座標を変換
            Vector2 localPos = _rectTrans.InverseTransformPoint(Input.mousePosition);
            // shaderに設定
            _image.material.SetVector(_touchPosPropertyId, localPos);
        }

        /// <summary>
        /// inspector操作時
        /// </summary>
        void OnValidate() {
            UpdateRipplesValue();
        }

        /// <summary>
        /// 波紋の値の更新
        /// </summary>
        private void UpdateRipplesValue() {
            if (_isInitialized == false) {
                return;
            }
            _image.material.SetFloat(_speedPropertyId, _speed);
            _image.material.SetFloat(_strengthPropertyId, _strength);
            _image.material.SetFloat(_radiusPropertyId, _effectRadius);
        }
    }
}


Shader

Shader "Unlit/RipplesImage" {
    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

        _TouchPos ("Touch Pos", Vector) = (0,0,0,0)
        _EffectRadius ("Radius", float) = 0
        _Strength ("Strength", float) = 0
        _Speed ("Speed", 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;
            fixed4 _Color;
            fixed4 _TextureSampleAdd;
            float4 _ClipRect;
            float4 _MainTex_ST;

            float2 _TouchPos;
            float _EffectRadius;
            float _Strength;
            float _Speed;

            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 {
                float2 gap = i.worldPosition - _TouchPos;
                float moveX = 0;
                float len = length(gap);
                float brightGap = 0;

                // 波紋の範囲内
                if (len <= _EffectRadius) {
                    // p = √100-x^2-y^2
                    float p = sqrt(pow(_EffectRadius, 2) - pow(gap.x, 2) - pow(gap.y, 2));
                    // 波紋の式である√x^2+y^2を元に調整
                    moveX = p * _Strength * 0.0002 * (cos((len - _Time.w * _Speed) * 0.2));
                    // 外側への移動を明るく、内側への移動を暗くする
                    brightGap = _Strength * 20 * moveX * cos(45);
                }
                half4 color = (tex2D(_MainTex, i.texcoord + float2(-moveX, 0)) + _TextureSampleAdd) * i.color;
                color.rgb += brightGap;
                #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
        }
    }
}

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

参考サイト様

naochang.me