fix: address shape mounting issues in StkRigAdapter
- Implement anisotropic scaling for shape mounting to prevent distortion. - Replace bounding-box midpoint anchors with joint-based anchors for correct rotation. - Reset node transforms to ensure clean scaling and rotation before mounting shapes. - Introduce new helper functions for computing bounding boxes, anchors, part lengths, and scales. - Neutralize driver rotations to maintain a consistent frame of reference during shape mounting. - Update documentation to reflect changes and provide detailed bugfix specifications.
This commit is contained in:
+140
-26
@@ -30,6 +30,11 @@ const PART_KEYS: PackedStringArray = [
|
||||
"right_upper_leg", "right_lower_leg",
|
||||
]
|
||||
|
||||
## Parts whose primary (bone-aligned) axis is horizontal (X).
|
||||
const X_AXIS_PARTS: PackedStringArray = [
|
||||
"left_upper_arm", "left_lower_arm", "right_upper_arm", "right_lower_arm",
|
||||
]
|
||||
|
||||
## Bone node paths (relative to rig root), keyed by part name.
|
||||
const BONE_PATHS: Dictionary = {
|
||||
"left_upper_arm": "Skeleton2D/Torso/LeftUpperArm",
|
||||
@@ -56,6 +61,24 @@ const BODY_PATHS: Dictionary = {
|
||||
"right_lower_leg": "Body/RightLowerLeg",
|
||||
}
|
||||
|
||||
## RemoteTransform2D driver node paths (relative to rig root), keyed by part
|
||||
## name. Each driver pushes its transform onto the matching Body/* visual node
|
||||
## (see master_rig.tscn). We neutralize their rotation so the Body/* nodes stay
|
||||
## in the clean unrotated frame the mount math assumes (position/scale pushes
|
||||
## are preserved).
|
||||
const DRIVER_PATHS: Dictionary = {
|
||||
"head": "Skeleton2D/Torso/Head/RemoteTransform2D",
|
||||
"torso": "Skeleton2D/Torso/RemoteTransform2D",
|
||||
"left_upper_arm": "Skeleton2D/Torso/LeftUpperArm/RemoteTransform2D",
|
||||
"left_lower_arm": "Skeleton2D/Torso/LeftUpperArm/LeftLowerArm/RemoteTransform2D",
|
||||
"right_upper_arm": "Skeleton2D/Torso/RightUpperArm/RemoteTransform2D",
|
||||
"right_lower_arm": "Skeleton2D/Torso/RightUpperArm/RightLowerArm/RemoteTransform2D",
|
||||
"left_upper_leg": "Skeleton2D/Torso/LeftUpperLeg/RemoteTransform2D",
|
||||
"left_lower_leg": "Skeleton2D/Torso/LeftUpperLeg/LeftLowerLeg/RemoteTransform2D",
|
||||
"right_upper_leg": "Skeleton2D/Torso/RightUpperLeg/RemoteTransform2D",
|
||||
"right_lower_leg": "Skeleton2D/Torso/RightUpperLeg/RightLowerLeg/RemoteTransform2D",
|
||||
}
|
||||
|
||||
const IK_LEFT_HAND := "IK_Targets/Left_Hand"
|
||||
const IK_RIGHT_HAND := "IK_Targets/Right_Hand"
|
||||
const IK_LEFT_LEG := "IK_Targets/Left_Leg"
|
||||
@@ -70,6 +93,7 @@ const HEAD_BONE_PATH := "Skeleton2D/Torso/Head"
|
||||
static func apply(stk_data: Dictionary, rig: Node2D) -> void:
|
||||
_fit_bones(stk_data, rig)
|
||||
_recalibrate_ik(stk_data, rig)
|
||||
_neutralize_driver_rotations(rig)
|
||||
_mount_shapes(stk_data, rig)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -159,6 +183,18 @@ static func _recalibrate_ik(stk_data: Dictionary, rig: Node2D) -> void:
|
||||
else:
|
||||
push_warning("StkRigAdapter: missing IK target '%s'." % IK_RIGHT_HAND)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Driver neutralization
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
static func _neutralize_driver_rotations(rig: Node2D) -> void:
|
||||
for part_name: String in DRIVER_PATHS:
|
||||
var driver := rig.get_node_or_null(NodePath(DRIVER_PATHS[part_name])) as RemoteTransform2D
|
||||
if driver == null:
|
||||
push_warning("StkRigAdapter: missing RemoteTransform2D driver '%s' for part '%s'; skipped." % [DRIVER_PATHS[part_name], part_name])
|
||||
continue
|
||||
driver.update_rotation = false
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Visual shape mount
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -177,25 +213,10 @@ static func _mount_shapes(stk_data: Dictionary, rig: Node2D) -> void:
|
||||
push_warning("StkRigAdapter: missing Body node for part '%s'; skipped." % part_name)
|
||||
continue
|
||||
|
||||
var shapes: Array = []
|
||||
var pivot := Vector2.ZERO
|
||||
var part_length := 0.0
|
||||
var part_data: Variant = body_parts.get(part_name, {})
|
||||
if part_data is Dictionary:
|
||||
var pd := part_data as Dictionary
|
||||
var shapes_var: Variant = pd.get("shapes", [])
|
||||
if shapes_var is Array:
|
||||
shapes = shapes_var as Array
|
||||
var pivot_var: Variant = pd.get("pivot", {})
|
||||
if pivot_var is Dictionary:
|
||||
var pv := pivot_var as Dictionary
|
||||
pivot = Vector2(float(pv.get("x", 0.0)), float(pv.get("y", 0.0)))
|
||||
part_length = float(pd.get("length", 0.0))
|
||||
|
||||
var bone_length := _bone_length_for(part_name, proportions)
|
||||
var scale_factor := 1.0
|
||||
if part_name != "head" and part_length > 0.0001:
|
||||
scale_factor = bone_length / part_length
|
||||
# Reset the node's authored transform so the mount math starts from a
|
||||
# clean unrotated, unit-scaled frame. Position is owned by the
|
||||
# RemoteTransform2D driver and is left untouched.
|
||||
_reset_node_transform(visual)
|
||||
|
||||
# Phase 9: the Body/Head node is a plain Node2D carrying an inline
|
||||
# @tool circle-drawing script; clear it so the head is mounted with the
|
||||
@@ -206,9 +227,28 @@ static func _mount_shapes(stk_data: Dictionary, rig: Node2D) -> void:
|
||||
_reset_own_geometry(visual)
|
||||
_clear_visual_children(visual)
|
||||
|
||||
var shapes: Array = []
|
||||
var part_data: Variant = body_parts.get(part_name, {})
|
||||
if part_data is Dictionary:
|
||||
var pd := part_data as Dictionary
|
||||
var shapes_var: Variant = pd.get("shapes", [])
|
||||
if shapes_var is Array:
|
||||
shapes = shapes_var as Array
|
||||
|
||||
# Recompute the joint anchor and part length from the shape bbox at
|
||||
# mount time (the file's pivot/length fields are write-only metadata
|
||||
# and are no longer trusted for anchoring/scaling).
|
||||
var bbox := _compute_part_bbox(shapes)
|
||||
if float(bbox["min_x"]) > float(bbox["max_x"]) or float(bbox["min_y"]) > float(bbox["max_y"]):
|
||||
continue
|
||||
var anchor := _compute_anchor(bbox, part_name)
|
||||
var part_length := _compute_part_length(bbox, part_name)
|
||||
var bone_length := _bone_length_for(part_name, proportions)
|
||||
var scale := _compute_scale(part_name, part_length, bone_length)
|
||||
|
||||
for shape in shapes:
|
||||
if shape is Dictionary:
|
||||
_mount_shape(visual, shape as Dictionary, pivot, scale_factor)
|
||||
_mount_shape(visual, shape as Dictionary, anchor, scale)
|
||||
|
||||
|
||||
static func _bone_length_for(part_name: String, proportions: Dictionary) -> float:
|
||||
@@ -227,8 +267,81 @@ static func _bone_length_for(part_name: String, proportions: Dictionary) -> floa
|
||||
return 1.0
|
||||
|
||||
|
||||
static func _mount_shape(visual: Node, shape: Dictionary, pivot: Vector2, scale_factor: float) -> void:
|
||||
var pts := _transform_points(shape.get("points", []), pivot, scale_factor)
|
||||
static func _compute_part_bbox(shapes: Array) -> Dictionary:
|
||||
var min_x := INF
|
||||
var min_y := INF
|
||||
var max_x := -INF
|
||||
var max_y := -INF
|
||||
for shape in shapes:
|
||||
if not shape is Dictionary:
|
||||
continue
|
||||
var pts_var: Variant = (shape as Dictionary).get("points", [])
|
||||
if pts_var is Array:
|
||||
for p in pts_var as Array:
|
||||
if p is Dictionary:
|
||||
var d := p as Dictionary
|
||||
var pt := Vector2(float(d.get("x", 0.0)), float(d.get("y", 0.0)))
|
||||
min_x = minf(min_x, pt.x)
|
||||
min_y = minf(min_y, pt.y)
|
||||
max_x = maxf(max_x, pt.x)
|
||||
max_y = maxf(max_y, pt.y)
|
||||
elif p is Vector2:
|
||||
var pt := p as Vector2
|
||||
min_x = minf(min_x, pt.x)
|
||||
min_y = minf(min_y, pt.y)
|
||||
max_x = maxf(max_x, pt.x)
|
||||
max_y = maxf(max_y, pt.y)
|
||||
elif pts_var is PackedVector2Array:
|
||||
for pt in pts_var as PackedVector2Array:
|
||||
min_x = minf(min_x, pt.x)
|
||||
min_y = minf(min_y, pt.y)
|
||||
max_x = maxf(max_x, pt.x)
|
||||
max_y = maxf(max_y, pt.y)
|
||||
return {
|
||||
"min_x": min_x,
|
||||
"min_y": min_y,
|
||||
"max_x": max_x,
|
||||
"max_y": max_y,
|
||||
}
|
||||
|
||||
|
||||
static func _compute_anchor(bbox: Dictionary, part_name: String) -> Vector2:
|
||||
var cx := (float(bbox["min_x"]) + float(bbox["max_x"])) * 0.5
|
||||
var cy := (float(bbox["min_y"]) + float(bbox["max_y"])) * 0.5
|
||||
match part_name:
|
||||
"head":
|
||||
return Vector2(cx, float(bbox["max_y"])) # neck base
|
||||
"left_upper_arm", "left_lower_arm":
|
||||
return Vector2(float(bbox["max_x"]), cy) # shoulder at right end
|
||||
"right_upper_arm", "right_lower_arm":
|
||||
return Vector2(float(bbox["min_x"]), cy) # shoulder at left end
|
||||
_: # torso + all legs
|
||||
return Vector2(cx, float(bbox["min_y"])) # hip/neck top-center
|
||||
|
||||
|
||||
static func _compute_part_length(bbox: Dictionary, part_name: String) -> float:
|
||||
if X_AXIS_PARTS.has(part_name):
|
||||
return float(bbox["max_x"]) - float(bbox["min_x"])
|
||||
return float(bbox["max_y"]) - float(bbox["min_y"])
|
||||
|
||||
|
||||
static func _compute_scale(part_name: String, part_length: float, bone_length: float) -> Vector2:
|
||||
var primary := 1.0
|
||||
if part_name != "head" and part_length > 0.0001:
|
||||
primary = bone_length / part_length
|
||||
if X_AXIS_PARTS.has(part_name):
|
||||
return Vector2(primary, 1.0) # arms: X is primary
|
||||
return Vector2(1.0, primary) # legs/torso/head: Y is primary (head → (1.0, 1.0))
|
||||
|
||||
|
||||
static func _reset_node_transform(visual: Node) -> void:
|
||||
if visual is Node2D:
|
||||
(visual as Node2D).scale = Vector2.ONE
|
||||
(visual as Node2D).rotation = 0.0
|
||||
|
||||
|
||||
static func _mount_shape(visual: Node, shape: Dictionary, anchor: Vector2, scale: Vector2) -> void:
|
||||
var pts := _transform_points(shape.get("points", []), anchor, scale)
|
||||
if pts.size() < 2:
|
||||
return
|
||||
|
||||
@@ -255,19 +368,20 @@ static func _mount_shape(visual: Node, shape: Dictionary, pivot: Vector2, scale_
|
||||
visual.add_child(line)
|
||||
|
||||
|
||||
static func _transform_points(pts_var: Variant, pivot: Vector2, scale_factor: float) -> PackedVector2Array:
|
||||
static func _transform_points(pts_var: Variant, anchor: Vector2, scale: Vector2) -> PackedVector2Array:
|
||||
var out := PackedVector2Array()
|
||||
if pts_var is Array:
|
||||
for p in pts_var as Array:
|
||||
if p is Dictionary:
|
||||
var d := p as Dictionary
|
||||
var pt := Vector2(float(d.get("x", 0.0)), float(d.get("y", 0.0)))
|
||||
out.append((pt - pivot) * scale_factor)
|
||||
out.append(Vector2((pt.x - anchor.x) * scale.x, (pt.y - anchor.y) * scale.y))
|
||||
elif p is Vector2:
|
||||
out.append(((p as Vector2) - pivot) * scale_factor)
|
||||
var pt := p as Vector2
|
||||
out.append(Vector2((pt.x - anchor.x) * scale.x, (pt.y - anchor.y) * scale.y))
|
||||
elif pts_var is PackedVector2Array:
|
||||
for pt in pts_var as PackedVector2Array:
|
||||
out.append((pt - pivot) * scale_factor)
|
||||
out.append(Vector2((pt.x - anchor.x) * scale.x, (pt.y - anchor.y) * scale.y))
|
||||
return out
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user