Animationen ingame und im editor angepasst

This commit is contained in:
2026-08-20 15:30:03 +02:00
parent 1df03e759f
commit f3b10586ad
18 changed files with 378 additions and 174 deletions

View File

@@ -10579,6 +10579,12 @@ public class EditorApp extends Application {
de.blight.game.animation.AnimOffset o = en.getValue();
input.animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz});
}
// Locomotion-Clips für snapRootBoneXZ ermitteln
java.util.Set<String> locomotionActions = java.util.Set.of("WALK", "RUN", "SPRINT", "JUMP", "RUNNING_JUMP", "IDLE", "DEFAULT");
input.animPreviewLocomotionClips.clear();
animSet.getActionMap().forEach((action, clip) -> {
if (locomotionActions.contains(action)) input.animPreviewLocomotionClips.add(clip);
});
Path animRootDir = ASSET_ROOT.resolve("Characters");
@@ -11460,6 +11466,12 @@ public class EditorApp extends Application {
de.blight.game.animation.AnimOffset o = en.getValue();
input.animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz});
}
// Locomotion-Clips für snapRootBoneXZ synchronisieren
java.util.Set<String> locoActions = java.util.Set.of("WALK", "RUN", "SPRINT", "JUMP", "RUNNING_JUMP", "IDLE", "DEFAULT");
input.animPreviewLocomotionClips.clear();
animSet.getActionMap().forEach((action, clip) -> {
if (locoActions.contains(action)) input.animPreviewLocomotionClips.add(clip);
});
// Vorschau-Modell-Pfad beibehalten
if (animSetModelCombo != null && animSetModelCombo.getValue() != null && !animSetModelCombo.getValue().isBlank()) {
animSet.setPreviewModelPath(animSetModelCombo.getValue());

View File

@@ -656,6 +656,9 @@ public class SharedInput {
/** JavaFX → JME3: Clip-Name → [tx, ty, tz] für alle Clips mit gesetztem Offset. */
public final java.util.concurrent.ConcurrentHashMap<String, float[]> animPreviewClipOffsets =
new java.util.concurrent.ConcurrentHashMap<>();
/** JavaFX → JME3: Clip-Namen die snapRootBoneXZ erhalten sollen (Locomotion). */
public final java.util.concurrent.ConcurrentSkipListSet<String> animPreviewLocomotionClips =
new java.util.concurrent.ConcurrentSkipListSet<>();
/**
* JME3 → JavaFX: Relativer Asset-Pfad des gerade geladenen Modells.

View File

@@ -18,9 +18,6 @@ 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;
@@ -89,10 +86,7 @@ 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 de.blight.game.animation.AnimPlayer animPlayer;
private Node axesNode;
@@ -216,12 +210,20 @@ 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();
// Blend voranschreiten via gemeinsamer AnimPlayer-Codebasis
if (animPlayer != null && currentModel != null) {
Action prevAction = animPlayer.getCurrentAction();
animPlayer.update(tpf);
Action newAction = animPlayer.getCurrentAction();
if (newAction != prevAction && newAction != null) {
// Blend abgeschlossen oder direkter Wechsel abgeschlossen
currentAction = newAction;
currentClipName = animPlayer.getCurrentClip();
if (currentAction != null) {
try { currentAction.setSpeed(input.animPreviewSpeed); } catch (Exception ignored) {}
input.animPreviewCurrentClipDuration = (float) currentAction.getLength();
}
}
}
// Loop-Steuerung: AnimComposer spielt einmal ab wir starten manuell neu
@@ -360,10 +362,12 @@ public class AnimPreviewState extends BaseAppState {
previewAC.setCurrentAction("__tpose__");
LOG.info("[AnimPreview] T-Pose aktiviert");
}
animPlayer = new de.blight.game.animation.AnimPlayer(previewAC, previewSC);
} else {
LOG.warn("[AnimPreview] T-Pose NICHT möglich: previewAC={}, previewSC={}",
previewAC != null ? "ok" : "NULL",
previewSC != null ? "ok" : "NULL");
animPlayer = null;
}
// Kamera: immer auf Hüfthöhe (0, 1, 0) zielen; Distanz aus BoundingBox.
@@ -454,49 +458,21 @@ public class AnimPreviewState extends BaseAppState {
input.animPreviewOffsetTz = 0f;
input.animPreviewOffsetActive = false;
}
// 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;
}
if (animPlayer != null && animPlayer.play(clipName, de.blight.game.animation.AnimPlayer.BLEND_DURATION)) {
currentAction = animPlayer.getCurrentAction();
currentClipName = animPlayer.getCurrentClip();
if (currentAction != null) {
try { currentAction.setSpeed(input.animPreviewSpeed); } catch (Exception ignored) {}
input.animPreviewCurrentClipDuration = (float) currentAction.getLength();
}
} else {
// Fallback: animPlayer nicht verfügbar (kein SkinningControl)
currentAction = null;
currentClipName = null;
playOnSpatial(currentModel, clipName);
}
currentAction = null;
currentClipName = null;
pendingClipName = null;
blendSpace = null;
playOnSpatial(currentModel, clipName);
}
private boolean playOnSpatial(Spatial s, String clipName) {
@@ -523,6 +499,10 @@ public class AnimPreviewState extends BaseAppState {
String rStr = (rr != null && rr.length > 0) ? rr[0].toString() : "null";
LOG.info("[DIAG] Track '{}' target='{}' frames={} t[0]={} r[0]={}",
clipName, j.getName(), tt.getTimes().length, tStr, rStr);
if (tr != null && tr.length > 0) {
float modelOffY = input.animPreviewOffsetActive ? input.animPreviewOffsetTy : 0f;
logHipPos(clipName, tr[0], tr[tr.length - 1], modelOffY);
}
}
}
// DIAG: Modell-Hips localTransform + IBM
@@ -557,6 +537,7 @@ public class AnimPreviewState extends BaseAppState {
private void stopAll() {
currentAction = null;
currentClipName = null;
if (animPlayer != null) animPlayer.stop();
if (currentModel != null) {
stopOnSpatial(currentModel);
// Zurück zur T-Pose: __tpose__ wiedergeben (leerer Clip = Bind-Pose)
@@ -568,6 +549,14 @@ public class AnimPreviewState extends BaseAppState {
}
}
private void logHipPos(String clipName, com.jme3.math.Vector3f first, com.jme3.math.Vector3f last, float modelOffY) {
LOG.info("[HipPos] Clip='{}' modelOffY={} | START x={} y={} z={} | END x={} y={} z={}",
clipName,
String.format("%.4f", modelOffY),
String.format("%.4f", first.x), String.format("%.4f", first.y), String.format("%.4f", first.z),
String.format("%.4f", last.x), String.format("%.4f", last.y), String.format("%.4f", last.z));
}
private void stopOnSpatial(Spatial s) {
AnimComposer ac = s.getControl(AnimComposer.class);
if (ac != null) {
@@ -718,7 +707,14 @@ public class AnimPreviewState extends BaseAppState {
saved++;
}
// Für den aktuellen Preview auch auf das Modell anwenden (wenn geladen)
if (targetAC != null) targetAC.addAnimClip(toSave);
if (targetAC != null) {
AnimClip forPreview = toSave;
if (snapArm != null && input.animPreviewLocomotionClips.contains(forPreview.getName())) {
AnimClip snapped = de.blight.game.animation.AnimationLibrary.snapRootBoneXZ(forPreview, snapArm);
if (snapped != forPreview) forPreview = snapped;
}
targetAC.addAnimClip(forPreview);
}
}
// Temporäre GLB aus clips/ löschen (nur wenn sie dort drin liegt nicht externe Dateien, nie FBX)
if (saved > 0 && animAssetPath.matches("(?i).*\\.(glb|gltf)$")
@@ -1204,51 +1200,6 @@ 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",

View File

@@ -395,7 +395,7 @@ public class TerrainEditorState extends BaseAppState {
if (loadedMapData != null) {
float[] src = loadedMapData.terrainHeight;
heights = new float[TOTAL_SIZE * TOTAL_SIZE];
int srcVerts = MapData.TERRAIN_VERTS; // 16385
int srcVerts = MapData.TERRAIN_VERTS; // 8193
if (srcVerts == TOTAL_SIZE) {
System.arraycopy(src, 0, heights, 0, heights.length);
} else {

View File

@@ -168,7 +168,7 @@ public class VoxelEditorState extends BaseAppState {
// ── Voxel-Splatmap ───────────────────────────────────────────────────────
private static final int VOX_SPLAT_SIZE = MapData.SPLAT_SIZE; // 2049
private static final int VOX_SPLAT_SIZE = MapData.SPLAT_SIZE; // 1025
private static final float VOX_WORLD_HALF = 2048f;
private static final float VOX_SPLAT_WE_PER_PX =
(VOX_WORLD_HALF * 2f) / (VOX_SPLAT_SIZE - 1); // ~2 WE/px