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
Showing only changes of commit e707229283 - Show all commits
+74 -4
View File
@@ -32,13 +32,27 @@ class_name SkinLegRepair
## 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
## 3. Cap how much the TORSO owns a leg vertex. The top of the thigh is
## weighted between the hips and the thigh; at a wide stride, linear-blend
## skinning averages the near-static hips against a thigh swung 60 degrees
## out, and the top of the leg flattens into a wedge. That is the hip
## collapse visible from the side while running and jumping, and it is the
## worst number the measurement reports (cross-section 0.85 at the hip
## against 0.97-0.99 at the knee). Leg vertices keep at most MAX_TORSO of
## hips/spine influence, and the excess goes to the leg bone that already
## dominates them, so the thigh follows its own bone.
## 4. 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.
##
## Measure with debug/limb_deform_check.gd.
const LEG_BONE_HINTS := ["thigh", "shin", "foot", "toe"]
## Bones that belong to the torso, not the leg.
const TORSO_BONE_HINTS := ["hips", "spine", "pelvis"]
## The most torso influence a leg vertex may keep. Some is wanted — it is what
## rounds the hip off — but past this the thigh stops following its own bone.
const MAX_TORSO := 0.15
## Ignore influences below this — they are rounding, not real weighting.
const EPSILON := 0.005
@@ -111,12 +125,13 @@ static func _repair_mesh(mi: MeshInstance3D, skeleton: Skeleton3D, knee: float,
chain_l: PackedVector3Array, chain_r: PackedVector3Array,
limb_radius: float) -> Array:
var side := _side_map(mi.skin, skeleton)
var torso_bone := _torso_map(mi.skin, skeleton)
var surfaces: Array = []
var snapped := 0
var removed := 0
for s in range(mi.mesh.get_surface_count()):
var arrays: Array = mi.mesh.surface_get_arrays(s)
var r := _repair_surface(arrays, side, knee, chain_l, chain_r, limb_radius)
var r := _repair_surface(arrays, side, torso_bone, knee, chain_l, chain_r, limb_radius)
snapped += r[0]
removed += r[1]
surfaces.append({
@@ -138,6 +153,23 @@ static func _repair_mesh(mi: MeshInstance3D, skeleton: Skeleton3D, knee: float,
return [snapped, removed]
## Is each bind a torso bone? Keyed by SKIN BIND index, like _side_map.
static func _torso_map(skin: Skin, skeleton: Skeleton3D) -> Array:
var out: Array = []
out.resize(skin.get_bind_count())
for b in skin.get_bind_count():
var n := skin.get_bind_name(b)
if n == "":
var bone := skin.get_bind_bone(b)
n = skeleton.get_bone_name(bone) if bone >= 0 else ""
out[b] = false
for hint in TORSO_BONE_HINTS:
if n.findn(hint) != -1:
out[b] = true
break
return out
## -1 left leg, +1 right leg, 0 anything else — keyed by SKIN BIND index, which
## is what ARRAY_BONES stores (not the skeleton's bone index).
static func _side_map(skin: Skin, skeleton: Skeleton3D) -> PackedInt32Array:
@@ -164,7 +196,8 @@ static func _side_map(skin: Skin, skeleton: Skeleton3D) -> PackedInt32Array:
return out
static func _repair_surface(arrays: Array, side: PackedInt32Array, knee: float,
static func _repair_surface(arrays: Array, side: PackedInt32Array,
torso_bone: Array, knee: float,
chain_l: PackedVector3Array, chain_r: PackedVector3Array,
limb_radius: float) -> Array:
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
@@ -220,7 +253,44 @@ static func _repair_surface(arrays: Array, side: PackedInt32Array, knee: float,
weights[v * per + k] /= total
snapped += 1
# Step 2 — drop triangles that still span the legs below the knee.
# Step 2 — stop the torso holding the top of the leg back. Only vertices that
# actually belong to a limb are touched, so the skirt keeps swinging from
# the hips as it should.
for v in verts.size():
if vside[v] == 0:
continue
var chain: PackedVector3Array = chain_l if vside[v] == -1 else chain_r
if _dist_to_chain(verts[v], chain) > limb_radius:
continue
var torso := 0.0
var dom_k := -1
var dom_w := 0.0
for k in per:
var b: int = bones[v * per + k]
var w: float = weights[v * per + k]
if torso_bone[b]:
torso += w
elif side[b] != 0 and w > dom_w:
dom_w = w
dom_k = k
if torso <= MAX_TORSO or dom_k < 0:
continue
# Scale the torso influence down to the cap and hand the rest to the
# leg bone this vertex already follows.
var keep_scale: float = MAX_TORSO / torso
for k in per:
if torso_bone[bones[v * per + k]]:
weights[v * per + k] *= keep_scale
weights[v * per + dom_k] += torso - MAX_TORSO
var sum := 0.0
for k in per:
sum += weights[v * per + k]
if sum > 0.0:
for k in per:
weights[v * per + k] /= sum
snapped += 1
# Step 3 — drop triangles that still span the legs below the knee.
var removed := 0
if not idx.is_empty():
var keep_idx := PackedInt32Array()