【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);// 计算 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 个三角形,再根据自己正反面的位置添加到对应的正反面网格数据中并更新索引

# 生成平面

请添加图片描述

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

简单的三角剖分方案

  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.mzph.cn/news/809099.shtml

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

相关文章

2024年免费试用云服务器一览表

随着云计算技术的不断发展和普及&#xff0c;越来越多的企业和个人开始寻求通过云服务器来满足其数据存储、应用部署等需求。而免费试用云服务器则成为了一个吸引用户的重要手段&#xff0c;本文将为大家分享2024年免费试用云服务器一览表&#xff0c;帮助大家更好地选择合适的…

Spring Boot 框架集成Knife4j

本次示例使用 Spring Boot 作为脚手架来快速集成 Knife4j,Spring Boot 版本2.3.5.RELEASE,Knife4j 版本2.0.7&#xff0c;完整代码可以去参考 knife4j-spring-boot-fast-demo pom.xml 完整文件代码如下 <?xml version"1.0" encoding"UTF-8"?> &l…

https加载http不安全脚本提示解决方案

大家好&#xff0c;我是咕噜铁蛋。今天&#xff0c;我想和大家探讨一个很常见但又很容易被忽视的问题——https加载http不安全脚本提示。相信很多网站开发者和维护者在日常工作中都遇到过这样的问题&#xff0c;那么我们应该如何解决这个问题呢&#xff1f;下面&#xff0c;我将…

将Ubuntu18.04默认的python3.6升级到python3.8

1、查看现有的 python3 版本 python3 --version 2、安装 python3.8 sudo apt install python3.8 3、将 python3.6 和 3.8 添加到 update-alternatives sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.6 1 sudo update-alternatives --insta…

算法设计与分析实验报告c++python实现(生命游戏、带锁的门、三壶谜题、串匹配问题、交替放置的碟子)

一、实验目的 1&#xff0e;加深学生对分治法算法设计方法的基本思想、基本步骤、基本方法的理解与掌握&#xff1b; 2&#xff0e;提高学生利用课堂所学知识解决实际问题的能力&#xff1b; 3&#xff0e;提高学生综合应用所学知识解决实际问题的能力。 二、实验任务 1、 编…

全面学习SpringCloud框架指南

要深入学习Spring Cloud框架,你需要系统地掌握其核心组件和概念,并了解如何在实际项目中应用这些知识。以下是一些关键的学习点和相应的学习内容: 一共分为10个模块包括: 1、微服务架构基础: 理解微服务架构的概念和优势。 学习单体架构向微服务架构演进的过程。 掌握…

计算机网络 Telnet远程访问交换机和Console终端连接交换机

一、实验要求和内容 1、配置交换机进入特权模式密文密码为“abcd两位班内学号”&#xff0c;远程登陆密码为“123456” 2、验证PC0通过远程登陆到交换机上&#xff0c;看是否可以进去特权模式 二、实验步骤 1、将一台还没配置的新交换机&#xff0c;利用console线连接设备的…

【高端电流检测IC储能产品应用方案】耐压28V侧轨的电流检测芯片FP130A 应用于电脑电源,开关电源以及多口快充充电器,户外移动电源,适配器,电池充电器等

电流检测技术常用于高压短路保护、电机控制、DC/DC换流器、系统功耗管理、二次电池的电流管理、蓄电池管理等电流侦测等场景。对于大多数应用而言&#xff0c;都是间接测量电阻两端的跨压差来获取待测电流。 如下面的高端电流检测芯片FP130A&#xff0c;丝印是FC915。电路原理图…

MySQL数据库的详解(1)

DDL&#xff08;数据库操作&#xff09; 查询 查询所有数据库&#xff1a;show databases;当前数据库&#xff1a;select database(); 创建 创建数据库&#xff1a;create database [ if not exists] 数据库名 ; 使用 使用数据库&#xff1a;use 数据库名 ; 删除 删除数…

利用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, …

网络安全JavaSE第六天

