3dゲーム上で影の上を歩けるようにしたい
大学の卒業制作でunityを使ってVRゲームを作っています。
オブジェクトに光をあてて、壁に投影させた影の上をキャラクターに歩かせたいです。
そのために、影のできる位置を座標でだしてその位置にコライダーオブジェクトを配置したいと考えています。
どのようなコードだと可能か、教えていただけると嬉しいです。
自分がまったくの初心者なので、少しでもアドバイス等あれば教えていただきたいです。
発生している問題・エラーメッセージ
エラーメッセージ
該当のソースコード
ソースコード
試したこと
ここに問題に対して試したことを記載してください。
補足情報(FW/ツールのバージョンなど)
ゲームの詳しい説明を追加します。
壁の前に様々な形のオブジェクトがあり、プレイヤーはそのオブジェクトを組み合わせて
ライトを当てながら、壁に影を投影させていきます。
その影を道にしながら、キャラクターがスタートからゴール(ここでは壁の端から端)を渡り切れば
クリアというルールです。
このようなことが実行できれば、座標を求めて~のやり方でなくても教えていただきたいです。
よろしくお願いします。
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2020/01/20 08:32
回答1件
0
ベストアンサー
シャドウボリューム法をアレンジして利用できないでしょうかね?
元のメッシュを引き伸ばしてシャドウボリュームメッシュを作り、これを影描画に使う代わりにメッシュコライダーにセットしてやれば、影の形で衝突判定ができないでしょうか。
まずこのようなメッシュ加工用スクリプトを用意しておいて...
C#
1using System; 2using System.Collections.Generic; 3using System.Linq; 4using UnityEngine; 5 6public static class ShadowMeshAssembler 7{ 8 public static Mesh AssembleShadowMesh(Mesh sourceMesh) 9 { 10 if (sourceMesh == null) 11 { 12 throw new ArgumentNullException(nameof(sourceMesh)); 13 } 14 15 if (sourceMesh.GetTopology(0) != MeshTopology.Triangles) 16 { 17 throw new InvalidOperationException("Topology is not Triangles."); 18 } 19 20 var sourceVertices = sourceMesh.vertices; 21 var sourceIndices = sourceMesh.triangles; 22 var sourceTriangleCount = sourceIndices.Length / 3; 23 24 // 元のメッシュは頂点を共有して三角形を作っている箇所があるかもしれないが、面倒を減らすため 25 // 三角形単位にバラバラにしてしまい、法線も元の法線の代わりに三角形の面法線を使うことにする 26 // (なお、法線は後述のShadowVolumeCaster中で面が光源の方を向いているかどうかを判定するのに使う) 27 var vertices = sourceIndices.Select(i => sourceVertices[i]).ToArray(); 28 var normals = Enumerable.Range(0, sourceTriangleCount).SelectMany( 29 triangleIndex => 30 { 31 var i0 = triangleIndex * 3; 32 var v0 = vertices[i0]; 33 var v1 = vertices[i0 + 1]; 34 var v2 = vertices[i0 + 2]; 35 var faceNormal = Vector3.Cross(v1 - v0, v2 - v0).normalized; 36 return Enumerable.Repeat(faceNormal, 3); 37 }).ToArray(); 38 var indices = Enumerable.Range(0, sourceIndices.Length).ToArray(); 39 40 // 位置が同じ頂点を識別できるように目印を付けて... 41 var vertexReferences = vertices.Select((_, i) => new VertexReference {Index = i}).ToArray(); 42 foreach (var g in vertexReferences.GroupBy(v => vertices[v.Index])) 43 { 44 var refs = g.ToArray(); 45 var identifier = new VertexIdentifier(); 46 foreach (var v in refs) 47 { 48 v.Identifier = identifier; 49 } 50 } 51 52 // 各頂点を結ぶエッジを求め... 53 var edges = Enumerable.Range(0, sourceTriangleCount).SelectMany( 54 triangleIndex => 55 { 56 var i0 = triangleIndex * 3; 57 return Enumerable.Range(0, 3).Select(j => new Edge {From = vertexReferences[i0 + j], To = vertexReferences[i0 + ((j + 1) % 3)]}); 58 }).ToArray(); 59 60 // あるエッジに対して、両端の2つの座標を逆向きに結ぶエッジを探し 61 // ババ抜きの要領でペアを作って取り除いていく 62 // すべての三角形が面積を持ち(縮退した三角形を持つものはダメ)、 63 // メッシュ表面に破れがなく(穴が開いていたり、Quadのように外周を 64 // 持っているものはダメ)、その他トポロジー的に特殊な構造がない 65 // (面の向きが部分的に反転していたり、1つのエッジから3つ以上の面が 66 // 生えているようなものはダメ)メッシュなら、それぞれのエッジに対して 67 // 対応するパートナーが1本だけ見つかるはず 68 var tempEdgeList = edges.ToList(); 69 var edgePairs = new List<EdgePair>(); 70 try 71 { 72 while (tempEdgeList.Any()) 73 { 74 var first = tempEdgeList[0]; 75 tempEdgeList.RemoveAt(0); 76 var second = tempEdgeList.Single(e => (e.To.Identifier == first.From.Identifier) && (e.From.Identifier == first.To.Identifier)); 77 tempEdgeList.Remove(second); 78 edgePairs.Add(new EdgePair {First = first, Second = second}); 79 } 80 } 81 catch (InvalidOperationException e) 82 { 83 throw new InvalidOperationException("Edge pairing failed. Is the source mesh sure a manifold?", e); 84 } 85 86 // 見つかったエッジペアの間に面を張る 87 var additionalIndices = new List<int>(); 88 foreach (var edgePair in edgePairs) 89 { 90 var i0 = edgePair.First.From.Index; 91 var i1 = edgePair.First.To.Index; 92 var i2 = edgePair.Second.From.Index; 93 var i3 = edgePair.Second.To.Index; 94 95 if (normals[i0] == normals[i3]) 96 { 97 // このエッジペアを挟んだ2枚の面が真っ平らになっているなら 98 // 引き伸ばし過程でこのペアが引き離されることはないだろうから 99 // (つまり、一方の三角形が光源に対して表を向いているなら 100 // それと同じ平面上にあるもう一方の三角形も表を向いている 101 // はずだから)面を張る必要はない 102 continue; 103 } 104 105 additionalIndices.Add(i0); 106 additionalIndices.Add(i3); 107 additionalIndices.Add(i1); 108 additionalIndices.Add(i2); 109 additionalIndices.Add(i1); 110 additionalIndices.Add(i3); 111 } 112 113 var mesh = new Mesh 114 { 115 name = sourceMesh.name + " (Shadow)", 116 vertices = vertices, 117 normals = normals, 118 triangles = indices.Concat(additionalIndices).ToArray() 119 }; 120 return mesh; 121 } 122 123 private class VertexReference 124 { 125 public int Index; 126 public VertexIdentifier Identifier; 127 } 128 129 private class VertexIdentifier 130 { 131 } 132 133 private struct Edge 134 { 135 public VertexReference From; 136 public VertexReference To; 137 } 138 139 private struct EdgePair 140 { 141 public Edge First; 142 public Edge Second; 143 } 144}
これでたとえばキューブを加工すると、下図のようにエッジ間に面が作られます。エッジの間に面を張っておくことによって、頂点を動かしてエッジ同士が離れても隙間を生じさせずに形状を変形させることができるかと思います。
実際にオブジェクトにアタッチするスクリプトは下記のようにしてみました。
C#
1using System.Linq; 2using UnityEngine; 3 4[RequireComponent(typeof(MeshFilter))] 5public class ShadowVolumeCaster : MonoBehaviour 6{ 7 // ここに光源となるオブジェクトをセットしておく 8 // 任意のタイミングで付け外ししてよく、もしセットされていなければ 9 // シャドウボリュームは非アクティブになる 10 public Transform lightTransform; 11 12 // シャドウボリュームを何m引き延ばすか? 13 public float extension = 10.0f; 14 15 // 余計な衝突判定を防止したい場合、ここにシャドウボリューム用のレイヤーを指定する 16 [SerializeField] private int shadowLayer; 17 18 // シャドウボリュームを実際にレンダリングして可視化してみたい場合、ここにマテリアルをセットする 19 [SerializeField] private Material shadowMaterial; 20 21 private GameObject shadowVolume; 22 private Mesh shadowMesh; 23 private MeshCollider shadowCollider; 24 private Vector3[] shadowLocalPositions; 25 private Vector3[] shadowWorldPositions; 26 private Vector3[] shadowLocalNormals; 27 private Vector3[] shadowWorldNormals; 28 29 private void Reset() 30 { 31 this.shadowLayer = LayerMask.NameToLayer("Default"); 32 } 33 34 private void Start() 35 { 36 // Start時にこのオブジェクトのメッシュを基にシャドウボリュームを作成し、 37 // それをセットしたMeshColliderを持つオブジェクトを作成する 38 this.shadowMesh = ShadowMeshAssembler.AssembleShadowMesh(this.GetComponent<MeshFilter>().sharedMesh); 39 this.shadowLocalPositions = this.shadowMesh.vertices; 40 this.shadowWorldPositions = new Vector3[this.shadowLocalPositions.Length]; 41 this.shadowLocalNormals = this.shadowMesh.normals.Where((_, i) => (i % 3) == 0).ToArray(); 42 this.shadowWorldNormals = new Vector3[this.shadowLocalNormals.Length]; 43 this.shadowVolume = new GameObject(this.name + " (Shadow)") {layer = this.shadowLayer}; 44 this.shadowCollider = this.shadowVolume.AddComponent<MeshCollider>(); 45 this.shadowCollider.sharedMesh = this.shadowMesh; 46 if (this.shadowMaterial == null) 47 { 48 return; 49 } 50 51 this.shadowVolume.AddComponent<MeshFilter>().sharedMesh = this.shadowMesh; 52 this.shadowVolume.AddComponent<MeshRenderer>().sharedMaterial = this.shadowMaterial; 53 } 54 55 // Updateだと、このスクリプトがメッシュを更新した後で他のスクリプトによって 56 // オブジェクトの位置その他が書き換えられた場合にずれが生じてしまうため、 57 // LateUpdateを使うようにしてそうなる可能性を低減した 58 private void LateUpdate() 59 { 60 if (this.lightTransform == null) 61 { 62 this.shadowVolume.SetActive(false); 63 return; 64 } 65 66 var localToWorldPosition = this.transform.localToWorldMatrix; 67 var localToWorldNormal = localToWorldPosition.inverse.transpose; 68 var lightPosition = this.lightTransform.position; 69 70 // ワールド法線を求める 71 // 向きだけ分かれば十分なので、長さの正規化は省略 72 for (var i = 0; i < this.shadowLocalNormals.Length; i++) 73 { 74 this.shadowWorldNormals[i] = localToWorldNormal.MultiplyVector(this.shadowLocalNormals[i]); 75 } 76 77 // ワールド座標を求める 78 // このとき、光源と逆方向に向いている頂点は光源から遠ざけるように移動させておく 79 for (var i = 0; i < this.shadowLocalPositions.Length; i++) 80 { 81 var p = localToWorldPosition.MultiplyPoint3x4(this.shadowLocalPositions[i]); 82 var d = p - lightPosition; 83 var n = this.shadowWorldNormals[i / 3]; 84 if (Vector3.Dot(n, d) > 0.0f) 85 { 86 p += d.normalized * this.extension; 87 } 88 89 this.shadowWorldPositions[i] = p; 90 } 91 92 // メッシュを更新 93 this.shadowMesh.vertices = this.shadowWorldPositions; 94 this.shadowMesh.RecalculateBounds(); 95 this.shadowCollider.sharedMesh = this.shadowMesh; 96 this.shadowVolume.SetActive(true); 97 } 98 99 private void OnDestroy() 100 { 101 Destroy(this.shadowVolume); 102 Destroy(this.shadowMesh); 103 } 104}
Cube、Sphere、Capsule、Cylinderにスクリプトをアタッチしてみたところ、下図のように影領域を表すメッシュが作られました。
ただし、この方法ですとコライダー自体の形状が瞬間瞬間で変化していくという挙動になりますので、もしたとえば影の上にキャラクターが乗っている状態でライトを素早く動かして影の位置を変化させキャラクターを吹っ飛ばそうと思っても、うまく力が伝達されず意図通りにならない可能性もありそうです。それに対して、「影に衝突判定を付けることは可能でしょうか」でsakura_hanaさんからのご提案のあった「Raycastで壁の位置算出→Colliderオブジェクト配置→距離から大きさ調整」であれば物理的アクションへの追従性に優れているように思います。
卒業研究ということですと、まさか提出期限は今年度中でしょうかね?
締め切りが迫っているのでしたら、むやみに未知の手法に手を出すよりもご質問者さんが挙動を十分ご存じの方法をとることにして、ゲームデザインの方をそれに合わせていくらか妥協した方がいいかもしれません(それだと新規性に欠けるようでしたら、未完成で提出することになるリスクを冒してでもやってみるというのはアリかもしれませんが...)。
期限が来年度以降なら時間は十分あるかと思いますので、さまざまな方法で試行錯誤してみますとプログラミング習熟の観点からも得るものが大きいんじゃないかと思います。
投稿2020/01/17 11:03
編集2020/01/20 22:01総合スコア10811
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2020/01/20 08:30
2020/01/20 22:02
2020/01/21 03:02
2020/01/21 03:55
2020/01/21 12:52
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。