An den LOD im Editor gearbeitet

This commit is contained in:
2026-07-20 13:35:27 +02:00
parent e6440e3a46
commit b17a71286a
20 changed files with 758 additions and 150 deletions

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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 0360° (Kompassrichtung), Elevation 090° (Höhe über Horizont). */ * Azimut 0360° (Kompassrichtung), Elevation 090° (Höhe über Horizont). */

View File

@@ -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(

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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) {}
}

View File

@@ -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));

View File

@@ -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 -->