知識0からのUnityShader勉強

知識0からのUnityShader勉強

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

【UnityShader】uGUIのImageをズラす 【2】#71

前回の成果

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

soramamenatan.hatenablog.com


今回やること

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


Update関数

こちらにuGUIをズラしています。
正確に言うと、マウスの移動量を色として変化させて、それをShader側に渡しています。
コメントで簡単に説明すると以下になります。

void Update () {
    // 元のテクスチャに戻る時間、最大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;
            }
            // 色を設定
            _touchTex.SetPixel(x, y, new Color(Mathf.Clamp01(r), Mathf.Clamp01(g), 0f));
        }
    }
    // 色を更新
    _touchTex.Apply();
    // shaderに設定
    _image.material.SetVector("_Touch", localPos);
    _image.material.SetTexture("_TouchMap", _touchTex);
    // 現在のマウス座標を前フレームの座標として保持
    _prevPos = localPos;
}

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


値の代入

ここで移動量を計算する前準備を行っています。

// 元のテクスチャに戻る時間、最大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;

localPosとdrawPosはImageの中央が(0, 0)となっています。

予約語を軽く解説します。

InverseTransformPoint

ワールド空間からローカル空間へと変換します。

public Vector3 InverseTransformPoint(Vector3 position);
Mathf.Round

引数に最も近い整数を返します。
0.5の場合は偶数の整数を返します。

// 定義
public static float Round(float f);

// 例
// 10が返ってくる
Mathf.Round(10.3f);
// 10が返ってくる
Mathf.Round(10.5f);
// 12が返ってくる
Mathf.Round(11.5f);
magnitude

ベクトルの長さを返します。
具体的には(x2 + y2 + z2)の平方根が返ってきます。

public float magnitude { get; }


軌跡を元に戻す

for文の中の軌跡を元に戻す処理をしている箇所になります。

// 移動時の軌跡を元に戻す
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;
    }
}

ピクセルの色を取得し、その色のrgのどちらかが0.5でなかった場合に0.5にeasingを使用して戻します。
Start関数で0.5を移動していない場合と定義したので、0.5がデフォルトの値となっています。


easingの値が小さい程、戻るのに時間がかかります。
逆に大きいとすぐにもとに戻ります。

easingが0.1の場合

f:id:soramamenatan:20200916161608g:plain

easingが1の場合

f:id:soramamenatan:20200916161628g:plain


また、軽微なズレを元に戻すで小さな値を許容するようにしています。
これがないとテクスチャがもとに戻る際にわずかに戻らない箇所が生まれてしまいます。

軽微なズレを元に戻すの部分を消した場合

A3のマスがもとに戻っていないのがわかります。

f:id:soramamenatan:20200916161643g:plain


移動量に応じて軌跡を大きくする

こちらもfor文の中の処理となります。
ここで軌跡の大きさを決めています。

// 移動量に応じて軌跡を大きくする
// 各ピクセルのローカルでの位置
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;
}

まずは予約語の説明からです。

Mathf.Sqrt

引数の平方根を返します。

public static float Sqrt(float f);
sqrMagnitude

ベクトルの2乗の長さを返すものです。

public float sqrMagnitude { get; }

distance2は、
タッチ座標 - 各ピクセルの座標の2乗
radius2は、
タッチ座標 - 前フレームのタッチ座標の2乗
となっています。
radius2以下ということは、影響範囲内ということになります。
そして、if文の中で影響範囲の円の中心に近いものほど影響を強くしています。

影響範囲のイメージ

黄土色から変色している箇所が影響範囲です。

f:id:soramamenatan:20200918110122p:plain


色の設定

Update関数最後の部分となります。
for文の途中から切り取っているのでインデントがズレていますが気にしないでください。

        // 色を設定
        _touchTex.SetPixel(x, y, new Color(Mathf.Clamp01(r), Mathf.Clamp01(g), 0f));
    }
}
// 色を更新
_touchTex.Apply();
// shaderに設定
_image.material.SetVector("_Touch", localPos);
_image.material.SetTexture("_TouchMap", _touchTex);
// 現在のマウス座標を前フレームの座標として保持
_prevPos = localPos;


for文で影響力をrとgに代入したので、それをSetPixel()でPixelの色を変色させています。
そして、それをApply()により適用させています。
最後にShader側に伝えて、前フレームのマウス座標を保持してUpdate関数は終了となります。


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

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;
                    }
                    // 色を設定
                    _touchTex.SetPixel(x, y, new Color(Mathf.Clamp01(r), Mathf.Clamp01(g), 0f));
                }
            }
            // 色を更新
            _touchTex.Apply();
            // shaderに設定
            _image.material.SetVector("_Touch", localPos);
            _image.material.SetTexture("_TouchMap", _touchTex);
            // 現在のマウス座標を前フレームの座標として保持
            _prevPos = localPos;
        }
    }
}


今回は以上となります。
次回はShader側の解説になります。
ここまでご視聴ありがとうございました。