Commit vor Änderung Umstellung auf eine einzelne Animation
This commit is contained in:
@@ -30,28 +30,8 @@
|
|||||||
"REVIVE": "alive_again"
|
"REVIVE": "alive_again"
|
||||||
},
|
},
|
||||||
"previewModelPath": "Models/Chars/mainchar.j3o",
|
"previewModelPath": "Models/Chars/mainchar.j3o",
|
||||||
"motionKeyframes": {
|
"animOffsets": {
|
||||||
"sitting": [
|
"sitting": {"tx": 0.0, "ty": 0.0, "tz": -0.5, "rx": 0.0, "ry": 0.0, "rz": 0.0},
|
||||||
{
|
"get_up_sitting": {"tx": 0.0, "ty": 0.0, "tz": -0.5, "rx": 0.0, "ry": 0.0, "rz": 0.0}
|
||||||
"time": 0.0,
|
|
||||||
"tx": 0.0,
|
|
||||||
"ty": 0.0,
|
|
||||||
"tz": -0.5,
|
|
||||||
"rx": 0.0,
|
|
||||||
"ry": 0.0,
|
|
||||||
"rz": 0.0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"get_up_sitting": [
|
|
||||||
{
|
|
||||||
"time": 0.0,
|
|
||||||
"tx": 0.0,
|
|
||||||
"ty": 0.0,
|
|
||||||
"tz": -0.5,
|
|
||||||
"rx": 0.0,
|
|
||||||
"ry": 0.0,
|
|
||||||
"rz": 0.0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,12 +157,12 @@ public class EditorApp extends Application {
|
|||||||
private boolean animSetDirty = false;
|
private boolean animSetDirty = false;
|
||||||
private String animSetCurrentName = null;
|
private String animSetCurrentName = null;
|
||||||
private Path animSetCurrentDir = null;
|
private Path animSetCurrentDir = null;
|
||||||
// Motion-Keyframe-Editor (innerhalb AnimSet-Editor)
|
// Anim-Offset-Editor (innerhalb AnimSet-Editor)
|
||||||
private javafx.scene.control.ListView<de.blight.game.animation.AnimKeyframe> animSetKfListView;
|
private javafx.scene.control.ListView<de.blight.game.animation.AnimOffset> animSetOffsetListView;
|
||||||
private javafx.collections.ObservableList<de.blight.game.animation.AnimKeyframe> animSetKfList =
|
private javafx.collections.ObservableList<de.blight.game.animation.AnimOffset> animSetOffsetList =
|
||||||
javafx.collections.FXCollections.observableArrayList();
|
javafx.collections.FXCollections.observableArrayList();
|
||||||
private java.util.Map<String, java.util.List<de.blight.game.animation.AnimKeyframe>>
|
private java.util.Map<String, de.blight.game.animation.AnimOffset>
|
||||||
animSetMotionKeyframes = new java.util.LinkedHashMap<>();
|
animSetOffsets = new java.util.LinkedHashMap<>();
|
||||||
|
|
||||||
// Character-Editor-Zustand
|
// Character-Editor-Zustand
|
||||||
private de.blight.editor.ui.DialogEditorView dialogEditorView;
|
private de.blight.editor.ui.DialogEditorView dialogEditorView;
|
||||||
@@ -8292,22 +8292,24 @@ public class EditorApp extends Application {
|
|||||||
animSetClipListView.getSelectionModel().selectedItemProperty()
|
animSetClipListView.getSelectionModel().selectedItemProperty()
|
||||||
.addListener((obs, ov, nv) -> {
|
.addListener((obs, ov, nv) -> {
|
||||||
removeClipBtn.setDisable(nv == null);
|
removeClipBtn.setDisable(nv == null);
|
||||||
// Keyframes des alten Clips sichern
|
// Offset des alten Clips sichern
|
||||||
if (ov != null && animSetKfList != null) {
|
if (ov != null && animSetOffsetList != null && !animSetOffsetList.isEmpty()) {
|
||||||
animSetMotionKeyframes.put(ov, new java.util.ArrayList<>(animSetKfList));
|
animSetOffsets.put(ov, animSetOffsetList.get(0));
|
||||||
|
} else if (ov != null) {
|
||||||
|
animSetOffsets.remove(ov);
|
||||||
}
|
}
|
||||||
// Keyframes des neuen Clips laden
|
// Offset des neuen Clips laden
|
||||||
if (animSetKfList != null) {
|
if (animSetOffsetList != null) {
|
||||||
animSetKfList.clear();
|
animSetOffsetList.clear();
|
||||||
if (nv != null) {
|
if (nv != null) {
|
||||||
var kfs = animSetMotionKeyframes.getOrDefault(nv, new java.util.ArrayList<>());
|
de.blight.game.animation.AnimOffset off = animSetOffsets.get(nv);
|
||||||
animSetKfList.setAll(kfs);
|
if (off != null) animSetOffsetList.setAll(off);
|
||||||
// Clip direkt in Vorschau abspielen
|
// Clip direkt in Vorschau abspielen
|
||||||
input.animPreviewPlayClip = nv;
|
input.animPreviewPlayClip = nv;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// KF-Bereich aktivieren/deaktivieren
|
// Offset-Bereich aktivieren/deaktivieren
|
||||||
if (animSetKfListView != null) animSetKfListView.setDisable(nv == null);
|
if (animSetOffsetListView != null) animSetOffsetListView.setDisable(nv == null);
|
||||||
});
|
});
|
||||||
|
|
||||||
addClipBtn.setOnAction(e -> {
|
addClipBtn.setOnAction(e -> {
|
||||||
@@ -8427,64 +8429,61 @@ public class EditorApp extends Application {
|
|||||||
HBox.setHgrow(removeActionBtn, Priority.ALWAYS);
|
HBox.setHgrow(removeActionBtn, Priority.ALWAYS);
|
||||||
inner.getChildren().addAll(animSetActionListView, actionBtns);
|
inner.getChildren().addAll(animSetActionListView, actionBtns);
|
||||||
|
|
||||||
// ── Motion Keyframes ──────────────────────────────────────────────────
|
// ── Anim-Offsets ──────────────────────────────────────────────────────
|
||||||
inner.getChildren().addAll(new Separator(), sectionTitle("Motion Keyframes"), new Separator());
|
inner.getChildren().addAll(new Separator(), sectionTitle("Anim-Offsets"), new Separator());
|
||||||
|
|
||||||
Label kfHint = new Label("TX/TZ = charakter-lokal (seitlich/vorwärts), TY = Welt-Y (hoch/runter). RX/RY/RZ in Grad, additiv zur Startrotation. Zeit ≤ Clip-Dauer (Clip vorab in Vorschau abspielen).");
|
Label kfHint = new Label("TX/TZ = charakter-lokal (seitlich/vorwärts), TY = Welt-Y (hoch/runter). RX/RY/RZ in Grad, additiv zur Startrotation.");
|
||||||
kfHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;");
|
kfHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;");
|
||||||
kfHint.setWrapText(true);
|
kfHint.setWrapText(true);
|
||||||
|
|
||||||
// Keyframes aus AnimSet laden
|
// Offsets aus AnimSet laden
|
||||||
animSetMotionKeyframes = new java.util.LinkedHashMap<>();
|
animSetOffsets = new java.util.LinkedHashMap<>(animSet.getAnimOffsets());
|
||||||
for (var entry : animSet.getMotionKeyframes().entrySet()) {
|
animSetOffsetList = javafx.collections.FXCollections.observableArrayList();
|
||||||
animSetMotionKeyframes.put(entry.getKey(), new java.util.ArrayList<>(entry.getValue()));
|
animSetOffsetList.addListener((javafx.collections.ListChangeListener<de.blight.game.animation.AnimOffset>)
|
||||||
}
|
change -> updateAnimPreviewOffset());
|
||||||
animSetKfList = javafx.collections.FXCollections.observableArrayList();
|
|
||||||
animSetKfList.addListener((javafx.collections.ListChangeListener<de.blight.game.animation.AnimKeyframe>)
|
|
||||||
change -> updateAnimPreviewKfOffset());
|
|
||||||
|
|
||||||
// ListView mit Zusammenfassung, Doppelklick öffnet Edit-Dialog
|
// ListView zeigt den einen Offset des gewählten Clips; Doppelklick öffnet Edit-Dialog
|
||||||
animSetKfListView = new javafx.scene.control.ListView<>(animSetKfList);
|
animSetOffsetListView = new javafx.scene.control.ListView<>(animSetOffsetList);
|
||||||
animSetKfListView.setPrefHeight(150);
|
animSetOffsetListView.setPrefHeight(60);
|
||||||
animSetKfListView.setPlaceholder(new Label("Keine Keyframes – [+ Keyframe] zum Hinzufügen"));
|
animSetOffsetListView.setPlaceholder(new Label("Kein Offset – [+ Offset] zum Setzen"));
|
||||||
animSetKfListView.setCellFactory(lv -> new javafx.scene.control.ListCell<>() {
|
animSetOffsetListView.setCellFactory(lv -> new javafx.scene.control.ListCell<>() {
|
||||||
@Override protected void updateItem(de.blight.game.animation.AnimKeyframe kf, boolean empty) {
|
@Override protected void updateItem(de.blight.game.animation.AnimOffset off, boolean empty) {
|
||||||
super.updateItem(kf, empty);
|
super.updateItem(off, empty);
|
||||||
if (empty || kf == null) { setText(null); return; }
|
if (empty || off == null) { setText(null); return; }
|
||||||
setText(String.format("%.3fs | TX%+.3f TY%+.3f TZ%+.3f | RX%+.1f° RY%+.1f° RZ%+.1f°",
|
setText(String.format("TX%+.3f TY%+.3f TZ%+.3f | RX%+.1f° RY%+.1f° RZ%+.1f°",
|
||||||
kf.time, kf.tx, kf.ty, kf.tz, kf.rx, kf.ry, kf.rz));
|
off.tx, off.ty, off.tz, off.rx, off.ry, off.rz));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
animSetKfListView.setOnMouseClicked(ev -> {
|
animSetOffsetListView.setOnMouseClicked(ev -> {
|
||||||
if (ev.getClickCount() == 2) {
|
if (ev.getClickCount() == 2) {
|
||||||
de.blight.game.animation.AnimKeyframe sel =
|
de.blight.game.animation.AnimOffset sel =
|
||||||
animSetKfListView.getSelectionModel().getSelectedItem();
|
animSetOffsetListView.getSelectionModel().getSelectedItem();
|
||||||
if (sel != null) showAnimKfDialog(sel);
|
if (sel != null) showAnimOffsetDialog(sel);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// initial deaktiviert – wird durch Clip-Selektion gesteuert
|
// initial deaktiviert – wird durch Clip-Selektion gesteuert
|
||||||
animSetKfListView.setDisable(true);
|
animSetOffsetListView.setDisable(true);
|
||||||
|
|
||||||
Button addKfBtn = new Button("+ Keyframe");
|
Button addKfBtn = new Button("+ Offset");
|
||||||
Button removeKfBtn = new Button("- Entfernen");
|
Button removeKfBtn = new Button("- Entfernen");
|
||||||
addKfBtn.setMaxWidth(Double.MAX_VALUE);
|
addKfBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
removeKfBtn.setMaxWidth(Double.MAX_VALUE);
|
removeKfBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
addKfBtn.setDisable(true);
|
addKfBtn.setDisable(true);
|
||||||
removeKfBtn.setDisable(true);
|
removeKfBtn.setDisable(true);
|
||||||
animSetKfListView.getSelectionModel().selectedItemProperty()
|
animSetOffsetListView.getSelectionModel().selectedItemProperty()
|
||||||
.addListener((obs, ov, nv) -> removeKfBtn.setDisable(nv == null));
|
.addListener((obs, ov, nv) -> removeKfBtn.setDisable(nv == null));
|
||||||
// addKfBtn folgt Clip-Selektion
|
// addKfBtn folgt Clip-Selektion (nur wenn noch kein Offset gesetzt)
|
||||||
animSetClipListView.getSelectionModel().selectedItemProperty()
|
animSetClipListView.getSelectionModel().selectedItemProperty()
|
||||||
.addListener((obs, ov, nv) -> addKfBtn.setDisable(nv == null));
|
.addListener((obs, ov, nv) -> addKfBtn.setDisable(nv == null));
|
||||||
|
|
||||||
addKfBtn.setOnAction(e -> showAnimKfDialog(null));
|
addKfBtn.setOnAction(e -> showAnimOffsetDialog(null));
|
||||||
|
|
||||||
removeKfBtn.setOnAction(e -> {
|
removeKfBtn.setOnAction(e -> {
|
||||||
de.blight.game.animation.AnimKeyframe sel =
|
de.blight.game.animation.AnimOffset sel =
|
||||||
animSetKfListView.getSelectionModel().getSelectedItem();
|
animSetOffsetListView.getSelectionModel().getSelectedItem();
|
||||||
if (sel != null) {
|
if (sel != null) {
|
||||||
animSetKfList.remove(sel);
|
animSetOffsetList.remove(sel);
|
||||||
animSetDirty = true;
|
animSetDirty = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -8492,7 +8491,7 @@ public class EditorApp extends Application {
|
|||||||
HBox kfBtns = new HBox(6, addKfBtn, removeKfBtn);
|
HBox kfBtns = new HBox(6, addKfBtn, removeKfBtn);
|
||||||
HBox.setHgrow(addKfBtn, Priority.ALWAYS);
|
HBox.setHgrow(addKfBtn, Priority.ALWAYS);
|
||||||
HBox.setHgrow(removeKfBtn, Priority.ALWAYS);
|
HBox.setHgrow(removeKfBtn, Priority.ALWAYS);
|
||||||
inner.getChildren().addAll(kfHint, animSetKfListView, kfBtns);
|
inner.getChildren().addAll(kfHint, animSetOffsetListView, kfBtns);
|
||||||
|
|
||||||
// ── Vorschau ─────────────────────────────────────────────────────────
|
// ── Vorschau ─────────────────────────────────────────────────────────
|
||||||
inner.getChildren().addAll(new Separator(), sectionTitle("Vorschau"), new Separator());
|
inner.getChildren().addAll(new Separator(), sectionTitle("Vorschau"), new Separator());
|
||||||
@@ -8552,16 +8551,10 @@ public class EditorApp extends Application {
|
|||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Öffnet den Keyframe-Dialog (null = Hinzufügen, non-null = Bearbeiten). */
|
/** Öffnet den Offset-Dialog (null = Hinzufügen, non-null = Bearbeiten). */
|
||||||
private void showAnimKfDialog(de.blight.game.animation.AnimKeyframe existing) {
|
private void showAnimOffsetDialog(de.blight.game.animation.AnimOffset existing) {
|
||||||
boolean isAdd = (existing == null);
|
boolean isAdd = (existing == null);
|
||||||
// Zeitlimit: zuletzt gemessene Clip-Dauer aus Preview, sonst 999
|
|
||||||
float maxTime = input.animPreviewCurrentClipDuration > 0.01f
|
|
||||||
? input.animPreviewCurrentClipDuration : 999.0f;
|
|
||||||
|
|
||||||
float initTime = isAdd
|
|
||||||
? (animSetKfList.isEmpty() ? 0f : Math.min(animSetKfList.get(animSetKfList.size() - 1).time + 0.5f, maxTime))
|
|
||||||
: existing.time;
|
|
||||||
float initTX = isAdd ? 0f : existing.tx;
|
float initTX = isAdd ? 0f : existing.tx;
|
||||||
float initTY = isAdd ? 0f : existing.ty;
|
float initTY = isAdd ? 0f : existing.ty;
|
||||||
float initTZ = isAdd ? 0f : existing.tz;
|
float initTZ = isAdd ? 0f : existing.tz;
|
||||||
@@ -8569,25 +8562,19 @@ public class EditorApp extends Application {
|
|||||||
float initRY = isAdd ? 0f : existing.ry;
|
float initRY = isAdd ? 0f : existing.ry;
|
||||||
float initRZ = isAdd ? 0f : existing.rz;
|
float initRZ = isAdd ? 0f : existing.rz;
|
||||||
|
|
||||||
Spinner<Double> spTime = new Spinner<>(0.0, maxTime, initTime, 0.05);
|
Spinner<Double> spTX = new Spinner<>(-10.0, 10.0, initTX, 0.05);
|
||||||
Spinner<Double> spTX = new Spinner<>(-10.0, 10.0, initTX, 0.05);
|
Spinner<Double> spTY = new Spinner<>(-10.0, 10.0, initTY, 0.05);
|
||||||
Spinner<Double> spTY = new Spinner<>(-10.0, 10.0, initTY, 0.05);
|
Spinner<Double> spTZ = new Spinner<>(-10.0, 10.0, initTZ, 0.05);
|
||||||
Spinner<Double> spTZ = new Spinner<>(-10.0, 10.0, initTZ, 0.05);
|
Spinner<Double> spRX = new Spinner<>(-360.0, 360.0, initRX, 1.0);
|
||||||
Spinner<Double> spRX = new Spinner<>(-360.0, 360.0, initRX, 1.0);
|
Spinner<Double> spRY = new Spinner<>(-360.0, 360.0, initRY, 1.0);
|
||||||
Spinner<Double> spRY = new Spinner<>(-360.0, 360.0, initRY, 1.0);
|
Spinner<Double> spRZ = new Spinner<>(-360.0, 360.0, initRZ, 1.0);
|
||||||
Spinner<Double> spRZ = new Spinner<>(-360.0, 360.0, initRZ, 1.0);
|
for (Spinner<Double> sp : new Spinner[]{spTX, spTY, spTZ, spRX, spRY, spRZ}) {
|
||||||
for (Spinner<Double> sp : new Spinner[]{spTime, spTX, spTY, spTZ, spRX, spRY, spRZ}) {
|
|
||||||
sp.setEditable(true);
|
sp.setEditable(true);
|
||||||
sp.setMaxWidth(Double.MAX_VALUE);
|
sp.setMaxWidth(Double.MAX_VALUE);
|
||||||
}
|
}
|
||||||
|
|
||||||
String timeLabel = maxTime < 999f
|
String[][] rows = {{"TX (m):"}, {"TY (m):"}, {"TZ (m):"}, {"RX (°):"}, {"RY (°):"}, {"RZ (°):"}};
|
||||||
? String.format("Zeit (0 – %.2fs):", maxTime)
|
Spinner<?>[] sps = {spTX, spTY, spTZ, spRX, spRY, spRZ};
|
||||||
: "Zeit (s):";
|
|
||||||
String[][] rows = {
|
|
||||||
{timeLabel}, {"TX (m):"}, {"TY (m):"}, {"TZ (m):"}, {"RX (°):"}, {"RY (°):"}, {"RZ (°):"}
|
|
||||||
};
|
|
||||||
Spinner<?>[] sps = {spTime, spTX, spTY, spTZ, spRX, spRY, spRZ};
|
|
||||||
javafx.scene.layout.GridPane kfGrid = new javafx.scene.layout.GridPane();
|
javafx.scene.layout.GridPane kfGrid = new javafx.scene.layout.GridPane();
|
||||||
kfGrid.setHgap(8); kfGrid.setVgap(5);
|
kfGrid.setHgap(8); kfGrid.setVgap(5);
|
||||||
for (int i = 0; i < sps.length; i++) {
|
for (int i = 0; i < sps.length; i++) {
|
||||||
@@ -8600,7 +8587,7 @@ public class EditorApp extends Application {
|
|||||||
|
|
||||||
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> kfDlg =
|
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> kfDlg =
|
||||||
new javafx.scene.control.Dialog<>();
|
new javafx.scene.control.Dialog<>();
|
||||||
kfDlg.setTitle(isAdd ? "Keyframe hinzufügen" : "Keyframe bearbeiten");
|
kfDlg.setTitle(isAdd ? "Offset hinzufügen" : "Offset bearbeiten");
|
||||||
javafx.scene.control.ButtonType okKf = new javafx.scene.control.ButtonType(
|
javafx.scene.control.ButtonType okKf = new javafx.scene.control.ButtonType(
|
||||||
isAdd ? "Hinzufügen" : "Übernehmen",
|
isAdd ? "Hinzufügen" : "Übernehmen",
|
||||||
javafx.scene.control.ButtonBar.ButtonData.OK_DONE);
|
javafx.scene.control.ButtonBar.ButtonData.OK_DONE);
|
||||||
@@ -8608,15 +8595,12 @@ public class EditorApp extends Application {
|
|||||||
kfDlg.getDialogPane().setContent(kfGrid);
|
kfDlg.getDialogPane().setContent(kfGrid);
|
||||||
kfDlg.showAndWait().ifPresent(bt -> {
|
kfDlg.showAndWait().ifPresent(bt -> {
|
||||||
if (bt != okKf) return;
|
if (bt != okKf) return;
|
||||||
float t = (float) Math.min(spTime.getValue(), maxTime);
|
|
||||||
if (isAdd) {
|
if (isAdd) {
|
||||||
de.blight.game.animation.AnimKeyframe kf = new de.blight.game.animation.AnimKeyframe(
|
de.blight.game.animation.AnimOffset off = new de.blight.game.animation.AnimOffset(
|
||||||
t,
|
|
||||||
spTX.getValue().floatValue(), spTY.getValue().floatValue(), spTZ.getValue().floatValue(),
|
spTX.getValue().floatValue(), spTY.getValue().floatValue(), spTZ.getValue().floatValue(),
|
||||||
spRX.getValue().floatValue(), spRY.getValue().floatValue(), spRZ.getValue().floatValue());
|
spRX.getValue().floatValue(), spRY.getValue().floatValue(), spRZ.getValue().floatValue());
|
||||||
animSetKfList.add(kf);
|
animSetOffsetList.setAll(off);
|
||||||
} else {
|
} else {
|
||||||
existing.time = t;
|
|
||||||
existing.tx = spTX.getValue().floatValue();
|
existing.tx = spTX.getValue().floatValue();
|
||||||
existing.ty = spTY.getValue().floatValue();
|
existing.ty = spTY.getValue().floatValue();
|
||||||
existing.tz = spTZ.getValue().floatValue();
|
existing.tz = spTZ.getValue().floatValue();
|
||||||
@@ -8624,8 +8608,7 @@ public class EditorApp extends Application {
|
|||||||
existing.ry = spRY.getValue().floatValue();
|
existing.ry = spRY.getValue().floatValue();
|
||||||
existing.rz = spRZ.getValue().floatValue();
|
existing.rz = spRZ.getValue().floatValue();
|
||||||
}
|
}
|
||||||
animSetKfList.sort(java.util.Comparator.comparingDouble(k -> k.time));
|
if (animSetOffsetListView != null) animSetOffsetListView.refresh();
|
||||||
if (animSetKfListView != null) animSetKfListView.refresh();
|
|
||||||
animSetDirty = true;
|
animSetDirty = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -8645,21 +8628,18 @@ public class EditorApp extends Application {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateAnimPreviewKfOffset() {
|
private void updateAnimPreviewOffset() {
|
||||||
if (animSetKfList == null || animSetKfList.isEmpty()) {
|
if (animSetOffsetList == null || animSetOffsetList.isEmpty()) {
|
||||||
input.animPreviewKfTx = 0f;
|
input.animPreviewOffsetTx = 0f;
|
||||||
input.animPreviewKfTy = 0f;
|
input.animPreviewOffsetTy = 0f;
|
||||||
input.animPreviewKfTz = 0f;
|
input.animPreviewOffsetTz = 0f;
|
||||||
input.animPreviewKfActive = false;
|
input.animPreviewOffsetActive = false;
|
||||||
} else {
|
} else {
|
||||||
de.blight.game.animation.AnimKeyframe first = animSetKfList.get(0);
|
de.blight.game.animation.AnimOffset off = animSetOffsetList.get(0);
|
||||||
for (de.blight.game.animation.AnimKeyframe k : animSetKfList) {
|
input.animPreviewOffsetTx = off.tx;
|
||||||
if (k.time < first.time) { first = k; }
|
input.animPreviewOffsetTy = off.ty;
|
||||||
}
|
input.animPreviewOffsetTz = off.tz;
|
||||||
input.animPreviewKfTx = first.tx;
|
input.animPreviewOffsetActive = true;
|
||||||
input.animPreviewKfTy = first.ty;
|
|
||||||
input.animPreviewKfTz = first.tz;
|
|
||||||
input.animPreviewKfActive = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8768,21 +8748,25 @@ public class EditorApp extends Application {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
animSet.setActionMap(actionMap);
|
animSet.setActionMap(actionMap);
|
||||||
// Motion Keyframes: aktuell sichtbare Liste (des selektierten Clips) sichern, dann schreiben
|
// Anim-Offsets: aktuell sichtbaren Offset (des selektierten Clips) sichern, dann schreiben
|
||||||
if (animSetClipListView != null) {
|
if (animSetClipListView != null) {
|
||||||
String selClip = animSetClipListView.getSelectionModel().getSelectedItem();
|
String selClip = animSetClipListView.getSelectionModel().getSelectedItem();
|
||||||
if (selClip != null) {
|
if (selClip != null) {
|
||||||
animSetMotionKeyframes.put(selClip, new java.util.ArrayList<>(animSetKfList));
|
if (!animSetOffsetList.isEmpty()) {
|
||||||
|
animSetOffsets.put(selClip, animSetOffsetList.get(0));
|
||||||
|
} else {
|
||||||
|
animSetOffsets.remove(selClip);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
java.util.Map<String, java.util.List<de.blight.game.animation.AnimKeyframe>> kfFinal =
|
java.util.Map<String, de.blight.game.animation.AnimOffset> offsetFinal =
|
||||||
new java.util.LinkedHashMap<>();
|
new java.util.LinkedHashMap<>();
|
||||||
for (var kfEntry : animSetMotionKeyframes.entrySet()) {
|
for (var entry : animSetOffsets.entrySet()) {
|
||||||
if (kfEntry.getValue() != null && !kfEntry.getValue().isEmpty()) {
|
if (entry.getValue() != null) {
|
||||||
kfFinal.put(kfEntry.getKey(), kfEntry.getValue());
|
offsetFinal.put(entry.getKey(), entry.getValue());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
animSet.setMotionKeyframes(kfFinal);
|
animSet.setAnimOffsets(offsetFinal);
|
||||||
// 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());
|
||||||
|
|||||||
@@ -572,11 +572,11 @@ public class SharedInput {
|
|||||||
animPreviewJointNames = new java.util.concurrent.atomic.AtomicReference<>();
|
animPreviewJointNames = new java.util.concurrent.atomic.AtomicReference<>();
|
||||||
/** JME3 → JavaFX: Länge des zuletzt gestarteten Clips in Sekunden (0 = unbekannt). */
|
/** JME3 → JavaFX: Länge des zuletzt gestarteten Clips in Sekunden (0 = unbekannt). */
|
||||||
public volatile float animPreviewCurrentClipDuration = 0f;
|
public volatile float animPreviewCurrentClipDuration = 0f;
|
||||||
/** JavaFX → JME3: Motion-Keyframe-Vorschau-Versatz (erster KF, tx/ty/tz in Metern). */
|
/** JavaFX → JME3: Anim-Offset-Vorschau (tx/ty/tz in Metern). */
|
||||||
public volatile float animPreviewKfTx = 0f;
|
public volatile float animPreviewOffsetTx = 0f;
|
||||||
public volatile float animPreviewKfTy = 0f;
|
public volatile float animPreviewOffsetTy = 0f;
|
||||||
public volatile float animPreviewKfTz = 0f;
|
public volatile float animPreviewOffsetTz = 0f;
|
||||||
public volatile boolean animPreviewKfActive = false;
|
public volatile boolean animPreviewOffsetActive = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* JME3 → JavaFX: Relativer Asset-Pfad des gerade geladenen Modells.
|
* JME3 → JavaFX: Relativer Asset-Pfad des gerade geladenen Modells.
|
||||||
|
|||||||
@@ -231,11 +231,11 @@ public class AnimPreviewState extends BaseAppState {
|
|||||||
axesNode.setLocalTranslation(Vector3f.ZERO);
|
axesNode.setLocalTranslation(Vector3f.ZERO);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Motion-Keyframe-Vorschau: Versatz auf Modell anwenden (TX/TY/TZ in Welt-Koordinaten)
|
// Anim-Offset-Vorschau: Versatz auf Modell anwenden (TX/TY/TZ in Welt-Koordinaten)
|
||||||
if (currentModel != null) {
|
if (currentModel != null) {
|
||||||
if (input.animPreviewKfActive) {
|
if (input.animPreviewOffsetActive) {
|
||||||
currentModel.setLocalTranslation(
|
currentModel.setLocalTranslation(
|
||||||
input.animPreviewKfTx, input.animPreviewKfTy, input.animPreviewKfTz);
|
input.animPreviewOffsetTx, input.animPreviewOffsetTy, input.animPreviewOffsetTz);
|
||||||
} else {
|
} else {
|
||||||
currentModel.setLocalTranslation(Vector3f.ZERO);
|
currentModel.setLocalTranslation(Vector3f.ZERO);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
package de.blight.game.animation;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ein Keyframe für manuellen Positions-/Rotations-Versatz während einer blockierenden Animation.
|
|
||||||
*
|
|
||||||
* TX/TZ: Versatz in Charakter-lokalem Raum (TX=seitlich, TZ=vorwärts relativ zur Blickrichtung).
|
|
||||||
* TY: Versatz in Welt-Y (hoch/runter).
|
|
||||||
* RX/RY/RZ: Additiver Rotations-Versatz in Grad (Euler XYZ, relativ zur Startrotation).
|
|
||||||
*
|
|
||||||
* Keyframes einer Aktion werden nach {@code time} sortiert und linear interpoliert.
|
|
||||||
*/
|
|
||||||
public class AnimKeyframe {
|
|
||||||
|
|
||||||
public float time; // Sekunden seit Animations-Start
|
|
||||||
public float tx, ty, tz; // Positions-Versatz (Meter)
|
|
||||||
public float rx, ry, rz; // Rotations-Versatz (Grad)
|
|
||||||
|
|
||||||
public AnimKeyframe() {}
|
|
||||||
|
|
||||||
public AnimKeyframe(float time,
|
|
||||||
float tx, float ty, float tz,
|
|
||||||
float rx, float ry, float rz) {
|
|
||||||
this.time = time;
|
|
||||||
this.tx = tx; this.ty = ty; this.tz = tz;
|
|
||||||
this.rx = rx; this.ry = ry; this.rz = rz;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -26,8 +26,8 @@ public class AnimSet {
|
|||||||
private Map<String, String> actionMap = new LinkedHashMap<>();
|
private Map<String, String> actionMap = new LinkedHashMap<>();
|
||||||
/** Zuletzt im Editor verwendeter Modell-Pfad (relativ zu Assets-Root). Wird beim Öffnen auto-geladen. */
|
/** Zuletzt im Editor verwendeter Modell-Pfad (relativ zu Assets-Root). Wird beim Öffnen auto-geladen. */
|
||||||
private String previewModelPath = null;
|
private String previewModelPath = null;
|
||||||
/** Manueller Positions-/Rotations-Versatz: Aktion → sortierte Keyframe-Liste. */
|
/** Manueller Positions-/Rotations-Versatz pro Clip-Name. */
|
||||||
private Map<String, List<AnimKeyframe>> motionKeyframes = new LinkedHashMap<>();
|
private Map<String, AnimOffset> animOffsets = new LinkedHashMap<>();
|
||||||
|
|
||||||
public List<String> getClips() { return clips; }
|
public List<String> getClips() { return clips; }
|
||||||
public void setClips(List<String> clips) { this.clips = clips; }
|
public void setClips(List<String> clips) { this.clips = clips; }
|
||||||
@@ -35,11 +35,11 @@ public class AnimSet {
|
|||||||
public void setActionMap(Map<String, String> actionMap) { this.actionMap = actionMap; }
|
public void setActionMap(Map<String, String> actionMap) { this.actionMap = actionMap; }
|
||||||
public String getPreviewModelPath() { return previewModelPath; }
|
public String getPreviewModelPath() { return previewModelPath; }
|
||||||
public void setPreviewModelPath(String previewModelPath) { this.previewModelPath = previewModelPath; }
|
public void setPreviewModelPath(String previewModelPath) { this.previewModelPath = previewModelPath; }
|
||||||
public Map<String, List<AnimKeyframe>> getMotionKeyframes() {
|
public Map<String, AnimOffset> getAnimOffsets() {
|
||||||
return motionKeyframes != null ? motionKeyframes : new LinkedHashMap<>();
|
return animOffsets != null ? animOffsets : new LinkedHashMap<>();
|
||||||
}
|
}
|
||||||
public void setMotionKeyframes(Map<String, List<AnimKeyframe>> motionKeyframes) {
|
public void setAnimOffsets(Map<String, AnimOffset> animOffsets) {
|
||||||
this.motionKeyframes = motionKeyframes;
|
this.animOffsets = animOffsets;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Speichert dieses Set als {@code <setName>.animset.json} im Verzeichnis {@code setDir}. */
|
/** Speichert dieses Set als {@code <setName>.animset.json} im Verzeichnis {@code setDir}. */
|
||||||
|
|||||||
@@ -1,226 +0,0 @@
|
|||||||
package de.blight.game.animation;
|
|
||||||
|
|
||||||
import com.jme3.anim.Armature;
|
|
||||||
import com.jme3.anim.Joint;
|
|
||||||
import com.jme3.math.FastMath;
|
|
||||||
import com.jme3.math.Quaternion;
|
|
||||||
import com.jme3.math.Vector3f;
|
|
||||||
import com.jme3.renderer.RenderManager;
|
|
||||||
import com.jme3.renderer.ViewPort;
|
|
||||||
import com.jme3.scene.Spatial;
|
|
||||||
import com.jme3.scene.control.AbstractControl;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 2-Bone-Foot-IK – hält die Füße an ihrer World-Space-Position fest.
|
|
||||||
*
|
|
||||||
* Muss NACH AnimComposer und SkinningControl zur Spatial hinzugefügt werden,
|
|
||||||
* damit controlUpdate() nach der Animations-Anwendung läuft.
|
|
||||||
*/
|
|
||||||
public final class FootIKControl extends AbstractControl {
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(FootIKControl.class);
|
|
||||||
|
|
||||||
private final Armature armature;
|
|
||||||
|
|
||||||
private final Joint leftThigh, leftCalf, leftFoot;
|
|
||||||
private final Joint rightThigh, rightCalf, rightFoot;
|
|
||||||
|
|
||||||
// IK-Ziele in World-Space; null = IK inaktiv
|
|
||||||
private Vector3f leftWorldTarget = null;
|
|
||||||
private Vector3f rightWorldTarget = null;
|
|
||||||
|
|
||||||
private static final float MIN_CORRECTION_DIST = 0.005f;
|
|
||||||
|
|
||||||
private int debugFramesLeft = 0;
|
|
||||||
|
|
||||||
public FootIKControl(Armature armature) {
|
|
||||||
this.armature = armature;
|
|
||||||
leftThigh = findJoint(armature, "L_Thigh", "LeftUpLeg", "mixamorig:LeftUpLeg");
|
|
||||||
leftCalf = findJoint(armature, "L_Calf", "LeftLeg", "mixamorig:LeftLeg");
|
|
||||||
leftFoot = findJoint(armature, "L_Foot", "LeftFoot", "mixamorig:LeftFoot");
|
|
||||||
rightThigh = findJoint(armature, "R_Thigh", "RightUpLeg", "mixamorig:RightUpLeg");
|
|
||||||
rightCalf = findJoint(armature, "R_Calf", "RightLeg", "mixamorig:RightLeg");
|
|
||||||
rightFoot = findJoint(armature, "R_Foot", "RightFoot", "mixamorig:RightFoot");
|
|
||||||
log.info("[FootIK] Init: L={} R={}",
|
|
||||||
leftFoot != null ? leftFoot.getName() : "n/a",
|
|
||||||
rightFoot != null ? rightFoot.getName() : "n/a");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fixiert die Füße an ihrer aktuellen World-Space-Position.
|
|
||||||
* Muss aufgerufen werden NACHDEM die neue Animation gesetzt wurde,
|
|
||||||
* aber BEVOR der kfOffset die Armature verschoben hat.
|
|
||||||
*/
|
|
||||||
public void lockFeetAtCurrentWorldPos() {
|
|
||||||
Spatial sp = getSpatial();
|
|
||||||
if (sp == null || leftFoot == null || rightFoot == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
leftWorldTarget = sp.localToWorld(
|
|
||||||
leftFoot.getModelTransform().getTranslation().clone(), new Vector3f());
|
|
||||||
rightWorldTarget = sp.localToWorld(
|
|
||||||
rightFoot.getModelTransform().getTranslation().clone(), new Vector3f());
|
|
||||||
debugFramesLeft = 10;
|
|
||||||
log.info("[FootIK] LockWorld: L={} R={}", fv(leftWorldTarget), fv(rightWorldTarget));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void releaseFeet() {
|
|
||||||
leftWorldTarget = null;
|
|
||||||
rightWorldTarget = null;
|
|
||||||
debugFramesLeft = 0;
|
|
||||||
log.info("[FootIK] Freigegeben");
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isActive() {
|
|
||||||
return leftWorldTarget != null || rightWorldTarget != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Control-Update ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void controlUpdate(float tpf) {
|
|
||||||
if (leftWorldTarget == null && rightWorldTarget == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Spatial sp = getSpatial();
|
|
||||||
if (sp == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean dbg = debugFramesLeft > 0;
|
|
||||||
if (dbg) {
|
|
||||||
debugFramesLeft--;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (leftWorldTarget != null && leftThigh != null && leftCalf != null && leftFoot != null) {
|
|
||||||
Vector3f modelTarget = sp.worldToLocal(leftWorldTarget, new Vector3f());
|
|
||||||
solveLeg(leftThigh, leftCalf, leftFoot, modelTarget, dbg, "L");
|
|
||||||
}
|
|
||||||
if (rightWorldTarget != null && rightThigh != null && rightCalf != null && rightFoot != null) {
|
|
||||||
Vector3f modelTarget = sp.worldToLocal(rightWorldTarget, new Vector3f());
|
|
||||||
solveLeg(rightThigh, rightCalf, rightFoot, modelTarget, dbg, "R");
|
|
||||||
}
|
|
||||||
|
|
||||||
armature.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void controlRender(RenderManager rm, ViewPort vp) {}
|
|
||||||
|
|
||||||
// ── 2-Bone-IK-Solver ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private void solveLeg(Joint thigh, Joint calf, Joint foot,
|
|
||||||
Vector3f target, boolean dbg, String side) {
|
|
||||||
Vector3f A = thigh.getModelTransform().getTranslation().clone();
|
|
||||||
Vector3f B = calf.getModelTransform().getTranslation().clone();
|
|
||||||
Vector3f C = foot.getModelTransform().getTranslation().clone();
|
|
||||||
|
|
||||||
float L1 = A.distance(B);
|
|
||||||
float L2 = B.distance(C);
|
|
||||||
if (L1 < 0.0001f || L2 < 0.0001f) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
float footErr = C.distance(target);
|
|
||||||
if (dbg) {
|
|
||||||
log.info("[FootIK][{}] A={} B={} C={} T={} err={} L1={} L2={}",
|
|
||||||
side, fv(A), fv(B), fv(C), fv(target),
|
|
||||||
String.format("%.4f", footErr),
|
|
||||||
String.format("%.3f", L1), String.format("%.3f", L2));
|
|
||||||
}
|
|
||||||
if (footErr < MIN_CORRECTION_DIST) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
float d = A.distance(target);
|
|
||||||
d = FastMath.clamp(d, Math.abs(L1 - L2) + 0.0001f, L1 + L2 - 0.0001f);
|
|
||||||
|
|
||||||
float cosA = (L1 * L1 + d * d - L2 * L2) / (2f * L1 * d);
|
|
||||||
float angleA = FastMath.acos(FastMath.clamp(cosA, -1f, 1f));
|
|
||||||
|
|
||||||
Vector3f dirAT = target.subtract(A).normalizeLocal();
|
|
||||||
Vector3f kneeDir = B.subtract(A).normalizeLocal();
|
|
||||||
float proj = dirAT.dot(kneeDir);
|
|
||||||
Vector3f perp = kneeDir.subtract(dirAT.mult(proj));
|
|
||||||
if (perp.lengthSquared() < 0.0001f) {
|
|
||||||
perp = findPerp(dirAT);
|
|
||||||
}
|
|
||||||
perp.normalizeLocal();
|
|
||||||
|
|
||||||
Vector3f B_new = A.add(
|
|
||||||
dirAT.mult(L1 * FastMath.cos(angleA)).addLocal(
|
|
||||||
perp.mult(L1 * FastMath.sin(angleA)))
|
|
||||||
);
|
|
||||||
|
|
||||||
// Oberschenkel rotieren
|
|
||||||
Vector3f curThighDir = B.subtract(A).normalizeLocal();
|
|
||||||
Vector3f dstThighDir = B_new.subtract(A).normalizeLocal();
|
|
||||||
Quaternion thighArc = rotArc(curThighDir, dstThighDir);
|
|
||||||
Quaternion thighModelRot = thigh.getModelTransform().getRotation().clone();
|
|
||||||
Quaternion newThighModelRot = thighArc.mult(thighModelRot);
|
|
||||||
Joint thighParent = thigh.getParent();
|
|
||||||
Quaternion parentRot = thighParent != null
|
|
||||||
? thighParent.getModelTransform().getRotation() : new Quaternion();
|
|
||||||
thigh.setLocalRotation(parentRot.inverse().mult(newThighModelRot));
|
|
||||||
|
|
||||||
// Unterschenkel rotieren — Calf-Richtung NACH der Thigh-Rotation verwenden
|
|
||||||
Vector3f curCalfDirOrig = C.subtract(B).normalizeLocal();
|
|
||||||
Vector3f curCalfDirRotated = thighArc.mult(curCalfDirOrig);
|
|
||||||
Vector3f dstCalfDir = target.subtract(B_new).normalizeLocal();
|
|
||||||
Quaternion calfArc = rotArc(curCalfDirRotated, dstCalfDir);
|
|
||||||
Quaternion oldCalfLocalRot = calf.getLocalRotation().clone();
|
|
||||||
Quaternion actualCalfModel = newThighModelRot.mult(oldCalfLocalRot);
|
|
||||||
Quaternion newCalfModelRot = calfArc.mult(actualCalfModel);
|
|
||||||
calf.setLocalRotation(newThighModelRot.inverse().mult(newCalfModelRot));
|
|
||||||
|
|
||||||
if (dbg) {
|
|
||||||
float thighAngle = thighArc.toAngleAxis(new Vector3f()) * FastMath.RAD_TO_DEG;
|
|
||||||
float calfAngle = calfArc.toAngleAxis(new Vector3f()) * FastMath.RAD_TO_DEG;
|
|
||||||
log.info("[FootIK][{}] B_new={} thigh={}° calf={}°",
|
|
||||||
side, fv(B_new),
|
|
||||||
String.format("%.1f", thighAngle),
|
|
||||||
String.format("%.1f", calfAngle));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Hilfsmethoden ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
private static Quaternion rotArc(Vector3f from, Vector3f to) {
|
|
||||||
float dot = FastMath.clamp(from.dot(to), -1f, 1f);
|
|
||||||
if (dot > 0.9999f) {
|
|
||||||
return new Quaternion();
|
|
||||||
}
|
|
||||||
if (dot < -0.9999f) {
|
|
||||||
return new Quaternion().fromAngleAxis(FastMath.PI, findPerp(from));
|
|
||||||
}
|
|
||||||
Vector3f axis = from.cross(to).normalizeLocal();
|
|
||||||
Quaternion q = new Quaternion();
|
|
||||||
q.fromAngleAxis(FastMath.acos(dot), axis);
|
|
||||||
return q;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Vector3f findPerp(Vector3f v) {
|
|
||||||
Vector3f p = v.cross(Vector3f.UNIT_X);
|
|
||||||
if (p.lengthSquared() < 0.0001f) {
|
|
||||||
p = v.cross(Vector3f.UNIT_Z);
|
|
||||||
}
|
|
||||||
return p.normalizeLocal();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Joint findJoint(Armature arm, String... names) {
|
|
||||||
for (String n : names) {
|
|
||||||
Joint j = arm.getJoint(n);
|
|
||||||
if (j != null) {
|
|
||||||
return j;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String fv(Vector3f v) {
|
|
||||||
return String.format("(%.3f,%.3f,%.3f)", v.x, v.y, v.z);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@ import com.jme3.math.Quaternion;
|
|||||||
import com.jme3.math.Vector3f;
|
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.AnimKeyframe;
|
import de.blight.game.animation.AnimOffset;
|
||||||
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;
|
||||||
@@ -23,7 +23,6 @@ import org.slf4j.LoggerFactory;
|
|||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class PlayerInputControl {
|
public class PlayerInputControl {
|
||||||
@@ -58,20 +57,21 @@ public class PlayerInputControl {
|
|||||||
|
|
||||||
private com.jme3.anim.SkinningControl skinningControl = null;
|
private com.jme3.anim.SkinningControl skinningControl = null;
|
||||||
|
|
||||||
// ── Motion-Keyframe-Offsets ───────────────────────────────────────────────
|
// ── Anim-Offsets ─────────────────────────────────────────────────────────
|
||||||
private Map<String, List<AnimKeyframe>> motionKeyframes = new LinkedHashMap<>();
|
private Map<String, AnimOffset> animOffsets = new LinkedHashMap<>();
|
||||||
/** Basis-Local-Translation des Visual-Nodes; wird beim Laden des AnimSet einmalig gespeichert. */
|
/** Basis-Local-Translation des Visual-Nodes; wird beim Laden des AnimSet einmalig gespeichert. */
|
||||||
private Vector3f visualBaseTranslation = new Vector3f();
|
private Vector3f visualBaseTranslation = new Vector3f();
|
||||||
private final Vector3f kfOffsetCurrent = new Vector3f();
|
private final Vector3f animOffsetCurrent = new Vector3f();
|
||||||
private final Vector3f kfOffsetTarget = new Vector3f();
|
private final Vector3f animOffsetTarget = new Vector3f();
|
||||||
// Lineare Geschwindigkeit des KF-Offsets in m/s; 0 = kein aktiver Lerp
|
private float animOffsetSpeed = 0f;
|
||||||
private float kfOffsetSpeed = 0f;
|
|
||||||
|
|
||||||
// ── Navigation ────────────────────────────────────────────────────────────
|
// ── Navigation ────────────────────────────────────────────────────────────
|
||||||
private CharacterNavigator navigator = null;
|
private CharacterNavigator navigator = null;
|
||||||
private de.blight.game.navigation.PathFinder navPathFinder = null;
|
private de.blight.game.navigation.PathFinder navPathFinder = null;
|
||||||
private TerrainChunkState navTerrain = null;
|
private TerrainChunkState navTerrain = null;
|
||||||
private int jumpFrames = 0;
|
private int jumpFrames = 0;
|
||||||
|
private int groundGraceFrames = 0;
|
||||||
|
private double nextTransitionLength = 0.0;
|
||||||
private boolean pickupActive = false;
|
private boolean pickupActive = false;
|
||||||
private float pickupRemaining = 0f;
|
private float pickupRemaining = 0f;
|
||||||
|
|
||||||
@@ -146,11 +146,11 @@ public class PlayerInputControl {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
AnimSet as = AnimSet.load(assetRoot.resolve("animations/sets"), animSetName);
|
AnimSet as = AnimSet.load(assetRoot.resolve("animations/sets"), animSetName);
|
||||||
motionKeyframes = as.getMotionKeyframes();
|
animOffsets = as.getAnimOffsets();
|
||||||
log.info("[AnimCtx] {} Motion-KF-Einträge geladen.", motionKeyframes.size());
|
log.info("[AnimCtx] {} Anim-Offset-Einträge geladen.", animOffsets.size());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("[AnimCtx] AnimSet-KF nicht ladbar: {}", e.getMessage());
|
log.warn("[AnimCtx] AnimSet-KF nicht ladbar: {}", e.getMessage());
|
||||||
motionKeyframes = new LinkedHashMap<>();
|
animOffsets = new LinkedHashMap<>();
|
||||||
}
|
}
|
||||||
// Navigator (neu) aufbauen sobald alle Abhängigkeiten bereit sind
|
// Navigator (neu) aufbauen sobald alle Abhängigkeiten bereit sind
|
||||||
if (navPathFinder != null && navTerrain != null && physicsChar != null && visual != null) {
|
if (navPathFinder != null && navTerrain != null && physicsChar != null && visual != null) {
|
||||||
@@ -219,7 +219,7 @@ public class PlayerInputControl {
|
|||||||
duration = resolveClipLength(action, 1.5f);
|
duration = resolveClipLength(action, 1.5f);
|
||||||
}
|
}
|
||||||
blockingAnimActive = true;
|
blockingAnimActive = true;
|
||||||
blockingAnimRemaining = duration + (1f / 60f);
|
blockingAnimRemaining = duration;
|
||||||
blockingAnimTotal = duration;
|
blockingAnimTotal = duration;
|
||||||
blockingAnimCallback = onComplete;
|
blockingAnimCallback = onComplete;
|
||||||
autopilotDir = null;
|
autopilotDir = null;
|
||||||
@@ -310,7 +310,7 @@ public class PlayerInputControl {
|
|||||||
forward = backward = left = right = false;
|
forward = backward = left = right = false;
|
||||||
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
|
||||||
autopilotDir = null;
|
autopilotDir = null;
|
||||||
clearMotionKfOffset();
|
clearAnimOffset();
|
||||||
if (arriveRadius > 0f) {
|
if (arriveRadius > 0f) {
|
||||||
navigator.navigateTo(target, speed, arriveRadius, onArrival, onFailed);
|
navigator.navigateTo(target, speed, arriveRadius, onArrival, onFailed);
|
||||||
} else {
|
} else {
|
||||||
@@ -350,17 +350,17 @@ public class PlayerInputControl {
|
|||||||
public void update(float tpf) {
|
public void update(float tpf) {
|
||||||
if (physicsChar == null) return;
|
if (physicsChar == null) return;
|
||||||
|
|
||||||
if (visual != null && kfOffsetSpeed > 0f) {
|
if (visual != null && animOffsetSpeed > 0f) {
|
||||||
Vector3f delta = kfOffsetTarget.subtract(kfOffsetCurrent);
|
Vector3f delta = animOffsetTarget.subtract(animOffsetCurrent);
|
||||||
float dist = delta.length();
|
float dist = delta.length();
|
||||||
float step = kfOffsetSpeed * tpf;
|
float step = animOffsetSpeed * tpf;
|
||||||
if (dist <= step) {
|
if (dist <= step) {
|
||||||
kfOffsetCurrent.set(kfOffsetTarget);
|
animOffsetCurrent.set(animOffsetTarget);
|
||||||
kfOffsetSpeed = 0f;
|
animOffsetSpeed = 0f;
|
||||||
} else {
|
} else {
|
||||||
kfOffsetCurrent.addLocal(delta.normalizeLocal().multLocal(step));
|
animOffsetCurrent.addLocal(delta.normalizeLocal().multLocal(step));
|
||||||
}
|
}
|
||||||
visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(kfOffsetCurrent));
|
visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(animOffsetCurrent));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (paused) {
|
if (paused) {
|
||||||
@@ -485,9 +485,10 @@ public class PlayerInputControl {
|
|||||||
|
|
||||||
// Animations-Auswahl
|
// Animations-Auswahl
|
||||||
if (jumpFrames > 0) jumpFrames--;
|
if (jumpFrames > 0) jumpFrames--;
|
||||||
|
if (groundGraceFrames > 0) groundGraceFrames--;
|
||||||
|
|
||||||
AnimationAction target;
|
AnimationAction target;
|
||||||
if (jumpFrames > 0 || !physicsChar.onGround()) {
|
if (jumpFrames > 0 || (!physicsChar.onGround() && groundGraceFrames <= 0)) {
|
||||||
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
|
target = moving ? AnimationAction.RUNNING_JUMP : AnimationAction.JUMP;
|
||||||
} else if (moving) {
|
} else if (moving) {
|
||||||
target = walk ? AnimationAction.WALK
|
target = walk ? AnimationAction.WALK
|
||||||
@@ -540,36 +541,66 @@ public class PlayerInputControl {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyMotionKfOffset(String clip) {
|
private void applyAnimOffset(String clip) {
|
||||||
if (visual == null) return;
|
if (visual == null) return;
|
||||||
List<AnimKeyframe> kfs = (clip != null) ? motionKeyframes.get(clip) : null;
|
AnimOffset off = (clip != null) ? animOffsets.get(clip) : null;
|
||||||
if (kfs == null || kfs.isEmpty()) { clearMotionKfOffset(); return; }
|
if (off == null) { clearAnimOffset(); return; }
|
||||||
AnimKeyframe kf = kfs.get(0);
|
|
||||||
Quaternion facing = visual.getLocalRotation().clone();
|
Quaternion facing = visual.getLocalRotation().clone();
|
||||||
Vector3f worldOffset = facing.mult(new Vector3f(kf.tx, kf.ty, kf.tz));
|
Vector3f worldOffset = facing.mult(new Vector3f(off.tx, off.ty, off.tz));
|
||||||
kfOffsetTarget.set(worldOffset);
|
animOffsetTarget.set(worldOffset);
|
||||||
if (kfOffsetCurrent.distanceSquared(worldOffset) > 1e-4f) {
|
if (animOffsetCurrent.distanceSquared(worldOffset) > 1e-4f) {
|
||||||
kfOffsetSpeed = 5.0f;
|
animOffsetSpeed = 5.0f;
|
||||||
}
|
}
|
||||||
log.info("[KF] Clip '{}' → Offset-Ziel ({},{},{})", clip, worldOffset.x, worldOffset.y, worldOffset.z);
|
log.info("[AnimOffset] Clip '{}' → Ziel ({},{},{})", clip, worldOffset.x, worldOffset.y, worldOffset.z);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clearKfOffset() {
|
public void clearAnimOffset() {
|
||||||
log.info("[KF] clearKfOffset() → Lerp zu 0, current=({},{},{})", kfOffsetCurrent.x, kfOffsetCurrent.y, kfOffsetCurrent.z);
|
log.info("[AnimOffset] clearAnimOffset() → Lerp zu 0, current=({},{},{})", animOffsetCurrent.x, animOffsetCurrent.y, animOffsetCurrent.z);
|
||||||
kfOffsetTarget.set(0, 0, 0);
|
animOffsetTarget.set(0, 0, 0);
|
||||||
kfOffsetSpeed = 5.0f;
|
animOffsetSpeed = 5.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearMotionKfOffset() {
|
/**
|
||||||
clearKfOffset();
|
* Setzt den Visual-Versatz sofort (kein Lerp), ohne den Physik-Körper zu bewegen.
|
||||||
|
* tx/ty/tz in charakter-lokalem Raum (tz = vorwärts/rückwärts in Blickrichtung).
|
||||||
|
*/
|
||||||
|
public void setAnimOffsetInstant(float tx, float ty, float tz) {
|
||||||
|
if (visual == null) return;
|
||||||
|
Quaternion facing = visual.getLocalRotation().clone();
|
||||||
|
Vector3f worldOffset = facing.mult(new Vector3f(tx, ty, tz));
|
||||||
|
animOffsetTarget.set(worldOffset);
|
||||||
|
animOffsetCurrent.set(worldOffset);
|
||||||
|
animOffsetSpeed = 0f;
|
||||||
|
visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(animOffsetCurrent));
|
||||||
|
log.info("[AnimOffset] Instant: ({},{},{}) → world ({},{},{})", tx, ty, tz, worldOffset.x, worldOffset.y, worldOffset.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Setzt den Visual-Versatz sofort auf 0 zurück (kein Lerp). */
|
||||||
|
public void clearAnimOffsetInstant() {
|
||||||
|
animOffsetTarget.set(0, 0, 0);
|
||||||
|
animOffsetCurrent.set(0, 0, 0);
|
||||||
|
animOffsetSpeed = 0f;
|
||||||
|
if (visual != null) {
|
||||||
|
visual.setLocalTranslation(visualBaseTranslation.clone());
|
||||||
|
}
|
||||||
|
log.info("[AnimOffset] Instant-Clear");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Verhindert für {@code frames} Frames, dass die JUMP-Animation durch kurzes onGround=false nach Teleport ausgelöst wird. */
|
||||||
|
public void setGroundGrace(int frames) {
|
||||||
|
groundGraceFrames = frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Setzt eine einmalige Überblend-Zeit für den nächsten Animations-Wechsel (sanfter Skelett-Übergang). */
|
||||||
|
public void setNextAnimTransition(double seconds) {
|
||||||
|
nextTransitionLength = seconds;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Gibt die konfigurierte Bank-Approach-Distanz (|sitting.tz|) zurück; 0.25m als Fallback. */
|
/** Gibt die konfigurierte Bank-Approach-Distanz (|sitting.tz|) zurück; 0.25m als Fallback. */
|
||||||
public float getBenchApproachDist() {
|
public float getBenchApproachDist() {
|
||||||
List<AnimKeyframe> kfs = motionKeyframes.get("sitting");
|
AnimOffset off = animOffsets.get("sitting");
|
||||||
if (kfs == null || kfs.isEmpty()) return 0.25f;
|
if (off == null) return 0.25f;
|
||||||
float tz = kfs.get(0).tz;
|
return Math.abs(off.tz) > 0.01f ? Math.abs(off.tz) : 0.25f;
|
||||||
return Math.abs(tz) > 0.01f ? Math.abs(tz) : 0.25f;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean tryPlay(String clip) {
|
private boolean tryPlay(String clip) {
|
||||||
@@ -577,6 +608,16 @@ 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)
|
||||||
|
double transLen = nextTransitionLength;
|
||||||
|
nextTransitionLength = 0.0;
|
||||||
|
if (transLen > 0.0) {
|
||||||
|
com.jme3.anim.tween.action.Action nextAction = animComposer.action(clip);
|
||||||
|
if (nextAction instanceof com.jme3.anim.tween.action.BlendableAction) {
|
||||||
|
((com.jme3.anim.tween.action.BlendableAction) nextAction).setTransitionLength(transLen);
|
||||||
|
log.info("[Anim] Transition {} → '{}' über {:.2f}s", runningClip, clip, transLen);
|
||||||
|
}
|
||||||
|
}
|
||||||
// Erst Action setzen, DANN SkinningControl aktivieren –
|
// Erst Action setzen, DANN SkinningControl aktivieren –
|
||||||
// vermeidet 1 Frame in Bind-Pose × Armature-Rx90° = liegender Charakter.
|
// vermeidet 1 Frame in Bind-Pose × Armature-Rx90° = liegender Charakter.
|
||||||
com.jme3.anim.tween.action.Action action = animComposer.setCurrentAction(clip);
|
com.jme3.anim.tween.action.Action action = animComposer.setCurrentAction(clip);
|
||||||
@@ -589,7 +630,7 @@ public class PlayerInputControl {
|
|||||||
log.info("[Anim] SkinningControl aktiviert nach Action '{}'", clip);
|
log.info("[Anim] SkinningControl aktiviert nach Action '{}'", clip);
|
||||||
}
|
}
|
||||||
runningClip = clip;
|
runningClip = clip;
|
||||||
applyMotionKfOffset(clip);
|
applyAnimOffset(clip);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.jme3.input.controls.ActionListener;
|
|||||||
import com.jme3.input.controls.KeyTrigger;
|
import com.jme3.input.controls.KeyTrigger;
|
||||||
import com.jme3.input.controls.MouseButtonTrigger;
|
import com.jme3.input.controls.MouseButtonTrigger;
|
||||||
import com.jme3.math.Vector3f;
|
import com.jme3.math.Vector3f;
|
||||||
|
import com.jme3.scene.Spatial;
|
||||||
import de.blight.common.PlacedModel;
|
import de.blight.common.PlacedModel;
|
||||||
import de.blight.common.PlacedModelIO;
|
import de.blight.common.PlacedModelIO;
|
||||||
import de.blight.common.model.Bed;
|
import de.blight.common.model.Bed;
|
||||||
@@ -53,17 +54,20 @@ public class WorldInteractableState extends BaseAppState {
|
|||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(WorldInteractableState.class);
|
private static final Logger log = LoggerFactory.getLogger(WorldInteractableState.class);
|
||||||
|
|
||||||
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
|
||||||
private static final float BENCH_REENABLE_TIMEOUT = 8f;
|
private static final float BENCH_REENABLE_TIMEOUT = 8f;
|
||||||
private String benchPendingId = null;
|
private String benchPendingId = null;
|
||||||
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 ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -264,6 +268,9 @@ 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));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,8 +296,20 @@ public class WorldInteractableState extends BaseAppState {
|
|||||||
AnimationAction idleAction = isBench(entry) ? AnimationAction.SITTING : AnimationAction.LYING;
|
AnimationAction idleAction = isBench(entry) ? AnimationAction.SITTING : AnimationAction.LYING;
|
||||||
|
|
||||||
playerInput.requestAnimation(downAction, 0f, () -> {
|
playerInput.requestAnimation(downAction, 0f, () -> {
|
||||||
if (!isBench(entry)) snapToSitPos(entry); // Bank: Physik bleibt am Approach-Punkt
|
if (isBench(entry) && currentBenchSitDir != null) {
|
||||||
|
Vector3f cur = physicsChar.getPhysicsLocation();
|
||||||
|
Vector3f delta = currentBenchSitDir.mult(-BENCH_SIT_MOVE_DIST);
|
||||||
|
physicsChar.setPhysicsLocation(cur.add(delta));
|
||||||
|
// Sofortiges Spatial-Update verhindert 1-Frame-Kamerazuckeln
|
||||||
|
Spatial phySpatial = physicsChar.getSpatial();
|
||||||
|
if (phySpatial != null) {
|
||||||
|
phySpatial.setLocalTranslation(phySpatial.getLocalTranslation().add(delta));
|
||||||
|
}
|
||||||
|
log.info("[WorldInteractable] Charakter 50cm zur Bank verschoben.");
|
||||||
|
}
|
||||||
|
if (!isBench(entry)) snapToSitPos(entry);
|
||||||
playerInput.lockInPlace();
|
playerInput.lockInPlace();
|
||||||
|
if (isBench(entry)) playerInput.setNextAnimTransition(0.2);
|
||||||
playerInput.playLockedAnimation(idleAction);
|
playerInput.playLockedAnimation(idleAction);
|
||||||
phase = Phase.RESTING;
|
phase = Phase.RESTING;
|
||||||
log.info("[WorldInteractable] Ruhezustand: {}", entry.type());
|
log.info("[WorldInteractable] Ruhezustand: {}", entry.type());
|
||||||
@@ -328,7 +347,21 @@ public class WorldInteractableState extends BaseAppState {
|
|||||||
|
|
||||||
playerInput.requestAnimation(upAction, 0f, () -> {
|
playerInput.requestAnimation(upAction, 0f, () -> {
|
||||||
if (isBench(entry)) {
|
if (isBench(entry)) {
|
||||||
playerInput.clearKfOffset();
|
// Nach stand_up_bench: Charakter 50cm von Bank wegbewegen
|
||||||
|
if (currentBenchSitDir != null) {
|
||||||
|
Vector3f cur = physicsChar.getPhysicsLocation();
|
||||||
|
Vector3f delta = currentBenchSitDir.mult(BENCH_SIT_MOVE_DIST);
|
||||||
|
physicsChar.setPhysicsLocation(cur.add(delta));
|
||||||
|
Spatial phySpatial = physicsChar.getSpatial();
|
||||||
|
if (phySpatial != null) {
|
||||||
|
phySpatial.setLocalTranslation(phySpatial.getLocalTranslation().add(delta));
|
||||||
|
}
|
||||||
|
currentBenchSitDir = null;
|
||||||
|
playerInput.setGroundGrace(4);
|
||||||
|
playerInput.setNextAnimTransition(0.2);
|
||||||
|
log.info("[WorldInteractable] Charakter 50cm von Bank wegbewegt.");
|
||||||
|
}
|
||||||
|
playerInput.clearAnimOffset();
|
||||||
benchPendingId = entry.interactableId();
|
benchPendingId = entry.interactableId();
|
||||||
benchPendingX = entry.worldX();
|
benchPendingX = entry.worldX();
|
||||||
benchPendingZ = entry.worldZ();
|
benchPendingZ = entry.worldZ();
|
||||||
|
|||||||
Reference in New Issue
Block a user