第二十六章:网格与模型导入
"3D模型是游戏视觉的核心,掌握导入和优化技巧至关重要。"
本章将讲解如何在Godot中导入和使用3D模型,包括支持的格式、导入设置、网格优化等内容。
26.1 支持的3D格式
26.1.1 格式概览
Godot支持的3D格式:
├── glTF 2.0 (.gltf, .glb) - 推荐格式
│ ├── 开放标准
│ ├── 支持PBR材质
│ ├── 支持骨骼动画
│ └── 支持场景层级
├── FBX (.fbx)
│ ├── 广泛使用
│ ├── 需要FBX2glTF转换
│ └── 兼容性好
├── Collada (.dae)
│ ├── 开放格式
│ ├── XML结构
│ └── 支持动画
├── OBJ (.obj)
│ ├── 简单静态网格
│ ├── 无动画支持
│ └── 广泛兼容
└── Blend (.blend) - 需要Blender
├── 直接导入Blender文件
└── 自动转换
26.1.2 格式选择建议
使用场景 推荐格式
────────────────────────────────────
新项目开发 glTF 2.0
从Blender导出 glTF 2.0
从3ds Max/Maya导出 FBX → glTF
简单静态模型 OBJ
需要编辑原始文件 Blend
26.2 导入工作流
26.2.1 基本导入
导入步骤:
1. 将模型文件放入项目文件夹
2. Godot自动检测并导入
3. 在FileSystem中双击预览
4. 调整导入设置(如需要)
5. 重新导入
导入后生成的文件:
├── model.glb (原始文件)
├── model.glb.import (导入配置)
└── .godot/imported/ (导入的资源)
26.2.2 导入设置
# 在导入面板中设置,或通过代码
# 导入选项(通过.import文件或Advanced Import Settings)
"""
[remap]
importer="scene"
importer_version=1
type="PackedScene"
[deps]
source_file="res://models/character.glb"
[params]
# 根节点类型
nodes/root_type="Node3D"
nodes/root_name="Character"
# 网格设置
meshes/ensure_tangents=true
meshes/generate_lods=true
meshes/create_shadow_meshes=true
meshes/light_baking=1
# 动画设置
animation/import=true
animation/fps=30
animation/trimming=false
# 骨骼设置
skins/use_named_skins=true
"""
26.2.3 高级导入设置面板
在导入面板点击"Advanced..."打开高级设置:
Scene
├── Root Type: 根节点类型
├── Root Name: 根节点名称
└── Root Scale: 根节点缩放
Meshes
├── Ensure Tangents: 生成切线(法线贴图需要)
├── Generate LODs: 自动生成LOD
├── Create Shadow Meshes: 阴影网格
├── Light Baking: 光照烘焙模式
└── Lightmap UV: 光照贴图UV
Animation
├── Import: 是否导入动画
├── FPS: 帧率
├── Trimming: 裁剪空白帧
└── Remove Immutable Tracks: 移除不变轨道
Materials
├── Import Materials: 导入材质
├── Force Disable Mesh Compression: 禁用压缩
└── Store Material in File: 材质存为文件
26.3 使用导入的模型
26.3.1 场景实例化
extends Node3D
func _ready():
# 加载模型场景
var model_scene = preload("res://models/character.glb")
# 实例化
var model = model_scene.instantiate()
add_child(model)
# 设置变换
model.position = Vector3(0, 0, 0)
model.rotation_degrees = Vector3(0, 180, 0)
model.scale = Vector3(1, 1, 1)
# 获取模型中的节点
func access_model_parts():
var model = $Character
# 获取网格
var mesh = model.get_node("Armature/Skeleton3D/Body") as MeshInstance3D
# 获取骨骼
var skeleton = model.get_node("Armature/Skeleton3D") as Skeleton3D
# 获取动画播放器
var anim_player = model.get_node("AnimationPlayer") as AnimationPlayer
26.3.2 运行时加载
extends Node3D
func load_model_async(path: String) -> void:
# 开始异步加载
ResourceLoader.load_threaded_request(path)
func _process(delta: float):
var path = "res://models/large_model.glb"
var status = ResourceLoader.load_threaded_get_status(path)
match status:
ResourceLoader.THREAD_LOAD_IN_PROGRESS:
# 显示加载进度
var progress = []
ResourceLoader.load_threaded_get_status(path, progress)
print("加载进度:", progress[0] * 100, "%")
ResourceLoader.THREAD_LOAD_LOADED:
var scene = ResourceLoader.load_threaded_get(path)
var model = scene.instantiate()
add_child(model)
ResourceLoader.THREAD_LOAD_FAILED:
push_error("模型加载失败")
26.4 网格处理
26.4.1 访问网格数据
extends MeshInstance3D
func analyze_mesh():
var m = mesh
# 基本信息
print("表面数量:", m.get_surface_count())
print("AABB:", m.get_aabb())
# 遍历表面
for i in m.get_surface_count():
var arrays = m.surface_get_arrays(i)
var vertices = arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
var normals = arrays[Mesh.ARRAY_NORMAL] as PackedVector3Array
var uvs = arrays[Mesh.ARRAY_TEX_UV] as PackedVector2Array
var indices = arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
print("表面 %d:" % i)
print(" 顶点数:", vertices.size())
print(" 三角形数:", indices.size() / 3 if indices.size() > 0 else vertices.size() / 3)
func get_vertex_count() -> int:
var count = 0
for i in mesh.get_surface_count():
var arrays = mesh.surface_get_arrays(i)
count += (arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array).size()
return count
26.4.2 修改网格
extends MeshInstance3D
func modify_mesh():
var m = mesh as ArrayMesh
var new_mesh = ArrayMesh.new()
for surf_idx in m.get_surface_count():
var arrays = m.surface_get_arrays(surf_idx)
var vertices = arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
# 修改顶点
var new_vertices = PackedVector3Array()
for v in vertices:
# 添加噪声偏移
var offset = Vector3(
randf_range(-0.1, 0.1),
randf_range(-0.1, 0.1),
randf_range(-0.1, 0.1)
)
new_vertices.append(v + offset)
arrays[Mesh.ARRAY_VERTEX] = new_vertices
new_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
mesh = new_mesh
func scale_uvs(scale: Vector2):
var m = mesh as ArrayMesh
var new_mesh = ArrayMesh.new()
for surf_idx in m.get_surface_count():
var arrays = m.surface_get_arrays(surf_idx)
var uvs = arrays[Mesh.ARRAY_TEX_UV] as PackedVector2Array
var new_uvs = PackedVector2Array()
for uv in uvs:
new_uvs.append(uv * scale)
arrays[Mesh.ARRAY_TEX_UV] = new_uvs
new_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
mesh = new_mesh
26.5 LOD系统
26.5.1 自动LOD
# Godot 4可以在导入时自动生成LOD
# 导入设置 → Meshes → Generate LODs = true
extends MeshInstance3D
func _ready():
# 检查LOD
if mesh.has_method("get_lod_count"):
print("LOD级别数:", mesh.get_lod_count())
26.5.2 手动LOD设置
extends Node3D
@export var lod_meshes: Array[Mesh] = []
@export var lod_distances: Array[float] = [10.0, 25.0, 50.0]
@onready var mesh_instance: MeshInstance3D = $MeshInstance3D
var camera: Camera3D
func _ready():
camera = get_viewport().get_camera_3d()
func _process(delta: float):
if not camera:
return
var distance = global_position.distance_to(camera.global_position)
var lod_index = get_lod_index(distance)
if lod_index < lod_meshes.size():
mesh_instance.mesh = lod_meshes[lod_index]
func get_lod_index(distance: float) -> int:
for i in range(lod_distances.size()):
if distance < lod_distances[i]:
return i
return lod_distances.size()
26.5.3 GeometryInstance3D LOD
extends MeshInstance3D
func _ready():
# 使用内置LOD系统
lod_bias = 1.0 # LOD偏移因子
# 可见性范围(简单LOD替代)
visibility_range_begin = 0.0
visibility_range_begin_margin = 0.0
visibility_range_end = 100.0
visibility_range_end_margin = 10.0
visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_SELF
26.6 网格优化
26.6.1 优化策略
网格优化策略:
1. 减少顶点数
- 使用适当的多边形数
- 移除不可见面
- 合并顶点
2. 减少材质数
- 使用纹理图集
- 合并相同材质的网格
3. 使用LOD
- 远距离使用简化网格
- 根据性能需求设置切换距离
4. 网格合并
- 合并静态物体
- 减少Draw Call
5. 遮挡剔除
- 使用遮挡体
- 启用视锥剔除
26.6.2 网格合并工具
# mesh_merger.gd
class_name MeshMerger
static func merge_meshes(mesh_instances: Array[MeshInstance3D]) -> ArrayMesh:
var merged = ArrayMesh.new()
var surface_data: Dictionary = {} # material -> arrays
for mi in mesh_instances:
var m = mi.mesh
var t = mi.global_transform
for surf_idx in m.get_surface_count():
var mat = mi.get_surface_override_material(surf_idx)
if not mat:
mat = m.surface_get_material(surf_idx)
var arrays = m.surface_get_arrays(surf_idx)
# 变换顶点到世界空间
var vertices = arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
var normals = arrays[Mesh.ARRAY_NORMAL] as PackedVector3Array
var transformed_verts = PackedVector3Array()
var transformed_normals = PackedVector3Array()
for v in vertices:
transformed_verts.append(t * v)
for n in normals:
transformed_normals.append(t.basis * n)
arrays[Mesh.ARRAY_VERTEX] = transformed_verts
arrays[Mesh.ARRAY_NORMAL] = transformed_normals
# 按材质分组
if not surface_data.has(mat):
surface_data[mat] = []
surface_data[mat].append(arrays)
# 合并每个材质的表面
for mat in surface_data:
var combined = _combine_surface_arrays(surface_data[mat])
var surf_idx = merged.get_surface_count()
merged.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, combined)
merged.surface_set_material(surf_idx, mat)
return merged
static func _combine_surface_arrays(arrays_list: Array) -> Array:
var combined = []
combined.resize(Mesh.ARRAY_MAX)
var all_vertices = PackedVector3Array()
var all_normals = PackedVector3Array()
var all_uvs = PackedVector2Array()
var all_indices = PackedInt32Array()
var vertex_offset = 0
for arrays in arrays_list:
var verts = arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
all_vertices.append_array(verts)
if arrays[Mesh.ARRAY_NORMAL]:
all_normals.append_array(arrays[Mesh.ARRAY_NORMAL])
if arrays[Mesh.ARRAY_TEX_UV]:
all_uvs.append_array(arrays[Mesh.ARRAY_TEX_UV])
if arrays[Mesh.ARRAY_INDEX]:
var indices = arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
for idx in indices:
all_indices.append(idx + vertex_offset)
vertex_offset += verts.size()
combined[Mesh.ARRAY_VERTEX] = all_vertices
combined[Mesh.ARRAY_NORMAL] = all_normals
combined[Mesh.ARRAY_TEX_UV] = all_uvs
combined[Mesh.ARRAY_INDEX] = all_indices
return combined
26.7 实际案例
26.7.1 模型加载器
# model_loader.gd
class_name ModelLoader
extends Node
signal model_loaded(model: Node3D)
signal load_progress(progress: float)
signal load_failed(path: String, error: String)
var _loading_queue: Array[String] = []
var _current_loading: String = ""
func load_model(path: String) -> void:
_loading_queue.append(path)
_process_queue()
func _process_queue():
if _current_loading != "" or _loading_queue.is_empty():
return
_current_loading = _loading_queue.pop_front()
ResourceLoader.load_threaded_request(_current_loading)
func _process(delta: float):
if _current_loading == "":
return
var progress = []
var status = ResourceLoader.load_threaded_get_status(_current_loading, progress)
match status:
ResourceLoader.THREAD_LOAD_IN_PROGRESS:
load_progress.emit(progress[0])
ResourceLoader.THREAD_LOAD_LOADED:
var scene = ResourceLoader.load_threaded_get(_current_loading)
var model = scene.instantiate()
model_loaded.emit(model)
_current_loading = ""
_process_queue()
ResourceLoader.THREAD_LOAD_FAILED:
load_failed.emit(_current_loading, "加载失败")
_current_loading = ""
_process_queue()
本章小结
本章讲解了3D模型的导入和使用:
- 支持的格式:glTF、FBX、OBJ等
- 导入工作流:基本导入、高级设置
- 使用模型:场景实例化、运行时加载
- 网格处理:访问和修改网格数据
- LOD系统:自动LOD、手动设置
- 网格优化:策略、合并工具
- 实际案例:模型加载器
下一章将学习3D材质与着色器。
上一章:3D变换与坐标系
下一章:3D材质与着色器