找回密码
 立即注册
查看: 298|回复: 0

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

[复制链接]
发表于 2022-8-16 17:44 | 显示全部楼层 |阅读模式
前言

我们在开发游戏的时候经常会有一些特殊的游戏玩法等,需要涉及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) >= 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开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-24 16:12 , Processed in 0.088880 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表