Feat/outline thickness and tp weapon hold #22
@@ -143,6 +143,14 @@ standing still changed. `pitch` exists at low ready only: down the sights the
|
|||||||
muzzle follows the camera, so there is nothing there to tune, and a slider that
|
muzzle follows the camera, so there is nothing there to tune, and a slider that
|
||||||
does nothing is worse than a missing one.
|
does nothing is worse than a missing one.
|
||||||
|
|
||||||
|
**The wrists turn the HAND, not the gun.** The weapon is a child of a
|
||||||
|
BoneAttachment3D on the trigger hand, so the two are welded by construction: a
|
||||||
|
wrist rotation swings the barrel off the aim line and takes every control that
|
||||||
|
could correct it along with it, which made the knob useless for aligning a hand
|
||||||
|
to a gun. `ShooterPoseModifier.wrist_comp_r` is the exact counter-rotation in
|
||||||
|
the hand's local frame, and `SkinnedPlayerModel._hold_weapon_still` applies it.
|
||||||
|
To rotate the GUN inside the hand instead, use the grip rotation in ANCHORS.
|
||||||
|
|
||||||
Knobs that describe the WEAPON and the hands on it — where each hand sits along
|
Knobs that describe the WEAPON and the hands on it — where each hand sits along
|
||||||
it and off its barrel line, the finger curls, the weapon size — are shared,
|
it and off its barrel line, the finger curls, the weapon size — are shared,
|
||||||
because shouldering a gun does not move the hand along it. Both wrists take
|
because shouldering a gun does not move the hand along it. Both wrists take
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ Two related traps:
|
|||||||
| `anchor_shift_check.gd` | the hand anchors move in the GUN's frame, both poses | 0 failures |
|
| `anchor_shift_check.gd` | the hand anchors move in the GUN's frame, both poses | 0 failures |
|
||||||
| `anchor_drag_check.gd` | dragging a marker writes the knob the mouse asked for | 0 failures |
|
| `anchor_drag_check.gd` | dragging a marker writes the knob the mouse asked for | 0 failures |
|
||||||
| `hold_pose_check.gd` | the lab shows only the selected pose's knobs; every wrist axis turns its hand | 0 failures |
|
| `hold_pose_check.gd` | the lab shows only the selected pose's knobs; every wrist axis turns its hand | 0 failures |
|
||||||
|
| `wrist_gun_check.gd` | the wrist turns the hand and NOT the gun welded to it | hand ~28°, gun < 1° |
|
||||||
| `anim_capture.gd` / `orbit_capture.gd` | renders, for looking | — |
|
| `anim_capture.gd` / `orbit_capture.gd` | renders, for looking | — |
|
||||||
| `roster_capture.gd` | one photo of every character, from the picker | — |
|
| `roster_capture.gd` | one photo of every character, from the picker | — |
|
||||||
| `ui_capture.gd` | one photo of every menu screen | — |
|
| `ui_capture.gd` | one photo of every menu screen | — |
|
||||||
|
|||||||
@@ -106,6 +106,9 @@ var hold_tune: Dictionary = {}
|
|||||||
## Live anchor overrides — where the grip sits in the palm, and how the gun
|
## Live anchor overrides — where the grip sits in the palm, and how the gun
|
||||||
## rolls in the fingers. Same two sources as hold_tune. See RigAnchors.
|
## rolls in the fingers. Same two sources as hold_tune. See RigAnchors.
|
||||||
var anchors: Dictionary = {}
|
var anchors: Dictionary = {}
|
||||||
|
## The weapon's local transform in the hand as `_measure_weapon` left it, before
|
||||||
|
## the wrist counter-rotation. See `ShooterPoseModifier.wrist_comp_r`.
|
||||||
|
var _weapon_seat: Transform3D = Transform3D.IDENTITY
|
||||||
|
|
||||||
# Animation blending: locomotion plays full-body through a Transition node;
|
# Animation blending: locomotion plays full-body through a Transition node;
|
||||||
# gameplay one-shots (reload/throw/shoot/hit) play through an
|
# gameplay one-shots (reload/throw/shoot/hit) play through an
|
||||||
@@ -708,6 +711,7 @@ func _process(delta: float) -> void:
|
|||||||
_update_cloth_lod(delta)
|
_update_cloth_lod(delta)
|
||||||
if not _pose_mod:
|
if not _pose_mod:
|
||||||
return
|
return
|
||||||
|
_hold_weapon_still()
|
||||||
var t := 1.0 - exp(-POSE_SMOOTH * delta)
|
var t := 1.0 - exp(-POSE_SMOOTH * delta)
|
||||||
# The body lean gets its own, much slower rate, and is scaled by how fast the
|
# The body lean gets its own, much slower rate, and is scaled by how fast the
|
||||||
# character is ACTUALLY moving rather than by which key is held.
|
# character is ACTUALLY moving rather than by which key is held.
|
||||||
@@ -946,6 +950,29 @@ func set_anchors(a: Dictionary) -> void:
|
|||||||
_reseat_weapon()
|
_reseat_weapon()
|
||||||
|
|
||||||
|
|
||||||
|
## Keep the weapon where the solver put it while the wrist turns under it.
|
||||||
|
##
|
||||||
|
## The two are welded by construction — the gun is a child of a BoneAttachment3D
|
||||||
|
## on the trigger hand — so a wrist rotation swings the barrel off the aim line
|
||||||
|
## and takes every control that could correct it along for the ride. The pose
|
||||||
|
## layer works out the exact counter-rotation in the hand's own local frame;
|
||||||
|
## this applies it.
|
||||||
|
##
|
||||||
|
## Deliberately in `_process` rather than inside the modifier pass. The gun's
|
||||||
|
## mount is not something the skeleton owns, and the value being compensated
|
||||||
|
## only changes when a slider moves or the ADS blend travels, so being one frame
|
||||||
|
## behind is a rotation of a fraction of a degree that nothing can see. Reaching
|
||||||
|
## into the modifier to touch a scene node would be worse.
|
||||||
|
func _hold_weapon_still() -> void:
|
||||||
|
if _weapon_attachment == null or _weapon_attachment.get_child_count() == 0:
|
||||||
|
return
|
||||||
|
var w := _weapon_attachment.get_child(0) as Node3D
|
||||||
|
if w == null:
|
||||||
|
return
|
||||||
|
var comp: Quaternion = _pose_mod.wrist_comp_r
|
||||||
|
w.transform = Transform3D(Basis(comp), Vector3.ZERO) * _weapon_seat
|
||||||
|
|
||||||
|
|
||||||
## Re-apply the grip anchor and re-measure, after either table changed.
|
## Re-apply the grip anchor and re-measure, after either table changed.
|
||||||
##
|
##
|
||||||
## `_measure_weapon` reads the weapon's transform to work out where its grip and
|
## `_measure_weapon` reads the weapon's transform to work out where its grip and
|
||||||
@@ -1173,6 +1200,11 @@ func _measure_weapon(w: Node3D) -> void:
|
|||||||
if hold_tune.get("gun_stock", 0.0) > 0.0001:
|
if hold_tune.get("gun_stock", 0.0) > 0.0001:
|
||||||
_pose_mod.gun_stock = float(hold_tune["gun_stock"])
|
_pose_mod.gun_stock = float(hold_tune["gun_stock"])
|
||||||
_pose_mod.tune = hold_tune
|
_pose_mod.tune = hold_tune
|
||||||
|
# Where the weapon sits in the hand once everything derived and tuned has
|
||||||
|
# been applied. Kept because `_process` re-derives the mount every frame from
|
||||||
|
# it plus the wrist counter-rotation, and recomputing the seat instead would
|
||||||
|
# re-run this whole measurement sixty times a second.
|
||||||
|
_weapon_seat = w.transform
|
||||||
|
|
||||||
|
|
||||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||||
@@ -1312,6 +1344,18 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
|||||||
## expressed in; without it, dragging left would mean something different at
|
## expressed in; without it, dragging left would mean something different at
|
||||||
## every pitch of the weapon.
|
## every pitch of the weapon.
|
||||||
var dbg_gun_basis: Basis = Basis.IDENTITY
|
var dbg_gun_basis: Basis = Basis.IDENTITY
|
||||||
|
## What the weapon mount must be rotated by, in the trigger hand's own local
|
||||||
|
## frame, to undo `wrist_r` — so the hand turns and the GUN does not.
|
||||||
|
##
|
||||||
|
## The weapon is parented to a BoneAttachment3D on that hand, so without this
|
||||||
|
## the two are welded: rotating the wrist swings the barrel off the aim line,
|
||||||
|
## and there is no second control that could bring it back, because every
|
||||||
|
## control that moves the gun is expressed relative to the same hand. The
|
||||||
|
## wrist knob was therefore unusable for the one thing it exists for.
|
||||||
|
##
|
||||||
|
## Identity when `wrist_r` is untuned, so a character nobody has touched
|
||||||
|
## mounts its weapon exactly as before.
|
||||||
|
var wrist_comp_r: Quaternion = Quaternion.IDENTITY
|
||||||
|
|
||||||
func _t(key: String, fallback: float) -> float:
|
func _t(key: String, fallback: float) -> float:
|
||||||
return float(tune.get(key, fallback))
|
return float(tune.get(key, fallback))
|
||||||
@@ -1772,8 +1816,22 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
|||||||
# the barrel has to lie on the aim line, which fixes two of the
|
# the barrel has to lie on the aim line, which fixes two of the
|
||||||
# hand's three freedoms, but nothing fixes how far the wrist is
|
# hand's three freedoms, but nothing fixes how far the wrist is
|
||||||
# cocked or broken, and those were unreachable.
|
# cocked or broken, and those were unreachable.
|
||||||
arc = _wrist("wrist_r", side, gun_up, aim_dir) * arc
|
#
|
||||||
_set_global_rot(skel, hand, g_fa_r, arc, _hold_r)
|
# The gun hangs off THIS bone, so rotating it carries the gun
|
||||||
|
# along and the barrel comes off the aim line — which made the
|
||||||
|
# knob useless for its actual purpose, since there was then no
|
||||||
|
# way to align the hand to a gun that had moved with it. Both
|
||||||
|
# outcomes are computed, the hand takes the rotated one, and the
|
||||||
|
# difference between them is published as the counter-rotation
|
||||||
|
# the weapon mount needs to stay exactly where the solver put it.
|
||||||
|
# See `wrist_comp_r`.
|
||||||
|
var cur := skel.get_bone_pose_rotation(hand)
|
||||||
|
var local_free := (g_fa_r.inverse() * arc).normalized()
|
||||||
|
var wrist_q := _wrist("wrist_r", side, gun_up, aim_dir)
|
||||||
|
var local_wrist := (g_fa_r.inverse() * (wrist_q * arc)).normalized()
|
||||||
|
var applied := cur.slerp(local_wrist, _hold_r)
|
||||||
|
wrist_comp_r = applied.inverse() * cur.slerp(local_free, _hold_r)
|
||||||
|
skel.set_bone_pose_rotation(hand, applied)
|
||||||
|
|
||||||
# 6. Support hand: WRAP the handguard.
|
# 6. Support hand: WRAP the handguard.
|
||||||
#
|
#
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dydhylqo46x1n
|
||||||
@@ -49,6 +49,18 @@ extends Node3D
|
|||||||
## If a hand is not ON its marker, the IK could not reach — a different problem
|
## If a hand is not ON its marker, the IK could not reach — a different problem
|
||||||
## from the marker being in the wrong place, and dragging the marker further will
|
## from the marker being in the wrong place, and dragging the marker further will
|
||||||
## not fix it.
|
## not fix it.
|
||||||
|
##
|
||||||
|
## HAND versus GUN. These rotate different things, and the split is the point:
|
||||||
|
##
|
||||||
|
## TRIGGER/SUPPORT WRIST (hold) turns the HAND. The gun stays exactly where
|
||||||
|
## the solver put it, on the aim line.
|
||||||
|
## Grip roll/pitch/yaw (anchors) turns the GUN inside the hand.
|
||||||
|
##
|
||||||
|
## The weapon is a child of a BoneAttachment3D on the trigger hand, so the two
|
||||||
|
## used to be welded — every degree the wrist turned swung the barrel the same
|
||||||
|
## degree, and took with it every control that could have corrected for it. The
|
||||||
|
## pose layer now publishes the counter-rotation and the mount applies it, so
|
||||||
|
## the wrist sliders align the hand TO the gun rather than dragging it around.
|
||||||
|
|
||||||
const POSES := [
|
const POSES := [
|
||||||
["Low ready", "ground", 0.0, 0.0],
|
["Low ready", "ground", 0.0, 0.0],
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
## Does rotating the trigger wrist turn the HAND without taking the GUN with it?
|
||||||
|
##
|
||||||
|
## The two are welded by construction: the weapon is a child of a
|
||||||
|
## BoneAttachment3D on the trigger hand, so every degree the wrist turns swings
|
||||||
|
## the barrel the same degree off the aim line — and takes with it every control
|
||||||
|
## that could have corrected for it, because they are all expressed relative to
|
||||||
|
## that same hand. The wrist knob was therefore unusable for the one thing it
|
||||||
|
## exists for, which is aligning a hand to a gun.
|
||||||
|
##
|
||||||
|
## Both halves have to be asserted, and the second is the one that was broken:
|
||||||
|
##
|
||||||
|
## the HAND turns or the knob does nothing
|
||||||
|
## the GUN does not or the knob cannot be used
|
||||||
|
##
|
||||||
|
## Measured through a PoseProbe for the hand, because Godot restores every
|
||||||
|
## bone's local pose after the modifier pass and reading it from here would
|
||||||
|
## report the animation rather than the hold. The gun is a scene node, so its
|
||||||
|
## own global transform is the truth and no probe is needed.
|
||||||
|
##
|
||||||
|
## godot --path . -s res://debug/wrist_gun_check.gd
|
||||||
|
|
||||||
|
const LAB := "res://debug/rig_lab.tscn"
|
||||||
|
## Large on purpose. A small twist could hide behind the tolerance of a gun that
|
||||||
|
## really was following the hand.
|
||||||
|
const TWIST := 0.5
|
||||||
|
## Degrees the barrel may wander. Not zero: the hand's rotation feeds the IK
|
||||||
|
## chain that positions the whole arm, so the shoulder and the aim line settle
|
||||||
|
## very slightly differently — but a gun still welded to the wrist would move by
|
||||||
|
## something on the order of TWIST, which is 28 degrees.
|
||||||
|
const GUN_TOLERANCE := 3.0
|
||||||
|
const HAND_MIN := 5.0
|
||||||
|
|
||||||
|
var _fails := 0
|
||||||
|
var _probe: PoseProbe = null
|
||||||
|
|
||||||
|
|
||||||
|
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 _init() -> void:
|
||||||
|
await process_frame
|
||||||
|
var lab: Node = load(LAB).instantiate()
|
||||||
|
root.add_child(lab)
|
||||||
|
for _i in 200:
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
var model = lab._model
|
||||||
|
if model == null or model._pose_mod == null or model.skeleton == null:
|
||||||
|
_expect(false, "the lab built a character holding a weapon")
|
||||||
|
_done()
|
||||||
|
return
|
||||||
|
var gun := _gun(model)
|
||||||
|
if gun == null:
|
||||||
|
_expect(false, "the character mounted a weapon")
|
||||||
|
_done()
|
||||||
|
return
|
||||||
|
_expect(true, "the lab built a character holding a weapon")
|
||||||
|
|
||||||
|
_probe = PoseProbe.new()
|
||||||
|
_probe.name = "WristGunProbe"
|
||||||
|
model.skeleton.add_child(_probe)
|
||||||
|
for _i in 10:
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
var hand: int = _role_bone(model, "hand.R")
|
||||||
|
for pose in ["hip", "ads"]:
|
||||||
|
lab._pose = 0 if pose == "hip" else 1
|
||||||
|
for axis in 3:
|
||||||
|
var v := Vector3.ZERO
|
||||||
|
v[axis] = TWIST
|
||||||
|
model.set_hold_tuning({})
|
||||||
|
await _settle(lab, pose)
|
||||||
|
var hand0 := _probe_rot(hand)
|
||||||
|
var gun0: Basis = gun.global_transform.basis.orthonormalized()
|
||||||
|
|
||||||
|
model.set_hold_tuning({"wrist_r_%s" % pose: v})
|
||||||
|
await _settle(lab, pose)
|
||||||
|
var hand1 := _probe_rot(hand)
|
||||||
|
var gun1: Basis = gun.global_transform.basis.orthonormalized()
|
||||||
|
|
||||||
|
var hand_moved := rad_to_deg(hand0.angle_to(hand1))
|
||||||
|
var gun_moved := rad_to_deg(
|
||||||
|
gun0.get_rotation_quaternion().angle_to(gun1.get_rotation_quaternion()))
|
||||||
|
_expect(hand_moved >= HAND_MIN,
|
||||||
|
"%s axis %d: the hand turned %.1f deg" % [pose, axis, hand_moved])
|
||||||
|
_expect(gun_moved <= GUN_TOLERANCE,
|
||||||
|
"%s axis %d: the gun stayed put (%.1f deg)" % [pose, axis, gun_moved])
|
||||||
|
model.set_hold_tuning({})
|
||||||
|
_done()
|
||||||
|
|
||||||
|
|
||||||
|
func _settle(lab: Node, pose: String) -> void:
|
||||||
|
lab._model.update_state("ground", 0.0, false)
|
||||||
|
lab._model.set_locomotion(0.0, 0.0, 1.0 if pose == "ads" else 0.0)
|
||||||
|
for _i in 60:
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
|
func _gun(model) -> Node3D:
|
||||||
|
if model.skeleton == null:
|
||||||
|
return null
|
||||||
|
var attach: Node = model.skeleton.get_node_or_null("WeaponAttachment")
|
||||||
|
if attach == null or attach.get_child_count() == 0:
|
||||||
|
return null
|
||||||
|
return attach.get_child(0) as Node3D
|
||||||
|
|
||||||
|
|
||||||
|
func _probe_rot(bone: int) -> Quaternion:
|
||||||
|
if _probe == null or bone < 0 or bone >= _probe.pose.size():
|
||||||
|
return Quaternion.IDENTITY
|
||||||
|
var t: Transform3D = _probe.pose[bone]
|
||||||
|
return t.basis.orthonormalized().get_rotation_quaternion()
|
||||||
|
|
||||||
|
|
||||||
|
func _role_bone(model, role: String) -> int:
|
||||||
|
var name: String = model._rig_info.get("roles", {}).get(role, "")
|
||||||
|
return model.skeleton.find_bone(name) if name != "" else -1
|
||||||
|
|
||||||
|
|
||||||
|
func _expect(ok: bool, what: String) -> void:
|
||||||
|
if ok:
|
||||||
|
print(" OK: %s" % what)
|
||||||
|
else:
|
||||||
|
print(" FAIL: %s" % what)
|
||||||
|
_fails += 1
|
||||||
|
|
||||||
|
|
||||||
|
func _done() -> void:
|
||||||
|
print("\n=== WRIST vs GUN ===\nFailures: %d" % _fails)
|
||||||
|
quit(1 if _fails > 0 else 0)
|
||||||
Reference in New Issue
Block a user