【Godot4.x】Mesh相关知识总结

概述

很早之前发布过一篇关于几何体程序生成的文章,当时对于三角面和网格的构造其实还没有特别深入的认识,直到自己脑海里想到用二维数组和点更新的方式构造2D类型的多边形Mesh结构,也意识到在Godot中其实Mesh不仅是3D网格,也可以构造2D网格。

在查看一些文章以及复习Godot相关的内置文档之后,发现基于三角面的Mesh构造,其基本结构包含两种形式,一种是三角扇,一种是三角带。几乎所有的几何图形和三维立体结构,都可以用这两种结构组合生成。

所以本文是仍然一个很基础的总结,但是后续基于这些总结,能搞出什么,就不一定了。

网格(Mesh)的基础知识

网格分类

按照计算机图形学,网格(Mesh)大致可以分为三角网格(Triangle Mesh)和多边形网格。三角网格的所有面都是由三角形组成的,而多边形网格是由四边形或更多边形组成的。

而在Godot中,Mesh(网格)是一种资源类型,存储三维空间中基于三角面的几何体数据,通常用于3D场景的MeshInstance3D节点显示3D物体。

但同时,在2D场景中,也有对应的MeshInstance2D节点,可以显示2D网格。

Godot并没有提供直接编辑模型网格的能力。网格还是需要使用像Blender这样的3D建模软件来创建,然后再导入到Godot中使用。

三角和三角面

无论是2D平面还是3D空间,都需要使用三个点来定义一个三角形。在Mesh中,每三个点定义一个三角面,这个三角面有正面和背面之分,而正面、背面是由定义它的三个顶点的环绕顺序和最终的三角面法向量决定的。

环绕顺序

当我们定义一组三角形顶点时,我们会以特定的环绕顺序来定义它们,可能是顺时针(Clockwise)的,也可能是逆时针(Counter-clockwise)的。每个三角形由3个顶点所组成,我们会从三角形中间来看,为这3个顶点设定一个环绕顺序。 – 面剔除 - LearnOpenGL CN (learnopengl-cn.github.io)

在这里插入图片描述

Godot 对三角形图元模式的正面使用顺时针环绕顺序。而OpenGL默认将逆时针顶点所定义的三角形当做是正向三角形。

三角扇(Triangle Fan)

巧妙的从一个“共点”出发,可以更轻松的构造连续性的“共边”三角面集合,也就更容易创建多边形。这种基于共点构造的连续相邻三角面结构被称为“三角扇”,可以构造开放和闭合的三角扇结构。

在这里插入图片描述

矩形是三角扇的一种特殊形式,可以拓扑为一般的三角扇形式。

在这里插入图片描述

三角带(Triangle Strip)

还有一种三角面结构交“三角带”,特点是相邻三角形共边

在这里插入图片描述

三角网格数据表示

了解三角网格的两种连续结构——三角带和三角扇之后,就可以基于两种结构进行顶点存储的设计。三角带和三角扇的顶点存储顺序是不一样的。

在这里插入图片描述

一般用两个数组,分别存储顶点数据和三角面数据,它们分别被称为顶点表和索引表。

  • 顶点表:用于顺序存储顶点(不重复)
  • 索引表:用每三个顶点的索引值代表一个三角面

三角带和三角扇如果顶点表顺序得当,就可以暗含三角形的信息。

可以通过遍历形式,快速生成索引表。

比如:

  • 上图左的三角扇,其三角形集合为:[[0,1,2],[0,2,3],[0,3,4],[0,4,5]],其规律十分明显,用遍历顶点表的方式可以快速生成
  • 上图右的三角带,其三角形集合为:[[0,1,2],[3,2,1],[2,3,4],[5,4,3]],其实可以理解为:[[0,1,2],[1,2,3].reverse(),[2,3,4],[3,4,5].reverse()],也很容易通过遍历顶点表生成。

三角带与三角扇的顶点数与三角形的关系

顶点数三角形数
31
42
53
64

可以看到无论是三角带还是三角扇,都符合顶点数 = 三角形数 + 2的规律。

三角带与三角扇网格生成函数

# 通过顶点表返回三角扇结构的三角形集合
func triangle_fan_3d(vetexs:PackedVector3Array) -> Array[PackedVector3Array]:
	var triangles:Array[PackedVector3Array]
	if vetexs.size() >2:
		for i in range(vetexs.size()-2):
			var tri:PackedVector3Array = [vetexs[0],vetexs[i+1],vetexs[i+2]]
			triangles.append(tri)
	return triangles

# 通过顶点表返回三角带结构的三角形集合
func triangle_strip_3d(vetexs:PackedVector3Array) -> Array[PackedVector3Array]:
	var triangles:Array[PackedVector3Array]
	if vetexs.size() >2:
		for i in range(vetexs.size()-2):
			var tri:PackedVector3Array
			if i % 2 == 0: # 奇数项
				tri = [vetexs[i],vetexs[i+1],vetexs[i+2]]
			else:
				tri = [vetexs[i],vetexs[i+1],vetexs[i+2]]
				tri.reverse()  # 翻转
			triangles.append(tri)
	return triangles

