Compare commits
5 Commits
bdd69e282c
...
1b3d9bf0af
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b3d9bf0af | |||
| b17a71286a | |||
| e6440e3a46 | |||
| 4328d66fe7 | |||
| ad37720edd |
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 998 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 990 B |
@@ -10,6 +10,30 @@ MaterialDef Tree {
|
||||
Vector3 LightDir : 0.6 -0.8 0.4
|
||||
Vector3 SunColor : 0.68 0.65 0.60
|
||||
Vector3 AmbientColor : 0.08 0.10 0.16
|
||||
|
||||
// Vom Shadow-Renderer befüllt (PostShadow-Pass)
|
||||
Int BoundDrawBuffer
|
||||
Int FilterMode
|
||||
Boolean HardwareShadows
|
||||
Texture2D ShadowMap0
|
||||
Texture2D ShadowMap1
|
||||
Texture2D ShadowMap2
|
||||
Texture2D ShadowMap3
|
||||
Texture2D ShadowMap4
|
||||
Texture2D ShadowMap5
|
||||
Float ShadowIntensity : 1.0
|
||||
Vector4 Splits
|
||||
Vector2 FadeInfo
|
||||
Matrix4 LightViewProjectionMatrix0
|
||||
Matrix4 LightViewProjectionMatrix1
|
||||
Matrix4 LightViewProjectionMatrix2
|
||||
Matrix4 LightViewProjectionMatrix3
|
||||
Matrix4 LightViewProjectionMatrix4
|
||||
Matrix4 LightViewProjectionMatrix5
|
||||
Vector3 LightPos
|
||||
Float PCFEdge
|
||||
Float ShadowMapSize
|
||||
Boolean BackfaceShadows : false
|
||||
}
|
||||
|
||||
Technique {
|
||||
@@ -25,4 +49,19 @@ MaterialDef Tree {
|
||||
Defines {
|
||||
}
|
||||
}
|
||||
|
||||
Technique PostShadow {
|
||||
VertexShader GLSL150 : Shaders/TreePostShadow.vert
|
||||
FragmentShader GLSL150 : Shaders/TreePostShadow.frag
|
||||
|
||||
WorldParameters {
|
||||
WorldViewProjectionMatrix
|
||||
WorldMatrix
|
||||
}
|
||||
|
||||
ForcedRenderState {
|
||||
Blend Modulate
|
||||
DepthWrite Off
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
#Sat Jul 18 13:44:33 CEST 2026
|
||||
#Mon Jul 20 20:34:07 CEST 2026
|
||||
attachedEmitters.count=0
|
||||
attachedLights.count=0
|
||||
castShadow=true
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,28 @@
|
||||
#Mon Jul 20 20:34:14 CEST 2026
|
||||
attachedEmitters.count=0
|
||||
attachedLights.count=0
|
||||
castShadow=true
|
||||
category=
|
||||
cullDistance=120.0
|
||||
footstepSurface=
|
||||
interactableOffsetX=0.0
|
||||
interactableOffsetY=0.5
|
||||
interactableOffsetZ=0.0
|
||||
interactableRotY=0.0
|
||||
interactableType=NONE
|
||||
lod1Distance=30.0
|
||||
lod1Path=
|
||||
lod2Distance=80.0
|
||||
lod2Path=
|
||||
name=pine_medium_20260706_190947
|
||||
pivotOffsetY=0.0
|
||||
placementOffsetY=0.0
|
||||
randomScaleMax=1.0
|
||||
randomScaleMin=1.0
|
||||
receiveShadow=true
|
||||
scaleX=1.0
|
||||
scaleY=1.0
|
||||
scaleZ=1.0
|
||||
solid=false
|
||||
tags=
|
||||
uniformScale=true
|
||||
Binary file not shown.
@@ -1,4 +1,4 @@
|
||||
#Sat Jul 18 11:14:25 CEST 2026
|
||||
#Mon Jul 20 20:34:23 CEST 2026
|
||||
attachedEmitters.count=0
|
||||
attachedLights.count=0
|
||||
castShadow=true
|
||||
|
||||
13
blight-assets/src/main/resources/Shaders/TreePostShadow.frag
Normal file
13
blight-assets/src/main/resources/Shaders/TreePostShadow.frag
Normal file
@@ -0,0 +1,13 @@
|
||||
#import "Common/ShaderLib/GLSLCompat.glsllib"
|
||||
|
||||
uniform sampler2D m_ShadowMap0;
|
||||
uniform float m_ShadowIntensity;
|
||||
|
||||
in vec4 shadowCoord;
|
||||
|
||||
void main() {
|
||||
vec3 coord = shadowCoord.xyz / shadowCoord.w;
|
||||
float mapDepth = texture2D(m_ShadowMap0, coord.xy).r;
|
||||
float lit = (coord.z > mapDepth + 0.001) ? (1.0 - m_ShadowIntensity) : 1.0;
|
||||
gl_FragColor = vec4(lit, lit, lit, 1.0);
|
||||
}
|
||||
15
blight-assets/src/main/resources/Shaders/TreePostShadow.vert
Normal file
15
blight-assets/src/main/resources/Shaders/TreePostShadow.vert
Normal file
@@ -0,0 +1,15 @@
|
||||
#import "Common/ShaderLib/GLSLCompat.glsllib"
|
||||
|
||||
uniform mat4 g_WorldViewProjectionMatrix;
|
||||
uniform mat4 g_WorldMatrix;
|
||||
uniform mat4 m_LightViewProjectionMatrix0;
|
||||
|
||||
in vec3 inPosition;
|
||||
|
||||
out vec4 shadowCoord;
|
||||
|
||||
void main() {
|
||||
gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0);
|
||||
vec4 worldPos = g_WorldMatrix * vec4(inPosition, 1.0);
|
||||
shadowCoord = m_LightViewProjectionMatrix0 * worldPos;
|
||||
}
|
||||
@@ -246,6 +246,7 @@ public class EditorApp extends Application {
|
||||
|
||||
// Modell-Editor-Zustand
|
||||
private Label modelEditorDimLabel;
|
||||
private Label modelEditorPolyLabel;
|
||||
private Spinner<Double> modelEditorSpinX, modelEditorSpinY, modelEditorSpinZ;
|
||||
private CheckBox modelEditorUniformCB;
|
||||
private boolean modelEditorSuppressListeners = false;
|
||||
@@ -254,16 +255,19 @@ public class EditorApp extends Application {
|
||||
private String modelEditorLod2Path = "";
|
||||
private Label modelEditorLod1Label;
|
||||
private Label modelEditorLod2Label;
|
||||
private ToggleButton modelEditorLod0Btn;
|
||||
private ToggleButton modelEditorLod1Btn;
|
||||
private ToggleButton modelEditorLod2Btn;
|
||||
private Label modelEditorLod1StatusLabel;
|
||||
private Label modelEditorLod2StatusLabel;
|
||||
private Button modelEditorLod1GenBtn;
|
||||
private Button modelEditorLod2GenBtn;
|
||||
private ToggleButton importLod0Btn;
|
||||
private ToggleButton importLod1Btn;
|
||||
private ToggleButton importLod2Btn;
|
||||
private Button modelImportLod1ClearBtn;
|
||||
private Button modelImportLod2ClearBtn;
|
||||
private Button modelImportLod2GenBtn;
|
||||
// Anhänge (veränderliche Listen, werden bei Änderung an JME übertragen)
|
||||
private final java.util.List<de.blight.common.ModelMeta.AttachedLight> modelEditorLights = new java.util.ArrayList<>();
|
||||
private final java.util.List<de.blight.common.ModelMeta.AttachedEmitter> modelEditorEmitters = new java.util.ArrayList<>();
|
||||
@@ -430,6 +434,7 @@ public class EditorApp extends Application {
|
||||
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION,
|
||||
"Es gibt ungespeicherte Änderungen.\nTrotzdem beenden?",
|
||||
ButtonType.YES, ButtonType.NO);
|
||||
confirm.initOwner(stage);
|
||||
confirm.setHeaderText("Ungespeicherte Änderungen");
|
||||
confirm.showAndWait().ifPresent(btn -> {
|
||||
if (btn != ButtonType.YES) e.consume();
|
||||
@@ -665,7 +670,8 @@ public class EditorApp extends Application {
|
||||
updateModelEditorDimensions(
|
||||
input.modelEditorBoundsW,
|
||||
input.modelEditorBoundsH,
|
||||
input.modelEditorBoundsD);
|
||||
input.modelEditorBoundsD,
|
||||
input.modelEditorPolyCount);
|
||||
}
|
||||
|
||||
// Modell-Editor: eingebettete LODs erkannt → Labels aktualisieren
|
||||
@@ -687,13 +693,25 @@ public class EditorApp extends Application {
|
||||
// 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);
|
||||
if (modelEditorLod2GenBtn != null) {
|
||||
boolean lod1Set = input.modelEditorHasEmbeddedLod1 || !modelEditorLod1Path.isBlank() || input.modelImportHasLod1;
|
||||
boolean lod2Set = input.modelEditorHasEmbeddedLod2 || !modelEditorLod2Path.isBlank() || input.modelImportHasLod2;
|
||||
modelEditorLod2GenBtn.setDisable(!lod1Set || lod2Set);
|
||||
}
|
||||
// Importer LOD-Buttons freischalten sobald generiert
|
||||
if (importLod1Btn != null) importLod1Btn.setDisable(!input.modelImportHasLod1);
|
||||
if (importLod2Btn != null) importLod2Btn.setDisable(!input.modelImportHasLod2);
|
||||
// Ausgewählten LOD-Button mit aktuellem Preview-Level synchronisieren
|
||||
int lodPreview = input.modelEditorLodPreview;
|
||||
if (modelEditorLod0Btn != null) { modelEditorLod0Btn.setSelected(lodPreview == 0); }
|
||||
if (modelEditorLod1Btn != null) { modelEditorLod1Btn.setSelected(lodPreview == 1); }
|
||||
if (modelEditorLod2Btn != null) { modelEditorLod2Btn.setSelected(lodPreview == 2); }
|
||||
if (importLod0Btn != null) { importLod0Btn.setSelected(lodPreview == 0); }
|
||||
if (importLod1Btn != null) { importLod1Btn.setSelected(lodPreview == 1); }
|
||||
if (importLod2Btn != null) { importLod2Btn.setSelected(lodPreview == 2); }
|
||||
if (modelImportLod1ClearBtn != null) modelImportLod1ClearBtn.setDisable(!input.modelImportHasLod1);
|
||||
if (modelImportLod2ClearBtn != null) modelImportLod2ClearBtn.setDisable(!input.modelImportHasLod2);
|
||||
if (modelImportLod2GenBtn != null) modelImportLod2GenBtn.setDisable(!input.modelImportHasLod1 || input.modelImportHasLod2);
|
||||
|
||||
// Modell-Import/Editor: LOD-Status
|
||||
String lodStatus = input.modelLodGenStatus;
|
||||
@@ -867,7 +885,8 @@ public class EditorApp extends Application {
|
||||
Label label = new Label("Model Editor");
|
||||
label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
|
||||
|
||||
ToggleButton lod0Btn = new ToggleButton("LOD 0");
|
||||
modelEditorLod0Btn = new ToggleButton("LOD 0");
|
||||
ToggleButton lod0Btn = modelEditorLod0Btn;
|
||||
modelEditorLod1Btn = new ToggleButton("LOD 1");
|
||||
modelEditorLod2Btn = new ToggleButton("LOD 2");
|
||||
javafx.scene.control.ToggleGroup lodGroup = new javafx.scene.control.ToggleGroup();
|
||||
@@ -2247,6 +2266,7 @@ public class EditorApp extends Application {
|
||||
objSaveBtn.setDisable(true);
|
||||
objSaveBtn.setOnAction(e -> {
|
||||
TextInputDialog dlg = new TextInputDialog("Vorlage");
|
||||
dlg.initOwner(primaryStage);
|
||||
dlg.setTitle("Vorlage speichern");
|
||||
dlg.setHeaderText("Name der Vorlage (ohne .j3o):");
|
||||
dlg.setContentText("Name:");
|
||||
@@ -4435,6 +4455,7 @@ public class EditorApp extends Application {
|
||||
"Textur-Set wirklich löschen?\n" + setDir.getFileName()
|
||||
+ "\n(Alle enthaltenen Dateien werden gelöscht!)",
|
||||
ButtonType.OK, ButtonType.CANCEL);
|
||||
confirm.initOwner(primaryStage);
|
||||
confirm.setHeaderText(null);
|
||||
confirm.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> {
|
||||
try {
|
||||
@@ -4604,6 +4625,7 @@ public class EditorApp extends Application {
|
||||
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION,
|
||||
"Datei wirklich löschen?\n" + p.getFileName(),
|
||||
ButtonType.OK, ButtonType.CANCEL);
|
||||
confirm.initOwner(primaryStage);
|
||||
confirm.setHeaderText(null);
|
||||
confirm.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> {
|
||||
try {
|
||||
@@ -4706,6 +4728,7 @@ public class EditorApp extends Application {
|
||||
Path parentPath = itemPaths.get(parentItem);
|
||||
if (parentPath == null) return;
|
||||
TextInputDialog dlg = new TextInputDialog("Neuer Ordner");
|
||||
dlg.initOwner(primaryStage);
|
||||
dlg.setTitle("Ordner erstellen");
|
||||
dlg.setHeaderText("Neuen Unterordner in " + parentPath.getFileName() + " erstellen:");
|
||||
dlg.setContentText("Name:");
|
||||
@@ -4747,6 +4770,7 @@ public class EditorApp extends Application {
|
||||
|
||||
String headerExt = ext.isEmpty() ? "" : " (Endung " + ext + " wird beibehalten)";
|
||||
TextInputDialog dlg = new TextInputDialog(baseName);
|
||||
dlg.initOwner(primaryStage);
|
||||
dlg.setTitle("Umbenennen");
|
||||
dlg.setHeaderText(oldFileName + " umbenennen:" + headerExt);
|
||||
dlg.setContentText("Neuer Name:");
|
||||
@@ -5985,6 +6009,10 @@ public class EditorApp extends Application {
|
||||
modelEditorDimLabel.setMaxWidth(Double.MAX_VALUE);
|
||||
modelEditorDimLabel.setWrapText(true);
|
||||
|
||||
modelEditorPolyLabel = new Label("– Polygone");
|
||||
modelEditorPolyLabel.setStyle("-fx-text-fill:#aaa; -fx-font-family:monospace; -fx-font-size:11;");
|
||||
modelEditorPolyLabel.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
// ── Pivot & Platzierungs-Versatz ─────────────────────────────────────
|
||||
Label offsetTitle = new Label("Versatz:");
|
||||
offsetTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;");
|
||||
@@ -6014,6 +6042,10 @@ public class EditorApp extends Application {
|
||||
receiveCB.setSelected(meta.receiveShadow());
|
||||
for (CheckBox cb : new CheckBox[]{solidCB, castCB, receiveCB})
|
||||
cb.setStyle("-fx-text-fill:#ccc;");
|
||||
if (relPath.startsWith("Models/trees/")) {
|
||||
solidCB.setSelected(false);
|
||||
solidCB.setDisable(true);
|
||||
}
|
||||
|
||||
// ── Zufällige Skalierung beim Multi-Place ─────────────────────────────
|
||||
Label rndTitle = new Label("Zufallsskalierung (Multi-Place):");
|
||||
@@ -6050,16 +6082,23 @@ public class EditorApp extends Application {
|
||||
lod1ClearBtn.setStyle("-fx-text-fill:#c0392b;");
|
||||
lod1ClearBtn.setOnAction(e -> {
|
||||
if (input.modelEditorHasEmbeddedLod1) {
|
||||
// ModelEditorState.removeEmbeddedLod(1) kaskadiert automatisch LOD2
|
||||
input.modelEditorRemoveEmbeddedLod = 1;
|
||||
}
|
||||
if (input.modelImportHasLod1) {
|
||||
// ModelImportState.clearLod==1 kaskadiert automatisch LOD2
|
||||
input.modelImportClearLodLevel = 1;
|
||||
if (modelEditorLod1StatusLabel != null) modelEditorLod1StatusLabel.setText("–");
|
||||
if (modelEditorLod2StatusLabel != null) modelEditorLod2StatusLabel.setText("–");
|
||||
}
|
||||
modelEditorLod1Path = "";
|
||||
input.modelEditorLod1Path = "";
|
||||
modelEditorLod1Label.setText("(kein Pfad)");
|
||||
if (input.modelEditorLodPreview == 1) {
|
||||
// Kaskade LOD2 (Pfad + Preview)
|
||||
modelEditorLod2Path = "";
|
||||
input.modelEditorLod2Path = "";
|
||||
if (modelEditorLod2Label != null) modelEditorLod2Label.setText("(kein Pfad)");
|
||||
if (input.modelEditorLodPreview == 1 || input.modelEditorLodPreview == 2) {
|
||||
input.modelEditorLodPreview = 0;
|
||||
input.modelEditorLodChanged = true;
|
||||
}
|
||||
@@ -6072,7 +6111,7 @@ public class EditorApp extends Application {
|
||||
modelEditorLod2GenBtn = new Button("LOD 2 generieren");
|
||||
modelEditorLod2GenBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
boolean lod2Exists = input.modelEditorHasEmbeddedLod2 || !modelEditorLod2Path.isBlank() || input.modelImportHasLod2;
|
||||
modelEditorLod2GenBtn.setDisable(lod2Exists);
|
||||
modelEditorLod2GenBtn.setDisable(!lod1Exists || lod2Exists);
|
||||
modelEditorLod2GenBtn.setOnAction(e -> showLodGenDialog(2, 85));
|
||||
|
||||
modelEditorLod2StatusLabel = new Label("–");
|
||||
@@ -6390,7 +6429,7 @@ public class EditorApp extends Application {
|
||||
tagsLabel, tagsTF,
|
||||
new Separator(),
|
||||
scaleTitle, modelEditorUniformCB, scaleGrid,
|
||||
modelEditorDimLabel,
|
||||
modelEditorDimLabel, modelEditorPolyLabel,
|
||||
new Separator(),
|
||||
offsetTitle, offsetGrid,
|
||||
new Separator(),
|
||||
@@ -6481,6 +6520,7 @@ public class EditorApp extends Application {
|
||||
modelImportLod2StatusLabel = null;
|
||||
modelImportLod1ClearBtn = null;
|
||||
modelImportLod2ClearBtn = null;
|
||||
modelImportLod2GenBtn = null;
|
||||
modelImportExportStatusLabel = null;
|
||||
input.modelImportHasLod1 = false;
|
||||
input.modelImportHasLod2 = false;
|
||||
@@ -6524,7 +6564,8 @@ public class EditorApp extends Application {
|
||||
Label label = new Label("Modell importieren");
|
||||
label.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
|
||||
|
||||
ToggleButton lod0Btn = new ToggleButton("LOD 0");
|
||||
importLod0Btn = new ToggleButton("LOD 0");
|
||||
ToggleButton lod0Btn = importLod0Btn;
|
||||
importLod1Btn = new ToggleButton("LOD 1");
|
||||
importLod2Btn = new ToggleButton("LOD 2");
|
||||
javafx.scene.control.ToggleGroup lodGroup = new javafx.scene.control.ToggleGroup();
|
||||
@@ -6644,6 +6685,10 @@ public class EditorApp extends Application {
|
||||
modelEditorDimLabel.setMaxWidth(Double.MAX_VALUE);
|
||||
modelEditorDimLabel.setWrapText(true);
|
||||
|
||||
modelEditorPolyLabel = new Label("– Polygone");
|
||||
modelEditorPolyLabel.setStyle("-fx-text-fill:#aaa; -fx-font-family:monospace; -fx-font-size:11;");
|
||||
modelEditorPolyLabel.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
// ── LOD-Algorithmus ───────────────────────────────────────────────────
|
||||
Label algoTitle = new Label("Reduktions-Algorithmus");
|
||||
algoTitle.setStyle("-fx-font-weight:bold; -fx-text-fill:#ccc;");
|
||||
@@ -6703,8 +6748,10 @@ public class EditorApp extends Application {
|
||||
modelImportLod1ClearBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
modelImportLod1ClearBtn.setDisable(true);
|
||||
modelImportLod1ClearBtn.setOnAction(e -> {
|
||||
// ModelImportState.clearLod==1 kaskadiert automatisch LOD2
|
||||
input.modelImportClearLodLevel = 1;
|
||||
modelImportLod1StatusLabel.setText("(nicht gesetzt)");
|
||||
modelImportLod2StatusLabel.setText("(nicht gesetzt)");
|
||||
});
|
||||
|
||||
// ── LOD 2 ─────────────────────────────────────────────────────────────
|
||||
@@ -6720,7 +6767,8 @@ public class EditorApp extends Application {
|
||||
lod2FileRB.setToggleGroup(lod2Group);
|
||||
lod2MeshRB.setSelected(true);
|
||||
|
||||
Button lod2GenBtn = new Button("LOD 2 generieren");
|
||||
modelImportLod2GenBtn = new Button("LOD 2 generieren");
|
||||
Button lod2GenBtn = modelImportLod2GenBtn;
|
||||
lod2GenBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
lod2GenBtn.setOnAction(e -> {
|
||||
if (lod2MeshRB.isSelected()) {
|
||||
@@ -6791,7 +6839,7 @@ public class EditorApp extends Application {
|
||||
panel.getChildren().addAll(
|
||||
title,
|
||||
new Separator(),
|
||||
scaleTitle, modelEditorUniformCB, scaleGrid, modelEditorDimLabel,
|
||||
scaleTitle, modelEditorUniformCB, scaleGrid, modelEditorDimLabel, modelEditorPolyLabel,
|
||||
new Separator(),
|
||||
algoTitle, algoBlightRB, algoJmeRB,
|
||||
new Separator(),
|
||||
@@ -6871,9 +6919,11 @@ public class EditorApp extends Application {
|
||||
input.modelEditorScaleChanged = true;
|
||||
}
|
||||
|
||||
private void updateModelEditorDimensions(float w, float h, float d) {
|
||||
private void updateModelEditorDimensions(float w, float h, float d, int polyCount) {
|
||||
if (modelEditorDimLabel != null)
|
||||
modelEditorDimLabel.setText(String.format("B %.2f × H %.2f × T %.2f m", w, h, d));
|
||||
if (modelEditorPolyLabel != null)
|
||||
modelEditorPolyLabel.setText(String.format("%,d Polygone", polyCount).replace(',', '.'));
|
||||
}
|
||||
|
||||
private void saveModelMeta(String relPath,
|
||||
@@ -6965,6 +7015,12 @@ public class EditorApp extends Application {
|
||||
input.modelEditorBakeScaleRequest = true;
|
||||
}
|
||||
|
||||
// In-Memory-LODs in j3o einbetten (nach Scale-Bake, vor Thumbnail)
|
||||
if (input.modelImportHasLod1 || input.modelImportHasLod2) {
|
||||
input.modelEditorEmbedLodsPath = finalJ3o;
|
||||
input.modelEditorEmbedLodsRequest = true;
|
||||
}
|
||||
|
||||
// Thumbnail generieren (JME3-Thread liest das Flag und rendert)
|
||||
input.modelEditorThumbnailRequest = finalJ3o;
|
||||
}
|
||||
@@ -7142,6 +7198,7 @@ public class EditorApp extends Application {
|
||||
|
||||
private void showLodGenDialog(int level, int defaultPct) {
|
||||
javafx.scene.control.Dialog<javafx.util.Pair<String, Integer>> dlg = new javafx.scene.control.Dialog<>();
|
||||
dlg.initOwner(primaryStage);
|
||||
dlg.setTitle("LOD " + level + " generieren");
|
||||
dlg.setHeaderText("Methode und Reduktion für LOD " + level + " wählen");
|
||||
|
||||
@@ -7599,8 +7656,18 @@ public class EditorApp extends Application {
|
||||
gameConsoleStage.setTitle("Spiel-Konsole");
|
||||
gameConsoleStage.setScene(scene);
|
||||
gameConsoleStage.initOwner(primaryStage);
|
||||
// Auf zweitem Monitor platzieren und maximieren, falls vorhanden
|
||||
var screens = javafx.stage.Screen.getScreens();
|
||||
if (screens.size() > 1) {
|
||||
javafx.geometry.Rectangle2D sb = screens.get(1).getVisualBounds();
|
||||
gameConsoleStage.setX(sb.getMinX());
|
||||
gameConsoleStage.setY(sb.getMinY());
|
||||
gameConsoleStage.setWidth(sb.getWidth());
|
||||
gameConsoleStage.setHeight(sb.getHeight());
|
||||
} else {
|
||||
gameConsoleStage.setX(primaryStage.getX());
|
||||
gameConsoleStage.setY(primaryStage.getY() + primaryStage.getHeight());
|
||||
}
|
||||
} else {
|
||||
gameConsoleArea.clear();
|
||||
}
|
||||
@@ -8748,6 +8815,7 @@ public class EditorApp extends Application {
|
||||
list.getSelectionModel().selectFirst();
|
||||
|
||||
javafx.scene.control.Dialog<java.util.List<String>> dlg = new javafx.scene.control.Dialog<>();
|
||||
dlg.initOwner(primaryStage);
|
||||
dlg.setTitle("Animation(en) hinzufügen");
|
||||
dlg.setHeaderText("Verfügbare Clips (noch nicht im Set) — Mehrfachauswahl möglich:");
|
||||
dlg.getDialogPane().setContent(list);
|
||||
@@ -9042,6 +9110,7 @@ public class EditorApp extends Application {
|
||||
|
||||
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> kfDlg =
|
||||
new javafx.scene.control.Dialog<>();
|
||||
kfDlg.initOwner(primaryStage);
|
||||
kfDlg.setTitle(isAdd ? "Offset hinzufügen" : "Offset bearbeiten");
|
||||
javafx.scene.control.ButtonType okKf = new javafx.scene.control.ButtonType(
|
||||
isAdd ? "Hinzufügen" : "Übernehmen",
|
||||
@@ -9174,6 +9243,7 @@ public class EditorApp extends Application {
|
||||
|
||||
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> dlg =
|
||||
new javafx.scene.control.Dialog<>();
|
||||
dlg.initOwner(primaryStage);
|
||||
dlg.setTitle("Aktion zuordnen");
|
||||
javafx.scene.control.ButtonType ok = new javafx.scene.control.ButtonType("Hinzufügen",
|
||||
javafx.scene.control.ButtonBar.ButtonData.OK_DONE);
|
||||
@@ -9226,6 +9296,7 @@ public class EditorApp extends Application {
|
||||
|
||||
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> dlg =
|
||||
new javafx.scene.control.Dialog<>();
|
||||
dlg.initOwner(primaryStage);
|
||||
dlg.setTitle(isEdit ? "Clip-Teil bearbeiten" : "Clip-Teil hinzufügen");
|
||||
dlg.setHeaderText((isEdit ? "Bearbeiten: " : "Neuer Teil für: ") + source);
|
||||
javafx.scene.control.ButtonType ok = new javafx.scene.control.ButtonType(
|
||||
@@ -9317,6 +9388,7 @@ public class EditorApp extends Application {
|
||||
Alert warn = new Alert(Alert.AlertType.CONFIRMATION,
|
||||
"Das aktuelle AnimSet hat ungespeicherte Änderungen.\nTrotzdem ein neues Set anlegen?",
|
||||
ButtonType.OK, ButtonType.CANCEL);
|
||||
warn.initOwner(primaryStage);
|
||||
warn.setHeaderText("Ungespeicherte Änderungen");
|
||||
warn.showAndWait().filter(b -> b == ButtonType.OK).ifPresent(b -> doCreateNewAnimSet());
|
||||
} else {
|
||||
@@ -9326,6 +9398,7 @@ public class EditorApp extends Application {
|
||||
|
||||
private void doCreateNewAnimSet() {
|
||||
TextInputDialog dlg = new TextInputDialog();
|
||||
dlg.initOwner(primaryStage);
|
||||
dlg.setTitle("Neues Animations-Set");
|
||||
dlg.setHeaderText("Name des neuen Animations-Sets:");
|
||||
dlg.setContentText("Name:");
|
||||
@@ -9634,6 +9707,7 @@ public class EditorApp extends Application {
|
||||
private void showSaveAnimSetDialog() {
|
||||
if (animClipListView == null || animClipListView.getItems().isEmpty()) return;
|
||||
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> dlg = new javafx.scene.control.Dialog<>();
|
||||
dlg.initOwner(primaryStage);
|
||||
dlg.setTitle("Animations-Set speichern");
|
||||
dlg.setHeaderText("Clips und Aktions-Zuweisung für das Set konfigurieren");
|
||||
javafx.scene.control.ButtonType ok = new javafx.scene.control.ButtonType("Speichern",
|
||||
@@ -9706,6 +9780,7 @@ public class EditorApp extends Application {
|
||||
|
||||
javafx.scene.control.Dialog<javafx.scene.control.ButtonType> addDlg =
|
||||
new javafx.scene.control.Dialog<>();
|
||||
addDlg.initOwner(primaryStage);
|
||||
addDlg.setTitle("Aktion zuweisen");
|
||||
javafx.scene.control.ButtonType addOk = new javafx.scene.control.ButtonType("Hinzufügen",
|
||||
javafx.scene.control.ButtonBar.ButtonData.OK_DONE);
|
||||
|
||||
@@ -631,6 +631,13 @@ public class SharedInput {
|
||||
public final java.util.concurrent.atomic.AtomicReference<String>
|
||||
animStripClipsRequest = new java.util.concurrent.atomic.AtomicReference<>();
|
||||
|
||||
// ── Impostor-Regenerierung ────────────────────────────────────────────────
|
||||
/** JFX → JME: Impostor (lod2) für das angegebene j3o neu generieren + in gleicher Datei speichern.
|
||||
* Wert: relativer Asset-Pfad (z.B. "Models/trees/pine/medium/pine_medium_xxx.j3o"). null = kein Auftrag. */
|
||||
public final AtomicReference<String> modelEditorRegenerateImpostor = new AtomicReference<>();
|
||||
/** JME → JFX: Status nach Impostor-Regen ("OK" oder "Fehler: …"). */
|
||||
public volatile String modelEditorRegenerateImpostorStatus = null;
|
||||
|
||||
/** JME3 → JavaFX: Status-Meldung für Clip- und Set-Operationen. */
|
||||
public volatile String animOpStatus = null;
|
||||
/** JME3 → JavaFX: Status-Meldung für Einbett-Operationen (Character Editor). */
|
||||
@@ -681,6 +688,9 @@ public class SharedInput {
|
||||
public volatile float modelEditorBoundsD = 0f;
|
||||
public volatile boolean modelEditorBoundsReady = false;
|
||||
|
||||
/** JME → JFX: Polygonanzahl der aktuell sichtbaren LOD-Stufe. */
|
||||
public volatile int modelEditorPolyCount = 0;
|
||||
|
||||
/** JFX → JME: aktuelle Skalierung (Echtzeit-Vorschau). */
|
||||
public volatile float modelEditorScaleX = 1f;
|
||||
public volatile float modelEditorScaleY = 1f;
|
||||
@@ -719,6 +729,9 @@ public class SharedInput {
|
||||
public volatile boolean modelEditorLodChanged = false;
|
||||
/** JFX → JME: Eingebettetes LOD aus j3o entfernen und speichern (1=lod1, 2=lod2, -1=kein Auftrag). */
|
||||
public volatile int modelEditorRemoveEmbeddedLod = -1;
|
||||
/** JFX → JME: In-Memory-LODs in die j3o-Datei einbetten und speichern. */
|
||||
public volatile boolean modelEditorEmbedLodsRequest = false;
|
||||
public volatile java.nio.file.Path modelEditorEmbedLodsPath = null;
|
||||
|
||||
/** JFX → JME: Richtung der Hauptlichtquelle in der Vorschau.
|
||||
* Azimut 0–360° (Kompassrichtung), Elevation 0–90° (Höhe über Horizont). */
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.bounding.BoundingBox;
|
||||
import com.jme3.export.binary.BinaryExporter;
|
||||
import com.jme3.export.binary.BinaryImporter;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
@@ -75,6 +76,10 @@ public class EzTreeState extends BaseAppState {
|
||||
private int capturePass = 0;
|
||||
private ByteBuffer[] capturePixels = new ByteBuffer[ImpostorUtil.DIRS];
|
||||
|
||||
// ── Impostor-Regen-Phase (bestehendes j3o aktualisieren) ─────────────────
|
||||
private Path pendingRegenAbsPath = null;
|
||||
private Node pendingRegenRoot = null;
|
||||
|
||||
public EzTreeState(SharedInput input) { this.input = input; }
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
@@ -100,11 +105,18 @@ public class EzTreeState extends BaseAppState {
|
||||
|
||||
if (pendingRequest != null && captureReady[0]) {
|
||||
finishCapture();
|
||||
} else if (pendingRequest == null) {
|
||||
} else if (pendingRegenAbsPath != null && captureReady[0]) {
|
||||
finishRegenCapture();
|
||||
} else if (pendingRequest == null && pendingRegenAbsPath == null) {
|
||||
String regenAsset = input.modelEditorRegenerateImpostor.getAndSet(null);
|
||||
if (regenAsset != null) {
|
||||
startImpostorRegen(regenAsset);
|
||||
} else {
|
||||
SharedInput.EzTreeGenRequest req = input.ezTreeGenQueue.poll();
|
||||
if (req != null) startGeneration(req);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 1: Generierung ──────────────────────────────────────────────────
|
||||
|
||||
@@ -122,6 +134,18 @@ public class EzTreeState extends BaseAppState {
|
||||
ld1Node.setLocalScale(1f / 3f);
|
||||
|
||||
BoundingBox bb = boundsOf(hdNode);
|
||||
// Stammbasis auf Y=0 setzen: 1/3-Scale legt den Stamm typischerweise
|
||||
// bei Y>0. Translation in hdNode/ld1Node einbacken, damit das j3o
|
||||
// ohne Laufzeit-Korrektur auf dem Terrain steht.
|
||||
if (bb != null) {
|
||||
float minY = bb.getCenter().y - bb.getYExtent();
|
||||
if (Math.abs(minY) > 0.01f) {
|
||||
hdNode.setLocalTranslation(0f, -minY, 0f);
|
||||
ld1Node.setLocalTranslation(0f, -minY, 0f);
|
||||
hdNode.updateGeometricState();
|
||||
bb = boundsOf(hdNode);
|
||||
}
|
||||
}
|
||||
float camDist = bb != null
|
||||
? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3.2f
|
||||
: 20f;
|
||||
@@ -155,9 +179,8 @@ public class EzTreeState extends BaseAppState {
|
||||
|
||||
private Node buildLod1Node(SharedInput.EzTreeGenRequest req) {
|
||||
TreeOptions opts = req.options();
|
||||
// Versuche Node.js mit halber Blattanzahl – gleiche Baumform/-größe wie LOD0.
|
||||
// Blattanzahl nicht reduzieren – nur Bark-Geometrie (sections/segments) vereinfachen.
|
||||
TreeOptions ld = opts.copy();
|
||||
ld.leaves.count = Math.max(0, opts.leaves.count / 2);
|
||||
Node n = tryNodeJsGeneration(new SharedInput.EzTreeGenRequest(ld, req.presetName(), false));
|
||||
if (n != null) {
|
||||
n.setName("EzTree_ld1");
|
||||
@@ -505,6 +528,106 @@ public class EzTreeState extends BaseAppState {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Impostor-Regen für bestehendes j3o ───────────────────────────────────
|
||||
|
||||
private void startImpostorRegen(String assetPath) {
|
||||
cleanupCapture();
|
||||
try {
|
||||
Path absPath = ASSET_ROOT.resolve(assetPath);
|
||||
BinaryImporter im = BinaryImporter.getInstance();
|
||||
im.setAssetManager(assets);
|
||||
Object s = im.load(absPath.toFile());
|
||||
if (!(s instanceof Node root)) {
|
||||
input.modelEditorRegenerateImpostorStatus = "Fehler: kein Node in " + absPath.getFileName();
|
||||
input.treeGenStatusMsg = input.modelEditorRegenerateImpostorStatus;
|
||||
return;
|
||||
}
|
||||
if (root.getChildren().isEmpty() || !(root.getChild(0) instanceof Node hdNode)) {
|
||||
input.modelEditorRegenerateImpostorStatus = "Fehler: kein HD-Kind in " + absPath.getFileName();
|
||||
input.treeGenStatusMsg = input.modelEditorRegenerateImpostorStatus;
|
||||
return;
|
||||
}
|
||||
root.updateGeometricState();
|
||||
BoundingBox bb = boundsOf(hdNode);
|
||||
if (bb == null) {
|
||||
bb = new BoundingBox(Vector3f.ZERO, 5f, 10f, 5f);
|
||||
}
|
||||
|
||||
pendingRegenAbsPath = absPath;
|
||||
pendingRegenRoot = root;
|
||||
pendingHdNode = hdNode;
|
||||
pendingBb = bb;
|
||||
capturePass = 0;
|
||||
capturePixels = new ByteBuffer[ImpostorUtil.DIRS];
|
||||
|
||||
startCapturePass(0);
|
||||
input.treeGenStatusMsg = "Impostor neu generieren (1/" + ImpostorUtil.DIRS + ")…";
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[EzTree] Impostor-Regen fehlgeschlagen: {}", e.getMessage(), e);
|
||||
input.modelEditorRegenerateImpostorStatus = "Fehler: " + e.getMessage();
|
||||
input.treeGenStatusMsg = input.modelEditorRegenerateImpostorStatus;
|
||||
}
|
||||
}
|
||||
|
||||
private void finishRegenCapture() {
|
||||
capturePixels[capturePass] = ImpostorUtil.readPixels(app, captureFB);
|
||||
ImpostorUtil.cleanup(app, captureVP, captureFB);
|
||||
captureVP = null;
|
||||
captureFB = null;
|
||||
captureReady[0] = false;
|
||||
|
||||
if (capturePass < ImpostorUtil.DIRS - 1) {
|
||||
capturePass++;
|
||||
input.treeGenStatusMsg = "Impostor neu generieren (" + (capturePass + 1) + "/" + ImpostorUtil.DIRS + ")…";
|
||||
startCapturePass(capturePass);
|
||||
return;
|
||||
}
|
||||
|
||||
Path absPath = pendingRegenAbsPath;
|
||||
Node root = pendingRegenRoot;
|
||||
BoundingBox bb = pendingBb;
|
||||
ByteBuffer[] pixels = capturePixels;
|
||||
|
||||
pendingRegenAbsPath = null;
|
||||
pendingRegenRoot = null;
|
||||
pendingHdNode = null;
|
||||
pendingBb = null;
|
||||
capturePixels = new ByteBuffer[ImpostorUtil.DIRS];
|
||||
|
||||
ByteBuffer atlas = ImpostorUtil.combineAtlas(pixels);
|
||||
Texture2D atlasTex = ImpostorUtil.buildAtlasTexture(atlas);
|
||||
|
||||
Spatial oldLod2 = root.getChild("lod2");
|
||||
if (oldLod2 != null) root.detachChild(oldLod2);
|
||||
|
||||
Node newLod2 = ImpostorUtil.makeImpostorNode(bb, atlasTex, assets);
|
||||
newLod2.setCullHint(Spatial.CullHint.Always);
|
||||
newLod2.setShadowMode(com.jme3.renderer.queue.RenderQueue.ShadowMode.Off);
|
||||
root.attachChild(newLod2);
|
||||
|
||||
stripControlsRecursive(root);
|
||||
|
||||
try {
|
||||
BinaryExporter.getInstance().save(root, absPath.toFile());
|
||||
log.info("[EzTree] Impostor gespeichert: {}", absPath.getFileName());
|
||||
input.treeGenStatusMsg = "Impostor gespeichert: " + absPath.getFileName();
|
||||
input.modelEditorRegenerateImpostorStatus = "OK";
|
||||
input.refreshAssets = true;
|
||||
} catch (java.io.IOException e) {
|
||||
log.error("[EzTree] Impostor-Regen Speichern fehlgeschlagen: {}", e.getMessage());
|
||||
input.treeGenStatusMsg = "Impostor-Regen-Fehler: " + e.getMessage();
|
||||
input.modelEditorRegenerateImpostorStatus = "Fehler: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private static void stripControlsRecursive(Spatial s) {
|
||||
while (s.getNumControls() > 0) s.removeControl(s.getControl(0));
|
||||
if (s instanceof Node n) {
|
||||
for (Spatial child : n.getChildren()) stripControlsRecursive(child);
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupCapture() {
|
||||
ImpostorUtil.cleanup(app, captureVP, captureFB);
|
||||
captureVP = null;
|
||||
@@ -579,8 +702,7 @@ public class EzTreeState extends BaseAppState {
|
||||
Path baseDir = ASSET_ROOT.resolve("Models").resolve("trees").resolve(subPath);
|
||||
Files.createDirectories(baseDir);
|
||||
Path outPath = baseDir.resolve(fileName + ".j3o");
|
||||
// Controls vor Export entfernen (nicht serialisierbar über BinaryExporter)
|
||||
while (lodRoot.getNumControls() > 0) lodRoot.removeControl(lodRoot.getControl(0));
|
||||
stripControlsRecursive(lodRoot);
|
||||
BinaryExporter.getInstance().save(lodRoot, outPath.toFile());
|
||||
|
||||
de.blight.common.ModelMeta meta = new de.blight.common.ModelMeta(
|
||||
|
||||
@@ -27,6 +27,7 @@ import com.jme3.anim.SkinningControl;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.editor.SharedInput;
|
||||
import de.blight.editor.util.ImpostorUtil;
|
||||
import de.blight.editor.util.ModelExportUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -104,6 +105,10 @@ public class ModelEditorState extends BaseAppState {
|
||||
private boolean hasEmbeddedLods = false;
|
||||
private Spatial[] embeddedLodSpatials = null;
|
||||
|
||||
// true wenn showSpatialDirectly() zuletzt einen generierten Preview in modelWrapper gelegt hat.
|
||||
// Beim Wechsel zurück zu einem eingebetteten LOD muss originalSpatial wiederhergestellt werden.
|
||||
private boolean showingDirectPreview = false;
|
||||
|
||||
// Materialien mit eigenem m_LightDir (Tree.j3md / TreeLeaf.j3md) – werden jedes Frame aktualisiert
|
||||
private final java.util.List<Material> treeLightMaterials = new java.util.ArrayList<>();
|
||||
// PBR-Materialien (PBRLighting) – brauchen EmissivePower=1 als IBL-Fallback
|
||||
@@ -148,6 +153,25 @@ public class ModelEditorState extends BaseAppState {
|
||||
// Welt sofort ausblenden, auch wenn noch kein Modell geladen ist
|
||||
if (previewRoot == null) enterPreview();
|
||||
|
||||
// Scale-Bake und LOD-Einbettung VOR dem Modellwechsel verarbeiten.
|
||||
// Sonst würde ein openPath im gleichen Frame die Preview-Nodes leeren,
|
||||
// bevor embedLodsAndSave() sie lesen kann → LODs würden nicht gespeichert.
|
||||
if (input.modelEditorBakeScaleRequest) {
|
||||
input.modelEditorBakeScaleRequest = false;
|
||||
Path bakePath = input.modelEditorBakeScalePath;
|
||||
if (bakePath != null) {
|
||||
bakeScaleIntoModel(bakePath,
|
||||
input.modelEditorScaleX,
|
||||
input.modelEditorScaleY,
|
||||
input.modelEditorScaleZ);
|
||||
}
|
||||
}
|
||||
|
||||
if (input.modelEditorEmbedLodsRequest) {
|
||||
input.modelEditorEmbedLodsRequest = false;
|
||||
embedLodsAndSave(input.modelEditorEmbedLodsPath);
|
||||
}
|
||||
|
||||
// Modell laden
|
||||
String openPath = input.modelEditorOpenPath;
|
||||
if (openPath != null) {
|
||||
@@ -186,8 +210,22 @@ public class ModelEditorState extends BaseAppState {
|
||||
}
|
||||
showSpatialDirectly(genPreview.clone());
|
||||
} else if (hasEmbeddedLods && embeddedLodSpatials != null) {
|
||||
// Nach showSpatialDirectly() ist originalSpatial aus modelWrapper raus – wiederherstellen
|
||||
if (showingDirectPreview && originalSpatial != null) {
|
||||
if (modelWrapper != null) previewRoot.detachChild(modelWrapper);
|
||||
modelWrapper = new Node("model_wrapper");
|
||||
modelWrapper.attachChild(originalSpatial);
|
||||
applyScale(input.modelEditorScaleX, input.modelEditorScaleY, input.modelEditorScaleZ);
|
||||
previewRoot.attachChild(modelWrapper);
|
||||
showingDirectPreview = false;
|
||||
}
|
||||
// Eingebettete LOD-Kinder direkt ein-/ausblenden
|
||||
int lodIdx = Math.min(input.modelEditorLodPreview, embeddedLodSpatials.length - 1);
|
||||
int lodIdx = input.modelEditorLodPreview;
|
||||
// Fallback auf 0 wenn der gewünschte Index keinen Eintrag hat
|
||||
if (lodIdx >= embeddedLodSpatials.length || embeddedLodSpatials[lodIdx] == null) {
|
||||
lodIdx = 0;
|
||||
input.modelEditorLodPreview = 0;
|
||||
}
|
||||
for (int i = 0; i < embeddedLodSpatials.length; i++) {
|
||||
if (embeddedLodSpatials[i] != null) {
|
||||
boolean show = (i == lodIdx);
|
||||
@@ -195,6 +233,7 @@ public class ModelEditorState extends BaseAppState {
|
||||
embeddedLodSpatials[i].setShadowMode(RenderQueue.ShadowMode.Off);
|
||||
}
|
||||
}
|
||||
updateBounds();
|
||||
} else {
|
||||
// Pfad-basierter Fallback
|
||||
String path = switch (input.modelEditorLodPreview) {
|
||||
@@ -251,18 +290,6 @@ public class ModelEditorState extends BaseAppState {
|
||||
input.modelEditorAttachedEmitters);
|
||||
}
|
||||
|
||||
// Scale in j3o einbrennen (vor Thumbnail, damit Thumbnail die gebackene Datei erhält)
|
||||
if (input.modelEditorBakeScaleRequest) {
|
||||
input.modelEditorBakeScaleRequest = false;
|
||||
Path bakePath = input.modelEditorBakeScalePath;
|
||||
if (bakePath != null) {
|
||||
bakeScaleIntoModel(bakePath,
|
||||
input.modelEditorScaleX,
|
||||
input.modelEditorScaleY,
|
||||
input.modelEditorScaleZ);
|
||||
}
|
||||
}
|
||||
|
||||
// Thumbnail auf Anforderung generieren
|
||||
Path thumbReq = input.modelEditorThumbnailRequest;
|
||||
if (thumbReq != null && modelWrapper != null) {
|
||||
@@ -364,6 +391,7 @@ public class ModelEditorState extends BaseAppState {
|
||||
currentPath = assetPath;
|
||||
|
||||
modelWrapper = new Node("model_wrapper");
|
||||
showingDirectPreview = false;
|
||||
hasEmbeddedLods = false;
|
||||
embeddedLodSpatials = null;
|
||||
input.modelEditorHasEmbeddedLods = false;
|
||||
@@ -383,18 +411,43 @@ public class ModelEditorState extends BaseAppState {
|
||||
originalSpatial = model;
|
||||
modelWrapper.attachChild(model);
|
||||
|
||||
// Eingebettete LODs erkennen: Node mit ≥2 Kindern, wobei Kind 0 sichtbar
|
||||
// und alle weiteren mit CullHint.Always gesetzt sind (EZ-Tree / TreeGenerator-Muster).
|
||||
if (model instanceof Node rootNode && rootNode.getChildren().size() >= 2) {
|
||||
// Eingebettete LODs erkennen: Node mit ≥2 Kindern, wobei mindestens eines
|
||||
// hinter Index 0 CullHint.Always hat (EZ-Tree / TreeGenerator-Muster).
|
||||
// embeddedLodSpatials ist immer 3 Einträge: [0]=HD, [1]=LOD1|null, [2]=LOD2|null
|
||||
if (model instanceof Node rootNode) {
|
||||
log.info("[ModelEditor] Geladen '{}': {} direkte Kinder", assetPath, rootNode.getChildren().size());
|
||||
for (int i = 0; i < rootNode.getChildren().size(); i++) {
|
||||
Spatial c = rootNode.getChild(i);
|
||||
log.info("[ModelEditor] [{}] '{}' cullHint={}", i, c.getName(), c.getCullHint());
|
||||
}
|
||||
if (rootNode.getChildren().size() >= 2) {
|
||||
var children = rootNode.getChildren();
|
||||
boolean lodPattern = children.stream().skip(1)
|
||||
.allMatch(s -> s.getCullHint() == Spatial.CullHint.Always);
|
||||
if (lodPattern) {
|
||||
embeddedLodSpatials = children.toArray(new Spatial[0]);
|
||||
hasEmbeddedLods = true;
|
||||
boolean hasAnyHidden = children.stream().skip(1)
|
||||
.anyMatch(s -> s.getCullHint() == Spatial.CullHint.Always);
|
||||
log.info("[ModelEditor] hasAnyHidden (ab Index 1)={}", hasAnyHidden);
|
||||
if (hasAnyHidden) {
|
||||
embeddedLodSpatials = new Spatial[3];
|
||||
embeddedLodSpatials[0] = children.get(0);
|
||||
for (int i = 1; i < children.size(); i++) {
|
||||
Spatial child = children.get(i);
|
||||
if (child.getCullHint() != Spatial.CullHint.Always) continue;
|
||||
if ("lod2".equals(child.getName())) {
|
||||
embeddedLodSpatials[2] = child;
|
||||
} else {
|
||||
embeddedLodSpatials[1] = child;
|
||||
}
|
||||
}
|
||||
hasEmbeddedLods = embeddedLodSpatials[1] != null || embeddedLodSpatials[2] != null;
|
||||
log.info("[ModelEditor] Eingebettete LODs erkannt: lod1={}, lod2={}",
|
||||
embeddedLodSpatials[1] != null, embeddedLodSpatials[2] != null);
|
||||
if (hasEmbeddedLods) {
|
||||
input.modelEditorHasEmbeddedLods = true;
|
||||
input.modelEditorHasEmbeddedLod1 = embeddedLodSpatials.length > 1;
|
||||
input.modelEditorHasEmbeddedLod2 = embeddedLodSpatials.length > 2;
|
||||
input.modelEditorHasEmbeddedLod1 = embeddedLodSpatials[1] != null;
|
||||
input.modelEditorHasEmbeddedLod2 = embeddedLodSpatials[2] != null;
|
||||
} else {
|
||||
embeddedLodSpatials = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@@ -481,6 +534,7 @@ public class ModelEditorState extends BaseAppState {
|
||||
if (previewRoot == null) enterPreview();
|
||||
if (modelWrapper != null) previewRoot.detachChild(modelWrapper);
|
||||
modelWrapper = new Node("model_wrapper");
|
||||
showingDirectPreview = true;
|
||||
stripControls(spatial);
|
||||
treeLightMaterials.clear();
|
||||
pbrMaterials.clear();
|
||||
@@ -630,8 +684,13 @@ public class ModelEditorState extends BaseAppState {
|
||||
|
||||
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 (lodIdx == 1) {
|
||||
input.modelEditorHasEmbeddedLod1 = false;
|
||||
// LOD2 setzt LOD1 voraus — Kaskade: LOD2 ebenfalls entfernen
|
||||
removeEmbeddedLod(2);
|
||||
} else if (lodIdx == 2) {
|
||||
input.modelEditorHasEmbeddedLod2 = false;
|
||||
}
|
||||
|
||||
if (input.modelEditorLodPreview == lodIdx) {
|
||||
input.modelEditorLodPreview = 0;
|
||||
@@ -654,7 +713,8 @@ public class ModelEditorState extends BaseAppState {
|
||||
java.nio.file.Path absPath = ASSET_ROOT.resolve(currentPath);
|
||||
Spatial root = modelWrapper.getChild(0);
|
||||
BinaryExporter.getInstance().save(root, absPath.toFile());
|
||||
app.getAssetManager().clearCache();
|
||||
syncToMirrors(absPath);
|
||||
app.getAssetManager().deleteFromCache(new com.jme3.asset.ModelKey(currentPath));
|
||||
log.info("[ModelEditor] LOD {} entfernt, Datei gespeichert: {}", lodIdx, currentPath);
|
||||
} catch (Exception ex) {
|
||||
log.error("[ModelEditor] Speichern nach LOD-Entfernung fehlgeschlagen: {}", ex.getMessage(), ex);
|
||||
@@ -781,10 +841,22 @@ public class ModelEditorState extends BaseAppState {
|
||||
input.modelEditorBoundsH = bb.getYExtent() * 2f;
|
||||
input.modelEditorBoundsD = bb.getZExtent() * 2f;
|
||||
input.modelEditorBoundsReady = true;
|
||||
input.modelEditorPolyCount = countVisibleTriangles(modelWrapper);
|
||||
orbitCenter = bb.getCenter().clone();
|
||||
}
|
||||
}
|
||||
|
||||
private static int countVisibleTriangles(Spatial s) {
|
||||
if (s == null || s.getCullHint() == Spatial.CullHint.Always) return 0;
|
||||
if (s instanceof Geometry g) return g.getMesh().getTriangleCount();
|
||||
if (s instanceof Node n) {
|
||||
int sum = 0;
|
||||
for (Spatial child : n.getChildren()) sum += countVisibleTriangles(child);
|
||||
return sum;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private BoundingBox getBoundingBox() {
|
||||
if (modelWrapper == null) return null;
|
||||
modelWrapper.updateGeometricState();
|
||||
@@ -894,15 +966,29 @@ public class ModelEditorState extends BaseAppState {
|
||||
int i = 0;
|
||||
while (i < s.getNumControls()) {
|
||||
com.jme3.scene.control.Control c = s.getControl(i);
|
||||
if (c instanceof ImpostorUtil.ImpostorViewControl) { i++; }
|
||||
if (c instanceof ImpostorUtil.ImpostorViewControl
|
||||
|| c instanceof com.jme3.scene.control.BillboardControl) { i++; }
|
||||
else { s.removeControl(c); }
|
||||
}
|
||||
if (s instanceof Node n) n.getChildren().forEach(ModelEditorState::stripControls);
|
||||
}
|
||||
|
||||
private static boolean isImpostorNode(Node n) {
|
||||
// Direktkinder prüfen (NICHT rekursiv) – Node.getChild() sucht den ganzen Baum
|
||||
for (Spatial c : n.getChildren()) {
|
||||
if ("quad_0".equals(c.getName())) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void initImpostorControls(Spatial s) {
|
||||
ImpostorUtil.ImpostorViewControl ctrl = s.getControl(ImpostorUtil.ImpostorViewControl.class);
|
||||
if (ctrl != null) ctrl.setCamera(cam);
|
||||
if (ctrl != null) {
|
||||
ctrl.setCamera(cam);
|
||||
} else if (s instanceof Node n && isImpostorNode(n)) {
|
||||
// Impostor-Node nach j3o-Reload ohne gespeicherten Control → neu hinzufügen
|
||||
ImpostorUtil.addViewControl(n, cam);
|
||||
}
|
||||
if (s instanceof Node n) {
|
||||
for (Spatial child : n.getChildren()) initImpostorControls(child);
|
||||
}
|
||||
@@ -964,6 +1050,190 @@ public class ModelEditorState extends BaseAppState {
|
||||
}
|
||||
}
|
||||
|
||||
// ── LOD-Einbettung ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Kopiert eine gespeicherte j3o-Datei in alle bekannten Spiegel-Verzeichnisse
|
||||
* (IntelliJ bin/main und Gradle build/resources/main), damit JME's ClasspathLocator
|
||||
* nicht die veraltete Fassung aus dem Classpath lädt.
|
||||
*/
|
||||
private void syncToMirrors(Path savedFile) {
|
||||
java.nio.file.Path rel;
|
||||
try {
|
||||
rel = ASSET_ROOT.relativize(savedFile);
|
||||
} catch (Exception e) {
|
||||
return;
|
||||
}
|
||||
java.nio.file.Path projectRoot = de.blight.editor.ProjectRoot.PATH;
|
||||
java.nio.file.Path[] mirrors = {
|
||||
projectRoot.resolve("blight-assets").resolve("bin").resolve("main").resolve(rel),
|
||||
projectRoot.resolve("blight-assets").resolve("build").resolve("resources").resolve("main").resolve(rel),
|
||||
};
|
||||
for (java.nio.file.Path mirror : mirrors) {
|
||||
if (java.nio.file.Files.exists(mirror)) {
|
||||
try {
|
||||
java.nio.file.Files.copy(savedFile, mirror,
|
||||
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
log.info("[ModelEditor] Spiegel aktualisiert: {}", mirror);
|
||||
} catch (Exception e) {
|
||||
log.warn("[ModelEditor] Spiegel-Update fehlgeschlagen ({}): {}", mirror, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void embedLodsAndSave(Path j3oPath) {
|
||||
log.info("[ModelEditor] embedLodsAndSave aufgerufen: path={}, originalSpatial={}",
|
||||
j3oPath, originalSpatial != null ? originalSpatial.getName() : "null");
|
||||
if (j3oPath == null || originalSpatial == null) {
|
||||
log.warn("[ModelEditor] embedLodsAndSave abgebrochen: j3oPath={} originalSpatial={}",
|
||||
j3oPath, originalSpatial);
|
||||
return;
|
||||
}
|
||||
Spatial lod1Preview = input.modelImportLod1PreviewNode.get();
|
||||
Spatial lod2Preview = input.modelImportLod2PreviewNode.get();
|
||||
log.info("[ModelEditor] Preview-Nodes: lod1={}, lod2={}",
|
||||
lod1Preview != null ? lod1Preview.getName() : "null",
|
||||
lod2Preview != null ? lod2Preview.getName() : "null");
|
||||
if (lod1Preview == null && lod2Preview == null) {
|
||||
log.warn("[ModelEditor] Beide Preview-Nodes null – kein Einbetten möglich");
|
||||
return;
|
||||
}
|
||||
if (!(originalSpatial instanceof Node rootNode)) {
|
||||
log.warn("[ModelEditor] LOD-Einbettung: Modell-Root kein Node – abgebrochen");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
log.info("[ModelEditor] rootNode '{}' hat vor Einbettung {} Kinder",
|
||||
rootNode.getName(), rootNode.getChildren().size());
|
||||
for (int i = 0; i < rootNode.getChildren().size(); i++) {
|
||||
Spatial c = rootNode.getChild(i);
|
||||
log.info("[ModelEditor] [{}] '{}' cullHint={}", i, c.getName(), c.getCullHint());
|
||||
}
|
||||
|
||||
// Vorhandene LODs selektiv ersetzen:
|
||||
// Index 0 (HD-Modell) NIEMALS entfernen, auch wenn CullHint.Always gesetzt ist.
|
||||
if (lod1Preview != null) {
|
||||
Spatial oldLod1 = null;
|
||||
for (int i = 1; i < rootNode.getChildren().size(); i++) {
|
||||
Spatial c = rootNode.getChild(i);
|
||||
if (!"lod2".equals(c.getName())) { oldLod1 = c; break; }
|
||||
}
|
||||
if (oldLod1 != null) {
|
||||
log.info("[ModelEditor] Altes LOD1 '{}' entfernt", oldLod1.getName());
|
||||
rootNode.detachChild(oldLod1);
|
||||
}
|
||||
}
|
||||
if (lod2Preview != null) {
|
||||
Spatial oldLod2 = rootNode.getChild("lod2");
|
||||
if (oldLod2 != null) {
|
||||
log.info("[ModelEditor] Altes LOD2 '{}' entfernt", oldLod2.getName());
|
||||
rootNode.detachChild(oldLod2);
|
||||
}
|
||||
}
|
||||
|
||||
// Controls aus dem gesamten Baum entfernen (nicht serialisierbar im Game-Modul)
|
||||
ModelExportUtil.stripControls(rootNode);
|
||||
|
||||
if (lod1Preview != null) {
|
||||
Spatial l1 = lod1Preview.clone();
|
||||
l1.setName("lod1");
|
||||
ModelExportUtil.stripControls(l1);
|
||||
l1.setCullHint(Spatial.CullHint.Always);
|
||||
rootNode.attachChild(l1);
|
||||
log.info("[ModelEditor] LOD1 eingebettet als '{}'", l1.getName());
|
||||
}
|
||||
if (lod2Preview != null) {
|
||||
Spatial l2 = lod2Preview.clone();
|
||||
l2.setName("lod2");
|
||||
ModelExportUtil.stripControls(l2);
|
||||
l2.setCullHint(Spatial.CullHint.Always);
|
||||
l2.setShadowMode(com.jme3.renderer.queue.RenderQueue.ShadowMode.Off);
|
||||
rootNode.attachChild(l2);
|
||||
log.info("[ModelEditor] LOD2 eingebettet als '{}'", l2.getName());
|
||||
}
|
||||
|
||||
// HD-Modell (Index 0): sichtbar speichern und als "lod0" kennzeichnen
|
||||
// damit WorldObjectsState Fall A (eingebettetes LOD) korrekt erkennt.
|
||||
if (!rootNode.getChildren().isEmpty()) {
|
||||
Spatial hd = rootNode.getChild(0);
|
||||
hd.setName("lod0");
|
||||
hd.setCullHint(Spatial.CullHint.Inherit);
|
||||
}
|
||||
|
||||
log.info("[ModelEditor] rootNode '{}' hat vor dem Speichern {} Kinder:",
|
||||
rootNode.getName(), rootNode.getChildren().size());
|
||||
for (int i = 0; i < rootNode.getChildren().size(); i++) {
|
||||
Spatial c = rootNode.getChild(i);
|
||||
log.info("[ModelEditor] [{}] '{}' cullHint={}", i, c.getName(), c.getCullHint());
|
||||
}
|
||||
|
||||
BinaryExporter.getInstance().save(rootNode, j3oPath.toFile());
|
||||
long fileSize = j3oPath.toFile().length();
|
||||
log.info("[ModelEditor] j3o gespeichert: {} ({} bytes)", j3oPath, fileSize);
|
||||
syncToMirrors(j3oPath);
|
||||
|
||||
String relPath = ASSET_ROOT.relativize(j3oPath).toString().replace('\\', '/');
|
||||
app.getAssetManager().deleteFromCache(new com.jme3.asset.ModelKey(relPath));
|
||||
|
||||
// In-Memory-Zustand ohne Reload synchronisieren (immer 3 Slots: [HD, LOD1, LOD2])
|
||||
embeddedLodSpatials = new Spatial[3];
|
||||
embeddedLodSpatials[0] = rootNode.getChild(0);
|
||||
for (int i = 1; i < rootNode.getChildren().size(); i++) {
|
||||
Spatial child = rootNode.getChild(i);
|
||||
if ("lod2".equals(child.getName())) {
|
||||
embeddedLodSpatials[2] = child;
|
||||
} else {
|
||||
embeddedLodSpatials[1] = child;
|
||||
}
|
||||
}
|
||||
hasEmbeddedLods = true;
|
||||
input.modelEditorHasEmbeddedLods = true;
|
||||
input.modelEditorHasEmbeddedLod1 = embeddedLodSpatials[1] != null;
|
||||
input.modelEditorHasEmbeddedLod2 = embeddedLodSpatials[2] != null;
|
||||
log.info("[ModelEditor] In-Memory-Status: hasLod1={}, hasLod2={}",
|
||||
input.modelEditorHasEmbeddedLod1, input.modelEditorHasEmbeddedLod2);
|
||||
|
||||
// ImpostorViewControl auf Impostor-Knoten wiederherstellen (für korrekte Editor-Anzeige)
|
||||
for (Spatial s : embeddedLodSpatials) {
|
||||
if (s instanceof Node ln && isImpostorNode(ln)) {
|
||||
if (ln.getControl(ImpostorUtil.ImpostorViewControl.class) == null) {
|
||||
ImpostorUtil.addViewControl(ln, cam);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// originalSpatial (= rootNode) in modelWrapper wiederherstellen.
|
||||
// showSpatialDirectly() könnte modelWrapper durch einen Preview-Klon ersetzt haben.
|
||||
if (previewRoot != null) {
|
||||
if (modelWrapper != null) previewRoot.detachChild(modelWrapper);
|
||||
modelWrapper = new Node("model_wrapper");
|
||||
modelWrapper.attachChild(rootNode);
|
||||
applyScale(input.modelEditorScaleX, input.modelEditorScaleY, input.modelEditorScaleZ);
|
||||
previewRoot.attachChild(modelWrapper);
|
||||
showingDirectPreview = false;
|
||||
}
|
||||
|
||||
// LOD-Vorschau auf HD (Index 0) zurücksetzen
|
||||
input.modelEditorLodPreview = 0;
|
||||
embeddedLodSpatials[0].setCullHint(Spatial.CullHint.Inherit);
|
||||
for (int i = 1; i < embeddedLodSpatials.length; i++) {
|
||||
if (embeddedLodSpatials[i] != null) {
|
||||
embeddedLodSpatials[i].setCullHint(Spatial.CullHint.Always);
|
||||
}
|
||||
}
|
||||
|
||||
input.modelImportHasLod1 = false;
|
||||
input.modelImportHasLod2 = false;
|
||||
input.modelImportLod1PreviewNode.set(null);
|
||||
input.modelImportLod2PreviewNode.set(null);
|
||||
|
||||
log.info("[ModelEditor] LOD-Einbettung abgeschlossen: {}", j3oPath.getFileName());
|
||||
} catch (Exception e) {
|
||||
log.error("[ModelEditor] LOD-Einbettung fehlgeschlagen: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Scale-Bake ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -985,11 +1255,12 @@ public class ModelEditorState extends BaseAppState {
|
||||
log.info("[ModelEditor] Animiert: Scale ({},{},{}) als Spatial-Transform gespeichert", sx, sy, sz);
|
||||
} else {
|
||||
root.setLocalScale(sx, sy, sz);
|
||||
ModelImportState.stripControls(root);
|
||||
ModelImportState.bakeTransform(root, new Matrix4f());
|
||||
ModelExportUtil.stripControls(root);
|
||||
ModelExportUtil.bakeTransform(root, new Matrix4f());
|
||||
log.info("[ModelEditor] Statisch: Scale ({},{},{}) in Vertices gebacken", sx, sy, sz);
|
||||
}
|
||||
BinaryExporter.getInstance().save(root, j3oPath.toFile());
|
||||
syncToMirrors(j3oPath);
|
||||
log.info("[ModelEditor] j3o nach Bake gespeichert: {}", j3oPath.getFileName());
|
||||
} catch (Exception e) {
|
||||
log.error("[ModelEditor] Scale-Bake fehlgeschlagen: {}", e.getMessage(), e);
|
||||
@@ -1030,6 +1301,7 @@ public class ModelEditorState extends BaseAppState {
|
||||
if (savable instanceof Spatial root) {
|
||||
ThumbnailRenderer.embed(root, pngBytes);
|
||||
BinaryExporter.getInstance().save(root, j3oPath.toFile());
|
||||
syncToMirrors(j3oPath);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[ModelEditor] j3o-Embed fehlgeschlagen: {}", e.getMessage(), e);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package de.blight.editor.state;
|
||||
|
||||
import de.blight.editor.util.ModelExportUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.FloatBuffer;
|
||||
@@ -104,10 +106,14 @@ public class ModelImportState extends BaseAppState {
|
||||
input.modelImportHasLod1 = false;
|
||||
input.modelImportLod1PreviewNode.set(null);
|
||||
input.modelLodGenStatus = "LOD1: (entfernt)";
|
||||
if (input.modelEditorLodPreview == 1) {
|
||||
if (input.modelEditorLodPreview == 1 || input.modelEditorLodPreview == 2) {
|
||||
input.modelEditorLodPreview = 0;
|
||||
input.modelEditorLodChanged = true;
|
||||
}
|
||||
// LOD2 setzt LOD1 voraus — Kaskade
|
||||
lod2Node = null;
|
||||
input.modelImportHasLod2 = false;
|
||||
input.modelImportLod2PreviewNode.set(null);
|
||||
} else if (clearLod == 2) {
|
||||
lod2Node = null;
|
||||
input.modelImportHasLod2 = false;
|
||||
@@ -161,17 +167,21 @@ public class ModelImportState extends BaseAppState {
|
||||
}
|
||||
try {
|
||||
Spatial clone = modelSpatial.clone();
|
||||
stripControls(clone);
|
||||
ModelExportUtil.stripControls(clone);
|
||||
|
||||
int trisBefore = countTriangles(clone);
|
||||
|
||||
// Blätter bei Bäumen von der Decimation ausnehmen (Vertices sollen unverändert bleiben)
|
||||
String path = modelEditorState.getCurrentPath();
|
||||
boolean skipLeaves = path != null && path.replace('\\', '/').contains("Models/trees");
|
||||
|
||||
// Welding schafft geteilte Vertices → Manifold-Kanten für beide Algorithmen nötig
|
||||
weldVerticesRecursive(clone, 0.01f);
|
||||
|
||||
if ("jme".equals(input.modelLodAlgorithm)) {
|
||||
decimateRecursiveJme(clone, 1.0f - reduction);
|
||||
decimateRecursiveJme(clone, 1.0f - reduction, skipLeaves);
|
||||
} else {
|
||||
decimateRecursiveBlight(clone, 1.0f - reduction);
|
||||
decimateRecursiveBlight(clone, 1.0f - reduction, skipLeaves);
|
||||
}
|
||||
|
||||
Node wrapper;
|
||||
@@ -212,7 +222,7 @@ public class ModelImportState extends BaseAppState {
|
||||
else input.modelImportHasLod2 = false;
|
||||
try {
|
||||
Spatial loaded = assets.loadModel(assetPath);
|
||||
stripControls(loaded);
|
||||
ModelExportUtil.stripControls(loaded);
|
||||
Node wrapper;
|
||||
if (loaded instanceof Node n) wrapper = n;
|
||||
else { wrapper = new Node("lod" + level); wrapper.attachChild(loaded); }
|
||||
@@ -253,7 +263,7 @@ public class ModelImportState extends BaseAppState {
|
||||
Spatial cleanClone = src.clone();
|
||||
// Controls entfernen, damit z.B. ein ImportLodControl den CullHint
|
||||
// nicht auf lod0 zurücksetzt und die Geometrie im Capture unsichtbar macht.
|
||||
stripControls(cleanClone);
|
||||
ModelExportUtil.stripControls(cleanClone);
|
||||
cleanClone.setLocalTranslation(Vector3f.ZERO);
|
||||
cleanClone.setLocalScale(input.modelEditorScaleX, input.modelEditorScaleY, input.modelEditorScaleZ);
|
||||
cleanClone.setLocalRotation(Quaternion.IDENTITY);
|
||||
@@ -337,7 +347,7 @@ public class ModelImportState extends BaseAppState {
|
||||
|
||||
// Clone original, apply target scale, compute bottom-aligned pivot
|
||||
Spatial lod0 = srcSpatial.clone();
|
||||
stripControls(lod0);
|
||||
ModelExportUtil.stripControls(lod0);
|
||||
lod0.setLocalScale(sx, sy, sz);
|
||||
lod0.setLocalTranslation(Vector3f.ZERO);
|
||||
lod0.updateGeometricState();
|
||||
@@ -348,7 +358,7 @@ public class ModelImportState extends BaseAppState {
|
||||
translationY = -(bb.getCenter().y - bb.getYExtent()) + pivotY;
|
||||
}
|
||||
lod0.setLocalTranslation(0f, translationY, 0f);
|
||||
bakeTransform(lod0, new Matrix4f());
|
||||
ModelExportUtil.bakeTransform(lod0, new Matrix4f());
|
||||
lod0.setName("lod0");
|
||||
|
||||
Node root = new Node(fileName);
|
||||
@@ -366,11 +376,17 @@ public class ModelImportState extends BaseAppState {
|
||||
}
|
||||
if (lod2Node != null) {
|
||||
lod2Node.setName("lod2");
|
||||
boolean isImpostor = (lod2Node instanceof Node ln && ln.getChild("quad_0") != null);
|
||||
if (isImpostor) {
|
||||
// Impostor-Quads haben vorberechnete Weltpositionen – bakeTransform würde die Geometrie zerstören
|
||||
ModelExportUtil.stripControls(lod2Node);
|
||||
} else {
|
||||
lod2Node.setLocalScale(sx, sy, sz);
|
||||
lod2Node.setLocalTranslation(0f, translationY, 0f);
|
||||
bakeSpatialInPlace(lod2Node);
|
||||
}
|
||||
lod2Node.setCullHint(Spatial.CullHint.Always);
|
||||
lod2Node.setShadowMode(RenderQueue.ShadowMode.Off);
|
||||
bakeSpatialInPlace(lod2Node);
|
||||
root.attachChild(lod2Node);
|
||||
}
|
||||
|
||||
@@ -428,8 +444,11 @@ public class ModelImportState extends BaseAppState {
|
||||
// ── Mesh-Decimation ───────────────────────────────────────────────────────
|
||||
|
||||
/** JME3 Progressive Mesh: verwendet JME's LodGenerator direkt. */
|
||||
private static void decimateRecursiveJme(Spatial s, float keepRatio) {
|
||||
private static void decimateRecursiveJme(Spatial s, float keepRatio, boolean skipLeaves) {
|
||||
if (s instanceof Geometry g) {
|
||||
if (skipLeaves && isLeafGeometry(g)) {
|
||||
return;
|
||||
}
|
||||
Mesh mesh = g.getMesh();
|
||||
int origTris = mesh.getTriangleCount();
|
||||
try {
|
||||
@@ -451,7 +470,7 @@ public class ModelImportState extends BaseAppState {
|
||||
log.warn("[ModelImport] JME bakeLods fehlgeschlagen für {}: {}", g.getName(), e.getMessage());
|
||||
}
|
||||
} else if (s instanceof Node n) {
|
||||
for (Spatial child : n.getChildren()) decimateRecursiveJme(child, keepRatio);
|
||||
for (Spatial child : n.getChildren()) decimateRecursiveJme(child, keepRatio, skipLeaves);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,14 +478,34 @@ public class ModelImportState extends BaseAppState {
|
||||
* Blight Edge Collapse: verschmilzt Kanten (je zwei geteilte Dreiecke) iterativ,
|
||||
* bis die Zielanzahl erreicht ist. Das Mesh bleibt durchgehend geschlossen.
|
||||
*/
|
||||
private static void decimateRecursiveBlight(Spatial s, float keepRatio) {
|
||||
private static void decimateRecursiveBlight(Spatial s, float keepRatio, boolean skipLeaves) {
|
||||
if (s instanceof Geometry g) {
|
||||
if (skipLeaves && isLeafGeometry(g)) {
|
||||
return;
|
||||
}
|
||||
g.setMesh(edgeCollapseMesh(g.getMesh(), keepRatio));
|
||||
} else if (s instanceof Node n) {
|
||||
for (Spatial child : n.getChildren()) decimateRecursiveBlight(child, keepRatio);
|
||||
for (Spatial child : n.getChildren()) decimateRecursiveBlight(child, keepRatio, skipLeaves);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isLeafGeometry(Geometry g) {
|
||||
String name = g.getName();
|
||||
if (name != null) {
|
||||
String lower = name.toLowerCase();
|
||||
if (lower.contains("leaf") || lower.contains("leaves")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (g.getMaterial() != null && g.getMaterial().getMaterialDef() != null) {
|
||||
String defName = g.getMaterial().getMaterialDef().getName();
|
||||
if (defName != null && defName.contains("TreeLeaf")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert ein LOD-Level aus JME3's bakeLods-Ergebnis als eigenständiges Mesh.
|
||||
* Handhabt alle möglichen Index-Buffer-Formate (UnsignedShort, UnsignedInt, Float).
|
||||
@@ -738,83 +777,14 @@ public class ModelImportState extends BaseAppState {
|
||||
|
||||
private Spatial bakeSpatial(Spatial source) {
|
||||
Spatial clone = source.clone();
|
||||
stripControls(clone);
|
||||
bakeTransform(clone, new Matrix4f());
|
||||
ModelExportUtil.stripControls(clone);
|
||||
ModelExportUtil.bakeTransform(clone, new Matrix4f());
|
||||
return clone;
|
||||
}
|
||||
|
||||
private void bakeSpatialInPlace(Spatial s) {
|
||||
stripControls(s);
|
||||
bakeTransform(s, new Matrix4f());
|
||||
}
|
||||
|
||||
static void bakeTransform(Spatial s, Matrix4f accum) {
|
||||
Matrix4f localMat = new Matrix4f();
|
||||
s.getLocalTransform().toTransformMatrix(localMat);
|
||||
Matrix4f combined = accum.mult(localMat);
|
||||
|
||||
if (s instanceof Geometry g) {
|
||||
applyMatrixToMesh(g, combined);
|
||||
g.setLocalTranslation(Vector3f.ZERO);
|
||||
g.setLocalScale(1f);
|
||||
g.setLocalRotation(Quaternion.IDENTITY);
|
||||
} else if (s instanceof Node n) {
|
||||
for (Spatial child : n.getChildren()) bakeTransform(child, combined);
|
||||
n.setLocalTranslation(Vector3f.ZERO);
|
||||
n.setLocalScale(1f);
|
||||
n.setLocalRotation(Quaternion.IDENTITY);
|
||||
}
|
||||
}
|
||||
|
||||
static void applyMatrixToMesh(Geometry g, Matrix4f mat) {
|
||||
Mesh newMesh = g.getMesh().deepClone();
|
||||
|
||||
FloatBuffer pos = newMesh.getFloatBuffer(VertexBuffer.Type.Position);
|
||||
if (pos != null) {
|
||||
pos.rewind();
|
||||
Vector3f v = new Vector3f();
|
||||
while (pos.remaining() >= 3) {
|
||||
int idx = pos.position();
|
||||
v.set(pos.get(), pos.get(), pos.get());
|
||||
mat.mult(v, v);
|
||||
pos.put(idx, v.x); pos.put(idx + 1, v.y); pos.put(idx + 2, v.z);
|
||||
}
|
||||
pos.rewind();
|
||||
newMesh.getBuffer(VertexBuffer.Type.Position).setUpdateNeeded();
|
||||
}
|
||||
|
||||
FloatBuffer norm = newMesh.getFloatBuffer(VertexBuffer.Type.Normal);
|
||||
if (norm != null) {
|
||||
Matrix3f normalMat = buildNormalMatrix(mat);
|
||||
norm.rewind();
|
||||
Vector3f n = new Vector3f();
|
||||
while (norm.remaining() >= 3) {
|
||||
int idx = norm.position();
|
||||
n.set(norm.get(), norm.get(), norm.get());
|
||||
normalMat.mult(n, n); n.normalizeLocal();
|
||||
norm.put(idx, n.x); norm.put(idx + 1, n.y); norm.put(idx + 2, n.z);
|
||||
}
|
||||
norm.rewind();
|
||||
newMesh.getBuffer(VertexBuffer.Type.Normal).setUpdateNeeded();
|
||||
}
|
||||
|
||||
newMesh.updateBound();
|
||||
g.setMesh(newMesh);
|
||||
}
|
||||
|
||||
static Matrix3f buildNormalMatrix(Matrix4f mat) {
|
||||
Matrix3f m3 = new Matrix3f(
|
||||
mat.m00, mat.m01, mat.m02,
|
||||
mat.m10, mat.m11, mat.m12,
|
||||
mat.m20, mat.m21, mat.m22);
|
||||
try { m3.invertLocal(); m3.transposeLocal(); }
|
||||
catch (Exception e) { m3.loadIdentity(); }
|
||||
return m3;
|
||||
}
|
||||
|
||||
static void stripControls(Spatial s) {
|
||||
while (s.getNumControls() > 0) s.removeControl(s.getControl(0));
|
||||
if (s instanceof Node n) n.getChildren().forEach(ModelImportState::stripControls);
|
||||
ModelExportUtil.stripControls(s);
|
||||
ModelExportUtil.bakeTransform(s, new Matrix4f());
|
||||
}
|
||||
|
||||
private static Node wrapInNode(Spatial s, String name) {
|
||||
|
||||
@@ -30,7 +30,10 @@ public class TreeMeshBuilder {
|
||||
|
||||
public MeshResult build(TreeParams p, float quality) {
|
||||
boolean hd = quality >= 0.5f;
|
||||
int maxLevel = hd ? p.levels : Math.max(1, p.levels - 1);
|
||||
// maxLevel immer voll – Tiefe (= Zahl der Verzweigungsebenen) gilt auch für LD.
|
||||
// Bark-Reduktion erfolgt nur über numSec (/2) und segs (-2), nicht über Tiefe.
|
||||
// Andernfalls fehlt in LD eine ganze Ast-Ebene → exponentiell weniger Blatt-Äste.
|
||||
int maxLevel = p.levels;
|
||||
|
||||
Rng rng = new Rng(p.seed);
|
||||
VertexCollector barkCol = new VertexCollector();
|
||||
@@ -49,7 +52,11 @@ public class TreeMeshBuilder {
|
||||
if (lv >= maxLevel) continue;
|
||||
|
||||
// Per-Level-Parameter
|
||||
int numSec = Math.max(1, hd ? TreeParams.lv(p.sections, lv)
|
||||
// Auf dem Blatt-Level (letzte Ebene) immer volle Sektion-Dichte:
|
||||
// Blätter sollen in LD genauso viele Vertices haben wie in HD.
|
||||
boolean isLeafLevel = (lv == maxLevel - 1) && p.generateLeaves;
|
||||
int numSec = Math.max(1, (hd || isLeafLevel)
|
||||
? TreeParams.lv(p.sections, lv)
|
||||
: TreeParams.lv(p.sections, lv) / 2);
|
||||
float branchLen = TreeParams.lv(p.length, lv);
|
||||
float baseRad = TreeParams.lv(p.radius, lv);
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.jme3.renderer.ViewPort;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.*;
|
||||
import com.jme3.scene.control.AbstractControl;
|
||||
import com.jme3.scene.control.BillboardControl;
|
||||
import com.jme3.texture.FrameBuffer;
|
||||
import com.jme3.texture.Image;
|
||||
import com.jme3.texture.Texture2D;
|
||||
@@ -87,17 +88,16 @@ public final class ImpostorUtil {
|
||||
applyTreeLighting(capNode, toLight);
|
||||
|
||||
Node scene = new Node("impCapScene");
|
||||
// Hauptlicht (warm, von Kamera-Seite)
|
||||
// Lichtstärken auf typisches In-Game-Tageslicht abgestimmt.
|
||||
// Das Game-FPP wendet Gamma-Korrektur an (lineare Werte erscheinen heller);
|
||||
// deshalb sind die Werte etwas niedriger als die rein visuell korrekten Editor-Werte.
|
||||
scene.addLight(new DirectionalLight(fromLight,
|
||||
new ColorRGBA(1.2f, 1.1f, 0.9f, 1f)));
|
||||
// Fülllicht (kühler, von gegenüberliegender Seite)
|
||||
new ColorRGBA(0.70f, 0.65f, 0.52f, 1f)));
|
||||
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)
|
||||
new ColorRGBA(0.16f, 0.16f, 0.22f, 1f)));
|
||||
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)));
|
||||
new ColorRGBA(0.12f, 0.14f, 0.17f, 1f)));
|
||||
scene.addLight(new AmbientLight(new ColorRGBA(0.30f, 0.30f, 0.32f, 1f)));
|
||||
scene.attachChild(capNode);
|
||||
|
||||
ViewPort vp = app.getRenderManager().createPostView("impCap_" + System.nanoTime(), capCam);
|
||||
@@ -181,17 +181,27 @@ public final class ImpostorUtil {
|
||||
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);
|
||||
// Alpha Discard (Cutout) statt Alpha Blend:
|
||||
// Transparente Pixel werden verworfen → Depth-Write bleibt aktiv → FogFilter,
|
||||
// WaterFilter und andere Post-Effects sehen die korrekte Tiefe des Impostors.
|
||||
mat.setFloat("AlphaDiscardThreshold", 0.5f);
|
||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Off);
|
||||
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));
|
||||
// Alle Quads in derselben Ebene (yRot=0). Der BillboardControl unten
|
||||
// dreht den gesamten Node zur Kamera; ImpostorViewControl wählt den
|
||||
// passenden UV-Streifen per Kamerawinkel.
|
||||
n.attachChild(buildBillboardQuad("quad_" + d, 0f, yOff, size, mat.clone(), uMin, uMax));
|
||||
}
|
||||
n.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
// Opaque-Bucket: Depth-Write aktiv → korrekte Tiefen für alle Post-Effects.
|
||||
n.setQueueBucket(RenderQueue.Bucket.Opaque);
|
||||
BillboardControl bc = new BillboardControl();
|
||||
bc.setAlignment(BillboardControl.Alignment.AxialY);
|
||||
n.addControl(bc);
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -347,20 +357,20 @@ public final class ImpostorUtil {
|
||||
protected void controlUpdate(float tpf) {
|
||||
if (cam == null || !(spatial instanceof Node n)) return;
|
||||
List<Spatial> quads = n.getChildren();
|
||||
if (quads.size() < DIRS) return;
|
||||
int dirs = quads.size();
|
||||
if (dirs < 2) return;
|
||||
|
||||
Vector3f toCamera = cam.getLocation().subtract(spatial.getWorldTranslation());
|
||||
float dx = toCamera.x, dz = toCamera.z;
|
||||
if (Math.abs(dx) < 0.001f && Math.abs(dz) < 0.001f) return;
|
||||
|
||||
// Winkelindex: t in Einheiten von 2π/DIRS; nächstes Capture = round(t) % DIRS
|
||||
// Winkelindex: t in Einheiten von 2π/dirs; nächstes Capture = round(t) % dirs
|
||||
float angle = (float) Math.atan2(dx, dz); // −π … +π
|
||||
float t = angle * DIRS / FastMath.TWO_PI;
|
||||
int best = ((int) Math.round(t) % DIRS + DIRS) % DIRS;
|
||||
int opp = (best + DIRS / 2) % DIRS; // gegenüberliegendes Quad
|
||||
float t = angle * dirs / FastMath.TWO_PI;
|
||||
int best = ((int) Math.round(t) % dirs + dirs) % dirs;
|
||||
|
||||
for (int i = 0; i < quads.size(); i++) {
|
||||
quads.get(i).setCullHint(i == best || i == opp
|
||||
for (int i = 0; i < dirs; i++) {
|
||||
quads.get(i).setCullHint(i == best
|
||||
? Spatial.CullHint.Inherit
|
||||
: Spatial.CullHint.Always);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
package de.blight.editor.util;
|
||||
|
||||
import com.jme3.math.Matrix3f;
|
||||
import com.jme3.math.Matrix4f;
|
||||
import com.jme3.math.Quaternion;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.scene.Geometry;
|
||||
import com.jme3.scene.Mesh;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.Spatial;
|
||||
import com.jme3.scene.VertexBuffer;
|
||||
import com.jme3.scene.control.BillboardControl;
|
||||
|
||||
import java.nio.FloatBuffer;
|
||||
|
||||
public final class ModelExportUtil {
|
||||
|
||||
private ModelExportUtil() {}
|
||||
|
||||
public static void stripControls(Spatial s) {
|
||||
int i = 0;
|
||||
while (i < s.getNumControls()) {
|
||||
// BillboardControl bleibt erhalten: JME3-Built-in, überlebt j3o-Roundtrip
|
||||
// und wird im Game korrekt deserialisiert (kein Camera-Feld, keine externe Klasse).
|
||||
if (s.getControl(i) instanceof BillboardControl) { i++; }
|
||||
else { s.removeControl(s.getControl(i)); }
|
||||
}
|
||||
if (s instanceof Node n) {
|
||||
n.getChildren().forEach(ModelExportUtil::stripControls);
|
||||
}
|
||||
}
|
||||
|
||||
public static void bakeTransform(Spatial s, Matrix4f accum) {
|
||||
Matrix4f localMat = new Matrix4f();
|
||||
s.getLocalTransform().toTransformMatrix(localMat);
|
||||
Matrix4f combined = accum.mult(localMat);
|
||||
|
||||
if (s instanceof Geometry g) {
|
||||
applyMatrixToMesh(g, combined);
|
||||
g.setLocalTranslation(Vector3f.ZERO);
|
||||
g.setLocalScale(1f);
|
||||
g.setLocalRotation(Quaternion.IDENTITY);
|
||||
} else if (s instanceof Node n) {
|
||||
for (Spatial child : n.getChildren()) bakeTransform(child, combined);
|
||||
n.setLocalTranslation(Vector3f.ZERO);
|
||||
n.setLocalScale(1f);
|
||||
n.setLocalRotation(Quaternion.IDENTITY);
|
||||
}
|
||||
}
|
||||
|
||||
public static void applyMatrixToMesh(Geometry g, Matrix4f mat) {
|
||||
Mesh newMesh = g.getMesh().deepClone();
|
||||
|
||||
FloatBuffer pos = newMesh.getFloatBuffer(VertexBuffer.Type.Position);
|
||||
if (pos != null) {
|
||||
pos.rewind();
|
||||
Vector3f v = new Vector3f();
|
||||
while (pos.remaining() >= 3) {
|
||||
int idx = pos.position();
|
||||
v.set(pos.get(), pos.get(), pos.get());
|
||||
mat.mult(v, v);
|
||||
pos.put(idx, v.x); pos.put(idx + 1, v.y); pos.put(idx + 2, v.z);
|
||||
}
|
||||
pos.rewind();
|
||||
newMesh.getBuffer(VertexBuffer.Type.Position).setUpdateNeeded();
|
||||
}
|
||||
|
||||
FloatBuffer norm = newMesh.getFloatBuffer(VertexBuffer.Type.Normal);
|
||||
if (norm != null) {
|
||||
Matrix3f normalMat = buildNormalMatrix(mat);
|
||||
norm.rewind();
|
||||
Vector3f n = new Vector3f();
|
||||
while (norm.remaining() >= 3) {
|
||||
int idx = norm.position();
|
||||
n.set(norm.get(), norm.get(), norm.get());
|
||||
normalMat.mult(n, n); n.normalizeLocal();
|
||||
norm.put(idx, n.x); norm.put(idx + 1, n.y); norm.put(idx + 2, n.z);
|
||||
}
|
||||
norm.rewind();
|
||||
newMesh.getBuffer(VertexBuffer.Type.Normal).setUpdateNeeded();
|
||||
}
|
||||
|
||||
newMesh.updateBound();
|
||||
g.setMesh(newMesh);
|
||||
}
|
||||
|
||||
public static Matrix3f buildNormalMatrix(Matrix4f mat) {
|
||||
Matrix3f m3 = new Matrix3f(
|
||||
mat.m00, mat.m01, mat.m02,
|
||||
mat.m10, mat.m11, mat.m12,
|
||||
mat.m20, mat.m21, mat.m22);
|
||||
try { m3.invertLocal(); m3.transposeLocal(); }
|
||||
catch (Exception e) { m3.loadIdentity(); }
|
||||
return m3;
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,9 @@ import org.lwjgl.glfw.GLFW;
|
||||
import org.lwjgl.glfw.GLFWVidMode;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
import com.jme3.system.lwjgl.LwjglWindow;
|
||||
import de.blight.game.state.HotbarState;
|
||||
import de.blight.game.state.HudState;
|
||||
import de.blight.game.state.LoadingScreenState;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.nio.IntBuffer;
|
||||
@@ -50,14 +53,16 @@ public class BlightGame extends SimpleApplication {
|
||||
private AudioScreen audioScreen;
|
||||
private PauseMenu pauseMenu;
|
||||
private MainMenuState mainMenuState;
|
||||
private LoadingScreenState loadingScreen;
|
||||
|
||||
/** Routed durch onClose-Callbacks, damit Config/Grafik-Screen zurück zum richtigen Menü springen. */
|
||||
private Runnable screenCloseTarget;
|
||||
|
||||
// Vollbild-Watchdog: erkennt, wenn das Fenster unerwartet die Auflösung wechselt
|
||||
// (z.B. GNOME Shell de-fullscreent OverrideRedirect-Fenster bei Monitorfokus-Wechsel)
|
||||
private long startTimeMs = 0;
|
||||
private long lastFullscreenFix = 0;
|
||||
private long pendingFullscreenFixAt = Long.MAX_VALUE;
|
||||
private boolean firstLaunch = true; // false nach dem ersten simpleInitApp()
|
||||
private boolean initialFixDone = false; // Initial-Fix bei 0.5s bereits gelaufen
|
||||
|
||||
private JWindow splashWindow;
|
||||
|
||||
@@ -101,6 +106,12 @@ public class BlightGame extends SimpleApplication {
|
||||
settings.setBitsPerPixel(32);
|
||||
settings.setVSync(gs.vsync);
|
||||
settings.setSamples(gs.samples);
|
||||
if (!gs.fullscreen) {
|
||||
// Fenster beim Start auf dem Primär-Monitor zentrieren.
|
||||
// Ohne diesen Hint öffnet GLFW das Fenster bei (0,0) oder wo der WM will –
|
||||
// bei Editor-Start typisch auf dem Sekundär-Monitor.
|
||||
settings.setCenterWindow(true);
|
||||
}
|
||||
// Nur unter X11-Linux: GLFW auf X11-Backend festlegen, damit libdecor keine
|
||||
// Fensterrahmen im Vollbild hinzufügt. Unter nativem Wayland nicht setzen.
|
||||
if ("x11".equalsIgnoreCase(System.getenv("XDG_SESSION_TYPE"))) {
|
||||
@@ -188,14 +199,23 @@ public class BlightGame extends SimpleApplication {
|
||||
audioSettings = AudioSettingsStore.load();
|
||||
|
||||
startTimeMs = System.currentTimeMillis();
|
||||
initialFixDone = false;
|
||||
if (graphicsSettings.fullscreen) {
|
||||
// Tatsächliche Cam-Auflösung als Watchdog-Baseline (main() nutzt native Monitorgröße)
|
||||
graphicsSettings.width = cam.getWidth();
|
||||
graphicsSettings.height = cam.getHeight();
|
||||
log.info("[Grafik] Vollbild-Start: cam={}×{} fullscreen={}",
|
||||
cam.getWidth(), cam.getHeight(), graphicsSettings.fullscreen);
|
||||
fixFullscreenMonitor();
|
||||
log.info("[Grafik] Vollbild-Start: cam={}×{} fullscreen={} gewünschte Aufl.={}×{}",
|
||||
cam.getWidth(), cam.getHeight(), graphicsSettings.fullscreen,
|
||||
graphicsSettings.width, graphicsSettings.height);
|
||||
if (firstLaunch) {
|
||||
pendingFullscreenFixAt = Long.MAX_VALUE;
|
||||
// Fenster NICHT verstecken: der Toggle windowed→fullscreen in fixFullscreenMonitor()
|
||||
// funktioniert nur zuverlässig, wenn das Fenster bereits in einem aktiven
|
||||
// Fullscreen-Zustand war (wie beim 5s-Watchdog). Aus dem Hidden-Zustand behandelt
|
||||
// Cinnamon den nächsten Fullscreen-Übergang als Neuerstplatzierung und ignoriert
|
||||
// den Ziel-Monitor zugunsten des aktiven Workspaces (Editor-Monitor).
|
||||
}
|
||||
// Restart nach Einstellungsänderung: pendingFullscreenFixAt (scheduleFullscreenFix)
|
||||
// bleibt gesetzt und übernimmt den Fix nach 1 s.
|
||||
}
|
||||
firstLaunch = false;
|
||||
stateManager.attach(new AudioSettingsState(audioSettings));
|
||||
|
||||
status("Lade Spielstand...");
|
||||
@@ -207,6 +227,12 @@ public class BlightGame extends SimpleApplication {
|
||||
worldScene.setEnabled(false);
|
||||
stateManager.attach(worldScene);
|
||||
|
||||
// Ladebildschirm – wird von startWorld() aktiviert und löst worldScene.setEnabled(true)
|
||||
// erst nach einem gerenderten Frame aus (kein schwarzer Startbildschirm).
|
||||
loadingScreen = new LoadingScreenState();
|
||||
loadingScreen.setEnabled(false);
|
||||
stateManager.attach(loadingScreen);
|
||||
|
||||
// screenCloseTarget: dynamisch gesetzt, je nachdem ob Config/Grafik vom Hauptmenü
|
||||
// oder vom Pausemenü geöffnet wurde.
|
||||
screenCloseTarget = () -> {};
|
||||
@@ -388,8 +414,16 @@ public class BlightGame extends SimpleApplication {
|
||||
if (mainMenuState != null) {
|
||||
mainMenuState.setEnabled(false);
|
||||
}
|
||||
worldScene.setEnabled(true);
|
||||
inputManager.setCursorVisible(false);
|
||||
loadingScreen.setEnabled(true);
|
||||
// LoadingScreenState löst worldScene.setEnabled(true) nach dem ersten gerenderten
|
||||
// Ladebildschirm-Frame aus, damit der Spieler keinen schwarzen Bildschirm sieht.
|
||||
}
|
||||
|
||||
/** Wird von GraphicsScreen aufgerufen bevor app.restart() – sorgt dafür, dass
|
||||
* fixFullscreenMonitor() ~1 s nach dem Neustart läuft (WM braucht Zeit zum Stabilisieren). */
|
||||
public void scheduleFullscreenFix() {
|
||||
pendingFullscreenFixAt = System.currentTimeMillis() + 1000;
|
||||
}
|
||||
|
||||
// ── Render-Loop ──────────────────────────────────────────────────────────
|
||||
@@ -400,11 +434,26 @@ public class BlightGame extends SimpleApplication {
|
||||
gameReady = true;
|
||||
status("Bereit");
|
||||
}
|
||||
// Vollbild-Watchdog: Wenn GNOME Shell oder der WM das Fenster unerwartet aus dem
|
||||
// Vollbild-Modus holt (erkennbar an falscher Auflösung), sofort wiederherstellen.
|
||||
// 5s Anlaufzeit überspringen (Initialisierungs-Resize-Events), 3s Cooldown.
|
||||
if (graphicsSettings != null && graphicsSettings.fullscreen) {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
// Initial-Fix: 200 ms nach dem ersten Start. Fenster war die ganze Zeit sichtbar
|
||||
// (kein glfwHideWindow) → Cinnamon kennt es als "existing fullscreen window" →
|
||||
// Toggle windowed→fullscreen respektiert Ziel-Monitor (identisch zum Watchdog-Pfad).
|
||||
if (!initialFixDone && graphicsSettings != null && graphicsSettings.fullscreen
|
||||
&& now - startTimeMs > 200) {
|
||||
initialFixDone = true;
|
||||
lastFullscreenFix = now;
|
||||
fixFullscreenMonitor();
|
||||
}
|
||||
|
||||
if (now >= pendingFullscreenFixAt) {
|
||||
pendingFullscreenFixAt = Long.MAX_VALUE;
|
||||
lastFullscreenFix = now;
|
||||
fixFullscreenMonitor();
|
||||
}
|
||||
|
||||
// Watchdog: Vollbild-Verlust durch GNOME/WM (erkennbar an Auflösungs-Mismatch).
|
||||
if (graphicsSettings != null && graphicsSettings.fullscreen) {
|
||||
if (now - startTimeMs > 5_000
|
||||
&& (cam.getWidth() != graphicsSettings.width
|
||||
|| cam.getHeight() != graphicsSettings.height)
|
||||
@@ -417,6 +466,28 @@ public class BlightGame extends SimpleApplication {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reshape(int w, int h) {
|
||||
super.reshape(w, h);
|
||||
if (gameReady) {
|
||||
rebuildScreenDependentStates();
|
||||
}
|
||||
}
|
||||
|
||||
private void rebuildScreenDependentStates() {
|
||||
rebuildState(HudState.class);
|
||||
rebuildState(HotbarState.class);
|
||||
rebuildState(LoadingScreenState.class);
|
||||
}
|
||||
|
||||
private <T extends com.jme3.app.state.BaseAppState> void rebuildState(Class<T> cls) {
|
||||
T state = stateManager.getState(cls);
|
||||
if (state != null && state.isEnabled()) {
|
||||
state.setEnabled(false);
|
||||
state.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Konsolen-Befehle ─────────────────────────────────────────────────────
|
||||
|
||||
private void registerGameCommands(JmeConsole console) {
|
||||
@@ -482,6 +553,14 @@ public class BlightGame extends SimpleApplication {
|
||||
}
|
||||
|
||||
// Vollbild sicherstellen: GLFW ist hier bereits durch JME3 initialisiert.
|
||||
/** Thread.sleep + glfwPollEvents: garantiertes Zeitfenster für WM-Reaktionen.
|
||||
* glfwWaitEventsTimeout() kehrt beim ersten Event zurück, nicht nach voller Zeit. */
|
||||
private static void glfwSleep(long ms) {
|
||||
GLFW.glfwPollEvents();
|
||||
try { Thread.sleep(ms); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
|
||||
GLFW.glfwPollEvents();
|
||||
}
|
||||
|
||||
// Problem: JME3 erstellt das Fenster sofort sichtbar. Cinnamon/Muffin (GLFW 3.5 nutzt
|
||||
// _NET_WM_STATE_FULLSCREEN statt OverrideRedirect) schiebt das Fenster beim ersten
|
||||
// Erscheinen um eine Panel-Höhe nach unten (z.B. Y+32), bevor der WM den State
|
||||
@@ -503,34 +582,20 @@ public class BlightGame extends SimpleApplication {
|
||||
// Ggf. iconifiziertes Fenster wiederherstellen
|
||||
GLFW.glfwRestoreWindow(win);
|
||||
|
||||
// Ist das Fenster bereits in GLFW-Vollbild (OverrideRedirect)?
|
||||
long target = GLFW.glfwGetPrimaryMonitor();
|
||||
long currentMonitor = GLFW.glfwGetWindowMonitor(win);
|
||||
|
||||
// AWT-Primary-Monitor mit GLFW-Monitoren abgleichen
|
||||
Rectangle awt = GraphicsEnvironment
|
||||
.getLocalGraphicsEnvironment()
|
||||
.getDefaultScreenDevice()
|
||||
.getDefaultConfiguration()
|
||||
.getBounds();
|
||||
|
||||
PointerBuffer mons = GLFW.glfwGetMonitors();
|
||||
if (mons == null || mons.limit() == 0) {
|
||||
log.warn("[Grafik] glfwGetMonitors() liefert nichts");
|
||||
GLFW.glfwFocusWindow(win);
|
||||
return;
|
||||
}
|
||||
|
||||
long target = GLFW.glfwGetPrimaryMonitor();
|
||||
if (mons != null) {
|
||||
try (MemoryStack stack = MemoryStack.stackPush()) {
|
||||
IntBuffer mx = stack.mallocInt(1);
|
||||
IntBuffer my = stack.mallocInt(1);
|
||||
for (int i = 0; i < mons.limit(); i++) {
|
||||
GLFW.glfwGetMonitorPos(mons.get(i), mx, my);
|
||||
log.info("[Grafik] GLFW-Monitor {}: pos=({},{}) | AWT-Primary: ({},{}) | aktuellerMonitor={}",
|
||||
i, mx.get(0), my.get(0), awt.x, awt.y,
|
||||
log.info("[Grafik] GLFW-Monitor {}: pos=({},{}), primary={}, current={}",
|
||||
i, mx.get(0), my.get(0),
|
||||
(target == mons.get(i)) ? "JA" : "nein",
|
||||
(currentMonitor == mons.get(i)) ? "JA" : "nein");
|
||||
if (mx.get(0) == awt.x && my.get(0) == awt.y) {
|
||||
target = mons.get(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -541,16 +606,39 @@ public class BlightGame extends SimpleApplication {
|
||||
return;
|
||||
}
|
||||
|
||||
// Windowed → Fullscreen Toggle:
|
||||
// JME3 erstellt das Fenster sofort sichtbar; Cinnamon setzt Y+32 (Panel-Offset)
|
||||
// bevor _NET_WM_STATE_FULLSCREEN vollständig verarbeitet ist.
|
||||
// Kurz in den Fenstermodus wechseln und zurück → WM verarbeitet den
|
||||
// State neu und setzt Y korrekt auf Monitor-Origin.
|
||||
log.info("[Grafik] Vollbild-Fix: Toggle Fenstermodus → Vollbild auf Monitor {} ({}×{}@{}Hz)",
|
||||
target, vm.width(), vm.height(), vm.refreshRate());
|
||||
GLFW.glfwSetWindowMonitor(win, 0L, awt.x, awt.y, vm.width(), vm.height(), 0);
|
||||
GLFW.glfwWaitEventsTimeout(0.1);
|
||||
GLFW.glfwSetWindowMonitor(win, target, 0, 0, vm.width(), vm.height(), vm.refreshRate());
|
||||
int fsW = (graphicsSettings != null && graphicsSettings.width > 0) ? graphicsSettings.width : vm.width();
|
||||
int fsH = (graphicsSettings != null && graphicsSettings.height > 0) ? graphicsSettings.height : vm.height();
|
||||
|
||||
// Primärmonitor-Position für den Windowed-Schritt
|
||||
int primX = 0, primY = 0;
|
||||
try (MemoryStack stack = MemoryStack.stackPush()) {
|
||||
IntBuffer px = stack.mallocInt(1);
|
||||
IntBuffer py = stack.mallocInt(1);
|
||||
GLFW.glfwGetMonitorPos(target, px, py);
|
||||
primX = px.get(0);
|
||||
primY = py.get(0);
|
||||
}
|
||||
|
||||
// Fenster VOR dem Toggle anzeigen und fokussieren.
|
||||
// Cinnamon respektiert beim windowed-Schritt die angeforderte Position nur dann,
|
||||
// wenn das Fenster fokussiert ist. Ohne Fokus verschiebt Cinnamon das Fenster
|
||||
// auf den aktiven Workspace (Editor-Monitor) → der Fullscreen-Schritt danach
|
||||
// landet dann ebenfalls auf dem falschen Monitor.
|
||||
GLFW.glfwShowWindow(win);
|
||||
GLFW.glfwFocusWindow(win);
|
||||
glfwSleep(100); // WM-Fokuswechsel abwarten
|
||||
|
||||
// Toggle Vollbild → Fenster → Vollbild auf Primär-Monitor.
|
||||
log.info("[Grafik] Vollbild-Fix: Toggle → Vollbild {}×{} auf Monitor {} (@{}Hz)",
|
||||
fsW, fsH, target, vm.refreshRate());
|
||||
GLFW.glfwSetWindowMonitor(win, 0L, primX, primY, fsW, fsH, 0);
|
||||
// Cursor auf Zielmonitor warpen: Muffin/Cinnamon nutzt meta_screen_get_current_monitor()
|
||||
// (= Cursor-Position), nicht die Fensterposition, für Fullscreen-Placement.
|
||||
// glfwSetCursorPos ist relativ zum Fenster → Mitte des Fensters = Mitte des Zielmonitors.
|
||||
GLFW.glfwSetCursorPos(win, fsW / 2.0, fsH / 2.0);
|
||||
glfwSleep(80);
|
||||
GLFW.glfwSetWindowMonitor(win, target, 0, 0, fsW, fsH, vm.refreshRate());
|
||||
glfwSleep(200);
|
||||
GLFW.glfwFocusWindow(win);
|
||||
|
||||
} catch (Throwable e) {
|
||||
|
||||
@@ -4,14 +4,23 @@ import com.jme3.font.BitmapText;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.system.AppSettings;
|
||||
import com.jme3.system.lwjgl.LwjglWindow;
|
||||
import de.blight.game.BlightGame;
|
||||
import de.blight.game.state.TerrainChunkState;
|
||||
import de.blight.game.state.WeatherState;
|
||||
import de.blight.game.state.WorldObjectsState;
|
||||
import de.blight.lang.TextResolver;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
import org.lwjgl.glfw.GLFWVidMode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
|
||||
public class GraphicsScreen extends MenuScreen {
|
||||
|
||||
private static final int[][] RESOLUTIONS = {
|
||||
{1280, 720}, {1600, 900}, {1920, 1080}, {2560, 1440}, {3840, 2160}
|
||||
private static final int[][] FALLBACK_RESOLUTIONS = {
|
||||
{1024, 768}, {1280, 720}, {1600, 900}, {1920, 1080}, {2560, 1440}, {3840, 2160}
|
||||
};
|
||||
private static final int[] SAMPLES = {0, 2, 4, 8};
|
||||
|
||||
@@ -20,12 +29,14 @@ public class GraphicsScreen extends MenuScreen {
|
||||
private static final int ROW_VSYNC = 2;
|
||||
private static final int ROW_AA = 3;
|
||||
private static final int ROW_VIEWDIST = 4;
|
||||
private static final int ROW_COUNT = 5;
|
||||
private static final int ROW_SHADOW = 5;
|
||||
private static final int ROW_COUNT = 6;
|
||||
|
||||
private final GraphicsSettings live;
|
||||
private GraphicsSettings edit;
|
||||
private final Runnable onClose;
|
||||
|
||||
private int[][] resolutions = FALLBACK_RESOLUTIONS;
|
||||
private int resIdx;
|
||||
private int samplesIdx;
|
||||
|
||||
@@ -43,16 +54,19 @@ public class GraphicsScreen extends MenuScreen {
|
||||
|
||||
@Override
|
||||
protected void onEnableExtras() {
|
||||
resolutions = buildResolutionList();
|
||||
|
||||
edit = new GraphicsSettings();
|
||||
edit.width = live.width; edit.height = live.height;
|
||||
edit.fullscreen = live.fullscreen;
|
||||
edit.vsync = live.vsync;
|
||||
edit.samples = live.samples;
|
||||
edit.viewDistance = live.viewDistance;
|
||||
edit.shadowQuality = live.shadowQuality;
|
||||
|
||||
resIdx = 0;
|
||||
for (int i = 0; i < RESOLUTIONS.length; i++) {
|
||||
if (RESOLUTIONS[i][0] == edit.width && RESOLUTIONS[i][1] == edit.height) {
|
||||
for (int i = 0; i < resolutions.length; i++) {
|
||||
if (resolutions[i][0] == edit.width && resolutions[i][1] == edit.height) {
|
||||
resIdx = i; break;
|
||||
}
|
||||
}
|
||||
@@ -71,7 +85,8 @@ public class GraphicsScreen extends MenuScreen {
|
||||
"menu.graphics.row.fullscreen",
|
||||
"menu.graphics.row.vsync",
|
||||
"menu.graphics.row.aa",
|
||||
"menu.graphics.row.viewdist"
|
||||
"menu.graphics.row.viewdist",
|
||||
"menu.graphics.row.shadow"
|
||||
};
|
||||
|
||||
float lblX = CONT_X + 30f;
|
||||
@@ -115,7 +130,7 @@ public class GraphicsScreen extends MenuScreen {
|
||||
|
||||
private void refreshText(int 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_VSYNC -> edit.vsync ? t("menu.graphics.val.on") : t("menu.graphics.val.off");
|
||||
case ROW_AA -> SAMPLES[samplesIdx] == 0
|
||||
@@ -125,6 +140,11 @@ public class GraphicsScreen extends MenuScreen {
|
||||
case NORMAL -> t("menu.graphics.val.viewdist.normal");
|
||||
case FAR -> t("menu.graphics.val.viewdist.far");
|
||||
};
|
||||
case ROW_SHADOW -> switch (edit.shadowQuality) {
|
||||
case LOW -> t("menu.graphics.val.shadow.low");
|
||||
case MEDIUM -> t("menu.graphics.val.shadow.medium");
|
||||
case HIGH -> t("menu.graphics.val.shadow.high");
|
||||
};
|
||||
default -> "";
|
||||
};
|
||||
BitmapText vt = valTexts[row];
|
||||
@@ -140,9 +160,9 @@ public class GraphicsScreen extends MenuScreen {
|
||||
private void cycleRow(int row, int dir) {
|
||||
switch (row) {
|
||||
case ROW_RES:
|
||||
resIdx = (resIdx + dir + RESOLUTIONS.length) % RESOLUTIONS.length;
|
||||
edit.width = RESOLUTIONS[resIdx][0];
|
||||
edit.height = RESOLUTIONS[resIdx][1];
|
||||
resIdx = (resIdx + dir + resolutions.length) % resolutions.length;
|
||||
edit.width = resolutions[resIdx][0];
|
||||
edit.height = resolutions[resIdx][1];
|
||||
break;
|
||||
case ROW_FULL: edit.fullscreen = !edit.fullscreen; break;
|
||||
case ROW_VSYNC: edit.vsync = !edit.vsync; break;
|
||||
@@ -154,16 +174,22 @@ public class GraphicsScreen extends MenuScreen {
|
||||
GraphicsSettings.ViewDistance[] vds = GraphicsSettings.ViewDistance.values();
|
||||
edit.viewDistance = vds[(edit.viewDistance.ordinal() + dir + vds.length) % vds.length];
|
||||
break;
|
||||
case ROW_SHADOW:
|
||||
GraphicsSettings.ShadowQuality[] sqs = GraphicsSettings.ShadowQuality.values();
|
||||
edit.shadowQuality = sqs[(edit.shadowQuality.ordinal() + dir + sqs.length) % sqs.length];
|
||||
break;
|
||||
}
|
||||
refreshText(row);
|
||||
}
|
||||
|
||||
private void applyAndSave() {
|
||||
boolean shadowChanged = live.shadowQuality != edit.shadowQuality;
|
||||
boolean needsRestart = live.width != edit.width
|
||||
|| live.height != edit.height
|
||||
|| live.fullscreen != edit.fullscreen
|
||||
|| live.vsync != edit.vsync
|
||||
|| live.samples != edit.samples;
|
||||
|| live.samples != edit.samples
|
||||
|| shadowChanged;
|
||||
|
||||
live.width = edit.width;
|
||||
live.height = edit.height;
|
||||
@@ -171,6 +197,7 @@ public class GraphicsScreen extends MenuScreen {
|
||||
live.vsync = edit.vsync;
|
||||
live.samples = edit.samples;
|
||||
live.viewDistance = edit.viewDistance;
|
||||
live.shadowQuality = edit.shadowQuality;
|
||||
GraphicsStore.save(live);
|
||||
|
||||
WeatherState ws = getApplication().getStateManager().getState(WeatherState.class);
|
||||
@@ -179,6 +206,9 @@ public class GraphicsScreen extends MenuScreen {
|
||||
TerrainChunkState tcs = getApplication().getStateManager().getState(TerrainChunkState.class);
|
||||
if (tcs != null) tcs.setLodRanges(live.viewDistance.lod0Range, live.viewDistance.lod1Range);
|
||||
|
||||
WorldObjectsState wos = getApplication().getStateManager().getState(WorldObjectsState.class);
|
||||
if (wos != null) wos.setLodFactor((float) live.viewDistance.lod0Range);
|
||||
|
||||
close();
|
||||
|
||||
if (needsRestart) {
|
||||
@@ -189,6 +219,9 @@ public class GraphicsScreen extends MenuScreen {
|
||||
s.setVSync(live.vsync);
|
||||
s.setSamples(live.samples);
|
||||
app.setSettings(s);
|
||||
if (live.fullscreen) {
|
||||
((BlightGame) getApplication()).scheduleFullscreenFix();
|
||||
}
|
||||
app.restart();
|
||||
}
|
||||
}
|
||||
@@ -198,5 +231,41 @@ public class GraphicsScreen extends MenuScreen {
|
||||
if (onClose != null) onClose.run();
|
||||
}
|
||||
|
||||
private int[][] buildResolutionList() {
|
||||
try {
|
||||
com.jme3.system.JmeContext ctx = getApplication().getContext();
|
||||
if (!(ctx instanceof LwjglWindow)) {
|
||||
return FALLBACK_RESOLUTIONS;
|
||||
}
|
||||
long monitor = GLFW.glfwGetPrimaryMonitor();
|
||||
if (monitor == 0L) {
|
||||
return FALLBACK_RESOLUTIONS;
|
||||
}
|
||||
GLFWVidMode.Buffer modes = GLFW.glfwGetVideoModes(monitor);
|
||||
if (modes == null || modes.limit() == 0) {
|
||||
return FALLBACK_RESOLUTIONS;
|
||||
}
|
||||
// Eindeutige (w, h) Paare sammeln — Duplikate (gleiche Auflösung, anderer Refresh) entfernen
|
||||
LinkedHashSet<Long> seen = new LinkedHashSet<>();
|
||||
List<int[]> list = new ArrayList<>();
|
||||
for (int i = 0; i < modes.limit(); i++) {
|
||||
modes.position(i);
|
||||
int w = modes.width();
|
||||
int h = modes.height();
|
||||
long key = ((long) w << 32) | (h & 0xFFFFFFFFL);
|
||||
if (w >= 1024 && h >= 600 && seen.add(key)) {
|
||||
list.add(new int[]{w, h});
|
||||
}
|
||||
}
|
||||
if (list.isEmpty()) {
|
||||
return FALLBACK_RESOLUTIONS;
|
||||
}
|
||||
list.sort((a, b) -> a[0] != b[0] ? Integer.compare(a[0], b[0]) : Integer.compare(a[1], b[1]));
|
||||
return list.toArray(new int[0][]);
|
||||
} catch (Throwable e) {
|
||||
return FALLBACK_RESOLUTIONS;
|
||||
}
|
||||
}
|
||||
|
||||
private static String t(String id) { return TextResolver.get().resolveId(id); }
|
||||
}
|
||||
|
||||
@@ -7,11 +7,41 @@ public class GraphicsSettings {
|
||||
public boolean vsync = false;
|
||||
public int samples = 4;
|
||||
public ViewDistance viewDistance = ViewDistance.NORMAL;
|
||||
public ShadowQuality shadowQuality = ShadowQuality.MEDIUM;
|
||||
|
||||
/**
|
||||
* Schattenqualitäts-Stufen.
|
||||
* mapSize/splits sind Konstruktor-Parameter des Shadow-Filters → Neustart nötig.
|
||||
* lambda/zExtend/zFade/backface können zur Laufzeit angepasst werden.
|
||||
*/
|
||||
public enum ShadowQuality {
|
||||
// mapSize splits lambda zExtend zFade backface
|
||||
LOW ( 1024, 2, 0.70f, 40f, 5f, false),
|
||||
MEDIUM ( 2048, 3, 0.75f, 60f, 8f, true ),
|
||||
HIGH ( 4096, 4, 0.80f, 80f, 10f, true );
|
||||
|
||||
public final int shadowMapSize;
|
||||
public final int splits;
|
||||
public final float lambda;
|
||||
public final float zExtend;
|
||||
public final float zFade;
|
||||
public final boolean backface;
|
||||
|
||||
ShadowQuality(int shadowMapSize, int splits, float lambda,
|
||||
float zExtend, float zFade, boolean backface) {
|
||||
this.shadowMapSize = shadowMapSize;
|
||||
this.splits = splits;
|
||||
this.lambda = lambda;
|
||||
this.zExtend = zExtend;
|
||||
this.zFade = zFade;
|
||||
this.backface = backface;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ViewDistance {
|
||||
SHORT (0.55f, 0, 2),
|
||||
NORMAL(1.00f, 1, 3),
|
||||
FAR (1.60f, 2, 5);
|
||||
SHORT (1.00f, 1, 3),
|
||||
NORMAL(1.60f, 2, 5),
|
||||
FAR (2.50f, 3, 7);
|
||||
|
||||
public final float fogFactor;
|
||||
public final int lod0Range;
|
||||
|
||||
@@ -219,8 +219,11 @@ public class WorldScene extends BaseAppState {
|
||||
animLib = new AnimationLibrary();
|
||||
app.getStateManager().attach(animLib);
|
||||
|
||||
// Früh starten damit DayNightState bis onEnable() bereits initialisiert ist
|
||||
// Früh starten damit DayNightState bis onEnable() bereits initialisiert ist.
|
||||
// withShadows=false: Shadow-Filter wird in buildLighting() extern übergeben.
|
||||
// setGameMode(true): Spiel-Beleuchtungswerte statt Editor-Werte verwenden.
|
||||
dayNight = new DayNightState(false);
|
||||
dayNight.setGameMode(true);
|
||||
app.getStateManager().attach(dayNight);
|
||||
}
|
||||
|
||||
@@ -251,7 +254,7 @@ public class WorldScene extends BaseAppState {
|
||||
BlightGame.status("Lade Welt-Objekte...");
|
||||
app.getStateManager().attach(new RiverState());
|
||||
WorldObjectsState worldObjects = new WorldObjectsState();
|
||||
worldObjects.setLodFactor(graphicsSettings.viewDistance.fogFactor);
|
||||
worldObjects.setLodFactor((float) graphicsSettings.viewDistance.lod0Range);
|
||||
app.getStateManager().attach(worldObjects);
|
||||
app.getStateManager().attach(new WorldLightState(sharedFPP));
|
||||
app.getStateManager().attach(new StoneWorldState());
|
||||
@@ -368,9 +371,19 @@ public class WorldScene extends BaseAppState {
|
||||
new JobProgressAdapter<LightProbe>() {
|
||||
@Override public void done(LightProbe result) {
|
||||
dayNight.getDayTime().setTimeOfDay(restoreTime);
|
||||
// 2 Frames warten: JME3 braucht 1-2 Frames um die neue
|
||||
// Probe auf die GPU zu übertragen. isGameReady() prüft
|
||||
// envCam == null, daher erst dann setzen wenn die Probe
|
||||
// für den Renderer sichtbar ist.
|
||||
app.enqueue(() -> {
|
||||
app.enqueue(() -> {
|
||||
app.getStateManager().detach(envCam);
|
||||
envCam = null;
|
||||
log.info("[WorldScene] LightProbe fertig, Uhrzeit zurück auf {}.", restoreTime);
|
||||
return null;
|
||||
});
|
||||
return null;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -532,6 +545,15 @@ public class WorldScene extends BaseAppState {
|
||||
* Setzt Ambient=(1,1,1,1) auf allen Lighting.j3md-Materialien eines Modells,
|
||||
* damit es den globalen AmbientLight korrekt empfängt (Blender-Export setzt Ambient oft auf schwarz).
|
||||
*/
|
||||
private static void applyShadowModeRecursive(Spatial s, RenderQueue.ShadowMode mode) {
|
||||
s.setShadowMode(mode);
|
||||
if (s instanceof Node n) {
|
||||
for (Spatial child : n.getChildren()) {
|
||||
applyShadowModeRecursive(child, mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void applyFullAmbient(Spatial s) {
|
||||
if (s instanceof Geometry g) {
|
||||
com.jme3.material.Material m = g.getMaterial();
|
||||
@@ -572,7 +594,7 @@ public class WorldScene extends BaseAppState {
|
||||
try {
|
||||
Spatial loaded = assetManager.loadModel(mc.getModelPath());
|
||||
stripEmbeddedClips(loaded, mc.getModelPath());
|
||||
loaded.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
|
||||
applyShadowModeRecursive(loaded, RenderQueue.ShadowMode.CastAndReceive);
|
||||
applyFullAmbient(loaded);
|
||||
collectPbrMaterials(loaded, characterPbrMaterials);
|
||||
log.info("[WorldScene] {} PBR-Materialien im Charakter-Modell gefunden.", characterPbrMaterials.size());
|
||||
@@ -682,8 +704,15 @@ public class WorldScene extends BaseAppState {
|
||||
// Licht, Ambient und Sky kommen von DayNightState (der wurde in initialize() attached).
|
||||
// Wir erstellen hier nur den Shadow-Filter und übergeben ihn an DayNightState,
|
||||
// damit dieser Intensität und Lichtbindung dynamisch verwaltet.
|
||||
GraphicsSettings.ShadowQuality sq = graphicsSettings.shadowQuality;
|
||||
DirectionalLightShadowFilter shadowFilter =
|
||||
new DirectionalLightShadowFilter(assetManager, 2048, 3);
|
||||
new DirectionalLightShadowFilter(assetManager, sq.shadowMapSize, sq.splits);
|
||||
shadowFilter.setEdgeFilteringMode(
|
||||
sq == GraphicsSettings.ShadowQuality.LOW ? EdgeFilteringMode.PCF4 : EdgeFilteringMode.PCFPOISSON);
|
||||
shadowFilter.setLambda(sq.lambda);
|
||||
shadowFilter.setShadowZExtend(sq.zExtend);
|
||||
shadowFilter.setShadowZFadeLength(sq.zFade);
|
||||
shadowFilter.setRenderBackFacesShadows(sq.backface);
|
||||
shadowFilter.setShadowIntensity(0.4f);
|
||||
shadowFilter.setEnabled(true);
|
||||
dayNight.setShadowFilter(shadowFilter); // bindet sun + übernimmt Intensitäts-Updates
|
||||
@@ -1372,6 +1401,10 @@ public class WorldScene extends BaseAppState {
|
||||
* Teleportiert den Spieler zu (x, y, z). Wenn y == Float.NaN wird die
|
||||
* Terrainhöhe an (x, z) verwendet und ein kleiner Offset nach oben addiert.
|
||||
*/
|
||||
public boolean isGameReady() {
|
||||
return !physicsCharPending && animContextReady && envCam == null;
|
||||
}
|
||||
|
||||
public String teleportPlayer(float x, float y, float z) {
|
||||
if (physicsChar == null) return "Spieler noch nicht initialisiert";
|
||||
if (Float.isNaN(y)) {
|
||||
|
||||
@@ -45,7 +45,7 @@ public class DayNightState extends BaseAppState implements TimeListener {
|
||||
|
||||
// ── Sonnenrichtungs-Drosselung ────────────────────────────────────────────
|
||||
|
||||
private static final float SUN_DIR_INTERVAL = 0.25f;
|
||||
private static final float SUN_DIR_INTERVAL = 2.0f;
|
||||
private float sunDirTimer = SUN_DIR_INTERVAL; // sofort bereit beim ersten Aufruf
|
||||
|
||||
// ── Höhlen-Abdunkelung ────────────────────────────────────────────────────
|
||||
@@ -57,6 +57,8 @@ public class DayNightState extends BaseAppState implements TimeListener {
|
||||
private ColorRGBA sunBaseColor = new ColorRGBA(1, 1, 1, 1);
|
||||
private float shadowBaseIntensity = 0f;
|
||||
private ColorRGBA customAmbient = new ColorRGBA();
|
||||
/** true = Spiel-Beleuchtungswerte, false = Editor-Werte. Standardmäßig = withShadows. */
|
||||
private boolean gameMode;
|
||||
|
||||
private static final float CAVE_RAY_HEIGHT = 12f;
|
||||
private static final float CAVE_CHECK_INTERVAL = 0.2f;
|
||||
@@ -69,7 +71,7 @@ public class DayNightState extends BaseAppState implements TimeListener {
|
||||
this(new DayTime(), true);
|
||||
}
|
||||
|
||||
/** @param withShadows false für Editor (kein Shadow-Renderer) */
|
||||
/** @param withShadows false für Editor (kein Shadow-Renderer, Editor-Beleuchtungswerte) */
|
||||
public DayNightState(boolean withShadows) {
|
||||
this(new DayTime(), withShadows);
|
||||
}
|
||||
@@ -77,6 +79,15 @@ public class DayNightState extends BaseAppState implements TimeListener {
|
||||
public DayNightState(DayTime dayTime, boolean withShadows) {
|
||||
this.dayTime = dayTime;
|
||||
this.withShadows = withShadows;
|
||||
this.gameMode = withShadows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schaltet auf Spiel-Beleuchtungswerte um, unabhängig vom withShadows-Flag.
|
||||
* Nützlich wenn der Shadow-Filter von außen gesetzt wird (z.B. WorldScene).
|
||||
*/
|
||||
public void setGameMode(boolean gameMode) {
|
||||
this.gameMode = gameMode;
|
||||
}
|
||||
|
||||
public DayTime getDayTime() { return dayTime; }
|
||||
@@ -234,24 +245,28 @@ public class DayNightState extends BaseAppState implements TimeListener {
|
||||
float elevC = FastMath.clamp(elevation, 0f, 1f);
|
||||
|
||||
// Spiel vs. Editor: unterschiedliche Helligkeitswerte aus LightingConfig
|
||||
ColorRGBA ambDay = withShadows ? LightingConfig.AMB_DAY : LightingConfig.EDITOR_AMB_DAY;
|
||||
ColorRGBA ambNight = withShadows ? LightingConfig.AMB_NIGHT : LightingConfig.EDITOR_AMB_NIGHT;
|
||||
ColorRGBA custDay = withShadows ? LightingConfig.CUSTOM_AMB_DAY : LightingConfig.EDITOR_CUSTOM_AMB_DAY;
|
||||
ColorRGBA custNight = withShadows ? LightingConfig.CUSTOM_AMB_NIGHT : LightingConfig.EDITOR_CUSTOM_AMB_NIGHT;
|
||||
float sunIntensity = withShadows ? LightingConfig.SUN_INTENSITY : LightingConfig.EDITOR_SUN_INTENSITY;
|
||||
ColorRGBA ambDay = gameMode ? LightingConfig.AMB_DAY : LightingConfig.EDITOR_AMB_DAY;
|
||||
ColorRGBA ambNight = gameMode ? LightingConfig.AMB_NIGHT : LightingConfig.EDITOR_AMB_NIGHT;
|
||||
ColorRGBA custDay = gameMode ? LightingConfig.CUSTOM_AMB_DAY : LightingConfig.EDITOR_CUSTOM_AMB_DAY;
|
||||
ColorRGBA custNight = gameMode ? LightingConfig.CUSTOM_AMB_NIGHT : LightingConfig.EDITOR_CUSTOM_AMB_NIGHT;
|
||||
float sunIntensity = gameMode ? LightingConfig.SUN_INTENSITY : LightingConfig.EDITOR_SUN_INTENSITY;
|
||||
|
||||
// ── Sonnenfarbe: Dämmerung (orange) → Tag (warm-weiß) — jedes Frame ──
|
||||
float dawnFactor = 1f - FastMath.clamp(elevation * LightingConfig.SUN_DAWN_SLOPE, 0f, 1f);
|
||||
ColorRGBA sunColor = LightingConfig.SUN_COLOR_DAWN.clone()
|
||||
ColorRGBA sunTint = LightingConfig.SUN_COLOR_DAWN.clone()
|
||||
.interpolateLocal(LightingConfig.SUN_COLOR_DAY, 1f - dawnFactor);
|
||||
sunBaseColor = sunColor.mult(elevC * sunIntensity);
|
||||
sunBaseColor = sunTint.mult(elevC * sunIntensity);
|
||||
sun.setColor(sunBaseColor.mult(1f - caveFactor));
|
||||
|
||||
// ── Ambient: Nacht → Tag — jedes Frame ─────────────────────────────
|
||||
// ── Ambient: Nacht (blau) → Tag (Sonnenfarbe-tinted) — jedes Frame ──
|
||||
// Tages-Ambient bekommt den Farbton der Sonne: orange bei Dämmerung,
|
||||
// warm-gelb tagsüber. Nachts bleibt das fixe Blau von AMB_NIGHT.
|
||||
ColorRGBA ambDayTinted = ambDay.mult(sunTint);
|
||||
ColorRGBA custDayTinted = custDay.mult(sunTint);
|
||||
float ambFactor = FastMath.clamp(
|
||||
(elevation - LightingConfig.AMB_ELEV_LOW) / LightingConfig.AMB_ELEV_RANGE, 0f, 1f);
|
||||
ambient.setColor(ambNight.clone().interpolateLocal(ambDay, ambFactor));
|
||||
customAmbient.set(custNight.clone().interpolateLocal(custDay, ambFactor));
|
||||
ambient.setColor(ambNight.clone().interpolateLocal(ambDayTinted, ambFactor));
|
||||
customAmbient.set(custNight.clone().interpolateLocal(custDayTinted, ambFactor));
|
||||
|
||||
shadowBaseIntensity = FastMath.clamp(
|
||||
elevation * LightingConfig.SHADOW_SLOPE, 0f, LightingConfig.SHADOW_MAX);
|
||||
|
||||
@@ -72,26 +72,26 @@ public class HudState extends BaseAppState {
|
||||
protected void initialize(Application application) {
|
||||
app = (SimpleApplication) application;
|
||||
ensureAssets();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application application) {}
|
||||
|
||||
@Override
|
||||
protected void onEnable() {
|
||||
hudNode = new Node("hud_bars");
|
||||
fills[0] = fills[1] = fills[2] = null;
|
||||
lastRatios[0] = lastRatios[1] = lastRatios[2] = -1f;
|
||||
buildBars();
|
||||
app.getGuiNode().attachChild(hudNode);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application application) {
|
||||
if (hudNode.getParent() != null) {
|
||||
app.getGuiNode().detachChild(hudNode);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnable() {
|
||||
hudNode.setCullHint(Spatial.CullHint.Inherit);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisable() {
|
||||
hudNode.setCullHint(Spatial.CullHint.Always);
|
||||
if (hudNode != null) {
|
||||
app.getGuiNode().detachChild(hudNode);
|
||||
hudNode = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package de.blight.game.state;
|
||||
|
||||
import com.jme3.math.FastMath;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.renderer.RenderManager;
|
||||
import com.jme3.renderer.ViewPort;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.Spatial;
|
||||
import com.jme3.scene.control.AbstractControl;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Wählt pro Frame das passende Quad-Paar eines 8-Richtungs-Impostors.
|
||||
* Pass d wurde mit Kamera bei sin(d·2π/8), cos(d·2π/8) aufgenommen.
|
||||
* Es werden immer zwei gegenüberliegende Quads (eine Ebene) angezeigt;
|
||||
* die anderen 6 werden per CullHint.Always ausgeblendet.
|
||||
*/
|
||||
public class ImpostorViewControl extends AbstractControl {
|
||||
|
||||
private static final int DIRS = 8;
|
||||
|
||||
private Camera cam;
|
||||
|
||||
public ImpostorViewControl() {}
|
||||
|
||||
public ImpostorViewControl(Camera cam) { this.cam = cam; }
|
||||
|
||||
public void setCamera(Camera cam) { this.cam = cam; }
|
||||
|
||||
@Override
|
||||
protected void controlUpdate(float tpf) {
|
||||
if (cam == null || !(spatial instanceof Node n)) return;
|
||||
List<Spatial> quads = n.getChildren();
|
||||
int dirs = quads.size();
|
||||
if (dirs < 2) return;
|
||||
|
||||
Vector3f toCamera = cam.getLocation().subtract(spatial.getWorldTranslation());
|
||||
float dx = toCamera.x, dz = toCamera.z;
|
||||
if (Math.abs(dx) < 0.001f && Math.abs(dz) < 0.001f) return;
|
||||
|
||||
float angle = (float) Math.atan2(dx, dz);
|
||||
float t = angle * dirs / FastMath.TWO_PI;
|
||||
int best = ((int) Math.round(t) % dirs + dirs) % dirs;
|
||||
|
||||
for (int i = 0; i < dirs; i++) {
|
||||
quads.get(i).setCullHint(i == best
|
||||
? Spatial.CullHint.Inherit
|
||||
: Spatial.CullHint.Always);
|
||||
}
|
||||
}
|
||||
|
||||
@Override protected void controlRender(RenderManager rm, ViewPort vp) {}
|
||||
}
|
||||
@@ -77,16 +77,17 @@ public final class LightingConfig {
|
||||
/**
|
||||
* AmbientLight-Farbe am Mittag.
|
||||
* Skaliert den LightProbe-IBL: envLight = probeIBL × AMB_DAY.
|
||||
* Hoch halten (≥ 0.7) damit PBR-Reflexionen sichtbar bleiben.
|
||||
* Nicht zu hoch – LightProbe bäckt bereits Mittags-Beleuchtung ein;
|
||||
* bei AMB_DAY=0.90 wäre probeIBL + direkte Sonne (0.68) zu hell für weiße Flächen.
|
||||
* Hat keinen Effekt solange kein LightProbe in der Szene ist.
|
||||
*/
|
||||
public static final ColorRGBA AMB_DAY = new ColorRGBA(0.90f, 0.90f, 0.90f, 1f);
|
||||
public static final ColorRGBA AMB_DAY = new ColorRGBA(0.28f, 0.28f, 0.28f, 1f);
|
||||
|
||||
/**
|
||||
* AmbientLight-Farbe in der Nacht (LightProbe-Multiplikator).
|
||||
* Leichter Blaustich simuliert Mondlicht-Reflexionen auf PBR-Flächen.
|
||||
*/
|
||||
public static final ColorRGBA AMB_NIGHT = new ColorRGBA(0.10f, 0.10f, 0.25f, 1f);
|
||||
public static final ColorRGBA AMB_NIGHT = new ColorRGBA(0.05f, 0.05f, 0.125f, 1f);
|
||||
|
||||
|
||||
// ╔══════════════════════════════════════════════════════════════════════════╗
|
||||
@@ -103,7 +104,7 @@ public final class LightingConfig {
|
||||
* ↓ senken: mehr Kontrast, tiefere Schatten (wirkt moodiger/dramatischer).
|
||||
* Empfehlung: 0.03–0.10 pro Kanal.
|
||||
*/
|
||||
public static final ColorRGBA CUSTOM_AMB_DAY = new ColorRGBA(0.04f, 0.05f, 0.08f, 1f);
|
||||
public static final ColorRGBA CUSTOM_AMB_DAY = new ColorRGBA(0.03f, 0.04f, 0.06f, 1f);
|
||||
|
||||
/**
|
||||
* Ambient-Farbe in der Nacht.
|
||||
@@ -111,7 +112,7 @@ public final class LightingConfig {
|
||||
* Zu hoch → Nacht wirkt grau statt dunkel.
|
||||
* Empfehlung: 0.01–0.04 pro Kanal, Blau-Kanal darf höher sein.
|
||||
*/
|
||||
public static final ColorRGBA CUSTOM_AMB_NIGHT = new ColorRGBA(0.025f, 0.025f, 0.06f, 1f);
|
||||
public static final ColorRGBA CUSTOM_AMB_NIGHT = new ColorRGBA(0.012f, 0.012f, 0.03f, 1f);
|
||||
|
||||
|
||||
// ╔══════════════════════════════════════════════════════════════════════════╗
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
package de.blight.game.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.Geometry;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.scene.shape.Quad;
|
||||
import com.jme3.texture.Texture2D;
|
||||
import com.jme3.ui.Picture;
|
||||
import de.blight.game.scene.WorldScene;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Zeigt Logo + Ladebalken, bis WorldScene spielbereit ist.
|
||||
*
|
||||
* Timing-Garantie: WorldScene.setEnabled(true) wird erst aufgerufen, nachdem
|
||||
* mindestens ein vollständiger Render-Frame mit dem Overlay stattgefunden hat.
|
||||
* Dazu wird app.enqueue() genutzt: Der eingereihte Task läuft zu Beginn des
|
||||
* NÄCHSTEN Application.update()-Aufrufs – also nach dem aktuellen Render-Frame.
|
||||
*/
|
||||
public class LoadingScreenState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LoadingScreenState.class);
|
||||
|
||||
private static final float PROGRESS_SPEED = 0.18f;
|
||||
private static final float TIMEOUT_SEC = 60f;
|
||||
|
||||
private SimpleApplication app;
|
||||
private Node overlayNode;
|
||||
private Geometry barFill;
|
||||
|
||||
private boolean readyToTrigger = false;
|
||||
private boolean worldTriggered = false;
|
||||
private float progress = 0f;
|
||||
private float timeWaiting = 0f;
|
||||
|
||||
@Override
|
||||
protected void initialize(Application application) {
|
||||
app = (SimpleApplication) application;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application application) {}
|
||||
|
||||
@Override
|
||||
protected void onEnable() {
|
||||
if (!worldTriggered) {
|
||||
readyToTrigger = false;
|
||||
progress = 0f;
|
||||
timeWaiting = 0f;
|
||||
// Trigger erst im nächsten Frame einplanen: so wird der Overlay
|
||||
// garantiert einmal gerendert, bevor WorldScene.onEnable() blockiert.
|
||||
app.enqueue((java.util.concurrent.Callable<Void>) () -> {
|
||||
if (!worldTriggered) {
|
||||
readyToTrigger = true;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
buildOverlay();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisable() {
|
||||
if (overlayNode != null) {
|
||||
app.getGuiNode().detachChild(overlayNode);
|
||||
overlayNode = null;
|
||||
barFill = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
if (!worldTriggered && readyToTrigger) {
|
||||
readyToTrigger = false;
|
||||
worldTriggered = true;
|
||||
WorldScene ws = app.getStateManager().getState(WorldScene.class);
|
||||
if (ws != null) {
|
||||
ws.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
timeWaiting += tpf;
|
||||
WorldScene ws = app.getStateManager().getState(WorldScene.class);
|
||||
boolean done = (ws != null && ws.isGameReady()) || timeWaiting >= TIMEOUT_SEC;
|
||||
|
||||
if (!done) {
|
||||
progress = Math.min(progress + tpf * PROGRESS_SPEED, 0.9f);
|
||||
} else {
|
||||
progress = 1.0f;
|
||||
}
|
||||
|
||||
if (barFill != null) {
|
||||
barFill.setLocalScale(Math.max(0.001f, progress), 1f, 1f);
|
||||
}
|
||||
|
||||
if (done) {
|
||||
setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Aufbau ───────────────────────────────────────────────────────────────────
|
||||
|
||||
private void buildOverlay() {
|
||||
if (overlayNode != null) {
|
||||
app.getGuiNode().detachChild(overlayNode);
|
||||
}
|
||||
float sw = app.getCamera().getWidth();
|
||||
float sh = app.getCamera().getHeight();
|
||||
|
||||
overlayNode = new Node("loading_overlay");
|
||||
|
||||
// Schwarzer Vollbild-Hintergrund (Z=200: liegt garantiert über allen HUD-Elementen)
|
||||
overlayNode.attachChild(makeRect(sw, sh, ColorRGBA.Black, 200f));
|
||||
|
||||
// Blight-Logo
|
||||
buildLogo(sw, sh);
|
||||
|
||||
// Ladebalken
|
||||
buildBar(sw, sh);
|
||||
|
||||
app.getGuiNode().attachChild(overlayNode);
|
||||
}
|
||||
|
||||
private void buildLogo(float sw, float sh) {
|
||||
try {
|
||||
Texture2D tex = (Texture2D) app.getAssetManager().loadTexture("logo.png");
|
||||
int imgW = tex.getImage().getWidth();
|
||||
int imgH = tex.getImage().getHeight();
|
||||
float logoW = sw * 0.40f;
|
||||
float logoH = (imgH > 0 && imgW > 0) ? logoW * imgH / imgW : logoW * 0.27f;
|
||||
Picture logo = new Picture("loading_logo");
|
||||
logo.setTexture(app.getAssetManager(), tex, true);
|
||||
logo.setWidth(logoW);
|
||||
logo.setHeight(logoH);
|
||||
logo.setLocalTranslation((sw - logoW) / 2f, sh * 0.42f, 202f);
|
||||
logo.setQueueBucket(RenderQueue.Bucket.Gui);
|
||||
overlayNode.attachChild(logo);
|
||||
} catch (Exception e) {
|
||||
log.warn("[LoadingScreen] logo.png nicht ladbar: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void buildBar(float sw, float sh) {
|
||||
float bw = sw * 0.50f;
|
||||
float bh = 16f;
|
||||
float bx = (sw - bw) / 2f;
|
||||
float by = sh * 0.14f;
|
||||
float border = 2f;
|
||||
|
||||
Geometry barBorder = makeRect(bw, bh, new ColorRGBA(0.45f, 0.35f, 0.08f, 1f), 1f);
|
||||
barBorder.setLocalTranslation(bx, by, 201f);
|
||||
overlayNode.attachChild(barBorder);
|
||||
|
||||
Geometry barBg = makeRect(bw - border * 2, bh - border * 2,
|
||||
new ColorRGBA(0.08f, 0.08f, 0.08f, 1f), 1f);
|
||||
barBg.setLocalTranslation(bx + border, by + border, 201f);
|
||||
overlayNode.attachChild(barBg);
|
||||
|
||||
// Füllbalken: X-Scale animiert die Breite von links nach rechts
|
||||
barFill = makeRect(bw - border * 2, bh - border * 2,
|
||||
new ColorRGBA(0.85f, 0.65f, 0.10f, 1f), 2f);
|
||||
barFill.setLocalTranslation(bx + border, by + border, 202f);
|
||||
barFill.setLocalScale(Math.max(0.001f, progress), 1f, 1f);
|
||||
overlayNode.attachChild(barFill);
|
||||
}
|
||||
|
||||
private Geometry makeRect(float w, float h, ColorRGBA color, float z) {
|
||||
Quad quad = new Quad(w, h);
|
||||
Geometry g = new Geometry("loading_rect", quad);
|
||||
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", color);
|
||||
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
g.setMaterial(mat);
|
||||
g.setQueueBucket(RenderQueue.Bucket.Gui);
|
||||
g.setLocalTranslation(0, 0, z);
|
||||
return g;
|
||||
}
|
||||
}
|
||||
@@ -34,8 +34,7 @@ public class ModelLodControl extends AbstractControl {
|
||||
private final Camera cam;
|
||||
private final float lod1DistSq;
|
||||
private final float lod2DistSq;
|
||||
private final float cullDistSq;
|
||||
private int currentSlot = 0; // 0=lod0, 1=lod1, 2=lod2, -1=culled
|
||||
private int currentSlot = 0; // 0=lod0, 1=lod1, 2=lod2, -1=ausgeblendet
|
||||
|
||||
// ── Embedded-LOD-Modus ────────────────────────────────────────────────────
|
||||
private final Spatial embLod0;
|
||||
@@ -55,18 +54,17 @@ public class ModelLodControl extends AbstractControl {
|
||||
/**
|
||||
* Embedded-LOD-Modus: Die j3o-Nodes lod0/lod1/lod2 existieren bereits als
|
||||
* Child-Nodes im Spatial, an dem dieser Control befestigt wird.
|
||||
* lod1 und lod2 dürfen null sein (dann bleibt lod0 bis zur cullDistance sichtbar).
|
||||
* lod1 und lod2 dürfen null sein (dann wird lod0 ab der jeweiligen Schwelle ausgeblendet).
|
||||
*/
|
||||
public ModelLodControl(Camera cam,
|
||||
Spatial lod0, Spatial lod1, Spatial lod2,
|
||||
float lod1Distance, float lod2Distance, float cullDistance) {
|
||||
float lod1Distance, float lod2Distance) {
|
||||
this.cam = cam;
|
||||
this.embLod0 = lod0;
|
||||
this.embLod1 = lod1;
|
||||
this.embLod2 = lod2;
|
||||
this.lod1DistSq = lod1 != null ? lod1Distance * lod1Distance : Float.MAX_VALUE;
|
||||
this.lod2DistSq = lod2 != null ? lod2Distance * lod2Distance : Float.MAX_VALUE;
|
||||
this.cullDistSq = cullDistance * cullDistance;
|
||||
this.lod1DistSq = lod1Distance > 0f ? lod1Distance * lod1Distance : Float.MAX_VALUE;
|
||||
this.lod2DistSq = lod2Distance > 0f ? lod2Distance * lod2Distance : Float.MAX_VALUE;
|
||||
this.assets = null;
|
||||
}
|
||||
|
||||
@@ -76,14 +74,13 @@ public class ModelLodControl extends AbstractControl {
|
||||
*/
|
||||
public ModelLodControl(AssetManager assets, Camera cam,
|
||||
String lod1Path, String lod2Path,
|
||||
float lod1Distance, float lod2Distance, float cullDistance) {
|
||||
float lod1Distance, float lod2Distance) {
|
||||
this.assets = assets;
|
||||
this.cam = cam;
|
||||
this.lod1Path = (lod1Path != null && !lod1Path.isBlank()) ? lod1Path : null;
|
||||
this.lod2Path = (lod2Path != null && !lod2Path.isBlank()) ? lod2Path : null;
|
||||
this.lod1DistSq = lod1Distance * lod1Distance;
|
||||
this.lod2DistSq = lod2Distance * lod2Distance;
|
||||
this.cullDistSq = cullDistance * cullDistance;
|
||||
this.lod1DistSq = lod1Distance > 0f ? lod1Distance * lod1Distance : Float.MAX_VALUE;
|
||||
this.lod2DistSq = lod2Distance > 0f ? lod2Distance * lod2Distance : Float.MAX_VALUE;
|
||||
this.embLod0 = null;
|
||||
this.embLod1 = null;
|
||||
this.embLod2 = null;
|
||||
@@ -115,21 +112,25 @@ public class ModelLodControl extends AbstractControl {
|
||||
|
||||
boolean embedded = embLod0 != null;
|
||||
|
||||
boolean hasLod1 = embedded ? embLod1 != null : lod1Path != null;
|
||||
boolean hasLod2 = embedded ? embLod2 != null : lod2Path != null;
|
||||
|
||||
// Sichtbarkeit hängt nur vom LOD-System ab — kein separates Culling.
|
||||
// Fehlt LOD1 aber LOD2 vorhanden: bei lod1Distance direkt zu LOD2 wechseln (nicht ausblenden).
|
||||
// Fehlt beides (z.B. Kaktusfeige): ab lod1Distance ausblenden.
|
||||
int targetSlot;
|
||||
if (distSq >= cullDistSq) {
|
||||
targetSlot = -1;
|
||||
} else if (embedded) {
|
||||
if (embLod2 != null && distSq >= lod2DistSq) targetSlot = 2;
|
||||
else if (embLod1 != null && distSq >= lod1DistSq) targetSlot = 1;
|
||||
else targetSlot = 0;
|
||||
if (distSq >= lod2DistSq) {
|
||||
targetSlot = hasLod2 ? 2 : -1;
|
||||
} else if (distSq >= lod1DistSq) {
|
||||
if (hasLod1) targetSlot = 1;
|
||||
else if (hasLod2) targetSlot = 2; // kein LOD1, aber LOD2 vorhanden → Impostor früher
|
||||
else targetSlot = -1; // weder LOD1 noch LOD2 → ausblenden
|
||||
} else {
|
||||
if (lod2Path != null && distSq >= lod2DistSq) targetSlot = 2;
|
||||
else if (lod1Path != null && distSq >= lod1DistSq) targetSlot = 1;
|
||||
else targetSlot = 0;
|
||||
targetSlot = 0;
|
||||
}
|
||||
|
||||
if (targetSlot == currentSlot) return;
|
||||
log.debug("[LOD] {} Slot {} → {} (dist={}m)",
|
||||
log.info("[LOD] {} Slot {} → {} (dist={}m)",
|
||||
spatial.getName(), currentSlot, targetSlot,
|
||||
String.format("%.1f", Math.sqrt(distSq)));
|
||||
currentSlot = targetSlot;
|
||||
|
||||
@@ -172,13 +172,21 @@ public class TerrainChunkState extends BaseAppState {
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
float refX, refZ;
|
||||
if (!Float.isNaN(spawnHintX)) {
|
||||
refX = spawnHintX; refZ = spawnHintZ;
|
||||
spawnHintX = Float.NaN; spawnHintZ = Float.NaN;
|
||||
} else {
|
||||
Vector3f camPos = cam.getLocation();
|
||||
refX = camPos.x; refZ = camPos.z;
|
||||
float refX = camPos.x;
|
||||
float refZ = camPos.z;
|
||||
if (!Float.isNaN(spawnHintX)) {
|
||||
int spawnCx = worldToChunk(spawnHintX);
|
||||
int spawnCz = worldToChunk(spawnHintZ);
|
||||
if (worldToChunk(refX) != spawnCx || worldToChunk(refZ) != spawnCz) {
|
||||
// Kamera noch nicht am Spawn-Chunk — Hint beibehalten
|
||||
refX = spawnHintX;
|
||||
refZ = spawnHintZ;
|
||||
} else {
|
||||
// Kamera hat den Spawn-Chunk erreicht — Hint freigeben
|
||||
spawnHintX = Float.NaN;
|
||||
spawnHintZ = Float.NaN;
|
||||
}
|
||||
}
|
||||
int pcx = worldToChunk(refX);
|
||||
int pcz = worldToChunk(refZ);
|
||||
|
||||
@@ -226,9 +226,9 @@ public class WorldItemsState extends BaseAppState {
|
||||
float hMargin = cam.getWidth() * 0.10f;
|
||||
float screenH = cam.getHeight();
|
||||
|
||||
boolean doLog = hoverLogTimer >= 1f;
|
||||
boolean doLog = hoverLogTimer >= 1f && !items.isEmpty();
|
||||
if (hoverLogTimer >= 1f) hoverLogTimer = 0f;
|
||||
if (doLog) {
|
||||
hoverLogTimer = 0f;
|
||||
log.info("[Hover] items={} screenW={} screenH={} centerX={} hMargin={}",
|
||||
items.size(), cam.getWidth(), cam.getHeight(), centerX, hMargin);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@ import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.asset.plugins.FileLocator;
|
||||
import com.jme3.bullet.BulletAppState;
|
||||
import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;
|
||||
import com.jme3.bullet.collision.shapes.CompoundCollisionShape;
|
||||
import com.jme3.bullet.control.RigidBodyControl;
|
||||
import com.jme3.bullet.util.CollisionShapeFactory;
|
||||
import com.jme3.bounding.BoundingBox;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.material.MatParam;
|
||||
import com.jme3.material.RenderState;
|
||||
@@ -38,6 +41,10 @@ public class WorldObjectsState extends BaseAppState {
|
||||
private final List<Material> sceneLitMaterials = new ArrayList<>();
|
||||
private final List<Material> windMaterials = new ArrayList<>();
|
||||
private final List<Material> pbrMaterials = new ArrayList<>();
|
||||
/** Impostor-Unshaded-Materialien: erhalten pro Frame einen Ambient-Tint (Color-Uniform). */
|
||||
private final List<Material> impostorMaterials = new ArrayList<>();
|
||||
/** Capture-Ambient, mit dem der Impostor-Atlas gerendert wurde. Normalisiert den Tint. */
|
||||
private static final float IMPOSTOR_CAP_AMB = 0.30f;
|
||||
private float debugTimer = 0f;
|
||||
private float lodFactor = 1.0f;
|
||||
|
||||
@@ -93,6 +100,30 @@ public class WorldObjectsState extends BaseAppState {
|
||||
else shadowMode = RenderQueue.ShadowMode.Off;
|
||||
s.setShadowMode(shadowMode);
|
||||
|
||||
// Baum-Stammkollision: Capsule statt Vollmesh (Blätter ausgeschlossen)
|
||||
String objPath = m.modelPath() != null ? m.modelPath() : "";
|
||||
if (objPath.startsWith("Models/trees/") && bulletAppState != null) {
|
||||
try {
|
||||
s.updateGeometricState();
|
||||
float treeHeight = 6f;
|
||||
com.jme3.bounding.BoundingVolume bv = s.getWorldBound();
|
||||
if (bv instanceof BoundingBox bb) {
|
||||
treeHeight = bb.getYExtent() * 2f;
|
||||
}
|
||||
float cylHeight = Math.max(0.5f, treeHeight - 0.5f);
|
||||
CapsuleCollisionShape capsule =
|
||||
new CapsuleCollisionShape(0.25f, cylHeight);
|
||||
CompoundCollisionShape compound =
|
||||
new CompoundCollisionShape();
|
||||
compound.addChildShape(capsule, new Vector3f(0f, treeHeight * 0.5f, 0f));
|
||||
RigidBodyControl treeRbc = new RigidBodyControl(compound, 0f);
|
||||
s.addControl(treeRbc);
|
||||
bulletAppState.getPhysicsSpace().add(treeRbc);
|
||||
} catch (Exception pe) {
|
||||
log.warn("[WorldObjects] Baum-Physik für '{}' nicht erzeugbar: {}", m.modelPath(), pe.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// Physik-Kollision für solide Objekte
|
||||
if (m.solid() && bulletAppState != null) {
|
||||
try {
|
||||
@@ -143,6 +174,17 @@ public class WorldObjectsState extends BaseAppState {
|
||||
for (Material mat : pbrMaterials) {
|
||||
mat.setColor("Emissive", new ColorRGBA(ac.r, ac.g, ac.b, 1f));
|
||||
}
|
||||
// Impostor-Tint: Unshaded-Atlas folgt dem Tag/Nacht-Zyklus über den Color-Multiplikator.
|
||||
// Normiert auf die Capture-Ambient (0.30): Bei gleichem Ambient = 1.0, Nacht ≈ 0.05.
|
||||
if (!impostorMaterials.isEmpty()) {
|
||||
float tR = Math.min(1.2f, ac.r / IMPOSTOR_CAP_AMB);
|
||||
float tG = Math.min(1.2f, ac.g / IMPOSTOR_CAP_AMB);
|
||||
float tB = Math.min(1.2f, ac.b / IMPOSTOR_CAP_AMB);
|
||||
ColorRGBA tint = new ColorRGBA(tR, tG, tB, 1f);
|
||||
for (Material mat : impostorMaterials) {
|
||||
mat.setColor("Color", tint);
|
||||
}
|
||||
}
|
||||
debugTimer += tpf;
|
||||
if (debugTimer >= 5f) {
|
||||
debugTimer = 0f;
|
||||
@@ -194,41 +236,80 @@ public class WorldObjectsState extends BaseAppState {
|
||||
}
|
||||
spatial.setName("obj_" + path);
|
||||
|
||||
// LOD / Distance-Culling
|
||||
if (m.cullDistance() > 0f) {
|
||||
// LOD — kein cullDistance mehr; Objekte ohne LOD-Stufe werden ab der jeweiligen Distanz ausgeblendet
|
||||
if (m.lod1Distance() > 0f || m.lod2Distance() > 0f) {
|
||||
if (spatial instanceof Node treeNode) {
|
||||
Spatial lod0 = treeNode.getChild("lod0");
|
||||
Spatial lod1 = treeNode.getChild("lod1");
|
||||
Spatial lod2 = treeNode.getChild("lod2");
|
||||
|
||||
// Fall A: j3o hat explizite lod0/lod1/lod2 Child-Nodes.
|
||||
// Neues eingebettetes Format: child[0] ist das HD-Modell, aber nicht explizit "lod0"
|
||||
// benannt. Erkennungsmerkmal: "lod1" ist vorhanden (alter Impostor-Format hat nie lod1).
|
||||
if (lod0 == null && lod1 != null && !treeNode.getChildren().isEmpty()) {
|
||||
lod0 = treeNode.getChild(0);
|
||||
}
|
||||
|
||||
// Fall A: j3o hat eingebettete LOD-Child-Nodes (lod0 explizit oder per Index 0 erkannt).
|
||||
if (lod0 != null) {
|
||||
log.info("[WorldObjects-LOD] Fall A: {} | Kinder={} | lod0='{}' lod1={} lod2={} | dist={}/{}m",
|
||||
path, treeNode.getChildren().size(),
|
||||
lod0.getName(), lod1 != null ? "'" + lod1.getName() + "'" : "null",
|
||||
lod2 != null ? "'" + lod2.getName() + "'" : "null",
|
||||
m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor);
|
||||
lod0.setCullHint(Spatial.CullHint.Inherit);
|
||||
if (lod1 != null) lod1.setCullHint(Spatial.CullHint.Always);
|
||||
if (lod2 != null) lod2.setCullHint(Spatial.CullHint.Always);
|
||||
if (lod2 != null) {
|
||||
lod2.setCullHint(Spatial.CullHint.Always);
|
||||
if (lod2 instanceof Node lod2Node && !lod2Node.getChildren().isEmpty()
|
||||
&& isImpostorNode(lod2Node)) {
|
||||
initImpostor(lod2Node);
|
||||
}
|
||||
}
|
||||
treeNode.addControl(new ModelLodControl(
|
||||
app.getCamera(), lod0, lod1, lod2,
|
||||
m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor, m.cullDistance() * lodFactor));
|
||||
m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor));
|
||||
return treeNode;
|
||||
}
|
||||
|
||||
// Vorhandene Controls im geladenen j3o loggen (sollten nach stripControls nicht vorhanden sein)
|
||||
if (treeNode.getNumControls() > 0) {
|
||||
log.warn("[WorldObjects-LOD] {} hat {} Control(s) nach dem Laden! Controls werden entfernt.",
|
||||
path, treeNode.getNumControls());
|
||||
while (treeNode.getNumControls() > 0) treeNode.removeControl(treeNode.getControl(0));
|
||||
}
|
||||
|
||||
// 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) {
|
||||
log.info("[WorldObjects-LOD] Fall B: {} | Kinder={} | lod2='{}' | dist=0/{}m",
|
||||
path, treeNode.getChildren().size(), lod2.getName(),
|
||||
m.lod2Distance() * lodFactor);
|
||||
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);
|
||||
// Pivot-Korrektur: Stammbasis auf Y=0 ausrichten (Sicherungsnetz für
|
||||
// j3o-Dateien, deren 1/3-Scale-Offset nicht eingebacken wurde).
|
||||
// treeNode und lod2 gemeinsam verschieben → Geometrie und Impostor-Bild
|
||||
// bleiben deckungsgleich.
|
||||
lodRoot.updateGeometricState();
|
||||
if (treeNode.getWorldBound() instanceof com.jme3.bounding.BoundingBox treeBb) {
|
||||
float groundY = treeBb.getCenter().y - treeBb.getYExtent();
|
||||
if (Math.abs(groundY) > 0.01f) {
|
||||
treeNode.setLocalTranslation(0f, -groundY, 0f);
|
||||
lod2.setLocalTranslation(0f, -groundY, 0f);
|
||||
}
|
||||
}
|
||||
// Impostor: wählt pro Frame das kamerawinkelrichtige Quad.
|
||||
if (lod2 instanceof Node lod2Node && !lod2Node.getChildren().isEmpty()
|
||||
&& isImpostorNode(lod2Node)) {
|
||||
initImpostor(lod2Node);
|
||||
}
|
||||
lodRoot.addControl(new ModelLodControl(
|
||||
app.getCamera(), treeNode, null, lod2,
|
||||
m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor, m.cullDistance() * lodFactor));
|
||||
m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor));
|
||||
return lodRoot;
|
||||
}
|
||||
}
|
||||
@@ -239,7 +320,7 @@ public class WorldObjectsState extends BaseAppState {
|
||||
ModelLodControl ctrl = new ModelLodControl(
|
||||
assets, app.getCamera(),
|
||||
m.lod1Path(), m.lod2Path(),
|
||||
m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor, m.cullDistance() * lodFactor);
|
||||
m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor);
|
||||
ctrl.setLodLoadCallback(lod -> {
|
||||
convertUnshadedToPbr(lod);
|
||||
collectSceneLitMaterials(lod);
|
||||
@@ -251,6 +332,83 @@ public class WorldObjectsState extends BaseAppState {
|
||||
return spatial;
|
||||
}
|
||||
|
||||
private boolean isImpostorNode(Node n) {
|
||||
if (n.getControl(BillboardControl.class) != null) {
|
||||
log.info("[Impostor] '{}' erkannt via BillboardControl", n.getName());
|
||||
return true;
|
||||
}
|
||||
if (n.getChildren().isEmpty()) {
|
||||
log.info("[Impostor] '{}' hat keine Kinder → kein Impostor", n.getName());
|
||||
return false;
|
||||
}
|
||||
Spatial first = n.getChild(0);
|
||||
log.info("[Impostor] '{}' erstes Kind: {} ({})", n.getName(), first.getName(), first.getClass().getSimpleName());
|
||||
if (!(first instanceof Geometry g)) {
|
||||
log.info("[Impostor] '{}' erstes Kind ist kein Geometry → kein Impostor", n.getName());
|
||||
return false;
|
||||
}
|
||||
Material mat = g.getMaterial();
|
||||
String matName = mat != null ? mat.getMaterialDef().getName() : "null";
|
||||
log.info("[Impostor] '{}' Material: '{}'", n.getName(), matName);
|
||||
return mat != null && "Unshaded".equals(matName);
|
||||
}
|
||||
|
||||
private void initImpostor(Node lod2Node) {
|
||||
lod2Node.addControl(new ImpostorViewControl(app.getCamera()));
|
||||
if (lod2Node.getControl(BillboardControl.class) == null) {
|
||||
BillboardControl bc = new BillboardControl();
|
||||
bc.setAlignment(BillboardControl.Alignment.AxialY);
|
||||
lod2Node.addControl(bc);
|
||||
}
|
||||
|
||||
// In Opaque-Bucket verschieben (auch für ältere j3os, die noch Transparent hatten).
|
||||
lod2Node.setQueueBucket(com.jme3.renderer.queue.RenderQueue.Bucket.Opaque);
|
||||
|
||||
java.util.Set<Material> seen = new java.util.HashSet<>();
|
||||
boolean firstMat = true;
|
||||
for (Spatial child : lod2Node.getChildren()) {
|
||||
if (!(child instanceof com.jme3.scene.Geometry geo)) continue;
|
||||
Material mat = geo.getMaterial();
|
||||
if (mat == null) continue;
|
||||
|
||||
mat.setFloat("AlphaDiscardThreshold", 0.5f);
|
||||
mat.getAdditionalRenderState().setBlendMode(com.jme3.material.RenderState.BlendMode.Off);
|
||||
// FaceCullMode.Off: Billboard dreht sich immer zur Kamera → beide Seiten rendern
|
||||
// ist sicherer als Back, da Winding-Order nach Billboard-Rotation nicht deterministisch ist.
|
||||
mat.getAdditionalRenderState().setFaceCullMode(com.jme3.material.RenderState.FaceCullMode.Off);
|
||||
geo.setQueueBucket(com.jme3.renderer.queue.RenderQueue.Bucket.Opaque);
|
||||
|
||||
com.jme3.material.MatParam colorMap = mat.getParam("ColorMap");
|
||||
if (colorMap != null && colorMap.getValue() instanceof com.jme3.texture.Texture t) {
|
||||
t.getImage().setColorSpace(com.jme3.texture.image.ColorSpace.sRGB);
|
||||
log.info("[Impostor] Tex '{}': {}×{} fmt={}",
|
||||
t.getName(), t.getImage().getWidth(), t.getImage().getHeight(), t.getImage().getFormat());
|
||||
// Diagnose: Alpha-Wert im Zentrum von Strip 0 prüfen (Pixel 256,256)
|
||||
if (firstMat) {
|
||||
firstMat = false;
|
||||
java.nio.ByteBuffer imgData = t.getImage().getData(0);
|
||||
if (imgData != null && imgData.capacity() >= (256 * 4096 + 256) * 4 + 4) {
|
||||
int off = (256 * 4096 + 256) * 4;
|
||||
int r = imgData.get(off) & 0xFF;
|
||||
int g = imgData.get(off + 1) & 0xFF;
|
||||
int b = imgData.get(off + 2) & 0xFF;
|
||||
int a = imgData.get(off + 3) & 0xFF;
|
||||
log.info("[Impostor] Atlas Pixel(256,256) Strip0: R={} G={} B={} A={}", r, g, b, a);
|
||||
} else {
|
||||
log.warn("[Impostor] Atlas-Daten nicht lesbar (capacity={})",
|
||||
imgData != null ? imgData.capacity() : -1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.warn("[Impostor] Quad '{}' hat KEINE ColorMap!", geo.getName());
|
||||
}
|
||||
|
||||
if (seen.add(mat)) {
|
||||
impostorMaterials.add(mat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Spatial createPrimitive(String type) {
|
||||
return switch (type) {
|
||||
case "sphere" -> new Geometry("sphere", new Sphere(16, 16, 1f));
|
||||
@@ -291,6 +449,7 @@ public class WorldObjectsState extends BaseAppState {
|
||||
|
||||
geo.setMaterial(pbrMat);
|
||||
} else if (spatial instanceof Node node) {
|
||||
if ("lod2".equals(node.getName())) return;
|
||||
for (Spatial child : new ArrayList<>(node.getChildren())) {
|
||||
convertUnshadedToPbr(child);
|
||||
}
|
||||
@@ -303,12 +462,18 @@ public class WorldObjectsState extends BaseAppState {
|
||||
if (mat == null) return;
|
||||
String name = mat.getMaterialDef().getName();
|
||||
if ("Tree".equals(name) || "TreeLeaf".equals(name)) {
|
||||
// j3o-Loading setzt keine j3md-Defaults → Uniforms explizit setzen
|
||||
// (Werte für Mittag; werden sofort in update() durch DayNightState überschrieben)
|
||||
mat.setVector3("SunColor", new Vector3f(1.00f, 0.95f, 0.88f));
|
||||
mat.setVector3("AmbientColor", new Vector3f(0.08f, 0.10f, 0.16f));
|
||||
// j3o-Loading setzt keine j3md-Defaults → Uniforms explizit setzen.
|
||||
// SunColor = SUN_COLOR_DAY × SUN_INTENSITY (wie in update() bei Mittag),
|
||||
// sonst leuchten Bäume weiß bis DayNightState das erste Mal überschreibt.
|
||||
mat.setVector3("SunColor", new Vector3f(0.68f, 0.65f, 0.60f));
|
||||
mat.setVector3("AmbientColor", new Vector3f(0.04f, 0.05f, 0.08f));
|
||||
mat.setVector3("LightDir", new Vector3f(0f, -1f, 0f));
|
||||
sceneLitMaterials.add(mat);
|
||||
if ("TreeLeaf".equals(name)) {
|
||||
// discard statt blend → Blätter müssen Tiefenwerte schreiben (Nebel, Wasser).
|
||||
geo.setQueueBucket(RenderQueue.Bucket.Opaque);
|
||||
mat.getAdditionalRenderState().setDepthWrite(true);
|
||||
}
|
||||
} else if ("Fern".equals(name)) {
|
||||
windMaterials.add(mat);
|
||||
} else if ("PBRLighting".equals(name)) {
|
||||
@@ -325,6 +490,7 @@ public class WorldObjectsState extends BaseAppState {
|
||||
log.info("[WorldObjects] Material-Def '{}' an '{}' – unbekannt, übersprungen.", name, geo.getName());
|
||||
}
|
||||
} else if (spatial instanceof Node node) {
|
||||
if ("lod2".equals(node.getName())) return;
|
||||
for (Spatial child : node.getChildren()) {
|
||||
collectSceneLitMaterials(child);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<configuration>
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||
<level>INFO</level>
|
||||
</filter>
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} %-5level [%logger{30}] %msg%n%ex</pattern>
|
||||
</encoder>
|
||||
@@ -20,6 +17,9 @@
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<!-- LOD-Slot-Wechsel auf DEBUG aktivieren -->
|
||||
<logger name="de.blight.game.state.ModelLodControl" level="DEBUG"/>
|
||||
|
||||
<!-- JME-interne JUL-Logs auf WARN reduzieren -->
|
||||
<logger name="com.jme3" level="WARN"/>
|
||||
<!-- GltfLoader meldet bei jeder Animation "only supports linear interpolation" – bekanntes JME-Verhalten, kein Fehler -->
|
||||
|
||||
@@ -54,6 +54,11 @@ 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.row.shadow=Schattenqualität
|
||||
menu.graphics.val.shadow.low=Niedrig
|
||||
menu.graphics.val.shadow.medium=Mittel
|
||||
menu.graphics.val.shadow.high=Hoch
|
||||
menu.graphics.val.resolution.native=Nativ
|
||||
menu.graphics.btn.apply=Übernehmen
|
||||
menu.graphics.btn.cancel=Abbrechen
|
||||
|
||||
|
||||
@@ -57,6 +57,11 @@ 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.row.shadow=Shadow Quality
|
||||
menu.graphics.val.shadow.low=Low
|
||||
menu.graphics.val.shadow.medium=Medium
|
||||
menu.graphics.val.shadow.high=High
|
||||
menu.graphics.val.resolution.native=Native
|
||||
menu.graphics.btn.apply=Apply
|
||||
menu.graphics.btn.cancel=Cancel
|
||||
|
||||
|
||||
Binary file not shown.
339
com/jme3/shadow/AbstractShadowFilter.java
Normal file
339
com/jme3/shadow/AbstractShadowFilter.java
Normal file
@@ -0,0 +1,339 @@
|
||||
/*
|
||||
* Copyright (c) 2009-2025 jMonkeyEngine
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are
|
||||
* met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package com.jme3.shadow;
|
||||
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.math.Matrix4f;
|
||||
import com.jme3.math.Vector4f;
|
||||
import com.jme3.post.Filter;
|
||||
import com.jme3.renderer.RenderManager;
|
||||
import com.jme3.renderer.ViewPort;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.texture.FrameBuffer;
|
||||
import com.jme3.util.TempVars;
|
||||
import com.jme3.util.clone.Cloner;
|
||||
import com.jme3.util.clone.JmeCloneable;
|
||||
|
||||
/**
|
||||
* Generic abstract filter that holds common implementations for the different
|
||||
* shadow filters
|
||||
*
|
||||
* @author Rémy Bouquet aka Nehon
|
||||
*/
|
||||
public abstract class AbstractShadowFilter<T extends AbstractShadowRenderer> extends Filter implements JmeCloneable {
|
||||
|
||||
protected T shadowRenderer;
|
||||
protected ViewPort viewPort;
|
||||
|
||||
private final Vector4f tempVec4 = new Vector4f();
|
||||
private final Matrix4f tempMat4 = new Matrix4f();
|
||||
|
||||
/**
|
||||
* For serialization only. Do not use.
|
||||
*/
|
||||
protected AbstractShadowFilter() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an AbstractShadowFilter. Subclasses invoke this constructor.
|
||||
*
|
||||
* @param assetManager The application's asset manager.
|
||||
* @param shadowMapSize The size of the rendered shadow maps (e.g., 512, 1024, 2048).
|
||||
* @param shadowRenderer The shadowRenderer to use for this Filter
|
||||
*/
|
||||
protected AbstractShadowFilter(AssetManager assetManager, int shadowMapSize, T shadowRenderer) {
|
||||
super("Post Shadow");
|
||||
this.shadowRenderer = shadowRenderer;
|
||||
// this is legacy setting for shadows with backface shadows
|
||||
this.shadowRenderer.setRenderBackFacesShadows(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Material getMaterial() {
|
||||
return material;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isRequiresDepthTexture() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use {@link #getMaterial()} instead.
|
||||
* @return The Material used by this filter.
|
||||
*/
|
||||
@Deprecated
|
||||
public Material getShadowMaterial() {
|
||||
return material;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void preFrame(float tpf) {
|
||||
shadowRenderer.preFrame(tpf);
|
||||
Matrix4f m = viewPort.getCamera().getViewProjectionMatrix();
|
||||
material.setMatrix4("ViewProjectionMatrixInverse", tempMat4.set(m).invertLocal());
|
||||
material.setVector4("ViewProjectionMatrixRow2", tempVec4.set(m.m20, m.m21, m.m22, m.m23));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void postQueue(RenderQueue queue) {
|
||||
shadowRenderer.postQueue(queue);
|
||||
if (shadowRenderer.skipPostPass) {
|
||||
// removing the shadow map so that the post pass is skipped
|
||||
material.setTexture("ShadowMap0", null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void postFrame(RenderManager renderManager, ViewPort viewPort, FrameBuffer prevFilterBuffer, FrameBuffer sceneBuffer) {
|
||||
if (!shadowRenderer.skipPostPass) {
|
||||
shadowRenderer.setPostShadowParams();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initFilter(AssetManager manager, RenderManager renderManager, ViewPort vp, int w, int h) {
|
||||
shadowRenderer.needsfallBackMaterial = true;
|
||||
material = new Material(manager, "Common/MatDefs/Shadow/PostShadowFilter.j3md");
|
||||
shadowRenderer.setPostShadowMaterial(material);
|
||||
shadowRenderer.initialize(renderManager, vp);
|
||||
this.viewPort = vp;
|
||||
}
|
||||
|
||||
/**
|
||||
* How far the shadows are rendered in the view
|
||||
*
|
||||
* @return shadowZExtend
|
||||
* @see #setShadowZExtend(float zFar)
|
||||
*/
|
||||
public float getShadowZExtend() {
|
||||
return shadowRenderer.getShadowZExtend();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the distance from the eye where the shadows will be rendered default
|
||||
* value is dynamically computed to the shadow casters/receivers union bound
|
||||
* zFar, capped to view frustum far value.
|
||||
*
|
||||
* @param zFar the zFar values that override the computed one
|
||||
*/
|
||||
public void setShadowZExtend(float zFar) {
|
||||
shadowRenderer.setShadowZExtend(zFar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the length over which the shadow will fade out when using a
|
||||
* shadowZextend
|
||||
*
|
||||
* @param length the fade length in world units
|
||||
*/
|
||||
public void setShadowZFadeLength(float length) {
|
||||
shadowRenderer.setShadowZFadeLength(length);
|
||||
}
|
||||
|
||||
/**
|
||||
* get the length over which the shadow will fade out when using a
|
||||
* shadowZextend
|
||||
*
|
||||
* @return the fade length in world units
|
||||
*/
|
||||
public float getShadowZFadeLength() {
|
||||
return shadowRenderer.getShadowZFadeLength();
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the shadow intensity
|
||||
*
|
||||
* @see #setShadowIntensity(float shadowIntensity)
|
||||
* @return shadowIntensity
|
||||
*/
|
||||
public float getShadowIntensity() {
|
||||
return shadowRenderer.getShadowIntensity();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the shadowIntensity, the value should be between 0 and 1, a 0 value
|
||||
* gives a bright and invisible shadow, a 1 value gives a pitch black
|
||||
* shadow, default is 0.7
|
||||
*
|
||||
* @param shadowIntensity the darkness of the shadow
|
||||
*/
|
||||
final public void setShadowIntensity(float shadowIntensity) {
|
||||
shadowRenderer.setShadowIntensity(shadowIntensity);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the edges thickness <br>
|
||||
*
|
||||
* @see #setEdgesThickness(int edgesThickness)
|
||||
* @return edgesThickness
|
||||
*/
|
||||
public int getEdgesThickness() {
|
||||
return shadowRenderer.getEdgesThickness();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the shadow edges thickness. Default is 10. Setting it to lower values
|
||||
* can help reduce the jagged effect of shadow edges.
|
||||
*
|
||||
* @param edgesThickness the desired thickness (in tenths of a pixel, default=10)
|
||||
*/
|
||||
public void setEdgesThickness(int edgesThickness) {
|
||||
shadowRenderer.setEdgesThickness(edgesThickness);
|
||||
}
|
||||
|
||||
/**
|
||||
* isFlushQueues does nothing and is kept only for backward compatibility
|
||||
*
|
||||
* @return false
|
||||
*/
|
||||
@Deprecated
|
||||
public boolean isFlushQueues() {
|
||||
return shadowRenderer.isFlushQueues();
|
||||
}
|
||||
|
||||
/**
|
||||
* sets the shadow compare mode see {@link CompareMode} for more info
|
||||
*
|
||||
* @param compareMode the desired mode
|
||||
*/
|
||||
final public void setShadowCompareMode(CompareMode compareMode) {
|
||||
shadowRenderer.setShadowCompareMode(compareMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the shadow compare mode
|
||||
*
|
||||
* @see CompareMode
|
||||
* @return the shadowCompareMode
|
||||
*/
|
||||
public CompareMode getShadowCompareMode() {
|
||||
return shadowRenderer.getShadowCompareMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the filtering mode for shadow edges see {@link EdgeFilteringMode}
|
||||
* for more info
|
||||
*
|
||||
* @param filterMode the desired mode
|
||||
*/
|
||||
final public void setEdgeFilteringMode(EdgeFilteringMode filterMode) {
|
||||
shadowRenderer.setEdgeFilteringMode(filterMode);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* !! WARNING !! this parameter is defaulted to true for the ShadowFilter.
|
||||
* Setting it to true, may produce edges artifacts on shadows.
|
||||
*
|
||||
* Set to true if you want back faces shadows on geometries.
|
||||
* Note that back faces shadows will be blended over dark lighten areas and may produce overly dark lighting.
|
||||
*
|
||||
* Setting this parameter will override this parameter for ALL materials in the scene.
|
||||
* This also will automatically adjust the faceCullMode and the PolyOffset of the pre shadow pass.
|
||||
* You can modify them by using {@link #getPreShadowForcedRenderState()}
|
||||
*
|
||||
* If you want to set it differently for each material in the scene you have to use the ShadowRenderer instead
|
||||
* of the shadow filter.
|
||||
*
|
||||
* @param renderBackFacesShadows true or false.
|
||||
*/
|
||||
public void setRenderBackFacesShadows(boolean renderBackFacesShadows) {
|
||||
shadowRenderer.setRenderBackFacesShadows(renderBackFacesShadows);
|
||||
}
|
||||
|
||||
/**
|
||||
* if this filter renders back faces shadows
|
||||
* @return true if this filter renders back faces shadows
|
||||
*/
|
||||
public boolean isRenderBackFacesShadows() {
|
||||
return shadowRenderer.isRenderBackFacesShadows();
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the pre shadows pass render state.
|
||||
* use it to adjust the RenderState parameters of the pre shadow pass.
|
||||
* Note that this will be overridden if the preShadow technique in the material has a ForcedRenderState
|
||||
* @return the pre shadow render state.
|
||||
*/
|
||||
public RenderState getPreShadowForcedRenderState() {
|
||||
return shadowRenderer.getPreShadowForcedRenderState();
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the edge filtering mode
|
||||
*
|
||||
* @see EdgeFilteringMode
|
||||
* @return the enum value
|
||||
*/
|
||||
public EdgeFilteringMode getEdgeFilteringMode() {
|
||||
return shadowRenderer.getEdgeFilteringMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the number of shadow maps rendered by this filter.
|
||||
*
|
||||
* @return count
|
||||
*/
|
||||
public int getNumShadowMaps() {
|
||||
return shadowRenderer.getNumShadowMaps();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the size of each shadow map rendered by this filter.
|
||||
*
|
||||
* @return a map's height (which is also its width, in pixels)
|
||||
*/
|
||||
public int getShadowMapSize() {
|
||||
return shadowRenderer.getShadowMapSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public AbstractShadowFilter<T> jmeClone() {
|
||||
try {
|
||||
return (AbstractShadowFilter<T>) super.clone();
|
||||
} catch (final CloneNotSupportedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cloneFields(final Cloner cloner, final Object original) {
|
||||
material = cloner.clone(material);
|
||||
shadowRenderer = cloner.clone(shadowRenderer);
|
||||
shadowRenderer.setPostShadowMaterial(material);
|
||||
}
|
||||
|
||||
}
|
||||
157
com/jme3/shadow/DirectionalLightShadowFilter.java
Normal file
157
com/jme3/shadow/DirectionalLightShadowFilter.java
Normal file
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright (c) 2009-2024 jMonkeyEngine
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are
|
||||
* met:
|
||||
*
|
||||
* * Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
*
|
||||
* * Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
||||
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
||||
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
package com.jme3.shadow;
|
||||
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.export.InputCapsule;
|
||||
import com.jme3.export.JmeExporter;
|
||||
import com.jme3.export.JmeImporter;
|
||||
import com.jme3.export.OutputCapsule;
|
||||
import com.jme3.light.DirectionalLight;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* This Filter does basically the same as a DirectionalLightShadowRenderer
|
||||
* except it renders the post shadow pass as a fullscreen quad pass instead of a
|
||||
* geometry pass. It's mostly faster than PssmShadowRenderer as long as you have
|
||||
* more than about ten shadow receiving objects. The expense is the drawback
|
||||
* that the shadow Receive mode set on spatial is ignored. So basically all and
|
||||
* only objects that render depth in the scene receive shadows.
|
||||
*
|
||||
* API is basically the same as the PssmShadowRenderer.
|
||||
*
|
||||
* @author Rémy Bouquet aka Nehon
|
||||
*/
|
||||
public class DirectionalLightShadowFilter extends AbstractShadowFilter<DirectionalLightShadowRenderer> {
|
||||
|
||||
/**
|
||||
* For serialization only. Do not use.
|
||||
*
|
||||
* @see #DirectionalLightShadowFilter(AssetManager assetManager, int shadowMapSize, int nbSplits)
|
||||
*/
|
||||
public DirectionalLightShadowFilter() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DirectionalLightShadowFilter.
|
||||
*
|
||||
* @param assetManager the application's asset manager
|
||||
* @param shadowMapSize the size of the rendered shadow maps (512, 1024, 2048, etc...)
|
||||
* @param nbSplits the number of shadow maps rendered (more shadow maps = better quality, but slower)
|
||||
*
|
||||
* @throws IllegalArgumentException if the provided 'nbSplits' is not within the valid range of 1 to 4.
|
||||
*/
|
||||
public DirectionalLightShadowFilter(AssetManager assetManager, int shadowMapSize, int nbSplits) {
|
||||
super(assetManager, shadowMapSize, new DirectionalLightShadowRenderer(assetManager, shadowMapSize, nbSplits));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the light used to cast shadows.
|
||||
*
|
||||
* @return the DirectionalLight
|
||||
*/
|
||||
public DirectionalLight getLight() {
|
||||
return shadowRenderer.getLight();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the light to use to cast shadows.
|
||||
*
|
||||
* @param light a DirectionalLight
|
||||
*/
|
||||
public void setLight(DirectionalLight light) {
|
||||
shadowRenderer.setLight(light);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the lambda parameter.
|
||||
*
|
||||
* @see #setLambda(float lambda)
|
||||
* @return lambda
|
||||
*/
|
||||
public float getLambda() {
|
||||
return shadowRenderer.getLambda();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the partition of the shadow extend into shadow maps. Lambda is
|
||||
* usually between 0 and 1.
|
||||
* <p>
|
||||
* A low value gives a more linear partition, resulting in consistent shadow
|
||||
* quality over the extend, but near shadows could look very jagged. A high
|
||||
* value gives a more logarithmic partition, resulting in high quality for near
|
||||
* shadows, but quality decreases rapidly with distance.
|
||||
* <p>
|
||||
* The default value is 0.65 (the theoretical optimum).
|
||||
*
|
||||
* @param lambda the lambda value
|
||||
*/
|
||||
public void setLambda(float lambda) {
|
||||
shadowRenderer.setLambda(lambda);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if stabilization is enabled.
|
||||
*
|
||||
* @return true if stabilization is enabled
|
||||
*/
|
||||
public boolean isEnabledStabilization() {
|
||||
return shadowRenderer.isEnabledStabilization();
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the stabilization of the shadow's edges. (default is true)
|
||||
* This prevents shadow edges from flickering when the camera moves.
|
||||
* However, it can lead to some loss of shadow quality in particular scenes.
|
||||
*
|
||||
* @param stabilize true to stabilize, false to disable stabilization
|
||||
*/
|
||||
public void setEnabledStabilization(boolean stabilize) {
|
||||
shadowRenderer.setEnabledStabilization(stabilize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(JmeExporter ex) throws IOException {
|
||||
super.write(ex);
|
||||
OutputCapsule oc = ex.getCapsule(this);
|
||||
oc.write(shadowRenderer, "shadowRenderer", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void read(JmeImporter im) throws IOException {
|
||||
super.read(im);
|
||||
InputCapsule ic = im.getCapsule(this);
|
||||
shadowRenderer = (DirectionalLightShadowRenderer) ic.readSavable("shadowRenderer", null);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user