Umbau der Animations Editor Geschichte

This commit is contained in:
2026-08-19 17:40:16 +02:00
parent 4ed0023a5c
commit ec0865d09b
45 changed files with 515 additions and 97 deletions

Binary file not shown.

View File

@@ -1,38 +1,24 @@
{ {
"clips": [ "clips": [
"Disappointed",
"Idle", "Idle",
"Jumping",
"Kneeling",
"Kneeling Down",
"Lifting",
"Lying Down",
"Male Crouch Pose",
"Male Laying Pose",
"Male Laying Pose(1)",
"Running", "Running",
"Running Jump",
"Sit To Stand", "Sit To Stand",
"Sitting",
"Sitting Idle", "Sitting Idle",
"Sitting Idle(1)",
"Sitting Talking",
"Sitting Talking(1)",
"Sneaking Forward",
"Sprint", "Sprint",
"Sprinting Forward Roll",
"Stand To Roll",
"Stand To Sit", "Stand To Sit",
"Standing",
"Standing Up",
"Standing Up(1)",
"T-Pose", "T-Pose",
"Talking", "Walking"
"Talking(1)",
"Walking",
"Yelling"
], ],
"actionMap": {}, "actionMap": {
"DEFAULT": "Idle",
"WALK": "Walking",
"RUN": "Running",
"SPRINT": "Sprint",
"SIT_DOWN": "Stand To Sit",
"SITTING": "Sitting Idle",
"SIT_UP": "Sit To Stand",
"IDLE": "Idle"
},
"previewModelPath": "Characters/Models/hero.j3o", "previewModelPath": "Characters/Models/hero.j3o",
"animOffsets": { "animOffsets": {
"sitting": { "sitting": {
@@ -50,7 +36,31 @@
"rx": 0.0, "rx": 0.0,
"ry": 0.0, "ry": 0.0,
"rz": 0.0 "rz": 0.0
},
"Sitting Idle": {
"tx": 0.0,
"ty": 0.0,
"tz": -0.5,
"rx": 0.0,
"ry": 0.0,
"rz": 0.0
},
"Sit To Stand": {
"tx": 0.0,
"ty": 0.0,
"tz": -0.5,
"rx": 0.0,
"ry": 0.0,
"rz": 0.0
} }
}, },
"subClips": {} "subClips": {},
"sequences": {
"Sitzen Loop": [
"SIT_DOWN",
"SITTING",
"SIT_UP",
"IDLE"
]
}
} }

View File