7. 数组 7.3.5 数组排序 7.3.5.1 冒泡排序 冒泡排序的思路&#xff1a;相邻两个元素进行比较&#xff0c;如果前面元素比后面元素大就交换位置&#xff0c;每一趟执行完后&#xff0c; 就会得到最大的数。 排序过程分析&#xff1a; package com.openlab; /** * 冒泡排序 */…

pyside6自定义部件库和软件框架的建设记录

一、自定义部件库 原则上尽量做到前后端分离&#xff0c;接口方便&#xff0c;复制简单。 1、单选框部件 # encoding: utf-8 ################################################### # 自定义的单选框 #################################################### 对外…

基于模型预测算法的含储能微网双层能量管理模型

基于模型预测算法的含储能微网双层能量管理模型 文章目录 基于模型预测算法的含储能微网双层能量管理模型一、项目介绍二、源程序下载 一、项目介绍 代码主要做的是一个微网双层优化调度模型&#xff0c;微网聚合单元包括风电、光伏、储能以及超级电容器&#xff0c;在微网的运…

Go语言mac环境搭建详解

Go语言mac环境搭建详解见视频&#xff0c;视频下方也有讲解具体的操作步骤。 Golang Mac电脑环境搭建、开发工具Vscode配置 Go语言mac环境搭建步骤如下&#xff1a; 1、下载安装Golang Go官网下载地址&#xff1a;https://golang.org/dl/ Go官方镜像站&#xff08;推荐&…

Windows下如何确定DLL动态库是32位还是64位

文章目录 Windows下如何确定DLL动态库是32位还是64位使用dumpbin工具可能出现的问题结果输出内容 Windows下如何确定DLL动态库是32位还是64位 使用dumpbin工具 dumpbin.exe通常位于Visual Studio的安装目录下的VC\bin或VC\Tools\MSVC\<version>\bin\Hostx64\x64 比如&am…

海山数据库(He3DB)Redis技术实践:继承开源Redis精髓,强化升级企业级服务

数字化转型中的企业数据的处理速度和效率直接关系到企业的竞争力&#xff0c;Redis作为业界广泛使用的开源键值对存储系统&#xff0c;以其卓越的性能和丰富的数据结构&#xff0c;成为了众多开发者和企业的首选。然而&#xff0c;近期Redis开源社区对Redis协议进行了变更&…

电力综合自动化系统对电力储能技术的影响有哪些?

电力综合自动化系统对电力储能技术的影响主要体现在以下几个方面&#xff1a; 提高能源利用效率&#xff1a;电力综合自动化系统通过优化调度和能量管理&#xff0c;可以实现储能设备的有效利用&#xff0c;提高能源利用效率。在电力系统中&#xff0c;储能设备可以有效地平抑风…

贪心算法:排列算式

题目描述 给出n数字&#xff0c;对于这些数字是否存在一种计算顺序&#xff0c;使得计算过程中数字不会超过3也不会小于0&#xff1f; 输入描述: 首行给出一个正整数t,(1≤t≤1000)代表测试数据组数每组测试数据第一行一个正整数n,(1≤n≤500)第二行包含n个以空格分隔的数字…

Flutter - flutter_gen 资源管理

引言&#xff1a; 在开发 Flutter 应用时&#xff0c;我们经常需要使用各种静态资源&#xff0c;如图片、字体和音频等。如何有效地管理和加载这些资源呢&#xff1f;本篇博客将以图片为例带你解密 Flutter 项目中是如何管理资源地。 assets 加载资源 具体文件名引入 在工程…

STC89C52学习笔记(九)

STC89C52学习笔记&#xff08;九&#xff09; 综述&#xff1a;本文主要介绍了蜂鸣器、蜂鸣器如何使用以及如何利用蜂鸣器播放不同频率声音。 一、蜂鸣器 1.定义和作用 电信号→声音信号&#xff0c;常用来产生按键音和报警音。 2.分类 有源&#xff1a;自带振荡器&#…