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 _current_clip: String = ""
|
||||
var _weapon_attachment: BoneAttachment3D
|
||||
## [shin_bone, helper_bone] pairs driven every frame by the pose modifier.
|
||||
var _knee_helpers: Array = []
|
||||
## [child_bone, helper_bone] pairs driven every frame by the pose modifier.
|
||||
var _joint_helpers: Array = []
|
||||
var is_holding_weapon: bool = false
|
||||
|
||||
# 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:
|
||||
print("SkinnedPlayerModel: '%s' — snapped %d cross-leg vertices, dropped %d bridging triangles"
|
||||
% [path.get_file(), fixed[0], fixed[1]])
|
||||
# Half-angle helper bones at the knees. Without them the knee pinches to
|
||||
# a ribbon at a hard bend — see SkinKneeHelper.
|
||||
_knee_helpers = SkinKneeHelper.install(scene, skeleton)
|
||||
# Half-angle helper bones at every leg joint. Without them the limb
|
||||
# pinches at a hard bend — see SkinJointHelper.
|
||||
_joint_helpers = SkinJointHelper.install(scene, skeleton)
|
||||
_pose_mod = ShooterPoseModifier.new()
|
||||
_pose_mod.knee_helpers = _knee_helpers
|
||||
_pose_mod.joint_helpers = _joint_helpers
|
||||
_pose_mod.name = "ShooterPose"
|
||||
skeleton.add_child(_pose_mod)
|
||||
|
||||
@@ -761,8 +761,8 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
||||
var gun_stock: float = 0.20
|
||||
# 0..1 through a reload — drives the support hand to the mag well and back.
|
||||
var reload_phase: float = 0.0
|
||||
# [shin_bone, helper_bone] pairs; see SkinKneeHelper.
|
||||
var knee_helpers: Array = []
|
||||
# [child_bone, helper_bone] pairs; see SkinJointHelper.
|
||||
var joint_helpers: Array = []
|
||||
|
||||
# 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
|
||||
@@ -821,11 +821,11 @@ class ShooterPoseModifier extends SkeletonModifier3D:
|
||||
_apply_recoil(skel)
|
||||
recoil = lerpf(recoil, 0.0, 0.25)
|
||||
|
||||
# Knees LAST, and inside the modification pass: the helper has to track
|
||||
# whatever final rotation the shin ended up with, or it deforms the leg
|
||||
# instead of saving it.
|
||||
if not knee_helpers.is_empty():
|
||||
SkinKneeHelper.update(skel, knee_helpers)
|
||||
# Joints LAST, and inside the modification pass: each helper has to track
|
||||
# whatever final rotation its child bone ended up with, or it deforms the
|
||||
# limb instead of saving it.
|
||||
if not joint_helpers.is_empty():
|
||||
SkinJointHelper.update(skel, joint_helpers)
|
||||
|
||||
|
||||
# Upper body follows the camera pitch: distributed over spine/neck/head
|
||||
|
||||
Reference in New Issue
Block a user