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

news2024/11/27 4:19:54

概述


说明:本文基础内容写于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 EditorScript


func _run():
	var ins:MeshInstance3D = get_scene().get_node("MeshInstance3D")
	ins.mesh = create_mesh()
	pass

func create_mesh():
	var st = SurfaceTool.new()
	# ...
	return mesh

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

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

@tool
extends EditorScript


func _run():
	var ins:MeshInstance3D = get_scene().get_node("MeshInstance3D")
	ins.mesh = create_mesh()
	pass

func 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 EditorScript


func _run():
	var ins:MeshInstance3D = get_scene().get_node("MeshInstance3D")
	ins.mesh = create_mesh()
	pass

func 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 EditorScript


func _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)
	# 构造AABB
	var position = Vector3.ZERO
	var size = Vector3.ONE
	var box:AABB = AABB(position,size)
	var end = box.end
	# ============= 获得立方体8个顶点的坐标
	# positon和and
	var p_P = position
	var 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 EditorScript


func _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:PackedVector3Array
	for area_pos in area_pos_arr:
		# 以16×16为一个小区域
		var area_start_pos = area_pos * 16
		var area_size = Vector3.ONE * 16
		var 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:
		# 构造AABB
		var size = Vector3.ONE
		var box:AABB = AABB(pos,size)
		var end = box.end
		# ============= 获得立方体8个顶点的坐标
		# positon和and
		var p_P = pos
		var 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.coloradmin.cn/o/1530695.html

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

相关文章

使用WordPress在US Domain Center上建立摄影网站的详细教程

第一部分:介绍摄影网站 摄影网站是摄影师展示作品、分享经验、提供服务的在线平台。在摄影网站上,摄影师可以展示自己的摄影作品、发布摄影日志、接受客户预约等。使用WordPress搭建摄影网站具有灵活性和可扩展性,可以通过选择适合的主题和插…

视频转文字怎么转?这几个转换方法收藏一下

视频转文字怎么转?在信息爆炸的时代,视频内容日益丰富,但如何快速、准确地提取视频中的关键信息却成为了一个挑战。本文将为你详细介绍视频转文字的方法,让你轻松提取视频内容,提高信息获取效率。 方法一:清…

【赠书第21期】游戏力:竞技游戏设计实战教程

文章目录 前言 1 竞技游戏设计的核心要素 1.1 游戏机制 1.2 角色与技能 1.3 地图与环境 2 竞技游戏设计的策略与方法 2.1 以玩家为中心 2.2 不断迭代与优化 2.3 营造竞技氛围与社区文化 3 实战案例分析 4 结语 5 推荐图书 6 粉丝福利 前言 在数字化时代的浪潮中&…

IO多分复用

#include<myhead.h> #define SER_PORT 8888 //服务器端口号 #define SER_IP "192.168.65.131" //服务器IPint main(int argc, const char *argv[]) {//1、创建一个套接字int sfd -1;sfd socket(AF_INET, SOCK_STREAM, 0); //参数1&#xff1a;…

win10共享了连接打印机报错提示0000011B

在Windows 10系统中&#xff0c;当用户尝试连接共享打印机时遇到错误代码0x0000011B&#xff0c;通常表示存在与打印机驱动程序或系统更新相关的兼容性问题。以下是针对这个问题的一些解决方案&#xff1a; 卸载特定系统更新&#xff1a; 根据多个来源的信息&#xff0c;错误0x…

软考考哪个好?软考中级到高级方向该如何计划

刚接触软考的朋友可能对软考的科目选择比较迷茫&#xff0c;不知道自己该怎么选方向&#xff0c;怎么选级别&#xff0c;这属于再正常不过的事情了&#xff0c;毕竟谁看到这些都可能有些迷茫&#xff0c;要理清自己的选择还是要从自身的几个方面开始的。 软考的考试专业类别就…

vue2从基础到高级学习笔记

在实际的工作中,我常使用vue的用法去实现效果,但是你要是问我为什么这样写,它的原理是啥就答不上来了。对vue的认知一直停留在表面,写这篇文章主要是为了理清并弄透彻vue的原理。 学习目标 1 学会一些基本用法的原理 2 弄懂vue核心设计原理 3 掌握vue高级api的用法 一 vue…

【前端寻宝之路】学习和总结HTML的标签属性

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法|MySQL| ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不…

【项目管理后台】Vue3+Ts+Sass实战框架搭建一

项目管理后台 建立项目最好是卸载Vetur 新建.env.d.ts文件安装Eslint安装校验忽略文件添加运行脚本 安装prettier新建.prettierrc.json添加规则新建.prettierignore忽略文件 安装配置stylelint新建.stylelintrc.cjs 添加后的运行脚本配置husky配置commitlint配置husky 强制使用…

