Unity Mesh切割算法详解
前言我们在开发游戏的时候经常会有一些特殊的游戏玩法等,需要涉及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 (!Intersections.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) >= 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); 牛哇
页:
[1]