Files
Papay-Shooter/debug/titanfall_motion_capture.gd
T

194 lines
6.7 KiB
GDScript

extends SceneTree
## Visual regression capture for the local Titanfall motion lab rigs.
##
## godot --path . --windowed --resolution 800x800 \
## --script res://debug/titanfall_motion_capture.gd -- <output-dir> [model.glb]
const DEFAULT_MODEL := \
"res://assets/characters/local_reference/titanfall_motion/mannequin.glb"
const SPEED := 13.0
var _out_dir := "tmp_titanfall_motion_capture"
var _model_path := DEFAULT_MODEL
var _model: SkinnedPlayerModel
func _initialize() -> void:
var args := OS.get_cmdline_user_args()
if not args.is_empty():
_out_dir = args[0]
if args.size() > 1:
_model_path = args[1]
if not _out_dir.is_absolute_path():
_out_dir = ProjectSettings.globalize_path(_out_dir)
DirAccess.make_dir_recursive_absolute(_out_dir)
call_deferred("_run")
func _run() -> void:
var scene := Node3D.new()
root.add_child(scene)
current_scene = scene
var environment_node := WorldEnvironment.new()
var environment := Environment.new()
environment.background_mode = Environment.BG_COLOR
environment.background_color = Color(0.08, 0.09, 0.12)
environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
environment.ambient_light_color = Color(0.78, 0.82, 0.95)
environment.ambient_light_energy = 1.3
environment.tonemap_mode = Environment.TONE_MAPPER_FILMIC
environment_node.environment = environment
scene.add_child(environment_node)
var sun := DirectionalLight3D.new()
sun.rotation_degrees = Vector3(-35.0, 35.0, 0.0)
sun.light_energy = 2.2
scene.add_child(sun)
var camera := Camera3D.new()
scene.add_child(camera)
camera.current = true
camera.global_position = Vector3(0.0, 1.15, 3.0)
camera.look_at(Vector3(0.0, 0.9, 0.0), Vector3.UP)
var body := CharacterBody3D.new()
scene.add_child(body)
_model = SkinnedPlayerModel.new()
_model.model_path = _model_path
_model.first_person_mode = false
_model.shadows_only = false
body.add_child(_model)
for _frame in 60:
await process_frame
if not _model.loaded:
printerr("titanfall_motion_capture: model failed to load")
quit(1)
return
await _drive("ground", 0.0, 90)
_snap("idle")
await _wall(-1.0, Vector3.RIGHT)
_snap("wall_left")
await _drive("ground", 0.0, 90)
await _wall(1.0, Vector3.LEFT)
_snap("wall_right")
_model.set_wall_side(0.0)
_model.set_wall_surface(Vector3.ZERO, Vector3.ZERO)
_model.set_wall_glide_motion(Vector3.ZERO)
await _drive("ground", 0.0, 1)
_snap("post_right_01")
await _drive("ground", 0.0, 19)
_snap("post_right_20")
await _drive("ground", 0.0, 70)
_snap("post_right_idle")
# Speed-scaled traversal entry: preserve three early frames so a hard cut is
# visually obvious in review while the final wall pose remains unchanged.
body.velocity = Vector3(0.0, -3.0, -SPEED)
await _drive("air", SPEED, 20)
_model.set_wall_side(1.0)
_model.set_wall_glide_motion(Vector3(0.0, 0.0, -SPEED))
await _drive("wall_run", SPEED, 1)
_snap("wall_transition_01")
await _drive("wall_run", SPEED, 3)
_snap("wall_transition_04")
await _drive("wall_run", SPEED, 6)
_snap("wall_transition_10")
# The stopping one-shot is driven while speed physically decays instead of
# leaving the high-rate Sprint cycle active until an instant zero.
_model.set_locomotion(0.0, 1.0, 0.0)
await _drive("ground", 11.0, 45)
_snap("stop_00_run")
for sample in [["stop_01", 10.0, 1], ["stop_06", 4.5, 5],
["stop_14", 1.2, 8]]:
await _drive("stop", float(sample[1]), int(sample[2]))
_snap(String(sample[0]))
await _drive("ground", 0.0, 18)
_snap("stop_32_idle")
# Successive hops select source and offline-mirrored source performances.
body.velocity = Vector3(0.0, 8.5, -8.0)
await _drive("air", 8.0, 12)
_snap("jump_normal")
await _drive("ground", 0.0, 24)
body.velocity = Vector3(0.0, 8.5, -8.0)
await _drive("air_alt", 8.0, 12)
_snap("jump_alternate")
quit()
func _wall(side: float, normal: Vector3) -> void:
_model.set_wall_side(side)
_model.set_wall_surface(normal, Vector3.ZERO)
_model.set_wall_glide_motion(Vector3(0.0, 0.0, -SPEED))
await _drive("wall_run", SPEED, 90)
func _drive(state: String, speed: float, frames: int) -> void:
for _frame in frames:
_model.update_state(state, speed, false)
await process_frame
func _snap(name: String) -> void:
var image := root.get_viewport().get_texture().get_image()
var path := _out_dir.path_join(name + ".png")
image.save_png(path)
print("titanfall_motion_capture: saved %s metrics=%s" % [
path, _pose_metrics()])
func _pose_metrics() -> Dictionary:
var roles: Dictionary = _model._rig_info.get("roles", {})
var chain := [_model._role_bone("hips", ["hips", "Hips", "pelvis"])]
var neck_name := String(roles.get("neck", ""))
var head_name := String(roles.get("head", ""))
for role in roles.get("spine", []):
var name := String(role)
if name == neck_name or name == head_name:
continue
chain.append(RigRoles.find_imported_bone(_model.skeleton, name))
var max_torso_joint_angle := 0.0
var max_torso_joint := ""
for index in range(1, chain.size()):
if chain[index - 1] < 0 or chain[index] < 0:
continue
var parent_pose := _model.skeleton.get_bone_global_pose(chain[index - 1])
var child_pose := _model.skeleton.get_bone_global_pose(chain[index])
var parent_rest := _model.skeleton.get_bone_global_rest(chain[index - 1])
var child_rest := _model.skeleton.get_bone_global_rest(chain[index])
var posed_relative := parent_pose.basis.inverse() * child_pose.basis
var rest_relative := parent_rest.basis.inverse() * child_rest.basis
var joint_angle := posed_relative.get_rotation_quaternion().angle_to(
rest_relative.get_rotation_quaternion())
if joint_angle > max_torso_joint_angle:
max_torso_joint_angle = joint_angle
max_torso_joint = _model.skeleton.get_bone_name(chain[index])
var max_leg_horizontal_ratio := 0.0
var knee_rise := {"L": 0.0, "R": 0.0}
for side in ["L", "R"]:
var thigh := _model._role_bone("thigh." + side,
["thigh." + side, "Left thigh" if side == "L" else "Right thigh"])
var shin := _model._role_bone("shin." + side,
["shin." + side, "Left shin" if side == "L" else "Right shin"])
if thigh < 0 or shin < 0:
continue
var segment := _model.skeleton.get_bone_global_pose(shin).origin \
- _model.skeleton.get_bone_global_pose(thigh).origin
segment = _model.skeleton.global_transform.basis * segment
knee_rise[side] = segment.y
if segment.length() > 0.0001:
max_leg_horizontal_ratio = maxf(max_leg_horizontal_ratio,
Vector2(segment.x, segment.z).length() / segment.length())
return {
"torso_joint_angle": max_torso_joint_angle,
"torso_joint": max_torso_joint,
"leg_horizontal_ratio": max_leg_horizontal_ratio,
"left_knee_segment_y": knee_rise["L"],
"right_knee_segment_y": knee_rise["R"],
"knee_lead": float(knee_rise["R"]) - float(knee_rise["L"]),
"clip": _model.current_clip_debug(),
"crossfade": _model._state_trans.xfade_time,
}