|
|
|
@@ -0,0 +1,176 @@
|
|
|
|
|
extends Object
|
|
|
|
|
class_name SkinKneeHelper
|
|
|
|
|
|
|
|
|
|
## Fixes the knee "candy wrapper" collapse with a half-angle helper bone.
|
|
|
|
|
##
|
|
|
|
|
## Linear-blend skinning averages TRANSFORMS, not shapes. A vertex sitting half
|
|
|
|
|
## on the thigh and half on the shin is placed at the average of two rotations,
|
|
|
|
|
## and when the knee bends hard that average falls well inside the leg — the
|
|
|
|
|
## limb pinches to a flat ribbon. Measured on Taila per vertex (ring averages
|
|
|
|
|
## hide this completely): 532 leg vertices lose more than 20% of their radius
|
|
|
|
|
## and the worst lose 40%, all at y=0.54, which is exactly the knee, and worst
|
|
|
|
|
## in the air/jump pose. That is the squashing visible from the side while
|
|
|
|
|
## running and jumping.
|
|
|
|
|
##
|
|
|
|
|
## The standard fix is to stop any vertex blending across the full bend. We add
|
|
|
|
|
## a bone at the knee, parented to the thigh, holding the shin's rest transform,
|
|
|
|
|
## and drive it every frame to HALF the shin's rotation. Vertices in the blend
|
|
|
|
|
## zone are re-weighted off the thigh/shin pair and onto this helper, so the
|
|
|
|
|
## sharpest blend any vertex sees is half the knee angle. Halving the angle
|
|
|
|
|
## roughly quarters the collapse.
|
|
|
|
|
##
|
|
|
|
|
## install() returns the [shin_bone, helper_bone] pairs; the caller must drive
|
|
|
|
|
## them every frame from inside the skeleton's modification pass (see
|
|
|
|
|
## SkinnedPlayerModel.ShooterPoseModifier), because a helper bone that is not
|
|
|
|
|
## updated in step with the shin is worse than none at all.
|
|
|
|
|
|
|
|
|
|
const PAIRS := [["DEF-thigh.L", "DEF-shin.L"], ["DEF-thigh.R", "DEF-shin.R"]]
|
|
|
|
|
## Blend-zone threshold: a vertex needs at least this much of BOTH bones before
|
|
|
|
|
## it is worth moving onto the helper.
|
|
|
|
|
const MIN_BLEND := 0.02
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Returns an Array of [shin_bone_idx, helper_bone_idx].
|
|
|
|
|
static func install(root: Node, skeleton: Skeleton3D) -> Array:
|
|
|
|
|
if skeleton == null:
|
|
|
|
|
return []
|
|
|
|
|
var driven: Array = []
|
|
|
|
|
var helper_of_shin := {} # shin bone idx -> helper bone idx
|
|
|
|
|
for pair in PAIRS:
|
|
|
|
|
var thigh := skeleton.find_bone(pair[0])
|
|
|
|
|
var shin := skeleton.find_bone(pair[1])
|
|
|
|
|
if thigh < 0 or shin < 0:
|
|
|
|
|
continue
|
|
|
|
|
var hname: String = "HELPER-knee" + pair[1].substr(pair[1].length() - 2)
|
|
|
|
|
if skeleton.find_bone(hname) >= 0:
|
|
|
|
|
continue # already installed
|
|
|
|
|
skeleton.add_bone(hname)
|
|
|
|
|
var h := skeleton.find_bone(hname)
|
|
|
|
|
skeleton.set_bone_parent(h, thigh)
|
|
|
|
|
# Same rest as the shin, so "half the shin's local rotation" lands the
|
|
|
|
|
# helper exactly halfway through the bend.
|
|
|
|
|
skeleton.set_bone_rest(h, skeleton.get_bone_rest(shin))
|
|
|
|
|
skeleton.reset_bone_pose(h)
|
|
|
|
|
helper_of_shin[shin] = h
|
|
|
|
|
driven.append([shin, h])
|
|
|
|
|
if driven.is_empty():
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
for mi in root.find_children("*", "MeshInstance3D", true, false):
|
|
|
|
|
if mi.mesh == null or mi.skin == null:
|
|
|
|
|
continue
|
|
|
|
|
if mi.mesh.get_blend_shape_count() > 0:
|
|
|
|
|
continue # rebuilding would drop the blend shapes
|
|
|
|
|
_reweight(mi, skeleton, helper_of_shin)
|
|
|
|
|
return driven
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Drive the helpers. MUST run inside the skeleton's modification pass.
|
|
|
|
|
static func update(skeleton: Skeleton3D, driven: Array) -> void:
|
|
|
|
|
for d in driven:
|
|
|
|
|
var shin: int = d[0]
|
|
|
|
|
var helper: int = d[1]
|
|
|
|
|
skeleton.set_bone_pose_rotation(helper,
|
|
|
|
|
Quaternion.IDENTITY.slerp(skeleton.get_bone_pose_rotation(shin), 0.5))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static func _reweight(mi: MeshInstance3D, skeleton: Skeleton3D,
|
|
|
|
|
helper_of_shin: Dictionary) -> void:
|
|
|
|
|
var skin: Skin = mi.skin
|
|
|
|
|
# bind index -> skeleton bone
|
|
|
|
|
var bone_of := {}
|
|
|
|
|
for b in skin.get_bind_count():
|
|
|
|
|
var bi := skin.get_bind_bone(b)
|
|
|
|
|
if bi < 0:
|
|
|
|
|
bi = skeleton.find_bone(skin.get_bind_name(b))
|
|
|
|
|
bone_of[b] = bi
|
|
|
|
|
# Make sure every helper has a bind, sharing the shin's bind pose (their
|
|
|
|
|
# global rests are identical, so the inverse-bind matrix is the same).
|
|
|
|
|
var bind_of_bone := {}
|
|
|
|
|
for b in skin.get_bind_count():
|
|
|
|
|
bind_of_bone[bone_of[b]] = b
|
|
|
|
|
var helper_bind := {}
|
|
|
|
|
for shin in helper_of_shin:
|
|
|
|
|
var helper: int = helper_of_shin[shin]
|
|
|
|
|
if not bind_of_bone.has(shin):
|
|
|
|
|
continue
|
|
|
|
|
var pose: Transform3D = skin.get_bind_pose(bind_of_bone[shin])
|
|
|
|
|
skin.add_named_bind(skeleton.get_bone_name(helper), pose)
|
|
|
|
|
helper_bind[bind_of_bone[shin]] = skin.get_bind_count() - 1
|
|
|
|
|
|
|
|
|
|
var thigh_of_shin := {}
|
|
|
|
|
for shin in helper_of_shin:
|
|
|
|
|
thigh_of_shin[shin] = skeleton.get_bone_parent(shin)
|
|
|
|
|
|
|
|
|
|
var surfaces: Array = []
|
|
|
|
|
var touched := 0
|
|
|
|
|
for s in range(mi.mesh.get_surface_count()):
|
|
|
|
|
var arrays: Array = mi.mesh.surface_get_arrays(s)
|
|
|
|
|
touched += _reweight_surface(arrays, bone_of, bind_of_bone, helper_bind,
|
|
|
|
|
helper_of_shin, thigh_of_shin)
|
|
|
|
|
surfaces.append({
|
|
|
|
|
"arrays": arrays,
|
|
|
|
|
"material": mi.mesh.surface_get_material(s),
|
|
|
|
|
"name": mi.mesh.surface_get_name(s),
|
|
|
|
|
})
|
|
|
|
|
if touched == 0:
|
|
|
|
|
return
|
|
|
|
|
var rebuilt := ArrayMesh.new()
|
|
|
|
|
for i in surfaces.size():
|
|
|
|
|
var e: Dictionary = surfaces[i]
|
|
|
|
|
rebuilt.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, e["arrays"])
|
|
|
|
|
rebuilt.surface_set_material(i, e["material"])
|
|
|
|
|
if e["name"] != "":
|
|
|
|
|
rebuilt.surface_set_name(i, e["name"])
|
|
|
|
|
mi.mesh = rebuilt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static func _reweight_surface(arrays: Array, bone_of: Dictionary,
|
|
|
|
|
bind_of_bone: Dictionary, helper_bind: Dictionary,
|
|
|
|
|
helper_of_shin: Dictionary, thigh_of_shin: Dictionary) -> int:
|
|
|
|
|
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
|
|
|
|
var bones: PackedInt32Array = arrays[Mesh.ARRAY_BONES]
|
|
|
|
|
var weights: PackedFloat32Array = arrays[Mesh.ARRAY_WEIGHTS]
|
|
|
|
|
if bones.is_empty() or verts.is_empty():
|
|
|
|
|
return 0
|
|
|
|
|
var per: int = bones.size() / verts.size()
|
|
|
|
|
var touched := 0
|
|
|
|
|
|
|
|
|
|
for v in verts.size():
|
|
|
|
|
for shin in helper_of_shin:
|
|
|
|
|
var shin_bind: int = bind_of_bone.get(shin, -1)
|
|
|
|
|
var thigh_bind: int = bind_of_bone.get(thigh_of_shin[shin], -1)
|
|
|
|
|
if shin_bind < 0 or thigh_bind < 0 or not helper_bind.has(shin_bind):
|
|
|
|
|
continue
|
|
|
|
|
# Find this vertex's thigh and shin slots.
|
|
|
|
|
var k_thigh := -1
|
|
|
|
|
var k_shin := -1
|
|
|
|
|
for k in per:
|
|
|
|
|
var b: int = bones[v * per + k]
|
|
|
|
|
if b == thigh_bind:
|
|
|
|
|
k_thigh = k
|
|
|
|
|
elif b == shin_bind:
|
|
|
|
|
k_shin = k
|
|
|
|
|
if k_thigh < 0 or k_shin < 0:
|
|
|
|
|
continue
|
|
|
|
|
var w_thigh: float = weights[v * per + k_thigh]
|
|
|
|
|
var w_shin: float = weights[v * per + k_shin]
|
|
|
|
|
var m: float = minf(w_thigh, w_shin)
|
|
|
|
|
if m < MIN_BLEND:
|
|
|
|
|
continue
|
|
|
|
|
# Move 2m onto the helper, taking m from each side. The slot whose
|
|
|
|
|
# weight goes to zero is reused, so no extra influence slot is
|
|
|
|
|
# needed and the weights still sum to exactly 1.
|
|
|
|
|
if w_thigh <= w_shin:
|
|
|
|
|
bones[v * per + k_thigh] = helper_bind[shin_bind]
|
|
|
|
|
weights[v * per + k_thigh] = 2.0 * m
|
|
|
|
|
weights[v * per + k_shin] = w_shin - m
|
|
|
|
|
else:
|
|
|
|
|
bones[v * per + k_shin] = helper_bind[shin_bind]
|
|
|
|
|
weights[v * per + k_shin] = 2.0 * m
|
|
|
|
|
weights[v * per + k_thigh] = w_thigh - m
|
|
|
|
|
touched += 1
|
|
|
|
|
if touched > 0:
|
|
|
|
|
arrays[Mesh.ARRAY_BONES] = bones
|
|
|
|
|
arrays[Mesh.ARRAY_WEIGHTS] = weights
|
|
|
|
|
return touched
|