Compare commits

...

2 Commits

Author SHA1 Message Date
0913da2f93 Imposter Generation angepassst 2026-07-19 10:37:10 +02:00
0b1cb4d0e9 Sichtweite und Nebel hinzugefügt 2026-07-17 22:20:44 +02:00
30 changed files with 1633 additions and 614 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,22 @@
MaterialDef BlightFog {
MaterialParameters {
Texture2D Texture
Texture2D DepthTexture
Int NumSamples
Int NumSamplesDepth
Color FogColor : 0.75 0.80 0.88 1.0
Float FogDensity : 0.40
Float FogDistance : 600.0
// Wird jeden Frame aus der echten Kamera gesetzt (near, far)
Vector2 FrustumNearFar : 1.0 1000.0
}
Technique {
VertexShader GLSL150: Common/MatDefs/Post/Post15.vert
FragmentShader GLSL150: Shaders/BlightFog.frag
WorldParameters {}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,37 @@
#import "Common/ShaderLib/GLSLCompat.glsllib"
uniform sampler2D m_Texture;
uniform sampler2D m_DepthTexture;
uniform vec4 m_FogColor;
uniform float m_FogDensity;
uniform float m_FogDistance;
// Kamera-Frustum (near, far) — wird jeden Frame aus der echten Kamera gesetzt
uniform vec2 m_FrustumNearFar;
in vec2 texCoord;
void main() {
vec4 sceneColor = texture2D(m_Texture, texCoord);
float rawDepth = texture2D(m_DepthTexture, texCoord).r;
// Sky-Pixel (Depth-Buffer = 1.0) nicht nebeln
if (rawDepth >= 1.0) {
gl_FragColor = sceneColor;
return;
}
float near = m_FrustumNearFar.x;
float far = m_FrustumNearFar.y;
// Depth-Buffer [0,1] → echte Weltdistanz (Meter)
// Herleitung: z_view = near*far / (far - rawDepth*(far-near))
float linearDist = (near * far) / (far - rawDepth * (far - near));
// Normiert auf FogDistance; d=1 entspricht genau der eingestellten Nebel-Distanz
float d = linearDist / m_FogDistance;
// Exponential-squared fog (gleiche Formel wie JME FogFilter, korrekte Tiefe)
float fogFactor = clamp(exp2(-m_FogDensity * m_FogDensity * d * d * 1.4426950408), 0.0, 1.0);
gl_FragColor = mix(m_FogColor, sceneColor, fogFactor);
}

View File

@@ -256,8 +256,14 @@ public class EditorApp extends Application {
private Label modelEditorLod2Label; private Label modelEditorLod2Label;
private ToggleButton modelEditorLod1Btn; private ToggleButton modelEditorLod1Btn;
private ToggleButton modelEditorLod2Btn; private ToggleButton modelEditorLod2Btn;
private Label modelEditorLod1StatusLabel;
private Label modelEditorLod2StatusLabel;
private Button modelEditorLod1GenBtn;
private Button modelEditorLod2GenBtn;
private ToggleButton importLod1Btn; private ToggleButton importLod1Btn;
private ToggleButton importLod2Btn; private ToggleButton importLod2Btn;
private Button modelImportLod1ClearBtn;
private Button modelImportLod2ClearBtn;
// Anhänge (veränderliche Listen, werden bei Änderung an JME übertragen) // 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.AttachedLight> modelEditorLights = new java.util.ArrayList<>();
private final java.util.List<de.blight.common.ModelMeta.AttachedEmitter> modelEditorEmitters = 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 // Modell-Editor: eingebettete LODs erkannt → Labels aktualisieren
if (input.modelEditorHasEmbeddedLods) { if (input.modelEditorHasEmbeddedLod1 && modelEditorLod1Label != null
if (modelEditorLod1Label != null && "(kein Pfad)".equals(modelEditorLod1Label.getText())) {
&& "(nicht gesetzt)".equals(modelEditorLod1Label.getText())) { modelEditorLod1Label.setText("(eingebettet im j3o)");
modelEditorLod1Label.setText("(eingebettet im j3o)"); modelEditorLod1Label.setStyle("-fx-text-fill:#6aaa6a; -fx-font-size:11;");
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;");
}
} }
// 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) if (modelEditorLod1Btn != null)
modelEditorLod1Btn.setDisable(!input.modelEditorHasEmbeddedLods && modelEditorLod1Path.isBlank()); modelEditorLod1Btn.setDisable(!input.modelEditorHasEmbeddedLod1 && modelEditorLod1Path.isBlank() && !input.modelImportHasLod1);
if (modelEditorLod2Btn != null) 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 // Importer LOD-Buttons freischalten sobald generiert
if (importLod1Btn != null) importLod1Btn.setDisable(!input.modelImportHasLod1); if (importLod1Btn != null) importLod1Btn.setDisable(!input.modelImportHasLod1);
if (importLod2Btn != null) importLod2Btn.setDisable(!input.modelImportHasLod2); if (importLod2Btn != null) importLod2Btn.setDisable(!input.modelImportHasLod2);
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; String lodStatus = input.modelLodGenStatus;
if (lodStatus != null) { if (lodStatus != null) {
input.modelLodGenStatus = null; input.modelLodGenStatus = null;
if (modelImportLod1StatusLabel != null && lodStatus.startsWith("LOD1")) boolean isLod1 = lodStatus.startsWith("LOD1");
modelImportLod1StatusLabel.setText(lodStatus); boolean isLod2 = lodStatus.startsWith("LOD2") || lodStatus.contains("Impostor");
else if (modelImportLod2StatusLabel != null && (lodStatus.startsWith("LOD2") || lodStatus.contains("Impostor"))) if (isLod1) {
modelImportLod2StatusLabel.setText(lodStatus); 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); setStatus(lodStatus);
} }
// Modell-Import: Export-Ergebnis // Modell-Import: Export-Ergebnis
@@ -867,14 +883,15 @@ public class EditorApp extends Application {
modelEditorLod1Btn.setOnAction(e -> { input.modelEditorLodPreview = 1; input.modelEditorLodChanged = true; }); modelEditorLod1Btn.setOnAction(e -> { input.modelEditorLodPreview = 1; input.modelEditorLodChanged = true; });
modelEditorLod2Btn.setOnAction(e -> { input.modelEditorLodPreview = 2; input.modelEditorLodChanged = true; }); modelEditorLod2Btn.setOnAction(e -> { input.modelEditorLodPreview = 2; input.modelEditorLodChanged = true; });
// Initial deaktiviert werden freigegeben sobald LOD-Pfade/eingebettete LODs bekannt sind // Initial deaktiviert werden freigegeben sobald LOD-Pfade/eingebettete LODs bekannt sind
boolean hasLod1 = input.modelEditorHasEmbeddedLods || !modelEditorLod1Path.isBlank(); boolean hasLod1 = input.modelEditorHasEmbeddedLod1 || !modelEditorLod1Path.isBlank() || input.modelImportHasLod1;
boolean hasLod2 = input.modelEditorHasEmbeddedLods || !modelEditorLod2Path.isBlank(); boolean hasLod2 = input.modelEditorHasEmbeddedLod2 || !modelEditorLod2Path.isBlank() || input.modelImportHasLod2;
modelEditorLod1Btn.setDisable(!hasLod1); modelEditorLod1Btn.setDisable(!hasLod1);
modelEditorLod2Btn.setDisable(!hasLod2); modelEditorLod2Btn.setDisable(!hasLod2);
ToolBar tb = new ToolBar(); ToolBar tb = new ToolBar();
tb.getItems().addAll(backBtn, new Separator(Orientation.VERTICAL), label, 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; return tb;
} }
@@ -4051,6 +4068,10 @@ public class EditorApp extends Application {
} }
if (cat == modelsNode) { if (cat == modelsNode) {
if ("modeleditor".equals(currentTool) && relPath.endsWith(".j3o")) {
openModelEditor(relPath, p);
return;
}
input.pendingModelPath = relPath; input.pendingModelPath = relPath;
input.activeLayer = SharedInput.LAYER_OBJECTS; input.activeLayer = SharedInput.LAYER_OBJECTS;
if (objPlaceBtn != null) objPlaceBtn.setSelected(true); 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. */ /** Gibt zurück, ob das aktive Tool den aufgeklappten Zustand des Asset-Baums behalten soll. */
private boolean shouldPreserveExpansion() { private boolean shouldPreserveExpansion() {
return "vegetation".equals(currentTool) || "eztree".equals(currentTool) return "vegetation".equals(currentTool) || "eztree".equals(currentTool)
|| "objecteditor".equals(currentTool) || "objecteditor".equals(currentTool) || "modeleditor".equals(currentTool)
|| ("world".equals(currentTool) || ("world".equals(currentTool)
&& (input.activeLayer == SharedInput.LAYER_OBJECTS && (input.activeLayer == SharedInput.LAYER_OBJECTS
|| input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT)); || input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT));
@@ -5812,8 +5833,8 @@ public class EditorApp extends Application {
topBar.getChildren().set(1, buildModelEditorToolBar()); topBar.getChildren().set(1, buildModelEditorToolBar());
// Kein Modell vorausgewählt Platzhalter zeigen // Kein Modell vorausgewählt Platzhalter zeigen
VBox placeholder = new VBox(12); VBox placeholder = new VBox(12);
placeholder.setPadding(new javafx.geometry.Insets(16)); placeholder.setPadding(new javafx.geometry.Insets(12));
placeholder.setPrefWidth(300); placeholder.setPrefWidth(280);
placeholder.setStyle("-fx-background-color: #2a2a3e;"); placeholder.setStyle("-fx-background-color: #2a2a3e;");
Label title = new Label("Modell-Editor"); Label title = new Label("Modell-Editor");
title.setStyle("-fx-font-weight:bold; -fx-font-size:14; -fx-text-fill:#ddd;"); 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;"); modelEditorScroll.setStyle("-fx-background-color:transparent;-fx-background:transparent;");
VBox modelEditorOuter = new VBox(modelEditorScroll); VBox modelEditorOuter = new VBox(modelEditorScroll);
VBox.setVgrow(modelEditorScroll, Priority.ALWAYS); VBox.setVgrow(modelEditorScroll, Priority.ALWAYS);
modelEditorOuter.setPrefWidth(300); modelEditorOuter.setPrefWidth(280);
modelEditorOuter.setMaxWidth(280);
root.setRight(modelEditorOuter); root.setRight(modelEditorOuter);
setStatus("Modell-Editor: " + relPath); setStatus("Modell-Editor: " + relPath);
} }
@@ -5862,9 +5884,9 @@ public class EditorApp extends Application {
private VBox buildModelEditorPanel(String relPath, private VBox buildModelEditorPanel(String relPath,
java.nio.file.Path absolutePath, java.nio.file.Path absolutePath,
de.blight.common.ModelMeta meta) { de.blight.common.ModelMeta meta) {
VBox panel = new VBox(8); VBox panel = new VBox(6);
panel.setPadding(new javafx.geometry.Insets(10)); panel.setPadding(new javafx.geometry.Insets(8));
panel.setPrefWidth(300); panel.setPrefWidth(280);
panel.setStyle("-fx-background-color: #2a2a3e;"); panel.setStyle("-fx-background-color: #2a2a3e;");
// ── Titel ──────────────────────────────────────────────────────────── // ── Titel ────────────────────────────────────────────────────────────
@@ -5958,8 +5980,10 @@ public class EditorApp extends Application {
}); });
// ── Maße (read-only, von JME aktualisiert) ─────────────────────────── // ── 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.setStyle("-fx-text-fill:#8cf; -fx-font-family:monospace;");
modelEditorDimLabel.setMaxWidth(Double.MAX_VALUE);
modelEditorDimLabel.setWrapText(true);
// ── Pivot & Platzierungs-Versatz ───────────────────────────────────── // ── Pivot & Platzierungs-Versatz ─────────────────────────────────────
Label offsetTitle = new Label("Versatz:"); Label offsetTitle = new Label("Versatz:");
@@ -6007,38 +6031,79 @@ public class EditorApp extends Application {
Label lodTitle = new Label("Level of Detail"); Label lodTitle = new Label("Level of Detail");
lodTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;"); 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.setStyle("-fx-text-fill:#888; -fx-font-size:11;");
modelEditorLod1Label.setWrapText(true); modelEditorLod1Label.setWrapText(true);
Button lod1ImportBtn = new Button("LOD 1 importieren…");
Button lod1ClearBtn = new Button(""); Button lod1ClearBtn = new Button("");
lod1ClearBtn.setStyle("-fx-text-fill:#c0392b;"); lod1ClearBtn.setStyle("-fx-text-fill:#c0392b;");
lod1ImportBtn.setMaxWidth(Double.MAX_VALUE);
lod1ImportBtn.setOnAction(e -> importLodFile(1));
lod1ClearBtn.setOnAction(e -> { lod1ClearBtn.setOnAction(e -> {
if (input.modelEditorHasEmbeddedLod1) {
input.modelEditorRemoveEmbeddedLod = 1;
}
if (input.modelImportHasLod1) {
input.modelImportClearLodLevel = 1;
if (modelEditorLod1StatusLabel != null) modelEditorLod1StatusLabel.setText("");
}
modelEditorLod1Path = ""; modelEditorLod1Path = "";
input.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.setStyle("-fx-text-fill:#888; -fx-font-size:11;");
modelEditorLod2Label.setWrapText(true); modelEditorLod2Label.setWrapText(true);
Button lod2ImportBtn = new Button("LOD 2 importieren…");
Button lod2ClearBtn = new Button(""); Button lod2ClearBtn = new Button("");
lod2ClearBtn.setStyle("-fx-text-fill:#c0392b;"); lod2ClearBtn.setStyle("-fx-text-fill:#c0392b;");
lod2ImportBtn.setMaxWidth(Double.MAX_VALUE);
lod2ImportBtn.setOnAction(e -> importLodFile(2));
lod2ClearBtn.setOnAction(e -> { lod2ClearBtn.setOnAction(e -> {
if (input.modelEditorHasEmbeddedLod2) {
input.modelEditorRemoveEmbeddedLod = 2;
}
if (input.modelImportHasLod2) {
input.modelImportClearLodLevel = 2;
if (modelEditorLod2StatusLabel != null) modelEditorLod2StatusLabel.setText("");
}
modelEditorLod2Path = ""; modelEditorLod2Path = "";
input.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, modelEditorLod2GenBtn, lod2ClearBtn);
HBox lod2Row = new HBox(4, lod2ImportBtn, lod2ClearBtn); HBox.setHgrow(modelEditorLod2GenBtn, javafx.scene.layout.Priority.ALWAYS);
HBox.setHgrow(lod1ImportBtn, javafx.scene.layout.Priority.ALWAYS);
HBox.setHgrow(lod2ImportBtn, javafx.scene.layout.Priority.ALWAYS);
// ── Anhänge: Lichter & Partikel ─────────────────────────────────────── // ── Anhänge: Lichter & Partikel ───────────────────────────────────────
Label attachTitle = new Label("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, new javafx.scene.control.SpinnerValueFactory.DoubleSpinnerValueFactory(-5.0, 5.0,
Math.round(meta.interactableOffsetX() * 100.0) / 100.0, 0.05)); Math.round(meta.interactableOffsetX() * 100.0) / 100.0, 0.05));
modelEditorInteractableXSpin.setEditable(true); modelEditorInteractableXSpin.setEditable(true);
modelEditorInteractableXSpin.setPrefWidth(100); modelEditorInteractableXSpin.setMaxWidth(Double.MAX_VALUE);
modelEditorInteractableXSpin.valueProperty().addListener((obs, ov, nv) -> { modelEditorInteractableXSpin.valueProperty().addListener((obs, ov, nv) -> {
if (updatingInteractableSpinnersFromJme) return; if (updatingInteractableSpinnersFromJme) return;
input.modelInteractableOffsetX = nv.floatValue(); input.modelInteractableOffsetX = nv.floatValue();
@@ -6145,7 +6210,7 @@ public class EditorApp extends Application {
new javafx.scene.control.SpinnerValueFactory.DoubleSpinnerValueFactory(-1.0, 4.0, new javafx.scene.control.SpinnerValueFactory.DoubleSpinnerValueFactory(-1.0, 4.0,
Math.round(meta.interactableOffsetY() * 100.0) / 100.0, 0.05)); Math.round(meta.interactableOffsetY() * 100.0) / 100.0, 0.05));
modelEditorInteractableYSpin.setEditable(true); modelEditorInteractableYSpin.setEditable(true);
modelEditorInteractableYSpin.setPrefWidth(100); modelEditorInteractableYSpin.setMaxWidth(Double.MAX_VALUE);
modelEditorInteractableYSpin.valueProperty().addListener((obs, ov, nv) -> { modelEditorInteractableYSpin.valueProperty().addListener((obs, ov, nv) -> {
if (updatingInteractableSpinnersFromJme) return; if (updatingInteractableSpinnersFromJme) return;
input.modelInteractableOffsetY = nv.floatValue(); input.modelInteractableOffsetY = nv.floatValue();
@@ -6156,7 +6221,7 @@ public class EditorApp extends Application {
new javafx.scene.control.SpinnerValueFactory.DoubleSpinnerValueFactory(-5.0, 5.0, new javafx.scene.control.SpinnerValueFactory.DoubleSpinnerValueFactory(-5.0, 5.0,
Math.round(meta.interactableOffsetZ() * 100.0) / 100.0, 0.05)); Math.round(meta.interactableOffsetZ() * 100.0) / 100.0, 0.05));
modelEditorInteractableZSpin.setEditable(true); modelEditorInteractableZSpin.setEditable(true);
modelEditorInteractableZSpin.setPrefWidth(100); modelEditorInteractableZSpin.setMaxWidth(Double.MAX_VALUE);
modelEditorInteractableZSpin.valueProperty().addListener((obs, ov, nv) -> { modelEditorInteractableZSpin.valueProperty().addListener((obs, ov, nv) -> {
if (updatingInteractableSpinnersFromJme) return; if (updatingInteractableSpinnersFromJme) return;
input.modelInteractableOffsetZ = nv.floatValue(); input.modelInteractableOffsetZ = nv.floatValue();
@@ -6165,6 +6230,11 @@ public class EditorApp extends Application {
javafx.scene.layout.GridPane posGrid = new javafx.scene.layout.GridPane(); javafx.scene.layout.GridPane posGrid = new javafx.scene.layout.GridPane();
posGrid.setHgap(4); posGrid.setVgap(2); 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("X:"), 0, 0); posGrid.add(modelEditorInteractableXSpin, 1, 0);
posGrid.add(new Label("Y:"), 0, 1); posGrid.add(modelEditorInteractableYSpin, 1, 1); posGrid.add(new Label("Y:"), 0, 1); posGrid.add(modelEditorInteractableYSpin, 1, 1);
posGrid.add(new Label("Z:"), 0, 2); posGrid.add(modelEditorInteractableZSpin, 1, 2); 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); Slider rotSlider = new Slider(0, 360, initDeg);
rotSlider.setShowTickMarks(true); rotSlider.setShowTickMarks(true);
rotSlider.setMajorTickUnit(90); rotSlider.setMajorTickUnit(90);
rotSlider.setPrefWidth(180); rotSlider.setPrefWidth(150);
TextField rotField = new TextField(String.format("%.1f", initDeg)); TextField rotField = new TextField(String.format("%.1f", initDeg));
rotField.setPrefWidth(60); rotField.setPrefWidth(60);
@@ -6329,8 +6399,8 @@ public class EditorApp extends Application {
rndTitle, rndGrid, rndTitle, rndGrid,
new Separator(), new Separator(),
lodTitle, lodTitle,
modelEditorLod1Label, lod1Row, lod1Row, modelEditorLod1StatusLabel, modelEditorLod1Label,
modelEditorLod2Label, lod2Row, lod2Row, modelEditorLod2StatusLabel, modelEditorLod2Label,
new Separator(), new Separator(),
attachTitle, attachTitle,
lightSectionLbl, modelEditorLightBox, addLightBtn, lightSectionLbl, modelEditorLightBox, addLightBtn,
@@ -6409,6 +6479,8 @@ public class EditorApp extends Application {
modelImportCurrentPath = relPath; modelImportCurrentPath = relPath;
modelImportLod1StatusLabel = null; modelImportLod1StatusLabel = null;
modelImportLod2StatusLabel = null; modelImportLod2StatusLabel = null;
modelImportLod1ClearBtn = null;
modelImportLod2ClearBtn = null;
modelImportExportStatusLabel = null; modelImportExportStatusLabel = null;
input.modelImportHasLod1 = false; input.modelImportHasLod1 = false;
input.modelImportHasLod2 = 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.setStyle("-fx-text-fill:#8cf; -fx-font-family:monospace;");
modelEditorDimLabel.setMaxWidth(Double.MAX_VALUE);
modelEditorDimLabel.setWrapText(true);
// ── LOD-Algorithmus ─────────────────────────────────────────────────── // ── LOD-Algorithmus ───────────────────────────────────────────────────
Label algoTitle = new Label("Reduktions-Algorithmus"); Label algoTitle = new Label("Reduktions-Algorithmus");
@@ -6624,6 +6698,14 @@ public class EditorApp extends Application {
modelImportLod1StatusLabel = new Label("(nicht gesetzt)"); modelImportLod1StatusLabel = new Label("(nicht gesetzt)");
modelImportLod1StatusLabel.setStyle("-fx-text-fill:#888; -fx-font-size:11;"); modelImportLod1StatusLabel.setStyle("-fx-text-fill:#888; -fx-font-size:11;");
modelImportLod1StatusLabel.setWrapText(true); 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 ───────────────────────────────────────────────────────────── // ── LOD 2 ─────────────────────────────────────────────────────────────
Label lod2Title = new Label("LOD 2"); Label lod2Title = new Label("LOD 2");
@@ -6665,6 +6747,14 @@ public class EditorApp extends Application {
modelImportLod2StatusLabel = new Label("(nicht gesetzt)"); modelImportLod2StatusLabel = new Label("(nicht gesetzt)");
modelImportLod2StatusLabel.setStyle("-fx-text-fill:#888; -fx-font-size:11;"); modelImportLod2StatusLabel.setStyle("-fx-text-fill:#888; -fx-font-size:11;");
modelImportLod2StatusLabel.setWrapText(true); 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 ──────────────────────────────────────────────────────────── // ── Export ────────────────────────────────────────────────────────────
Label exportTitle = new Label("Export"); Label exportTitle = new Label("Export");
@@ -6705,9 +6795,9 @@ public class EditorApp extends Application {
new Separator(), new Separator(),
algoTitle, algoBlightRB, algoJmeRB, algoTitle, algoBlightRB, algoJmeRB,
new Separator(), new Separator(),
lod1Title, lod1AutoRB, lod1FileRB, lod1GenBtn, modelImportLod1StatusLabel, lod1Title, lod1AutoRB, lod1FileRB, lod1GenBtn, modelImportLod1StatusLabel, modelImportLod1ClearBtn,
new Separator(), new Separator(),
lod2Title, lod2MeshRB, lod2FileRB, lod2GenBtn, modelImportLod2StatusLabel, lod2Title, lod2MeshRB, lod2FileRB, lod2GenBtn, modelImportLod2StatusLabel, modelImportLod2ClearBtn,
new Separator(), new Separator(),
exportTitle, assetNameLabel, nameTF, modelImportExportStatusLabel, exportTitle, assetNameLabel, nameTF, modelImportExportStatusLabel,
exportBtn, cancelBtn exportBtn, cancelBtn
@@ -6758,12 +6848,19 @@ public class EditorApp extends Application {
} }
private void addLabeledRow(GridPane grid, int row, String labelText, javafx.scene.Node ctrl) { 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); Label l = new Label(labelText);
l.setStyle("-fx-text-fill:#aaa;"); l.setStyle("-fx-text-fill:#aaa;");
l.setMinWidth(90);
grid.add(l, 0, row); grid.add(l, 0, row);
grid.add(ctrl, 1, row); grid.add(ctrl, 1, row);
javafx.scene.layout.GridPane.setHgrow(ctrl, javafx.scene.layout.Priority.ALWAYS);
} }
private void pushScaleToJme() { private void pushScaleToJme() {
@@ -6776,8 +6873,7 @@ public class EditorApp extends Application {
private void updateModelEditorDimensions(float w, float h, float d) { private void updateModelEditorDimensions(float w, float h, float d) {
if (modelEditorDimLabel != null) if (modelEditorDimLabel != null)
modelEditorDimLabel.setText(String.format( modelEditorDimLabel.setText(String.format("B %.2f × H %.2f × T %.2f m", w, h, d));
"Maße: B %.2f m × H %.2f m × T %.2f m", w, h, d));
} }
private void saveModelMeta(String relPath, 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 ──────────────────────────────────── // ── Zentraler Bereich: JME3-Viewport ────────────────────────────────────
private StackPane buildViewport() { private StackPane buildViewport() {

View File

@@ -709,12 +709,16 @@ public class SharedInput {
/** JME → JFX: true wenn das geladene Modell eingebettete LOD-Kinder hat (kein separater Pfad nötig). */ /** JME → JFX: true wenn das geladene Modell eingebettete LOD-Kinder hat (kein separater Pfad nötig). */
public volatile boolean modelEditorHasEmbeddedLods = false; 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). */ /** JFX → JME: welche LOD-Stufe anzeigen (0=LOD0, 1=LOD1, 2=LOD2). */
public volatile int modelEditorLodPreview = 0; public volatile int modelEditorLodPreview = 0;
public volatile String modelEditorLod1Path = ""; public volatile String modelEditorLod1Path = "";
public volatile String modelEditorLod2Path = ""; public volatile String modelEditorLod2Path = "";
public volatile boolean modelEditorLodChanged = false; public volatile boolean modelEditorLodChanged = false;
/** JFX → JME: Eingebettetes LOD aus j3o entfernen und speichern (1=lod1, 2=lod2, -1=kein Auftrag). */
public volatile int modelEditorRemoveEmbeddedLod = -1;
/** JFX → JME: Richtung der Hauptlichtquelle in der Vorschau. /** JFX → JME: Richtung der Hauptlichtquelle in der Vorschau.
* Azimut 0360° (Kompassrichtung), Elevation 090° (Höhe über Horizont). */ * Azimut 0360° (Kompassrichtung), Elevation 090° (Höhe über Horizont). */
@@ -790,9 +794,15 @@ public class SharedInput {
* JFX → JME: LOD-Generierungsanfrage. * JFX → JME: LOD-Generierungsanfrage.
* level: 1 oder 2 * level: 1 oder 2
* method: "auto_mesh" | "file" | "auto_impostor" * 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 = public final AtomicReference<ModelLodGenRequest> modelLodGenRequest =
new AtomicReference<>(); new AtomicReference<>();
@@ -821,6 +831,8 @@ public class SharedInput {
/** JME → JFX: LOD-Stufen vorhanden. */ /** JME → JFX: LOD-Stufen vorhanden. */
public volatile boolean modelImportHasLod1 = false; public volatile boolean modelImportHasLod1 = false;
public volatile boolean modelImportHasLod2 = 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. */ /** JFX → JME: Dateiname (ohne Erweiterung) für den Export-Auftrag. null = kein Auftrag. */
public volatile String modelImportExportName = null; public volatile String modelImportExportName = null;

View File

@@ -8,16 +8,11 @@ import com.jme3.app.state.BaseAppState;
import com.jme3.asset.AssetManager; import com.jme3.asset.AssetManager;
import com.jme3.bounding.BoundingBox; import com.jme3.bounding.BoundingBox;
import com.jme3.export.binary.BinaryExporter; import com.jme3.export.binary.BinaryExporter;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material; import com.jme3.material.Material;
import com.jme3.material.RenderState; import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA; import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f; import com.jme3.math.Vector3f;
import com.jme3.post.SceneProcessor;
import com.jme3.profile.AppProfiler;
import com.jme3.renderer.Camera; import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort; import com.jme3.renderer.ViewPort;
import com.jme3.renderer.queue.RenderQueue; import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry; import com.jme3.scene.Geometry;
@@ -31,6 +26,7 @@ import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D; import com.jme3.texture.Texture2D;
import com.jme3.util.BufferUtils; import com.jme3.util.BufferUtils;
import de.blight.editor.SharedInput; import de.blight.editor.SharedInput;
import de.blight.editor.util.ImpostorUtil;
import de.blight.eztree.Tree; import de.blight.eztree.Tree;
import de.blight.eztree.TreeOptions; import de.blight.eztree.TreeOptions;
import org.slf4j.Logger; 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 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 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 Path TOOLS_DIR = de.blight.editor.ProjectRoot.resolve("tools");
private static final Gson GSON = new Gson(); private static final Gson GSON = new Gson();
@@ -79,9 +71,9 @@ public class EzTreeState extends BaseAppState {
private BoundingBox pendingBb = null; private BoundingBox pendingBb = null;
private ViewPort captureVP = null; private ViewPort captureVP = null;
private FrameBuffer captureFB = null; private FrameBuffer captureFB = null;
private volatile boolean captureReady = false; private boolean[] captureReady = new boolean[]{false};
private int capturePass = 0; 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; } public EzTreeState(SharedInput input) { this.input = input; }
@@ -106,7 +98,7 @@ public class EzTreeState extends BaseAppState {
if (previewHost == null) return; if (previewHost == null) return;
} }
if (pendingRequest != null && captureReady) { if (pendingRequest != null && captureReady[0]) {
finishCapture(); finishCapture();
} else if (pendingRequest == null) { } else if (pendingRequest == null) {
SharedInput.EzTreeGenRequest req = input.ezTreeGenQueue.poll(); SharedInput.EzTreeGenRequest req = input.ezTreeGenQueue.poll();
@@ -150,9 +142,9 @@ public class EzTreeState extends BaseAppState {
pendingLd1Node = finalLd1; pendingLd1Node = finalLd1;
pendingBb = finalBb; pendingBb = finalBb;
capturePass = 0; capturePass = 0;
capturePixels = new ByteBuffer[ATLAS_DIRS]; capturePixels = new ByteBuffer[ImpostorUtil.DIRS];
startCapturePass(0, finalHd, finalBb); startCapturePass(0);
input.treeGenStatusMsg = "EZ-Tree: rendere Impostor (1/" + ATLAS_DIRS + ")…"; input.treeGenStatusMsg = "EZ-Tree: rendere Impostor (1/" + ImpostorUtil.DIRS + ")…";
} }
}); });
@@ -384,27 +376,29 @@ public class EzTreeState extends BaseAppState {
// ── Phase 2: Impostor-Capture (4-Richtungen) ───────────────────────────── // ── Phase 2: Impostor-Capture (4-Richtungen) ─────────────────────────────
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
private void startCapturePass(int pass, Node treeNode, BoundingBox bb) { private void startCapturePass(int pass) {
BoundingBox safeBb = bb != null ? bb : new BoundingBox(Vector3f.ZERO, 5f, 10f, 5f); BoundingBox safeBb = pendingBb != null ? pendingBb : new BoundingBox(Vector3f.ZERO, 5f, 10f, 5f);
Texture2D capTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8); com.jme3.texture.Texture2D capTex = new com.jme3.texture.Texture2D(
captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1); ImpostorUtil.SIZE, ImpostorUtil.SIZE, Image.Format.RGBA8);
captureFB = new FrameBuffer(ImpostorUtil.SIZE, ImpostorUtil.SIZE, 1);
captureFB.addColorTexture(capTex); captureFB.addColorTexture(capTex);
captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth)); captureFB.setDepthTexture(new com.jme3.texture.Texture2D(
float angle = pass * com.jme3.math.FastMath.HALF_PI; ImpostorUtil.SIZE, ImpostorUtil.SIZE, Image.Format.Depth));
captureVP = buildCaptureViewPort(treeNode, safeBb, captureFB, angle); captureReady[0] = false;
captureReady = false; captureVP = ImpostorUtil.startCapturePass(app, pendingHdNode, safeBb, captureFB, pass, captureReady);
} }
private void finishCapture() { private void finishCapture() {
ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4); capturePixels[capturePass] = ImpostorUtil.readPixels(app, captureFB);
app.getRenderer().readFrameBuffer(captureFB, pixels); ImpostorUtil.cleanup(app, captureVP, captureFB);
capturePixels[capturePass] = pixels; captureVP = null;
cleanupCapture(); captureFB = null;
captureReady[0] = false;
if (capturePass < ATLAS_DIRS - 1) { if (capturePass < ImpostorUtil.DIRS - 1) {
capturePass++; capturePass++;
input.treeGenStatusMsg = "EZ-Tree: rendere Impostor (" + (capturePass + 1) + "/" + ATLAS_DIRS + ")…"; input.treeGenStatusMsg = "EZ-Tree: rendere Impostor (" + (capturePass + 1) + "/" + ImpostorUtil.DIRS + ")…";
startCapturePass(capturePass, pendingHdNode, pendingBb); startCapturePass(capturePass);
return; return;
} }
@@ -418,7 +412,7 @@ public class EzTreeState extends BaseAppState {
pendingHdNode = null; pendingHdNode = null;
pendingLd1Node = null; pendingLd1Node = null;
pendingBb = null; pendingBb = null;
capturePixels = new ByteBuffer[ATLAS_DIRS]; capturePixels = new ByteBuffer[ImpostorUtil.DIRS];
String subPath = resolveSubPath(req.presetName()); String subPath = resolveSubPath(req.presetName());
String namePart = req.presetName() != null String namePart = req.presetName() != null
@@ -427,8 +421,8 @@ public class EzTreeState extends BaseAppState {
String timestamp = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now()); String timestamp = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
String exportName = namePart + "_" + timestamp; String exportName = namePart + "_" + timestamp;
ByteBuffer atlas = combineAtlas(savedPixels); ByteBuffer atlas = ImpostorUtil.combineAtlas(savedPixels);
Texture2D atlasT2d = saveImpostor(atlas, "ez_impostor_" + exportName, ATLAS_W, ATLAS_H); com.jme3.texture.Texture2D atlasT2d = ImpostorUtil.buildAtlasTexture(atlas);
Node lodRoot = assembleLodNode(req.presetName(), hdNode, ld1Node, bb, atlasT2d); Node lodRoot = assembleLodNode(req.presetName(), hdNode, ld1Node, bb, atlasT2d);
exportTree(lodRoot, exportName, subPath); 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 ───────────────────────────────────────────────────────── // ── Hilfsmethoden ─────────────────────────────────────────────────────────
private static BoundingBox boundsOf(Node node) { private static BoundingBox boundsOf(Node node) {
@@ -586,48 +506,17 @@ public class EzTreeState extends BaseAppState {
} }
private void cleanupCapture() { private void cleanupCapture() {
if (captureVP != null) { ImpostorUtil.cleanup(app, captureVP, captureFB);
app.getRenderManager().removePostView(captureVP); captureVP = null;
captureVP = null; captureFB = null;
} captureReady[0] = false;
if (captureFB != null) {
try { captureFB.dispose(); } catch (Exception ignored) {}
captureFB = null;
}
captureReady = false;
} }
private Texture2D saveImpostor(ByteBuffer pixels, String name, int width, int height) { private Node assembleLodNode(String name, Node hd, Node ld1, BoundingBox bb, com.jme3.texture.Texture2D atlasTex) {
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) {
Node root = new Node(name != null ? name : "ez_tree"); Node root = new Node(name != null ? name : "ez_tree");
root.attachChild(hd); root.attachChild(hd);
root.attachChild(ld1); root.attachChild(ld1);
Node lod2 = makeImpostorNode(bb, atlasTex); Node lod2 = ImpostorUtil.makeImpostorNode(bb, atlasTex, assets);
root.attachChild(lod2); root.attachChild(lod2);
hd.setCullHint(Spatial.CullHint.Inherit); hd.setCullHint(Spatial.CullHint.Inherit);
@@ -635,56 +524,11 @@ public class EzTreeState extends BaseAppState {
lod2.setCullHint(Spatial.CullHint.Always); lod2.setCullHint(Spatial.CullHint.Always);
lod2.setShadowMode(RenderQueue.ShadowMode.Off); lod2.setShadowMode(RenderQueue.ShadowMode.Off);
ImpostorUtil.addViewControl(lod2, app.getCamera());
root.addControl(new EzTreeLodControl(app.getCamera(), hd, ld1, lod2, 40f, 120f)); root.addControl(new EzTreeLodControl(app.getCamera(), hd, ld1, lod2, 40f, 120f));
return root; 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. * 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 * Blätter bekommen Gewicht 1.0 (volle Animation); Äste werden per Y-Position
@@ -778,10 +622,17 @@ public class EzTreeState extends BaseAppState {
@Override @Override
protected void controlUpdate(float tpf) { protected void controlUpdate(float tpf) {
float dSq = cam.getLocation().distanceSquared(spatial.getWorldTranslation()); float dSq = cam.getLocation().distanceSquared(spatial.getWorldTranslation());
lod0.setCullHint(dSq < d01sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); boolean atL2 = dSq >= d12sq;
lod1.setCullHint(dSq >= d01sq && dSq < d12sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); setLod(lod0, dSq < d01sq, !atL2);
lod2.setCullHint(dSq >= d12sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); 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, @Override protected void controlRender(com.jme3.renderer.RenderManager rm,

View File

@@ -13,6 +13,9 @@ import com.jme3.light.PointLight;
import com.jme3.material.Material; import com.jme3.material.Material;
import com.jme3.math.*; import com.jme3.math.*;
import com.jme3.renderer.Camera; 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.*;
import com.jme3.collision.CollisionResults; import com.jme3.collision.CollisionResults;
import com.jme3.math.Ray; import com.jme3.math.Ray;
@@ -23,6 +26,7 @@ import com.jme3.scene.shape.Sphere;
import com.jme3.anim.SkinningControl; import com.jme3.anim.SkinningControl;
import com.jme3.util.BufferUtils; import com.jme3.util.BufferUtils;
import de.blight.editor.SharedInput; import de.blight.editor.SharedInput;
import de.blight.editor.util.ImpostorUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -73,6 +77,8 @@ public class ModelEditorState extends BaseAppState {
/** Hauptlichtquelle der Vorschau (per UI steuerbar). */ /** Hauptlichtquelle der Vorschau (per UI steuerbar). */
private DirectionalLight mainLight = null; private DirectionalLight mainLight = null;
/** Kamera-folgendes Füll-Licht immer aus Sicht der Kamera. */
private DirectionalLight camLight = null;
// gespeicherter Kamerazustand aus dem Editor-Modus // gespeicherter Kamerazustand aus dem Editor-Modus
private Vector3f savedCamPos; private Vector3f savedCamPos;
@@ -98,6 +104,11 @@ public class ModelEditorState extends BaseAppState {
private boolean hasEmbeddedLods = false; private boolean hasEmbeddedLods = false;
private Spatial[] embeddedLodSpatials = null; 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) // Mittelpunkt für Orbit (Bounding-Box-Zentrum des Modells)
private Vector3f orbitCenter = Vector3f.ZERO.clone(); private Vector3f orbitCenter = Vector3f.ZERO.clone();
@@ -134,6 +145,9 @@ public class ModelEditorState extends BaseAppState {
return; return;
} }
// Welt sofort ausblenden, auch wenn noch kein Modell geladen ist
if (previewRoot == null) enterPreview();
// Modell laden // Modell laden
String openPath = input.modelEditorOpenPath; String openPath = input.modelEditorOpenPath;
if (openPath != null) { if (openPath != null) {
@@ -145,37 +159,54 @@ public class ModelEditorState extends BaseAppState {
loadModel(openPath); loadModel(openPath);
} }
int removeLod = input.modelEditorRemoveEmbeddedLod;
if (removeLod > 0) {
input.modelEditorRemoveEmbeddedLod = -1;
if (hasEmbeddedLods && embeddedLodSpatials != null) {
removeEmbeddedLod(removeLod);
}
}
if (input.modelEditorLodChanged) { if (input.modelEditorLodChanged) {
input.modelEditorLodChanged = false; 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 // Eingebettete LOD-Kinder direkt ein-/ausblenden
int lodIdx = Math.min(input.modelEditorLodPreview, embeddedLodSpatials.length - 1); int lodIdx = Math.min(input.modelEditorLodPreview, embeddedLodSpatials.length - 1);
for (int i = 0; i < embeddedLodSpatials.length; i++) { for (int i = 0; i < embeddedLodSpatials.length; i++) {
embeddedLodSpatials[i].setCullHint( if (embeddedLodSpatials[i] != null) {
i == lodIdx ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); boolean show = (i == lodIdx);
embeddedLodSpatials[i].setCullHint(show ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
embeddedLodSpatials[i].setShadowMode(RenderQueue.ShadowMode.Off);
}
} }
} else { } else {
// In-memory Preview-Node aus Importer hat Vorrang vor Dateipfaden // Pfad-basierter Fallback
Spatial previewNode = null; String path = switch (input.modelEditorLodPreview) {
if (input.modelEditorLodPreview == 1) case 1 -> input.modelEditorLod1Path;
previewNode = input.modelImportLod1PreviewNode.get(); case 2 -> input.modelEditorLod2Path;
else if (input.modelEditorLodPreview == 2) default -> mainModelPath;
previewNode = input.modelImportLod2PreviewNode.get(); };
if (path == null || path.isBlank()) {
if (previewNode != null) { path = mainModelPath;
showSpatialDirectly(previewNode.clone()); input.modelEditorLodPreview = 0;
} 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);
} }
if (path != null) loadModel(path);
} }
} }
@@ -305,6 +336,15 @@ public class ModelEditorState extends BaseAppState {
} }
applyOrbitCamera(); 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.updateLogicalState(tpf);
previewRoot.updateGeometricState(); previewRoot.updateGeometricState();
@@ -327,6 +367,8 @@ public class ModelEditorState extends BaseAppState {
hasEmbeddedLods = false; hasEmbeddedLods = false;
embeddedLodSpatials = null; embeddedLodSpatials = null;
input.modelEditorHasEmbeddedLods = false; input.modelEditorHasEmbeddedLods = false;
input.modelEditorHasEmbeddedLod1 = false;
input.modelEditorHasEmbeddedLod2 = false;
try { try {
// Cache vor dem Laden löschen verhindert, dass zwischengespeicherte // Cache vor dem Laden löschen verhindert, dass zwischengespeicherte
// Materialzustände (z.B. aus vorherigen Thumbnail-Renders) das erste // Materialzustände (z.B. aus vorherigen Thumbnail-Renders) das erste
@@ -335,6 +377,7 @@ public class ModelEditorState extends BaseAppState {
catch (Exception ignored) {} catch (Exception ignored) {}
Spatial model = app.getAssetManager().loadModel(assetPath); Spatial model = app.getAssetManager().loadModel(assetPath);
stripControls(model); stripControls(model);
initImpostorControls(model);
recalcNormalsIfMissing(model); recalcNormalsIfMissing(model);
markBuffersUpdateNeeded(model); markBuffersUpdateNeeded(model);
originalSpatial = model; originalSpatial = model;
@@ -350,6 +393,8 @@ public class ModelEditorState extends BaseAppState {
embeddedLodSpatials = children.toArray(new Spatial[0]); embeddedLodSpatials = children.toArray(new Spatial[0]);
hasEmbeddedLods = true; hasEmbeddedLods = true;
input.modelEditorHasEmbeddedLods = true; input.modelEditorHasEmbeddedLods = true;
input.modelEditorHasEmbeddedLod1 = embeddedLodSpatials.length > 1;
input.modelEditorHasEmbeddedLod2 = embeddedLodSpatials.length > 2;
} }
} }
} catch (Exception e) { } catch (Exception e) {
@@ -375,6 +420,22 @@ public class ModelEditorState extends BaseAppState {
input.modelEditorBakedScaleDetected = true; 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 // Skalierung aus SharedInput anwenden
applyScale(input.modelEditorScaleX, input.modelEditorScaleY, input.modelEditorScaleZ); applyScale(input.modelEditorScaleX, input.modelEditorScaleY, input.modelEditorScaleZ);
applyPivot(input.modelEditorPivotY); applyPivot(input.modelEditorPivotY);
@@ -391,6 +452,18 @@ public class ModelEditorState extends BaseAppState {
orbitCenter = bb.getCenter().clone(); 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 // Vergleichszylinder neu positionieren
repositionCompCylinder(); repositionCompCylinder();
@@ -409,6 +482,21 @@ public class ModelEditorState extends BaseAppState {
if (modelWrapper != null) previewRoot.detachChild(modelWrapper); if (modelWrapper != null) previewRoot.detachChild(modelWrapper);
modelWrapper = new Node("model_wrapper"); modelWrapper = new Node("model_wrapper");
stripControls(spatial); 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); modelWrapper.attachChild(spatial);
applyScale(input.modelEditorScaleX, input.modelEditorScaleY, input.modelEditorScaleZ); applyScale(input.modelEditorScaleX, input.modelEditorScaleY, input.modelEditorScaleZ);
applyPivot(input.modelEditorPivotY); applyPivot(input.modelEditorPivotY);
@@ -469,6 +557,8 @@ public class ModelEditorState extends BaseAppState {
computeLightDirection(input.modelEditorLightAzimuth, input.modelEditorLightElevation), computeLightDirection(input.modelEditorLightAzimuth, input.modelEditorLightElevation),
new ColorRGBA(1.8f, 1.7f, 1.4f, 1f)); new ColorRGBA(1.8f, 1.7f, 1.4f, 1f));
previewRoot.addLight(mainLight); 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( previewRoot.addLight(new DirectionalLight(
new Vector3f(0.6f, -0.4f, 0.7f).normalizeLocal(), new Vector3f(0.6f, -0.4f, 0.7f).normalizeLocal(),
new ColorRGBA(0.5f, 0.55f, 0.75f, 1f))); 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)); 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 = new Node("interactableArrow");
interactableArrowNode.setCullHint(Spatial.CullHint.Always); interactableArrowNode.setCullHint(Spatial.CullHint.Always);
previewRoot.attachChild(interactableArrowNode); previewRoot.attachChild(interactableArrowNode);
@@ -510,7 +604,13 @@ public class ModelEditorState extends BaseAppState {
embeddedLodSpatials = null; embeddedLodSpatials = null;
originalSpatial = null; originalSpatial = null;
interactableArrowNode = null; interactableArrowNode = null;
camLight = null;
mainLight = null;
treeLightMaterials.clear();
pbrMaterials.clear();
input.modelEditorHasEmbeddedLods = false; input.modelEditorHasEmbeddedLods = false;
input.modelEditorHasEmbeddedLod1 = false;
input.modelEditorHasEmbeddedLod2 = false;
app.getRootNode().setCullHint(Spatial.CullHint.Inherit); app.getRootNode().setCullHint(Spatial.CullHint.Inherit);
if (savedCamPos != null) { if (savedCamPos != null) {
@@ -521,6 +621,65 @@ public class ModelEditorState extends BaseAppState {
currentPath = null; 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 ──────────────────────────────────────────────────────────────── // ── Gitter ────────────────────────────────────────────────────────────────
private void rebuildGrid() { private void rebuildGrid() {
@@ -732,10 +891,23 @@ public class ModelEditorState extends BaseAppState {
} }
private static void stripControls(Spatial s) { 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); 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. * Berechnet Smooth-Normals immer neu aus der Geometrie.
* Behebt TripoAI-Modelle mit fehlenden, null oder nach innen zeigenden Normals. * Behebt TripoAI-Modelle mit fehlenden, null oder nach innen zeigenden Normals.

View File

@@ -12,11 +12,15 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.nio.ByteBuffer;
import com.jme3.app.Application; import com.jme3.app.Application;
import com.jme3.app.SimpleApplication; import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState; import com.jme3.app.state.BaseAppState;
import com.jme3.asset.AssetManager; import com.jme3.asset.AssetManager;
import com.jme3.export.binary.BinaryExporter; import com.jme3.export.binary.BinaryExporter;
import com.jme3.bounding.BoundingBox;
import com.jme3.material.Material;
import com.jme3.math.*; import com.jme3.math.*;
import com.jme3.renderer.Camera; import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager; import com.jme3.renderer.RenderManager;
@@ -25,9 +29,13 @@ import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.*; import com.jme3.scene.*;
import com.jme3.scene.VertexBuffer; import com.jme3.scene.VertexBuffer;
import com.jme3.scene.control.AbstractControl; 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 com.jme3.util.BufferUtils;
import de.blight.editor.SharedInput; import de.blight.editor.SharedInput;
import de.blight.editor.util.ImpostorUtil;
import de.blight.editor.util.MeshUtils; import de.blight.editor.util.MeshUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -55,6 +63,16 @@ public class ModelImportState extends BaseAppState {
private Node lod1Node = null; private Node lod1Node = null;
private Node lod2Node = 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) { public ModelImportState(SharedInput input) {
this.input = input; this.input = input;
} }
@@ -78,21 +96,51 @@ public class ModelImportState extends BaseAppState {
if (modelEditorState == null) return; 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); SharedInput.ModelLodGenRequest req = input.modelLodGenRequest.getAndSet(null);
if (req != null) { if (req != null) {
switch (req.level()) { float pct = req.reductionPct() > 0 ? req.reductionPct()
case 1 -> { : (req.level() == 1 ? 0.50f : 0.85f);
if ("auto_mesh".equals(req.method())) generateLodAuto(1, 0.5f); switch (req.method()) {
else if ("file".equals(req.method())) generateLodFromFile(1, req.fileAssetPath()); case "auto_mesh", "blight_auto", "jme_auto" -> {
input.modelLodAlgorithm = req.method().startsWith("jme") ? "jme" : "blight";
generateLodAuto(req.level(), pct);
} }
case 2 -> { case "auto_mesh_heavy" -> generateLodAuto(req.level(), pct);
if ("auto_mesh_heavy".equals(req.method())) generateLodAuto(2, 0.85f); case "file" -> generateLodFromFile(req.level(), req.fileAssetPath());
else if ("file".equals(req.method())) generateLodFromFile(2, req.fileAssetPath()); case "impostor" -> generateImpostorLod(req.level());
} default -> log.warn("[ModelImport] Unbekannte Methode: {}", req.method());
default -> log.warn("[ModelImport] Unbekannte LOD-Stufe: {}", req.level());
} }
} }
// Impostor: nächsten Frame abwarten und Pixel lesen
if (impostorLevel >= 0 && impostorReady[0] && impostorFB != null) {
finishImpostorPass();
}
String exportName = input.modelImportExportName; String exportName = input.modelImportExportName;
if (exportName != null) { if (exportName != null) {
performExport(exportName); 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 ──────────────────────────────────────────────────────────────── // ── Export ────────────────────────────────────────────────────────────────
private void performExport(String fileName) { private void performExport(String fileName) {
@@ -708,12 +838,17 @@ public class ModelImportState extends BaseAppState {
@Override @Override
protected void controlUpdate(float tpf) { protected void controlUpdate(float tpf) {
float dSq = cam.getLocation().distanceSquared(spatial.getWorldTranslation()); float dSq = cam.getLocation().distanceSquared(spatial.getWorldTranslation());
lod0.setCullHint(dSq < d01sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); boolean atLod2 = (lod2 != null && dSq >= d12sq);
if (lod1 != null) setLod(lod0, dSq < d01sq, !atLod2, RenderQueue.ShadowMode.CastAndReceive);
lod1.setCullHint(dSq >= d01sq && dSq < d12sq setLod(lod1, dSq >= d01sq && dSq < d12sq, !atLod2, RenderQueue.ShadowMode.CastAndReceive);
? Spatial.CullHint.Inherit : Spatial.CullHint.Always); setLod(lod2, atLod2, false, RenderQueue.ShadowMode.Off);
if (lod2 != null) }
lod2.setCullHint(dSq >= d12sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
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) {} @Override protected void controlRender(RenderManager rm, ViewPort vp) {}

View File

@@ -1616,7 +1616,7 @@ public class TerrainEditorState extends BaseAppState {
// Geschwindigkeit skaliert linear mit Abstand zum Terrain (näher = langsamer) // Geschwindigkeit skaliert linear mit Abstand zum Terrain (näher = langsamer)
float terrainDist = terrainDistBelow(); float terrainDist = terrainDistBelow();
float speed = FastMath.clamp(terrainDist, 5f, CAM_SPEED) * tpf; float speed = FastMath.clamp(terrainDist, 5f, CAM_SPEED) * tpf * (input.shiftHeld ? 5f : 1f);
if (input.camMode == SharedInput.CAM_FREEFLY) { if (input.camMode == SharedInput.CAM_FREEFLY) {
Vector3f fwd = cam.getDirection().clone(); Vector3f fwd = cam.getDirection().clone();

View File

@@ -46,6 +46,7 @@ import com.jme3.util.SkyFactory;
import de.blight.editor.FrameTransfer; import de.blight.editor.FrameTransfer;
import de.blight.editor.SharedInput; import de.blight.editor.SharedInput;
import de.blight.editor.util.ImpostorUtil;
import de.blight.editor.tree.TreeMeshBuilder; import de.blight.editor.tree.TreeMeshBuilder;
import de.blight.editor.tree.TreeParams; import de.blight.editor.tree.TreeParams;
import org.slf4j.Logger; 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 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 int PREVIEW_SIZE = 1024;
private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve("blight-assets", "src", "main", "resources"); 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 ViewPort captureVP = null;
private FrameBuffer captureFB = null; private FrameBuffer captureFB = null;
private Texture2D captureTex = null; private Texture2D captureTex = null;
private volatile boolean captureReady = false; private boolean[] captureReady = new boolean[]{false};
private int capturePass = 0; 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; } public TreeGeneratorState(SharedInput input) { this.input = input; }
@@ -191,7 +188,7 @@ public class TreeGeneratorState extends BaseAppState {
@Override @Override
public void update(float tpf) { public void update(float tpf) {
// 1. Szenen-Änderungen zuerst (bevor updateGeometricState) // 1. Szenen-Änderungen zuerst (bevor updateGeometricState)
if (pendingRequest != null && captureReady) { if (pendingRequest != null && captureReady[0]) {
finishCapture(); finishCapture();
} else if (pendingRequest == null) { } else if (pendingRequest == null) {
SharedInput.TreeGenRequest req = input.treeGenQueue.poll(); SharedInput.TreeGenRequest req = input.treeGenQueue.poll();
@@ -286,23 +283,25 @@ public class TreeGeneratorState extends BaseAppState {
pendingBarkMat = barkMat; pendingBarkMat = barkMat;
pendingLeafMat = leafMat; pendingLeafMat = leafMat;
capturePass = 0; capturePass = 0;
capturePixels = new ByteBuffer[ATLAS_DIRS]; capturePixels = new ByteBuffer[ImpostorUtil.DIRS];
startCapturePass(0); startCapturePass(0);
input.treeGenStatusMsg = "Rendere Impostor (1/" + ATLAS_DIRS + ")…"; input.treeGenStatusMsg = "Rendere Impostor (1/" + ImpostorUtil.DIRS + ")…";
} }
// ── Phase 2: Capture abschließen ────────────────────────────────────────── // ── Phase 2: Capture abschließen ──────────────────────────────────────────
private void finishCapture() { private void finishCapture() {
ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4); capturePixels[capturePass] = ImpostorUtil.readPixels(app, captureFB);
app.getRenderer().readFrameBuffer(captureFB, pixels); ImpostorUtil.cleanup(app, captureVP, captureFB);
capturePixels[capturePass] = pixels; captureVP = null;
cleanupCapture(); captureFB = null;
captureTex = null;
captureReady[0] = false;
if (capturePass < ATLAS_DIRS - 1) { if (capturePass < ImpostorUtil.DIRS - 1) {
capturePass++; capturePass++;
input.treeGenStatusMsg = "Rendere Impostor (" + (capturePass + 1) + "/" + ATLAS_DIRS + ")…"; input.treeGenStatusMsg = "Rendere Impostor (" + (capturePass + 1) + "/" + ImpostorUtil.DIRS + ")…";
startCapturePass(capturePass); startCapturePass(capturePass);
return; return;
} }
@@ -311,9 +310,8 @@ public class TreeGeneratorState extends BaseAppState {
String treeType = pendingRequest.treeType(); String treeType = pendingRequest.treeType();
String timestamp = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now()); String timestamp = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
ByteBuffer atlas = combineAtlas(capturePixels); ByteBuffer atlas = ImpostorUtil.combineAtlas(capturePixels);
Texture2D impostorTex = saveImpostor(atlas, "impostor_" + treeType + "_" + timestamp, Texture2D impostorTex = ImpostorUtil.buildAtlasTexture(atlas);
ATLAS_W, ATLAS_H);
Node previewTree = makeTreeNode(pendingHdResult, Node previewTree = makeTreeNode(pendingHdResult,
pendingBarkMat.clone(), pendingLeafMat.clone(), "prev"); pendingBarkMat.clone(), pendingLeafMat.clone(), "prev");
@@ -339,7 +337,7 @@ public class TreeGeneratorState extends BaseAppState {
pendingHdResult = null; pendingHdResult = null;
pendingBarkMat = null; pendingBarkMat = null;
pendingLeafMat = null; pendingLeafMat = null;
capturePixels = new ByteBuffer[ATLAS_DIRS]; capturePixels = new ByteBuffer[ImpostorUtil.DIRS];
} }
// ── LOD-Aufbau ──────────────────────────────────────────────────────────── // ── LOD-Aufbau ────────────────────────────────────────────────────────────
@@ -349,7 +347,7 @@ public class TreeGeneratorState extends BaseAppState {
root.attachChild(pendingHdNode); root.attachChild(pendingHdNode);
root.attachChild(pendingLdNode); root.attachChild(pendingLdNode);
Node lod2 = makeImpostorNode(pendingHdResult.bounds(), impostorTex); Node lod2 = ImpostorUtil.makeImpostorNode(pendingHdResult.bounds(), impostorTex, assets);
root.attachChild(lod2); root.attachChild(lod2);
// Nur LOD0 initial sichtbar; Control steuert je nach Distanz // Nur LOD0 initial sichtbar; Control steuert je nach Distanz
@@ -357,88 +355,21 @@ public class TreeGeneratorState extends BaseAppState {
lod2.setCullHint(Spatial.CullHint.Always); lod2.setCullHint(Spatial.CullHint.Always);
lod2.setShadowMode(RenderQueue.ShadowMode.Off); lod2.setShadowMode(RenderQueue.ShadowMode.Off);
ImpostorUtil.addViewControl(lod2, app.getCamera());
root.addControl(new TreeLodControl(app.getCamera(), root.addControl(new TreeLodControl(app.getCamera(),
pendingHdNode, pendingLdNode, lod2, 60f, 200f)); pendingHdNode, pendingLdNode, lod2, 60f, 200f));
return root; 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") @SuppressWarnings("deprecation")
private void startCapturePass(int pass) { private void startCapturePass(int pass) {
captureTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8); captureTex = new Texture2D(ImpostorUtil.SIZE, ImpostorUtil.SIZE, Image.Format.RGBA8);
captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1); captureFB = new FrameBuffer(ImpostorUtil.SIZE, ImpostorUtil.SIZE, 1);
captureFB.addColorTexture(captureTex); captureFB.addColorTexture(captureTex);
captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth)); captureFB.setDepthTexture(new Texture2D(ImpostorUtil.SIZE, ImpostorUtil.SIZE, Image.Format.Depth));
float angle = pass * FastMath.HALF_PI; captureReady[0] = false;
captureVP = buildCaptureViewPort(pendingHdNode, pendingHdResult.bounds(), captureFB, angle); captureVP = ImpostorUtil.startCapturePass(app, pendingHdNode, pendingHdResult.bounds(),
captureReady = false; captureFB, pass, captureReady);
} }
// ── Material-Factories ──────────────────────────────────────────────────── // ── Material-Factories ────────────────────────────────────────────────────
@@ -513,79 +444,6 @@ public class TreeGeneratorState extends BaseAppState {
return n; 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 ─────────────────────────────────────────────────────────── // ── .j3o-Export ───────────────────────────────────────────────────────────
private void exportTree(Node treeNode, String treeType, String timestamp, float height) { private void exportTree(Node treeNode, String treeType, String timestamp, float height) {
@@ -611,16 +469,11 @@ public class TreeGeneratorState extends BaseAppState {
// ── Aufräumen ───────────────────────────────────────────────────────────── // ── Aufräumen ─────────────────────────────────────────────────────────────
private void cleanupCapture() { private void cleanupCapture() {
if (captureVP != null) { ImpostorUtil.cleanup(app, captureVP, captureFB);
app.getRenderManager().removePostView(captureVP); captureVP = null;
captureVP = null; captureFB = null;
}
if (captureFB != null) {
try { captureFB.dispose(); } catch (Exception ignored) {}
captureFB = null;
}
captureTex = null; captureTex = null;
captureReady = false; captureReady[0] = false;
} }
// ── LOD-Control ─────────────────────────────────────────────────────────── // ── LOD-Control ───────────────────────────────────────────────────────────
@@ -639,10 +492,17 @@ public class TreeGeneratorState extends BaseAppState {
@Override @Override
protected void controlUpdate(float tpf) { protected void controlUpdate(float tpf) {
float dSq = cam.getLocation().distanceSquared(spatial.getWorldTranslation()); float dSq = cam.getLocation().distanceSquared(spatial.getWorldTranslation());
lod0.setCullHint(dSq < d01sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); boolean atL2 = dSq >= d12sq;
lod1.setCullHint(dSq>=d01sq && dSq<d12sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); setLod(lod0, dSq < d01sq, !atL2);
lod2.setCullHint(dSq >= d12sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); 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) {} @Override protected void controlRender(RenderManager rm, ViewPort vp) {}

View File

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

View File

@@ -203,7 +203,7 @@ public class BlightGame extends SimpleApplication {
// WorldScene erst vorbereiten, aber nicht aktivieren (onEnable lädt die Welt) // WorldScene erst vorbereiten, aber nicht aktivieren (onEnable lädt die Welt)
status("Initialisiere Welt..."); status("Initialisiere Welt...");
worldScene = new WorldScene(keyBindings); worldScene = new WorldScene(keyBindings, graphicsSettings);
worldScene.setEnabled(false); worldScene.setEnabled(false);
stateManager.attach(worldScene); stateManager.attach(worldScene);
@@ -268,6 +268,13 @@ public class BlightGame extends SimpleApplication {
}); });
stateManager.attach(console); stateManager.attach(console);
// ── Debug: Nebel-Toggle (F7) ─────────────────────────────────────────────
inputManager.addMapping("DebugFog", new KeyTrigger(KeyInput.KEY_F7));
inputManager.addListener((ActionListener) (name, isPressed, tpf) -> {
if (!isPressed) return;
worldScene.toggleDebugFog();
}, "DebugFog");
// ── Debug: Lighting-Toggle (F8) ────────────────────────────────────────── // ── Debug: Lighting-Toggle (F8) ──────────────────────────────────────────
inputManager.addMapping("DebugNoLight", new KeyTrigger(KeyInput.KEY_F8)); inputManager.addMapping("DebugNoLight", new KeyTrigger(KeyInput.KEY_F8));
inputManager.addListener((ActionListener) (name, isPressed, tpf) -> { inputManager.addListener((ActionListener) (name, isPressed, tpf) -> {

View File

@@ -4,6 +4,8 @@ import com.jme3.font.BitmapText;
import com.jme3.math.ColorRGBA; import com.jme3.math.ColorRGBA;
import com.jme3.scene.Node; import com.jme3.scene.Node;
import com.jme3.system.AppSettings; import com.jme3.system.AppSettings;
import de.blight.game.state.TerrainChunkState;
import de.blight.game.state.WeatherState;
import de.blight.lang.TextResolver; import de.blight.lang.TextResolver;
public class GraphicsScreen extends MenuScreen { public class GraphicsScreen extends MenuScreen {
@@ -13,11 +15,12 @@ public class GraphicsScreen extends MenuScreen {
}; };
private static final int[] SAMPLES = {0, 2, 4, 8}; private static final int[] SAMPLES = {0, 2, 4, 8};
private static final int ROW_RES = 0; private static final int ROW_RES = 0;
private static final int ROW_FULL = 1; private static final int ROW_FULL = 1;
private static final int ROW_VSYNC = 2; private static final int ROW_VSYNC = 2;
private static final int ROW_AA = 3; private static final int ROW_AA = 3;
private static final int ROW_COUNT = 4; private static final int ROW_VIEWDIST = 4;
private static final int ROW_COUNT = 5;
private final GraphicsSettings live; private final GraphicsSettings live;
private GraphicsSettings edit; private GraphicsSettings edit;
@@ -42,9 +45,10 @@ public class GraphicsScreen extends MenuScreen {
protected void onEnableExtras() { protected void onEnableExtras() {
edit = new GraphicsSettings(); edit = new GraphicsSettings();
edit.width = live.width; edit.height = live.height; edit.width = live.width; edit.height = live.height;
edit.fullscreen = live.fullscreen; edit.fullscreen = live.fullscreen;
edit.vsync = live.vsync; edit.vsync = live.vsync;
edit.samples = live.samples; edit.samples = live.samples;
edit.viewDistance = live.viewDistance;
resIdx = 0; resIdx = 0;
for (int i = 0; i < RESOLUTIONS.length; i++) { for (int i = 0; i < RESOLUTIONS.length; i++) {
@@ -66,7 +70,8 @@ public class GraphicsScreen extends MenuScreen {
"menu.graphics.row.resolution", "menu.graphics.row.resolution",
"menu.graphics.row.fullscreen", "menu.graphics.row.fullscreen",
"menu.graphics.row.vsync", "menu.graphics.row.vsync",
"menu.graphics.row.aa" "menu.graphics.row.aa",
"menu.graphics.row.viewdist"
}; };
float lblX = CONT_X + 30f; float lblX = CONT_X + 30f;
@@ -110,11 +115,16 @@ public class GraphicsScreen extends MenuScreen {
private void refreshText(int row) { private void refreshText(int row) {
String val = switch (row) { String val = switch (row) {
case ROW_RES -> RESOLUTIONS[resIdx][0] + "x" + RESOLUTIONS[resIdx][1]; case ROW_RES -> RESOLUTIONS[resIdx][0] + "x" + RESOLUTIONS[resIdx][1];
case ROW_FULL -> edit.fullscreen ? t("menu.graphics.val.on") : t("menu.graphics.val.off"); case ROW_FULL -> edit.fullscreen ? t("menu.graphics.val.on") : t("menu.graphics.val.off");
case ROW_VSYNC -> edit.vsync ? t("menu.graphics.val.on") : t("menu.graphics.val.off"); case ROW_VSYNC -> edit.vsync ? t("menu.graphics.val.on") : t("menu.graphics.val.off");
case ROW_AA -> SAMPLES[samplesIdx] == 0 case ROW_AA -> SAMPLES[samplesIdx] == 0
? t("menu.graphics.val.off") : SAMPLES[samplesIdx] + "x MSAA"; ? t("menu.graphics.val.off") : SAMPLES[samplesIdx] + "x MSAA";
case ROW_VIEWDIST -> switch (edit.viewDistance) {
case SHORT -> t("menu.graphics.val.viewdist.short");
case NORMAL -> t("menu.graphics.val.viewdist.normal");
case FAR -> t("menu.graphics.val.viewdist.far");
};
default -> ""; default -> "";
}; };
BitmapText vt = valTexts[row]; BitmapText vt = valTexts[row];
@@ -140,27 +150,47 @@ public class GraphicsScreen extends MenuScreen {
samplesIdx = (samplesIdx + dir + SAMPLES.length) % SAMPLES.length; samplesIdx = (samplesIdx + dir + SAMPLES.length) % SAMPLES.length;
edit.samples = SAMPLES[samplesIdx]; edit.samples = SAMPLES[samplesIdx];
break; break;
case ROW_VIEWDIST:
GraphicsSettings.ViewDistance[] vds = GraphicsSettings.ViewDistance.values();
edit.viewDistance = vds[(edit.viewDistance.ordinal() + dir + vds.length) % vds.length];
break;
} }
refreshText(row); refreshText(row);
} }
private void applyAndSave() { private void applyAndSave() {
live.width = edit.width; live.height = edit.height; boolean needsRestart = live.width != edit.width
live.fullscreen = edit.fullscreen; || live.height != edit.height
live.vsync = edit.vsync; || live.fullscreen != edit.fullscreen
live.samples = edit.samples; || live.vsync != edit.vsync
|| live.samples != edit.samples;
live.width = edit.width;
live.height = edit.height;
live.fullscreen = edit.fullscreen;
live.vsync = edit.vsync;
live.samples = edit.samples;
live.viewDistance = edit.viewDistance;
GraphicsStore.save(live); GraphicsStore.save(live);
AppSettings s = app.getContext().getSettings(); WeatherState ws = getApplication().getStateManager().getState(WeatherState.class);
s.setResolution(live.width, live.height); if (ws != null) ws.setViewDistanceFactor(live.viewDistance.fogFactor);
s.setFullscreen(live.fullscreen);
s.setBitsPerPixel(32); TerrainChunkState tcs = getApplication().getStateManager().getState(TerrainChunkState.class);
s.setVSync(live.vsync); if (tcs != null) tcs.setLodRanges(live.viewDistance.lod0Range, live.viewDistance.lod1Range);
s.setSamples(live.samples);
app.setSettings(s);
close(); close();
app.restart();
if (needsRestart) {
AppSettings s = app.getContext().getSettings();
s.setResolution(live.width, live.height);
s.setFullscreen(live.fullscreen);
s.setBitsPerPixel(32);
s.setVSync(live.vsync);
s.setSamples(live.samples);
app.setSettings(s);
app.restart();
}
} }
private void close() { private void close() {

View File

@@ -1,9 +1,26 @@
package de.blight.game.config; package de.blight.game.config;
public class GraphicsSettings { public class GraphicsSettings {
public int width = 1280; public int width = 1280;
public int height = 720; public int height = 720;
public boolean fullscreen = false; public boolean fullscreen = false;
public boolean vsync = false; public boolean vsync = false;
public int samples = 4; public int samples = 4;
public ViewDistance viewDistance = ViewDistance.NORMAL;
public enum ViewDistance {
SHORT (0.55f, 0, 2),
NORMAL(1.00f, 1, 3),
FAR (1.60f, 2, 5);
public final float fogFactor;
public final int lod0Range;
public final int lod1Range;
ViewDistance(float fogFactor, int lod0Range, int lod1Range) {
this.fogFactor = fogFactor;
this.lod0Range = lod0Range;
this.lod1Range = lod1Range;
}
}
} }

View File

@@ -0,0 +1,96 @@
package de.blight.game.post;
import com.jme3.asset.AssetManager;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector2f;
import com.jme3.post.Filter;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
/**
* Nebelfilter mit korrekter Tiefen-Linearisierung.
*
* JME3s eingebauter FogFilter setzt FrustumNearFar=[1, fogDistance], was bei
* camera.far=4000m dazu führt, dass alle Objekte jenseits von fogDistance
* praktisch den gleichen Nebelwert bekommen (NDC-z clustert um 1.0).
* Dieser Filter liest jeden Frame near/far aus der echten Kamera und rechnet
* korrekt zurück auf die tatsächliche Weltdistanz.
*/
public class BlightFogFilter extends Filter {
private float fogDensity = 0.40f;
private float fogDistance = 600f;
private ColorRGBA fogColor = new ColorRGBA(0.75f, 0.80f, 0.88f, 1f);
private Camera camera;
public BlightFogFilter() {
super("BlightFog");
}
// ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
protected boolean isRequiresDepthTexture() {
return true;
}
@Override
protected void initFilter(AssetManager manager, RenderManager rm,
ViewPort vp, int w, int h) {
camera = vp.getCamera();
material = new Material(manager, "MatDefs/BlightFog.j3md");
material.setColor("FogColor", fogColor.clone());
material.setFloat("FogDensity", fogDensity);
material.setFloat("FogDistance", fogDistance);
syncFrustum();
}
@Override
protected void preFrame(float tpf) {
syncFrustum();
}
@Override
protected Material getMaterial() {
return material;
}
// ── Setter (gleiche API wie FogFilter) ───────────────────────────────────
public void setFogDensity(float density) {
this.fogDensity = density;
if (material != null) {
material.setFloat("FogDensity", density);
}
}
public void setFogDistance(float distance) {
this.fogDistance = distance;
if (material != null) {
material.setFloat("FogDistance", distance);
}
}
public void setFogColor(ColorRGBA color) {
this.fogColor = color;
if (material != null) {
material.setColor("FogColor", color);
}
}
public float getFogDensity() { return fogDensity; }
public float getFogDistance() { return fogDistance; }
public ColorRGBA getFogColor() { return fogColor; }
// ── Intern ────────────────────────────────────────────────────────────────
private void syncFrustum() {
if (camera == null || material == null) {
return;
}
material.setVector2("FrustumNearFar",
new Vector2f(camera.getFrustumNear(), camera.getFrustumFar()));
}
}

View File

@@ -33,11 +33,12 @@ import de.blight.common.model.GameCharacter;
import de.blight.common.model.MainCharacter; import de.blight.common.model.MainCharacter;
import de.blight.game.BlightGame; import de.blight.game.BlightGame;
import de.blight.game.animation.AnimationLibrary; import de.blight.game.animation.AnimationLibrary;
import de.blight.game.config.GraphicsSettings;
import de.blight.game.config.KeyBindings; import de.blight.game.config.KeyBindings;
import de.blight.game.control.PlayerInputControl; import de.blight.game.control.PlayerInputControl;
import de.blight.game.control.ThirdPersonCamera; import de.blight.game.control.ThirdPersonCamera;
import com.jme3.post.FilterPostProcessor; import com.jme3.post.FilterPostProcessor;
import com.jme3.post.filters.FogFilter; import de.blight.game.post.BlightFogFilter;
import com.jme3.water.WaterFilter; import com.jme3.water.WaterFilter;
import de.blight.game.state.DynamicWaterFilter; import de.blight.game.state.DynamicWaterFilter;
import de.blight.game.state.WaterInteractionState; import de.blight.game.state.WaterInteractionState;
@@ -78,10 +79,12 @@ public class WorldScene extends BaseAppState {
private BulletAppState bulletAppState; private BulletAppState bulletAppState;
private MapData loadedMapData; private MapData loadedMapData;
private FilterPostProcessor sharedFPP; private FilterPostProcessor sharedFPP;
private BlightFogFilter fogFilter;
private TerrainChunkState terrainChunkState; private TerrainChunkState terrainChunkState;
private Material terrainMaterial; private Material terrainMaterial;
private final KeyBindings keyBindings; private final KeyBindings keyBindings;
private final GraphicsSettings graphicsSettings;
private ThirdPersonCamera thirdPersonCam; private ThirdPersonCamera thirdPersonCam;
private PlayerInputControl playerInput; private PlayerInputControl playerInput;
private InventoryState inventoryState; private InventoryState inventoryState;
@@ -101,8 +104,9 @@ public class WorldScene extends BaseAppState {
private de.blight.game.state.DrownState drownState; private de.blight.game.state.DrownState drownState;
private WaterInteractionState waterInteractionState; private WaterInteractionState waterInteractionState;
public WorldScene(KeyBindings keyBindings) { public WorldScene(KeyBindings keyBindings, GraphicsSettings graphicsSettings) {
this.keyBindings = keyBindings; this.keyBindings = keyBindings;
this.graphicsSettings = graphicsSettings;
} }
public InventoryState getInventoryState() { return inventoryState; } public InventoryState getInventoryState() { return inventoryState; }
@@ -246,7 +250,9 @@ public class WorldScene extends BaseAppState {
BlightGame.status("Lade Welt-Objekte..."); BlightGame.status("Lade Welt-Objekte...");
app.getStateManager().attach(new RiverState()); app.getStateManager().attach(new RiverState());
app.getStateManager().attach(new WorldObjectsState()); WorldObjectsState worldObjects = new WorldObjectsState();
worldObjects.setLodFactor(graphicsSettings.viewDistance.fogFactor);
app.getStateManager().attach(worldObjects);
app.getStateManager().attach(new WorldLightState(sharedFPP)); app.getStateManager().attach(new WorldLightState(sharedFPP));
app.getStateManager().attach(new StoneWorldState()); app.getStateManager().attach(new StoneWorldState());
@@ -466,6 +472,13 @@ public class WorldScene extends BaseAppState {
} }
} }
public void toggleDebugFog() {
if (fogFilter == null) return;
boolean nowEnabled = !fogFilter.isEnabled();
fogFilter.setEnabled(nowEnabled);
log.info("[Debug] Nebel {}", nowEnabled ? "AN" : "AUS");
}
@Override protected void cleanup(Application app) { @Override protected void cleanup(Application app) {
} }
@Override protected void onDisable() {} @Override protected void onDisable() {}
@@ -712,13 +725,14 @@ public class WorldScene extends BaseAppState {
WeatherState weather = new WeatherState(); WeatherState weather = new WeatherState();
weather.setWaterFilter(waterFilter); weather.setWaterFilter(waterFilter);
FogFilter fogFilter = new FogFilter(); fogFilter = new BlightFogFilter();
fogFilter.setFogColor(new ColorRGBA(0.75f, 0.80f, 0.88f, 1f)); fogFilter.setFogColor(new ColorRGBA(0.75f, 0.80f, 0.88f, 1f));
fogFilter.setFogDensity(0.0f); fogFilter.setFogDensity(0.40f);
fogFilter.setFogDistance(600f); fogFilter.setFogDistance(600f);
fpp.addFilter(fogFilter); fpp.addFilter(fogFilter);
weather.setFogFilter(fogFilter); weather.setFogFilter(fogFilter);
weather.setViewDistanceFactor(graphicsSettings.viewDistance.fogFactor);
weather.setSkyControl(dayNight.getSkyControl()); weather.setSkyControl(dayNight.getSkyControl());
app.getStateManager().attach(weather); app.getStateManager().attach(weather);
} catch (Exception e) { } catch (Exception e) {
@@ -775,6 +789,9 @@ public class WorldScene extends BaseAppState {
terrainMaterial = buildTerrainMaterial(loadedMapData); terrainMaterial = buildTerrainMaterial(loadedMapData);
terrainChunkState = new TerrainChunkState(bulletAppState, terrainMaterial, loadedMapData); terrainChunkState = new TerrainChunkState(bulletAppState, terrainMaterial, loadedMapData);
terrainChunkState.setLodRanges(
graphicsSettings.viewDistance.lod0Range,
graphicsSettings.viewDistance.lod1Range);
// Höhen vorab laden, damit getHeightAt() bereits hier (vor initialize()) korrekte Werte liefert // Höhen vorab laden, damit getHeightAt() bereits hier (vor initialize()) korrekte Werte liefert
terrainChunkState.loadChunkHeights(); terrainChunkState.loadChunkHeights();
app.getStateManager().attach(terrainChunkState); app.getStateManager().attach(terrainChunkState);

View File

@@ -10,48 +10,59 @@ import com.jme3.material.Material;
import com.jme3.material.RenderState; import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA; import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath; import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f; import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera; import com.jme3.renderer.Camera;
import com.jme3.renderer.queue.RenderQueue; import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry; import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node; import com.jme3.scene.Node;
import com.jme3.scene.Spatial; import com.jme3.scene.VertexBuffer;
import com.jme3.scene.shape.Quad; import com.jme3.util.BufferUtils;
/** /**
* Kompass unten links. Zeigt N/O/S/W-Labels, die um das Kompasszentrum * Kompass unten links runde rotierende Rose, identisch mit dem Editor-Kompass.
* rotieren wenn sich die Kamera dreht. Eine feste Markierung oben *
* zeigt immer die aktuelle Blickrichtung an. * Die Rose dreht sich mit dem Kamera-Yaw; die Buchstaben konter-rotieren,
* damit sie immer aufrecht bleiben. Ein festes goldenes Dreieck oben zeigt
* die aktuelle Blickrichtung an.
*/ */
public class CompassHudState extends BaseAppState { public class CompassHudState extends BaseAppState {
// ── Layout ──────────────────────────────────────────────────────────────── // ── Layout ────────────────────────────────────────────────────────────────
private static final float SIZE = 76f; // Kompass-Durchmesser (Pixel) private static final float SIZE = 100f; // Canvas-Größe (px)
private static final float RADIUS = 26f; // Label-Radius vom Mittelpunkt private static final float RADIUS = 47f; // Kreis-Außenradius
private static final float MARGIN = 16f; // Abstand zum Bildschirmrand private static final float LABEL_R = 30f; // Beschriftungsradius vom Mittelpunkt
private static final float LABEL_Z = 4f; private static final float MARGIN = 16f; // Abstand zum Bildschirmrand
private static final float DOT_SZ = 6f; // Vorwärts-Marker
private static final ColorRGBA COL_BG = new ColorRGBA(0.06f, 0.06f, 0.10f, 0.88f); // ── Farben (matching EditorApp.drawCompass) ───────────────────────────────
private static final ColorRGBA COL_BORDER = new ColorRGBA(0.30f, 0.30f, 0.44f, 1.00f); private static final ColorRGBA COL_BG = new ColorRGBA(0.047f, 0.047f, 0.094f, 0.82f);
private static final ColorRGBA COL_N = new ColorRGBA(0.90f, 0.20f, 0.20f, 1.00f); private static final ColorRGBA COL_BORDER = new ColorRGBA(0.392f, 0.392f, 0.627f, 0.85f);
private static final ColorRGBA COL_CARD = new ColorRGBA(0.75f, 0.75f, 0.78f, 1.00f); private static final ColorRGBA COL_TICK_MJ = new ColorRGBA(0.627f, 0.627f, 0.784f, 0.65f);
private static final ColorRGBA COL_MARKER = new ColorRGBA(1.00f, 0.90f, 0.20f, 1.00f); private static final ColorRGBA COL_TICK_MN = new ColorRGBA(0.627f, 0.627f, 0.784f, 0.45f);
private static final ColorRGBA COL_N = new ColorRGBA(1.00f, 0.314f, 0.314f, 1.00f);
private static final ColorRGBA COL_CARD = new ColorRGBA(0.824f, 0.824f, 0.902f, 1.00f);
private static final ColorRGBA COL_MARKER = new ColorRGBA(1.00f, 0.863f, 0.235f, 0.95f);
private static final ColorRGBA COL_CENTER = new ColorRGBA(0.784f, 0.784f, 0.941f, 0.85f);
// ── Himmelsrichtungen: Name, Winkel-Offset von Nord (Rad) ───────────────── // Kompasswinkel (Grad von Nord CW): N=0, O=90, S=180, W=270
private static final String[] LABELS = {"N", "O", "S", "W"}; private static final String[] LABELS = {"N", "O", "S", "W"};
private static final float[] OFFSETS = {0f, FastMath.HALF_PI, FastMath.PI, -FastMath.HALF_PI}; private static final float[] LABEL_DEG = {0f, 90f, 180f, 270f};
private SimpleApplication app; private SimpleApplication app;
private Camera cam; private Camera cam;
private AssetManager assets; private AssetManager assets;
private BitmapFont font; private BitmapFont font;
private Node compassNode;
private float cx, cy; // Mittelpunkt (Bildschirmkoordinaten)
private final BitmapText[] dirLabels = new BitmapText[4]; private Node compassNode;
private Node roseNode;
private final Node[] labelNodes = new Node[4];
private final Quaternion roseRot = new Quaternion();
private final Quaternion labelRot = new Quaternion();
// ── Lifecycle ───────────────────────────────────────────────────────────── // ── Lifecycle ─────────────────────────────────────────────────────────────
@Override @Override
protected void initialize(Application application) { protected void initialize(Application application) {
app = (SimpleApplication) application; app = (SimpleApplication) application;
@@ -70,6 +81,7 @@ public class CompassHudState extends BaseAppState {
if (compassNode != null) { if (compassNode != null) {
app.getGuiNode().detachChild(compassNode); app.getGuiNode().detachChild(compassNode);
compassNode = null; compassNode = null;
roseNode = null;
} }
} }
@@ -77,80 +89,173 @@ public class CompassHudState extends BaseAppState {
protected void cleanup(Application application) {} protected void cleanup(Application application) {}
// ── Update ──────────────────────────────────────────────────────────────── // ── Update ────────────────────────────────────────────────────────────────
@Override @Override
public void update(float tpf) { public void update(float tpf) {
if (compassNode == null) { return; } if (compassNode == null) { return; }
Vector3f dir = cam.getDirection(); Vector3f dir = cam.getDirection();
// Yaw vom Weltursprung: 0 = Blick nach +Z (Nord), π/2 = Osten (+X) // yaw=0 = Blick nach +Z (Nord); positiv = Osten
float yaw = FastMath.atan2(dir.x, dir.z); float yaw = FastMath.atan2(dir.x, dir.z);
for (int i = 0; i < 4; i++) { // Rose dreht sich so, dass Nord immer in der echten Nord-Richtung liegt
// Winkel des Labels im Kompass-Raum (von Oben CW = von +Y Achse) roseRot.fromAngleAxis(-yaw, Vector3f.UNIT_Z);
float a = -yaw + OFFSETS[i]; roseNode.setLocalRotation(roseRot);
float lx = cx + RADIUS * FastMath.sin(a);
float ly = cy + RADIUS * FastMath.cos(a); // Buchstaben konter-rotieren → bleiben immer aufrecht
BitmapText lbl = dirLabels[i]; labelRot.fromAngleAxis(yaw, Vector3f.UNIT_Z);
lbl.setLocalTranslation(lx - lbl.getLineWidth() * 0.5f, ly + lbl.getLineHeight() * 0.4f, LABEL_Z); for (Node ln : labelNodes) {
ln.setLocalRotation(labelRot);
} }
} }
// ── Aufbau ─────────────────────────────────────────────────────────────── // ── Aufbau ───────────────────────────────────────────────────────────────
private void buildCompass() {
float sw = cam.getWidth();
float sh = cam.getHeight();
// Kompass unten links private void buildCompass() {
float ox = MARGIN;
// Direkt über der Hotbar: Hotbar.MARGIN_BOT + SLOT_SIZE + Gap
float oy = HotbarState.MARGIN_BOT + HotbarState.SLOT_SIZE + 8f; float oy = HotbarState.MARGIN_BOT + HotbarState.SLOT_SIZE + 8f;
cx = ox + SIZE / 2f; float cx = MARGIN + SIZE / 2f;
cy = oy + SIZE / 2f; float cy = oy + SIZE / 2f;
compassNode = new Node("compass"); compassNode = new Node("compass");
compassNode.setLocalTranslation(cx, cy, 0f);
// Rahmen // Rahmen (etwas größerer Kreis) + Hintergrund
compassNode.attachChild(makeQuad(ox, oy, SIZE, SIZE, COL_BORDER, 1f)); compassNode.attachChild(makeCircle(RADIUS + 1.5f, 64, COL_BORDER, 1f));
// Hintergrund compassNode.attachChild(makeCircle(RADIUS, 64, COL_BG, 2f));
compassNode.attachChild(makeQuad(ox + 1, oy + 1, SIZE - 2, SIZE - 2, COL_BG, 2f));
// Vorwärts-Marker: kleine goldene Raute oben in der Mitte // Rotierende Rose
compassNode.attachChild(makeQuad(cx - DOT_SZ * 0.5f, cy + RADIUS - DOT_SZ * 0.5f, roseNode = new Node("rose");
DOT_SZ, DOT_SZ, COL_MARKER, 3f)); compassNode.attachChild(roseNode);
// Himmelsrichtungs-Labels (Positionen werden in update() gesetzt) // 8 Tick-Striche (major = Hauptrichtungen, minor = Zwischenrichtungen)
for (int i = 0; i < 4; i++) { for (int i = 0; i < 8; i++) {
ColorRGBA col = i == 0 ? COL_N : COL_CARD; float deg = i * 45f;
BitmapText lbl = txt(LABELS[i], 13, col); boolean maj = (i % 2 == 0);
lbl.setLocalTranslation(cx, cy, LABEL_Z); float innerR = RADIUS - (maj ? 9f : 5f);
dirLabels[i] = lbl; roseNode.attachChild(makeTickQuad(deg, innerR, maj ? COL_TICK_MJ : COL_TICK_MN, 3f));
compassNode.attachChild(lbl);
} }
// Himmelsrichtungs-Beschriftungen
for (int i = 0; i < 4; i++) {
float ca = LABEL_DEG[i] * FastMath.DEG_TO_RAD;
float lx = FastMath.sin(ca) * LABEL_R;
float ly = FastMath.cos(ca) * LABEL_R;
BitmapText lbl = makeBitmapText(LABELS[i], i == 0 ? 14 : 12, i == 0 ? COL_N : COL_CARD);
// Buchstabe am Label-Ursprung zentrieren
lbl.setLocalTranslation(-lbl.getLineWidth() * 0.5f, lbl.getLineHeight() * 0.4f, 0f);
Node ln = new Node("lbl_" + LABELS[i]);
ln.setLocalTranslation(lx, ly, 4f);
ln.attachChild(lbl);
labelNodes[i] = ln;
roseNode.attachChild(ln);
}
// Fixer Richtungs-Zeiger: gelbes Dreieck oben (nicht in roseNode)
compassNode.attachChild(makeTriangle(COL_MARKER, 5f));
// Mittelpunkt-Punkt
compassNode.attachChild(makeCircle(2.5f, 16, COL_CENTER, 6f));
app.getGuiNode().attachChild(compassNode); app.getGuiNode().attachChild(compassNode);
} }
// ── Hilfsmethoden ──────────────────────────────────────────────────────── // ── Geometry-Helfer ───────────────────────────────────────────────────────
private Geometry makeQuad(float x, float y, float w, float h, ColorRGBA col, float z) {
Geometry g = new Geometry("q", new Quad(w, h)); /** Gefüllter Kreis (Triangle-Fan) zentriert bei (0,0). */
private Geometry makeCircle(float r, int segs, ColorRGBA col, float z) {
float[] verts = new float[(segs + 2) * 3];
// Mittelpunkt
verts[0] = 0f; verts[1] = 0f; verts[2] = 0f;
for (int i = 0; i <= segs; i++) {
float a = FastMath.TWO_PI * i / segs;
int base = (i + 1) * 3;
verts[base] = FastMath.cos(a) * r;
verts[base + 1] = FastMath.sin(a) * r;
verts[base + 2] = 0f;
}
int[] idx = new int[segs * 3];
for (int i = 0; i < segs; i++) {
idx[i * 3] = 0;
idx[i * 3 + 1] = i + 1;
idx[i * 3 + 2] = i + 2;
}
Geometry g = new Geometry("circle", buildMesh(verts, idx));
g.setMaterial(makeMat(col));
g.setLocalTranslation(0f, 0f, z);
g.setQueueBucket(col.a < 1f ? RenderQueue.Bucket.Transparent : RenderQueue.Bucket.Gui);
return g;
}
/**
* Tick-Strich als dünnes Quad entlang der radialen Richtung.
* ca_deg: Kompasswinkel (0=Nord, 90=Ost, CW).
*/
private Geometry makeTickQuad(float ca_deg, float innerR, ColorRGBA col, float z) {
float ca = ca_deg * FastMath.DEG_TO_RAD;
float sx = FastMath.sin(ca); // Richtung entlang des Radius
float cy_ = FastMath.cos(ca);
float px = -cy_ * 0.75f; // Halbe Breite (senkrecht zur Radiale)
float py = sx * 0.75f;
float[] v = {
sx * innerR + px, cy_ * innerR + py, z, // v0 links innen
sx * innerR - px, cy_ * innerR - py, z, // v1 rechts innen
sx * RADIUS + px, cy_ * RADIUS + py, z, // v2 links außen
sx * RADIUS - px, cy_ * RADIUS - py, z // v3 rechts außen
};
int[] idx = {0, 1, 2, 1, 3, 2};
Geometry g = new Geometry("tick", buildMesh(v, idx));
g.setMaterial(makeMat(col));
g.setQueueBucket(RenderQueue.Bucket.Transparent);
return g;
}
/**
* Festes gelbes Dreieck zeigt immer nach oben (Blickrichtung).
* Spitze bei (0, RADIUS5), Basis bei (±5, RADIUS16).
*/
private Geometry makeTriangle(ColorRGBA col, float z) {
float tipY = RADIUS - 5f;
float baseY = RADIUS - 16f;
float[] v = {
0f, tipY, 0f, // Spitze
-5f, baseY, 0f, // Basis links
5f, baseY, 0f // Basis rechts
};
// CCW von +Z-Seite: (0,high)→(-5,low)→(5,low) ✓
int[] idx = {0, 1, 2};
Geometry g = new Geometry("triangle", buildMesh(v, idx));
g.setMaterial(makeMat(col));
g.setLocalTranslation(0f, 0f, z);
g.setQueueBucket(RenderQueue.Bucket.Transparent);
return g;
}
private static Mesh buildMesh(float[] verts, int[] idx) {
Mesh m = new Mesh();
m.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(verts));
m.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createIntBuffer(idx));
m.updateBound();
return m;
}
private Material makeMat(ColorRGBA col) {
Material m = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); Material m = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
m.setColor("Color", col.clone()); m.setColor("Color", col.clone());
if (col.a < 1f) { if (col.a < 1f) {
m.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); m.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
g.setQueueBucket(RenderQueue.Bucket.Transparent);
} else {
g.setQueueBucket(RenderQueue.Bucket.Gui);
} }
g.setMaterial(m); return m;
g.setLocalTranslation(x, y, z);
return g;
} }
private BitmapText txt(String s, int size, ColorRGBA col) { private BitmapText makeBitmapText(String text, int size, ColorRGBA col) {
BitmapText t = new BitmapText(font); BitmapText t = new BitmapText(font);
t.setSize(size); t.setSize(size);
t.setColor(col); t.setColor(col.clone());
t.setText(s); t.setText(text);
t.setQueueBucket(RenderQueue.Bucket.Gui); t.setQueueBucket(RenderQueue.Bucket.Gui);
return t; return t;
} }

View File

@@ -1,12 +1,15 @@
package de.blight.game.state; package de.blight.game.state;
import com.jme3.asset.AssetManager; import com.jme3.asset.AssetManager;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera; import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager; import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort; import com.jme3.renderer.ViewPort;
import com.jme3.scene.Node; import com.jme3.scene.Node;
import com.jme3.scene.Spatial; import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl; import com.jme3.scene.control.AbstractControl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* Wechselt zwischen LOD-Stufen und blendet Objekte ab einer Sichtweite aus. * Wechselt zwischen LOD-Stufen und blendet Objekte ab einer Sichtweite aus.
@@ -26,6 +29,8 @@ import com.jme3.scene.control.AbstractControl;
*/ */
public class ModelLodControl extends AbstractControl { public class ModelLodControl extends AbstractControl {
private static final Logger log = LoggerFactory.getLogger(ModelLodControl.class);
private final Camera cam; private final Camera cam;
private final float lod1DistSq; private final float lod1DistSq;
private final float lod2DistSq; private final float lod2DistSq;
@@ -93,9 +98,19 @@ public class ModelLodControl extends AbstractControl {
@Override @Override
protected void controlUpdate(float tpf) { protected void controlUpdate(float tpf) {
float dx = cam.getLocation().x - spatial.getWorldTranslation().x; // getWorldTranslation() liefert auf dem ersten Frame (0,0,0) solange updateGeometricState()
float dy = cam.getLocation().y - spatial.getWorldTranslation().y; // noch nicht gelaufen ist. Deshalb: wenn Translation noch (0,0,0) ist und der spatial
float dz = cam.getLocation().z - spatial.getWorldTranslation().z; // eine LocalTranslation hat, die verschieden ist, überspringen wir diesen Frame.
Vector3f wt = spatial.getWorldTranslation();
Vector3f lt = spatial.getLocalTranslation();
if (wt.x == 0f && wt.y == 0f && wt.z == 0f
&& (lt.x != 0f || lt.y != 0f || lt.z != 0f)) {
return;
}
float dx = cam.getLocation().x - wt.x;
float dy = cam.getLocation().y - wt.y;
float dz = cam.getLocation().z - wt.z;
float distSq = dx*dx + dy*dy + dz*dz; float distSq = dx*dx + dy*dy + dz*dz;
boolean embedded = embLod0 != null; boolean embedded = embLod0 != null;
@@ -114,6 +129,9 @@ public class ModelLodControl extends AbstractControl {
} }
if (targetSlot == currentSlot) return; if (targetSlot == currentSlot) return;
log.debug("[LOD] {} Slot {} → {} (dist={}m)",
spatial.getName(), currentSlot, targetSlot,
String.format("%.1f", Math.sqrt(distSq)));
currentSlot = targetSlot; currentSlot = targetSlot;
if (targetSlot == -1) { if (targetSlot == -1) {

View File

@@ -72,9 +72,19 @@ public class TerrainChunkState extends BaseAppState {
private final RigidBodyControl[] physics = new RigidBodyControl[TOTAL]; private final RigidBodyControl[] physics = new RigidBodyControl[TOTAL];
private final List<ChunkListener> listeners = new ArrayList<>(); private final List<ChunkListener> listeners = new ArrayList<>();
private int lod0Range = ChunkTerrainIO.LOD0_RANGE;
private int lod1Range = ChunkTerrainIO.LOD1_RANGE;
private int lastPlayerCx = Integer.MIN_VALUE; private int lastPlayerCx = Integer.MIN_VALUE;
private int lastPlayerCz = Integer.MIN_VALUE; private int lastPlayerCz = Integer.MIN_VALUE;
public void setLodRanges(int lod0, int lod1) {
this.lod0Range = lod0;
this.lod1Range = lod1;
lastPlayerCx = Integer.MIN_VALUE;
lastPlayerCz = Integer.MIN_VALUE;
}
/** Beim nächsten update() diese Position statt Kamera-Position verwenden (einmalig). */ /** Beim nächsten update() diese Position statt Kamera-Position verwenden (einmalig). */
private float spawnHintX = Float.NaN; private float spawnHintX = Float.NaN;
private float spawnHintZ = Float.NaN; private float spawnHintZ = Float.NaN;
@@ -184,7 +194,7 @@ public class TerrainChunkState extends BaseAppState {
for (int cx = 0; cx < N; cx++) { for (int cx = 0; cx < N; cx++) {
int ci = ChunkTerrainIO.chunkIndex(cx, cz); int ci = ChunkTerrainIO.chunkIndex(cx, cz);
int dist = ChunkTerrainIO.chebyshev(cx, cz, pcx, pcz); int dist = ChunkTerrainIO.chebyshev(cx, cz, pcx, pcz);
targetLod[ci] = ChunkTerrainIO.lodForDistance(dist); targetLod[ci] = dist <= lod0Range ? 0 : dist <= lod1Range ? 1 : 2;
dirty[ci] = targetLod[ci] != chunkLod[ci]; dirty[ci] = targetLod[ci] != chunkLod[ci];
} }
} }

View File

@@ -6,7 +6,7 @@ import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath; import com.jme3.math.FastMath;
import com.jme3.math.Vector2f; import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f; import com.jme3.math.Vector3f;
import com.jme3.post.filters.FogFilter; import de.blight.game.post.BlightFogFilter;
import com.jme3.water.WaterFilter; import com.jme3.water.WaterFilter;
import jme3utilities.sky.SkyControl; import jme3utilities.sky.SkyControl;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -16,37 +16,40 @@ public class WeatherState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(WeatherState.class); private static final Logger log = LoggerFactory.getLogger(WeatherState.class);
public enum Weather { SUNNY, CLOUDY, OVERCAST, STORM } public enum Weather { SUNNY, CLOUDY, OVERCAST, STORM, FOG }
// ── Per-weather targets (Reihenfolge: SUNNY, CLOUDY, OVERCAST, STORM) ──── // ── Per-weather targets (Reihenfolge: SUNNY, CLOUDY, OVERCAST, STORM, FOG)
private static final float[] FOG_DENSITY = { 0.00f, 0.30f, 0.65f, 0.90f }; private static final float[] FOG_DENSITY = { 0.40f, 0.55f, 0.75f, 0.90f, 0.88f };
private static final float[] FOG_DISTANCE = { 600f, 350f, 140f, 50f }; private static final float[] FOG_DISTANCE = { 600f, 350f, 140f, 50f, 80f };
private static final float[] WIND_SPEED = { 4f, 14f, 26f, 55f }; private static final float[] WIND_SPEED = { 4f, 14f, 26f, 55f, 3f };
private static final float[] WAVE_SPEED = { 0.5f, 1.0f, 1.5f, 3.2f }; private static final float[] WAVE_SPEED = { 0.5f, 1.0f, 1.5f, 3.2f, 0.4f };
private static final float[] WAVE_AMP = { 0.3f, 0.5f, 0.8f, 1.8f }; private static final float[] WAVE_AMP = { 0.3f, 0.5f, 0.8f, 1.8f, 0.2f };
private static final float[] WAVE_SCALE = { 0.008f, 0.007f, 0.006f, 0.005f}; private static final float[] WAVE_SCALE = { 0.008f, 0.007f, 0.006f, 0.005f, 0.009f};
private static final float[] WATER_TRANS = { 0.15f, 0.10f, 0.07f, 0.02f }; private static final float[] WATER_TRANS = { 0.15f, 0.10f, 0.07f, 0.02f, 0.12f };
private static final float[] FOAM_INTENSITY= { 0.0f, 0.20f, 0.45f, 0.90f }; private static final float[] FOAM_INTENSITY= { 0.0f, 0.20f, 0.45f, 0.90f, 0.0f };
private static final float[] CLOUD_OPACITY = { 0.0f, 0.40f, 0.80f, 1.00f }; private static final float[] CLOUD_OPACITY = { 0.0f, 0.40f, 0.80f, 1.00f, 0.95f };
private static final ColorRGBA[] FOG_COLOR = { private static final ColorRGBA[] FOG_COLOR = {
new ColorRGBA(0.75f, 0.80f, 0.88f, 1f), new ColorRGBA(0.75f, 0.80f, 0.88f, 1f),
new ColorRGBA(0.62f, 0.65f, 0.70f, 1f), new ColorRGBA(0.62f, 0.65f, 0.70f, 1f),
new ColorRGBA(0.42f, 0.43f, 0.46f, 1f), new ColorRGBA(0.42f, 0.43f, 0.46f, 1f),
new ColorRGBA(0.18f, 0.19f, 0.21f, 1f), new ColorRGBA(0.18f, 0.19f, 0.21f, 1f),
new ColorRGBA(0.70f, 0.72f, 0.75f, 1f),
}; };
private static final ColorRGBA[] WATER_COLOR = { private static final ColorRGBA[] WATER_COLOR = {
new ColorRGBA(0.05f, 0.25f, 0.55f, 1f), new ColorRGBA(0.05f, 0.25f, 0.55f, 1f),
new ColorRGBA(0.04f, 0.18f, 0.42f, 1f), new ColorRGBA(0.04f, 0.18f, 0.42f, 1f),
new ColorRGBA(0.03f, 0.12f, 0.28f, 1f), new ColorRGBA(0.03f, 0.12f, 0.28f, 1f),
new ColorRGBA(0.02f, 0.06f, 0.14f, 1f), new ColorRGBA(0.02f, 0.06f, 0.14f, 1f),
new ColorRGBA(0.04f, 0.20f, 0.45f, 1f),
}; };
private static final ColorRGBA[] DEEP_WATER_COLOR = { private static final ColorRGBA[] DEEP_WATER_COLOR = {
new ColorRGBA(0.02f, 0.12f, 0.30f, 1f), new ColorRGBA(0.02f, 0.12f, 0.30f, 1f),
new ColorRGBA(0.01f, 0.08f, 0.20f, 1f), new ColorRGBA(0.01f, 0.08f, 0.20f, 1f),
new ColorRGBA(0.01f, 0.04f, 0.12f, 1f), new ColorRGBA(0.01f, 0.04f, 0.12f, 1f),
new ColorRGBA(0.00f, 0.02f, 0.06f, 1f), new ColorRGBA(0.00f, 0.02f, 0.06f, 1f),
new ColorRGBA(0.01f, 0.09f, 0.23f, 1f),
}; };
// ── State ──────────────────────────────────────────────────────────────── // ── State ────────────────────────────────────────────────────────────────
@@ -54,7 +57,7 @@ public class WeatherState extends BaseAppState {
private Weather active = Weather.SUNNY; private Weather active = Weather.SUNNY;
private float changeTimer = 120f; private float changeTimer = 120f;
private float fogDensity = 0f; private float fogDensity = 0.40f; // Startwert = SUNNY-Ziel, kein langsames Fade-in
private float fogDistance = 600f; private float fogDistance = 600f;
private float windSpeed = 4f; private float windSpeed = 4f;
private float waveSpeed = 0.5f; private float waveSpeed = 0.5f;
@@ -69,13 +72,19 @@ public class WeatherState extends BaseAppState {
private final ColorRGBA waterColor = new ColorRGBA(0.05f, 0.25f, 0.55f, 1f); private final ColorRGBA waterColor = new ColorRGBA(0.05f, 0.25f, 0.55f, 1f);
private final ColorRGBA deepWaterColor= new ColorRGBA(0.02f, 0.12f, 0.30f, 1f); private final ColorRGBA deepWaterColor= new ColorRGBA(0.02f, 0.12f, 0.30f, 1f);
// ── Sichtweiten-Faktor (aus Grafikeinstellungen) ──────────────────────────
private float viewDistanceFactor = 1.0f;
public void setViewDistanceFactor(float f) { this.viewDistanceFactor = f; }
// ── Externe Referenzen ──────────────────────────────────────────────────── // ── Externe Referenzen ────────────────────────────────────────────────────
private FogFilter fogFilter; private BlightFogFilter fogFilter;
private WaterFilter waterFilter; private WaterFilter waterFilter;
private SkyControl skyControl; private SkyControl skyControl;
public void setFogFilter(FogFilter f) { this.fogFilter = f; } public void setFogFilter(BlightFogFilter f) { this.fogFilter = f; }
public void setWaterFilter(WaterFilter f) { this.waterFilter = f; } public void setWaterFilter(WaterFilter f) { this.waterFilter = f; }
public void setSkyControl(SkyControl sc) { this.skyControl = sc; } public void setSkyControl(SkyControl sc) { this.skyControl = sc; }
@@ -114,8 +123,8 @@ public class WeatherState extends BaseAppState {
int i = active.ordinal(); int i = active.ordinal();
fogDensity = approach(fogDensity, FOG_DENSITY[i], tpf * 0.025f); fogDensity = approach(fogDensity, FOG_DENSITY[i], tpf * 0.025f);
fogDistance = approach(fogDistance, FOG_DISTANCE[i], tpf * 0.025f); fogDistance = approach(fogDistance, FOG_DISTANCE[i] * viewDistanceFactor, tpf * 0.025f);
windSpeed = approach(windSpeed, WIND_SPEED[i], tpf * 0.040f); windSpeed = approach(windSpeed, WIND_SPEED[i], tpf * 0.040f);
waveSpeed = approach(waveSpeed, WAVE_SPEED[i], tpf * 0.030f); waveSpeed = approach(waveSpeed, WAVE_SPEED[i], tpf * 0.030f);
waveAmp = approach(waveAmp, WAVE_AMP[i], tpf * 0.025f); waveAmp = approach(waveAmp, WAVE_AMP[i], tpf * 0.025f);
@@ -156,10 +165,11 @@ public class WeatherState extends BaseAppState {
private void pickNextWeather() { private void pickNextWeather() {
float r = FastMath.nextRandomFloat(); float r = FastMath.nextRandomFloat();
Weather next = r < 0.40f ? Weather.SUNNY Weather next = r < 0.38f ? Weather.SUNNY
: r < 0.70f ? Weather.CLOUDY : r < 0.65f ? Weather.CLOUDY
: r < 0.88f ? Weather.OVERCAST : r < 0.82f ? Weather.OVERCAST
: Weather.STORM; : r < 0.92f ? Weather.STORM
: Weather.FOG;
windAngleTgt = FastMath.nextRandomFloat() * FastMath.TWO_PI; windAngleTgt = FastMath.nextRandomFloat() * FastMath.TWO_PI;
if (next != active) { if (next != active) {
active = next; active = next;

View File

@@ -14,6 +14,7 @@ import com.jme3.material.RenderState;
import com.jme3.math.*; import com.jme3.math.*;
import com.jme3.renderer.queue.RenderQueue; import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.*; import com.jme3.scene.*;
import com.jme3.scene.control.BillboardControl;
import com.jme3.scene.shape.*; import com.jme3.scene.shape.*;
import com.jme3.texture.Texture; import com.jme3.texture.Texture;
import de.blight.common.PlacedModel; import de.blight.common.PlacedModel;
@@ -38,6 +39,9 @@ public class WorldObjectsState extends BaseAppState {
private final List<Material> windMaterials = new ArrayList<>(); private final List<Material> windMaterials = new ArrayList<>();
private final List<Material> pbrMaterials = new ArrayList<>(); private final List<Material> pbrMaterials = new ArrayList<>();
private float debugTimer = 0f; private float debugTimer = 0f;
private float lodFactor = 1.0f;
public void setLodFactor(float f) { this.lodFactor = f; }
/** RigidBodyControl pro Interactable-ID, damit Kollision während Animationen deaktiviert werden kann. */ /** RigidBodyControl pro Interactable-ID, damit Kollision während Animationen deaktiviert werden kann. */
private final Map<String, RigidBodyControl> interactableRbcs = new HashMap<>(); private final Map<String, RigidBodyControl> interactableRbcs = new HashMap<>();
@@ -192,22 +196,41 @@ public class WorldObjectsState extends BaseAppState {
// LOD / Distance-Culling // LOD / Distance-Culling
if (m.cullDistance() > 0f) { if (m.cullDistance() > 0f) {
// Embedded-LOD: j3o enthält bereits lod0/lod1/lod2 Child-Nodes (TreeGenerator).
// TreeLodControl im j3o ist editor-only und wird im Spiel nicht deserialisiert —
// wir übernehmen die LOD-Steuerung selbst, direkt am j3o-Root-Node.
if (spatial instanceof Node treeNode) { if (spatial instanceof Node treeNode) {
Spatial lod0 = treeNode.getChild("lod0"); Spatial lod0 = treeNode.getChild("lod0");
Spatial lod1 = treeNode.getChild("lod1"); Spatial lod1 = treeNode.getChild("lod1");
Spatial lod2 = treeNode.getChild("lod2"); Spatial lod2 = treeNode.getChild("lod2");
// Fall A: j3o hat explizite lod0/lod1/lod2 Child-Nodes.
if (lod0 != null) { if (lod0 != null) {
lod0.setCullHint(Spatial.CullHint.Inherit); lod0.setCullHint(Spatial.CullHint.Inherit);
if (lod1 != null) lod1.setCullHint(Spatial.CullHint.Always); if (lod1 != null) lod1.setCullHint(Spatial.CullHint.Always);
if (lod2 != null) lod2.setCullHint(Spatial.CullHint.Always); if (lod2 != null) lod2.setCullHint(Spatial.CullHint.Always);
treeNode.addControl(new ModelLodControl( treeNode.addControl(new ModelLodControl(
app.getCamera(), lod0, lod1, lod2, app.getCamera(), lod0, lod1, lod2,
m.lod1Distance(), m.lod2Distance(), m.cullDistance())); m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor, m.cullDistance() * lodFactor));
return treeNode; return treeNode;
} }
// Fall B: j3o hat nur "lod2" als Kind — Root ist lod0, lod2 ist das vereinfachte Mesh.
// lod2 aus dem Root ausgliedern, damit beide unabhängig ein/ausgeblendet werden können.
if (lod2 != null) {
treeNode.detachChild(lod2);
Node lodRoot = new Node("lodRoot_" + path);
lodRoot.attachChild(treeNode); // index 0 = lod0 (volle Qualität)
lodRoot.attachChild(lod2); // index 1 = lod2 (Impostor-Karte)
treeNode.setCullHint(Spatial.CullHint.Inherit);
lod2.setCullHint(Spatial.CullHint.Always);
// Billboard: Impostor-Karte dreht sich immer zur Kamera,
// unabhängig von der Baum-Rotation im BLO.
BillboardControl bc = new BillboardControl();
bc.setAlignment(BillboardControl.Alignment.Camera);
lod2.addControl(bc);
lodRoot.addControl(new ModelLodControl(
app.getCamera(), treeNode, null, lod2,
m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor, m.cullDistance() * lodFactor));
return lodRoot;
}
} }
// Fallback: externe LOD-Dateien (lod1Path / lod2Path) // Fallback: externe LOD-Dateien (lod1Path / lod2Path)
@@ -216,7 +239,7 @@ public class WorldObjectsState extends BaseAppState {
ModelLodControl ctrl = new ModelLodControl( ModelLodControl ctrl = new ModelLodControl(
assets, app.getCamera(), assets, app.getCamera(),
m.lod1Path(), m.lod2Path(), m.lod1Path(), m.lod2Path(),
m.lod1Distance(), m.lod2Distance(), m.cullDistance()); m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor, m.cullDistance() * lodFactor);
ctrl.setLodLoadCallback(lod -> { ctrl.setLodLoadCallback(lod -> {
convertUnshadedToPbr(lod); convertUnshadedToPbr(lod);
collectSceneLitMaterials(lod); collectSceneLitMaterials(lod);

View File

@@ -48,8 +48,12 @@ menu.graphics.row.resolution=Auflösung
menu.graphics.row.fullscreen=Vollbild menu.graphics.row.fullscreen=Vollbild
menu.graphics.row.vsync=VSync menu.graphics.row.vsync=VSync
menu.graphics.row.aa=Kantenglättung menu.graphics.row.aa=Kantenglättung
menu.graphics.row.viewdist=Sichtweite
menu.graphics.val.on=An menu.graphics.val.on=An
menu.graphics.val.off=Aus menu.graphics.val.off=Aus
menu.graphics.val.viewdist.short=Kurz
menu.graphics.val.viewdist.normal=Normal
menu.graphics.val.viewdist.far=Weit
menu.graphics.btn.apply=Übernehmen menu.graphics.btn.apply=Übernehmen
menu.graphics.btn.cancel=Abbrechen menu.graphics.btn.cancel=Abbrechen

View File

@@ -51,8 +51,12 @@ menu.graphics.row.resolution=Resolution
menu.graphics.row.fullscreen=Fullscreen menu.graphics.row.fullscreen=Fullscreen
menu.graphics.row.vsync=VSync menu.graphics.row.vsync=VSync
menu.graphics.row.aa=Anti-aliasing menu.graphics.row.aa=Anti-aliasing
menu.graphics.row.viewdist=View Distance
menu.graphics.val.on=On menu.graphics.val.on=On
menu.graphics.val.off=Off menu.graphics.val.off=Off
menu.graphics.val.viewdist.short=Short
menu.graphics.val.viewdist.normal=Normal
menu.graphics.val.viewdist.far=Far
menu.graphics.btn.apply=Apply menu.graphics.btn.apply=Apply
menu.graphics.btn.cancel=Cancel menu.graphics.btn.cancel=Cancel