Files

123 lines
6.5 KiB
GDScript

class_name OperatorPose
extends RefCounted
# Shared immutable clips, per-instance skeleton and blend state. CharacterBody3D
# remains authoritative: animation root motion must never move a network actor.
static var library: AnimationLibrary
var skeleton: Skeleton3D
var rotations: Array[Quaternion] = []
var rest_rotations: Array[Quaternion] = []
var rest_positions: Array[Vector3] = []
var upper_body: PackedByteArray = []
var track_cache: Dictionary = {}
var global_poses: Array[Transform3D] = []
func setup(rig: Skeleton3D) -> void:
skeleton = rig
if library == null:
library = load("res://assets/animations/operators.res") as AnimationLibrary
for bone in rig.get_bone_count():
var rest := rig.get_bone_rest(bone)
rest_positions.append(rest.origin)
rest_rotations.append(rest.basis.get_rotation_quaternion())
rotations.append(rest.basis.get_rotation_quaternion())
global_poses.append(Transform3D.IDENTITY)
var bone_name := str(rig.get_bone_name(bone))
upper_body.append(0 if bone_name.contains("Hips") or bone_name.contains("Leg") or bone_name.contains("Foot") or bone_name.contains("Toe") else 1)
func clip(key: String) -> Animation:
return library.get_animation(key) if library.has_animation(key) else library.get_animation("pro_rifle__idle")
func tracks(key: String) -> PackedInt32Array:
if track_cache.has(key): return track_cache[key]
var result := PackedInt32Array()
var animation := clip(key)
for track in animation.get_track_count():
var path := animation.track_get_path(track)
result.append(skeleton.find_bone(path.get_subname(0)) if path.get_subname_count() > 0 else -1)
track_cache[key] = result
return result
func sample(key: String, time: float, looping: bool, upper_only: bool, weight: float, positions: Array[Vector3]) -> void:
var animation := clip(key)
var stamp := fposmod(time, animation.length) if looping else clampf(time, 0, animation.length)
var indices := tracks(key)
for track in animation.get_track_count():
var bone := indices[track]
if bone < 0 or (upper_only and upper_body[bone] == 0): continue
if animation.track_get_type(track) == Animation.TYPE_ROTATION_3D:
# Mixamo clips encode local bone rotations in the same named axes.
# Keep target bone lengths; never replace the target bind skeleton.
var rotation := animation.rotation_track_interpolate(track, stamp).normalized()
rotations[bone] = rotations[bone].slerp(rotation, weight)
elif animation.track_get_type(track) == Animation.TYPE_POSITION_3D and not upper_only:
var value := animation.position_track_interpolate(track, stamp)
if str(skeleton.get_bone_name(bone)).ends_with("Hips"):
var source_rests: Dictionary = animation.get_meta("source_rests", {})
var source_rest: Transform3D = source_rests.get(str(skeleton.get_bone_name(bone)), skeleton.get_bone_rest(bone))
value *= rest_positions[bone].y / maxf(0.01, source_rest.origin.y)
value.x = rest_positions[bone].x
value.z = rest_positions[bone].z
if "jump" in key:
value.y = minf(value.y, rest_positions[bone].y)
positions[bone] = positions[bone].lerp(value, weight)
func apply(base: String, time: float, looping: bool, action: String, action_time: float, action_weight: float, delta: float, pitch: float) -> void:
rotations.assign(rest_rotations)
var positions: Array[Vector3] = rest_positions.duplicate()
sample(base, time, looping, false, 1.0, positions)
if not action.is_empty(): sample(action, action_time, false, true, action_weight, positions)
var spine := skeleton.find_bone("mixamorig_Spine2")
if spine >= 0:
rotations[spine] = Quaternion(Vector3.RIGHT, -clampf(pitch, -0.8, 0.8)) * rotations[spine]
var blend := 1.0 if delta <= 0 else 1.0 - exp(-delta * 22.0)
for bone in skeleton.get_bone_count():
skeleton.set_bone_pose_rotation(bone, skeleton.get_bone_pose_rotation(bone).slerp(rotations[bone], blend))
skeleton.set_bone_pose_position(bone, skeleton.get_bone_pose_position(bone).lerp(positions[bone], blend))
update_globals()
func update_globals() -> void:
# Skeleton3D's global pose cache can still reflect the previous render frame.
# Compose the freshly written local poses for same-frame sockets and IK.
for bone in skeleton.get_bone_count():
var parent := skeleton.get_bone_parent(bone)
global_poses[bone] = skeleton.get_bone_pose(bone)
if parent >= 0: global_poses[bone] = global_poses[parent] * global_poses[bone]
func move_shoulder(side: String, target: Vector3) -> void:
var bone := skeleton.find_bone("mixamorig_" + side + "Arm")
var parent := skeleton.get_bone_parent(bone)
skeleton.set_bone_pose_position(bone, global_poses[parent].affine_inverse() * target)
update_globals()
func reach_arm(side: String, target: Vector3) -> void:
var arm := skeleton.find_bone("mixamorig_" + side + "Arm")
var elbow := skeleton.find_bone("mixamorig_" + side + "ForeArm")
var hand := skeleton.find_bone("mixamorig_" + side + "Hand")
if arm < 0 or elbow < 0 or hand < 0: return
var palm := global_poses[hand].basis.get_rotation_quaternion()
var start := global_poses[arm].origin
var joint := global_poses[elbow].origin
var end := global_poses[hand].origin
var upper_length := start.distance_to(joint)
var lower_length := joint.distance_to(end)
var axis := (target - start).normalized()
var distance := clampf(start.distance_to(target), absf(upper_length - lower_length) + 0.001, upper_length + lower_length - 0.001)
var along := (upper_length * upper_length + distance * distance - lower_length * lower_length) / (2 * distance)
var bend := (joint - start) - axis * axis.dot(joint - start)
if bend.length_squared() < 0.00001: bend = Vector3.DOWN - axis * axis.dot(Vector3.DOWN)
var desired_elbow := start + axis * along + bend.normalized() * sqrt(maxf(0, upper_length * upper_length - along * along))
rotate_towards(arm, joint - start, desired_elbow - start)
rotate_towards(elbow, global_poses[hand].origin - global_poses[elbow].origin, start + axis * distance - global_poses[elbow].origin)
var parent := skeleton.get_bone_parent(hand)
skeleton.set_bone_pose_rotation(hand, global_poses[parent].basis.get_rotation_quaternion().inverse() * palm)
update_globals()
func rotate_towards(bone: int, from: Vector3, to: Vector3) -> void:
if from.length_squared() < 0.000001 or to.length_squared() < 0.000001: return
var parent := skeleton.get_bone_parent(bone)
var parent_rotation := global_poses[parent].basis.get_rotation_quaternion()
var delta := Quaternion(from.normalized(), to.normalized())
skeleton.set_bone_pose_rotation(bone, (parent_rotation.inverse() * delta * parent_rotation * skeleton.get_bone_pose_rotation(bone)).normalized())
update_globals()