测试:

@tool
extends EditorScript

var points:PackedVector3Array = [
	Vector3(0,0,0),
	Vector3(0,1,0),
	Vector3(1,1,0),
	Vector3(1,0,0),
]

func _run() -> void:
	print(triangle_fan_3d(points))

打印输出:

[[(0, 0, 0), (0, 1, 0), (1, 1, 0)], [(0, 0, 0), (1, 1, 0), (1, 0, 0)]]
@tool
extends EditorScript

var points:PackedVector3Array = [
	Vector3(0,0,0),
	Vector3(0,1,0),
	Vector3(2,0,0),
	Vector3(0,3,0),
]

func _run() -> void:
	print(triangle_strip_3d(points))

输出:

[[(0, 0, 0), (0, 1, 0), (2, 0, 0)], [(0, 3, 0), (2, 0, 0), (0, 1, 0)]]

注意

这里为了方便演示效果,直接获取了三角形的顶点组合,实际中,主需要生成顶点索引组成的三角形列表就可以了。


顶点着色

三角网格的每个顶点都可以设置一个单独的颜色。

贴图与UV坐标

三角网格的每个顶点都可以设置一个UV坐标,用于对应贴图的一部分。

法向量与切向量

平滑组

程序式几何体生成

涉及SurfaceToolArrayMeshImmediateMesh以及 MeshDataTool等内部类。

使用SurfaceTool

在这里插入图片描述
在这里插入图片描述

依次添加顶点形式

@tool
extends Node3D

@onready var mesh_instance_3d: MeshInstance3D = $MeshInstance3D
@onready var mesh_instance_2d: MeshInstance2D = $MeshInstance2D


func _enter_tree() -> void:
	await ready	
	var sf = SurfaceTool.new()
	sf.begin(Mesh.PRIMITIVE_TRIANGLES)
	
	# 添加第1点
	sf.set_color(Color.AQUA)
	sf.set_uv(Vector2(0,0))
	sf.add_vertex(Vector3(0,0,0))
	# 添加第2点
	sf.set_color(Color.AQUAMARINE)
	sf.set_uv(Vector2(0,1))
	sf.add_vertex(Vector3(0,1,0))
	# 添加第3点
	sf.set_color(Color.RED)
	sf.set_uv(Vector2(1,1))
	sf.add_vertex(Vector3(1,1,0))
	
	mesh_instance_2d.mesh = sf.commit()

在这里插入图片描述

  • 顶点颜色在MeshInstance3D不起作用,在MeshInstance2D中起作用
  • MeshInstance2D中,要让纹理起作用,必须在添加顶点之前用set_uv()设定UV坐标

)

用数组形式添加

@tool
extends Node3D

@onready var mesh_instance_3d: MeshInstance3D = $MeshInstance3D
@onready var mesh_instance_2d: MeshInstance2D = $MeshInstance2D


func _enter_tree() -> void:
	await ready	
	var sf = SurfaceTool.new()
	sf.begin(Mesh.PRIMITIVE_TRIANGLES)
	
	
	# 多个三角形组成的三角扇
	var triangles:= [
		# 第1个三角形
		Vector3(0,0,0),
		Vector3(0,1,0),
		Vector3(1,1,0)
	]
	
	var uvs:=[
		# 第1个三角形对应顶点的UV坐标
		Vector2(0,0),
		Vector2(0,1),
		Vector2(1,1)
	]
	
	sf.add_triangle_fan(triangles,uvs)
	
	mesh_instance_3d.mesh = sf.commit()
	mesh_instance_2d.mesh = sf.commit()

创建2个三角面组成矩形

@tool
extends Node3D

@onready var mesh_instance_3d: MeshInstance3D = $MeshInstance3D
@onready var mesh_instance_2d: MeshInstance2D = $MeshInstance2D


func _enter_tree() -> void:
	await ready	
	var sf = SurfaceTool.new()
	sf.begin(Mesh.PRIMITIVE_TRIANGLES)
	
	
	# 多个三角形组成的三角扇
	var triangles:= [
		# 第1个三角形
		Vector3(0,0,0),
		Vector3(0,1,0),
		Vector3(1,1,0),
		# 第2个三角形
		Vector3(1,1,0),
		Vector3(1,0,0),
		Vector3(0,0,0),
	]
	
	var uvs:=[
		# 第1个三角形对应顶点的UV坐标
		Vector2(0,0),
		Vector2(0,1),
		Vector2(1,1),
		# 第2个三角形对应顶点的UV坐标
		Vector2(1,1),
		Vector2(1,0),
		Vector2(0,0),
	]
	
	sf.add_triangle_fan(triangles,uvs)
	
	mesh_instance_3d.mesh = sf.commit()
	mesh_instance_2d.mesh = sf.commit()

