【muzzik 分享】3D模型平面切割

# 前言
一年一度的征稿到了,倒腾点存货,3D平面切割通常用于一些解压游戏里,例如水果忍者,切菜这些,今天我就给大家讲讲怎么实现3D切割以及其原理,帮助大家更理解3D中的 Mesh(网格),以及UV贴图和法线
由于和参赛帖另一篇文章主题相同,先自证一下这是存货
本来想等 Store 审核通过再发,但是免得大家说我抄袭就先上了

# 准备工作
了解模型
想要切割一个模型,首先要了解模型是怎么组成的,其实所有模型都是由一个个三角面组成,如下


一个平面最少由两个三角形组成,而模型就是由多个三角形组成,我们要切割模型,其实就是做三角形的分割
做三角形的分割,首先我们需要一个方向,在 2D 中是一个方向向量,在 3D 中就是一个平面
创建平面对象
在 Creator3.x 版本下怎么创建这个平面对象?在 cc.geometry 中有很多几何对象类型,我们就使用其中的 cc.geometry.Plane 进行创建
const node_ui_transform =node_.getComponent(cc.UITransform) ||node_.addComponent(cc.UITransform);
const panel_ui_transform =panel_.getComponent(cc.UITransform) ||panel_.addComponent(cc.UITransform);this._plane = cc.geometry.Plane.fromNormalAndPoint(new cc.geometry.Plane(),// 法线方向(基于被切割节点坐标系,平面上方到自身的方向向量)node_ui_transform.convertToNodeSpaceAR(panel_ui_transform.convertToWorldSpaceAR(cc.Vec3.UP)).subtract(node_ui_transform.convertToNodeSpaceAR(panel_.worldPosition)).normalize(),// 平面在切割节点的本地坐标node_ui_transform.convertToNodeSpaceAR(panel_.worldPosition)
);
- node_:被切割节点
- panel_:平面节点
获取网格数据
有了用于切割时的平面对象,我们还需要 Mesh 数据,这些数据有什么?看下图

- 顶点数据:例如 [p1,p2,p3],存放所有三角形点的坐标数据
- 顶点索引:例如 [0,1,2],是顶点数据数组的下标,用来指定下标的数据组成一个三角形
怎么获取?
// 获取 cc.Mesh
this._mesh = node_.getComponent(cc.MeshRenderer)!.mesh!;/** 网格数据 */
const mesh = cc.utils.readMesh(this._mesh, 0);
注意,这里只是获取的下标为 0 的子网格,如果一个模型包含多个子网格,那么还是需要遍历获取再切割,可以通过 this._mesh.struct.primitives.length 获取子网格数量
# 开始切割
前面说了模型是由一个个三角形组成的,那么我们只需要遍历模型的网格数据针对每个和平面相交的三角形切割就行了
-
首先需要准备两个
cc.primitives.IGeometry类型的对象,用于分别存储正反面的网格数据 -
遍历需要切割的网格三角形数据,与平面相交就切割三角形后放入对应的
cc.primitives.IGeometry,不相交就不需要切割
/** 三角形点 */
const triangle_point_as = [new _mesh_slicer.point_data(),new _mesh_slicer.point_data(),new _mesh_slicer.point_data(),
];
/** 正面 */
const positive_geometry = (this._positive_mesh.geometry =this._create_geometry());
/** 反面 */
const negative_geometry = (this._negative_mesh.geometry =this._create_geometry());// 遍历三角形切割
for (let k_n = 0, len_n = geometry_.indices!.length;k_n < len_n;k_n += 3
) {/** 三角形索引 */const indices_ns = [geometry_.indices![k_n],geometry_.indices![k_n + 1],geometry_.indices![k_n + 2],];...
}
判断三角形是否与平面相交
这里我们只需要知道三角形的顶点是否在平面的正面或者反面就可以判断是否相交,
如果三个点全在一侧则肯定不相交,如果不全在一侧则一点相交 ,我们可以使用点乘 dot 判断在平面的哪一侧
// 平面的法线 dot(三角形点) - 平面距离原点距离 > 0 即为正面
positive_b = this._plane.n.dot(p) - this._plane.d > 0;
和上面说的一样,如果三角形的三个点 positive_b 一致则是全在平面的一侧不需要切割,不一致则需要切割
// 所有顶点都在同一侧
if (triangle_point_as[0].positive_b === triangle_point_as[1].positive_b &&triangle_point_as[1].positive_b === triangle_point_as[2].positive_b
) {const mesh = triangle_point_as[0].positive_b? this._positive_mesh: this._negative_mesh;// 更新旧索引triangle_point_as.forEach((v) => {this._update_old_indices(mesh, v);});// 添加点到几何数据this._add_point_to_geometry(mesh.geometry, triangle_point_as);
}
// 不在同一侧则切割三角形
else {// 顶点 0,1 在同一侧if (triangle_point_as[0].positive_b === triangle_point_as[1].positive_b) {this._slice_triangle([triangle_point_as[2],triangle_point_as[0],triangle_point_as[1],]);}// 顶点 0,2 在同一侧else if (triangle_point_as[0].positive_b === triangle_point_as[2].positive_b) {this._slice_triangle([triangle_point_as[1],triangle_point_as[2],triangle_point_as[0],]);}// 顶点 1,2 在同一侧else {this._slice_triangle([triangle_point_as[0],triangle_point_as[1],triangle_point_as[2],]);}
}
切割三角形

