【Godot4.2】 基于SurfaceTool的3D网格生成与体素网格探索

概述


说明:本文基础内容写于2023年6月,由三五篇文章汇总而成,因为当时写的比较潦草,过去时间也比较久了,我自己都得重新阅读和理解一番,才能知道自己说了什么,才有可能重新优化整理。

因为我对体素网格的原始算法并不精通,当时只是依靠自己的直觉以及Godot4.2提供的工具类来实现了自己的一套Godot体素网格生成算法。

你也可以把本文当做这些工具类的实例教程进来看。因此体素不体素的你也可以当我没说:),重点在于在Godot中用代码进行3D网格的生成。


体素的性能问题

如果直接使用立方体网格堆叠的方式,则会存在很多不显示但是真实存在的面,立方体一多,就会加重GPU进行各种矩阵变换、投影计算的负担,从而导致卡顿。

因此,好的体素网格生成算法,一定首先是要做到面数优化,最好是看不见的面一个都不生成。这样才能在地图尺寸比较大,存在数以万计的体素时,能够做到最少的顶点数量,从而最大程度减少GPU运算和渲染的负担,减少卡顿情况的出现。

在Godot中程序化生成3D网格

Godot提供了一个叫SurfaceTool的类(当然还有其他的类),可以用十分底层(定义顶点、法线、UV)的方式创建3D网格资源,这就让程序化生成3D网格有了可能,并有了动态融并网格的算法可能。

与2D中使用多个点自定义、多边形、折线等几何图形一样,3D中也是用三维空间中的多个点来定义三维网格(Mesh)。

不同点是,三维网格,是由三角面构成的,也就是由3个顶点构成一个三角面,多个三角面构成一个三维网格。

SurfaceTool的应用

搭建SurfaceTool简单测试场景

通过创建一个含有MeshInstance3D的简单3D场景+一个简单的EditorScript脚本,就可以搭建一个基础的SurfaceTool测试场景。

image.png

框架代码如下:

@tool
extends EditorScriptfunc _run():var ins:MeshInstance3D = get_scene().get_node("MeshInstance3D")ins.mesh = create_mesh()passfunc create_mesh():var st = SurfaceTool.new()# ...return mesh

其中自定义函数create_mesh()用于SurfaceTool实例的创建和3D网格的返回。
_run()则在运行后,对场景中的MeshInstance3D节点的mesh属性赋值。

这样做的好处是,可以直接在编辑器中看到网格的样子。
比如下面的代码:

@tool
extends EditorScriptfunc _run():var ins:MeshInstance3D = get_scene().get_node("MeshInstance3D")ins.mesh = create_mesh()passfunc create_mesh():var st = SurfaceTool.new()st.begin(Mesh.PRIMITIVE_TRIANGLES)# Prepare attributes for set_vertex.st.set_normal(Vector3(0, 0, 1))st.set_uv(Vector2(0, 0))# Call last for each vertex, adds the above attributes.st.add_vertex(Vector3(-1, -1, 0))st.set_normal(Vector3(0, 0, 1))st.set_uv(Vector2(0, 1))st.add_vertex(Vector3(-1, 1, 0))st.set_normal(Vector3(0, 0, 1))st.set_uv(Vector2(1, 1))st.add_vertex(Vector3(1, 1, 0))# Commit to a mesh.var mesh = st.commit()return mesh

运行后,生成了如下的3D网格:
image.png

在后视图可以看到其实际的形状是一个等腰直角三角形的三角面。

image.png
法线都是(0,0,1)。而且UV坐标在垂直方向是反的。
image.png

生成方形网格

会生成三角面了,那么生成方形网格也就不难了,方形网格可以看做是两个三角面组成。
执行如下脚本:

@tool
extends EditorScriptfunc _run():var ins:MeshInstance3D = get_scene().get_node("MeshInstance3D")ins.mesh = create_mesh()passfunc create_mesh():var st = SurfaceTool.new()st.begin(Mesh.PRIMITIVE_TRIANGLES)st.set_normal(Vector3(0, 0, 1))st.set_uv(Vector2(0, 0))st.add_vertex(Vector3(-1, -1, 0))st.set_normal(Vector3(0, 0, 1))st.set_uv(Vector2(0, 1))st.add_vertex(Vector3(-1, 1, 0))st.set_normal(Vector3(0, 0, 1))st.set_uv(Vector2(1, 1))st.add_vertex(Vector3(1, 1, 0))st.set_normal(Vector3(0, 0, 1))st.set_uv(Vector2(1, 1))st.add_vertex(Vector3(1, 1, 0))st.set_normal(Vector3(0, 0, 1))st.set_uv(Vector2(1, 0))st.add_vertex(Vector3(1, -1, 0))st.set_normal(Vector3(0, 0, 1))st.set_uv(Vector2(0, 0))st.add_vertex(Vector3(-1, -1, 0))# Commit to a mesh.var mesh = st.commit()return mesh

