序文
ゲームを開発していると、メッシュカットが必要な特殊なゲームプレイなどがよくあります。たとえば、3D で果物をカットしたり、マップの市壁に穴を掘ったりする場合、今日はプロジェクトで使用する必要があるメッシュ切断の問題を解決するのに役立つメッシュ切断アルゴリズムを共有します。この記事ではメッシュカットを主に以下の観点から解説します。
はい、ここにゲーム開発交流グループがあります。皆さんもクリックして一緒に開発経験を交換していただければ幸いです。
(1) プレイヤーのタッチ操作をどのように受け付けて切断面を生成するか。
(2) 3D モデルのメッシュ オブジェクトの主要なデータ コンポーネント。
(3) モデルメッシュ切断アルゴリズムのステップの詳細な説明。
プレイヤーのタッチ操作を受け付けて切断面を生成する方法
モデルを切断する際には、まずプレイヤーのタッチ操作に基づいて切断面を生成する必要があります。図 1 に示すように、プレイヤーの黒い線に従って、黒い線に基づいて切断面を生成する必要があります。主な手順は次のとおりです。
(1) 開始点のスクリーン座標を取得し、3D カメラと組み合わせて、開始点のスクリーン座標をカメラのビューポート座標系に転送します。
if (!dragg
ing && Input.GetMouseButtonDown(0)) {
start = cam.ScreenToViewportPoint(Input.mousePosition);
dragging = true;
}
(2) 終点のスクリーン座標を取得し、それを 3D カメラと組み合わせて、焦点のスクリーン座標をカメラのビューポート座標系に置きます。
if (dragging && Input.GetMouseButtonUp(0)) {
// Finished dragging. We draw the line segment
end = cam.ScreenToViewportPoint(Input.mousePosition);
dragging = false;
}
(3) カメラを基準に、始点と終点から 2 本の光線を生成し、光線とカメラの Near 面との交点を計算します。
var startRay = cam.ViewportPointToRay(start);
var endRay = cam.ViewportPointToRay(end);
var startPoint = startRay.GetPoint(cam.nearClipPlane),
var endPoint = endRay.GetPoint(cam.nearClipPlane),
(4) 2点ができたら線があり、平面を決めるには方向が必要です(線は方向に従って移動して面になります) この方向はendRayのレイの方向をベクトルとしますA.
(5)終点-始点からベクトルを求め、endRay方向の画像量と組み合わせて外積し、傾斜面の上回転ベクトルを求める。図に示すように、ヤゲンは始点と終点を同時に通過するため、切削面が決定されます。
var planeTangent = (end - start).normalized;
if (planeTangent == Vector3.zero)
planeTangent = Vector3.right;
var normalVec = Vector3.Cross(depth, planeTangent);
startとnormalVecに基づいて切断面を決定することができます.それは開始点を通過する平面であり、上方向ベクトルがnormalVecです。
3D モデルの主要なデータ コンポーネント メッシュ オブジェクト
メッシュ切断アルゴリズムを詳しく説明する前に、モデルのメッシュ データの主にどの部分が含まれているかを説明し、新しいメッシュを生成するときに、データの各部分が何を表しているかを知ることになります。 Unity のメッシュ データは Mesh オブジェクトに生成されます。Mesh オブジェクトには主に次の重要なデータが含まれます。
頂点データ: メッシュ オブジェクト内のすべてのメッシュ頂点。
法線データ: メッシュ オブジェクトの各頂点に対応する法線データ。
テクスチャ UV データ: メッシュ オブジェクトの各頂点に対応するテクスチャ UV データ。
三角形頂点インデックス データ: Mesh オブジェクトの各面に対応する三角形データ。三角形は頂点インデックスに基づいています。たとえば、面 A は 3 つの頂点 A1、A2、A3 で構成されます。これが頂点インデックスです。A1 は頂点データにインデックス付けされて頂点データを取得します。A1 は法線データにインデックス付けされて法線を取得します。 Unity では、次のように、Mesh オブジェクトのインターフェイス関数を通じて上記のデータを取得できます。
mesh.GetVertices(ogVertices);
mesh.GetTriangles(ogTriangles, 0);
mesh.GetNormals(ogNormals);
mesh.GetUVs(0, ogUvs);
モデルメッシュ切断アルゴリズムの手順の詳細な説明
切断面を決定したら、メッシュ切断アルゴリズムの主なステップを分析しましょう。
ステップ 1: モデル座標系で切断面を取得する
「切断面」の点 (平面が通過する点) + 法線 (平面の上向きベクトル) をビューポート座標から切断するモデル座標系に転送します。
var localNormal = ((Vector3)(obj.transform.localToWorldMatrix.transpose * normal)).normalized;
var localPoint = obj.transform.InverseTransformPoint(point);
次に、localPoint に従って、法線ベクトルは localNormal となり、Plane オブジェクトが生成されます。ここでの平面オブジェクトはモデルの座標系を基準としています。
var slicePlane = new Plane();
slicePlane.SetNormalAndPosition(localNormal , localPoint);
ステップ 2: 切断面に従って、オブジェクトを前方部分と後方部分に分割します。オブジェクトを切断した後、平面より上の部分が正の部分になります (正の部分) )、平面より下の部分は ネガティブです。
ステップ 3: 次のメッシュが平面と交差するかどうかを判断します。メッシュと平面に焦点がまったくない場合、アルゴリズムはメッシュを分割せずに終了します。
if (!Intersections.BoundPlaneIntersect(mesh, ref slice))
return false;
ステップ 4: 元のメッシュのメッシュ データを取得し、切断後に保存された順方向および逆方向のメッシュ データ セットをクリーンアップします。
mesh.GetVertices(ogVertices);
mesh.GetTriangles(ogTriangles, 0);
mesh.GetNormals(ogNormals);
mesh.GetUVs(0, ogUvs);
PositiveMesh.Clear();
NegativeMesh.Clear();
addedPairs.Clear();
ステップ 5: すべての頂点をトラバースして、どの頂点が切断面の上部にあり、どの頂点が切断面の下部にあるのかを確認し、それらを別々に配置します。ポジティブメッシュとネガティブメッシュ。
for(int i = 0; i < ogVertices.Count; ++i) {
if (slice.GetDistanceToPoint(ogVertices[i]) >= 0)
PositiveMesh.AddVertex(ogVertices, ogNormals, ogUvs, i);
else
NegativeMesh.AddVertex(ogVertices, ogNormals, ogUvs, i);
}
ステップ 6: 新しい表面を生成する準備として、切断面とオブジェクトの間の交点を計算します。
// 3. Separate triangles and cut those that intersect the plane
for (int i = 0; i < ogTriangles.Count; i += 3)
{
if (intersect.TrianglePlaneIntersect(ogVertices, ogUvs, ogTriangles, i, ref slice, PositiveMesh, NegativeMesh, intersectPair))
addedPairs.AddRange(intersectPair);
}
ステップ 7: 新しいポイントに従って新しい面を生成する
private void FillBoundaryFace(List<Vector3> added)
{
// 1. Reorder added so in order ot their occurence along the perimeter.
MeshUtils.ReorderList(added);
// 2. Find actual face vertices
var face = FindRealPolygon(added);
// 3. Create triangle fans
int t_fwd = 0,
t_bwd = face.Count - 1,
t_new = 1;
bool incr_fwd = true;
while (t_new != t_fwd && t_new != t_bwd)
{
AddTriangle(face, t_bwd, t_fwd, t_new);
if (incr_fwd) t_fwd = t_new;
else t_bwd = t_new;
incr_fwd = !incr_fwd;
t_new = incr_fwd ? t_fwd + 1 : t_bwd - 1;
}
}
ステップ 8: 新しいオブジェクトをインスタンス化し、2 つの切り取ったメッシュをコピーします。1 つは元のノードに、もう 1 つは新しく作成したノードにコピーします。
// Create new Sliced object with the other mesh
GameObject newObject = Instantiate(obj, ObjectContainer);
newObject.transform.SetPositionAndRotation(obj.transform.position, obj.transform.rotation);
var newObjMesh = newObject.GetComponent<MeshFilter>().mesh;
// Put the bigger mesh in the original object
// TODO: Enable collider generation (either the exact mesh or compute smallest enclosing sphere)
ReplaceMesh(mesh, biggerMesh);
ReplaceMesh(newObjMesh, smallerMesh);
添付: ビデオチュートリアル