Files
agame/addons/mesh_collider/README.md
T
2026-10-04 19:46:30 +08:00

241 lines
14 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**。
---
## 用法
**拖进去就行。** 把 FBX / GLB 拖进场景,插件会弹窗问一句要不要加碰撞:
| 弹窗里的选择 | 结果 |
| --- | --- |
| **添加碰撞** | 给这一批模型加上碰撞体,走撤销系统 —— **Ctrl+Z 可以撤销,Ctrl+Shift+Z 可以重做** |
| **不添加** | 什么都不做,模型留在场景里 |
| 勾上**不再询问**再选 | 记住这次的选择,以后不再弹窗(想恢复询问,去 项目设置 → mesh_collider 打开 `ask_before_adding`) |
一次拖入多个模型只弹**一个**弹窗,确认后整批处理。不想被问就把 `ask_before_adding` 关掉,改由 `auto_add_collision` 决定。
加完碰撞的模型会被自动**编组**:视图里点它的任何子节点,选中的都是模型本身(见下面「生成的节点结构」)。
菜单里还有四个手动操作:
| 菜单项 | 作用 |
| --- | --- |
| 扫描场景中的模型 | 列出场景里所有还没加碰撞的模型(`auto_add_collision` 打开时直接加) |
| 给选中模型加碰撞 | 对当前选中的模型实例执行一次 |
| 移除选中模型的碰撞 | 移除本插件加的那个碰撞体(同样可撤销) |
| 打开项目设置 | 跳到项目设置页 |
---
## 生成的节点结构
```
Chair_PC_04 ← 模型实例,仍然引用 res://.../chair.glb
└─ CollisionBody (StaticBody3D) ← 本插件加的,属于你的场景
└─ Chair_PC_04Shape (CollisionShape3D)
```
加碰撞的同时,插件会给**模型实例**打上编辑器自己的「编组」标记(在场景面板点 Ctrl+G 是同一个东西,存成 `metadata/_edit_group_ = true`)。效果是:**在 2D / 3D 视图里点这个模型的任何子节点,选中的都是模型本身**,而不是 `CollisionBody` 或某个 `CollisionShape3D`。
- 这个标记会写进 `.tscn`,关掉编辑器再打开依然有效;
- 只在模型**还没有**这个标记时才打 —— 你自己手工编组过的不会被覆盖,撤销一次注入也不会把你自己编的组撤掉;
- 「移除选中模型的碰撞」**不会**取消编组(编组对模型本身是有益的,而且可能本来就是你加的)。
这个布局与 **Godot 自己**用 `-col` / `-convcol` 名字后缀导入时生成的完全一致。我实测过每种后缀的行为:
| 后缀 | Godot 生成的形状 |
| --- | --- |
| `-col` / `_col` | 网格下挂 `StaticBody3D` → `ConcavePolygonShape3D`(三角网格) |
| `-convcol` / `_convcol` | 同上,形状换成 `ConvexPolygonShape3D`(凸包) |
| `-colonly` / `-convcolonly` | 网格同级生成 `@StaticBody3D@<id>`,网格隐藏 |
保持一致是有意的:两种机制产出的东西对你后续编辑而言是同一种东西,可以混用、可以互相替换。
### 那为什么不直接用 `-col`?
`-col` **更省事,建议先用它** —— 能用就别引插件。插件的价值在这几种情况:
- 要**按实例**调整碰撞(不同实例用不同形状/层);
- 模型是买来的成品,不想改它的名字或导入设置;
- 只想给**场景里某一个**实例加碰撞,而不是所有用这个模型的地方;
- 想在编辑器里直接看到、选中、手动微调这些碰撞节点。
---
## 设置
设置只有**一个入口**:**项目设置 → mesh_collider**(菜单里的『打开项目设置』会直接跳过去)。分节名显示为 **Mesh Collider**,每一项的名字由 Godot 按 key 自动美化:
```
Ask Before Adding [✓ 启用]
Auto Add Collision [ 启用]
Collision Shape [Trimesh ▾]
Collision Body [StaticBody3D ▾]
Skip Meshes with Collision [ 启用]
碰撞层 [1][2][3][4] … [32] ← Godot 原生的层勾选网格
碰撞遮罩 [1][2][3][4] … [32]
Model Extensions fbx,glb,gltf,…
```
- 改动**立即写入**,插件下一次扫描就读到,没有"应用"这一步;
- 保存交给编辑器(退出或保存项目时落盘)—— 唯一的例外是下面那个"不再询问";
- **没有逐项说明**:项目设置的每一行只有名字和控件,Godot 没给插件留"显示名 / 描述"的位置。每项的含义看下面这张表。
> 早期版本还有一个右列停靠面板(`MC` 标签页),只是把同一批键换了种画法,现已删掉:两处 UI 意味着两处要维护的布局问题,而设置本来就该在一个地方看。顺带一提,项目设置里 `collision_layer` / `collision_mask` 用的是 Godot 原生的 32 层勾选网格,比面板里的数字输入框好用。
| 设置 | 默认 | 说明 |
| --- | --- | --- |
| `ask_before_adding` | **`true`** | 拖入模型时**弹窗询问**是否添加碰撞 |
| `auto_add_collision` | `false` | 不询问时直接添加;仅在 `ask_before_adding` 关闭后生效 |
| `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 …` | 识别为模型的后缀 |
### 拖入模型时的三种行为
默认**不静默改动你的场景**,而是弹窗询问:
| `ask_before_adding` | `auto_add_collision` | 拖入模型时 |
| --- | --- | --- |
| **`true`**(默认) | 忽略 | **弹窗询问**,带「不再询问」勾选框 |
| `false` | `true` | 直接添加碰撞,不问 |
| `false` | `false` | 什么都不做,只能从 项目 > 工具 手动执行 |
弹窗里的「不再询问」按你本次的选择记忆:选「添加碰撞」→ 以后直接添加;选「不添加」→ 以后不再添加也不再询问。两种情况都会把 `ask_before_adding` 关掉,想恢复询问就去 **项目设置 → mesh_collider** 把它重新打开。
> 这一项是唯一会**主动落盘**的设置(`ProjectSettings.save()`),因为"不再询问"必须活过这次编辑器会话。其余设置不主动落盘,交给编辑器自己保存。
### 形状怎么选
| 形状 | 精度 | 代价 |
| --- | --- | --- |
| **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 没有暴露"模型被拖进来"的信号 —— 编辑器自己在内部处理这次 drop,不通知任何人 —— 所以只能盯着场景。为了不重蹈"每帧扫全场景"的覆辙,轮询有两道闸:先用 `SceneTree.get_node_count()`(O(1))判断整棵树有没有增删,**没变就直接跳过扫描**;变了也只扫当前编辑的场景。编辑器空转时这个定时器不做任何事。
- **基线不等于"新拖入"**。切换场景时,场景里已有的模型会被记进"已知"集合,不弹窗 —— 否则每开一个场景都会被问一遍。代价是:在场景刚打开后的 0.4 秒内拖入的模型会被当成基线,不弹窗(用菜单手动处理即可)。
- **撤销/重做会当成新拖入**。Ctrl+Z 删掉模型再 Ctrl+Shift+Z 放回来,节点实例 id 变了,插件会再问一次。功能上没错,只是多问一句。
- **变换在注入前一次性快照**。所有网格的世界变换在**模型确实处于场景树中**时解析好,再交给构建函数。这有两个原因:刚体是**故意游离**的(由撤销系统负责挂载),而 `global_transform` 在树外会打印 `Condition "!is_inside_tree()" is true` 并返回单位阵;更重要的是,依赖"读取瞬间节点仍在树里"会让碰撞在边界情况下静默烘焙到原点。快照把这件事变成确定的,缺了变换的网格会被跳过而不是猜一个。
- **已验证的 API 兼容性**:本插件只使用在 Godot 4.7.2 上实测过的 API。踩过的坑包括 `MeshInstance3D.skeleton` 是 `NodePath` 而不是 `Skeleton3D`、`ClassDB.can_instantiate()` 才是判断节点能否容纳子节点的正确方法、GDScript 不允许 `const` 字典的值引用其他常量(所以库里的枚举默认值写成字面量索引)、以及 `AcceptDialog.add_child()` 会把子控件**铺满**内容区而不是排在正文下面(所以弹窗的正文和勾选框要自己装进一个 `VBoxContainer`)。
---
## 工程结构
```
demo_project/
├─ addons/mesh_collider/ ← 插件本体(复制这个目录即可)
│ ├─ plugin.cfg
│ ├─ mesh_collider_plugin.gd 编辑器集成(检测 / 拖入弹窗 / 注入 / 撤销)
│ ├─ mesh_collider.gd 核心逻辑(纯逻辑,不依赖编辑器)
│ ├─ mesh_collider_settings.gd 项目设置 + 每项的 label / tooltip 文本
│ └─ README.md
├─ assets/models/ 测试模型(GLB + 带骨骼动画的 FBX)
└─ tests/ 辅助脚本
```
*Mesh Collider 2.0 · 面向 Godot 4.7.2*