Feat/outline thickness and tp weapon hold #22
@@ -0,0 +1,225 @@
|
|||||||
|
extends Object
|
||||||
|
class_name SkinJointHelper
|
||||||
|
|
||||||
|
## Handles bending with JOINTS instead of letting the mesh collapse.
|
||||||
|
##
|
||||||
|
## Linear-blend skinning averages TRANSFORMS, not shapes. A vertex weighted half
|
||||||
|
## to the thigh and half to the shin is placed at the average of two rotations,
|
||||||
|
## and when the joint bends that average falls inside the limb — the leg pinches
|
||||||
|
## and reads as "the thighs get really thin". Measured per vertex against the
|
||||||
|
## rest transforms, Taila's thigh was losing up to 20% of its radius along its
|
||||||
|
## whole length during a jump, not just at the knee.
|
||||||
|
##
|
||||||
|
## The fix is to stop any vertex ever blending across a full bend. At each leg
|
||||||
|
## joint we insert a helper bone that sits on the child's rest transform and is
|
||||||
|
## driven to HALF the child's rotation. Vertices in that joint's blend zone are
|
||||||
|
## re-weighted off the parent/child pair and onto the helper, so the sharpest
|
||||||
|
## blend any vertex sees is half the joint angle — and halving the angle roughly
|
||||||
|
## quarters the collapse. This is the standard half-angle (or "twist") joint
|
||||||
|
## used to make skinned limbs hold their volume.
|
||||||
|
##
|
||||||
|
## KNEES ONLY, deliberately. Adding hip (hips->thigh) and ankle (shin->foot)
|
||||||
|
## helpers was tried and reverted: it made the knee region measurably WORSE
|
||||||
|
## (0.80 -> 0.50) and dragged the skirt down with it (0.80 -> 0.72). The knee is
|
||||||
|
## the joint that actually bends far enough to pinch; the hip mostly swings,
|
||||||
|
## which linear-blend skinning handles far better. Re-measure before adding a
|
||||||
|
## joint here — more helpers is not automatically better.
|
||||||
|
##
|
||||||
|
## install() returns the [child_bone, helper_bone] pairs; the caller MUST drive
|
||||||
|
## them every frame from inside the skeleton's modification pass (see
|
||||||
|
## SkinnedPlayerModel.ShooterPoseModifier). A helper that is not updated in step
|
||||||
|
## with its child deforms the limb instead of saving it.
|
||||||
|
|
||||||
|
## [parent, child] per joint, both sides. The helper is parented to `parent` and
|
||||||
|
## takes `child`'s rest transform.
|
||||||
|
const JOINTS := [
|
||||||
|
["DEF-thigh.L", "DEF-shin.L"], ["DEF-thigh.R", "DEF-shin.R"],
|
||||||
|
]
|
||||||
|
## A vertex needs at least this much of BOTH bones before it is worth moving.
|
||||||
|
const MIN_BLEND := 0.02
|
||||||
|
## Joints are for the LIMB only. A helper is driven to half its child's swing,
|
||||||
|
## which is right for a thigh but wrong for a skirt — cloth wants to hang from
|
||||||
|
## the hips, not follow half a leg swing. Moving skirt vertices onto the hip
|
||||||
|
## helper measurably made them worse (0.80 -> 0.72), so anything hanging clear
|
||||||
|
## of the leg's own bone chain by more than this is left alone.
|
||||||
|
const LIMB_HINTS := ["DEF-thigh", "DEF-shin", "DEF-foot", "DEF-toe"]
|
||||||
|
|
||||||
|
|
||||||
|
## Returns an Array of [child_bone_idx, helper_bone_idx].
|
||||||
|
static func install(root: Node, skeleton: Skeleton3D) -> Array:
|
||||||
|
if skeleton == null:
|
||||||
|
return []
|
||||||
|
var driven: Array = []
|
||||||
|
# child bone -> [helper bone, parent bone]
|
||||||
|
var helper_of_child := {}
|
||||||
|
for joint in JOINTS:
|
||||||
|
var parent := skeleton.find_bone(joint[0])
|
||||||
|
var child := skeleton.find_bone(joint[1])
|
||||||
|
if parent < 0 or child < 0:
|
||||||
|
continue
|
||||||
|
var hname: String = "HELPER-" + joint[1]
|
||||||
|
if skeleton.find_bone(hname) >= 0:
|
||||||
|
continue # already installed
|
||||||
|
skeleton.add_bone(hname)
|
||||||
|
var h := skeleton.find_bone(hname)
|
||||||
|
skeleton.set_bone_parent(h, parent)
|
||||||
|
# Same rest as the child, so "half the child's local rotation" lands the
|
||||||
|
# helper exactly halfway through the bend.
|
||||||
|
skeleton.set_bone_rest(h, skeleton.get_bone_rest(child))
|
||||||
|
skeleton.reset_bone_pose(h)
|
||||||
|
helper_of_child[child] = [h, parent]
|
||||||
|
driven.append([child, h])
|
||||||
|
if driven.is_empty():
|
||||||
|
return []
|
||||||
|
|
||||||
|
var chain_l := _leg_chain(skeleton, ".L")
|
||||||
|
var chain_r := _leg_chain(skeleton, ".R")
|
||||||
|
var limb_radius := 0.09
|
||||||
|
if chain_l.size() > 0 and chain_r.size() > 0:
|
||||||
|
limb_radius = maxf(absf(chain_l[0].x - chain_r[0].x) * 0.5, 0.02)
|
||||||
|
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_child, chain_l, chain_r, limb_radius)
|
||||||
|
return driven
|
||||||
|
|
||||||
|
|
||||||
|
## Rest-pose joints down one leg, as a polyline for limb-membership tests.
|
||||||
|
static func _leg_chain(skeleton: Skeleton3D, suffix: String) -> PackedVector3Array:
|
||||||
|
var out := PackedVector3Array()
|
||||||
|
for stem in LIMB_HINTS:
|
||||||
|
var i := skeleton.find_bone(stem + suffix)
|
||||||
|
if i >= 0:
|
||||||
|
out.append(skeleton.get_bone_global_rest(i).origin)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
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 ab: Vector3 = chain[i + 1] - a
|
||||||
|
var l2: float = ab.length_squared()
|
||||||
|
var t: float = 0.0 if l2 < 0.000001 else clampf((p - a).dot(ab) / l2, 0.0, 1.0)
|
||||||
|
best = minf(best, p.distance_to(a + ab * t))
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
## Drive the helpers. MUST run inside the skeleton's modification pass.
|
||||||
|
static func update(skeleton: Skeleton3D, driven: Array) -> void:
|
||||||
|
for d in driven:
|
||||||
|
skeleton.set_bone_pose_rotation(d[1],
|
||||||
|
Quaternion.IDENTITY.slerp(skeleton.get_bone_pose_rotation(d[0]), 0.5))
|
||||||
|
|
||||||
|
|
||||||
|
static func _reweight(mi: MeshInstance3D, skeleton: Skeleton3D,
|
||||||
|
helper_of_child: Dictionary, chain_l: PackedVector3Array,
|
||||||
|
chain_r: PackedVector3Array, limb_radius: float) -> void:
|
||||||
|
var skin: Skin = mi.skin
|
||||||
|
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
|
||||||
|
var bind_of_bone := {}
|
||||||
|
for b in skin.get_bind_count():
|
||||||
|
bind_of_bone[bone_of[b]] = b
|
||||||
|
|
||||||
|
# One bind per helper, sharing the child's bind pose — their global rests
|
||||||
|
# are identical, so the inverse-bind matrix is the same.
|
||||||
|
# joint list of [parent_bind, child_bind, helper_bind]
|
||||||
|
var jobs: Array = []
|
||||||
|
for child in helper_of_child:
|
||||||
|
var helper: int = helper_of_child[child][0]
|
||||||
|
var parent: int = helper_of_child[child][1]
|
||||||
|
if not bind_of_bone.has(child) or not bind_of_bone.has(parent):
|
||||||
|
continue
|
||||||
|
var child_bind: int = bind_of_bone[child]
|
||||||
|
skin.add_named_bind(skeleton.get_bone_name(helper),
|
||||||
|
skin.get_bind_pose(child_bind))
|
||||||
|
jobs.append([bind_of_bone[parent], child_bind, skin.get_bind_count() - 1])
|
||||||
|
if jobs.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
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, jobs, chain_l, chain_r, limb_radius)
|
||||||
|
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, jobs: Array,
|
||||||
|
chain_l: PackedVector3Array, chain_r: PackedVector3Array,
|
||||||
|
limb_radius: float) -> 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():
|
||||||
|
# NO limb-radius gate here. It was added to keep a hip helper off the
|
||||||
|
# skirt, and with knees only there is nothing to protect against — but
|
||||||
|
# it does exclude the outside of the knee, which sits further from the
|
||||||
|
# bone axis than the limb radius and is exactly what needs helping
|
||||||
|
# (measured: gating it put the knee back to 0.50).
|
||||||
|
for job in jobs:
|
||||||
|
var parent_bind: int = job[0]
|
||||||
|
var child_bind: int = job[1]
|
||||||
|
var helper_bind: int = job[2]
|
||||||
|
var k_parent := -1
|
||||||
|
var k_child := -1
|
||||||
|
for k in per:
|
||||||
|
var b: int = bones[v * per + k]
|
||||||
|
if b == parent_bind:
|
||||||
|
k_parent = k
|
||||||
|
elif b == child_bind:
|
||||||
|
k_child = k
|
||||||
|
if k_parent < 0 or k_child < 0:
|
||||||
|
continue
|
||||||
|
var w_parent: float = weights[v * per + k_parent]
|
||||||
|
var w_child: float = weights[v * per + k_child]
|
||||||
|
var m: float = minf(w_parent, w_child)
|
||||||
|
if m < MIN_BLEND:
|
||||||
|
continue
|
||||||
|
# Move 2m onto the helper, taking m from each side. The slot whose
|
||||||
|
# weight falls to zero is reused, so no extra influence slot is
|
||||||
|
# needed and the weights still sum to exactly 1.
|
||||||
|
if w_parent <= w_child:
|
||||||
|
bones[v * per + k_parent] = helper_bind
|
||||||
|
weights[v * per + k_parent] = 2.0 * m
|
||||||
|
weights[v * per + k_child] = w_child - m
|
||||||
|
else:
|
||||||
|
bones[v * per + k_child] = helper_bind
|
||||||
|
weights[v * per + k_child] = 2.0 * m
|
||||||
|
weights[v * per + k_parent] = w_parent - m
|
||||||
|
touched += 1
|
||||||
|
# ONE joint per vertex. Falling through to the next job would read
|
||||||
|
# the bones this one just rewrote and move the vertex twice, which
|
||||||
|
# corrupts the blend rather than halving it (measured: the knee got
|
||||||
|
# worse, 0.80 -> 0.50, purely from cascading).
|
||||||
|
break
|
||||||
|
if touched > 0:
|
||||||
|
arrays[Mesh.ARRAY_BONES] = bones
|
||||||
|
arrays[Mesh.ARRAY_WEIGHTS] = weights
|
||||||
|
return touched
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dkypjuyv81kg7
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://cxgs0k8sy6jcg
|
|
||||||
@@ -86,8 +86,8 @@ var loaded: bool = false
|
|||||||
var _resolved_clips: Dictionary = {} # canonical name -> actual clip name
|
var _resolved_clips: Dictionary = {} # canonical name -> actual clip name
|
||||||
var _current_clip: String = ""
|
var _current_clip: String = ""
|
||||||
var _weapon_attachment: BoneAttachment3D
|
var _weapon_attachment: BoneAttachment3D
|
||||||
## [shin_bone, helper_bone] pairs driven every frame by the pose modifier.
|
## [child_bone, helper_bone] pairs driven every frame by the pose modifier.
|
||||||
var _knee_helpers: Array = []
|
var _joint_helpers: Array = []
|
||||||
var is_holding_weapon: bool = false
|
var is_holding_weapon: bool = false
|
||||||
|
|
||||||
# Animation blending: locomotion plays full-body through a Transition node;
|
# Animation blending: locomotion plays full-body through a Transition node;
|
||||||
@@ -170,11 +170,11 @@ func load_model(path: String) -> void:
|
|||||||
if fixed[0] > 0 or fixed[1] > 0:
|
if fixed[0] > 0 or fixed[1] > 0:
|
||||||
print("SkinnedPlayerModel: '%s' — snapped %d cross-leg vertices, dropped %d bridging triangles"
|
print("SkinnedPlayerModel: '%s' — snapped %d cross-leg vertices, dropped %d bridging triangles"
|
||||||
% [path.get_file(), fixed[0], fixed[1]])
|
% [path.get_file(), fixed[0], fixed[1]])
|
||||||
# Half-angle helper bones at the knees. Without them the knee pinches to
|
# Half-angle helper bones at every leg joint. Without them the limb
|
||||||
# a ribbon at a hard bend — see SkinKneeHelper.
|
# pinches at a hard bend — see SkinJointHelper.
|
||||||
_knee_helpers = SkinKneeHelper.install(scene, skeleton)
|
_joint_helpers = SkinJointHelper.install(scene, skeleton)
|
||||||
_pose_mod = ShooterPoseModifier.new()
|
_pose_mod = ShooterPoseModifier.new()
|
||||||
_pose_mod.knee_helpers = _knee_helpers
|
_pose_mod.joint_helpers = _joint_helpers
|
||||||
_pose_mod.name = "ShooterPose"
|
_pose_mod.name = "ShooterPose"
|
||||||
skeleton.add_child(_pose_mod)
|
skeleton.add_child(_pose_mod)
|
||||||
|
|
||||||
@@ -761,8 +761,8 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
|||||||
var gun_stock: float = 0.20
|
var gun_stock: float = 0.20
|
||||||
# 0..1 through a reload — drives the support hand to the mag well and back.
|
# 0..1 through a reload — drives the support hand to the mag well and back.
|
||||||
var reload_phase: float = 0.0
|
var reload_phase: float = 0.0
|
||||||
# [shin_bone, helper_bone] pairs; see SkinKneeHelper.
|
# [child_bone, helper_bone] pairs; see SkinJointHelper.
|
||||||
var knee_helpers: Array = []
|
var joint_helpers: Array = []
|
||||||
|
|
||||||
# Tuning (radians). Positive pitch leans forward; positive roll leans right.
|
# Tuning (radians). Positive pitch leans forward; positive roll leans right.
|
||||||
# The lean is the ONLY thing that tells a viewer which way this character is
|
# The lean is the ONLY thing that tells a viewer which way this character is
|
||||||
@@ -821,11 +821,11 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
|||||||
_apply_recoil(skel)
|
_apply_recoil(skel)
|
||||||
recoil = lerpf(recoil, 0.0, 0.25)
|
recoil = lerpf(recoil, 0.0, 0.25)
|
||||||
|
|
||||||
# Knees LAST, and inside the modification pass: the helper has to track
|
# Joints LAST, and inside the modification pass: each helper has to track
|
||||||
# whatever final rotation the shin ended up with, or it deforms the leg
|
# whatever final rotation its child bone ended up with, or it deforms the
|
||||||
# instead of saving it.
|
# limb instead of saving it.
|
||||||
if not knee_helpers.is_empty():
|
if not joint_helpers.is_empty():
|
||||||
SkinKneeHelper.update(skel, knee_helpers)
|
SkinJointHelper.update(skel, joint_helpers)
|
||||||
|
|
||||||
|
|
||||||
# Upper body follows the camera pitch: distributed over spine/neck/head
|
# Upper body follows the camera pitch: distributed over spine/neck/head
|
||||||
|
|||||||
Reference in New Issue
Block a user