diff --git a/blight-assets/src/main/resources/Characters/sets/human.animset.json b/blight-assets/src/main/resources/Characters/sets/human.animset.json index ce4c426..4b0ebdc 100644 --- a/blight-assets/src/main/resources/Characters/sets/human.animset.json +++ b/blight-assets/src/main/resources/Characters/sets/human.animset.json @@ -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 diff --git a/blight-assets/src/main/resources/Shaders/BakedTerrain.frag b/blight-assets/src/main/resources/Shaders/BakedTerrain.frag index 8466d3f..d032e46 100644 --- a/blight-assets/src/main/resources/Shaders/BakedTerrain.frag +++ b/blight-assets/src/main/resources/Shaders/BakedTerrain.frag @@ -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); diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index 9491272..d118b8a 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -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 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 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()); diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index f913ff2..7e6cfa7 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -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 animPreviewClipOffsets = new java.util.concurrent.ConcurrentHashMap<>(); + /** JavaFX → JME3: Clip-Namen die snapRootBoneXZ erhalten sollen (Locomotion). */ + public final java.util.concurrent.ConcurrentSkipListSet animPreviewLocomotionClips = + new java.util.concurrent.ConcurrentSkipListSet<>(); /** * JME3 → JavaFX: Relativer Asset-Pfad des gerade geladenen Modells. 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 b10d983..80461f3 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,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", diff --git a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java index 2e365f9..0b709e9 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java @@ -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 { diff --git a/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java index 2b2a19d..37be0a0 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java @@ -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 diff --git a/blight-game/src/main/java/de/blight/game/animation/AnimPlayer.java b/blight-game/src/main/java/de/blight/game/animation/AnimPlayer.java new file mode 100644 index 0000000..3493c08 --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/animation/AnimPlayer.java @@ -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; + } +} diff --git a/blight-game/src/main/java/de/blight/game/animation/AnimationLibrary.java b/blight-game/src/main/java/de/blight/game/animation/AnimationLibrary.java index 44a78d6..fdd55f6 100644 --- a/blight-game/src/main/java/de/blight/game/animation/AnimationLibrary.java +++ b/blight-game/src/main/java/de/blight/game/animation/AnimationLibrary.java @@ -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 clips = new LinkedHashMap<>(); /** clip name → Armatur der Quell-Datei */ private final Map armatures = new LinkedHashMap<>(); + /** Clips die snapRootBoneXZ erhalten (Locomotion: Walk, Run, Sprint, …). */ + private final Set snapClipNames = new LinkedHashSet<>(); + + /** Standard-Locomotion-Aktionen, deren gemappte Clips snapRootBoneXZ erhalten. */ + private static final Set 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); diff --git a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java index 3e13a8a..4258d17 100644 --- a/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java +++ b/blight-game/src/main/java/de/blight/game/control/PlayerInputControl.java @@ -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; } } 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 e40bd89..3fe0c0a 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 @@ -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, diff --git a/blight-game/src/main/java/de/blight/game/state/WorldInteractableState.java b/blight-game/src/main/java/de/blight/game/state/WorldInteractableState.java index b49016e..db6b951 100644 --- a/blight-game/src/main/java/de/blight/game/state/WorldInteractableState.java +++ b/blight-game/src/main/java/de/blight/game/state/WorldInteractableState.java @@ -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(); diff --git a/blight-map/src/main/map/benches/19f0ce17-838d-4938-8739-fc17de491657.bench b/blight-map/src/main/map/benches/19f0ce17-838d-4938-8739-fc17de491657.bench new file mode 100644 index 0000000..fa6b14d --- /dev/null +++ b/blight-map/src/main/map/benches/19f0ce17-838d-4938-8739-fc17de491657.bench @@ -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 +} \ No newline at end of file diff --git a/blight-map/src/main/map/benches/5a91d7c8-244c-488d-b4c9-6429a63b2afe.bench b/blight-map/src/main/map/benches/5a91d7c8-244c-488d-b4c9-6429a63b2afe.bench new file mode 100644 index 0000000..46487d7 --- /dev/null +++ b/blight-map/src/main/map/benches/5a91d7c8-244c-488d-b4c9-6429a63b2afe.bench @@ -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 +} \ No newline at end of file diff --git a/blight-map/src/main/map/blight_grass_vertex.blgv b/blight-map/src/main/map/blight_grass_vertex.blgv index fcc159b..aa2016d 100644 Binary files a/blight-map/src/main/map/blight_grass_vertex.blgv and b/blight-map/src/main/map/blight_grass_vertex.blgv differ diff --git a/blight-map/src/main/map/blight_map.blm b/blight-map/src/main/map/blight_map.blm index 6203fa8..fa61d1f 100644 Binary files a/blight-map/src/main/map/blight_map.blm and b/blight-map/src/main/map/blight_map.blm differ diff --git a/blight-map/src/main/map/blight_objects.blo b/blight-map/src/main/map/blight_objects.blo index 83b35fc..044bcfc 100644 --- a/blight-map/src/main/map/blight_objects.blo +++ b/blight-map/src/main/map/blight_objects.blo @@ -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 diff --git a/blight-map/src/main/map/chunks/voxel_17_m1_08.blvc b/blight-map/src/main/map/chunks/voxel_17_m1_08.blvc index b3323cb..7180d8a 100644 Binary files a/blight-map/src/main/map/chunks/voxel_17_m1_08.blvc and b/blight-map/src/main/map/chunks/voxel_17_m1_08.blvc differ