生成的方形网格如下:
在这里插入图片描述
其顶点和UV以及三角面的示意图如下:
image.png
注意:

  • 添加顶点的顺序非常重要,如果不按首尾顺序添加,则法线可能相反。
  • 每3个点组成一个三角面,如果顶点不够,就会报错。

创建立方体

在Godot中,可以使用一个AABB来表示一个基本的立方体。
在这里插入图片描述

通过简单的三维向量加减法运算,就可以获得AABB代表的立方体的所有顶点坐标。

而通过立方体的顶点坐标,就可以求出组成六个面各自的两个三角面。
image.png

# 三条坐标轴方向的边的端点
var p_Z = (0,0,end.z) # Zvar p_Y = (0,end.y,0) # Yvar p_X = (end.x,0,0) # X轴
# 三个平面上的点的端点
var p_YZ = (0,end.y,end.z) # YZ平面
var p_XZ = (end.x,0,end.z) # XZ平面
var p_XY = (end.x,end.y,0) # XY平面

上面的点坐标是基于一个position=Vector3.ZERO,然后size = Vector3.ONEAABB进行计算的。
其他位置的立方体可以通过AABB的变换得到。
如果想要获得以坐标原点为中心的矩形,可以设置position=-Vector3.ONE/2,然后size = Vector3.ONEAABB。并将上述所有的0变成postion相应轴上的分量。也就是:

# 三条坐标轴方向的边的端点
var p_Z = (position.x,position.y,end.z) # Zvar p_Y = (position.x,end.y,position.z) # Yvar p_X = (end.x,position.y,position.z) # X轴
# 三个平面上的点的端点
var p_YZ = (position.x,end.y,end.z) # YZ平面
var p_XZ = (end.x,position.y,end.z) # XZ平面
var p_XY = (end.x,end.y,position.z) # XY平面

定义6个面的法线和顶点顺序

为六个面定义名称:
image.png

# UV坐标
var uvs = [Vector2.DOWN,Vector2.ZERO,Vector2.RIGHT,Vector2.ONE]
# 右侧面
var f_right = {normal = Vector3.RIGHT,vectors = [p_XZ,p_E,p_XY,p_XY,p_X,p_XZ]
}

实现后的完整代码如下:

@tool
extends EditorScriptfunc _run():var ins:MeshInstance3D = get_scene().get_node("MeshInstance3D")ins.mesh = create_cubemesh()func create_cubemesh():var st = SurfaceTool.new()st.begin(Mesh.PRIMITIVE_TRIANGLES)# 构造AABBvar position = Vector3.ZEROvar size = Vector3.ONEvar box:AABB = AABB(position,size)var end = box.end# ============= 获得立方体8个顶点的坐标# positon和andvar p_P = positionvar p_E = end# 三条坐标轴方向的边的端点var p_Z = Vector3(position.x,position.y,end.z) # Zvar p_Y = Vector3(position.x,end.y,position.z) # Yvar p_X = Vector3(end.x,position.y,position.z) # X轴# 三个平面上的点的端点var p_YZ = Vector3(position.x,end.y,end.z) # YZ平面var p_XZ = Vector3(end.x,position.y,end.z) # XZ平面var p_XY = Vector3(end.x,end.y,position.z) # XY平面# 6个顶点的UV坐标顺序var uvs = [Vector2.DOWN,Vector2.ZERO,Vector2.RIGHT,Vector2.ONE]# ============= 定义立方体6个面的法线和顶点绘制顺序# 右侧面var faces = {f_right = { # 右侧面normal = Vector3.RIGHT,vectors = [p_XZ,p_E,p_XY,p_X]},f_up = { # 上面normal = Vector3.UP,vectors = [p_E,p_YZ,p_Y,p_XY]},f_left = { # 左侧面normal = Vector3.LEFT,vectors = [p_P,p_Y,p_YZ,p_Z]},f_bottom = { # 底面normal = Vector3.DOWN,vectors = [p_Z,p_XZ,p_X,p_P]},f_front = { # 前面normal = Vector3.FORWARD,vectors = [p_X,p_XY,p_Y,p_P]},f_bcak = { # 后面normal = Vector3.BACK,vectors = [p_Z,p_YZ,p_E,p_XZ]}}for face in faces:for i in [0,1,2,2,3,0]:st.set_normal(faces[face]["normal"])st.set_uv(uvs[i])st.add_vertex(faces[face]["vectors"][i])var mesh = st.commit()return mesh

