diff --git a/blight-assets/src/main/resources/.thumbnails/Models/trees/pine/medium/pine_medium_20260706_190939.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/trees/pine/medium/pine_medium_20260706_190939.j3o.thumb.png index 4a9ce4c..3d20f4d 100644 Binary files a/blight-assets/src/main/resources/.thumbnails/Models/trees/pine/medium/pine_medium_20260706_190939.j3o.thumb.png and b/blight-assets/src/main/resources/.thumbnails/Models/trees/pine/medium/pine_medium_20260706_190939.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/.thumbnails/Models/trees/pine/medium/pine_medium_20260706_190947.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/trees/pine/medium/pine_medium_20260706_190947.j3o.thumb.png new file mode 100644 index 0000000..bc90de5 Binary files /dev/null and b/blight-assets/src/main/resources/.thumbnails/Models/trees/pine/medium/pine_medium_20260706_190947.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/.thumbnails/Models/trees/pine/medium/pine_medium_20260706_190953.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/trees/pine/medium/pine_medium_20260706_190953.j3o.thumb.png index ac112f4..4e2d719 100644 Binary files a/blight-assets/src/main/resources/.thumbnails/Models/trees/pine/medium/pine_medium_20260706_190953.j3o.thumb.png and b/blight-assets/src/main/resources/.thumbnails/Models/trees/pine/medium/pine_medium_20260706_190953.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190939.j3o b/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190939.j3o index 7cc0a63..d7b01bc 100644 Binary files a/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190939.j3o and b/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190939.j3o differ diff --git a/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190939.j3o.meta b/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190939.j3o.meta index 415d7f6..540baf5 100644 --- a/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190939.j3o.meta +++ b/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190939.j3o.meta @@ -1,4 +1,4 @@ -#Sat Jul 18 13:44:33 CEST 2026 +#Mon Jul 20 11:46:10 CEST 2026 attachedEmitters.count=0 attachedLights.count=0 castShadow=true diff --git a/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190947.j3o b/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190947.j3o index 1b34f12..9f78144 100644 Binary files a/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190947.j3o and b/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190947.j3o differ diff --git a/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190947.j3o.meta b/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190947.j3o.meta new file mode 100644 index 0000000..4fba9c1 --- /dev/null +++ b/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190947.j3o.meta @@ -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 diff --git a/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190953.j3o b/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190953.j3o index 29776a0..37f58e8 100644 Binary files a/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190953.j3o and b/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190953.j3o differ diff --git a/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190953.j3o.meta b/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190953.j3o.meta index 4fe207d..ceb8d29 100644 --- a/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190953.j3o.meta +++ b/blight-assets/src/main/resources/Models/trees/pine/medium/pine_medium_20260706_190953.j3o.meta @@ -1,4 +1,4 @@ -#Sat Jul 18 11:14:25 CEST 2026 +#Mon Jul 20 13:25:15 CEST 2026 attachedEmitters.count=0 attachedLights.count=0 castShadow=true diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index 02edd36..9337971 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -246,6 +246,7 @@ public class EditorApp extends Application { // Modell-Editor-Zustand private Label modelEditorDimLabel; + private Label modelEditorPolyLabel; private Spinner modelEditorSpinX, modelEditorSpinY, modelEditorSpinZ; private CheckBox modelEditorUniformCB; private boolean modelEditorSuppressListeners = false; @@ -254,12 +255,14 @@ public class EditorApp extends Application { private String modelEditorLod2Path = ""; private Label modelEditorLod1Label; private Label modelEditorLod2Label; + private ToggleButton modelEditorLod0Btn; private ToggleButton modelEditorLod1Btn; private ToggleButton modelEditorLod2Btn; private Label modelEditorLod1StatusLabel; private Label modelEditorLod2StatusLabel; private Button modelEditorLod1GenBtn; private Button modelEditorLod2GenBtn; + private ToggleButton importLod0Btn; private ToggleButton importLod1Btn; private ToggleButton importLod2Btn; private Button modelImportLod1ClearBtn; @@ -430,6 +433,7 @@ public class EditorApp extends Application { Alert confirm = new Alert(Alert.AlertType.CONFIRMATION, "Es gibt ungespeicherte Änderungen.\nTrotzdem beenden?", ButtonType.YES, ButtonType.NO); + confirm.initOwner(stage); confirm.setHeaderText("Ungespeicherte Änderungen"); confirm.showAndWait().ifPresent(btn -> { if (btn != ButtonType.YES) e.consume(); @@ -665,7 +669,8 @@ public class EditorApp extends Application { updateModelEditorDimensions( input.modelEditorBoundsW, input.modelEditorBoundsH, - input.modelEditorBoundsD); + input.modelEditorBoundsD, + input.modelEditorPolyCount); } // Modell-Editor: eingebettete LODs erkannt → Labels aktualisieren @@ -692,6 +697,14 @@ public class EditorApp extends Application { // Importer LOD-Buttons freischalten sobald generiert if (importLod1Btn != null) importLod1Btn.setDisable(!input.modelImportHasLod1); 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 (modelImportLod2ClearBtn != null) modelImportLod2ClearBtn.setDisable(!input.modelImportHasLod2); @@ -867,7 +880,8 @@ public class EditorApp extends Application { Label label = new Label("Model Editor"); 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"); modelEditorLod2Btn = new ToggleButton("LOD 2"); javafx.scene.control.ToggleGroup lodGroup = new javafx.scene.control.ToggleGroup(); @@ -2247,6 +2261,7 @@ public class EditorApp extends Application { objSaveBtn.setDisable(true); objSaveBtn.setOnAction(e -> { TextInputDialog dlg = new TextInputDialog("Vorlage"); + dlg.initOwner(primaryStage); dlg.setTitle("Vorlage speichern"); dlg.setHeaderText("Name der Vorlage (ohne .j3o):"); dlg.setContentText("Name:"); @@ -4435,6 +4450,7 @@ public class EditorApp extends Application { "Textur-Set wirklich löschen?\n" + setDir.getFileName() + "\n(Alle enthaltenen Dateien werden gelöscht!)", ButtonType.OK, ButtonType.CANCEL); + confirm.initOwner(primaryStage); confirm.setHeaderText(null); confirm.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> { try { @@ -4604,6 +4620,7 @@ public class EditorApp extends Application { Alert confirm = new Alert(Alert.AlertType.CONFIRMATION, "Datei wirklich löschen?\n" + p.getFileName(), ButtonType.OK, ButtonType.CANCEL); + confirm.initOwner(primaryStage); confirm.setHeaderText(null); confirm.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> { try { @@ -4706,6 +4723,7 @@ public class EditorApp extends Application { Path parentPath = itemPaths.get(parentItem); if (parentPath == null) return; TextInputDialog dlg = new TextInputDialog("Neuer Ordner"); + dlg.initOwner(primaryStage); dlg.setTitle("Ordner erstellen"); dlg.setHeaderText("Neuen Unterordner in " + parentPath.getFileName() + " erstellen:"); dlg.setContentText("Name:"); @@ -4747,6 +4765,7 @@ public class EditorApp extends Application { String headerExt = ext.isEmpty() ? "" : " (Endung " + ext + " wird beibehalten)"; TextInputDialog dlg = new TextInputDialog(baseName); + dlg.initOwner(primaryStage); dlg.setTitle("Umbenennen"); dlg.setHeaderText(oldFileName + " umbenennen:" + headerExt); dlg.setContentText("Neuer Name:"); @@ -5985,6 +6004,10 @@ public class EditorApp extends Application { modelEditorDimLabel.setMaxWidth(Double.MAX_VALUE); 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 ───────────────────────────────────── Label offsetTitle = new Label("Versatz:"); offsetTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); @@ -6390,7 +6413,7 @@ public class EditorApp extends Application { tagsLabel, tagsTF, new Separator(), scaleTitle, modelEditorUniformCB, scaleGrid, - modelEditorDimLabel, + modelEditorDimLabel, modelEditorPolyLabel, new Separator(), offsetTitle, offsetGrid, new Separator(), @@ -6524,7 +6547,8 @@ public class EditorApp extends Application { Label label = new Label("Modell importieren"); 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"); importLod2Btn = new ToggleButton("LOD 2"); 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.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 ─────────────────────────────────────────────────── Label algoTitle = new Label("Reduktions-Algorithmus"); algoTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); @@ -6791,7 +6819,7 @@ public class EditorApp extends Application { panel.getChildren().addAll( title, new Separator(), - scaleTitle, modelEditorUniformCB, scaleGrid, modelEditorDimLabel, + scaleTitle, modelEditorUniformCB, scaleGrid, modelEditorDimLabel, modelEditorPolyLabel, new Separator(), algoTitle, algoBlightRB, algoJmeRB, new Separator(), @@ -6871,9 +6899,11 @@ public class EditorApp extends Application { 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) 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, @@ -6965,6 +6995,12 @@ public class EditorApp extends Application { 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) input.modelEditorThumbnailRequest = finalJ3o; } @@ -7142,6 +7178,7 @@ public class EditorApp extends Application { private void showLodGenDialog(int level, int defaultPct) { javafx.scene.control.Dialog> dlg = new javafx.scene.control.Dialog<>(); + dlg.initOwner(primaryStage); dlg.setTitle("LOD " + level + " generieren"); 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.setScene(scene); gameConsoleStage.initOwner(primaryStage); - gameConsoleStage.setX(primaryStage.getX()); - gameConsoleStage.setY(primaryStage.getY() + primaryStage.getHeight()); + // 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.setY(primaryStage.getY() + primaryStage.getHeight()); + } } else { gameConsoleArea.clear(); } @@ -8748,6 +8795,7 @@ public class EditorApp extends Application { list.getSelectionModel().selectFirst(); javafx.scene.control.Dialog> dlg = new javafx.scene.control.Dialog<>(); + dlg.initOwner(primaryStage); dlg.setTitle("Animation(en) hinzufügen"); dlg.setHeaderText("Verfügbare Clips (noch nicht im Set) — Mehrfachauswahl möglich:"); dlg.getDialogPane().setContent(list); @@ -9042,6 +9090,7 @@ public class EditorApp extends Application { javafx.scene.control.Dialog kfDlg = new javafx.scene.control.Dialog<>(); + kfDlg.initOwner(primaryStage); kfDlg.setTitle(isAdd ? "Offset hinzufügen" : "Offset bearbeiten"); javafx.scene.control.ButtonType okKf = new javafx.scene.control.ButtonType( isAdd ? "Hinzufügen" : "Übernehmen", @@ -9174,6 +9223,7 @@ public class EditorApp extends Application { javafx.scene.control.Dialog dlg = new javafx.scene.control.Dialog<>(); + dlg.initOwner(primaryStage); dlg.setTitle("Aktion zuordnen"); javafx.scene.control.ButtonType ok = new javafx.scene.control.ButtonType("Hinzufügen", javafx.scene.control.ButtonBar.ButtonData.OK_DONE); @@ -9226,6 +9276,7 @@ public class EditorApp extends Application { javafx.scene.control.Dialog dlg = new javafx.scene.control.Dialog<>(); + dlg.initOwner(primaryStage); dlg.setTitle(isEdit ? "Clip-Teil bearbeiten" : "Clip-Teil hinzufügen"); dlg.setHeaderText((isEdit ? "Bearbeiten: " : "Neuer Teil für: ") + source); 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, "Das aktuelle AnimSet hat ungespeicherte Änderungen.\nTrotzdem ein neues Set anlegen?", ButtonType.OK, ButtonType.CANCEL); + warn.initOwner(primaryStage); warn.setHeaderText("Ungespeicherte Änderungen"); warn.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> doCreateNewAnimSet()); } else { @@ -9326,6 +9378,7 @@ public class EditorApp extends Application { private void doCreateNewAnimSet() { TextInputDialog dlg = new TextInputDialog(); + dlg.initOwner(primaryStage); dlg.setTitle("Neues Animations-Set"); dlg.setHeaderText("Name des neuen Animations-Sets:"); dlg.setContentText("Name:"); @@ -9634,6 +9687,7 @@ public class EditorApp extends Application { private void showSaveAnimSetDialog() { if (animClipListView == null || animClipListView.getItems().isEmpty()) return; javafx.scene.control.Dialog dlg = new javafx.scene.control.Dialog<>(); + dlg.initOwner(primaryStage); dlg.setTitle("Animations-Set speichern"); dlg.setHeaderText("Clips und Aktions-Zuweisung für das Set konfigurieren"); javafx.scene.control.ButtonType ok = new javafx.scene.control.ButtonType("Speichern", @@ -9706,6 +9760,7 @@ public class EditorApp extends Application { javafx.scene.control.Dialog addDlg = new javafx.scene.control.Dialog<>(); + addDlg.initOwner(primaryStage); addDlg.setTitle("Aktion zuweisen"); javafx.scene.control.ButtonType addOk = new javafx.scene.control.ButtonType("Hinzufügen", javafx.scene.control.ButtonBar.ButtonData.OK_DONE); diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index d53a798..a40e6bf 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -631,6 +631,13 @@ public class SharedInput { public final 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 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. */ public volatile String animOpStatus = null; /** JME3 → JavaFX: Status-Meldung für Einbett-Operationen (Character Editor). */ @@ -681,6 +688,9 @@ public class SharedInput { public volatile float modelEditorBoundsD = 0f; public volatile boolean modelEditorBoundsReady = false; + /** JME → JFX: Polygonanzahl der aktuell sichtbaren LOD-Stufe. */ + public volatile int modelEditorPolyCount = 0; + /** JFX → JME: aktuelle Skalierung (Echtzeit-Vorschau). */ public volatile float modelEditorScaleX = 1f; public volatile float modelEditorScaleY = 1f; @@ -719,6 +729,9 @@ public class SharedInput { public volatile boolean modelEditorLodChanged = false; /** JFX → JME: Eingebettetes LOD aus j3o entfernen und speichern (1=lod1, 2=lod2, -1=kein Auftrag). */ 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. * Azimut 0–360° (Kompassrichtung), Elevation 0–90° (Höhe über Horizont). */ diff --git a/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java b/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java index 8dbff8e..c60c3e9 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java @@ -8,6 +8,7 @@ import com.jme3.app.state.BaseAppState; import com.jme3.asset.AssetManager; import com.jme3.bounding.BoundingBox; import com.jme3.export.binary.BinaryExporter; +import com.jme3.export.binary.BinaryImporter; import com.jme3.material.Material; import com.jme3.material.RenderState; import com.jme3.math.ColorRGBA; @@ -75,6 +76,10 @@ public class EzTreeState extends BaseAppState { private int capturePass = 0; 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; } // ── Lifecycle ───────────────────────────────────────────────────────────── @@ -100,9 +105,16 @@ public class EzTreeState extends BaseAppState { if (pendingRequest != null && captureReady[0]) { finishCapture(); - } else if (pendingRequest == null) { - SharedInput.EzTreeGenRequest req = input.ezTreeGenQueue.poll(); - if (req != null) startGeneration(req); + } 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(); + if (req != null) startGeneration(req); + } } } @@ -122,6 +134,18 @@ public class EzTreeState extends BaseAppState { ld1Node.setLocalScale(1f / 3f); 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 ? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3.2f : 20f; @@ -155,9 +179,8 @@ public class EzTreeState extends BaseAppState { private Node buildLod1Node(SharedInput.EzTreeGenRequest req) { 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(); - ld.leaves.count = Math.max(0, opts.leaves.count / 2); Node n = tryNodeJsGeneration(new SharedInput.EzTreeGenRequest(ld, req.presetName(), false)); if (n != null) { n.setName("EzTree_ld1"); @@ -505,6 +528,106 @@ public class EzTreeState extends BaseAppState { 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() { ImpostorUtil.cleanup(app, captureVP, captureFB); captureVP = null; @@ -579,8 +702,7 @@ public class EzTreeState extends BaseAppState { Path baseDir = ASSET_ROOT.resolve("Models").resolve("trees").resolve(subPath); Files.createDirectories(baseDir); Path outPath = baseDir.resolve(fileName + ".j3o"); - // Controls vor Export entfernen (nicht serialisierbar über BinaryExporter) - while (lodRoot.getNumControls() > 0) lodRoot.removeControl(lodRoot.getControl(0)); + stripControlsRecursive(lodRoot); BinaryExporter.getInstance().save(lodRoot, outPath.toFile()); de.blight.common.ModelMeta meta = new de.blight.common.ModelMeta( diff --git a/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java index 4cd18d1..240ecb3 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/ModelEditorState.java @@ -27,6 +27,7 @@ import com.jme3.anim.SkinningControl; import com.jme3.util.BufferUtils; import de.blight.editor.SharedInput; import de.blight.editor.util.ImpostorUtil; +import de.blight.editor.util.ModelExportUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -104,6 +105,10 @@ public class ModelEditorState extends BaseAppState { private boolean hasEmbeddedLods = false; 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 private final java.util.List treeLightMaterials = new java.util.ArrayList<>(); // 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 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 String openPath = input.modelEditorOpenPath; if (openPath != null) { @@ -186,8 +210,22 @@ public class ModelEditorState extends BaseAppState { } showSpatialDirectly(genPreview.clone()); } 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 - 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++) { if (embeddedLodSpatials[i] != null) { boolean show = (i == lodIdx); @@ -195,6 +233,7 @@ public class ModelEditorState extends BaseAppState { embeddedLodSpatials[i].setShadowMode(RenderQueue.ShadowMode.Off); } } + updateBounds(); } else { // Pfad-basierter Fallback String path = switch (input.modelEditorLodPreview) { @@ -251,18 +290,6 @@ public class ModelEditorState extends BaseAppState { 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 Path thumbReq = input.modelEditorThumbnailRequest; if (thumbReq != null && modelWrapper != null) { @@ -364,6 +391,7 @@ public class ModelEditorState extends BaseAppState { currentPath = assetPath; modelWrapper = new Node("model_wrapper"); + showingDirectPreview = false; hasEmbeddedLods = false; embeddedLodSpatials = null; input.modelEditorHasEmbeddedLods = false; @@ -383,18 +411,43 @@ public class ModelEditorState extends BaseAppState { originalSpatial = model; modelWrapper.attachChild(model); - // Eingebettete LODs erkennen: Node mit ≥2 Kindern, wobei Kind 0 sichtbar - // und alle weiteren mit CullHint.Always gesetzt sind (EZ-Tree / TreeGenerator-Muster). - if (model instanceof Node rootNode && rootNode.getChildren().size() >= 2) { - var children = rootNode.getChildren(); - boolean lodPattern = children.stream().skip(1) - .allMatch(s -> s.getCullHint() == Spatial.CullHint.Always); - if (lodPattern) { - embeddedLodSpatials = children.toArray(new Spatial[0]); - hasEmbeddedLods = true; - input.modelEditorHasEmbeddedLods = true; - input.modelEditorHasEmbeddedLod1 = embeddedLodSpatials.length > 1; - input.modelEditorHasEmbeddedLod2 = embeddedLodSpatials.length > 2; + // Eingebettete LODs erkennen: Node mit ≥2 Kindern, wobei mindestens eines + // hinter Index 0 CullHint.Always hat (EZ-Tree / TreeGenerator-Muster). + // 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(); + boolean hasAnyHidden = children.stream().skip(1) + .anyMatch(s -> s.getCullHint() == Spatial.CullHint.Always); + log.info("[ModelEditor] hasAnyHidden (ab Index 1)={}", hasAnyHidden); + if (hasAnyHidden) { + 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.modelEditorHasEmbeddedLod1 = embeddedLodSpatials[1] != null; + input.modelEditorHasEmbeddedLod2 = embeddedLodSpatials[2] != null; + } else { + embeddedLodSpatials = null; + } + } } } } catch (Exception e) { @@ -481,6 +534,7 @@ public class ModelEditorState extends BaseAppState { if (previewRoot == null) enterPreview(); if (modelWrapper != null) previewRoot.detachChild(modelWrapper); modelWrapper = new Node("model_wrapper"); + showingDirectPreview = true; stripControls(spatial); treeLightMaterials.clear(); pbrMaterials.clear(); @@ -654,7 +708,8 @@ public class ModelEditorState extends BaseAppState { java.nio.file.Path absPath = ASSET_ROOT.resolve(currentPath); Spatial root = modelWrapper.getChild(0); 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); } catch (Exception 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.modelEditorBoundsD = bb.getZExtent() * 2f; input.modelEditorBoundsReady = true; + input.modelEditorPolyCount = countVisibleTriangles(modelWrapper); 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() { if (modelWrapper == null) return null; modelWrapper.updateGeometricState(); @@ -900,9 +967,22 @@ public class ModelEditorState extends BaseAppState { 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) { 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) { 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 ──────────────────────────────────────────────────────────── /** @@ -985,11 +1246,12 @@ public class ModelEditorState extends BaseAppState { log.info("[ModelEditor] Animiert: Scale ({},{},{}) als Spatial-Transform gespeichert", sx, sy, sz); } else { root.setLocalScale(sx, sy, sz); - ModelImportState.stripControls(root); - ModelImportState.bakeTransform(root, new Matrix4f()); + ModelExportUtil.stripControls(root); + ModelExportUtil.bakeTransform(root, new Matrix4f()); log.info("[ModelEditor] Statisch: Scale ({},{},{}) in Vertices gebacken", sx, sy, sz); } BinaryExporter.getInstance().save(root, j3oPath.toFile()); + syncToMirrors(j3oPath); log.info("[ModelEditor] j3o nach Bake gespeichert: {}", j3oPath.getFileName()); } catch (Exception e) { log.error("[ModelEditor] Scale-Bake fehlgeschlagen: {}", e.getMessage(), e); @@ -1030,6 +1292,7 @@ public class ModelEditorState extends BaseAppState { if (savable instanceof Spatial root) { ThumbnailRenderer.embed(root, pngBytes); BinaryExporter.getInstance().save(root, j3oPath.toFile()); + syncToMirrors(j3oPath); } } catch (Exception e) { log.error("[ModelEditor] j3o-Embed fehlgeschlagen: {}", e.getMessage(), e); diff --git a/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java b/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java index 77a0329..ea6c8fb 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java @@ -1,5 +1,7 @@ package de.blight.editor.state; +import de.blight.editor.util.ModelExportUtil; + import java.io.File; import java.io.IOException; import java.nio.FloatBuffer; @@ -161,17 +163,21 @@ public class ModelImportState extends BaseAppState { } try { Spatial clone = modelSpatial.clone(); - stripControls(clone); + ModelExportUtil.stripControls(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 weldVerticesRecursive(clone, 0.01f); if ("jme".equals(input.modelLodAlgorithm)) { - decimateRecursiveJme(clone, 1.0f - reduction); + decimateRecursiveJme(clone, 1.0f - reduction, skipLeaves); } else { - decimateRecursiveBlight(clone, 1.0f - reduction); + decimateRecursiveBlight(clone, 1.0f - reduction, skipLeaves); } Node wrapper; @@ -212,7 +218,7 @@ public class ModelImportState extends BaseAppState { else input.modelImportHasLod2 = false; try { Spatial loaded = assets.loadModel(assetPath); - stripControls(loaded); + ModelExportUtil.stripControls(loaded); Node wrapper; if (loaded instanceof Node n) wrapper = n; else { wrapper = new Node("lod" + level); wrapper.attachChild(loaded); } @@ -253,7 +259,7 @@ public class ModelImportState extends BaseAppState { Spatial cleanClone = src.clone(); // Controls entfernen, damit z.B. ein ImportLodControl den CullHint // nicht auf lod0 zurücksetzt und die Geometrie im Capture unsichtbar macht. - stripControls(cleanClone); + ModelExportUtil.stripControls(cleanClone); cleanClone.setLocalTranslation(Vector3f.ZERO); cleanClone.setLocalScale(input.modelEditorScaleX, input.modelEditorScaleY, input.modelEditorScaleZ); cleanClone.setLocalRotation(Quaternion.IDENTITY); @@ -337,7 +343,7 @@ public class ModelImportState extends BaseAppState { // Clone original, apply target scale, compute bottom-aligned pivot Spatial lod0 = srcSpatial.clone(); - stripControls(lod0); + ModelExportUtil.stripControls(lod0); lod0.setLocalScale(sx, sy, sz); lod0.setLocalTranslation(Vector3f.ZERO); lod0.updateGeometricState(); @@ -348,7 +354,7 @@ public class ModelImportState extends BaseAppState { translationY = -(bb.getCenter().y - bb.getYExtent()) + pivotY; } lod0.setLocalTranslation(0f, translationY, 0f); - bakeTransform(lod0, new Matrix4f()); + ModelExportUtil.bakeTransform(lod0, new Matrix4f()); lod0.setName("lod0"); Node root = new Node(fileName); @@ -366,11 +372,17 @@ public class ModelImportState extends BaseAppState { } if (lod2Node != null) { lod2Node.setName("lod2"); - lod2Node.setLocalScale(sx, sy, sz); - lod2Node.setLocalTranslation(0f, translationY, 0f); + 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.setLocalTranslation(0f, translationY, 0f); + bakeSpatialInPlace(lod2Node); + } lod2Node.setCullHint(Spatial.CullHint.Always); lod2Node.setShadowMode(RenderQueue.ShadowMode.Off); - bakeSpatialInPlace(lod2Node); root.attachChild(lod2Node); } @@ -428,8 +440,11 @@ public class ModelImportState extends BaseAppState { // ── Mesh-Decimation ─────────────────────────────────────────────────────── /** 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 (skipLeaves && isLeafGeometry(g)) { + return; + } Mesh mesh = g.getMesh(); int origTris = mesh.getTriangleCount(); try { @@ -451,7 +466,7 @@ public class ModelImportState extends BaseAppState { log.warn("[ModelImport] JME bakeLods fehlgeschlagen für {}: {}", g.getName(), e.getMessage()); } } 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, * 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 (skipLeaves && isLeafGeometry(g)) { + return; + } g.setMesh(edgeCollapseMesh(g.getMesh(), keepRatio)); } 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. * Handhabt alle möglichen Index-Buffer-Formate (UnsignedShort, UnsignedInt, Float). @@ -738,83 +773,14 @@ public class ModelImportState extends BaseAppState { private Spatial bakeSpatial(Spatial source) { Spatial clone = source.clone(); - stripControls(clone); - bakeTransform(clone, new Matrix4f()); + ModelExportUtil.stripControls(clone); + ModelExportUtil.bakeTransform(clone, new Matrix4f()); return clone; } private void bakeSpatialInPlace(Spatial s) { - stripControls(s); - 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); + ModelExportUtil.stripControls(s); + ModelExportUtil.bakeTransform(s, new Matrix4f()); } private static Node wrapInNode(Spatial s, String name) { diff --git a/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder.java b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder.java index 946221d..8e4851c 100644 --- a/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder.java +++ b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder.java @@ -30,7 +30,10 @@ public class TreeMeshBuilder { public MeshResult build(TreeParams p, float quality) { 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); VertexCollector barkCol = new VertexCollector(); @@ -49,7 +52,11 @@ public class TreeMeshBuilder { if (lv >= maxLevel) continue; // 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); float branchLen = TreeParams.lv(p.length, lv); float baseRad = TreeParams.lv(p.radius, lv); diff --git a/blight-editor/src/main/java/de/blight/editor/util/ImpostorUtil.java b/blight-editor/src/main/java/de/blight/editor/util/ImpostorUtil.java index 8e17308..da4b56a 100644 --- a/blight-editor/src/main/java/de/blight/editor/util/ImpostorUtil.java +++ b/blight-editor/src/main/java/de/blight/editor/util/ImpostorUtil.java @@ -347,19 +347,20 @@ public final class ImpostorUtil { protected void controlUpdate(float tpf) { if (cam == null || !(spatial instanceof Node n)) return; List quads = n.getChildren(); - if (quads.size() < DIRS) return; + 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; - // 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 t = angle * DIRS / FastMath.TWO_PI; - int best = ((int) Math.round(t) % DIRS + DIRS) % DIRS; - int opp = (best + DIRS / 2) % DIRS; // gegenüberliegendes Quad + float t = angle * dirs / FastMath.TWO_PI; + int best = ((int) Math.round(t) % dirs + dirs) % dirs; + 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 ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); diff --git a/blight-editor/src/main/java/de/blight/editor/util/ModelExportUtil.java b/blight-editor/src/main/java/de/blight/editor/util/ModelExportUtil.java new file mode 100644 index 0000000..acf208e --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/util/ModelExportUtil.java @@ -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; + } +} diff --git a/blight-game/src/main/java/de/blight/game/state/ImpostorViewControl.java b/blight-game/src/main/java/de/blight/game/state/ImpostorViewControl.java new file mode 100644 index 0000000..3f0a7df --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/ImpostorViewControl.java @@ -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 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) {} +} diff --git a/blight-game/src/main/java/de/blight/game/state/WorldObjectsState.java b/blight-game/src/main/java/de/blight/game/state/WorldObjectsState.java index 5ef01e1..466353b 100644 --- a/blight-game/src/main/java/de/blight/game/state/WorldObjectsState.java +++ b/blight-game/src/main/java/de/blight/game/state/WorldObjectsState.java @@ -14,7 +14,6 @@ import com.jme3.material.RenderState; import com.jme3.math.*; import com.jme3.renderer.queue.RenderQueue; import com.jme3.scene.*; -import com.jme3.scene.control.BillboardControl; import com.jme3.scene.shape.*; import com.jme3.texture.Texture; import de.blight.common.PlacedModel; @@ -221,11 +220,20 @@ public class WorldObjectsState extends BaseAppState { lodRoot.attachChild(lod2); // index 1 = lod2 (Impostor-Karte) treeNode.setCullHint(Spatial.CullHint.Inherit); lod2.setCullHint(Spatial.CullHint.Always); - // Billboard: Impostor-Karte dreht sich immer zur Kamera, - // unabhängig von der Baum-Rotation im BLO. - BillboardControl bc = new BillboardControl(); - bc.setAlignment(BillboardControl.Alignment.Camera); - lod2.addControl(bc); + // Pivot-Korrektur: Stammbasis auf Y=0 ausrichten (Sicherungsnetz für + // j3o-Dateien, deren 1/3-Scale-Offset nicht eingebacken wurde). + // treeNode und lod2 gemeinsam verschieben → Geometrie und Impostor-Bild + // bleiben deckungsgleich. + 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( app.getCamera(), treeNode, null, lod2, m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor, m.cullDistance() * lodFactor)); diff --git a/blight-game/src/main/resources/logback.xml b/blight-game/src/main/resources/logback.xml index 338cdce..174b74e 100644 --- a/blight-game/src/main/resources/logback.xml +++ b/blight-game/src/main/resources/logback.xml @@ -1,9 +1,6 @@ - - INFO - %d{HH:mm:ss.SSS} %-5level [%logger{30}] %msg%n%ex @@ -20,6 +17,9 @@ + + +