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 451c395..b10d983 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 @@ -18,6 +18,9 @@ import com.jme3.anim.AnimClip; import com.jme3.anim.AnimComposer; import com.jme3.anim.SkinningControl; import com.jme3.anim.tween.action.Action; +import com.jme3.anim.tween.action.BlendAction; +import com.jme3.anim.tween.action.BlendableAction; +import com.jme3.anim.tween.action.LinearBlendSpace; import com.jme3.app.Application; import com.jme3.app.SimpleApplication; import com.jme3.app.state.BaseAppState; @@ -86,6 +89,11 @@ public class AnimPreviewState extends BaseAppState { private float transitionElapsed = 0.15f; private static final float TRANSITION_DURATION = 0.15f; + private LinearBlendSpace blendSpace = null; + private String pendingClipName = null; + private float blendElapsed = 0f; + private static final float BLEND_DURATION = TRANSITION_DURATION; // Pose- und Offset-Blend synchron + private Node axesNode; public AnimPreviewState(SharedInput input) { this.input = input; } @@ -208,6 +216,14 @@ public class AnimPreviewState extends BaseAppState { try { currentAction.setSpeed(input.animPreviewSpeed); } catch (Exception ignored) {} } + // Blend voranschreiten: BlendWeight 0→1 über BLEND_DURATION + if (pendingClipName != null && blendSpace != null && currentModel != null) { + blendElapsed += tpf; + float bt = Math.min(1f, blendElapsed / BLEND_DURATION); + blendSpace.setValue(bt); + if (bt >= 1f) finishBlend(); + } + // Loop-Steuerung: AnimComposer spielt einmal ab – wir starten manuell neu if (currentClipName != null && currentModel != null && currentAction != null) { AnimComposer ac = findControl(currentModel, AnimComposer.class); @@ -425,8 +441,6 @@ public class AnimPreviewState extends BaseAppState { private void playClip(String clipName) { if (currentModel == null) return; - currentAction = null; - currentClipName = null; // Offset für diesen Clip aus der Map lesen (wird von EditorApp befüllt) float[] off = input.animPreviewClipOffsets.get(clipName); if (off != null && off.length >= 3) { @@ -443,6 +457,45 @@ public class AnimPreviewState extends BaseAppState { // SkinningControls auf dem gesamten Modell aktivieren – AnimComposer und // SkinningControl sitzen oft auf verschiedenen Geschwisterknoten. setSkinningEnabled(currentModel, true); + + // Wenn schon ein Clip läuft: Freeze-Pose einfrieren + BlendAction. + // blend.setTransitionLength(0) verhindert den internen 0.4s-Cross-Fade der + // BlendableAction, der sonst parallel zu unserem blendSpace.setValue läuft + // und ein "hin und her" erzeugt. + AnimComposer ac = findControl(currentModel, AnimComposer.class); + SkinningControl sc = findControl(currentModel, SkinningControl.class); + if (ac != null && sc != null && ac.getAnimClipsNames().contains(clipName) + && (currentClipName != null || pendingClipName != null)) { + AnimClip freeze = buildFreezeClip(sc.getArmature()); + if (freeze != null) { + try { + if (ac.hasAnimClip("__freeze__")) ac.removeAnimClip(ac.getAnimClip("__freeze__")); + ac.addAnimClip(freeze); + if (ac.hasAction("__blend__")) ac.removeAction("__blend__"); + LinearBlendSpace space = new LinearBlendSpace(0f, 1f); + space.setValue(0f); + BlendAction blend = ac.actionBlended("__blend__", space, "__freeze__", clipName); + blend.clearSpeedFactors(); // N/0=Infinity für length=0 Freeze-Clip vermeiden + blend.setTransitionLength(0.0); // keinen 2. internen Cross-Fade doppelt laufen lassen + ac.getLayer(AnimComposer.DEFAULT_LAYER).setCurrentAction(blend); + blendSpace = space; + pendingClipName = clipName; + blendElapsed = 0f; + currentAction = blend; + currentClipName = null; + return; + } catch (Exception e) { + LOG.warn("[AnimPreview] BlendAction fehlgeschlagen, direktes Wechseln: {}", e.getMessage()); + blendSpace = null; + pendingClipName = null; + } + } + } + + currentAction = null; + currentClipName = null; + pendingClipName = null; + blendSpace = null; playOnSpatial(currentModel, clipName); } @@ -1151,6 +1204,51 @@ public class AnimPreviewState extends BaseAppState { return clip; } + @SuppressWarnings("rawtypes") + private static AnimClip buildFreezeClip(com.jme3.anim.Armature arm) { + int n = arm.getJointCount(); + if (n == 0) return null; + AnimClip clip = new AnimClip("__freeze__"); + com.jme3.anim.AnimTrack[] tracks = new com.jme3.anim.AnimTrack[n]; + for (int i = 0; i < n; i++) { + com.jme3.anim.Joint j = arm.getJoint(i); + com.jme3.math.Transform lt = j.getLocalTransform(); + tracks[i] = new com.jme3.anim.TransformTrack( + j, new float[]{0f}, + new com.jme3.math.Vector3f[]{lt.getTranslation().clone()}, + new com.jme3.math.Quaternion[]{lt.getRotation().clone()}, + new com.jme3.math.Vector3f[]{lt.getScale().clone()}); + } + clip.setTracks(tracks); + return clip; + } + + private void finishBlend() { + String clipName = pendingClipName; + float elapsed = blendElapsed; + pendingClipName = null; + blendSpace = null; + if (currentModel == null || clipName == null) return; + AnimComposer ac = findControl(currentModel, AnimComposer.class); + if (ac == null) return; + if (ac.hasAction("__blend__")) ac.removeAction("__blend__"); + try { + // transitionLength=0 verhindert den Default-0.4s-Cross-Fade nach dem Blend + Action next = ac.action(clipName); + if (next instanceof BlendableAction ba) ba.setTransitionLength(0.0); + currentAction = ac.setCurrentAction(clipName); + currentClipName = clipName; + if (currentAction != null) { + currentAction.setSpeed(input.animPreviewSpeed); + input.animPreviewCurrentClipDuration = (float) currentAction.getLength(); + double len = currentAction.getLength(); + if (len > 0.01 && elapsed > 0) ac.setTime(elapsed % len); + } + } catch (Exception e) { + LOG.warn("[AnimPreview] Direkt-Wechsel nach Blend fehlgeschlagen: {}", e.getMessage()); + } + } + private static final String[] MODEL_SAVE_ROOTS = { null, // ASSET_ROOT (src) – wird durch ASSET_ROOT ersetzt "blight-assets/bin/main",