UnityでUIを作っています。
ボタンを用意して、フォーカスされているボタンは赤く縁取りを取りたいと思っています。
縁取り自体は別のGameObjectにしてフォーカス場所によって枠が移動&変形するようにしたいと思っているのですが良い実装方法が思い浮かびません。
ざっくりですが、アニメーションさせるとこんな感じです。
(途中赤枠の形が汚いのは絵が下手なだけです。)
また、ボタンの形、位置は様々ありますので固定のアニメーションを作成、ということは出来ません。
こういう遷移を指せる場合どのように作るとよいかアドバイスいただけますと助かります。
DoTweenを使っていますのでできればDOTweenを使っていただけると助かります。
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
回答2件
0
##パート2
Raw Imageには下記のGraphicOutline
がアタッチされており、フォーカスの移動に合わせてアウトラインを移動させます。
C#
1using System; 2using DG.Tweening; 3using UnityEngine; 4using UnityEngine.Events; 5using UnityEngine.EventSystems; 6using UnityEngine.UI; 7 8[RequireComponent(typeof(RawImage))] 9public class GraphicOutline : MonoBehaviour 10{ 11 private static readonly int CurrentField = Shader.PropertyToID("_CurrentField"); 12 private static readonly int PreviousField = Shader.PropertyToID("_PreviousField"); 13 private static readonly int Progress = Shader.PropertyToID("_Progress"); 14 private static readonly int Threshold = Shader.PropertyToID("_Threshold"); 15 16 [SerializeField] private Shader outlineShader; 17 [ColorUsage(false, true)] public Color color = Color.red; 18 [Range(0.0f, 16.0f)] public float width = 4.0f; 19 [Range(0.0f, 2.0f)] public float tweenDuration = 1.0f; 20 [SerializeField] private TweenerEvent onTween = new TweenerEvent(); 21 22 private GraphicDistanceField anchoredField; 23 private new Tweener animation; 24 private Material outlineMaterial; 25 private GameObject previousSelection; 26 private RawImage rawImage; 27 private RectTransform rectTransform; 28 29 private void Awake() 30 { 31 this.rectTransform = this.transform as RectTransform; 32 this.rawImage = this.GetComponent<RawImage>(); 33 if (this.outlineShader == null) 34 { 35 Debug.LogError($"{nameof(this.outlineShader)} not set."); 36 this.enabled = false; 37 return; 38 } 39 40 this.outlineMaterial = new Material(this.outlineShader); 41 this.rawImage.material = this.outlineMaterial; 42 this.rawImage.raycastTarget = false; 43 this.rawImage.enabled = false; 44 this.rectTransform.pivot = Vector2.one * 0.5f; 45 this.rectTransform.anchorMin = this.rectTransform.anchorMax = Vector2.zero; 46 } 47 48 private void Update() 49 { 50 var currentSelection = EventSystem.current.currentSelectedGameObject; 51 if (currentSelection != this.previousSelection) 52 { 53 var currentField = currentSelection == null 54 ? null 55 : currentSelection.GetComponent<GraphicDistanceField>(); 56 if (currentField != null) 57 { 58 // 新たに選択されたオブジェクトがアウトライン対応なら、直前の選択対象を調べ... 59 var previousField = this.previousSelection == null ? null : this.anchoredField; 60 if (previousField != null) 61 { 62 // 直前の選択対象もアウトライン対応なら、2つの間を移動するトゥイーンを開始する 63 if ((this.animation != null) && this.animation.active) 64 { 65 this.animation.Kill(); 66 this.animation = null; 67 } 68 69 this.animation = this.DoAnimation(previousField, currentField); 70 this.onTween.Invoke(this.animation); 71 } 72 else 73 { 74 // 直前の選択対象がアウトライン非対応だった、または選択解除状態だったなら 75 // 新たな選択対象の上にアウトラインを表示する 76 this.outlineMaterial.SetTexture(PreviousField, currentField.Field); 77 this.outlineMaterial.SetFloat(Progress, 0.0f); 78 this.rectTransform.sizeDelta = Vector2.one * currentField.OutlineSize; 79 this.rawImage.enabled = true; 80 } 81 82 this.anchoredField = currentField; 83 } 84 else 85 { 86 // 選択解除、またはアウトライン非対応のオブジェクトが 87 // 選択された場合はアウトラインを非表示にする 88 this.rawImage.enabled = false; 89 this.anchoredField = null; 90 } 91 92 this.previousSelection = currentSelection; 93 } 94 95 if ((this.animation == null) && this.rawImage.enabled && (this.anchoredField != null)) 96 { 97 var (anchoredPosition, threshold) = this.GetOutlineParameters(this.anchoredField); 98 this.rectTransform.anchoredPosition = anchoredPosition; 99 this.outlineMaterial.SetVector(Threshold, threshold); 100 this.outlineMaterial.color = this.color; 101 } 102 } 103 104 private void OnDestroy() 105 { 106 Destroy(this.outlineMaterial); 107 } 108 109 private (Vector2, Vector4) GetOutlineParameters(GraphicDistanceField field) 110 { 111 var canvas = this.rawImage.canvas; 112 var canvasCamera = canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera; 113 var anchoredPosition = RectTransformUtility.WorldToScreenPoint(canvasCamera, field.transform.position) + 114 field.OutlineCenterOffset; 115 var minDistance = (field.MinDistance * field.Resolution) / this.rectTransform.sizeDelta.x; 116 var outerOffset = this.width * this.width * minDistance; 117 var innerOffset = Mathf.Min(outerOffset, minDistance * 4.0f); 118 var threshold = new Vector4(0.5f - innerOffset, 0.5f, 0.5f + outerOffset, 0.0f); 119 return (anchoredPosition, threshold); 120 } 121 122 private Tweener DoAnimation(GraphicDistanceField fromField, GraphicDistanceField toField) 123 { 124 return DOVirtual.Float( 125 0.0f, 126 1.0f, 127 this.tweenDuration, 128 t => 129 { 130 var sizeDeltaFrom = Vector2.one * fromField.OutlineSize; 131 var sizeDeltaTo = Vector2.one * toField.OutlineSize; 132 var (anchoredPositionFrom, thresholdFrom) = this.GetOutlineParameters(fromField); 133 var (anchoredPositionTo, thresholdTo) = this.GetOutlineParameters(toField); 134 this.rectTransform.anchoredPosition = 135 Vector2.LerpUnclamped(anchoredPositionFrom, anchoredPositionTo, t); 136 this.rectTransform.sizeDelta = Vector2.LerpUnclamped(sizeDeltaFrom, sizeDeltaTo, t); 137 this.outlineMaterial.SetTexture(PreviousField, fromField.Field); 138 this.outlineMaterial.SetTexture(CurrentField, toField.Field); 139 this.outlineMaterial.SetVector(Threshold, Vector4.LerpUnclamped(thresholdFrom, thresholdTo, t)); 140 this.outlineMaterial.SetFloat(Progress, t); 141 this.outlineMaterial.color = this.color; 142 }); 143 } 144 145 [Serializable] 146 public class TweenerEvent : UnityEvent<Tweener> 147 { 148 } 149}
outlineShader
には下記のようなシェーダーをセットしています。
ShaderLab
1Shader "Effect/Outline" 2{ 3 Properties 4 { 5 _MainTex ("Texture", 2D) = "white" {} 6 [NoScaleOffset] _PreviousField ("Previous Field", 2D) = "white" {} 7 [NoScaleOffset] _CurrentField ("Current Field", 2D) = "white" {} 8 _Progress ("Progress", Range(0.0, 1.0)) = 0.0 9 _Threshold ("Threshold", Vector) = (0.49975585937, 0.5, 0.5009765625, 0.0) 10 _Color ("Color", Color) = (1.0, 0.0, 0.0, 1.0) 11 } 12 SubShader 13 { 14 Tags { "Queue"="Transparent" "RenderType"="Transparent" } 15 16 Cull Off 17 ZTest Always 18 ZWrite Off 19 Blend SrcAlpha OneMinusSrcAlpha 20 21 Pass 22 { 23 CGPROGRAM 24 #pragma vertex vert 25 #pragma fragment frag 26 27 #include "UnityCG.cginc" 28 29 struct appdata 30 { 31 float4 vertex : POSITION; 32 float2 uv : TEXCOORD0; 33 }; 34 35 struct v2f 36 { 37 float2 uv : TEXCOORD0; 38 float4 vertex : SV_POSITION; 39 }; 40 41 sampler2D _MainTex; 42 sampler2D _PreviousField; 43 sampler2D _CurrentField; 44 float _Progress; 45 float3 _Threshold; 46 float4 _Color; 47 48 v2f vert(appdata v) 49 { 50 v2f o; 51 o.vertex = UnityObjectToClipPos(v.vertex); 52 o.uv = v.uv; 53 return o; 54 } 55 56 fixed4 frag(v2f i) : SV_Target 57 { 58 float distance = lerp(tex2D(_PreviousField, i.uv).r, tex2D(_CurrentField, i.uv).r, _Progress); 59 float innerMask = _Threshold.x < _Threshold.y ? smoothstep(_Threshold.x, _Threshold.y, distance) : step(_Threshold.y, distance); 60 float outerMask = _Threshold.z > _Threshold.y ? smoothstep(_Threshold.z, _Threshold.y, distance) : 1.0 - step(_Threshold.y, distance); 61 return float4(_Color.rgb, innerMask * outerMask); 62 } 63 ENDCG 64 } 65 } 66}
ボタンのクリック、またはキーボードナビゲーションによるフォーカスの移動が起こると、アウトラインオブジェクトが目標へ向かって移動します。移動中の線の太さが一定でなく見苦しくはありますが、まあ一応目標に合わせて変形させることができました。
DOTweenとの連携をどうしようか悩んだのですが、結局上述のコードにあるように「遷移アニメーションはDOTweenを使い、アニメーション開始時にそのトゥイーンを引数にしたイベントを発生させる」ということにしました。
別途下記のようなスクリプトを用意し...
C#
1using DG.Tweening; 2using UnityEngine; 3 4public class TweenModifier : MonoBehaviour 5{ 6 public void ModifyTween(Tweener tweener) 7 { 8 tweener.SetEase(Ease.OutElastic).OnComplete(() => { Debug.Log("Completed"); }); 9 } 10}
GraphicOutline
のonTween
にメソッドをセットすると...
下図のようにアニメーションが変化し、移動完了時にコンソールに文章が出力されました。
投稿2021/02/02 21:35
総合スコア10811
0
ベストアンサー
物体の形状を距離場として定義し、距離場同士を混ぜ合わせることで形状をモーフィングさせる表現技法がありますが(たとえば「【UnityShader】レイマーチング入門【5】 #36 - 知識0からのUnityShader勉強」)、それと同様にできないかと思って試してみました。
実験用のシーンには下図のようにボタン3つとアウトライン用のRaw Imageを配置しています。
ボタンにアタッチしてあるGraphicDistanceField
は下記のようになっており、「Distance Fields」で紹介されていたMin Erosionとかいう方法をまねて距離場テクスチャを生成させました。
C#
1using System.Reflection; 2using UnityEngine; 3using UnityEngine.UI; 4 5[RequireComponent(typeof(Graphic))] 6public class GraphicDistanceField : MonoBehaviour 7{ 8 private static readonly PropertyInfo WorkerMeshInfo = typeof(Graphic).GetProperty( 9 "workerMesh", 10 BindingFlags.DeclaredOnly | BindingFlags.Static | BindingFlags.NonPublic); 11 private static readonly MethodInfo DoMeshGenerationInfo = typeof(Graphic).GetMethod( 12 "DoMeshGeneration", 13 BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.NonPublic); 14 private static readonly int Increment = Shader.PropertyToID("_Increment"); 15 private static readonly int MaxDistance = Shader.PropertyToID("_MaxDistance"); 16 private static readonly int Direction = Shader.PropertyToID("_Direction"); 17 18 [SerializeField][Min(16)] private int resolution = 256; 19 [SerializeField] private Shader distanceFieldShader; 20 [SerializeField] private RenderTexture field; 21 [SerializeField][Range(1.0f, 2.0f)] private float imageScale = 1.5f; 22 23 private Material distanceFieldMaterial; 24 private Graphic graphic; 25 private float maxDistance; 26 private RectTransform rectTransform; 27 28 public Vector2 OutlineCenterOffset { get; private set; } 29 public float OutlineSize { get; private set; } 30 public Texture Field => this.field; 31 public float MinDistance => 0.5f / this.maxDistance; 32 public int Resolution => this.resolution; 33 34 private void Awake() 35 { 36 this.graphic = this.GetComponent<Graphic>(); 37 this.rectTransform = this.transform as RectTransform; 38 if (this.distanceFieldShader == null) 39 { 40 Debug.LogError($"{nameof(this.distanceFieldShader)} not set."); 41 this.enabled = false; 42 return; 43 } 44 45 this.distanceFieldMaterial = new Material(this.distanceFieldShader); 46 this.field = new RenderTexture(this.resolution, this.resolution, 0, RenderTextureFormat.RFloat); 47 } 48 49 private void Start() 50 { 51 this.UpdateField(); 52 } 53 54 private void OnDestroy() 55 { 56 Destroy(this.distanceFieldMaterial); 57 Destroy(this.field); 58 } 59 60 public void UpdateField() 61 { 62 // まず、このUIオブジェクトが占めるスクリーン空間内の領域を求める 63 // rectTransformの四隅の位置を取得し... 64 var rootCanvas = this.graphic.canvas.rootCanvas; 65 var canvasCamera = rootCanvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : rootCanvas.worldCamera; 66 var worldCorners = new Vector3[4]; 67 this.rectTransform.GetWorldCorners(worldCorners); 68 var screenCorners = new Vector2[worldCorners.Length]; 69 for (var i = 0; i < worldCorners.Length; i++) 70 { 71 screenCorners[i] = RectTransformUtility.WorldToScreenPoint(canvasCamera, worldCorners[i]); 72 } 73 74 // それを内包する矩形の左下と右上を求め... 75 var bottomLeft = new Vector2(Mathf.Infinity, Mathf.Infinity); 76 var topRight = new Vector2(Mathf.NegativeInfinity, Mathf.NegativeInfinity); 77 foreach (var screenCorner in screenCorners) 78 { 79 bottomLeft = Vector2.Min(bottomLeft, screenCorner); 80 topRight = Vector2.Max(topRight, screenCorner); 81 } 82 83 // Rect構造体としておく 84 var graphicRect = new Rect 85 { 86 min = bottomLeft, 87 max = topRight 88 }; 89 90 // 各空間の座標を変換するための行列を用意する 91 var screenSize = new Vector2(Screen.width, Screen.height); 92 var halfScreenSize = screenSize * 0.5f; 93 var localToWorld = this.rectTransform.localToWorldMatrix; 94 var worldToCamera = canvasCamera == null ? Matrix4x4.identity : canvasCamera.worldToCameraMatrix; 95 var cameraToScreen = canvasCamera == null 96 ? Matrix4x4.Scale(new Vector3(1.0f, 1.0f, 0.0f)) 97 : Matrix4x4.TRS(halfScreenSize, Quaternion.identity, halfScreenSize) * canvasCamera.projectionMatrix; 98 var scale = Mathf.Max(graphicRect.size.x, graphicRect.size.y); 99 var rectToClip = Matrix4x4.Scale(Vector2.one * (this.imageScale / scale)) * 100 Matrix4x4.Translate(-graphicRect.center); 101 var cameraToTexture = rectToClip * cameraToScreen; 102 103 // このUIオブジェクトの原点とgraphicRectの中心点がどれだけずれているかを求める 104 // 後述のGraphicOutlineにおいて、UIオブジェクトの座標にこのずれを足すことで 105 // graphicRectの中心点にアウトラインを配置するようにしている 106 var graphicScreenPosition = (cameraToScreen * worldToCamera).MultiplyPoint(this.rectTransform.position); 107 this.OutlineCenterOffset = graphicRect.center - (Vector2)graphicScreenPosition; 108 109 // 同じく後述のGraphicOutlineにおいて、アウトラインのサイズを決めるための値を求める 110 this.OutlineSize = (scale * 2.0f) / this.imageScale; 111 112 // UIオブジェクトのメッシュを取得し、縦横resolutionピクセルのテクスチャ上に描画して... 113 DoMeshGenerationInfo.Invoke(this.graphic, null); 114 var mesh = WorkerMeshInfo.GetValue(null) as Mesh; 115 var activeTexture = RenderTexture.active; 116 var graphicMaterial = this.graphic.materialForRendering; 117 var sourceTexture = RenderTexture.GetTemporary(this.resolution, this.resolution); 118 RenderTexture.active = sourceTexture; 119 GL.Clear(true, true, Color.clear); 120 GL.PushMatrix(); 121 var graphicMaterialTexture = graphicMaterial.mainTexture; 122 graphicMaterial.mainTexture = this.graphic.mainTexture; 123 graphicMaterial.SetPass(0); 124 GL.LoadProjectionMatrix(cameraToTexture); 125 Graphics.DrawMeshNow(mesh, worldToCamera * localToWorld); 126 GL.PopMatrix(); 127 128 // アルファ0.5を境に二値化し、距離場計算の初期状態とする 129 var forwardField = RenderTexture.GetTemporary( 130 this.resolution, 131 this.resolution, 132 0, 133 RenderTextureFormat.RGFloat); 134 var backField = RenderTexture.GetTemporary( 135 this.resolution, 136 this.resolution, 137 0, 138 RenderTextureFormat.RGFloat); 139 this.maxDistance = this.resolution * this.resolution * 0.25f; 140 this.distanceFieldMaterial.SetFloat(MaxDistance, this.maxDistance); 141 Graphics.Blit(sourceTexture, forwardField, this.distanceFieldMaterial, 0); 142 143 // Min Erosion法により各点の距離を更新していく 144 var iterationCount = this.resolution / 2; 145 for (var j = 0; j < 2; j++) 146 { 147 this.distanceFieldMaterial.SetVector(Direction, j == 0 ? Vector2.right : Vector2.up); 148 for (var i = 0; i < iterationCount; i++) 149 { 150 this.distanceFieldMaterial.SetFloat(Increment, 1.0f + (i * 2.0f)); 151 Graphics.Blit(forwardField, backField, this.distanceFieldMaterial, 1); 152 var nextForwardField = backField; 153 backField = forwardField; 154 forwardField = nextForwardField; 155 } 156 } 157 158 // 距離場のプラス側とマイナス側を統合し、1チャンネルのテクスチャにする 159 // 距離場は符号付きのままでもいいのだが、インスペクター上でテクスチャの仕上がりを 160 // 確認しやすいよう、0.5を中心として0.0~1.0に正規化することにした 161 Graphics.Blit(forwardField, this.field, this.distanceFieldMaterial, 2); 162 163 // 後始末 164 RenderTexture.active = activeTexture; 165 RenderTexture.ReleaseTemporary(sourceTexture); 166 RenderTexture.ReleaseTemporary(forwardField); 167 RenderTexture.ReleaseTemporary(backField); 168 graphicMaterial.mainTexture = graphicMaterialTexture; 169 } 170}
distanceFieldShader
には下記のようなシェーダーがセットされています。
ShaderLab
1Shader "Hidden/DistanceField" 2{ 3 Properties 4 { 5 _MainTex ("Texture", 2D) = "white" {} 6 _MaxDistance ("Max Distance", Float) = 0.0 7 _Increment ("Increment", Float) = 0.0 8 _Direction ("Direction", Vector) = (1.0, 0.0, 0.0, 0.0) 9 } 10 SubShader 11 { 12 CGINCLUDE 13 #include "UnityCG.cginc" 14 15 struct appdata 16 { 17 float4 vertex : POSITION; 18 float2 uv : TEXCOORD0; 19 }; 20 21 struct v2f 22 { 23 float2 uv : TEXCOORD0; 24 float4 vertex : SV_POSITION; 25 }; 26 27 v2f vert(appdata v) 28 { 29 v2f o; 30 o.vertex = UnityObjectToClipPos(v.vertex); 31 o.uv = v.uv; 32 return o; 33 } 34 35 sampler2D _MainTex; 36 float4 _MainTex_TexelSize; 37 float _MaxDistance; 38 float _Increment; 39 float2 _Direction; 40 ENDCG 41 42 Cull Off 43 ZWrite Off 44 ZTest Always 45 46 Pass 47 { 48 CGPROGRAM 49 #pragma vertex vert 50 #pragma fragment frag 51 52 float4 frag(v2f i) : SV_Target 53 { 54 return tex2D(_MainTex, i.uv).a > 0.5 ? float4(0.0, _MaxDistance, 0.0, 0.0) : float4(_MaxDistance, 0.0, 0.0, 0.0); 55 } 56 ENDCG 57 } 58 59 Pass 60 { 61 CGPROGRAM 62 #pragma vertex vert 63 #pragma fragment frag 64 65 float4 frag(v2f i) : SV_Target 66 { 67 float2 offset = _MainTex_TexelSize.xy * _Direction; 68 return float4(min(tex2D(_MainTex, i.uv).rg, min(tex2D(_MainTex, i.uv + offset).rg, tex2D(_MainTex, i.uv - offset).rg) + _Increment), 0.0, 0.0); 69 } 70 ENDCG 71 } 72 73 Pass 74 { 75 CGPROGRAM 76 #pragma vertex vert 77 #pragma fragment frag 78 79 float4 frag(v2f i) : SV_Target 80 { 81 float2 distance = tex2D(_MainTex, i.uv).rg; 82 return 0.5 + (dot(distance, float2(1.0, -1.0)) * 0.5 / _MaxDistance); 83 } 84 ENDCG 85 } 86 } 87}
(投稿字数が尽きてしまったため、パート2へ移動します...)
投稿2021/02/02 21:34
総合スコア10811
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2021/02/03 14:14