質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.31%
Unity

Unityは、Unity Technologiesが開発・販売している、IDEを内蔵するゲームエンジンです。主にC#を用いたプログラミングでコンテンツの開発が可能です。

Q&A

解決済

1回答

1582閲覧

Unityで2次元のMeshに対して固定のpixel幅のアウトラインを付ける方法

concern12

総合スコア18

Unity

Unityは、Unity Technologiesが開発・販売している、IDEを内蔵するゲームエンジンです。主にC#を用いたプログラミングでコンテンツの開発が可能です。

0グッド

0クリップ

投稿2023/02/11 04:54

編集2023/02/11 04:57

AssetStoreなどからMeshに固定のpixel幅のアウトラインを付けるアセットを試してみましたが、2DのMeshで正常に動作するものがなく、自分で実装しようと試みて行き詰まったため質問です。

Meshの重心を求めて、「各頂点-重心」をすることで各頂点を拡大する方向を求め、その値を元にShaderで頂点座標を移動して、1pass目でアウトラインの色で描画、2pass目で通常の色、頂点座標で描画しましたが下の画像のようになり、各辺の太さが不均一になってしまいました。

イメージ説明

cs

1 //擬似コード 2 3 var directions = vertices.Select(v => (v-center)).ToArray(); 4 5 static Vector3 GetCenter(IEnumerable<Vector3> vector3s) 6 { 7 var count = vector3s.Count(); 8 var x = vector3s.Select(v => v.x).Sum(); 9 var y = vector3s.Select(v => v.y).Sum(); 10 var z = vector3s.Select(v => v.z).Sum(); 11 return new Vector3(x, y, z) / count; 12 }

やっていることは、下記リンク先の手法と同じことを行っています(リンク先は内側にアウトラインを付けている)。
https://forum.unity.com/threads/shader-to-create-an-outline-inside-a-polygon.1009576/#post-7033693

原因がわかる方、もしくは2次元のMeshに対して固定のpixel幅のアウトラインを付ける方法をご存知の方がいらっしゃればご教授いただければ幸いです。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

均一な太さにするとなると、形状によっては一点を中心に拡大するのでは無理なんじゃないかと思います。対策案として、下記のように拡大方向を決めるのはどうでしょうか。

C#

