Imposter Generation angepassst
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,28 @@
|
||||
#Sat Jul 18 13:44:33 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_190939
|
||||
pivotOffsetY=0.0
|
||||
placementOffsetY=0.0
|
||||
randomScaleMax=1.0
|
||||
randomScaleMin=1.0
|
||||
receiveShadow=true
|
||||
scaleX=1.0
|
||||
scaleY=1.0
|
||||
scaleZ=1.0
|
||||
solid=false
|
||||
tags=
|
||||
uniformScale=true
|
||||
Binary file not shown.
@@ -0,0 +1,28 @@
|
||||
#Sat Jul 18 11:14:25 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_190953
|
||||
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
|
||||
@@ -256,8 +256,14 @@ public class EditorApp extends Application {
|
||||
private Label modelEditorLod2Label;
|
||||
private ToggleButton modelEditorLod1Btn;
|
||||
private ToggleButton modelEditorLod2Btn;
|
||||
private Label modelEditorLod1StatusLabel;
|
||||
private Label modelEditorLod2StatusLabel;
|
||||
private Button modelEditorLod1GenBtn;
|
||||
private Button modelEditorLod2GenBtn;
|
||||
private ToggleButton importLod1Btn;
|
||||
private ToggleButton importLod2Btn;
|
||||
private Button modelImportLod1ClearBtn;
|
||||
private Button modelImportLod2ClearBtn;
|
||||
// Anhänge (veränderliche Listen, werden bei Änderung an JME übertragen)
|
||||
private final java.util.List<de.blight.common.ModelMeta.AttachedLight> modelEditorLights = new java.util.ArrayList<>();
|
||||
private final java.util.List<de.blight.common.ModelMeta.AttachedEmitter> modelEditorEmitters = new java.util.ArrayList<>();
|
||||
@@ -663,35 +669,45 @@ public class EditorApp extends Application {
|
||||
}
|
||||
|
||||
// Modell-Editor: eingebettete LODs erkannt → Labels aktualisieren
|
||||
if (input.modelEditorHasEmbeddedLods) {
|
||||
if (modelEditorLod1Label != null
|
||||
&& "(nicht gesetzt)".equals(modelEditorLod1Label.getText())) {
|
||||
modelEditorLod1Label.setText("(eingebettet im j3o)");
|
||||
modelEditorLod1Label.setStyle("-fx-text-fill:#6aaa6a; -fx-font-size:11;");
|
||||
}
|
||||
if (modelEditorLod2Label != null
|
||||
&& "(nicht gesetzt)".equals(modelEditorLod2Label.getText())) {
|
||||
modelEditorLod2Label.setText("(eingebettet im j3o)");
|
||||
modelEditorLod2Label.setStyle("-fx-text-fill:#6aaa6a; -fx-font-size:11;");
|
||||
}
|
||||
if (input.modelEditorHasEmbeddedLod1 && modelEditorLod1Label != null
|
||||
&& "(kein Pfad)".equals(modelEditorLod1Label.getText())) {
|
||||
modelEditorLod1Label.setText("(eingebettet im j3o)");
|
||||
modelEditorLod1Label.setStyle("-fx-text-fill:#6aaa6a; -fx-font-size:11;");
|
||||
}
|
||||
// Model-Editor LOD-Buttons freischalten/sperren
|
||||
if (input.modelEditorHasEmbeddedLod2 && modelEditorLod2Label != null
|
||||
&& "(kein Pfad)".equals(modelEditorLod2Label.getText())) {
|
||||
modelEditorLod2Label.setText("(eingebettet im j3o)");
|
||||
modelEditorLod2Label.setStyle("-fx-text-fill:#6aaa6a; -fx-font-size:11;");
|
||||
}
|
||||
// Model-Editor LOD-Buttons freischalten/sperren (per Level)
|
||||
if (modelEditorLod1Btn != null)
|
||||
modelEditorLod1Btn.setDisable(!input.modelEditorHasEmbeddedLods && modelEditorLod1Path.isBlank());
|
||||
modelEditorLod1Btn.setDisable(!input.modelEditorHasEmbeddedLod1 && modelEditorLod1Path.isBlank() && !input.modelImportHasLod1);
|
||||
if (modelEditorLod2Btn != null)
|
||||
modelEditorLod2Btn.setDisable(!input.modelEditorHasEmbeddedLods && modelEditorLod2Path.isBlank());
|
||||
modelEditorLod2Btn.setDisable(!input.modelEditorHasEmbeddedLod2 && modelEditorLod2Path.isBlank() && !input.modelImportHasLod2);
|
||||
// Generieren-Buttons: nur aktiv wenn genau dieses LOD-Level noch nicht gesetzt
|
||||
if (modelEditorLod1GenBtn != null)
|
||||
modelEditorLod1GenBtn.setDisable(input.modelEditorHasEmbeddedLod1 || !modelEditorLod1Path.isBlank() || input.modelImportHasLod1);
|
||||
if (modelEditorLod2GenBtn != null)
|
||||
modelEditorLod2GenBtn.setDisable(input.modelEditorHasEmbeddedLod2 || !modelEditorLod2Path.isBlank() || input.modelImportHasLod2);
|
||||
// Importer LOD-Buttons freischalten sobald generiert
|
||||
if (importLod1Btn != null) importLod1Btn.setDisable(!input.modelImportHasLod1);
|
||||
if (importLod2Btn != null) importLod2Btn.setDisable(!input.modelImportHasLod2);
|
||||
if (modelImportLod1ClearBtn != null) modelImportLod1ClearBtn.setDisable(!input.modelImportHasLod1);
|
||||
if (modelImportLod2ClearBtn != null) modelImportLod2ClearBtn.setDisable(!input.modelImportHasLod2);
|
||||
|
||||
// Modell-Import: LOD-Status
|
||||
// Modell-Import/Editor: LOD-Status
|
||||
String lodStatus = input.modelLodGenStatus;
|
||||
if (lodStatus != null) {
|
||||
input.modelLodGenStatus = null;
|
||||
if (modelImportLod1StatusLabel != null && lodStatus.startsWith("LOD1"))
|
||||
modelImportLod1StatusLabel.setText(lodStatus);
|
||||
else if (modelImportLod2StatusLabel != null && (lodStatus.startsWith("LOD2") || lodStatus.contains("Impostor")))
|
||||
modelImportLod2StatusLabel.setText(lodStatus);
|
||||
boolean isLod1 = lodStatus.startsWith("LOD1");
|
||||
boolean isLod2 = lodStatus.startsWith("LOD2") || lodStatus.contains("Impostor");
|
||||
if (isLod1) {
|
||||
if (modelImportLod1StatusLabel != null) modelImportLod1StatusLabel.setText(lodStatus);
|
||||
if (modelEditorLod1StatusLabel != null) modelEditorLod1StatusLabel.setText(lodStatus);
|
||||
} else if (isLod2) {
|
||||
if (modelImportLod2StatusLabel != null) modelImportLod2StatusLabel.setText(lodStatus);
|
||||
if (modelEditorLod2StatusLabel != null) modelEditorLod2StatusLabel.setText(lodStatus);
|
||||
}
|
||||
setStatus(lodStatus);
|
||||
}
|
||||
// Modell-Import: Export-Ergebnis
|
||||
@@ -867,14 +883,15 @@ public class EditorApp extends Application {
|
||||
modelEditorLod1Btn.setOnAction(e -> { input.modelEditorLodPreview = 1; input.modelEditorLodChanged = true; });
|
||||
modelEditorLod2Btn.setOnAction(e -> { input.modelEditorLodPreview = 2; input.modelEditorLodChanged = true; });
|
||||
// Initial deaktiviert – werden freigegeben sobald LOD-Pfade/eingebettete LODs bekannt sind
|
||||
boolean hasLod1 = input.modelEditorHasEmbeddedLods || !modelEditorLod1Path.isBlank();
|
||||
boolean hasLod2 = input.modelEditorHasEmbeddedLods || !modelEditorLod2Path.isBlank();
|
||||
boolean hasLod1 = input.modelEditorHasEmbeddedLod1 || !modelEditorLod1Path.isBlank() || input.modelImportHasLod1;
|
||||
boolean hasLod2 = input.modelEditorHasEmbeddedLod2 || !modelEditorLod2Path.isBlank() || input.modelImportHasLod2;
|
||||
modelEditorLod1Btn.setDisable(!hasLod1);
|
||||
modelEditorLod2Btn.setDisable(!hasLod2);
|
||||
|
||||
ToolBar tb = new ToolBar();
|
||||
tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label,
|
||||
new Separator(Orientation.VERTICAL), lod0Btn, modelEditorLod1Btn, modelEditorLod2Btn);
|
||||
new Separator(Orientation.VERTICAL), lod0Btn,
|
||||
modelEditorLod1Btn, modelEditorLod2Btn);
|
||||
return tb;
|
||||
}
|
||||
|
||||
@@ -4051,6 +4068,10 @@ public class EditorApp extends Application {
|
||||
}
|
||||
|
||||
if (cat == modelsNode) {
|
||||
if ("modeleditor".equals(currentTool) && relPath.endsWith(".j3o")) {
|
||||
openModelEditor(relPath, p);
|
||||
return;
|
||||
}
|
||||
input.pendingModelPath = relPath;
|
||||
input.activeLayer = SharedInput.LAYER_OBJECTS;
|
||||
if (objPlaceBtn != null) objPlaceBtn.setSelected(true);
|
||||
@@ -5059,7 +5080,7 @@ public class EditorApp extends Application {
|
||||
/** Gibt zurück, ob das aktive Tool den aufgeklappten Zustand des Asset-Baums behalten soll. */
|
||||
private boolean shouldPreserveExpansion() {
|
||||
return "vegetation".equals(currentTool) || "eztree".equals(currentTool)
|
||||
|| "objecteditor".equals(currentTool)
|
||||
|| "objecteditor".equals(currentTool) || "modeleditor".equals(currentTool)
|
||||
|| ("world".equals(currentTool)
|
||||
&& (input.activeLayer == SharedInput.LAYER_OBJECTS
|
||||
|| input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT));
|
||||
@@ -5812,8 +5833,8 @@ public class EditorApp extends Application {
|
||||
topBar.getChildren().set(1, buildModelEditorToolBar());
|
||||
// Kein Modell vorausgewählt – Platzhalter zeigen
|
||||
VBox placeholder = new VBox(12);
|
||||
placeholder.setPadding(new javafx.geometry.Insets(16));
|
||||
placeholder.setPrefWidth(300);
|
||||
placeholder.setPadding(new javafx.geometry.Insets(12));
|
||||
placeholder.setPrefWidth(280);
|
||||
placeholder.setStyle("-fx-background-color: #2a2a3e;");
|
||||
Label title = new Label("Modell-Editor");
|
||||
title.setStyle("-fx-font-weight:bold; -fx-font-size:14; -fx-text-fill:#ddd;");
|
||||
@@ -5854,7 +5875,8 @@ public class EditorApp extends Application {
|
||||
modelEditorScroll.setStyle("-fx-background-color:transparent;-fx-background:transparent;");
|
||||
VBox modelEditorOuter = new VBox(modelEditorScroll);
|
||||
VBox.setVgrow(modelEditorScroll, Priority.ALWAYS);
|
||||
modelEditorOuter.setPrefWidth(300);
|
||||
modelEditorOuter.setPrefWidth(280);
|
||||
modelEditorOuter.setMaxWidth(280);
|
||||
root.setRight(modelEditorOuter);
|
||||
setStatus("Modell-Editor: " + relPath);
|
||||
}
|
||||
@@ -5862,9 +5884,9 @@ public class EditorApp extends Application {
|
||||
private VBox buildModelEditorPanel(String relPath,
|
||||
java.nio.file.Path absolutePath,
|
||||
de.blight.common.ModelMeta meta) {
|
||||
VBox panel = new VBox(8);
|
||||
panel.setPadding(new javafx.geometry.Insets(10));
|
||||
panel.setPrefWidth(300);
|
||||
VBox panel = new VBox(6);
|
||||
panel.setPadding(new javafx.geometry.Insets(8));
|
||||
panel.setPrefWidth(280);
|
||||
panel.setStyle("-fx-background-color: #2a2a3e;");
|
||||
|
||||
// ── Titel ────────────────────────────────────────────────────────────
|
||||
@@ -5958,8 +5980,10 @@ public class EditorApp extends Application {
|
||||
});
|
||||
|
||||
// ── Maße (read-only, von JME aktualisiert) ───────────────────────────
|
||||
modelEditorDimLabel = new Label("Maße: – × – × – m");
|
||||
modelEditorDimLabel = new Label("B – × H – × T – m");
|
||||
modelEditorDimLabel.setStyle("-fx-text-fill:#8cf; -fx-font-family:monospace;");
|
||||
modelEditorDimLabel.setMaxWidth(Double.MAX_VALUE);
|
||||
modelEditorDimLabel.setWrapText(true);
|
||||
|
||||
// ── Pivot & Platzierungs-Versatz ─────────────────────────────────────
|
||||
Label offsetTitle = new Label("Versatz:");
|
||||
@@ -6007,38 +6031,79 @@ public class EditorApp extends Application {
|
||||
Label lodTitle = new Label("Level of Detail");
|
||||
lodTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;");
|
||||
|
||||
modelEditorLod1Label = new Label(modelEditorLod1Path.isBlank() ? "(nicht gesetzt)" : modelEditorLod1Path);
|
||||
// ── LOD 1 ─────────────────────────────────────────────────────────────
|
||||
modelEditorLod1GenBtn = new Button("LOD 1 generieren");
|
||||
modelEditorLod1GenBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
boolean lod1Exists = input.modelEditorHasEmbeddedLod1 || !modelEditorLod1Path.isBlank() || input.modelImportHasLod1;
|
||||
modelEditorLod1GenBtn.setDisable(lod1Exists);
|
||||
modelEditorLod1GenBtn.setOnAction(e -> showLodGenDialog(1, 50));
|
||||
|
||||
modelEditorLod1StatusLabel = new Label("–");
|
||||
modelEditorLod1StatusLabel.setStyle("-fx-text-fill:#888; -fx-font-size:11;");
|
||||
modelEditorLod1StatusLabel.setWrapText(true);
|
||||
|
||||
modelEditorLod1Label = new Label(modelEditorLod1Path.isBlank() ? "(kein Pfad)" : modelEditorLod1Path);
|
||||
modelEditorLod1Label.setStyle("-fx-text-fill:#888; -fx-font-size:11;");
|
||||
modelEditorLod1Label.setWrapText(true);
|
||||
Button lod1ImportBtn = new Button("LOD 1 importieren…");
|
||||
Button lod1ClearBtn = new Button("✕");
|
||||
|
||||
Button lod1ClearBtn = new Button("✕");
|
||||
lod1ClearBtn.setStyle("-fx-text-fill:#c0392b;");
|
||||
lod1ImportBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
lod1ImportBtn.setOnAction(e -> importLodFile(1));
|
||||
lod1ClearBtn.setOnAction(e -> {
|
||||
if (input.modelEditorHasEmbeddedLod1) {
|
||||
input.modelEditorRemoveEmbeddedLod = 1;
|
||||
}
|
||||
if (input.modelImportHasLod1) {
|
||||
input.modelImportClearLodLevel = 1;
|
||||
if (modelEditorLod1StatusLabel != null) modelEditorLod1StatusLabel.setText("–");
|
||||
}
|
||||
modelEditorLod1Path = "";
|
||||
input.modelEditorLod1Path = "";
|
||||
modelEditorLod1Label.setText("(nicht gesetzt)");
|
||||
modelEditorLod1Label.setText("(kein Pfad)");
|
||||
if (input.modelEditorLodPreview == 1) {
|
||||
input.modelEditorLodPreview = 0;
|
||||
input.modelEditorLodChanged = true;
|
||||
}
|
||||
});
|
||||
|
||||
modelEditorLod2Label = new Label(modelEditorLod2Path.isBlank() ? "(nicht gesetzt)" : modelEditorLod2Path);
|
||||
HBox lod1Row = new HBox(4, modelEditorLod1GenBtn, lod1ClearBtn);
|
||||
HBox.setHgrow(modelEditorLod1GenBtn, javafx.scene.layout.Priority.ALWAYS);
|
||||
|
||||
// ── LOD 2 ─────────────────────────────────────────────────────────────
|
||||
modelEditorLod2GenBtn = new Button("LOD 2 generieren");
|
||||
modelEditorLod2GenBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
boolean lod2Exists = input.modelEditorHasEmbeddedLod2 || !modelEditorLod2Path.isBlank() || input.modelImportHasLod2;
|
||||
modelEditorLod2GenBtn.setDisable(lod2Exists);
|
||||
modelEditorLod2GenBtn.setOnAction(e -> showLodGenDialog(2, 85));
|
||||
|
||||
modelEditorLod2StatusLabel = new Label("–");
|
||||
modelEditorLod2StatusLabel.setStyle("-fx-text-fill:#888; -fx-font-size:11;");
|
||||
modelEditorLod2StatusLabel.setWrapText(true);
|
||||
|
||||
modelEditorLod2Label = new Label(modelEditorLod2Path.isBlank() ? "(kein Pfad)" : modelEditorLod2Path);
|
||||
modelEditorLod2Label.setStyle("-fx-text-fill:#888; -fx-font-size:11;");
|
||||
modelEditorLod2Label.setWrapText(true);
|
||||
Button lod2ImportBtn = new Button("LOD 2 importieren…");
|
||||
Button lod2ClearBtn = new Button("✕");
|
||||
|
||||
Button lod2ClearBtn = new Button("✕");
|
||||
lod2ClearBtn.setStyle("-fx-text-fill:#c0392b;");
|
||||
lod2ImportBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
lod2ImportBtn.setOnAction(e -> importLodFile(2));
|
||||
lod2ClearBtn.setOnAction(e -> {
|
||||
if (input.modelEditorHasEmbeddedLod2) {
|
||||
input.modelEditorRemoveEmbeddedLod = 2;
|
||||
}
|
||||
if (input.modelImportHasLod2) {
|
||||
input.modelImportClearLodLevel = 2;
|
||||
if (modelEditorLod2StatusLabel != null) modelEditorLod2StatusLabel.setText("–");
|
||||
}
|
||||
modelEditorLod2Path = "";
|
||||
input.modelEditorLod2Path = "";
|
||||
modelEditorLod2Label.setText("(nicht gesetzt)");
|
||||
modelEditorLod2Label.setText("(kein Pfad)");
|
||||
if (input.modelEditorLodPreview == 2) {
|
||||
input.modelEditorLodPreview = 0;
|
||||
input.modelEditorLodChanged = true;
|
||||
}
|
||||
});
|
||||
|
||||
HBox lod1Row = new HBox(4, lod1ImportBtn, lod1ClearBtn);
|
||||
HBox lod2Row = new HBox(4, lod2ImportBtn, lod2ClearBtn);
|
||||
HBox.setHgrow(lod1ImportBtn, javafx.scene.layout.Priority.ALWAYS);
|
||||
HBox.setHgrow(lod2ImportBtn, javafx.scene.layout.Priority.ALWAYS);
|
||||
HBox lod2Row = new HBox(4, modelEditorLod2GenBtn, lod2ClearBtn);
|
||||
HBox.setHgrow(modelEditorLod2GenBtn, javafx.scene.layout.Priority.ALWAYS);
|
||||
|
||||
// ── Anhänge: Lichter & Partikel ───────────────────────────────────────
|
||||
Label attachTitle = new Label("Anhänge: Lichter & Partikel");
|
||||
@@ -6134,7 +6199,7 @@ public class EditorApp extends Application {
|
||||
new javafx.scene.control.SpinnerValueFactory.DoubleSpinnerValueFactory(-5.0, 5.0,
|
||||
Math.round(meta.interactableOffsetX() * 100.0) / 100.0, 0.05));
|
||||
modelEditorInteractableXSpin.setEditable(true);
|
||||
modelEditorInteractableXSpin.setPrefWidth(100);
|
||||
modelEditorInteractableXSpin.setMaxWidth(Double.MAX_VALUE);
|
||||
modelEditorInteractableXSpin.valueProperty().addListener((obs, ov, nv) -> {
|
||||
if (updatingInteractableSpinnersFromJme) return;
|
||||
input.modelInteractableOffsetX = nv.floatValue();
|
||||
@@ -6145,7 +6210,7 @@ public class EditorApp extends Application {
|
||||
new javafx.scene.control.SpinnerValueFactory.DoubleSpinnerValueFactory(-1.0, 4.0,
|
||||
Math.round(meta.interactableOffsetY() * 100.0) / 100.0, 0.05));
|
||||
modelEditorInteractableYSpin.setEditable(true);
|
||||
modelEditorInteractableYSpin.setPrefWidth(100);
|
||||
modelEditorInteractableYSpin.setMaxWidth(Double.MAX_VALUE);
|
||||
modelEditorInteractableYSpin.valueProperty().addListener((obs, ov, nv) -> {
|
||||
if (updatingInteractableSpinnersFromJme) return;
|
||||
input.modelInteractableOffsetY = nv.floatValue();
|
||||
@@ -6156,7 +6221,7 @@ public class EditorApp extends Application {
|
||||
new javafx.scene.control.SpinnerValueFactory.DoubleSpinnerValueFactory(-5.0, 5.0,
|
||||
Math.round(meta.interactableOffsetZ() * 100.0) / 100.0, 0.05));
|
||||
modelEditorInteractableZSpin.setEditable(true);
|
||||
modelEditorInteractableZSpin.setPrefWidth(100);
|
||||
modelEditorInteractableZSpin.setMaxWidth(Double.MAX_VALUE);
|
||||
modelEditorInteractableZSpin.valueProperty().addListener((obs, ov, nv) -> {
|
||||
if (updatingInteractableSpinnersFromJme) return;
|
||||
input.modelInteractableOffsetZ = nv.floatValue();
|
||||
@@ -6165,6 +6230,11 @@ public class EditorApp extends Application {
|
||||
|
||||
javafx.scene.layout.GridPane posGrid = new javafx.scene.layout.GridPane();
|
||||
posGrid.setHgap(4); posGrid.setVgap(2);
|
||||
javafx.scene.layout.ColumnConstraints posCC0 = new javafx.scene.layout.ColumnConstraints();
|
||||
posCC0.setMinWidth(20); posCC0.setHgrow(javafx.scene.layout.Priority.NEVER);
|
||||
javafx.scene.layout.ColumnConstraints posCC1 = new javafx.scene.layout.ColumnConstraints();
|
||||
posCC1.setHgrow(javafx.scene.layout.Priority.ALWAYS); posCC1.setFillWidth(true);
|
||||
posGrid.getColumnConstraints().addAll(posCC0, posCC1);
|
||||
posGrid.add(new Label("X:"), 0, 0); posGrid.add(modelEditorInteractableXSpin, 1, 0);
|
||||
posGrid.add(new Label("Y:"), 0, 1); posGrid.add(modelEditorInteractableYSpin, 1, 1);
|
||||
posGrid.add(new Label("Z:"), 0, 2); posGrid.add(modelEditorInteractableZSpin, 1, 2);
|
||||
@@ -6179,7 +6249,7 @@ public class EditorApp extends Application {
|
||||
Slider rotSlider = new Slider(0, 360, initDeg);
|
||||
rotSlider.setShowTickMarks(true);
|
||||
rotSlider.setMajorTickUnit(90);
|
||||
rotSlider.setPrefWidth(180);
|
||||
rotSlider.setPrefWidth(150);
|
||||
|
||||
TextField rotField = new TextField(String.format("%.1f", initDeg));
|
||||
rotField.setPrefWidth(60);
|
||||
@@ -6329,8 +6399,8 @@ public class EditorApp extends Application {
|
||||
rndTitle, rndGrid,
|
||||
new Separator(),
|
||||
lodTitle,
|
||||
modelEditorLod1Label, lod1Row,
|
||||
modelEditorLod2Label, lod2Row,
|
||||
lod1Row, modelEditorLod1StatusLabel, modelEditorLod1Label,
|
||||
lod2Row, modelEditorLod2StatusLabel, modelEditorLod2Label,
|
||||
new Separator(),
|
||||
attachTitle,
|
||||
lightSectionLbl, modelEditorLightBox, addLightBtn,
|
||||
@@ -6409,6 +6479,8 @@ public class EditorApp extends Application {
|
||||
modelImportCurrentPath = relPath;
|
||||
modelImportLod1StatusLabel = null;
|
||||
modelImportLod2StatusLabel = null;
|
||||
modelImportLod1ClearBtn = null;
|
||||
modelImportLod2ClearBtn = null;
|
||||
modelImportExportStatusLabel = null;
|
||||
input.modelImportHasLod1 = false;
|
||||
input.modelImportHasLod2 = false;
|
||||
@@ -6567,8 +6639,10 @@ public class EditorApp extends Application {
|
||||
}
|
||||
});
|
||||
|
||||
modelEditorDimLabel = new Label("Maße: – × – × – m");
|
||||
modelEditorDimLabel = new Label("B – × H – × T – m");
|
||||
modelEditorDimLabel.setStyle("-fx-text-fill:#8cf; -fx-font-family:monospace;");
|
||||
modelEditorDimLabel.setMaxWidth(Double.MAX_VALUE);
|
||||
modelEditorDimLabel.setWrapText(true);
|
||||
|
||||
// ── LOD-Algorithmus ───────────────────────────────────────────────────
|
||||
Label algoTitle = new Label("Reduktions-Algorithmus");
|
||||
@@ -6624,6 +6698,14 @@ public class EditorApp extends Application {
|
||||
modelImportLod1StatusLabel = new Label("(nicht gesetzt)");
|
||||
modelImportLod1StatusLabel.setStyle("-fx-text-fill:#888; -fx-font-size:11;");
|
||||
modelImportLod1StatusLabel.setWrapText(true);
|
||||
modelImportLod1ClearBtn = new Button("✕ LOD 1 entfernen");
|
||||
modelImportLod1ClearBtn.setStyle("-fx-text-fill:#c0392b;");
|
||||
modelImportLod1ClearBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
modelImportLod1ClearBtn.setDisable(true);
|
||||
modelImportLod1ClearBtn.setOnAction(e -> {
|
||||
input.modelImportClearLodLevel = 1;
|
||||
modelImportLod1StatusLabel.setText("(nicht gesetzt)");
|
||||
});
|
||||
|
||||
// ── LOD 2 ─────────────────────────────────────────────────────────────
|
||||
Label lod2Title = new Label("LOD 2");
|
||||
@@ -6665,6 +6747,14 @@ public class EditorApp extends Application {
|
||||
modelImportLod2StatusLabel = new Label("(nicht gesetzt)");
|
||||
modelImportLod2StatusLabel.setStyle("-fx-text-fill:#888; -fx-font-size:11;");
|
||||
modelImportLod2StatusLabel.setWrapText(true);
|
||||
modelImportLod2ClearBtn = new Button("✕ LOD 2 entfernen");
|
||||
modelImportLod2ClearBtn.setStyle("-fx-text-fill:#c0392b;");
|
||||
modelImportLod2ClearBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
modelImportLod2ClearBtn.setDisable(true);
|
||||
modelImportLod2ClearBtn.setOnAction(e -> {
|
||||
input.modelImportClearLodLevel = 2;
|
||||
modelImportLod2StatusLabel.setText("(nicht gesetzt)");
|
||||
});
|
||||
|
||||
// ── Export ────────────────────────────────────────────────────────────
|
||||
Label exportTitle = new Label("Export");
|
||||
@@ -6705,9 +6795,9 @@ public class EditorApp extends Application {
|
||||
new Separator(),
|
||||
algoTitle, algoBlightRB, algoJmeRB,
|
||||
new Separator(),
|
||||
lod1Title, lod1AutoRB, lod1FileRB, lod1GenBtn, modelImportLod1StatusLabel,
|
||||
lod1Title, lod1AutoRB, lod1FileRB, lod1GenBtn, modelImportLod1StatusLabel, modelImportLod1ClearBtn,
|
||||
new Separator(),
|
||||
lod2Title, lod2MeshRB, lod2FileRB, lod2GenBtn, modelImportLod2StatusLabel,
|
||||
lod2Title, lod2MeshRB, lod2FileRB, lod2GenBtn, modelImportLod2StatusLabel, modelImportLod2ClearBtn,
|
||||
new Separator(),
|
||||
exportTitle, assetNameLabel, nameTF, modelImportExportStatusLabel,
|
||||
exportBtn, cancelBtn
|
||||
@@ -6758,12 +6848,19 @@ public class EditorApp extends Application {
|
||||
}
|
||||
|
||||
private void addLabeledRow(GridPane grid, int row, String labelText, javafx.scene.Node ctrl) {
|
||||
if (grid.getColumnConstraints().isEmpty()) {
|
||||
javafx.scene.layout.ColumnConstraints cc0 = new javafx.scene.layout.ColumnConstraints();
|
||||
cc0.setMinWidth(80);
|
||||
cc0.setHgrow(javafx.scene.layout.Priority.NEVER);
|
||||
javafx.scene.layout.ColumnConstraints cc1 = new javafx.scene.layout.ColumnConstraints();
|
||||
cc1.setHgrow(javafx.scene.layout.Priority.ALWAYS);
|
||||
cc1.setFillWidth(true);
|
||||
grid.getColumnConstraints().addAll(cc0, cc1);
|
||||
}
|
||||
Label l = new Label(labelText);
|
||||
l.setStyle("-fx-text-fill:#aaa;");
|
||||
l.setMinWidth(90);
|
||||
grid.add(l, 0, row);
|
||||
grid.add(ctrl, 1, row);
|
||||
javafx.scene.layout.GridPane.setHgrow(ctrl, javafx.scene.layout.Priority.ALWAYS);
|
||||
}
|
||||
|
||||
private void pushScaleToJme() {
|
||||
@@ -6776,8 +6873,7 @@ public class EditorApp extends Application {
|
||||
|
||||
private void updateModelEditorDimensions(float w, float h, float d) {
|
||||
if (modelEditorDimLabel != null)
|
||||
modelEditorDimLabel.setText(String.format(
|
||||
"Maße: B %.2f m × H %.2f m × T %.2f m", w, h, d));
|
||||
modelEditorDimLabel.setText(String.format("B %.2f × H %.2f × T %.2f m", w, h, d));
|
||||
}
|
||||
|
||||
private void saveModelMeta(String relPath,
|
||||
@@ -7044,6 +7140,56 @@ 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.setTitle("LOD " + level + " generieren");
|
||||
dlg.setHeaderText("Methode und Reduktion für LOD " + level + " wählen");
|
||||
|
||||
RadioButton jmeRB = new RadioButton("JME (Progressive Mesh)");
|
||||
RadioButton blightRB = new RadioButton("Blight (Edge Collapse)");
|
||||
RadioButton impostorRB = new RadioButton("Impostor Image (Billboard)");
|
||||
jmeRB.setSelected(true);
|
||||
javafx.scene.control.ToggleGroup tg = new javafx.scene.control.ToggleGroup();
|
||||
jmeRB.setToggleGroup(tg);
|
||||
blightRB.setToggleGroup(tg);
|
||||
impostorRB.setToggleGroup(tg);
|
||||
|
||||
Label pctLabel = new Label("Reduktion (%):");
|
||||
Spinner<Integer> pctSpinner = new Spinner<>(
|
||||
new javafx.scene.control.SpinnerValueFactory.IntegerSpinnerValueFactory(5, 95, defaultPct, 5));
|
||||
pctSpinner.setEditable(true);
|
||||
pctSpinner.setPrefWidth(90);
|
||||
|
||||
impostorRB.selectedProperty().addListener((obs, ov, nv) -> {
|
||||
pctLabel.setDisable(nv);
|
||||
pctSpinner.setDisable(nv);
|
||||
});
|
||||
|
||||
javafx.scene.layout.VBox content = new javafx.scene.layout.VBox(8,
|
||||
jmeRB, blightRB, impostorRB,
|
||||
new Separator(),
|
||||
new HBox(8, pctLabel, pctSpinner));
|
||||
content.setPadding(new javafx.geometry.Insets(12));
|
||||
dlg.getDialogPane().setContent(content);
|
||||
dlg.getDialogPane().getButtonTypes().addAll(
|
||||
javafx.scene.control.ButtonType.OK, javafx.scene.control.ButtonType.CANCEL);
|
||||
|
||||
dlg.setResultConverter(bt -> {
|
||||
if (bt != javafx.scene.control.ButtonType.OK) return null;
|
||||
String method = impostorRB.isSelected() ? "impostor"
|
||||
: blightRB.isSelected() ? "blight_auto"
|
||||
: "jme_auto";
|
||||
return new javafx.util.Pair<>(method, pctSpinner.getValue());
|
||||
});
|
||||
|
||||
dlg.showAndWait().ifPresent(result -> {
|
||||
String method = result.getKey();
|
||||
float reductionPct = result.getValue() / 100f;
|
||||
input.modelLodGenRequest.set(
|
||||
new SharedInput.ModelLodGenRequest(level, method, null, reductionPct));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Zentraler Bereich: JME3-Viewport ────────────────────────────────────
|
||||
|
||||
private StackPane buildViewport() {
|
||||
|
||||
@@ -709,12 +709,16 @@ public class SharedInput {
|
||||
|
||||
/** JME → JFX: true wenn das geladene Modell eingebettete LOD-Kinder hat (kein separater Pfad nötig). */
|
||||
public volatile boolean modelEditorHasEmbeddedLods = false;
|
||||
public volatile boolean modelEditorHasEmbeddedLod1 = false;
|
||||
public volatile boolean modelEditorHasEmbeddedLod2 = false;
|
||||
|
||||
/** JFX → JME: welche LOD-Stufe anzeigen (0=LOD0, 1=LOD1, 2=LOD2). */
|
||||
public volatile int modelEditorLodPreview = 0;
|
||||
public volatile String modelEditorLod1Path = "";
|
||||
public volatile String modelEditorLod2Path = "";
|
||||
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: Richtung der Hauptlichtquelle in der Vorschau.
|
||||
* Azimut 0–360° (Kompassrichtung), Elevation 0–90° (Höhe über Horizont). */
|
||||
@@ -790,9 +794,15 @@ public class SharedInput {
|
||||
* JFX → JME: LOD-Generierungsanfrage.
|
||||
* level: 1 oder 2
|
||||
* method: "auto_mesh" | "file" | "auto_impostor"
|
||||
* fileAssetPath: nur bei method=="file" relevant
|
||||
* fileAssetPath: nur bei method=="file" relevant.
|
||||
* reductionPct: 0..1 (z.B. 0.5 = 50 % der Dreiecke entfernen), ignoriert bei "impostor".
|
||||
*/
|
||||
public record ModelLodGenRequest(int level, String method, String fileAssetPath) {}
|
||||
public record ModelLodGenRequest(int level, String method, String fileAssetPath, float reductionPct) {
|
||||
/** Kompatibler Konstruktor für Code der keine Prozentzahl übergibt (benutzt j3md-Defaults). */
|
||||
public ModelLodGenRequest(int level, String method, String fileAssetPath) {
|
||||
this(level, method, fileAssetPath, -1f);
|
||||
}
|
||||
}
|
||||
public final AtomicReference<ModelLodGenRequest> modelLodGenRequest =
|
||||
new AtomicReference<>();
|
||||
|
||||
@@ -821,6 +831,8 @@ public class SharedInput {
|
||||
/** JME → JFX: LOD-Stufen vorhanden. */
|
||||
public volatile boolean modelImportHasLod1 = false;
|
||||
public volatile boolean modelImportHasLod2 = false;
|
||||
/** JFX → JME: Generierten Import-LOD löschen (1=lod1, 2=lod2, -1=kein Auftrag). */
|
||||
public volatile int modelImportClearLodLevel = -1;
|
||||
|
||||
/** JFX → JME: Dateiname (ohne Erweiterung) für den Export-Auftrag. null = kein Auftrag. */
|
||||
public volatile String modelImportExportName = null;
|
||||
|
||||
@@ -8,16 +8,11 @@ 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.light.AmbientLight;
|
||||
import com.jme3.light.DirectionalLight;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.post.SceneProcessor;
|
||||
import com.jme3.profile.AppProfiler;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.renderer.RenderManager;
|
||||
import com.jme3.renderer.ViewPort;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.Geometry;
|
||||
@@ -31,6 +26,7 @@ import com.jme3.texture.Texture;
|
||||
import com.jme3.texture.Texture2D;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.editor.SharedInput;
|
||||
import de.blight.editor.util.ImpostorUtil;
|
||||
import de.blight.eztree.Tree;
|
||||
import de.blight.eztree.TreeOptions;
|
||||
import org.slf4j.Logger;
|
||||
@@ -59,10 +55,6 @@ public class EzTreeState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(EzTreeState.class);
|
||||
|
||||
private static final int IMPOSTOR_SIZE = 512;
|
||||
private static final int ATLAS_DIRS = 4;
|
||||
private static final int ATLAS_W = IMPOSTOR_SIZE * ATLAS_DIRS;
|
||||
private static final int ATLAS_H = IMPOSTOR_SIZE;
|
||||
private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("blight-assets", "src", "main", "resources");
|
||||
private static final Path TOOLS_DIR = de.blight.editor.ProjectRoot.resolve("tools");
|
||||
private static final Gson GSON = new Gson();
|
||||
@@ -79,9 +71,9 @@ public class EzTreeState extends BaseAppState {
|
||||
private BoundingBox pendingBb = null;
|
||||
private ViewPort captureVP = null;
|
||||
private FrameBuffer captureFB = null;
|
||||
private volatile boolean captureReady = false;
|
||||
private boolean[] captureReady = new boolean[]{false};
|
||||
private int capturePass = 0;
|
||||
private ByteBuffer[] capturePixels = new ByteBuffer[ATLAS_DIRS];
|
||||
private ByteBuffer[] capturePixels = new ByteBuffer[ImpostorUtil.DIRS];
|
||||
|
||||
public EzTreeState(SharedInput input) { this.input = input; }
|
||||
|
||||
@@ -106,7 +98,7 @@ public class EzTreeState extends BaseAppState {
|
||||
if (previewHost == null) return;
|
||||
}
|
||||
|
||||
if (pendingRequest != null && captureReady) {
|
||||
if (pendingRequest != null && captureReady[0]) {
|
||||
finishCapture();
|
||||
} else if (pendingRequest == null) {
|
||||
SharedInput.EzTreeGenRequest req = input.ezTreeGenQueue.poll();
|
||||
@@ -150,9 +142,9 @@ public class EzTreeState extends BaseAppState {
|
||||
pendingLd1Node = finalLd1;
|
||||
pendingBb = finalBb;
|
||||
capturePass = 0;
|
||||
capturePixels = new ByteBuffer[ATLAS_DIRS];
|
||||
startCapturePass(0, finalHd, finalBb);
|
||||
input.treeGenStatusMsg = "EZ-Tree: rendere Impostor (1/" + ATLAS_DIRS + ")…";
|
||||
capturePixels = new ByteBuffer[ImpostorUtil.DIRS];
|
||||
startCapturePass(0);
|
||||
input.treeGenStatusMsg = "EZ-Tree: rendere Impostor (1/" + ImpostorUtil.DIRS + ")…";
|
||||
}
|
||||
});
|
||||
|
||||
@@ -384,27 +376,29 @@ public class EzTreeState extends BaseAppState {
|
||||
// ── Phase 2: Impostor-Capture (4-Richtungen) ─────────────────────────────
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void startCapturePass(int pass, Node treeNode, BoundingBox bb) {
|
||||
BoundingBox safeBb = bb != null ? bb : new BoundingBox(Vector3f.ZERO, 5f, 10f, 5f);
|
||||
Texture2D capTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8);
|
||||
captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1);
|
||||
private void startCapturePass(int pass) {
|
||||
BoundingBox safeBb = pendingBb != null ? pendingBb : new BoundingBox(Vector3f.ZERO, 5f, 10f, 5f);
|
||||
com.jme3.texture.Texture2D capTex = new com.jme3.texture.Texture2D(
|
||||
ImpostorUtil.SIZE, ImpostorUtil.SIZE, Image.Format.RGBA8);
|
||||
captureFB = new FrameBuffer(ImpostorUtil.SIZE, ImpostorUtil.SIZE, 1);
|
||||
captureFB.addColorTexture(capTex);
|
||||
captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth));
|
||||
float angle = pass * com.jme3.math.FastMath.HALF_PI;
|
||||
captureVP = buildCaptureViewPort(treeNode, safeBb, captureFB, angle);
|
||||
captureReady = false;
|
||||
captureFB.setDepthTexture(new com.jme3.texture.Texture2D(
|
||||
ImpostorUtil.SIZE, ImpostorUtil.SIZE, Image.Format.Depth));
|
||||
captureReady[0] = false;
|
||||
captureVP = ImpostorUtil.startCapturePass(app, pendingHdNode, safeBb, captureFB, pass, captureReady);
|
||||
}
|
||||
|
||||
private void finishCapture() {
|
||||
ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4);
|
||||
app.getRenderer().readFrameBuffer(captureFB, pixels);
|
||||
capturePixels[capturePass] = pixels;
|
||||
cleanupCapture();
|
||||
capturePixels[capturePass] = ImpostorUtil.readPixels(app, captureFB);
|
||||
ImpostorUtil.cleanup(app, captureVP, captureFB);
|
||||
captureVP = null;
|
||||
captureFB = null;
|
||||
captureReady[0] = false;
|
||||
|
||||
if (capturePass < ATLAS_DIRS - 1) {
|
||||
if (capturePass < ImpostorUtil.DIRS - 1) {
|
||||
capturePass++;
|
||||
input.treeGenStatusMsg = "EZ-Tree: rendere Impostor (" + (capturePass + 1) + "/" + ATLAS_DIRS + ")…";
|
||||
startCapturePass(capturePass, pendingHdNode, pendingBb);
|
||||
input.treeGenStatusMsg = "EZ-Tree: rendere Impostor (" + (capturePass + 1) + "/" + ImpostorUtil.DIRS + ")…";
|
||||
startCapturePass(capturePass);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -418,7 +412,7 @@ public class EzTreeState extends BaseAppState {
|
||||
pendingHdNode = null;
|
||||
pendingLd1Node = null;
|
||||
pendingBb = null;
|
||||
capturePixels = new ByteBuffer[ATLAS_DIRS];
|
||||
capturePixels = new ByteBuffer[ImpostorUtil.DIRS];
|
||||
|
||||
String subPath = resolveSubPath(req.presetName());
|
||||
String namePart = req.presetName() != null
|
||||
@@ -427,8 +421,8 @@ public class EzTreeState extends BaseAppState {
|
||||
String timestamp = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
|
||||
String exportName = namePart + "_" + timestamp;
|
||||
|
||||
ByteBuffer atlas = combineAtlas(savedPixels);
|
||||
Texture2D atlasT2d = saveImpostor(atlas, "ez_impostor_" + exportName, ATLAS_W, ATLAS_H);
|
||||
ByteBuffer atlas = ImpostorUtil.combineAtlas(savedPixels);
|
||||
com.jme3.texture.Texture2D atlasT2d = ImpostorUtil.buildAtlasTexture(atlas);
|
||||
|
||||
Node lodRoot = assembleLodNode(req.presetName(), hdNode, ld1Node, bb, atlasT2d);
|
||||
exportTree(lodRoot, exportName, subPath);
|
||||
@@ -503,80 +497,6 @@ public class EzTreeState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Offscreen-Viewport für Impostor ───────────────────────────────────────
|
||||
|
||||
private ViewPort buildCaptureViewPort(Node src, BoundingBox bb, FrameBuffer fb, float angle) {
|
||||
Vector3f center = bb.getCenter();
|
||||
float yExt = bb.getYExtent();
|
||||
float hExt = Math.max(bb.getXExtent(), bb.getZExtent());
|
||||
float extent = Math.max(yExt, hExt);
|
||||
float dist = extent * 3.5f;
|
||||
|
||||
float camX = com.jme3.math.FastMath.sin(angle) * dist;
|
||||
float camZ = com.jme3.math.FastMath.cos(angle) * dist;
|
||||
Camera cam = new Camera(IMPOSTOR_SIZE, IMPOSTOR_SIZE);
|
||||
cam.setLocation(center.add(camX, 0f, camZ));
|
||||
cam.lookAt(center, Vector3f.UNIT_Y);
|
||||
// FOV groß genug, um den vollen Baum (+ 15 % Rand) zu erfassen
|
||||
float fovY = (float) Math.toDegrees(2.0 * Math.atan2(yExt * 1.15, dist));
|
||||
float fovH = (float) Math.toDegrees(2.0 * Math.atan2(hExt * 1.15, dist));
|
||||
cam.setFrustumPerspective(Math.max(fovY, fovH), 1f, 0.1f, dist * 4f);
|
||||
|
||||
ViewPort vp = app.getRenderManager()
|
||||
.createPostView("ezCapture_" + System.nanoTime(), cam);
|
||||
vp.setOutputFrameBuffer(fb);
|
||||
vp.setBackgroundColor(new ColorRGBA(0f, 0f, 0f, 0f));
|
||||
vp.setClearFlags(true, true, true);
|
||||
|
||||
Node scene = new Node("ezCapScene");
|
||||
scene.addLight(new DirectionalLight(
|
||||
new Vector3f(-0.4f, -1f, -0.5f).normalizeLocal(), new ColorRGBA(2.0f, 1.85f, 1.5f, 1f)));
|
||||
scene.addLight(new AmbientLight(new ColorRGBA(0.60f, 0.60f, 0.60f, 1f)));
|
||||
scene.attachChild(cloneForCapture(src));
|
||||
vp.attachScene(scene);
|
||||
scene.updateGeometricState();
|
||||
|
||||
vp.addProcessor(new SceneProcessor() {
|
||||
@Override public void initialize(RenderManager rm, ViewPort v) {}
|
||||
@Override public void reshape(ViewPort v, int w, int h) {}
|
||||
@Override public boolean isInitialized() { return true; }
|
||||
@Override public void preFrame(float t) {}
|
||||
@Override public void postQueue(RenderQueue rq) {}
|
||||
@Override public void cleanup() {}
|
||||
@Override public void setProfiler(AppProfiler p) {}
|
||||
@Override
|
||||
public void postFrame(FrameBuffer out) {
|
||||
vp.removeProcessor(this);
|
||||
captureReady = true;
|
||||
}
|
||||
});
|
||||
return vp;
|
||||
}
|
||||
|
||||
private static Node cloneForCapture(Node src) {
|
||||
Node copy = new Node("ezCap");
|
||||
copy.setLocalTranslation(src.getLocalTranslation());
|
||||
copy.setLocalScale(src.getLocalScale());
|
||||
for (Spatial child : src.getChildren()) {
|
||||
if (child instanceof Geometry g) {
|
||||
Geometry gc = new Geometry(g.getName(), g.getMesh());
|
||||
gc.setMaterial(g.getMaterial().clone());
|
||||
copy.attachChild(gc);
|
||||
} else if (child instanceof Node n) {
|
||||
Node nc = new Node(n.getName());
|
||||
for (Spatial ns : n.getChildren()) {
|
||||
if (ns instanceof Geometry ng) {
|
||||
Geometry ngc = new Geometry(ng.getName(), ng.getMesh());
|
||||
ngc.setMaterial(ng.getMaterial().clone());
|
||||
nc.attachChild(ngc);
|
||||
}
|
||||
}
|
||||
copy.attachChild(nc);
|
||||
}
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
private static BoundingBox boundsOf(Node node) {
|
||||
@@ -586,48 +506,17 @@ public class EzTreeState extends BaseAppState {
|
||||
}
|
||||
|
||||
private void cleanupCapture() {
|
||||
if (captureVP != null) {
|
||||
app.getRenderManager().removePostView(captureVP);
|
||||
captureVP = null;
|
||||
}
|
||||
if (captureFB != null) {
|
||||
try { captureFB.dispose(); } catch (Exception ignored) {}
|
||||
captureFB = null;
|
||||
}
|
||||
captureReady = false;
|
||||
ImpostorUtil.cleanup(app, captureVP, captureFB);
|
||||
captureVP = null;
|
||||
captureFB = null;
|
||||
captureReady[0] = false;
|
||||
}
|
||||
|
||||
private Texture2D saveImpostor(ByteBuffer pixels, String name, int width, int height) {
|
||||
pixels.rewind();
|
||||
Image jmeImg = new Image(Image.Format.RGBA8, width, height, pixels, null,
|
||||
com.jme3.texture.image.ColorSpace.sRGB);
|
||||
return new Texture2D(jmeImg);
|
||||
}
|
||||
|
||||
private ByteBuffer combineAtlas(ByteBuffer[] passes) {
|
||||
ByteBuffer atlas = BufferUtils.createByteBuffer(ATLAS_W * ATLAS_H * 4);
|
||||
for (int d = 0; d < ATLAS_DIRS; d++) {
|
||||
ByteBuffer src = passes[d];
|
||||
src.rewind();
|
||||
for (int y = 0; y < IMPOSTOR_SIZE; y++) {
|
||||
for (int x = 0; x < IMPOSTOR_SIZE; x++) {
|
||||
int srcOff = (y * IMPOSTOR_SIZE + x) * 4;
|
||||
int dstOff = (y * ATLAS_W + d * IMPOSTOR_SIZE + x) * 4;
|
||||
atlas.put(dstOff, src.get(srcOff));
|
||||
atlas.put(dstOff + 1, src.get(srcOff + 1));
|
||||
atlas.put(dstOff + 2, src.get(srcOff + 2));
|
||||
atlas.put(dstOff + 3, src.get(srcOff + 3));
|
||||
}
|
||||
}
|
||||
}
|
||||
return atlas;
|
||||
}
|
||||
|
||||
private Node assembleLodNode(String name, Node hd, Node ld1, BoundingBox bb, Texture2D atlasTex) {
|
||||
private Node assembleLodNode(String name, Node hd, Node ld1, BoundingBox bb, com.jme3.texture.Texture2D atlasTex) {
|
||||
Node root = new Node(name != null ? name : "ez_tree");
|
||||
root.attachChild(hd);
|
||||
root.attachChild(ld1);
|
||||
Node lod2 = makeImpostorNode(bb, atlasTex);
|
||||
Node lod2 = ImpostorUtil.makeImpostorNode(bb, atlasTex, assets);
|
||||
root.attachChild(lod2);
|
||||
|
||||
hd.setCullHint(Spatial.CullHint.Inherit);
|
||||
@@ -635,56 +524,11 @@ public class EzTreeState extends BaseAppState {
|
||||
lod2.setCullHint(Spatial.CullHint.Always);
|
||||
lod2.setShadowMode(RenderQueue.ShadowMode.Off);
|
||||
|
||||
ImpostorUtil.addViewControl(lod2, app.getCamera());
|
||||
root.addControl(new EzTreeLodControl(app.getCamera(), hd, ld1, lod2, 40f, 120f));
|
||||
return root;
|
||||
}
|
||||
|
||||
private Node makeImpostorNode(BoundingBox bb, Texture2D tex) {
|
||||
float h = bb != null ? bb.getYExtent() * 2f : 10f;
|
||||
float w = bb != null ? Math.max(bb.getXExtent(), bb.getZExtent()) * 2f : 4f;
|
||||
float size = Math.max(h, w);
|
||||
float yOff = bb != null ? bb.getCenter().y : 5f;
|
||||
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
if (tex != null) mat.setTexture("ColorMap", tex);
|
||||
else mat.setColor("Color", new ColorRGBA(0.18f, 0.5f, 0.1f, 0.9f));
|
||||
mat.getAdditionalRenderState().setBlendMode(com.jme3.material.RenderState.BlendMode.Alpha);
|
||||
mat.getAdditionalRenderState().setFaceCullMode(com.jme3.material.RenderState.FaceCullMode.Off);
|
||||
|
||||
Node n = new Node("lod2");
|
||||
for (int d = 0; d < ATLAS_DIRS; d++) {
|
||||
float angle = d * com.jme3.math.FastMath.HALF_PI;
|
||||
float uMin = (float) d / ATLAS_DIRS;
|
||||
float uMax = (float)(d + 1) / ATLAS_DIRS;
|
||||
n.attachChild(buildBillboardQuad("quad_" + d, angle, yOff, size, mat.clone(), uMin, uMax));
|
||||
}
|
||||
n.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
return n;
|
||||
}
|
||||
|
||||
private Geometry buildBillboardQuad(String name, float yRot, float yCent,
|
||||
float size, Material mat, float uMin, float uMax) {
|
||||
float hw = size * 0.5f;
|
||||
float hh = size * 0.5f;
|
||||
float cos = com.jme3.math.FastMath.cos(yRot);
|
||||
float sin = com.jme3.math.FastMath.sin(yRot);
|
||||
Mesh mesh = new Mesh();
|
||||
mesh.setBuffer(VertexBuffer.Type.Position, 3, new float[]{
|
||||
-hw*cos, yCent-hh, -hw*sin,
|
||||
hw*cos, yCent-hh, hw*sin,
|
||||
hw*cos, yCent+hh, hw*sin,
|
||||
-hw*cos, yCent+hh, -hw*sin
|
||||
});
|
||||
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, new float[]{
|
||||
uMin, 0f, uMax, 0f, uMax, 1f, uMin, 1f
|
||||
});
|
||||
mesh.setBuffer(VertexBuffer.Type.Index, 3, new int[]{0,1,2, 0,2,3, 2,1,0, 3,2,0});
|
||||
mesh.updateBound();
|
||||
Geometry g = new Geometry(name, mesh);
|
||||
g.setMaterial(mat);
|
||||
return g;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt Vertex-Farben (R = Wind-Gewicht) für alle Geometrien eines Baum-Knotens.
|
||||
* Blätter bekommen Gewicht 1.0 (volle Animation); Äste werden per Y-Position
|
||||
@@ -778,10 +622,17 @@ public class EzTreeState extends BaseAppState {
|
||||
|
||||
@Override
|
||||
protected void controlUpdate(float tpf) {
|
||||
float dSq = cam.getLocation().distanceSquared(spatial.getWorldTranslation());
|
||||
lod0.setCullHint(dSq < d01sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
lod1.setCullHint(dSq >= d01sq && dSq < d12sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
lod2.setCullHint(dSq >= d12sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
float dSq = cam.getLocation().distanceSquared(spatial.getWorldTranslation());
|
||||
boolean atL2 = dSq >= d12sq;
|
||||
setLod(lod0, dSq < d01sq, !atL2);
|
||||
setLod(lod1, dSq >= d01sq && !atL2, !atL2);
|
||||
setLod(lod2, atL2, false);
|
||||
}
|
||||
|
||||
private static void setLod(Node node, boolean visible, boolean castShadow) {
|
||||
node.setCullHint(visible ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
node.setShadowMode(visible && castShadow
|
||||
? RenderQueue.ShadowMode.CastAndReceive : RenderQueue.ShadowMode.Off);
|
||||
}
|
||||
|
||||
@Override protected void controlRender(com.jme3.renderer.RenderManager rm,
|
||||
|
||||
@@ -13,6 +13,9 @@ import com.jme3.light.PointLight;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.math.*;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.renderer.RenderManager;
|
||||
import com.jme3.renderer.ViewPort;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.*;
|
||||
import com.jme3.collision.CollisionResults;
|
||||
import com.jme3.math.Ray;
|
||||
@@ -23,6 +26,7 @@ import com.jme3.scene.shape.Sphere;
|
||||
import com.jme3.anim.SkinningControl;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.editor.SharedInput;
|
||||
import de.blight.editor.util.ImpostorUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -73,6 +77,8 @@ public class ModelEditorState extends BaseAppState {
|
||||
|
||||
/** Hauptlichtquelle der Vorschau (per UI steuerbar). */
|
||||
private DirectionalLight mainLight = null;
|
||||
/** Kamera-folgendes Füll-Licht – immer aus Sicht der Kamera. */
|
||||
private DirectionalLight camLight = null;
|
||||
|
||||
// gespeicherter Kamerazustand aus dem Editor-Modus
|
||||
private Vector3f savedCamPos;
|
||||
@@ -98,6 +104,11 @@ public class ModelEditorState extends BaseAppState {
|
||||
private boolean hasEmbeddedLods = false;
|
||||
private Spatial[] embeddedLodSpatials = null;
|
||||
|
||||
// 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
|
||||
private final java.util.List<Material> pbrMaterials = new java.util.ArrayList<>();
|
||||
|
||||
// Mittelpunkt für Orbit (Bounding-Box-Zentrum des Modells)
|
||||
private Vector3f orbitCenter = Vector3f.ZERO.clone();
|
||||
|
||||
@@ -134,6 +145,9 @@ public class ModelEditorState extends BaseAppState {
|
||||
return;
|
||||
}
|
||||
|
||||
// Welt sofort ausblenden, auch wenn noch kein Modell geladen ist
|
||||
if (previewRoot == null) enterPreview();
|
||||
|
||||
// Modell laden
|
||||
String openPath = input.modelEditorOpenPath;
|
||||
if (openPath != null) {
|
||||
@@ -145,37 +159,54 @@ public class ModelEditorState extends BaseAppState {
|
||||
loadModel(openPath);
|
||||
}
|
||||
|
||||
int removeLod = input.modelEditorRemoveEmbeddedLod;
|
||||
if (removeLod > 0) {
|
||||
input.modelEditorRemoveEmbeddedLod = -1;
|
||||
if (hasEmbeddedLods && embeddedLodSpatials != null) {
|
||||
removeEmbeddedLod(removeLod);
|
||||
}
|
||||
}
|
||||
|
||||
if (input.modelEditorLodChanged) {
|
||||
input.modelEditorLodChanged = false;
|
||||
if (hasEmbeddedLods && embeddedLodSpatials != null) {
|
||||
|
||||
// Generierte In-Memory-Previews haben immer Vorrang (auch über eingebettete LODs)
|
||||
Spatial genPreview = null;
|
||||
if (input.modelEditorLodPreview == 1)
|
||||
genPreview = input.modelImportLod1PreviewNode.get();
|
||||
else if (input.modelEditorLodPreview == 2)
|
||||
genPreview = input.modelImportLod2PreviewNode.get();
|
||||
|
||||
if (genPreview != null) {
|
||||
if (hasEmbeddedLods && embeddedLodSpatials != null) {
|
||||
// Eingebettete LODs alle ausblenden, damit das generierte Preview sichtbar ist
|
||||
for (Spatial s : embeddedLodSpatials) {
|
||||
if (s != null) s.setCullHint(Spatial.CullHint.Always);
|
||||
}
|
||||
}
|
||||
showSpatialDirectly(genPreview.clone());
|
||||
} else if (hasEmbeddedLods && embeddedLodSpatials != null) {
|
||||
// Eingebettete LOD-Kinder direkt ein-/ausblenden
|
||||
int lodIdx = Math.min(input.modelEditorLodPreview, embeddedLodSpatials.length - 1);
|
||||
for (int i = 0; i < embeddedLodSpatials.length; i++) {
|
||||
embeddedLodSpatials[i].setCullHint(
|
||||
i == lodIdx ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
if (embeddedLodSpatials[i] != null) {
|
||||
boolean show = (i == lodIdx);
|
||||
embeddedLodSpatials[i].setCullHint(show ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
embeddedLodSpatials[i].setShadowMode(RenderQueue.ShadowMode.Off);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// In-memory Preview-Node aus Importer hat Vorrang vor Dateipfaden
|
||||
Spatial previewNode = null;
|
||||
if (input.modelEditorLodPreview == 1)
|
||||
previewNode = input.modelImportLod1PreviewNode.get();
|
||||
else if (input.modelEditorLodPreview == 2)
|
||||
previewNode = input.modelImportLod2PreviewNode.get();
|
||||
|
||||
if (previewNode != null) {
|
||||
showSpatialDirectly(previewNode.clone());
|
||||
} else {
|
||||
String path = switch (input.modelEditorLodPreview) {
|
||||
case 1 -> input.modelEditorLod1Path;
|
||||
case 2 -> input.modelEditorLod2Path;
|
||||
default -> mainModelPath;
|
||||
};
|
||||
if (path == null || path.isBlank()) {
|
||||
path = mainModelPath;
|
||||
input.modelEditorLodPreview = 0;
|
||||
}
|
||||
if (path != null) loadModel(path);
|
||||
// Pfad-basierter Fallback
|
||||
String path = switch (input.modelEditorLodPreview) {
|
||||
case 1 -> input.modelEditorLod1Path;
|
||||
case 2 -> input.modelEditorLod2Path;
|
||||
default -> mainModelPath;
|
||||
};
|
||||
if (path == null || path.isBlank()) {
|
||||
path = mainModelPath;
|
||||
input.modelEditorLodPreview = 0;
|
||||
}
|
||||
if (path != null) loadModel(path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,6 +336,15 @@ public class ModelEditorState extends BaseAppState {
|
||||
}
|
||||
|
||||
applyOrbitCamera();
|
||||
if (camLight != null) {
|
||||
Vector3f camDir = cam.getDirection();
|
||||
camLight.setDirection(camDir);
|
||||
// Tree.j3md erwartet Richtung VON Oberfläche ZUR Lichtquelle = –camDir
|
||||
Vector3f treeLightDir = camDir.negate();
|
||||
for (Material m : treeLightMaterials) {
|
||||
m.setVector3("LightDir", treeLightDir);
|
||||
}
|
||||
}
|
||||
|
||||
previewRoot.updateLogicalState(tpf);
|
||||
previewRoot.updateGeometricState();
|
||||
@@ -327,6 +367,8 @@ public class ModelEditorState extends BaseAppState {
|
||||
hasEmbeddedLods = false;
|
||||
embeddedLodSpatials = null;
|
||||
input.modelEditorHasEmbeddedLods = false;
|
||||
input.modelEditorHasEmbeddedLod1 = false;
|
||||
input.modelEditorHasEmbeddedLod2 = false;
|
||||
try {
|
||||
// Cache vor dem Laden löschen – verhindert, dass zwischengespeicherte
|
||||
// Materialzustände (z.B. aus vorherigen Thumbnail-Renders) das erste
|
||||
@@ -335,6 +377,7 @@ public class ModelEditorState extends BaseAppState {
|
||||
catch (Exception ignored) {}
|
||||
Spatial model = app.getAssetManager().loadModel(assetPath);
|
||||
stripControls(model);
|
||||
initImpostorControls(model);
|
||||
recalcNormalsIfMissing(model);
|
||||
markBuffersUpdateNeeded(model);
|
||||
originalSpatial = model;
|
||||
@@ -350,6 +393,8 @@ public class ModelEditorState extends BaseAppState {
|
||||
embeddedLodSpatials = children.toArray(new Spatial[0]);
|
||||
hasEmbeddedLods = true;
|
||||
input.modelEditorHasEmbeddedLods = true;
|
||||
input.modelEditorHasEmbeddedLod1 = embeddedLodSpatials.length > 1;
|
||||
input.modelEditorHasEmbeddedLod2 = embeddedLodSpatials.length > 2;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -375,6 +420,22 @@ public class ModelEditorState extends BaseAppState {
|
||||
input.modelEditorBakedScaleDetected = true;
|
||||
}
|
||||
|
||||
// Materialien klassifizieren und sofort für den Editor initialisieren
|
||||
treeLightMaterials.clear();
|
||||
pbrMaterials.clear();
|
||||
collectEditorMaterials(originalSpatial);
|
||||
Vector3f initialLightDir = cam.getDirection().negate();
|
||||
for (Material m : treeLightMaterials) {
|
||||
m.setVector3("LightDir", initialLightDir);
|
||||
m.setVector3("SunColor", new Vector3f(1.1f, 1.0f, 0.9f));
|
||||
m.setVector3("AmbientColor", new Vector3f(0.30f, 0.30f, 0.32f));
|
||||
}
|
||||
for (Material m : pbrMaterials) {
|
||||
m.setFloat("EmissivePower", 1f);
|
||||
m.setFloat("EmissiveIntensity", 1f);
|
||||
m.setColor("Emissive", new ColorRGBA(0.25f, 0.25f, 0.28f, 1f));
|
||||
}
|
||||
|
||||
// Skalierung aus SharedInput anwenden
|
||||
applyScale(input.modelEditorScaleX, input.modelEditorScaleY, input.modelEditorScaleZ);
|
||||
applyPivot(input.modelEditorPivotY);
|
||||
@@ -391,6 +452,18 @@ public class ModelEditorState extends BaseAppState {
|
||||
orbitCenter = bb.getCenter().clone();
|
||||
}
|
||||
|
||||
// Kamera sofort positionieren und Baum-LightDir initialisieren –
|
||||
// damit der allererste Render bereits korrekte Beleuchtung zeigt.
|
||||
applyOrbitCamera();
|
||||
if (camLight != null) {
|
||||
Vector3f camDir = cam.getDirection();
|
||||
camLight.setDirection(camDir);
|
||||
Vector3f treeLightDir = camDir.negate();
|
||||
for (Material m : treeLightMaterials) {
|
||||
m.setVector3("LightDir", treeLightDir);
|
||||
}
|
||||
}
|
||||
|
||||
// Vergleichszylinder neu positionieren
|
||||
repositionCompCylinder();
|
||||
|
||||
@@ -409,6 +482,21 @@ public class ModelEditorState extends BaseAppState {
|
||||
if (modelWrapper != null) previewRoot.detachChild(modelWrapper);
|
||||
modelWrapper = new Node("model_wrapper");
|
||||
stripControls(spatial);
|
||||
treeLightMaterials.clear();
|
||||
pbrMaterials.clear();
|
||||
collectEditorMaterials(spatial);
|
||||
Vector3f lodLightDir = cam.getDirection().negate();
|
||||
for (Material m : treeLightMaterials) {
|
||||
m.setVector3("LightDir", lodLightDir);
|
||||
m.setVector3("SunColor", new Vector3f(1.1f, 1.0f, 0.9f));
|
||||
m.setVector3("AmbientColor", new Vector3f(0.30f, 0.30f, 0.32f));
|
||||
}
|
||||
for (Material m : pbrMaterials) {
|
||||
m.setFloat("EmissivePower", 1f);
|
||||
m.setFloat("EmissiveIntensity", 1f);
|
||||
m.setColor("Emissive", new ColorRGBA(0.25f, 0.25f, 0.28f, 1f));
|
||||
}
|
||||
spatial.setShadowMode(RenderQueue.ShadowMode.Off);
|
||||
modelWrapper.attachChild(spatial);
|
||||
applyScale(input.modelEditorScaleX, input.modelEditorScaleY, input.modelEditorScaleZ);
|
||||
applyPivot(input.modelEditorPivotY);
|
||||
@@ -469,6 +557,8 @@ public class ModelEditorState extends BaseAppState {
|
||||
computeLightDirection(input.modelEditorLightAzimuth, input.modelEditorLightElevation),
|
||||
new ColorRGBA(1.8f, 1.7f, 1.4f, 1f));
|
||||
previewRoot.addLight(mainLight);
|
||||
camLight = new DirectionalLight(Vector3f.UNIT_Z.negate(), new ColorRGBA(1.4f, 1.3f, 1.1f, 1f));
|
||||
previewRoot.addLight(camLight);
|
||||
previewRoot.addLight(new DirectionalLight(
|
||||
new Vector3f(0.6f, -0.4f, 0.7f).normalizeLocal(),
|
||||
new ColorRGBA(0.5f, 0.55f, 0.75f, 1f)));
|
||||
@@ -479,6 +569,10 @@ public class ModelEditorState extends BaseAppState {
|
||||
|
||||
app.getViewPort().setBackgroundColor(new ColorRGBA(0.15f, 0.15f, 0.18f, 1f));
|
||||
|
||||
// Sofort am Viewport registrieren – auch ohne Modell sichtbar (leerer, dunkler Viewport)
|
||||
app.getViewPort().attachScene(previewRoot);
|
||||
previewSceneAttached = true;
|
||||
|
||||
interactableArrowNode = new Node("interactableArrow");
|
||||
interactableArrowNode.setCullHint(Spatial.CullHint.Always);
|
||||
previewRoot.attachChild(interactableArrowNode);
|
||||
@@ -510,7 +604,13 @@ public class ModelEditorState extends BaseAppState {
|
||||
embeddedLodSpatials = null;
|
||||
originalSpatial = null;
|
||||
interactableArrowNode = null;
|
||||
camLight = null;
|
||||
mainLight = null;
|
||||
treeLightMaterials.clear();
|
||||
pbrMaterials.clear();
|
||||
input.modelEditorHasEmbeddedLods = false;
|
||||
input.modelEditorHasEmbeddedLod1 = false;
|
||||
input.modelEditorHasEmbeddedLod2 = false;
|
||||
app.getRootNode().setCullHint(Spatial.CullHint.Inherit);
|
||||
|
||||
if (savedCamPos != null) {
|
||||
@@ -521,6 +621,65 @@ public class ModelEditorState extends BaseAppState {
|
||||
currentPath = null;
|
||||
}
|
||||
|
||||
// ── Eingebettetes LOD entfernen ───────────────────────────────────────────
|
||||
|
||||
private void removeEmbeddedLod(int lodIdx) {
|
||||
if (lodIdx < 0 || lodIdx >= embeddedLodSpatials.length) return;
|
||||
Spatial toRemove = embeddedLodSpatials[lodIdx];
|
||||
if (toRemove == null) return;
|
||||
|
||||
if (toRemove.getParent() != null) toRemove.getParent().detachChild(toRemove);
|
||||
embeddedLodSpatials[lodIdx] = null;
|
||||
if (lodIdx == 1) input.modelEditorHasEmbeddedLod1 = false;
|
||||
else if (lodIdx == 2) input.modelEditorHasEmbeddedLod2 = false;
|
||||
|
||||
if (input.modelEditorLodPreview == lodIdx) {
|
||||
input.modelEditorLodPreview = 0;
|
||||
if (embeddedLodSpatials[0] != null)
|
||||
embeddedLodSpatials[0].setCullHint(Spatial.CullHint.Inherit);
|
||||
}
|
||||
|
||||
boolean anyLodLeft = false;
|
||||
for (int i = 1; i < embeddedLodSpatials.length; i++) {
|
||||
if (embeddedLodSpatials[i] != null) { anyLodLeft = true; break; }
|
||||
}
|
||||
if (!anyLodLeft) {
|
||||
hasEmbeddedLods = false;
|
||||
embeddedLodSpatials = null;
|
||||
input.modelEditorHasEmbeddedLods = false;
|
||||
}
|
||||
|
||||
if (currentPath != null && !modelWrapper.getChildren().isEmpty()) {
|
||||
try {
|
||||
java.nio.file.Path absPath = ASSET_ROOT.resolve(currentPath);
|
||||
Spatial root = modelWrapper.getChild(0);
|
||||
BinaryExporter.getInstance().save(root, absPath.toFile());
|
||||
app.getAssetManager().clearCache();
|
||||
log.info("[ModelEditor] LOD {} entfernt, Datei gespeichert: {}", lodIdx, currentPath);
|
||||
} catch (Exception ex) {
|
||||
log.error("[ModelEditor] Speichern nach LOD-Entfernung fehlgeschlagen: {}", ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void collectEditorMaterials(Spatial s) {
|
||||
if (s == null) return;
|
||||
if (s instanceof Geometry g) {
|
||||
Material mat = g.getMaterial();
|
||||
if (mat == null) return;
|
||||
String defName = mat.getMaterialDef().getName();
|
||||
if ("Tree".equals(defName) || "TreeLeaf".equals(defName)) {
|
||||
treeLightMaterials.add(mat);
|
||||
} else if ("PBRLighting".equals(defName)) {
|
||||
pbrMaterials.add(mat);
|
||||
}
|
||||
} else if (s instanceof Node n) {
|
||||
for (Spatial child : n.getChildren()) {
|
||||
collectEditorMaterials(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Gitter ────────────────────────────────────────────────────────────────
|
||||
|
||||
private void rebuildGrid() {
|
||||
@@ -732,10 +891,23 @@ public class ModelEditorState extends BaseAppState {
|
||||
}
|
||||
|
||||
private static void stripControls(Spatial s) {
|
||||
while (s.getNumControls() > 0) s.removeControl(s.getControl(0));
|
||||
int i = 0;
|
||||
while (i < s.getNumControls()) {
|
||||
com.jme3.scene.control.Control c = s.getControl(i);
|
||||
if (c instanceof ImpostorUtil.ImpostorViewControl) { i++; }
|
||||
else { s.removeControl(c); }
|
||||
}
|
||||
if (s instanceof Node n) n.getChildren().forEach(ModelEditorState::stripControls);
|
||||
}
|
||||
|
||||
private void initImpostorControls(Spatial s) {
|
||||
ImpostorUtil.ImpostorViewControl ctrl = s.getControl(ImpostorUtil.ImpostorViewControl.class);
|
||||
if (ctrl != null) ctrl.setCamera(cam);
|
||||
if (s instanceof Node n) {
|
||||
for (Spatial child : n.getChildren()) initImpostorControls(child);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet Smooth-Normals immer neu aus der Geometrie.
|
||||
* Behebt TripoAI-Modelle mit fehlenden, null oder nach innen zeigenden Normals.
|
||||
|
||||
@@ -12,11 +12,15 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.export.binary.BinaryExporter;
|
||||
import com.jme3.bounding.BoundingBox;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.math.*;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.renderer.RenderManager;
|
||||
@@ -25,9 +29,13 @@ import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.*;
|
||||
import com.jme3.scene.VertexBuffer;
|
||||
import com.jme3.scene.control.AbstractControl;
|
||||
import com.jme3.texture.FrameBuffer;
|
||||
import com.jme3.texture.Image;
|
||||
import com.jme3.texture.Texture2D;
|
||||
import com.jme3.util.BufferUtils;
|
||||
|
||||
import de.blight.editor.SharedInput;
|
||||
import de.blight.editor.util.ImpostorUtil;
|
||||
import de.blight.editor.util.MeshUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -55,6 +63,16 @@ public class ModelImportState extends BaseAppState {
|
||||
private Node lod1Node = null;
|
||||
private Node lod2Node = null;
|
||||
|
||||
// ── Impostor-Capture-State ────────────────────────────────────────────────
|
||||
private int impostorLevel = -1;
|
||||
private int impostorPass = 0;
|
||||
private boolean[] impostorReady = new boolean[]{false};
|
||||
private FrameBuffer impostorFB = null;
|
||||
private ViewPort impostorVP = null;
|
||||
private BoundingBox impostorBb = null;
|
||||
private Node impostorSrcNode = null;
|
||||
private ByteBuffer[] impostorPixels = new ByteBuffer[ImpostorUtil.DIRS];
|
||||
|
||||
public ModelImportState(SharedInput input) {
|
||||
this.input = input;
|
||||
}
|
||||
@@ -78,21 +96,51 @@ public class ModelImportState extends BaseAppState {
|
||||
if (modelEditorState == null) return;
|
||||
}
|
||||
|
||||
int clearLod = input.modelImportClearLodLevel;
|
||||
if (clearLod != -1) {
|
||||
input.modelImportClearLodLevel = -1;
|
||||
if (clearLod == 1) {
|
||||
lod1Node = null;
|
||||
input.modelImportHasLod1 = false;
|
||||
input.modelImportLod1PreviewNode.set(null);
|
||||
input.modelLodGenStatus = "LOD1: (entfernt)";
|
||||
if (input.modelEditorLodPreview == 1) {
|
||||
input.modelEditorLodPreview = 0;
|
||||
input.modelEditorLodChanged = true;
|
||||
}
|
||||
} else if (clearLod == 2) {
|
||||
lod2Node = null;
|
||||
input.modelImportHasLod2 = false;
|
||||
input.modelImportLod2PreviewNode.set(null);
|
||||
input.modelLodGenStatus = "LOD2: (entfernt)";
|
||||
if (input.modelEditorLodPreview == 2) {
|
||||
input.modelEditorLodPreview = 0;
|
||||
input.modelEditorLodChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SharedInput.ModelLodGenRequest req = input.modelLodGenRequest.getAndSet(null);
|
||||
if (req != null) {
|
||||
switch (req.level()) {
|
||||
case 1 -> {
|
||||
if ("auto_mesh".equals(req.method())) generateLodAuto(1, 0.5f);
|
||||
else if ("file".equals(req.method())) generateLodFromFile(1, req.fileAssetPath());
|
||||
float pct = req.reductionPct() > 0 ? req.reductionPct()
|
||||
: (req.level() == 1 ? 0.50f : 0.85f);
|
||||
switch (req.method()) {
|
||||
case "auto_mesh", "blight_auto", "jme_auto" -> {
|
||||
input.modelLodAlgorithm = req.method().startsWith("jme") ? "jme" : "blight";
|
||||
generateLodAuto(req.level(), pct);
|
||||
}
|
||||
case 2 -> {
|
||||
if ("auto_mesh_heavy".equals(req.method())) generateLodAuto(2, 0.85f);
|
||||
else if ("file".equals(req.method())) generateLodFromFile(2, req.fileAssetPath());
|
||||
}
|
||||
default -> log.warn("[ModelImport] Unbekannte LOD-Stufe: {}", req.level());
|
||||
case "auto_mesh_heavy" -> generateLodAuto(req.level(), pct);
|
||||
case "file" -> generateLodFromFile(req.level(), req.fileAssetPath());
|
||||
case "impostor" -> generateImpostorLod(req.level());
|
||||
default -> log.warn("[ModelImport] Unbekannte Methode: {}", req.method());
|
||||
}
|
||||
}
|
||||
|
||||
// Impostor: nächsten Frame abwarten und Pixel lesen
|
||||
if (impostorLevel >= 0 && impostorReady[0] && impostorFB != null) {
|
||||
finishImpostorPass();
|
||||
}
|
||||
|
||||
String exportName = input.modelImportExportName;
|
||||
if (exportName != null) {
|
||||
performExport(exportName);
|
||||
@@ -189,6 +237,88 @@ public class ModelImportState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Impostor-Generierung (Modell-Editor) ──────────────────────────────────
|
||||
|
||||
private void generateImpostorLod(int level) {
|
||||
Spatial src = modelEditorState.getOriginalSpatial();
|
||||
if (src == null) {
|
||||
input.modelLodGenStatus = "FEHLER: Kein Modell geladen";
|
||||
return;
|
||||
}
|
||||
|
||||
// Sauberer eigenständiger Clone bei Nulltranslation + Editor-Scale:
|
||||
// originalSpatial kann nach showSpatialDirectly() aus dem Scenegraph entfernt worden
|
||||
// sein, dann wäre getWorldBound() veraltet. Ein frischer Clone mit Identity-Transform
|
||||
// garantiert korrekte lokale Bounds für die Kamera-Rahmung.
|
||||
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);
|
||||
cleanClone.setLocalTranslation(Vector3f.ZERO);
|
||||
cleanClone.setLocalScale(input.modelEditorScaleX, input.modelEditorScaleY, input.modelEditorScaleZ);
|
||||
cleanClone.setLocalRotation(Quaternion.IDENTITY);
|
||||
cleanClone.updateGeometricState();
|
||||
cleanClone.updateModelBound();
|
||||
|
||||
impostorBb = cleanClone.getWorldBound() instanceof BoundingBox bb ? bb
|
||||
: new BoundingBox(new Vector3f(0f, 2f, 0f), 2f, 2f, 2f);
|
||||
impostorSrcNode = cleanClone instanceof Node n ? n : wrapInNode(cleanClone, "imp_src");
|
||||
|
||||
input.modelLodGenStatus = "Impostor wird gerendert (1/" + ImpostorUtil.DIRS + ")…";
|
||||
impostorLevel = level;
|
||||
impostorPass = 0;
|
||||
impostorPixels = new ByteBuffer[ImpostorUtil.DIRS];
|
||||
startImpostorPass(0);
|
||||
}
|
||||
|
||||
private void startImpostorPass(int pass) {
|
||||
Texture2D capTex = new Texture2D(ImpostorUtil.SIZE, ImpostorUtil.SIZE, Image.Format.RGBA8);
|
||||
impostorFB = new FrameBuffer(ImpostorUtil.SIZE, ImpostorUtil.SIZE, 1);
|
||||
impostorFB.addColorTexture(capTex);
|
||||
impostorFB.setDepthTexture(new Texture2D(ImpostorUtil.SIZE, ImpostorUtil.SIZE, Image.Format.Depth));
|
||||
impostorReady[0] = false;
|
||||
impostorVP = ImpostorUtil.startCapturePass(app, impostorSrcNode, impostorBb, impostorFB, pass, impostorReady);
|
||||
}
|
||||
|
||||
private void finishImpostorPass() {
|
||||
impostorPixels[impostorPass] = ImpostorUtil.readPixels(app, impostorFB);
|
||||
ImpostorUtil.cleanup(app, impostorVP, impostorFB);
|
||||
impostorVP = null;
|
||||
impostorFB = null;
|
||||
impostorReady[0] = false;
|
||||
|
||||
if (impostorPass < ImpostorUtil.DIRS - 1) {
|
||||
impostorPass++;
|
||||
input.modelLodGenStatus = "Impostor wird gerendert ("
|
||||
+ (impostorPass + 1) + "/" + ImpostorUtil.DIRS + ")…";
|
||||
startImpostorPass(impostorPass);
|
||||
return;
|
||||
}
|
||||
|
||||
// Alle Richtungen fertig
|
||||
int level = impostorLevel;
|
||||
impostorLevel = -1;
|
||||
ByteBuffer atlas = ImpostorUtil.combineAtlas(impostorPixels);
|
||||
ImpostorUtil.saveDebugImages(impostorPixels, atlas); // DEBUG – kann nach Diagnose entfernt werden
|
||||
Texture2D atlasTex = ImpostorUtil.buildAtlasTexture(atlas);
|
||||
Node impNode = ImpostorUtil.makeImpostorNode(impostorBb, atlasTex, assets);
|
||||
impNode.setName("lod" + level);
|
||||
impNode.setShadowMode(RenderQueue.ShadowMode.Off);
|
||||
ImpostorUtil.addViewControl(impNode, app.getCamera());
|
||||
if (level == 1) {
|
||||
lod1Node = impNode;
|
||||
input.modelImportLod1PreviewNode.set(impNode);
|
||||
input.modelImportHasLod1 = true;
|
||||
} else {
|
||||
lod2Node = impNode;
|
||||
input.modelImportLod2PreviewNode.set(impNode);
|
||||
input.modelImportHasLod2 = true;
|
||||
}
|
||||
input.modelLodGenStatus = "LOD" + level + " Impostor generiert (" + ImpostorUtil.DIRS + " Richtungen)";
|
||||
input.modelEditorLodPreview = level;
|
||||
input.modelEditorLodChanged = true;
|
||||
}
|
||||
|
||||
// ── Export ────────────────────────────────────────────────────────────────
|
||||
|
||||
private void performExport(String fileName) {
|
||||
@@ -708,12 +838,17 @@ public class ModelImportState extends BaseAppState {
|
||||
@Override
|
||||
protected void controlUpdate(float tpf) {
|
||||
float dSq = cam.getLocation().distanceSquared(spatial.getWorldTranslation());
|
||||
lod0.setCullHint(dSq < d01sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
if (lod1 != null)
|
||||
lod1.setCullHint(dSq >= d01sq && dSq < d12sq
|
||||
? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
if (lod2 != null)
|
||||
lod2.setCullHint(dSq >= d12sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
boolean atLod2 = (lod2 != null && dSq >= d12sq);
|
||||
setLod(lod0, dSq < d01sq, !atLod2, RenderQueue.ShadowMode.CastAndReceive);
|
||||
setLod(lod1, dSq >= d01sq && dSq < d12sq, !atLod2, RenderQueue.ShadowMode.CastAndReceive);
|
||||
setLod(lod2, atLod2, false, RenderQueue.ShadowMode.Off);
|
||||
}
|
||||
|
||||
private static void setLod(Node node, boolean visible,
|
||||
boolean castShadow, RenderQueue.ShadowMode shadowMode) {
|
||||
if (node == null) return;
|
||||
node.setCullHint(visible ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
node.setShadowMode(visible && castShadow ? shadowMode : RenderQueue.ShadowMode.Off);
|
||||
}
|
||||
|
||||
@Override protected void controlRender(RenderManager rm, ViewPort vp) {}
|
||||
|
||||
@@ -46,6 +46,7 @@ import com.jme3.util.SkyFactory;
|
||||
|
||||
import de.blight.editor.FrameTransfer;
|
||||
import de.blight.editor.SharedInput;
|
||||
import de.blight.editor.util.ImpostorUtil;
|
||||
import de.blight.editor.tree.TreeMeshBuilder;
|
||||
import de.blight.editor.tree.TreeParams;
|
||||
import org.slf4j.Logger;
|
||||
@@ -64,10 +65,6 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TreeGeneratorState.class);
|
||||
|
||||
private static final int IMPOSTOR_SIZE = 512;
|
||||
private static final int ATLAS_DIRS = 4;
|
||||
private static final int ATLAS_W = IMPOSTOR_SIZE * ATLAS_DIRS; // 2048
|
||||
private static final int ATLAS_H = IMPOSTOR_SIZE; // 512
|
||||
private static final int PREVIEW_SIZE = 1024;
|
||||
private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("blight-assets", "src", "main", "resources");
|
||||
|
||||
@@ -99,9 +96,9 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
private ViewPort captureVP = null;
|
||||
private FrameBuffer captureFB = null;
|
||||
private Texture2D captureTex = null;
|
||||
private volatile boolean captureReady = false;
|
||||
private boolean[] captureReady = new boolean[]{false};
|
||||
private int capturePass = 0;
|
||||
private ByteBuffer[] capturePixels = new ByteBuffer[ATLAS_DIRS];
|
||||
private ByteBuffer[] capturePixels = new ByteBuffer[ImpostorUtil.DIRS];
|
||||
|
||||
public TreeGeneratorState(SharedInput input) { this.input = input; }
|
||||
|
||||
@@ -191,7 +188,7 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
// 1. Szenen-Änderungen zuerst (bevor updateGeometricState)
|
||||
if (pendingRequest != null && captureReady) {
|
||||
if (pendingRequest != null && captureReady[0]) {
|
||||
finishCapture();
|
||||
} else if (pendingRequest == null) {
|
||||
SharedInput.TreeGenRequest req = input.treeGenQueue.poll();
|
||||
@@ -286,23 +283,25 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
pendingBarkMat = barkMat;
|
||||
pendingLeafMat = leafMat;
|
||||
capturePass = 0;
|
||||
capturePixels = new ByteBuffer[ATLAS_DIRS];
|
||||
capturePixels = new ByteBuffer[ImpostorUtil.DIRS];
|
||||
|
||||
startCapturePass(0);
|
||||
input.treeGenStatusMsg = "Rendere Impostor (1/" + ATLAS_DIRS + ")…";
|
||||
input.treeGenStatusMsg = "Rendere Impostor (1/" + ImpostorUtil.DIRS + ")…";
|
||||
}
|
||||
|
||||
// ── Phase 2: Capture abschließen ──────────────────────────────────────────
|
||||
|
||||
private void finishCapture() {
|
||||
ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4);
|
||||
app.getRenderer().readFrameBuffer(captureFB, pixels);
|
||||
capturePixels[capturePass] = pixels;
|
||||
cleanupCapture();
|
||||
capturePixels[capturePass] = ImpostorUtil.readPixels(app, captureFB);
|
||||
ImpostorUtil.cleanup(app, captureVP, captureFB);
|
||||
captureVP = null;
|
||||
captureFB = null;
|
||||
captureTex = null;
|
||||
captureReady[0] = false;
|
||||
|
||||
if (capturePass < ATLAS_DIRS - 1) {
|
||||
if (capturePass < ImpostorUtil.DIRS - 1) {
|
||||
capturePass++;
|
||||
input.treeGenStatusMsg = "Rendere Impostor (" + (capturePass + 1) + "/" + ATLAS_DIRS + ")…";
|
||||
input.treeGenStatusMsg = "Rendere Impostor (" + (capturePass + 1) + "/" + ImpostorUtil.DIRS + ")…";
|
||||
startCapturePass(capturePass);
|
||||
return;
|
||||
}
|
||||
@@ -311,9 +310,8 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
String treeType = pendingRequest.treeType();
|
||||
String timestamp = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
|
||||
|
||||
ByteBuffer atlas = combineAtlas(capturePixels);
|
||||
Texture2D impostorTex = saveImpostor(atlas, "impostor_" + treeType + "_" + timestamp,
|
||||
ATLAS_W, ATLAS_H);
|
||||
ByteBuffer atlas = ImpostorUtil.combineAtlas(capturePixels);
|
||||
Texture2D impostorTex = ImpostorUtil.buildAtlasTexture(atlas);
|
||||
|
||||
Node previewTree = makeTreeNode(pendingHdResult,
|
||||
pendingBarkMat.clone(), pendingLeafMat.clone(), "prev");
|
||||
@@ -339,7 +337,7 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
pendingHdResult = null;
|
||||
pendingBarkMat = null;
|
||||
pendingLeafMat = null;
|
||||
capturePixels = new ByteBuffer[ATLAS_DIRS];
|
||||
capturePixels = new ByteBuffer[ImpostorUtil.DIRS];
|
||||
}
|
||||
|
||||
// ── LOD-Aufbau ────────────────────────────────────────────────────────────
|
||||
@@ -349,7 +347,7 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
root.attachChild(pendingHdNode);
|
||||
root.attachChild(pendingLdNode);
|
||||
|
||||
Node lod2 = makeImpostorNode(pendingHdResult.bounds(), impostorTex);
|
||||
Node lod2 = ImpostorUtil.makeImpostorNode(pendingHdResult.bounds(), impostorTex, assets);
|
||||
root.attachChild(lod2);
|
||||
|
||||
// Nur LOD0 initial sichtbar; Control steuert je nach Distanz
|
||||
@@ -357,88 +355,21 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
lod2.setCullHint(Spatial.CullHint.Always);
|
||||
lod2.setShadowMode(RenderQueue.ShadowMode.Off);
|
||||
|
||||
ImpostorUtil.addViewControl(lod2, app.getCamera());
|
||||
root.addControl(new TreeLodControl(app.getCamera(),
|
||||
pendingHdNode, pendingLdNode, lod2, 60f, 200f));
|
||||
return root;
|
||||
}
|
||||
|
||||
private Node makeImpostorNode(BoundingBox bb, Texture2D tex) {
|
||||
float h = bb.getYExtent() * 2f;
|
||||
float w = Math.max(bb.getXExtent(), bb.getZExtent()) * 2f;
|
||||
float size = Math.max(h, w);
|
||||
float yOff = bb.getCenter().y + 2f;
|
||||
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
if (tex != null) mat.setTexture("ColorMap", tex);
|
||||
else mat.setColor("Color", new ColorRGBA(0.18f, 0.5f, 0.1f, 0.9f));
|
||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
||||
|
||||
Node n = new Node("lod2");
|
||||
for (int d = 0; d < ATLAS_DIRS; d++) {
|
||||
float angle = d * FastMath.HALF_PI;
|
||||
float uMin = (float) d / ATLAS_DIRS;
|
||||
float uMax = (float)(d + 1) / ATLAS_DIRS;
|
||||
n.attachChild(buildBillboardQuad("quad_" + d, angle, yOff, size, mat.clone(), uMin, uMax));
|
||||
}
|
||||
n.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
return n;
|
||||
}
|
||||
|
||||
private Geometry buildBillboardQuad(String name, float yRot, float yCent,
|
||||
float size, Material mat, float uMin, float uMax) {
|
||||
float hw = size * 0.5f;
|
||||
float hh = size * 0.5f;
|
||||
float cos = FastMath.cos(yRot);
|
||||
float sin = FastMath.sin(yRot);
|
||||
|
||||
Mesh mesh = new Mesh();
|
||||
mesh.setBuffer(VertexBuffer.Type.Position, 3, new float[]{
|
||||
-hw*cos, yCent-hh, -hw*sin,
|
||||
hw*cos, yCent-hh, hw*sin,
|
||||
hw*cos, yCent+hh, hw*sin,
|
||||
-hw*cos, yCent+hh, -hw*sin
|
||||
});
|
||||
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, new float[]{
|
||||
uMin, 0, uMax, 0, uMax, 1, uMin, 1
|
||||
});
|
||||
mesh.setBuffer(VertexBuffer.Type.Index, 3, new int[]{0,1,2, 0,2,3, 2,1,0, 3,2,0});
|
||||
mesh.updateBound();
|
||||
|
||||
Geometry g = new Geometry(name, mesh);
|
||||
g.setMaterial(mat);
|
||||
return g;
|
||||
}
|
||||
|
||||
/** Kombiniert 4 einzelne 512×512-Puffer zu einem horizontalen 2048×512-Atlas. */
|
||||
private ByteBuffer combineAtlas(ByteBuffer[] passes) {
|
||||
ByteBuffer atlas = BufferUtils.createByteBuffer(ATLAS_W * ATLAS_H * 4);
|
||||
for (int d = 0; d < ATLAS_DIRS; d++) {
|
||||
ByteBuffer src = passes[d];
|
||||
src.rewind();
|
||||
for (int y = 0; y < IMPOSTOR_SIZE; y++) {
|
||||
for (int x = 0; x < IMPOSTOR_SIZE; x++) {
|
||||
int srcOff = (y * IMPOSTOR_SIZE + x) * 4;
|
||||
int dstOff = (y * ATLAS_W + d * IMPOSTOR_SIZE + x) * 4;
|
||||
atlas.put(dstOff, src.get(srcOff));
|
||||
atlas.put(dstOff + 1, src.get(srcOff + 1));
|
||||
atlas.put(dstOff + 2, src.get(srcOff + 2));
|
||||
atlas.put(dstOff + 3, src.get(srcOff + 3));
|
||||
}
|
||||
}
|
||||
}
|
||||
return atlas;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private void startCapturePass(int pass) {
|
||||
captureTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8);
|
||||
captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1);
|
||||
captureTex = new Texture2D(ImpostorUtil.SIZE, ImpostorUtil.SIZE, Image.Format.RGBA8);
|
||||
captureFB = new FrameBuffer(ImpostorUtil.SIZE, ImpostorUtil.SIZE, 1);
|
||||
captureFB.addColorTexture(captureTex);
|
||||
captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth));
|
||||
float angle = pass * FastMath.HALF_PI;
|
||||
captureVP = buildCaptureViewPort(pendingHdNode, pendingHdResult.bounds(), captureFB, angle);
|
||||
captureReady = false;
|
||||
captureFB.setDepthTexture(new Texture2D(ImpostorUtil.SIZE, ImpostorUtil.SIZE, Image.Format.Depth));
|
||||
captureReady[0] = false;
|
||||
captureVP = ImpostorUtil.startCapturePass(app, pendingHdNode, pendingHdResult.bounds(),
|
||||
captureFB, pass, captureReady);
|
||||
}
|
||||
|
||||
// ── Material-Factories ────────────────────────────────────────────────────
|
||||
@@ -513,79 +444,6 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
return n;
|
||||
}
|
||||
|
||||
// ── Offscreen-ViewPort ────────────────────────────────────────────────────
|
||||
|
||||
private ViewPort buildCaptureViewPort(Node treeNode, BoundingBox bb, FrameBuffer fb, float angle) {
|
||||
Camera cam = new Camera(IMPOSTOR_SIZE, IMPOSTOR_SIZE);
|
||||
Vector3f center = bb.getCenter().add(0f, 2f, 0f);
|
||||
float extent = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent()));
|
||||
float dist = extent * 3.0f;
|
||||
|
||||
float camX = FastMath.sin(angle) * dist;
|
||||
float camZ = FastMath.cos(angle) * dist;
|
||||
cam.setLocation(center.add(camX, 0f, camZ));
|
||||
cam.lookAt(center, Vector3f.UNIT_Y);
|
||||
cam.setFrustumPerspective(35f, 1f, 0.1f, dist * 4f);
|
||||
|
||||
ViewPort vp = app.getRenderManager()
|
||||
.createPostView("impostorCap_" + System.nanoTime(), cam);
|
||||
vp.setOutputFrameBuffer(fb);
|
||||
vp.setBackgroundColor(new ColorRGBA(0f, 0f, 0f, 0f));
|
||||
vp.setClearFlags(true, true, true);
|
||||
|
||||
// Capture-Szene: Kopien der Geometrien + Beleuchtung
|
||||
Node scene = new Node("captureScene");
|
||||
scene.addLight(new DirectionalLight(
|
||||
new Vector3f(-0.4f, -1f, -0.5f).normalizeLocal(), new ColorRGBA(2.0f, 1.85f, 1.5f, 1f)));
|
||||
scene.addLight(new AmbientLight(new ColorRGBA(0.60f, 0.60f, 0.60f, 1f)));
|
||||
|
||||
Node capTree = cloneForCapture(treeNode);
|
||||
scene.attachChild(capTree);
|
||||
vp.attachScene(scene);
|
||||
scene.updateGeometricState();
|
||||
|
||||
// Einmaliger SceneProcessor signalisiert Fertigstellung
|
||||
vp.addProcessor(new SceneProcessor() {
|
||||
@Override public void initialize(RenderManager rm, ViewPort v) {}
|
||||
@Override public void reshape(ViewPort v, int w, int h) {}
|
||||
@Override public boolean isInitialized() { return true; }
|
||||
@Override public void preFrame(float t) {}
|
||||
@Override public void postQueue(RenderQueue rq) {}
|
||||
@Override public void cleanup() {}
|
||||
@Override public void setProfiler(AppProfiler profiler) {}
|
||||
@Override
|
||||
public void postFrame(FrameBuffer out) {
|
||||
vp.removeProcessor(this);
|
||||
captureReady = true;
|
||||
}
|
||||
});
|
||||
|
||||
return vp;
|
||||
}
|
||||
|
||||
private Node cloneForCapture(Node src) {
|
||||
Node copy = new Node(src.getName() + "_cap");
|
||||
copy.setLocalTranslation(src.getLocalTranslation());
|
||||
copy.setLocalScale(src.getLocalScale());
|
||||
for (Spatial child : src.getChildren()) {
|
||||
if (child instanceof Geometry g) {
|
||||
Geometry gc = new Geometry(g.getName() + "_c", g.getMesh());
|
||||
gc.setMaterial(g.getMaterial().clone());
|
||||
copy.attachChild(gc);
|
||||
}
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
// ── Impostor-Textur in-memory erzeugen ────────────────────────────────────
|
||||
|
||||
private Texture2D saveImpostor(ByteBuffer pixels, String name, int width, int height) {
|
||||
pixels.rewind();
|
||||
Image jmeImg = new Image(Image.Format.RGBA8, width, height,
|
||||
pixels, null, com.jme3.texture.image.ColorSpace.sRGB);
|
||||
return new Texture2D(jmeImg);
|
||||
}
|
||||
|
||||
// ── .j3o-Export ───────────────────────────────────────────────────────────
|
||||
|
||||
private void exportTree(Node treeNode, String treeType, String timestamp, float height) {
|
||||
@@ -611,16 +469,11 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
// ── Aufräumen ─────────────────────────────────────────────────────────────
|
||||
|
||||
private void cleanupCapture() {
|
||||
if (captureVP != null) {
|
||||
app.getRenderManager().removePostView(captureVP);
|
||||
captureVP = null;
|
||||
}
|
||||
if (captureFB != null) {
|
||||
try { captureFB.dispose(); } catch (Exception ignored) {}
|
||||
captureFB = null;
|
||||
}
|
||||
ImpostorUtil.cleanup(app, captureVP, captureFB);
|
||||
captureVP = null;
|
||||
captureFB = null;
|
||||
captureTex = null;
|
||||
captureReady = false;
|
||||
captureReady[0] = false;
|
||||
}
|
||||
|
||||
// ── LOD-Control ───────────────────────────────────────────────────────────
|
||||
@@ -639,10 +492,17 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
|
||||
@Override
|
||||
protected void controlUpdate(float tpf) {
|
||||
float dSq = cam.getLocation().distanceSquared(spatial.getWorldTranslation());
|
||||
lod0.setCullHint(dSq < d01sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
lod1.setCullHint(dSq>=d01sq && dSq<d12sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
lod2.setCullHint(dSq >= d12sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
float dSq = cam.getLocation().distanceSquared(spatial.getWorldTranslation());
|
||||
boolean atL2 = dSq >= d12sq;
|
||||
setLod(lod0, dSq < d01sq, !atL2);
|
||||
setLod(lod1, dSq >= d01sq && !atL2, !atL2);
|
||||
setLod(lod2, atL2, false);
|
||||
}
|
||||
|
||||
private static void setLod(Node node, boolean visible, boolean castShadow) {
|
||||
node.setCullHint(visible ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
node.setShadowMode(visible && castShadow
|
||||
? RenderQueue.ShadowMode.CastAndReceive : RenderQueue.ShadowMode.Off);
|
||||
}
|
||||
|
||||
@Override protected void controlRender(RenderManager rm, ViewPort vp) {}
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
package de.blight.editor.util;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.bounding.BoundingBox;
|
||||
import com.jme3.light.AmbientLight;
|
||||
import com.jme3.light.DirectionalLight;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.math.*;
|
||||
import com.jme3.post.SceneProcessor;
|
||||
import com.jme3.profile.AppProfiler;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.renderer.RenderManager;
|
||||
import com.jme3.renderer.ViewPort;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.*;
|
||||
import com.jme3.scene.control.AbstractControl;
|
||||
import com.jme3.texture.FrameBuffer;
|
||||
import com.jme3.texture.Image;
|
||||
import com.jme3.texture.Texture2D;
|
||||
import com.jme3.util.BufferUtils;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
/**
|
||||
* Gemeinsame Impostor-Generierung für EzTreeState, TreeGeneratorState und ModelImportState.
|
||||
*
|
||||
* Erzeugt 8 Offscreen-Ansichten eines Modells (45°-Schritte), kombiniert diese zu einem
|
||||
* horizontalen 4096×512-Atlas und baut daraus einen Billboard-Node aus 8 Quads (4 Ebenen).
|
||||
* Korrekt in Bezug auf CullHint: Kinder mit Always werden beim Klonen übersprungen,
|
||||
* sodass nur die sichtbare LOD0-Geometrie im Capture landet.
|
||||
*/
|
||||
public final class ImpostorUtil {
|
||||
|
||||
public static final int SIZE = 512;
|
||||
public static final int DIRS = 8;
|
||||
public static final int ATL_W = SIZE * DIRS;
|
||||
public static final int ATL_H = SIZE;
|
||||
/** Sicherheitspuffer um die BBox – gilt für Capture-FOV UND Quad-Größe. */
|
||||
private static final float MARGIN = 1.02f;
|
||||
|
||||
private ImpostorUtil() {}
|
||||
|
||||
// ── Offscreen-Capture ─────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Startet einen einzelnen Capture-Pass (ein Winkel).
|
||||
* Setzt {@code ready[0] = true} nachdem der Frame gerendert wurde.
|
||||
*
|
||||
* @param app JME3-Application
|
||||
* @param src Quell-Node (wird geklont; nur Kinder mit CullHint ≠ Always)
|
||||
* @param bb Bounding-Box des Modells (für Kamera-Rahmung)
|
||||
* @param fb Ziel-FrameBuffer (512×512, RGBA8)
|
||||
* @param pass Pass-Index 0..7 (entspricht 0°, 45°, 90°, 135°, 180°, 225°, 270°, 315°)
|
||||
* @param ready Ein-Element-Array; wird auf {@code true} gesetzt wenn fertig
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public static ViewPort startCapturePass(Application app, Node src, BoundingBox bb,
|
||||
FrameBuffer fb, int pass, boolean[] ready) {
|
||||
float angle = pass * FastMath.TWO_PI / DIRS;
|
||||
Vector3f center = bb.getCenter();
|
||||
float yExt = bb.getYExtent();
|
||||
float hExt = Math.max(bb.getXExtent(), bb.getZExtent());
|
||||
float dist = Math.max(yExt, hExt) * 3.5f;
|
||||
|
||||
float camX = FastMath.sin(angle) * dist;
|
||||
float camZ = FastMath.cos(angle) * dist;
|
||||
|
||||
Camera capCam = new Camera(SIZE, SIZE);
|
||||
capCam.setLocation(center.add(camX, 0f, camZ));
|
||||
capCam.lookAt(center, Vector3f.UNIT_Y);
|
||||
float fovY = (float) Math.toDegrees(2.0 * Math.atan2(yExt * MARGIN, dist));
|
||||
float fovH = (float) Math.toDegrees(2.0 * Math.atan2(hExt * MARGIN, dist));
|
||||
capCam.setFrustumPerspective(Math.max(fovY, fovH), 1f, 0.1f, dist * 4f);
|
||||
|
||||
// Hauptlicht aus Kamera-Richtung (Richtungsvektor zeigt VOM Licht ZUM Objekt)
|
||||
Vector3f toLight = new Vector3f(camX, dist * 0.5f, camZ).normalizeLocal();
|
||||
Vector3f fromLight = toLight.negate();
|
||||
Node capNode = cloneForCapture(src);
|
||||
applyTreeLighting(capNode, toLight);
|
||||
|
||||
Node scene = new Node("impCapScene");
|
||||
// Hauptlicht (warm, von Kamera-Seite)
|
||||
scene.addLight(new DirectionalLight(fromLight,
|
||||
new ColorRGBA(1.2f, 1.1f, 0.9f, 1f)));
|
||||
// Fülllicht (kühler, von gegenüberliegender Seite)
|
||||
scene.addLight(new DirectionalLight(toLight.mult(-0.7f).addLocal(0f, 0.3f, 0f).normalizeLocal(),
|
||||
new ColorRGBA(0.35f, 0.35f, 0.45f, 1f)));
|
||||
// Oberlicht (Himmelslicht von oben)
|
||||
scene.addLight(new DirectionalLight(new Vector3f(0f, -1f, 0f),
|
||||
new ColorRGBA(0.25f, 0.30f, 0.35f, 1f)));
|
||||
// Starkes Ambient: verhindert vollständig schwarze Stellen bei Lighting.j3md
|
||||
scene.addLight(new AmbientLight(new ColorRGBA(0.55f, 0.55f, 0.57f, 1f)));
|
||||
scene.attachChild(capNode);
|
||||
|
||||
ViewPort vp = app.getRenderManager().createPostView("impCap_" + System.nanoTime(), capCam);
|
||||
vp.setOutputFrameBuffer(fb);
|
||||
vp.setBackgroundColor(new ColorRGBA(0f, 0f, 0f, 0f));
|
||||
vp.setClearFlags(true, true, true);
|
||||
vp.attachScene(scene);
|
||||
scene.updateGeometricState();
|
||||
|
||||
vp.addProcessor(new SceneProcessor() {
|
||||
@Override public void initialize(RenderManager rm, ViewPort v) {}
|
||||
@Override public void reshape(ViewPort v, int w, int h) {}
|
||||
@Override public boolean isInitialized() { return true; }
|
||||
@Override public void preFrame(float t) {}
|
||||
@Override public void postQueue(RenderQueue rq) {}
|
||||
@Override public void cleanup() {}
|
||||
@Override public void setProfiler(AppProfiler p) {}
|
||||
@Override public void postFrame(FrameBuffer out) {
|
||||
vp.removeProcessor(this);
|
||||
ready[0] = true;
|
||||
}
|
||||
});
|
||||
return vp;
|
||||
}
|
||||
|
||||
/** Liest den aktuellen FrameBuffer in einen neuen ByteBuffer. */
|
||||
public static ByteBuffer readPixels(Application app, FrameBuffer fb) {
|
||||
ByteBuffer pixels = BufferUtils.createByteBuffer(SIZE * SIZE * 4);
|
||||
app.getRenderer().readFrameBuffer(fb, pixels);
|
||||
return pixels;
|
||||
}
|
||||
|
||||
/** Räumt ViewPort und FrameBuffer auf. */
|
||||
public static void cleanup(Application app, ViewPort vp, FrameBuffer fb) {
|
||||
if (vp != null) app.getRenderManager().removePostView(vp);
|
||||
if (fb != null) { try { fb.dispose(); } catch (Exception ignored) {} }
|
||||
}
|
||||
|
||||
// ── Atlas-Aufbau ──────────────────────────────────────────────────────────
|
||||
|
||||
/** Kombiniert {@code DIRS} 512×512-Puffer zu einem horizontalen 4096×512-Atlas. */
|
||||
public static ByteBuffer combineAtlas(ByteBuffer[] passes) {
|
||||
ByteBuffer atlas = BufferUtils.createByteBuffer(ATL_W * ATL_H * 4);
|
||||
for (int d = 0; d < DIRS; d++) {
|
||||
ByteBuffer src = passes[d];
|
||||
src.rewind();
|
||||
for (int y = 0; y < SIZE; y++) {
|
||||
for (int x = 0; x < SIZE; x++) {
|
||||
int srcOff = (y * SIZE + x) * 4;
|
||||
int dstOff = (y * ATL_W + d * SIZE + x) * 4;
|
||||
atlas.put(dstOff, src.get(srcOff));
|
||||
atlas.put(dstOff + 1, src.get(srcOff + 1));
|
||||
atlas.put(dstOff + 2, src.get(srcOff + 2));
|
||||
atlas.put(dstOff + 3, src.get(srcOff + 3));
|
||||
}
|
||||
}
|
||||
}
|
||||
return atlas;
|
||||
}
|
||||
|
||||
/** Baut eine Texture2D aus dem Atlas-ByteBuffer. */
|
||||
public static Texture2D buildAtlasTexture(ByteBuffer atlas) {
|
||||
atlas.rewind();
|
||||
Image img = new Image(Image.Format.RGBA8, ATL_W, ATL_H, atlas, null,
|
||||
com.jme3.texture.image.ColorSpace.Linear);
|
||||
return new Texture2D(img);
|
||||
}
|
||||
|
||||
// ── Billboard-Node ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Baut einen Impostor-Billboard-Node aus {@code DIRS} gekreuzten Quads.
|
||||
* Die Quads nutzen vorberechnete, rotierte Vertex-Positionen (kein Transform-Rotation).
|
||||
*/
|
||||
public static Node makeImpostorNode(BoundingBox bb, Texture2D tex, AssetManager assets) {
|
||||
float h = bb != null ? bb.getYExtent() * 2f * MARGIN : 10f;
|
||||
float w = bb != null ? Math.max(bb.getXExtent(), bb.getZExtent()) * 2f * MARGIN : 4f;
|
||||
float size = Math.max(h, w);
|
||||
float yOff = bb != null ? bb.getCenter().y : size * 0.5f;
|
||||
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
if (tex != null) mat.setTexture("ColorMap", tex);
|
||||
else mat.setColor("Color", new ColorRGBA(0.18f, 0.5f, 0.1f, 0.9f));
|
||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Back);
|
||||
|
||||
Node n = new Node("lod2");
|
||||
for (int d = 0; d < DIRS; d++) {
|
||||
float angle = d * FastMath.TWO_PI / DIRS;
|
||||
float uMin = (float) d / DIRS;
|
||||
float uMax = (float)(d + 1) / DIRS;
|
||||
n.attachChild(buildBillboardQuad("quad_" + d, angle, yOff, size, mat.clone(), uMin, uMax));
|
||||
}
|
||||
n.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
return n;
|
||||
}
|
||||
|
||||
/** Erzeugt einen einzelnen Billboard-Quad mit vorberechneten rotierten Vertex-Positionen. */
|
||||
public static Geometry buildBillboardQuad(String name, float yRot, float yCent,
|
||||
float size, Material mat, float uMin, float uMax) {
|
||||
float hw = size * 0.5f;
|
||||
float hh = size * 0.5f;
|
||||
float cos = FastMath.cos(yRot);
|
||||
float sin = FastMath.sin(yRot);
|
||||
|
||||
Mesh mesh = new Mesh();
|
||||
mesh.setBuffer(VertexBuffer.Type.Position, 3, new float[]{
|
||||
-hw*cos, yCent - hh, hw*sin,
|
||||
hw*cos, yCent - hh, -hw*sin,
|
||||
hw*cos, yCent + hh, -hw*sin,
|
||||
-hw*cos, yCent + hh, hw*sin
|
||||
});
|
||||
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, new float[]{
|
||||
uMin, 0f, uMax, 0f, uMax, 1f, uMin, 1f
|
||||
});
|
||||
mesh.setBuffer(VertexBuffer.Type.Index, 3, new int[]{0,1,2, 0,2,3});
|
||||
mesh.updateBound();
|
||||
|
||||
Geometry g = new Geometry(name, mesh);
|
||||
g.setMaterial(mat);
|
||||
return g;
|
||||
}
|
||||
|
||||
// ── Klon & Beleuchtung ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Klont einen Node rekursiv mit eigenen Material-Instanzen.
|
||||
* Eingebettete LOD-Knoten ("lod1", "lod2") werden per Name übersprungen,
|
||||
* damit sie nicht im Impostor-Capture landen.
|
||||
* CullHint wird auf allen geklonten Knoten auf Inherit zurückgesetzt,
|
||||
* um einen LOD-Control-Zustand aus dem Spiel-Thread zu ignorieren.
|
||||
*/
|
||||
public static Node cloneForCapture(Node src) {
|
||||
Node copy = new Node(src.getName());
|
||||
copy.setLocalTranslation(src.getLocalTranslation());
|
||||
copy.setLocalScale(src.getLocalScale());
|
||||
copy.setLocalRotation(src.getLocalRotation());
|
||||
for (Spatial child : src.getChildren()) {
|
||||
String name = child.getName();
|
||||
if ("lod1".equals(name) || "lod2".equals(name)) continue;
|
||||
Spatial childCopy = cloneSpatial(child);
|
||||
if (childCopy != null) copy.attachChild(childCopy);
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
private static Spatial cloneSpatial(Spatial s) {
|
||||
if (s instanceof Geometry g) {
|
||||
Geometry gc = new Geometry(g.getName(), g.getMesh());
|
||||
gc.setMaterial(g.getMaterial().clone());
|
||||
gc.setLocalTranslation(g.getLocalTranslation());
|
||||
gc.setLocalScale(g.getLocalScale());
|
||||
gc.setLocalRotation(g.getLocalRotation());
|
||||
gc.setCullHint(Spatial.CullHint.Inherit);
|
||||
return gc;
|
||||
} else if (s instanceof Node n) {
|
||||
Node nc = new Node(n.getName());
|
||||
nc.setLocalTranslation(n.getLocalTranslation());
|
||||
nc.setLocalScale(n.getLocalScale());
|
||||
nc.setLocalRotation(n.getLocalRotation());
|
||||
nc.setCullHint(Spatial.CullHint.Inherit);
|
||||
for (Spatial child : n.getChildren()) {
|
||||
String childName = child.getName();
|
||||
if ("lod1".equals(childName) || "lod2".equals(childName)) continue;
|
||||
Spatial cc = cloneSpatial(child);
|
||||
if (cc != null) nc.attachChild(cc);
|
||||
}
|
||||
return nc;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Debug: Impostor-Bilder auf Disk ablegen ───────────────────────────────
|
||||
|
||||
/**
|
||||
* Speichert alle Capture-Passes und den kombinierten Atlas als PNG nach ~/blight_impostor_debug/.
|
||||
* Nur für Debug-Zwecke – kann nach Diagnose entfernt werden.
|
||||
*/
|
||||
public static void saveDebugImages(ByteBuffer[] passes, ByteBuffer atlas) {
|
||||
try {
|
||||
java.nio.file.Path dir = Paths.get(System.getProperty("user.home"), "blight_impostor_debug");
|
||||
Files.createDirectories(dir);
|
||||
long ts = System.currentTimeMillis();
|
||||
for (int i = 0; i < passes.length; i++) {
|
||||
File f = dir.resolve("pass_" + i + "_" + ts + ".png").toFile();
|
||||
saveRgba(passes[i], SIZE, SIZE, f);
|
||||
}
|
||||
File atlasFile = dir.resolve("atlas_" + ts + ".png").toFile();
|
||||
saveRgba(atlas, ATL_W, ATL_H, atlasFile);
|
||||
Logger.getLogger(ImpostorUtil.class.getName())
|
||||
.info("[ImpostorUtil] Debug-Bilder gespeichert in: " + dir.toAbsolutePath());
|
||||
} catch (Exception e) {
|
||||
Logger.getLogger(ImpostorUtil.class.getName())
|
||||
.warning("[ImpostorUtil] Debug-Save fehlgeschlagen: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void saveRgba(ByteBuffer buf, int w, int h, File file) throws Exception {
|
||||
buf.rewind();
|
||||
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
|
||||
for (int y = 0; y < h; y++) {
|
||||
for (int x = 0; x < w; x++) {
|
||||
int off = (y * w + x) * 4;
|
||||
int r = buf.get(off) & 0xFF;
|
||||
int g = buf.get(off + 1) & 0xFF;
|
||||
int b = buf.get(off + 2) & 0xFF;
|
||||
int a = buf.get(off + 3) & 0xFF;
|
||||
img.setRGB(x, y, (a << 24) | (r << 16) | (g << 8) | b);
|
||||
}
|
||||
}
|
||||
javax.imageio.ImageIO.write(img, "PNG", file);
|
||||
}
|
||||
|
||||
// ── View-Control ──────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Hängt einen {@link ImpostorViewControl} an den Impostor-Node.
|
||||
* Der Control zeigt pro Frame nur das Quad, dessen Capture-Richtung
|
||||
* dem aktuellen Kamerawinkel am nächsten liegt.
|
||||
* Die Camera-Referenz wird NICHT serialisiert; nach j3o-Laden ggf.
|
||||
* via {@code setCamera()} neu setzen.
|
||||
*/
|
||||
public static ImpostorViewControl addViewControl(Node impostorNode, Camera cam) {
|
||||
ImpostorViewControl ctrl = new ImpostorViewControl(cam);
|
||||
impostorNode.addControl(ctrl);
|
||||
return ctrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wählt per Kamerawinkel das relevante Quad-Paar des Impostor-Node.
|
||||
* Pass d wurde mit Kamera bei sin(d·2π/DIRS), cos(d·2π/DIRS) aufgenommen.
|
||||
* Es werden immer zwei gegenüberliegende Quads (eine Ebene) angezeigt;
|
||||
* die anderen 6 werden per CullHint.Always ausgeblendet.
|
||||
* Wechsel erfolgt alle 360°/DIRS = 45° → max. Winkelabweichung 22,5°.
|
||||
*/
|
||||
public static final class ImpostorViewControl extends AbstractControl {
|
||||
|
||||
private Camera cam;
|
||||
|
||||
public ImpostorViewControl() {}
|
||||
|
||||
public ImpostorViewControl(Camera cam) { this.cam = cam; }
|
||||
|
||||
public void setCamera(Camera cam) { this.cam = cam; }
|
||||
|
||||
@Override
|
||||
protected void controlUpdate(float tpf) {
|
||||
if (cam == null || !(spatial instanceof Node n)) return;
|
||||
List<Spatial> quads = n.getChildren();
|
||||
if (quads.size() < DIRS) 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
|
||||
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
|
||||
|
||||
for (int i = 0; i < quads.size(); i++) {
|
||||
quads.get(i).setCullHint(i == best || i == opp
|
||||
? Spatial.CullHint.Inherit
|
||||
: Spatial.CullHint.Always);
|
||||
}
|
||||
}
|
||||
|
||||
@Override protected void controlRender(RenderManager rm, ViewPort vp) {}
|
||||
}
|
||||
|
||||
/** Setzt LightDir/SunColor/AmbientColor auf Tree.j3md-Materialien (rekursiv). */
|
||||
public static void applyTreeLighting(Node root, Vector3f lightDir) {
|
||||
for (Spatial s : root.getChildren()) {
|
||||
if (s instanceof Geometry g) {
|
||||
Material mat = g.getMaterial();
|
||||
if (mat.getMaterialDef().getMaterialParam("LightDir") != null) {
|
||||
mat.setVector3("LightDir", lightDir);
|
||||
mat.setVector3("SunColor", new Vector3f(1.3f, 1.2f, 1.0f));
|
||||
mat.setVector3("AmbientColor", new Vector3f(0.55f, 0.55f, 0.57f));
|
||||
}
|
||||
} else if (s instanceof Node n) {
|
||||
applyTreeLighting(n, lightDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user