Animationsübergänge weiter verbessert

This commit is contained in:
2026-08-20 08:27:39 +02:00
parent 974ee5765e
commit 1df03e759f

View File

@@ -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",