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

12 KiB
Raw Blame History

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 打开,插件会跳过那些已经有碰撞的子树,避免重复。


脚本里直接用

核心库不依赖编辑器,运行时也能用:

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