Ship the Quaternius npc_casual pilot base: rig-agnostic pose sync, five tint slots, unwrapped UVs and wrist-strap camera
This commit is contained in:
1 parent
68f49253fe
commit
cd285b3a39
12 files changed
+242
-74
No files matched your search
@@ -141,8 +141,8 @@ func test_skinned_model_loads_and_keeps_the_head_contract() -> bool:
|
||||
"a skinned library character is used")
|
||||
var b := TestUtils.check(avatar.get_node_or_null("Rig/Head") != null,
|
||||
"Rig/Head survives the skinned path")
|
||||
var c := TestUtils.check(avatar._skeleton != null and avatar._bone_indices.has("Head")
|
||||
and avatar._bone_indices.has("HipL") and avatar._bone_indices.has("HandR"),
|
||||
var c := TestUtils.check(avatar._skeleton != null and avatar.bone_name("head") != ""
|
||||
and avatar.bone_name("hip", 0) != "" and avatar.bone_name("hand", 1) != "",
|
||||
"armature bones are bound")
|
||||
avatar.free()
|
||||
return a and b and c
|
||||
@@ -152,10 +152,10 @@ func test_skinned_model_loads_and_keeps_the_head_contract() -> bool:
|
||||
func test_pose_reaches_the_skeleton_bones() -> bool:
|
||||
var avatar := _avatar(Rules.NpcType.TOURIST, 55)
|
||||
var sk := avatar._skeleton
|
||||
var hip: int = int(avatar._bone_indices["HipL"])
|
||||
var knee: int = int(avatar._bone_indices["KneeL"])
|
||||
var rest_hip := sk.get_bone_rest(hip).basis.get_rotation_quaternion()
|
||||
var rest_knee := sk.get_bone_rest(knee).basis.get_rotation_quaternion()
|
||||
var hip: int = int(avatar._bone_indices[avatar.bone_name("hip", 0)])
|
||||
var knee: int = int(avatar._bone_indices[avatar.bone_name("knee", 0)])
|
||||
var rest_hip := sk.get_bone_pose_rotation(hip)
|
||||
var rest_knee := sk.get_bone_pose_rotation(knee)
|
||||
avatar.arrived = true
|
||||
avatar.npc.intent = "sit"
|
||||
for _i in 45:
|
||||
@@ -179,6 +179,49 @@ func test_pose_reaches_the_skeleton_bones() -> bool:
|
||||
avatar.free()
|
||||
return a and b and c and d
|
||||
|
||||
## Regression (ADR-0018): the shipped base must resolve every animation joint on
|
||||
## the rig it actually uses, keep the baked arm-down rest, and put each proxy on
|
||||
## the matching body side. The Quaternius rig labels its dot-suffixed joints with
|
||||
## the opposite side (its `L` sits at +x), so `RIG_BONES` is the single mapping and
|
||||
## a silent side swap would splay the arms across the torso.
|
||||
##
|
||||
## The swing itself is proven by the pose-capture gate; `_swing_bone()` writes the
|
||||
## global pose, which Godot only computes for a skeleton inside the scene tree and
|
||||
## the headless runner executes tests before the tree is ready.
|
||||
func test_shipped_rig_resolves_the_animation_joints() -> bool:
|
||||
var avatar := _avatar(Rules.NpcType.TOURIST, 91)
|
||||
var sk := avatar._skeleton
|
||||
var problems: Array[String] = []
|
||||
for joint in NpcAvatar.RIG_BONES:
|
||||
for side in 2:
|
||||
var bone := avatar.bone_name(joint, side)
|
||||
if bone == "" or not avatar._bone_indices.has(bone):
|
||||
problems.append("%s/%d unresolved" % [joint, side])
|
||||
var a := TestUtils.check(problems.is_empty(),
|
||||
"the shipped rig resolves every animation joint (%s)" % str(problems))
|
||||
var wrist := avatar.bone_name("hand", 0)
|
||||
var shoulder := avatar.bone_name("shoulder", 0)
|
||||
var b := TestUtils.check(wrist != "" and shoulder != "", "shoulder/hand joints resolve")
|
||||
if not b:
|
||||
avatar.free()
|
||||
return false
|
||||
var shoulder_rest := sk.get_bone_global_rest(int(avatar._bone_indices[shoulder]))
|
||||
var wrist_rest := sk.get_bone_global_rest(int(avatar._bone_indices[wrist]))
|
||||
var c := TestUtils.check(shoulder_rest.origin.y - wrist_rest.origin.y > 0.3,
|
||||
"the shipped base is baked arm-down (%.3f below the shoulder)"
|
||||
% (shoulder_rest.origin.y - wrist_rest.origin.y))
|
||||
# A straight-down arm axis is what makes an X swing read as forward/back.
|
||||
var arm_down := shoulder_rest.basis.y.normalized().dot(Vector3.DOWN)
|
||||
var d := TestUtils.check(arm_down > 0.95,
|
||||
"the arm rest axis points down (dot %.3f)" % arm_down)
|
||||
# Proxy side 0 must drive the -x arm and side 1 the +x arm on either rig.
|
||||
var left := sk.get_bone_global_rest(int(avatar._bone_indices[avatar.bone_name("shoulder", 0)]))
|
||||
var right := sk.get_bone_global_rest(int(avatar._bone_indices[avatar.bone_name("shoulder", 1)]))
|
||||
var e := TestUtils.check(left.origin.x < right.origin.x,
|
||||
"proxy side 0 drives the -x arm (%.3f vs %.3f)" % [left.origin.x, right.origin.x])
|
||||
avatar.free()
|
||||
return a and b and c and d and e
|
||||
|
||||
func test_character_selection_is_deterministic_and_complete() -> bool:
|
||||
# Seed 5 resolves each role to a fixed pool member; the routine crowd roles
|
||||
# now pick from the crowd-variety pools instead of a single npc_casual.
|
||||
|
||||
Reference in new issue
Block a user