1using System; 2using System.Collections.Generic; 3using System.Linq; 4using UnityEngine; 5 6[RequireComponent(typeof(MeshFilter))] 7public class InflationVectorEmbedder : MonoBehaviour 8{ 9 private MeshFilter meshFilter; 10 private Mesh sourceMesh; 11 private Mesh modifiedMesh; 12 13 // 両端の頂点インデックスを組にした、ポリゴンの辺を表現する型を定義しておく 14 private readonly struct Edge : IEquatable<Edge> 15 { 16 public readonly int From; 17 public readonly int To; 18 19 public Edge(int from, int to) 20 { 21 this.From = from; 22 this.To = to; 23 } 24 25 public bool Equals(Edge other) => (this.From == other.From) && (this.To == other.To); 26 public override bool Equals(object obj) => obj is Edge other && this.Equals(other); 27 public override int GetHashCode() => HashCode.Combine(this.From, this.To); 28 public static bool operator ==(Edge left, Edge right) => left.Equals(right); 29 public static bool operator !=(Edge left, Edge right) => !left.Equals(right); 30 } 31 32 private void Start() 33 { 34 this.meshFilter = this.GetComponent<MeshFilter>(); 35 this.sourceMesh = this.meshFilter.sharedMesh; 36 37 // 三角形を構成する3つの辺を配列化する 38 var sourceIndices = this.sourceMesh.triangles; 39 static IEnumerable<Edge> EmitEdges(int i, int j, int k) 40 { 41 yield return new Edge(i, j); 42 yield return new Edge(j, k); 43 yield return new Edge(k, i); 44 } 45 var sourceEdges = Enumerable.Range(0, sourceIndices.Length / 3).SelectMany( 46 i => 47 { 48 var j = i * 3; 49 return EmitEdges(sourceIndices[j], sourceIndices[j + 1], sourceIndices[j + 2]); 50 }).ToArray(); 51 52 // 辺の配列をハッシュセットとして複製し... 53 var outlineEdges = sourceEdges.ToHashSet(); 54 55 // 逆向きの辺があれば除去することで、外周の辺だけを残す 56 // ※頂点インデックスだけで判断していますので、このやり方だと頂点座標が同じでも他の属性...たとえば 57 //  UV座標が異なるような辺は、外周扱いされて取り残されてしまうかもしれない点にご注意ください 58 outlineEdges.ExceptWith(sourceEdges.Select(edge => new Edge(edge.To, edge.From))); 59 60 // 閉じた輪になっている辺を抜き出し、外周のインデックスとして整理する 61 var outlines = new List<List<int>>(); 62 while (outlineEdges.Count > 0) 63 { 64 var outline = new List<int>(); 65 var currentEdge = outlineEdges.Take(1).First(); 66 outlineEdges.Remove(currentEdge); 67 var origin = currentEdge.From; 68 outline.Add(origin); 69 while (currentEdge.To != origin) 70 { 71 currentEdge = outlineEdges.Where(edge => edge.From == currentEdge.To).Take(1).First(); 72 outlineEdges.Remove(currentEdge); 73 outline.Add(currentEdge.From); 74 } 75 outlines.Add(outline); 76 } 77 78 // 外周膨張ベクトルを求める 79 var sourceVertices = this.sourceMesh.vertices; 80 var inflationVectors = new Vector4[sourceVertices.Length]; 81 foreach (var outline in outlines) 82 { 83 var outlineVertexCount = outline.Count; 84 for (var i = 0; i < outlineVertexCount; i++) 85 { 86 // 注目頂点のインデックス、およびその前後のインデックスを取得し... 87 var previousIndex = outline[i == 0 ? outlineVertexCount - 1 : i - 1]; 88 var index = outline[i]; 89 var nextIndex = outline[i == (outlineVertexCount - 1) ? 0 : i + 1]; 90 91 // それらの座標を取得する 92 var previousVertex = sourceVertices[previousIndex]; 93 var vertex = sourceVertices[index]; 94 var nextVertex = sourceVertices[nextIndex]; 95 96 // 注目頂点の前後の2つの辺の向きを求め... 97 var d0 = ((Vector2)(vertex - previousVertex)).normalized; 98 var d1 = ((Vector2)(nextVertex - vertex)).normalized; 99 100 // それらの中間の向きを90°倒して膨張方向とする 101 var v = d0 + d1; 102 var d = new Vector2(v.y, -v.x).normalized; 103 104 // 2つの辺の角度を加味して膨張量を増幅し... 105 var factor = 1.0f / Mathf.Sqrt(Mathf.Max(0.0f, (Vector2.Dot(d0, d1) + 1.0f) * 0.5f)); 106 107 // 膨張ベクトルをセットする 108 inflationVectors[index] = d * factor; 109 } 110 } 111 112 // 膨張ベクトルを適当な頂点属性(さしあたりtangentsにしました)に埋め込む 113 this.modifiedMesh = Instantiate(this.sourceMesh); 114 this.modifiedMesh.name = $"{this.sourceMesh.name} (Inflation Vector Embedded)"; 115 this.modifiedMesh.tangents = inflationVectors; 116 this.modifiedMesh.UploadMeshData(false); 117 this.meshFilter.sharedMesh = this.modifiedMesh; 118 } 119 120 private void OnDestroy() 121 { 122 this.meshFilter.sharedMesh = this.sourceMesh; 123 Destroy(this.modifiedMesh); 124 } 125}

上記のスクリプトをメッシュオブジェクトにアタッチし、マテリアルにはご提示いただいたPhil_42さんのシェーダーをベースにした下記のようなものを使用したところ...

ShaderLab

1Shader "Custom/Outline2D" 2{ 3 Properties 4 { 5 _MainTex ("Texture", 2D) = "white" {} 6 _Color("Color", Color) = (1,1,1,1) 7 8 _BorderColor("Border Color", Color) = (0,0,0,1) 9 _BorderWidth("Border Width", Range(0,0.2)) = 0.02 10 } 11 SubShader 12 { 13 Tags { "RenderType"="Opaque" } 14 LOD 100 15 16 // パス1...メッシュを膨張させてアウトライン色で塗る 17 Pass 18 { 19 CGPROGRAM 20 #pragma vertex vert 21 #pragma fragment frag 22 23 #include "UnityCG.cginc" 24 25 struct appdata 26 { 27 float4 vertex : POSITION; 28 float2 uv : TEXCOORD0; 29 float4 inflationVector : TANGENT; // 頂点属性にTANGENTを追加する 30 }; 31 32 struct v2f 33 { 34 float2 uv : TEXCOORD0; 35 float4 vertex : SV_POSITION; 36 }; 37 38 float4 _BorderColor; 39 float _BorderWidth; 40 41 v2f vert (appdata IN) 42 { 43 v2f OUT; 44 45 // アウトライン幅の分だけ頂点の位置をずらす 46 float3 position = IN.vertex; 47 position.xy += IN.inflationVector.xy * _BorderWidth; 48 position.z -= 0.001; 49 OUT.vertex = UnityObjectToClipPos(position); 50 51 OUT.uv = IN.uv; 52 return OUT; 53 } 54 55 fixed4 frag (v2f IN) : SV_Target 56 { 57 return _BorderColor; 58 } 59 ENDCG 60 } 61 62 63 // パス2...メッシュを膨張させずに塗る 64 Pass 65 { 66 CGPROGRAM 67 #pragma vertex vert 68 #pragma fragment frag 69 70 #include "UnityCG.cginc" 71 72 struct appdata 73 { 74 float4 vertex : POSITION; 75 float2 uv : TEXCOORD0; 76 }; 77 78 struct v2f 79 { 80 float2 uv : TEXCOORD0; 81 float4 vertex : SV_POSITION; 82 }; 83 84 sampler2D _MainTex; 85 float4 _Color; 86 87 v2f vert(appdata IN) 88 { 89 v2f OUT; 90 OUT.vertex = UnityObjectToClipPos(IN.vertex); 91 OUT.uv = IN.uv; 92 return OUT; 93 } 94 95 fixed4 frag(v2f IN) : SV_Target 96 { 97 fixed4 col = tex2D(_MainTex, IN.uv); 98 col = col * _Color; 99 return col; 100 } 101 ENDCG 102 } 103 } 104}

アウトラインは下図のようになりました。

図1

各ポリゴンは下図のように拡大されています。

図2

なお、例示しましたスクリプトはメッシュに面積0の三角形がないこと、同じ座標に重複した複数の頂点がないこと...などの条件を満たしていることを前提にしており、そうでない場合の対策は組み込んでいないため、メッシュによっては正しく動かないかもしれません。変な挙動をするようでしたら、メッシュをご提示いただければ可能であれば修正してみようと思います。

頂点の膨張方向、および膨張量の算出について

図3

Pが移動前の頂点座標、Qが膨張幅1の時の移動先です。d0とd1をそれぞれ90°回転させたd0'、d1'をP点に生やし、その先端をP0、P1とします。
△PQP0と△PQP1は斜辺を共有しており、d0'とd1'の長さはどちらも1なため、斜辺他一辺相等な直角三角形同士ですので合同と言えるはずです。そのため∠QPP0と∠QPP1の大きさは等しく、つまりPQは∠P0PP1の二等分線で、Qの方角はd0'とd1'のちょうど中間だろうと考えました。
d0'とd1'を求めてから(つまりd0、d1を90°回転させてから)中間の方向を求めてもよかったのですが、今回は私の気まぐれでd0とd1の中間を求めてから90°回転させました。

次にPQの長さですが、△PQP0をご覧いただきますと、これは直角三角形ですのでPQ * cos(θ/2) == |d0'| == 1という関係になるはずです。
つまりPQ == 1 / cos(θ/2)であり、さらにcos(θ/2)はcos(θ)から算出でき、cos(θ)はd0とd1(あるいはd0'とd1')のドット積から算出できますので、結果としてPQ == 1 / √((d0・d1 + 1) / 2)と表現できるかと思います。

投稿2023/02/11 18:10

編集2023/02/12 11:20
Bongo

総合スコア10816

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

concern12

2023/02/12 08:10

ご回答ありがとうございます! 詳細なコードまでありがとうございます。 >// それらの中間の向きを90°倒して膨張方向とする の部分の理屈がわからないのですが、なぜそのベクトルを90度回転させたものが膨張方向となり、その手法でいい感じになるのでしょうか? また、 >var factor = 1.0f / Mathf.Sqrt(Mathf.Max(0.0f, (Vector2.Dot(d0, d1) + 1.0f) * 0.5f)); の部分も理解できなかったので、お手数をおかけいたしますがご説明いただけると幸いです。
Bongo

2023/02/12 11:23

ちょっと絵を描いてみまして、解答に追記いたしました。ご不明点がありましたらご遠慮なくコメントください。
concern12

2023/02/12 14:54

理解できました!助かりました。 図がとてもわかりやすく見やすかったです。 ご丁寧にわかりやすく回答してくださりどうもありがとうございました!
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.31%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問