Mesh切割算法/ 3.1 Unity Mesh切割算法详解

前言

我们在开发游戏的时候经常会有一些特殊的游戏玩法等,需要涉及Mesh切割。比如3D切水果, 在地图的城墙上挖一个洞,今天给大家来分享一个Mesh切割的算法,帮助大家解决项目中需要用到的Mesh切割的问题。本文主要从一下几个方面来讲解Mesh切割。

对惹,这里有一个游戏开发交流小组,希望大家可以点击进来一起交流一下开发经验呀!

(1) 如何接受玩家的触摸操作,生成切割面;

(2) 3D模型Mesh对象中的主要数据组成;

(3) 模型Mesh切割算法步骤详解;

动图封面

如何接受玩家的触摸操作,生成切割面

做模型切割的时候,我们首先要根据玩家的触摸操作来生成一个切割平面。如图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) 基于摄像机,将起点与终点生成两条射线出来,并计算出射线与摄像机的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。

(5) 把终点-起点得到一个向量,结合方向endRay射线方向的像量,来做叉积,这样就得到了斜面的旋转后的up向量。如图所示,同时斜面过start, end中的点,所以这样就可以确定切割面出来。

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对象中的所有的网格顶点;

法线数据: Mesh对象中每个顶点所对应的法线数据;

纹理uv数据: Mesh对象中每个顶点所对应的纹理uv数据;

三角形顶点索引数据: 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切割算法的主要步骤:

Step1: 获取模型坐标系下的切割面

将”切割面” point(平面经过的点)+normal(平面的up向量)由视口坐标转到要切割的模型坐标系:

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);

Step2: 根据切割面,来将物体分成正向部分与反向部分,切割物体后,平面上面的是正向部分(Positive),平面下面的是反向部分(Negative)

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[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;
        }
}

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);
 

附:视频教程

Unity / 精选推荐 Unity架构师进阶: Mesh切割算法详解​www.bycwedu.com/promotion_channels/1997539005

猜你喜欢

转载自blog.csdn.net/Thomas_YXQ/article/details/134579036
今日推荐