更新自研插件

This commit is contained in:
2026-10-04 17:19:42 +08:00
parent dfd8636476
commit b99952d3c9
4 changed files with 189 additions and 30 deletions
+102 -20
View File
@@ -67,6 +67,32 @@ const BODY_TYPE_NAMES: PackedStringArray = [
"static", "rigid", "area", "character",
]
## Resolve a requested shape/body pair into one that actually works.
##
## A `ConcavePolygonShape3D` (Trimesh) is a static-only shape: Godot will not
## drive a moving RigidBody3D / CharacterBody3D from one, and the failure is
## silent. Rather than let that through, fall back to a convex hull and say so.
## Returns `{ shape, body, changed, reason }`.
static func resolve_shape_and_body(requested_shape: ShapeType,
requested_body: BodyType) -> Dictionary:
var out := {
"shape": requested_shape,
"body": requested_body,
"changed": false,
"reason": "",
}
if requested_shape != ShapeType.TRIMESH:
return out
if requested_body == BodyType.STATIC or requested_body == BodyType.AREA:
return out
out["shape"] = ShapeType.CONVEX_HULL
out["changed"] = true
out["reason"] = ("Trimesh collision is static-only, so %s cannot use it; "
+ "fell back to Convex Hull. Pick a convex shape type for moving bodies, "
+ "or use StaticBody3D.") % BODY_TYPE_NAMES[requested_body]
return out
# ---------------------------------------------------------------------------
# Path helpers
# ---------------------------------------------------------------------------
@@ -106,6 +132,32 @@ static func is_model_instance(node: Node) -> bool:
# Collision generation
# ---------------------------------------------------------------------------
## The transform that maps a mesh's vertices into the space of `body_global`.
##
## Resolved from `body_global` (a plain Transform3D) rather than by reading
## `body.global_transform` off the body node, because the body is deliberately
## detached while its shapes are baked - and reading `global_transform` on a node
## outside the tree makes Godot log `Condition "!is_inside_tree()" is true` and
## return an identity transform.
static func mesh_space_in(body_global: Transform3D, mi: MeshInstance3D) -> Transform3D:
return body_global.affine_inverse() * mi.global_transform
## Build the `mesh_spaces` map consumed by `build_collision_body`.
##
## MUST be called while `model` is inside the scene tree: global transforms do
## not resolve otherwise, and every shape would silently end up baked at the
## origin. Keys are mesh instance ids.
static func collect_mesh_spaces(model: Node, body_global: Transform3D,
exclude_existing_bodies: bool = false) -> Dictionary:
var meshes: Array[MeshInstance3D] = []
_collect_meshes(model, meshes, exclude_existing_bodies)
var spaces: Dictionary = {}
for mi in meshes:
spaces[mi.get_instance_id()] = mesh_space_in(body_global, mi)
return spaces
## Build a physics body with baked collision shapes for `model`, and return it
## DETACHED, or `null` if there was nothing to add.
##
@@ -113,10 +165,14 @@ static func is_model_instance(node: Node) -> bool:
## tree (the editor plugin does so through the undo system, so an injection can
## be reverted with Ctrl+Z). Intended to be added as a child of `model`.
##
## `model` must be inside the scene tree: global transforms do not resolve on a
## detached node, which would silently bake every shape at the origin. The body
## itself is deliberately never parented here, because its own transform stays
## at identity and the shapes are already baked into that space.
## `body_global` is the transform the body will end up with once parented - for
## the usual "body as a child of the model instance" case that is simply
## `model.global_transform`. Shapes are baked relative to it, so the body's own
## transform stays at identity.
##
## `mesh_spaces` must come from `collect_mesh_spaces`, gathered while the model
## was still in the tree. A mesh missing from it is skipped rather than guessed
## at, because a wrong transform is worse than a missing shape.
##
## `exclude_existing_bodies` skips meshes that already sit under a physics body.
## Turn it on for models that ship their own collision (Godot's `-col` suffixes).
@@ -126,8 +182,13 @@ static func is_model_instance(node: Node) -> bool:
## constant expressions, and a qualified enum member is not reliably accepted -
## the same restriction that rules out `const` dictionaries referencing other
## constants. Keep these literals in step with the enum order above.
##
## `shape_type` is subject to `resolve_shape_and_body` and may be corrected (with
## a warning) if the requested combination cannot work.
static func build_collision_body(
model: Node,
mesh_spaces: Dictionary,
body_global: Transform3D = Transform3D.IDENTITY,
shape_type: ShapeType = 0, # ShapeType.CONVEX_HULL
body_type: BodyType = 0, # BodyType.STATIC
exclude_existing_bodies: bool = false,
@@ -139,10 +200,15 @@ static func build_collision_body(
return null
if _find_added_body(model) != null:
return null # already has collision from this library
if not model.is_inside_tree():
push_warning("[MeshCollider] %s is not inside the scene tree; cannot bake "
% model.name + "collision without resolved global transforms.")
return null
# Enforce the static-only rule for Trimesh here, at the single choke point, so
# neither the plugin nor a script caller can accidentally ask for a concave
# shape on a moving body.
var resolved := resolve_shape_and_body(shape_type, body_type)
if resolved["changed"]:
push_warning("[MeshCollider] " + String(resolved["reason"]))
shape_type = resolved["shape"]
body_type = resolved["body"]
var meshes: Array[MeshInstance3D] = []
_collect_meshes(model, meshes, exclude_existing_bodies)
@@ -162,7 +228,11 @@ static func build_collision_body(
var added := 0
for mi in meshes:
var shape := build_shape_for_mesh(mi, body as Node3D, shape_type)
var key := mi.get_instance_id()
if not mesh_spaces.has(key):
continue # no resolved transform for this mesh; don't guess
var to_local: Transform3D = mesh_spaces[key]
var shape := build_shape_for_mesh(mi, to_local, shape_type)
if shape == null:
continue
var cs := CollisionShape3D.new()
@@ -211,8 +281,20 @@ static func add_collision(
exclude_existing_bodies: bool = false,
collision_layer: int = 1,
collision_mask: int = 1) -> Node:
var body := build_collision_body(model, shape_type, body_type,
exclude_existing_bodies, collision_layer, collision_mask)
if model == null or not is_instance_valid(model):
return null
if not (model is Node3D):
return null
if not model.is_inside_tree():
push_warning("[MeshCollider] %s is not inside the scene tree; collision "
% model.name + "cannot be baked without resolved global transforms.")
return null
# Resolve every mesh transform up front, while tree membership is certain.
var model_xform: Transform3D = (model as Node3D).global_transform
var spaces := collect_mesh_spaces(model, model_xform, exclude_existing_bodies)
var body := build_collision_body(model, spaces, model_xform, shape_type,
body_type, exclude_existing_bodies, collision_layer, collision_mask)
if body == null:
return null
model.add_child(body, true)
@@ -325,24 +407,24 @@ static func _unique_child_name(parent: Node, wanted: String) -> String:
# Shape construction
# ---------------------------------------------------------------------------
## Build one shape resource for `mi`, baked into the coordinate space of
## `body`.
## Build one shape resource for `mi`, baked into the space described by
## `to_local`.
##
## `to_local` comes from `mesh_space_in`, resolved while the model was known to
## be inside the scene tree. Passing it in - rather than reading
## `mi.global_transform` here - is what keeps this function from depending on
## tree membership at bake time.
##
## Returns null when no shape can be produced. The `shape_type` default is a
## literal enum index for the reason given on `build_collision_body`.
static func build_shape_for_mesh(mi: MeshInstance3D, body: Node3D,
static func build_shape_for_mesh(mi: MeshInstance3D, to_local: Transform3D,
shape_type: ShapeType = 0) -> Shape3D: # ShapeType.CONVEX_HULL
if mi == null or body == null:
if mi == null:
return null
var mesh := mi.mesh
if mesh == null or mesh.get_surface_count() == 0:
return null
# Bake into the body's space. `global_transform` is the anchor even for a
# skinned mesh: the mesh's own node transform carries the armature scale and
# offset, which is where the geometry is actually drawn.
var to_local := body.global_transform.affine_inverse() * mi.global_transform
match shape_type:
ShapeType.TRIMESH:
return _trimesh_shape(mi, to_local)