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

news2024/11/28 6:35:39

请添加图片描述

# 前言

一年一度的征稿到了,倒腾点存货,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 获取子网格数量

# 开始切割

前面说了模型是由一个个三角形组成的,那么我们只需要遍历模型的网格数据针对每个和平面相交的三角形切割就行了

  1. 首先需要准备两个 cc.primitives.IGeometry 类型的对象,用于分别存储正反面的网格数据

  2. 遍历需要切割的网格三角形数据,与平面相交就切割三角形后放入对应的 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
  1. 如果三角形三个顶点形成的线段不与平面相交,那么则不需要新建顶点
  2. 如果三角形线段与平面相交,则切割为三个三角形,怎么判断相交,看下面

怎么确定交点(i1, i2)?

交点也就是 i1,i2 的坐标,知道了交点才能分割三角形,以下以获取 i1 的坐标为例

  1. 射线公式:P = P0 + tV;
  2. 平面公式:A(P−P1) = 0;

这两个公式里, P 是射线上也在平面上的一个点,也就是射线和平面的交点。 P0 是射线的起点, V 是射线的方向。 t 是一个数字,当它变化时,P就会在射线上移动。 P1 是平面上的一个特定点, A 是平面的法向量。

我们将射线的公式代入到平面的公式中,就得到: A(P0 + tV - P1) = 0,求解为:t = (A * (P1 - P0))/(A * V),这里 Creator 有内置的函数,就不用自己写了

步骤为:

  1. 确定 i1 的坐标,从 p0 到 p1 的方向创建一条射线
    cc.geometry.Ray.fromPoints(ray, p0, p1);
    
  2. 计算与平面的交点距离
    const distance_n = cc.geometry.intersect.rayPlane(ray, this._plane);
    
  3. 获取交点坐标
    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);
	// 计算 uv
	cc.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);
			// 计算 uv
			cc.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 个三角形,再根据自己正反面的位置添加到对应的正反面网格数据中并更新索引

# 生成平面

请添加图片描述

在切割结束后如果没有问题你会发现这是个空心模型,如果我们需要一个平面封住切口呢?怎么做?
这就被称为平面的 三角剖分

简单的三角剖分方案

  1. 求平均点,不完全支持凹多边形
    请添加图片描述

  2. 左右横跳,不完全支持凹多边形
    请添加图片描述

  3. 单点遍历,不完全支持凹多边形
    请添加图片描述

不支持凹面多边形的后果

可以看下图

请添加图片描述

这样的话,无论是使用平均点,还是图中的单点遍历新建三角形,都会有可能出现生成的三角形错误的情况

那么如何做?步骤如下

  1. 记录新增的顶点坐标并排序(连线)

  2. 将排序后的多边形顶点分解为凸多边形

  3. 为所有凸多边形生成三角形

怎么判断凹凸?

请添加图片描述

判断 p0 - p1 - p2 的夹角角度即可,这也是我们需要对新增顶点坐标排序的原因

将凹多边形分解为凸多边形

在找到凹角之后,我们只需要从 p1 的位置开始遍历至顶点,只要找到 p0 - p1 - pn 夹角不为凹角的 pn 顶点就可以分割为两个多边形,再对分割后的多边形重复执行此操作

平面带孔的情况

请添加图片描述

将排序后的两个多边形合并为一个,将内多边形的点连接到最近的一个外多边形,组合成为一个单独的多边形

但是还有一个问题,那就是单独的两个多边形可以依靠法线和碰撞检测来判断当前多边形是否在另一个内,那么多个多边形嵌套呢?

我这里想到的是使用面积判断,从大到小对多边形排序,内多边形的面积一定比外多边形小

# 源码

  • 保证切割后模型原表面法线、UV 的正常

  • 切口平面支持凹多边形

  • 支持同时切割多个模型

  • 使用共享顶点,可以节省模型内存占用

Cocos Store:https://store.cocos.com/app/detail/6118

# 其他参赛文章

原生预览调试!我给Cocos加了个新功能,原生开发者福音

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1587072.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ssm+vue的实验室课程管理系统(有报告)。Javaee项目,ssm vue前后端分离项目。

演示视频&#xff1a; ssmvue的实验室课程管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm vue前后端分离项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构…

【NLP笔记】大模型微调方法概述

对于一些生成式场景而言&#xff0c;没有固定的回答结果&#xff0c;采用AI Agent的增强范式&#xff0c;可以极大地提升模型生成的效果。但是对于有固定格式、输出目标的场景而言&#xff0c;仅从prompt优化的角度出发很难突破瓶颈&#xff0c;需要通过微调来提升效果&#xf…

背 单 词 (考研词汇闪过)

单词&#xff1a; 买考研词汇闪过 研究艾宾浩斯遗忘曲线 https://www.bilibili.com/video/BV18Y4y1h7YR/?spm_id_from333.337.search-card.all.click&vd_source5cbefe6dd70d6d84830a5891ceab2bf9 单词方法 闪记背两排&#xff08;5min&#xff09;重复一遍&#xff08;2mi…

vue中预览docx、xlsx、pptx、pdf

前言&#xff1a;其实本来是要做全类型文件预览的&#xff0c;但是一直找不到合适的doc,xlx,ppt预览插件。要是有可以使用的&#xff0c;可以评论推荐给我 我使用的node版本&#xff1a;v18.19.1 参考官网&#xff1a;preview 文件预览 | ran 引入方式&#xff1a; //安装组…

Flask快速搭建文件上传服务与接口

