概述
很早之前发布过一篇关于几何体程序生成的文章,当时对于三角面和网格的构造其实还没有特别深入的认识,直到自己脑海里想到用二维数组和点更新的方式构造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()]
,也很容易通过遍历顶点表生成。
三角带与三角扇的顶点数与三角形的关系
顶点数 | 三角形数 |
---|---|
3 | 1 |
4 | 2 |
5 | 3 |
6 | 4 |
可以看到无论是三角带还是三角扇,都符合顶点数 = 三角形数 + 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坐标,用于对应贴图的一部分。
法向量与切向量
平滑组
程序式几何体生成
涉及SurfaceTool
,ArrayMesh
、ImmediateMesh
以及 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
参考
- 面剔除 - LearnOpenGL CN (learnopengl-cn.github.io)
- Godot4.3官方文档和引擎内置文档
- 基本3D图形:多边形网格浅谈 - 知乎 (zhihu.com)
- 三角网格(Triangle Mesh)与四角mesh网格理解总结 - 知乎 (zhihu.com)