fix(characters): rig-independent weapon mount, measured facing, revert Miku
WEAPON MOUNT, all models. set_weapon() seated the gun with a fixed rotation_degrees = (0, 90, -90). A bone attachment is expressed in the BONE's axes and no two rigs agree on those, so one constant mounted the weapon differently on every character. It never needed to be right: the pose layer aims the gun by rotating the wrist until the weapon's forward lies on the aim line, so the identity means "forward is the hand bone's -Z", which is true on any rig, and the wrist absorbs the roll. The grip now sits at the bone origin, so the gun is in the hand rather than at an offset from a differently-oriented bone. Verified by render on kiyoko: rifle shouldered, both hands on it. FACING. flatten_and_scale() now measures toes-versus-ankles and snaps the character to face Blender -Y, the convention the runtime's blanket flip is built around. Kiyoko was 180 degrees off. Snapped to the nearest quarter turn so splayed feet in a rest pose are not read as a turned character. MIKU. Re-imported without --grow-cloth. Her grown hair chains were the cause of the stretching: cloth_bones.py clears a vertex's body weights and re-assigns it to the fitted polyline, so a poor fit does not degrade to "stiff", it degrades to "torn". She now has no hair simulation — stiff but correct — until that tool blends against the weights it replaces instead of destroying them. ARIA'S SKIRT IS NOT SIMULATED. Worth recording plainly, because it looks better than Taila's and the obvious conclusion is the wrong one: aria has ZERO cloth chains. Her skirt never clips because it is rigidly skinned and follows the legs it is weighted to. There is nothing to port to Taila except switching her simulation off. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
27c4117c25
commit
c8b0337aa7
@@ -134,9 +134,13 @@ faces the other way, the two compose to a character running backwards — and
|
|||||||
nothing anywhere measures the finished result.
|
nothing anywhere measures the finished result.
|
||||||
|
|
||||||
**Rule:** facing is measurable from the skeleton — the toes are forward of the
|
**Rule:** facing is measurable from the skeleton — the toes are forward of the
|
||||||
ankles, and the nose/head is forward of the spine. Assert it on the OUTPUT, after
|
ankles. `flatten_and_scale()` now snaps that to Blender -Y, the convention the
|
||||||
every transform has been applied. A blanket constant like `facing_flip` is a
|
runtime flip is built around, so every character leaves the pipeline pointing the
|
||||||
guess about the source that must be replaced by a measurement.
|
same way whatever the source did. Kiyoko was 180 degrees off and is now correct.
|
||||||
|
|
||||||
|
Snap to the nearest QUARTER TURN, not to the measured angle: a rest pose with the
|
||||||
|
feet slightly splayed is not a character who is 7 degrees turned, and correcting
|
||||||
|
it as one puts a permanent yaw on the whole skeleton.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -174,9 +178,21 @@ against proportions like Taila's. A rig with different arm lengths, a different
|
|||||||
rest pose (A-pose vs T-pose) or a different bone roll will put the hands
|
rest pose (A-pose vs T-pose) or a different bone roll will put the hands
|
||||||
somewhere plausible for the maths and wrong for the eye.
|
somewhere plausible for the maths and wrong for the eye.
|
||||||
|
|
||||||
**Rule:** the hold has measurable success criteria — the distance from each hand
|
**The real cause turned out to be #2's sibling, and it affected every model.**
|
||||||
bone to the grip point it was solved onto. Assert those, per character, rather
|
`set_weapon` mounted the gun with a fixed `rotation_degrees = (0, 90, -90)`. A
|
||||||
than judging by eye.
|
bone attachment is expressed in the BONE's axes, and no two rigs agree on those,
|
||||||
|
so a constant there mounts the weapon differently on every character.
|
||||||
|
|
||||||
|
It does not need to be right at all. The pose layer aims the gun by rotating the
|
||||||
|
WRIST until the weapon's forward axis lies on the aim line, so handing it the
|
||||||
|
IDENTITY means "the gun's forward is the hand bone's -Z" — true by construction
|
||||||
|
on any rig — and the wrist absorbs whatever roll that bone happens to have. The
|
||||||
|
grip is placed at the bone's origin, so the gun sits *in* the hand instead of at
|
||||||
|
a fixed offset from a differently-oriented bone.
|
||||||
|
|
||||||
|
**Rule:** never express a mount as a constant in a bone's local frame. Either
|
||||||
|
derive the frame from the skeleton, or hand the downstream solver an identity and
|
||||||
|
let it do the work — it already knows where the gun must point.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -9,20 +9,20 @@
|
|||||||
"Neck",
|
"Neck",
|
||||||
"Head"
|
"Head"
|
||||||
],
|
],
|
||||||
"shoulder.L": "Left shoulder",
|
|
||||||
"shoulder.R": "Right shoulder",
|
"shoulder.R": "Right shoulder",
|
||||||
"foot.R": "Right ankle",
|
"shoulder.L": "Left shoulder",
|
||||||
"foot.L": "Left ankle",
|
"foot.L": "Left ankle",
|
||||||
"hand.L": "Left wrist",
|
"foot.R": "Right ankle",
|
||||||
"hand.R": "Right wrist",
|
"hand.R": "Right wrist",
|
||||||
"forearm.L": "Left elbow",
|
"hand.L": "Left wrist",
|
||||||
"forearm.R": "Right elbow",
|
"forearm.R": "Right elbow",
|
||||||
"shin.R": "Right knee",
|
"forearm.L": "Left elbow",
|
||||||
"shin.L": "Left knee",
|
"shin.L": "Left knee",
|
||||||
|
"shin.R": "Right knee",
|
||||||
"toe.R": "Right toe",
|
"toe.R": "Right toe",
|
||||||
"toe.L": "Left toe",
|
"toe.L": "Left toe",
|
||||||
"upper_arm.L": "Left arm",
|
|
||||||
"upper_arm.R": "Right arm",
|
"upper_arm.R": "Right arm",
|
||||||
|
"upper_arm.L": "Left arm",
|
||||||
"thigh.L": "Left leg",
|
"thigh.L": "Left leg",
|
||||||
"thigh.R": "Right leg"
|
"thigh.R": "Right leg"
|
||||||
},
|
},
|
||||||
|
|||||||
Binary file not shown.
@@ -15,16 +15,16 @@
|
|||||||
"shin.L": "J_Bip_L_LowerLeg",
|
"shin.L": "J_Bip_L_LowerLeg",
|
||||||
"thigh.R": "J_Bip_R_UpperLeg",
|
"thigh.R": "J_Bip_R_UpperLeg",
|
||||||
"thigh.L": "J_Bip_L_UpperLeg",
|
"thigh.L": "J_Bip_L_UpperLeg",
|
||||||
"shoulder.R": "J_Bip_R_Shoulder",
|
|
||||||
"shoulder.L": "J_Bip_L_Shoulder",
|
"shoulder.L": "J_Bip_L_Shoulder",
|
||||||
|
"shoulder.R": "J_Bip_R_Shoulder",
|
||||||
"forearm.R": "J_Bip_R_LowerArm",
|
"forearm.R": "J_Bip_R_LowerArm",
|
||||||
"forearm.L": "J_Bip_L_LowerArm",
|
"forearm.L": "J_Bip_L_LowerArm",
|
||||||
"upper_arm.R": "J_Bip_R_UpperArm",
|
|
||||||
"upper_arm.L": "J_Bip_L_UpperArm",
|
"upper_arm.L": "J_Bip_L_UpperArm",
|
||||||
|
"upper_arm.R": "J_Bip_R_UpperArm",
|
||||||
"toe.L": "J_Bip_L_ToeBase",
|
"toe.L": "J_Bip_L_ToeBase",
|
||||||
"toe.R": "J_Bip_R_ToeBase",
|
"toe.R": "J_Bip_R_ToeBase",
|
||||||
"foot.R": "J_Bip_R_Foot",
|
|
||||||
"foot.L": "J_Bip_L_Foot",
|
"foot.L": "J_Bip_L_Foot",
|
||||||
|
"foot.R": "J_Bip_R_Foot",
|
||||||
"hand.L": "J_Bip_L_Hand",
|
"hand.L": "J_Bip_L_Hand",
|
||||||
"hand.R": "J_Bip_R_Hand"
|
"hand.R": "J_Bip_R_Hand"
|
||||||
},
|
},
|
||||||
@@ -550,7 +550,7 @@
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
-0.02312,
|
-0.02312,
|
||||||
0.02503,
|
0.02504,
|
||||||
0.00687
|
0.00687
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
@@ -1899,7 +1899,7 @@
|
|||||||
0.0013
|
0.0013
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
-0.01334,
|
-0.01333,
|
||||||
0.01982,
|
0.01982,
|
||||||
-0.00015
|
-0.00015
|
||||||
],
|
],
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -11,18 +11,18 @@
|
|||||||
"Unused_Noname_004",
|
"Unused_Noname_004",
|
||||||
"Unused_Noname_010"
|
"Unused_Noname_010"
|
||||||
],
|
],
|
||||||
"shoulder.L": "Left shoulder",
|
|
||||||
"shoulder.R": "Right shoulder",
|
"shoulder.R": "Right shoulder",
|
||||||
"foot.R": "Right ankle",
|
"shoulder.L": "Left shoulder",
|
||||||
"foot.L": "Left ankle",
|
"foot.L": "Left ankle",
|
||||||
|
"foot.R": "Right ankle",
|
||||||
"hand.L": "Left wrist",
|
"hand.L": "Left wrist",
|
||||||
"hand.R": "Right wrist",
|
"hand.R": "Right wrist",
|
||||||
"forearm.L": "Left elbow",
|
"forearm.L": "Left elbow",
|
||||||
"forearm.R": "Right elbow",
|
"forearm.R": "Right elbow",
|
||||||
"shin.R": "Right knee",
|
|
||||||
"shin.L": "Left knee",
|
"shin.L": "Left knee",
|
||||||
"toe.R": "Toes2_R",
|
"shin.R": "Right knee",
|
||||||
"toe.L": "Toes3_L",
|
"toe.R": "Toes0_R",
|
||||||
|
"toe.L": "Toes4_L",
|
||||||
"upper_arm.L": "Left arm",
|
"upper_arm.L": "Left arm",
|
||||||
"upper_arm.R": "Right arm",
|
"upper_arm.R": "Right arm",
|
||||||
"thigh.L": "Left leg",
|
"thigh.L": "Left leg",
|
||||||
@@ -1804,8 +1804,8 @@
|
|||||||
"Root_006",
|
"Root_006",
|
||||||
"Root_007",
|
"Root_007",
|
||||||
"Spine",
|
"Spine",
|
||||||
"Toes2_R",
|
"Toes0_R",
|
||||||
"Toes3_L",
|
"Toes4_L",
|
||||||
"Unused_Noname_004",
|
"Unused_Noname_004",
|
||||||
"Unused_Noname_010"
|
"Unused_Noname_010"
|
||||||
]
|
]
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -908,17 +908,27 @@ func set_weapon(script_path: String) -> void:
|
|||||||
# (weapon swap while toggled), else the new weapon comes up invisible.
|
# (weapon swap while toggled), else the new weapon comes up invisible.
|
||||||
if (shadows_only or first_person_mode) and not _owner_visible:
|
if (shadows_only or first_person_mode) and not _owner_visible:
|
||||||
_set_shadows_recursive(w)
|
_set_shadows_recursive(w)
|
||||||
# Undo the first-person viewmodel placement from the weapon's _ready:
|
# Seat the weapon in the hand with NO hand-relative rotation.
|
||||||
# lie along the hand's grip, scaled down to character proportions.
|
#
|
||||||
w.position = Vector3(-0.02, 0.07, 0.0)
|
# There used to be a fixed `rotation_degrees = (0, 90, -90)` here, which
|
||||||
w.rotation_degrees = Vector3(0, 90, -90)
|
# is the offset that happens to be right for the library's own DEF-hand.R
|
||||||
w.scale = Vector3(1.0, 1.0, 1.0)
|
# and is wrong for every rig whose hand bone has a different roll. A bone
|
||||||
# Tell the pose layer the gun's axes in hand-bone space so it can
|
# attachment is expressed in the BONE's axes, and no two rigs agree on
|
||||||
# aim the wrist to point the muzzle exactly where the player looks.
|
# those, so a constant here mounts the gun differently on every character
|
||||||
|
# — which is what "the hand mount points are totally wrong on all models"
|
||||||
|
# was.
|
||||||
|
#
|
||||||
|
# It does not need to be right, because the pose layer aims the gun by
|
||||||
|
# rotating the WRIST until the weapon's forward axis lies on the aim line
|
||||||
|
# (see _apply_rifle_hold). Handing it the identity means "the gun's
|
||||||
|
# forward is the hand bone's -Z", which is true by construction on any
|
||||||
|
# rig, and the wrist then absorbs whatever that bone's roll happens to be.
|
||||||
|
# The grip is placed at the bone's origin below, so the gun sits IN the
|
||||||
|
# hand rather than at a fixed offset from a differently-oriented bone.
|
||||||
|
w.transform = Transform3D.IDENTITY
|
||||||
if _pose_mod:
|
if _pose_mod:
|
||||||
var b: Basis = w.transform.basis.orthonormalized()
|
_pose_mod.gun_fwd_hand = Vector3(0, 0, -1)
|
||||||
_pose_mod.gun_fwd_hand = b * Vector3(0, 0, -1)
|
_pose_mod.gun_up_hand = Vector3(0, 1, 0)
|
||||||
_pose_mod.gun_up_hand = b * Vector3(0, 1, 0)
|
|
||||||
_measure_weapon(w)
|
_measure_weapon(w)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -413,6 +413,45 @@ def flatten_and_scale(arm, meshes, target_height, roles=None):
|
|||||||
print(f"Stood the character up: feet->hips ran along "
|
print(f"Stood the character up: feet->hips ran along "
|
||||||
f"{'XYZ'[k]}{'+' if d[k] > 0 else '-'}, rotated to Z+")
|
f"{'XYZ'[k]}{'+' if d[k] > 0 else '-'}, rotated to Z+")
|
||||||
|
|
||||||
|
# TURN THE CHARACTER TO FACE THE SAME WAY AS EVERY OTHER CHARACTER.
|
||||||
|
#
|
||||||
|
# Which way a model faces is a property of the file, not a constant, and two
|
||||||
|
# separate things were guessing at it: facing_correction() aligns the rest
|
||||||
|
# pose to the library's, and the runtime then applies a blanket 180 degrees
|
||||||
|
# because "glTF forward is +Z". When a source disagrees with either, the two
|
||||||
|
# compose into a character who runs backwards, and nothing measured the
|
||||||
|
# result. Kiyoko shipped like that.
|
||||||
|
#
|
||||||
|
# The skeleton knows: toes are in front of ankles. Snap that to Blender -Y,
|
||||||
|
# which is the convention the rest of the pipeline and the runtime flip are
|
||||||
|
# built around, so every character leaves here pointing the same way whatever
|
||||||
|
# the source did.
|
||||||
|
if roles is not None:
|
||||||
|
fwd = Vector((0.0, 0.0, 0.0))
|
||||||
|
n_f = 0
|
||||||
|
for side in ("L", "R"):
|
||||||
|
a = arm.data.bones.get(roles.limb.get(("foot", side), ""))
|
||||||
|
t = arm.data.bones.get(roles.limb.get(("toe", side), ""))
|
||||||
|
if a and t:
|
||||||
|
fwd += (t.head_local - a.head_local)
|
||||||
|
n_f += 1
|
||||||
|
if n_f:
|
||||||
|
fwd.z = 0.0
|
||||||
|
if fwd.length > 1e-4:
|
||||||
|
fwd.normalize()
|
||||||
|
want = Vector((0.0, -1.0, 0.0))
|
||||||
|
ang = math.atan2(fwd.x, -fwd.y) # signed yaw from -Y to fwd
|
||||||
|
# Snap to the nearest quarter turn: a rest pose with the feet
|
||||||
|
# slightly splayed must not be counted as a turn.
|
||||||
|
q = round(ang / (math.pi / 2)) * (math.pi / 2)
|
||||||
|
if abs(q) > 1e-6:
|
||||||
|
R = Matrix.Rotation(-q, 4, Vector((0.0, 0.0, 1.0)))
|
||||||
|
for obj in [arm] + meshes:
|
||||||
|
obj.matrix_world = R @ obj.matrix_world
|
||||||
|
apply_all()
|
||||||
|
print("Turned the character to face -Y: was %.0f degrees off"
|
||||||
|
% math.degrees(q))
|
||||||
|
|
||||||
lo = Vector((1e9, 1e9, 1e9))
|
lo = Vector((1e9, 1e9, 1e9))
|
||||||
hi = -lo.copy()
|
hi = -lo.copy()
|
||||||
for m in meshes:
|
for m in meshes:
|
||||||
|
|||||||
Reference in New Issue
Block a user