说明&#xff1a;仅供学习使用&#xff0c;请勿用于非法用途&#xff0c;若有侵权&#xff0c;请联系博主删除 作者&#xff1a;zhu6201976 一、需求背景 前端通过浏览器&#xff0c;访问后端服务器地址&#xff0c;将目标文件进行上传。 访问地址&#xff1a;http://127.0.0…

✔ ★Java项目——设计一个消息队列(二)

Java项目——设计一个消息队列 四. 项⽬创建五. 创建核⼼类创建 Exchange&#xff08;名字、类型、持久化&#xff09;创建 MSGQueue&#xff08;名字、持久化、独占标识&#xff09;创建 Binding&#xff08;交换机名字、队列名字、bindingKey用于与routingKey匹配&#xff09…

前端docker jenkins nginx CI/CD持续集成持续部署-实战

最近用go react ts开发了一个todolist后端基本开发完了,前端采用CI/CD方式去部署。 步骤总结 先安装docker 和 docker-compose。安装jenkins镜像,跑容器的时候要配好数据卷。配置gitee或github(我这里使用gitee)在服务器上一定要创建好dokcer的数据卷,以便持久保存jenkin…

【MySQL】锁篇

SueWakeup 个人主页&#xff1a;SueWakeup 系列专栏&#xff1a;学习技术栈 个性签名&#xff1a;保留赤子之心也许是种幸运吧 本文封面由 凯楠&#x1f4f8;友情提供 目录 本系列专栏 1. MySQ 中的锁 2. 表锁和行锁 表锁 行锁 3. InnoDB 存储引擎的三种行级锁 4. 悲观锁…

怎么开发一个预约小程序_一键预约新体验

预约小程序&#xff0c;让生活更便捷——轻松掌握未来&#xff0c;一键预约新体验 在快节奏的现代生活中&#xff0c;我们总是在不断地奔波&#xff0c;为了工作、为了生活&#xff0c;不停地忙碌着。然而&#xff0c;在这繁忙的生活中&#xff0c;我们是否曾想过如何更加高效…

探探各个微前端框架

本文作者为 360 奇舞团前端开发工程师 微前端架构是为了在解决单体应用在一个相对长的时间跨度下&#xff0c;由于参与的人员、团队的增多、变迁&#xff0c;从一个普通应用演变成一个巨石应用(Frontend Monolith)后&#xff0c;随之而来的应用不可维护的问题。这类问题在企业级…

点击按钮(文字)调起elementUI大图预览

时隔一年&#xff0c;我又回来了 ~ 最近在做后台&#xff0c;遇到一个需求&#xff0c;就是点击“查看详情”按钮&#xff0c;调起elementUI的大图预览功能&#xff0c;预览多张图片&#xff0c;如下图&#xff1a; 首先想到的是使用element-ui的el-image组件&#xff0c;但它是…

Towards Geolocation of Millions of IP Addresses(2012年)

下载地址: Towards geolocation of millions of IP addresses | Proceedings of the 2012 Internet Measurement Conference 被引用次数:70 Hu Z, Heidemann J, Pradkin Y. Towards geolocation of millions of IP addresses[C]//Proceedings of the 2012 Internet Measure…

利用Python实现可视化交互界面:Dash

Dash是一个低代码数据框架&#xff0c;用Python实现可视化交互界面&#xff0c;不用写Javascript&#xff0c;开源&#xff0c;支持回调、HTML组件等功能。 安装 pip install dash使用 # Import packages from dash import Dash, html, dash_table, dcc, callback, Output, …

[BT]BUUCTF刷题第14天(4.10)

第14天&#xff08;共5题&#xff09; Web [BJDCTF2020]ZJCTF&#xff0c;不过如此 打开网站直接显示源代码&#xff1a; <?php error_reporting(0); //关闭报错 $text $_GET["text"]; $file $_GET["file"]; if(isset($text)&&(file…

C#如何用NPOI创建、读取、更新Excel文件

一.获取引用NPOI VS2017&#xff0c;通过Nuget工具包下载NPOI到指定的项目中&#xff0c;如下 二.添加如下命名空间,其中HSSF是操作*.xls文件&#xff0c;XSSF操作*.xlsx文件. using NPOI; using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; using NPOI.HSSF.UserModel; …

Debian 安装 Docker

Debian 安装 Docker。 这是官方安装文档 Install Docker Engine on Debian | Docker DocsLearn how to install Docker Engine on Debian. These instructions cover the different installation methods, how to uninstall, and next steps.https://docs.docker.com/engine/i…

如何将普通maven项目转为maven-web项目

文件-项目结构&#xff08;File-->Project Structure &#xff09; 模块-->learn&#xff08;moudle-->learn&#xff09; 选中需要添加web的moudle&#xff0c;点击加号&#xff0c;我得是learn&#xff0c;单击选中后进行下如图操作&#xff1a; 编辑路径 结果如下…

【网站项目】英语学习激励系统小程序

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

神经网络背后的数学原理

原文地址&#xff1a;The Math Behind Neural Networks 2024 年 3 月 29 日 深入研究现代人工智能的支柱——神经网络&#xff0c;了解其数学原理&#xff0c;从头开始实现它&#xff0c;并探索其应用。 神经网络是人工智能 &#xff08;AI&#xff09; 的核心&#xff0c;为…

性能优化“万金油”:缓存Cache

1、首次请求数据时,先从缓存中获取,如果没有,则继续向数据库中获取。获取到数据后,将数据保存到缓存中。再次请求数据,一样先从缓存中获取,成功获取,“缓存命中”。多次请求中,命中次数占全部请求次数的比例,叫“命中率”。如果数据源的数据发生变化,而缓存中的数据没…