diff --git a/src/main/java/com/tiedup/remake/client/gltf/GltfPoseConverter.java b/src/main/java/com/tiedup/remake/client/gltf/GltfPoseConverter.java index c35b3eb..d19b831 100644 --- a/src/main/java/com/tiedup/remake/client/gltf/GltfPoseConverter.java +++ b/src/main/java/com/tiedup/remake/client/gltf/GltfPoseConverter.java @@ -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 { * * * + *

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.

+ * * @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 ownedParts, Set enabledParts, - Set partsWithKeyframes + Set 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); }