在这里插入图片描述

当实现一个立方体网格的绘制后,就可以基于此创建融并的网格。

创建融并网格和区域

@tool
extends EditorScriptfunc _run():var ins:MeshInstance3D = get_scene().get_node("MeshInstance3D")ins.mesh = create_area([Vector3.ZERO,Vector3.ONE])# 在area_pos_arr传入的所有3维空间位置创建一个16×16的立方体网格区域
func create_area(area_pos_arr:PackedVector3Array):var pos_arr:PackedVector3Arrayfor area_pos in area_pos_arr:# 以16×16为一个小区域var area_start_pos = area_pos * 16var area_size = Vector3.ONE * 16var box:AABB = AABB(area_start_pos,area_size)# 随机生成box位置for x in range(area_start_pos.x,box.end.x):for y in range(area_start_pos.y,box.end.y):for z in range(area_start_pos.z,box.end.z):var is_empty = randi_range(0,1)if not is_empty:pos_arr.append(Vector3(x,y,z))return create_cubemesh(pos_arr)# 在pos_arr传入的所有3维空间位置创建一个立方体网格
func create_cubemesh(pos_arr:PackedVector3Array):var st = SurfaceTool.new()st.begin(Mesh.PRIMITIVE_TRIANGLES)for pos in pos_arr:# 构造AABBvar size = Vector3.ONEvar box:AABB = AABB(pos,size)var end = box.end# ============= 获得立方体8个顶点的坐标# positon和andvar p_P = posvar p_E = end# 三条坐标轴方向的边的端点var p_Z = Vector3(pos.x,pos.y,end.z) # Zvar p_Y = Vector3(pos.x,end.y,pos.z) # Yvar p_X = Vector3(end.x,pos.y,pos.z) # X轴# 三个平面上的点的端点var p_YZ = Vector3(pos.x,end.y,end.z) # YZ平面var p_XZ = Vector3(end.x,pos.y,end.z) # XZ平面var p_XY = Vector3(end.x,end.y,pos.z) # XY平面# 6个顶点的UV坐标顺序var uvs = [Vector2.DOWN,Vector2.ZERO,Vector2.RIGHT,Vector2.ONE]# ============= 定义立方体6个面的法线和顶点绘制顺序# 右侧面var faces = {f_right = { # 右侧面normal = Vector3.RIGHT,vectors = [p_XZ,p_E,p_XY,p_X]},f_up = { # 上面normal = Vector3.UP,vectors = [p_E,p_YZ,p_Y,p_XY]},f_left = { # 左侧面normal = Vector3.LEFT,vectors = [p_P,p_Y,p_YZ,p_Z]},f_bottom = { # 底面normal = Vector3.DOWN,vectors = [p_Z,p_XZ,p_X,p_P]},f_front = { # 前面normal = Vector3.FORWARD,vectors = [p_X,p_XY,p_Y,p_P]},f_bcak = { # 后面normal = Vector3.BACK,vectors = [p_Z,p_YZ,p_E,p_XZ]}}# 检测上下左右前后方向有无立方体# 有的话就删除相应的面for face in faces:if pos + faces[face]["normal"] not in pos_arr:for i in [0,1,2,2,3,0]:st.set_normal(faces[face]["normal"])st.set_uv(uvs[i])st.add_vertex(faces[face]["vectors"][i])var mesh = st.commit()return mesh

运行后,将基于一个MeshInstance3D节点生成如下复杂的网格:
但是后期最好的做法是一个MeshInstance3D节点只生成一个16×16小区域的网格。
生成的两个区域的网格
未采用网格融并算法采用了网格融并算法
对比之后可以看到,采用了融并网格算法生成的三角面数比没有采用的少了将近7000左右。

