153 lines
5.2 KiB
GDScript
153 lines
5.2 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")
|
|
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
|
|
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
|
|
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,
|
|
}
|