@@ -202,7 +202,6 @@ public class EditorApp extends Application {
private ListView<String> animSetActionListView; private ListView<String> animSetActionListView;
private String animSetPendingPlayClip = null; private String animSetPendingPlayClip = null;
private ComboBox<String> animSetModelCombo; private ComboBox<String> animSetModelCombo;
private boolean animSetDirty = false;
private String animSetCurrentName = null; private String animSetCurrentName = null;
private Path animSetCurrentDir = null; private Path animSetCurrentDir = null;
// Anim-Offset-Editor (innerhalb AnimSet-Editor) // Anim-Offset-Editor (innerhalb AnimSet-Editor)
@@ -215,6 +214,10 @@ public class EditorApp extends Application {
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<>();
// Sequenz-Editor (innerhalb AnimSet-Editor)
private ListView<String> animSetSequenceListView;
private java.util.Map<String, java.util.List<String>>
editableSequences = new java.util.LinkedHashMap<>();
// Character-Editor-Zustand // Character-Editor-Zustand
private de.blight.editor.ui.DialogEditorView dialogEditorView; private de.blight.editor.ui.DialogEditorView dialogEditorView;
@@ -1537,7 +1540,7 @@ public class EditorApp extends Application {
MenuItem vegetationsItem = new MenuItem("Vegetations Generator"); MenuItem vegetationsItem = new MenuItem("Vegetations Generator");
MenuItem ezTreeItem = new MenuItem("Baum Generator (EZ Tree)"); MenuItem ezTreeItem = new MenuItem("Baum Generator (EZ Tree)");
MenuItem tripoItem = new MenuItem("AI Modell-Generator (Tripo3D)"); MenuItem tripoItem = new MenuItem("AI Modell-Generator (Tripo3D)");
MenuItem animPrevItem = new MenuItem("Character-Editor"); MenuItem animPrevItem = new MenuItem("Animationset Editor");
MenuItem objEditorItem = new MenuItem("Model Editor"); MenuItem objEditorItem = new MenuItem("Model Editor");
MenuItem thumbItem = new MenuItem("Thumbnail-Verwaltung"); MenuItem thumbItem = new MenuItem("Thumbnail-Verwaltung");
vegetationsItem.setOnAction(e -> switchToVegetationGenerator()); vegetationsItem.setOnAction(e -> switchToVegetationGenerator());
@@ -5542,7 +5545,14 @@ public class EditorApp extends Application {
if (p == null) continue; if (p == null) continue;
try { try {
Files.deleteIfExists(p); Files.deleteIfExists(p);
if (p.toString().endsWith(".j3o")) deleteJ3oSideFiles(p); if (p.toString().endsWith(".j3o")) {
deleteJ3oSideFiles(p);
String rel = ASSET_ROOT.relativize(p).toString().replace('\\', '/');
if (rel.startsWith("Characters/clips/")) {
String clipName = p.getFileName().toString().replaceFirst("\\.j3o$", "");
removeClipFromAllAnimSets(clipName);
}
}
si.getParent().getChildren().remove(si); si.getParent().getChildren().remove(si);
itemPaths.remove(si); itemPaths.remove(si);
deleted++; deleted++;
@@ -5769,7 +5779,14 @@ public class EditorApp extends Application {
Files.deleteIfExists(p); Files.deleteIfExists(p);
item.getParent().getChildren().remove(item); item.getParent().getChildren().remove(item);
itemPaths.remove(item); itemPaths.remove(item);
if (pStr.endsWith(".j3o")) deleteJ3oSideFiles(p); if (pStr.endsWith(".j3o")) {
deleteJ3oSideFiles(p);
String rel = ASSET_ROOT.relativize(p).toString().replace('\\', '/');
if (rel.startsWith("Characters/clips/")) {
String clipName = p.getFileName().toString().replaceFirst("\\.j3o$", "");
removeClipFromAllAnimSets(clipName);
}
}
setStatus("Gelöscht: " + p.getFileName()); setStatus("Gelöscht: " + p.getFileName());
} catch (IOException ex) { } catch (IOException ex) {
setStatus("Fehler: " + ex.getMessage()); setStatus("Fehler: " + ex.getMessage());
@@ -6244,6 +6261,41 @@ public class EditorApp extends Application {
} catch (IOException ignored) {} } catch (IOException ignored) {}
} }
/**
* Entfernt einen Animations-Clip aus allen AnimSet-Dateien in Characters/sets/.
* Wird aufgerufen wenn eine .j3o-Datei aus Characters/clips/ gelöscht wird.
*/
private void removeClipFromAllAnimSets(String clipName) {
Path setsDir = ASSET_ROOT.resolve("Characters").resolve("sets");
if (!Files.isDirectory(setsDir)) return;
try (var walk = Files.walk(setsDir, 1)) {
walk.filter(p -> p.toString().endsWith(".animset.json")).forEach(setFile -> {
try {
String json = Files.readString(setFile);
com.google.gson.JsonObject root = com.google.gson.JsonParser.parseString(json).getAsJsonObject();
com.google.gson.JsonArray clips = root.has("clips") ? root.getAsJsonArray("clips") : null;
if (clips == null) return;
boolean changed = false;
com.google.gson.JsonArray updated = new com.google.gson.JsonArray();
for (com.google.gson.JsonElement el : clips) {
if (el.isJsonPrimitive() && el.getAsString().equals(clipName)) {
changed = true;
} else {
updated.add(el);
}
}
if (!changed) return;
root.add("clips", updated);
String pretty = new com.google.gson.GsonBuilder().setPrettyPrinting().create().toJson(root);
Files.writeString(setFile, pretty);
log.info("[AssetTree] Clip '{}' aus AnimSet '{}' entfernt", clipName, setFile.getFileName());
} catch (Exception ex) {
log.warn("[AssetTree] Fehler beim Bereinigen von {}: {}", setFile.getFileName(), ex.getMessage());
}
});
} catch (IOException ignored) {}
}
/** /**
* Befüllt den Asset-Baum vollständig aus dem Dateisystem. * Befüllt den Asset-Baum vollständig aus dem Dateisystem.
* Bekannte Verzeichnisse werden den Kategorie-Feldern zugewiesen; * Bekannte Verzeichnisse werden den Kategorie-Feldern zugewiesen;
@@ -10364,7 +10416,7 @@ public class EditorApp extends Application {
// Dialog: vorhandenes AnimSet wählen oder neues anlegen // Dialog: vorhandenes AnimSet wählen oder neues anlegen
javafx.scene.control.Dialog<String> dlg = new javafx.scene.control.Dialog<>(); javafx.scene.control.Dialog<String> dlg = new javafx.scene.control.Dialog<>();
dlg.initOwner(primaryStage); dlg.initOwner(primaryStage);
dlg.setTitle("Character-Editor"); dlg.setTitle("Animationset Editor");
dlg.setHeaderText("AnimSet auswählen oder neu anlegen"); dlg.setHeaderText("AnimSet auswählen oder neu anlegen");
ListView<String> listView = new ListView<>(); ListView<String> listView = new ListView<>();
@@ -10420,7 +10472,6 @@ public class EditorApp extends Application {
animSetCurrentName = setName; animSetCurrentName = setName;
animSetCurrentDir = setDir; animSetCurrentDir = setDir;
animSetDirty = false;
currentTool = "animseteditor"; currentTool = "animseteditor";
topBar.getChildren().set(1, buildAnimSetToolBar(setName)); topBar.getChildren().set(1, buildAnimSetToolBar(setName));
@@ -10501,7 +10552,7 @@ public class EditorApp extends Application {
if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade…"); if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade…");
// Pfad im AnimSet merken und sofort speichern // Pfad im AnimSet merken und sofort speichern
animSet.setPreviewModelPath(path); animSet.setPreviewModelPath(path);
animSetDirty = true; autoSaveAnimSet();
}); });
Button importCharBtn = new Button("Character-Modell importieren (FBX)…"); Button importCharBtn = new Button("Character-Modell importieren (FBX)…");
importCharBtn.setMaxWidth(Double.MAX_VALUE); importCharBtn.setMaxWidth(Double.MAX_VALUE);
@@ -10520,7 +10571,14 @@ public class EditorApp extends Application {
animSetClipListView = new ListView<>(); animSetClipListView = new ListView<>();
animSetClipListView.getItems().addAll(animSet.getClips()); animSetClipListView.getItems().addAll(animSet.getClips());
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());
// Clip-Offsets direkt in SharedInput spiegeln damit AnimPreviewState sie kennt
input.animPreviewClipOffsets.clear();
for (var en : animSet.getAnimOffsets().entrySet()) {
de.blight.game.animation.AnimOffset o = en.getValue();
input.animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz});
}
Path animRootDir = ASSET_ROOT.resolve("Characters"); Path animRootDir = ASSET_ROOT.resolve("Characters");
@@ -10621,7 +10679,7 @@ public class EditorApp extends Application {
for (String clip : selected) { for (String clip : selected) {
if (!animSetClipListView.getItems().contains(clip)) { if (!animSetClipListView.getItems().contains(clip)) {
animSetClipListView.getItems().add(clip); animSetClipListView.getItems().add(clip);
animSetDirty = true; autoSaveAnimSet();
} }
} }
}); });
@@ -10635,18 +10693,14 @@ public class EditorApp extends Application {
animSetActionListView.getItems().removeIf(it -> it.endsWith("" + sel)); animSetActionListView.getItems().removeIf(it -> it.endsWith("" + sel));
editableSubClips.entrySet().removeIf(en -> sel.equals(en.getValue().source)); editableSubClips.entrySet().removeIf(en -> sel.equals(en.getValue().source));
if (animSetPartsListView != null) animSetPartsListView.getItems().clear(); if (animSetPartsListView != null) animSetPartsListView.getItems().clear();
animSetDirty = true; 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);
Button importAnimBtn = new Button("Animation hinzufügen…"); inner.getChildren().addAll(animSetClipListView, clipBtns);
importAnimBtn.setMaxWidth(Double.MAX_VALUE);
importAnimBtn.setOnAction(e -> handleAnimationImport(importAnimBtn.getScene().getWindow()));
inner.getChildren().addAll(animSetClipListView, clipBtns, importAnimBtn);
// ── 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());
@@ -10709,7 +10763,7 @@ public class EditorApp extends Application {
String name = sel.substring(0, sel.indexOf(" [")); String name = sel.substring(0, sel.indexOf(" ["));
editableSubClips.remove(name); editableSubClips.remove(name);
animSetPartsListView.getItems().remove(sel); animSetPartsListView.getItems().remove(sel);
animSetDirty = true; autoSaveAnimSet();
}); });
HBox partsBtns = new HBox(6, addPartBtn, removePartBtn); HBox partsBtns = new HBox(6, addPartBtn, removePartBtn);
@@ -10739,7 +10793,7 @@ public class EditorApp extends Application {
String sel = animSetActionListView.getSelectionModel().getSelectedItem(); String sel = animSetActionListView.getSelectionModel().getSelectedItem();
if (sel != null) { if (sel != null) {
animSetActionListView.getItems().remove(sel); animSetActionListView.getItems().remove(sel);
animSetDirty = true; autoSaveAnimSet();
} }
}); });
@@ -10803,7 +10857,7 @@ public class EditorApp extends Application {
animSetOffsetListView.getSelectionModel().getSelectedItem(); animSetOffsetListView.getSelectionModel().getSelectedItem();
if (sel != null) { if (sel != null) {
animSetOffsetList.remove(sel); animSetOffsetList.remove(sel);
animSetDirty = true; autoSaveAnimSet();
} }
}); });
@@ -10812,6 +10866,64 @@ public class EditorApp extends Application {
HBox.setHgrow(removeKfBtn, Priority.ALWAYS); HBox.setHgrow(removeKfBtn, Priority.ALWAYS);
inner.getChildren().addAll(kfHint, animSetOffsetListView, kfBtns); inner.getChildren().addAll(kfHint, animSetOffsetListView, kfBtns);
// ── Abläufe ───────────────────────────────────────────────────────────
inner.getChildren().addAll(new Separator(), sectionTitle("Abläufe"), new Separator());
Label seqHint = new Label("Aktionen aus der Zuordnung zu Abläufen zusammenstellen und testen.");
seqHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;");
seqHint.setWrapText(true);
animSetSequenceListView = new ListView<>();
animSetSequenceListView.setPrefHeight(120);
animSetSequenceListView.setPlaceholder(new Label("Noch keine Abläufe definiert"));
for (var en : editableSequences.entrySet())
animSetSequenceListView.getItems().add(formatSequenceEntry(en.getKey(), en.getValue()));
Button addSeqBtn = new Button("+ Ablauf…");
Button removeSeqBtn = new Button("- Entfernen");
addSeqBtn.setMaxWidth(Double.MAX_VALUE);
removeSeqBtn.setMaxWidth(Double.MAX_VALUE);
removeSeqBtn.setDisable(true);
animSetSequenceListView.getSelectionModel().selectedItemProperty()
.addListener((obs, ov, nv) -> {
removeSeqBtn.setDisable(nv == null);
addSeqBtn.setText(nv == null ? "+ Ablauf…" : "Bearbeiten…");
});
animSetSequenceListView.setOnMouseClicked(ev -> {
String sel = animSetSequenceListView.getSelectionModel().getSelectedItem();
if (sel == null) return;
if (ev.getClickCount() == 2) {
playSequence(sel);
}
});
addSeqBtn.setOnAction(e -> {
String sel = animSetSequenceListView.getSelectionModel().getSelectedItem();
if (sel == null) {
showSequenceBuilderDialog(null, null);
} else {
String seqName = extractSequenceName(sel);
showSequenceBuilderDialog(seqName, new java.util.ArrayList<>(
editableSequences.getOrDefault(seqName, java.util.List.of())));
}
});
removeSeqBtn.setOnAction(e -> {
String sel = animSetSequenceListView.getSelectionModel().getSelectedItem();
if (sel == null) return;
String seqName = extractSequenceName(sel);
editableSequences.remove(seqName);
animSetSequenceListView.getItems().remove(sel);
autoSaveAnimSet();
});
HBox seqBtns = new HBox(6, addSeqBtn, removeSeqBtn);
HBox.setHgrow(addSeqBtn, Priority.ALWAYS);
HBox.setHgrow(removeSeqBtn, Priority.ALWAYS);
inner.getChildren().addAll(seqHint, animSetSequenceListView, seqBtns);
// ── Vorschau ───────────────────────────────────────────────────────── // ── Vorschau ─────────────────────────────────────────────────────────
inner.getChildren().addAll(new Separator(), sectionTitle("Vorschau"), new Separator()); inner.getChildren().addAll(new Separator(), sectionTitle("Vorschau"), new Separator());
@@ -10851,14 +10963,6 @@ public class EditorApp extends Application {
inner.getChildren().addAll(previewHint, previewBtns, loopCB, inner.getChildren().addAll(previewHint, previewBtns, loopCB,
new Label("Tempo:"), speedSlider, animPreviewStatusLabel); new Label("Tempo:"), speedSlider, animPreviewStatusLabel);
// ── Speichern ─────────────────────────────────────────────────────────
inner.getChildren().add(new Separator());
Button saveBtn = new Button("💾 Speichern");
saveBtn.setMaxWidth(Double.MAX_VALUE);
saveBtn.setStyle("-fx-background-color:#2e7d32; -fx-text-fill:#fff; -fx-font-weight:bold;");
saveBtn.setOnAction(e -> saveCurrentAnimSet(setName, setDir));
inner.getChildren().add(saveBtn);
ScrollPane scroll = new ScrollPane(inner); ScrollPane scroll = new ScrollPane(inner);
scroll.setFitToWidth(true); scroll.setFitToWidth(true);
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
@@ -10929,11 +11033,182 @@ public class EditorApp extends Application {
existing.rz = spRZ.getValue().floatValue(); existing.rz = spRZ.getValue().floatValue();
} }
if (animSetOffsetListView != null) animSetOffsetListView.refresh(); if (animSetOffsetListView != null) animSetOffsetListView.refresh();
animSetDirty = true; autoSaveAnimSet();
}); });
} }
/** Spielt den Clip der gegebenen Aktion in der Vorschau ab, um animPreviewCurrentClipDuration zu setzen. */ /** Spielt den Clip der gegebenen Aktion in der Vorschau ab, um animPreviewCurrentClipDuration zu setzen. */
private String formatSequenceEntry(String name, java.util.List<String> actions) {
return name + " (" + actions.size() + " Aktionen: " + String.join("", actions) + ")";
}
private String extractSequenceName(String entry) {
int idx = entry.indexOf(" (");
return idx >= 0 ? entry.substring(0, idx) : entry;
}
private void playSequence(String listEntry) {
String seqName = extractSequenceName(listEntry);
java.util.List<String> actions = editableSequences.get(seqName);
if (actions == null || actions.isEmpty()) return;
java.util.Map<String, String> actionMap = new java.util.LinkedHashMap<>();
if (animSetActionListView != null) {
for (String it : animSetActionListView.getItems()) {
String[] parts = it.split("", 2);
if (parts.length == 2) actionMap.put(parts[0], parts[1]);
}
}
java.util.List<String> clips = new java.util.ArrayList<>();
for (String action : actions) {
String clip = actionMap.get(action);
if (clip != null) clips.add(clip);
else log.warn("[Sequenz] Keine Clip-Zuordnung für Aktion '{}'", action);
}
if (clips.isEmpty()) {
javafx.scene.control.Alert err = new javafx.scene.control.Alert(
javafx.scene.control.Alert.AlertType.WARNING);
err.initOwner(primaryStage);
err.setHeaderText("Keine Clips für Sequenz");
err.setContentText("Die Aktionen des Ablaufs sind keinen Clips zugeordnet.");
err.showAndWait();
return;
}
// Alle Clips vorab laden (wie previewSelectedClip): animImportQueue lädt sie in den AnimComposer.
// Clip[0] wird über animSetPendingPlayClip gestartet sobald er geladen ist.
// Clips[1..n] sind währenddessen schon in der Sequenz-Queue und werden geladen während Clip[0] läuft.
input.animPreviewSequenceQueue.clear();
input.animPreviewLoop = false;
animSetPendingPlayClip = clips.get(0);
input.animImportQueue.offer(findAnimClipPath(clips.get(0)));
for (int i = 1; i < clips.size(); i++) {
input.animPreviewSequenceQueue.add(clips.get(i));
input.animImportQueue.offer(findAnimClipPath(clips.get(i)));
}
if (animPreviewStatusLabel != null)
animPreviewStatusLabel.setText("Lade Sequenz \"" + seqName + "\"");
}
/** Dialog: linke Liste = verfügbare Aktionen, rechte Liste = Ablauf-Reihenfolge. */
private void showSequenceBuilderDialog(String existingName, java.util.List<String> existingActions) {
// Verfügbare Aktionen aus animSetActionListView auslesen
java.util.List<String> availableActions = new java.util.ArrayList<>();
if (animSetActionListView != null) {
for (String it : animSetActionListView.getItems()) {
String[] parts = it.split("", 2);
if (parts.length == 2) availableActions.add(parts[0]);
}
}
if (availableActions.isEmpty()) {
javafx.scene.control.Alert err = new javafx.scene.control.Alert(
javafx.scene.control.Alert.AlertType.WARNING);
err.initOwner(primaryStage);
err.setHeaderText("Keine Aktionen");
err.setContentText("Zuerst in der Aktions-Zuordnung Aktionen einem Clip zuweisen.");
err.showAndWait();
return;
}
// ── Namensfeld ────────────────────────────────────────────────────────
javafx.scene.control.TextField nameFld = new javafx.scene.control.TextField(
existingName != null ? existingName : "");
nameFld.setPromptText("Name des Ablaufs…");
// ── Linke Liste (Aktionen) ────────────────────────────────────────────
ListView<String> leftList = new ListView<>();
leftList.getItems().addAll(availableActions);
leftList.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE);
leftList.setPrefSize(200, 240);
// ── Rechte Liste (Ablauf) ────────────────────────────────────────────
ListView<String> rightList = new ListView<>();
if (existingActions != null) rightList.getItems().addAll(existingActions);
rightList.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE);
rightList.setPrefSize(200, 240);
// ── Buttons Mitte ────────────────────────────────────────────────────
Button addBtn = new Button("");
Button removeBtn = new Button("×");
addBtn.setMaxWidth(Double.MAX_VALUE);
removeBtn.setMaxWidth(Double.MAX_VALUE);
addBtn.setStyle("-fx-font-size: 14;");
removeBtn.setStyle("-fx-font-size: 14;");
addBtn.setOnAction(ev -> {
java.util.List<String> sel = new java.util.ArrayList<>(
leftList.getSelectionModel().getSelectedItems());
rightList.getItems().addAll(sel);
});
removeBtn.setOnAction(ev -> {
java.util.List<String> sel = new java.util.ArrayList<>(
rightList.getSelectionModel().getSelectedItems());
rightList.getItems().removeAll(sel);
});
VBox midBtns = new VBox(8, addBtn, removeBtn);
midBtns.setAlignment(javafx.geometry.Pos.CENTER);
// ── Layout ────────────────────────────────────────────────────────────
javafx.scene.layout.GridPane grid = new javafx.scene.layout.GridPane();
grid.setHgap(8);
grid.setVgap(6);
grid.add(new Label("Verfügbare Aktionen:"), 0, 0);
grid.add(new Label("Ablauf:"), 2, 0);
grid.add(leftList, 0, 1);
grid.add(midBtns, 1, 1);
grid.add(rightList, 2, 1);
javafx.scene.layout.ColumnConstraints ccFill = new javafx.scene.layout.ColumnConstraints();
ccFill.setHgrow(Priority.ALWAYS);
javafx.scene.layout.ColumnConstraints ccFixed = new javafx.scene.layout.ColumnConstraints(48);
grid.getColumnConstraints().addAll(ccFill, ccFixed, ccFill);
VBox content = new VBox(8, new Label("Ablauf-Name:"), nameFld, grid);
content.setPadding(new Insets(4));
javafx.scene.control.ButtonType ok = new javafx.scene.control.ButtonType(
"Speichern", javafx.scene.control.ButtonBar.ButtonData.OK_DONE);
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> dlg =
new javafx.scene.control.Dialog<>();
dlg.initOwner(primaryStage);
dlg.setTitle(existingName == null ? "Ablauf erstellen" : "Ablauf bearbeiten");
dlg.getDialogPane().setContent(content);
dlg.getDialogPane().setPrefWidth(520);
dlg.getDialogPane().getButtonTypes().addAll(ok, javafx.scene.control.ButtonType.CANCEL);
dlg.setResultConverter(bt -> bt);
dlg.showAndWait().ifPresent(bt -> {
if (bt != ok) return;
String name = nameFld.getText().trim();
if (name.isEmpty()) return;
java.util.List<String> actions = new java.util.ArrayList<>(rightList.getItems());
// Alten Eintrag ggf. umbenennen
if (existingName != null && !existingName.equals(name)) {
editableSequences.remove(existingName);
animSetSequenceListView.getItems().removeIf(it -> extractSequenceName(it).equals(existingName));
}
editableSequences.put(name, actions);
// ListView-Eintrag aktualisieren oder neu anlegen
String newEntry = formatSequenceEntry(name, actions);
boolean replaced = false;
if (existingName != null) {
for (int i = 0; i < animSetSequenceListView.getItems().size(); i++) {
if (extractSequenceName(animSetSequenceListView.getItems().get(i)).equals(name)) {
animSetSequenceListView.getItems().set(i, newEntry);
replaced = true;
break;
}
}
}
if (!replaced) animSetSequenceListView.getItems().add(newEntry);
autoSaveAnimSet();
});
}
private void autoPreviewClipForAction(de.blight.game.animation.AnimationAction action) { private void autoPreviewClipForAction(de.blight.game.animation.AnimationAction action) {
if (animSetActionListView == null || action == null) return; if (animSetActionListView == null || action == null) return;
String prefix = action.name() + ""; String prefix = action.name() + "";
@@ -11126,10 +11401,15 @@ public class EditorApp extends Application {
animSetPartsListView.getItems().removeIf(s -> s.startsWith(name + " [")); animSetPartsListView.getItems().removeIf(s -> s.startsWith(name + " ["));
animSetPartsListView.getItems().add(formatPartEntry(name, def)); animSetPartsListView.getItems().add(formatPartEntry(name, def));
} }
animSetDirty = true; autoSaveAnimSet();
}); });
} }
private void autoSaveAnimSet() {
if (animSetCurrentName != null && animSetCurrentDir != null)
saveCurrentAnimSet(animSetCurrentName, animSetCurrentDir);
}
private void saveCurrentAnimSet(String setName, Path setDir) { private void saveCurrentAnimSet(String setName, Path setDir) {
if (animSetClipListView == null) { if (animSetClipListView == null) {
return; return;
@@ -11166,6 +11446,13 @@ public class EditorApp extends Application {
} }
animSet.setAnimOffsets(offsetFinal); animSet.setAnimOffsets(offsetFinal);
animSet.setSubClips(new java.util.LinkedHashMap<>(editableSubClips)); animSet.setSubClips(new java.util.LinkedHashMap<>(editableSubClips));
animSet.setSequences(new java.util.LinkedHashMap<>(editableSequences));
// Clip-Offset-Map für AnimPreviewState synchronisieren
input.animPreviewClipOffsets.clear();
for (var en : offsetFinal.entrySet()) {
de.blight.game.animation.AnimOffset o = en.getValue();
input.animPreviewClipOffsets.put(en.getKey(), new float[]{o.tx, o.ty, o.tz});
}
// 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());
@@ -11180,16 +11467,7 @@ public class EditorApp extends Application {
} }
private void createNewAnimSet() { private void createNewAnimSet() {
if (animSetDirty) { doCreateNewAnimSet();
Alert warn = new Alert(Alert.AlertType.CONFIRMATION,
"Das aktuelle AnimSet hat ungespeicherte Änderungen.\nTrotzdem ein neues Set anlegen?",
ButtonType.OK, ButtonType.CANCEL);
warn.initOwner(primaryStage);
warn.setHeaderText("Ungespeicherte Änderungen");
warn.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> doCreateNewAnimSet());
} else {
doCreateNewAnimSet();
}
} }
private void doCreateNewAnimSet() { private void doCreateNewAnimSet() {
@@ -11746,7 +12024,7 @@ public class EditorApp extends Application {
private ToolBar buildCharacterEditorToolBar() { private ToolBar buildCharacterEditorToolBar() {
Button backBtn = new Button("← Welteneditor"); Button backBtn = new Button("← Welteneditor");
backBtn.setOnAction(e -> switchToWorldEditor()); backBtn.setOnAction(e -> switchToWorldEditor());
Label label = new Label("Character Editor"); Label label = new Label("Animationset Editor");
label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;"); label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
ToolBar tb = new ToolBar(); ToolBar tb = new ToolBar();
tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label); tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label);
@@ -11804,9 +12082,9 @@ public class EditorApp extends Application {
charModelCombo = new ComboBox<>(); charModelCombo = new ComboBox<>();
charModelCombo.setMaxWidth(Double.MAX_VALUE); charModelCombo.setMaxWidth(Double.MAX_VALUE);
charModelCombo.setPromptText("Modell wählen…"); charModelCombo.setPromptText("Modell wählen…");
Path modelBase = ASSET_ROOT.resolve("Models"); Path charModelBase = ASSET_ROOT.resolve("Characters").resolve("Models");
if (java.nio.file.Files.isDirectory(modelBase)) { if (java.nio.file.Files.isDirectory(charModelBase)) {
try (var walk = java.nio.file.Files.walk(modelBase)) { try (var walk = java.nio.file.Files.walk(charModelBase)) {
walk.filter(p -> p.toString().endsWith(".j3o")) walk.filter(p -> p.toString().endsWith(".j3o"))
.map(p -> ASSET_ROOT.relativize(p).toString().replace('\\', '/')) .map(p -> ASSET_ROOT.relativize(p).toString().replace('\\', '/'))
.sorted().forEach(charModelCombo.getItems()::add); .sorted().forEach(charModelCombo.getItems()::add);

View File

@@ -617,6 +617,8 @@ public class SharedInput {
public volatile String animPreviewLoadPath = null; public volatile String animPreviewLoadPath = null;
/** JavaFX → JME3: Clip abspielen; "" = Stop. null = kein Auftrag. */ /** JavaFX → JME3: Clip abspielen; "" = Stop. null = kein Auftrag. */
public volatile String animPreviewPlayClip = null; public volatile String animPreviewPlayClip = null;
/** JavaFX → JME3: Sequenz-Warteschlange Clip-Namen werden nacheinander abgespielt (kein Loop). */
public final ConcurrentLinkedQueue<String> animPreviewSequenceQueue = new ConcurrentLinkedQueue<>();
/** JavaFX → JME3: Animation-j3o-Pfad zum Retargeting + Hinzufügen. null = kein Auftrag. */ /** JavaFX → JME3: Animation-j3o-Pfad zum Retargeting + Hinzufügen. null = kein Auftrag. */
public final ConcurrentLinkedQueue<String> animImportQueue = new ConcurrentLinkedQueue<>(); public final ConcurrentLinkedQueue<String> animImportQueue = new ConcurrentLinkedQueue<>();
/** JavaFX → JME3: Absoluter FBX-Pfad für Character-Modell-Import (nach Models/Chars/). */ /** JavaFX → JME3: Absoluter FBX-Pfad für Character-Modell-Import (nach Models/Chars/). */
@@ -651,6 +653,9 @@ public class SharedInput {
public volatile float animPreviewOffsetTy = 0f; public volatile float animPreviewOffsetTy = 0f;
public volatile float animPreviewOffsetTz = 0f; public volatile float animPreviewOffsetTz = 0f;
public volatile boolean animPreviewOffsetActive = false; public volatile boolean animPreviewOffsetActive = false;
/** JavaFX → JME3: Clip-Name → [tx, ty, tz] für alle Clips mit gesetztem Offset. */
public final java.util.concurrent.ConcurrentHashMap<String, float[]> animPreviewClipOffsets =
new java.util.concurrent.ConcurrentHashMap<>();
/** /**
* JME3 → JavaFX: Relativer Asset-Pfad des gerade geladenen Modells. * JME3 → JavaFX: Relativer Asset-Pfad des gerade geladenen Modells.

View File

@@ -74,6 +74,17 @@ public class AnimPreviewState extends BaseAppState {
private String currentModelPath; private String currentModelPath;
private Action currentAction; private Action currentAction;
private String currentClipName; private String currentClipName;
/** Stabiler Ziel-Offset (wird bei Clip-Wechsel sofort gesetzt). */
private final com.jme3.math.Vector3f previewOffsetCurrent = new com.jme3.math.Vector3f();
/** Startposition des laufenden Übergangs (= angezeigte Position beim letzten Offset-Wechsel). */
private final com.jme3.math.Vector3f previewOffsetFrom = new com.jme3.math.Vector3f();
/** Angezeigte Zwischenposition (kein GC-Druck pro Frame). */
private final com.jme3.math.Vector3f previewOffsetDisplay = new com.jme3.math.Vector3f();
/** Bisherige Zielwerte zum Erkennen von Änderungen. */
private float prevOffTx = 0f, prevOffTy = 0f, prevOffTz = 0f;
/** Verstrichene Zeit seit letztem Offset-Wechsel; >= TRANSITION_DURATION = stabil. */
private float transitionElapsed = 0.15f;
private static final float TRANSITION_DURATION = 0.15f;
private Node axesNode; private Node axesNode;
@@ -146,8 +157,12 @@ public class AnimPreviewState extends BaseAppState {
String playClip = input.animPreviewPlayClip; String playClip = input.animPreviewPlayClip;
if (playClip != null) { if (playClip != null) {
input.animPreviewPlayClip = null; input.animPreviewPlayClip = null;
if (playClip.isEmpty()) stopAll(); if (playClip.isEmpty()) {
else playClip(playClip); input.animPreviewSequenceQueue.clear();
stopAll();
} else {
playClip(playClip);
}
} }
String addAnimPath = input.animImportQueue.poll(); String addAnimPath = input.animImportQueue.poll();
@@ -204,7 +219,11 @@ public class AnimPreviewState extends BaseAppState {
stopAll(); stopAll();
} else if (time >= length - 0.02) { } else if (time >= length - 0.02) {
LOG.trace("[AnimPreview] Loop-Check fired: time={} length={}", time, length); LOG.trace("[AnimPreview] Loop-Check fired: time={} length={}", time, length);
if (input.animPreviewLoop) { String nextInSeq = input.animPreviewSequenceQueue.poll();
if (nextInSeq != null) {
LOG.debug("[AnimPreview] Sequenz weiter: '{}'", nextInSeq);
playClip(nextInSeq);
} else if (input.animPreviewLoop) {
currentAction = ac.setCurrentAction(currentClipName); currentAction = ac.setCurrentAction(currentClipName);
if (currentAction != null) currentAction.setSpeed(input.animPreviewSpeed); if (currentAction != null) currentAction.setSpeed(input.animPreviewSpeed);
} else { } else {
@@ -239,14 +258,27 @@ public class AnimPreviewState extends BaseAppState {
axesNode.setLocalTranslation(Vector3f.ZERO); axesNode.setLocalTranslation(Vector3f.ZERO);
} }
// Anim-Offset-Vorschau: Versatz auf Modell anwenden (TX/TY/TZ in Welt-Koordinaten) // Anim-Offset-Vorschau: Frame 0 nach Wechsel zeigt alte Position (kein Snap),
// dann linearer Übergang über TRANSITION_DURATION.
if (currentModel != null) { if (currentModel != null) {
if (input.animPreviewOffsetActive) { float tgtX = input.animPreviewOffsetActive ? input.animPreviewOffsetTx : 0f;
currentModel.setLocalTranslation( float tgtY = input.animPreviewOffsetActive ? input.animPreviewOffsetTy : 0f;
input.animPreviewOffsetTx, input.animPreviewOffsetTy, input.animPreviewOffsetTz); float tgtZ = input.animPreviewOffsetActive ? input.animPreviewOffsetTz : 0f;
} else { if (tgtX != prevOffTx || tgtY != prevOffTy || tgtZ != prevOffTz) {
currentModel.setLocalTranslation(Vector3f.ZERO); float tCurr = Math.min(1f, transitionElapsed / TRANSITION_DURATION);
previewOffsetFrom.set(
previewOffsetFrom.x + tCurr * (previewOffsetCurrent.x - previewOffsetFrom.x),
previewOffsetFrom.y + tCurr * (previewOffsetCurrent.y - previewOffsetFrom.y),
previewOffsetFrom.z + tCurr * (previewOffsetCurrent.z - previewOffsetFrom.z));
previewOffsetCurrent.set(tgtX, tgtY, tgtZ);
transitionElapsed = 0f;
prevOffTx = tgtX; prevOffTy = tgtY; prevOffTz = tgtZ;
} }
float t = Math.min(1f, transitionElapsed / TRANSITION_DURATION);
previewOffsetDisplay.set(previewOffsetFrom);
previewOffsetDisplay.interpolateLocal(previewOffsetCurrent, t);
currentModel.setLocalTranslation(previewOffsetDisplay);
transitionElapsed += tpf;
} }
previewScene.updateLogicalState(tpf); previewScene.updateLogicalState(tpf);
@@ -261,6 +293,10 @@ public class AnimPreviewState extends BaseAppState {
currentModel = null; currentModel = null;
currentModelPath = null; currentModelPath = null;
currentAction = null; currentAction = null;
previewOffsetCurrent.set(0f, 0f, 0f);
previewOffsetFrom.set(0f, 0f, 0f);
prevOffTx = 0f; prevOffTy = 0f; prevOffTz = 0f;
transitionElapsed = TRANSITION_DURATION;
try { try {
Spatial model = loadFresh(assetPath); Spatial model = loadFresh(assetPath);
@@ -391,6 +427,19 @@ public class AnimPreviewState extends BaseAppState {
if (currentModel == null) return; if (currentModel == null) return;
currentAction = null; currentAction = null;
currentClipName = null; currentClipName = null;
// Offset für diesen Clip aus der Map lesen (wird von EditorApp befüllt)
float[] off = input.animPreviewClipOffsets.get(clipName);
if (off != null && off.length >= 3) {
input.animPreviewOffsetTx = off[0];
input.animPreviewOffsetTy = off[1];
input.animPreviewOffsetTz = off[2];
input.animPreviewOffsetActive = true;
} else {
input.animPreviewOffsetTx = 0f;
input.animPreviewOffsetTy = 0f;
input.animPreviewOffsetTz = 0f;
input.animPreviewOffsetActive = false;
}
// SkinningControls auf dem gesamten Modell aktivieren AnimComposer und // SkinningControls auf dem gesamten Modell aktivieren AnimComposer und
// SkinningControl sitzen oft auf verschiedenen Geschwisterknoten. // SkinningControl sitzen oft auf verschiedenen Geschwisterknoten.
setSkinningEnabled(currentModel, true); setSkinningEnabled(currentModel, true);
@@ -587,12 +636,16 @@ public class AnimPreviewState extends BaseAppState {
AnimClip result; AnimClip result;
if (!retarget) { if (!retarget) {
result = clip; result = clip;
} else { } else if (isJ3oSource) {
// Immer erst redirect-by-name wie FbxTestApp: mappt alle passenden Joints, // j3o-Clips wurden bereits beim FBX-Import normalisiert (in Modell-Raum gespeichert).
// droppt nur die wenigen fehlenden ($AssimpFbx$-Varianten die im Model nicht existieren). // Plain-Retarget wie FbxTestApp.retargetClip: nur Joint-Targets umbiegen, keine Delta-Korrektur.
result = redirectAnimClipByName(clip, dstArm); result = redirectAnimClipByName(clip, dstArm);
} else {
// FBX-Import: Normalisierung in Modell-Raum (Orphan-Fold + Bind-Pose-Delta).
// Das resultierende j3o ist dann mit plain redirectAnimClipByName kompatibel.
result = normalizeAndRetargetClip(clip, dstArm);
if (result == null) { if (result == null) {
LOG.warn("[AnimPreview] redirectByName: keine Joints gemappt, versuche Retargeting"); LOG.warn("[AnimPreview] normalizeAndRetarget: keine Joints gemappt, versuche Retargeting");
result = de.blight.game.animation.RetargetingSystem.retarget(clip, srcArm, dstArm); result = de.blight.game.animation.RetargetingSystem.retarget(clip, srcArm, dstArm);
} }
} }
@@ -1567,7 +1620,67 @@ public class AnimPreviewState extends BaseAppState {
} }
/** Leitet Clip-Tracks per Gelenk-Name auf die Ziel-Armatur um (FbxTestApp-Ansatz für Same-Rig). */ /** Leitet Clip-Tracks per Gelenk-Name auf die Ziel-Armatur um (FbxTestApp-Ansatz für Same-Rig). */
/**
* Plain Retarget wie FbxTestApp.retargetClip: Joint-Targets per Name auf
* targetArm umbiegen, Keyframe-Werte 1:1 übernehmen. Funktioniert korrekt
* wenn Quell- und Ziel-Armature dieselbe Bind-Pose haben (z.B. j3o-Clips
* die bereits mit normalizeAndRetargetClip in Modell-Raum gespeichert wurden).
*/
private static AnimClip redirectAnimClipByName(AnimClip src, com.jme3.anim.Armature targetArm) { private static AnimClip redirectAnimClipByName(AnimClip src, com.jme3.anim.Armature targetArm) {
List<com.jme3.anim.AnimTrack<?>> newTracks = new ArrayList<>();
for (com.jme3.anim.AnimTrack<?> track : src.getTracks()) {
if (!(track instanceof com.jme3.anim.TransformTrack tt)) continue;
if (!(tt.getTarget() instanceof com.jme3.anim.Joint srcJoint)) continue;
com.jme3.anim.Joint dstJoint = targetArm.getJoint(srcJoint.getName());
if (dstJoint == null) continue;
newTracks.add(new com.jme3.anim.TransformTrack(dstJoint, tt.getTimes(),
tt.getTranslations(), tt.getRotations(), tt.getScales()));
}
if (newTracks.isEmpty()) return null;
AnimClip result = new AnimClip(src.getName());
result.setTracks(newTracks.toArray(new com.jme3.anim.AnimTrack[0]));
LOG.debug("[AnimPreview] redirectByName '{}': {} Tracks, length={}", src.getName(), newTracks.size(), result.getLength());
return result;
}
/**
* Normalisiert einen FBX-Animations-Clip in den Modell-Koordinatenraum und
* retargeted ihn auf targetArm. Wird NUR beim FBX-Import (Speichern) verwendet.
*
* Löst das "Without Skin"-Decompositions-Problem: Mixamo "Without Skin"-FBX
* trennt Hips in Base-Pivot (Y≈0.52) + $AssimpFbx$_Translation (Y≈54.2).
* Das Modell ("With Skin") hat Hips auf voller Höhe im Base-Joint. Ohne
* Normalisierung stimmt das IBM nicht → Skinning-Deformation.
*
* Zwei Schritte:
* 1. Orphan-Fold: $AssimpFbx$_Translation-Tracks ohne Modell-Match werden
* in den nächsten Modell-Vorfahren gefaltet (beide Translations addiert).
* 2. Bind-Pose-Delta: Für jeden Track wird die Differenz zwischen
* Animations-Bind-Pose und Modell-Bind-Pose auf alle Keyframes addiert.
*/
private static AnimClip normalizeAndRetargetClip(AnimClip src, com.jme3.anim.Armature targetArm) {
// Pass 1: Orphan-$AssimpFbx$_Translation finden
java.util.Map<String, com.jme3.anim.TransformTrack> orphanByParent = new java.util.HashMap<>();
java.util.Map<String, com.jme3.math.Vector3f> orphanInitT = new java.util.HashMap<>();
for (com.jme3.anim.AnimTrack<?> t : src.getTracks()) {
if (!(t instanceof com.jme3.anim.TransformTrack tt)) continue;
if (!(tt.getTarget() instanceof com.jme3.anim.Joint srcJ)) continue;
if (targetArm.getJoint(srcJ.getName()) != null) continue;
if (!srcJ.getName().contains("$AssimpFbx$_Translation")) continue;
if (tt.getTranslations() == null) continue;
com.jme3.anim.Joint ancestor = srcJ.getParent();
while (ancestor != null && targetArm.getJoint(ancestor.getName()) == null) {
ancestor = ancestor.getParent();
}
if (ancestor == null) continue;
orphanByParent.put(ancestor.getName(), tt);
orphanInitT.put(ancestor.getName(), srcJ.getInitialTransform().getTranslation().clone());
LOG.info("[AnimPreview] Normalisierung: Falte '{}' in '{}'", srcJ.getName(), ancestor.getName());
}
// Pass 2: Tracks umbiegen + Orphan-Fold
// Delta wird NUR angewendet wenn ein Orphan gefaltet wurde (verhindert falschen Offset
// bei Sitz-/Liege-Animationen, deren Hips-initT != Steh-Bindpose).
List<com.jme3.anim.AnimTrack<?>> newTracks = new ArrayList<>(); List<com.jme3.anim.AnimTrack<?>> newTracks = new ArrayList<>();
for (com.jme3.anim.AnimTrack<?> track : src.getTracks()) { for (com.jme3.anim.AnimTrack<?> track : src.getTracks()) {
if (!(track instanceof com.jme3.anim.TransformTrack tt)) continue; if (!(track instanceof com.jme3.anim.TransformTrack tt)) continue;
@@ -1575,31 +1688,37 @@ public class AnimPreviewState extends BaseAppState {
com.jme3.anim.Joint dstJoint = targetArm.getJoint(srcJoint.getName()); com.jme3.anim.Joint dstJoint = targetArm.getJoint(srcJoint.getName());
if (dstJoint == null) continue; if (dstJoint == null) continue;
// Bind-Pose-Delta: "Without Skin"-FBX kann dieselbe Gesamt-Translation
// anders auf den Base-Joint und $AssimpFbx$-Helper aufteilen als "With Skin".
// Alle Translation-Keyframes um die Differenz (dstInit - srcInit) verschieben,
// damit die Modell-Bind-Pose korrekt abgebildet wird.
com.jme3.math.Vector3f[] translations = tt.getTranslations(); com.jme3.math.Vector3f[] translations = tt.getTranslations();
if (translations != null && translations.length > 0) { if (translations != null && translations.length > 0) {
com.jme3.math.Vector3f srcInitT = srcJoint.getInitialTransform().getTranslation(); com.jme3.anim.TransformTrack orphan = orphanByParent.get(srcJoint.getName());
com.jme3.math.Vector3f dstInitT = dstJoint.getInitialTransform().getTranslation(); com.jme3.math.Vector3f oInit = orphanInitT.get(srcJoint.getName());
com.jme3.math.Vector3f delta = dstInitT.subtract(srcInitT); com.jme3.math.Vector3f[] oT = orphan != null ? orphan.getTranslations() : null;
if (delta.lengthSquared() > 1e-6f) {
com.jme3.math.Vector3f[] retargeted = new com.jme3.math.Vector3f[translations.length];
for (int i = 0; i < translations.length; i++) {
retargeted[i] = translations[i].add(delta);
}
translations = retargeted;
}
}
com.jme3.math.Vector3f delta = null;
if (orphan != null) {
com.jme3.math.Vector3f srcInitT = srcJoint.getInitialTransform().getTranslation();
com.jme3.math.Vector3f dstInitT = dstJoint.getInitialTransform().getTranslation();
delta = dstInitT.subtract(srcInitT.add(oInit));
LOG.debug("[AnimPreview] Orphan-Delta '{}': srcInit+orphan={} dstInit={} delta={}",
srcJoint.getName(), srcInitT.add(oInit), dstInitT, delta);
}
com.jme3.math.Vector3f[] retargeted = new com.jme3.math.Vector3f[translations.length];
for (int i = 0; i < translations.length; i++) {
com.jme3.math.Vector3f frameT = translations[i].clone();
if (oT != null && i < oT.length) frameT.addLocal(oT[i]);
if (delta != null) frameT.addLocal(delta);
retargeted[i] = frameT;
}
translations = retargeted;
}
newTracks.add(new com.jme3.anim.TransformTrack(dstJoint, tt.getTimes(), newTracks.add(new com.jme3.anim.TransformTrack(dstJoint, tt.getTimes(),
translations, tt.getRotations(), tt.getScales())); translations, tt.getRotations(), tt.getScales()));
} }
if (newTracks.isEmpty()) return null; if (newTracks.isEmpty()) return null;
AnimClip result = new AnimClip(src.getName()); AnimClip result = new AnimClip(src.getName());
result.setTracks(newTracks.toArray(new com.jme3.anim.AnimTrack[0])); result.setTracks(newTracks.toArray(new com.jme3.anim.AnimTrack[0]));
LOG.debug("[AnimPreview] redirectByName '{}': {} Tracks, length={}", src.getName(), newTracks.size(), result.getLength()); LOG.debug("[AnimPreview] normalizeAndRetarget '{}': {} Tracks, length={}", src.getName(), newTracks.size(), result.getLength());
return result; return result;
} }

View File

@@ -30,6 +30,8 @@ public class AnimSet {
private Map<String, AnimOffset> animOffsets = new LinkedHashMap<>(); private Map<String, AnimOffset> animOffsets = new LinkedHashMap<>();
/** Sub-Clip-Definitionen: Name → Zeitfenster aus einem kombinierten Quell-Clip. */ /** Sub-Clip-Definitionen: Name → Zeitfenster aus einem kombinierten Quell-Clip. */
private Map<String, SubClipDef> subClips = new LinkedHashMap<>(); private Map<String, SubClipDef> subClips = new LinkedHashMap<>();
/** Ablauf-Definitionen: Name → geordnete Liste von Aktions-Namen (aus actionMap). */
private Map<String, List<String>> sequences = new LinkedHashMap<>();
/** 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 {
@@ -54,6 +56,10 @@ public class AnimSet {
return subClips != null ? subClips : new LinkedHashMap<>(); return subClips != null ? subClips : new LinkedHashMap<>();
} }
public void setSubClips(Map<String, SubClipDef> subClips) { this.subClips = subClips; } public void setSubClips(Map<String, SubClipDef> subClips) { this.subClips = subClips; }
public Map<String, List<String>> getSequences() {
return sequences != null ? sequences : new LinkedHashMap<>();
}
public void setSequences(Map<String, List<String>> sequences) { this.sequences = sequences; }
/** 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 {