【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 获取子网格数量

# 开始切割

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

  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加了个新功能,原生开发者福音

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

机器学习和深度学习--李宏毅(笔记与个人理解)Day11-12

Day11 when gradient is small…… 怎么知道是局部小 还是鞍点&#xff1f; using Math 这里巧妙的说明了hessan矩阵可以决定一个二次函数的凹凸性 也就是 θ \theta θ 是min 还是max&#xff0c;最后那个有些有些 哈 是一个saddle&#xff1b; 然后这里只要看hessan矩阵是不…

Element-UI 下拉框单选转多选回显不清空绑定的值

需求 根据radio切换来更改下拉框是否多选 原因 单选和多选这两个 input 看上去没差别&#xff08;自身和层级都一致&#xff09;&#xff0c;vue出于提高性能&#xff0c;所以 vue 给复用了 解决方案 <template><section><el-radio-group v-model"radi…

风储微网虚拟惯性控制系统simulink建模与仿真

目录 1.课题概述 2.系统仿真结果 3.核心程序与模型 4.系统原理简介 5.完整工程文件 1.课题概述 风储微网虚拟惯性控制系统simulink建模与仿真。风储微网虚拟惯性控制系统是一种模仿传统同步发电机惯性特性的控制策略&#xff0c;它通过集成风力发电系统、储能系统和其他分…

聚丙烯PP它的化学特性是什么? UV胶水能够粘接聚丙烯PP吗?

聚丙烯PP它的化学特性是什么? UV胶水能够粘接聚丙烯PP吗&#xff1f; 聚丙烯&#xff08;Polypropylene&#xff0c;简称PP&#xff09;是一种热塑性聚合物&#xff0c;属于聚烯烃类塑料之一。以下是聚丙烯的一些化学特性&#xff1a; 1. 分子结构&#xff1a; 聚丙烯是由丙烯…

再说vue响应式数据

请说一下你对响应式数据的理解 如何实现响应式数据据 对象 vue2 响应式核心代码 数组 vue2 处理缺陷Vue3则采用 proxy - vue3 响应式核心代码 请说一下你对响应式数据的理解 如何实现响应式数据据 数组和对象类型当值变化时如何劫持到。 对象 对象内部通过defineReactive方…

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

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

Centos7 k8s 集群 - Rook Ceph 安装

环境准备 基础环境 系统名称操作系统CPU内存硬盘Kubernete 版本Docker版本IPmasterCentos74c4gsdb 20G1.17.023.0.1192.168.1.128node01Centos74c4gsdb 20G1.17.023.0.1192.168.1.129node02Centos74c4gsdb 20G1.17.023.0.1192.168.1.130node03Centos74c4gsdb 20G1.17.023.0.1…

OpenHarmony4.0分布式任务调度浅析

1 概述 OpenHarmony 分布式任务调度是一种基于分布式软总线、分布式数据管理、分布式 Profile 等技术特性的任务调度方式。它通过构建一种统一的分布式服务管理机制&#xff0c;包括服务发现、同步、注册和调用等环节&#xff0c;实现了对跨设备的应用进行远程启动、远程调用、…

3d怎么按路径制作模型---模大狮模型网

在3D建模中&#xff0c;按路径制作模型是一种常见的技术&#xff0c;特别适用于创建曲线、管道、绳索等线性形状的物体。虽然这项技术可能对初学者来说有些复杂&#xff0c;但通过一步步的指导和实践&#xff0c;你将能够掌握它。本文将详细介绍按路径制作模型的步骤&#xff0…

OpenDDS-3.27构建与用法

一、OpenDDS-3.27构建 ./configure To enable Java bindings, use ./configure --java make 二、运行Messenger Example&#xff1a; source setenv.sh For the C example&#xff1a;cd DevGuideExamples/DCPS/Messenger For the Java example&#xff1a;cd java/tests/mes…

【JVM】JVM堆占用情况分析(频繁创建的对象、内存泄露等问题)、jmap+jhat、jvisualvm工具使用

文章目录 一. 相关命令1. 查看进程堆内存整体使用情况&#xff1a;OOM的可能2. 统计类的对象数量以及内存占用&#xff1a;定位内存泄漏 二. 分析内存占用1. 使用 jhat 排查对象堆占用情况1.1. 排查步骤1.2. 具体分析例子a. 分析频繁创建对象导致的OOM 1.3. OQL查看某一个对象的…

Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单视频处理实战案例 之十 简单视频浮雕画效果

Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单视频处理实战案例 之十 简单视频浮雕画效果 目录 Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单视频处理实战案例 之十 简单视频浮雕画效果 一、简单介绍 二、简单视频浮雕画效果实现原理 三、简单视频浮雕画效果…

关于MCU产品开发参数存储的几种方案

关于MCU产品开发参数存储的几种方案 Chapter1 关于MCU产品开发参数存储的几种方案Chapter2 单片机参数处理[保存与读取]Chapter3 嵌入式设备参数存储技巧Chapter4 STM32硬件I2C的一点心得(AT24C32C和AT24C64C) Chapter1 关于MCU产品开发参数存储的几种方案 原文链接 在工作中…

Python 批量检测ip地址连通性,以json格式显示(支持传参单IP或者网段)

代码 ########################################################################## File Name: check_ip_test.py# Author: eight# Mail: 18847097110163.com # Created Time: Thu 11 Apr 2024 08:52:45 AM CST################################################…

突破界限 千视将在 NAB 2024 展会上展示领先的 AV over IP 技术

突破界限&#xff01;千视将在 NAB 2024 展会上展示领先的 AV over IP技术 作为AV over IP领域的先驱者&#xff0c;Kiloview将于2024年4月14日至17日在NAB展会&#xff08;展台号&#xff1a;SU6029&#xff09;隆重登场&#xff0c;展示我们领先业界的AV over IP产品、解决方…

Windows下安装GPU版Pytorch

升级Driver到最新版本 Windows搜索栏中输入设备管理器找到显示适配器一项&#xff0c;点击展开&#xff0c;你将看到你的NVIDIA显卡列在其中右键点击你的NVIDIA显卡&#xff0c;选择更新驱动软件…。在弹出的对话框中&#xff0c;选择自动搜索更新的驱动软件。之后&#xff0c…

nginx反向代理conf

打开nginx配置。 对登录功能测试完毕后&#xff0c;接下来&#xff0c;我们思考一个问题&#xff1a;前端发送的请求&#xff0c;是如何请求到后端服务的&#xff1f; 前端请求地址&#xff1a;http://localhost/api/employee/login 后端接口地址&#xff1a;http://localho…

计算机网络——NAT技术

目录 前言 前篇 引言 SNAT&#xff08;Source Network Address Translation&#xff09;源网络地址转换 SNAT流程 确定性标记 DNAT&#xff08;Destination Network Address Translation&#xff0c;目标网络地址转换&#xff09; NAT技术重要性 前言 本博客是博主用于…

ShardingSphere再回首

概念&#xff1a; 连接&#xff1a;通过协议 方言及库存储的适配&#xff0c;连接数据和应用&#xff0c;关注多模数据苦之间的合作 增量&#xff1a;抓取库入口流量题提供重定向&#xff0c; 流量变形(加密脱敏)/鉴权/治理(熔断限流)/分析等 可插拔&#xff1a;微内核 DDL:cr…

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;三层体系结构…