diff --git a/assets/imported/animations/Idle.fbx b/assets/imported/animations/Idle.fbx index 9cd726c..e11adba 100644 Binary files a/assets/imported/animations/Idle.fbx and b/assets/imported/animations/Idle.fbx differ diff --git a/assets/imported/animations/Jumping.fbx b/assets/imported/animations/Jumping.fbx index e9ad892..b8e3043 100644 Binary files a/assets/imported/animations/Jumping.fbx and b/assets/imported/animations/Jumping.fbx differ diff --git a/assets/imported/animations/Running.fbx b/assets/imported/animations/Running.fbx index b74e946..b0f31d1 100644 Binary files a/assets/imported/animations/Running.fbx and b/assets/imported/animations/Running.fbx differ diff --git a/assets/imported/animations/Sit To Stand.fbx b/assets/imported/animations/Sit To Stand.fbx index 72b5a52..8920092 100644 Binary files a/assets/imported/animations/Sit To Stand.fbx and b/assets/imported/animations/Sit To Stand.fbx differ diff --git a/assets/imported/animations/Sitting Idle.fbx b/assets/imported/animations/Sitting Idle.fbx index 876ceac..be6d87f 100644 Binary files a/assets/imported/animations/Sitting Idle.fbx and b/assets/imported/animations/Sitting Idle.fbx differ diff --git a/assets/imported/animations/Sprint.fbx b/assets/imported/animations/Sprint.fbx index 0fd0086..721773e 100644 Binary files a/assets/imported/animations/Sprint.fbx and b/assets/imported/animations/Sprint.fbx differ diff --git a/assets/imported/animations/Stand To Sit.fbx b/assets/imported/animations/Stand To Sit.fbx index 2b03ad6..621c99f 100644 Binary files a/assets/imported/animations/Stand To Sit.fbx and b/assets/imported/animations/Stand To Sit.fbx differ diff --git a/assets/imported/animations/T-Pose.fbx b/assets/imported/animations/T-Pose.fbx index f5b5bb6..55535df 100644 Binary files a/assets/imported/animations/T-Pose.fbx and b/assets/imported/animations/T-Pose.fbx differ diff --git a/assets/imported/animations/Walking.fbx b/assets/imported/animations/Walking.fbx index 3730473..0d5cafd 100644 Binary files a/assets/imported/animations/Walking.fbx and b/assets/imported/animations/Walking.fbx differ diff --git a/blight-assets/src/main/resources/Characters/clips/Disappointed.j3o b/blight-assets/src/main/resources/Characters/clips/Disappointed.j3o deleted file mode 100644 index 7c703c1..0000000 Binary files a/blight-assets/src/main/resources/Characters/clips/Disappointed.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Characters/clips/Idle.j3o b/blight-assets/src/main/resources/Characters/clips/Idle.j3o index 6ce8adb..93594ec 100644 Binary files a/blight-assets/src/main/resources/Characters/clips/Idle.j3o and b/blight-assets/src/main/resources/Characters/clips/Idle.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Jumping.j3o b/blight-assets/src/main/resources/Characters/clips/Jumping.j3o index 1ffbf8f..6402273 100644 Binary files a/blight-assets/src/main/resources/Characters/clips/Jumping.j3o and b/blight-assets/src/main/resources/Characters/clips/Jumping.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Kneeling Down.j3o b/blight-assets/src/main/resources/Characters/clips/Kneeling Down.j3o deleted file mode 100644 index 8d050a7..0000000 Binary files a/blight-assets/src/main/resources/Characters/clips/Kneeling Down.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Characters/clips/Kneeling.j3o b/blight-assets/src/main/resources/Characters/clips/Kneeling.j3o deleted file mode 100644 index f1820e6..0000000 Binary files a/blight-assets/src/main/resources/Characters/clips/Kneeling.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Characters/clips/Lifting.j3o b/blight-assets/src/main/resources/Characters/clips/Lifting.j3o deleted file mode 100644 index f8382a1..0000000 Binary files a/blight-assets/src/main/resources/Characters/clips/Lifting.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Characters/clips/Lying Down.j3o b/blight-assets/src/main/resources/Characters/clips/Lying Down.j3o deleted file mode 100644 index 4422c9a..0000000 Binary files a/blight-assets/src/main/resources/Characters/clips/Lying Down.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Characters/clips/Male Crouch Pose.j3o b/blight-assets/src/main/resources/Characters/clips/Male Crouch Pose.j3o deleted file mode 100644 index 3c6fe5f..0000000 Binary files a/blight-assets/src/main/resources/Characters/clips/Male Crouch Pose.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Characters/clips/Male Laying Pose(1).j3o b/blight-assets/src/main/resources/Characters/clips/Male Laying Pose(1).j3o deleted file mode 100644 index 934e0f6..0000000 Binary files a/blight-assets/src/main/resources/Characters/clips/Male Laying Pose(1).j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Characters/clips/Male Laying Pose.j3o b/blight-assets/src/main/resources/Characters/clips/Male Laying Pose.j3o deleted file mode 100644 index 705d7c8..0000000 Binary files a/blight-assets/src/main/resources/Characters/clips/Male Laying Pose.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Characters/clips/Running Jump.j3o b/blight-assets/src/main/resources/Characters/clips/Running Jump.j3o deleted file mode 100644 index ee35998..0000000 Binary files a/blight-assets/src/main/resources/Characters/clips/Running Jump.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Characters/clips/Running.j3o b/blight-assets/src/main/resources/Characters/clips/Running.j3o index 3b4558a..8af5bb5 100644 Binary files a/blight-assets/src/main/resources/Characters/clips/Running.j3o and b/blight-assets/src/main/resources/Characters/clips/Running.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sit To Stand.j3o b/blight-assets/src/main/resources/Characters/clips/Sit To Stand.j3o index 15d4b18..176b1e7 100644 Binary files a/blight-assets/src/main/resources/Characters/clips/Sit To Stand.j3o and b/blight-assets/src/main/resources/Characters/clips/Sit To Stand.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sitting Idle(1).j3o b/blight-assets/src/main/resources/Characters/clips/Sitting Idle(1).j3o deleted file mode 100644 index 9adff1d..0000000 Binary files a/blight-assets/src/main/resources/Characters/clips/Sitting Idle(1).j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sitting Idle.j3o b/blight-assets/src/main/resources/Characters/clips/Sitting Idle.j3o index c9b4d27..d0e030f 100644 Binary files a/blight-assets/src/main/resources/Characters/clips/Sitting Idle.j3o and b/blight-assets/src/main/resources/Characters/clips/Sitting Idle.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sitting Talking(1).j3o b/blight-assets/src/main/resources/Characters/clips/Sitting Talking(1).j3o deleted file mode 100644 index 7b2ce22..0000000 Binary files a/blight-assets/src/main/resources/Characters/clips/Sitting Talking(1).j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sitting Talking.j3o b/blight-assets/src/main/resources/Characters/clips/Sitting Talking.j3o deleted file mode 100644 index 13258df..0000000 Binary files a/blight-assets/src/main/resources/Characters/clips/Sitting Talking.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sitting.j3o b/blight-assets/src/main/resources/Characters/clips/Sitting.j3o deleted file mode 100644 index adc437e..0000000 Binary files a/blight-assets/src/main/resources/Characters/clips/Sitting.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sneaking Forward.j3o b/blight-assets/src/main/resources/Characters/clips/Sneaking Forward.j3o deleted file mode 100644 index 6157ad5..0000000 Binary files a/blight-assets/src/main/resources/Characters/clips/Sneaking Forward.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sprint.j3o b/blight-assets/src/main/resources/Characters/clips/Sprint.j3o index bbea304..931f4d8 100644 Binary files a/blight-assets/src/main/resources/Characters/clips/Sprint.j3o and b/blight-assets/src/main/resources/Characters/clips/Sprint.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Sprinting Forward Roll.j3o b/blight-assets/src/main/resources/Characters/clips/Sprinting Forward Roll.j3o deleted file mode 100644 index f6ab96c..0000000 Binary files a/blight-assets/src/main/resources/Characters/clips/Sprinting Forward Roll.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Characters/clips/Stand To Roll.j3o b/blight-assets/src/main/resources/Characters/clips/Stand To Roll.j3o deleted file mode 100644 index 7b980c3..0000000 Binary files a/blight-assets/src/main/resources/Characters/clips/Stand To Roll.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Characters/clips/Stand To Sit.j3o b/blight-assets/src/main/resources/Characters/clips/Stand To Sit.j3o index 79b875e..c0bd816 100644 Binary files a/blight-assets/src/main/resources/Characters/clips/Stand To Sit.j3o and b/blight-assets/src/main/resources/Characters/clips/Stand To Sit.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Standing Up(1).j3o b/blight-assets/src/main/resources/Characters/clips/Standing Up(1).j3o deleted file mode 100644 index 56f4e58..0000000 Binary files a/blight-assets/src/main/resources/Characters/clips/Standing Up(1).j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Characters/clips/Standing Up.j3o b/blight-assets/src/main/resources/Characters/clips/Standing Up.j3o deleted file mode 100644 index b658032..0000000 Binary files a/blight-assets/src/main/resources/Characters/clips/Standing Up.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Characters/clips/Standing.j3o b/blight-assets/src/main/resources/Characters/clips/Standing.j3o deleted file mode 100644 index 451efcd..0000000 Binary files a/blight-assets/src/main/resources/Characters/clips/Standing.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Characters/clips/T-Pose.j3o b/blight-assets/src/main/resources/Characters/clips/T-Pose.j3o index 577f606..4df5d77 100644 Binary files a/blight-assets/src/main/resources/Characters/clips/T-Pose.j3o and b/blight-assets/src/main/resources/Characters/clips/T-Pose.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Talking(1).j3o b/blight-assets/src/main/resources/Characters/clips/Talking(1).j3o deleted file mode 100644 index 07114d2..0000000 Binary files a/blight-assets/src/main/resources/Characters/clips/Talking(1).j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Characters/clips/Talking.j3o b/blight-assets/src/main/resources/Characters/clips/Talking.j3o deleted file mode 100644 index 3fd1410..0000000 Binary files a/blight-assets/src/main/resources/Characters/clips/Talking.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Characters/clips/Walking.j3o b/blight-assets/src/main/resources/Characters/clips/Walking.j3o index a3d66bd..5fd91fd 100644 Binary files a/blight-assets/src/main/resources/Characters/clips/Walking.j3o and b/blight-assets/src/main/resources/Characters/clips/Walking.j3o differ diff --git a/blight-assets/src/main/resources/Characters/clips/Yelling.j3o b/blight-assets/src/main/resources/Characters/clips/Yelling.j3o deleted file mode 100644 index 37cc7a3..0000000 Binary files a/blight-assets/src/main/resources/Characters/clips/Yelling.j3o and /dev/null differ diff --git a/blight-assets/src/main/resources/Characters/sets/human.animset.json b/blight-assets/src/main/resources/Characters/sets/human.animset.json index 7449c8b..ce4c426 100644 --- a/blight-assets/src/main/resources/Characters/sets/human.animset.json +++ b/blight-assets/src/main/resources/Characters/sets/human.animset.json @@ -1,38 +1,24 @@ { "clips": [ - "Disappointed", "Idle", - "Jumping", - "Kneeling", - "Kneeling Down", - "Lifting", - "Lying Down", - "Male Crouch Pose", - "Male Laying Pose", - "Male Laying Pose(1)", "Running", - "Running Jump", "Sit To Stand", - "Sitting", "Sitting Idle", - "Sitting Idle(1)", - "Sitting Talking", - "Sitting Talking(1)", - "Sneaking Forward", "Sprint", - "Sprinting Forward Roll", - "Stand To Roll", "Stand To Sit", - "Standing", - "Standing Up", - "Standing Up(1)", "T-Pose", - "Talking", - "Talking(1)", - "Walking", - "Yelling" + "Walking" ], - "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", "animOffsets": { "sitting": { @@ -50,7 +36,31 @@ "rx": 0.0, "ry": 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" + ] + } } \ No newline at end of file diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index 82d9ab1..63c3fcd 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -202,7 +202,6 @@ public class EditorApp extends Application { private ListView animSetActionListView; private String animSetPendingPlayClip = null; private ComboBox animSetModelCombo; - private boolean animSetDirty = false; private String animSetCurrentName = null; private Path animSetCurrentDir = null; // Anim-Offset-Editor (innerhalb AnimSet-Editor) @@ -215,6 +214,10 @@ public class EditorApp extends Application { private ListView animSetPartsListView; private java.util.Map editableSubClips = new java.util.LinkedHashMap<>(); + // Sequenz-Editor (innerhalb AnimSet-Editor) + private ListView animSetSequenceListView; + private java.util.Map> + editableSequences = new java.util.LinkedHashMap<>(); // Character-Editor-Zustand private de.blight.editor.ui.DialogEditorView dialogEditorView; @@ -1537,7 +1540,7 @@ public class EditorApp extends Application { MenuItem vegetationsItem = new MenuItem("Vegetations Generator"); MenuItem ezTreeItem = new MenuItem("Baum Generator (EZ Tree)"); 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 thumbItem = new MenuItem("Thumbnail-Verwaltung"); vegetationsItem.setOnAction(e -> switchToVegetationGenerator()); @@ -5542,7 +5545,14 @@ public class EditorApp extends Application { if (p == null) continue; try { 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); itemPaths.remove(si); deleted++; @@ -5769,7 +5779,14 @@ public class EditorApp extends Application { Files.deleteIfExists(p); item.getParent().getChildren().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()); } catch (IOException ex) { setStatus("Fehler: " + ex.getMessage()); @@ -6244,6 +6261,41 @@ public class EditorApp extends Application { } 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. * Bekannte Verzeichnisse werden den Kategorie-Feldern zugewiesen; @@ -10364,7 +10416,7 @@ public class EditorApp extends Application { // Dialog: vorhandenes AnimSet wählen oder neues anlegen javafx.scene.control.Dialog dlg = new javafx.scene.control.Dialog<>(); dlg.initOwner(primaryStage); - dlg.setTitle("Character-Editor"); + dlg.setTitle("Animationset Editor"); dlg.setHeaderText("AnimSet auswählen oder neu anlegen"); ListView listView = new ListView<>(); @@ -10420,7 +10472,6 @@ public class EditorApp extends Application { animSetCurrentName = setName; animSetCurrentDir = setDir; - animSetDirty = false; currentTool = "animseteditor"; topBar.getChildren().set(1, buildAnimSetToolBar(setName)); @@ -10501,7 +10552,7 @@ public class EditorApp extends Application { if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade…"); // Pfad im AnimSet merken und sofort speichern animSet.setPreviewModelPath(path); - animSetDirty = true; + autoSaveAnimSet(); }); Button importCharBtn = new Button("Character-Modell importieren (FBX)…"); importCharBtn.setMaxWidth(Double.MAX_VALUE); @@ -10520,7 +10571,14 @@ public class EditorApp extends Application { animSetClipListView = new ListView<>(); animSetClipListView.getItems().addAll(animSet.getClips()); 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"); @@ -10621,7 +10679,7 @@ public class EditorApp extends Application { for (String clip : selected) { if (!animSetClipListView.getItems().contains(clip)) { animSetClipListView.getItems().add(clip); - animSetDirty = true; + autoSaveAnimSet(); } } }); @@ -10635,18 +10693,14 @@ public class EditorApp extends Application { animSetActionListView.getItems().removeIf(it -> it.endsWith(" → " + sel)); editableSubClips.entrySet().removeIf(en -> sel.equals(en.getValue().source)); if (animSetPartsListView != null) animSetPartsListView.getItems().clear(); - animSetDirty = true; + autoSaveAnimSet(); }); HBox clipBtns = new HBox(6, addClipBtn, removeClipBtn); HBox.setHgrow(addClipBtn, Priority.ALWAYS); HBox.setHgrow(removeClipBtn, Priority.ALWAYS); - Button importAnimBtn = new Button("Animation hinzufügen…"); - importAnimBtn.setMaxWidth(Double.MAX_VALUE); - importAnimBtn.setOnAction(e -> handleAnimationImport(importAnimBtn.getScene().getWindow())); - - inner.getChildren().addAll(animSetClipListView, clipBtns, importAnimBtn); + inner.getChildren().addAll(animSetClipListView, clipBtns); // ── Clip-Teile (Sub-Clips) ──────────────────────────────────────────── 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(" [")); editableSubClips.remove(name); animSetPartsListView.getItems().remove(sel); - animSetDirty = true; + autoSaveAnimSet(); }); HBox partsBtns = new HBox(6, addPartBtn, removePartBtn); @@ -10739,7 +10793,7 @@ public class EditorApp extends Application { String sel = animSetActionListView.getSelectionModel().getSelectedItem(); if (sel != null) { animSetActionListView.getItems().remove(sel); - animSetDirty = true; + autoSaveAnimSet(); } }); @@ -10803,7 +10857,7 @@ public class EditorApp extends Application { animSetOffsetListView.getSelectionModel().getSelectedItem(); if (sel != null) { animSetOffsetList.remove(sel); - animSetDirty = true; + autoSaveAnimSet(); } }); @@ -10812,6 +10866,64 @@ public class EditorApp extends Application { HBox.setHgrow(removeKfBtn, Priority.ALWAYS); 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 ───────────────────────────────────────────────────────── inner.getChildren().addAll(new Separator(), sectionTitle("Vorschau"), new Separator()); @@ -10851,14 +10963,6 @@ public class EditorApp extends Application { inner.getChildren().addAll(previewHint, previewBtns, loopCB, 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); scroll.setFitToWidth(true); scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); @@ -10929,11 +11033,182 @@ public class EditorApp extends Application { existing.rz = spRZ.getValue().floatValue(); } if (animSetOffsetListView != null) animSetOffsetListView.refresh(); - animSetDirty = true; + autoSaveAnimSet(); }); } /** Spielt den Clip der gegebenen Aktion in der Vorschau ab, um animPreviewCurrentClipDuration zu setzen. */ + private String formatSequenceEntry(String name, java.util.List 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 actions = editableSequences.get(seqName); + if (actions == null || actions.isEmpty()) return; + + java.util.Map 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 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 existingActions) { + // Verfügbare Aktionen aus animSetActionListView auslesen + java.util.List 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 leftList = new ListView<>(); + leftList.getItems().addAll(availableActions); + leftList.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); + leftList.setPrefSize(200, 240); + + // ── Rechte Liste (Ablauf) ──────────────────────────────────────────── + ListView 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 sel = new java.util.ArrayList<>( + leftList.getSelectionModel().getSelectedItems()); + rightList.getItems().addAll(sel); + }); + + removeBtn.setOnAction(ev -> { + java.util.List 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 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 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) { if (animSetActionListView == null || action == null) return; String prefix = action.name() + " → "; @@ -11126,10 +11401,15 @@ public class EditorApp extends Application { animSetPartsListView.getItems().removeIf(s -> s.startsWith(name + " [")); 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) { if (animSetClipListView == null) { return; @@ -11166,6 +11446,13 @@ public class EditorApp extends Application { } animSet.setAnimOffsets(offsetFinal); 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 if (animSetModelCombo != null && animSetModelCombo.getValue() != null && !animSetModelCombo.getValue().isBlank()) { animSet.setPreviewModelPath(animSetModelCombo.getValue()); @@ -11180,16 +11467,7 @@ public class EditorApp extends Application { } private void createNewAnimSet() { - if (animSetDirty) { - 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(); - } + doCreateNewAnimSet(); } private void doCreateNewAnimSet() { @@ -11746,7 +12024,7 @@ public class EditorApp extends Application { private ToolBar buildCharacterEditorToolBar() { Button backBtn = new Button("← Welteneditor"); 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;"); ToolBar tb = new ToolBar(); tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label); @@ -11804,9 +12082,9 @@ public class EditorApp extends Application { charModelCombo = new ComboBox<>(); charModelCombo.setMaxWidth(Double.MAX_VALUE); charModelCombo.setPromptText("Modell wählen…"); - Path modelBase = ASSET_ROOT.resolve("Models"); - if (java.nio.file.Files.isDirectory(modelBase)) { - try (var walk = java.nio.file.Files.walk(modelBase)) { + Path charModelBase = ASSET_ROOT.resolve("Characters").resolve("Models"); + if (java.nio.file.Files.isDirectory(charModelBase)) { + try (var walk = java.nio.file.Files.walk(charModelBase)) { walk.filter(p -> p.toString().endsWith(".j3o")) .map(p -> ASSET_ROOT.relativize(p).toString().replace('\\', '/')) .sorted().forEach(charModelCombo.getItems()::add); diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index 3178348..f913ff2 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -617,6 +617,8 @@ public class SharedInput { public volatile String animPreviewLoadPath = null; /** JavaFX → JME3: Clip abspielen; "" = Stop. null = kein Auftrag. */ public volatile String animPreviewPlayClip = null; + /** JavaFX → JME3: Sequenz-Warteschlange – Clip-Namen werden nacheinander abgespielt (kein Loop). */ + public final ConcurrentLinkedQueue animPreviewSequenceQueue = new ConcurrentLinkedQueue<>(); /** JavaFX → JME3: Animation-j3o-Pfad zum Retargeting + Hinzufügen. null = kein Auftrag. */ public final ConcurrentLinkedQueue animImportQueue = new ConcurrentLinkedQueue<>(); /** 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 animPreviewOffsetTz = 0f; 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 animPreviewClipOffsets = + new java.util.concurrent.ConcurrentHashMap<>(); /** * JME3 → JavaFX: Relativer Asset-Pfad des gerade geladenen Modells. diff --git a/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java b/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java index 4f9748c..451c395 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/AnimPreviewState.java @@ -74,6 +74,17 @@ public class AnimPreviewState extends BaseAppState { private String currentModelPath; private Action currentAction; 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; @@ -146,8 +157,12 @@ public class AnimPreviewState extends BaseAppState { String playClip = input.animPreviewPlayClip; if (playClip != null) { input.animPreviewPlayClip = null; - if (playClip.isEmpty()) stopAll(); - else playClip(playClip); + if (playClip.isEmpty()) { + input.animPreviewSequenceQueue.clear(); + stopAll(); + } else { + playClip(playClip); + } } String addAnimPath = input.animImportQueue.poll(); @@ -204,7 +219,11 @@ public class AnimPreviewState extends BaseAppState { stopAll(); } else if (time >= length - 0.02) { 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); if (currentAction != null) currentAction.setSpeed(input.animPreviewSpeed); } else { @@ -239,14 +258,27 @@ public class AnimPreviewState extends BaseAppState { 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 (input.animPreviewOffsetActive) { - currentModel.setLocalTranslation( - input.animPreviewOffsetTx, input.animPreviewOffsetTy, input.animPreviewOffsetTz); - } else { - currentModel.setLocalTranslation(Vector3f.ZERO); + float tgtX = input.animPreviewOffsetActive ? input.animPreviewOffsetTx : 0f; + float tgtY = input.animPreviewOffsetActive ? input.animPreviewOffsetTy : 0f; + float tgtZ = input.animPreviewOffsetActive ? input.animPreviewOffsetTz : 0f; + if (tgtX != prevOffTx || tgtY != prevOffTy || tgtZ != prevOffTz) { + 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); @@ -261,6 +293,10 @@ public class AnimPreviewState extends BaseAppState { currentModel = null; currentModelPath = null; currentAction = null; + previewOffsetCurrent.set(0f, 0f, 0f); + previewOffsetFrom.set(0f, 0f, 0f); + prevOffTx = 0f; prevOffTy = 0f; prevOffTz = 0f; + transitionElapsed = TRANSITION_DURATION; try { Spatial model = loadFresh(assetPath); @@ -391,6 +427,19 @@ public class AnimPreviewState extends BaseAppState { if (currentModel == null) return; currentAction = 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 // SkinningControl sitzen oft auf verschiedenen Geschwisterknoten. setSkinningEnabled(currentModel, true); @@ -587,12 +636,16 @@ public class AnimPreviewState extends BaseAppState { AnimClip result; if (!retarget) { result = clip; - } else { - // Immer erst redirect-by-name wie FbxTestApp: mappt alle passenden Joints, - // droppt nur die wenigen fehlenden ($AssimpFbx$-Varianten die im Model nicht existieren). + } else if (isJ3oSource) { + // j3o-Clips wurden bereits beim FBX-Import normalisiert (in Modell-Raum gespeichert). + // Plain-Retarget wie FbxTestApp.retargetClip: nur Joint-Targets umbiegen, keine Delta-Korrektur. 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) { - 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); } } @@ -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). */ + /** + * 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) { + List> 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 orphanByParent = new java.util.HashMap<>(); + java.util.Map 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> newTracks = new ArrayList<>(); for (com.jme3.anim.AnimTrack track : src.getTracks()) { 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()); 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(); if (translations != null && translations.length > 0) { - com.jme3.math.Vector3f srcInitT = srcJoint.getInitialTransform().getTranslation(); - com.jme3.math.Vector3f dstInitT = dstJoint.getInitialTransform().getTranslation(); - com.jme3.math.Vector3f delta = dstInitT.subtract(srcInitT); - 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.anim.TransformTrack orphan = orphanByParent.get(srcJoint.getName()); + com.jme3.math.Vector3f oInit = orphanInitT.get(srcJoint.getName()); + com.jme3.math.Vector3f[] oT = orphan != null ? orphan.getTranslations() : null; + 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(), translations, 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()); + LOG.debug("[AnimPreview] normalizeAndRetarget '{}': {} Tracks, length={}", src.getName(), newTracks.size(), result.getLength()); return result; } diff --git a/blight-game/src/main/java/de/blight/game/animation/AnimSet.java b/blight-game/src/main/java/de/blight/game/animation/AnimSet.java index 0281ac5..3c4f14b 100644 --- a/blight-game/src/main/java/de/blight/game/animation/AnimSet.java +++ b/blight-game/src/main/java/de/blight/game/animation/AnimSet.java @@ -30,6 +30,8 @@ public class AnimSet { private Map animOffsets = new LinkedHashMap<>(); /** Sub-Clip-Definitionen: Name → Zeitfenster aus einem kombinierten Quell-Clip. */ private Map subClips = new LinkedHashMap<>(); + /** Ablauf-Definitionen: Name → geordnete Liste von Aktions-Namen (aus actionMap). */ + private Map> sequences = new LinkedHashMap<>(); /** Beschreibt einen Sub-Clip: Zeitfenster [start, end] innerhalb eines kombinierten Quell-Clips. */ public static class SubClipDef { @@ -54,6 +56,10 @@ public class AnimSet { return subClips != null ? subClips : new LinkedHashMap<>(); } public void setSubClips(Map subClips) { this.subClips = subClips; } + public Map> getSequences() { + return sequences != null ? sequences : new LinkedHashMap<>(); + } + public void setSequences(Map> sequences) { this.sequences = sequences; } /** Speichert dieses Set als {@code .animset.json} im Verzeichnis {@code setDir}. */ public void save(Path setDir, String setName) throws IOException {