Animationen ingame und im editor angepasst
This commit is contained in:
@@ -40,7 +40,7 @@
|
|||||||
"Sitting Idle": {
|
"Sitting Idle": {
|
||||||
"tx": 0.0,
|
"tx": 0.0,
|
||||||
"ty": 0.0,
|
"ty": 0.0,
|
||||||
"tz": -0.5,
|
"tz": -0.55,
|
||||||
"rx": 0.0,
|
"rx": 0.0,
|
||||||
"ry": 0.0,
|
"ry": 0.0,
|
||||||
"rz": 0.0
|
"rz": 0.0
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
"Sit To Stand": {
|
"Sit To Stand": {
|
||||||
"tx": 0.0,
|
"tx": 0.0,
|
||||||
"ty": 0.0,
|
"ty": 0.0,
|
||||||
"tz": -0.5,
|
"tz": -0.55,
|
||||||
"rx": 0.0,
|
"rx": 0.0,
|
||||||
"ry": 0.0,
|
"ry": 0.0,
|
||||||
"rz": 0.0
|
"rz": 0.0
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ vec4 triplanarArray(vec2 uvX, vec2 uvY, vec2 uvZ, vec3 bw, int idx) {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
void main() {
|
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(
|
vec2 splatUV = vec2(
|
||||||
(vWorldPos.x + 2048.0) / 4096.0,
|
(vWorldPos.x + 1024.0) / 2048.0,
|
||||||
1.0 - (vWorldPos.z + 2048.0) / 4096.0
|
1.0 - (vWorldPos.z + 1024.0) / 2048.0
|
||||||
);
|
);
|
||||||
|
|
||||||
vec4 a0 = texture(m_AlphaMap, splatUV);
|
vec4 a0 = texture(m_AlphaMap, splatUV);
|
||||||
|
|||||||
@@ -10579,6 +10579,12 @@ public class EditorApp extends Application {
|
|||||||
de.blight.game.animation.AnimOffset o = en.getValue();
|
de.blight.game.animation.AnimOffset o = en.getValue();
|
||||||
input.animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz});
|
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");
|
Path animRootDir = ASSET_ROOT.resolve("Characters");
|
||||||
|
|
||||||
@@ -11460,6 +11466,12 @@ public class EditorApp extends Application {
|
|||||||
de.blight.game.animation.AnimOffset o = en.getValue();
|
de.blight.game.animation.AnimOffset o = en.getValue();
|
||||||
input.animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz});
|
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
|
// Vorschau-Modell-Pfad beibehalten
|
||||||
if (animSetModelCombo != null && animSetModelCombo.getValue() != null && !animSetModelCombo.getValue().isBlank()) {
|
if (animSetModelCombo != null && animSetModelCombo.getValue() != null && !animSetModelCombo.getValue().isBlank()) {
|
||||||
animSet.setPreviewModelPath(animSetModelCombo.getValue());
|
animSet.setPreviewModelPath(animSetModelCombo.getValue());
|
||||||
|
|||||||
@@ -656,6 +656,9 @@ public class SharedInput {
|
|||||||
/** JavaFX → JME3: Clip-Name → [tx, ty, tz] für alle Clips mit gesetztem Offset. */
|
/** JavaFX → JME3: Clip-Name → [tx, ty, tz] für alle Clips mit gesetztem Offset. */
|
||||||
public final java.util.concurrent.ConcurrentHashMap<String, float[]> animPreviewClipOffsets =
|
public final java.util.concurrent.ConcurrentHashMap<String, float[]> animPreviewClipOffsets =
|
||||||
new java.util.concurrent.ConcurrentHashMap<>();
|
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.
|
* JME3 → JavaFX: Relativer Asset-Pfad des gerade geladenen Modells.
|
||||||
|
|||||||
@@ -18,9 +18,6 @@ import com.jme3.anim.AnimClip;
|
|||||||
import com.jme3.anim.AnimComposer;
|
import com.jme3.anim.AnimComposer;
|
||||||
import com.jme3.anim.SkinningControl;
|
import com.jme3.anim.SkinningControl;
|
||||||
import com.jme3.anim.tween.action.Action;
|
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.Application;
|
||||||
import com.jme3.app.SimpleApplication;
|
import com.jme3.app.SimpleApplication;
|
||||||
import com.jme3.app.state.BaseAppState;
|
import com.jme3.app.state.BaseAppState;
|
||||||
@@ -89,10 +86,7 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
private float transitionElapsed = 0.15f;
|
private float transitionElapsed = 0.15f;
|
||||||
private static final float TRANSITION_DURATION = 0.15f;
|
private static final float TRANSITION_DURATION = 0.15f;
|
||||||
|
|
||||||
private LinearBlendSpace blendSpace = null;
|
private de.blight.game.animation.AnimPlayer animPlayer;
|
||||||
private String pendingClipName = null;
|
|
||||||
private float blendElapsed = 0f;
|
|
||||||
private static final float BLEND_DURATION = TRANSITION_DURATION; // Pose- und Offset-Blend synchron
|
|
||||||
|
|
||||||
private Node axesNode;
|
private Node axesNode;
|
||||||
|
|
||||||
@@ -216,12 +210,20 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
try { currentAction.setSpeed(input.animPreviewSpeed); } catch (Exception ignored) {}
|
try { currentAction.setSpeed(input.animPreviewSpeed); } catch (Exception ignored) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Blend voranschreiten: BlendWeight 0→1 über BLEND_DURATION
|
// Blend voranschreiten via gemeinsamer AnimPlayer-Codebasis
|
||||||
if (pendingClipName != null && blendSpace != null && currentModel != null) {
|
if (animPlayer != null && currentModel != null) {
|
||||||
blendElapsed += tpf;
|
Action prevAction = animPlayer.getCurrentAction();
|
||||||
float bt = Math.min(1f, blendElapsed / BLEND_DURATION);
|
animPlayer.update(tpf);
|
||||||
blendSpace.setValue(bt);
|
Action newAction = animPlayer.getCurrentAction();
|
||||||
if (bt >= 1f) finishBlend();
|
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
|
// Loop-Steuerung: AnimComposer spielt einmal ab – wir starten manuell neu
|
||||||
@@ -360,10 +362,12 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
previewAC.setCurrentAction("__tpose__");
|
previewAC.setCurrentAction("__tpose__");
|
||||||
LOG.info("[AnimPreview] T-Pose aktiviert");
|
LOG.info("[AnimPreview] T-Pose aktiviert");
|
||||||
}
|
}
|
||||||
|
animPlayer = new de.blight.game.animation.AnimPlayer(previewAC, previewSC);
|
||||||
} else {
|
} else {
|
||||||
LOG.warn("[AnimPreview] T-Pose NICHT möglich: previewAC={}, previewSC={}",
|
LOG.warn("[AnimPreview] T-Pose NICHT möglich: previewAC={}, previewSC={}",
|
||||||
previewAC != null ? "ok" : "NULL",
|
previewAC != null ? "ok" : "NULL",
|
||||||
previewSC != null ? "ok" : "NULL");
|
previewSC != null ? "ok" : "NULL");
|
||||||
|
animPlayer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kamera: immer auf Hüfthöhe (0, 1, 0) zielen; Distanz aus BoundingBox.
|
// 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.animPreviewOffsetTz = 0f;
|
||||||
input.animPreviewOffsetActive = false;
|
input.animPreviewOffsetActive = false;
|
||||||
}
|
}
|
||||||
// SkinningControls auf dem gesamten Modell aktivieren – AnimComposer und
|
|
||||||
// SkinningControl sitzen oft auf verschiedenen Geschwisterknoten.
|
|
||||||
setSkinningEnabled(currentModel, true);
|
setSkinningEnabled(currentModel, true);
|
||||||
|
|
||||||
// Wenn schon ein Clip läuft: Freeze-Pose einfrieren + BlendAction.
|
if (animPlayer != null && animPlayer.play(clipName, de.blight.game.animation.AnimPlayer.BLEND_DURATION)) {
|
||||||
// blend.setTransitionLength(0) verhindert den internen 0.4s-Cross-Fade der
|
currentAction = animPlayer.getCurrentAction();
|
||||||
// BlendableAction, der sonst parallel zu unserem blendSpace.setValue läuft
|
currentClipName = animPlayer.getCurrentClip();
|
||||||
// und ein "hin und her" erzeugt.
|
if (currentAction != null) {
|
||||||
AnimComposer ac = findControl(currentModel, AnimComposer.class);
|
try { currentAction.setSpeed(input.animPreviewSpeed); } catch (Exception ignored) {}
|
||||||
SkinningControl sc = findControl(currentModel, SkinningControl.class);
|
input.animPreviewCurrentClipDuration = (float) currentAction.getLength();
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} 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) {
|
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";
|
String rStr = (rr != null && rr.length > 0) ? rr[0].toString() : "null";
|
||||||
LOG.info("[DIAG] Track '{}' target='{}' frames={} t[0]={} r[0]={}",
|
LOG.info("[DIAG] Track '{}' target='{}' frames={} t[0]={} r[0]={}",
|
||||||
clipName, j.getName(), tt.getTimes().length, tStr, rStr);
|
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
|
// DIAG: Modell-Hips localTransform + IBM
|
||||||
@@ -557,6 +537,7 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
private void stopAll() {
|
private void stopAll() {
|
||||||
currentAction = null;
|
currentAction = null;
|
||||||
currentClipName = null;
|
currentClipName = null;
|
||||||
|
if (animPlayer != null) animPlayer.stop();
|
||||||
if (currentModel != null) {
|
if (currentModel != null) {
|
||||||
stopOnSpatial(currentModel);
|
stopOnSpatial(currentModel);
|
||||||
// Zurück zur T-Pose: __tpose__ wiedergeben (leerer Clip = Bind-Pose)
|
// 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) {
|
private void stopOnSpatial(Spatial s) {
|
||||||
AnimComposer ac = s.getControl(AnimComposer.class);
|
AnimComposer ac = s.getControl(AnimComposer.class);
|
||||||
if (ac != null) {
|
if (ac != null) {
|
||||||
@@ -718,7 +707,14 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
saved++;
|
saved++;
|
||||||
}
|
}
|
||||||
// Für den aktuellen Preview auch auf das Modell anwenden (wenn geladen)
|
// 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)
|
// 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)$")
|
if (saved > 0 && animAssetPath.matches("(?i).*\\.(glb|gltf)$")
|
||||||
@@ -1204,51 +1200,6 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
return clip;
|
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 = {
|
private static final String[] MODEL_SAVE_ROOTS = {
|
||||||
null, // ASSET_ROOT (src) – wird durch ASSET_ROOT ersetzt
|
null, // ASSET_ROOT (src) – wird durch ASSET_ROOT ersetzt
|
||||||
"blight-assets/bin/main",
|
"blight-assets/bin/main",
|
||||||
|
|||||||
@@ -395,7 +395,7 @@ public class TerrainEditorState extends BaseAppState {
|
|||||||
if (loadedMapData != null) {
|
if (loadedMapData != null) {
|
||||||
float[] src = loadedMapData.terrainHeight;
|
float[] src = loadedMapData.terrainHeight;
|
||||||
heights = new float[TOTAL_SIZE * TOTAL_SIZE];
|
heights = new float[TOTAL_SIZE * TOTAL_SIZE];
|
||||||
int srcVerts = MapData.TERRAIN_VERTS; // 16385
|
int srcVerts = MapData.TERRAIN_VERTS; // 8193
|
||||||
if (srcVerts == TOTAL_SIZE) {
|
if (srcVerts == TOTAL_SIZE) {
|
||||||
System.arraycopy(src, 0, heights, 0, heights.length);
|
System.arraycopy(src, 0, heights, 0, heights.length);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ public class VoxelEditorState extends BaseAppState {
|
|||||||
|
|
||||||
// ── Voxel-Splatmap ───────────────────────────────────────────────────────
|
// ── 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_WORLD_HALF = 2048f;
|
||||||
private static final float VOX_SPLAT_WE_PER_PX =
|
private static final float VOX_SPLAT_WE_PER_PX =
|
||||||
(VOX_WORLD_HALF * 2f) / (VOX_SPLAT_SIZE - 1); // ~2 WE/px
|
(VOX_WORLD_HALF * 2f) / (VOX_SPLAT_SIZE - 1); // ~2 WE/px
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import java.nio.file.Path;
|
|||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lädt alle Clip-Dateien aus {@code animations/clips/} beim Start.
|
* Lädt alle Clip-Dateien aus {@code animations/clips/} beim Start.
|
||||||
* Clip-Schlüssel entsprechen dem Dateinamen ohne Extension (= Clip-Name).
|
* 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<>();
|
private final Map<String, AnimClip> clips = new LinkedHashMap<>();
|
||||||
/** clip name → Armatur der Quell-Datei */
|
/** clip name → Armatur der Quell-Datei */
|
||||||
private final Map<String, Armature> armatures = new LinkedHashMap<>();
|
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 ─────────────────────────────────────────────────────────────
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -118,6 +126,9 @@ public class AnimationLibrary extends BaseAppState {
|
|||||||
log.warn("[AnimLib] applyTo: Retargeting für '{}' schlug fehl", clipName);
|
log.warn("[AnimLib] applyTo: Retargeting für '{}' schlug fehl", clipName);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (snapClipNames.contains(clipName)) {
|
||||||
|
target = snapRootBoneXZ(target, sc.getArmature());
|
||||||
|
}
|
||||||
|
|
||||||
ac.addAnimClip(target);
|
ac.addAnimClip(target);
|
||||||
log.info("[AnimLib] Clip '{}' zu AnimComposer von '{}' hinzugefügt", clipName, model.getName());
|
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 ───────────────────────────────────────────────────────────────
|
// ── Loading ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private void loadAll() {
|
private void loadAll() {
|
||||||
@@ -307,9 +336,6 @@ public class AnimationLibrary extends BaseAppState {
|
|||||||
|
|
||||||
for (String name : ac.getAnimClipsNames()) {
|
for (String name : ac.getAnimClipsNames()) {
|
||||||
com.jme3.anim.AnimClip animClip = ac.getAnimClip(name);
|
com.jme3.anim.AnimClip animClip = ac.getAnimClip(name);
|
||||||
if (armature != null) {
|
|
||||||
animClip = snapRootBoneXZ(animClip, armature);
|
|
||||||
}
|
|
||||||
clips.put(name, animClip);
|
clips.put(name, animClip);
|
||||||
if (armature != null) armatures.put(name, armature);
|
if (armature != null) armatures.put(name, armature);
|
||||||
log.info("[AnimLib] Clip geladen: '{}' aus {}", name, assetKey);
|
log.info("[AnimLib] Clip geladen: '{}' aus {}", name, assetKey);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import com.jme3.math.Vector3f;
|
|||||||
import com.jme3.renderer.Camera;
|
import com.jme3.renderer.Camera;
|
||||||
import com.jme3.scene.Spatial;
|
import com.jme3.scene.Spatial;
|
||||||
import de.blight.game.animation.AnimOffset;
|
import de.blight.game.animation.AnimOffset;
|
||||||
|
import de.blight.game.animation.AnimPlayer;
|
||||||
import de.blight.game.animation.AnimSet;
|
import de.blight.game.animation.AnimSet;
|
||||||
import de.blight.game.animation.AnimationAction;
|
import de.blight.game.animation.AnimationAction;
|
||||||
import de.blight.game.animation.AnimationLibrary;
|
import de.blight.game.animation.AnimationLibrary;
|
||||||
@@ -53,6 +54,7 @@ public class PlayerInputControl {
|
|||||||
private boolean animCtxLogged = false;
|
private boolean animCtxLogged = false;
|
||||||
|
|
||||||
private AnimComposer animComposer;
|
private AnimComposer animComposer;
|
||||||
|
private AnimPlayer animPlayer;
|
||||||
private String runningClip;
|
private String runningClip;
|
||||||
|
|
||||||
private com.jme3.anim.SkinningControl skinningControl = null;
|
private com.jme3.anim.SkinningControl skinningControl = null;
|
||||||
@@ -91,6 +93,7 @@ public class PlayerInputControl {
|
|||||||
private int jumpFrames = 0;
|
private int jumpFrames = 0;
|
||||||
private int groundGraceFrames = 0;
|
private int groundGraceFrames = 0;
|
||||||
private double nextTransitionLength = 0.0;
|
private double nextTransitionLength = 0.0;
|
||||||
|
|
||||||
private boolean pickupActive = false;
|
private boolean pickupActive = false;
|
||||||
private float pickupRemaining = 0f;
|
private float pickupRemaining = 0f;
|
||||||
|
|
||||||
@@ -156,6 +159,7 @@ public class PlayerInputControl {
|
|||||||
this.assetRoot = assetRoot;
|
this.assetRoot = assetRoot;
|
||||||
this.currentAnim = null;
|
this.currentAnim = null;
|
||||||
this.runningClip = null;
|
this.runningClip = null;
|
||||||
|
animLib.configureLocomotionSnap(assetRoot, animSetName);
|
||||||
this.animComposer = (visual != null) ? RetargetingSystem.findAnimComposer(visual) : null;
|
this.animComposer = (visual != null) ? RetargetingSystem.findAnimComposer(visual) : null;
|
||||||
log.info("[AnimCtx] AnimComposer gefunden: {}", animComposer != null);
|
log.info("[AnimCtx] AnimComposer gefunden: {}", animComposer != null);
|
||||||
skinningControl = findSkinningControl(visual);
|
skinningControl = findSkinningControl(visual);
|
||||||
@@ -164,6 +168,9 @@ public class PlayerInputControl {
|
|||||||
armature = skinningControl.getArmature();
|
armature = skinningControl.getArmature();
|
||||||
logJointNames(armature);
|
logJointNames(armature);
|
||||||
}
|
}
|
||||||
|
animPlayer = (animComposer != null && skinningControl != null)
|
||||||
|
? new AnimPlayer(animComposer, skinningControl)
|
||||||
|
: null;
|
||||||
if (visual != null) {
|
if (visual != null) {
|
||||||
visualBaseTranslation = visual.getLocalTranslation().clone();
|
visualBaseTranslation = visual.getLocalTranslation().clone();
|
||||||
}
|
}
|
||||||
@@ -336,18 +343,16 @@ public class PlayerInputControl {
|
|||||||
inputsBlocked = true;
|
inputsBlocked = true;
|
||||||
if (animComposer != null) {
|
if (animComposer != null) {
|
||||||
if (animLib != null && visual != null) animLib.ensureApplied(reviveClip, visual);
|
if (animLib != null && visual != null) animLib.ensureApplied(reviveClip, visual);
|
||||||
// transitionLength=0: verhindert den 0.4 s Überblend-Effekt bei speed=0.
|
if (animPlayer == null) animPlayer = new AnimPlayer(animComposer, skinningControl);
|
||||||
// Ohne diesen Fix wäre transitionWeight=time/0.4=0 → Charakter zeigt Idle-Pose.
|
// transition=0: harter Schnitt, kein BlendAction → Charakter startet sofort in frame 0
|
||||||
com.jme3.anim.tween.action.Action reviveAction = animComposer.action(reviveClip);
|
animPlayer.play(reviveClip, 0f);
|
||||||
if (reviveAction instanceof com.jme3.anim.tween.action.BlendableAction ba) {
|
frozenReviveAction = animPlayer.getCurrentAction();
|
||||||
ba.setTransitionLength(0.0);
|
|
||||||
}
|
|
||||||
frozenReviveAction = animComposer.setCurrentAction(reviveClip);
|
|
||||||
if (frozenReviveAction != null) {
|
if (frozenReviveAction != null) {
|
||||||
frozenReviveAction.setSpeed(0f);
|
frozenReviveAction.setSpeed(0f);
|
||||||
|
runningClip = animPlayer.getCurrentClip();
|
||||||
log.info("[Intro] REVIVE '{}' eingefroren (frame 0)", reviveClip);
|
log.info("[Intro] REVIVE '{}' eingefroren (frame 0)", reviveClip);
|
||||||
} else {
|
} else {
|
||||||
log.warn("[Intro] setCurrentAction('{}') → null – REVIVE nicht eingefroren", reviveClip);
|
log.warn("[Intro] REVIVE '{}' → action null", reviveClip);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.warn("[Intro] animComposer null – REVIVE kann nicht eingefroren werden");
|
log.warn("[Intro] animComposer null – REVIVE kann nicht eingefroren werden");
|
||||||
@@ -456,6 +461,11 @@ public class PlayerInputControl {
|
|||||||
public void update(float tpf) {
|
public void update(float tpf) {
|
||||||
if (physicsChar == null) return;
|
if (physicsChar == null) return;
|
||||||
|
|
||||||
|
if (animPlayer != null) {
|
||||||
|
animPlayer.update(tpf);
|
||||||
|
runningClip = animPlayer.getCurrentClip();
|
||||||
|
}
|
||||||
|
|
||||||
if (visual != null && animOffsetSpeed > 0f) {
|
if (visual != null && animOffsetSpeed > 0f) {
|
||||||
Vector3f delta = animOffsetTarget.subtract(animOffsetCurrent);
|
Vector3f delta = animOffsetTarget.subtract(animOffsetCurrent);
|
||||||
float dist = delta.length();
|
float dist = delta.length();
|
||||||
@@ -764,7 +774,28 @@ public class PlayerInputControl {
|
|||||||
if (animOffsetCurrent.distanceSquared(worldOffset) > 1e-4f) {
|
if (animOffsetCurrent.distanceSquared(worldOffset) > 1e-4f) {
|
||||||
animOffsetSpeed = 5.0f;
|
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() {
|
public void clearAnimOffset() {
|
||||||
@@ -821,29 +852,17 @@ public class PlayerInputControl {
|
|||||||
log.info("[Anim] tryPlay('{}') → applyTo FAILED", clip);
|
log.info("[Anim] tryPlay('{}') → applyTo FAILED", clip);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Optionale Überblend-Zeit (einmalig, wird nach Verwendung auf 0 zurückgesetzt)
|
if (animPlayer == null) {
|
||||||
double transLen = nextTransitionLength;
|
animPlayer = new AnimPlayer(animComposer, skinningControl);
|
||||||
|
}
|
||||||
|
float transLen = (float) nextTransitionLength;
|
||||||
nextTransitionLength = 0.0;
|
nextTransitionLength = 0.0;
|
||||||
if (transLen > 0.0) {
|
boolean ok = animPlayer.play(clip, transLen);
|
||||||
com.jme3.anim.tween.action.Action nextAction = animComposer.action(clip);
|
log.info("[Anim] AnimPlayer.play('{}') transLen={} → {}", clip, transLen, ok ? "OK" : "FAILED");
|
||||||
if (nextAction instanceof com.jme3.anim.tween.action.BlendableAction) {
|
if (ok) {
|
||||||
((com.jme3.anim.tween.action.BlendableAction) nextAction).setTransitionLength(transLen);
|
runningClip = animPlayer.getCurrentClip();
|
||||||
log.info("[Anim] Transition {} → '{}' über {:.2f}s", runningClip, clip, transLen);
|
applyAnimOffset(clip);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Erst Action setzen, DANN SkinningControl aktivieren –
|
return ok;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,9 +130,9 @@ public class WorldScene extends BaseAppState {
|
|||||||
if (loadedMapData == null) return result;
|
if (loadedMapData == null) return result;
|
||||||
|
|
||||||
int size = de.blight.common.MapData.SPLAT_SIZE;
|
int size = de.blight.common.MapData.SPLAT_SIZE;
|
||||||
int px = Math.max(0, Math.min(size - 1, Math.round((worldX + 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+2048)/2))
|
// 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 + 2048f) / 2f)));
|
int pz = Math.max(0, Math.min(size - 1, (size - 1) - Math.round((worldZ + 1024f) / 2f)));
|
||||||
int idx = pz * size + px;
|
int idx = pz * size + px;
|
||||||
|
|
||||||
// Upper/Third-Splatmap unterdrücken die Basistextur visuell (Overlay-Modell).
|
// Upper/Third-Splatmap unterdrücken die Basistextur visuell (Overlay-Modell).
|
||||||
@@ -540,9 +540,12 @@ public class WorldScene extends BaseAppState {
|
|||||||
@Override protected void onDisable() {}
|
@Override protected void onDisable() {}
|
||||||
|
|
||||||
private void setupAnimationContext() {
|
private void setupAnimationContext() {
|
||||||
animLib.applyAllTo(characterVisual != null ? characterVisual : character);
|
|
||||||
MainCharacter mc = findMainCharacter();
|
MainCharacter mc = findMainCharacter();
|
||||||
String setName = (mc != null) ? mc.getAnimSetPath() : null;
|
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: {}",
|
log.info("[AnimCtx] MainCharacter: {} animSetPath: {} clipCount: {} clips: {}",
|
||||||
mc != null ? mc.getCharacterId() : "null",
|
mc != null ? mc.getCharacterId() : "null",
|
||||||
setName,
|
setName,
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ public class WorldInteractableState extends BaseAppState {
|
|||||||
private static final float BENCH_RANGE = 5f;
|
private static final float BENCH_RANGE = 5f;
|
||||||
private static final float BED_RANGE = 6f;
|
private static final float BED_RANGE = 6f;
|
||||||
private static final float WALK_TIMEOUT = 12f;
|
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
|
// 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
|
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 benchPendingX = 0f;
|
||||||
private float benchPendingZ = 0f;
|
private float benchPendingZ = 0f;
|
||||||
private float benchPendingTimer = 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 ────────────────────────────────────────────────────────
|
// ── Abhängigkeiten ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -333,9 +330,6 @@ public class WorldInteractableState extends BaseAppState {
|
|||||||
InteractableEntry entry = entries.get(targetIdx);
|
InteractableEntry entry = entries.get(targetIdx);
|
||||||
float rotY = getSitFacingRotY(entry);
|
float rotY = getSitFacingRotY(entry);
|
||||||
Vector3f sitDir = new Vector3f((float) Math.cos(rotY), 0f, (float) Math.sin(rotY));
|
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));
|
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 downAction = isBench(entry) ? AnimationAction.SIT_DOWN : AnimationAction.LIE_DOWN;
|
||||||
AnimationAction idleAction = isBench(entry) ? AnimationAction.SITTING : AnimationAction.LYING;
|
AnimationAction idleAction = isBench(entry) ? AnimationAction.SITTING : AnimationAction.LYING;
|
||||||
|
|
||||||
|
if (isBench(entry)) {
|
||||||
|
playerInput.applyActionOffsetInstant(downAction);
|
||||||
|
}
|
||||||
|
|
||||||
playerInput.requestAnimation(downAction, 0f, () -> {
|
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);
|
if (!isBench(entry)) snapToSitPos(entry);
|
||||||
playerInput.lockInPlace();
|
playerInput.lockInPlace();
|
||||||
if (isBench(entry)) playerInput.setNextAnimTransition(0.2);
|
if (isBench(entry)) playerInput.setNextAnimTransition(0.2);
|
||||||
@@ -412,20 +399,8 @@ public class WorldInteractableState extends BaseAppState {
|
|||||||
|
|
||||||
playerInput.requestAnimation(upAction, 0f, () -> {
|
playerInput.requestAnimation(upAction, 0f, () -> {
|
||||||
if (isBench(entry)) {
|
if (isBench(entry)) {
|
||||||
// Nach stand_up_bench: Charakter 50cm von Bank wegbewegen
|
playerInput.setGroundGrace(4);
|
||||||
if (currentBenchSitDir != null) {
|
playerInput.setNextAnimTransition(0.2);
|
||||||
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.clearAnimOffset();
|
playerInput.clearAnimOffset();
|
||||||
benchPendingId = entry.interactableId();
|
benchPendingId = entry.interactableId();
|
||||||
benchPendingX = entry.worldX();
|
benchPendingX = entry.worldX();
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@@ -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/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 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/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
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user