233 lines
13 KiB
Markdown
233 lines
13 KiB
Markdown
# 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)
|
||
```
|
||
|
||
这个布局与 **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*
|