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:
@@ -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(
|
enableSelectiveParts(
|
||||||
builder,
|
builder,
|
||||||
ownedParts,
|
ownedParts,
|
||||||
enabledParts,
|
enabledParts,
|
||||||
partsWithKeyframes
|
partsWithKeyframes,
|
||||||
|
animName
|
||||||
);
|
);
|
||||||
|
|
||||||
KeyframeAnimation anim = builder.build();
|
KeyframeAnimation anim = builder.build();
|
||||||
@@ -554,22 +556,35 @@ public final class GltfPoseConverter {
|
|||||||
*
|
*
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>Owned parts: always enabled (the item controls these bones)</li>
|
* <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 WITH keyframes AND "Full" animation: enabled (explicit opt-in to full-body)</li>
|
||||||
* <li>Free parts WITHOUT keyframes: disabled (no data to animate, pass through to context)</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>
|
* <li>Other items' parts: disabled (pass through to their own layer)</li>
|
||||||
* </ul>
|
* </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 builder the animation builder with keyframes already added
|
||||||
* @param ownedParts parts the item explicitly owns (always enabled)
|
* @param ownedParts parts the item explicitly owns (always enabled)
|
||||||
* @param enabledParts parts the item may animate (owned + free)
|
* @param enabledParts parts the item may animate (owned + free)
|
||||||
* @param partsWithKeyframes parts that received actual animation data from the GLB
|
* @param partsWithKeyframes parts that received actual animation data from the GLB
|
||||||
|
* @param animName resolved animation name (checked for "Full" prefix)
|
||||||
*/
|
*/
|
||||||
private static void enableSelectiveParts(
|
private static void enableSelectiveParts(
|
||||||
KeyframeAnimation.AnimationBuilder builder,
|
KeyframeAnimation.AnimationBuilder builder,
|
||||||
Set<String> ownedParts,
|
Set<String> ownedParts,
|
||||||
Set<String> enabledParts,
|
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 = {
|
String[] allParts = {
|
||||||
"head",
|
"head",
|
||||||
"body",
|
"body",
|
||||||
@@ -588,13 +603,16 @@ public final class GltfPoseConverter {
|
|||||||
// Always enable owned parts — the item controls these bones
|
// Always enable owned parts — the item controls these bones
|
||||||
part.fullyEnablePart(false);
|
part.fullyEnablePart(false);
|
||||||
} else if (
|
} else if (
|
||||||
|
isFullBodyAnimation &&
|
||||||
enabledParts.contains(partName) &&
|
enabledParts.contains(partName) &&
|
||||||
partsWithKeyframes.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);
|
part.fullyEnablePart(false);
|
||||||
} else {
|
} 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.
|
// Disabled parts pass through to the lower-priority context layer.
|
||||||
part.setEnabled(false);
|
part.setEnabled(false);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user