- (i1, i2) :平面
- (p0, p1, p2) :原本的三角形(逆时针为正面)
- (p0, i1, i2) :切割后的三角形
- (i1, p1, p2) : 切割后的三角形2
- (i2, i1, p2) : 切割后的三角形3
- 如果三角形三个顶点形成的线段不与平面相交,那么则不需要新建顶点
- 如果三角形线段与平面相交,则切割为三个三角形,怎么判断相交,看下面
怎么确定交点(i1, i2)?
交点也就是 i1,i2 的坐标,知道了交点才能分割三角形,以下以获取 i1 的坐标为例
- 射线公式:P = P0 + tV;
- 平面公式:A(P−P1) = 0;
这两个公式里, P 是射线上也在平面上的一个点,也就是射线和平面的交点。 P0 是射线的起点, V 是射线的方向。 t 是一个数字,当它变化时,P就会在射线上移动。 P1 是平面上的一个特定点, A 是平面的法向量。
我们将射线的公式代入到平面的公式中,就得到: A(P0 + tV - P1) = 0,求解为:t = (A * (P1 - P0))/(A * V),这里 Creator 有内置的函数,就不用自己写了
步骤为:
- 确定 i1 的坐标,从 p0 到 p1 的方向创建一条射线
cc.geometry.Ray.fromPoints(ray, p0, p1); - 计算与平面的交点距离
const distance_n = cc.geometry.intersect.rayPlane(ray, this._plane); - 获取交点坐标
ray.computeHit(point, distance_n);
这样就得到了交点,除了交点,我们还要计算法线和UV
法线和UV
法线
法线就是决定你模型的凹凸效果的,它存在于每个顶点数据中,是一个三维向量
UV
UV 就是你的模型贴图的图片坐标,它决定了你这个顶点位置展示的贴图内容在图片的什么部分,是一个二维向量
法线和UV的计算很简单,根据交点的位置使用 lerp 函数从起点和终点线段做一个插值就行了
/*** 获取线段和平面交点* @param point_as_ 线段起始和结束点* @param out_point_ 输出点* @returns*/
private _get_line_segment_and_plane_intersect(out_point_: _mesh_slicer.point_data,point_as_: _mesh_slicer.point_data[]
): _mesh_slicer.point_data {/** 射线 */const ray = cc.geometry.Ray.fromPoints(this._temp_tab.ray, point_as_[0].position_v3, point_as_[1].position_v3);/** 距离 */const distance_n = cc.geometry.intersect.rayPlane(ray, this._plane);/** 两点之间的长度 */const line_length_n = this._temp_tab.value_v3.set(point_as_[0].position_v3).subtract(point_as_[1].position_v3).length();// 计算碰撞位置ray.computeHit(out_point_.position_v3, distance_n);// 计算 uvcc.Vec2.lerp(out_point_.uv_v2, point_as_[0].uv_v2, point_as_[1].uv_v2, distance_n / line_length_n);// 计算法线cc.Vec3.lerp(out_point_.normal_v3, point_as_[0].normal_v3, point_as_[1].normal_v3, distance_n / line_length_n);return out_point_;
}/*** 获取线段和平面交点* @param point_as_ 线段起始和结束点* @param out_point_ 输出点* @returns*/private _get_line_segment_and_plane_intersect(out_point_: _mesh_slicer.point_data,point_as_: _mesh_slicer.point_data[]): _mesh_slicer.point_data {/** 射线 */const ray = cc.geometry.Ray.fromPoints(this._temp_tab.ray, point_as_[0].position_v3, point_as_[1].position_v3);/** 距离 */const distance_n = cc.geometry.intersect.rayPlane(ray, this._plane);/** 两点之间的长度 */const line_length_n = this._temp_tab.value_v3.set(point_as_[0].position_v3).subtract(point_as_[1].position_v3).length();// 计算碰撞位置ray.computeHit(out_point_.position_v3, distance_n);// 计算 uvcc.Vec2.lerp(out_point_.uv_v2, point_as_[0].uv_v2, point_as_[1].uv_v2, distance_n / line_length_n);// 计算法线cc.Vec3.lerp(out_point_.normal_v3, point_as_[0].normal_v3, point_as_[1].normal_v3, distance_n / line_length_n);return out_point_;}
/*** 切割三角形* @param point_as_ 三角形点(逆时针,首个点切割后为单三角)*/
private _slice_triangle(point_as_: _mesh_slicer.point_data[]): void {/** 单三角网格 */const mesh = point_as_[0].positive_b? this._positive_mesh: this._negative_mesh;/** 双三角网格 */const mesh2 = point_as_[0].positive_b? this._negative_mesh: this._positive_mesh;// 获取交点this._get_line_segment_and_plane_intersect(this._temp_tab.point, [point_as_[0],point_as_[1],]);this._get_line_segment_and_plane_intersect(this._temp_tab.point2, [point_as_[0],point_as_[2],]);// 添加单三角{// 更新索引this._update_new_indices(mesh, this._temp_tab.point, point_as_[1]);this._update_new_indices(mesh, this._temp_tab.point2, point_as_[2]);this._update_old_indices(mesh, point_as_[0]);// 添加三角this._add_point_to_geometry(mesh.geometry, [point_as_[0],this._temp_tab.point,this._temp_tab.point2,]);}// 添加双三角{// 更新索引this._update_new_indices(mesh2, this._temp_tab.point, point_as_[1]);this._update_new_indices(mesh2, this._temp_tab.point2, point_as_[2]);this._update_old_indices(mesh2, point_as_[1]);this._update_old_indices(mesh2, point_as_[2]);// 添加三角this._add_point_to_geometry(mesh2.geometry, [this._temp_tab.point2,this._temp_tab.point,point_as_[1],]);this._add_point_to_geometry(mesh2.geometry, [this._temp_tab.point2,point_as_[1],point_as_[2],]);}
}
简单来说就是根据交点将原本的 1 个三角形分为 3 个三角形,再根据自己正反面的位置添加到对应的正反面网格数据中并更新索引
# 生成平面

在切割结束后如果没有问题你会发现这是个空心模型,如果我们需要一个平面封住切口呢?怎么做?
这就被称为平面的 三角剖分
简单的三角剖分方案
-
求平均点,不完全支持凹多边形

-
左右横跳,不完全支持凹多边形

-
单点遍历,不完全支持凹多边形

不支持凹面多边形的后果
可以看下图

这样的话,无论是使用平均点,还是图中的单点遍历新建三角形,都会有可能出现生成的三角形错误的情况
那么如何做?步骤如下
-
记录新增的顶点坐标并排序(连线)
-
将排序后的多边形顶点分解为凸多边形
-
为所有凸多边形生成三角形
怎么判断凹凸?

判断 p0 - p1 - p2 的夹角角度即可,这也是我们需要对新增顶点坐标排序的原因
将凹多边形分解为凸多边形
在找到凹角之后,我们只需要从 p1 的位置开始遍历至顶点,只要找到 p0 - p1 - pn 夹角不为凹角的 pn 顶点就可以分割为两个多边形,再对分割后的多边形重复执行此操作
平面带孔的情况

将排序后的两个多边形合并为一个,将内多边形的点连接到最近的一个外多边形,组合成为一个单独的多边形
但是还有一个问题,那就是单独的两个多边形可以依靠法线和碰撞检测来判断当前多边形是否在另一个内,那么多个多边形嵌套呢?
我这里想到的是使用面积判断,从大到小对多边形排序,内多边形的面积一定比外多边形小
# 源码
-
保证切割后模型原表面法线、UV 的正常
-
切口平面支持凹多边形
-
支持同时切割多个模型
-
使用共享顶点,可以节省模型内存占用
Cocos Store:https://store.cocos.com/app/detail/6118
# 其他参赛文章
原生预览调试!我给Cocos加了个新功能,原生开发者福音
相关文章:
【muzzik 分享】3D模型平面切割
# 前言 一年一度的征稿到了,倒腾点存货,3D平面切割通常用于一些解压游戏里,例如水果忍者,切菜这些,今天我就给大家讲讲怎么实现3D切割以及其原理,帮助大家更理解3D中的 Mesh(网格),以及UV贴图和…...
SCI一区 | Matlab实现OOA-TCN-BiGRU-Attention鱼鹰算法优化时间卷积双向门控循环单元融合注意力机制多变量时间序列预测
SCI一区 | Matlab实现OOA-TCN-BiGRU-Attention鱼鹰算法优化时间卷积双向门控循环单元融合注意力机制多变量时间序列预测 目录 SCI一区 | Matlab实现OOA-TCN-BiGRU-Attention鱼鹰算法优化时间卷积双向门控循环单元融合注意力机制多变量时间序列预测预测效果基本介绍模型描述程序…...
nodejs安装常用命令
安装 Node.js 后,你可以在命令行中使用以下常用命令: node:启动 Node.js 的交互式解释器,可以直接在命令行中执行 JavaScript 代码。 npm install <package-name>:安装一个 Node.js 模块,<packag…...
使用 Prometheus 在 KubeSphere 上监控 KubeEdge 边缘节点(Jetson) CPU、GPU 状态
作者:朱亚光,之江实验室工程师,云原生/开源爱好者。 KubeSphere 边缘节点的可观测性 在边缘计算场景下,KubeSphere 基于 KubeEdge 实现应用与工作负载在云端与边缘节点的统一分发与管理,解决在海量边、端设备上完成应…...
OSI七层网络模型 —— 筑梦之路
在信息技术领域,OSI七层模型是一个经典的网络通信框架,它将网络通信分为七个层次,每一层都有其独特的功能和作用。为了帮助记忆这七个层次,有一个巧妙的方法:将每个层次的英文单词首字母组合起来,形成了一句…...
状态模式:管理对象状态转换的动态策略
在软件开发中,状态模式是一种行为型设计模式,它允许一个对象在其内部状态改变时改变它的行为。这种模式把与特定状态相关的行为局部化,并且将不同状态的行为分散到对应的状态类中,使得状态和行为可以独立变化。本文将详细介绍状态…...
【论文阅读】MCTformer: 弱监督语义分割的多类令牌转换器
【论文阅读】MCTformer: 弱监督语义分割的多类令牌转换器 文章目录 【论文阅读】MCTformer: 弱监督语义分割的多类令牌转换器一、介绍二、联系工作三、方法四、实验结果 Multi-class Token Transformer for Weakly Supervised Semantic Segmentation 本文提出了一种新的基于变换…...
FMix: Enhancing Mixed Sample Data Augmentation 论文阅读
1 Abstract 近年来,混合样本数据增强(Mixed Sample Data Augmentation,MSDA)受到了越来越多的关注,出现了许多成功的变体,例如MixUp和CutMix。通过研究VAE在原始数据和增强数据上学习到的函数之间的互信息…...
2024蓝桥A组A题
艺术与篮球(蓝桥) 问题描述格式输入格式输出评测用例规模与约定解析参考程序难度等级 问题描述 格式输入 无 格式输出 一个整数 评测用例规模与约定 无 解析 模拟就好从20000101-20240413每一天计算笔画数是否大于50然后天数; 记得判断平…...
Linux journalctl命令详解
文章目录 1.介紹2.概念设置system time基本的日志查阅方法按时过滤日志(by Time)显示本次启动以来的日志(Current Boot)按Past Boots按时间窗口按感兴趣的消息筛选按unit按进程、用户、Group ID按组件路径显示内核消息按消息优先级…...
恢复MySQL!是我的条件反射,PXB开源的力量...
📢📢📢📣📣📣 哈喽!大家好,我是【IT邦德】,江湖人称jeames007,10余年DBA及大数据工作经验 一位上进心十足的【大数据领域博主】!😜&am…...
Storm详细配置
一、认识Storm Apache Storm是个实时数据处理的“大能”,它可以实时接收、处理并转发大量数据流,就像一个高速运转的物流中心,确保数据及时、准确地到达目的地。我们要做的,就是把这个物流中心搭建起来,并且根据我们的…...
linux redis部署教程
单节点部署: 单节点部署 Redis 非常简单,只需要在一台服务器上安装 Redis 服务即可。以下是在 Linux 环境下的单节点部署步骤: 安装 Redis:打开终端,并执行以下命令来更新软件包列表并安装 Redis 服务器:…...
【Java】隐式锁(synchronized):如何解决餐厅等座的并发难题
当你走进一家熙熙攘攘的餐厅,准备享受一顿美味的晚餐时,你是否曾想过,这里正上演着一场场微观的线程战争?在这个场景中,每一张桌子都代表着珍贵的共享资源,而每一位顾客(线程)都在争…...
科技论文和会议录制高质量Presentation Video视频方法
一、背景 机器人领域,许多高质量的期刊和会议(如IEEE旗下的TRO,RAL,IROS,ICRA等)在你的论文收录后,需要上传一个Presentation Video材料,且对设备兼容性和视频质量有较高要求&#…...
Spring高手之路17——动态代理的艺术与实践
文章目录 1. 背景2. JDK动态代理2.1 定义和演示2.2 不同方法分别代理2.3 熔断限流和日志监控 3. CGLIB动态代理3.1 定义和演示3.2 不同方法分别代理(对比JDK动态代理写法)3.3 熔断限流和日志监控(对比JDK动态代理写法) 4. 动态代理…...
如何在Unity中使用设计模式
在 Unity 环境中,设计模式是游戏开发人员遇到的常见问题的通用解决方案。将它们视为解决游戏开发中特定挑战的经过验证的模板或蓝图。以下是一些简单易懂的设计模式: 1. 单例=> 单例模式确保一个类只有一个实例,并提供对该实例的全局访问点。在 Unity 中,可以使用单例模…...
基于springboot+vue+Mysql的旅游管理系统
开发语言:Java框架:springbootJDK版本:JDK1.8服务器:tomcat7数据库:mysql 5.7(一定要5.7版本)数据库工具:Navicat11开发软件:eclipse/myeclipse/ideaMaven包:…...
vue3+ts中判断输入的值是不是经纬度格式
vue3ts中判断输入的值是不是经纬度格式 vue代码: <template #bdjhwz"{ record }"><a-row :gutter"8" v-show"!record.editable"><a-col :span"12"><a-input placeholder"经度" v-model:v…...
python常用知识总结
文章目录 1. 常用内置函数1. ASCII码与字符相互转换 1. 常用内置函数 1. ASCII码与字符相互转换 # 用户输入字符 c input("请输入一个字符: ")# 用户输入ASCII码,并将输入的数字转为整型 a int(input("请输入一个ASCII码: "))print( c &qu…...
使用VSCode开发Django指南
使用VSCode开发Django指南 一、概述 Django 是一个高级 Python 框架,专为快速、安全和可扩展的 Web 开发而设计。Django 包含对 URL 路由、页面模板和数据处理的丰富支持。 本文将创建一个简单的 Django 应用,其中包含三个使用通用基本模板的页面。在此…...
Day131 | 灵神 | 回溯算法 | 子集型 子集
Day131 | 灵神 | 回溯算法 | 子集型 子集 78.子集 78. 子集 - 力扣(LeetCode) 思路: 笔者写过很多次这道题了,不想写题解了,大家看灵神讲解吧 回溯算法套路①子集型回溯【基础算法精讲 14】_哔哩哔哩_bilibili 完…...
(二)原型模式
原型的功能是将一个已经存在的对象作为源目标,其余对象都是通过这个源目标创建。发挥复制的作用就是原型模式的核心思想。 一、源型模式的定义 原型模式是指第二次创建对象可以通过复制已经存在的原型对象来实现,忽略对象创建过程中的其它细节。 📌 核心特点: 避免重复初…...
【Web 进阶篇】优雅的接口设计:统一响应、全局异常处理与参数校验
系列回顾: 在上一篇中,我们成功地为应用集成了数据库,并使用 Spring Data JPA 实现了基本的 CRUD API。我们的应用现在能“记忆”数据了!但是,如果你仔细审视那些 API,会发现它们还很“粗糙”:有…...
Ascend NPU上适配Step-Audio模型
1 概述 1.1 简述 Step-Audio 是业界首个集语音理解与生成控制一体化的产品级开源实时语音对话系统,支持多语言对话(如 中文,英文,日语),语音情感(如 开心,悲伤)&#x…...
MySQL 8.0 OCP 英文题库解析(十三)
Oracle 为庆祝 MySQL 30 周年,截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始,将英文题库免费公布出来,并进行解析,帮助大家在一个月之内轻松通过OCP认证。 本期公布试题111~120 试题1…...
前端开发面试题总结-JavaScript篇(一)
文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包(Closure)?闭包有什么应用场景和潜在问题?2.解释 JavaScript 的作用域链(Scope Chain) 二、原型与继承3.原型链是什么?如何实现继承&a…...
学校时钟系统,标准考场时钟系统,AI亮相2025高考,赛思时钟系统为教育公平筑起“精准防线”
2025年#高考 将在近日拉开帷幕,#AI 监考一度冲上热搜。当AI深度融入高考,#时间同步 不再是辅助功能,而是决定AI监考系统成败的“生命线”。 AI亮相2025高考,40种异常行为0.5秒精准识别 2025年高考即将拉开帷幕,江西、…...
2025季度云服务器排行榜
在全球云服务器市场,各厂商的排名和地位并非一成不变,而是由其独特的优势、战略布局和市场适应性共同决定的。以下是根据2025年市场趋势,对主要云服务器厂商在排行榜中占据重要位置的原因和优势进行深度分析: 一、全球“三巨头”…...
AirSim/Cosys-AirSim 游戏开发(四)外部固定位置监控相机
这个博客介绍了如何通过 settings.json 文件添加一个无人机外的 固定位置监控相机,因为在使用过程中发现 Airsim 对外部监控相机的描述模糊,而 Cosys-Airsim 在官方文档中没有提供外部监控相机设置,最后在源码示例中找到了,所以感…...