总结

本篇文章简单实现了一个体素融并网格的生成算法。
如果要实现类似Minecraft那样效果,就需要在每个16×16小区域内判定对某个位置的方块进行删除或添加。所谓的删除或添加也就是像数组添加或删除一个位置信息。然后重新生成这个区域的网格就行了。实际可能会更复杂一些。
另外在地形生成上可以使用随机地图生成技术中的柏林算法,应该可以获得更自然的效果。

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

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

相关文章

【计算机网络】常见面试题汇总

文章目录 1.计算机网络基础1.1网络分层模型/OSI七层模型是什么?1.2TCP/IP四层模型是什么?每一层的作用?1.2.1TCP四层模型?1.2.2为什么网络要分层? 1.2常见网络协议1.2.1应用层常见的协议1.2.2网络层常见的协议 2.HTTP2…

动态规划——斐波那契问题(Java)

目录 什么是动态规划? 练习 练习1:斐波那契数 练习2:三步问题 练习3:使用最小花费爬楼梯 练习4:解码方法 什么是动态规划? 动态规划(Dynamic Programming,DP)&…

锂电池寿命预测 | Matlab基于ALO-SVR蚁狮优化支持向量回归的锂离子电池剩余寿命预测

目录 预测效果基本介绍程序设计参考资料 预测效果 基本介绍 锂电池寿命预测 | Matlab基于ALO-SVR蚁狮优化支持向量回归的锂离子电池剩余寿命预测 基于蚁狮优化和支持向量回归的锂离子电池剩余寿命预测: 1、提取NASA数据集的电池容量,以历史容量作为输入,…

电脑安装双系统windows和ubuntu server

1.创建Ubuntu-server的启动盘 首先要从官网下载Ubuntu-server18.04的ISO文件,用rufs烧录到U盘。如下所示 2. 磁盘分区 在windows创建两个盘(linuxboot 和linuxroot),后面一个一个用于boot,一个用于root. 3.开机U盘启…

AI开源概览及工具使用

一、前言 随着ChatGPT热度的攀升,越来越多的公司也相继推出了自己的AI大模型,如文心一言、通义千问等。各大应用也开始内置AI玩法,如抖音的AI特效; 关联资源:代码 GitHub、相关论文、项目Demo、产品文档、Grok Ai、gr…

【项目自我反思之vue的组件通信】

为什么子组件不能通过props实时接收父组件修改后动态变化的值 一、现象二、可能的原因1.响应式系统的限制2.异步更新队列3.父组件和子组件的生命周期4.子组件内部对 props 的处理 三、组件通信的几种场景(解决方案)1.子组件想修改父组件的数据2.子组件传…

【数据结构】猛猛干7道链表OJ

