第二十六章:网格与模型导入

第二十六章:网格与模型导入

"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模型的导入和使用:

  1. 支持的格式:glTF、FBX、OBJ等
  2. 导入工作流:基本导入、高级设置
  3. 使用模型:场景实例化、运行时加载
  4. 网格处理:访问和修改网格数据
  5. LOD系统:自动LOD、手动设置
  6. 网格优化:策略、合并工具
  7. 实际案例:模型加载器

下一章将学习3D材质与着色器。


上一章:3D变换与坐标系

下一章:3D材质与着色器

← 返回目录