知識0からのUnityShader勉強

知識0からのUnityShader勉強

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

【UnityShader】uGUIのImageをズラす 【1】#70

前回の成果

SerializedObjectについて学んだ。

soramamenatan.hatenablog.com


今回やること

Shaderを使用して、uGUIのImageをズラしていきます。


事前準備

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

f:id:soramamenatan:20200912150415p:plain

Imageに

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

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

f:id:soramamenatan:20200912150408p:plain

備忘録程度に、今回使用する画像はカラーグリッドと呼ばれるものになります。
使用用途は、UVの歪みがないかを確認するもので、Blenderにデフォルトで入っているもののようです。

f:id:soramamenatan:20200912150404p:plain

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


ソースコード

Script

using UnityEngine;
using UnityEngine.UI;

namespace onMouseMove {
    public class OnMouseMoveImage : MonoBehaviour {
        [SerializeField]
        private Image _previewImage;
        [SerializeField]
        private Shader _shader;
        private Texture2D _touchTex;
        private Material _material;
        private Image _image;
        private RectTransform _rectTrans;
        private Vector2 _ratio;
        private Vector2 _inverseRatio;
        private Vector2 _prevPos;

        void Start () {
            _image = GetComponent<Image>();
            _rectTrans = GetComponent<RectTransform>();
            _material = new Material(_shader);
            _previewImage.material = _material;
            _touchTex = new Texture2D(128, 128);
            _touchTex.wrapMode = TextureWrapMode.Clamp;
            _prevPos = _rectTrans.InverseTransformPoint(Input.mousePosition);
            _ratio = new Vector3(_touchTex.width / _rectTrans.sizeDelta.x, _touchTex.height / _rectTrans.sizeDelta.y);
            _inverseRatio = new Vector2(1f / _ratio.x, 1f / _ratio.y);;
            for (int y = 0; y < _touchTex.height; ++y) {
                Color[] colors = new Color[_touchTex.width];
                for (int i = 0; i < colors.Length; ++i) {
                    colors[i] = new Color(0.5f, 0.5f, 0f);
                }
                _touchTex.SetPixels(y, 0, 1, _touchTex.width, colors);
            }
            _touchTex.Apply ();
        }

        void Update () {
            float easing = 0.1f;
            float maxR = 100f;
            Vector2 localPos = _rectTrans.InverseTransformPoint(Input.mousePosition);
            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) {
                        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 imagePos = new Vector2(x * _inverseRatio.x - _rectTrans.sizeDelta.x / 2f, y * _inverseRatio.y - _rectTrans.sizeDelta.y / 2f);
                    float distance2 = (localPos - imagePos).sqrMagnitude;
                    if (distance2 < radius2) {
                        float strength = 1 - Mathf.Sqrt(distance2) / radius;
                        r += v.x * 1f / maxR * strength;
                        g += v.y * 1f / maxR * strength;
                    }
                    _touchTex.SetPixel(x, y, new Color(Mathf.Clamp01(r), Mathf.Clamp01(g), 0f));
                }
            }
            _touchTex.Apply();
            _image.material.SetVector("_Touch", localPos);
            _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;
            float4 _Touch;

            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;
                float2 gap = i.worldPosition - _Touch;
                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;
                color.a *= UnityGet2DClipping(i.worldPosition.xy, _ClipRect);
                #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


OnMouseMoveImageクラスの解説

今回は量が多いので、小分けにして解説していきます。


Start関数

変数の初期化を主に行っています。
コメントで簡単に解説すると以下になります。

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 : フェッチするピクセル配列の高さ
        _touchTex.SetPixels(y, 0, 1, _touchTex.width, colors);
    }
    _touchTex.Apply ();
    // _previewImage.sprite = Sprite.Create (_touchTex, new Rect (0, 0, _touchTex.width, _touchTex.height), Vector2.zero);
    // _previewImage.GetComponent<RectTransform> ().sizeDelta = _rectTrans.sizeDelta;
}

以下で細かく解説していきます。


2のべき乗でテクスチャ制作

_touchTex = new Texture2D(128, 128);

2のべき乗とは、

指数
0 1
1 2
2 4
3 8
4 16
5 32
6 64
7 128
8 256
9 512
10 1024
... ...

のように2nで表される数になります。
今回は、テクスチャを128で指定していますが2のべき乗でしたらどれでも良いです。
数字を小さくすると、軽くなるが描画が荒くなる、大きくすると重くなるが描画が綺麗になります。
なぜ2のべき乗を使用するかを端的に言うとデータを無駄なく使うためになります。
詳しくは以下のサイト様が参考になります。

light11.hatenadiary.com

variously.sblo.jp


テクスチャの繰り返し設定をoff

_touchTex.wrapMode = TextureWrapMode.Clamp;

テクスチャがズレるので繰り返さないようにoffにしています。
他にもwrapModeはあるので、以下の表を参考にしてください。

mode名 意味
Repeat テクスチャを繰り返し表示
Clamp テクスチャの端のピクセルを引き伸ばす
Mirror 反転させて繰り返し表示
Mirror Once UV座標(0, 0)を中心にMirrorを一度だけ行い、それ以降はClampする


xベクトルをred、yベクトルをgreenに設定する

for (int y = 0; y < _touchTex.height; ++y) {
    Color[] colors = new Color[_touchTex.width];
    for (int i = 0; i < colors.Length; ++i) {
        colors[i] = new Color(0.5f, 0.5f, 0f);
    }
    _touchTex.SetPixels(y, 0, 1, _touchTex.width, colors);
}
_touchTex.Apply ();

制作したテクスチャに色を渡しています。
今回、色によってxyの移動量を計算するのでrとgに0.5を代入しています。
ですので、何も移動していないときのテクスチャは以下の画像のようになります。

f:id:soramamenatan:20200912155223p:plain

SetPixelsに関して、おそらく以下でも動きます。

for (int y = 0; y < _touchTex.height; ++y) {
    for (int x = 0; y < _touchTex.width; ++x) {
        _touchTex.SetPixel(x, y, colors);
    }
}

ただし、for文の回る回数が非常に多くなってしまうため今回は避けています。
Applyをしないと反映されないので、気をつけてください。


今回は以上となります。
次回はUpdate関数について説明する予定となっています。
ここまでご視聴ありがとうございました。

参考サイト様

naochang.me

tsubakit1.hateblo.jp