Zwischenstand commited
This commit is contained in:
BIN
assets/imported/animations/Disappointed.fbx
Normal file
BIN
assets/imported/animations/Disappointed.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Idle.fbx
Normal file
BIN
assets/imported/animations/Idle.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Jumping.fbx
Normal file
BIN
assets/imported/animations/Jumping.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Kneeling Down.fbx
Normal file
BIN
assets/imported/animations/Kneeling Down.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Kneeling.fbx
Normal file
BIN
assets/imported/animations/Kneeling.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Lifting.fbx
Normal file
BIN
assets/imported/animations/Lifting.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Lying Down.fbx
Normal file
BIN
assets/imported/animations/Lying Down.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Male Crouch Pose.fbx
Normal file
BIN
assets/imported/animations/Male Crouch Pose.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Male Laying Pose(1).fbx
Normal file
BIN
assets/imported/animations/Male Laying Pose(1).fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Male Laying Pose.fbx
Normal file
BIN
assets/imported/animations/Male Laying Pose.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Running Jump.fbx
Normal file
BIN
assets/imported/animations/Running Jump.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Running.fbx
Normal file
BIN
assets/imported/animations/Running.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sit To Stand.fbx
Normal file
BIN
assets/imported/animations/Sit To Stand.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sitting Idle(1).fbx
Normal file
BIN
assets/imported/animations/Sitting Idle(1).fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sitting Idle.fbx
Normal file
BIN
assets/imported/animations/Sitting Idle.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sitting Talking(1).fbx
Normal file
BIN
assets/imported/animations/Sitting Talking(1).fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sitting Talking.fbx
Normal file
BIN
assets/imported/animations/Sitting Talking.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sitting.fbx
Normal file
BIN
assets/imported/animations/Sitting.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sneaking Forward.fbx
Normal file
BIN
assets/imported/animations/Sneaking Forward.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sprint.fbx
Normal file
BIN
assets/imported/animations/Sprint.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Sprinting Forward Roll.fbx
Normal file
BIN
assets/imported/animations/Sprinting Forward Roll.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Stand To Roll.fbx
Normal file
BIN
assets/imported/animations/Stand To Roll.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Stand To Sit.fbx
Normal file
BIN
assets/imported/animations/Stand To Sit.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Standing Up(1).fbx
Normal file
BIN
assets/imported/animations/Standing Up(1).fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Standing Up.fbx
Normal file
BIN
assets/imported/animations/Standing Up.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Standing.fbx
Normal file
BIN
assets/imported/animations/Standing.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/T-Pose.fbx
Normal file
BIN
assets/imported/animations/T-Pose.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Talking(1).fbx
Normal file
BIN
assets/imported/animations/Talking(1).fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Talking.fbx
Normal file
BIN
assets/imported/animations/Talking.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Walking.fbx
Normal file
BIN
assets/imported/animations/Walking.fbx
Normal file
Binary file not shown.
BIN
assets/imported/animations/Yelling.fbx
Normal file
BIN
assets/imported/animations/Yelling.fbx
Normal file
Binary file not shown.
BIN
assets/imported/models/hero.fbx
Normal file
BIN
assets/imported/models/hero.fbx
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/Characters/Models/hero.j3o
Normal file
BIN
blight-assets/src/main/resources/Characters/Models/hero.j3o
Normal file
Binary file not shown.
Binary file not shown.
BIN
blight-assets/src/main/resources/Characters/clips/Idle.j3o
Normal file
BIN
blight-assets/src/main/resources/Characters/clips/Idle.j3o
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/Characters/clips/Jumping.j3o
Normal file
BIN
blight-assets/src/main/resources/Characters/clips/Jumping.j3o
Normal file
Binary file not shown.
Binary file not shown.
BIN
blight-assets/src/main/resources/Characters/clips/Kneeling.j3o
Normal file
BIN
blight-assets/src/main/resources/Characters/clips/Kneeling.j3o
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/Characters/clips/Lifting.j3o
Normal file
BIN
blight-assets/src/main/resources/Characters/clips/Lifting.j3o
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/Characters/clips/Lying Down.j3o
Normal file
BIN
blight-assets/src/main/resources/Characters/clips/Lying Down.j3o
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
blight-assets/src/main/resources/Characters/clips/Running.j3o
Normal file
BIN
blight-assets/src/main/resources/Characters/clips/Running.j3o
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
blight-assets/src/main/resources/Characters/clips/Sitting.j3o
Normal file
BIN
blight-assets/src/main/resources/Characters/clips/Sitting.j3o
Normal file
Binary file not shown.
Binary file not shown.
BIN
blight-assets/src/main/resources/Characters/clips/Sprint.j3o
Normal file
BIN
blight-assets/src/main/resources/Characters/clips/Sprint.j3o
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
blight-assets/src/main/resources/Characters/clips/Standing.j3o
Normal file
BIN
blight-assets/src/main/resources/Characters/clips/Standing.j3o
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/Characters/clips/T-Pose.j3o
Normal file
BIN
blight-assets/src/main/resources/Characters/clips/T-Pose.j3o
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/Characters/clips/Talking(1).j3o
Normal file
BIN
blight-assets/src/main/resources/Characters/clips/Talking(1).j3o
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/Characters/clips/Talking.j3o
Normal file
BIN
blight-assets/src/main/resources/Characters/clips/Talking.j3o
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/Characters/clips/Walking.j3o
Normal file
BIN
blight-assets/src/main/resources/Characters/clips/Walking.j3o
Normal file
Binary file not shown.
BIN
blight-assets/src/main/resources/Characters/clips/Yelling.j3o
Normal file
BIN
blight-assets/src/main/resources/Characters/clips/Yelling.j3o
Normal file
Binary file not shown.
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"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"
|
||||
],
|
||||
"actionMap": {},
|
||||
"previewModelPath": "Characters/Models/hero.j3o",
|
||||
"animOffsets": {
|
||||
"sitting": {
|
||||
"tx": 0.0,
|
||||
"ty": 0.0,
|
||||
"tz": -0.5,
|
||||
"rx": 0.0,
|
||||
"ry": 0.0,
|
||||
"rz": 0.0
|
||||
},
|
||||
"get_up_sitting": {
|
||||
"tx": 0.0,
|
||||
"ty": 0.0,
|
||||
"tz": -0.5,
|
||||
"rx": 0.0,
|
||||
"ry": 0.0,
|
||||
"rz": 0.0
|
||||
}
|
||||
},
|
||||
"subClips": {}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,53 +0,0 @@
|
||||
{
|
||||
"clips": [
|
||||
"alive_again",
|
||||
"idle",
|
||||
"idle_jump",
|
||||
"pickup",
|
||||
"running",
|
||||
"running_jump",
|
||||
"sit_down_bench",
|
||||
"sitting_bench",
|
||||
"sitting_floor",
|
||||
"sprinting",
|
||||
"stand_up",
|
||||
"stand_up_bench",
|
||||
"tpose",
|
||||
"walking",
|
||||
"sitting_talking"
|
||||
],
|
||||
"actionMap": {
|
||||
"DEFAULT": "tpose",
|
||||
"IDLE": "idle",
|
||||
"WALK": "walking",
|
||||
"RUN": "running",
|
||||
"SPRINT": "sprinting",
|
||||
"JUMP": "idle_jump",
|
||||
"RUNNING_JUMP": "running_jump",
|
||||
"PICK_UP": "pickup",
|
||||
"SIT_DOWN": "sit_down_bench",
|
||||
"SIT_UP": "stand_up_bench",
|
||||
"SITTING": "sitting_bench",
|
||||
"REVIVE": "alive_again"
|
||||
},
|
||||
"previewModelPath": "Models/Chars/mainchar.j3o",
|
||||
"animOffsets": {
|
||||
"sitting": {
|
||||
"tx": 0.0,
|
||||
"ty": 0.0,
|
||||
"tz": -0.5,
|
||||
"rx": 0.0,
|
||||
"ry": 0.0,
|
||||
"rz": 0.0
|
||||
},
|
||||
"get_up_sitting": {
|
||||
"tx": 0.0,
|
||||
"ty": 0.0,
|
||||
"tz": -0.5,
|
||||
"rx": 0.0,
|
||||
"ry": 0.0,
|
||||
"rz": 0.0
|
||||
}
|
||||
},
|
||||
"subClips": {}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ dependencies {
|
||||
implementation "org.jmonkeyengine:jme3-effects:${jmeVersion}"
|
||||
implementation "org.jmonkeyengine:jme3-plugins:${jmeVersion}"
|
||||
implementation "org.jmonkeyengine:jme3-testdata:${jmeVersion}"
|
||||
implementation 'com.github.stephengold:MonkeyWrench:1.0.0'
|
||||
implementation 'com.google.code.gson:gson:2.11.0'
|
||||
implementation 'org.slf4j:slf4j-api:2.0.17'
|
||||
implementation 'org.slf4j:jul-to-slf4j:2.0.17'
|
||||
|
||||
@@ -190,7 +190,6 @@ public class EditorApp extends Application {
|
||||
private ListView<String> animClipListView;
|
||||
private Label animPreviewStatusLabel;
|
||||
private ComboBox<String> animPreviewModelCombo;
|
||||
private ComboBox<String> addAnimComboField;
|
||||
private double animPrevDragX, animPrevDragY;
|
||||
// Cache: Menge aller j3o-Pfade die ein Skelett besitzen (null = Scan noch nicht abgeschlossen)
|
||||
private java.util.Set<String> skeletalModelPaths = null;
|
||||
@@ -413,6 +412,7 @@ public class EditorApp extends Application {
|
||||
private TreeItem<String> jmeModelsNode;
|
||||
private TreeItem<String> jmeTexturesNode;
|
||||
private TreeItem<String> animationsNode;
|
||||
private TreeItem<String> charactersNode;
|
||||
private TreeItem<String> itemsNode;
|
||||
|
||||
// ── JavaFX Entry-Point ───────────────────────────────────────────────────
|
||||
@@ -620,8 +620,8 @@ public class EditorApp extends Application {
|
||||
}
|
||||
if (input.animImportCompleted) {
|
||||
input.animImportCompleted = false;
|
||||
refreshAddAnimCombo(addAnimComboField);
|
||||
refreshCategoryNode(animationsNode, shouldPreserveExpansion());
|
||||
if (charactersNode != null) refreshCategoryNode(charactersNode, shouldPreserveExpansion());
|
||||
if (animationsNode != null) refreshCategoryNode(animationsNode, shouldPreserveExpansion());
|
||||
}
|
||||
|
||||
String animOp = input.animOpStatus;
|
||||
@@ -630,6 +630,33 @@ public class EditorApp extends Application {
|
||||
if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText(animOp);
|
||||
}
|
||||
|
||||
SharedInput.CharModelImportResult charImport = input.charModelImportResult.getAndSet(null);
|
||||
if (charImport != null) {
|
||||
if (charImport.error() != null) {
|
||||
javafx.scene.control.Alert alert = new javafx.scene.control.Alert(
|
||||
javafx.scene.control.Alert.AlertType.ERROR);
|
||||
alert.initOwner(primaryStage);
|
||||
alert.setTitle("Character-Import fehlgeschlagen");
|
||||
alert.setHeaderText("FBX konnte nicht als Character-Modell importiert werden");
|
||||
alert.setContentText(charImport.error());
|
||||
alert.showAndWait();
|
||||
if (charEditorStatusLabel != null)
|
||||
charEditorStatusLabel.setText("Import fehlgeschlagen");
|
||||
} else {
|
||||
String relPath = charImport.relPath();
|
||||
if (charModelCombo != null && !charModelCombo.getItems().contains(relPath)) {
|
||||
charModelCombo.getItems().add(relPath);
|
||||
}
|
||||
if (charModelCombo != null) charModelCombo.setValue(relPath);
|
||||
if (charactersNode != null) refreshCategoryNode(charactersNode, shouldPreserveExpansion());
|
||||
if (animSetModelCombo != null && !animSetModelCombo.getItems().contains(relPath))
|
||||
animSetModelCombo.getItems().add(relPath);
|
||||
if (charEditorStatusLabel != null)
|
||||
charEditorStatusLabel.setText("Modell importiert: " + relPath);
|
||||
setStatus("Character-Modell importiert: " + relPath);
|
||||
}
|
||||
}
|
||||
|
||||
String embedStatus = input.animEmbedStatus;
|
||||
if (embedStatus != null) {
|
||||
input.animEmbedStatus = null;
|
||||
@@ -663,9 +690,9 @@ public class EditorApp extends Application {
|
||||
if (input.refreshAssets) {
|
||||
input.refreshAssets = false;
|
||||
boolean pe = shouldPreserveExpansion();
|
||||
refreshCategoryNode(modelsNode, pe);
|
||||
refreshCategoryNode(animationsNode, pe);
|
||||
refreshAddAnimCombo(addAnimComboField);
|
||||
if (modelsNode != null) refreshCategoryNode(modelsNode, pe);
|
||||
if (animationsNode != null) refreshCategoryNode(animationsNode, pe);
|
||||
if (charactersNode != null) refreshCategoryNode(charactersNode, pe);
|
||||
}
|
||||
|
||||
if (input.refreshTreeFolders) {
|
||||
@@ -1494,26 +1521,29 @@ public class EditorApp extends Application {
|
||||
MenuItem importTexSetItem = new MenuItem("Textur-Set importieren…");
|
||||
MenuItem importAudioItem = new MenuItem("Audio…");
|
||||
MenuItem importAnimItem = new MenuItem("Animationen…");
|
||||
MenuItem importCharModelItem = new MenuItem("Character-Modell (FBX)…");
|
||||
importModelLodItem.setOnAction(e -> openModelImport(primaryStage));
|
||||
importTexItem.setOnAction(e -> handleTextureImport(primaryStage));
|
||||
importTexZipItem.setOnAction(e -> handleZipTextureImport(primaryStage));
|
||||
importTexSetItem.setOnAction(e -> openTextureSetImport(primaryStage));
|
||||
importAudioItem.setOnAction(e -> handleAudioImport(primaryStage));
|
||||
importAnimItem.setOnAction(e -> handleAnimationImport(primaryStage));
|
||||
importCharModelItem.setOnAction(e -> handleCharacterModelImport(primaryStage));
|
||||
importMenu.getItems().addAll(importModelLodItem, importTexItem, importTexZipItem,
|
||||
importTexSetItem, importAudioItem, importAnimItem);
|
||||
importTexSetItem, importAudioItem, importAnimItem,
|
||||
new javafx.scene.control.SeparatorMenuItem(), importCharModelItem);
|
||||
|
||||
Menu toolsMenu = new Menu("Werkzeuge");
|
||||
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("Animationseditor");
|
||||
MenuItem animPrevItem = new MenuItem("Character-Editor");
|
||||
MenuItem objEditorItem = new MenuItem("Model Editor");
|
||||
MenuItem thumbItem = new MenuItem("Thumbnail-Verwaltung");
|
||||
vegetationsItem.setOnAction(e -> switchToVegetationGenerator());
|
||||
ezTreeItem.setOnAction(e -> switchToEzTree());
|
||||
tripoItem.setOnAction(e -> switchToTripo());
|
||||
animPrevItem.setOnAction(e -> switchToAnimPreview());
|
||||
animPrevItem.setOnAction(e -> openCharacterEditor());
|
||||
objEditorItem.setOnAction(e -> openModelEditor(null, null));
|
||||
thumbItem.setOnAction(e -> switchToThumbnailManager());
|
||||
toolsMenu.getItems().addAll(vegetationsItem, ezTreeItem, tripoItem,
|
||||
@@ -3410,7 +3440,7 @@ public class EditorApp extends Application {
|
||||
ComboBox<String> animCombo = new ComboBox<>();
|
||||
animCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
animCombo.getItems().add("(keine)");
|
||||
Path animDir = ASSET_ROOT.resolve("animations");
|
||||
Path animDir = ASSET_ROOT.resolve("Characters").resolve("clips");
|
||||
if (java.nio.file.Files.isDirectory(animDir)) {
|
||||
try (java.util.stream.Stream<Path> walk = java.nio.file.Files.walk(animDir)) {
|
||||
walk.filter(p -> p.toString().endsWith(".j3o"))
|
||||
@@ -5069,6 +5099,7 @@ public class EditorApp extends Application {
|
||||
assetTree = tree;
|
||||
tree.setShowRoot(false);
|
||||
VBox.setVgrow(tree, Priority.ALWAYS);
|
||||
tree.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE);
|
||||
tree.setCellFactory(tv -> buildAssetCell());
|
||||
|
||||
// F2 → Umbenennen
|
||||
@@ -5076,7 +5107,8 @@ public class EditorApp extends Application {
|
||||
if (e.getCode() != KeyCode.F2) return;
|
||||
TreeItem<String> sel = tree.getSelectionModel().getSelectedItem();
|
||||
if (sel == null || sel == modelsNode || sel == texturesNode
|
||||
|| sel == audioNode || sel == animationsNode || sel == itemsNode) return;
|
||||
|| sel == audioNode || sel == animationsNode || sel == itemsNode
|
||||
|| sel == charactersNode) return;
|
||||
renameAsset(sel);
|
||||
e.consume();
|
||||
});
|
||||
@@ -5116,9 +5148,9 @@ public class EditorApp extends Application {
|
||||
boolean isSkeletal = skeletalModelPaths == null /* noch nicht gescannt – zulassen */
|
||||
|| skeletalModelPaths.contains(relPath);
|
||||
|
||||
// Im Animationseditor: j3o aus Models/ oder animations/ direkt laden
|
||||
// Im Animations Viewer: j3o direkt laden
|
||||
if ("animpreview".equals(currentTool)
|
||||
&& (cat == modelsNode || cat == animationsNode)
|
||||
&& (cat == modelsNode || cat == animationsNode || cat == charactersNode)
|
||||
&& relPath.endsWith(".j3o")) {
|
||||
if (!isSkeletal) { setStatus("Kein Skelett: " + relPath); return; }
|
||||
input.animPreviewLoadPath = relPath;
|
||||
@@ -5148,7 +5180,7 @@ public class EditorApp extends Application {
|
||||
if (objPlaceBtn != null) objPlaceBtn.setSelected(true);
|
||||
root.setRight(buildObjectPlacePanel());
|
||||
setStatus("Textur: " + relPath + " | Wird auf platzierte Primitive angewendet");
|
||||
} else if (cat == animationsNode && relPath.endsWith(".j3o") && isSkeletal) {
|
||||
} else if ((cat == animationsNode || cat == charactersNode) && relPath.endsWith(".j3o") && isSkeletal) {
|
||||
switchToAnimPreview();
|
||||
input.animPreviewLoadPath = relPath;
|
||||
if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade…");
|
||||
@@ -5179,7 +5211,10 @@ public class EditorApp extends Application {
|
||||
miTexZip.setOnAction(ev -> handleZipTextureImport(importBtn.getScene().getWindow()));
|
||||
miAudio.setOnAction(ev -> handleAudioImport(importBtn.getScene().getWindow()));
|
||||
miAnim.setOnAction(ev -> handleAnimationImport(importBtn.getScene().getWindow()));
|
||||
popup.getItems().addAll(miModel, miTex, miTexZip, miAudio, miAnim);
|
||||
MenuItem miCharModel = new MenuItem("Character-Modell (FBX)…");
|
||||
miCharModel.setOnAction(ev -> handleCharacterModelImport(importBtn.getScene().getWindow()));
|
||||
popup.getItems().addAll(miModel, miTex, miTexZip, miAudio, miAnim,
|
||||
new javafx.scene.control.SeparatorMenuItem(), miCharModel);
|
||||
popup.show(importBtn, javafx.geometry.Side.TOP, 0, 0);
|
||||
});
|
||||
|
||||
@@ -5396,7 +5431,7 @@ public class EditorApp extends Application {
|
||||
if (jmePaths.containsKey(item) || jmeFolderNodes.contains(item)) return;
|
||||
// Kategorie-Wurzeln (Models, Textures, …) nicht verschiebbar
|
||||
if (item == modelsNode || item == texturesNode || item == audioNode
|
||||
|| item == animationsNode || item == itemsNode) return;
|
||||
|| item == animationsNode || item == itemsNode || item == charactersNode) return;
|
||||
draggedItem = item;
|
||||
Dragboard db = cell.startDragAndDrop(TransferMode.MOVE);
|
||||
ClipboardContent cc = new ClipboardContent();
|
||||
@@ -5477,6 +5512,53 @@ public class EditorApp extends Application {
|
||||
if (jmeFolderNodes.contains(item) || jmePaths.containsKey(item)) return;
|
||||
ContextMenu ctx = new ContextMenu();
|
||||
|
||||
// ── Multi-Select: Auswahl löschen ────────────────────────────────
|
||||
var selectedItems = assetTree.getSelectionModel().getSelectedItems();
|
||||
java.util.List<TreeItem<String>> deletableSelection = selectedItems.stream()
|
||||
.filter(si -> si != null && !isAssetFolder(si)
|
||||
&& itemPaths.containsKey(si)
|
||||
&& !jmePaths.containsKey(si)
|
||||
&& !jmeFolderNodes.contains(si))
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
if (deletableSelection.size() > 1) {
|
||||
MenuItem multiDel = new MenuItem("🗑 Auswahl löschen (" + deletableSelection.size() + " Dateien)");
|
||||
multiDel.setStyle("-fx-text-fill: #c0392b; -fx-font-weight: bold;");
|
||||
multiDel.setOnAction(ev -> {
|
||||
StringBuilder names = new StringBuilder();
|
||||
for (int i = 0; i < Math.min(5, deletableSelection.size()); i++) {
|
||||
names.append(" ").append(deletableSelection.get(i).getValue()).append('\n');
|
||||
}
|
||||
if (deletableSelection.size() > 5)
|
||||
names.append(" … und ").append(deletableSelection.size() - 5).append(" weitere");
|
||||
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION,
|
||||
deletableSelection.size() + " Dateien löschen?\n\n" + names,
|
||||
ButtonType.OK, ButtonType.CANCEL);
|
||||
confirm.initOwner(primaryStage);
|
||||
confirm.setHeaderText(null);
|
||||
confirm.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> {
|
||||
int deleted = 0;
|
||||
for (TreeItem<String> si : new java.util.ArrayList<>(deletableSelection)) {
|
||||
Path p = itemPaths.get(si);
|
||||
if (p == null) continue;
|
||||
try {
|
||||
Files.deleteIfExists(p);
|
||||
if (p.toString().endsWith(".j3o")) deleteJ3oSideFiles(p);
|
||||
si.getParent().getChildren().remove(si);
|
||||
itemPaths.remove(si);
|
||||
deleted++;
|
||||
} catch (IOException ex) {
|
||||
setStatus("Fehler beim Löschen: " + p.getFileName());
|
||||
}
|
||||
}
|
||||
setStatus(deleted + " Datei(en) gelöscht");
|
||||
});
|
||||
});
|
||||
ctx.getItems().addAll(multiDel, new SeparatorMenuItem());
|
||||
ctx.show(cell, e.getScreenX(), e.getScreenY());
|
||||
e.consume();
|
||||
return;
|
||||
}
|
||||
|
||||
// Texture-Set-Knoten: eigenes Menü
|
||||
if (textureSetNodes.contains(item)) {
|
||||
Path setDir = itemPaths.get(item);
|
||||
@@ -5527,7 +5609,8 @@ public class EditorApp extends Application {
|
||||
ctx.getItems().add(newSub);
|
||||
|
||||
boolean isCatRoot = (item == modelsNode || item == texturesNode
|
||||
|| item == audioNode || item == animationsNode || item == itemsNode);
|
||||
|| item == audioNode || item == animationsNode || item == itemsNode
|
||||
|| item == charactersNode);
|
||||
Path dir = itemPaths.get(item);
|
||||
|
||||
// Ordner unter Models: Zufällig-Platzieren-Eintrag
|
||||
@@ -5554,7 +5637,15 @@ public class EditorApp extends Application {
|
||||
ctx.getItems().add(importAnim);
|
||||
}
|
||||
|
||||
Path setsDir = ASSET_ROOT.resolve("animations").resolve("sets");
|
||||
if (item == charactersNode) {
|
||||
MenuItem importChar = new MenuItem("⊕ Character-Modell importieren (FBX)…");
|
||||
importChar.setOnAction(ev -> handleCharacterModelImport(ctx.getOwnerWindow()));
|
||||
MenuItem importAnim = new MenuItem("⊕ Animation hinzufügen…");
|
||||
importAnim.setOnAction(ev -> handleAnimationImport(ctx.getOwnerWindow()));
|
||||
ctx.getItems().addAll(importChar, importAnim);
|
||||
}
|
||||
|
||||
Path setsDir = ASSET_ROOT.resolve("Characters").resolve("sets");
|
||||
if (dir != null && setsDir.equals(dir)) {
|
||||
MenuItem newSet = new MenuItem("➕ Neues Set…");
|
||||
newSet.setOnAction(ev -> createNewAnimSet());
|
||||
@@ -5714,11 +5805,11 @@ public class EditorApp extends Application {
|
||||
return p != null && Files.isDirectory(p);
|
||||
}
|
||||
|
||||
/** Findet die übergeordnete Kategorie (modelsNode / texturesNode / audioNode / animationsNode / itemsNode). */
|
||||
/** Findet die übergeordnete Kategorie (modelsNode / texturesNode / audioNode / animationsNode / itemsNode / charactersNode). */
|
||||
private TreeItem<String> getCategoryRoot(TreeItem<String> item) {
|
||||
for (TreeItem<String> cur = item; cur != null; cur = cur.getParent())
|
||||
if (cur == modelsNode || cur == texturesNode || cur == audioNode
|
||||
|| cur == animationsNode || cur == itemsNode) return cur;
|
||||
|| cur == animationsNode || cur == itemsNode || cur == charactersNode) return cur;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -6161,6 +6252,7 @@ public class EditorApp extends Application {
|
||||
private void populateAssetTree(TreeItem<String> root) {
|
||||
modelsNode = null; texturesNode = null;
|
||||
audioNode = null; animationsNode = null; itemsNode = null;
|
||||
charactersNode = null;
|
||||
jmeModelsNode = null; jmeTexturesNode = null;
|
||||
jmeFolderNodes.clear();
|
||||
textureSetNodes.clear();
|
||||
@@ -6203,6 +6295,7 @@ public class EditorApp extends Application {
|
||||
animationsNode = node;
|
||||
addAnimClipSubNodes(node);
|
||||
}
|
||||
case "characters" -> charactersNode = node;
|
||||
case "items" -> itemsNode = node;
|
||||
}
|
||||
}
|
||||
@@ -6279,7 +6372,7 @@ public class EditorApp extends Application {
|
||||
|
||||
private void clearItemPathsFor(TreeItem<String> item) {
|
||||
for (TreeItem<String> child : item.getChildren()) clearItemPathsFor(child);
|
||||
if (item != modelsNode && item != texturesNode && item != audioNode && item != animationsNode)
|
||||
if (item != modelsNode && item != texturesNode && item != audioNode && item != animationsNode && item != charactersNode)
|
||||
itemPaths.remove(item);
|
||||
textureSetNodes.remove(item);
|
||||
}
|
||||
@@ -10256,6 +10349,63 @@ public class EditorApp extends Application {
|
||||
|
||||
// ── AnimSet-Editor ────────────────────────────────────────────────────────
|
||||
|
||||
private void openCharacterEditor() {
|
||||
Path setsDir = ASSET_ROOT.resolve("Characters").resolve("sets");
|
||||
java.util.List<String> available = new java.util.ArrayList<>();
|
||||
if (java.nio.file.Files.isDirectory(setsDir)) {
|
||||
try (var walk = java.nio.file.Files.walk(setsDir, 1)) {
|
||||
walk.filter(p -> p.toString().endsWith(".animset.json"))
|
||||
.map(p -> p.getFileName().toString().replaceFirst("\\.animset\\.json$", ""))
|
||||
.sorted()
|
||||
.forEach(available::add);
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
|
||||
// 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.setHeaderText("AnimSet auswählen oder neu anlegen");
|
||||
|
||||
ListView<String> listView = new ListView<>();
|
||||
listView.getItems().addAll(available);
|
||||
listView.setPrefHeight(200);
|
||||
listView.setPrefWidth(320);
|
||||
|
||||
ButtonType openType = new ButtonType("Öffnen", javafx.scene.control.ButtonBar.ButtonData.OK_DONE);
|
||||
ButtonType newType = new ButtonType("Neu…", javafx.scene.control.ButtonBar.ButtonData.OTHER);
|
||||
ButtonType cancelType = ButtonType.CANCEL;
|
||||
dlg.getDialogPane().getButtonTypes().addAll(openType, newType, cancelType);
|
||||
|
||||
javafx.scene.Node openNode = dlg.getDialogPane().lookupButton(openType);
|
||||
openNode.setDisable(true);
|
||||
listView.getSelectionModel().selectedItemProperty()
|
||||
.addListener((obs, ov, nv) -> openNode.setDisable(nv == null));
|
||||
listView.setOnMouseClicked(e -> {
|
||||
if (e.getClickCount() == 2 && listView.getSelectionModel().getSelectedItem() != null) {
|
||||
dlg.setResult(listView.getSelectionModel().getSelectedItem());
|
||||
dlg.close();
|
||||
}
|
||||
});
|
||||
|
||||
dlg.getDialogPane().setContent(listView);
|
||||
dlg.setResultConverter(bt -> {
|
||||
if (bt == openType) return listView.getSelectionModel().getSelectedItem();
|
||||
if (bt == newType) return "__NEW__";
|
||||
return null;
|
||||
});
|
||||
|
||||
dlg.showAndWait().ifPresent(result -> {
|
||||
if ("__NEW__".equals(result)) {
|
||||
createNewAnimSet();
|
||||
} else if (result != null) {
|
||||
Path setFile = setsDir.resolve(result + ".animset.json");
|
||||
String relPath = ASSET_ROOT.relativize(setFile).toString().replace('\\', '/');
|
||||
openAnimSetEditor(relPath, setFile);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void openAnimSetEditor(String relPath, Path absPath) {
|
||||
String setName = absPath.getFileName().toString().replaceFirst("\\.animset\\.json$", "");
|
||||
Path setDir = absPath.getParent();
|
||||
@@ -10327,9 +10477,9 @@ public class EditorApp extends Application {
|
||||
animSetModelCombo = new ComboBox<>();
|
||||
animSetModelCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
animSetModelCombo.setPromptText("j3o-Datei wählen…");
|
||||
for (String dir : new String[]{"Models", "animations"}) {
|
||||
Path base = ASSET_ROOT.resolve(dir);
|
||||
if (!java.nio.file.Files.isDirectory(base)) continue;
|
||||
{
|
||||
Path base = ASSET_ROOT.resolve("Characters").resolve("Models");
|
||||
if (java.nio.file.Files.isDirectory(base)) {
|
||||
try (var walk = java.nio.file.Files.walk(base)) {
|
||||
walk.filter(p -> p.toString().endsWith(".j3o"))
|
||||
.map(p -> ASSET_ROOT.relativize(p).toString().replace('\\', '/'))
|
||||
@@ -10337,6 +10487,7 @@ public class EditorApp extends Application {
|
||||
.forEach(animSetModelCombo.getItems()::add);
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
}
|
||||
// Gespeicherten Modell-Pfad vorauswählen
|
||||
if (animSet.getPreviewModelPath() != null && !animSet.getPreviewModelPath().isBlank()) {
|
||||
animSetModelCombo.setValue(animSet.getPreviewModelPath());
|
||||
@@ -10352,7 +10503,10 @@ public class EditorApp extends Application {
|
||||
animSet.setPreviewModelPath(path);
|
||||
animSetDirty = true;
|
||||
});
|
||||
inner.getChildren().addAll(animSetModelCombo, loadModelBtn);
|
||||
Button importCharBtn = new Button("Character-Modell importieren (FBX)…");
|
||||
importCharBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
importCharBtn.setOnAction(e -> handleCharacterModelImport(importCharBtn.getScene().getWindow()));
|
||||
inner.getChildren().addAll(animSetModelCombo, loadModelBtn, importCharBtn);
|
||||
|
||||
// Modell beim Öffnen automatisch laden, wenn Pfad bekannt
|
||||
if (animSet.getPreviewModelPath() != null && !animSet.getPreviewModelPath().isBlank()) {
|
||||
@@ -10368,7 +10522,7 @@ public class EditorApp extends Application {
|
||||
animSetClipListView.setPrefHeight(180);
|
||||
editableSubClips = new java.util.LinkedHashMap<>(animSet.getSubClips());
|
||||
|
||||
Path animRootDir = ASSET_ROOT.resolve("animations");
|
||||
Path animRootDir = ASSET_ROOT.resolve("Characters");
|
||||
|
||||
Button addClipBtn = new Button("+ Hinzufügen…");
|
||||
Button removeClipBtn = new Button("- Entfernen");
|
||||
@@ -10411,9 +10565,10 @@ public class EditorApp extends Application {
|
||||
String name = cp.getFileName().toString();
|
||||
boolean isAnim = name.endsWith(".j3o") || name.endsWith(".glb")
|
||||
|| name.endsWith(".gltf") || name.endsWith(".fbx");
|
||||
boolean notSets = !animRootDir.relativize(cp).startsWith("sets");
|
||||
_log.debug("[ClipScan] {} | isAnim={} notSets={}", cp, isAnim, notSets);
|
||||
return isAnim && notSets;
|
||||
java.nio.file.Path rel = animRootDir.relativize(cp);
|
||||
boolean onlyClips = rel.startsWith("clips");
|
||||
_log.debug("[ClipScan] {} | isAnim={} onlyClips={}", cp, isAnim, onlyClips);
|
||||
return isAnim && onlyClips;
|
||||
})
|
||||
.map(cp -> cp.getFileName().toString().replaceFirst("\\.[^.]+$", ""))
|
||||
.distinct()
|
||||
@@ -10486,7 +10641,12 @@ public class EditorApp extends Application {
|
||||
HBox clipBtns = new HBox(6, addClipBtn, removeClipBtn);
|
||||
HBox.setHgrow(addClipBtn, Priority.ALWAYS);
|
||||
HBox.setHgrow(removeClipBtn, Priority.ALWAYS);
|
||||
inner.getChildren().addAll(animSetClipListView, clipBtns);
|
||||
|
||||
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);
|
||||
|
||||
// ── Clip-Teile (Sub-Clips) ────────────────────────────────────────────
|
||||
inner.getChildren().addAll(new Separator(), sectionTitle("Clip-Teile"), new Separator());
|
||||
@@ -10818,14 +10978,14 @@ public class EditorApp extends Application {
|
||||
}
|
||||
|
||||
private String findAnimClipPath(String clipName) {
|
||||
Path animDir = ASSET_ROOT.resolve("animations");
|
||||
Path charDir = ASSET_ROOT.resolve("Characters");
|
||||
for (String ext : new String[]{".j3o", ".glb", ".gltf", ".fbx"}) {
|
||||
if (java.nio.file.Files.exists(animDir.resolve("clips").resolve(clipName + ext)))
|
||||
return "animations/clips/" + clipName + ext;
|
||||
if (java.nio.file.Files.exists(animDir.resolve(clipName + ext)))
|
||||
return "animations/" + clipName + ext;
|
||||
if (java.nio.file.Files.exists(charDir.resolve("clips").resolve(clipName + ext)))
|
||||
return "Characters/clips/" + clipName + ext;
|
||||
if (java.nio.file.Files.exists(charDir.resolve(clipName + ext)))
|
||||
return "Characters/" + clipName + ext;
|
||||
}
|
||||
return "animations/clips/" + clipName + ".j3o";
|
||||
return "Characters/clips/" + clipName + ".j3o";
|
||||
}
|
||||
|
||||
private void showAddActionToSetDialog() {
|
||||
@@ -11041,12 +11201,12 @@ public class EditorApp extends Application {
|
||||
dlg.showAndWait().ifPresent(raw -> {
|
||||
String name = raw.trim();
|
||||
if (name.isBlank()) return;
|
||||
Path setDir = ASSET_ROOT.resolve("animations").resolve("sets");
|
||||
Path setDir = ASSET_ROOT.resolve("Characters").resolve("sets");
|
||||
Path setFile = setDir.resolve(name + ".animset.json");
|
||||
try {
|
||||
Files.createDirectories(setDir);
|
||||
new de.blight.game.animation.AnimSet().save(setDir, name);
|
||||
refreshCategoryNode(animationsNode, shouldPreserveExpansion());
|
||||
refreshCategoryNode(charactersNode, shouldPreserveExpansion());
|
||||
String relPath = ASSET_ROOT.relativize(setFile).toString().replace('\\', '/');
|
||||
openAnimSetEditor(relPath, setFile);
|
||||
} catch (IOException ex) {
|
||||
@@ -11058,7 +11218,7 @@ public class EditorApp extends Application {
|
||||
private ToolBar buildAnimPreviewToolBar() {
|
||||
Button backBtn = new Button("← Welteneditor");
|
||||
backBtn.setOnAction(e -> switchToWorldEditor());
|
||||
Label label = new Label("Animationseditor");
|
||||
Label label = new Label("Animations Viewer");
|
||||
label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
|
||||
ToolBar tb = new ToolBar();
|
||||
tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label);
|
||||
@@ -11069,39 +11229,8 @@ public class EditorApp extends Application {
|
||||
VBox inner = new VBox(8);
|
||||
inner.setPadding(new Insets(10));
|
||||
|
||||
// ── Modell-Auswahl ────────────────────────────────────────────────────
|
||||
inner.getChildren().addAll(sectionTitle("Modell"), new Separator());
|
||||
animPreviewModelCombo = new ComboBox<>();
|
||||
animPreviewModelCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
animPreviewModelCombo.setPromptText("j3o-Datei wählen…");
|
||||
for (String dir : new String[]{"Models", "animations"}) {
|
||||
Path base = ASSET_ROOT.resolve(dir);
|
||||
if (!java.nio.file.Files.isDirectory(base)) continue;
|
||||
try (var walk = java.nio.file.Files.walk(base)) {
|
||||
walk.filter(p -> p.toString().endsWith(".j3o"))
|
||||
.map(p -> ASSET_ROOT.relativize(p).toString().replace('\\', '/'))
|
||||
.sorted()
|
||||
.forEach(animPreviewModelCombo.getItems()::add);
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
|
||||
Button loadBtn = new Button("Laden");
|
||||
loadBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
loadBtn.setStyle("-fx-font-weight: bold;");
|
||||
loadBtn.setOnAction(e -> {
|
||||
String path = animPreviewModelCombo.getValue();
|
||||
if (path == null || path.isEmpty()) return;
|
||||
input.animPreviewLoadPath = path;
|
||||
if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Lade…");
|
||||
if (animClipListView != null) animClipListView.getItems().clear();
|
||||
});
|
||||
Button reimportBtn = new Button("Modell neu importieren (GLB/GLTF)…");
|
||||
reimportBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
reimportBtn.setOnAction(e -> reimportModelForPreview(reimportBtn.getScene().getWindow()));
|
||||
inner.getChildren().addAll(new Label("Modell:"), animPreviewModelCombo, loadBtn, reimportBtn);
|
||||
|
||||
// ── Animationen ───────────────────────────────────────────────────────
|
||||
inner.getChildren().addAll(new Separator(), sectionTitle("Animationen"), new Separator());
|
||||
inner.getChildren().addAll(sectionTitle("Animationen"), new Separator());
|
||||
animClipListView = new ListView<>();
|
||||
animClipListView.setPrefHeight(200);
|
||||
animClipListView.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.SINGLE);
|
||||
@@ -11116,7 +11245,6 @@ public class EditorApp extends Application {
|
||||
if (clip != null) input.animPreviewPlayClip = clip;
|
||||
});
|
||||
stopBtn.setOnAction(e -> input.animPreviewPlayClip = "");
|
||||
// Doppelklick auf Clip → direkt abspielen
|
||||
animClipListView.setOnMouseClicked(e -> {
|
||||
if (e.getClickCount() == 2) {
|
||||
String clip = animClipListView.getSelectionModel().getSelectedItem();
|
||||
@@ -11128,26 +11256,6 @@ public class EditorApp extends Application {
|
||||
HBox.setHgrow(stopBtn, Priority.ALWAYS);
|
||||
inner.getChildren().addAll(animClipListView, btnRow);
|
||||
|
||||
Button renameClipBtn = new Button("Clip umbenennen / exportieren…");
|
||||
renameClipBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
renameClipBtn.setOnAction(e -> {
|
||||
String clip = animClipListView.getSelectionModel().getSelectedItem();
|
||||
if (clip == null) return;
|
||||
javafx.scene.control.TextInputDialog dlg = new javafx.scene.control.TextInputDialog(clip);
|
||||
dlg.setTitle("Clip umbenennen");
|
||||
dlg.setHeaderText("Clip '" + clip + "' unter neuem Namen speichern");
|
||||
dlg.setContentText("Neuer Name:");
|
||||
dlg.showAndWait().ifPresent(newName -> {
|
||||
if (!newName.isBlank()) input.clipRenameRequest.set(new SharedInput.ClipRenameRequest(clip, newName.trim()));
|
||||
});
|
||||
});
|
||||
|
||||
Button saveSetBtn = new Button("Als Animations-Set speichern…");
|
||||
saveSetBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
saveSetBtn.setStyle("-fx-font-weight: bold;");
|
||||
saveSetBtn.setOnAction(e -> showSaveAnimSetDialog());
|
||||
inner.getChildren().addAll(renameClipBtn, saveSetBtn);
|
||||
|
||||
// ── Steuerung ─────────────────────────────────────────────────────────
|
||||
inner.getChildren().addAll(new Separator(), sectionTitle("Steuerung"), new Separator());
|
||||
|
||||
@@ -11163,51 +11271,11 @@ public class EditorApp extends Application {
|
||||
speedSlider.valueProperty().addListener((obs, ov, nv) -> input.animPreviewSpeed = nv.floatValue());
|
||||
inner.getChildren().addAll(loopCB, speedLbl, withField(speedSlider, "%.2f"));
|
||||
|
||||
// ── Clip importieren ──────────────────────────────────────────────────
|
||||
inner.getChildren().addAll(new Separator(), sectionTitle("Clip importieren"), new Separator());
|
||||
|
||||
addAnimComboField = new ComboBox<>();
|
||||
ComboBox<String> addAnimCombo = addAnimComboField;
|
||||
|
||||
// Beim Hinzufügen wird der Clip direkt in animations/clips/ gespeichert
|
||||
Label animHint = new Label("Clip wird retargeted und direkt in animations/clips/ gespeichert. Mixamo: \"With Skin\" wählen.");
|
||||
animHint.setWrapText(true);
|
||||
animHint.setStyle("-fx-font-size: 10; -fx-text-fill: #888;");
|
||||
Button importAnimBtn = new Button("Animation importieren (GLB/GLTF)…");
|
||||
importAnimBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
importAnimBtn.setOnAction(e ->
|
||||
handleAnimationImport(importAnimBtn.getScene().getWindow()));
|
||||
addAnimCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
addAnimCombo.setPromptText("Animation aus animations/ wählen…");
|
||||
refreshAddAnimCombo(addAnimCombo);
|
||||
|
||||
Button addAnimBtn = new Button("Animation hinzufügen");
|
||||
addAnimBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
addAnimBtn.setStyle("-fx-font-weight: bold;");
|
||||
addAnimBtn.setOnAction(e -> {
|
||||
String animPath = addAnimCombo.getValue();
|
||||
if (animPath == null || animPath.isEmpty()) {
|
||||
if (animPreviewStatusLabel != null)
|
||||
animPreviewStatusLabel.setText("Bitte eine Animation auswählen");
|
||||
return;
|
||||
}
|
||||
input.animImportQueue.offer(animPath);
|
||||
if (animPreviewStatusLabel != null) animPreviewStatusLabel.setText("Füge Clips hinzu…");
|
||||
});
|
||||
inner.getChildren().addAll(animHint, importAnimBtn, addAnimCombo, addAnimBtn);
|
||||
|
||||
// ── Diagnose ──────────────────────────────────────────────────────────
|
||||
inner.getChildren().addAll(new Separator(), sectionTitle("Diagnose"), new Separator());
|
||||
Button dumpBtn = new Button("Skelett-Info ins Log dumpen");
|
||||
dumpBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
dumpBtn.setOnAction(e -> input.animDumpRequested = true);
|
||||
inner.getChildren().add(dumpBtn);
|
||||
|
||||
// ── Status ────────────────────────────────────────────────────────────
|
||||
inner.getChildren().addAll(new Separator());
|
||||
animPreviewStatusLabel = new Label("Kein Modell geladen");
|
||||
animPreviewStatusLabel = new Label("");
|
||||
animPreviewStatusLabel.setWrapText(true);
|
||||
animPreviewStatusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #555;");
|
||||
animPreviewStatusLabel.setStyle("-fx-font-size: 11; -fx-text-fill: #c00;");
|
||||
inner.getChildren().add(animPreviewStatusLabel);
|
||||
|
||||
ScrollPane scroll = new ScrollPane(inner);
|
||||
@@ -11221,24 +11289,6 @@ public class EditorApp extends Application {
|
||||
return panel;
|
||||
}
|
||||
|
||||
private void refreshAddAnimCombo(ComboBox<String> combo) {
|
||||
if (combo == null) return;
|
||||
String current = combo.getValue();
|
||||
combo.getItems().clear();
|
||||
Path animBase = ASSET_ROOT.resolve("animations");
|
||||
if (java.nio.file.Files.isDirectory(animBase)) {
|
||||
try (var walk = java.nio.file.Files.walk(animBase)) {
|
||||
walk.filter(ap -> {
|
||||
String s = ap.toString();
|
||||
return s.endsWith(".j3o") || s.endsWith(".glb") || s.endsWith(".gltf");
|
||||
})
|
||||
.map(ap -> ASSET_ROOT.relativize(ap).toString().replace('\\', '/'))
|
||||
.sorted()
|
||||
.forEach(combo.getItems()::add);
|
||||
} catch (IOException ignored) {}
|
||||
}
|
||||
if (current != null && combo.getItems().contains(current)) combo.setValue(current);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kopiert die Original-Quelldatei nach {@code <projekt-root>/assets/imported/<assetType>/}.
|
||||
@@ -11259,9 +11309,9 @@ public class EditorApp extends Application {
|
||||
|
||||
private void handleAnimationImport(javafx.stage.Window owner) {
|
||||
FileChooser fc = new FileChooser();
|
||||
fc.setTitle("Animation importieren (GLB/GLTF)");
|
||||
fc.setTitle("Animation importieren (FBX/GLB/GLTF)");
|
||||
fc.getExtensionFilters().add(
|
||||
new FileChooser.ExtensionFilter("Animationen (GLTF, GLB)", "*.gltf", "*.glb"));
|
||||
new FileChooser.ExtensionFilter("Animationen (FBX, GLTF, GLB)", "*.fbx", "*.gltf", "*.glb"));
|
||||
var files = fc.showOpenMultipleDialog(owner);
|
||||
if (files == null) return;
|
||||
for (File file : files) {
|
||||
@@ -11273,6 +11323,20 @@ public class EditorApp extends Application {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCharacterModelImport(javafx.stage.Window owner) {
|
||||
FileChooser fc = new FileChooser();
|
||||
fc.setTitle("Character-Modell importieren (Mixamo T-Pose FBX)");
|
||||
fc.getExtensionFilters().add(
|
||||
new FileChooser.ExtensionFilter("FBX-Modelle", "*.fbx"));
|
||||
File file = fc.showOpenDialog(owner);
|
||||
if (file == null) return;
|
||||
archiveOriginal(file, "models");
|
||||
input.charModelImportQueue.offer(file.getAbsolutePath());
|
||||
if (charEditorStatusLabel != null)
|
||||
charEditorStatusLabel.setText("Importiere: " + file.getName() + " …");
|
||||
setStatus("Importiere Character-Modell: " + file.getName() + " …");
|
||||
}
|
||||
|
||||
private void reimportModelForPreview(javafx.stage.Window owner) {
|
||||
FileChooser fc = new FileChooser();
|
||||
fc.setTitle("Modell (GLB/GLTF) neu importieren");
|
||||
@@ -11370,7 +11434,7 @@ public class EditorApp extends Application {
|
||||
|
||||
java.util.List<String> allClips = animClipListView.getItems();
|
||||
|
||||
Path setDir = ASSET_ROOT.resolve("animations").resolve("sets");
|
||||
Path setDir = ASSET_ROOT.resolve("Characters").resolve("sets");
|
||||
|
||||
// Aktions-Zuweisung: Liste mit Add/Remove
|
||||
javafx.scene.control.ListView<String> actionList = new javafx.scene.control.ListView<>();
|
||||
@@ -11511,7 +11575,7 @@ public class EditorApp extends Application {
|
||||
topBar.getChildren().set(1, buildCharacterEditorToolBar());
|
||||
dialogEditorView = new de.blight.editor.ui.DialogEditorView();
|
||||
dialogEditorView.setDisable(true);
|
||||
routineEditorView = new de.blight.editor.ui.RoutineEditorView(input, ASSET_ROOT.resolve("character"));
|
||||
routineEditorView = new de.blight.editor.ui.RoutineEditorView(input, ASSET_ROOT.resolve("Characters"));
|
||||
routineEditorView.setDisable(true);
|
||||
monologueEditorView = new de.blight.editor.ui.MonologueEditorView();
|
||||
|
||||
@@ -11690,7 +11754,7 @@ public class EditorApp extends Application {
|
||||
}
|
||||
|
||||
private ScrollPane buildCharacterSettingsPane() {
|
||||
Path charDir = ASSET_ROOT.resolve("character");
|
||||
Path charDir = ASSET_ROOT.resolve("Characters");
|
||||
|
||||
VBox inner = new VBox(8);
|
||||
inner.setPadding(new Insets(10));
|
||||
@@ -11698,7 +11762,13 @@ public class EditorApp extends Application {
|
||||
Button newCharBtn = new Button("Neuer Charakter");
|
||||
newCharBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
newCharBtn.setOnAction(e -> clearCharacterForm());
|
||||
inner.getChildren().add(newCharBtn);
|
||||
|
||||
Button importCharModelBtn = new Button("Character-Modell importieren (FBX)…");
|
||||
importCharModelBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
importCharModelBtn.setStyle("-fx-font-weight: bold;");
|
||||
importCharModelBtn.setOnAction(e -> handleCharacterModelImport(importCharModelBtn.getScene().getWindow()));
|
||||
|
||||
inner.getChildren().addAll(newCharBtn, importCharModelBtn);
|
||||
|
||||
charEditorStatusLabel = new Label("Wähle einen Character im Baum oder erstelle einen neuen");
|
||||
charEditorStatusLabel.setWrapText(true);
|
||||
@@ -11891,7 +11961,7 @@ public class EditorApp extends Application {
|
||||
if (charAnimSetCombo == null) return;
|
||||
String cur = charAnimSetCombo.getValue();
|
||||
charAnimSetCombo.getItems().clear();
|
||||
Path setDir = ASSET_ROOT.resolve("animations").resolve("sets");
|
||||
Path setDir = ASSET_ROOT.resolve("Characters").resolve("sets");
|
||||
if (java.nio.file.Files.isDirectory(setDir)) {
|
||||
try (var walk = java.nio.file.Files.walk(setDir)) {
|
||||
walk.filter(p -> p.toString().endsWith(".animset.json"))
|
||||
@@ -11912,7 +11982,7 @@ public class EditorApp extends Application {
|
||||
charActionLabelsBox.getChildren().add(hint);
|
||||
return;
|
||||
}
|
||||
Path setDir = ASSET_ROOT.resolve("animations").resolve("sets");
|
||||
Path setDir = ASSET_ROOT.resolve("Characters").resolve("sets");
|
||||
de.blight.game.animation.AnimSet animSet;
|
||||
try {
|
||||
animSet = de.blight.game.animation.AnimSet.load(setDir, setName);
|
||||
@@ -12695,7 +12765,7 @@ public class EditorApp extends Application {
|
||||
}
|
||||
|
||||
private void deleteCharacterById(String id) {
|
||||
Path charFile = ASSET_ROOT.resolve("character").resolve(id + ".character");
|
||||
Path charFile = ASSET_ROOT.resolve("Characters").resolve(id + ".character");
|
||||
try {
|
||||
java.nio.file.Files.deleteIfExists(charFile);
|
||||
} catch (IOException ex) {
|
||||
@@ -12728,7 +12798,7 @@ public class EditorApp extends Application {
|
||||
dlg.setContentText("ID:");
|
||||
dlg.showAndWait().ifPresent(newId -> {
|
||||
if (newId == null || newId.isBlank()) return;
|
||||
Path charDir = ASSET_ROOT.resolve("character");
|
||||
Path charDir = ASSET_ROOT.resolve("Characters");
|
||||
Path src = charDir.resolve(sourceId + ".character");
|
||||
Path dest = charDir.resolve(newId.trim() + ".character");
|
||||
try {
|
||||
@@ -12763,7 +12833,7 @@ public class EditorApp extends Application {
|
||||
contentTreeUpdating = true;
|
||||
try {
|
||||
contentCharNode.getChildren().clear();
|
||||
Path charDir = ASSET_ROOT.resolve("character");
|
||||
Path charDir = ASSET_ROOT.resolve("Characters");
|
||||
if (!java.nio.file.Files.isDirectory(charDir)) return;
|
||||
java.util.List<javafx.scene.control.TreeItem<String>> items = new java.util.ArrayList<>();
|
||||
try (var walk = java.nio.file.Files.list(charDir)) {
|
||||
@@ -12780,7 +12850,7 @@ public class EditorApp extends Application {
|
||||
|
||||
private void loadCharacterById(String id) {
|
||||
if (id == null || id.isBlank()) return;
|
||||
Path charDir = ASSET_ROOT.resolve("character");
|
||||
Path charDir = ASSET_ROOT.resolve("Characters");
|
||||
try {
|
||||
de.blight.common.model.GameCharacter c =
|
||||
de.blight.common.model.CharacterIO.load(charDir.resolve(id + ".character"));
|
||||
|
||||
@@ -619,6 +619,12 @@ public class SharedInput {
|
||||
public volatile String animPreviewPlayClip = null;
|
||||
/** 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/). */
|
||||
public final ConcurrentLinkedQueue<String> charModelImportQueue = new ConcurrentLinkedQueue<>();
|
||||
/** JME3 → JavaFX: Ergebnis des Character-Modell-Imports. relPath=null wenn Fehler. */
|
||||
public record CharModelImportResult(String relPath, String error) {}
|
||||
public final java.util.concurrent.atomic.AtomicReference<CharModelImportResult>
|
||||
charModelImportResult = new java.util.concurrent.atomic.AtomicReference<>();
|
||||
/** JavaFX → JME3: Clip-Name zum Entfernen aus dem geladenen Modell. null = kein Auftrag. */
|
||||
public volatile String animPreviewRemoveClip = null;
|
||||
/** JavaFX → JME3: Scan aller j3o auf Skelett-Controls anstoßen. */
|
||||
|
||||
@@ -85,6 +85,8 @@ public class AnimPreviewState extends BaseAppState {
|
||||
protected void initialize(Application app) {
|
||||
this.app = (SimpleApplication) app;
|
||||
this.assets = app.getAssetManager();
|
||||
assets.registerLoader(com.github.stephengold.wrench.LwjglAssetLoader.class,
|
||||
"fbx", "dae", "bvh");
|
||||
|
||||
previewFB = buildFrameBuffer(PREVIEW_SIZE, PREVIEW_SIZE);
|
||||
|
||||
@@ -153,6 +155,11 @@ public class AnimPreviewState extends BaseAppState {
|
||||
addAnimation(addAnimPath);
|
||||
}
|
||||
|
||||
String charModelPath = input.charModelImportQueue.poll();
|
||||
if (charModelPath != null) {
|
||||
importFbxAsCharacterModel(charModelPath);
|
||||
}
|
||||
|
||||
String removeClip = input.animPreviewRemoveClip;
|
||||
if (removeClip != null) {
|
||||
input.animPreviewRemoveClip = null;
|
||||
@@ -193,7 +200,8 @@ public class AnimPreviewState extends BaseAppState {
|
||||
double length = currentAction.getLength();
|
||||
double time = ac.getTime();
|
||||
if (length <= 0) {
|
||||
LOG.warn("[AnimPreview] Loop-Check: length=0, Clip hat keine Dauer!");
|
||||
LOG.warn("[AnimPreview] Clip '{}' hat keine Dauer, stoppe.", currentClipName);
|
||||
stopAll();
|
||||
} else if (time >= length - 0.02) {
|
||||
LOG.trace("[AnimPreview] Loop-Check fired: time={} length={}", time, length);
|
||||
if (input.animPreviewLoop) {
|
||||
@@ -267,7 +275,7 @@ public class AnimPreviewState extends BaseAppState {
|
||||
// Alle Clips in-place snappen (verhindert Drift im Preview)
|
||||
AnimComposer previewAC = findControl(model, AnimComposer.class);
|
||||
SkinningControl previewSC = findControl(model, SkinningControl.class);
|
||||
LOG.info("[AnimPreview] Modell-Controls: AnimComposer={}, SkinningControl={}, EmbeddedClips={}",
|
||||
LOG.debug("[AnimPreview] Modell-Controls: AnimComposer={}, SkinningControl={}, EmbeddedClips={}",
|
||||
previewAC != null ? "gefunden" : "NULL",
|
||||
previewSC != null ? "gefunden" : "NULL",
|
||||
previewAC != null ? previewAC.getAnimClips().size() : "n/a");
|
||||
@@ -365,7 +373,10 @@ public class AnimPreviewState extends BaseAppState {
|
||||
private void collectClips(Spatial s, List<String> out) {
|
||||
AnimComposer ac = s.getControl(AnimComposer.class);
|
||||
if (ac != null) {
|
||||
List<String> names = new ArrayList<>(ac.getAnimClipsNames());
|
||||
List<String> names = new ArrayList<>();
|
||||
for (String n : ac.getAnimClipsNames()) {
|
||||
if (!n.startsWith("__")) names.add(n);
|
||||
}
|
||||
Collections.sort(names);
|
||||
out.addAll(names);
|
||||
}
|
||||
@@ -395,7 +406,38 @@ public class AnimPreviewState extends BaseAppState {
|
||||
if (currentAction != null) {
|
||||
currentAction.setSpeed(input.animPreviewSpeed);
|
||||
input.animPreviewCurrentClipDuration = (float) currentAction.getLength();
|
||||
LOG.info("[AnimPreview] Play '{}' length={}", clipName, currentAction.getLength());
|
||||
// DIAG: Clip-Inhalt + aktueller Hips-Zustand
|
||||
AnimClip diagClip = ac.getAnimClip(clipName);
|
||||
if (diagClip != null) {
|
||||
LOG.info("[DIAG] Play '{}' length={} tracks={}", clipName,
|
||||
currentAction.getLength(), diagClip.getTracks().length);
|
||||
for (com.jme3.anim.AnimTrack<?> t : diagClip.getTracks()) {
|
||||
if (!(t instanceof com.jme3.anim.TransformTrack tt)) continue;
|
||||
if (!(tt.getTarget() instanceof com.jme3.anim.Joint j)) continue;
|
||||
if (!j.getName().contains("Hips")) continue;
|
||||
com.jme3.math.Vector3f[] tr = tt.getTranslations();
|
||||
com.jme3.math.Quaternion[] rr = tt.getRotations();
|
||||
String tStr = (tr != null && tr.length > 0) ? tr[0].toString() : "null";
|
||||
String rStr = (rr != null && rr.length > 0) ? rr[0].toString() : "null";
|
||||
LOG.info("[DIAG] Track '{}' target='{}' frames={} t[0]={} r[0]={}",
|
||||
clipName, j.getName(), tt.getTimes().length, tStr, rStr);
|
||||
}
|
||||
}
|
||||
// DIAG: Modell-Hips localTransform + IBM
|
||||
SkinningControl diaSC = findControl(currentModel, SkinningControl.class);
|
||||
if (diaSC != null) {
|
||||
com.jme3.anim.Armature diaArm = diaSC.getArmature();
|
||||
for (int di = 0; di < diaArm.getJointCount(); di++) {
|
||||
com.jme3.anim.Joint dj = diaArm.getJoint(di);
|
||||
if (dj.getParent() == null) {
|
||||
com.jme3.math.Vector3f lt = dj.getLocalTransform().getTranslation();
|
||||
com.jme3.math.Matrix4f ibm = dj.getInverseModelBindMatrix();
|
||||
LOG.info("[DIAG] Model-Root '{}': localT=({},{},{}) IBM_transl=({},{},{})",
|
||||
dj.getName(), lt.x, lt.y, lt.z,
|
||||
ibm != null ? ibm.m30 : 0, ibm != null ? ibm.m31 : 0, ibm != null ? ibm.m32 : 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
input.animPreviewStatus = "Abspielen fehlgeschlagen: " + e.getMessage();
|
||||
@@ -499,40 +541,39 @@ public class AnimPreviewState extends BaseAppState {
|
||||
com.jme3.anim.Armature srcArm = sourceSC != null ? sourceSC.getArmature() : null;
|
||||
com.jme3.anim.Armature dstArm = targetSC != null ? targetSC.getArmature() : null;
|
||||
|
||||
if (srcArm != null) {
|
||||
LOG.trace("[Retarget] Quell-Knochen ({}):", srcArm.getJointCount());
|
||||
for (var j : srcArm.getJointList()) LOG.trace(" src: {}", j.getName());
|
||||
} else {
|
||||
LOG.warn("[Retarget] Keine SkinningControl in Quelle!");
|
||||
}
|
||||
if (dstArm != null) {
|
||||
LOG.trace("[Retarget] Ziel-Knochen ({}):", dstArm.getJointCount());
|
||||
for (var j : dstArm.getJointList()) LOG.trace(" dst: {}", j.getName());
|
||||
} else {
|
||||
LOG.warn("[Retarget] Keine SkinningControl im Modell!");
|
||||
if (srcArm == null) {
|
||||
LOG.debug("[Retarget] Quelle hat keine SkinningControl (j3o-Clip oder reines Anim-FBX).");
|
||||
} else if (dstArm == null) {
|
||||
LOG.debug("[Retarget] Kein Charakter-Modell geladen – Clip wird ohne Retargeting gespeichert.");
|
||||
}
|
||||
|
||||
boolean retarget = srcArm != null && dstArm != null && srcArm != dstArm;
|
||||
if (retarget) {
|
||||
var mapping = de.blight.game.animation.BoneNameMapping.buildMapping(srcArm, dstArm);
|
||||
LOG.trace("[Retarget] Mapping ({} Treffer): {}", mapping.size(), mapping);
|
||||
}
|
||||
// Retarget wenn Ziel-Armature bekannt UND entweder andere Quelle-Armature
|
||||
// oder Quelle hat gar keine Armature (z.B. j3o-Clip-Wrapper ohne SkinningControl).
|
||||
// Ohne Retarget würden die Track-Targets auf fremde (Standalone-)Joints zeigen
|
||||
// und die Modell-Joints gar nicht bewegen.
|
||||
boolean retarget = dstArm != null && (srcArm == null || srcArm != dstArm);
|
||||
|
||||
java.util.Set<String> srcNames = new java.util.HashSet<>();
|
||||
for (AnimClip c : sourceAC.getAnimClips()) srcNames.add(c.getName());
|
||||
|
||||
java.nio.file.Path clipsDir = ASSET_ROOT.resolve("animations").resolve("clips");
|
||||
java.nio.file.Path clipsDir = ASSET_ROOT.resolve("Characters").resolve("clips");
|
||||
java.nio.file.Files.createDirectories(clipsDir);
|
||||
|
||||
// Blender exportiert GLB-Dateien oft mit internem Namen "Action" statt dem Dateinamen.
|
||||
// Bei einer GLB/GLTF-Datei mit genau einem Clip: Dateinamen als Clip-Namen verwenden.
|
||||
boolean isSingleClipGlb = animAssetPath.matches(".*\\.(glb|gltf)$")
|
||||
// GLB/FBX mit genau einem Clip: Dateinamen als Clip-Namen verwenden.
|
||||
// Mixamo FBX nennt alle Clips "mixamo.com" unabhängig vom Inhalt.
|
||||
boolean isSingleClipSource = animAssetPath.matches("(?i).*\\.(glb|gltf|fbx)$")
|
||||
&& sourceAC.getAnimClips().size() == 1;
|
||||
String fileBaseName = isSingleClipGlb
|
||||
boolean isSingleClipGlb = isSingleClipSource; // Kompatibilität mit unterem Delete-Block
|
||||
String fileBaseName = isSingleClipSource
|
||||
? java.nio.file.Paths.get(animAssetPath).getFileName().toString()
|
||||
.replaceFirst("\\.(glb|gltf)$", "")
|
||||
.replaceFirst("(?i)\\.(glb|gltf|fbx)$", "")
|
||||
: null;
|
||||
|
||||
// j3o-Quelle: Clip bereits gespeichert – nur Preview, kein erneutes Speichern.
|
||||
// snap passiert ausschließlich zur Laufzeit im Spiel via AnimationLibrary.
|
||||
boolean isJ3oSource = animAssetPath.matches("(?i).*\\.j3o$");
|
||||
com.jme3.anim.Armature snapArm = dstArm != null ? dstArm : srcArm;
|
||||
|
||||
int saved = 0;
|
||||
for (AnimClip clip : sourceAC.getAnimClips()) {
|
||||
String name = clip.getName();
|
||||
@@ -543,9 +584,18 @@ public class AnimPreviewState extends BaseAppState {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
AnimClip result = retarget
|
||||
? de.blight.game.animation.RetargetingSystem.retarget(clip, srcArm, dstArm)
|
||||
: clip;
|
||||
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).
|
||||
result = redirectAnimClipByName(clip, dstArm);
|
||||
if (result == null) {
|
||||
LOG.warn("[AnimPreview] redirectByName: keine Joints gemappt, versuche Retargeting");
|
||||
result = de.blight.game.animation.RetargetingSystem.retarget(clip, srcArm, dstArm);
|
||||
}
|
||||
}
|
||||
if (result == null) continue;
|
||||
|
||||
String saveName = (fileBaseName != null) ? fileBaseName : name;
|
||||
@@ -556,18 +606,16 @@ public class AnimPreviewState extends BaseAppState {
|
||||
LOG.info("[AnimPreview] Clip '{}' als '{}' gespeichert (Dateiname-Alias)", name, saveName);
|
||||
}
|
||||
|
||||
// XZ-Drift einfrieren bevor gespeichert wird – Clip-Dateien bleiben immer sauber
|
||||
com.jme3.anim.Armature snapArm = dstArm != null ? dstArm : srcArm;
|
||||
toSave = de.blight.game.animation.AnimationLibrary.snapRootBoneXZ(toSave, snapArm);
|
||||
|
||||
// Direkt in die Clip-Bibliothek speichern – das Modell wird nicht modifiziert
|
||||
if (!isJ3oSource) {
|
||||
// Direkt in die Clip-Bibliothek speichern – snap passiert nur im Spiel zur Laufzeit
|
||||
saveClipToFile(toSave, snapArm, clipsDir.resolve(saveName + ".j3o"));
|
||||
// Für den aktuellen Preview-Session auch auf das Modell anwenden (wenn geladen)
|
||||
if (targetAC != null) targetAC.addAnimClip(toSave);
|
||||
saved++;
|
||||
}
|
||||
// Temporäre GLB aus clips/ löschen (nur wenn sie dort drin liegt – nicht externe Dateien)
|
||||
if (saved > 0 && animAssetPath.matches(".*\\.(glb|gltf)$")
|
||||
// Für den aktuellen Preview auch auf das Modell anwenden (wenn geladen)
|
||||
if (targetAC != null) targetAC.addAnimClip(toSave);
|
||||
}
|
||||
// Temporäre GLB aus clips/ löschen (nur wenn sie dort drin liegt – nicht externe Dateien, nie FBX)
|
||||
if (saved > 0 && animAssetPath.matches("(?i).*\\.(glb|gltf)$")
|
||||
&& !Path.of(animAssetPath).isAbsolute()) {
|
||||
Path srcGlb = ASSET_ROOT.resolve(animAssetPath.replace('/', java.io.File.separatorChar));
|
||||
try {
|
||||
@@ -578,9 +626,13 @@ public class AnimPreviewState extends BaseAppState {
|
||||
List<String> clips = new ArrayList<>();
|
||||
if (currentModel != null) collectClips(currentModel, clips);
|
||||
input.animPreviewClips.set(Collections.unmodifiableList(clips));
|
||||
input.animPreviewStatus = saved + " Clip(s) in animations/clips/ gespeichert"
|
||||
if (isJ3oSource) {
|
||||
input.animPreviewStatus = "Clip geladen (Preview)";
|
||||
} else {
|
||||
input.animPreviewStatus = saved + " Clip(s) in Characters/clips/ gespeichert"
|
||||
+ (retarget ? " (retargeted)" : " (direkt)");
|
||||
if (saved > 0) input.animImportCompleted = true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("[AnimPreview] Fehler beim Importieren von {}", animAssetPath, e);
|
||||
input.animPreviewStatus = "Fehler beim Hinzufügen: " + e.getMessage();
|
||||
@@ -897,10 +949,10 @@ public class AnimPreviewState extends BaseAppState {
|
||||
ac.addAnimClip(renamed);
|
||||
// kein saveModel() – Quell-Modell bleibt unverändert
|
||||
|
||||
// Als eigenständige .j3o nach animations/clips/ exportieren
|
||||
// Als eigenständige .j3o nach Characters/clips/ exportieren
|
||||
try {
|
||||
SkinningControl sc = findControl(currentModel, SkinningControl.class);
|
||||
java.nio.file.Path clipsDir = ASSET_ROOT.resolve("animations").resolve("clips");
|
||||
java.nio.file.Path clipsDir = ASSET_ROOT.resolve("Characters").resolve("clips");
|
||||
saveClipToFile(renamed, sc != null ? sc.getArmature() : null,
|
||||
clipsDir.resolve(req.newName() + ".j3o"));
|
||||
java.util.List<String> clips = new java.util.ArrayList<>();
|
||||
@@ -916,7 +968,7 @@ public class AnimPreviewState extends BaseAppState {
|
||||
|
||||
private void executeAnimSetSave(SharedInput.AnimSetSaveRequest req) {
|
||||
try {
|
||||
java.nio.file.Path setDir = ASSET_ROOT.resolve("animations").resolve("sets");
|
||||
java.nio.file.Path setDir = ASSET_ROOT.resolve("Characters").resolve("sets");
|
||||
java.nio.file.Files.createDirectories(setDir);
|
||||
de.blight.game.animation.AnimSet animSet = new de.blight.game.animation.AnimSet();
|
||||
animSet.setClips(req.clips());
|
||||
@@ -929,8 +981,8 @@ public class AnimPreviewState extends BaseAppState {
|
||||
}
|
||||
|
||||
private void executeAnimEmbed(SharedInput.AnimEmbedRequest req) {
|
||||
java.nio.file.Path setDir = ASSET_ROOT.resolve("animations").resolve("sets");
|
||||
java.nio.file.Path clipsDir = ASSET_ROOT.resolve("animations").resolve("clips");
|
||||
java.nio.file.Path setDir = ASSET_ROOT.resolve("Characters").resolve("sets");
|
||||
java.nio.file.Path clipsDir = ASSET_ROOT.resolve("Characters").resolve("clips");
|
||||
|
||||
de.blight.game.animation.AnimSet set;
|
||||
try {
|
||||
@@ -1096,15 +1148,461 @@ public class AnimPreviewState extends BaseAppState {
|
||||
BinaryExporter.getInstance().save(holder, outFile.toFile());
|
||||
}
|
||||
|
||||
/** Returns "animations/clips/<name>.<ext>" for the first matching file, or null if not found. */
|
||||
// ── FBX-Import ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Importiert eine FBX-Datei als Character-Modell nach Models/Chars/.
|
||||
* Prüft vorab ob ein Mixamo-Skelett (mixamorig:-Präfix) vorhanden ist.
|
||||
* Ergebnis wird via charModelImportResult zurück an die UI gemeldet.
|
||||
*/
|
||||
private void importFbxAsCharacterModel(String absolutePath) {
|
||||
java.io.File file = new java.io.File(absolutePath);
|
||||
if (!file.exists()) {
|
||||
input.charModelImportResult.set(new SharedInput.CharModelImportResult(
|
||||
null, "Datei nicht gefunden: " + absolutePath));
|
||||
return;
|
||||
}
|
||||
String basename = file.getName()
|
||||
.replaceFirst("(?i)\\.[^.]+$", "")
|
||||
.replaceAll("[^a-zA-Z0-9_\\-]", "_");
|
||||
|
||||
try {
|
||||
assets.registerLocator(file.getParent(), com.jme3.asset.plugins.FileLocator.class);
|
||||
String fbmBase = file.getName().replaceAll("\\.[^.]+$", "");
|
||||
java.io.File fbmDir = new java.io.File(file.getParentFile(), fbmBase + ".fbm");
|
||||
if (fbmDir.isDirectory()) {
|
||||
assets.registerLocator(fbmDir.getAbsolutePath(), com.jme3.asset.plugins.FileLocator.class);
|
||||
}
|
||||
|
||||
com.github.stephengold.wrench.LwjglAssetKey key =
|
||||
new com.github.stephengold.wrench.LwjglAssetKey(file.getName());
|
||||
key.setVerboseLogging(true);
|
||||
Spatial model = (Spatial) assets.loadAsset(key);
|
||||
|
||||
// Mixamo-Skelett prüfen
|
||||
if (!hasMixamoSkeleton(model)) {
|
||||
input.charModelImportResult.set(new SharedInput.CharModelImportResult(
|
||||
null, "Kein Mixamo-Skelett gefunden (kein 'mixamorig:'-Joint).\n"
|
||||
+ "Bitte Mixamo-FBX mit 'With Skin' exportieren."));
|
||||
return;
|
||||
}
|
||||
|
||||
java.io.File embeddedDir = extractEmbeddedTextures(file, model);
|
||||
if (embeddedDir != null) {
|
||||
assets.registerLocator(embeddedDir.getAbsolutePath(), com.jme3.asset.plugins.FileLocator.class);
|
||||
}
|
||||
|
||||
fixFbxMaterials(model);
|
||||
fixFbxMeshAndSkeleton(model);
|
||||
fixBrokenTexturePaths(model, file.getParentFile());
|
||||
|
||||
// Textur-Keys entfernen → BinaryExporter bettet Image-Daten direkt in die j3o ein.
|
||||
// Filename-only Keys ("character.png") wären nach dem nächsten Editor-Start nicht
|
||||
// mehr auflösbar (kein Temp-Dir mehr registriert) → gelbe Fallback-Textur.
|
||||
embedTexturesInModel(model);
|
||||
|
||||
model.setLocalTranslation(Vector3f.ZERO);
|
||||
model.updateGeometricState();
|
||||
float scale = 0.01f;
|
||||
if (model.getWorldBound() instanceof com.jme3.bounding.BoundingBox bb) {
|
||||
float h = bb.getYExtent() * 2f;
|
||||
if (h > 0.01f) scale = 2.0f / h;
|
||||
}
|
||||
model.setLocalScale(scale);
|
||||
model.updateGeometricState();
|
||||
|
||||
// Animationsclips entfernen
|
||||
AnimComposer ac = findControl(model, AnimComposer.class);
|
||||
if (ac != null) {
|
||||
for (AnimClip c : new java.util.ArrayList<>(ac.getAnimClips())) {
|
||||
ac.removeAnimClip(c);
|
||||
}
|
||||
}
|
||||
|
||||
java.nio.file.Path destJ3o = ASSET_ROOT.resolve("Characters").resolve("Models").resolve(basename + ".j3o");
|
||||
java.nio.file.Files.createDirectories(destJ3o.getParent());
|
||||
com.jme3.export.binary.BinaryExporter.getInstance().save(model, destJ3o.toFile());
|
||||
|
||||
String relStr = ASSET_ROOT.relativize(destJ3o).toString().replace('\\', '/');
|
||||
for (String root : new String[]{"blight-assets/bin/main", "blight-assets/build/resources/main"}) {
|
||||
java.nio.file.Path mirror = java.nio.file.Paths.get(root).resolve(
|
||||
ASSET_ROOT.relativize(destJ3o));
|
||||
if (java.nio.file.Files.exists(mirror)) {
|
||||
try {
|
||||
java.nio.file.Files.copy(destJ3o, mirror,
|
||||
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
assets.deleteFromCache(new com.jme3.asset.ModelKey(relStr));
|
||||
LOG.info("[CharImport] Character-Modell gespeichert: {} (scale={})", destJ3o, scale);
|
||||
input.charModelImportResult.set(new SharedInput.CharModelImportResult(relStr, null));
|
||||
|
||||
} catch (Exception e) {
|
||||
LOG.error("[CharImport] Fehler bei {}", absolutePath, e);
|
||||
input.charModelImportResult.set(new SharedInput.CharModelImportResult(
|
||||
null, "Import-Fehler: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt den AssetKey aller Texturen im Baum, so dass BinaryExporter die Image-Daten direkt
|
||||
* in die j3o-Datei einbettet (kein externer Textur-Verweis → funktioniert nach Editor-Restart).
|
||||
* JME3: Texture mit key != null → AssetLink-Referenz gespeichert; key == null → Image inline.
|
||||
*/
|
||||
private void embedTexturesInModel(Spatial s) {
|
||||
if (s instanceof com.jme3.scene.Geometry geo && geo.getMaterial() != null) {
|
||||
com.jme3.material.Material mat = geo.getMaterial();
|
||||
for (com.jme3.material.MatParam mp : new java.util.ArrayList<>(mat.getParams())) {
|
||||
if (!(mp.getValue() instanceof com.jme3.texture.Texture tex)) continue;
|
||||
if (tex.getImage() == null) continue;
|
||||
com.jme3.texture.Texture2D embedded = new com.jme3.texture.Texture2D(tex.getImage());
|
||||
embedded.setMinFilter(tex.getMinFilter());
|
||||
embedded.setMagFilter(tex.getMagFilter());
|
||||
embedded.setWrap(com.jme3.texture.Texture.WrapAxis.S,
|
||||
tex.getWrap(com.jme3.texture.Texture.WrapAxis.S));
|
||||
embedded.setWrap(com.jme3.texture.Texture.WrapAxis.T,
|
||||
tex.getWrap(com.jme3.texture.Texture.WrapAxis.T));
|
||||
mat.setTexture(mp.getName(), embedded);
|
||||
}
|
||||
}
|
||||
if (s instanceof Node n) {
|
||||
for (Spatial child : n.getChildren()) embedTexturesInModel(child);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kopiert Textur-Dateien in ein permanentes Asset-Verzeichnis und setzt die Material-Referenzen
|
||||
* auf asset-relative Pfade um, damit das j3o auch nach dem nächsten Editor-Start funktioniert.
|
||||
*/
|
||||
private void relinkTexturesAsAssets(Spatial s,
|
||||
java.util.List<java.io.File> srcDirs,
|
||||
java.nio.file.Path texDestDir,
|
||||
String texRelBase) {
|
||||
if (s instanceof com.jme3.scene.Geometry geo && geo.getMaterial() != null) {
|
||||
com.jme3.material.Material mat = geo.getMaterial();
|
||||
for (com.jme3.material.MatParam mp : new java.util.ArrayList<>(mat.getParams())) {
|
||||
if (!(mp.getValue() instanceof com.jme3.texture.Texture tex)) continue;
|
||||
com.jme3.asset.AssetKey<?> key = tex.getKey();
|
||||
if (key == null) continue;
|
||||
String filename = new java.io.File(key.getName()).getName();
|
||||
if (filename.isEmpty()) continue;
|
||||
if (key.getName().contains("/") || key.getName().contains("\\")) continue; // bereits relativ
|
||||
|
||||
// Quelldatei suchen
|
||||
java.io.File src = null;
|
||||
for (java.io.File dir : srcDirs) {
|
||||
java.io.File c = new java.io.File(dir, filename);
|
||||
if (c.exists()) { src = c; break; }
|
||||
}
|
||||
if (src == null) {
|
||||
LOG.warn("[CharImport] Textur-Quelle nicht gefunden: {}", filename);
|
||||
continue;
|
||||
}
|
||||
|
||||
// In Asset-Verzeichnis kopieren
|
||||
java.nio.file.Path dest = texDestDir.resolve(filename);
|
||||
try {
|
||||
java.nio.file.Files.copy(src.toPath(), dest,
|
||||
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (Exception ex) {
|
||||
LOG.warn("[CharImport] Kopieren fehlgeschlagen ({}): {}", filename, ex.getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Material mit asset-relativem Key neu laden und setzen
|
||||
String relPath = texRelBase + "/" + filename;
|
||||
try {
|
||||
com.jme3.asset.TextureKey newKey = new com.jme3.asset.TextureKey(relPath, true);
|
||||
newKey.setGenerateMips(true);
|
||||
assets.deleteFromCache(newKey);
|
||||
com.jme3.texture.Texture reloaded = assets.loadTexture(newKey);
|
||||
mat.setTexture(mp.getName(), reloaded);
|
||||
LOG.info("[CharImport] Textur → {}", relPath);
|
||||
} catch (Exception ex) {
|
||||
LOG.warn("[CharImport] Textur-Reload fehlgeschlagen ({}): {}", relPath, ex.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (s instanceof Node n) {
|
||||
for (Spatial child : n.getChildren())
|
||||
relinkTexturesAsAssets(child, srcDirs, texDestDir, texRelBase);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasMixamoSkeleton(Spatial model) {
|
||||
SkinningControl sc = findControl(model, SkinningControl.class);
|
||||
if (sc == null) return false;
|
||||
com.jme3.anim.Armature arm = sc.getArmature();
|
||||
for (int i = 0; i < arm.getJointCount(); i++) {
|
||||
if (arm.getJoint(i).getName().startsWith("mixamorig:")) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── FBX-Hilfsroutinen (portiert aus FbxTestApp) ───────────────────────────
|
||||
|
||||
/**
|
||||
* Bereinigt FBX-Mesh-Knoten und Skelett nach MonkeyWrench-Import.
|
||||
* Assimp konvertiert Animations-Tracks zu Y-up, aber NICHT die Bind-Pose-Translation
|
||||
* des Root-Joints. Der Root-Joint liegt in Z-up vor: t=(x,y,z) wird zu t=(x,z,-y).
|
||||
* Danach werden die inversen Bind-Matrizen aller Joints neu berechnet.
|
||||
*/
|
||||
private void fixFbxMeshAndSkeleton(Spatial model) {
|
||||
fixFbxZeroMeshRotations(model);
|
||||
SkinningControl skinCtrl = findControl(model, SkinningControl.class);
|
||||
if (skinCtrl == null) return;
|
||||
com.jme3.anim.Armature arm = skinCtrl.getArmature();
|
||||
// Assimp konvertiert Mesh-Vertices zu Y-up, aber NICHT die Bind-Pose-Translation
|
||||
// des Root-Joints – der liegt noch in Z-up vor: (x,y,z) → (x,z,-y).
|
||||
boolean fixed = false;
|
||||
for (int i = 0; i < arm.getJointCount(); i++) {
|
||||
com.jme3.anim.Joint j = arm.getJoint(i);
|
||||
if (j.getParent() == null) {
|
||||
com.jme3.math.Vector3f t = j.getLocalTransform().getTranslation();
|
||||
float ox = t.x, oy = t.y, oz = t.z;
|
||||
t.set(ox, oz, -oy);
|
||||
LOG.debug("[fixSkeleton] Root-Joint '{}': ({},{},{}) -> ({},{},{})",
|
||||
j.getName(), ox, oy, oz, t.x, t.y, t.z);
|
||||
fixed = true;
|
||||
}
|
||||
}
|
||||
if (!fixed) return;
|
||||
// Inverse Bind-Matrizen aus den korrigierten lokalen Transforms neu berechnen.
|
||||
java.util.HashMap<com.jme3.anim.Joint, com.jme3.math.Matrix4f> modelMats = new java.util.HashMap<>();
|
||||
for (int i = 0; i < arm.getJointCount(); i++) {
|
||||
buildFbxJointModelMatrix(arm.getJoint(i), modelMats);
|
||||
}
|
||||
for (int i = 0; i < arm.getJointCount(); i++) {
|
||||
com.jme3.anim.Joint j = arm.getJoint(i);
|
||||
com.jme3.math.Matrix4f ibm = j.getInverseModelBindMatrix();
|
||||
if (ibm != null) ibm.set(modelMats.get(j).invert());
|
||||
}
|
||||
arm.saveBindPose();
|
||||
arm.saveInitialPose();
|
||||
}
|
||||
|
||||
private com.jme3.math.Matrix4f buildFbxJointModelMatrix(
|
||||
com.jme3.anim.Joint j,
|
||||
java.util.HashMap<com.jme3.anim.Joint, com.jme3.math.Matrix4f> cache) {
|
||||
com.jme3.math.Matrix4f cached = cache.get(j);
|
||||
if (cached != null) return cached;
|
||||
com.jme3.math.Matrix4f local = j.getLocalTransform().toTransformMatrix();
|
||||
com.jme3.math.Matrix4f result = (j.getParent() != null)
|
||||
? buildFbxJointModelMatrix(j.getParent(), cache).mult(local)
|
||||
: local;
|
||||
cache.put(j, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void fixFbxZeroMeshRotations(Spatial s) {
|
||||
if (!(s instanceof Node n)) return;
|
||||
boolean hasSkeleton = n.getControl(AnimComposer.class) != null
|
||||
|| n.getControl(SkinningControl.class) != null;
|
||||
if (!hasSkeleton) {
|
||||
com.jme3.math.Quaternion rot = n.getLocalRotation();
|
||||
boolean isIdentity = Math.abs(rot.getW() - 1f) < 0.01f && Math.abs(rot.getX()) < 0.01f
|
||||
&& Math.abs(rot.getY()) < 0.01f && Math.abs(rot.getZ()) < 0.01f;
|
||||
if (!isIdentity) n.setLocalRotation(com.jme3.math.Quaternion.IDENTITY);
|
||||
}
|
||||
for (Spatial child : n.getChildren()) fixFbxZeroMeshRotations(child);
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert PBR-Materialien in Lighting.j3md (Editor hat keine LightProbe).
|
||||
* Vorhandene Lighting.j3md-Materialien werden nur um DiffuseColor ergänzt.
|
||||
*/
|
||||
private void fixFbxMaterials(Spatial s) {
|
||||
if (s instanceof com.jme3.scene.Geometry geo && geo.getMaterial() != null) {
|
||||
com.jme3.material.Material mat = geo.getMaterial();
|
||||
String defName = mat.getMaterialDef().getName();
|
||||
if (defName.contains("Lighting")) {
|
||||
com.jme3.material.MatParam dm = mat.getParam("DiffuseMap");
|
||||
if (dm != null && dm.getValue() instanceof com.jme3.texture.Texture) {
|
||||
mat.setColor("Diffuse", com.jme3.math.ColorRGBA.White.clone());
|
||||
mat.setColor("Ambient", new com.jme3.math.ColorRGBA(0.45f, 0.45f, 0.45f, 1f));
|
||||
}
|
||||
return;
|
||||
}
|
||||
com.jme3.texture.Texture diffuseTex = null;
|
||||
com.jme3.texture.Texture normalTex = null;
|
||||
com.jme3.math.ColorRGBA baseColor = null;
|
||||
for (String nm : new String[]{"BaseColorMap", "DiffuseMap", "Albedo", "ColorMap"}) {
|
||||
com.jme3.material.MatParam p = mat.getParam(nm);
|
||||
if (p != null && p.getValue() instanceof com.jme3.texture.Texture t) { diffuseTex = t; break; }
|
||||
}
|
||||
for (String nm : new String[]{"NormalMap", "NormalCamera"}) {
|
||||
com.jme3.material.MatParam p = mat.getParam(nm);
|
||||
if (p != null && p.getValue() instanceof com.jme3.texture.Texture t) { normalTex = t; break; }
|
||||
}
|
||||
for (String nm : new String[]{"BaseColor", "Diffuse"}) {
|
||||
com.jme3.material.MatParam p = mat.getParam(nm);
|
||||
if (p != null && p.getValue() instanceof com.jme3.math.ColorRGBA c) { baseColor = c.clone(); break; }
|
||||
}
|
||||
com.jme3.material.Material lit = new com.jme3.material.Material(assets, "Common/MatDefs/Light/Lighting.j3md");
|
||||
lit.setBoolean("UseMaterialColors", true);
|
||||
if (diffuseTex != null) {
|
||||
lit.setTexture("DiffuseMap", diffuseTex);
|
||||
lit.setColor("Diffuse", com.jme3.math.ColorRGBA.White.clone());
|
||||
lit.setColor("Ambient", new com.jme3.math.ColorRGBA(0.45f, 0.45f, 0.45f, 1f));
|
||||
} else {
|
||||
com.jme3.math.ColorRGBA col = baseColor != null ? baseColor : com.jme3.math.ColorRGBA.LightGray.clone();
|
||||
lit.setColor("Diffuse", col);
|
||||
lit.setColor("Ambient", col.mult(0.4f).setAlpha(1f));
|
||||
}
|
||||
if (normalTex != null) lit.setTexture("NormalMap", normalTex);
|
||||
lit.setColor("Specular", new com.jme3.math.ColorRGBA(0.1f, 0.1f, 0.1f, 1f));
|
||||
lit.setFloat("Shininess", 24f);
|
||||
geo.setMaterial(lit);
|
||||
} else if (s instanceof Node n) {
|
||||
for (Spatial child : n.getChildren()) fixFbxMaterials(child);
|
||||
}
|
||||
}
|
||||
|
||||
/** Sammelt alle Dateinamen aus Textur-Params eines Spatial-Baums. */
|
||||
private java.util.Set<String> collectTextureFilenames(Spatial s) {
|
||||
java.util.Set<String> names = new java.util.LinkedHashSet<>();
|
||||
if (s instanceof com.jme3.scene.Geometry geo && geo.getMaterial() != null) {
|
||||
for (com.jme3.material.MatParam p : geo.getMaterial().getParams()) {
|
||||
if (p.getValue() instanceof com.jme3.texture.Texture tex && tex.getKey() != null) {
|
||||
String name = new java.io.File(tex.getKey().getName()).getName();
|
||||
if (!name.isEmpty()) names.add(name);
|
||||
}
|
||||
}
|
||||
} else if (s instanceof Node n) {
|
||||
for (Spatial child : n.getChildren()) names.addAll(collectTextureFilenames(child));
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sucht eingebettete PNG-Texturen im FBX-Binär (Dateiname → PNG-Magic → IEND)
|
||||
* und schreibt sie in ein temporäres Verzeichnis.
|
||||
*/
|
||||
private java.io.File extractEmbeddedTextures(java.io.File fbxFile, Spatial model) {
|
||||
java.util.Set<String> filenames = collectTextureFilenames(model);
|
||||
if (filenames.isEmpty()) return null;
|
||||
try {
|
||||
byte[] data = java.nio.file.Files.readAllBytes(fbxFile.toPath());
|
||||
java.io.File tempDir = new java.io.File(System.getProperty("java.io.tmpdir"),
|
||||
"fbxtex_" + Math.abs(fbxFile.getAbsolutePath().hashCode()));
|
||||
tempDir.mkdirs();
|
||||
byte[] pngMagic = {(byte)0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A};
|
||||
byte[] iend = {0x00,0x00,0x00,0x00,0x49,0x45,0x4E,0x44,(byte)0xAE,0x42,0x60,(byte)0x82};
|
||||
boolean any = false;
|
||||
for (String filename : filenames) {
|
||||
byte[] nameBytes = filename.getBytes(java.nio.charset.StandardCharsets.US_ASCII);
|
||||
int namePos = binaryIndexOf(data, nameBytes, 0);
|
||||
if (namePos < 0) continue;
|
||||
int pngPos = binaryIndexOf(data, pngMagic, namePos);
|
||||
if (pngPos < 0 || pngPos - namePos > 65536) continue;
|
||||
int endPos = binaryIndexOf(data, iend, pngPos + 8);
|
||||
if (endPos < 0) continue;
|
||||
byte[] png = java.util.Arrays.copyOfRange(data, pngPos, endPos + iend.length);
|
||||
java.nio.file.Files.write(new java.io.File(tempDir, filename).toPath(), png);
|
||||
LOG.info("[FbxImport] Embedded-Textur extrahiert: {} ({} B)", filename, png.length);
|
||||
any = true;
|
||||
}
|
||||
return any ? tempDir : null;
|
||||
} catch (Exception e) {
|
||||
LOG.warn("[FbxImport] Embedded-Extraktion fehlgeschlagen: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static int binaryIndexOf(byte[] data, byte[] pattern, int from) {
|
||||
outer:
|
||||
for (int i = from, max = data.length - pattern.length; i <= max; i++) {
|
||||
for (int j = 0; j < pattern.length; j++) {
|
||||
if (data[i + j] != pattern[j]) continue outer;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** Ersetzt kaputte Textur-Pfade (z.B. Mixamo-Server-Pfade) durch Reload per Dateiname. */
|
||||
private void fixBrokenTexturePaths(Spatial s, java.io.File baseDir) {
|
||||
if (s instanceof com.jme3.scene.Geometry geo && geo.getMaterial() != null) {
|
||||
com.jme3.material.Material mat = geo.getMaterial();
|
||||
for (com.jme3.material.MatParam mp : new java.util.ArrayList<>(mat.getParams())) {
|
||||
if (!(mp.getValue() instanceof com.jme3.texture.Texture tex)) continue;
|
||||
com.jme3.asset.AssetKey<?> key = tex.getKey();
|
||||
if (key == null) continue;
|
||||
String filename = new java.io.File(key.getName()).getName();
|
||||
if (filename.isEmpty()) continue;
|
||||
try {
|
||||
com.jme3.asset.TextureKey texKey = new com.jme3.asset.TextureKey(filename, true);
|
||||
texKey.setGenerateMips(true);
|
||||
com.jme3.texture.Texture reloaded = assets.loadTexture(texKey);
|
||||
mat.setTexture(mp.getName(), reloaded);
|
||||
continue;
|
||||
} catch (Exception ignored) {}
|
||||
try {
|
||||
final String fn = filename;
|
||||
java.nio.file.Path found = java.nio.file.Files.walk(baseDir.toPath(), 3)
|
||||
.filter(p -> p.getFileName().toString().equalsIgnoreCase(fn))
|
||||
.findFirst().orElse(null);
|
||||
if (found != null) {
|
||||
assets.registerLocator(found.getParent().toAbsolutePath().toString(),
|
||||
com.jme3.asset.plugins.FileLocator.class);
|
||||
com.jme3.asset.TextureKey texKey = new com.jme3.asset.TextureKey(filename, true);
|
||||
texKey.setGenerateMips(true);
|
||||
mat.setTexture(mp.getName(), assets.loadTexture(texKey));
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
} else if (s instanceof Node n) {
|
||||
for (Spatial child : n.getChildren()) fixBrokenTexturePaths(child, baseDir);
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns "Characters/clips/<name>.<ext>" for the first matching file, or null if not found. */
|
||||
private static String resolveClipFile(java.nio.file.Path clipsDir, String clipName) {
|
||||
for (String ext : new String[]{".j3o", ".glb", ".gltf", ".fbx"}) {
|
||||
if (Files.exists(clipsDir.resolve(clipName + ext)))
|
||||
return "animations/clips/" + clipName + ext;
|
||||
return "Characters/clips/" + clipName + ext;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Leitet Clip-Tracks per Gelenk-Name auf die Ziel-Armatur um (FbxTestApp-Ansatz für Same-Rig). */
|
||||
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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean haveSameBoneNames(com.jme3.anim.Armature a, com.jme3.anim.Armature b) {
|
||||
if (a.getJointCount() != b.getJointCount()) return false;
|
||||
java.util.Set<String> namesA = new java.util.HashSet<>();
|
||||
|
||||
@@ -1362,7 +1362,7 @@ public class DialogEditorView extends BorderPane {
|
||||
odt.normal("Exportiert: " + LocalDate.now());
|
||||
odt.empty();
|
||||
|
||||
Path charDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources", "character");
|
||||
Path charDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources", "Characters");
|
||||
for (GameCharacter gc : CharacterIO.loadAll(charDir)) {
|
||||
if (!(gc instanceof NPC npc)) continue;
|
||||
if (npc.getDialogOptions() == null || npc.getDialogOptions().isEmpty()) continue;
|
||||
|
||||
@@ -290,7 +290,7 @@ public class TtsGeneratorDialog extends Dialog<Void> {
|
||||
|
||||
private List<String> collectHeroKeys() {
|
||||
List<String> keys = new ArrayList<>();
|
||||
Path charDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources", "character");
|
||||
Path charDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources", "Characters");
|
||||
for (GameCharacter gc : CharacterIO.loadAll(charDir)) {
|
||||
if (!(gc instanceof NPC npc) || npc.getDialogOptions() == null) continue;
|
||||
for (Map.Entry<String, DialogOption> e : npc.getDialogOptions().entrySet()) {
|
||||
|
||||
@@ -192,7 +192,7 @@ public class AnimationLibrary extends BaseAppState {
|
||||
* @return Clip-Name oder {@code null}
|
||||
*/
|
||||
public static String getClipForAction(Path assetRoot, String setName, AnimationAction action) {
|
||||
Path setDir = assetRoot.resolve("animations").resolve("sets");
|
||||
Path setDir = assetRoot.resolve("Characters").resolve("sets");
|
||||
try {
|
||||
AnimSet set = AnimSet.load(setDir, setName);
|
||||
return set.getActionMap().get(action.name());
|
||||
@@ -223,7 +223,7 @@ public class AnimationLibrary extends BaseAppState {
|
||||
log.info("[AnimLib] {} Clips geladen: {}", clips.size(), clips.keySet());
|
||||
}
|
||||
|
||||
Path setDir = findAssetRoot().resolve("animations").resolve("sets");
|
||||
Path setDir = findAssetRoot().resolve("Characters").resolve("sets");
|
||||
try {
|
||||
AnimSet animSet = AnimSet.load(setDir, "human");
|
||||
createSubClips(animSet);
|
||||
@@ -293,7 +293,7 @@ public class AnimationLibrary extends BaseAppState {
|
||||
private void loadClipFromFile(Path file) {
|
||||
String fileName = file.getFileName().toString();
|
||||
String clipName = fileName.replaceFirst("\\.j3o$", "");
|
||||
String assetKey = "animations/clips/" + fileName;
|
||||
String assetKey = "Characters/clips/" + fileName;
|
||||
|
||||
try {
|
||||
Spatial loaded = assetManager.loadModel(assetKey);
|
||||
@@ -319,18 +319,18 @@ public class AnimationLibrary extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
/** Gibt den Asset-Wurzelpfad zurück (erstes Verzeichnis mit {@code animations/}-Unterordner). */
|
||||
/** Gibt den Asset-Wurzelpfad zurück (erstes Verzeichnis mit {@code Characters/}-Unterordner). */
|
||||
public static Path findAssetRoot() {
|
||||
for (String base : ASSET_BASES) {
|
||||
Path p = Paths.get(base);
|
||||
if (Files.isDirectory(p.resolve("animations"))) return p;
|
||||
if (Files.isDirectory(p.resolve("Characters"))) return p;
|
||||
}
|
||||
return Paths.get(ASSET_BASES[0]);
|
||||
}
|
||||
|
||||
private static Path findClipsDir() {
|
||||
for (String base : ASSET_BASES) {
|
||||
Path p = Paths.get(base, "animations", "clips");
|
||||
Path p = Paths.get(base, "Characters", "clips");
|
||||
if (Files.isDirectory(p)) return p;
|
||||
}
|
||||
return null;
|
||||
@@ -367,7 +367,7 @@ public class AnimationLibrary extends BaseAppState {
|
||||
}
|
||||
if (minDepth == Integer.MAX_VALUE) return clip;
|
||||
|
||||
boolean isLocomotion = LOCOMOTION_CLIPS.contains(clip.getName());
|
||||
boolean isLocomotion = LOCOMOTION_CLIPS.contains(clip.getName().toLowerCase(java.util.Locale.ROOT));
|
||||
|
||||
List<AnimTrack<?>> newTracks = new ArrayList<>();
|
||||
boolean modified = false;
|
||||
|
||||
@@ -550,7 +550,7 @@ public class WorldScene extends BaseAppState {
|
||||
animLib.getClipKeys());
|
||||
// AnimSet-ActionMap ausgeben
|
||||
if (setName != null) {
|
||||
java.nio.file.Path setDir = AnimationLibrary.findAssetRoot().resolve("animations").resolve("sets");
|
||||
java.nio.file.Path setDir = AnimationLibrary.findAssetRoot().resolve("Characters").resolve("sets");
|
||||
try {
|
||||
de.blight.game.animation.AnimSet set = de.blight.game.animation.AnimSet.load(setDir, setName);
|
||||
log.info("[AnimCtx] AnimSet '{}' actionMap: {}", setName, set.getActionMap());
|
||||
@@ -736,7 +736,7 @@ public class WorldScene extends BaseAppState {
|
||||
}
|
||||
|
||||
private MainCharacter findMainCharacter() {
|
||||
java.nio.file.Path charDir = AnimationLibrary.findAssetRoot().resolve("character");
|
||||
java.nio.file.Path charDir = AnimationLibrary.findAssetRoot().resolve("Characters");
|
||||
for (GameCharacter c : CharacterIO.loadAll(charDir)) {
|
||||
if (c instanceof MainCharacter mc) return mc;
|
||||
}
|
||||
|
||||
@@ -668,7 +668,7 @@ public class WorldNpcsState extends BaseAppState {
|
||||
&& saveState.getSave().character.positionSaved
|
||||
&& SaveGameIO.exists();
|
||||
try {
|
||||
java.nio.file.Path charDir = AnimationLibrary.findAssetRoot().resolve("character");
|
||||
java.nio.file.Path charDir = AnimationLibrary.findAssetRoot().resolve("Characters");
|
||||
for (GameCharacter gc : CharacterIO.loadAll(charDir)) {
|
||||
if (gc instanceof NPC npc) {
|
||||
java.util.List<String> savedState = useSaved
|
||||
|
||||
389
com/jme3/anim/TransformTrack.java
Normal file
389
com/jme3/anim/TransformTrack.java
Normal file
@@ -0,0 +1,389 @@
|
||||
/*
|
||||
* Copyright (c) 2009-2023 jMonkeyEngine
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are
|
||||
* met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package com.jme3.anim;
|
||||
|
||||
import com.jme3.anim.interpolator.FrameInterpolator;
|
||||
import com.jme3.anim.util.HasLocalTransform;
|
||||
import com.jme3.animation.CompactQuaternionArray;
|
||||
import com.jme3.animation.CompactVector3Array;
|
||||
import com.jme3.export.*;
|
||||
import com.jme3.math.*;
|
||||
import com.jme3.util.clone.Cloner;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* An AnimTrack that transforms a Joint or Spatial
|
||||
* using a list of transforms and times for keyframes.
|
||||
*
|
||||
* @author Rémy Bouquet
|
||||
*/
|
||||
public class TransformTrack implements AnimTrack<Transform> {
|
||||
|
||||
private double length;
|
||||
/**
|
||||
* The interpolator to use, or null to always use the default interpolator
|
||||
* of the current thread.
|
||||
*/
|
||||
private FrameInterpolator interpolator = null;
|
||||
private HasLocalTransform target;
|
||||
|
||||
/**
|
||||
* Transforms and times for keyframes.
|
||||
*/
|
||||
private CompactVector3Array translations;
|
||||
private CompactQuaternionArray rotations;
|
||||
private CompactVector3Array scales;
|
||||
private float[] times;
|
||||
|
||||
/**
|
||||
* Serialization-only. Do not use.
|
||||
*/
|
||||
protected TransformTrack() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a transform track for the given target.
|
||||
*
|
||||
* @param target the target Joint or Spatial of the new track
|
||||
* @param times the time for each keyframe, or null for none
|
||||
* @param translations the translation of the target for each keyframe
|
||||
* (same length as times) or null for no translation
|
||||
* @param rotations the rotation of the target for each keyframe
|
||||
* (same length as times) or null for no rotation
|
||||
* @param scales the scale of the target for each keyframe
|
||||
* (same length as times) or null for no scaling
|
||||
*/
|
||||
public TransformTrack(HasLocalTransform target, float[] times, Vector3f[] translations, Quaternion[] rotations, Vector3f[] scales) {
|
||||
this.target = target;
|
||||
this.setKeyframes(times, translations, rotations, scales);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the rotations.
|
||||
*
|
||||
* @return a new array, or null if no rotations
|
||||
*/
|
||||
public Quaternion[] getRotations() {
|
||||
return rotations == null ? null : rotations.toObjectArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the scales.
|
||||
*
|
||||
* @return a new array, or null if no scales
|
||||
*/
|
||||
public Vector3f[] getScales() {
|
||||
return scales == null ? null : scales.toObjectArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives access to the keyframe times.
|
||||
*
|
||||
* @return the pre-existing array
|
||||
*/
|
||||
public float[] getTimes() {
|
||||
return times;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the translations.
|
||||
*
|
||||
* @return a new array, or null if no translations
|
||||
*/
|
||||
public Vector3f[] getTranslations() {
|
||||
return translations == null ? null : translations.toObjectArray();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the keyframe times.
|
||||
*
|
||||
* @param times the desired keyframe times (alias created, not null, not empty)
|
||||
*/
|
||||
public void setTimes(float[] times) {
|
||||
if (times == null || times.length == 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"No keyframe times were provided.");
|
||||
}
|
||||
this.times = times;
|
||||
length = times[times.length - 1] - times[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the translations.
|
||||
*
|
||||
* @param translations the desired translation of the target for each
|
||||
* keyframe (not null, same length as "times")
|
||||
*/
|
||||
public void setKeyframesTranslation(Vector3f[] translations) {
|
||||
if (times == null) {
|
||||
throw new IllegalStateException(
|
||||
"TransformTrack lacks keyframe times. "
|
||||
+ "Please invoke setTimes() first.");
|
||||
}
|
||||
if (translations == null || translations.length == 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"No translations were provided.");
|
||||
}
|
||||
this.translations = new CompactVector3Array();
|
||||
this.translations.add(translations);
|
||||
this.translations.freeze();
|
||||
|
||||
assert times != null && times.length == translations.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the scales.
|
||||
*
|
||||
* @param scales the desired scale of the target for each keyframe (not
|
||||
* null, same length as "times")
|
||||
*/
|
||||
public void setKeyframesScale(Vector3f[] scales) {
|
||||
if (times == null) {
|
||||
throw new IllegalStateException(
|
||||
"TransformTrack lacks keyframe times. "
|
||||
+ "Please invoke setTimes() first.");
|
||||
}
|
||||
if (scales == null || scales.length == 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"No scale vectors were provided.");
|
||||
}
|
||||
this.scales = new CompactVector3Array();
|
||||
this.scales.add(scales);
|
||||
this.scales.freeze();
|
||||
|
||||
assert times != null && times.length == scales.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the rotations.
|
||||
*
|
||||
* @param rotations the desired rotation of the target for each keyframe
|
||||
* (not null, same length as "times")
|
||||
*/
|
||||
public void setKeyframesRotation(Quaternion[] rotations) {
|
||||
if (times == null) {
|
||||
throw new IllegalStateException(
|
||||
"TransformTrack lacks keyframe times. "
|
||||
+ "Please invoke setTimes() first.");
|
||||
}
|
||||
if (rotations == null || rotations.length == 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"No rotations were provided.");
|
||||
}
|
||||
this.rotations = new CompactQuaternionArray();
|
||||
this.rotations.add(rotations);
|
||||
this.rotations.freeze();
|
||||
|
||||
assert times != null && times.length == rotations.length;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the translations, rotations, and/or scales.
|
||||
*
|
||||
* @param times the desired time for each keyframe,
|
||||
* or null to leave the times unchanged
|
||||
* @param translations the desired translation of the target for each
|
||||
* keyframe (same length as times)
|
||||
* or null to leave the translations unchanged
|
||||
* @param rotations the desired rotation of the target for each keyframe
|
||||
* (same length as times)
|
||||
* or null to leave the rotations unchanged
|
||||
* @param scales the desired scale of the target for each keyframe
|
||||
* (same length as times)
|
||||
* or null to leave the scales unchanged
|
||||
*/
|
||||
public void setKeyframes(float[] times, Vector3f[] translations, Quaternion[] rotations, Vector3f[] scales) {
|
||||
if (times != null) {
|
||||
setTimes(times);
|
||||
}
|
||||
if (translations != null) {
|
||||
setKeyframesTranslation(translations);
|
||||
}
|
||||
if (rotations != null) {
|
||||
setKeyframesRotation(rotations);
|
||||
}
|
||||
if (scales != null) {
|
||||
setKeyframesScale(scales);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public double getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getDataAtTime(double t, Transform transform) {
|
||||
float time = (float) t;
|
||||
|
||||
int lastFrame = times.length - 1;
|
||||
if (time < 0 || lastFrame == 0) {
|
||||
if (translations != null) {
|
||||
translations.get(0, transform.getTranslation());
|
||||
}
|
||||
if (rotations != null) {
|
||||
rotations.get(0, transform.getRotation());
|
||||
}
|
||||
if (scales != null) {
|
||||
scales.get(0, transform.getScale());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
int startFrame = 0;
|
||||
int endFrame = 1;
|
||||
float blend = 0;
|
||||
if (time >= times[lastFrame]) {
|
||||
// extrapolate beyond the final keyframe of the animation
|
||||
startFrame = lastFrame;
|
||||
|
||||
float inferredInterval = times[lastFrame] - times[lastFrame - 1];
|
||||
if (inferredInterval > 0f) {
|
||||
blend = (time - times[startFrame]) / inferredInterval;
|
||||
}
|
||||
|
||||
} else {
|
||||
// use lastFrame so we never overflow the array
|
||||
int i;
|
||||
for (i = 0; i < lastFrame && times[i] < time; i++) {
|
||||
startFrame = i;
|
||||
endFrame = i + 1;
|
||||
}
|
||||
blend = (time - times[startFrame])
|
||||
/ (times[endFrame] - times[startFrame]);
|
||||
}
|
||||
|
||||
FrameInterpolator fi = (interpolator == null)
|
||||
? FrameInterpolator.getThreadDefault() : interpolator;
|
||||
Transform interpolated = fi.interpolate(
|
||||
blend, startFrame, translations, rotations, scales, times);
|
||||
|
||||
if (translations != null) {
|
||||
transform.setTranslation(interpolated.getTranslation());
|
||||
}
|
||||
if (rotations != null) {
|
||||
transform.setRotation(interpolated.getRotation());
|
||||
}
|
||||
if (scales != null) {
|
||||
transform.setScale(interpolated.getScale());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Access the FrameInterpolator.
|
||||
*
|
||||
* @return the pre-existing instance or null
|
||||
*/
|
||||
public FrameInterpolator getFrameInterpolator() {
|
||||
return interpolator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the frame interpolator.
|
||||
*
|
||||
* @param interpolator the interpolator to use (alias created)
|
||||
*/
|
||||
public void setFrameInterpolator(FrameInterpolator interpolator) {
|
||||
this.interpolator = interpolator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gives access to the target, which might be a Joint or a Spatial.
|
||||
*
|
||||
* @return the pre-existing instance
|
||||
*/
|
||||
public HasLocalTransform getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the target, which might be a Joint or a Spatial.
|
||||
*
|
||||
* @param target the target to use (alias created)
|
||||
*/
|
||||
public void setTarget(HasLocalTransform target) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes this track to the specified exporter, for example when
|
||||
* saving to a J3O file.
|
||||
*
|
||||
* @param ex the exporter to write to (not null)
|
||||
* @throws IOException from the exporter
|
||||
*/
|
||||
@Override
|
||||
public void write(JmeExporter ex) throws IOException {
|
||||
OutputCapsule oc = ex.getCapsule(this);
|
||||
oc.write(translations, "translations", null);
|
||||
oc.write(rotations, "rotations", null);
|
||||
oc.write(times, "times", null);
|
||||
oc.write(scales, "scales", null);
|
||||
oc.write(target, "target", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* De-serializes this track from the specified importer, for example when
|
||||
* loading from a J3O file.
|
||||
*
|
||||
* @param im the importer to read from (not null)
|
||||
* @throws IOException from the importer
|
||||
*/
|
||||
@Override
|
||||
public void read(JmeImporter im) throws IOException {
|
||||
InputCapsule ic = im.getCapsule(this);
|
||||
translations = (CompactVector3Array) ic.readSavable("translations", null);
|
||||
rotations = (CompactQuaternionArray) ic.readSavable("rotations", null);
|
||||
times = ic.readFloatArray("times", null);
|
||||
scales = (CompactVector3Array) ic.readSavable("scales", null);
|
||||
target = (HasLocalTransform) ic.readSavable("target", null);
|
||||
setTimes(times);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransformTrack jmeClone() {
|
||||
try {
|
||||
TransformTrack clone = (TransformTrack) super.clone();
|
||||
return clone;
|
||||
} catch (CloneNotSupportedException ex) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cloneFields(Cloner cloner, Object original) {
|
||||
this.target = cloner.clone(target);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user