前言
我們在開發遊戲的時候經常會有一些特殊的遊戲玩法等,需要涉及Mesh切割。比如3D切水果, 在地圖的城墻上挖一個洞,今天給大家來分享一個Mesh切割的演算法,幫助大家解決計畫中需要用到的Mesh切割的問題。本文主要從一下幾個方面來講解Mesh切割。
對啦!這裏有個遊戲開發交流小組裏面聚集了一幫熱愛學習unity的零基礎小白,也有一些正在從事unity開發的技術大佬,源碼素材獲取 歡迎你來交流學習。
(圖1)
如何接受玩家的觸摸操作,生成切割面
做模型切割的時候,我們首先要根據玩家的觸摸操作來生成一個切割平面。如圖1所示,根據玩家的黑色的劃線,我們要基於黑色的劃線來生成一個切割平面。主要的步驟如下:
(1) 獲取起點的螢幕座標,並結合3D攝影機,把起點的螢幕座標轉到攝影機的視口座標系下。
if (!dragging && 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) 基於攝影機,將起點與終點生成兩條射線出來,並計算出射線與攝影機的Near平面的交點。
var startRay = cam.ViewportPointToRay(start);
var endRay = cam.ViewportPointToRay(end);
var startPoint = startRay.GetPoint(cam.nearClipPlane),
var endPoint = endRay.GetPoint(cam.nearClipPlane),
(4) 有了兩個點後,就有了一條線,還要有一個方向才能確定一個平面(線按照方向移動,就成了面),這個方向我們取endRay射線的方向為向量A。
(圖2)
(圖3)
var planeTangent = (end - start).normalized;
if (planeTangent == Vector3.zero)
planeTangent = Vector3.right;
var normalVec = Vector3.Cross(depth, planeTangent);
根據 start與normalVec,就可以確定我們的切割面了,它就是過start點,向上的方向向量為normalVec的平面。
3D模型Mesh物件中的主要數據組成
在詳細的講解Mesh切割演算法之前,先給大家講解一下一個模型的Mesh數據主要包含哪些部份,等下生成新的Mesh的時候,我們知道每一部份的數據都代表什麽。Unity中網格數據被生成到Mesh物件裏面,一個Mesh物件主要包含以下重要的數據:
三角形頂點索引數據 : Mesh物件中的每個面對應的三角形數據,而三角形是基於頂點索引的。如面A由三個頂點A1, A2, A3組成,這裏是頂點索引,A1由索引,到頂點數據裏面獲取頂點數據,由A1索引到法線數據裏面獲取法線。在Unity裏面可以透過Mesh物件的介面函式獲取上面的數據,如下:
mesh.GetVertices(ogVertices);
mesh.GetTriangles(ogTriangles, 0);
mesh.GetNormals(ogNormals);
mesh.GetUVs(0, ogUvs);
模型Mesh切割演算法步驟詳解
確定了切割面以後,接下來我們來分析Mesh切割演算法的主要步驟:
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);
Step3: 判斷以下Mesh與平面是否相交,如果Mesh與平面完全沒有焦點,則演算法結束,不用分割Mesh;
if (!Interps.BoundPlaneIntersect(mesh, ref slice))
return false;
Step4: 獲取原來Mesh中的網格數據, 並清理切割後存放的正向與反向的Mesh數據集合。
mesh.GetVertices(ogVertices);
mesh.GetTriangles(ogTriangles, 0);
mesh.GetNormals(ogNormals);
mesh.GetUVs(0, ogUvs);
PositiveMesh.Clear();
NegativeMesh.Clear();
addedPairs.Clear();
Step5: 遍歷所有的頂點,看哪些頂點在切割面的上部,哪些頂點在切割面的下步,將他們分開放置到PositiveMesh與NegativeMesh中。
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);
}
Step6: 將切割面與物體的交點計算出來,為生成新的面做好準備;
// 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);
}
Step7: 根據新增的點來生成新的面
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;
}
}
(圖4)
Step8: 例項化一個新物體,將切割後的2個Mesh,一個復制給原來的節點,一個復制給新建立的節點。
// 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);