更新自研插件
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user