An den LOD im Editor gearbeitet
This commit is contained in:
@@ -246,6 +246,7 @@ public class EditorApp extends Application {
|
||||
|
||||
// Modell-Editor-Zustand
|
||||
private Label modelEditorDimLabel;
|
||||
private Label modelEditorPolyLabel;
|
||||
private Spinner<Double> 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<javafx.util.Pair<String, Integer>> 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<java.util.List<String>> 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<javafx.scene.control.ButtonType> 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<javafx.scene.control.ButtonType> 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<javafx.scene.control.ButtonType> 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<javafx.scene.control.ButtonType> 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<javafx.scene.control.ButtonType> 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);
|
||||
|
||||
@@ -631,6 +631,13 @@ public class SharedInput {
|
||||
public final java.util.concurrent.atomic.AtomicReference<String>
|
||||
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. */
|
||||
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). */
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<Material> 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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -347,19 +347,20 @@ public final class ImpostorUtil {
|
||||
protected void controlUpdate(float tpf) {
|
||||
if (cam == null || !(spatial instanceof Node n)) return;
|
||||
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());
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user