feat(client): follow-facing — body commits to path, mouse drives layered head/torso look-at (T-1088)
During an RMB path-follow the body yaw locks to the leg direction (rig commit_body_to_motion, from interpolated velocity — immune to SetFacing interleaving between throttled steps). The mouse instead drives a layered look-at: LookAtModifier3D pair on Head (±70°) + spine_02 (±30° torso twist for looking far lateral/behind — past their sum the character physically cannot look further without turning). Forward axis measured from the armature rest pose (+Z), not guessed. Influence fades in/out on follow start/end. Pure client presentation per D-249; SetFacing still rides the wire, so the server vision cone follows the mouse while walking — the character looks where the player points. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
class_name SandboxHeadLook
|
||||
extends RefCounted
|
||||
## T-1088 follow-facing refinement: layered mouse look-at while a path-follow
|
||||
## commits the body to the leg direction (locomotion_rig.commit_body_to_motion).
|
||||
##
|
||||
## Two LookAtModifier3D nodes under the CharacterVisual skeleton aim at the
|
||||
## shared HeadAimTarget: spine_02 contributes a small torso twist (looking far
|
||||
## lateral/behind), Head does the bulk. Past their combined limits the character
|
||||
## physically cannot look further without turning — correct. Pure client
|
||||
## presentation on the D-249 model; SetFacing still rides the wire, so the
|
||||
## SERVER vision cone follows the mouse while the body walks the path.
|
||||
##
|
||||
## Bone axes verified from armature.glb rest pose (bone_axes_dump.py): both
|
||||
## bones are Y-up with +Z as the facing axis in Godot bone space — forward_axis
|
||||
## is set from measurement, not guessed (the E/W-mirror lesson).
|
||||
|
||||
var _mods: Array[LookAtModifier3D] = []
|
||||
var _target_influence: float = 0.0
|
||||
|
||||
|
||||
## Create + configure the modifier pair. Call once after CharacterVisual has
|
||||
## built its skeleton (the sandbox never rebuilds the descriptor; a rebuild
|
||||
## would free the skeleton and require a re-setup).
|
||||
func setup(skeleton: Skeleton3D, aim_target: Node3D) -> void:
|
||||
_mods.clear()
|
||||
var configs: Array[Dictionary] = [
|
||||
{"bone": "spine_02", "limit_deg": SandboxConstants.HEAD_LOOK_TORSO_LIMIT_DEG},
|
||||
{"bone": "Head", "limit_deg": SandboxConstants.HEAD_LOOK_HEAD_LIMIT_DEG},
|
||||
]
|
||||
for cfg in configs:
|
||||
var m := LookAtModifier3D.new()
|
||||
m.name = "LookAt_%s" % cfg["bone"]
|
||||
skeleton.add_child(m)
|
||||
m.bone_name = str(cfg["bone"])
|
||||
m.target_node = m.get_path_to(aim_target)
|
||||
m.forward_axis = SkeletonModifier3D.BONE_AXIS_PLUS_Z
|
||||
m.primary_rotation_axis = Vector3.AXIS_Y
|
||||
m.use_secondary_rotation = true # slight pitch toward the ground point
|
||||
m.use_angle_limitation = true
|
||||
m.symmetry_limitation = true
|
||||
m.primary_limit_angle = deg_to_rad(float(cfg["limit_deg"]))
|
||||
m.influence = 0.0
|
||||
_mods.append(m)
|
||||
|
||||
|
||||
## Fade the look-at in while a follow is active, out otherwise.
|
||||
func set_active(active: bool) -> void:
|
||||
_target_influence = 1.0 if active else 0.0
|
||||
|
||||
|
||||
func update(delta: float) -> void:
|
||||
if _mods.is_empty():
|
||||
return
|
||||
var step := delta / maxf(SandboxConstants.HEAD_LOOK_FADE_S, 0.001)
|
||||
for m in _mods:
|
||||
if is_instance_valid(m):
|
||||
m.influence = move_toward(m.influence, _target_influence, step)
|
||||
Reference in New Issue
Block a user