在这里插入图片描述

使用“共点”和构造扇面的思维,顶点和uv数据都可以大大简化。

# 不重复的顶点
var vertexs = [
    Vector3(0,0,0),
    Vector3(0,1,0),
    Vector3(1,1,0),
    Vector3(1,0,0),
]
# UV坐标
var uv_arr = [
    Vector2(0,0),
    Vector2(0,1),
    Vector2(1,1),
    Vector2(1,0),
]

# 多个三角形组成的三角扇
var triangles:= [
    # 第1个三角形
    vertexs[0],
    vertexs[1],
    vertexs[2],
    # 第2个三角形
    vertexs[0],
    vertexs[2],
    vertexs[3],
]

var uvs:=[
    # 第1个三角形对应顶点的UV坐标
    uv_arr[0],
    uv_arr[1],
    uv_arr[2],
    # 第2个三角形对应顶点的UV坐标
    uv_arr[0],
    uv_arr[2],
    uv_arr[3],
]

直接构造ArrayMesh实例

@tool
extends Node3D

@onready var mesh_instance_3d: MeshInstance3D = $MeshInstance3D
@onready var mesh_instance_2d: MeshInstance2D = $MeshInstance2D


func _enter_tree() -> void:
	await ready	
	
	# 多个三角形组成的三角扇
	var triangles:PackedVector3Array= [
		# 第1个三角形
		Vector3(0,0,0),
		Vector3(0,1,0),
		Vector3(1,1,0),
		# 第2个三角形
		Vector3(1,1,0),
		Vector3(1,0,0),
		Vector3(0,0,0),
	]
	
	var uvs:PackedVector2Array=[
		# 第1个三角形对应顶点的UV坐标
		Vector2(0,0),
		Vector2(0,1),
		Vector2(1,1),
		# 第2个三角形对应顶点的UV坐标
		Vector2(1,1),
		Vector2(1,0),
		Vector2(0,0),
	]
	
	var mesh = ArrayMesh.new()
	var arr = []
	arr.resize(Mesh.ARRAY_MAX)
	arr[Mesh.ARRAY_VERTEX] = triangles
	arr[Mesh.ARRAY_TEX_UV] = uvs
	mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES,arr)
	
	mesh_instance_3d.mesh = mesh
	mesh_instance_2d.mesh = mesh

可以看到,Mesh的核心数据,是一个固定结构的二维数组,其每个子数组分别对应不同的数据信息。ArrayMesh通过这样的数组,可以直接创建Mesh也就不足为奇。

var arr = [
    [],  # 0,ARRAY_VERTEX
    [],  # 1,ARRAY_NORMAL
    [],  # 2,ARRAY_TANGENT
    [],  # 3,ARRAY_COLOR
    [],  # 4,ARRAY_TEX_UV
    [],  # 5,ARRAY_TEX_UV2
    [],  # 6,ARRAY_CUSTOM0
    [],  # 7,ARRAY_CUSTOM1
    [],  # 8,ARRAY_CUSTOM2
    [],  # 9,ARRAY_CUSTOM3
    [],  # 10,ARRAY_BONES
    [],  # 11,ARRAY_WEIGHTS
    [],  # 12,ARRAY_INDEX
] 

其中:顶点数组可以是PackedVector2Array也可以是PackedVector3Array

ImmediateMesh

在这里插入图片描述

示例:

@tool
extends Node3D

@onready var mesh_instance_3d: MeshInstance3D = $MeshInstance3D
@onready var mesh_instance_2d: MeshInstance2D = $MeshInstance2D


func _enter_tree() -> void:
	await ready	
	
	# 多个三角形组成的三角扇
	var triangles:PackedVector3Array= [
		# 第1个三角形
		Vector3(0,0,0),
		Vector3(0,1,0),
		Vector3(1,1,0),
		# 第2个三角形
		Vector3(1,1,0),
		Vector3(1,0,0),
		Vector3(0,0,0),
	]
	
	var uvs:PackedVector2Array=[
		# 第1个三角形对应顶点的UV坐标
		Vector2(0,0),
		Vector2(0,1),
		Vector2(1,1),
		# 第2个三角形对应顶点的UV坐标
		Vector2(1,1),
		Vector2(1,0),
		Vector2(0,0),
	]
	
	var mesh = ImmediateMesh.new()
	
	mesh.surface_begin(Mesh.PRIMITIVE_TRIANGLES)  # 开始创建表面
	for i in range(triangles.size()):
		mesh.surface_set_uv(uvs[i])               # 设定顶点UV坐标
		mesh.surface_add_vertex(triangles[i])     # 添加顶点
	mesh.surface_end()                            # 结束
	
	mesh_instance_3d.mesh = mesh
	mesh_instance_2d.mesh = mesh

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

巽星石

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值