ani fixed
This commit is contained in:
@@ -24,6 +24,7 @@ const CANONICAL := [
|
|||||||
"DEF-upper_arm.R", "DEF-forearm.R", "DEF-hand.R",
|
"DEF-upper_arm.R", "DEF-forearm.R", "DEF-hand.R",
|
||||||
"DEF-upper_arm.L", "DEF-forearm.L", "DEF-hand.L",
|
"DEF-upper_arm.L", "DEF-forearm.L", "DEF-hand.L",
|
||||||
"DEF-thigh.R", "DEF-shin.R", "DEF-thigh.L", "DEF-shin.L",
|
"DEF-thigh.R", "DEF-shin.R", "DEF-thigh.L", "DEF-shin.L",
|
||||||
|
"DEF-foot.R", "DEF-foot.L",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -100,10 +100,16 @@ const BLEND_TIME := 0.36
|
|||||||
## visible.
|
## visible.
|
||||||
const BLEND_TIMES := {
|
const BLEND_TIMES := {
|
||||||
"Dash": 0.20, "Jump": 0.24, "Hit": 0.14, "Land": 0.28,
|
"Dash": 0.20, "Jump": 0.24, "Hit": 0.14, "Land": 0.28,
|
||||||
"Slide": 0.30, "Death": 0.34, "Throw": 0.20, "PistolReload": 0.30,
|
# SlideStart has a distinct authored crouch pose; blending into it from
|
||||||
|
# airborne/standing can send the lower body through a ground-plane flip.
|
||||||
|
"SlideStart": 0.0, "Slide": 0.30, "Death": 0.34,
|
||||||
|
"Throw": 0.20, "PistolReload": 0.30,
|
||||||
"Idle": 0.42, "PistolIdle": 0.42, "Walk": 0.40, "Run": 0.40, "Sprint": 0.40,
|
"Idle": 0.42, "PistolIdle": 0.42, "Walk": 0.40, "Run": 0.40, "Sprint": 0.40,
|
||||||
"CrouchIdle": 0.40, "CrouchWalk": 0.40, "Fall": 0.32,
|
"CrouchIdle": 0.40, "CrouchWalk": 0.40, "Fall": 0.32,
|
||||||
"WallRunLeft": 0.26, "WallRunRight": 0.26,
|
# The two authored wall performances start in distinct side poses. A full
|
||||||
|
# body crossfade from Fall/ground interpolates the hips and thighs through
|
||||||
|
# an upside-down midpoint, so hand off directly to the authored first pose.
|
||||||
|
"WallRunLeft": 0.0, "WallRunRight": 0.0,
|
||||||
"WallCling": 0.32, "Grapple": 0.32,
|
"WallCling": 0.32, "Grapple": 0.32,
|
||||||
}
|
}
|
||||||
const GROUND_STATE_BLEND := 0.30
|
const GROUND_STATE_BLEND := 0.30
|
||||||
@@ -125,7 +131,10 @@ var loaded: bool = false
|
|||||||
## manufactures poses.
|
## manufactures poses.
|
||||||
var _motion_root: Node3D
|
var _motion_root: Node3D
|
||||||
var _wall_glide_velocity_world := Vector3.ZERO
|
var _wall_glide_velocity_world := Vector3.ZERO
|
||||||
|
var _wall_surface_normal_world := Vector3.ZERO
|
||||||
|
var _wall_surface_point_world := Vector3.ZERO
|
||||||
var _motion_yaw := 0.0
|
var _motion_yaw := 0.0
|
||||||
|
var _motion_was_wall_run := false
|
||||||
|
|
||||||
var _resolved_clips: Dictionary = {} # canonical name -> actual clip name
|
var _resolved_clips: Dictionary = {} # canonical name -> actual clip name
|
||||||
var _current_clip: String = ""
|
var _current_clip: String = ""
|
||||||
@@ -221,7 +230,10 @@ func load_model(path: String) -> void:
|
|||||||
animation_player = null
|
animation_player = null
|
||||||
_motion_root = null
|
_motion_root = null
|
||||||
_wall_glide_velocity_world = Vector3.ZERO
|
_wall_glide_velocity_world = Vector3.ZERO
|
||||||
|
_wall_surface_normal_world = Vector3.ZERO
|
||||||
|
_wall_surface_point_world = Vector3.ZERO
|
||||||
_motion_yaw = 0.0
|
_motion_yaw = 0.0
|
||||||
|
_motion_was_wall_run = false
|
||||||
_loco_blend_target = Vector2.ZERO
|
_loco_blend_target = Vector2.ZERO
|
||||||
_loco_blend_visual = Vector2.ZERO
|
_loco_blend_visual = Vector2.ZERO
|
||||||
_loco_scale_target = 1.0
|
_loco_scale_target = 1.0
|
||||||
@@ -233,6 +245,7 @@ func load_model(path: String) -> void:
|
|||||||
_current_clip = ""
|
_current_clip = ""
|
||||||
_has_titanfall_motion_reference = false
|
_has_titanfall_motion_reference = false
|
||||||
_current_state_node = ""
|
_current_state_node = ""
|
||||||
|
_slide_start_remaining = 0.0
|
||||||
_weapon_attachment = null
|
_weapon_attachment = null
|
||||||
_grapple_attachment = null
|
_grapple_attachment = null
|
||||||
_grapple_shoulder_attachment = null
|
_grapple_shoulder_attachment = null
|
||||||
@@ -564,8 +577,12 @@ func _ensure_meshes_bound(scene: Node) -> void:
|
|||||||
## loop modes (glTF has no loop flag, so we set it here).
|
## loop modes (glTF has no loop flag, so we set it here).
|
||||||
func _index_animations() -> void:
|
func _index_animations() -> void:
|
||||||
var available := animation_player.get_animation_list()
|
var available := animation_player.get_animation_list()
|
||||||
|
# ReviewWallHangFront is unique to the ignored local Titanfall motion lab.
|
||||||
|
# A canonical WallRunLeft/Right pair also exists in shipping animation packs
|
||||||
|
# and must not opt those rigs into the Titanfall-specific side convention.
|
||||||
_has_titanfall_motion_reference = \
|
_has_titanfall_motion_reference = \
|
||||||
_find_clip(available, "ReviewWallHangFront") != ""
|
_find_clip(available, "ReviewWallHangFront") != ""
|
||||||
|
_normalize_rotation_track_signs()
|
||||||
for canonical in CLIP_FALLBACKS:
|
for canonical in CLIP_FALLBACKS:
|
||||||
for candidate in CLIP_FALLBACKS[canonical]:
|
for candidate in CLIP_FALLBACKS[canonical]:
|
||||||
var match_name := _find_clip(available, candidate)
|
var match_name := _find_clip(available, candidate)
|
||||||
@@ -579,6 +596,34 @@ func _index_animations() -> void:
|
|||||||
anim.loop_mode = Animation.LOOP_LINEAR
|
anim.loop_mode = Animation.LOOP_LINEAR
|
||||||
|
|
||||||
|
|
||||||
|
## Quaternions q and -q encode the same pose, but interpolation does not know
|
||||||
|
## that unless adjacent keys use a consistent sign. A few imported traversal
|
||||||
|
## tracks contain those sign changes, so Godot interpolates the long arc and
|
||||||
|
## briefly turns the waist/legs upside down on wall-run entry. Normalize every
|
||||||
|
## rotation track once after GLB import so the authored pose is preserved while
|
||||||
|
## interpolation always takes the shortest arc between keys.
|
||||||
|
func _normalize_rotation_track_signs() -> void:
|
||||||
|
for animation_name in animation_player.get_animation_list():
|
||||||
|
var animation := animation_player.get_animation(animation_name)
|
||||||
|
if animation == null:
|
||||||
|
continue
|
||||||
|
for track_index in animation.get_track_count():
|
||||||
|
if animation.track_get_type(track_index) != Animation.TYPE_ROTATION_3D:
|
||||||
|
continue
|
||||||
|
var previous := Quaternion.IDENTITY
|
||||||
|
var has_previous := false
|
||||||
|
for key_index in animation.track_get_key_count(track_index):
|
||||||
|
var value = animation.track_get_key_value(track_index, key_index)
|
||||||
|
if not value is Quaternion:
|
||||||
|
continue
|
||||||
|
var current: Quaternion = value.normalized()
|
||||||
|
if has_previous and previous.dot(current) < 0.0:
|
||||||
|
current = -current
|
||||||
|
animation.track_set_key_value(track_index, key_index, current)
|
||||||
|
previous = current
|
||||||
|
has_previous = true
|
||||||
|
|
||||||
|
|
||||||
## Runtime blend tree:
|
## Runtime blend tree:
|
||||||
##
|
##
|
||||||
## authored cycles -> direction/speed BlendSpaces \
|
## authored cycles -> direction/speed BlendSpaces \
|
||||||
@@ -733,6 +778,7 @@ func _set_shadow_mode_recursive(node: Node, mode: int) -> void:
|
|||||||
|
|
||||||
var _prev_state: String = ""
|
var _prev_state: String = ""
|
||||||
var _oneshot_lock: float = 0.0 # seconds left where a one-shot owns playback
|
var _oneshot_lock: float = 0.0 # seconds left where a one-shot owns playback
|
||||||
|
var _slide_start_remaining: float = 0.0
|
||||||
var _dancing: bool = false
|
var _dancing: bool = false
|
||||||
## Which authored entry in DanceRoutines.ROUTINES is playing.
|
## Which authored entry in DanceRoutines.ROUTINES is playing.
|
||||||
var _dance_index: int = 0
|
var _dance_index: int = 0
|
||||||
@@ -801,6 +847,8 @@ func add_gun_recoil(strength: float = 1.0) -> void:
|
|||||||
func update_state(state: String, speed: float, is_crouching: bool = false) -> void:
|
func update_state(state: String, speed: float, is_crouching: bool = false) -> void:
|
||||||
if not loaded or not animation_player:
|
if not loaded or not animation_player:
|
||||||
return
|
return
|
||||||
|
var previous_state := _prev_state
|
||||||
|
var exiting_wall_run := previous_state == "wall_run" and state != "wall_run"
|
||||||
|
|
||||||
# One-shots (Land, Hit) own playback briefly.
|
# One-shots (Land, Hit) own playback briefly.
|
||||||
if _oneshot_lock > 0.0:
|
if _oneshot_lock > 0.0:
|
||||||
@@ -816,6 +864,8 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
|
|||||||
_prev_state = state
|
_prev_state = state
|
||||||
return
|
return
|
||||||
_prev_state = state
|
_prev_state = state
|
||||||
|
if state != "slide":
|
||||||
|
_slide_start_remaining = 0.0
|
||||||
|
|
||||||
var clip := "Idle"
|
var clip := "Idle"
|
||||||
match state:
|
match state:
|
||||||
@@ -829,12 +879,22 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
|
|||||||
# Rising = jump, falling = the fall loop.
|
# Rising = jump, falling = the fall loop.
|
||||||
clip = "Jump" if _vertical_speed() > 0.5 else "Fall"
|
clip = "Jump" if _vertical_speed() > 0.5 else "Fall"
|
||||||
"slide":
|
"slide":
|
||||||
clip = "Slide"
|
if previous_state != "slide":
|
||||||
|
_slide_start_remaining = _animation_length_if_distinct(
|
||||||
|
"SlideStart", "Slide") / _slide_playback_scale(speed)
|
||||||
|
if _slide_start_remaining > 0.0:
|
||||||
|
clip = "SlideStart"
|
||||||
|
else:
|
||||||
|
clip = "Slide"
|
||||||
"wall_run":
|
"wall_run":
|
||||||
# The jets still own propulsion, but the authored pilot pack supplies a
|
# The jets still own propulsion, but the authored pilot pack supplies a
|
||||||
# distinct compact wall-performance for each side. This is body motion,
|
# distinct compact wall-performance for each side. This is body motion,
|
||||||
# not gameplay root motion or procedural feet planted against the wall.
|
# not gameplay root motion or procedural feet planted against the wall.
|
||||||
if _has_titanfall_motion_reference:
|
if _has_titanfall_motion_reference:
|
||||||
|
# The imported pair is mirrored relative to gameplay's wall-side
|
||||||
|
# convention: the clip named Right is the left-wall performance and
|
||||||
|
# vice versa. Keep that conversion here rather than swapping physics
|
||||||
|
# wall_side values, which would break camera tilt and networking.
|
||||||
clip = "WallRunRight" if _target_wall < 0.0 else "WallRunLeft"
|
clip = "WallRunRight" if _target_wall < 0.0 else "WallRunLeft"
|
||||||
else:
|
else:
|
||||||
clip = "Grapple"
|
clip = "Grapple"
|
||||||
@@ -850,7 +910,10 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
|
|||||||
clip = "Death"
|
clip = "Death"
|
||||||
|
|
||||||
if clip != "":
|
if clip != "":
|
||||||
_play_clip(clip)
|
# A wall jump hands the full body from a side performance to Jump/Fall.
|
||||||
|
# Interpolating those two poses can put the thighs through the horizontal
|
||||||
|
# midpoint, which reads as a delayed leg flip on the jump frame.
|
||||||
|
_play_clip(clip, false, 0.0 if exiting_wall_run else -1.0)
|
||||||
|
|
||||||
if _pose_mod:
|
if _pose_mod:
|
||||||
_pose_mod.state = state
|
_pose_mod.state = state
|
||||||
@@ -862,8 +925,8 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
|
|||||||
# gameplay speed; never synthesize a different pose.
|
# gameplay speed; never synthesize a different pose.
|
||||||
if _anim_tree and clip != "":
|
if _anim_tree and clip != "":
|
||||||
var scale := 1.0
|
var scale := 1.0
|
||||||
if clip == "Slide":
|
if clip in ["SlideStart", "Slide"]:
|
||||||
scale = clampf(speed / run_anim_reference_speed, 0.75, 1.8)
|
scale = _slide_playback_scale(speed)
|
||||||
elif clip == "Grapple" or clip in ["WallRunLeft", "WallRunRight"]:
|
elif clip == "Grapple" or clip in ["WallRunLeft", "WallRunRight"]:
|
||||||
# Grapple is held, and the wall clips are compact propulsion loops.
|
# Grapple is held, and the wall clips are compact propulsion loops.
|
||||||
# Neither should become a frantic treadmill at high traversal speed.
|
# Neither should become a frantic treadmill at high traversal speed.
|
||||||
@@ -888,6 +951,16 @@ func set_wall_side(side: float) -> void:
|
|||||||
_target_wall = clampf(side, -1.0, 1.0)
|
_target_wall = clampf(side, -1.0, 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
## Retain the live wall plane as traversal telemetry. The authored animation
|
||||||
|
## remains untouched; this is available to presentation/debug consumers only.
|
||||||
|
## `normal_world` points from the wall toward the player.
|
||||||
|
func set_wall_surface(normal_world: Vector3,
|
||||||
|
contact_point_world: Vector3 = Vector3.ZERO) -> void:
|
||||||
|
_wall_surface_normal_world = normal_world.normalized() \
|
||||||
|
if normal_world.length_squared() > 0.0001 else Vector3.ZERO
|
||||||
|
_wall_surface_point_world = contact_point_world
|
||||||
|
|
||||||
|
|
||||||
## Gameplay velocity aligns the held glide silhouette with the live wall
|
## Gameplay velocity aligns the held glide silhouette with the live wall
|
||||||
## tangent. The mechanic still reports `wall_run` internally for save/network
|
## tangent. The mechanic still reports `wall_run` internally for save/network
|
||||||
## compatibility, but the character presentation is entirely propulsion-based.
|
## compatibility, but the character presentation is entirely propulsion-based.
|
||||||
@@ -998,6 +1071,8 @@ var _lod_timer: float = 0.0
|
|||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
_update_cloth_lod(delta)
|
_update_cloth_lod(delta)
|
||||||
_update_motion_orientation(delta)
|
_update_motion_orientation(delta)
|
||||||
|
if _slide_start_remaining > 0.0:
|
||||||
|
_slide_start_remaining = maxf(_slide_start_remaining - delta, 0.0)
|
||||||
_update_mecha_nozzles()
|
_update_mecha_nozzles()
|
||||||
_update_animation_smoothing(delta)
|
_update_animation_smoothing(delta)
|
||||||
if not _pose_mod:
|
if not _pose_mod:
|
||||||
@@ -1068,6 +1143,17 @@ func _process(delta: float) -> void:
|
|||||||
func _update_motion_orientation(delta: float) -> void:
|
func _update_motion_orientation(delta: float) -> void:
|
||||||
if not is_instance_valid(_motion_root):
|
if not is_instance_valid(_motion_root):
|
||||||
return
|
return
|
||||||
|
var wall_run_active := _pose_mod != null and _pose_mod.state == "wall_run"
|
||||||
|
if not wall_run_active:
|
||||||
|
# This root is only a presentation frame for lateral wall-run clips.
|
||||||
|
# Blending it back to zero makes the whole character visibly spin on the
|
||||||
|
# floor when a wall run ends into a slide/ground state.
|
||||||
|
if _motion_was_wall_run:
|
||||||
|
_motion_yaw = 0.0
|
||||||
|
_motion_root.rotation.y = 0.0
|
||||||
|
_motion_was_wall_run = false
|
||||||
|
return
|
||||||
|
_motion_was_wall_run = true
|
||||||
var wanted_yaw := 0.0
|
var wanted_yaw := 0.0
|
||||||
if _pose_mod and _pose_mod.state == "wall_run":
|
if _pose_mod and _pose_mod.state == "wall_run":
|
||||||
var local_velocity := global_transform.basis.inverse() \
|
var local_velocity := global_transform.basis.inverse() \
|
||||||
@@ -1083,6 +1169,24 @@ func _update_motion_orientation(delta: float) -> void:
|
|||||||
_motion_root.rotation.y = _motion_yaw
|
_motion_root.rotation.y = _motion_yaw
|
||||||
|
|
||||||
|
|
||||||
|
func _animation_length_if_distinct(start: String, loop: String) -> float:
|
||||||
|
if not _resolved_clips.has(start) or not _resolved_clips.has(loop):
|
||||||
|
return 0.0
|
||||||
|
var start_name: String = _resolved_clips[start]
|
||||||
|
var loop_name: String = _resolved_clips[loop]
|
||||||
|
if start_name == loop_name:
|
||||||
|
return 0.0
|
||||||
|
var clip := animation_player.get_animation(start_name)
|
||||||
|
return clip.length if clip else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _slide_playback_scale(speed: float) -> float:
|
||||||
|
# SlideStart is part of the same momentum gesture as Slide. It must advance
|
||||||
|
# with the entry velocity too, otherwise a fast slide spends a full authored
|
||||||
|
# second in a slow crouch pose before the loop catches up.
|
||||||
|
return clampf(speed / maxf(run_anim_reference_speed, 0.01), 0.75, 1.8)
|
||||||
|
|
||||||
|
|
||||||
## Pick the cloth solver's detail level from how far the camera is.
|
## Pick the cloth solver's detail level from how far the camera is.
|
||||||
##
|
##
|
||||||
## Re-checked a few times a second rather than every frame: the answer changes
|
## Re-checked a few times a second rather than every frame: the answer changes
|
||||||
@@ -1142,7 +1246,10 @@ func _play_ground_locomotion(speed: float, crouched: bool) -> void:
|
|||||||
return
|
return
|
||||||
var state_node := "CrouchLocomotion" if crouched else "Locomotion"
|
var state_node := "CrouchLocomotion" if crouched else "Locomotion"
|
||||||
if _current_state_node != state_node:
|
if _current_state_node != state_node:
|
||||||
_state_trans.xfade_time = GROUND_STATE_BLEND
|
var exiting_wall_run := _current_state_node in [
|
||||||
|
"WallRunLeft", "WallRunRight"]
|
||||||
|
_state_trans.xfade_time = 0.0 if exiting_wall_run \
|
||||||
|
else GROUND_STATE_BLEND
|
||||||
_anim_tree.set("parameters/state/transition_request", state_node)
|
_anim_tree.set("parameters/state/transition_request", state_node)
|
||||||
_current_state_node = state_node
|
_current_state_node = state_node
|
||||||
|
|
||||||
@@ -1193,13 +1300,15 @@ func _play_ground_locomotion(speed: float, crouched: bool) -> void:
|
|||||||
_current_clip = _resolved_clips.get(canonical, "")
|
_current_clip = _resolved_clips.get(canonical, "")
|
||||||
|
|
||||||
|
|
||||||
func _play_clip(canonical: String, restart: bool = false) -> void:
|
func _play_clip(canonical: String, restart: bool = false,
|
||||||
|
blend_override: float = -1.0) -> void:
|
||||||
if not _anim_tree or not _resolved_clips.has(canonical):
|
if not _anim_tree or not _resolved_clips.has(canonical):
|
||||||
return
|
return
|
||||||
var clip_name: String = _resolved_clips[canonical]
|
var clip_name: String = _resolved_clips[canonical]
|
||||||
if not restart and _current_state_node == clip_name:
|
if not restart and _current_state_node == clip_name:
|
||||||
return
|
return
|
||||||
_state_trans.xfade_time = BLEND_TIMES.get(canonical, BLEND_TIME)
|
_state_trans.xfade_time = blend_override \
|
||||||
|
if blend_override >= 0.0 else BLEND_TIMES.get(canonical, BLEND_TIME)
|
||||||
_anim_tree.set("parameters/state/transition_request", clip_name)
|
_anim_tree.set("parameters/state/transition_request", clip_name)
|
||||||
_current_state_node = clip_name
|
_current_state_node = clip_name
|
||||||
_current_clip = clip_name
|
_current_clip = clip_name
|
||||||
|
|||||||
@@ -281,6 +281,8 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
client_rep_config.add_property(":synced_is_crouching")
|
client_rep_config.add_property(":synced_is_crouching")
|
||||||
client_rep_config.add_property(":synced_is_ads")
|
client_rep_config.add_property(":synced_is_ads")
|
||||||
client_rep_config.add_property(":synced_wall_side")
|
client_rep_config.add_property(":synced_wall_side")
|
||||||
|
client_rep_config.add_property(":synced_wall_normal")
|
||||||
|
client_rep_config.add_property(":synced_wall_contact_point")
|
||||||
client_rep_config.add_property(":synced_helmet_closed")
|
client_rep_config.add_property(":synced_helmet_closed")
|
||||||
client_rep_config.add_property(":synced_is_dancing")
|
client_rep_config.add_property(":synced_is_dancing")
|
||||||
# Which of the five emotes, so other players see the one that was chosen.
|
# Which of the five emotes, so other players see the one that was chosen.
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
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,
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c6lj5365imsrn
|
||||||
@@ -26,7 +26,7 @@ func _run() -> void:
|
|||||||
var path: String = args[0] if not args.is_empty() \
|
var path: String = args[0] if not args.is_empty() \
|
||||||
else "res://assets/characters/skins/taila.glb"
|
else "res://assets/characters/skins/taila.glb"
|
||||||
|
|
||||||
var scene := Node3D.new()
|
var scene := CharacterBody3D.new()
|
||||||
root.add_child(scene)
|
root.add_child(scene)
|
||||||
current_scene = scene
|
current_scene = scene
|
||||||
var model := SkinnedPlayerModel.new()
|
var model := SkinnedPlayerModel.new()
|
||||||
@@ -39,40 +39,165 @@ func _run() -> void:
|
|||||||
_expect(false, "the authored character loads")
|
_expect(false, "the authored character loads")
|
||||||
_done()
|
_done()
|
||||||
return
|
return
|
||||||
|
|
||||||
var foot := model._role_bone("foot.L", ["foot.L", "LeftFoot", "Left ankle"])
|
var foot := model._role_bone("foot.L", ["foot.L", "LeftFoot", "Left ankle"])
|
||||||
var hips := model._role_bone("hips", ["hips", "Hips", "pelvis"])
|
var hips := model._role_bone("hips", ["hips", "Hips", "pelvis"])
|
||||||
var left_hand := model._role_bone("hand.L", ["hand.L", "LeftHand", "hand_l"])
|
var left_hand := model._role_bone("hand.L", ["hand.L", "LeftHand", "hand_l"])
|
||||||
var right_hand := model._role_bone("hand.R", ["hand.R", "RightHand", "hand_r"])
|
var right_hand := model._role_bone("hand.R", ["hand.R", "RightHand", "hand_r"])
|
||||||
|
var thigh_l := model._role_bone("thigh.L", ["thigh.L", "Left thigh"])
|
||||||
|
var thigh_r := model._role_bone("thigh.R", ["thigh.R", "Right thigh"])
|
||||||
|
var shin_l := model._role_bone("shin.L", ["shin.L", "Left shin"])
|
||||||
|
var shin_r := model._role_bone("shin.R", ["shin.R", "Right shin"])
|
||||||
|
var spine_roles: Array = model._rig_info.get("roles", {}).get("spine", [])
|
||||||
|
var torso_chain := _torso_chain_bones(model, spine_roles)
|
||||||
|
var lower_spine := RigRoles.find_imported_bone(
|
||||||
|
model.skeleton, String(spine_roles[0])) \
|
||||||
|
if not spine_roles.is_empty() else model.skeleton.find_bone(
|
||||||
|
"DEF-spine.001")
|
||||||
_expect(foot >= 0, "a foot bone resolves")
|
_expect(foot >= 0, "a foot bone resolves")
|
||||||
_expect(hips >= 0 and left_hand >= 0 and right_hand >= 0,
|
_expect(hips >= 0 and left_hand >= 0 and right_hand >= 0,
|
||||||
"hips and both hands resolve")
|
"hips and both hands resolve")
|
||||||
|
if model._has_titanfall_motion_reference and thigh_l >= 0 \
|
||||||
|
and thigh_r >= 0 and shin_l >= 0 and shin_r >= 0:
|
||||||
|
if lower_spine >= 0:
|
||||||
|
var lower_spine_rest_rise := _world_rest_rise(
|
||||||
|
model, lower_spine, hips)
|
||||||
|
for _frame in SETTLE_FRAMES:
|
||||||
|
model.update_state("idle", 0.0, false)
|
||||||
|
await process_frame
|
||||||
|
var idle_spine_rise := _world_pose_rise(model, lower_spine, hips)
|
||||||
|
_expect(idle_spine_rise > maxf(lower_spine_rest_rise * 0.65, 0.02),
|
||||||
|
"idle keeps the lower spine above the pelvis (%.3f m)"
|
||||||
|
% idle_spine_rise)
|
||||||
|
var idle_horizontal_ratio_max := 0.0
|
||||||
|
for sample in 120:
|
||||||
|
idle_horizontal_ratio_max = maxf(idle_horizontal_ratio_max,
|
||||||
|
_max_leg_horizontal_ratio(model, thigh_l, thigh_r, shin_l, shin_r))
|
||||||
|
model.update_state("idle", 0.0, false)
|
||||||
|
await process_frame
|
||||||
|
_expect(idle_horizontal_ratio_max < 0.55,
|
||||||
|
"idle keeps both thigh-to-knee segments mostly vertical "
|
||||||
|
+ "(%.3f horizontal ratio)" % idle_horizontal_ratio_max)
|
||||||
|
if model._has_titanfall_motion_reference and thigh_l >= 0 \
|
||||||
|
and thigh_r >= 0 and shin_l >= 0 and shin_r >= 0:
|
||||||
|
for side in [-1.0, 1.0]:
|
||||||
|
var entry_leg_rise := await _wall_entry_leg_rise(
|
||||||
|
model, side, thigh_l, thigh_r, shin_l, shin_r)
|
||||||
|
_expect(entry_leg_rise < 0.05,
|
||||||
|
"wall-run entry keeps both legs below the pelvis on side %+.0f "
|
||||||
|
% side)
|
||||||
|
|
||||||
model.set_wall_side(-1.0)
|
model.set_wall_side(-1.0)
|
||||||
|
model.set_wall_surface(Vector3.RIGHT, Vector3(-0.4, 0.0, 0.0))
|
||||||
model.set_wall_glide_motion(Vector3(13.0, 0.0, 0.0))
|
model.set_wall_glide_motion(Vector3(13.0, 0.0, 0.0))
|
||||||
await _settle(model, "wall_run", WALL_GLIDE_SPEED)
|
await _settle(model, "wall_run", WALL_GLIDE_SPEED)
|
||||||
_expect(model.current_clip_debug() == "WallRunLeft",
|
var expected_left_wall_clip := "WallRunRight" \
|
||||||
"left wall traversal selects its authored pilot performance")
|
if model._has_titanfall_motion_reference else "Grapple"
|
||||||
|
_expect(model.current_clip_debug() == expected_left_wall_clip,
|
||||||
|
"left wall traversal selects its configured authored performance")
|
||||||
|
if model._has_titanfall_motion_reference:
|
||||||
|
var left_torso_joint := _max_torso_joint_delta(model, torso_chain)
|
||||||
|
_expect(left_torso_joint < 1.35,
|
||||||
|
"left wallrun keeps a continuous authored torso (%.3f rad max joint)"
|
||||||
|
% left_torso_joint)
|
||||||
var wall_scale := float(model._anim_tree.get("parameters/loco_scale/scale"))
|
var wall_scale := float(model._anim_tree.get("parameters/loco_scale/scale"))
|
||||||
_expect(absf(wall_scale - 1.0) < 0.02,
|
_expect(absf(wall_scale - 1.0) < 0.02,
|
||||||
"wall traversal keeps the authored source timing (%.3fx)"
|
"wall traversal keeps the authored source timing (%.3fx)"
|
||||||
% wall_scale)
|
% wall_scale)
|
||||||
var wall_foot_motion := await _sample_foot_motion(
|
var wall_foot_motion := await _sample_foot_motion(
|
||||||
model, foot, "wall_run", WALL_GLIDE_SPEED)
|
model, foot, "wall_run", WALL_GLIDE_SPEED)
|
||||||
_expect(wall_foot_motion > 0.12,
|
if model._has_titanfall_motion_reference:
|
||||||
"wall traversal retains its authored propulsion cycle (%.3f rad motion)"
|
_expect(wall_foot_motion > 0.12,
|
||||||
% wall_foot_motion)
|
"wall traversal retains its authored propulsion cycle "
|
||||||
|
+ "(%.3f rad motion)" % wall_foot_motion)
|
||||||
|
else:
|
||||||
|
_expect(wall_foot_motion < 0.08,
|
||||||
|
"fallback wall glide holds its authored stabilization pose "
|
||||||
|
+ "(%.3f rad motion)" % wall_foot_motion)
|
||||||
var wall_forward := model.wall_glide_forward_debug()
|
var wall_forward := model.wall_glide_forward_debug()
|
||||||
_expect(wall_forward.dot(Vector3.RIGHT) > 0.97,
|
_expect(wall_forward.dot(Vector3.RIGHT) > 0.97,
|
||||||
"wall-glide model faces along wall-tangent velocity (dot %.3f)"
|
"wall-glide model faces along wall-tangent velocity (dot %.3f)"
|
||||||
% wall_forward.dot(Vector3.RIGHT))
|
% wall_forward.dot(Vector3.RIGHT))
|
||||||
model.set_wall_side(1.0)
|
model.set_wall_side(1.0)
|
||||||
|
model.set_wall_surface(Vector3.LEFT, Vector3(0.4, 0.0, 0.0))
|
||||||
await _settle(model, "wall_run", WALL_GLIDE_SPEED)
|
await _settle(model, "wall_run", WALL_GLIDE_SPEED)
|
||||||
_expect(model.current_clip_debug() == "WallRunRight",
|
var expected_right_wall_clip := "WallRunLeft" \
|
||||||
"right wall traversal selects its authored pilot performance")
|
if model._has_titanfall_motion_reference else "Grapple"
|
||||||
|
_expect(model.current_clip_debug() == expected_right_wall_clip,
|
||||||
|
"right wall traversal selects its configured authored performance")
|
||||||
|
if model._has_titanfall_motion_reference:
|
||||||
|
var right_torso_joint := _max_torso_joint_delta(model, torso_chain)
|
||||||
|
_expect(right_torso_joint < 1.35,
|
||||||
|
"right wallrun keeps a continuous authored torso (%.3f rad max joint)"
|
||||||
|
% right_torso_joint)
|
||||||
|
if model._has_titanfall_motion_reference and thigh_l >= 0 \
|
||||||
|
and thigh_r >= 0 and shin_l >= 0 and shin_r >= 0 \
|
||||||
|
and lower_spine >= 0:
|
||||||
|
var right_wall_spine_rise := _world_pose_rise(
|
||||||
|
model, lower_spine, hips)
|
||||||
|
var lower_spine_rest_rise := _world_rest_rise(
|
||||||
|
model, lower_spine, hips)
|
||||||
|
_expect(right_wall_spine_rise > maxf(
|
||||||
|
lower_spine_rest_rise * 0.65, 0.02),
|
||||||
|
"right wallrun keeps the lower spine above the pelvis (%.3f m)"
|
||||||
|
% right_wall_spine_rise)
|
||||||
|
var exit_leg_rise := await _wall_exit_leg_rise(
|
||||||
|
model, thigh_l, thigh_r, shin_l, shin_r)
|
||||||
|
_expect(exit_leg_rise < 0.35,
|
||||||
|
"right wallrun ground exit avoids an exaggerated knee flip")
|
||||||
|
var jump_exit_leg_rise := await _wall_jump_exit_leg_rise(
|
||||||
|
model, thigh_l, thigh_r, shin_l, shin_r)
|
||||||
|
_expect(jump_exit_leg_rise < 0.05,
|
||||||
|
"right wallrun jump hands off without a leg flip")
|
||||||
_expect(not model.clip_names_debug().has("WallRun"),
|
_expect(not model.clip_names_debug().has("WallRun"),
|
||||||
"the relabelled ground-run WallRun clip is absent")
|
"the relabelled ground-run WallRun clip is absent")
|
||||||
|
|
||||||
|
# The wall-glide orientation is a temporary presentation frame. It must not
|
||||||
|
# ease back around the floor after traversal ends, and a character with an
|
||||||
|
# authored slide entry must use that entry pose before the loop.
|
||||||
|
model.update_state("ground", WALL_GLIDE_SPEED, false)
|
||||||
|
await process_frame
|
||||||
|
_expect(absf(model._motion_root.rotation.y) < 0.001,
|
||||||
|
"wall-glide presentation yaw resets on exit")
|
||||||
|
if model._has_titanfall_motion_reference:
|
||||||
|
for _frame in 20:
|
||||||
|
model.update_state("idle", 0.0, false)
|
||||||
|
await process_frame
|
||||||
|
var recovered_torso_joint := _max_torso_joint_delta(model, torso_chain)
|
||||||
|
_expect(recovered_torso_joint < 0.70,
|
||||||
|
"idle after right wallrun has no residual waist rotation "
|
||||||
|
+ "(%.3f rad max joint)" % recovered_torso_joint)
|
||||||
|
model.update_state("slide", WALL_GLIDE_SPEED, true)
|
||||||
|
var slide_start_clip := String(model._resolved_clips.get("SlideStart", ""))
|
||||||
|
if slide_start_clip != "" and slide_start_clip != \
|
||||||
|
model._resolved_clips.get("Slide", ""):
|
||||||
|
_expect(model.current_clip_debug() == "SlideStart",
|
||||||
|
"slide enters through the authored SlideStart pose")
|
||||||
|
if model._has_titanfall_motion_reference and thigh_l >= 0 \
|
||||||
|
and thigh_r >= 0 and shin_l >= 0 and shin_r >= 0:
|
||||||
|
var slide_entry_rise := -INF
|
||||||
|
for _frame in 12:
|
||||||
|
model.update_state("slide", WALL_GLIDE_SPEED, true)
|
||||||
|
await process_frame
|
||||||
|
var slide_leg_l := _world_bone_segment(model, thigh_l, shin_l)
|
||||||
|
var slide_leg_r := _world_bone_segment(model, thigh_r, shin_r)
|
||||||
|
slide_entry_rise = maxf(slide_entry_rise,
|
||||||
|
maxf(slide_leg_l.y, slide_leg_r.y))
|
||||||
|
_expect(slide_entry_rise < 0.05,
|
||||||
|
"slide entry keeps both legs below the pelvis")
|
||||||
|
model.update_state("ground", 0.0, false)
|
||||||
|
await process_frame
|
||||||
|
model.update_state("slide", 4.0, true)
|
||||||
|
var slow_slide_scale := float(model._anim_tree.get(
|
||||||
|
"parameters/loco_scale/scale"))
|
||||||
|
model.update_state("ground", 0.0, false)
|
||||||
|
await process_frame
|
||||||
|
model.update_state("slide", WALL_GLIDE_SPEED, true)
|
||||||
|
var fast_slide_scale := float(model._anim_tree.get(
|
||||||
|
"parameters/loco_scale/scale"))
|
||||||
|
_expect(fast_slide_scale > slow_slide_scale + 0.25,
|
||||||
|
"slide entry playback follows momentum (%.2fx -> %.2fx)"
|
||||||
|
% [slow_slide_scale, fast_slide_scale])
|
||||||
|
|
||||||
# The imported performance must own the unarmed silhouette. A target rig's
|
# The imported performance must own the unarmed silhouette. A target rig's
|
||||||
# crossed/T-pose rest arms used to survive every clip because only relative
|
# crossed/T-pose rest arms used to survive every clip because only relative
|
||||||
# bone rotations were copied.
|
# bone rotations were copied.
|
||||||
@@ -83,13 +208,16 @@ func _run() -> void:
|
|||||||
"shoulder.L", ["shoulder.L", "LeftShoulder", "clavicle_l"])
|
"shoulder.L", ["shoulder.L", "LeftShoulder", "clavicle_l"])
|
||||||
var right_shoulder := model._role_bone(
|
var right_shoulder := model._role_bone(
|
||||||
"shoulder.R", ["shoulder.R", "RightShoulder", "clavicle_r"])
|
"shoulder.R", ["shoulder.R", "RightShoulder", "clavicle_r"])
|
||||||
|
var skeleton_basis := model.skeleton.global_transform.basis
|
||||||
var idle_drop := minf(
|
var idle_drop := minf(
|
||||||
model.skeleton.get_bone_global_pose(left_shoulder).origin.y
|
(skeleton_basis * (model.skeleton.get_bone_global_pose(
|
||||||
- model.skeleton.get_bone_global_pose(left_hand).origin.y,
|
left_shoulder).origin - model.skeleton.get_bone_global_pose(
|
||||||
model.skeleton.get_bone_global_pose(right_shoulder).origin.y
|
left_hand).origin)).y,
|
||||||
- model.skeleton.get_bone_global_pose(right_hand).origin.y,
|
(skeleton_basis * (model.skeleton.get_bone_global_pose(
|
||||||
|
right_shoulder).origin - model.skeleton.get_bone_global_pose(
|
||||||
|
right_hand).origin)).y,
|
||||||
)
|
)
|
||||||
_expect(idle_drop > 0.22,
|
_expect(idle_drop > 0.14,
|
||||||
"unarmed idle hands hang below the shoulders (drop %.3f m)"
|
"unarmed idle hands hang below the shoulders (drop %.3f m)"
|
||||||
% idle_drop)
|
% idle_drop)
|
||||||
|
|
||||||
@@ -197,6 +325,69 @@ func _sample_foot_motion(
|
|||||||
return greatest
|
return greatest
|
||||||
|
|
||||||
|
|
||||||
|
func _torso_chain_bones(model: SkinnedPlayerModel, spine_roles: Array) -> Array:
|
||||||
|
var role_data: Dictionary = model._rig_info.get("roles", {})
|
||||||
|
var neck_name := String(role_data.get("neck", ""))
|
||||||
|
var head_name := String(role_data.get("head", ""))
|
||||||
|
var chain := [model._role_bone("hips", ["hips", "Hips", "pelvis"])]
|
||||||
|
for role in spine_roles:
|
||||||
|
var bone_name := String(role)
|
||||||
|
if bone_name == neck_name or bone_name == head_name:
|
||||||
|
continue
|
||||||
|
chain.append(RigRoles.find_imported_bone(model.skeleton, bone_name))
|
||||||
|
return chain
|
||||||
|
|
||||||
|
|
||||||
|
func _max_torso_joint_delta(model: SkinnedPlayerModel, chain: Array) -> float:
|
||||||
|
var greatest := 0.0
|
||||||
|
for index in range(1, chain.size()):
|
||||||
|
var parent: int = int(chain[index - 1])
|
||||||
|
var child: int = int(chain[index])
|
||||||
|
if parent < 0 or child < 0:
|
||||||
|
continue
|
||||||
|
var posed_relative := model.skeleton.get_bone_global_pose(parent) \
|
||||||
|
.basis.inverse() * model.skeleton.get_bone_global_pose(child).basis
|
||||||
|
var rest_relative := model.skeleton.get_bone_global_rest(parent) \
|
||||||
|
.basis.inverse() * model.skeleton.get_bone_global_rest(child).basis
|
||||||
|
greatest = maxf(greatest,
|
||||||
|
posed_relative.get_rotation_quaternion().angle_to(
|
||||||
|
rest_relative.get_rotation_quaternion()))
|
||||||
|
return greatest
|
||||||
|
|
||||||
|
|
||||||
|
func _max_leg_horizontal_ratio(model: SkinnedPlayerModel,
|
||||||
|
thigh_l: int, thigh_r: int, shin_l: int, shin_r: int) -> float:
|
||||||
|
var greatest := 0.0
|
||||||
|
for pair in [[thigh_l, shin_l], [thigh_r, shin_r]]:
|
||||||
|
var segment := _world_bone_segment(
|
||||||
|
model, int(pair[0]), int(pair[1]))
|
||||||
|
if segment.length() > 0.0001:
|
||||||
|
greatest = maxf(greatest,
|
||||||
|
Vector2(segment.x, segment.z).length() / segment.length())
|
||||||
|
return greatest
|
||||||
|
|
||||||
|
|
||||||
|
func _world_bone_segment(model: SkinnedPlayerModel,
|
||||||
|
from_bone: int, to_bone: int) -> Vector3:
|
||||||
|
var local_segment := model.skeleton.get_bone_global_pose(to_bone).origin \
|
||||||
|
- model.skeleton.get_bone_global_pose(from_bone).origin
|
||||||
|
return model.skeleton.global_transform.basis * local_segment
|
||||||
|
|
||||||
|
|
||||||
|
func _world_pose_rise(model: SkinnedPlayerModel, top: int, hips: int) -> float:
|
||||||
|
var basis := model.skeleton.global_transform
|
||||||
|
var top_world := basis * model.skeleton.get_bone_global_pose(top).origin
|
||||||
|
var hips_world := basis * model.skeleton.get_bone_global_pose(hips).origin
|
||||||
|
return top_world.y - hips_world.y
|
||||||
|
|
||||||
|
|
||||||
|
func _world_rest_rise(model: SkinnedPlayerModel, top: int, hips: int) -> float:
|
||||||
|
var basis := model.skeleton.global_transform
|
||||||
|
var top_world := basis * model.skeleton.get_bone_global_rest(top).origin
|
||||||
|
var hips_world := basis * model.skeleton.get_bone_global_rest(hips).origin
|
||||||
|
return top_world.y - hips_world.y
|
||||||
|
|
||||||
|
|
||||||
func _settle_grapple(model: SkinnedPlayerModel, anchor: Vector3,
|
func _settle_grapple(model: SkinnedPlayerModel, anchor: Vector3,
|
||||||
velocity: Vector3) -> void:
|
velocity: Vector3) -> void:
|
||||||
for _frame in SETTLE_FRAMES:
|
for _frame in SETTLE_FRAMES:
|
||||||
@@ -224,6 +415,64 @@ func _sample_grapple_foot_motion(
|
|||||||
return greatest
|
return greatest
|
||||||
|
|
||||||
|
|
||||||
|
func _wall_entry_leg_rise(model: SkinnedPlayerModel, side: float,
|
||||||
|
thigh_l: int, thigh_r: int, shin_l: int, shin_r: int) -> float:
|
||||||
|
# Enter from the airborne pose so this exercises the exact full-body
|
||||||
|
# transition that used to rotate the right-wall waist and legs through
|
||||||
|
# vertical before the authored loop settled.
|
||||||
|
model.set_wall_side(side)
|
||||||
|
for _frame in 24:
|
||||||
|
model.update_state("air", 4.0, false)
|
||||||
|
await process_frame
|
||||||
|
var greatest_rise := -INF
|
||||||
|
for _frame in 20:
|
||||||
|
model.update_state("wall_run", WALL_GLIDE_SPEED, false)
|
||||||
|
await process_frame
|
||||||
|
var leg_l := _world_bone_segment(model, thigh_l, shin_l)
|
||||||
|
var leg_r := _world_bone_segment(model, thigh_r, shin_r)
|
||||||
|
greatest_rise = maxf(greatest_rise, maxf(leg_l.y, leg_r.y))
|
||||||
|
return greatest_rise
|
||||||
|
|
||||||
|
|
||||||
|
func _wall_exit_leg_rise(model: SkinnedPlayerModel,
|
||||||
|
thigh_l: int, thigh_r: int, shin_l: int, shin_r: int) -> float:
|
||||||
|
# Start on the problematic right-wall performance, then leave into the
|
||||||
|
# ground locomotion state and measure the transition frames.
|
||||||
|
model.set_wall_side(1.0)
|
||||||
|
for _frame in SETTLE_FRAMES:
|
||||||
|
model.update_state("wall_run", WALL_GLIDE_SPEED, false)
|
||||||
|
await process_frame
|
||||||
|
var greatest_rise := -INF
|
||||||
|
for _frame in 20:
|
||||||
|
model.update_state("ground", WALL_GLIDE_SPEED, false)
|
||||||
|
await process_frame
|
||||||
|
var leg_l := _world_bone_segment(model, thigh_l, shin_l)
|
||||||
|
var leg_r := _world_bone_segment(model, thigh_r, shin_r)
|
||||||
|
greatest_rise = maxf(greatest_rise, maxf(leg_l.y, leg_r.y))
|
||||||
|
return greatest_rise
|
||||||
|
|
||||||
|
|
||||||
|
func _wall_jump_exit_leg_rise(model: SkinnedPlayerModel,
|
||||||
|
thigh_l: int, thigh_r: int, shin_l: int, shin_r: int) -> float:
|
||||||
|
# The real controller reports a positive parent velocity on the first frame
|
||||||
|
# after a wall jump, so this exercises WallRun -> Jump rather than Fall.
|
||||||
|
model.set_wall_side(1.0)
|
||||||
|
for _frame in SETTLE_FRAMES:
|
||||||
|
model.update_state("wall_run", WALL_GLIDE_SPEED, false)
|
||||||
|
await process_frame
|
||||||
|
var body := model.get_parent() as CharacterBody3D
|
||||||
|
var greatest_rise := -INF
|
||||||
|
for _frame in 20:
|
||||||
|
if body:
|
||||||
|
body.velocity.y = 8.0
|
||||||
|
model.update_state("air", WALL_GLIDE_SPEED, false)
|
||||||
|
await process_frame
|
||||||
|
var leg_l := _world_bone_segment(model, thigh_l, shin_l)
|
||||||
|
var leg_r := _world_bone_segment(model, thigh_r, shin_r)
|
||||||
|
greatest_rise = maxf(greatest_rise, maxf(leg_l.y, leg_r.y))
|
||||||
|
return greatest_rise
|
||||||
|
|
||||||
|
|
||||||
func _expect(ok: bool, description: String) -> void:
|
func _expect(ok: bool, description: String) -> void:
|
||||||
if ok:
|
if ok:
|
||||||
print(" OK: ", description)
|
print(" OK: ", description)
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ var is_grapple_shooting: bool = false
|
|||||||
|
|
||||||
# ── State tracking ────────────────────────────────────────────────────────────
|
# ── State tracking ────────────────────────────────────────────────────────────
|
||||||
var wall_normal: Vector3 = Vector3.ZERO
|
var wall_normal: Vector3 = Vector3.ZERO
|
||||||
|
var wall_contact_point: Vector3 = Vector3.ZERO
|
||||||
var wall_side: float = 0.0 # -1 left, +1 right, 0 none
|
var wall_side: float = 0.0 # -1 left, +1 right, 0 none
|
||||||
var on_ground: bool = false
|
var on_ground: bool = false
|
||||||
var last_ground_time: float = 0.0
|
var last_ground_time: float = 0.0
|
||||||
@@ -268,6 +269,7 @@ func detect_wall_horizontal() -> Vector3:
|
|||||||
# Wall must be roughly vertical (normal mostly horizontal)
|
# Wall must be roughly vertical (normal mostly horizontal)
|
||||||
if abs(n.y) < 0.3 and n.length_squared() > 0.0:
|
if abs(n.y) < 0.3 and n.length_squared() > 0.0:
|
||||||
wall_normal = n.normalized()
|
wall_normal = n.normalized()
|
||||||
|
wall_contact_point = hit.get("position", origin)
|
||||||
wall_side = side_data["side"]
|
wall_side = side_data["side"]
|
||||||
return wall_normal
|
return wall_normal
|
||||||
|
|
||||||
@@ -288,10 +290,12 @@ func detect_wall_horizontal() -> Vector3:
|
|||||||
var n: Vector3 = hit.get("normal", Vector3.ZERO)
|
var n: Vector3 = hit.get("normal", Vector3.ZERO)
|
||||||
if abs(n.y) < 0.3 and n.length_squared() > 0.0:
|
if abs(n.y) < 0.3 and n.length_squared() > 0.0:
|
||||||
wall_normal = n.normalized()
|
wall_normal = n.normalized()
|
||||||
|
wall_contact_point = hit.get("position", origin)
|
||||||
wall_side = side_data["side"]
|
wall_side = side_data["side"]
|
||||||
return wall_normal
|
return wall_normal
|
||||||
|
|
||||||
wall_normal = Vector3.ZERO
|
wall_normal = Vector3.ZERO
|
||||||
|
wall_contact_point = Vector3.ZERO
|
||||||
wall_side = 0.0
|
wall_side = 0.0
|
||||||
return Vector3.ZERO
|
return Vector3.ZERO
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ var synced_position: Vector3 = Vector3.ZERO
|
|||||||
var synced_velocity: Vector3 = Vector3.ZERO
|
var synced_velocity: Vector3 = Vector3.ZERO
|
||||||
var synced_is_ads: bool = false
|
var synced_is_ads: bool = false
|
||||||
var synced_wall_side: float = 0.0 # -1 wall left, +1 wall right (wall-glide bank)
|
var synced_wall_side: float = 0.0 # -1 wall left, +1 wall right (wall-glide bank)
|
||||||
|
var synced_wall_normal: Vector3 = Vector3.ZERO
|
||||||
|
var synced_wall_contact_point: Vector3 = Vector3.ZERO
|
||||||
var synced_helmet_closed: bool = false
|
var synced_helmet_closed: bool = false
|
||||||
var synced_is_dancing: bool = false # dance emote (B), shown on the model
|
var synced_is_dancing: bool = false # dance emote (B), shown on the model
|
||||||
## Which of the five routines in DanceRoutines is playing. Replicated, so other
|
## Which of the five routines in DanceRoutines is playing. Replicated, so other
|
||||||
@@ -1071,6 +1073,8 @@ func _physics_process(_delta: float) -> void:
|
|||||||
# otherwise the first movement frame still samples last frame's Idle.
|
# otherwise the first movement frame still samples last frame's Idle.
|
||||||
if visual.has_method("set_wall_side"):
|
if visual.has_method("set_wall_side"):
|
||||||
visual.set_wall_side(sm.wall_side)
|
visual.set_wall_side(sm.wall_side)
|
||||||
|
if visual.has_method("set_wall_surface"):
|
||||||
|
visual.set_wall_surface(sm.wall_normal, sm.wall_contact_point)
|
||||||
if visual.has_method("set_wall_glide_motion"):
|
if visual.has_method("set_wall_glide_motion"):
|
||||||
visual.set_wall_glide_motion(velocity)
|
visual.set_wall_glide_motion(velocity)
|
||||||
elif visual.has_method("set_wall_run_motion"):
|
elif visual.has_method("set_wall_run_motion"):
|
||||||
@@ -1089,6 +1093,8 @@ func _physics_process(_delta: float) -> void:
|
|||||||
synced_movement_speed = Vector2(velocity.x, velocity.z).length()
|
synced_movement_speed = Vector2(velocity.x, velocity.z).length()
|
||||||
synced_is_crouching = sm.input_crouch
|
synced_is_crouching = sm.input_crouch
|
||||||
synced_wall_side = sm.wall_side
|
synced_wall_side = sm.wall_side
|
||||||
|
synced_wall_normal = sm.wall_normal
|
||||||
|
synced_wall_contact_point = sm.wall_contact_point
|
||||||
synced_position = position
|
synced_position = position
|
||||||
synced_velocity = velocity
|
synced_velocity = velocity
|
||||||
|
|
||||||
@@ -1167,6 +1173,9 @@ func _process(delta: float) -> void:
|
|||||||
visual.set_locomotion(d.x, d.y, 1.0 if synced_is_ads else 0.0)
|
visual.set_locomotion(d.x, d.y, 1.0 if synced_is_ads else 0.0)
|
||||||
if visual.has_method("set_wall_side"):
|
if visual.has_method("set_wall_side"):
|
||||||
visual.set_wall_side(synced_wall_side)
|
visual.set_wall_side(synced_wall_side)
|
||||||
|
if visual.has_method("set_wall_surface"):
|
||||||
|
visual.set_wall_surface(
|
||||||
|
synced_wall_normal, synced_wall_contact_point)
|
||||||
if visual.has_method("set_wall_glide_motion"):
|
if visual.has_method("set_wall_glide_motion"):
|
||||||
visual.set_wall_glide_motion(synced_velocity)
|
visual.set_wall_glide_motion(synced_velocity)
|
||||||
elif visual.has_method("set_wall_run_motion"):
|
elif visual.has_method("set_wall_run_motion"):
|
||||||
|
|||||||
@@ -153,6 +153,8 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
client_rep_config.add_property(":synced_is_crouching")
|
client_rep_config.add_property(":synced_is_crouching")
|
||||||
client_rep_config.add_property(":synced_is_ads")
|
client_rep_config.add_property(":synced_is_ads")
|
||||||
client_rep_config.add_property(":synced_wall_side")
|
client_rep_config.add_property(":synced_wall_side")
|
||||||
|
client_rep_config.add_property(":synced_wall_normal")
|
||||||
|
client_rep_config.add_property(":synced_wall_contact_point")
|
||||||
client_rep_config.add_property(":synced_helmet_closed")
|
client_rep_config.add_property(":synced_helmet_closed")
|
||||||
client_rep_config.add_property(":synced_is_dancing")
|
client_rep_config.add_property(":synced_is_dancing")
|
||||||
# Which of the five emotes, so other players see the one that was chosen.
|
# Which of the five emotes, so other players see the one that was chosen.
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ func _spawn_player(pid: int) -> CharacterBody3D:
|
|||||||
client_rep_config.add_property(":synced_is_crouching")
|
client_rep_config.add_property(":synced_is_crouching")
|
||||||
client_rep_config.add_property(":synced_is_ads")
|
client_rep_config.add_property(":synced_is_ads")
|
||||||
client_rep_config.add_property(":synced_wall_side")
|
client_rep_config.add_property(":synced_wall_side")
|
||||||
|
client_rep_config.add_property(":synced_wall_normal")
|
||||||
|
client_rep_config.add_property(":synced_wall_contact_point")
|
||||||
client_rep_config.add_property(":synced_helmet_closed")
|
client_rep_config.add_property(":synced_helmet_closed")
|
||||||
client_rep_config.add_property(":synced_is_dancing")
|
client_rep_config.add_property(":synced_is_dancing")
|
||||||
# Which of the five emotes, so other players see the one that was chosen.
|
# Which of the five emotes, so other players see the one that was chosen.
|
||||||
|
|||||||
Reference in New Issue
Block a user