fix: repair thigh weights too, and keep the gun shouldered at low-ready
Legs: the previous pass deliberately stopped at the knee, on the reasoning that cross-leg weighting above it is legitimate for skirts. It is — but the LEGS themselves are up there too, and 122 vertices on Taila's thigh mesh carry up to 35% of the opposite leg. Running and jumping are exactly what splits the legs far enough for that to show, which is why the cuffs came good while the legs kept squashing. The repair now runs above the knee as well, but only on vertices that belong to a leg. Limb membership is decided by distance to that leg's own bone chain, not by height and not by surface name: a thigh vertex hugs its bone, while a skirt vertex hangs clear of both chains and is left blended so it can still drape over the two legs. The radius is half the gap between the thigh bones, so it scales with the character rather than being a magic number. Taila now snaps 1217 vertices (was 720), Miku 370 (was 214). Weapons: at low-ready the stock was dropped 0.20 m below the shoulder joint down to the ribs, with the muzzle pitched 22 degrees down. That pulls the whole weapon off the shoulder and out of the supporting arm — it read as dangling from the hands rather than being carried. Low-ready now stays in the shoulder pocket and merely relaxes out of it (0.07 m drop), with a shallow 9 degree muzzle tilt. All six weapons now sit in the shoulder with both hands on them. FSM tests 11/11, spawn smoke test 0 failures. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f60941d1fb
commit
a6e009d24a
@@ -19,14 +19,22 @@ class_name SkinLegRepair
|
|||||||
## not. Freezing the AnimationTree at the rest pose renders the boots perfectly,
|
## not. Freezing the AnimationTree at the rest pose renders the boots perfectly,
|
||||||
## which is what proves it is a skinning problem rather than a pose one.
|
## which is what proves it is a skinning problem rather than a pose one.
|
||||||
##
|
##
|
||||||
## Two steps, both limited to BELOW THE KNEE (taken from the skeleton's own rest
|
## Three steps:
|
||||||
## pose, so it scales to any character). Above the knee, cross-leg weighting is
|
|
||||||
## legitimate — the skirt and shorts really do span both thighs.
|
|
||||||
##
|
##
|
||||||
## 1. Snap each vertex to the leg that already dominates it and renormalise,
|
## 1. BELOW THE KNEE (height taken from the skeleton's own rest pose, so this
|
||||||
## so nothing is pulled in two directions.
|
## scales to any character): snap each vertex to the leg that already
|
||||||
## 2. Drop any triangle still spanning the two legs afterwards. Those are the
|
## dominates it and renormalise, so nothing is pulled in two directions.
|
||||||
## midline band between the ankles, which has no correct pose either way.
|
## 2. ABOVE THE KNEE, only for vertices that are part of a LEG rather than
|
||||||
|
## draping cloth: same snap, to the nearer leg. 122 vertices on Taila's
|
||||||
|
## thigh mesh carry up to 35% of the opposite leg — that is the squashing
|
||||||
|
## that survived the below-knee pass and showed up when running and
|
||||||
|
## jumping split the legs. Limb membership is decided by distance to the
|
||||||
|
## leg's own bone chain, NOT by height or by surface name: a thigh vertex
|
||||||
|
## hugs its bone, while a skirt vertex hangs well clear of both and is
|
||||||
|
## left blended, which is what lets a skirt drape across both legs.
|
||||||
|
## 3. Drop any triangle still spanning the two legs below the knee. Those are
|
||||||
|
## the midline band between the ankles, which has no correct pose either
|
||||||
|
## way.
|
||||||
|
|
||||||
const LEG_BONE_HINTS := ["thigh", "shin", "foot", "toe"]
|
const LEG_BONE_HINTS := ["thigh", "shin", "foot", "toe"]
|
||||||
## Ignore influences below this — they are rounding, not real weighting.
|
## Ignore influences below this — they are rounding, not real weighting.
|
||||||
@@ -38,6 +46,14 @@ static func repair(root: Node, skeleton: Skeleton3D) -> Array:
|
|||||||
var knee := _knee_height(skeleton)
|
var knee := _knee_height(skeleton)
|
||||||
if is_nan(knee):
|
if is_nan(knee):
|
||||||
return [0, 0]
|
return [0, 0]
|
||||||
|
var chain_l := _leg_chain(skeleton, ".L")
|
||||||
|
var chain_r := _leg_chain(skeleton, ".R")
|
||||||
|
if chain_l.is_empty() or chain_r.is_empty():
|
||||||
|
return [0, 0]
|
||||||
|
# "Part of a leg" means within roughly half the gap between the two legs of
|
||||||
|
# that leg's bone chain — self-scaling to the character's proportions.
|
||||||
|
var limb_radius: float = maxf(absf(chain_l[0].x - chain_r[0].x) * 0.5, 0.02)
|
||||||
|
|
||||||
var snapped_total := 0
|
var snapped_total := 0
|
||||||
var removed_total := 0
|
var removed_total := 0
|
||||||
for mi in root.find_children("*", "MeshInstance3D", true, false):
|
for mi in root.find_children("*", "MeshInstance3D", true, false):
|
||||||
@@ -47,12 +63,38 @@ static func repair(root: Node, skeleton: Skeleton3D) -> Array:
|
|||||||
# rig) is left alone rather than silently losing its expressions.
|
# rig) is left alone rather than silently losing its expressions.
|
||||||
if mi.mesh.get_blend_shape_count() > 0:
|
if mi.mesh.get_blend_shape_count() > 0:
|
||||||
continue
|
continue
|
||||||
var r := _repair_mesh(mi, skeleton, knee)
|
var r := _repair_mesh(mi, skeleton, knee, chain_l, chain_r, limb_radius)
|
||||||
snapped_total += r[0]
|
snapped_total += r[0]
|
||||||
removed_total += r[1]
|
removed_total += r[1]
|
||||||
return [snapped_total, removed_total]
|
return [snapped_total, removed_total]
|
||||||
|
|
||||||
|
|
||||||
|
## Rest-pose joint positions down one leg, used as a polyline to measure how
|
||||||
|
## close a vertex sits to that limb.
|
||||||
|
static func _leg_chain(skeleton: Skeleton3D, suffix: String) -> PackedVector3Array:
|
||||||
|
var out := PackedVector3Array()
|
||||||
|
for stem in ["DEF-thigh", "DEF-shin", "DEF-foot", "DEF-toe"]:
|
||||||
|
var i := skeleton.find_bone(stem + suffix)
|
||||||
|
if i < 0:
|
||||||
|
i = skeleton.find_bone(stem.trim_prefix("DEF-") + suffix)
|
||||||
|
if i >= 0:
|
||||||
|
out.append(skeleton.get_bone_global_rest(i).origin)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## Distance from a point to a polyline.
|
||||||
|
static func _dist_to_chain(p: Vector3, chain: PackedVector3Array) -> float:
|
||||||
|
var best := INF
|
||||||
|
for i in range(chain.size() - 1):
|
||||||
|
var a: Vector3 = chain[i]
|
||||||
|
var b: Vector3 = chain[i + 1]
|
||||||
|
var ab: Vector3 = b - a
|
||||||
|
var len2: float = ab.length_squared()
|
||||||
|
var t: float = 0.0 if len2 < 0.000001 else clampf((p - a).dot(ab) / len2, 0.0, 1.0)
|
||||||
|
best = minf(best, p.distance_to(a + ab * t))
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
static func _knee_height(skeleton: Skeleton3D) -> float:
|
static func _knee_height(skeleton: Skeleton3D) -> float:
|
||||||
if skeleton == null:
|
if skeleton == null:
|
||||||
return NAN
|
return NAN
|
||||||
@@ -63,15 +105,16 @@ static func _knee_height(skeleton: Skeleton3D) -> float:
|
|||||||
return NAN
|
return NAN
|
||||||
|
|
||||||
|
|
||||||
static func _repair_mesh(mi: MeshInstance3D, skeleton: Skeleton3D,
|
static func _repair_mesh(mi: MeshInstance3D, skeleton: Skeleton3D, knee: float,
|
||||||
knee: float) -> Array:
|
chain_l: PackedVector3Array, chain_r: PackedVector3Array,
|
||||||
|
limb_radius: float) -> Array:
|
||||||
var side := _side_map(mi.skin, skeleton)
|
var side := _side_map(mi.skin, skeleton)
|
||||||
var surfaces: Array = []
|
var surfaces: Array = []
|
||||||
var snapped := 0
|
var snapped := 0
|
||||||
var removed := 0
|
var removed := 0
|
||||||
for s in range(mi.mesh.get_surface_count()):
|
for s in range(mi.mesh.get_surface_count()):
|
||||||
var arrays: Array = mi.mesh.surface_get_arrays(s)
|
var arrays: Array = mi.mesh.surface_get_arrays(s)
|
||||||
var r := _repair_surface(arrays, side, knee)
|
var r := _repair_surface(arrays, side, knee, chain_l, chain_r, limb_radius)
|
||||||
snapped += r[0]
|
snapped += r[0]
|
||||||
removed += r[1]
|
removed += r[1]
|
||||||
surfaces.append({
|
surfaces.append({
|
||||||
@@ -119,8 +162,9 @@ static func _side_map(skin: Skin, skeleton: Skeleton3D) -> PackedInt32Array:
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
static func _repair_surface(arrays: Array, side: PackedInt32Array,
|
static func _repair_surface(arrays: Array, side: PackedInt32Array, knee: float,
|
||||||
knee: float) -> Array:
|
chain_l: PackedVector3Array, chain_r: PackedVector3Array,
|
||||||
|
limb_radius: float) -> Array:
|
||||||
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||||
var bones: PackedInt32Array = arrays[Mesh.ARRAY_BONES]
|
var bones: PackedInt32Array = arrays[Mesh.ARRAY_BONES]
|
||||||
var weights: PackedFloat32Array = arrays[Mesh.ARRAY_WEIGHTS]
|
var weights: PackedFloat32Array = arrays[Mesh.ARRAY_WEIGHTS]
|
||||||
@@ -148,10 +192,20 @@ static func _repair_surface(arrays: Array, side: PackedInt32Array,
|
|||||||
continue
|
continue
|
||||||
var keep: int = -1 if wl >= wr else 1
|
var keep: int = -1 if wl >= wr else 1
|
||||||
vside[v] = keep
|
vside[v] = keep
|
||||||
if verts[v].y > knee:
|
|
||||||
continue
|
|
||||||
if minf(wl, wr) <= EPSILON:
|
if minf(wl, wr) <= EPSILON:
|
||||||
continue # already single-legged
|
continue # already single-legged
|
||||||
|
if verts[v].y > knee:
|
||||||
|
# Above the knee, only repair vertices that belong to a LEG. Cloth
|
||||||
|
# that drapes across both legs sits clear of either bone chain and
|
||||||
|
# is left blended so it can keep draping.
|
||||||
|
var dl: float = _dist_to_chain(verts[v], chain_l)
|
||||||
|
var dr: float = _dist_to_chain(verts[v], chain_r)
|
||||||
|
var near: float = minf(dl, dr)
|
||||||
|
var far: float = maxf(dl, dr)
|
||||||
|
if near > limb_radius or far < near * 1.25:
|
||||||
|
continue # drapes over both, or hugs neither — leave it alone
|
||||||
|
keep = -1 if dl < dr else 1
|
||||||
|
vside[v] = keep
|
||||||
# Drop the losing leg's influence and renormalise what remains.
|
# Drop the losing leg's influence and renormalise what remains.
|
||||||
var total := 0.0
|
var total := 0.0
|
||||||
for k in per:
|
for k in per:
|
||||||
|
|||||||
@@ -940,11 +940,16 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
|||||||
# the handguard, instead of both arms waving at art-directed angles near it.
|
# the handguard, instead of both arms waving at art-directed angles near it.
|
||||||
# Skeleton space: character faces +Z, up +Y, character-right -X.
|
# Skeleton space: character faces +Z, up +Y, character-right -X.
|
||||||
|
|
||||||
const GUN_PITCH_HIP := 0.38 # muzzle tilts down this much at low-ready
|
# Muzzle tilt at low-ready. Kept shallow: at the old 0.38 rad (22 degrees)
|
||||||
|
# the gun read as dangling from the hands rather than being carried.
|
||||||
|
const GUN_PITCH_HIP := 0.16
|
||||||
# Where the butt of the stock sits, relative to the right shoulder joint.
|
# Where the butt of the stock sits, relative to the right shoulder joint.
|
||||||
# The pocket is on the FRONT of the shoulder, slightly inboard of the joint.
|
# The pocket is on the FRONT of the shoulder, slightly inboard of the joint.
|
||||||
|
# Low-ready used to drop the butt 0.20 m to the ribs, which pulled the whole
|
||||||
|
# weapon off the shoulder and out of the arms — it never looked held. It now
|
||||||
|
# stays in the pocket and only relaxes slightly out of ADS.
|
||||||
const POCKET_ADS := Vector3(0.05, 0.01, 0.07) # in the shoulder pocket
|
const POCKET_ADS := Vector3(0.05, 0.01, 0.07) # in the shoulder pocket
|
||||||
const POCKET_HIP := Vector3(0.02, -0.20, 0.05) # tucked down at the ribs
|
const POCKET_HIP := Vector3(0.03, -0.07, 0.06) # still shouldered, relaxed
|
||||||
# Elbow bend hints (skeleton space). At the hip the firing elbow rides
|
# Elbow bend hints (skeleton space). At the hip the firing elbow rides
|
||||||
# down by the ribs; shouldered it flares OUT and level (the classic
|
# down by the ribs; shouldered it flares OUT and level (the classic
|
||||||
# "chicken wing"), which is what keeps the tight fold from folding the
|
# "chicken wing"), which is what keeps the tight fold from folding the
|
||||||
|
|||||||
Reference in New Issue
Block a user