【UnityShader】uGUIのImageをズラす 【3】#72
前回の成果
uGUIのImageをズラすScriptのUpdate関数まで学んだ。
今回やること
前回、前々回に引き続き、uGUIのImageをズラすことをしていこうと思います。
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; 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; 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; #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公式サイトから取得することができます。
対象のUnityバージョンを選択して、以下画像のようにダウンロードしてください。
ピクセルの移動
ビルドインシェーダーから、フラグメントシェーダーのみを変えています。
fixed4 frag(v2f i) : SV_Target { float adj = 0.1; float moveX = 0.0; float moveY = 0.0; // Scriptで定義した_touchTexを元にtex2D half4 touchC = tex2D(_TouchMap, i.texcoord); // 0.5が移動量0として扱っているので減算 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; #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; }
特に難しいことはしていないので、コメントで簡易的に解説しました。
結果
マウスの移動に応じて、uGUIが移動すれば成功です。
隣接しているピクセルも移動させる
現状ですと動きが単調ですので、隣接しているピクセルも移動させてみようと思います。
下記のScriptの追加の部分を加えてみてください。
if (distance2 < radius2) { float strength = 1 - Mathf.Sqrt(distance2) / radius; r += v.x * 1f / maxR * strength; g += v.y * 1f / maxR * strength; } // 追加 // 隣接しているピクセルに影響を与える float effect = 0.01f; for (int xx = Mathf.Max(0, x - 1); xx <= Mathf.Min(_touchTex.width, x + 1); ++xx) { for (int yy = Mathf.Max(0, y - 1); yy <= Mathf.Min(_touchTex.height, y + 1); ++yy) { if (xx == x && yy == y) { continue; } // 隣接しているピクセルとの距離 int distance = Mathf.Abs(xx - x) + Mathf.Abs(yy - y); r += (_touchTex.GetPixel(xx, yy).r - 0.5f) * (distance == 1 ? effect : effect * 0.7f); } } // ここまで _touchTex.SetPixel(x, y, new Color(Mathf.Clamp01(r), Mathf.Clamp01(g), 0f));
やっていることは、隣接しているピクセルの色を距離に応じて変化させています。
結果
先程のものより滑らかに移動していれば成功です。
ただし、かなり重い処理となります。
明暗を付ける
移動した箇所がより分かりやすくなるように明暗をつけてみます。
フラグメントシェーダーを以下のように置き換えてください。
fixed4 frag(v2f i) : SV_Target { float adj = 0.1; float moveX = 0.0; float moveY = 0.0; half4 touchC = tex2D(_TouchMap, i.texcoord); moveX += adj * ((touchC.r - 0.5)); moveY += adj * ((touchC.g - 0.5)); float2 move = float2(-moveX, -moveY); float moveTotal = abs(moveX) + abs(moveY); float ripples = 80; float rightCoefficient = 1.5; float brightGap = rightCoefficient * moveTotal * (sin(ripples * moveTotal * UNITY_PI)); half4 color = (tex2D(_MainTex, i.texcoord + move + _TextureSampleAdd)) * i.color; #ifdef UNITY_UI_CLIP_RECT color.a *= UnityGet2DClipping(i.worldPosition.xy, _ClipRect); #endif color.rgb += brightGap; #ifdef UNITY_UI_ALPHACLIP clip (color.a - 0.001); #endif return color; }
追加した部分は以下の箇所で、
float moveTotal = abs(moveX) + abs(moveY); float ripples = 80; float rightCoefficient = 1.5; float brightGap = rightCoefficient * moveTotal * (sin(ripples * moveTotal * UNITY_PI));
sin関数を使用し、移動量を波紋となるように扱います。
グラフを見ると分かりやすいかと思います。
ripplesは波紋量、rightCoefficientはライトの係数となります。
ripples = 80, rightCoefficient = 1.5
ripples = 80, rightCoefficient = 0.1
ripples = 10, rightCoefficient = 1.5
結果
マウスが移動した箇所に明暗が付けば成功です。
明暗の調整
最後に、暗すぎる部分があったので調整します。
下記の部分をフラグメントシェーダーに追加してください。
if文を使っているのには目をつぶってください。
// 8に変更 float rightCoefficient = 8; float brightGap = rightCoefficient * moveTotal * (sin(ripples * moveTotal * UNITY_PI)); // 追加 brightGap = clamp(-1.0, 1.0, brightGap); if (brightGap <= 0.5) { brightGap = sign(brightGap) * 2 * brightGap * brightGap; } else { brightGap = sign(brightGap) -2 * (brightGap - 1) * (brightGap - 1) + 1; } if (brightGap < 0) { brightGap *= 0.2; } // ここまで half4 color = (tex2D(_MainTex, i.texcoord + move + _TextureSampleAdd)) * i.color;
ここでは、easingのeaseInOutExpoを使用しています。
移動に関してはこちらのサイト様が分かりやすいです。
また、式は以下の画像のようになっています。
下記の部分が式に当てはめたものとなります。
if (brightGap <= 0.5) { brightGap = sign(brightGap) * 2 * brightGap * brightGap; } else { brightGap = sign(brightGap) -2 * (brightGap - 1) * (brightGap - 1) + 1; }
結果
暗い部分がなくなれば完成です。
ソースコードにコメントを付与
最後にソースコードを添付します。
Script
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; } // 重いので注意 // 隣接しているピクセルに影響を与える float effect = 0.01f; for (int xx = Mathf.Max(0, x - 1); xx <= Mathf.Min(_touchTex.width, x + 1); ++xx) { for (int yy = Mathf.Max(0, y - 1); yy <= Mathf.Min(_touchTex.height, y + 1); ++yy) { if (xx == x && yy == y) { continue; } // 隣接しているピクセルとの距離 int distance = Mathf.Abs(xx - x) + Mathf.Abs(yy - y); r += (_touchTex.GetPixel(xx, yy).r - 0.5f) * (distance == 1 ? effect : effect * 0.7f); } } // 色を設定 _touchTex.SetPixel(x, y, new Color(Mathf.Clamp01(r), Mathf.Clamp01(g), 0f)); } } // 色を更新 _touchTex.Apply(); // shaderに設定 _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; 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; // Scriptで制作したタッチ画像 half4 touchC = tex2D(_TouchMap, i.texcoord); // 0.5が移動していない状態なので-0.5 moveX += adj * ((touchC.r - 0.5)); moveY += adj * ((touchC.g - 0.5)); // 反転 float2 move = float2(-moveX, -moveY); float moveTotal = abs(moveX) + abs(moveY); // 波紋数 float ripples = 80; // ライト係数 float rightCoefficient = 8; // 波紋状にする float brightGap = rightCoefficient * moveTotal * (sin(ripples * moveTotal * UNITY_PI)); brightGap = clamp(-1.0, 1.0, brightGap); // easeInOutExpoの式に当てはめて計算 if (brightGap <= 0.5) { brightGap = sign(brightGap) * 2 * brightGap * brightGap; } else { brightGap = sign(brightGap) -2 * (brightGap - 1) * (brightGap - 1) + 1; } if (brightGap < 0) { brightGap *= 0.2; } half4 color = (tex2D(_MainTex, i.texcoord + move + _TextureSampleAdd)) * i.color; #ifdef UNITY_UI_CLIP_RECT color.a *= UnityGet2DClipping(i.worldPosition.xy, _ClipRect); #endif color.rgb += brightGap; #ifdef UNITY_UI_ALPHACLIP clip (color.a - 0.001); #endif return color; } ENDCG } } }
今回は以上となります。
ここまでご視聴ありがとうございました。