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 850bd44..5d91074 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 @@ -200,9 +200,7 @@ public class AnimPreviewState extends BaseAppState { currentAction = ac.setCurrentAction(currentClipName); if (currentAction != null) currentAction.setSpeed(input.animPreviewSpeed); } else { - currentAction = null; - currentClipName = null; - setSkinningEnabled(currentModel, false); + stopAll(); } } } @@ -227,11 +225,11 @@ public class AnimPreviewState extends BaseAppState { previewTarget.z + FastMath.cos(rotY) * FastMath.cos(rotX) * dist)); c.lookAt(previewTarget, Vector3f.UNIT_Y); - // Achsen: Größe proportional zur Kameradistanz, immer am Ursprung + // Achsen: Größe proportional zur Kameradistanz, immer am Weltpunkt (0,0,0) if (axesNode != null) { float s = previewCamDist * input.animPreviewZoom * 0.18f; axesNode.setLocalScale(s); - axesNode.setLocalTranslation(previewTarget); + axesNode.setLocalTranslation(Vector3f.ZERO); } previewScene.updateLogicalState(tpf); @@ -249,12 +247,6 @@ public class AnimPreviewState extends BaseAppState { try { Spatial model = loadFresh(assetPath); - // SkinningControl deaktiviert + Modell unsichtbar bis Animation läuft: - // das Armature-Node hat Rx(90°) aus dem GLB-Import → ohne Animation liegt - // das Mesh auf dem Boden (Bind-Pose × Rx90° = liegender Charakter). - setSkinningEnabled(model, false); - model.setCullHint(com.jme3.scene.Spatial.CullHint.Always); - // Im Animations-Editor soll der Charakter immer am Ursprung stehen. // Eventuelle Translation die beim GLB-Export eingebacken wurde entfernen. model.setLocalTranslation(Vector3f.ZERO); @@ -267,6 +259,7 @@ public class AnimPreviewState extends BaseAppState { AnimComposer previewAC = findControl(model, AnimComposer.class); SkinningControl previewSC = findControl(model, SkinningControl.class); if (previewAC != null && previewSC != null) { + // Eingebettete Clips snappen for (AnimClip c : new java.util.ArrayList<>(previewAC.getAnimClips())) { AnimClip snapped = de.blight.game.animation.AnimationLibrary.snapRootBoneXZ(c, previewSC.getArmature()); if (snapped != c) { @@ -274,6 +267,25 @@ public class AnimPreviewState extends BaseAppState { previewAC.addAnimClip(snapped); } } + // Eingebettete Clips aus der Datei entfernen und speichern (Datei-Größe reduzieren) + if (!previewAC.getAnimClips().isEmpty()) { + int embedCount = previewAC.getAnimClips().size(); + for (AnimClip c : new java.util.ArrayList<>(previewAC.getAnimClips())) { + previewAC.removeAnimClip(c); + } + saveModelStripped(model, assetPath, embedCount); + } + // T-Pose: AnimClip mit einem Einzel-Frame-Track für den Root-Joint. + // 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 + // → Vertices in Y-up = stehender Charakter. + AnimClip tpose = buildTPoseClip(previewSC.getArmature()); + if (tpose != null) { + previewAC.addAnimClip(tpose); + setSkinningEnabled(model, true); + previewAC.setCurrentAction("__tpose__"); + } } // Kamera auf Bounding Box ausrichten @@ -350,8 +362,6 @@ public class AnimPreviewState extends BaseAppState { // SkinningControls auf dem gesamten Modell aktivieren – AnimComposer und // SkinningControl sitzen oft auf verschiedenen Geschwisterknoten. setSkinningEnabled(currentModel, true); - // Modell erst nach Animation sichtbar machen (CullHint aus loadModel) - currentModel.setCullHint(com.jme3.scene.Spatial.CullHint.Inherit); playOnSpatial(currentModel, clipName); } @@ -383,8 +393,12 @@ public class AnimPreviewState extends BaseAppState { currentClipName = null; if (currentModel != null) { stopOnSpatial(currentModel); - setSkinningEnabled(currentModel, false); - currentModel.setCullHint(com.jme3.scene.Spatial.CullHint.Always); + // Zurück zur T-Pose: __tpose__ wiedergeben (leerer Clip = Bind-Pose) + AnimComposer ac = findControl(currentModel, AnimComposer.class); + if (ac != null && ac.getAnimClipsNames().contains("__tpose__")) { + setSkinningEnabled(currentModel, true); + ac.setCurrentAction("__tpose__"); + } } } @@ -982,6 +996,68 @@ public class AnimPreviewState extends BaseAppState { } } + /** + * Erzeugt einen AnimClip "__tpose__" mit einem Einzel-Frame-Track für alle Root-Joints. + * Hält jeden Root-Joint an seiner lokalen Bind-Pose-Transform → SC-Matrix = I → T-Pose. + * 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; + 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]; + tracks[i] = new com.jme3.anim.TransformTrack( + root, + 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()}); + } + clip.setTracks(tracks); + return clip; + } + + private static final String[] MODEL_SAVE_ROOTS = { + null, // ASSET_ROOT (src) – wird durch ASSET_ROOT ersetzt + "blight-assets/bin/main", + "blight-assets/build/resources/main", + }; + + /** Speichert das (bereits strip-bereinigte) Modell in allen bekannten Asset-Verzeichnissen. */ + private void saveModelStripped(Spatial model, String modelPath, int removedCount) { + String rel = modelPath.replace('/', java.io.File.separatorChar); + int saved = 0; + // Zuerst in ASSET_ROOT (= blight-assets/src/main/resources) + java.nio.file.Path srcFile = ASSET_ROOT.resolve(rel); + if (java.nio.file.Files.exists(srcFile)) { + try { + BinaryExporter.getInstance().save(model, srcFile.toFile()); + assets.deleteFromCache(new com.jme3.asset.ModelKey(modelPath)); + LOG.info("[AnimPreview] {} Clips entfernt, gespeichert: {}", removedCount, srcFile.toAbsolutePath()); + saved++; + } catch (Exception e) { + LOG.warn("[AnimPreview] Speichern fehlgeschlagen (src): {}", e.getMessage()); + } + } + // Auch in Gradle-Output-Verzeichnisse (damit der Laufzeit-Loader die bereinigte Datei sieht) + for (String root : new String[]{"blight-assets/bin/main", "blight-assets/build/resources/main"}) { + java.nio.file.Path f = java.nio.file.Paths.get(root).resolve(rel); + if (!java.nio.file.Files.exists(f)) continue; + try { + BinaryExporter.getInstance().save(model, f.toFile()); + LOG.info("[AnimPreview] Auch gespeichert ({}): {}", root, f.toAbsolutePath()); + saved++; + } catch (Exception e) { + LOG.warn("[AnimPreview] Speichern fehlgeschlagen ({}): {}", root, e.getMessage()); + } + } + if (saved == 0) { + LOG.warn("[AnimPreview] Modell nicht gespeichert – kein Pfad gefunden für '{}'", modelPath); + } + } + private void saveClipToFile(AnimClip clip, com.jme3.anim.Armature armature, java.nio.file.Path outFile) throws Exception { Node holder = new Node("clip_" + clip.getName()); diff --git a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java index 2885625..2e3073b 100644 --- a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java +++ b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java @@ -307,7 +307,6 @@ public class WorldScene extends BaseAppState { } } playerInput.setAnimationContext(animLib, setName, AnimationLibrary.findAssetRoot()); - // Charakter sichtbar machen: idle-Animation läuft jetzt if (characterVisual != null) { characterVisual.setCullHint(Spatial.CullHint.Inherit); } @@ -347,8 +346,8 @@ public class WorldScene extends BaseAppState { Node rotNode = new Node("charRot"); loaded.setLocalTranslation(0, offsetY, 0); rotNode.attachChild(loaded); - // Charakter verstecken bis Animation läuft: das Armature-Node hat Rx(90°) aus dem - // GLB-Import → ohne laufende Animation liegt das Mesh auf dem Boden. + // Verstecken bis idle-Animation läuft: Armature hat Rx(90°) aus GLB-Import + // → ohne Animation liegt das Mesh (SkinningControl inaktiv oder Bind-Pose unklar). rotNode.setCullHint(Spatial.CullHint.Always); Node wrapper = new Node("character"); @@ -367,11 +366,18 @@ public class WorldScene extends BaseAppState { } + private static final String[] MODEL_SAVE_ROOTS = { + "blight-assets/src/main/resources", + "blight-assets/bin/main", + "blight-assets/build/resources/main", + "assets", + }; + private void stripEmbeddedClips(Spatial model, String modelPath) { com.jme3.anim.AnimComposer ac = de.blight.game.animation.RetargetingSystem.findAnimComposer(model); if (ac == null || ac.getAnimClips().isEmpty()) { - log.info("[WorldScene] Keine eingebetteten Clips in '{}' (AnimComposer={})", - modelPath, ac != null ? "leer" : "nicht gefunden"); + log.info("[WorldScene] Keine eingebetteten Clips in '{}' ({})", + modelPath, ac != null ? "AnimComposer leer" : "kein AnimComposer"); return; } int count = ac.getAnimClips().size(); @@ -379,19 +385,22 @@ public class WorldScene extends BaseAppState { for (com.jme3.anim.AnimClip c : new java.util.ArrayList<>(ac.getAnimClips())) { ac.removeAnimClip(c); } - java.nio.file.Path assetRoot = AnimationLibrary.findAssetRoot(); - java.nio.file.Path file = assetRoot.resolve(modelPath.replace('/', java.io.File.separatorChar)); - if (!java.nio.file.Files.exists(file)) { - log.warn("[WorldScene] Modelldatei nicht gefunden zum Speichern: {} (assetRoot={})", - file.toAbsolutePath(), assetRoot.toAbsolutePath()); - assetManager.deleteFromCache(new com.jme3.asset.ModelKey(modelPath)); - return; + String rel = modelPath.replace('/', java.io.File.separatorChar); + int saved = 0; + for (String root : MODEL_SAVE_ROOTS) { + java.nio.file.Path file = java.nio.file.Paths.get(root).resolve(rel); + if (!java.nio.file.Files.exists(file)) continue; + try { + com.jme3.export.binary.BinaryExporter.getInstance().save(model, file.toFile()); + log.info("[WorldScene] Gespeichert ({}): {}", root, file.toAbsolutePath()); + saved++; + } catch (Exception e) { + log.warn("[WorldScene] Speichern fehlgeschlagen ({}): {}", file, e.getMessage()); + } } - try { - com.jme3.export.binary.BinaryExporter.getInstance().save(model, file.toFile()); - log.info("[WorldScene] {} Clips entfernt, Datei gespeichert: {}", count, file.toAbsolutePath()); - } catch (Exception e) { - log.warn("[WorldScene] Speichern fehlgeschlagen ({}): {}", file, e.getMessage()); + if (saved == 0) { + log.warn("[WorldScene] Modell nicht gespeichert – kein Pfad gefunden für '{}' (CWD={})", + modelPath, java.nio.file.Paths.get(".").toAbsolutePath()); } assetManager.deleteFromCache(new com.jme3.asset.ModelKey(modelPath)); }