Feat/outline thickness and tp weapon hold #22

Merged
Dotts merged 43 commits from feat/outline-thickness-and-tp-weapon-hold into main 2026-07-27 23:22:53 -07:00
2 changed files with 68 additions and 17 deletions
Showing only changes of commit 2a6b321984 - Show all commits
@@ -225,10 +225,33 @@ Blender-export `IndexFinger1_L`. Segments are ordered by DEPTH BELOW THE HAND,
not by the number in the name — the numbering is not consistent between families, not by the number in the name — the numbering is not consistent between families,
but the hierarchy always runs knuckle to fingertip. but the hierarchy always runs knuckle to fingertip.
**The support hand came out upside down**, because its orientation was built as
a shortest arc plus a constant twist: align the hand's forearm line to the barrel
(`Quaternion(fa_rest_dir, aim_dir)`), then add 0.5 rad of roll. A shortest arc
says NOTHING about roll — it is the minimal rotation between two directions — so
the entire roll came from that constant, and a constant is right only for the rig
it was tuned on.
Orienting a hand onto something it grips is a FRAME-TO-FRAME problem, and framing
it that way leaves nothing free to guess:
```
curl axis must lie along the object's axis, or the fingers close ACROSS the
handguard instead of around it
palm must face the object — up, for a hand supporting from underneath
along falls out of the other two (palm x curl)
```
Map the hand's rest anatomical frame onto that target and the roll is determined,
not chosen. Verified on both the Rigify-named mannequin and the VRoid-named
Kiyoko: fingers wrap the handguard from below, over the top.
**Rule:** anything expressed as a constant in a rig's local frame — a mount **Rule:** anything expressed as a constant in a rig's local frame — a mount
rotation, a grip offset, a curl axis, a weapon size — is a guess about one rotation, a grip offset, a curl axis, a weapon size, a wrist twist — is a guess
skeleton. Derive it from the skeleton, or hand it to a solver that already knows about one skeleton. Derive it from the skeleton, or hand it to a solver that
the answer. already knows the answer. And when a rotation needs a specific ROLL, never build
it from a shortest arc: that operator has no opinion about roll, so whatever you
add afterwards is doing all the work.
## 6. Some sources are not salvageable, and the gate should say so ## 6. Some sources are not salvageable, and the gate should say so
+42 -14
View File
@@ -1174,6 +1174,8 @@ class ShooterPoseModifier extends SkeletonModifier3D:
var fingers: Dictionary = {} var fingers: Dictionary = {}
var _fing: Dictionary = {} # same, resolved to bone indices var _fing: Dictionary = {} # same, resolved to bone indices
var _curl: Dictionary = {} # "L"/"R" -> curl axis in the rest frame var _curl: Dictionary = {} # "L"/"R" -> curl axis in the rest frame
## "L"/"R" -> Basis(along, palm, curl), the hand's anatomy in the rest pose.
var _hand_frame: Dictionary = {}
func _resolve() -> void: func _resolve() -> void:
var skel := get_skeleton() var skel := get_skeleton()
@@ -1255,7 +1257,19 @@ class ShooterPoseModifier extends SkeletonModifier3D:
palm -= along * palm.dot(along) palm -= along * palm.dot(along)
if palm.length() < 0.0001: if palm.length() < 0.0001:
continue continue
_curl[side] = along.cross(palm.normalized()).normalized() palm = palm.normalized()
var curl := along.cross(palm).normalized()
_curl[side] = curl
# The whole hand as an ANATOMICAL FRAME, not just the curl axis.
#
# Orienting a hand onto something it is gripping is a frame-to-frame
# problem: the fingers have to wrap AROUND the object, so the curl
# axis must lie along the object's axis, and the palm has to face it.
# Both are answered at once by mapping this frame onto the target's.
# Building the rotation from a shortest arc plus a constant twist —
# which is what was here — leaves the roll about the barrel
# unspecified, so the support hand landed upside down.
_hand_frame[side] = Basis(along, palm, curl)
if OS.has_environment("HAND_DEBUG"): if OS.has_environment("HAND_DEBUG"):
print("HANDS fingers=%d resolved=%d curl=%s" % [ print("HANDS fingers=%d resolved=%d curl=%s" % [
fingers.size(), _fing.size(), _curl]) fingers.size(), _fing.size(), _curl])
@@ -1467,7 +1481,6 @@ class ShooterPoseModifier extends SkeletonModifier3D:
const POLE_L_HIP := Vector3(0.45, -0.90, -0.10) const POLE_L_HIP := Vector3(0.45, -0.90, -0.10)
const POLE_L_ADS := Vector3(0.30, -0.95, -0.05) const POLE_L_ADS := Vector3(0.30, -0.95, -0.05)
const R_HAND_TWIST := 0.0 const R_HAND_TWIST := 0.0
const L_HAND_TWIST := 0.5
func _apply_rifle_hold(skel: Skeleton3D) -> void: func _apply_rifle_hold(skel: Skeleton3D) -> void:
var ua_r: int = _idx.get("DEF-upper_arm.R", -1) var ua_r: int = _idx.get("DEF-upper_arm.R", -1)
@@ -1570,19 +1583,34 @@ class ShooterPoseModifier extends SkeletonModifier3D:
arc = Quaternion(aim_dir, roll + R_HAND_TWIST) * arc arc = Quaternion(aim_dir, roll + R_HAND_TWIST) * arc
_set_global_rot(skel, hand, g_fa_r, arc, _hold_r) _set_global_rot(skel, hand, g_fa_r, arc, _hold_r)
# 6. Support hand: palm wraps the handguard, following its forearm. # 6. Support hand: WRAP the handguard.
if _hold_l > 0.001 and g_fa_l != Quaternion.IDENTITY and ua_l >= 0: #
# Built as a frame, not as an arc plus a twist. A hand gripping a
# cylinder has its fingers curling AROUND that cylinder, which fixes two
# things at once and leaves nothing free:
#
# curl axis must lie along the BARREL, or the fingers close across
# the handguard instead of around it
# palm must face the barrel — up, for a hand supporting from
# underneath
#
# The old version aligned the hand's forearm line to the barrel with a
# shortest arc and then added a constant 0.5 rad twist. A shortest arc
# says nothing about roll, so the roll came entirely from that constant,
# and a constant is only ever right for the one rig it was tuned on — the
# support hand came out upside down.
if _hold_l > 0.001 and g_fa_l != Quaternion.IDENTITY and ua_l >= 0 \
and _hand_frame.has("L"):
var hand_l: int = _idx.get("DEF-hand.L", -1) var hand_l: int = _idx.get("DEF-hand.L", -1)
var fa_l_idx: int = _idx.get("DEF-forearm.L", -1) if hand_l >= 0:
if hand_l >= 0 and fa_l_idx >= 0: # -aim_dir so the hand comes at the handguard from the body side
var fa_o := skel.get_bone_global_rest(fa_l_idx).origin # rather than reaching over it backwards.
var hand_o := skel.get_bone_global_rest(hand_l).origin var want := Basis(gun_up.cross(-aim_dir).normalized(), gun_up,
var fa_rest_dir := (hand_o - fa_o).normalized() -aim_dir)
var hand_rest_q := skel.get_bone_global_rest(hand_l).basis.get_rotation_quaternion() var rest: Basis = _hand_frame["L"]
# Point the palm along the barrel so the fingers close over it. var g_hand := (want * rest.inverse()).get_rotation_quaternion() \
var g_hand := Quaternion(aim_dir, L_HAND_TWIST) \ * skel.get_bone_global_rest(hand_l).basis.get_rotation_quaternion()
* Quaternion(fa_rest_dir, aim_dir) * hand_rest_q _set_global_rot(skel, hand_l, g_fa_l, g_hand.normalized(), _hold_l)
_set_global_rot(skel, hand_l, g_fa_l, g_hand, _hold_l)
# How far each segment of a finger closes, knuckle -> tip, in radians. # How far each segment of a finger closes, knuckle -> tip, in radians.
# #