Feat/outline thickness and tp weapon hold #22
@@ -135,6 +135,11 @@ var _cur_fwd: float = 0.0
|
|||||||
var _cur_ads: float = 0.0
|
var _cur_ads: float = 0.0
|
||||||
var _cur_slide: float = 0.0
|
var _cur_slide: float = 0.0
|
||||||
var _cur_wall: float = 0.0
|
var _cur_wall: float = 0.0
|
||||||
|
## Which way the legs are actually travelling, in radians about the character's
|
||||||
|
## own up axis, and whether the locomotion cycle is running backwards to achieve
|
||||||
|
## it. See _update_travel.
|
||||||
|
var _cur_travel: float = 0.0
|
||||||
|
var _travel_reverse: bool = false
|
||||||
var _owner_visible: bool = false
|
var _owner_visible: bool = false
|
||||||
## Horizontal speed from the last update_state, so the lean can scale with how
|
## Horizontal speed from the last update_state, so the lean can scale with how
|
||||||
## fast the character is really moving.
|
## fast the character is really moving.
|
||||||
@@ -612,8 +617,11 @@ func update_state(state: String, speed: float, is_crouching: bool = false) -> vo
|
|||||||
is_loco = true
|
is_loco = true
|
||||||
# Backpedalling: run the cycle BACKWARDS rather than moon-walking with
|
# Backpedalling: run the cycle BACKWARDS rather than moon-walking with
|
||||||
# the forward clip. The shared library has no authored reverse run, and
|
# the forward clip. The shared library has no authored reverse run, and
|
||||||
# a reversed stride reads correctly for a backpedal.
|
# a reversed stride reads correctly for a backpedal. The hip yaw in
|
||||||
if is_loco and _cur_fwd < -0.25:
|
# _update_travel picks the regime and points the legs to match, so the
|
||||||
|
# two must agree — reading a different threshold here used to leave a
|
||||||
|
# band where the feet ran one way and pointed the other.
|
||||||
|
if is_loco and _travel_reverse:
|
||||||
s = -s
|
s = -s
|
||||||
_anim_tree.set("parameters/loco_scale/scale", s)
|
_anim_tree.set("parameters/loco_scale/scale", s)
|
||||||
|
|
||||||
@@ -671,8 +679,10 @@ func _process(delta: float) -> void:
|
|||||||
_cur_slide = lerpf(_cur_slide, slide_target, t)
|
_cur_slide = lerpf(_cur_slide, slide_target, t)
|
||||||
var wall_target := _target_wall if _pose_mod.state == "wall_run" else 0.0
|
var wall_target := _target_wall if _pose_mod.state == "wall_run" else 0.0
|
||||||
_cur_wall = lerpf(_cur_wall, wall_target, lean_t)
|
_cur_wall = lerpf(_cur_wall, wall_target, lean_t)
|
||||||
|
_update_travel(delta, drive)
|
||||||
_pose_mod.strafe = _cur_strafe
|
_pose_mod.strafe = _cur_strafe
|
||||||
_pose_mod.fwd = _cur_fwd
|
_pose_mod.fwd = _cur_fwd
|
||||||
|
_pose_mod.travel_yaw = _cur_travel
|
||||||
_pose_mod.ads = _cur_ads
|
_pose_mod.ads = _cur_ads
|
||||||
_pose_mod.slide = _cur_slide
|
_pose_mod.slide = _cur_slide
|
||||||
_pose_mod.wall = _cur_wall
|
_pose_mod.wall = _cur_wall
|
||||||
@@ -742,6 +752,56 @@ func _process(delta: float) -> void:
|
|||||||
_pose_mod.reload_phase = rl_target
|
_pose_mod.reload_phase = rl_target
|
||||||
|
|
||||||
|
|
||||||
|
## Which way the LEGS should point, and whether the stride runs backwards.
|
||||||
|
##
|
||||||
|
## The shared clip library has one forward locomotion cycle and no strafe or
|
||||||
|
## backpedal clips, so a character sidestepping used to run forwards on the spot
|
||||||
|
## while sliding sideways — nothing in the animation said which way they were
|
||||||
|
## going, and a body lean was carrying the whole burden of telling the player.
|
||||||
|
##
|
||||||
|
## Turning the HIPS toward the travel direction is what actually says it, and it
|
||||||
|
## costs no new animation: the legs are children of the hips, so the whole stride
|
||||||
|
## turns with them, while the spine counter-rotates so the chest and the gun stay
|
||||||
|
## on the aim. It is the same split every third-person shooter uses, and the same
|
||||||
|
## one Hoyoverse's locked-on locomotion uses.
|
||||||
|
##
|
||||||
|
## Beyond about a right angle the hips cannot follow, so the cycle plays in
|
||||||
|
## REVERSE and the legs point the other way instead — a real backpedal rather
|
||||||
|
## than a moonwalk. Which regime is in force is hysteretic, and the yaw is eased
|
||||||
|
## rather than snapped, so switching between them reads as a pivot on the spot,
|
||||||
|
## which is what a person actually does there.
|
||||||
|
## The most the hips may turn away from where the character is facing.
|
||||||
|
##
|
||||||
|
## A right angle would point the legs exactly along a sidestep, but a person
|
||||||
|
## sidestepping does not stand with their hips square to their path — they open
|
||||||
|
## maybe half that and let the feet cross. Past this the silhouette stops reading
|
||||||
|
## as a shooter holding an aim and starts reading as someone who has turned round.
|
||||||
|
const MAX_TRAVEL_YAW := 0.95
|
||||||
|
const TRAVEL_SMOOTH := 9.0
|
||||||
|
const TRAVEL_REVERSE_IN := -0.35
|
||||||
|
const TRAVEL_REVERSE_OUT := -0.12
|
||||||
|
|
||||||
|
func _update_travel(delta: float, drive: float) -> void:
|
||||||
|
var st: float = _target_strafe
|
||||||
|
var fw: float = _target_fwd
|
||||||
|
if _travel_reverse:
|
||||||
|
if fw > TRAVEL_REVERSE_OUT:
|
||||||
|
_travel_reverse = false
|
||||||
|
elif fw < TRAVEL_REVERSE_IN:
|
||||||
|
_travel_reverse = true
|
||||||
|
var want := 0.0
|
||||||
|
if absf(st) > 0.01 or absf(fw) > 0.01:
|
||||||
|
# Skeleton space: the character faces +Z and character-right is -X, so a
|
||||||
|
# travel direction of (strafe right, forward) is (-strafe, 0, fwd). A yaw
|
||||||
|
# of `want` about +Y points the legs along it — or along the opposite of
|
||||||
|
# it when the stride is running backwards.
|
||||||
|
want = atan2(st, -fw) if _travel_reverse else atan2(-st, fw)
|
||||||
|
want = clampf(want, -MAX_TRAVEL_YAW, MAX_TRAVEL_YAW) * drive
|
||||||
|
# Shortest way round, so a pivot never takes the long route.
|
||||||
|
var d := wrapf(want - _cur_travel, -PI, PI)
|
||||||
|
_cur_travel += d * (1.0 - exp(-TRAVEL_SMOOTH * delta))
|
||||||
|
|
||||||
|
|
||||||
## 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
|
||||||
@@ -802,6 +862,12 @@ func get_lean_debug() -> float:
|
|||||||
return _cur_fwd
|
return _cur_fwd
|
||||||
|
|
||||||
|
|
||||||
|
## Whether the locomotion cycle is running backwards, which points the stride
|
||||||
|
## the opposite way to the hips. For debug/travel_dir_check.gd.
|
||||||
|
func stride_reversed_debug() -> bool:
|
||||||
|
return _travel_reverse
|
||||||
|
|
||||||
|
|
||||||
## Clip currently playing. For debug/transition_check.gd.
|
## Clip currently playing. For debug/transition_check.gd.
|
||||||
func current_clip_debug() -> String:
|
func current_clip_debug() -> String:
|
||||||
return _current_clip
|
return _current_clip
|
||||||
@@ -971,6 +1037,9 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
|||||||
var ads: float = 0.0 # 0 hip .. 1 aiming
|
var ads: float = 0.0 # 0 hip .. 1 aiming
|
||||||
var slide: float = 0.0 # 0 .. 1 slide blend
|
var slide: float = 0.0 # 0 .. 1 slide blend
|
||||||
var wall: float = 0.0 # -1 wall left .. +1 wall right (wall-run lean)
|
var wall: float = 0.0 # -1 wall left .. +1 wall right (wall-run lean)
|
||||||
|
# Yaw of the LOWER body about the character's up axis, in radians — which way
|
||||||
|
# the legs are actually travelling. See SkinnedPlayerModel._update_travel.
|
||||||
|
var travel_yaw: float = 0.0
|
||||||
var aim_pitch: float = 0.0 # radians, up positive — upper body follows aim
|
var aim_pitch: float = 0.0 # radians, up positive — upper body follows aim
|
||||||
var recoil: float = 0.0 # decaying shot kick
|
var recoil: float = 0.0 # decaying shot kick
|
||||||
var state: String = "idle"
|
var state: String = "idle"
|
||||||
@@ -1000,6 +1069,11 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
|||||||
# The lean is the ONLY thing that tells a viewer which way this character is
|
# The lean is the ONLY thing that tells a viewer which way this character is
|
||||||
# travelling — the library has one forward locomotion cycle and no strafe
|
# travelling — the library has one forward locomotion cycle and no strafe
|
||||||
# clips — so it has to be legible, not subtle.
|
# clips — so it has to be legible, not subtle.
|
||||||
|
## How much of the hip yaw the spine takes back, so the chest, the head and
|
||||||
|
## the gun stay pointed where the player is aiming. Not all of it: a real
|
||||||
|
## torso does follow the hips a little, and countering the whole thing makes
|
||||||
|
## the waist look broken.
|
||||||
|
const TRAVEL_COUNTER := 0.82
|
||||||
const LEAN_ROLL := 0.42
|
const LEAN_ROLL := 0.42
|
||||||
const LEAN_PITCH := 0.30
|
const LEAN_PITCH := 0.30
|
||||||
const SLIDE_BACK := 0.75 # torso lean-back during slide
|
const SLIDE_BACK := 0.75 # torso lean-back during slide
|
||||||
@@ -1108,16 +1182,42 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
|||||||
for n in ["DEF-spine.002", "DEF-spine.003"]:
|
for n in ["DEF-spine.002", "DEF-spine.003"]:
|
||||||
_add_space(skel, _idx.get(n, -1), back)
|
_add_space(skel, _idx.get(n, -1), back)
|
||||||
|
|
||||||
# Distribute a skeleton-space lean across the spine bones.
|
# Turn the lower body to face the way the character is travelling, and lean
|
||||||
|
# along that direction rather than along the facing.
|
||||||
|
#
|
||||||
|
# The legs hang off the hips, so yawing the hips turns the whole stride — the
|
||||||
|
# one thing that makes a sidestep look like a sidestep when the clip library
|
||||||
|
# has only a forward run. The spine takes most of it back so the chest and the
|
||||||
|
# gun stay on the aim.
|
||||||
|
#
|
||||||
|
# The LEAN has to move into the travel frame with it. Leaning "forward" along
|
||||||
|
# the character's facing while the legs run off to one side leans them
|
||||||
|
# sideways relative to their own stride, which is exactly the sensation of a
|
||||||
|
# character being dragged rather than running.
|
||||||
func _apply_lean(skel: Skeleton3D) -> void:
|
func _apply_lean(skel: Skeleton3D) -> void:
|
||||||
var pitch := fwd * LEAN_PITCH * (1.0 - slide)
|
var yaw := travel_yaw * (1.0 - slide)
|
||||||
var roll := strafe * LEAN_ROLL * (1.0 - slide)
|
# How hard the character is driving, regardless of which way. Signed
|
||||||
if absf(pitch) < 0.001 and absf(roll) < 0.001:
|
# `fwd` is wrong now that the legs turn: a sidestep has fwd near zero and
|
||||||
|
# should still lean into its own stride.
|
||||||
|
var effort := clampf(Vector2(strafe, fwd).length(), 0.0, 1.0)
|
||||||
|
var pitch := effort * LEAN_PITCH * (1.0 - slide)
|
||||||
|
if absf(pitch) < 0.001 and absf(yaw) < 0.001:
|
||||||
return
|
return
|
||||||
var q := Quaternion(Vector3(1, 0, 0), pitch) * Quaternion(Vector3(0, 0, 1), roll)
|
var turn := Quaternion(Vector3(0, 1, 0), yaw)
|
||||||
var per := Quaternion.IDENTITY.slerp(q, 1.0 / SPINE.size())
|
# Pitch about the axis ACROSS the direction of travel, not across the
|
||||||
for n in SPINE:
|
# facing — see the comment above the function.
|
||||||
_add_space(skel, _idx.get(n, -1), per)
|
var lean := Quaternion((turn * Vector3(1, 0, 0)).normalized(), pitch)
|
||||||
|
# The hips carry the yaw, and a little of the lean.
|
||||||
|
_add_space(skel, _idx.get("DEF-hips", -1),
|
||||||
|
turn * Quaternion.IDENTITY.slerp(lean, 0.25))
|
||||||
|
# The torso unwinds the yaw and takes the rest of the lean.
|
||||||
|
var torso: Array = SPINE.slice(1)
|
||||||
|
var n := maxf(torso.size(), 1)
|
||||||
|
var back := Quaternion(Vector3(0, 1, 0), -yaw * TRAVEL_COUNTER)
|
||||||
|
var per_yaw := Quaternion.IDENTITY.slerp(back, 1.0 / n)
|
||||||
|
var per_lean := Quaternion.IDENTITY.slerp(lean, 0.75 / n)
|
||||||
|
for b in torso:
|
||||||
|
_add_space(skel, _idx.get(b, -1), per_yaw * per_lean)
|
||||||
|
|
||||||
# Grapple zip: the whole body pivots to fly along the line to the anchor,
|
# Grapple zip: the whole body pivots to fly along the line to the anchor,
|
||||||
# legs trail behind, and the FREE (left) hand reaches up the rope — the
|
# legs trail behind, and the FREE (left) hand reaches up the rope — the
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
## Dev tool: do the LEGS point where the character is going, and does the CHEST
|
||||||
|
## stay pointed where the player is aiming?
|
||||||
|
##
|
||||||
|
## godot --headless --path . -s res://debug/travel_dir_check.gd -- [skin_glb]
|
||||||
|
##
|
||||||
|
## The clip library has one forward locomotion cycle and no strafe or backpedal
|
||||||
|
## clips, so nothing in the animation itself says which way the character is
|
||||||
|
## travelling. SkinnedPlayerModel._update_travel says it by yawing the hips onto
|
||||||
|
## the travel direction and unwinding that yaw up the spine.
|
||||||
|
##
|
||||||
|
## The number that matters is LEGS OFF BY: how far the stride points from the
|
||||||
|
## direction the character is actually travelling. A backpedal is achieved by
|
||||||
|
## running the cycle BACKWARDS with the hips near square, so it is scored on
|
||||||
|
## where the feet go, not on where the hips face.
|
||||||
|
##
|
||||||
|
## CHEST is reported for information only, and is not a pass/fail. Most of it is
|
||||||
|
## the clip's own torso twist — a run cycle counter-rotates the chest against the
|
||||||
|
## hips by tens of degrees twice a stride — and averaging over a stride only
|
||||||
|
## partly removes it. It is also not what keeps the gun on target: the rifle hold
|
||||||
|
## places the weapon from the SHOULDER and the aim direction and then solves the
|
||||||
|
## arms onto it, so the aim survives whatever the torso is doing. Read this
|
||||||
|
## column as "does the upper body still look square-ish", nothing stronger.
|
||||||
|
|
||||||
|
## label, strafe, forward, the direction of travel this should read as (degrees
|
||||||
|
## about the character's up axis, 0 = straight ahead)
|
||||||
|
const CASES := [
|
||||||
|
["forward", 0.0, 1.0, 0.0],
|
||||||
|
["strafe right", 1.0, 0.0, -90.0],
|
||||||
|
["strafe left", -1.0, 0.0, 90.0],
|
||||||
|
["fwd-right", 0.7, 0.7, -45.0],
|
||||||
|
["fwd-left", -0.7, 0.7, 45.0],
|
||||||
|
["backpedal", 0.0, -1.0, 180.0],
|
||||||
|
["back-right", 0.7, -0.7, -135.0],
|
||||||
|
]
|
||||||
|
const SETTLE := 70
|
||||||
|
## Frames averaged at the end of each case — comfortably more than one stride.
|
||||||
|
const MEASURE_OVER := 40
|
||||||
|
|
||||||
|
var _frames := 0
|
||||||
|
var _model: SkinnedPlayerModel = null
|
||||||
|
var _probe = null
|
||||||
|
var _case := 0
|
||||||
|
var _rows: Array = []
|
||||||
|
var _hips_sum := 0.0
|
||||||
|
var _chest_sum := 0.0
|
||||||
|
var _n := 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var args := OS.get_cmdline_user_args()
|
||||||
|
var path: String = args[0] if args.size() > 0 \
|
||||||
|
else "res://assets/characters/skins/taila.glb"
|
||||||
|
var scene := Node3D.new()
|
||||||
|
root.add_child(scene)
|
||||||
|
current_scene = scene
|
||||||
|
_model = SkinnedPlayerModel.new()
|
||||||
|
_model.model_path = path
|
||||||
|
scene.add_child(_model)
|
||||||
|
|
||||||
|
|
||||||
|
## Bone poses read from INSIDE the modifier pass — Godot restores them after it,
|
||||||
|
## so anything read later is the animation alone with the pose layer missing.
|
||||||
|
class PoseProbe extends SkeletonModifier3D:
|
||||||
|
var pose: Array = []
|
||||||
|
|
||||||
|
func _process_modification() -> void:
|
||||||
|
var skel := get_skeleton()
|
||||||
|
if skel == null:
|
||||||
|
return
|
||||||
|
pose.resize(skel.get_bone_count())
|
||||||
|
for i in skel.get_bone_count():
|
||||||
|
pose[i] = skel.get_bone_global_pose(i)
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> bool:
|
||||||
|
_frames += 1
|
||||||
|
if _frames < 8 or not _model.loaded:
|
||||||
|
return false
|
||||||
|
var skel: Skeleton3D = _model.skeleton
|
||||||
|
if skel == null:
|
||||||
|
return true
|
||||||
|
if _probe == null:
|
||||||
|
_probe = PoseProbe.new()
|
||||||
|
_probe.name = "TravelProbe"
|
||||||
|
skel.add_child(_probe)
|
||||||
|
return false
|
||||||
|
|
||||||
|
var c: Array = CASES[_case]
|
||||||
|
# Run fast enough that the lean, which scales with speed, is fully in.
|
||||||
|
_model.update_state("ground", 9.0, false)
|
||||||
|
_model.set_locomotion(c[1], c[2], 0.0)
|
||||||
|
# AVERAGED over the tail of each case, not sampled at one instant.
|
||||||
|
#
|
||||||
|
# A run cycle twists the torso against the hips by tens of degrees twice per
|
||||||
|
# stride, all of it authored and correct. Reading a single frame therefore
|
||||||
|
# measures wherever the clip happened to be, and the numbers swing far wider
|
||||||
|
# than anything the pose layer does — a backpedal read as 50 degrees of chest
|
||||||
|
# yaw with the hips almost square, which is the clip, not the layer.
|
||||||
|
var phase: int = (_frames - 9) % SETTLE
|
||||||
|
if phase >= SETTLE - MEASURE_OVER and _probe.pose.size() == skel.get_bone_count():
|
||||||
|
_hips_sum += _yaw_of(skel, "hips")
|
||||||
|
_chest_sum += _yaw_of(skel, "spine_top")
|
||||||
|
_n += 1.0
|
||||||
|
if phase != SETTLE - 1:
|
||||||
|
return false
|
||||||
|
|
||||||
|
if _probe.pose.size() == skel.get_bone_count():
|
||||||
|
_rows.append([c[0], _hips_sum / _n, _chest_sum / _n,
|
||||||
|
float(c[3]), _model.stride_reversed_debug()])
|
||||||
|
_hips_sum = 0.0
|
||||||
|
_chest_sum = 0.0
|
||||||
|
_n = 0.0
|
||||||
|
_case += 1
|
||||||
|
if _case >= CASES.size():
|
||||||
|
_report()
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
## Yaw of a bone away from its own rest orientation, in degrees, about the
|
||||||
|
## character's up axis. Measured against REST rather than against the parent, so
|
||||||
|
## it reads as "how far round from square" whatever the rig's bone axes are.
|
||||||
|
func _yaw_of(skel: Skeleton3D, which: String) -> float:
|
||||||
|
var roles: Dictionary = _model._rig_info.get("roles", {})
|
||||||
|
var name := ""
|
||||||
|
if which == "hips":
|
||||||
|
name = String(roles.get("hips", "DEF-hips"))
|
||||||
|
else:
|
||||||
|
var spine: Array = roles.get("spine", [])
|
||||||
|
# The chest: the last spine bone that is not the neck or the head.
|
||||||
|
var neck := String(roles.get("neck", ""))
|
||||||
|
var head := String(roles.get("head", ""))
|
||||||
|
for n in spine:
|
||||||
|
if String(n) != neck and String(n) != head:
|
||||||
|
name = String(n)
|
||||||
|
var b := skel.find_bone(name)
|
||||||
|
if b < 0:
|
||||||
|
return 0.0
|
||||||
|
var now: Basis = (_probe.pose[b] as Transform3D).basis
|
||||||
|
var rest: Basis = skel.get_bone_global_rest(b).basis
|
||||||
|
var delta := rest.inverse() * now
|
||||||
|
# The character faces +Z; how far that axis has swung in the ground plane.
|
||||||
|
var f: Vector3 = rest * (delta * (rest.inverse() * Vector3(0, 0, 1)))
|
||||||
|
return rad_to_deg(atan2(f.x, f.z))
|
||||||
|
|
||||||
|
|
||||||
|
func _report() -> void:
|
||||||
|
print("\n=== does the body say which way it is going? ===")
|
||||||
|
print(" %-13s %9s %9s %9s %s" % [
|
||||||
|
"input", "hips", "chest", "want legs", "stride"])
|
||||||
|
for r in _rows:
|
||||||
|
# A reversed stride points the feet the opposite way to the hips.
|
||||||
|
var legs: float = r[1] + (180.0 if r[4] else 0.0)
|
||||||
|
var err: float = absf(wrapf(legs - r[3], -180.0, 180.0))
|
||||||
|
print(" %-13s %+8.1f° %+8.1f° %+8.1f° %-8s legs off by %5.1f°%s" % [
|
||||||
|
r[0], r[1], r[2], r[3], "reversed" if r[4] else "forward", err,
|
||||||
|
" CHEST OFF AIM" if absf(r[2]) > 25.0 else ""])
|
||||||
|
print("\n 'legs off by' is how far the stride points from the travel")
|
||||||
|
print(" direction. The hips are capped (see MAX_TRAVEL_YAW), so a pure")
|
||||||
|
print(" sidestep is expected to fall short — a shooter keeps its aim.\n")
|
||||||
Reference in New Issue
Block a user