【UnityShader】uGUIのImageをズラす 【1】#70
前回の成果
SerializedObjectについて学んだ。
今回やること
Shaderを使用して、uGUIのImageをズラしていきます。
事前準備
まず、Scene上にImageを配置します。
Imageに
- 適当な画像
- 今回制作したScript
上記2点をアタッチして準備完了となります。
備忘録程度に、今回使用する画像はカラーグリッドと呼ばれるものになります。
使用用途は、UVの歪みがないかを確認するもので、Blenderにデフォルトで入っているもののようです。
ソースコード
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公式サイトから取得することができます。
対象のUnityバージョンを選択して、以下画像のようにダウンロードしてください。
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のべき乗を使用するかを端的に言うとデータを無駄なく使うためになります。
詳しくは以下のサイト様が参考になります。
テクスチャの繰り返し設定を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を代入しています。
ですので、何も移動していないときのテクスチャは以下の画像のようになります。
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関数について説明する予定となっています。
ここまでご視聴ありがとうございました。