Files
agame/addons/mesh_collider/README.md
T
2026-10-04 18:32:58 +08:00

225 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Mesh Collider
把 FBX / GLB 拖进 Godot 场景,**自动给这个实例加上碰撞体**。
**不复制模型、不生成新场景、不写任何文件。** 加进去的碰撞节点属于**你正在编辑的场景**,所以模型文件在项目里仍然只被引用一份。
面向 Godot **4.7.2**。
---
## 2.0 改了什么
1.0 的版本会把模型"本地化"——把整棵节点树压平、把网格数据内嵌进一个新的 `_local.tscn`。实测代价是 **1.0–3.1× 的额外存储**,以及"模型重导出后场景里是旧几何而且不报错"的风险。
2.0 **删掉了全部本地化逻辑**。现在只做一件事:往实例上挂碰撞。
之所以这样可行,是因为一个容易被忽略的事实:**实例化的场景节点可以添加子节点**。你只是不能改它内部已有的节点,而注入的碰撞体是**新子节点** —— 它作为父场景自己的内容被序列化,不引用模型文件,也就没有膨胀。
---
## 安装
1. 把 `addons/mesh_collider/` 复制到你项目的 `addons/` 下;
2. **项目 → 项目设置 → 插件**,启用 **Mesh Collider**;
3. 菜单出现在 **项目 → 工具** 下。
设置项在 **项目设置 → mesh_collider**。
---
## 用法
**拖进去就行。** 插件会轮询当前编辑的场景,发现新拖入的模型实例就自动加碰撞,并且整个操作走编辑器的撤销系统 —— **Ctrl+Z 可以撤销,Ctrl+Shift+Z 可以重做**。
菜单里还有三个手动操作:
| 菜单项 | 作用 |
| --- | --- |
| 给选中模型加碰撞 | 对当前选中的模型实例执行一次 |
| 移除选中模型的碰撞 | 移除本插件加的那个碰撞体(同样可撤销) |
| 设置 | 跳到项目设置页 |
---
## 生成的节点结构
```
Chair_PC_04 ← 模型实例,仍然引用 res://.../chair.glb
└─ CollisionBody (StaticBody3D) ← 本插件加的,属于你的场景
└─ Chair_PC_04Shape (CollisionShape3D)
```
这个布局与 **Godot 自己**用 `-col` / `-convcol` 名字后缀导入时生成的完全一致。我实测过每种后缀的行为:
| 后缀 | Godot 生成的形状 |
| --- | --- |
| `-col` / `_col` | 网格下挂 `StaticBody3D` → `ConcavePolygonShape3D`(三角网格) |
| `-convcol` / `_convcol` | 同上,形状换成 `ConvexPolygonShape3D`(凸包) |
| `-colonly` / `-convcolonly` | 网格同级生成 `@StaticBody3D@<id>`,网格隐藏 |
保持一致是有意的:两种机制产出的东西对你后续编辑而言是同一种东西,可以混用、可以互相替换。
### 那为什么不直接用 `-col`?
`-col` **更省事,建议先用它** —— 能用就别引插件。插件的价值在这几种情况:
- 要**按实例**调整碰撞(不同实例用不同形状/层);
- 模型是买来的成品,不想改它的名字或导入设置;
- 只想给**场景里某一个**实例加碰撞,而不是所有用这个模型的地方;
- 想在编辑器里直接看到、选中、手动微调这些碰撞节点。
---
## 设置
设置有两个入口,值完全同步:
- **右侧停靠栏的 `MC` 标签页**(检查器那一列)—— 装了插件就有;
- **项目设置 → mesh_collider** —— 想和其他项目设置一起看时用。
> **关于位置。** 你希望它出现在「检查器 / 信号 / 分组」那一排。那三个标签属于 Godot **内建的检查器容器**,插件 API 没有向它注入标签页的接口,所以做不到完全并排。可达的最接近位置是**右列停靠栏**,面板会和检查器出现在同一列里。`MC` 之上悬停会显示完整名称。
>
> **关于悬浮说明。** 面板**没有**使用 `EditorInspector`,而是手写表单 —— 因为 `EditorInspector` 仅在少数列出的枚举类型上把 `hint_string` 当 tooltip,勾选框和数值框悬停时什么都不显示,做不到"每个设置项都有说明"。手写之后每个设置项的标签和控件都能带 tooltip。
面板不使用 `EditorInspector`,而是逐项构建控件,但值仍然读写**同一批 ProjectSettings 键**,所以和项目设置页不会漂移。改动**立即写入**,插件下一次扫描就读到,没有"应用"这一步。
页面底部三个按钮:
| 按钮 | 作用 |
| --- | --- |
| 恢复默认值 | 把插件拥有的**全部**设置改回默认值 |
| 保存到 project.godot | 立即落盘(不点的话,编辑器退出或保存项目时也会写) |
| 在项目设置中打开 | 跳到项目设置页 |
| 设置 | 默认 | 说明 |
| --- | --- | --- |
| `ask_before_adding` | **`true`** | 拖入模型时**弹窗询问**是否添加碰撞 |
| `auto_add_collision` | `false` | 不询问时直接添加;仅在 `ask_before_adding` 关闭后生效 |
| `always_add_without_asking` | `false` | 由弹窗的「不再询问」写入 |
| `collision_shape` | **`Trimesh`** | `Convex Hull` / `Trimesh` / `Box` / `Sphere` / `Capsule` / `Cylinder` |
| `collision_body` | `StaticBody3D` | `StaticBody3D` / `RigidBody3D` / `Area3D` / `CharacterBody3D` |
| `skip_meshes_with_collision` | `false` | 跳过已经位于物理体下的网格(模型自带碰撞时打开) |
| `collision_layer` / `collision_mask` | `1` | 碰撞层 / 掩码 |
| `model_extensions` | `fbx glb gltf dae obj blend …` | 识别为模型的后缀 |
| `skip_dirs` | `addons/`, `.godot/` | 永不扫描的目录 |
### 拖入模型时的三种行为
默认**不静默改动你的场景**,而是弹窗询问:
| `ask_before_adding` | `auto_add_collision` | 拖入模型时 |
| --- | --- | --- |
| **`true`**(默认) | 忽略 | **弹窗询问**,包含「不再询问」勾选框和设置入口提示 |
| `false` | `true` | 直接添加碰撞,不问 |
| `false` | `false` | 什么都不做,只能从 项目 > 工具 手动执行 |
弹窗里的「不再询问」按你本次的选择记忆:选「添加碰撞」→ 以后直接添加;选「不添加」→ 以后不再添加也不再询问。两种情况都会把 `ask_before_adding` 关掉,想恢复询问就在 `MC` 面板里重新打开。
一次拖入多个模型只会弹**一个**弹窗,确认后整批处理。
### 形状怎么选
| 形状 | 精度 | 代价 |
| --- | --- | --- |
| **Trimesh**(默认) | 逐三角面精确,索引一个不差,不做任何简化 | 顶点多时开销大;**只能用于静态物体** |
| **Convex Hull** | 凸包,凹形被"填平"(椅子坐垫下方、脚轮之间会变实心) | 可用于移动物体,开销可控 |
| **Box / Sphere / Capsule / Cylinder** | 单个包围体,最粗 | 最省性能 |
**默认给 Trimesh,是因为静态场景物件本来就该用最精确的形状。** 精度不足的典型症状是"碰撞比模型胖"——那是凸包把凹陷填平了,换成 Trimesh 即可。
这几种基础形状会**按每个网格的实际位置自动偏移居中**,不会全部堆在原点。
> **Trimesh 与移动刚体不兼容。** `ConcavePolygonShape3D` 是静态专用形状,Godot 不会用它驱动物理模拟,而且**失效是静默的**。为避免踩坑,构建时会自动降级为凸包并打印告警 —— 需要会移动的碰撞体时,请把 `collision_shape` 换成凸包或基础形状。
### 怎么确认当前用的是哪种形状
在场景里选中 `CollisionBody` 下的 `CollisionShape3D`,看检视面板里的 **Shape** 属性:
| 看到 | 说明 |
| --- | --- |
| `ConcavePolygonShape3D` | **Trimesh** —— 逐面精确 |
| `ConvexPolygonShape3D` | **凸包** —— 凹形已被填平 |
| `BoxShape3D` / `SphereShape3D` / … | 基础形状 |
用凸包时,"碰撞比模型胖"是必然的,不是精度不足:凸包**无法表达凹形**,增加网格密度也不会改善。
### 升级提示
`collision_shape` 的默认值在 2.0 内定稿时从 `Convex Hull` 改成了 `Trimesh`。如果你之前跑过更早的版本,启动时插件会检测到"这个值仍是旧默认值"并自动迁移,同时打印一条日志。**已经生成的碰撞体不会跟着变** —— 要重新执行一次「移除选中模型的碰撞」→「给选中模型加碰撞」。
### 模型自带碰撞时
如果模型是用 `-col` 后缀导入的,它内部已经有碰撞体了。这时把 `skip_meshes_with_collision` 打开,插件会跳过那些已经有碰撞的子树,避免重复。
---
## 脚本里直接用
核心库不依赖编辑器,运行时也能用:
```gdscript
const MeshColliderLib := preload("res://addons/mesh_collider/mesh_collider.gd")
# 运行时:直接挂上去
var body := MeshColliderLib.add_collision(
model_instance,
MeshColliderLib.ShapeType.CONVEX_HULL,
MeshColliderLib.BodyType.STATIC)
if body != null:
print("shapes: ", MeshColliderLib.count_shapes(body))
# 检查 / 移除
MeshColliderLib.has_injected_collision(model_instance) # bool
MeshColliderLib.has_any_collision(model_instance) # 子树里任何碰撞
MeshColliderLib.remove_collision(model_instance) # bool
```
编辑器里请用插件(走撤销系统)。`add_collision` 是给运行时和工具脚本用的便捷包装;**放进场景保存时**记得节点要有 `owner`,否则不会被序列化。
---
## 三个必须知道的限制
**1. 碰撞体不会跟着动画变形。**
碰撞是按模型**静止姿态的几何**烘焙的,不是按骨骼运行时变形后的形状。这是静态烘焙的固有性质,游戏里通常也是用胶囊/凸包代替。
需要碰撞跟随骨骼时,把 `collision_body` 改成 `RigidBody3D` / `CharacterBody3D`,并自己用骨骼驱动它。
**2. 模型重新导出后,碰撞体不会自动更新。**
碰撞体是你场景里的普通节点,Godot 不会因为模型变了就重建它。**换了模型要重新生成**:先「移除选中模型的碰撞」,再「给选中模型加碰撞」即可。
> 这一条正是 1.0 本地化方案想解决的问题,但它的代价(1–3× 存储 + 静默过期)不值得。原生 `-col` 方案没有这个问题,因为它把碰撞生成放在导入流程里 —— 这也是我建议优先考虑它的原因。
**3. 模型的世界尺寸太小,物理会不准。**
Godot 的物理按"1 单位 = 1 米"调参。如果模型导入后只有几厘米大(某些 FBX 会这样),碰撞虽然几何正确,但**碰撞检测会变得不可靠** —— 表现为穿透、抖动、站在物体上却下滑。
这不是碰撞体的问题,改形状类型也没用。正确做法是**把模型调制到真实比例**:在导入设置里调 `Scale`,或给模型的父节点设 `scale`,让它变成真实尺寸(一把椅子约 1 米、一个角色约 1.7 米)。
---
## 已知取舍
- **检测靠轮询**(0.4 秒一次)。Godot 没有暴露"模型被拖进来"的信号,这是唯一可靠的做法。
- **变换在注入前一次性快照**。所有网格的世界变换在**模型确实处于场景树中**时解析好,再交给构建函数。这有两个原因:刚体是**故意游离**的(由撤销系统负责挂载),而 `global_transform` 在树外会打印 `Condition "!is_inside_tree()" is true` 并返回单位阵;更重要的是,依赖"读取瞬间节点仍在树里"会让碰撞在边界情况下静默烘焙到原点。快照把这件事变成确定的,缺了变换的网格会被跳过而不是猜一个。
- **已验证的 API 兼容性**:本插件只使用在 Godot 4.7.2 上实测过的 API。踩过的坑包括 `MeshInstance3D.skeleton` 是 `NodePath` 而不是 `Skeleton3D`、`ClassDB.can_instantiate()` 才是判断节点能否容纳子节点的正确方法、以及 GDScript 不允许 `const` 字典的值引用其他常量(所以库里的枚举默认值写成字面量索引)。
---
## 工程结构
```
demo_project/
├─ addons/mesh_collider/ ← 插件本体(复制这个目录即可)
│ ├─ plugin.cfg
│ ├─ mesh_collider_plugin.gd 编辑器集成(检测 / 注入 / 撤销 / 停靠面板注册)
│ ├─ mesh_collider.gd 核心逻辑(纯逻辑,不依赖编辑器)
│ ├─ mesh_collider_settings.gd 项目设置 + 面板用的 label / tooltip 文本
│ ├─ mesh_collider_panel.gd 右侧 `MC` 标签页的设置面板
│ └─ README.md
├─ assets/models/ 测试模型(GLB + 带骨骼动画的 FBX)
└─ tests/ 辅助脚本
```
*Mesh Collider 2.0 · 面向 Godot 4.7.2*