Compare commits
2 Commits
5ed7b4017c
...
0913da2f93
| Author | SHA1 | Date | |
|---|---|---|---|
| 0913da2f93 | |||
| 0b1cb4d0e9 |
Binary file not shown.
|
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
22
blight-assets/src/main/resources/MatDefs/BlightFog.j3md
Normal file
22
blight-assets/src/main/resources/MatDefs/BlightFog.j3md
Normal 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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,28 @@
|
|||||||
|
#Sat Jul 18 13:44:33 CEST 2026
|
||||||
|
attachedEmitters.count=0
|
||||||
|
attachedLights.count=0
|
||||||
|
castShadow=true
|
||||||
|
category=
|
||||||
|
cullDistance=120.0
|
||||||
|
footstepSurface=
|
||||||
|
interactableOffsetX=0.0
|
||||||
|
interactableOffsetY=0.5
|
||||||
|
interactableOffsetZ=0.0
|
||||||
|
interactableRotY=0.0
|
||||||
|
interactableType=NONE
|
||||||
|
lod1Distance=30.0
|
||||||
|
lod1Path=
|
||||||
|
lod2Distance=80.0
|
||||||
|
lod2Path=
|
||||||
|
name=pine_medium_20260706_190939
|
||||||
|
pivotOffsetY=0.0
|
||||||
|
placementOffsetY=0.0
|
||||||
|
randomScaleMax=1.0
|
||||||
|
randomScaleMin=1.0
|
||||||
|
receiveShadow=true
|
||||||
|
scaleX=1.0
|
||||||
|
scaleY=1.0
|
||||||
|
scaleZ=1.0
|
||||||
|
solid=false
|
||||||
|
tags=
|
||||||
|
uniformScale=true
|
||||||
Binary file not shown.
@@ -0,0 +1,28 @@
|
|||||||
|
#Sat Jul 18 11:14:25 CEST 2026
|
||||||
|
attachedEmitters.count=0
|
||||||
|
attachedLights.count=0
|
||||||
|
castShadow=true
|
||||||
|
category=
|
||||||
|
cullDistance=120.0
|
||||||
|
footstepSurface=
|
||||||
|
interactableOffsetX=0.0
|
||||||
|
interactableOffsetY=0.5
|
||||||
|
interactableOffsetZ=0.0
|
||||||
|
interactableRotY=0.0
|
||||||
|
interactableType=NONE
|
||||||
|
lod1Distance=30.0
|
||||||
|
lod1Path=
|
||||||
|
lod2Distance=80.0
|
||||||
|
lod2Path=
|
||||||
|
name=pine_medium_20260706_190953
|
||||||
|
pivotOffsetY=0.0
|
||||||
|
placementOffsetY=0.0
|
||||||
|
randomScaleMax=1.0
|
||||||
|
randomScaleMin=1.0
|
||||||
|
receiveShadow=true
|
||||||
|
scaleX=1.0
|
||||||
|
scaleY=1.0
|
||||||
|
scaleZ=1.0
|
||||||
|
solid=false
|
||||||
|
tags=
|
||||||
|
uniformScale=true
|
||||||
37
blight-assets/src/main/resources/Shaders/BlightFog.frag
Normal file
37
blight-assets/src/main/resources/Shaders/BlightFog.frag
Normal 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);
|
||||||
|
}
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 0–360° (Kompassrichtung), Elevation 0–90° (Höhe über Horizont). */
|
* Azimut 0–360° (Kompassrichtung), Elevation 0–90° (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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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) {}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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) {}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) -> {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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, RADIUS−5), Basis bei (±5, RADIUS−16).
|
||||||
|
*/
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user