# 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*