【UnityShader】uGUIのImageをズラす 【2】#71
前回の成果
uGUIのImageをズラすScriptのStart関数まで学んだ。
今回やること
前回に引き続き、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の場合
easingが1の場合
また、軽微なズレを元に戻すで小さな値を許容するようにしています。
これがないとテクスチャがもとに戻る際にわずかに戻らない箇所が生まれてしまいます。
軽微なズレを元に戻すの部分を消した場合
A3のマスがもとに戻っていないのがわかります。
移動量に応じて軌跡を大きくする
こちらも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文の中で影響範囲の円の中心に近いものほど影響を強くしています。
影響範囲のイメージ
黄土色から変色している箇所が影響範囲です。
色の設定
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側の解説になります。
ここまでご視聴ありがとうございました。