前言知识点 链表的调试技巧 int main() {struct ListNode* n1(struct ListNode*)malloc(sizeof(struct ListNode));assert(n1);struct ListNode* n2(struct ListNode*)malloc(sizeof(struct ListNode));assert(n2);struct ListNode* n3(struct ListNode*)malloc(sizeof(struc…

如何从零开始拆解uni-app开发的vue项目(一)

uni-app项目分析: 背景:最近接手一个前同事留下的半拉子项目,出拿过来觉得很简单;当我看到app.vue的时候很确定是vue项目,心里不怎么慌,果断安装node.js,然后就去npm ;安装VS code,事实并不是我期盼的那样,或者说根本就不能运行。 报错:应用vs code打开文件,输入命…

力扣每日一题 2024/3/23 统计桌面上的不同数字

题目描述 用例说明 思路讲解 给定整数n&#xff0c;找出循环十亿天后桌上的数字。可以先通过一天来找找规律。 第一天 n%i1 &#xff08;1<i<n&#xff09;只有n-1符合.加入桌面 第二天(n-1)%i1 &#xff08;1<i<n-1&#xff09;只有n-2符合 加入桌面 依次类推…

低代码开发平台开源:依靠科技力量实现数字化转型!

在竞争激烈的当今社会&#xff0c;数字化转型、流程化办公等字眼早已充斥在我们的职场生活中。虽然如此&#xff0c;但是我们依然要面临着这样一个现实问题&#xff1a;很多中小企业发展面临着资源有限、技术储备不足、人才短缺的现实问题&#xff0c;进入流程化办公困境依然明…

记录C++中,子类同名属性并不能完全覆盖父类属性的问题

问题代码&#xff1a; 首先看一段代码&#xff1a;很简单&#xff0c;就是BBB继承自AAA&#xff0c;然后BBB重写定义了同名属性&#xff0c;然后调用父类AAA的打印函数&#xff1a; #include <iostream> using namespace std;class AAA { public:AAA() {}~AAA() {}void …

145 Linux 网络编程1 ,协议,C/S B/S ,OSI 7层模型,TCP/IP 4层模型,

一 协议的概念 从应用的角度出发&#xff0c;协议可理解为“规则”&#xff0c;是数据传输和数据的解释的规则。 典型协议 传输层 常见协议有TCP/UDP协议。 应用层 常见的协议有HTTP协议&#xff0c;FTP协议。 网络层 常见协议有IP协议、ICMP协议、IGMP协议。 网络接口层 常…

NLP 笔记:LDA(训练篇)

1 前言&#xff1a;吉布斯采样 吉布斯采样的基本思想是&#xff0c;通过迭代的方式&#xff0c;逐个维度地更新所有变量的状态 1.1 举例 收拾东西 假设我们现在有一个很乱的屋子&#xff0c;我们不知道东西应该放在哪里&#xff08;绝对位置&#xff09;&#xff0c;但知道哪…

【排序算法】实现快速排序值(霍尔法三指针法挖坑法优化随即选key中位数法小区间法非递归版本)

文章目录 &#x1f4dd;快速排序&#x1f320;霍尔法&#x1f309;三指针法&#x1f320;挖坑法✏️优化快速排序 &#x1f320;随机选key&#x1f309;三位数取中 &#x1f320;小区间选择走插入&#xff0c;可以减少90%左右的递归&#x1f309; 快速排序改非递归版本&#x1…

设计模式及其在项目、框架中的应用

设计模式的作用&#xff1a; 1、类之间关系图&#xff0c;明确的角色及其关系、作用&#xff1b; 2、符合开闭原则&#xff0c;职责明确&#xff0c;并且开放的拓展点可以有效应对后期的变化。 &#xff08;一&#xff09;、责任链模式 适用场景&#xff1a; 在一个流程中&…

【QT入门】 Qt实现自定义信号

往期回顾&#xff1a; 【QT入门】图片查看软件(优化)-CSDN博客 【QT入门】 lambda表达式(函数)详解-CSDN博客 【QT入门】 Qt槽函数五种常用写法介绍-CSDN博客 【QT入门】 Qt实现自定义信号 一、为什么需要自定义信号 比如说现在一个小需求&#xff0c;我们想要实现跨ui通信&a…

Git 使用笔记

基本操作&#xff1a; 初始化 &#xff08;git init&#xff09; 使用背景和作用&#xff1a; 在本地建立一个文件夹后&#xff0c;基于这个文件夹进行git 操作&#xff0c;赋予git操作本文件夹的权限 。查看当前文件夹状态&#xff08;git status&#xff09; 每次打开文件夹…

环信新版单群聊UIKit集成指南——Android篇

前言 环信新版UIKit已重磅发布&#xff01;目前包含单群聊UIKit、聊天室ChatroomUIKit&#xff0c;本文详细讲解Android端单群聊UIKit的集成教程。 环信单群聊 UIKit 是基于环信即时通讯云 IM SDK 开发的一款即时通讯 UI 组件库&#xff0c;提供各种组件实现会话列表、聊天界…

机器学习:智能时代的核心引擎

目录 一、什么是机器学习 二、监督学习 三、无监督学习 四、半监督学习 五、强化学习 一、什么是机器学习 机器学习是人工智能的一个分支&#xff0c;它主要基于计算机科学&#xff0c;旨在使计算机系统能够自动地从经验和数据中进行学习并改进&#xff0c;而无需进行明确…

鸿蒙Harmony应用开发—ArkTS(stateStyles:多态样式)

Styles和Extend仅仅应用于静态页面的样式复用&#xff0c;stateStyles可以依据组件的内部状态的不同&#xff0c;快速设置不同样式。这就是我们本章要介绍的内容stateStyles&#xff08;又称为&#xff1a;多态样式&#xff09;。 概述 stateStyles是属性方法&#xff0c;可以…