An den LOD im Editor gearbeitet
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 904 B |
Binary file not shown.
|
After Width: | Height: | Size: 757 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
@@ -1,4 +1,4 @@
|
|||||||
#Sat Jul 18 13:44:33 CEST 2026
|
#Mon Jul 20 11:46:10 CEST 2026
|
||||||
attachedEmitters.count=0
|
attachedEmitters.count=0
|
||||||
attachedLights.count=0
|
attachedLights.count=0
|
||||||
castShadow=true
|
castShadow=true
|
||||||
|
|||||||
Binary file not shown.
@@ -0,0 +1,28 @@
|
|||||||
|
#Mon Jul 20 13:09:03 CEST 2026
|
||||||
|
attachedEmitters.count=0
|
||||||
|
attachedLights.count=0
|
||||||
|
castShadow=true
|
||||||
|
category=
|
||||||
|
cullDistance=120.0
|
||||||
|
footstepSurface=
|
||||||
|
interactableOffsetX=0.0
|
||||||
|
interactableOffsetY=0.5
|
||||||
|
interactableOffsetZ=0.0
|
||||||
|
interactableRotY=0.0
|
||||||
|
interactableType=NONE
|
||||||
|
lod1Distance=30.0
|
||||||
|
lod1Path=
|
||||||
|
lod2Distance=80.0
|
||||||
|
lod2Path=
|
||||||
|
name=pine_medium_20260706_190947
|
||||||
|
pivotOffsetY=0.0
|
||||||
|
placementOffsetY=0.0
|
||||||
|
randomScaleMax=1.0
|
||||||
|
randomScaleMin=1.0
|
||||||
|
receiveShadow=true
|
||||||
|
scaleX=1.0
|
||||||
|
scaleY=1.0
|
||||||
|
scaleZ=1.0
|
||||||
|
solid=false
|
||||||
|
tags=
|
||||||
|
uniformScale=true
|
||||||
Binary file not shown.
@@ -1,4 +1,4 @@
|
|||||||
#Sat Jul 18 11:14:25 CEST 2026
|
#Mon Jul 20 13:25:15 CEST 2026
|
||||||
attachedEmitters.count=0
|
attachedEmitters.count=0
|
||||||
attachedLights.count=0
|
attachedLights.count=0
|
||||||
castShadow=true
|
castShadow=true
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ public class EditorApp extends Application {
|
|||||||
|
|
||||||
// Modell-Editor-Zustand
|
// Modell-Editor-Zustand
|
||||||
private Label modelEditorDimLabel;
|
private Label modelEditorDimLabel;
|
||||||
|
private Label modelEditorPolyLabel;
|
||||||
private Spinner<Double> modelEditorSpinX, modelEditorSpinY, modelEditorSpinZ;
|
private Spinner<Double> modelEditorSpinX, modelEditorSpinY, modelEditorSpinZ;
|
||||||
private CheckBox modelEditorUniformCB;
|
private CheckBox modelEditorUniformCB;
|
||||||
private boolean modelEditorSuppressListeners = false;
|
private boolean modelEditorSuppressListeners = false;
|
||||||
@@ -254,12 +255,14 @@ public class EditorApp extends Application {
|
|||||||
private String modelEditorLod2Path = "";
|
private String modelEditorLod2Path = "";
|
||||||
private Label modelEditorLod1Label;
|
private Label modelEditorLod1Label;
|
||||||
private Label modelEditorLod2Label;
|
private Label modelEditorLod2Label;
|
||||||
|
private ToggleButton modelEditorLod0Btn;
|
||||||
private ToggleButton modelEditorLod1Btn;
|
private ToggleButton modelEditorLod1Btn;
|
||||||
private ToggleButton modelEditorLod2Btn;
|
private ToggleButton modelEditorLod2Btn;
|
||||||
private Label modelEditorLod1StatusLabel;
|
private Label modelEditorLod1StatusLabel;
|
||||||
private Label modelEditorLod2StatusLabel;
|
private Label modelEditorLod2StatusLabel;
|
||||||
private Button modelEditorLod1GenBtn;
|
private Button modelEditorLod1GenBtn;
|
||||||
private Button modelEditorLod2GenBtn;
|
private Button modelEditorLod2GenBtn;
|
||||||
|
private ToggleButton importLod0Btn;
|
||||||
private ToggleButton importLod1Btn;
|
private ToggleButton importLod1Btn;
|
||||||
private ToggleButton importLod2Btn;
|
private ToggleButton importLod2Btn;
|
||||||
private Button modelImportLod1ClearBtn;
|
private Button modelImportLod1ClearBtn;
|
||||||
@@ -430,6 +433,7 @@ public class EditorApp extends Application {
|
|||||||
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION,
|
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION,
|
||||||
"Es gibt ungespeicherte Änderungen.\nTrotzdem beenden?",
|
"Es gibt ungespeicherte Änderungen.\nTrotzdem beenden?",
|
||||||
ButtonType.YES, ButtonType.NO);
|
ButtonType.YES, ButtonType.NO);
|
||||||
|
confirm.initOwner(stage);
|
||||||
confirm.setHeaderText("Ungespeicherte Änderungen");
|
confirm.setHeaderText("Ungespeicherte Änderungen");
|
||||||
confirm.showAndWait().ifPresent(btn -> {
|
confirm.showAndWait().ifPresent(btn -> {
|
||||||
if (btn != ButtonType.YES) e.consume();
|
if (btn != ButtonType.YES) e.consume();
|
||||||
@@ -665,7 +669,8 @@ public class EditorApp extends Application {
|
|||||||
updateModelEditorDimensions(
|
updateModelEditorDimensions(
|
||||||
input.modelEditorBoundsW,
|
input.modelEditorBoundsW,
|
||||||
input.modelEditorBoundsH,
|
input.modelEditorBoundsH,
|
||||||
input.modelEditorBoundsD);
|
input.modelEditorBoundsD,
|
||||||
|
input.modelEditorPolyCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modell-Editor: eingebettete LODs erkannt → Labels aktualisieren
|
// Modell-Editor: eingebettete LODs erkannt → Labels aktualisieren
|
||||||
@@ -692,6 +697,14 @@ public class EditorApp extends Application {
|
|||||||
// Importer LOD-Buttons freischalten sobald generiert
|
// Importer LOD-Buttons freischalten sobald generiert
|
||||||
if (importLod1Btn != null) importLod1Btn.setDisable(!input.modelImportHasLod1);
|
if (importLod1Btn != null) importLod1Btn.setDisable(!input.modelImportHasLod1);
|
||||||
if (importLod2Btn != null) importLod2Btn.setDisable(!input.modelImportHasLod2);
|
if (importLod2Btn != null) importLod2Btn.setDisable(!input.modelImportHasLod2);
|
||||||
|
// Ausgewählten LOD-Button mit aktuellem Preview-Level synchronisieren
|
||||||
|
int lodPreview = input.modelEditorLodPreview;
|
||||||
|
if (modelEditorLod0Btn != null) { modelEditorLod0Btn.setSelected(lodPreview == 0); }
|
||||||
|
if (modelEditorLod1Btn != null) { modelEditorLod1Btn.setSelected(lodPreview == 1); }
|
||||||
|
if (modelEditorLod2Btn != null) { modelEditorLod2Btn.setSelected(lodPreview == 2); }
|
||||||
|
if (importLod0Btn != null) { importLod0Btn.setSelected(lodPreview == 0); }
|
||||||
|
if (importLod1Btn != null) { importLod1Btn.setSelected(lodPreview == 1); }
|
||||||
|
if (importLod2Btn != null) { importLod2Btn.setSelected(lodPreview == 2); }
|
||||||
if (modelImportLod1ClearBtn != null) modelImportLod1ClearBtn.setDisable(!input.modelImportHasLod1);
|
if (modelImportLod1ClearBtn != null) modelImportLod1ClearBtn.setDisable(!input.modelImportHasLod1);
|
||||||
if (modelImportLod2ClearBtn != null) modelImportLod2ClearBtn.setDisable(!input.modelImportHasLod2);
|
if (modelImportLod2ClearBtn != null) modelImportLod2ClearBtn.setDisable(!input.modelImportHasLod2);
|
||||||
|
|
||||||
@@ -867,7 +880,8 @@ public class EditorApp extends Application {
|
|||||||
Label label = new Label("Model Editor");
|
Label label = new Label("Model Editor");
|
||||||
label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
|
label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
|
||||||
|
|
||||||
ToggleButton lod0Btn = new ToggleButton("LOD 0");
|
modelEditorLod0Btn = new ToggleButton("LOD 0");
|
||||||
|
ToggleButton lod0Btn = modelEditorLod0Btn;
|
||||||
modelEditorLod1Btn = new ToggleButton("LOD 1");
|
modelEditorLod1Btn = new ToggleButton("LOD 1");
|
||||||
modelEditorLod2Btn = new ToggleButton("LOD 2");
|
modelEditorLod2Btn = new ToggleButton("LOD 2");
|
||||||
javafx.scene.control.ToggleGroup lodGroup = new javafx.scene.control.ToggleGroup();
|
javafx.scene.control.ToggleGroup lodGroup = new javafx.scene.control.ToggleGroup();
|
||||||
@@ -2247,6 +2261,7 @@ public class EditorApp extends Application {
|
|||||||
objSaveBtn.setDisable(true);
|
objSaveBtn.setDisable(true);
|
||||||
objSaveBtn.setOnAction(e -> {
|
objSaveBtn.setOnAction(e -> {
|
||||||
TextInputDialog dlg = new TextInputDialog("Vorlage");
|
TextInputDialog dlg = new TextInputDialog("Vorlage");
|
||||||
|
dlg.initOwner(primaryStage);
|
||||||
dlg.setTitle("Vorlage speichern");
|
dlg.setTitle("Vorlage speichern");
|
||||||
dlg.setHeaderText("Name der Vorlage (ohne .j3o):");
|
dlg.setHeaderText("Name der Vorlage (ohne .j3o):");
|
||||||
dlg.setContentText("Name:");
|
dlg.setContentText("Name:");
|
||||||
@@ -4435,6 +4450,7 @@ public class EditorApp extends Application {
|
|||||||
"Textur-Set wirklich löschen?\n" + setDir.getFileName()
|
"Textur-Set wirklich löschen?\n" + setDir.getFileName()
|
||||||
+ "\n(Alle enthaltenen Dateien werden gelöscht!)",
|
+ "\n(Alle enthaltenen Dateien werden gelöscht!)",
|
||||||
ButtonType.OK, ButtonType.CANCEL);
|
ButtonType.OK, ButtonType.CANCEL);
|
||||||
|
confirm.initOwner(primaryStage);
|
||||||
confirm.setHeaderText(null);
|
confirm.setHeaderText(null);
|
||||||
confirm.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> {
|
confirm.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> {
|
||||||
try {
|
try {
|
||||||
@@ -4604,6 +4620,7 @@ public class EditorApp extends Application {
|
|||||||
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION,
|
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION,
|
||||||
"Datei wirklich löschen?\n" + p.getFileName(),
|
"Datei wirklich löschen?\n" + p.getFileName(),
|
||||||
ButtonType.OK, ButtonType.CANCEL);
|
ButtonType.OK, ButtonType.CANCEL);
|
||||||
|
confirm.initOwner(primaryStage);
|
||||||
confirm.setHeaderText(null);
|
confirm.setHeaderText(null);
|
||||||
confirm.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> {
|
confirm.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> {
|
||||||
try {
|
try {
|
||||||
@@ -4706,6 +4723,7 @@ public class EditorApp extends Application {
|
|||||||
Path parentPath = itemPaths.get(parentItem);
|
Path parentPath = itemPaths.get(parentItem);
|
||||||
if (parentPath == null) return;
|
if (parentPath == null) return;
|
||||||
TextInputDialog dlg = new TextInputDialog("Neuer Ordner");
|
TextInputDialog dlg = new TextInputDialog("Neuer Ordner");
|
||||||
|
dlg.initOwner(primaryStage);
|
||||||
dlg.setTitle("Ordner erstellen");
|
dlg.setTitle("Ordner erstellen");
|
||||||
dlg.setHeaderText("Neuen Unterordner in " + parentPath.getFileName() + " erstellen:");
|
dlg.setHeaderText("Neuen Unterordner in " + parentPath.getFileName() + " erstellen:");
|
||||||
dlg.setContentText("Name:");
|
dlg.setContentText("Name:");
|
||||||
@@ -4747,6 +4765,7 @@ public class EditorApp extends Application {
|
|||||||
|
|
||||||
String headerExt = ext.isEmpty() ? "" : " (Endung " + ext + " wird beibehalten)";
|
String headerExt = ext.isEmpty() ? "" : " (Endung " + ext + " wird beibehalten)";
|
||||||
TextInputDialog dlg = new TextInputDialog(baseName);
|
TextInputDialog dlg = new TextInputDialog(baseName);
|
||||||
|
dlg.initOwner(primaryStage);
|
||||||
dlg.setTitle("Umbenennen");
|
dlg.setTitle("Umbenennen");
|
||||||
dlg.setHeaderText(oldFileName + " umbenennen:" + headerExt);
|
dlg.setHeaderText(oldFileName + " umbenennen:" + headerExt);
|
||||||
dlg.setContentText("Neuer Name:");
|
dlg.setContentText("Neuer Name:");
|
||||||
@@ -5985,6 +6004,10 @@ public class EditorApp extends Application {
|
|||||||
modelEditorDimLabel.setMaxWidth(Double.MAX_VALUE);
|
modelEditorDimLabel.setMaxWidth(Double.MAX_VALUE);
|
||||||
modelEditorDimLabel.setWrapText(true);
|
modelEditorDimLabel.setWrapText(true);
|
||||||
|
|
||||||
|
modelEditorPolyLabel = new Label("– Polygone");
|
||||||
|
modelEditorPolyLabel.setStyle("-fx-text-fill:#aaa; -fx-font-family:monospace; -fx-font-size:11;");
|
||||||
|
modelEditorPolyLabel.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
|
||||||
// ── Pivot & Platzierungs-Versatz ─────────────────────────────────────
|
// ── Pivot & Platzierungs-Versatz ─────────────────────────────────────
|
||||||
Label offsetTitle = new Label("Versatz:");
|
Label offsetTitle = new Label("Versatz:");
|
||||||
offsetTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;");
|
offsetTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;");
|
||||||
@@ -6390,7 +6413,7 @@ public class EditorApp extends Application {
|
|||||||
tagsLabel, tagsTF,
|
tagsLabel, tagsTF,
|
||||||
new Separator(),
|
new Separator(),
|
||||||
scaleTitle, modelEditorUniformCB, scaleGrid,
|
scaleTitle, modelEditorUniformCB, scaleGrid,
|
||||||
modelEditorDimLabel,
|
modelEditorDimLabel, modelEditorPolyLabel,
|
||||||
new Separator(),
|
new Separator(),
|
||||||
offsetTitle, offsetGrid,
|
offsetTitle, offsetGrid,
|
||||||
new Separator(),
|
new Separator(),
|
||||||
@@ -6524,7 +6547,8 @@ public class EditorApp extends Application {
|
|||||||
Label label = new Label("Modell importieren");
|
Label label = new Label("Modell importieren");
|
||||||
label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
|
label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
|
||||||
|
|
||||||
ToggleButton lod0Btn = new ToggleButton("LOD 0");
|
importLod0Btn = new ToggleButton("LOD 0");
|
||||||
|
ToggleButton lod0Btn = importLod0Btn;
|
||||||
importLod1Btn = new ToggleButton("LOD 1");
|
importLod1Btn = new ToggleButton("LOD 1");
|
||||||
importLod2Btn = new ToggleButton("LOD 2");
|
importLod2Btn = new ToggleButton("LOD 2");
|
||||||
javafx.scene.control.ToggleGroup lodGroup = new javafx.scene.control.ToggleGroup();
|
javafx.scene.control.ToggleGroup lodGroup = new javafx.scene.control.ToggleGroup();
|
||||||
@@ -6644,6 +6668,10 @@ public class EditorApp extends Application {
|
|||||||
modelEditorDimLabel.setMaxWidth(Double.MAX_VALUE);
|
modelEditorDimLabel.setMaxWidth(Double.MAX_VALUE);
|
||||||
modelEditorDimLabel.setWrapText(true);
|
modelEditorDimLabel.setWrapText(true);
|
||||||
|
|
||||||
|
modelEditorPolyLabel = new Label("– Polygone");
|
||||||
|
modelEditorPolyLabel.setStyle("-fx-text-fill:#aaa; -fx-font-family:monospace; -fx-font-size:11;");
|
||||||
|
modelEditorPolyLabel.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
|
||||||
// ── LOD-Algorithmus ───────────────────────────────────────────────────
|
// ── LOD-Algorithmus ───────────────────────────────────────────────────
|
||||||
Label algoTitle = new Label("Reduktions-Algorithmus");
|
Label algoTitle = new Label("Reduktions-Algorithmus");
|
||||||
algoTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;");
|
algoTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;");
|
||||||
@@ -6791,7 +6819,7 @@ public class EditorApp extends Application {
|
|||||||
panel.getChildren().addAll(
|
panel.getChildren().addAll(
|
||||||
title,
|
title,
|
||||||
new Separator(),
|
new Separator(),
|
||||||
scaleTitle, modelEditorUniformCB, scaleGrid, modelEditorDimLabel,
|
scaleTitle, modelEditorUniformCB, scaleGrid, modelEditorDimLabel, modelEditorPolyLabel,
|
||||||
new Separator(),
|
new Separator(),
|
||||||
algoTitle, algoBlightRB, algoJmeRB,
|
algoTitle, algoBlightRB, algoJmeRB,
|
||||||
new Separator(),
|
new Separator(),
|
||||||
@@ -6871,9 +6899,11 @@ public class EditorApp extends Application {
|
|||||||
input.modelEditorScaleChanged = true;
|
input.modelEditorScaleChanged = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateModelEditorDimensions(float w, float h, float d) {
|
private void updateModelEditorDimensions(float w, float h, float d, int polyCount) {
|
||||||
if (modelEditorDimLabel != null)
|
if (modelEditorDimLabel != null)
|
||||||
modelEditorDimLabel.setText(String.format("B %.2f × H %.2f × T %.2f m", w, h, d));
|
modelEditorDimLabel.setText(String.format("B %.2f × H %.2f × T %.2f m", w, h, d));
|
||||||
|
if (modelEditorPolyLabel != null)
|
||||||
|
modelEditorPolyLabel.setText(String.format("%,d Polygone", polyCount).replace(',', '.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveModelMeta(String relPath,
|
private void saveModelMeta(String relPath,
|
||||||
@@ -6965,6 +6995,12 @@ public class EditorApp extends Application {
|
|||||||
input.modelEditorBakeScaleRequest = true;
|
input.modelEditorBakeScaleRequest = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In-Memory-LODs in j3o einbetten (nach Scale-Bake, vor Thumbnail)
|
||||||
|
if (input.modelImportHasLod1 || input.modelImportHasLod2) {
|
||||||
|
input.modelEditorEmbedLodsPath = finalJ3o;
|
||||||
|
input.modelEditorEmbedLodsRequest = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Thumbnail generieren (JME3-Thread liest das Flag und rendert)
|
// Thumbnail generieren (JME3-Thread liest das Flag und rendert)
|
||||||
input.modelEditorThumbnailRequest = finalJ3o;
|
input.modelEditorThumbnailRequest = finalJ3o;
|
||||||
}
|
}
|
||||||
@@ -7142,6 +7178,7 @@ public class EditorApp extends Application {
|
|||||||
|
|
||||||
private void showLodGenDialog(int level, int defaultPct) {
|
private void showLodGenDialog(int level, int defaultPct) {
|
||||||
javafx.scene.control.Dialog<javafx.util.Pair<String, Integer>> dlg = new javafx.scene.control.Dialog<>();
|
javafx.scene.control.Dialog<javafx.util.Pair<String, Integer>> dlg = new javafx.scene.control.Dialog<>();
|
||||||
|
dlg.initOwner(primaryStage);
|
||||||
dlg.setTitle("LOD " + level + " generieren");
|
dlg.setTitle("LOD " + level + " generieren");
|
||||||
dlg.setHeaderText("Methode und Reduktion für LOD " + level + " wählen");
|
dlg.setHeaderText("Methode und Reduktion für LOD " + level + " wählen");
|
||||||
|
|
||||||
@@ -7599,8 +7636,18 @@ public class EditorApp extends Application {
|
|||||||
gameConsoleStage.setTitle("Spiel-Konsole");
|
gameConsoleStage.setTitle("Spiel-Konsole");
|
||||||
gameConsoleStage.setScene(scene);
|
gameConsoleStage.setScene(scene);
|
||||||
gameConsoleStage.initOwner(primaryStage);
|
gameConsoleStage.initOwner(primaryStage);
|
||||||
|
// Auf zweitem Monitor platzieren und maximieren, falls vorhanden
|
||||||
|
var screens = javafx.stage.Screen.getScreens();
|
||||||
|
if (screens.size() > 1) {
|
||||||
|
javafx.geometry.Rectangle2D sb = screens.get(1).getVisualBounds();
|
||||||
|
gameConsoleStage.setX(sb.getMinX());
|
||||||
|
gameConsoleStage.setY(sb.getMinY());
|
||||||
|
gameConsoleStage.setWidth(sb.getWidth());
|
||||||
|
gameConsoleStage.setHeight(sb.getHeight());
|
||||||
|
} else {
|
||||||
gameConsoleStage.setX(primaryStage.getX());
|
gameConsoleStage.setX(primaryStage.getX());
|
||||||
gameConsoleStage.setY(primaryStage.getY() + primaryStage.getHeight());
|
gameConsoleStage.setY(primaryStage.getY() + primaryStage.getHeight());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
gameConsoleArea.clear();
|
gameConsoleArea.clear();
|
||||||
}
|
}
|
||||||
@@ -8748,6 +8795,7 @@ public class EditorApp extends Application {
|
|||||||
list.getSelectionModel().selectFirst();
|
list.getSelectionModel().selectFirst();
|
||||||
|
|
||||||
javafx.scene.control.Dialog<java.util.List<String>> dlg = new javafx.scene.control.Dialog<>();
|
javafx.scene.control.Dialog<java.util.List<String>> dlg = new javafx.scene.control.Dialog<>();
|
||||||
|
dlg.initOwner(primaryStage);
|
||||||
dlg.setTitle("Animation(en) hinzufügen");
|
dlg.setTitle("Animation(en) hinzufügen");
|
||||||
dlg.setHeaderText("Verfügbare Clips (noch nicht im Set) — Mehrfachauswahl möglich:");
|
dlg.setHeaderText("Verfügbare Clips (noch nicht im Set) — Mehrfachauswahl möglich:");
|
||||||
dlg.getDialogPane().setContent(list);
|
dlg.getDialogPane().setContent(list);
|
||||||
@@ -9042,6 +9090,7 @@ public class EditorApp extends Application {
|
|||||||
|
|
||||||
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> kfDlg =
|
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> kfDlg =
|
||||||
new javafx.scene.control.Dialog<>();
|
new javafx.scene.control.Dialog<>();
|
||||||
|
kfDlg.initOwner(primaryStage);
|
||||||
kfDlg.setTitle(isAdd ? "Offset hinzufügen" : "Offset bearbeiten");
|
kfDlg.setTitle(isAdd ? "Offset hinzufügen" : "Offset bearbeiten");
|
||||||
javafx.scene.control.ButtonType okKf = new javafx.scene.control.ButtonType(
|
javafx.scene.control.ButtonType okKf = new javafx.scene.control.ButtonType(
|
||||||
isAdd ? "Hinzufügen" : "Übernehmen",
|
isAdd ? "Hinzufügen" : "Übernehmen",
|
||||||
@@ -9174,6 +9223,7 @@ public class EditorApp extends Application {
|
|||||||
|
|
||||||
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> dlg =
|
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> dlg =
|
||||||
new javafx.scene.control.Dialog<>();
|
new javafx.scene.control.Dialog<>();
|
||||||
|
dlg.initOwner(primaryStage);
|
||||||
dlg.setTitle("Aktion zuordnen");
|
dlg.setTitle("Aktion zuordnen");
|
||||||
javafx.scene.control.ButtonType ok = new javafx.scene.control.ButtonType("Hinzufügen",
|
javafx.scene.control.ButtonType ok = new javafx.scene.control.ButtonType("Hinzufügen",
|
||||||
javafx.scene.control.ButtonBar.ButtonData.OK_DONE);
|
javafx.scene.control.ButtonBar.ButtonData.OK_DONE);
|
||||||
@@ -9226,6 +9276,7 @@ public class EditorApp extends Application {
|
|||||||
|
|
||||||
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> dlg =
|
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> dlg =
|
||||||
new javafx.scene.control.Dialog<>();
|
new javafx.scene.control.Dialog<>();
|
||||||
|
dlg.initOwner(primaryStage);
|
||||||
dlg.setTitle(isEdit ? "Clip-Teil bearbeiten" : "Clip-Teil hinzufügen");
|
dlg.setTitle(isEdit ? "Clip-Teil bearbeiten" : "Clip-Teil hinzufügen");
|
||||||
dlg.setHeaderText((isEdit ? "Bearbeiten: " : "Neuer Teil für: ") + source);
|
dlg.setHeaderText((isEdit ? "Bearbeiten: " : "Neuer Teil für: ") + source);
|
||||||
javafx.scene.control.ButtonType ok = new javafx.scene.control.ButtonType(
|
javafx.scene.control.ButtonType ok = new javafx.scene.control.ButtonType(
|
||||||
@@ -9317,6 +9368,7 @@ public class EditorApp extends Application {
|
|||||||
Alert warn = new Alert(Alert.AlertType.CONFIRMATION,
|
Alert warn = new Alert(Alert.AlertType.CONFIRMATION,
|
||||||
"Das aktuelle AnimSet hat ungespeicherte Änderungen.\nTrotzdem ein neues Set anlegen?",
|
"Das aktuelle AnimSet hat ungespeicherte Änderungen.\nTrotzdem ein neues Set anlegen?",
|
||||||
ButtonType.OK, ButtonType.CANCEL);
|
ButtonType.OK, ButtonType.CANCEL);
|
||||||
|
warn.initOwner(primaryStage);
|
||||||
warn.setHeaderText("Ungespeicherte Änderungen");
|
warn.setHeaderText("Ungespeicherte Änderungen");
|
||||||
warn.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> doCreateNewAnimSet());
|
warn.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> doCreateNewAnimSet());
|
||||||
} else {
|
} else {
|
||||||
@@ -9326,6 +9378,7 @@ public class EditorApp extends Application {
|
|||||||
|
|
||||||
private void doCreateNewAnimSet() {
|
private void doCreateNewAnimSet() {
|
||||||
TextInputDialog dlg = new TextInputDialog();
|
TextInputDialog dlg = new TextInputDialog();
|
||||||
|
dlg.initOwner(primaryStage);
|
||||||
dlg.setTitle("Neues Animations-Set");
|
dlg.setTitle("Neues Animations-Set");
|
||||||
dlg.setHeaderText("Name des neuen Animations-Sets:");
|
dlg.setHeaderText("Name des neuen Animations-Sets:");
|
||||||
dlg.setContentText("Name:");
|
dlg.setContentText("Name:");
|
||||||
@@ -9634,6 +9687,7 @@ public class EditorApp extends Application {
|
|||||||
private void showSaveAnimSetDialog() {
|
private void showSaveAnimSetDialog() {
|
||||||
if (animClipListView == null || animClipListView.getItems().isEmpty()) return;
|
if (animClipListView == null || animClipListView.getItems().isEmpty()) return;
|
||||||
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> dlg = new javafx.scene.control.Dialog<>();
|
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> dlg = new javafx.scene.control.Dialog<>();
|
||||||
|
dlg.initOwner(primaryStage);
|
||||||
dlg.setTitle("Animations-Set speichern");
|
dlg.setTitle("Animations-Set speichern");
|
||||||
dlg.setHeaderText("Clips und Aktions-Zuweisung für das Set konfigurieren");
|
dlg.setHeaderText("Clips und Aktions-Zuweisung für das Set konfigurieren");
|
||||||
javafx.scene.control.ButtonType ok = new javafx.scene.control.ButtonType("Speichern",
|
javafx.scene.control.ButtonType ok = new javafx.scene.control.ButtonType("Speichern",
|
||||||
@@ -9706,6 +9760,7 @@ public class EditorApp extends Application {
|
|||||||
|
|
||||||
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> addDlg =
|
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> addDlg =
|
||||||
new javafx.scene.control.Dialog<>();
|
new javafx.scene.control.Dialog<>();
|
||||||
|
addDlg.initOwner(primaryStage);
|
||||||
addDlg.setTitle("Aktion zuweisen");
|
addDlg.setTitle("Aktion zuweisen");
|
||||||
javafx.scene.control.ButtonType addOk = new javafx.scene.control.ButtonType("Hinzufügen",
|
javafx.scene.control.ButtonType addOk = new javafx.scene.control.ButtonType("Hinzufügen",
|
||||||
javafx.scene.control.ButtonBar.ButtonData.OK_DONE);
|
javafx.scene.control.ButtonBar.ButtonData.OK_DONE);
|
||||||
|
|||||||
@@ -631,6 +631,13 @@ public class SharedInput {
|
|||||||
public final java.util.concurrent.atomic.AtomicReference<String>
|
public final java.util.concurrent.atomic.AtomicReference<String>
|
||||||
animStripClipsRequest = new java.util.concurrent.atomic.AtomicReference<>();
|
animStripClipsRequest = new java.util.concurrent.atomic.AtomicReference<>();
|
||||||
|
|
||||||
|
// ── Impostor-Regenerierung ────────────────────────────────────────────────
|
||||||
|
/** JFX → JME: Impostor (lod2) für das angegebene j3o neu generieren + in gleicher Datei speichern.
|
||||||
|
* Wert: relativer Asset-Pfad (z.B. "Models/trees/pine/medium/pine_medium_xxx.j3o"). null = kein Auftrag. */
|
||||||
|
public final AtomicReference<String> modelEditorRegenerateImpostor = new AtomicReference<>();
|
||||||
|
/** JME → JFX: Status nach Impostor-Regen ("OK" oder "Fehler: …"). */
|
||||||
|
public volatile String modelEditorRegenerateImpostorStatus = null;
|
||||||
|
|
||||||
/** JME3 → JavaFX: Status-Meldung für Clip- und Set-Operationen. */
|
/** JME3 → JavaFX: Status-Meldung für Clip- und Set-Operationen. */
|
||||||
public volatile String animOpStatus = null;
|
public volatile String animOpStatus = null;
|
||||||
/** JME3 → JavaFX: Status-Meldung für Einbett-Operationen (Character Editor). */
|
/** JME3 → JavaFX: Status-Meldung für Einbett-Operationen (Character Editor). */
|
||||||
@@ -681,6 +688,9 @@ public class SharedInput {
|
|||||||
public volatile float modelEditorBoundsD = 0f;
|
public volatile float modelEditorBoundsD = 0f;
|
||||||
public volatile boolean modelEditorBoundsReady = false;
|
public volatile boolean modelEditorBoundsReady = false;
|
||||||
|
|
||||||
|
/** JME → JFX: Polygonanzahl der aktuell sichtbaren LOD-Stufe. */
|
||||||
|
public volatile int modelEditorPolyCount = 0;
|
||||||
|
|
||||||
/** JFX → JME: aktuelle Skalierung (Echtzeit-Vorschau). */
|
/** JFX → JME: aktuelle Skalierung (Echtzeit-Vorschau). */
|
||||||
public volatile float modelEditorScaleX = 1f;
|
public volatile float modelEditorScaleX = 1f;
|
||||||
public volatile float modelEditorScaleY = 1f;
|
public volatile float modelEditorScaleY = 1f;
|
||||||
@@ -719,6 +729,9 @@ public class SharedInput {
|
|||||||
public volatile boolean modelEditorLodChanged = false;
|
public volatile boolean modelEditorLodChanged = false;
|
||||||
/** JFX → JME: Eingebettetes LOD aus j3o entfernen und speichern (1=lod1, 2=lod2, -1=kein Auftrag). */
|
/** JFX → JME: Eingebettetes LOD aus j3o entfernen und speichern (1=lod1, 2=lod2, -1=kein Auftrag). */
|
||||||
public volatile int modelEditorRemoveEmbeddedLod = -1;
|
public volatile int modelEditorRemoveEmbeddedLod = -1;
|
||||||
|
/** JFX → JME: In-Memory-LODs in die j3o-Datei einbetten und speichern. */
|
||||||
|
public volatile boolean modelEditorEmbedLodsRequest = false;
|
||||||
|
public volatile java.nio.file.Path modelEditorEmbedLodsPath = null;
|
||||||
|
|
||||||
/** JFX → JME: Richtung der Hauptlichtquelle in der Vorschau.
|
/** JFX → JME: Richtung der Hauptlichtquelle in der Vorschau.
|
||||||
* Azimut 0–360° (Kompassrichtung), Elevation 0–90° (Höhe über Horizont). */
|
* Azimut 0–360° (Kompassrichtung), Elevation 0–90° (Höhe über Horizont). */
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.jme3.app.state.BaseAppState;
|
|||||||
import com.jme3.asset.AssetManager;
|
import com.jme3.asset.AssetManager;
|
||||||
import com.jme3.bounding.BoundingBox;
|
import com.jme3.bounding.BoundingBox;
|
||||||
import com.jme3.export.binary.BinaryExporter;
|
import com.jme3.export.binary.BinaryExporter;
|
||||||
|
import com.jme3.export.binary.BinaryImporter;
|
||||||
import com.jme3.material.Material;
|
import com.jme3.material.Material;
|
||||||
import com.jme3.material.RenderState;
|
import com.jme3.material.RenderState;
|
||||||
import com.jme3.math.ColorRGBA;
|
import com.jme3.math.ColorRGBA;
|
||||||
@@ -75,6 +76,10 @@ public class EzTreeState extends BaseAppState {
|
|||||||
private int capturePass = 0;
|
private int capturePass = 0;
|
||||||
private ByteBuffer[] capturePixels = new ByteBuffer[ImpostorUtil.DIRS];
|
private ByteBuffer[] capturePixels = new ByteBuffer[ImpostorUtil.DIRS];
|
||||||
|
|
||||||
|
// ── Impostor-Regen-Phase (bestehendes j3o aktualisieren) ─────────────────
|
||||||
|
private Path pendingRegenAbsPath = null;
|
||||||
|
private Node pendingRegenRoot = null;
|
||||||
|
|
||||||
public EzTreeState(SharedInput input) { this.input = input; }
|
public EzTreeState(SharedInput input) { this.input = input; }
|
||||||
|
|
||||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
@@ -100,11 +105,18 @@ public class EzTreeState extends BaseAppState {
|
|||||||
|
|
||||||
if (pendingRequest != null && captureReady[0]) {
|
if (pendingRequest != null && captureReady[0]) {
|
||||||
finishCapture();
|
finishCapture();
|
||||||
} else if (pendingRequest == null) {
|
} else if (pendingRegenAbsPath != null && captureReady[0]) {
|
||||||
|
finishRegenCapture();
|
||||||
|
} else if (pendingRequest == null && pendingRegenAbsPath == null) {
|
||||||
|
String regenAsset = input.modelEditorRegenerateImpostor.getAndSet(null);
|
||||||
|
if (regenAsset != null) {
|
||||||
|
startImpostorRegen(regenAsset);
|
||||||
|
} else {
|
||||||
SharedInput.EzTreeGenRequest req = input.ezTreeGenQueue.poll();
|
SharedInput.EzTreeGenRequest req = input.ezTreeGenQueue.poll();
|
||||||
if (req != null) startGeneration(req);
|
if (req != null) startGeneration(req);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Phase 1: Generierung ──────────────────────────────────────────────────
|
// ── Phase 1: Generierung ──────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -122,6 +134,18 @@ public class EzTreeState extends BaseAppState {
|
|||||||
ld1Node.setLocalScale(1f / 3f);
|
ld1Node.setLocalScale(1f / 3f);
|
||||||
|
|
||||||
BoundingBox bb = boundsOf(hdNode);
|
BoundingBox bb = boundsOf(hdNode);
|
||||||
|
// Stammbasis auf Y=0 setzen: 1/3-Scale legt den Stamm typischerweise
|
||||||
|
// bei Y>0. Translation in hdNode/ld1Node einbacken, damit das j3o
|
||||||
|
// ohne Laufzeit-Korrektur auf dem Terrain steht.
|
||||||
|
if (bb != null) {
|
||||||
|
float minY = bb.getCenter().y - bb.getYExtent();
|
||||||
|
if (Math.abs(minY) > 0.01f) {
|
||||||
|
hdNode.setLocalTranslation(0f, -minY, 0f);
|
||||||
|
ld1Node.setLocalTranslation(0f, -minY, 0f);
|
||||||
|
hdNode.updateGeometricState();
|
||||||
|
bb = boundsOf(hdNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
float camDist = bb != null
|
float camDist = bb != null
|
||||||
? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3.2f
|
? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3.2f
|
||||||
: 20f;
|
: 20f;
|
||||||
@@ -155,9 +179,8 @@ public class EzTreeState extends BaseAppState {
|
|||||||
|
|
||||||
private Node buildLod1Node(SharedInput.EzTreeGenRequest req) {
|
private Node buildLod1Node(SharedInput.EzTreeGenRequest req) {
|
||||||
TreeOptions opts = req.options();
|
TreeOptions opts = req.options();
|
||||||
// Versuche Node.js mit halber Blattanzahl – gleiche Baumform/-größe wie LOD0.
|
// Blattanzahl nicht reduzieren – nur Bark-Geometrie (sections/segments) vereinfachen.
|
||||||
TreeOptions ld = opts.copy();
|
TreeOptions ld = opts.copy();
|
||||||
ld.leaves.count = Math.max(0, opts.leaves.count / 2);
|
|
||||||
Node n = tryNodeJsGeneration(new SharedInput.EzTreeGenRequest(ld, req.presetName(), false));
|
Node n = tryNodeJsGeneration(new SharedInput.EzTreeGenRequest(ld, req.presetName(), false));
|
||||||
if (n != null) {
|
if (n != null) {
|
||||||
n.setName("EzTree_ld1");
|
n.setName("EzTree_ld1");
|
||||||
@@ -505,6 +528,106 @@ public class EzTreeState extends BaseAppState {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Impostor-Regen für bestehendes j3o ───────────────────────────────────
|
||||||
|
|
||||||
|
private void startImpostorRegen(String assetPath) {
|
||||||
|
cleanupCapture();
|
||||||
|
try {
|
||||||
|
Path absPath = ASSET_ROOT.resolve(assetPath);
|
||||||
|
BinaryImporter im = BinaryImporter.getInstance();
|
||||||
|
im.setAssetManager(assets);
|
||||||
|
Object s = im.load(absPath.toFile());
|
||||||
|
if (!(s instanceof Node root)) {
|
||||||
|
input.modelEditorRegenerateImpostorStatus = "Fehler: kein Node in " + absPath.getFileName();
|
||||||
|
input.treeGenStatusMsg = input.modelEditorRegenerateImpostorStatus;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (root.getChildren().isEmpty() || !(root.getChild(0) instanceof Node hdNode)) {
|
||||||
|
input.modelEditorRegenerateImpostorStatus = "Fehler: kein HD-Kind in " + absPath.getFileName();
|
||||||
|
input.treeGenStatusMsg = input.modelEditorRegenerateImpostorStatus;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.updateGeometricState();
|
||||||
|
BoundingBox bb = boundsOf(hdNode);
|
||||||
|
if (bb == null) {
|
||||||
|
bb = new BoundingBox(Vector3f.ZERO, 5f, 10f, 5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingRegenAbsPath = absPath;
|
||||||
|
pendingRegenRoot = root;
|
||||||
|
pendingHdNode = hdNode;
|
||||||
|
pendingBb = bb;
|
||||||
|
capturePass = 0;
|
||||||
|
capturePixels = new ByteBuffer[ImpostorUtil.DIRS];
|
||||||
|
|
||||||
|
startCapturePass(0);
|
||||||
|
input.treeGenStatusMsg = "Impostor neu generieren (1/" + ImpostorUtil.DIRS + ")…";
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[EzTree] Impostor-Regen fehlgeschlagen: {}", e.getMessage(), e);
|
||||||
|
input.modelEditorRegenerateImpostorStatus = "Fehler: " + e.getMessage();
|
||||||
|
input.treeGenStatusMsg = input.modelEditorRegenerateImpostorStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void finishRegenCapture() {
|
||||||
|
capturePixels[capturePass] = ImpostorUtil.readPixels(app, captureFB);
|
||||||
|
ImpostorUtil.cleanup(app, captureVP, captureFB);
|
||||||
|
captureVP = null;
|
||||||
|
captureFB = null;
|
||||||
|
captureReady[0] = false;
|
||||||
|
|
||||||
|
if (capturePass < ImpostorUtil.DIRS - 1) {
|
||||||
|
capturePass++;
|
||||||
|
input.treeGenStatusMsg = "Impostor neu generieren (" + (capturePass + 1) + "/" + ImpostorUtil.DIRS + ")…";
|
||||||
|
startCapturePass(capturePass);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Path absPath = pendingRegenAbsPath;
|
||||||
|
Node root = pendingRegenRoot;
|
||||||
|
BoundingBox bb = pendingBb;
|
||||||
|
ByteBuffer[] pixels = capturePixels;
|
||||||
|
|
||||||
|
pendingRegenAbsPath = null;
|
||||||
|
pendingRegenRoot = null;
|
||||||
|
pendingHdNode = null;
|
||||||
|
pendingBb = null;
|
||||||
|
capturePixels = new ByteBuffer[ImpostorUtil.DIRS];
|
||||||
|
|
||||||
|
ByteBuffer atlas = ImpostorUtil.combineAtlas(pixels);
|
||||||
|
Texture2D atlasTex = ImpostorUtil.buildAtlasTexture(atlas);
|
||||||
|
|
||||||
|
Spatial oldLod2 = root.getChild("lod2");
|
||||||
|
if (oldLod2 != null) root.detachChild(oldLod2);
|
||||||
|
|
||||||
|
Node newLod2 = ImpostorUtil.makeImpostorNode(bb, atlasTex, assets);
|
||||||
|
newLod2.setCullHint(Spatial.CullHint.Always);
|
||||||
|
newLod2.setShadowMode(com.jme3.renderer.queue.RenderQueue.ShadowMode.Off);
|
||||||
|
root.attachChild(newLod2);
|
||||||
|
|
||||||
|
stripControlsRecursive(root);
|
||||||
|
|
||||||
|
try {
|
||||||
|
BinaryExporter.getInstance().save(root, absPath.toFile());
|
||||||
|
log.info("[EzTree] Impostor gespeichert: {}", absPath.getFileName());
|
||||||
|
input.treeGenStatusMsg = "Impostor gespeichert: " + absPath.getFileName();
|
||||||
|
input.modelEditorRegenerateImpostorStatus = "OK";
|
||||||
|
input.refreshAssets = true;
|
||||||
|
} catch (java.io.IOException e) {
|
||||||
|
log.error("[EzTree] Impostor-Regen Speichern fehlgeschlagen: {}", e.getMessage());
|
||||||
|
input.treeGenStatusMsg = "Impostor-Regen-Fehler: " + e.getMessage();
|
||||||
|
input.modelEditorRegenerateImpostorStatus = "Fehler: " + e.getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void stripControlsRecursive(Spatial s) {
|
||||||
|
while (s.getNumControls() > 0) s.removeControl(s.getControl(0));
|
||||||
|
if (s instanceof Node n) {
|
||||||
|
for (Spatial child : n.getChildren()) stripControlsRecursive(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void cleanupCapture() {
|
private void cleanupCapture() {
|
||||||
ImpostorUtil.cleanup(app, captureVP, captureFB);
|
ImpostorUtil.cleanup(app, captureVP, captureFB);
|
||||||
captureVP = null;
|
captureVP = null;
|
||||||
@@ -579,8 +702,7 @@ public class EzTreeState extends BaseAppState {
|
|||||||
Path baseDir = ASSET_ROOT.resolve("Models").resolve("trees").resolve(subPath);
|
Path baseDir = ASSET_ROOT.resolve("Models").resolve("trees").resolve(subPath);
|
||||||
Files.createDirectories(baseDir);
|
Files.createDirectories(baseDir);
|
||||||
Path outPath = baseDir.resolve(fileName + ".j3o");
|
Path outPath = baseDir.resolve(fileName + ".j3o");
|
||||||
// Controls vor Export entfernen (nicht serialisierbar über BinaryExporter)
|
stripControlsRecursive(lodRoot);
|
||||||
while (lodRoot.getNumControls() > 0) lodRoot.removeControl(lodRoot.getControl(0));
|
|
||||||
BinaryExporter.getInstance().save(lodRoot, outPath.toFile());
|
BinaryExporter.getInstance().save(lodRoot, outPath.toFile());
|
||||||
|
|
||||||
de.blight.common.ModelMeta meta = new de.blight.common.ModelMeta(
|
de.blight.common.ModelMeta meta = new de.blight.common.ModelMeta(
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import com.jme3.anim.SkinningControl;
|
|||||||
import com.jme3.util.BufferUtils;
|
import com.jme3.util.BufferUtils;
|
||||||
import de.blight.editor.SharedInput;
|
import de.blight.editor.SharedInput;
|
||||||
import de.blight.editor.util.ImpostorUtil;
|
import de.blight.editor.util.ImpostorUtil;
|
||||||
|
import de.blight.editor.util.ModelExportUtil;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -104,6 +105,10 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
private boolean hasEmbeddedLods = false;
|
private boolean hasEmbeddedLods = false;
|
||||||
private Spatial[] embeddedLodSpatials = null;
|
private Spatial[] embeddedLodSpatials = null;
|
||||||
|
|
||||||
|
// true wenn showSpatialDirectly() zuletzt einen generierten Preview in modelWrapper gelegt hat.
|
||||||
|
// Beim Wechsel zurück zu einem eingebetteten LOD muss originalSpatial wiederhergestellt werden.
|
||||||
|
private boolean showingDirectPreview = false;
|
||||||
|
|
||||||
// Materialien mit eigenem m_LightDir (Tree.j3md / TreeLeaf.j3md) – werden jedes Frame aktualisiert
|
// Materialien mit eigenem m_LightDir (Tree.j3md / TreeLeaf.j3md) – werden jedes Frame aktualisiert
|
||||||
private final java.util.List<Material> treeLightMaterials = new java.util.ArrayList<>();
|
private final java.util.List<Material> treeLightMaterials = new java.util.ArrayList<>();
|
||||||
// PBR-Materialien (PBRLighting) – brauchen EmissivePower=1 als IBL-Fallback
|
// PBR-Materialien (PBRLighting) – brauchen EmissivePower=1 als IBL-Fallback
|
||||||
@@ -148,6 +153,25 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
// Welt sofort ausblenden, auch wenn noch kein Modell geladen ist
|
// Welt sofort ausblenden, auch wenn noch kein Modell geladen ist
|
||||||
if (previewRoot == null) enterPreview();
|
if (previewRoot == null) enterPreview();
|
||||||
|
|
||||||
|
// Scale-Bake und LOD-Einbettung VOR dem Modellwechsel verarbeiten.
|
||||||
|
// Sonst würde ein openPath im gleichen Frame die Preview-Nodes leeren,
|
||||||
|
// bevor embedLodsAndSave() sie lesen kann → LODs würden nicht gespeichert.
|
||||||
|
if (input.modelEditorBakeScaleRequest) {
|
||||||
|
input.modelEditorBakeScaleRequest = false;
|
||||||
|
Path bakePath = input.modelEditorBakeScalePath;
|
||||||
|
if (bakePath != null) {
|
||||||
|
bakeScaleIntoModel(bakePath,
|
||||||
|
input.modelEditorScaleX,
|
||||||
|
input.modelEditorScaleY,
|
||||||
|
input.modelEditorScaleZ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.modelEditorEmbedLodsRequest) {
|
||||||
|
input.modelEditorEmbedLodsRequest = false;
|
||||||
|
embedLodsAndSave(input.modelEditorEmbedLodsPath);
|
||||||
|
}
|
||||||
|
|
||||||
// Modell laden
|
// Modell laden
|
||||||
String openPath = input.modelEditorOpenPath;
|
String openPath = input.modelEditorOpenPath;
|
||||||
if (openPath != null) {
|
if (openPath != null) {
|
||||||
@@ -186,8 +210,22 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
showSpatialDirectly(genPreview.clone());
|
showSpatialDirectly(genPreview.clone());
|
||||||
} else if (hasEmbeddedLods && embeddedLodSpatials != null) {
|
} else if (hasEmbeddedLods && embeddedLodSpatials != null) {
|
||||||
|
// Nach showSpatialDirectly() ist originalSpatial aus modelWrapper raus – wiederherstellen
|
||||||
|
if (showingDirectPreview && originalSpatial != null) {
|
||||||
|
if (modelWrapper != null) previewRoot.detachChild(modelWrapper);
|
||||||
|
modelWrapper = new Node("model_wrapper");
|
||||||
|
modelWrapper.attachChild(originalSpatial);
|
||||||
|
applyScale(input.modelEditorScaleX, input.modelEditorScaleY, input.modelEditorScaleZ);
|
||||||
|
previewRoot.attachChild(modelWrapper);
|
||||||
|
showingDirectPreview = false;
|
||||||
|
}
|
||||||
// Eingebettete LOD-Kinder direkt ein-/ausblenden
|
// Eingebettete LOD-Kinder direkt ein-/ausblenden
|
||||||
int lodIdx = Math.min(input.modelEditorLodPreview, embeddedLodSpatials.length - 1);
|
int lodIdx = input.modelEditorLodPreview;
|
||||||
|
// Fallback auf 0 wenn der gewünschte Index keinen Eintrag hat
|
||||||
|
if (lodIdx >= embeddedLodSpatials.length || embeddedLodSpatials[lodIdx] == null) {
|
||||||
|
lodIdx = 0;
|
||||||
|
input.modelEditorLodPreview = 0;
|
||||||
|
}
|
||||||
for (int i = 0; i < embeddedLodSpatials.length; i++) {
|
for (int i = 0; i < embeddedLodSpatials.length; i++) {
|
||||||
if (embeddedLodSpatials[i] != null) {
|
if (embeddedLodSpatials[i] != null) {
|
||||||
boolean show = (i == lodIdx);
|
boolean show = (i == lodIdx);
|
||||||
@@ -195,6 +233,7 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
embeddedLodSpatials[i].setShadowMode(RenderQueue.ShadowMode.Off);
|
embeddedLodSpatials[i].setShadowMode(RenderQueue.ShadowMode.Off);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
updateBounds();
|
||||||
} else {
|
} else {
|
||||||
// Pfad-basierter Fallback
|
// Pfad-basierter Fallback
|
||||||
String path = switch (input.modelEditorLodPreview) {
|
String path = switch (input.modelEditorLodPreview) {
|
||||||
@@ -251,18 +290,6 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
input.modelEditorAttachedEmitters);
|
input.modelEditorAttachedEmitters);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scale in j3o einbrennen (vor Thumbnail, damit Thumbnail die gebackene Datei erhält)
|
|
||||||
if (input.modelEditorBakeScaleRequest) {
|
|
||||||
input.modelEditorBakeScaleRequest = false;
|
|
||||||
Path bakePath = input.modelEditorBakeScalePath;
|
|
||||||
if (bakePath != null) {
|
|
||||||
bakeScaleIntoModel(bakePath,
|
|
||||||
input.modelEditorScaleX,
|
|
||||||
input.modelEditorScaleY,
|
|
||||||
input.modelEditorScaleZ);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Thumbnail auf Anforderung generieren
|
// Thumbnail auf Anforderung generieren
|
||||||
Path thumbReq = input.modelEditorThumbnailRequest;
|
Path thumbReq = input.modelEditorThumbnailRequest;
|
||||||
if (thumbReq != null && modelWrapper != null) {
|
if (thumbReq != null && modelWrapper != null) {
|
||||||
@@ -364,6 +391,7 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
currentPath = assetPath;
|
currentPath = assetPath;
|
||||||
|
|
||||||
modelWrapper = new Node("model_wrapper");
|
modelWrapper = new Node("model_wrapper");
|
||||||
|
showingDirectPreview = false;
|
||||||
hasEmbeddedLods = false;
|
hasEmbeddedLods = false;
|
||||||
embeddedLodSpatials = null;
|
embeddedLodSpatials = null;
|
||||||
input.modelEditorHasEmbeddedLods = false;
|
input.modelEditorHasEmbeddedLods = false;
|
||||||
@@ -383,18 +411,43 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
originalSpatial = model;
|
originalSpatial = model;
|
||||||
modelWrapper.attachChild(model);
|
modelWrapper.attachChild(model);
|
||||||
|
|
||||||
// Eingebettete LODs erkennen: Node mit ≥2 Kindern, wobei Kind 0 sichtbar
|
// Eingebettete LODs erkennen: Node mit ≥2 Kindern, wobei mindestens eines
|
||||||
// und alle weiteren mit CullHint.Always gesetzt sind (EZ-Tree / TreeGenerator-Muster).
|
// hinter Index 0 CullHint.Always hat (EZ-Tree / TreeGenerator-Muster).
|
||||||
if (model instanceof Node rootNode && rootNode.getChildren().size() >= 2) {
|
// embeddedLodSpatials ist immer 3 Einträge: [0]=HD, [1]=LOD1|null, [2]=LOD2|null
|
||||||
|
if (model instanceof Node rootNode) {
|
||||||
|
log.info("[ModelEditor] Geladen '{}': {} direkte Kinder", assetPath, rootNode.getChildren().size());
|
||||||
|
for (int i = 0; i < rootNode.getChildren().size(); i++) {
|
||||||
|
Spatial c = rootNode.getChild(i);
|
||||||
|
log.info("[ModelEditor] [{}] '{}' cullHint={}", i, c.getName(), c.getCullHint());
|
||||||
|
}
|
||||||
|
if (rootNode.getChildren().size() >= 2) {
|
||||||
var children = rootNode.getChildren();
|
var children = rootNode.getChildren();
|
||||||
boolean lodPattern = children.stream().skip(1)
|
boolean hasAnyHidden = children.stream().skip(1)
|
||||||
.allMatch(s -> s.getCullHint() == Spatial.CullHint.Always);
|
.anyMatch(s -> s.getCullHint() == Spatial.CullHint.Always);
|
||||||
if (lodPattern) {
|
log.info("[ModelEditor] hasAnyHidden (ab Index 1)={}", hasAnyHidden);
|
||||||
embeddedLodSpatials = children.toArray(new Spatial[0]);
|
if (hasAnyHidden) {
|
||||||
hasEmbeddedLods = true;
|
embeddedLodSpatials = new Spatial[3];
|
||||||
|
embeddedLodSpatials[0] = children.get(0);
|
||||||
|
for (int i = 1; i < children.size(); i++) {
|
||||||
|
Spatial child = children.get(i);
|
||||||
|
if (child.getCullHint() != Spatial.CullHint.Always) continue;
|
||||||
|
if ("lod2".equals(child.getName())) {
|
||||||
|
embeddedLodSpatials[2] = child;
|
||||||
|
} else {
|
||||||
|
embeddedLodSpatials[1] = child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hasEmbeddedLods = embeddedLodSpatials[1] != null || embeddedLodSpatials[2] != null;
|
||||||
|
log.info("[ModelEditor] Eingebettete LODs erkannt: lod1={}, lod2={}",
|
||||||
|
embeddedLodSpatials[1] != null, embeddedLodSpatials[2] != null);
|
||||||
|
if (hasEmbeddedLods) {
|
||||||
input.modelEditorHasEmbeddedLods = true;
|
input.modelEditorHasEmbeddedLods = true;
|
||||||
input.modelEditorHasEmbeddedLod1 = embeddedLodSpatials.length > 1;
|
input.modelEditorHasEmbeddedLod1 = embeddedLodSpatials[1] != null;
|
||||||
input.modelEditorHasEmbeddedLod2 = embeddedLodSpatials.length > 2;
|
input.modelEditorHasEmbeddedLod2 = embeddedLodSpatials[2] != null;
|
||||||
|
} else {
|
||||||
|
embeddedLodSpatials = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -481,6 +534,7 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
if (previewRoot == null) enterPreview();
|
if (previewRoot == null) enterPreview();
|
||||||
if (modelWrapper != null) previewRoot.detachChild(modelWrapper);
|
if (modelWrapper != null) previewRoot.detachChild(modelWrapper);
|
||||||
modelWrapper = new Node("model_wrapper");
|
modelWrapper = new Node("model_wrapper");
|
||||||
|
showingDirectPreview = true;
|
||||||
stripControls(spatial);
|
stripControls(spatial);
|
||||||
treeLightMaterials.clear();
|
treeLightMaterials.clear();
|
||||||
pbrMaterials.clear();
|
pbrMaterials.clear();
|
||||||
@@ -654,7 +708,8 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
java.nio.file.Path absPath = ASSET_ROOT.resolve(currentPath);
|
java.nio.file.Path absPath = ASSET_ROOT.resolve(currentPath);
|
||||||
Spatial root = modelWrapper.getChild(0);
|
Spatial root = modelWrapper.getChild(0);
|
||||||
BinaryExporter.getInstance().save(root, absPath.toFile());
|
BinaryExporter.getInstance().save(root, absPath.toFile());
|
||||||
app.getAssetManager().clearCache();
|
syncToMirrors(absPath);
|
||||||
|
app.getAssetManager().deleteFromCache(new com.jme3.asset.ModelKey(currentPath));
|
||||||
log.info("[ModelEditor] LOD {} entfernt, Datei gespeichert: {}", lodIdx, currentPath);
|
log.info("[ModelEditor] LOD {} entfernt, Datei gespeichert: {}", lodIdx, currentPath);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.error("[ModelEditor] Speichern nach LOD-Entfernung fehlgeschlagen: {}", ex.getMessage(), ex);
|
log.error("[ModelEditor] Speichern nach LOD-Entfernung fehlgeschlagen: {}", ex.getMessage(), ex);
|
||||||
@@ -781,10 +836,22 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
input.modelEditorBoundsH = bb.getYExtent() * 2f;
|
input.modelEditorBoundsH = bb.getYExtent() * 2f;
|
||||||
input.modelEditorBoundsD = bb.getZExtent() * 2f;
|
input.modelEditorBoundsD = bb.getZExtent() * 2f;
|
||||||
input.modelEditorBoundsReady = true;
|
input.modelEditorBoundsReady = true;
|
||||||
|
input.modelEditorPolyCount = countVisibleTriangles(modelWrapper);
|
||||||
orbitCenter = bb.getCenter().clone();
|
orbitCenter = bb.getCenter().clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int countVisibleTriangles(Spatial s) {
|
||||||
|
if (s == null || s.getCullHint() == Spatial.CullHint.Always) return 0;
|
||||||
|
if (s instanceof Geometry g) return g.getMesh().getTriangleCount();
|
||||||
|
if (s instanceof Node n) {
|
||||||
|
int sum = 0;
|
||||||
|
for (Spatial child : n.getChildren()) sum += countVisibleTriangles(child);
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
private BoundingBox getBoundingBox() {
|
private BoundingBox getBoundingBox() {
|
||||||
if (modelWrapper == null) return null;
|
if (modelWrapper == null) return null;
|
||||||
modelWrapper.updateGeometricState();
|
modelWrapper.updateGeometricState();
|
||||||
@@ -900,9 +967,22 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
if (s instanceof Node n) n.getChildren().forEach(ModelEditorState::stripControls);
|
if (s instanceof Node n) n.getChildren().forEach(ModelEditorState::stripControls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isImpostorNode(Node n) {
|
||||||
|
// Direktkinder prüfen (NICHT rekursiv) – Node.getChild() sucht den ganzen Baum
|
||||||
|
for (Spatial c : n.getChildren()) {
|
||||||
|
if ("quad_0".equals(c.getName())) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private void initImpostorControls(Spatial s) {
|
private void initImpostorControls(Spatial s) {
|
||||||
ImpostorUtil.ImpostorViewControl ctrl = s.getControl(ImpostorUtil.ImpostorViewControl.class);
|
ImpostorUtil.ImpostorViewControl ctrl = s.getControl(ImpostorUtil.ImpostorViewControl.class);
|
||||||
if (ctrl != null) ctrl.setCamera(cam);
|
if (ctrl != null) {
|
||||||
|
ctrl.setCamera(cam);
|
||||||
|
} else if (s instanceof Node n && isImpostorNode(n)) {
|
||||||
|
// Impostor-Node nach j3o-Reload ohne gespeicherten Control → neu hinzufügen
|
||||||
|
ImpostorUtil.addViewControl(n, cam);
|
||||||
|
}
|
||||||
if (s instanceof Node n) {
|
if (s instanceof Node n) {
|
||||||
for (Spatial child : n.getChildren()) initImpostorControls(child);
|
for (Spatial child : n.getChildren()) initImpostorControls(child);
|
||||||
}
|
}
|
||||||
@@ -964,6 +1044,187 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── LOD-Einbettung ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kopiert eine gespeicherte j3o-Datei in alle bekannten Spiegel-Verzeichnisse
|
||||||
|
* (IntelliJ bin/main und Gradle build/resources/main), damit JME's ClasspathLocator
|
||||||
|
* nicht die veraltete Fassung aus dem Classpath lädt.
|
||||||
|
*/
|
||||||
|
private void syncToMirrors(Path savedFile) {
|
||||||
|
java.nio.file.Path rel;
|
||||||
|
try {
|
||||||
|
rel = ASSET_ROOT.relativize(savedFile);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
java.nio.file.Path projectRoot = de.blight.editor.ProjectRoot.PATH;
|
||||||
|
java.nio.file.Path[] mirrors = {
|
||||||
|
projectRoot.resolve("blight-assets").resolve("bin").resolve("main").resolve(rel),
|
||||||
|
projectRoot.resolve("blight-assets").resolve("build").resolve("resources").resolve("main").resolve(rel),
|
||||||
|
};
|
||||||
|
for (java.nio.file.Path mirror : mirrors) {
|
||||||
|
if (java.nio.file.Files.exists(mirror)) {
|
||||||
|
try {
|
||||||
|
java.nio.file.Files.copy(savedFile, mirror,
|
||||||
|
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
log.info("[ModelEditor] Spiegel aktualisiert: {}", mirror);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[ModelEditor] Spiegel-Update fehlgeschlagen ({}): {}", mirror, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void embedLodsAndSave(Path j3oPath) {
|
||||||
|
log.info("[ModelEditor] embedLodsAndSave aufgerufen: path={}, originalSpatial={}",
|
||||||
|
j3oPath, originalSpatial != null ? originalSpatial.getName() : "null");
|
||||||
|
if (j3oPath == null || originalSpatial == null) {
|
||||||
|
log.warn("[ModelEditor] embedLodsAndSave abgebrochen: j3oPath={} originalSpatial={}",
|
||||||
|
j3oPath, originalSpatial);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Spatial lod1Preview = input.modelImportLod1PreviewNode.get();
|
||||||
|
Spatial lod2Preview = input.modelImportLod2PreviewNode.get();
|
||||||
|
log.info("[ModelEditor] Preview-Nodes: lod1={}, lod2={}",
|
||||||
|
lod1Preview != null ? lod1Preview.getName() : "null",
|
||||||
|
lod2Preview != null ? lod2Preview.getName() : "null");
|
||||||
|
if (lod1Preview == null && lod2Preview == null) {
|
||||||
|
log.warn("[ModelEditor] Beide Preview-Nodes null – kein Einbetten möglich");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!(originalSpatial instanceof Node rootNode)) {
|
||||||
|
log.warn("[ModelEditor] LOD-Einbettung: Modell-Root kein Node – abgebrochen");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
log.info("[ModelEditor] rootNode '{}' hat vor Einbettung {} Kinder",
|
||||||
|
rootNode.getName(), rootNode.getChildren().size());
|
||||||
|
for (int i = 0; i < rootNode.getChildren().size(); i++) {
|
||||||
|
Spatial c = rootNode.getChild(i);
|
||||||
|
log.info("[ModelEditor] [{}] '{}' cullHint={}", i, c.getName(), c.getCullHint());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vorhandene LODs selektiv ersetzen:
|
||||||
|
// Index 0 (HD-Modell) NIEMALS entfernen, auch wenn CullHint.Always gesetzt ist.
|
||||||
|
if (lod1Preview != null) {
|
||||||
|
Spatial oldLod1 = null;
|
||||||
|
for (int i = 1; i < rootNode.getChildren().size(); i++) {
|
||||||
|
Spatial c = rootNode.getChild(i);
|
||||||
|
if (!"lod2".equals(c.getName())) { oldLod1 = c; break; }
|
||||||
|
}
|
||||||
|
if (oldLod1 != null) {
|
||||||
|
log.info("[ModelEditor] Altes LOD1 '{}' entfernt", oldLod1.getName());
|
||||||
|
rootNode.detachChild(oldLod1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lod2Preview != null) {
|
||||||
|
Spatial oldLod2 = rootNode.getChild("lod2");
|
||||||
|
if (oldLod2 != null) {
|
||||||
|
log.info("[ModelEditor] Altes LOD2 '{}' entfernt", oldLod2.getName());
|
||||||
|
rootNode.detachChild(oldLod2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Controls aus dem gesamten Baum entfernen (nicht serialisierbar im Game-Modul)
|
||||||
|
ModelExportUtil.stripControls(rootNode);
|
||||||
|
|
||||||
|
if (lod1Preview != null) {
|
||||||
|
Spatial l1 = lod1Preview.clone();
|
||||||
|
l1.setName("lod1");
|
||||||
|
ModelExportUtil.stripControls(l1);
|
||||||
|
l1.setCullHint(Spatial.CullHint.Always);
|
||||||
|
rootNode.attachChild(l1);
|
||||||
|
log.info("[ModelEditor] LOD1 eingebettet als '{}'", l1.getName());
|
||||||
|
}
|
||||||
|
if (lod2Preview != null) {
|
||||||
|
Spatial l2 = lod2Preview.clone();
|
||||||
|
l2.setName("lod2");
|
||||||
|
ModelExportUtil.stripControls(l2);
|
||||||
|
l2.setCullHint(Spatial.CullHint.Always);
|
||||||
|
l2.setShadowMode(com.jme3.renderer.queue.RenderQueue.ShadowMode.Off);
|
||||||
|
rootNode.attachChild(l2);
|
||||||
|
log.info("[ModelEditor] LOD2 eingebettet als '{}'", l2.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// HD-Modell (Index 0) immer als sichtbar speichern – unabhängig vom aktuellen Preview-Zustand
|
||||||
|
if (!rootNode.getChildren().isEmpty()) {
|
||||||
|
rootNode.getChild(0).setCullHint(Spatial.CullHint.Inherit);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("[ModelEditor] rootNode '{}' hat vor dem Speichern {} Kinder:",
|
||||||
|
rootNode.getName(), rootNode.getChildren().size());
|
||||||
|
for (int i = 0; i < rootNode.getChildren().size(); i++) {
|
||||||
|
Spatial c = rootNode.getChild(i);
|
||||||
|
log.info("[ModelEditor] [{}] '{}' cullHint={}", i, c.getName(), c.getCullHint());
|
||||||
|
}
|
||||||
|
|
||||||
|
BinaryExporter.getInstance().save(rootNode, j3oPath.toFile());
|
||||||
|
long fileSize = j3oPath.toFile().length();
|
||||||
|
log.info("[ModelEditor] j3o gespeichert: {} ({} bytes)", j3oPath, fileSize);
|
||||||
|
syncToMirrors(j3oPath);
|
||||||
|
|
||||||
|
String relPath = ASSET_ROOT.relativize(j3oPath).toString().replace('\\', '/');
|
||||||
|
app.getAssetManager().deleteFromCache(new com.jme3.asset.ModelKey(relPath));
|
||||||
|
|
||||||
|
// In-Memory-Zustand ohne Reload synchronisieren (immer 3 Slots: [HD, LOD1, LOD2])
|
||||||
|
embeddedLodSpatials = new Spatial[3];
|
||||||
|
embeddedLodSpatials[0] = rootNode.getChild(0);
|
||||||
|
for (int i = 1; i < rootNode.getChildren().size(); i++) {
|
||||||
|
Spatial child = rootNode.getChild(i);
|
||||||
|
if ("lod2".equals(child.getName())) {
|
||||||
|
embeddedLodSpatials[2] = child;
|
||||||
|
} else {
|
||||||
|
embeddedLodSpatials[1] = child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hasEmbeddedLods = true;
|
||||||
|
input.modelEditorHasEmbeddedLods = true;
|
||||||
|
input.modelEditorHasEmbeddedLod1 = embeddedLodSpatials[1] != null;
|
||||||
|
input.modelEditorHasEmbeddedLod2 = embeddedLodSpatials[2] != null;
|
||||||
|
log.info("[ModelEditor] In-Memory-Status: hasLod1={}, hasLod2={}",
|
||||||
|
input.modelEditorHasEmbeddedLod1, input.modelEditorHasEmbeddedLod2);
|
||||||
|
|
||||||
|
// ImpostorViewControl auf Impostor-Knoten wiederherstellen (für korrekte Editor-Anzeige)
|
||||||
|
for (Spatial s : embeddedLodSpatials) {
|
||||||
|
if (s instanceof Node ln && isImpostorNode(ln)) {
|
||||||
|
if (ln.getControl(ImpostorUtil.ImpostorViewControl.class) == null) {
|
||||||
|
ImpostorUtil.addViewControl(ln, cam);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// originalSpatial (= rootNode) in modelWrapper wiederherstellen.
|
||||||
|
// showSpatialDirectly() könnte modelWrapper durch einen Preview-Klon ersetzt haben.
|
||||||
|
if (previewRoot != null) {
|
||||||
|
if (modelWrapper != null) previewRoot.detachChild(modelWrapper);
|
||||||
|
modelWrapper = new Node("model_wrapper");
|
||||||
|
modelWrapper.attachChild(rootNode);
|
||||||
|
applyScale(input.modelEditorScaleX, input.modelEditorScaleY, input.modelEditorScaleZ);
|
||||||
|
previewRoot.attachChild(modelWrapper);
|
||||||
|
showingDirectPreview = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// LOD-Vorschau auf HD (Index 0) zurücksetzen
|
||||||
|
input.modelEditorLodPreview = 0;
|
||||||
|
embeddedLodSpatials[0].setCullHint(Spatial.CullHint.Inherit);
|
||||||
|
for (int i = 1; i < embeddedLodSpatials.length; i++) {
|
||||||
|
if (embeddedLodSpatials[i] != null) {
|
||||||
|
embeddedLodSpatials[i].setCullHint(Spatial.CullHint.Always);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input.modelImportHasLod1 = false;
|
||||||
|
input.modelImportHasLod2 = false;
|
||||||
|
input.modelImportLod1PreviewNode.set(null);
|
||||||
|
input.modelImportLod2PreviewNode.set(null);
|
||||||
|
|
||||||
|
log.info("[ModelEditor] LOD-Einbettung abgeschlossen: {}", j3oPath.getFileName());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[ModelEditor] LOD-Einbettung fehlgeschlagen: {}", e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Scale-Bake ────────────────────────────────────────────────────────────
|
// ── Scale-Bake ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -985,11 +1246,12 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
log.info("[ModelEditor] Animiert: Scale ({},{},{}) als Spatial-Transform gespeichert", sx, sy, sz);
|
log.info("[ModelEditor] Animiert: Scale ({},{},{}) als Spatial-Transform gespeichert", sx, sy, sz);
|
||||||
} else {
|
} else {
|
||||||
root.setLocalScale(sx, sy, sz);
|
root.setLocalScale(sx, sy, sz);
|
||||||
ModelImportState.stripControls(root);
|
ModelExportUtil.stripControls(root);
|
||||||
ModelImportState.bakeTransform(root, new Matrix4f());
|
ModelExportUtil.bakeTransform(root, new Matrix4f());
|
||||||
log.info("[ModelEditor] Statisch: Scale ({},{},{}) in Vertices gebacken", sx, sy, sz);
|
log.info("[ModelEditor] Statisch: Scale ({},{},{}) in Vertices gebacken", sx, sy, sz);
|
||||||
}
|
}
|
||||||
BinaryExporter.getInstance().save(root, j3oPath.toFile());
|
BinaryExporter.getInstance().save(root, j3oPath.toFile());
|
||||||
|
syncToMirrors(j3oPath);
|
||||||
log.info("[ModelEditor] j3o nach Bake gespeichert: {}", j3oPath.getFileName());
|
log.info("[ModelEditor] j3o nach Bake gespeichert: {}", j3oPath.getFileName());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[ModelEditor] Scale-Bake fehlgeschlagen: {}", e.getMessage(), e);
|
log.error("[ModelEditor] Scale-Bake fehlgeschlagen: {}", e.getMessage(), e);
|
||||||
@@ -1030,6 +1292,7 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
if (savable instanceof Spatial root) {
|
if (savable instanceof Spatial root) {
|
||||||
ThumbnailRenderer.embed(root, pngBytes);
|
ThumbnailRenderer.embed(root, pngBytes);
|
||||||
BinaryExporter.getInstance().save(root, j3oPath.toFile());
|
BinaryExporter.getInstance().save(root, j3oPath.toFile());
|
||||||
|
syncToMirrors(j3oPath);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("[ModelEditor] j3o-Embed fehlgeschlagen: {}", e.getMessage(), e);
|
log.error("[ModelEditor] j3o-Embed fehlgeschlagen: {}", e.getMessage(), e);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package de.blight.editor.state;
|
package de.blight.editor.state;
|
||||||
|
|
||||||
|
import de.blight.editor.util.ModelExportUtil;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.FloatBuffer;
|
import java.nio.FloatBuffer;
|
||||||
@@ -161,17 +163,21 @@ public class ModelImportState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
Spatial clone = modelSpatial.clone();
|
Spatial clone = modelSpatial.clone();
|
||||||
stripControls(clone);
|
ModelExportUtil.stripControls(clone);
|
||||||
|
|
||||||
int trisBefore = countTriangles(clone);
|
int trisBefore = countTriangles(clone);
|
||||||
|
|
||||||
|
// Blätter bei Bäumen von der Decimation ausnehmen (Vertices sollen unverändert bleiben)
|
||||||
|
String path = modelEditorState.getCurrentPath();
|
||||||
|
boolean skipLeaves = path != null && path.replace('\\', '/').contains("Models/trees");
|
||||||
|
|
||||||
// Welding schafft geteilte Vertices → Manifold-Kanten für beide Algorithmen nötig
|
// Welding schafft geteilte Vertices → Manifold-Kanten für beide Algorithmen nötig
|
||||||
weldVerticesRecursive(clone, 0.01f);
|
weldVerticesRecursive(clone, 0.01f);
|
||||||
|
|
||||||
if ("jme".equals(input.modelLodAlgorithm)) {
|
if ("jme".equals(input.modelLodAlgorithm)) {
|
||||||
decimateRecursiveJme(clone, 1.0f - reduction);
|
decimateRecursiveJme(clone, 1.0f - reduction, skipLeaves);
|
||||||
} else {
|
} else {
|
||||||
decimateRecursiveBlight(clone, 1.0f - reduction);
|
decimateRecursiveBlight(clone, 1.0f - reduction, skipLeaves);
|
||||||
}
|
}
|
||||||
|
|
||||||
Node wrapper;
|
Node wrapper;
|
||||||
@@ -212,7 +218,7 @@ public class ModelImportState extends BaseAppState {
|
|||||||
else input.modelImportHasLod2 = false;
|
else input.modelImportHasLod2 = false;
|
||||||
try {
|
try {
|
||||||
Spatial loaded = assets.loadModel(assetPath);
|
Spatial loaded = assets.loadModel(assetPath);
|
||||||
stripControls(loaded);
|
ModelExportUtil.stripControls(loaded);
|
||||||
Node wrapper;
|
Node wrapper;
|
||||||
if (loaded instanceof Node n) wrapper = n;
|
if (loaded instanceof Node n) wrapper = n;
|
||||||
else { wrapper = new Node("lod" + level); wrapper.attachChild(loaded); }
|
else { wrapper = new Node("lod" + level); wrapper.attachChild(loaded); }
|
||||||
@@ -253,7 +259,7 @@ public class ModelImportState extends BaseAppState {
|
|||||||
Spatial cleanClone = src.clone();
|
Spatial cleanClone = src.clone();
|
||||||
// Controls entfernen, damit z.B. ein ImportLodControl den CullHint
|
// Controls entfernen, damit z.B. ein ImportLodControl den CullHint
|
||||||
// nicht auf lod0 zurücksetzt und die Geometrie im Capture unsichtbar macht.
|
// nicht auf lod0 zurücksetzt und die Geometrie im Capture unsichtbar macht.
|
||||||
stripControls(cleanClone);
|
ModelExportUtil.stripControls(cleanClone);
|
||||||
cleanClone.setLocalTranslation(Vector3f.ZERO);
|
cleanClone.setLocalTranslation(Vector3f.ZERO);
|
||||||
cleanClone.setLocalScale(input.modelEditorScaleX, input.modelEditorScaleY, input.modelEditorScaleZ);
|
cleanClone.setLocalScale(input.modelEditorScaleX, input.modelEditorScaleY, input.modelEditorScaleZ);
|
||||||
cleanClone.setLocalRotation(Quaternion.IDENTITY);
|
cleanClone.setLocalRotation(Quaternion.IDENTITY);
|
||||||
@@ -337,7 +343,7 @@ public class ModelImportState extends BaseAppState {
|
|||||||
|
|
||||||
// Clone original, apply target scale, compute bottom-aligned pivot
|
// Clone original, apply target scale, compute bottom-aligned pivot
|
||||||
Spatial lod0 = srcSpatial.clone();
|
Spatial lod0 = srcSpatial.clone();
|
||||||
stripControls(lod0);
|
ModelExportUtil.stripControls(lod0);
|
||||||
lod0.setLocalScale(sx, sy, sz);
|
lod0.setLocalScale(sx, sy, sz);
|
||||||
lod0.setLocalTranslation(Vector3f.ZERO);
|
lod0.setLocalTranslation(Vector3f.ZERO);
|
||||||
lod0.updateGeometricState();
|
lod0.updateGeometricState();
|
||||||
@@ -348,7 +354,7 @@ public class ModelImportState extends BaseAppState {
|
|||||||
translationY = -(bb.getCenter().y - bb.getYExtent()) + pivotY;
|
translationY = -(bb.getCenter().y - bb.getYExtent()) + pivotY;
|
||||||
}
|
}
|
||||||
lod0.setLocalTranslation(0f, translationY, 0f);
|
lod0.setLocalTranslation(0f, translationY, 0f);
|
||||||
bakeTransform(lod0, new Matrix4f());
|
ModelExportUtil.bakeTransform(lod0, new Matrix4f());
|
||||||
lod0.setName("lod0");
|
lod0.setName("lod0");
|
||||||
|
|
||||||
Node root = new Node(fileName);
|
Node root = new Node(fileName);
|
||||||
@@ -366,11 +372,17 @@ public class ModelImportState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
if (lod2Node != null) {
|
if (lod2Node != null) {
|
||||||
lod2Node.setName("lod2");
|
lod2Node.setName("lod2");
|
||||||
|
boolean isImpostor = (lod2Node instanceof Node ln && ln.getChild("quad_0") != null);
|
||||||
|
if (isImpostor) {
|
||||||
|
// Impostor-Quads haben vorberechnete Weltpositionen – bakeTransform würde die Geometrie zerstören
|
||||||
|
ModelExportUtil.stripControls(lod2Node);
|
||||||
|
} else {
|
||||||
lod2Node.setLocalScale(sx, sy, sz);
|
lod2Node.setLocalScale(sx, sy, sz);
|
||||||
lod2Node.setLocalTranslation(0f, translationY, 0f);
|
lod2Node.setLocalTranslation(0f, translationY, 0f);
|
||||||
|
bakeSpatialInPlace(lod2Node);
|
||||||
|
}
|
||||||
lod2Node.setCullHint(Spatial.CullHint.Always);
|
lod2Node.setCullHint(Spatial.CullHint.Always);
|
||||||
lod2Node.setShadowMode(RenderQueue.ShadowMode.Off);
|
lod2Node.setShadowMode(RenderQueue.ShadowMode.Off);
|
||||||
bakeSpatialInPlace(lod2Node);
|
|
||||||
root.attachChild(lod2Node);
|
root.attachChild(lod2Node);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,8 +440,11 @@ public class ModelImportState extends BaseAppState {
|
|||||||
// ── Mesh-Decimation ───────────────────────────────────────────────────────
|
// ── Mesh-Decimation ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** JME3 Progressive Mesh: verwendet JME's LodGenerator direkt. */
|
/** JME3 Progressive Mesh: verwendet JME's LodGenerator direkt. */
|
||||||
private static void decimateRecursiveJme(Spatial s, float keepRatio) {
|
private static void decimateRecursiveJme(Spatial s, float keepRatio, boolean skipLeaves) {
|
||||||
if (s instanceof Geometry g) {
|
if (s instanceof Geometry g) {
|
||||||
|
if (skipLeaves && isLeafGeometry(g)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
Mesh mesh = g.getMesh();
|
Mesh mesh = g.getMesh();
|
||||||
int origTris = mesh.getTriangleCount();
|
int origTris = mesh.getTriangleCount();
|
||||||
try {
|
try {
|
||||||
@@ -451,7 +466,7 @@ public class ModelImportState extends BaseAppState {
|
|||||||
log.warn("[ModelImport] JME bakeLods fehlgeschlagen für {}: {}", g.getName(), e.getMessage());
|
log.warn("[ModelImport] JME bakeLods fehlgeschlagen für {}: {}", g.getName(), e.getMessage());
|
||||||
}
|
}
|
||||||
} else if (s instanceof Node n) {
|
} else if (s instanceof Node n) {
|
||||||
for (Spatial child : n.getChildren()) decimateRecursiveJme(child, keepRatio);
|
for (Spatial child : n.getChildren()) decimateRecursiveJme(child, keepRatio, skipLeaves);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,14 +474,34 @@ public class ModelImportState extends BaseAppState {
|
|||||||
* Blight Edge Collapse: verschmilzt Kanten (je zwei geteilte Dreiecke) iterativ,
|
* Blight Edge Collapse: verschmilzt Kanten (je zwei geteilte Dreiecke) iterativ,
|
||||||
* bis die Zielanzahl erreicht ist. Das Mesh bleibt durchgehend geschlossen.
|
* bis die Zielanzahl erreicht ist. Das Mesh bleibt durchgehend geschlossen.
|
||||||
*/
|
*/
|
||||||
private static void decimateRecursiveBlight(Spatial s, float keepRatio) {
|
private static void decimateRecursiveBlight(Spatial s, float keepRatio, boolean skipLeaves) {
|
||||||
if (s instanceof Geometry g) {
|
if (s instanceof Geometry g) {
|
||||||
|
if (skipLeaves && isLeafGeometry(g)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
g.setMesh(edgeCollapseMesh(g.getMesh(), keepRatio));
|
g.setMesh(edgeCollapseMesh(g.getMesh(), keepRatio));
|
||||||
} else if (s instanceof Node n) {
|
} else if (s instanceof Node n) {
|
||||||
for (Spatial child : n.getChildren()) decimateRecursiveBlight(child, keepRatio);
|
for (Spatial child : n.getChildren()) decimateRecursiveBlight(child, keepRatio, skipLeaves);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isLeafGeometry(Geometry g) {
|
||||||
|
String name = g.getName();
|
||||||
|
if (name != null) {
|
||||||
|
String lower = name.toLowerCase();
|
||||||
|
if (lower.contains("leaf") || lower.contains("leaves")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (g.getMaterial() != null && g.getMaterial().getMaterialDef() != null) {
|
||||||
|
String defName = g.getMaterial().getMaterialDef().getName();
|
||||||
|
if (defName != null && defName.contains("TreeLeaf")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extrahiert ein LOD-Level aus JME3's bakeLods-Ergebnis als eigenständiges Mesh.
|
* Extrahiert ein LOD-Level aus JME3's bakeLods-Ergebnis als eigenständiges Mesh.
|
||||||
* Handhabt alle möglichen Index-Buffer-Formate (UnsignedShort, UnsignedInt, Float).
|
* Handhabt alle möglichen Index-Buffer-Formate (UnsignedShort, UnsignedInt, Float).
|
||||||
@@ -738,83 +773,14 @@ public class ModelImportState extends BaseAppState {
|
|||||||
|
|
||||||
private Spatial bakeSpatial(Spatial source) {
|
private Spatial bakeSpatial(Spatial source) {
|
||||||
Spatial clone = source.clone();
|
Spatial clone = source.clone();
|
||||||
stripControls(clone);
|
ModelExportUtil.stripControls(clone);
|
||||||
bakeTransform(clone, new Matrix4f());
|
ModelExportUtil.bakeTransform(clone, new Matrix4f());
|
||||||
return clone;
|
return clone;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void bakeSpatialInPlace(Spatial s) {
|
private void bakeSpatialInPlace(Spatial s) {
|
||||||
stripControls(s);
|
ModelExportUtil.stripControls(s);
|
||||||
bakeTransform(s, new Matrix4f());
|
ModelExportUtil.bakeTransform(s, new Matrix4f());
|
||||||
}
|
|
||||||
|
|
||||||
static void bakeTransform(Spatial s, Matrix4f accum) {
|
|
||||||
Matrix4f localMat = new Matrix4f();
|
|
||||||
s.getLocalTransform().toTransformMatrix(localMat);
|
|
||||||
Matrix4f combined = accum.mult(localMat);
|
|
||||||
|
|
||||||
if (s instanceof Geometry g) {
|
|
||||||
applyMatrixToMesh(g, combined);
|
|
||||||
g.setLocalTranslation(Vector3f.ZERO);
|
|
||||||
g.setLocalScale(1f);
|
|
||||||
g.setLocalRotation(Quaternion.IDENTITY);
|
|
||||||
} else if (s instanceof Node n) {
|
|
||||||
for (Spatial child : n.getChildren()) bakeTransform(child, combined);
|
|
||||||
n.setLocalTranslation(Vector3f.ZERO);
|
|
||||||
n.setLocalScale(1f);
|
|
||||||
n.setLocalRotation(Quaternion.IDENTITY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void applyMatrixToMesh(Geometry g, Matrix4f mat) {
|
|
||||||
Mesh newMesh = g.getMesh().deepClone();
|
|
||||||
|
|
||||||
FloatBuffer pos = newMesh.getFloatBuffer(VertexBuffer.Type.Position);
|
|
||||||
if (pos != null) {
|
|
||||||
pos.rewind();
|
|
||||||
Vector3f v = new Vector3f();
|
|
||||||
while (pos.remaining() >= 3) {
|
|
||||||
int idx = pos.position();
|
|
||||||
v.set(pos.get(), pos.get(), pos.get());
|
|
||||||
mat.mult(v, v);
|
|
||||||
pos.put(idx, v.x); pos.put(idx + 1, v.y); pos.put(idx + 2, v.z);
|
|
||||||
}
|
|
||||||
pos.rewind();
|
|
||||||
newMesh.getBuffer(VertexBuffer.Type.Position).setUpdateNeeded();
|
|
||||||
}
|
|
||||||
|
|
||||||
FloatBuffer norm = newMesh.getFloatBuffer(VertexBuffer.Type.Normal);
|
|
||||||
if (norm != null) {
|
|
||||||
Matrix3f normalMat = buildNormalMatrix(mat);
|
|
||||||
norm.rewind();
|
|
||||||
Vector3f n = new Vector3f();
|
|
||||||
while (norm.remaining() >= 3) {
|
|
||||||
int idx = norm.position();
|
|
||||||
n.set(norm.get(), norm.get(), norm.get());
|
|
||||||
normalMat.mult(n, n); n.normalizeLocal();
|
|
||||||
norm.put(idx, n.x); norm.put(idx + 1, n.y); norm.put(idx + 2, n.z);
|
|
||||||
}
|
|
||||||
norm.rewind();
|
|
||||||
newMesh.getBuffer(VertexBuffer.Type.Normal).setUpdateNeeded();
|
|
||||||
}
|
|
||||||
|
|
||||||
newMesh.updateBound();
|
|
||||||
g.setMesh(newMesh);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Matrix3f buildNormalMatrix(Matrix4f mat) {
|
|
||||||
Matrix3f m3 = new Matrix3f(
|
|
||||||
mat.m00, mat.m01, mat.m02,
|
|
||||||
mat.m10, mat.m11, mat.m12,
|
|
||||||
mat.m20, mat.m21, mat.m22);
|
|
||||||
try { m3.invertLocal(); m3.transposeLocal(); }
|
|
||||||
catch (Exception e) { m3.loadIdentity(); }
|
|
||||||
return m3;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void stripControls(Spatial s) {
|
|
||||||
while (s.getNumControls() > 0) s.removeControl(s.getControl(0));
|
|
||||||
if (s instanceof Node n) n.getChildren().forEach(ModelImportState::stripControls);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Node wrapInNode(Spatial s, String name) {
|
private static Node wrapInNode(Spatial s, String name) {
|
||||||
|
|||||||
@@ -30,7 +30,10 @@ public class TreeMeshBuilder {
|
|||||||
|
|
||||||
public MeshResult build(TreeParams p, float quality) {
|
public MeshResult build(TreeParams p, float quality) {
|
||||||
boolean hd = quality >= 0.5f;
|
boolean hd = quality >= 0.5f;
|
||||||
int maxLevel = hd ? p.levels : Math.max(1, p.levels - 1);
|
// maxLevel immer voll – Tiefe (= Zahl der Verzweigungsebenen) gilt auch für LD.
|
||||||
|
// Bark-Reduktion erfolgt nur über numSec (/2) und segs (-2), nicht über Tiefe.
|
||||||
|
// Andernfalls fehlt in LD eine ganze Ast-Ebene → exponentiell weniger Blatt-Äste.
|
||||||
|
int maxLevel = p.levels;
|
||||||
|
|
||||||
Rng rng = new Rng(p.seed);
|
Rng rng = new Rng(p.seed);
|
||||||
VertexCollector barkCol = new VertexCollector();
|
VertexCollector barkCol = new VertexCollector();
|
||||||
@@ -49,7 +52,11 @@ public class TreeMeshBuilder {
|
|||||||
if (lv >= maxLevel) continue;
|
if (lv >= maxLevel) continue;
|
||||||
|
|
||||||
// Per-Level-Parameter
|
// Per-Level-Parameter
|
||||||
int numSec = Math.max(1, hd ? TreeParams.lv(p.sections, lv)
|
// Auf dem Blatt-Level (letzte Ebene) immer volle Sektion-Dichte:
|
||||||
|
// Blätter sollen in LD genauso viele Vertices haben wie in HD.
|
||||||
|
boolean isLeafLevel = (lv == maxLevel - 1) && p.generateLeaves;
|
||||||
|
int numSec = Math.max(1, (hd || isLeafLevel)
|
||||||
|
? TreeParams.lv(p.sections, lv)
|
||||||
: TreeParams.lv(p.sections, lv) / 2);
|
: TreeParams.lv(p.sections, lv) / 2);
|
||||||
float branchLen = TreeParams.lv(p.length, lv);
|
float branchLen = TreeParams.lv(p.length, lv);
|
||||||
float baseRad = TreeParams.lv(p.radius, lv);
|
float baseRad = TreeParams.lv(p.radius, lv);
|
||||||
|
|||||||
@@ -347,19 +347,20 @@ public final class ImpostorUtil {
|
|||||||
protected void controlUpdate(float tpf) {
|
protected void controlUpdate(float tpf) {
|
||||||
if (cam == null || !(spatial instanceof Node n)) return;
|
if (cam == null || !(spatial instanceof Node n)) return;
|
||||||
List<Spatial> quads = n.getChildren();
|
List<Spatial> quads = n.getChildren();
|
||||||
if (quads.size() < DIRS) return;
|
int dirs = quads.size();
|
||||||
|
if (dirs < 2) return;
|
||||||
|
|
||||||
Vector3f toCamera = cam.getLocation().subtract(spatial.getWorldTranslation());
|
Vector3f toCamera = cam.getLocation().subtract(spatial.getWorldTranslation());
|
||||||
float dx = toCamera.x, dz = toCamera.z;
|
float dx = toCamera.x, dz = toCamera.z;
|
||||||
if (Math.abs(dx) < 0.001f && Math.abs(dz) < 0.001f) return;
|
if (Math.abs(dx) < 0.001f && Math.abs(dz) < 0.001f) return;
|
||||||
|
|
||||||
// Winkelindex: t in Einheiten von 2π/DIRS; nächstes Capture = round(t) % DIRS
|
// Winkelindex: t in Einheiten von 2π/dirs; nächstes Capture = round(t) % dirs
|
||||||
float angle = (float) Math.atan2(dx, dz); // −π … +π
|
float angle = (float) Math.atan2(dx, dz); // −π … +π
|
||||||
float t = angle * DIRS / FastMath.TWO_PI;
|
float t = angle * dirs / FastMath.TWO_PI;
|
||||||
int best = ((int) Math.round(t) % DIRS + DIRS) % DIRS;
|
int best = ((int) Math.round(t) % dirs + dirs) % dirs;
|
||||||
int opp = (best + DIRS / 2) % DIRS; // gegenüberliegendes Quad
|
int opp = (best + dirs / 2) % dirs; // gegenüberliegendes Quad
|
||||||
|
|
||||||
for (int i = 0; i < quads.size(); i++) {
|
for (int i = 0; i < dirs; i++) {
|
||||||
quads.get(i).setCullHint(i == best || i == opp
|
quads.get(i).setCullHint(i == best || i == opp
|
||||||
? Spatial.CullHint.Inherit
|
? Spatial.CullHint.Inherit
|
||||||
: Spatial.CullHint.Always);
|
: Spatial.CullHint.Always);
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package de.blight.editor.util;
|
||||||
|
|
||||||
|
import com.jme3.math.Matrix3f;
|
||||||
|
import com.jme3.math.Matrix4f;
|
||||||
|
import com.jme3.math.Quaternion;
|
||||||
|
import com.jme3.math.Vector3f;
|
||||||
|
import com.jme3.scene.Geometry;
|
||||||
|
import com.jme3.scene.Mesh;
|
||||||
|
import com.jme3.scene.Node;
|
||||||
|
import com.jme3.scene.Spatial;
|
||||||
|
import com.jme3.scene.VertexBuffer;
|
||||||
|
|
||||||
|
import java.nio.FloatBuffer;
|
||||||
|
|
||||||
|
public final class ModelExportUtil {
|
||||||
|
|
||||||
|
private ModelExportUtil() {}
|
||||||
|
|
||||||
|
public static void stripControls(Spatial s) {
|
||||||
|
while (s.getNumControls() > 0) s.removeControl(s.getControl(0));
|
||||||
|
if (s instanceof Node n) {
|
||||||
|
n.getChildren().forEach(ModelExportUtil::stripControls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bakeTransform(Spatial s, Matrix4f accum) {
|
||||||
|
Matrix4f localMat = new Matrix4f();
|
||||||
|
s.getLocalTransform().toTransformMatrix(localMat);
|
||||||
|
Matrix4f combined = accum.mult(localMat);
|
||||||
|
|
||||||
|
if (s instanceof Geometry g) {
|
||||||
|
applyMatrixToMesh(g, combined);
|
||||||
|
g.setLocalTranslation(Vector3f.ZERO);
|
||||||
|
g.setLocalScale(1f);
|
||||||
|
g.setLocalRotation(Quaternion.IDENTITY);
|
||||||
|
} else if (s instanceof Node n) {
|
||||||
|
for (Spatial child : n.getChildren()) bakeTransform(child, combined);
|
||||||
|
n.setLocalTranslation(Vector3f.ZERO);
|
||||||
|
n.setLocalScale(1f);
|
||||||
|
n.setLocalRotation(Quaternion.IDENTITY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void applyMatrixToMesh(Geometry g, Matrix4f mat) {
|
||||||
|
Mesh newMesh = g.getMesh().deepClone();
|
||||||
|
|
||||||
|
FloatBuffer pos = newMesh.getFloatBuffer(VertexBuffer.Type.Position);
|
||||||
|
if (pos != null) {
|
||||||
|
pos.rewind();
|
||||||
|
Vector3f v = new Vector3f();
|
||||||
|
while (pos.remaining() >= 3) {
|
||||||
|
int idx = pos.position();
|
||||||
|
v.set(pos.get(), pos.get(), pos.get());
|
||||||
|
mat.mult(v, v);
|
||||||
|
pos.put(idx, v.x); pos.put(idx + 1, v.y); pos.put(idx + 2, v.z);
|
||||||
|
}
|
||||||
|
pos.rewind();
|
||||||
|
newMesh.getBuffer(VertexBuffer.Type.Position).setUpdateNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
FloatBuffer norm = newMesh.getFloatBuffer(VertexBuffer.Type.Normal);
|
||||||
|
if (norm != null) {
|
||||||
|
Matrix3f normalMat = buildNormalMatrix(mat);
|
||||||
|
norm.rewind();
|
||||||
|
Vector3f n = new Vector3f();
|
||||||
|
while (norm.remaining() >= 3) {
|
||||||
|
int idx = norm.position();
|
||||||
|
n.set(norm.get(), norm.get(), norm.get());
|
||||||
|
normalMat.mult(n, n); n.normalizeLocal();
|
||||||
|
norm.put(idx, n.x); norm.put(idx + 1, n.y); norm.put(idx + 2, n.z);
|
||||||
|
}
|
||||||
|
norm.rewind();
|
||||||
|
newMesh.getBuffer(VertexBuffer.Type.Normal).setUpdateNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
newMesh.updateBound();
|
||||||
|
g.setMesh(newMesh);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Matrix3f buildNormalMatrix(Matrix4f mat) {
|
||||||
|
Matrix3f m3 = new Matrix3f(
|
||||||
|
mat.m00, mat.m01, mat.m02,
|
||||||
|
mat.m10, mat.m11, mat.m12,
|
||||||
|
mat.m20, mat.m21, mat.m22);
|
||||||
|
try { m3.invertLocal(); m3.transposeLocal(); }
|
||||||
|
catch (Exception e) { m3.loadIdentity(); }
|
||||||
|
return m3;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package de.blight.game.state;
|
||||||
|
|
||||||
|
import com.jme3.math.FastMath;
|
||||||
|
import com.jme3.math.Vector3f;
|
||||||
|
import com.jme3.renderer.Camera;
|
||||||
|
import com.jme3.renderer.RenderManager;
|
||||||
|
import com.jme3.renderer.ViewPort;
|
||||||
|
import com.jme3.scene.Node;
|
||||||
|
import com.jme3.scene.Spatial;
|
||||||
|
import com.jme3.scene.control.AbstractControl;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wählt pro Frame das passende Quad-Paar eines 8-Richtungs-Impostors.
|
||||||
|
* Pass d wurde mit Kamera bei sin(d·2π/8), cos(d·2π/8) aufgenommen.
|
||||||
|
* Es werden immer zwei gegenüberliegende Quads (eine Ebene) angezeigt;
|
||||||
|
* die anderen 6 werden per CullHint.Always ausgeblendet.
|
||||||
|
*/
|
||||||
|
public class ImpostorViewControl extends AbstractControl {
|
||||||
|
|
||||||
|
private static final int DIRS = 8;
|
||||||
|
|
||||||
|
private Camera cam;
|
||||||
|
|
||||||
|
public ImpostorViewControl() {}
|
||||||
|
|
||||||
|
public ImpostorViewControl(Camera cam) { this.cam = cam; }
|
||||||
|
|
||||||
|
public void setCamera(Camera cam) { this.cam = cam; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void controlUpdate(float tpf) {
|
||||||
|
if (cam == null || !(spatial instanceof Node n)) return;
|
||||||
|
List<Spatial> quads = n.getChildren();
|
||||||
|
int dirs = quads.size();
|
||||||
|
if (dirs < 2) return;
|
||||||
|
|
||||||
|
Vector3f toCamera = cam.getLocation().subtract(spatial.getWorldTranslation());
|
||||||
|
float dx = toCamera.x, dz = toCamera.z;
|
||||||
|
if (Math.abs(dx) < 0.001f && Math.abs(dz) < 0.001f) return;
|
||||||
|
|
||||||
|
float angle = (float) Math.atan2(dx, dz);
|
||||||
|
float t = angle * dirs / FastMath.TWO_PI;
|
||||||
|
int best = ((int) Math.round(t) % dirs + dirs) % dirs;
|
||||||
|
int opp = (best + dirs / 2) % dirs;
|
||||||
|
|
||||||
|
for (int i = 0; i < dirs; i++) {
|
||||||
|
quads.get(i).setCullHint(i == best || i == opp
|
||||||
|
? Spatial.CullHint.Inherit
|
||||||
|
: Spatial.CullHint.Always);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void controlRender(RenderManager rm, ViewPort vp) {}
|
||||||
|
}
|
||||||
@@ -14,7 +14,6 @@ import com.jme3.material.RenderState;
|
|||||||
import com.jme3.math.*;
|
import com.jme3.math.*;
|
||||||
import com.jme3.renderer.queue.RenderQueue;
|
import com.jme3.renderer.queue.RenderQueue;
|
||||||
import com.jme3.scene.*;
|
import com.jme3.scene.*;
|
||||||
import com.jme3.scene.control.BillboardControl;
|
|
||||||
import com.jme3.scene.shape.*;
|
import com.jme3.scene.shape.*;
|
||||||
import com.jme3.texture.Texture;
|
import com.jme3.texture.Texture;
|
||||||
import de.blight.common.PlacedModel;
|
import de.blight.common.PlacedModel;
|
||||||
@@ -221,11 +220,20 @@ public class WorldObjectsState extends BaseAppState {
|
|||||||
lodRoot.attachChild(lod2); // index 1 = lod2 (Impostor-Karte)
|
lodRoot.attachChild(lod2); // index 1 = lod2 (Impostor-Karte)
|
||||||
treeNode.setCullHint(Spatial.CullHint.Inherit);
|
treeNode.setCullHint(Spatial.CullHint.Inherit);
|
||||||
lod2.setCullHint(Spatial.CullHint.Always);
|
lod2.setCullHint(Spatial.CullHint.Always);
|
||||||
// Billboard: Impostor-Karte dreht sich immer zur Kamera,
|
// Pivot-Korrektur: Stammbasis auf Y=0 ausrichten (Sicherungsnetz für
|
||||||
// unabhängig von der Baum-Rotation im BLO.
|
// j3o-Dateien, deren 1/3-Scale-Offset nicht eingebacken wurde).
|
||||||
BillboardControl bc = new BillboardControl();
|
// treeNode und lod2 gemeinsam verschieben → Geometrie und Impostor-Bild
|
||||||
bc.setAlignment(BillboardControl.Alignment.Camera);
|
// bleiben deckungsgleich.
|
||||||
lod2.addControl(bc);
|
lodRoot.updateGeometricState();
|
||||||
|
if (treeNode.getWorldBound() instanceof com.jme3.bounding.BoundingBox treeBb) {
|
||||||
|
float groundY = treeBb.getCenter().y - treeBb.getYExtent();
|
||||||
|
if (Math.abs(groundY) > 0.01f) {
|
||||||
|
treeNode.setLocalTranslation(0f, -groundY, 0f);
|
||||||
|
lod2.setLocalTranslation(0f, -groundY, 0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Impostor: wählt pro Frame das kamerawinkelrichtige Quad-Paar.
|
||||||
|
lod2.addControl(new ImpostorViewControl(app.getCamera()));
|
||||||
lodRoot.addControl(new ModelLodControl(
|
lodRoot.addControl(new ModelLodControl(
|
||||||
app.getCamera(), treeNode, null, lod2,
|
app.getCamera(), treeNode, null, lod2,
|
||||||
m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor, m.cullDistance() * lodFactor));
|
m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor, m.cullDistance() * lodFactor));
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
<configuration>
|
<configuration>
|
||||||
|
|
||||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
|
||||||
<level>INFO</level>
|
|
||||||
</filter>
|
|
||||||
<encoder>
|
<encoder>
|
||||||
<pattern>%d{HH:mm:ss.SSS} %-5level [%logger{30}] %msg%n%ex</pattern>
|
<pattern>%d{HH:mm:ss.SSS} %-5level [%logger{30}] %msg%n%ex</pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
@@ -20,6 +17,9 @@
|
|||||||
</encoder>
|
</encoder>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
|
<!-- LOD-Slot-Wechsel auf DEBUG aktivieren -->
|
||||||
|
<logger name="de.blight.game.state.ModelLodControl" level="DEBUG"/>
|
||||||
|
|
||||||
<!-- JME-interne JUL-Logs auf WARN reduzieren -->
|
<!-- JME-interne JUL-Logs auf WARN reduzieren -->
|
||||||
<logger name="com.jme3" level="WARN"/>
|
<logger name="com.jme3" level="WARN"/>
|
||||||
<!-- GltfLoader meldet bei jeder Animation "only supports linear interpolation" – bekanntes JME-Verhalten, kein Fehler -->
|
<!-- GltfLoader meldet bei jeder Animation "only supports linear interpolation" – bekanntes JME-Verhalten, kein Fehler -->
|
||||||
|
|||||||
Reference in New Issue
Block a user