Files
agame/addons/model_localizer/README.md
T
2026-10-04 15:01:12 +08:00

206 lines
10 KiB
Markdown

# Model Localizer
把 FBX / GLB 拖进 Godot 4.7 编辑器后,自动生成**独立的本地 `.tscn` 场景**并从网格烘焙**凸包碰撞体**。
针对 Godot **4.7.2** 编写并实测。
![生成的本地场景](docs/generated_scene.png)
*由 `Chair_PC_04.glb` 生成的 `Chair_PC_04_local.tscn`,几何、材质、贴图完整。*
![碰撞可视化](docs/collision_debug.png)
*绿色为烘焙出的三角网格碰撞体叠加显示 —— 它逐面贴合模型(连椅子脚轮的细节都覆盖到了),这是 `Trimesh` 模式的效果。用 `tests/capture_dbg.tscn` 渲染。*
---
## 它做什么
往 3D 视口里拖入一个模型,插件会:
1. **感知拖入** —— 外部文件先被移动到 `res://models/`(可配置),不再散落在 `res://` 根目录;
2. **生成本地场景** —— 在模型旁边写出 `模型名_local.tscn`,节点树完全独立,不再引用模型的导入结果;
3. **烘焙碰撞** —— 每个网格生成一个 `ConvexPolygonShape3D`,挂到 `StaticBody3D` 下;
4. **替换实例** —— 把场景里那个模型实例换成生成好的本地场景(**可 Ctrl+Z 撤销**)。
已经自带碰撞的模型(`-col` / `-colonly` 后缀,或导入设置里生成过碰撞)会被**原样跳过**,绝不重复添加。
---
## 安装
1. 把整个 `addons/model_localizer/` 目录复制到你项目的 `addons/` 下;
2. 打开 **项目 → 项目设置 → 插件**,启用 **Model Localizer**;
3. 插件菜单出现在 **项目 → 工具** 下。
设置项在 **项目设置 → model_localizer**。
---
## 设置
| 设置 | 默认 | 说明 |
| --- | --- | --- |
| `auto_process_scene_instances` | `true` | 自动处理拖进场景的模型 |
| `auto_swap_to_local` | `true` | 用生成的本地场景替换拖入的实例 |
| `auto_import_dropped_files` | `true` | 把外部拖入的模型文件移进下面的目录 |
| `dropped_files_dir` | `res://models/` | 外部文件的落地目录 |
| `output_dir` | `presets` | **相对路径** = 模型同目录下的子文件夹;`res://` 开头 = 用该绝对目录;留空 = 与模型同目录 |
| `output_suffix` | `_local` | 生成文件名后缀 |
| `add_collision` | `true` | 是否烘焙碰撞 |
| `collision_shape` | **`Trimesh`** | `Convex Hull` / `Trimesh` / `Box` / `Sphere` / `Capsule` / `Cylinder` |
| `collision_body` | **`StaticBody3D`** | `StaticBody3D` / `RigidBody3D` / `Area3D` / `CharacterBody3D` |
| `force_convex_hull` | `true` | **仅 `Convex Hull` 有效**:用真实三角面构建凸包 |
| `collision_layer` / `collision_mask` | `1` | 碰撞层 / 掩码 |
| `skip_dirs` | `addons/`, `.godot/` | 永不扫描的目录 |
### 默认布局
按默认设置,以 `Chair_PC_04.glb` 为例,文件和生成的场景是这样摆放的:
```
assets/models/
├─ Chair_PC_04.glb ← 你拖进来的模型
├─ Chair_PC_04_Textures_4.png ← 它带来的贴图
└─ presets/
└─ Chair_PC_04_local.tscn ← 自动生成:静态物体子节点 + 三角网格碰撞
```
也就是说**场景永远生成在「模型所在目录的 `presets` 子文件夹」里**,该目录不存在时插件会自动创建。
### 形状怎么选
- **Trimesh**(默认)—— 逐面精确,不做凸包简化。**只能用在静态物体上**,顶点多时开销较大。适合地形、建筑,以及任何不需要移动的模型。
- **Convex Hull** —— 每个网格一个凸包。性能好,可用于会移动的物体;凹形物体会被"填平"。
- **Box / Sphere / Capsule / Cylinder** —— 最省性能。这几种形状会按网格的实际位置自动偏移居中。
### 生成的场景根节点保持单位变换
生成的 `.tscn` **根节点变换固定为单位阵**,拖放位置记录在**场景实例**上。这样做有两个原因:
1. Godot 会对"根节点被变换"的场景弹出警告(`节点配置警告!建议不要变换场景的根节点`);
2. 即使你不理这个警告,Godot 里**实例自身的变换优先级高于场景内根节点的变换** —— 烘焙进根节点的位置会被静默丢弃。
所以位置信息放在它真正生效的地方:实例节点。序列化时 `placement_root_transform` 返回给插件,由插件负责把实例摆到正确位置。可以用 `tests/root_transform_warning_test.gd` 复核。
---
## 手动操作
**项目 → 工具** 菜单:
- **处理选中模型** —— 对场景里选中的模型实例执行一次转换;
- **重置记录** —— 清空本次会话的处理记录(想重新处理同一个模型时用);
- **设置** —— 跳到项目设置页。
---
## 行为细节
- **幂等**:同一个模型在同一场景里只处理一次,不会反复生成。
- **模型重导出自动同步**:模型的 `.import` 变了(重新导入 / 换文件),生成场景会跟着刷新。
- **动画安全**:骨骼、`Skeleton3D`、`AnimationPlayer`、动画轨道路径都会保留并校验。实测一个 49 节点、8 个网格、2 段动画、434 条轨道的绑定模型,转换后轨道 **0 条失效**。
- **碰撞几何取自网格的绑定姿态几何**,按节点累积变换放置。实测每个碰撞体与对应网格的顶点包围盒**完全重合**(偏差 0.000000)。
### 关于蒙皮(绑定)模型的限制
对**骨骼绑定**的模型,碰撞体是按它在**静止姿态下的几何**生成的,而不是按运行时骨骼变形后的形状。这是静态烘焙的固有性质(游戏里常见的做法是用胶囊/凸包代替,而不是让碰撞跟随骨骼)。
需要知道的一点:某些 FBX 的**绑定姿态与静止姿态互相矛盾**(导出器没有保持一致)。在这类资产上,骨骼静止姿态给出的位置和渲染出来的位置对不上,没有一个统一变换能同时匹配两者。遇到这种情况,插件的碰撞体会贴合**绑定姿态几何**;如果你发现碰撞和看到的位置不符,**换用 `Convex Hull` 或 `Box` 通常更容易得到符合直觉的结果**。
需要碰撞跟随骨骼运动时,请把 `collision_body` 改成 `RigidBody3D` 或 `CharacterBody3D`,并自行用骨骼驱动它。
- **贴图仍共享**:生成场景通过 `[ext_resource]` 引用贴图,所以同一模型的多个实例共享贴图,不会把贴图复制多份。
- **只在编辑器里运行**,不进游戏包。
---
## 常见问题
**生成场景很大?**
网格数据(顶点、索引、UV)内嵌在 `.tscn` 里,这是"完全本地化"的代价。若模型本身不大而文件异常大,说明网格顶点很多,属于正常。想减小体积可以把 `collision_shape` 换成 `Box`,或把 `add_collision` 关掉后单独处理。
**碰撞体不跟着动画动?**
烘焙出来的是**静态**碰撞体。动画驱动的角色请把 `collision_body` 改成 `RigidBody3D`,或按需换成 `CharacterBody3D` 后自己接管移动。
**模型已有碰撞,插件又加了一遍?**
不会。检测到子树里已有 `CollisionShape3D` / `CollisionPolygon3D` 时整体跳过。如果你**想**重新烘焙,先删掉旧碰撞再执行一次「处理选中模型」(并先「重置记录」)。
**拖动后没反应?**
检查三件事:插件是否已在项目设置里启用;模型根节点是否直接挂在当前编辑场景根下;`auto_process_scene_instances` 是否为 `true`。也可以用「处理选中模型」手动触发。
**`Invalid array format for surface` 警告?**
FBX 的压缩网格在编辑器里重载时会打印这个。已实测生成的 `.tscn` 重新加载后**顶点数与索引数与原始完全一致**,几何无损,可忽略。
---
## 测试
`tests/` 下是随附的验证脚本(跑在本目录的 demo 工程里)。
```bash
# 1) 核心逻辑自测:72 项断言,覆盖路径识别、结构保真、碰撞、动画、序列化往返
godot --headless --path . --script tests/self_test.gd
# 2) 模拟「拖入模型」:先造出带模型实例的场景,再让编辑器加载它
godot --headless --path . --script tests/build_drop_scene.gd
godot --headless --editor --quit --path . --scene res://tests/drop_test.tscn
# 3) 校验生成的场景是否完好(网格表面 / 顶点 / 索引 / 碰撞)
godot --headless --path . --script tests/verify_local.gd -- \
res://assets/models/Chair_PC_04_local.tscn
# 4) 复核生成的场景根节点没有变换(不触发 Godot 的根节点警告)
godot --headless --path . --script tests/root_transform_warning_test.gd
# 5) 看一眼模型(务必带 opengl3;本机 Vulkan 不可用时默认渲染器会崩)
godot --rendering-driver opengl3 --path . res://tests/capture.tscn -- \
res://assets/models/Chair_PC_04_local.tscn shot.png
# 6) 把碰撞体以半透明绿色叠加显示
godot --rendering-driver opengl3 --path . res://tests/capture_dbg.tscn -- \
res://assets/models/Chair_PC_04_local.tscn collision.png
# 7) 物理验证:小球落到碰撞体上(窗口模式)
godot --rendering-driver opengl3 --path . res://tests/physics_test.tscn -- \
res://assets/models/Chair_PC_04_local.tscn
```
> **注意**:步骤 5/6/7 依赖真实的场景树。headless `--script` 模式下 `SceneTree.root`
> **本身不在树里**,`global_transform` 不会传播 —— 任何涉及变换传播的检查都会读到
> 单位阵而给出**假结果**(包括假通过)。所以这类验证必须走编辑器 `--editor`,
> 或带 `--rendering-driver opengl3` 以窗口模式运行。
### 实测结果
在 `Chair_PC_04.glb`(8 670 面)与 `GruffaloMouse.fbx`(18 126 面、49 节点、骨骼 + 434 条动画轨道)上:
| 检查项 | GLB | FBX |
| --- | --- | --- |
| 节点数(不含碰撞) | 2 → 2 | 49 → 49 |
| 网格实例 | 1 → 1 | 8 → 8 |
| 索引数 | 8 670 → 8 670 | 18 126 → 18 126 |
| 骨骼 / 动画播放器 | — | 1 → 1 / 1 → 1 |
| 动画轨道失效 | 0 | **0 / 434** |
| 碰撞体与网格顶点包围盒偏差 | 0.000000 | 0.000000(8/8 个网格) |
| 小球落到碰撞体上 | PASS | 见上方蒙皮限制说明 |
| 生成场景重量校验 | PASS | PASS |
---
## 工程结构
```
demo_project/
├─ addons/model_localizer/ ← 插件本体(复制这个目录即可)
│ ├─ plugin.cfg
│ ├─ model_localizer_plugin.gd 编辑器集成(探测 / 生成 / 替换)
│ ├─ model_localizer_core.gd 核心逻辑(纯逻辑,可 headless 自测)
│ ├─ localizer_settings.gd 项目设置
│ └─ README.md
├─ assets/models/ 测试模型(GLB + 带骨骼动画的 FBX)
├─ docs/ 截图
└─ tests/ 验证脚本
```
*Model Localizer · 面向 Godot 4.7.2*