extends SceneTree # Regression for locomotion crossfades: blending world-space joint endpoints # briefly shortened or stretched limbs. Parent/child segment lengths must stay # close to the two endpoint poses while wait/run transitions are in progress. const PlayerView = preload("res://ui/player_view.gd") var failures: Array[String] = [] var checked_segments := 0 func _initialize() -> void: call_deferred("run") func _check(ok: bool, message: String) -> void: if not ok: failures.append(message) printerr("FAIL: ", message) func _pose(anim: Node, skeleton: Skeleton3D) -> Dictionary: var result := {} for bone in skeleton.get_bone_count(): var data: Dictionary = anim.call("get_effect_bone_pose", skeleton.get_bone_name(bone)) if data.has("transform"): result[bone] = (data["transform"] as Transform3D).origin return result func _segments(pose: Dictionary, skeleton: Skeleton3D) -> Dictionary: var result := {} for bone in skeleton.get_bone_count(): var parent := skeleton.get_bone_parent(bone) if parent >= 0 and pose.has(bone) and pose.has(parent): var length := (pose[bone] as Vector3).distance_to(pose[parent] as Vector3) if length > 0.01: result[bone] = length return result func run() -> void: OS.set_environment("MTGODOT_GPUSKIN", "0") for race in 8: var view := PlayerView.new() root.add_child(view) if not view.build(AssetRoot.path(), race): _check(false, "race %d failed to build" % race) view.queue_free() continue view.model.set("lod_enabled", false) view.anim.set("blend_time", 0.15) await process_frame view.anim.set("time", minf(0.25, float(view.anim.call("get_duration")) * 0.5)) var skeleton := view.model.get_node("Skeleton3D") as Skeleton3D var start := _segments(_pose(view.anim, skeleton), skeleton) view.set_anim_state("run") var transition: Array[Dictionary] = [] for frame in 12: await process_frame transition.append(_segments(_pose(view.anim, skeleton), skeleton)) var finish := _segments(_pose(view.anim, skeleton), skeleton) for bone in start: if not finish.has(bone): continue # Terminal *Nub helpers are zero-area animation handles and carry no # skinned child segment. Some clips intentionally collapse them. if String(skeleton.get_bone_name(bone)).ends_with("Nub"): continue var endpoint_min: float = minf(float(start[bone]), float(finish[bone])) var endpoint_max: float = maxf(float(start[bone]), float(finish[bone])) # Sub-5 cm helpers/finger tips may carry authored local translation # animation and do not explain whole-character deformation. Keep this # gate focused on visible limb and torso segments. if endpoint_min < 5.0: continue for frame in transition.size(): if not transition[frame].has(bone): continue var value: float = transition[frame][bone] checked_segments += 1 _check(value >= endpoint_min * 0.75 and value <= endpoint_max * 1.25, "race %d bone %s deforms in blend frame %d: %.3f not near %.3f..%.3f" % [race, skeleton.get_bone_name(bone), frame, value, endpoint_min, endpoint_max]) view.queue_free() await process_frame _check(checked_segments > 1000, "blend regression did not inspect enough skinned segments") print("animation_blend_test: failures=", failures.size(), " segments=", checked_segments, " (8 races, wait -> run)") quit(0 if failures.is_empty() else 1)