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

@@ -40,7 +40,7 @@
"Sitting Idle": {
"tx": 0.0,
"ty": 0.0,
"tz": -0.5,
"tz": -0.55,
"rx": 0.0,
"ry": 0.0,
"rz": 0.0
@@ -48,7 +48,7 @@
"Sit To Stand": {
"tx": 0.0,
"ty": 0.0,
"tz": -0.5,
"tz": -0.55,
"rx": 0.0,
"ry": 0.0,
"rz": 0.0

View File

@@ -34,10 +34,10 @@ vec4 triplanarArray(vec2 uvX, vec2 uvY, vec2 uvZ, vec3 bw, int idx) {
#endif
void main() {
// World-space splatmap UV (identisch zur Painting-Logik in TerrainEditorState)
// World-space splatmap UV (identisch zur Painting-Logik in TerrainEditorState, WORLD_HALF=1024)
vec2 splatUV = vec2(
(vWorldPos.x + 2048.0) / 4096.0,
1.0 - (vWorldPos.z + 2048.0) / 4096.0
(vWorldPos.x + 1024.0) / 2048.0,
1.0 - (vWorldPos.z + 1024.0) / 2048.0
);
vec4 a0 = texture(m_AlphaMap, splatUV);

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

View File

@@ -0,0 +1,190 @@
package de.blight.game.animation;
import com.jme3.anim.AnimClip;
import com.jme3.anim.AnimComposer;
import com.jme3.anim.SkinningControl;
import com.jme3.anim.TransformTrack;
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.math.Quaternion;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Kapselt das Abspielen von AnimClips: BlendAction+FreezeClip für nahtlose
* Übergänge. Gemeinsame Codebasis für Editor (AnimPreviewState) und Spiel
* (PlayerInputControl).
*
* snapRootBoneXZ für Locomotion-Clips bleibt Aufgabe des Aufrufers (AnimationLibrary).
*/
public class AnimPlayer {
private static final Logger log = LoggerFactory.getLogger(AnimPlayer.class);
/** Standard-Übergangsdauer in Sekunden. */
public static final float BLEND_DURATION = 0.15f;
private final AnimComposer composer;
private final SkinningControl skinning;
private String currentClip;
private Action currentAction;
private LinearBlendSpace blendSpace;
private String pendingClip;
private float blendElapsed;
public AnimPlayer(AnimComposer composer, SkinningControl skinning) {
this.composer = composer;
this.skinning = skinning;
}
/**
* Spielt einen Clip ab.
* Bei laufendem Clip und {@code transition > 0}: nahtloser Übergang
* via BlendAction+FreezeClip (kein interner JME3-Cross-Fade).
*
* @param clipName Ziel-Clip (muss im AnimComposer bekannt sein)
* @param transition Übergangsdauer in Sekunden; 0 = harter Schnitt
* @return true wenn Playback erfolgreich gestartet
*/
public boolean play(String clipName, float transition) {
if (composer == null) return false;
if (!composer.getAnimClipsNames().contains(clipName)) {
log.warn("[AnimPlayer] Clip '{}' nicht im AnimComposer", clipName);
return false;
}
if (skinning != null && !skinning.isEnabled()) skinning.setEnabled(true);
if ((currentClip != null || pendingClip != null) && transition > 0f) {
com.jme3.anim.Armature arm = skinning != null ? skinning.getArmature() : null;
AnimClip freeze = buildFreezeClip(arm);
if (freeze != null) {
try {
if (composer.hasAnimClip("__freeze__"))
composer.removeAnimClip(composer.getAnimClip("__freeze__"));
composer.addAnimClip(freeze);
if (composer.hasAction("__blend__"))
composer.removeAction("__blend__");
LinearBlendSpace space = new LinearBlendSpace(0f, 1f);
space.setValue(0f);
BlendAction blend = composer.actionBlended("__blend__", space, "__freeze__", clipName);
blend.clearSpeedFactors();
// setTransitionLength(0) verhindert internen JME3-Cross-Fade,
// der sonst parallel zu unserem blendSpace.setValue läuft.
blend.setTransitionLength(0.0);
composer.getLayer(AnimComposer.DEFAULT_LAYER).setCurrentAction(blend);
blendSpace = space;
pendingClip = clipName;
blendElapsed = 0f;
currentAction = blend;
currentClip = null;
log.debug("[AnimPlayer] Blend → '{}'", clipName);
return true;
} catch (Exception e) {
log.warn("[AnimPlayer] BlendAction fehlgeschlagen, direktes Wechseln: {}", e.getMessage());
blendSpace = null;
pendingClip = null;
}
}
}
return directPlay(clipName);
}
/** Muss jeden Frame aufgerufen werden (Blend-Fortschritt). */
public void update(float tpf) {
if (pendingClip == null || blendSpace == null) return;
blendElapsed += tpf;
float bt = Math.min(1f, blendElapsed / BLEND_DURATION);
blendSpace.setValue(bt);
if (bt >= 1f) finishBlend();
}
/** Setzt Playback-Zustand zurück (kein AnimComposer-Stop). */
public void stop() {
pendingClip = null;
blendSpace = null;
currentClip = null;
currentAction = null;
}
/**
* Name des aktiven Clips: Target-Clip während Blend, stabiler Clip danach.
* Gibt null zurück wenn nichts spielt.
*/
public String getCurrentClip() {
return pendingClip != null ? pendingClip : currentClip;
}
/** Aktuelle JME3-Action (BlendAction während Blend, ClipAction danach). */
public Action getCurrentAction() { return currentAction; }
// ── intern ───────────────────────────────────────────────────────────────
private boolean directPlay(String clipName) {
try {
Action act = composer.action(clipName);
if (act instanceof BlendableAction ba) ba.setTransitionLength(0.0);
currentAction = composer.setCurrentAction(clipName);
currentClip = clipName;
pendingClip = null;
blendSpace = null;
log.debug("[AnimPlayer] direktPlay → '{}'", clipName);
return currentAction != null;
} catch (Exception e) {
log.warn("[AnimPlayer] direktPlay('{}') fehlgeschlagen: {}", clipName, e.getMessage());
return false;
}
}
private void finishBlend() {
String clip = pendingClip;
float elapsed = blendElapsed;
pendingClip = null;
blendSpace = null;
if (clip == null) return;
try {
if (composer.hasAction("__blend__")) composer.removeAction("__blend__");
} catch (Exception ignored) {}
try {
Action next = composer.action(clip);
if (next instanceof BlendableAction ba) ba.setTransitionLength(0.0);
currentAction = composer.setCurrentAction(clip);
currentClip = clip;
double len = currentAction != null ? currentAction.getLength() : 0.0;
// AnimLayer.setTime: kein extra controlUpdate, kein loop=false-Seiteneffekt.
// Clip startet dort, wo der Übergang endete (kein sichtbarer Sprung).
if (len > 0.01 && elapsed > 0) {
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime(elapsed % len);
}
log.debug("[AnimPlayer] finishBlend → '{}'", clip);
} catch (Exception e) {
log.warn("[AnimPlayer] finishBlend('{}') fehlgeschlagen: {}", clip, e.getMessage());
}
}
@SuppressWarnings("rawtypes")
private static AnimClip buildFreezeClip(com.jme3.anim.Armature arm) {
if (arm == null || arm.getJointCount() == 0) return null;
int n = arm.getJointCount();
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);
Transform lt = j.getLocalTransform();
tracks[i] = new TransformTrack(
j, new float[]{0f},
new Vector3f[]{lt.getTranslation().clone()},
new Quaternion[]{lt.getRotation().clone()},
new Vector3f[]{lt.getScale().clone()});
}
clip.setTracks(tracks);
return clip;
}
}

View File

@@ -18,6 +18,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
/**
* Lädt alle Clip-Dateien aus {@code animations/clips/} beim Start.
* Clip-Schlüssel entsprechen dem Dateinamen ohne Extension (= Clip-Name).
@@ -39,6 +40,13 @@ public class AnimationLibrary extends BaseAppState {
private final Map<String, AnimClip> clips = new LinkedHashMap<>();
/** clip name → Armatur der Quell-Datei */
private final Map<String, Armature> armatures = new LinkedHashMap<>();
/** Clips die snapRootBoneXZ erhalten (Locomotion: Walk, Run, Sprint, …). */
private final Set<String> snapClipNames = new LinkedHashSet<>();
/** Standard-Locomotion-Aktionen, deren gemappte Clips snapRootBoneXZ erhalten. */
private static final Set<String> LOCOMOTION_ACTIONS = Set.of(
"WALK", "RUN", "SPRINT", "JUMP", "RUNNING_JUMP", "IDLE", "DEFAULT"
);
// ── Lifecycle ─────────────────────────────────────────────────────────────
@@ -118,6 +126,9 @@ public class AnimationLibrary extends BaseAppState {
log.warn("[AnimLib] applyTo: Retargeting für '{}' schlug fehl", clipName);
return false;
}
if (snapClipNames.contains(clipName)) {
target = snapRootBoneXZ(target, sc.getArmature());
}
ac.addAnimClip(target);
log.info("[AnimLib] Clip '{}' zu AnimComposer von '{}' hinzugefügt", clipName, model.getName());
@@ -201,6 +212,24 @@ public class AnimationLibrary extends BaseAppState {
}
}
/**
* Bestimmt anhand des AnimSets welche Clips snapRootBoneXZ erhalten sollen
* (Locomotion-Aktionen: Walk, Run, Sprint, Jump, Idle, Default).
* Muss vor dem ersten {@link #applyTo}-Aufruf für das Player-Modell gesetzt werden.
*/
public void configureLocomotionSnap(Path assetRoot, String animSetName) {
snapClipNames.clear();
try {
AnimSet set = AnimSet.load(assetRoot.resolve("Characters").resolve("sets"), animSetName);
set.getActionMap().forEach((action, clip) -> {
if (LOCOMOTION_ACTIONS.contains(action)) snapClipNames.add(clip);
});
log.info("[AnimLib] snapRootBoneXZ für Locomotion-Clips: {}", snapClipNames);
} catch (Exception e) {
log.warn("[AnimLib] AnimSet '{}' nicht ladbar für Locomotion-Snap: {}", animSetName, e.getMessage());
}
}
// ── Loading ───────────────────────────────────────────────────────────────
private void loadAll() {
@@ -307,9 +336,6 @@ public class AnimationLibrary extends BaseAppState {
for (String name : ac.getAnimClipsNames()) {
com.jme3.anim.AnimClip animClip = ac.getAnimClip(name);
if (armature != null) {
animClip = snapRootBoneXZ(animClip, armature);
}
clips.put(name, animClip);
if (armature != null) armatures.put(name, armature);
log.info("[AnimLib] Clip geladen: '{}' aus {}", name, assetKey);

View File

@@ -10,6 +10,7 @@ import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.scene.Spatial;
import de.blight.game.animation.AnimOffset;
import de.blight.game.animation.AnimPlayer;
import de.blight.game.animation.AnimSet;
import de.blight.game.animation.AnimationAction;
import de.blight.game.animation.AnimationLibrary;
@@ -53,6 +54,7 @@ public class PlayerInputControl {
private boolean animCtxLogged = false;
private AnimComposer animComposer;
private AnimPlayer animPlayer;
private String runningClip;
private com.jme3.anim.SkinningControl skinningControl = null;
@@ -91,6 +93,7 @@ public class PlayerInputControl {
private int jumpFrames = 0;
private int groundGraceFrames = 0;
private double nextTransitionLength = 0.0;
private boolean pickupActive = false;
private float pickupRemaining = 0f;
@@ -156,6 +159,7 @@ public class PlayerInputControl {
this.assetRoot = assetRoot;
this.currentAnim = null;
this.runningClip = null;
animLib.configureLocomotionSnap(assetRoot, animSetName);
this.animComposer = (visual != null) ? RetargetingSystem.findAnimComposer(visual) : null;
log.info("[AnimCtx] AnimComposer gefunden: {}", animComposer != null);
skinningControl = findSkinningControl(visual);
@@ -164,6 +168,9 @@ public class PlayerInputControl {
armature = skinningControl.getArmature();
logJointNames(armature);
}
animPlayer = (animComposer != null && skinningControl != null)
? new AnimPlayer(animComposer, skinningControl)
: null;
if (visual != null) {
visualBaseTranslation = visual.getLocalTranslation().clone();
}
@@ -336,18 +343,16 @@ public class PlayerInputControl {
inputsBlocked = true;
if (animComposer != null) {
if (animLib != null && visual != null) animLib.ensureApplied(reviveClip, visual);
// transitionLength=0: verhindert den 0.4 s Überblend-Effekt bei speed=0.
// Ohne diesen Fix wäre transitionWeight=time/0.4=0 → Charakter zeigt Idle-Pose.
com.jme3.anim.tween.action.Action reviveAction = animComposer.action(reviveClip);
if (reviveAction instanceof com.jme3.anim.tween.action.BlendableAction ba) {
ba.setTransitionLength(0.0);
}
frozenReviveAction = animComposer.setCurrentAction(reviveClip);
if (animPlayer == null) animPlayer = new AnimPlayer(animComposer, skinningControl);
// transition=0: harter Schnitt, kein BlendAction → Charakter startet sofort in frame 0
animPlayer.play(reviveClip, 0f);
frozenReviveAction = animPlayer.getCurrentAction();
if (frozenReviveAction != null) {
frozenReviveAction.setSpeed(0f);
runningClip = animPlayer.getCurrentClip();
log.info("[Intro] REVIVE '{}' eingefroren (frame 0)", reviveClip);
} else {
log.warn("[Intro] setCurrentAction('{}')null REVIVE nicht eingefroren", reviveClip);
log.warn("[Intro] REVIVE '{}' → action null", reviveClip);
}
} else {
log.warn("[Intro] animComposer null REVIVE kann nicht eingefroren werden");
@@ -456,6 +461,11 @@ public class PlayerInputControl {
public void update(float tpf) {
if (physicsChar == null) return;
if (animPlayer != null) {
animPlayer.update(tpf);
runningClip = animPlayer.getCurrentClip();
}
if (visual != null && animOffsetSpeed > 0f) {
Vector3f delta = animOffsetTarget.subtract(animOffsetCurrent);
float dist = delta.length();
@@ -764,7 +774,28 @@ public class PlayerInputControl {
if (animOffsetCurrent.distanceSquared(worldOffset) > 1e-4f) {
animOffsetSpeed = 5.0f;
}
log.info("[AnimOffset] Clip '{}' → Ziel ({},{},{})", clip, worldOffset.x, worldOffset.y, worldOffset.z);
log.debug("[AnimOffset] Clip '{}' → Ziel ({},{},{})", clip, worldOffset.x, worldOffset.y, worldOffset.z);
}
/**
* Setzt den Anim-Offset für eine AnimationAction sofort (kein Lerp).
* Verwenden bei Übergängen, bei denen der Lerp die Skelett-Animation kompensieren würde
* (z.B. sit-down: Offset-Lerp 0→-0.5m hebt Hip-Bone-Vorwärtsbewegung auf → Hüfte wirkt statisch).
*/
public void applyActionOffsetInstant(AnimationAction action) {
if (visual == null || animLib == null || animSetName == null) return;
String clip = AnimationLibrary.getClipForAction(assetRoot, animSetName, action);
if (clip == null) return;
AnimOffset off = animOffsets.get(clip);
if (off == null) { clearAnimOffsetInstant(); return; }
Quaternion facing = visual.getLocalRotation().clone();
Vector3f worldOffset = facing.mult(new Vector3f(off.tx, off.ty, off.tz));
animOffsetTarget.set(worldOffset);
animOffsetCurrent.set(worldOffset);
animOffsetSpeed = 0f;
visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(animOffsetCurrent));
log.info("[AnimOffset] Sofort: action={} clip='{}' → ({},{},{})", action, clip,
worldOffset.x, worldOffset.y, worldOffset.z);
}
public void clearAnimOffset() {
@@ -821,29 +852,17 @@ public class PlayerInputControl {
log.info("[Anim] tryPlay('{}') → applyTo FAILED", clip);
return false;
}
// Optionale Überblend-Zeit (einmalig, wird nach Verwendung auf 0 zurückgesetzt)
double transLen = nextTransitionLength;
if (animPlayer == null) {
animPlayer = new AnimPlayer(animComposer, skinningControl);
}
float transLen = (float) nextTransitionLength;
nextTransitionLength = 0.0;
if (transLen > 0.0) {
com.jme3.anim.tween.action.Action nextAction = animComposer.action(clip);
if (nextAction instanceof com.jme3.anim.tween.action.BlendableAction) {
((com.jme3.anim.tween.action.BlendableAction) nextAction).setTransitionLength(transLen);
log.info("[Anim] Transition {} → '{}' über {:.2f}s", runningClip, clip, transLen);
}
boolean ok = animPlayer.play(clip, transLen);
log.info("[Anim] AnimPlayer.play('{}') transLen={} → {}", clip, transLen, ok ? "OK" : "FAILED");
if (ok) {
runningClip = animPlayer.getCurrentClip();
applyAnimOffset(clip);
}
// Erst Action setzen, DANN SkinningControl aktivieren
// vermeidet 1 Frame in Bind-Pose × Armature-Rx90° = liegender Charakter.
com.jme3.anim.tween.action.Action action = animComposer.setCurrentAction(clip);
log.info("[Anim] setCurrentAction('{}') → {}", clip, action != null ? "OK" : "FAILED");
if (action == null) {
return false;
}
if (skinningControl != null && !skinningControl.isEnabled()) {
skinningControl.setEnabled(true);
log.info("[Anim] SkinningControl aktiviert nach Action '{}'", clip);
}
runningClip = clip;
applyAnimOffset(clip);
return true;
return ok;
}
}

View File

@@ -130,9 +130,9 @@ public class WorldScene extends BaseAppState {
if (loadedMapData == null) return result;
int size = de.blight.common.MapData.SPLAT_SIZE;
int px = Math.max(0, Math.min(size - 1, Math.round((worldX + 2048f) / 2f)));
// Z ist im Splatmap gespiegelt (Editor: pz = (size-1) - round((z+2048)/2))
int pz = Math.max(0, Math.min(size - 1, (size - 1) - Math.round((worldZ + 2048f) / 2f)));
int px = Math.max(0, Math.min(size - 1, Math.round((worldX + 1024f) / 2f)));
// Z ist im Splatmap gespiegelt (Editor: pz = (size-1) - round((z+1024)/2))
int pz = Math.max(0, Math.min(size - 1, (size - 1) - Math.round((worldZ + 1024f) / 2f)));
int idx = pz * size + px;
// Upper/Third-Splatmap unterdrücken die Basistextur visuell (Overlay-Modell).
@@ -540,9 +540,12 @@ public class WorldScene extends BaseAppState {
@Override protected void onDisable() {}
private void setupAnimationContext() {
animLib.applyAllTo(characterVisual != null ? characterVisual : character);
MainCharacter mc = findMainCharacter();
String setName = (mc != null) ? mc.getAnimSetPath() : null;
if (setName != null) {
animLib.configureLocomotionSnap(AnimationLibrary.findAssetRoot(), setName);
}
animLib.applyAllTo(characterVisual != null ? characterVisual : character);
log.info("[AnimCtx] MainCharacter: {} animSetPath: {} clipCount: {} clips: {}",
mc != null ? mc.getCharacterId() : "null",
setName,

View File

@@ -58,7 +58,6 @@ public class WorldInteractableState extends BaseAppState {
private static final float BENCH_RANGE = 5f;
private static final float BED_RANGE = 6f;
private static final float WALK_TIMEOUT = 12f;
private static final float BENCH_SIT_MOVE_DIST = 0.5f;
// Nach dem Aufstehen: Bank-Physik erst re-enablen wenn Charakter weit genug weg
private static final float BENCH_REENABLE_DIST_SQ = 0.36f; // 0.6m Radius
@@ -67,8 +66,6 @@ public class WorldInteractableState extends BaseAppState {
private float benchPendingX = 0f;
private float benchPendingZ = 0f;
private float benchPendingTimer = 0f;
/** Blickrichtung des Charakters beim Hinsetzen (weg von der Bank); für Positions-Versatz nach Anim. */
private Vector3f currentBenchSitDir = null;
// ── Abhängigkeiten ────────────────────────────────────────────────────────
@@ -333,9 +330,6 @@ public class WorldInteractableState extends BaseAppState {
InteractableEntry entry = entries.get(targetIdx);
float rotY = getSitFacingRotY(entry);
Vector3f sitDir = new Vector3f((float) Math.cos(rotY), 0f, (float) Math.sin(rotY));
if (isBench(entry)) {
currentBenchSitDir = sitDir.clone();
}
playerInput.requestTurn(sitDir, 0.35f, () -> startSitAnim(entry));
}
@@ -360,18 +354,11 @@ public class WorldInteractableState extends BaseAppState {
AnimationAction downAction = isBench(entry) ? AnimationAction.SIT_DOWN : AnimationAction.LIE_DOWN;
AnimationAction idleAction = isBench(entry) ? AnimationAction.SITTING : AnimationAction.LYING;
if (isBench(entry)) {
playerInput.applyActionOffsetInstant(downAction);
}
playerInput.requestAnimation(downAction, 0f, () -> {
if (isBench(entry) && currentBenchSitDir != null) {
Vector3f cur = physicsChar.getPhysicsLocation();
Vector3f delta = currentBenchSitDir.mult(-BENCH_SIT_MOVE_DIST);
physicsChar.setPhysicsLocation(cur.add(delta));
// Sofortiges Spatial-Update verhindert 1-Frame-Kamerazuckeln
Spatial phySpatial = physicsChar.getSpatial();
if (phySpatial != null) {
phySpatial.setLocalTranslation(phySpatial.getLocalTranslation().add(delta));
}
log.info("[WorldInteractable] Charakter 50cm zur Bank verschoben.");
}
if (!isBench(entry)) snapToSitPos(entry);
playerInput.lockInPlace();
if (isBench(entry)) playerInput.setNextAnimTransition(0.2);
@@ -412,20 +399,8 @@ public class WorldInteractableState extends BaseAppState {
playerInput.requestAnimation(upAction, 0f, () -> {
if (isBench(entry)) {
// Nach stand_up_bench: Charakter 50cm von Bank wegbewegen
if (currentBenchSitDir != null) {
Vector3f cur = physicsChar.getPhysicsLocation();
Vector3f delta = currentBenchSitDir.mult(BENCH_SIT_MOVE_DIST);
physicsChar.setPhysicsLocation(cur.add(delta));
Spatial phySpatial = physicsChar.getSpatial();
if (phySpatial != null) {
phySpatial.setLocalTranslation(phySpatial.getLocalTranslation().add(delta));
}
currentBenchSitDir = null;
playerInput.setGroundGrace(4);
playerInput.setNextAnimTransition(0.2);
log.info("[WorldInteractable] Charakter 50cm von Bank wegbewegt.");
}
playerInput.setGroundGrace(4);
playerInput.setNextAnimTransition(0.2);
playerInput.clearAnimOffset();
benchPendingId = entry.interactableId();
benchPendingX = entry.worldX();

View File

@@ -0,0 +1,9 @@
{
"id": "19f0ce17-838d-4938-8739-fc17de491657",
"benchType": "Simple",
"sitzX": 237.17992,
"sitzY": 6.99606,
"sitzZ": -891.853,
"sitzRotY": -0.8307737,
"sitzSet": true
}

View File

@@ -0,0 +1,9 @@
{
"id": "5a91d7c8-244c-488d-b4c9-6429a63b2afe",
"benchType": "Simple",
"sitzX": 238.92255,
"sitzY": 6.996063,
"sitzZ": -892.69714,
"sitzRotY": 1.9757149,
"sitzSet": true
}

View File

@@ -2,3 +2,10 @@
Models/northcoast/wrack1.j3o 286.06750 -3.50554 -947.28595 -1.76657 1.00000 -0.00000 -0.50184 false true true 30.00000 80.00000 120.00000
Models/trees/palm/palm_20260816_213338.j3o 277.36694 2.74803 -956.39905 0.43292 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/trees/palm/palm_20260816_213338.j3o 280.27863 2.66144 -928.60248 -1.51992 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/imported/bank1.j3o 237.17992 6.49606 -891.85303 -2.40157 1.00000 -0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BENCH 19f0ce17-838d-4938-8739-fc17de491657
Models/trees/palm/palm_20260816_213341.j3o 249.12691 6.49606 -895.80035 -0.96942 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/trees/palm/palm_20260816_213341.j3o 243.51527 6.49606 -894.84039 -2.48591 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/trees/palm/palm_20260816_213338.j3o 233.49467 4.51015 -892.32526 2.66339 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/trees/palm/palm_20260816_213341.j3o 247.05431 6.44288 -888.47949 1.23606 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/trees/palm/palm_20260816_213341.j3o 239.43253 6.49157 -886.23218 -2.49448 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/trees/palm/palm_20260816_213341.j3o 270.73062 3.06689 -913.67090 -1.45828 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000