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
+70
-37
@@ -418,33 +418,36 @@ func _build_skinned_body(npc_type: int) -> bool:
|
||||
_head.name = "Head"
|
||||
_head.position = _rest_origin(["Head"])
|
||||
_rig.add_child(_head)
|
||||
for side in [-1.0, 1.0]:
|
||||
var suffix := "L" if side < 0.0 else "R"
|
||||
# The Quaternius rig labels its joints with the opposite side (its "L" sits
|
||||
# at +x while the project's "L" sits at −x), so the dot-suffixed names are
|
||||
# looked up with a swapped suffix.
|
||||
var qsuffix := "R" if side < 0.0 else "L"
|
||||
for i in 2:
|
||||
var suffix := "L" if i == 0 else "R"
|
||||
# The proxy pivots sit on the resolved rig bones (see RIG_BONES), so the
|
||||
# proxy chirality and the exported joint naming never have to agree.
|
||||
var hip_bone := bone_name("hip", i)
|
||||
var knee_bone := bone_name("knee", i)
|
||||
var shoulder_bone := bone_name("shoulder", i)
|
||||
var elbow_bone := bone_name("elbow", i)
|
||||
var hand_bone := bone_name("hand", i)
|
||||
var hip := Node3D.new()
|
||||
hip.name = "Hip" + suffix
|
||||
hip.position = _rest_origin(["Hip" + suffix, "UpperLeg." + qsuffix])
|
||||
hip.position = _rest_origin([hip_bone])
|
||||
_rig.add_child(hip)
|
||||
var knee := Node3D.new()
|
||||
knee.name = "Knee"
|
||||
knee.position = _rest_origin(["Knee" + suffix, "LowerLeg." + qsuffix]) - hip.position
|
||||
knee.position = _rest_origin([knee_bone]) - hip.position
|
||||
hip.add_child(knee)
|
||||
_legs.append(hip)
|
||||
_knees.append(knee)
|
||||
var shoulder := Node3D.new()
|
||||
shoulder.name = "Shoulder" + suffix
|
||||
shoulder.position = _rest_origin(["Shoulder" + suffix, "UpperArm." + qsuffix])
|
||||
shoulder.position = _rest_origin([shoulder_bone])
|
||||
_rig.add_child(shoulder)
|
||||
var elbow := Node3D.new()
|
||||
elbow.name = "Elbow"
|
||||
elbow.position = _rest_origin(["Elbow" + suffix, "LowerArm." + qsuffix]) - shoulder.position
|
||||
elbow.position = _rest_origin([elbow_bone]) - shoulder.position
|
||||
shoulder.add_child(elbow)
|
||||
var hand := Node3D.new()
|
||||
hand.name = "Hand"
|
||||
hand.position = _rest_origin(["Hand" + suffix, "Wrist." + qsuffix]) - _rest_origin(["Elbow" + suffix, "LowerArm." + qsuffix])
|
||||
hand.position = _rest_origin([hand_bone]) - _rest_origin([elbow_bone])
|
||||
elbow.add_child(hand)
|
||||
_arms.append(shoulder)
|
||||
_elbows.append(elbow)
|
||||
@@ -509,44 +512,74 @@ func _rest_origin(names: Array) -> Vector3:
|
||||
return _skeleton.get_bone_global_rest(_bone_indices[bone_name]).origin
|
||||
return Vector3.ZERO
|
||||
|
||||
## Bone-name candidates per animation joint, in proxy order: index 0 is the
|
||||
## avatar's left (-x) and index 1 its right (+x). The Quaternius rig labels its
|
||||
## dot-suffixed joints with the opposite side (its `L` sits at +x), so the pairs
|
||||
## carry the rig-specific mapping and nothing else has to know about it.
|
||||
const RIG_BONES := {
|
||||
"head": [["Head"], ["Head"]],
|
||||
"hip": [["UpperLeg.R", "HipL"], ["UpperLeg.L", "HipR"]],
|
||||
"knee": [["LowerLeg.R", "KneeL"], ["LowerLeg.L", "KneeR"]],
|
||||
"shoulder": [["UpperArm.R", "ShoulderL"], ["UpperArm.L", "ShoulderR"]],
|
||||
"elbow": [["LowerArm.R", "ElbowL"], ["LowerArm.L", "ElbowR"]],
|
||||
"hand": [["Wrist.R", "HandL"], ["Wrist.L", "HandR"]],
|
||||
}
|
||||
|
||||
## Resolved skeleton bone name for an animation joint on one side. Returns an
|
||||
## empty string when the rig has neither candidate, so both the Quaternius
|
||||
## 62-joint rig and the 13-core fallback resolve through the same call.
|
||||
func bone_name(joint: String, side: int = 0) -> String:
|
||||
if not RIG_BONES.has(joint):
|
||||
return ""
|
||||
var pair: Array = RIG_BONES[joint]
|
||||
return _bone_of(pair[0], pair[1], side)
|
||||
|
||||
## Copy the proxy pivot rotations onto armature bones. Limb bones point down, so
|
||||
## an X rotation swings them forward/back; the Head bone points up, so a Y
|
||||
## rotation is the look yaw. The Quaternius rig uses dot-suffixed names
|
||||
## (UpperLeg.L, UpperArm.L, ...); the old 13-core rig uses HipL/ShoulderL, so
|
||||
## both are tried and the first one present in the skeleton wins.
|
||||
## rotation is the look yaw.
|
||||
func _sync_skeleton() -> void:
|
||||
if _skeleton == null or _bone_indices.is_empty():
|
||||
return
|
||||
for i in 2:
|
||||
_pose_bone_any(["UpperLeg.R", "HipL"], ["UpperLeg.L", "HipR"], i, _legs[i].rotation.x)
|
||||
for i in 2:
|
||||
_pose_bone_any(["LowerLeg.R", "KneeL"], ["LowerLeg.L", "KneeR"], i, _knees[i].rotation.x)
|
||||
for i in 2:
|
||||
_pose_bone_any(["UpperArm.R", "ShoulderL"], ["UpperArm.L", "ShoulderR"], i, _arms[i].rotation.x, Vector3.LEFT)
|
||||
for i in 2:
|
||||
_pose_bone_any(["LowerArm.R", "ElbowL"], ["LowerArm.L", "ElbowR"], i, _elbows[i].rotation.x, Vector3.LEFT)
|
||||
var hip_swing := Quaternion(Vector3.RIGHT, _legs[i].rotation.x)
|
||||
_swing_bone(bone_name("hip", i), hip_swing)
|
||||
# `_swing_bone` writes the absolute global rotation, so a child bone has to
|
||||
# receive the swing it inherits from its parent as well as its own.
|
||||
_swing_bone(bone_name("knee", i), hip_swing * Quaternion(Vector3.RIGHT, _knees[i].rotation.x))
|
||||
var shoulder_swing := Quaternion(Vector3.LEFT, _arms[i].rotation.x)
|
||||
_swing_bone(bone_name("shoulder", i), shoulder_swing)
|
||||
_swing_bone(bone_name("elbow", i), shoulder_swing * Quaternion(Vector3.LEFT, _elbows[i].rotation.x))
|
||||
if _head != null:
|
||||
_pose_bone("Head", _head.rotation.y, Vector3.UP)
|
||||
var head := bone_name("head")
|
||||
if head != "":
|
||||
_swing_bone(head, Quaternion(Vector3.UP, _head.rotation.y))
|
||||
|
||||
func _pose_bone_any(left: Array, right: Array, index: int, angle: float, axis: Vector3 = Vector3.RIGHT) -> void:
|
||||
var names: Array = left if index == 0 else right
|
||||
for bone_name in names:
|
||||
## First bone present from the left list (index 0) or the right list (index 1).
|
||||
func _bone_of(left: Array, right: Array, index: int) -> String:
|
||||
for bone_name in (left if index == 0 else right):
|
||||
if _bone_indices.has(bone_name):
|
||||
_pose_bone(bone_name, angle, axis)
|
||||
return
|
||||
return str(bone_name)
|
||||
return ""
|
||||
|
||||
func _pose_bone(bone_name: String, angle: float, axis: Vector3) -> void:
|
||||
if not _bone_indices.has(bone_name):
|
||||
## Swing a bone about a world axis through its head, keeping its rest orientation
|
||||
## as the neutral. Writing the *global* pose is rig-agnostic: it does not depend
|
||||
## on how the armature pack exported its rest rolls or on the pose/rest split the
|
||||
## glTF importer chose, both of which differ between the 62-joint Quaternius rig
|
||||
## and the 13-core fallback. Measuring `get_bone_global_pose()` after the call is
|
||||
## the only reliable check (see the pose-capture gate).
|
||||
##
|
||||
## `rest` must be the **global** rest. `get_bone_rest()` is parent-relative and
|
||||
## already carries the importer's axis conversion, so conjugating with it swung
|
||||
## the limbs about the wrong axis and splayed the arms.
|
||||
func _swing_bone(bone_name: String, swing: Quaternion) -> void:
|
||||
if bone_name == "":
|
||||
return
|
||||
var index: int = _bone_indices[bone_name]
|
||||
# The pose is rest-relative (global = parent * rest * pose), so the proxy's
|
||||
# world-space swing axis must be expressed in the bone's rest-local frame.
|
||||
# `rest * Quaternion(rest.inverse() * axis, angle)` swings around the world
|
||||
# `axis`; for the old rig (rest = 180 deg) this reduces to the legacy
|
||||
# `rest * Quaternion(axis, angle)`, so the 13-core fallback is unchanged.
|
||||
var rest := _skeleton.get_bone_rest(index).basis.get_rotation_quaternion()
|
||||
var local_axis := rest.inverse() * axis
|
||||
_skeleton.set_bone_pose_rotation(index, rest * Quaternion(local_axis, angle))
|
||||
_skeleton.reset_bone_pose(index)
|
||||
var rest := _skeleton.get_bone_global_rest(index).basis.get_rotation_quaternion()
|
||||
var current := _skeleton.get_bone_global_pose(index)
|
||||
var target := Basis((swing * rest).normalized()).scaled(current.basis.get_scale())
|
||||
_skeleton.set_bone_global_pose(index, Transform3D(target, current.origin))
|
||||
|
||||
func _build_body(npc_type: int) -> void:
|
||||
_rig = Node3D.new()
|
||||
|
||||
Reference in new issue
Block a user