Weiter am Kampfsystem gearbeitet, hier Schwertkampf und die animationen
This commit is contained in:
BIN
assets/imported/animations/Sword And Shield Attack.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Attack.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Idle.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Idle.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Run.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Run.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Slash 2.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Slash 2.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sword And Shield Walk.fbx
Normal file
BIN
assets/imported/animations/Sword And Shield Walk.fbx
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -9,7 +9,12 @@
|
|||||||
"T-Pose",
|
"T-Pose",
|
||||||
"Walking",
|
"Walking",
|
||||||
"Jumping",
|
"Jumping",
|
||||||
"Sword And Shield Slash"
|
"Sword And Shield Slash",
|
||||||
|
"Sword And Shield Idle",
|
||||||
|
"Sword And Shield Run",
|
||||||
|
"Sword And Shield Walk",
|
||||||
|
"Sword And Shield Attack",
|
||||||
|
"Sword And Shield Slash 2"
|
||||||
],
|
],
|
||||||
"actionMap": {
|
"actionMap": {
|
||||||
"DEFAULT": "Idle",
|
"DEFAULT": "Idle",
|
||||||
@@ -22,7 +27,11 @@
|
|||||||
"IDLE": "Idle",
|
"IDLE": "Idle",
|
||||||
"JUMP": "Jumping",
|
"JUMP": "Jumping",
|
||||||
"SWORD_ATTACK_1": "Sword And Shield Slash",
|
"SWORD_ATTACK_1": "Sword And Shield Slash",
|
||||||
"SWORD_ATTACK_2": "Sword And Shield Slash"
|
"SWORD_IDLE": "Sword And Shield Idle",
|
||||||
|
"SWORD_WALK": "Sword And Shield Walk",
|
||||||
|
"SWORD_RUN": "Sword And Shield Run",
|
||||||
|
"SWORD_ATTACK_3": "Sword And Shield Attack",
|
||||||
|
"SWORD_ATTACK_2": "Sword And Shield Slash 2"
|
||||||
},
|
},
|
||||||
"previewModelPath": "Characters/Models/hero.j3o",
|
"previewModelPath": "Characters/Models/hero.j3o",
|
||||||
"animOffsets": {
|
"animOffsets": {
|
||||||
@@ -67,5 +76,16 @@
|
|||||||
"SIT_UP",
|
"SIT_UP",
|
||||||
"IDLE"
|
"IDLE"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"snapClips": [
|
||||||
|
"Idle",
|
||||||
|
"Walking",
|
||||||
|
"Running",
|
||||||
|
"Sprint",
|
||||||
|
"Jumping",
|
||||||
|
"Sword And Shield Idle",
|
||||||
|
"Sword And Shield Run",
|
||||||
|
"Sword And Shield Walk",
|
||||||
|
"Sword And Shield Attack"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
Binary file not shown.
@@ -1,4 +1,4 @@
|
|||||||
#Tue Sep 15 12:31:37 CEST 2026
|
#Sun Sep 20 15:31:32 CEST 2026
|
||||||
attachedEmitters.count=0
|
attachedEmitters.count=0
|
||||||
attachedLights.count=0
|
attachedLights.count=0
|
||||||
attachedSounds.count=0
|
attachedSounds.count=0
|
||||||
|
|||||||
@@ -11,12 +11,12 @@
|
|||||||
"worthGold": 0,
|
"worthGold": 0,
|
||||||
"damage": 0,
|
"damage": 0,
|
||||||
"grip": {
|
"grip": {
|
||||||
"posX": 0.24,
|
"posX": 0.37,
|
||||||
"posY": 0.05,
|
"posY": 0.08,
|
||||||
"posZ": 0.0,
|
"posZ": 0.0,
|
||||||
"rotX": 0.0,
|
"rotX": 0.0,
|
||||||
"rotY": -90.0,
|
"rotY": -90.0,
|
||||||
"rotZ": 0.0
|
"rotZ": 90.0
|
||||||
},
|
},
|
||||||
"modelRef": {
|
"modelRef": {
|
||||||
"path": "Models/waffen/schwerter/rostiges_kurzschwert.j3o"
|
"path": "Models/waffen/schwerter/rostiges_kurzschwert.j3o"
|
||||||
|
|||||||
@@ -1562,17 +1562,22 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
headRot.fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X);
|
headRot.fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X);
|
||||||
head.setLocalRotation(headRot);
|
head.setLocalRotation(headRot);
|
||||||
|
|
||||||
// Klingenflosse: dünnes flaches Rechteck senkrecht zum Schaft entlang +Y,
|
// Griffachsen-Zylinder: runder Zylinder senkrecht zum Schaft (+Y),
|
||||||
// damit Rotation um Z (Roll) sichtbar ist. Breite 6cm, Dicke 2mm, entlang Schaft.
|
// zeigt die Roll-Richtung (ersetzt die frühere flache Klingenflosse).
|
||||||
|
// JME3-Cylinder liegt standardmäßig auf +Z → -90° um X dreht ihn nach +Y.
|
||||||
Material matFin = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
Material matFin = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
matFin.setColor("Color", new ColorRGBA(1f, 0.8f, 0.1f, 1f));
|
matFin.setColor("Color", new ColorRGBA(1f, 0.8f, 0.1f, 1f));
|
||||||
Geometry fin = new Geometry("gFin", new Box(0.001f, 0.03f, shaftLen * 0.5f));
|
float axisLen = 0.06f;
|
||||||
fin.setMaterial(matFin);
|
Geometry gripAxis = new Geometry("gGripAxis", new Cylinder(4, 8, 0.006f, axisLen, true));
|
||||||
fin.setLocalTranslation(0f, 0.03f, shaftLen * 0.5f);
|
gripAxis.setMaterial(matFin);
|
||||||
|
Quaternion axisRot = new Quaternion();
|
||||||
|
axisRot.fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X);
|
||||||
|
gripAxis.setLocalRotation(axisRot);
|
||||||
|
gripAxis.setLocalTranslation(0f, axisLen * 0.5f, 0f);
|
||||||
|
|
||||||
group.attachChild(marker);
|
group.attachChild(marker);
|
||||||
group.attachChild(shaft);
|
group.attachChild(shaft);
|
||||||
group.attachChild(fin);
|
group.attachChild(gripAxis);
|
||||||
group.attachChild(head);
|
group.attachChild(head);
|
||||||
gripGizmoNode.attachChild(group);
|
gripGizmoNode.attachChild(group);
|
||||||
gripGizmoNode.setCullHint(Spatial.CullHint.Inherit);
|
gripGizmoNode.setCullHint(Spatial.CullHint.Inherit);
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ public class AnimSetController implements ToolController {
|
|||||||
private ListView<String> animSetPartsListView;
|
private ListView<String> animSetPartsListView;
|
||||||
private java.util.Map<String, de.blight.game.animation.AnimSet.SubClipDef>
|
private java.util.Map<String, de.blight.game.animation.AnimSet.SubClipDef>
|
||||||
editableSubClips = new java.util.LinkedHashMap<>();
|
editableSubClips = new java.util.LinkedHashMap<>();
|
||||||
|
private java.util.Set<String> editableSnapClips = new java.util.LinkedHashSet<>();
|
||||||
private ListView<String> animSetSequenceListView;
|
private ListView<String> animSetSequenceListView;
|
||||||
private java.util.Map<String, java.util.List<String>>
|
private java.util.Map<String, java.util.List<String>>
|
||||||
editableSequences = new java.util.LinkedHashMap<>();
|
editableSequences = new java.util.LinkedHashMap<>();
|
||||||
@@ -76,6 +77,16 @@ public class AnimSetController implements ToolController {
|
|||||||
animSetCurrentName = setName;
|
animSetCurrentName = setName;
|
||||||
animSetCurrentDir = setDir;
|
animSetCurrentDir = setDir;
|
||||||
currentAnimSet = animSet;
|
currentAnimSet = animSet;
|
||||||
|
editableSnapClips = new java.util.LinkedHashSet<>(animSet.getSnapClips());
|
||||||
|
// Migration: wenn snapClips noch leer, Fallback-Clips (LOCOMOTION_ACTIONS) einmalig übernehmen
|
||||||
|
// damit der Checkbox-Zustand die tatsächliche Snap-Konfiguration widerspiegelt.
|
||||||
|
if (editableSnapClips.isEmpty()) {
|
||||||
|
java.util.Set<String> locoFallback = java.util.Set.of(
|
||||||
|
"WALK", "RUN", "SPRINT", "JUMP", "RUNNING_JUMP", "IDLE", "DEFAULT");
|
||||||
|
animSet.getActionMap().forEach((action, clip) -> {
|
||||||
|
if (locoFallback.contains(action)) editableSnapClips.add(clip);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -337,17 +348,14 @@ public class AnimSetController implements ToolController {
|
|||||||
animSetClipListView.setPrefHeight(180);
|
animSetClipListView.setPrefHeight(180);
|
||||||
editableSubClips = new java.util.LinkedHashMap<>(animSet.getSubClips());
|
editableSubClips = new java.util.LinkedHashMap<>(animSet.getSubClips());
|
||||||
editableSequences = new java.util.LinkedHashMap<>(animSet.getSequences());
|
editableSequences = new java.util.LinkedHashMap<>(animSet.getSequences());
|
||||||
|
// editableSnapClips wird bereits in openWithSet gesetzt (inkl. Migration)
|
||||||
ctx.input().animPreviewClipOffsets.clear();
|
ctx.input().animPreviewClipOffsets.clear();
|
||||||
for (var en : animSet.getAnimOffsets().entrySet()) {
|
for (var en : animSet.getAnimOffsets().entrySet()) {
|
||||||
de.blight.game.animation.AnimOffset o = en.getValue();
|
de.blight.game.animation.AnimOffset o = en.getValue();
|
||||||
ctx.input().animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz});
|
ctx.input().animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz});
|
||||||
}
|
}
|
||||||
java.util.Set<String> locomotionActions = java.util.Set.of(
|
|
||||||
"WALK", "RUN", "SPRINT", "JUMP", "RUNNING_JUMP", "IDLE", "DEFAULT");
|
|
||||||
ctx.input().animPreviewLocomotionClips.clear();
|
ctx.input().animPreviewLocomotionClips.clear();
|
||||||
animSet.getActionMap().forEach((action, clip) -> {
|
ctx.input().animPreviewLocomotionClips.addAll(editableSnapClips);
|
||||||
if (locomotionActions.contains(action)) ctx.input().animPreviewLocomotionClips.add(clip);
|
|
||||||
});
|
|
||||||
|
|
||||||
Path animRootDir = ctx.assetRoot().resolve("Characters");
|
Path animRootDir = ctx.assetRoot().resolve("Characters");
|
||||||
|
|
||||||
@@ -454,10 +462,41 @@ public class AnimSetController implements ToolController {
|
|||||||
autoSaveAnimSet();
|
autoSaveAnimSet();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
CheckBox snapCB = new CheckBox("Root-Motion entfernen (auf der Stelle)");
|
||||||
|
snapCB.setDisable(true);
|
||||||
|
snapCB.setTooltip(new Tooltip("Bewegt sich die Animation durch den Raum (Walk/Run)?\nMit dieser Option bleibt der Charakter an Ort und Stelle\nund das Spiel übernimmt die Fortbewegung."));
|
||||||
|
final boolean[] snapCbUpdating = {false};
|
||||||
|
|
||||||
|
animSetClipListView.getSelectionModel().selectedItemProperty()
|
||||||
|
.addListener((obs, ov, nv) -> {
|
||||||
|
snapCB.setDisable(nv == null);
|
||||||
|
if (nv != null) {
|
||||||
|
snapCbUpdating[0] = true;
|
||||||
|
snapCB.setSelected(editableSnapClips.contains(nv));
|
||||||
|
snapCbUpdating[0] = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
snapCB.selectedProperty().addListener((obs, ov, nv) -> {
|
||||||
|
if (snapCbUpdating[0]) return;
|
||||||
|
String sel = animSetClipListView.getSelectionModel().getSelectedItem();
|
||||||
|
if (sel == null) return;
|
||||||
|
if (nv) {
|
||||||
|
editableSnapClips.add(sel);
|
||||||
|
} else {
|
||||||
|
editableSnapClips.remove(sel);
|
||||||
|
}
|
||||||
|
ctx.input().animPreviewLocomotionClips.clear();
|
||||||
|
ctx.input().animPreviewLocomotionClips.addAll(editableSnapClips);
|
||||||
|
// Clip neu laden damit Snap-Änderung sofort in der Vorschau sichtbar wird
|
||||||
|
ctx.input().animImportQueue.offer(findAnimClipPath(sel));
|
||||||
|
autoSaveAnimSet();
|
||||||
|
});
|
||||||
|
|
||||||
HBox clipBtns = new HBox(6, addClipBtn, removeClipBtn);
|
HBox clipBtns = new HBox(6, addClipBtn, removeClipBtn);
|
||||||
HBox.setHgrow(addClipBtn, Priority.ALWAYS);
|
HBox.setHgrow(addClipBtn, Priority.ALWAYS);
|
||||||
HBox.setHgrow(removeClipBtn, Priority.ALWAYS);
|
HBox.setHgrow(removeClipBtn, Priority.ALWAYS);
|
||||||
inner.getChildren().addAll(animSetClipListView, clipBtns);
|
inner.getChildren().addAll(animSetClipListView, snapCB, clipBtns);
|
||||||
|
|
||||||
// ── Clip-Teile (Sub-Clips) ─────────────────────────────────────────────
|
// ── Clip-Teile (Sub-Clips) ─────────────────────────────────────────────
|
||||||
inner.getChildren().addAll(new Separator(), sectionTitle("Clip-Teile"), new Separator());
|
inner.getChildren().addAll(new Separator(), sectionTitle("Clip-Teile"), new Separator());
|
||||||
@@ -1172,12 +1211,9 @@ public class AnimSetController implements ToolController {
|
|||||||
de.blight.game.animation.AnimOffset o = en.getValue();
|
de.blight.game.animation.AnimOffset o = en.getValue();
|
||||||
ctx.input().animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz});
|
ctx.input().animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz});
|
||||||
}
|
}
|
||||||
java.util.Set<String> locoActions = java.util.Set.of(
|
animSet.setSnapClips(new java.util.LinkedHashSet<>(editableSnapClips));
|
||||||
"WALK", "RUN", "SPRINT", "JUMP", "RUNNING_JUMP", "IDLE", "DEFAULT");
|
|
||||||
ctx.input().animPreviewLocomotionClips.clear();
|
ctx.input().animPreviewLocomotionClips.clear();
|
||||||
animSet.getActionMap().forEach((action, clip) -> {
|
ctx.input().animPreviewLocomotionClips.addAll(editableSnapClips);
|
||||||
if (locoActions.contains(action)) ctx.input().animPreviewLocomotionClips.add(clip);
|
|
||||||
});
|
|
||||||
if (animSetModelCombo != null && animSetModelCombo.getValue() != null
|
if (animSetModelCombo != null && animSetModelCombo.getValue() != null
|
||||||
&& !animSetModelCombo.getValue().isBlank()) {
|
&& !animSetModelCombo.getValue().isBlank()) {
|
||||||
animSet.setPreviewModelPath(animSetModelCombo.getValue());
|
animSet.setPreviewModelPath(animSetModelCombo.getValue());
|
||||||
|
|||||||
@@ -38,12 +38,25 @@ public class AnimPlayer {
|
|||||||
private LinearBlendSpace blendSpace;
|
private LinearBlendSpace blendSpace;
|
||||||
private String pendingClip;
|
private String pendingClip;
|
||||||
private float blendElapsed;
|
private float blendElapsed;
|
||||||
|
private float blendDuration;
|
||||||
|
/** Einsprungzeit: Clip startet bei diesem Zeitwert statt bei 0 (überspringt z.B. das Ausholen). */
|
||||||
|
private float pendingStartOffset;
|
||||||
|
|
||||||
public AnimPlayer(AnimComposer composer, SkinningControl skinning) {
|
public AnimPlayer(AnimComposer composer, SkinningControl skinning) {
|
||||||
this.composer = composer;
|
this.composer = composer;
|
||||||
this.skinning = skinning;
|
this.skinning = skinning;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spielt einen Clip ab, optional ab einem Zeitversatz (z.B. Ausholen überspringen).
|
||||||
|
*
|
||||||
|
* @param startTimeOffset Clip startet ab dieser Zeit statt bei 0; 0 = von Anfang
|
||||||
|
*/
|
||||||
|
public boolean play(String clipName, float transition, float startTimeOffset) {
|
||||||
|
this.pendingStartOffset = startTimeOffset;
|
||||||
|
return play(clipName, transition);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spielt einen Clip ab.
|
* Spielt einen Clip ab.
|
||||||
* Bei laufendem Clip und {@code transition > 0}: nahtloser Übergang
|
* Bei laufendem Clip und {@code transition > 0}: nahtloser Übergang
|
||||||
@@ -82,6 +95,7 @@ public class AnimPlayer {
|
|||||||
blendSpace = space;
|
blendSpace = space;
|
||||||
pendingClip = clipName;
|
pendingClip = clipName;
|
||||||
blendElapsed = 0f;
|
blendElapsed = 0f;
|
||||||
|
blendDuration = Math.max(0.01f, transition);
|
||||||
currentAction = blend;
|
currentAction = blend;
|
||||||
currentClip = null;
|
currentClip = null;
|
||||||
log.debug("[AnimPlayer] Blend → '{}'", clipName);
|
log.debug("[AnimPlayer] Blend → '{}'", clipName);
|
||||||
@@ -101,7 +115,7 @@ public class AnimPlayer {
|
|||||||
public void update(float tpf) {
|
public void update(float tpf) {
|
||||||
if (pendingClip == null || blendSpace == null) return;
|
if (pendingClip == null || blendSpace == null) return;
|
||||||
blendElapsed += tpf;
|
blendElapsed += tpf;
|
||||||
float bt = Math.min(1f, blendElapsed / BLEND_DURATION);
|
float bt = Math.min(1f, blendElapsed / blendDuration);
|
||||||
blendSpace.setValue(bt);
|
blendSpace.setValue(bt);
|
||||||
if (bt >= 1f) finishBlend();
|
if (bt >= 1f) finishBlend();
|
||||||
}
|
}
|
||||||
@@ -129,13 +143,35 @@ public class AnimPlayer {
|
|||||||
|
|
||||||
private boolean directPlay(String clipName) {
|
private boolean directPlay(String clipName) {
|
||||||
try {
|
try {
|
||||||
Action act = composer.action(clipName);
|
// Diagnose: zeigt warum length=0 entsteht
|
||||||
if (act instanceof BlendableAction ba) ba.setTransitionLength(0.0);
|
com.jme3.anim.AnimClip rawClip = composer.getAnimClip(clipName);
|
||||||
|
if (rawClip != null) {
|
||||||
|
com.jme3.anim.AnimTrack[] rawTracks = rawClip.getTracks();
|
||||||
|
if (rawTracks != null && rawTracks.length > 0 && rawTracks[0] instanceof com.jme3.anim.TransformTrack tt) {
|
||||||
|
float[] times = tt.getTimes();
|
||||||
|
log.info("[AnimDiag] '{}' clipLen={}s tracks={} t0Len={}s times=[{}..{}] n={}",
|
||||||
|
clipName, rawClip.getLength(), rawTracks.length, tt.getLength(),
|
||||||
|
times != null && times.length > 0 ? times[0] : -1,
|
||||||
|
times != null && times.length > 0 ? times[times.length-1] : -1,
|
||||||
|
times != null ? times.length : 0);
|
||||||
|
} else {
|
||||||
|
log.info("[AnimDiag] '{}' clipLen={}s tracks={}", clipName, rawClip.getLength(),
|
||||||
|
rawTracks != null ? rawTracks.length : -1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.info("[AnimDiag] '{}' → clip NOT in animClipMap!", clipName);
|
||||||
|
}
|
||||||
|
|
||||||
currentAction = composer.setCurrentAction(clipName);
|
currentAction = composer.setCurrentAction(clipName);
|
||||||
currentClip = clipName;
|
currentClip = clipName;
|
||||||
pendingClip = null;
|
pendingClip = null;
|
||||||
blendSpace = null;
|
blendSpace = null;
|
||||||
log.debug("[AnimPlayer] direktPlay → '{}'", clipName);
|
double len = currentAction != null ? currentAction.getLength() : -1;
|
||||||
|
if (pendingStartOffset > 0f && currentAction != null && len > 0.01) {
|
||||||
|
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime(pendingStartOffset % (float) len);
|
||||||
|
}
|
||||||
|
pendingStartOffset = 0f;
|
||||||
|
log.info("[AnimPlayer] direktPlay → '{}' length={}s", clipName, len);
|
||||||
return currentAction != null;
|
return currentAction != null;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[AnimPlayer] direktPlay('{}') fehlgeschlagen: {}", clipName, e.getMessage());
|
log.warn("[AnimPlayer] direktPlay('{}') fehlgeschlagen: {}", clipName, e.getMessage());
|
||||||
@@ -153,15 +189,15 @@ public class AnimPlayer {
|
|||||||
if (composer.hasAction("__blend__")) composer.removeAction("__blend__");
|
if (composer.hasAction("__blend__")) composer.removeAction("__blend__");
|
||||||
} catch (Exception ignored) {}
|
} catch (Exception ignored) {}
|
||||||
try {
|
try {
|
||||||
Action next = composer.action(clip);
|
|
||||||
if (next instanceof BlendableAction ba) ba.setTransitionLength(0.0);
|
|
||||||
currentAction = composer.setCurrentAction(clip);
|
currentAction = composer.setCurrentAction(clip);
|
||||||
currentClip = clip;
|
currentClip = clip;
|
||||||
double len = currentAction != null ? currentAction.getLength() : 0.0;
|
double len = currentAction != null ? currentAction.getLength() : 0.0;
|
||||||
// AnimLayer.setTime: kein extra controlUpdate, kein loop=false-Seiteneffekt.
|
// AnimLayer.setTime: kein extra controlUpdate, kein loop=false-Seiteneffekt.
|
||||||
// Clip startet dort, wo der Übergang endete (kein sichtbarer Sprung).
|
// Clip startet dort, wo der Übergang endete (kein sichtbarer Sprung).
|
||||||
if (len > 0.01 && elapsed > 0) {
|
if (len > 0.01 && (elapsed > 0 || pendingStartOffset > 0)) {
|
||||||
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime(elapsed % len);
|
float t = (pendingStartOffset + elapsed) % (float) len;
|
||||||
|
composer.getLayer(AnimComposer.DEFAULT_LAYER).setTime(t);
|
||||||
|
pendingStartOffset = 0f;
|
||||||
}
|
}
|
||||||
log.debug("[AnimPlayer] finishBlend → '{}'", clip);
|
log.debug("[AnimPlayer] finishBlend → '{}'", clip);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Beschreibt ein Animations-Set: eine Liste von Clip-Namen sowie die
|
* Beschreibt ein Animations-Set: eine Liste von Clip-Namen sowie die
|
||||||
@@ -32,6 +34,8 @@ public class AnimSet {
|
|||||||
private Map<String, SubClipDef> subClips = new LinkedHashMap<>();
|
private Map<String, SubClipDef> subClips = new LinkedHashMap<>();
|
||||||
/** Ablauf-Definitionen: Name → geordnete Liste von Aktions-Namen (aus actionMap). */
|
/** Ablauf-Definitionen: Name → geordnete Liste von Aktions-Namen (aus actionMap). */
|
||||||
private Map<String, List<String>> sequences = new LinkedHashMap<>();
|
private Map<String, List<String>> sequences = new LinkedHashMap<>();
|
||||||
|
/** Clip-Namen die snapRootBoneXZ erhalten sollen (Root-Motion entfernen, Bewegung auf der Stelle). */
|
||||||
|
private Set<String> snapClips = new LinkedHashSet<>();
|
||||||
|
|
||||||
/** Beschreibt einen Sub-Clip: Zeitfenster [start, end] innerhalb eines kombinierten Quell-Clips. */
|
/** Beschreibt einen Sub-Clip: Zeitfenster [start, end] innerhalb eines kombinierten Quell-Clips. */
|
||||||
public static class SubClipDef {
|
public static class SubClipDef {
|
||||||
@@ -60,6 +64,10 @@ public class AnimSet {
|
|||||||
return sequences != null ? sequences : new LinkedHashMap<>();
|
return sequences != null ? sequences : new LinkedHashMap<>();
|
||||||
}
|
}
|
||||||
public void setSequences(Map<String, List<String>> sequences) { this.sequences = sequences; }
|
public void setSequences(Map<String, List<String>> sequences) { this.sequences = sequences; }
|
||||||
|
public Set<String> getSnapClips() {
|
||||||
|
return snapClips != null ? snapClips : new LinkedHashSet<>();
|
||||||
|
}
|
||||||
|
public void setSnapClips(Set<String> snapClips) { this.snapClips = snapClips; }
|
||||||
|
|
||||||
/** Speichert dieses Set als {@code <setName>.animset.json} im Verzeichnis {@code setDir}. */
|
/** Speichert dieses Set als {@code <setName>.animset.json} im Verzeichnis {@code setDir}. */
|
||||||
public void save(Path setDir, String setName) throws IOException {
|
public void save(Path setDir, String setName) throws IOException {
|
||||||
|
|||||||
@@ -35,17 +35,17 @@ public enum AnimationAction {
|
|||||||
// ── Schwert ───────────────────────────────────────────────────────────────
|
// ── Schwert ───────────────────────────────────────────────────────────────
|
||||||
SWORD_ATTACK_1,
|
SWORD_ATTACK_1,
|
||||||
SWORD_DRAW, SWORD_SHEATHE, SWORD_BLOCK, SWORD_HIT_TAKEN,
|
SWORD_DRAW, SWORD_SHEATHE, SWORD_BLOCK, SWORD_HIT_TAKEN,
|
||||||
SWORD_ATTACK_2, SWORD_HEAVY_ATTACK,
|
SWORD_ATTACK_2, SWORD_ATTACK_3, SWORD_HEAVY_ATTACK,
|
||||||
SWORD_IDLE, SWORD_WALK, SWORD_RUN,
|
SWORD_IDLE, SWORD_WALK, SWORD_RUN,
|
||||||
|
|
||||||
// ── Axt ───────────────────────────────────────────────────────────────────
|
// ── Axt ───────────────────────────────────────────────────────────────────
|
||||||
AXE_DRAW, AXE_SHEATHE, AXE_BLOCK, AXE_HIT_TAKEN,
|
AXE_DRAW, AXE_SHEATHE, AXE_BLOCK, AXE_HIT_TAKEN,
|
||||||
AXE_ATTACK_1, AXE_ATTACK_2, AXE_HEAVY_ATTACK,
|
AXE_ATTACK_1, AXE_ATTACK_2, AXE_ATTACK_3, AXE_HEAVY_ATTACK,
|
||||||
AXE_IDLE, AXE_WALK, AXE_RUN,
|
AXE_IDLE, AXE_WALK, AXE_RUN,
|
||||||
|
|
||||||
// ── Zweihänder ────────────────────────────────────────────────────────────
|
// ── Zweihänder ────────────────────────────────────────────────────────────
|
||||||
TWO_HAND_DRAW, TWO_HAND_SHEATHE, TWO_HAND_BLOCK, TWO_HAND_HIT_TAKEN,
|
TWO_HAND_DRAW, TWO_HAND_SHEATHE, TWO_HAND_BLOCK, TWO_HAND_HIT_TAKEN,
|
||||||
TWO_HAND_ATTACK_1, TWO_HAND_ATTACK_2, TWO_HAND_HEAVY_ATTACK,
|
TWO_HAND_ATTACK_1, TWO_HAND_ATTACK_2, TWO_HAND_ATTACK_3, TWO_HAND_HEAVY_ATTACK,
|
||||||
TWO_HAND_IDLE, TWO_HAND_WALK, TWO_HAND_RUN,
|
TWO_HAND_IDLE, TWO_HAND_WALK, TWO_HAND_RUN,
|
||||||
|
|
||||||
// ── Bogen ─────────────────────────────────────────────────────────────────
|
// ── Bogen ─────────────────────────────────────────────────────────────────
|
||||||
@@ -98,6 +98,7 @@ public enum AnimationAction {
|
|||||||
case SWORD_BLOCK -> TextRegistry.resolve(null, key, "Block (Schwert)");
|
case SWORD_BLOCK -> TextRegistry.resolve(null, key, "Block (Schwert)");
|
||||||
case SWORD_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Schwert)");
|
case SWORD_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Schwert)");
|
||||||
case SWORD_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Schwert)");
|
case SWORD_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Schwert)");
|
||||||
|
case SWORD_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Schwert)");
|
||||||
case SWORD_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Schwert)");
|
case SWORD_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Schwert)");
|
||||||
case SWORD_IDLE -> TextRegistry.resolve(null, key, "Stehen (Schwert)");
|
case SWORD_IDLE -> TextRegistry.resolve(null, key, "Stehen (Schwert)");
|
||||||
case SWORD_WALK -> TextRegistry.resolve(null, key, "Gehen (Schwert)");
|
case SWORD_WALK -> TextRegistry.resolve(null, key, "Gehen (Schwert)");
|
||||||
@@ -109,6 +110,7 @@ public enum AnimationAction {
|
|||||||
case AXE_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Axt)");
|
case AXE_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Axt)");
|
||||||
case AXE_ATTACK_1 -> TextRegistry.resolve(null, key, "Angriff 1 (Axt)");
|
case AXE_ATTACK_1 -> TextRegistry.resolve(null, key, "Angriff 1 (Axt)");
|
||||||
case AXE_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Axt)");
|
case AXE_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Axt)");
|
||||||
|
case AXE_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Axt)");
|
||||||
case AXE_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Axt)");
|
case AXE_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Axt)");
|
||||||
case AXE_IDLE -> TextRegistry.resolve(null, key, "Stehen (Axt)");
|
case AXE_IDLE -> TextRegistry.resolve(null, key, "Stehen (Axt)");
|
||||||
case AXE_WALK -> TextRegistry.resolve(null, key, "Gehen (Axt)");
|
case AXE_WALK -> TextRegistry.resolve(null, key, "Gehen (Axt)");
|
||||||
@@ -120,6 +122,7 @@ public enum AnimationAction {
|
|||||||
case TWO_HAND_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Zweihänder)");
|
case TWO_HAND_HIT_TAKEN -> TextRegistry.resolve(null, key, "Treffer (Zweihänder)");
|
||||||
case TWO_HAND_ATTACK_1 -> TextRegistry.resolve(null, key, "Angriff 1 (Zweihänder)");
|
case TWO_HAND_ATTACK_1 -> TextRegistry.resolve(null, key, "Angriff 1 (Zweihänder)");
|
||||||
case TWO_HAND_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Zweihänder)");
|
case TWO_HAND_ATTACK_2 -> TextRegistry.resolve(null, key, "Angriff 2 (Zweihänder)");
|
||||||
|
case TWO_HAND_ATTACK_3 -> TextRegistry.resolve(null, key, "Angriff 3 – Kritisch (Zweihänder)");
|
||||||
case TWO_HAND_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Zweihänder)");
|
case TWO_HAND_HEAVY_ATTACK -> TextRegistry.resolve(null, key, "Schwerer Angriff (Zweihänder)");
|
||||||
case TWO_HAND_IDLE -> TextRegistry.resolve(null, key, "Stehen (Zweihänder)");
|
case TWO_HAND_IDLE -> TextRegistry.resolve(null, key, "Stehen (Zweihänder)");
|
||||||
case TWO_HAND_WALK -> TextRegistry.resolve(null, key, "Gehen (Zweihänder)");
|
case TWO_HAND_WALK -> TextRegistry.resolve(null, key, "Gehen (Zweihänder)");
|
||||||
|
|||||||
@@ -43,10 +43,6 @@ public class AnimationLibrary extends BaseAppState {
|
|||||||
/** Clips die snapRootBoneXZ erhalten (Locomotion: Walk, Run, Sprint, …). */
|
/** Clips die snapRootBoneXZ erhalten (Locomotion: Walk, Run, Sprint, …). */
|
||||||
private final Set<String> snapClipNames = new LinkedHashSet<>();
|
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 ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -103,6 +99,14 @@ public class AnimationLibrary extends BaseAppState {
|
|||||||
|
|
||||||
if (ac.getAnimClip(clipName) != null) return true; // bereits vorhanden
|
if (ac.getAnimClip(clipName) != null) return true; // bereits vorhanden
|
||||||
|
|
||||||
|
{
|
||||||
|
com.jme3.anim.AnimTrack<?>[] st = src.getTracks();
|
||||||
|
int sn0 = 0;
|
||||||
|
if (st != null && st.length > 0 && st[0] instanceof TransformTrack stt)
|
||||||
|
sn0 = stt.getTimes() != null ? stt.getTimes().length : 0;
|
||||||
|
log.info("[AnimLib] applyTo pre-retarget '{}': srcLen={}s srcTracks={} srcT0Frames={}",
|
||||||
|
clipName, src.getLength(), st != null ? st.length : 0, sn0);
|
||||||
|
}
|
||||||
AnimClip target;
|
AnimClip target;
|
||||||
if (srcArm != null && srcArm != sc.getArmature()) {
|
if (srcArm != null && srcArm != sc.getArmature()) {
|
||||||
// Immer retarget() aufrufen – auch bei gleichen Knochen-Namen.
|
// Immer retarget() aufrufen – auch bei gleichen Knochen-Namen.
|
||||||
@@ -131,8 +135,10 @@ public class AnimationLibrary extends BaseAppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ac.addAnimClip(target);
|
ac.addAnimClip(target);
|
||||||
log.info("[AnimLib] Clip '{}' zu AnimComposer von '{}' hinzugefügt", clipName, model.getName());
|
int trackCount = target.getTracks() != null ? target.getTracks().length : 0;
|
||||||
if (clipName.equals("sit_down")) {
|
log.info("[AnimLib] Clip '{}' → AnimComposer '{}': length={}s tracks={}",
|
||||||
|
clipName, model.getName(), target.getLength(), trackCount);
|
||||||
|
if (clipName.equals("sit_down") || target.getLength() == 0) {
|
||||||
dumpClipTracks(target);
|
dumpClipTracks(target);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -221,10 +227,8 @@ public class AnimationLibrary extends BaseAppState {
|
|||||||
snapClipNames.clear();
|
snapClipNames.clear();
|
||||||
try {
|
try {
|
||||||
AnimSet set = AnimSet.load(assetRoot.resolve("Characters").resolve("sets"), animSetName);
|
AnimSet set = AnimSet.load(assetRoot.resolve("Characters").resolve("sets"), animSetName);
|
||||||
set.getActionMap().forEach((action, clip) -> {
|
snapClipNames.addAll(set.getSnapClips());
|
||||||
if (LOCOMOTION_ACTIONS.contains(action)) snapClipNames.add(clip);
|
log.info("[AnimLib] snapRootBoneXZ für Clips: {}", snapClipNames);
|
||||||
});
|
|
||||||
log.info("[AnimLib] snapRootBoneXZ für Locomotion-Clips: {}", snapClipNames);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[AnimLib] AnimSet '{}' nicht ladbar für Locomotion-Snap: {}", animSetName, e.getMessage());
|
log.warn("[AnimLib] AnimSet '{}' nicht ladbar für Locomotion-Snap: {}", animSetName, e.getMessage());
|
||||||
}
|
}
|
||||||
@@ -320,9 +324,9 @@ public class AnimationLibrary extends BaseAppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void loadClipFromFile(Path file) {
|
private void loadClipFromFile(Path file) {
|
||||||
String fileName = file.getFileName().toString();
|
String fileName = file.getFileName().toString();
|
||||||
String clipName = fileName.replaceFirst("\\.j3o$", "");
|
String fileClipKey = fileName.replaceFirst("\\.j3o$", "");
|
||||||
String assetKey = "Characters/clips/" + fileName;
|
String assetKey = "Characters/clips/" + fileName;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Spatial loaded = assetManager.loadModel(assetKey);
|
Spatial loaded = assetManager.loadModel(assetKey);
|
||||||
@@ -334,11 +338,37 @@ public class AnimationLibrary extends BaseAppState {
|
|||||||
}
|
}
|
||||||
Armature armature = sc != null ? sc.getArmature() : null;
|
Armature armature = sc != null ? sc.getArmature() : null;
|
||||||
|
|
||||||
|
AnimClip bestClip = null;
|
||||||
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);
|
||||||
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);
|
com.jme3.anim.AnimTrack<?>[] rawTracks = animClip.getTracks();
|
||||||
|
int n0 = 0;
|
||||||
|
if (rawTracks != null && rawTracks.length > 0 && rawTracks[0] instanceof TransformTrack tt0) {
|
||||||
|
n0 = tt0.getTimes() != null ? tt0.getTimes().length : 0;
|
||||||
|
}
|
||||||
|
log.info("[AnimLib] Clip geladen: '{}' aus {} | clipLen={}s tracks={} t0Frames={}",
|
||||||
|
name, assetKey, animClip.getLength(),
|
||||||
|
rawTracks != null ? rawTracks.length : 0, n0);
|
||||||
|
if (bestClip == null || animClip.getLength() > bestClip.getLength()) {
|
||||||
|
bestClip = animClip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Kanonischen Dateiname-Key auf den längsten Clip mappen.
|
||||||
|
// Verhindert dass ein leerer Null-Frame-Clip einen guten Clip überschreibt,
|
||||||
|
// wenn die j3o mehrere Clips mit leicht unterschiedlichen Namen enthält
|
||||||
|
// (z.B. "clip_Sword And Shield Slash" vs. "Sword And Shield Slash").
|
||||||
|
if (bestClip != null) {
|
||||||
|
AnimClip current = clips.get(fileClipKey);
|
||||||
|
if (current == null || bestClip.getLength() > current.getLength()) {
|
||||||
|
clips.put(fileClipKey, bestClip);
|
||||||
|
if (armature != null) armatures.put(fileClipKey, armature);
|
||||||
|
if (!fileClipKey.equals(bestClip.getName())) {
|
||||||
|
log.info("[AnimLib] Key '{}' → '{}' (clipLen={}s, besser)",
|
||||||
|
fileClipKey, bestClip.getName(), bestClip.getLength());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[AnimLib] Fehler beim Laden von {}: {}", assetKey, e.getMessage());
|
log.warn("[AnimLib] Fehler beim Laden von {}: {}", assetKey, e.getMessage());
|
||||||
|
|||||||
@@ -81,11 +81,15 @@ public final class RetargetingSystem {
|
|||||||
if (t instanceof TransformTrack tt && tt.getTarget() instanceof Joint j)
|
if (t instanceof TransformTrack tt && tt.getTarget() instanceof Joint j)
|
||||||
srcTrackMap.put(j.getName(), tt);
|
srcTrackMap.put(j.getName(), tt);
|
||||||
|
|
||||||
// Reference keyframe times
|
// Reference keyframe times – pick the longest array (some bind-pose tracks have only 1 frame)
|
||||||
float[] times = null;
|
float[] times = null;
|
||||||
for (String name : nameMap.keySet()) {
|
for (String name : nameMap.keySet()) {
|
||||||
TransformTrack tt = srcTrackMap.get(name);
|
TransformTrack tt = srcTrackMap.get(name);
|
||||||
if (tt != null) { times = tt.getTimes(); break; }
|
if (tt != null && tt.getTimes() != null) {
|
||||||
|
if (times == null || tt.getTimes().length > times.length) {
|
||||||
|
times = tt.getTimes();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (times == null || times.length == 0) {
|
if (times == null || times.length == 0) {
|
||||||
log.warn("[Retarget] Keine Keyframe-Zeiten für '{}'", sourceClip.getName());
|
log.warn("[Retarget] Keine Keyframe-Zeiten für '{}'", sourceClip.getName());
|
||||||
@@ -398,18 +402,10 @@ public final class RetargetingSystem {
|
|||||||
|
|
||||||
// ── Same-rig detection & redirect ────────────────────────────────────────
|
// ── Same-rig detection & redirect ────────────────────────────────────────
|
||||||
|
|
||||||
/** True nur wenn alle Namen exakt selbst-gemappt UND alle Bind-Posen übereinstimmen. */
|
/** True wenn alle Namen exakt selbst-gemappt sind (gleiche Knochen-Namen → gleicher Rig-Typ). */
|
||||||
private static boolean isSameRig(Map<String, String> nameMap, Armature src, Armature dst) {
|
private static boolean isSameRig(Map<String, String> nameMap, Armature src, Armature dst) {
|
||||||
for (var e : nameMap.entrySet()) {
|
for (var e : nameMap.entrySet()) {
|
||||||
if (!e.getKey().equals(e.getValue())) return false;
|
if (!e.getKey().equals(e.getValue())) return false;
|
||||||
Joint srcJ = src.getJoint(e.getKey());
|
|
||||||
Joint dstJ = dst.getJoint(e.getValue());
|
|
||||||
if (srcJ == null || dstJ == null) continue;
|
|
||||||
Quaternion sb = srcJ.getInitialTransform() != null
|
|
||||||
? srcJ.getInitialTransform().getRotation() : new Quaternion();
|
|
||||||
Quaternion db = dstJ.getInitialTransform() != null
|
|
||||||
? dstJ.getInitialTransform().getRotation() : new Quaternion();
|
|
||||||
if (Math.abs(sb.dot(db)) < 0.9999f) return false;
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,10 +34,18 @@ public class PlayerInputControl {
|
|||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(PlayerInputControl.class);
|
private static final Logger log = LoggerFactory.getLogger(PlayerInputControl.class);
|
||||||
|
|
||||||
private static final float MOVE_SPEED = 0.07f;
|
private static final float MOVE_SPEED = 0.07f;
|
||||||
private static final float SPRINT_MULT = 1.5f;
|
private static final float SPRINT_MULT = 1.5f;
|
||||||
private static final float WALK_MULT = 0.5f;
|
private static final float WALK_MULT = 0.5f;
|
||||||
private static final float ROTATE_SPEED = 10f;
|
private static final float ROTATE_SPEED = 10f;
|
||||||
|
/** Kürzere Überblend-Zeit für Kampf-Animationen (Angriff fühlt sich "snappy" an). */
|
||||||
|
private static final float COMBAT_BLEND = 0.05f;
|
||||||
|
/** Zeit in Sekunden nach einem Angriff, in der der nächste Kombo-Schlag gedrückt werden kann. */
|
||||||
|
private static final float COMBO_WINDOW = 0.6f;
|
||||||
|
// Combo-Overlap-Utility: MELEE_COMBO_ACTIVE + COMBO_ATK2_OFFSET hier eintragen,
|
||||||
|
// sobald die Animationen in Blender angeglichen wurden (passende Übergangsposen).
|
||||||
|
// AnimPlayer.play(clip, transition, startOffset) und nextStartOffset in tryPlay() sind
|
||||||
|
// bereits verdrahtet — nur die Konstanten reaktivieren und in handleCombatAttack() einsetzen.
|
||||||
|
|
||||||
private static final String ACT_DRAW_MELEE = "DrawMeleeWeapon";
|
private static final String ACT_DRAW_MELEE = "DrawMeleeWeapon";
|
||||||
private static final String ACT_DRAW_RANGED = "DrawRangedWeapon";
|
private static final String ACT_DRAW_RANGED = "DrawRangedWeapon";
|
||||||
@@ -64,11 +72,17 @@ public class PlayerInputControl {
|
|||||||
private boolean weaponDrawn = false;
|
private boolean weaponDrawn = false;
|
||||||
private ItemSubCategory drawnWeaponType = null;
|
private ItemSubCategory drawnWeaponType = null;
|
||||||
private boolean isAiming = false;
|
private boolean isAiming = false;
|
||||||
private boolean meleeAttackToggle = false;
|
|
||||||
private boolean rightMouseHeld = false;
|
private boolean rightMouseHeld = false;
|
||||||
private boolean combatAnimActive = false;
|
private boolean combatAnimActive = false;
|
||||||
private float combatAnimRemaining = 0f;
|
private float combatAnimRemaining = 0f;
|
||||||
private Runnable combatAnimCallback = null;
|
private Runnable combatAnimCallback = null;
|
||||||
|
/** Aktueller Schritt in der Kombo-Sequenz (0–3): 0=ATK1, 1=ATK2, 2=ATK1, 3=ATK3-kritisch. */
|
||||||
|
private int comboStep = 0;
|
||||||
|
/** Zeitfenster nach einem Angriff, in dem der nächste Kombo-Schlag akzeptiert wird. */
|
||||||
|
private boolean comboWindowActive = false;
|
||||||
|
private float comboWindowRemaining = 0f;
|
||||||
|
/** Letzter Angriff war ein kritischer Treffer (Kombo-Schritt 3). Für das Schadenssystem. */
|
||||||
|
private boolean lastAttackCritical = false;
|
||||||
|
|
||||||
private AnimationLibrary animLib;
|
private AnimationLibrary animLib;
|
||||||
private String animSetName;
|
private String animSetName;
|
||||||
@@ -121,6 +135,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 float nextStartOffset = 0f;
|
||||||
|
|
||||||
private boolean pickupActive = false;
|
private boolean pickupActive = false;
|
||||||
private float pickupRemaining = 0f;
|
private float pickupRemaining = 0f;
|
||||||
@@ -241,6 +256,7 @@ public class PlayerInputControl {
|
|||||||
public void setMainCharacter(MainCharacter mc) { this.mainCharacter = mc; }
|
public void setMainCharacter(MainCharacter mc) { this.mainCharacter = mc; }
|
||||||
public boolean isWeaponDrawn() { return weaponDrawn; }
|
public boolean isWeaponDrawn() { return weaponDrawn; }
|
||||||
public ItemSubCategory getDrawnWeaponType() { return drawnWeaponType; }
|
public ItemSubCategory getDrawnWeaponType() { return drawnWeaponType; }
|
||||||
|
public boolean isLastAttackCritical() { return lastAttackCritical; }
|
||||||
public com.jme3.anim.SkinningControl getSkinningControl() { return skinningControl; }
|
public com.jme3.anim.SkinningControl getSkinningControl() { return skinningControl; }
|
||||||
|
|
||||||
public boolean isPaused() { return paused; }
|
public boolean isPaused() { return paused; }
|
||||||
@@ -255,6 +271,8 @@ public class PlayerInputControl {
|
|||||||
rightMouseHeld = false;
|
rightMouseHeld = false;
|
||||||
combatAnimActive = false;
|
combatAnimActive = false;
|
||||||
combatAnimCallback = null;
|
combatAnimCallback = null;
|
||||||
|
comboStep = 0;
|
||||||
|
comboWindowActive = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,12 +675,32 @@ public class PlayerInputControl {
|
|||||||
// Kampf-Anim-Timer
|
// Kampf-Anim-Timer
|
||||||
if (combatAnimActive) {
|
if (combatAnimActive) {
|
||||||
combatAnimRemaining -= tpf;
|
combatAnimRemaining -= tpf;
|
||||||
|
physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||||
if (combatAnimRemaining <= 0f) {
|
if (combatAnimRemaining <= 0f) {
|
||||||
combatAnimActive = false;
|
combatAnimActive = false;
|
||||||
currentAnim = null;
|
currentAnim = null;
|
||||||
|
nextTransitionLength = COMBAT_BLEND;
|
||||||
Runnable cb = combatAnimCallback;
|
Runnable cb = combatAnimCallback;
|
||||||
combatAnimCallback = null;
|
combatAnimCallback = null;
|
||||||
if (cb != null) cb.run();
|
if (cb != null) cb.run();
|
||||||
|
if (!combatAnimActive && isMeleeType(drawnWeaponType)) {
|
||||||
|
comboWindowActive = true;
|
||||||
|
comboWindowRemaining = COMBO_WINDOW;
|
||||||
|
log.info("[Combat] Kombo-Fenster geöffnet (step={} next={})", comboStep, comboStep);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pollFootsteps(tpf);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kombo-Fenster: läuft nach einem Nahkampfangriff; verfällt es → Kombo-Reset
|
||||||
|
if (comboWindowActive) {
|
||||||
|
comboWindowRemaining -= tpf;
|
||||||
|
if (comboWindowRemaining <= 0f) {
|
||||||
|
comboWindowActive = false;
|
||||||
|
comboStep = 0;
|
||||||
|
log.info("[Combat] Kombo-Fenster abgelaufen → Reset");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -670,7 +708,9 @@ public class PlayerInputControl {
|
|||||||
if (jumpFrames > 0) jumpFrames--;
|
if (jumpFrames > 0) jumpFrames--;
|
||||||
if (groundGraceFrames > 0) groundGraceFrames--;
|
if (groundGraceFrames > 0) groundGraceFrames--;
|
||||||
|
|
||||||
if (!combatAnimActive) {
|
// Während des Kombo-Fensters läuft die letzte Angriffs-Animation weiter —
|
||||||
|
// kein Wechsel zu IDLE/WALK, sonst entsteht ein sichtbares Zucken beim nächsten Schlag.
|
||||||
|
if (!combatAnimActive && !comboWindowActive) {
|
||||||
AnimationAction target;
|
AnimationAction target;
|
||||||
if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) {
|
if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) {
|
||||||
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
|
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
|
||||||
@@ -707,7 +747,8 @@ public class PlayerInputControl {
|
|||||||
}
|
}
|
||||||
String clip = AnimationLibrary.getClipForAction(assetRoot, animSetName, action);
|
String clip = AnimationLibrary.getClipForAction(assetRoot, animSetName, action);
|
||||||
log.info("[Anim] {} → clip='{}' (set={})", action, clip, animSetName);
|
log.info("[Anim] {} → clip='{}' (set={})", action, clip, animSetName);
|
||||||
if (runningClip != null) nextTransitionLength = 0.15;
|
// Negativer Wert = Hard-Cut angefordert (kein Blend); 0 = noch nicht gesetzt → Default 0.15s
|
||||||
|
if (runningClip != null && nextTransitionLength == 0.0) nextTransitionLength = 0.15;
|
||||||
if (clip != null && tryPlay(clip)) return;
|
if (clip != null && tryPlay(clip)) return;
|
||||||
if (action != AnimationAction.DEFAULT) {
|
if (action != AnimationAction.DEFAULT) {
|
||||||
String defClip = AnimationLibrary.getClipForAction(assetRoot, animSetName, AnimationAction.DEFAULT);
|
String defClip = AnimationLibrary.getClipForAction(assetRoot, animSetName, AnimationAction.DEFAULT);
|
||||||
@@ -765,16 +806,25 @@ public class PlayerInputControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void handleCombatAttack() {
|
private void handleCombatAttack() {
|
||||||
if (!weaponDrawn || blockingAnimActive || combatAnimActive) return;
|
if (!weaponDrawn || blockingAnimActive || combatAnimActive) {
|
||||||
|
log.info("[Combat] BLOCKIERT: weaponDrawn={} blockingAnim={} combatAnim={}",
|
||||||
|
weaponDrawn, blockingAnimActive, combatAnimActive);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isMeleeType(drawnWeaponType)) {
|
if (isMeleeType(drawnWeaponType)) {
|
||||||
AnimationAction atk = meleeAttackToggle
|
int step = comboStep;
|
||||||
? getMeleeAttack2Action(drawnWeaponType)
|
boolean isComboFollow = comboWindowActive; // war Kombo-Fenster offen?
|
||||||
: getMeleeAttack1Action(drawnWeaponType);
|
AnimationAction atk = getMeleeComboAction(drawnWeaponType, step);
|
||||||
meleeAttackToggle = !meleeAttackToggle;
|
lastAttackCritical = (step == 3);
|
||||||
|
comboStep = (step + 1) % 4;
|
||||||
|
comboWindowActive = false;
|
||||||
float dur = resolveClipLength(atk, 0.6f);
|
float dur = resolveClipLength(atk, 0.6f);
|
||||||
|
// Kombo-Folgeschlag: kurzer Blend für sauberen Übergang; erster Schlag: Hard-Cut
|
||||||
|
nextTransitionLength = isComboFollow ? COMBAT_BLEND : -1.0;
|
||||||
|
log.info("[Combat] Kombo-Schritt {}: action={} dur={}s kritisch={}",
|
||||||
|
step, atk, dur, lastAttackCritical);
|
||||||
combatAnimActive = true;
|
combatAnimActive = true;
|
||||||
// Blend-out zu Idle startet BLEND_DURATION früher → kein sichtbares Einfrieren am letzten Frame
|
combatAnimRemaining = Math.max(0.1f, dur - COMBAT_BLEND);
|
||||||
combatAnimRemaining = Math.max(0.1f, dur - de.blight.game.animation.AnimPlayer.BLEND_DURATION);
|
|
||||||
combatAnimCallback = null;
|
combatAnimCallback = null;
|
||||||
playAction(atk);
|
playAction(atk);
|
||||||
currentAnim = atk;
|
currentAnim = atk;
|
||||||
@@ -785,14 +835,16 @@ public class PlayerInputControl {
|
|||||||
float fireDur = resolveClipLength(fireAnim, 0.5f);
|
float fireDur = resolveClipLength(fireAnim, 0.5f);
|
||||||
float reloadDur = resolveClipLength(reloadAnim, 0.8f);
|
float reloadDur = resolveClipLength(reloadAnim, 0.8f);
|
||||||
combatAnimActive = true;
|
combatAnimActive = true;
|
||||||
combatAnimRemaining = Math.max(0.1f, fireDur - de.blight.game.animation.AnimPlayer.BLEND_DURATION);
|
combatAnimRemaining = Math.max(0.1f, fireDur - COMBAT_BLEND);
|
||||||
combatAnimCallback = () -> {
|
combatAnimCallback = () -> {
|
||||||
combatAnimActive = true;
|
combatAnimActive = true;
|
||||||
combatAnimRemaining = Math.max(0.1f, reloadDur - de.blight.game.animation.AnimPlayer.BLEND_DURATION);
|
combatAnimRemaining = Math.max(0.1f, reloadDur - COMBAT_BLEND);
|
||||||
combatAnimCallback = null;
|
combatAnimCallback = null;
|
||||||
|
nextTransitionLength = COMBAT_BLEND;
|
||||||
playAction(reloadAnim);
|
playAction(reloadAnim);
|
||||||
currentAnim = reloadAnim;
|
currentAnim = reloadAnim;
|
||||||
};
|
};
|
||||||
|
nextTransitionLength = COMBAT_BLEND;
|
||||||
playAction(fireAnim);
|
playAction(fireAnim);
|
||||||
currentAnim = fireAnim;
|
currentAnim = fireAnim;
|
||||||
}
|
}
|
||||||
@@ -909,6 +961,23 @@ public class PlayerInputControl {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static AnimationAction getMeleeAttack3Action(ItemSubCategory s) {
|
||||||
|
return switch (s) {
|
||||||
|
case AXE -> AnimationAction.AXE_ATTACK_3;
|
||||||
|
case TWO_HANDED_SWORD -> AnimationAction.TWO_HAND_ATTACK_3;
|
||||||
|
default -> AnimationAction.SWORD_ATTACK_3;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kombo-Sequenz: 0=ATK1, 1=ATK2, 2=ATK1, 3=ATK3 (kritisch). */
|
||||||
|
private static AnimationAction getMeleeComboAction(ItemSubCategory type, int step) {
|
||||||
|
return switch (step) {
|
||||||
|
case 1 -> getMeleeAttack2Action(type);
|
||||||
|
case 3 -> getMeleeAttack3Action(type);
|
||||||
|
default -> getMeleeAttack1Action(type);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static AnimationAction getMeleeBlockAction(ItemSubCategory s) {
|
private static AnimationAction getMeleeBlockAction(ItemSubCategory s) {
|
||||||
return switch (s) {
|
return switch (s) {
|
||||||
case AXE -> AnimationAction.AXE_BLOCK;
|
case AXE -> AnimationAction.AXE_BLOCK;
|
||||||
@@ -1185,9 +1254,11 @@ public class PlayerInputControl {
|
|||||||
if (animPlayer == null) {
|
if (animPlayer == null) {
|
||||||
animPlayer = new AnimPlayer(animComposer, skinningControl);
|
animPlayer = new AnimPlayer(animComposer, skinningControl);
|
||||||
}
|
}
|
||||||
float transLen = (float) nextTransitionLength;
|
float transLen = (float) nextTransitionLength;
|
||||||
|
float startOff = nextStartOffset;
|
||||||
nextTransitionLength = 0.0;
|
nextTransitionLength = 0.0;
|
||||||
boolean ok = animPlayer.play(clip, transLen);
|
nextStartOffset = 0f;
|
||||||
|
boolean ok = animPlayer.play(clip, transLen, startOff);
|
||||||
log.info("[Anim] AnimPlayer.play('{}') transLen={} → {}", clip, transLen, ok ? "OK" : "FAILED");
|
log.info("[Anim] AnimPlayer.play('{}') transLen={} → {}", clip, transLen, ok ? "OK" : "FAILED");
|
||||||
if (ok) {
|
if (ok) {
|
||||||
runningClip = animPlayer.getCurrentClip();
|
runningClip = animPlayer.getCurrentClip();
|
||||||
|
|||||||
@@ -29,28 +29,33 @@ public class WeaponAttachmentControl extends AbstractControl {
|
|||||||
|
|
||||||
private static final String BONE_SPINE = "mixamorig:Spine2";
|
private static final String BONE_SPINE = "mixamorig:Spine2";
|
||||||
private static final String BONE_HAND = "mixamorig:RightHand";
|
private static final String BONE_HAND = "mixamorig:RightHand";
|
||||||
|
private static final String BONE_PALM = "mixamorig:RightHandMiddle1";
|
||||||
private static final float DEG_TO_RAD = (float) Math.PI / 180f;
|
private static final float DEG_TO_RAD = (float) Math.PI / 180f;
|
||||||
private static final float HALF_PI = (float) Math.PI * 0.5f;
|
private static final float HALF_PI = (float) Math.PI * 0.5f;
|
||||||
|
|
||||||
// Grip-Zielposition im Spine2-lokalen Raum (Meter): Griff ~40cm über dem Brustbein
|
private static final Vector3f BACK_GRIP_OFFSET = new Vector3f(0.0f, 0.40f, -0.13f);
|
||||||
private static final Vector3f BACK_GRIP_OFFSET = new Vector3f(0.05f, 0.40f, -0.08f);
|
/** Zusatz-Versatz beim gezückten Schwert in lokalem Waffen-Space (X=entlang Klinge, Y=entlang Griff, Z=senkrecht zur Schneide). */
|
||||||
|
private static final Vector3f HAND_GRIP_OFFSET = new Vector3f(0.1f, 0.0f, 0.05f);
|
||||||
|
|
||||||
private final AssetManager assetManager;
|
private final AssetManager assetManager;
|
||||||
private final PlayerInputControl playerInput;
|
private final PlayerInputControl playerInput;
|
||||||
private final MainCharacter mc;
|
private final MainCharacter mc;
|
||||||
|
|
||||||
private com.jme3.anim.SkinningControl skinCtrl;
|
private com.jme3.anim.SkinningControl skinCtrl;
|
||||||
private Node backNode; // Spine2-Attachment-Node (im skalierten Char-Modell)
|
private Node backNode;
|
||||||
private Node handNode; // RightHand-Attachment-Node
|
private Node handNode;
|
||||||
|
private Node palmNodeRef;
|
||||||
|
private int palmOffsetFrameDelay = 0;
|
||||||
|
|
||||||
// Bind-Pose-Rotation des RightHand-Knochens in Character-lokalem Raum (T-Pose).
|
|
||||||
// Wird einmalig beim Initialisieren aus dem InverseBindMatrix abgeleitet.
|
|
||||||
// Erlaubt, die Grip-Rotation aus dem Modell-Editor direkt im Spiel zu verwenden.
|
|
||||||
private Quaternion handBindRotInChar;
|
private Quaternion handBindRotInChar;
|
||||||
|
// Offset Handgelenk → Fingerknöchel in Hand-Bone-Local-Space.
|
||||||
|
// Wird im 2. Frame aus den Runtime-Node-Positionen berechnet (skalierungs-unabhängig).
|
||||||
|
private Vector3f palmOffsetLocal;
|
||||||
|
|
||||||
private Item lastEquippedWeapon;
|
private Item lastEquippedWeapon;
|
||||||
private boolean lastDrawnState = false;
|
private boolean lastDrawnState = false;
|
||||||
private Spatial weaponSpatial;
|
private Spatial weaponSpatial;
|
||||||
|
private boolean gripDiagLogged = false;
|
||||||
|
|
||||||
public WeaponAttachmentControl(AssetManager assetManager,
|
public WeaponAttachmentControl(AssetManager assetManager,
|
||||||
PlayerInputControl playerInput,
|
PlayerInputControl playerInput,
|
||||||
@@ -62,38 +67,45 @@ public class WeaponAttachmentControl extends AbstractControl {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void controlUpdate(float tpf) {
|
protected void controlUpdate(float tpf) {
|
||||||
// Lazy init: SkinningControl erst nach setAnimationContext() verfügbar
|
|
||||||
if (skinCtrl == null) {
|
if (skinCtrl == null) {
|
||||||
skinCtrl = playerInput.getSkinningControl();
|
skinCtrl = playerInput.getSkinningControl();
|
||||||
if (skinCtrl == null) return;
|
if (skinCtrl == null) return;
|
||||||
try {
|
try {
|
||||||
backNode = skinCtrl.getAttachmentsNode(BONE_SPINE);
|
backNode = skinCtrl.getAttachmentsNode(BONE_SPINE);
|
||||||
log.info("[WeaponAttach] backNode ({}): {}", BONE_SPINE, backNode);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[WeaponAttach] Bone '{}' nicht gefunden: {}", BONE_SPINE, e.getMessage());
|
log.warn("[WeaponAttach] Bone '{}' nicht gefunden: {}", BONE_SPINE, e.getMessage());
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
handNode = skinCtrl.getAttachmentsNode(BONE_HAND);
|
handNode = skinCtrl.getAttachmentsNode(BONE_HAND);
|
||||||
log.info("[WeaponAttach] handNode ({}): {}", BONE_HAND, handNode);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[WeaponAttach] Bone '{}' nicht gefunden: {}", BONE_HAND, e.getMessage());
|
log.warn("[WeaponAttach] Bone '{}' nicht gefunden: {}", BONE_HAND, e.getMessage());
|
||||||
}
|
}
|
||||||
handBindRotInChar = extractJointBindRot(skinCtrl, BONE_HAND);
|
handBindRotInChar = extractJointBindRot(skinCtrl, BONE_HAND);
|
||||||
log.info("[WeaponAttach] handBindRotInChar: {}", handBindRotInChar);
|
try {
|
||||||
|
palmNodeRef = skinCtrl.getAttachmentsNode(BONE_PALM);
|
||||||
|
palmOffsetFrameDelay = 2; // SkinningControl muss mindestens 1x laufen
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[WeaponAttach] Bone '{}' nicht gefunden: {}", BONE_PALM, e.getMessage());
|
||||||
|
}
|
||||||
|
log.info("[WeaponAttach] handBindRotInChar={}", handBindRotInChar);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Palm-Offset verzögert berechnen (SkinningControl muss mindestens 1x gelaufen sein)
|
||||||
|
if (palmOffsetFrameDelay > 0 && palmNodeRef != null && handNode != null) {
|
||||||
|
palmOffsetFrameDelay--;
|
||||||
|
if (palmOffsetFrameDelay == 0) {
|
||||||
|
palmOffsetLocal = computePalmOffsetFromNodes(handNode, palmNodeRef);
|
||||||
|
log.info("[WeaponAttach] palmOffsetLocal (from nodes)={}", palmOffsetLocal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Item current = mc.getEquippedMeleeWeapon();
|
Item current = mc.getEquippedMeleeWeapon();
|
||||||
boolean drawn = playerInput.isWeaponDrawn();
|
boolean drawn = playerInput.isWeaponDrawn();
|
||||||
|
|
||||||
if (current != lastEquippedWeapon) {
|
if (current != lastEquippedWeapon) {
|
||||||
log.info("[WeaponAttach] Waffe geändert: {} → {}",
|
|
||||||
lastEquippedWeapon != null ? lastEquippedWeapon.getItemId() : "null",
|
|
||||||
current != null ? current.getItemId() : "null");
|
|
||||||
detachWeapon();
|
detachWeapon();
|
||||||
lastEquippedWeapon = current;
|
lastEquippedWeapon = current;
|
||||||
if (current != null) {
|
if (current != null) loadWeapon(current);
|
||||||
loadWeapon(current);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (weaponSpatial != null) {
|
if (weaponSpatial != null) {
|
||||||
@@ -111,12 +123,8 @@ public class WeaponAttachmentControl extends AbstractControl {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
weaponSpatial = assetManager.loadModel(item.getModelRef().getPath());
|
weaponSpatial = assetManager.loadModel(item.getModelRef().getPath());
|
||||||
// Wichtig: Waffe direkt am character-Node hängen (ohne geerbten 0.02-Scale
|
|
||||||
// aus der Charakter-Modell-Hierarchie). Position/Rotation wird jedes Frame
|
|
||||||
// aus den Bone-World-Transforms berechnet.
|
|
||||||
((Node) spatial).attachChild(weaponSpatial);
|
((Node) spatial).attachChild(weaponSpatial);
|
||||||
log.info("[WeaponAttach] Waffe geladen und an character-Node gehängt: {}",
|
log.info("[WeaponAttach] Waffe geladen: {}", item.getModelRef().getPath());
|
||||||
item.getModelRef().getPath());
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[WeaponAttach] Waffenmodell nicht ladbar ({}): {}",
|
log.warn("[WeaponAttach] Waffenmodell nicht ladbar ({}): {}",
|
||||||
item.getModelRef().getPath(), e.getMessage());
|
item.getModelRef().getPath(), e.getMessage());
|
||||||
@@ -130,31 +138,23 @@ public class WeaponAttachmentControl extends AbstractControl {
|
|||||||
Node boneNode = drawn ? handNode : backNode;
|
Node boneNode = drawn ? handNode : backNode;
|
||||||
if (boneNode == null) return;
|
if (boneNode == null) return;
|
||||||
|
|
||||||
// Bone-World-Transform (korrekt, auch wenn Eltern skaliert sind)
|
|
||||||
Vector3f boneWorldPos = boneNode.getWorldTranslation();
|
|
||||||
Quaternion boneWorldRot = boneNode.getWorldRotation();
|
|
||||||
|
|
||||||
// In den lokalen Raum des character-Nodes transformieren
|
|
||||||
// (character hat in der Regel nur Translation + Rotation, keinen Scale)
|
|
||||||
Quaternion charWorldRotInv = spatial.getWorldRotation().inverse();
|
Quaternion charWorldRotInv = spatial.getWorldRotation().inverse();
|
||||||
Vector3f boneInChar = charWorldRotInv.mult(
|
Vector3f charWorldPos = spatial.getWorldTranslation();
|
||||||
boneWorldPos.subtract(spatial.getWorldTranslation()));
|
|
||||||
Quaternion boneRotInChar = charWorldRotInv.mult(boneWorldRot);
|
Vector3f boneInChar = charWorldRotInv.mult(
|
||||||
|
boneNode.getWorldTranslation().subtract(charWorldPos));
|
||||||
|
Quaternion boneRotInChar = charWorldRotInv.mult(boneNode.getWorldRotation());
|
||||||
|
|
||||||
if (drawn) {
|
if (drawn) {
|
||||||
applyHandTransform(boneInChar, boneRotInChar, grip);
|
Vector3f attachInChar = (palmOffsetLocal != null)
|
||||||
|
? boneInChar.add(boneRotInChar.mult(palmOffsetLocal.mult(0.5f)))
|
||||||
|
: boneInChar;
|
||||||
|
applyHandTransform(attachInChar, boneRotInChar, grip);
|
||||||
} else {
|
} else {
|
||||||
applyBackTransform(boneInChar, boneRotInChar, grip);
|
applyBackTransform(boneInChar, boneRotInChar, grip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Waffe in der Hand: Griff-Punkt soll am Handknochen liegen, Rotation aus ItemGrip.
|
|
||||||
*
|
|
||||||
* Die Grip-Rotation ist im Character-/Welt-Raum definiert (wie im Grip-Editor sichtbar).
|
|
||||||
* Der Animations-Delta (Bewegung relativ zur Bind-Pose) wird zusätzlich aufaddiert,
|
|
||||||
* damit die Waffe den Handknochen durch alle Animationen folgt.
|
|
||||||
*/
|
|
||||||
private void applyHandTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) {
|
private void applyHandTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) {
|
||||||
Quaternion gripRot = (grip != null)
|
Quaternion gripRot = (grip != null)
|
||||||
? new Quaternion().fromAngles(DEG_TO_RAD * grip.getRotX(), DEG_TO_RAD * grip.getRotY(), DEG_TO_RAD * grip.getRotZ())
|
? new Quaternion().fromAngles(DEG_TO_RAD * grip.getRotX(), DEG_TO_RAD * grip.getRotY(), DEG_TO_RAD * grip.getRotZ())
|
||||||
@@ -163,21 +163,65 @@ public class WeaponAttachmentControl extends AbstractControl {
|
|||||||
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
|
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
|
||||||
: Vector3f.ZERO;
|
: Vector3f.ZERO;
|
||||||
|
|
||||||
// animDelta = wie weit der Knochen sich von der Bind-Pose bewegt hat.
|
|
||||||
// In der Bind-Pose (T-Pose / Idle) ist animDelta ≈ identity → weaponRot ≈ gripRot.
|
|
||||||
// Während Angriffsanimationen dreht animDelta die Waffe mit der Hand mit.
|
|
||||||
Quaternion animDelta = (handBindRotInChar != null)
|
Quaternion animDelta = (handBindRotInChar != null)
|
||||||
? boneRotInChar.mult(handBindRotInChar.inverse())
|
? boneRotInChar.mult(handBindRotInChar.inverse())
|
||||||
: boneRotInChar;
|
: boneRotInChar;
|
||||||
|
Quaternion weaponRot = animDelta.mult(gripRot.inverse());
|
||||||
|
|
||||||
Quaternion weaponRot = animDelta.mult(gripRot);
|
if (!gripDiagLogged && weaponSpatial != null) {
|
||||||
|
gripDiagLogged = true;
|
||||||
|
float[] wa = weaponRot.toAngles(null);
|
||||||
|
Vector3f bx = weaponRot.mult(Vector3f.UNIT_X);
|
||||||
|
Vector3f by = weaponRot.mult(Vector3f.UNIT_Y);
|
||||||
|
Vector3f bz = weaponRot.mult(Vector3f.UNIT_Z);
|
||||||
|
float[] ad = animDelta.toAngles(null);
|
||||||
|
float[] gr = gripRot.toAngles(null);
|
||||||
|
log.info("[GripDiag] animDelta(°): x={} y={} z={}",
|
||||||
|
Math.round(Math.toDegrees(ad[0])), Math.round(Math.toDegrees(ad[1])), Math.round(Math.toDegrees(ad[2])));
|
||||||
|
log.info("[GripDiag] gripRot(°): x={} y={} z={}",
|
||||||
|
Math.round(Math.toDegrees(gr[0])), Math.round(Math.toDegrees(gr[1])), Math.round(Math.toDegrees(gr[2])));
|
||||||
|
log.info("[GripDiag] weaponRot(°): x={} y={} z={}",
|
||||||
|
Math.round(Math.toDegrees(wa[0])), Math.round(Math.toDegrees(wa[1])), Math.round(Math.toDegrees(wa[2])));
|
||||||
|
log.info("[GripDiag] Waffe +X in CharSpace: ({},{},{})", round1(bx.x), round1(bx.y), round1(bx.z));
|
||||||
|
log.info("[GripDiag] Waffe +Y in CharSpace: ({},{},{})", round1(by.x), round1(by.y), round1(by.z));
|
||||||
|
log.info("[GripDiag] Waffe +Z in CharSpace: ({},{},{})", round1(bz.x), round1(bz.y), round1(bz.z));
|
||||||
|
// Vergleich RightHand vs RightHandMiddle1 – welcher Bone gibt die bessere Orientierung?
|
||||||
|
Quaternion charWorldRotInv2 = spatial.getWorldRotation().inverse();
|
||||||
|
if (palmNodeRef != null) {
|
||||||
|
Quaternion palmRotInChar = charWorldRotInv2.mult(palmNodeRef.getWorldRotation());
|
||||||
|
Quaternion palmBind = extractJointBindRot(skinCtrl, BONE_PALM);
|
||||||
|
Quaternion palmDelta = (palmBind != null) ? palmRotInChar.mult(palmBind.inverse()) : palmRotInChar;
|
||||||
|
float[] pd = palmDelta.toAngles(null);
|
||||||
|
Vector3f px = palmDelta.mult(Vector3f.UNIT_X);
|
||||||
|
Vector3f py = palmDelta.mult(Vector3f.UNIT_Y);
|
||||||
|
log.info("[GripDiag] palmDelta(°) [Middle1]: x={} y={} z={}",
|
||||||
|
Math.round(Math.toDegrees(pd[0])), Math.round(Math.toDegrees(pd[1])), Math.round(Math.toDegrees(pd[2])));
|
||||||
|
log.info("[GripDiag] palm +X in CharSpace: ({},{},{})", round1(px.x), round1(px.y), round1(px.z));
|
||||||
|
log.info("[GripDiag] palm +Y in CharSpace: ({},{},{})", round1(py.x), round1(py.y), round1(py.z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Waffenposition: Griff-Punkt liegt am Knochen-Ursprung
|
Vector3f weaponPos = bonePosInChar.subtract(weaponRot.mult(gripPos));
|
||||||
|
weaponPos = weaponPos.add(weaponRot.mult(HAND_GRIP_OFFSET));
|
||||||
weaponSpatial.setLocalRotation(weaponRot);
|
weaponSpatial.setLocalRotation(weaponRot);
|
||||||
weaponSpatial.setLocalTranslation(bonePosInChar.subtract(weaponRot.mult(gripPos)));
|
weaponSpatial.setLocalTranslation(weaponPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float round1(float v) { return Math.round(v * 10f) / 10f; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Berechnet den Offset Handgelenk → Fingerknöchel in Hand-Bone-Local-Space
|
||||||
|
* direkt aus den World-Positionen der Attachment-Nodes.
|
||||||
|
* Dieser Ansatz umgeht alle InverseBindMatrix-Skalierungsprobleme.
|
||||||
|
* Muss aufgerufen werden NACHDEM SkinningControl mindestens einmal gelaufen ist.
|
||||||
|
*/
|
||||||
|
private static Vector3f computePalmOffsetFromNodes(Node handNode, Node palmNode) {
|
||||||
|
Vector3f deltaWorld = palmNode.getWorldTranslation().subtract(handNode.getWorldTranslation());
|
||||||
|
// Delta in Hand-Bone-Local-Space ausdrücken (rotiert mit der Hand mit)
|
||||||
|
Quaternion handWorldRotInv = handNode.getWorldRotation().inverse();
|
||||||
|
return handWorldRotInv.mult(deltaWorld);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Liest die Bind-Pose-Rotation eines Knochens (Model-Raum ≈ Character-lokal) aus dem InverseBindMatrix. */
|
|
||||||
private static Quaternion extractJointBindRot(com.jme3.anim.SkinningControl sc, String jointName) {
|
private static Quaternion extractJointBindRot(com.jme3.anim.SkinningControl sc, String jointName) {
|
||||||
com.jme3.anim.Armature arm = sc.getArmature();
|
com.jme3.anim.Armature arm = sc.getArmature();
|
||||||
if (arm == null) return null;
|
if (arm == null) return null;
|
||||||
@@ -190,22 +234,13 @@ public class WeaponAttachmentControl extends AbstractControl {
|
|||||||
return bindMat.toRotationQuat().normalizeLocal();
|
return bindMat.toRotationQuat().normalizeLocal();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Waffe am Rücken: BACK_GRIP_OFFSET über Spine2, Klinge zeigt nach unten.
|
|
||||||
*/
|
|
||||||
private void applyBackTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) {
|
private void applyBackTransform(Vector3f bonePosInChar, Quaternion boneRotInChar, ItemGrip grip) {
|
||||||
// fromAngles(0, 0, HALF_PI): Spine2-FBX-Flip kompensiert, Klinge zeigt nach unten
|
Quaternion backRot = new Quaternion().fromAngles(0f, 0f, HALF_PI);
|
||||||
Quaternion backRot = new Quaternion().fromAngles(0f, 0f, HALF_PI);
|
|
||||||
Quaternion weaponRot = boneRotInChar.mult(backRot);
|
Quaternion weaponRot = boneRotInChar.mult(backRot);
|
||||||
|
Vector3f gripTarget = bonePosInChar.add(boneRotInChar.mult(BACK_GRIP_OFFSET));
|
||||||
// Zielposition des Griff-Punktes: BACK_GRIP_OFFSET im Knochen-lokalen Raum
|
Vector3f gripPos = (grip != null)
|
||||||
Vector3f gripTarget = bonePosInChar.add(boneRotInChar.mult(BACK_GRIP_OFFSET));
|
|
||||||
|
|
||||||
Vector3f gripPos = (grip != null)
|
|
||||||
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
|
? new Vector3f(grip.getPosX(), grip.getPosY(), grip.getPosZ())
|
||||||
: Vector3f.ZERO;
|
: Vector3f.ZERO;
|
||||||
|
|
||||||
// weaponPos + weaponRot * gripPos = gripTarget
|
|
||||||
weaponSpatial.setLocalRotation(weaponRot);
|
weaponSpatial.setLocalRotation(weaponRot);
|
||||||
weaponSpatial.setLocalTranslation(gripTarget.subtract(weaponRot.mult(gripPos)));
|
weaponSpatial.setLocalTranslation(gripTarget.subtract(weaponRot.mult(gripPos)));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user