Umbau der Animations Editor Geschichte
This commit is contained in:
@@ -202,7 +202,6 @@ public class EditorApp extends Application {
|
||||
private ListView<String> animSetActionListView;
|
||||
private String animSetPendingPlayClip = null;
|
||||
private ComboBox<String> 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<String> animSetPartsListView;
|
||||
private java.util.Map<String, de.blight.game.animation.AnimSet.SubClipDef>
|
||||
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
|
||||
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<String> 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<String> 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<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) {
|
||||
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);
|
||||
|
||||
@@ -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<String> animPreviewSequenceQueue = new ConcurrentLinkedQueue<>();
|
||||
/** JavaFX → JME3: Animation-j3o-Pfad zum Retargeting + Hinzufügen. null = kein Auftrag. */
|
||||
public final ConcurrentLinkedQueue<String> 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<String, float[]> animPreviewClipOffsets =
|
||||
new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* JME3 → JavaFX: Relativer Asset-Pfad des gerade geladenen Modells.
|
||||
|
||||
@@ -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<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<>();
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user