resize-observer源码解读

resize-observer github 地址&#xff1a;https://github.com/devrelm/resize-observer 本地启动 npm installnpm startnode 18.16.0 (npm 9.5.1) 启动失败报错 node:internal/crypto/hash:71this[kHandle] new _Hash(algorithm, xofLen);^Error: error:0308010C:digital …

spark RDD 创建及相关算子

RDD编程入口 RDD编程入口对象是SparkContext对象&#xff0c;想要调用相关的计算api都需要通过构造出的sparkcontext对象调用 RDD的创建 通过并行化集合创建RDD&#xff08;本地集合转为分布式&#xff09;&#xff0c;api如下 rdd sc.parrallize(param1, param2)参数1是本…

第112讲:Mycat实践指南:字符串Hash算法分片下的水平分表详解

文章目录 1.字符串Hash算法分片的概念1.1.字符串Hash算法的概念1.2.字符串Hash算法是如何将数据路由到分片节点的 2.使用字符串Hash算法分片对某张表进行水平拆分2.1.在所有的分片节点中创建表结构2.2.配置Mycat实现字符串Hash算法分片的水平分表2.2.1.配置Schema配置文件2.2.2…

边缘检测-Tiny and Efficient Model for the Edge Detection Generalization

源代码: https://github.com/xavysp/TEED 论文地址&#xff1a;https://arxiv.org/pdf/2308.06468.pdf 大多数高级计算机视觉任务依赖于低级图像操作作为其初始过程。边缘检测、图像增强和超分辨率等操作为更高级的图像分析提供了基础。在这项工作中&#xff0c;我们考虑三个…

公众号获取token失败,当日access_token超过1万次处理

问题&#xff1a;如果你当日的 access_token 获取次数已经超过了 1 万次&#xff0c;那么很有可能是由于频繁获取 access_token 而被微信限制了。在这种情况下&#xff0c;你需要等待到下一个自然日或者等待一段时间后再尝试获取 access_token。或者直接去公众号去刷新掉用量。…

JAVA EE (计算机是如何工作的)

学前注意事项 出去面试的时候java岗位不需要懂前端&#xff08;会少量讲解&#xff09; 但是我们做项目的时候多少回用到一些前端的东西 1.什么是计算机 1.1前情提要 不仅仅只有电脑是计算机 计算机还不仅仅是电脑手机和平板 路由器 智能洗衣机 刷脸打卡机都可以说是计算…

MySQL中数据库表的监控

MySQL中数据库表的监控 &#xff08;1&#xff09;查看数据库中当前打开了哪些表&#xff1a;show OPEN TABLES &#xff0c;如图6-1-5所示。另外&#xff0c;还可以通过show OPEN TABLES where In_use > 0过滤出当前已经被锁定的表。 查看数据库中表的状态&#xff1a;SHO…

月销12万,卖出6万件,1688跨境热销榜第一是它!

店雷达1688热销商品榜&#xff0c;食品类目下&#xff0c;月度排名第一的热销商品是这款【三只松鼠_多味鹌鹑蛋混合口味 休闲零食小吃卤蛋夜宵熟食】月销售额达到12万&#xff0c;一个月内卖出5万笔&#xff0c;复购率保持在51%。看来我们的这款中国小吃&#xff0c;也收到很多…

linux -- I2C设备驱动 -- MS32006(低压5V多通道电机驱动器)

产品简述 MS32006 是一款多通道电机驱动芯片, 其中包含两路步进电机驱动, 一路直流电机驱动; 每个通道的电流最高电流1.0A; 支持两相四线与四相五线步进电机。芯片采用 I2C 的通信接口控制模式, 兼容 3.3V/5V 的标准工业接口。 MS32006 总共集成了两路步进电机驱动器与一…

数据分析能力模型分析与展示

具体内容&#xff1a; 专业素质 专业素质-01 数据处理 能力定义•能通过各种数据处理工具及数据处理方法&#xff0c;对内外部海量数据进行清洗和运用&#xff0c;提供统一数据标准&#xff0c;为业务分析做好数据支持工作。 L1•掌握一…

【数据结构】数据结构和算法的重要性复杂度详解

主页&#xff1a;醋溜马桶圈-CSDN博客 专栏&#xff1a;数据结构_醋溜马桶圈的博客-CSDN博客 gitee&#xff1a;mnxcc (mnxcc) - Gitee.com 目录 1.数据结构和算法 1.1什么是数据结构&#xff1f; 1.2 什么是算法&#xff1f; 1.3 数据结构和算法的重要性 1.4 如何学好数据…