diff --git a/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java b/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java index 5d91074..7b2a90a 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java @@ -258,6 +258,10 @@ public class AnimPreviewState extends BaseAppState { // Alle Clips in-place snappen (verhindert Drift im Preview) AnimComposer previewAC = findControl(model, AnimComposer.class); SkinningControl previewSC = findControl(model, SkinningControl.class); + LOG.info("[AnimPreview] Modell-Controls: AnimComposer={}, SkinningControl={}, EmbeddedClips={}", + previewAC != null ? "gefunden" : "NULL", + previewSC != null ? "gefunden" : "NULL", + previewAC != null ? previewAC.getAnimClips().size() : "n/a"); if (previewAC != null && previewSC != null) { // Eingebettete Clips snappen for (AnimClip c : new java.util.ArrayList<>(previewAC.getAnimClips())) { @@ -275,29 +279,33 @@ public class AnimPreviewState extends BaseAppState { } saveModelStripped(model, assetPath, embedCount); } - // T-Pose: AnimClip mit einem Einzel-Frame-Track für den Root-Joint. + // T-Pose: AnimClip für alle Joints in Bind-Pose. // Leerer Clip → NPE in ClipAction.doInterpolate (JME3 erwartet tracks != null). - // Ein Frame am Root-Joint in Bind-Pose → alle anderen Joints bleiben an - // ihren lokalen Bind-Pose-Transforms → SC-Matrix = Bind × Bind⁻¹ = I + // getInitialTransform() liefert die Bind-Pose → SC-Matrix = Bind × Bind⁻¹ = I // → Vertices in Y-up = stehender Charakter. AnimClip tpose = buildTPoseClip(previewSC.getArmature()); + LOG.info("[AnimPreview] T-Pose Clip: {}", tpose != null ? "erstellt" : "NULL (keine Root-Joints?)"); if (tpose != null) { previewAC.addAnimClip(tpose); setSkinningEnabled(model, true); previewAC.setCurrentAction("__tpose__"); + LOG.info("[AnimPreview] T-Pose aktiviert"); } + } else { + LOG.warn("[AnimPreview] T-Pose NICHT möglich: previewAC={}, previewSC={}", + previewAC != null ? "ok" : "NULL", + previewSC != null ? "ok" : "NULL"); } - // Kamera auf Bounding Box ausrichten + // Kamera: immer auf Hüfthöhe (0, 1, 0) zielen; Distanz aus BoundingBox model.updateGeometricState(); if (model.getWorldBound() instanceof BoundingBox bb) { float ext = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())); previewCamDist = ext * 2.8f; - previewTarget.set(bb.getCenter()); } else { previewCamDist = 3f; - previewTarget.set(0, 1, 0); } + previewTarget.set(0, 1, 0); input.animPreviewZoom = 1.0f; // Clips sammeln und melden @@ -1002,18 +1010,20 @@ public class AnimPreviewState extends BaseAppState { * Gibt null zurück wenn das Armature keine Root-Joints hat. */ private static AnimClip buildTPoseClip(com.jme3.anim.Armature armature) { - com.jme3.anim.Joint[] roots = armature.getRoots(); - if (roots == null || roots.length == 0) return null; + int jointCount = armature.getJointCount(); + if (jointCount == 0) return null; AnimClip clip = new AnimClip("__tpose__"); - com.jme3.anim.AnimTrack[] tracks = new com.jme3.anim.AnimTrack[roots.length]; - for (int i = 0; i < roots.length; i++) { - com.jme3.anim.Joint root = roots[i]; + com.jme3.anim.AnimTrack[] tracks = new com.jme3.anim.AnimTrack[jointCount]; + for (int i = 0; i < jointCount; i++) { + com.jme3.anim.Joint joint = armature.getJoint(i); + // getInitialTransform() liefert die echte Bind-Pose (nicht den aktuellen Zustand) + com.jme3.math.Transform bt = joint.getInitialTransform(); tracks[i] = new com.jme3.anim.TransformTrack( - root, + joint, new float[]{0f}, - new com.jme3.math.Vector3f[]{root.getLocalTranslation().clone()}, - new com.jme3.math.Quaternion[]{root.getLocalRotation().clone()}, - new com.jme3.math.Vector3f[]{root.getLocalScale().clone()}); + new com.jme3.math.Vector3f[]{bt.getTranslation().clone()}, + new com.jme3.math.Quaternion[]{bt.getRotation().clone()}, + new com.jme3.math.Vector3f[]{bt.getScale().clone()}); } clip.setTracks(tracks); return clip;