fix(animation): free bones only enabled for Full-prefixed animations

Previously, any GLB with keyframes on free bones would animate them,
even for standard animations like Idle. This caused accidental bone
hijacking — e.g., handcuffs freezing the player's head because the
artist keyframed all bones in Blender.

Now the Full prefix (FullIdle, FullStruggle, FullWalk) is enforced:
only Full-prefixed animations can animate free bones. Standard
animations (Idle, Struggle, Walk) only animate owned bones.

This aligns the code with the documented convention in ARTIST_GUIDE.md.
This commit is contained in:
NotEvil
2026-04-17 02:49:34 +02:00
parent b0766fecc6
commit 229fc66340

View File

@@ -208,12 +208,14 @@ public final class GltfPoseConverter {
}
}
// Selective: enable owned parts always, free parts only if they have keyframes
// Selective: enable owned parts always, free parts only for "Full" animations
// that explicitly opt into full-body control.
enableSelectiveParts(
builder,
ownedParts,
enabledParts,
partsWithKeyframes
partsWithKeyframes,
animName
);
KeyframeAnimation anim = builder.build();
@@ -554,22 +556,35 @@ public final class GltfPoseConverter {
*
* <ul>
* <li>Owned parts: always enabled (the item controls these bones)</li>
* <li>Free parts WITH keyframes: enabled (the GLB has animation data for them)</li>
* <li>Free parts WITHOUT keyframes: disabled (no data to animate, pass through to context)</li>
* <li>Free parts WITH keyframes AND "Full" animation: enabled (explicit opt-in to full-body)</li>
* <li>Free parts without "Full" prefix: disabled (prevents accidental bone hijacking)</li>
* <li>Other items' parts: disabled (pass through to their own layer)</li>
* </ul>
*
* <p>The "Full" prefix convention (FullIdle, FullStruggle, FullWalk) is the artist's
* explicit declaration that this animation is designed to control the entire body,
* not just the item's owned regions. Without this prefix, free bones are never enabled,
* even if the GLB contains keyframes for them. This prevents accidental bone hijacking
* when an artist keyframes all bones in Blender by default.</p>
*
* @param builder the animation builder with keyframes already added
* @param ownedParts parts the item explicitly owns (always enabled)
* @param enabledParts parts the item may animate (owned + free)
* @param partsWithKeyframes parts that received actual animation data from the GLB
* @param animName resolved animation name (checked for "Full" prefix)
*/
private static void enableSelectiveParts(
KeyframeAnimation.AnimationBuilder builder,
Set<String> ownedParts,
Set<String> enabledParts,
Set<String> partsWithKeyframes
Set<String> partsWithKeyframes,
String animName
) {
// Free bones are only enabled for "Full" animations (FullIdle, FullStruggle, etc.)
// The "gltf_" prefix is added by convertClipSelective, so check for "gltf_Full"
boolean isFullBodyAnimation = animName != null &&
animName.startsWith("gltf_Full");
String[] allParts = {
"head",
"body",
@@ -588,13 +603,16 @@ public final class GltfPoseConverter {
// Always enable owned parts — the item controls these bones
part.fullyEnablePart(false);
} else if (
isFullBodyAnimation &&
enabledParts.contains(partName) &&
partsWithKeyframes.contains(partName)
) {
// Free part WITH keyframes: enable so the GLB animation drives it
// Full-body animation: free part WITH keyframes enable.
// The "Full" prefix is the artist's explicit opt-in to animate
// bones outside their declared regions.
part.fullyEnablePart(false);
} else {
// Other item's part, or free part without keyframes: disable.
// Non-Full animation, other item's part, or free part without keyframes.
// Disabled parts pass through to the lower-priority context layer.
part.setEnabled(false);
}