Fehler im LOD System behoben
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 904 B After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 757 B After Width: | Height: | Size: 998 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 990 B |
@@ -10,6 +10,30 @@ MaterialDef Tree {
|
|||||||
Vector3 LightDir : 0.6 -0.8 0.4
|
Vector3 LightDir : 0.6 -0.8 0.4
|
||||||
Vector3 SunColor : 0.68 0.65 0.60
|
Vector3 SunColor : 0.68 0.65 0.60
|
||||||
Vector3 AmbientColor : 0.08 0.10 0.16
|
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 {
|
Technique {
|
||||||
@@ -25,4 +49,19 @@ MaterialDef Tree {
|
|||||||
Defines {
|
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 @@
|
|||||||
#Mon Jul 20 11:46:10 CEST 2026
|
#Mon Jul 20 20:34:07 CEST 2026
|
||||||
attachedEmitters.count=0
|
attachedEmitters.count=0
|
||||||
attachedLights.count=0
|
attachedLights.count=0
|
||||||
castShadow=true
|
castShadow=true
|
||||||
|
|||||||
Binary file not shown.
@@ -1,4 +1,4 @@
|
|||||||
#Mon Jul 20 13:09:03 CEST 2026
|
#Mon Jul 20 20:34:14 CEST 2026
|
||||||
attachedEmitters.count=0
|
attachedEmitters.count=0
|
||||||
attachedLights.count=0
|
attachedLights.count=0
|
||||||
castShadow=true
|
castShadow=true
|
||||||
|
|||||||
Binary file not shown.
@@ -1,4 +1,4 @@
|
|||||||
#Mon Jul 20 13:25:15 CEST 2026
|
#Mon Jul 20 20:34:23 CEST 2026
|
||||||
attachedEmitters.count=0
|
attachedEmitters.count=0
|
||||||
attachedLights.count=0
|
attachedLights.count=0
|
||||||
castShadow=true
|
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;
|
||||||
|
}
|
||||||
@@ -267,6 +267,7 @@ public class EditorApp extends Application {
|
|||||||
private ToggleButton importLod2Btn;
|
private ToggleButton importLod2Btn;
|
||||||
private Button modelImportLod1ClearBtn;
|
private Button modelImportLod1ClearBtn;
|
||||||
private Button modelImportLod2ClearBtn;
|
private Button modelImportLod2ClearBtn;
|
||||||
|
private Button modelImportLod2GenBtn;
|
||||||
// Anhänge (veränderliche Listen, werden bei Änderung an JME übertragen)
|
// Anhänge (veränderliche Listen, werden bei Änderung an JME übertragen)
|
||||||
private final java.util.List<de.blight.common.ModelMeta.AttachedLight> modelEditorLights = new java.util.ArrayList<>();
|
private final java.util.List<de.blight.common.ModelMeta.AttachedLight> modelEditorLights = new java.util.ArrayList<>();
|
||||||
private final java.util.List<de.blight.common.ModelMeta.AttachedEmitter> modelEditorEmitters = new java.util.ArrayList<>();
|
private final java.util.List<de.blight.common.ModelMeta.AttachedEmitter> modelEditorEmitters = new java.util.ArrayList<>();
|
||||||
@@ -692,8 +693,11 @@ public class EditorApp extends Application {
|
|||||||
// Generieren-Buttons: nur aktiv wenn genau dieses LOD-Level noch nicht gesetzt
|
// Generieren-Buttons: nur aktiv wenn genau dieses LOD-Level noch nicht gesetzt
|
||||||
if (modelEditorLod1GenBtn != null)
|
if (modelEditorLod1GenBtn != null)
|
||||||
modelEditorLod1GenBtn.setDisable(input.modelEditorHasEmbeddedLod1 || !modelEditorLod1Path.isBlank() || input.modelImportHasLod1);
|
modelEditorLod1GenBtn.setDisable(input.modelEditorHasEmbeddedLod1 || !modelEditorLod1Path.isBlank() || input.modelImportHasLod1);
|
||||||
if (modelEditorLod2GenBtn != null)
|
if (modelEditorLod2GenBtn != null) {
|
||||||
modelEditorLod2GenBtn.setDisable(input.modelEditorHasEmbeddedLod2 || !modelEditorLod2Path.isBlank() || input.modelImportHasLod2);
|
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
|
// Importer LOD-Buttons freischalten sobald generiert
|
||||||
if (importLod1Btn != null) importLod1Btn.setDisable(!input.modelImportHasLod1);
|
if (importLod1Btn != null) importLod1Btn.setDisable(!input.modelImportHasLod1);
|
||||||
if (importLod2Btn != null) importLod2Btn.setDisable(!input.modelImportHasLod2);
|
if (importLod2Btn != null) importLod2Btn.setDisable(!input.modelImportHasLod2);
|
||||||
@@ -707,6 +711,7 @@ public class EditorApp extends Application {
|
|||||||
if (importLod2Btn != null) { importLod2Btn.setSelected(lodPreview == 2); }
|
if (importLod2Btn != null) { importLod2Btn.setSelected(lodPreview == 2); }
|
||||||
if (modelImportLod1ClearBtn != null) modelImportLod1ClearBtn.setDisable(!input.modelImportHasLod1);
|
if (modelImportLod1ClearBtn != null) modelImportLod1ClearBtn.setDisable(!input.modelImportHasLod1);
|
||||||
if (modelImportLod2ClearBtn != null) modelImportLod2ClearBtn.setDisable(!input.modelImportHasLod2);
|
if (modelImportLod2ClearBtn != null) modelImportLod2ClearBtn.setDisable(!input.modelImportHasLod2);
|
||||||
|
if (modelImportLod2GenBtn != null) modelImportLod2GenBtn.setDisable(!input.modelImportHasLod1 || input.modelImportHasLod2);
|
||||||
|
|
||||||
// Modell-Import/Editor: LOD-Status
|
// Modell-Import/Editor: LOD-Status
|
||||||
String lodStatus = input.modelLodGenStatus;
|
String lodStatus = input.modelLodGenStatus;
|
||||||
@@ -6037,6 +6042,10 @@ public class EditorApp extends Application {
|
|||||||
receiveCB.setSelected(meta.receiveShadow());
|
receiveCB.setSelected(meta.receiveShadow());
|
||||||
for (CheckBox cb : new CheckBox[]{solidCB, castCB, receiveCB})
|
for (CheckBox cb : new CheckBox[]{solidCB, castCB, receiveCB})
|
||||||
cb.setStyle("-fx-text-fill:#ccc;");
|
cb.setStyle("-fx-text-fill:#ccc;");
|
||||||
|
if (relPath.startsWith("Models/trees/")) {
|
||||||
|
solidCB.setSelected(false);
|
||||||
|
solidCB.setDisable(true);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Zufällige Skalierung beim Multi-Place ─────────────────────────────
|
// ── Zufällige Skalierung beim Multi-Place ─────────────────────────────
|
||||||
Label rndTitle = new Label("Zufallsskalierung (Multi-Place):");
|
Label rndTitle = new Label("Zufallsskalierung (Multi-Place):");
|
||||||
@@ -6073,16 +6082,23 @@ public class EditorApp extends Application {
|
|||||||
lod1ClearBtn.setStyle("-fx-text-fill:#c0392b;");
|
lod1ClearBtn.setStyle("-fx-text-fill:#c0392b;");
|
||||||
lod1ClearBtn.setOnAction(e -> {
|
lod1ClearBtn.setOnAction(e -> {
|
||||||
if (input.modelEditorHasEmbeddedLod1) {
|
if (input.modelEditorHasEmbeddedLod1) {
|
||||||
|
// ModelEditorState.removeEmbeddedLod(1) kaskadiert automatisch LOD2
|
||||||
input.modelEditorRemoveEmbeddedLod = 1;
|
input.modelEditorRemoveEmbeddedLod = 1;
|
||||||
}
|
}
|
||||||
if (input.modelImportHasLod1) {
|
if (input.modelImportHasLod1) {
|
||||||
|
// ModelImportState.clearLod==1 kaskadiert automatisch LOD2
|
||||||
input.modelImportClearLodLevel = 1;
|
input.modelImportClearLodLevel = 1;
|
||||||
if (modelEditorLod1StatusLabel != null) modelEditorLod1StatusLabel.setText("–");
|
if (modelEditorLod1StatusLabel != null) modelEditorLod1StatusLabel.setText("–");
|
||||||
|
if (modelEditorLod2StatusLabel != null) modelEditorLod2StatusLabel.setText("–");
|
||||||
}
|
}
|
||||||
modelEditorLod1Path = "";
|
modelEditorLod1Path = "";
|
||||||
input.modelEditorLod1Path = "";
|
input.modelEditorLod1Path = "";
|
||||||
modelEditorLod1Label.setText("(kein Pfad)");
|
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.modelEditorLodPreview = 0;
|
||||||
input.modelEditorLodChanged = true;
|
input.modelEditorLodChanged = true;
|
||||||
}
|
}
|
||||||
@@ -6095,7 +6111,7 @@ public class EditorApp extends Application {
|
|||||||
modelEditorLod2GenBtn = new Button("LOD 2 generieren");
|
modelEditorLod2GenBtn = new Button("LOD 2 generieren");
|
||||||
modelEditorLod2GenBtn.setMaxWidth(Double.MAX_VALUE);
|
modelEditorLod2GenBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
boolean lod2Exists = input.modelEditorHasEmbeddedLod2 || !modelEditorLod2Path.isBlank() || input.modelImportHasLod2;
|
boolean lod2Exists = input.modelEditorHasEmbeddedLod2 || !modelEditorLod2Path.isBlank() || input.modelImportHasLod2;
|
||||||
modelEditorLod2GenBtn.setDisable(lod2Exists);
|
modelEditorLod2GenBtn.setDisable(!lod1Exists || lod2Exists);
|
||||||
modelEditorLod2GenBtn.setOnAction(e -> showLodGenDialog(2, 85));
|
modelEditorLod2GenBtn.setOnAction(e -> showLodGenDialog(2, 85));
|
||||||
|
|
||||||
modelEditorLod2StatusLabel = new Label("–");
|
modelEditorLod2StatusLabel = new Label("–");
|
||||||
@@ -6504,6 +6520,7 @@ public class EditorApp extends Application {
|
|||||||
modelImportLod2StatusLabel = null;
|
modelImportLod2StatusLabel = null;
|
||||||
modelImportLod1ClearBtn = null;
|
modelImportLod1ClearBtn = null;
|
||||||
modelImportLod2ClearBtn = null;
|
modelImportLod2ClearBtn = null;
|
||||||
|
modelImportLod2GenBtn = null;
|
||||||
modelImportExportStatusLabel = null;
|
modelImportExportStatusLabel = null;
|
||||||
input.modelImportHasLod1 = false;
|
input.modelImportHasLod1 = false;
|
||||||
input.modelImportHasLod2 = false;
|
input.modelImportHasLod2 = false;
|
||||||
@@ -6731,8 +6748,10 @@ public class EditorApp extends Application {
|
|||||||
modelImportLod1ClearBtn.setMaxWidth(Double.MAX_VALUE);
|
modelImportLod1ClearBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
modelImportLod1ClearBtn.setDisable(true);
|
modelImportLod1ClearBtn.setDisable(true);
|
||||||
modelImportLod1ClearBtn.setOnAction(e -> {
|
modelImportLod1ClearBtn.setOnAction(e -> {
|
||||||
|
// ModelImportState.clearLod==1 kaskadiert automatisch LOD2
|
||||||
input.modelImportClearLodLevel = 1;
|
input.modelImportClearLodLevel = 1;
|
||||||
modelImportLod1StatusLabel.setText("(nicht gesetzt)");
|
modelImportLod1StatusLabel.setText("(nicht gesetzt)");
|
||||||
|
modelImportLod2StatusLabel.setText("(nicht gesetzt)");
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── LOD 2 ─────────────────────────────────────────────────────────────
|
// ── LOD 2 ─────────────────────────────────────────────────────────────
|
||||||
@@ -6748,7 +6767,8 @@ public class EditorApp extends Application {
|
|||||||
lod2FileRB.setToggleGroup(lod2Group);
|
lod2FileRB.setToggleGroup(lod2Group);
|
||||||
lod2MeshRB.setSelected(true);
|
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.setMaxWidth(Double.MAX_VALUE);
|
||||||
lod2GenBtn.setOnAction(e -> {
|
lod2GenBtn.setOnAction(e -> {
|
||||||
if (lod2MeshRB.isSelected()) {
|
if (lod2MeshRB.isSelected()) {
|
||||||
|
|||||||
@@ -684,8 +684,13 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
|
|
||||||
if (toRemove.getParent() != null) toRemove.getParent().detachChild(toRemove);
|
if (toRemove.getParent() != null) toRemove.getParent().detachChild(toRemove);
|
||||||
embeddedLodSpatials[lodIdx] = null;
|
embeddedLodSpatials[lodIdx] = null;
|
||||||
if (lodIdx == 1) input.modelEditorHasEmbeddedLod1 = false;
|
if (lodIdx == 1) {
|
||||||
else if (lodIdx == 2) input.modelEditorHasEmbeddedLod2 = false;
|
input.modelEditorHasEmbeddedLod1 = false;
|
||||||
|
// LOD2 setzt LOD1 voraus — Kaskade: LOD2 ebenfalls entfernen
|
||||||
|
removeEmbeddedLod(2);
|
||||||
|
} else if (lodIdx == 2) {
|
||||||
|
input.modelEditorHasEmbeddedLod2 = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (input.modelEditorLodPreview == lodIdx) {
|
if (input.modelEditorLodPreview == lodIdx) {
|
||||||
input.modelEditorLodPreview = 0;
|
input.modelEditorLodPreview = 0;
|
||||||
@@ -961,7 +966,8 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
int i = 0;
|
int i = 0;
|
||||||
while (i < s.getNumControls()) {
|
while (i < s.getNumControls()) {
|
||||||
com.jme3.scene.control.Control c = s.getControl(i);
|
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); }
|
else { s.removeControl(c); }
|
||||||
}
|
}
|
||||||
if (s instanceof Node n) n.getChildren().forEach(ModelEditorState::stripControls);
|
if (s instanceof Node n) n.getChildren().forEach(ModelEditorState::stripControls);
|
||||||
@@ -1147,9 +1153,12 @@ public class ModelEditorState extends BaseAppState {
|
|||||||
log.info("[ModelEditor] LOD2 eingebettet als '{}'", l2.getName());
|
log.info("[ModelEditor] LOD2 eingebettet als '{}'", l2.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
// HD-Modell (Index 0) immer als sichtbar speichern – unabhängig vom aktuellen Preview-Zustand
|
// HD-Modell (Index 0): sichtbar speichern und als "lod0" kennzeichnen
|
||||||
|
// damit WorldObjectsState Fall A (eingebettetes LOD) korrekt erkennt.
|
||||||
if (!rootNode.getChildren().isEmpty()) {
|
if (!rootNode.getChildren().isEmpty()) {
|
||||||
rootNode.getChild(0).setCullHint(Spatial.CullHint.Inherit);
|
Spatial hd = rootNode.getChild(0);
|
||||||
|
hd.setName("lod0");
|
||||||
|
hd.setCullHint(Spatial.CullHint.Inherit);
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("[ModelEditor] rootNode '{}' hat vor dem Speichern {} Kinder:",
|
log.info("[ModelEditor] rootNode '{}' hat vor dem Speichern {} Kinder:",
|
||||||
|
|||||||
@@ -106,10 +106,14 @@ public class ModelImportState extends BaseAppState {
|
|||||||
input.modelImportHasLod1 = false;
|
input.modelImportHasLod1 = false;
|
||||||
input.modelImportLod1PreviewNode.set(null);
|
input.modelImportLod1PreviewNode.set(null);
|
||||||
input.modelLodGenStatus = "LOD1: (entfernt)";
|
input.modelLodGenStatus = "LOD1: (entfernt)";
|
||||||
if (input.modelEditorLodPreview == 1) {
|
if (input.modelEditorLodPreview == 1 || input.modelEditorLodPreview == 2) {
|
||||||
input.modelEditorLodPreview = 0;
|
input.modelEditorLodPreview = 0;
|
||||||
input.modelEditorLodChanged = true;
|
input.modelEditorLodChanged = true;
|
||||||
}
|
}
|
||||||
|
// LOD2 setzt LOD1 voraus — Kaskade
|
||||||
|
lod2Node = null;
|
||||||
|
input.modelImportHasLod2 = false;
|
||||||
|
input.modelImportLod2PreviewNode.set(null);
|
||||||
} else if (clearLod == 2) {
|
} else if (clearLod == 2) {
|
||||||
lod2Node = null;
|
lod2Node = null;
|
||||||
input.modelImportHasLod2 = false;
|
input.modelImportHasLod2 = false;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import com.jme3.renderer.ViewPort;
|
|||||||
import com.jme3.renderer.queue.RenderQueue;
|
import com.jme3.renderer.queue.RenderQueue;
|
||||||
import com.jme3.scene.*;
|
import com.jme3.scene.*;
|
||||||
import com.jme3.scene.control.AbstractControl;
|
import com.jme3.scene.control.AbstractControl;
|
||||||
|
import com.jme3.scene.control.BillboardControl;
|
||||||
import com.jme3.texture.FrameBuffer;
|
import com.jme3.texture.FrameBuffer;
|
||||||
import com.jme3.texture.Image;
|
import com.jme3.texture.Image;
|
||||||
import com.jme3.texture.Texture2D;
|
import com.jme3.texture.Texture2D;
|
||||||
@@ -87,17 +88,16 @@ public final class ImpostorUtil {
|
|||||||
applyTreeLighting(capNode, toLight);
|
applyTreeLighting(capNode, toLight);
|
||||||
|
|
||||||
Node scene = new Node("impCapScene");
|
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,
|
scene.addLight(new DirectionalLight(fromLight,
|
||||||
new ColorRGBA(1.2f, 1.1f, 0.9f, 1f)));
|
new ColorRGBA(0.70f, 0.65f, 0.52f, 1f)));
|
||||||
// Fülllicht (kühler, von gegenüberliegender Seite)
|
|
||||||
scene.addLight(new DirectionalLight(toLight.mult(-0.7f).addLocal(0f, 0.3f, 0f).normalizeLocal(),
|
scene.addLight(new DirectionalLight(toLight.mult(-0.7f).addLocal(0f, 0.3f, 0f).normalizeLocal(),
|
||||||
new ColorRGBA(0.35f, 0.35f, 0.45f, 1f)));
|
new ColorRGBA(0.16f, 0.16f, 0.22f, 1f)));
|
||||||
// Oberlicht (Himmelslicht von oben)
|
|
||||||
scene.addLight(new DirectionalLight(new Vector3f(0f, -1f, 0f),
|
scene.addLight(new DirectionalLight(new Vector3f(0f, -1f, 0f),
|
||||||
new ColorRGBA(0.25f, 0.30f, 0.35f, 1f)));
|
new ColorRGBA(0.12f, 0.14f, 0.17f, 1f)));
|
||||||
// Starkes Ambient: verhindert vollständig schwarze Stellen bei Lighting.j3md
|
scene.addLight(new AmbientLight(new ColorRGBA(0.30f, 0.30f, 0.32f, 1f)));
|
||||||
scene.addLight(new AmbientLight(new ColorRGBA(0.55f, 0.55f, 0.57f, 1f)));
|
|
||||||
scene.attachChild(capNode);
|
scene.attachChild(capNode);
|
||||||
|
|
||||||
ViewPort vp = app.getRenderManager().createPostView("impCap_" + System.nanoTime(), capCam);
|
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");
|
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
if (tex != null) mat.setTexture("ColorMap", tex);
|
if (tex != null) mat.setTexture("ColorMap", tex);
|
||||||
else mat.setColor("Color", new ColorRGBA(0.18f, 0.5f, 0.1f, 0.9f));
|
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);
|
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Back);
|
||||||
|
|
||||||
Node n = new Node("lod2");
|
Node n = new Node("lod2");
|
||||||
for (int d = 0; d < DIRS; d++) {
|
for (int d = 0; d < DIRS; d++) {
|
||||||
float angle = d * FastMath.TWO_PI / DIRS;
|
|
||||||
float uMin = (float) d / DIRS;
|
float uMin = (float) d / DIRS;
|
||||||
float uMax = (float)(d + 1) / 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;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,10 +368,9 @@ public final class ImpostorUtil {
|
|||||||
float angle = (float) Math.atan2(dx, dz); // −π … +π
|
float angle = (float) Math.atan2(dx, dz); // −π … +π
|
||||||
float t = angle * dirs / FastMath.TWO_PI;
|
float t = angle * dirs / FastMath.TWO_PI;
|
||||||
int best = ((int) Math.round(t) % dirs + dirs) % dirs;
|
int best = ((int) Math.round(t) % dirs + dirs) % dirs;
|
||||||
int opp = (best + dirs / 2) % dirs; // gegenüberliegendes Quad
|
|
||||||
|
|
||||||
for (int i = 0; i < dirs; i++) {
|
for (int i = 0; i < dirs; i++) {
|
||||||
quads.get(i).setCullHint(i == best || i == opp
|
quads.get(i).setCullHint(i == best
|
||||||
? Spatial.CullHint.Inherit
|
? Spatial.CullHint.Inherit
|
||||||
: Spatial.CullHint.Always);
|
: Spatial.CullHint.Always);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.jme3.scene.Mesh;
|
|||||||
import com.jme3.scene.Node;
|
import com.jme3.scene.Node;
|
||||||
import com.jme3.scene.Spatial;
|
import com.jme3.scene.Spatial;
|
||||||
import com.jme3.scene.VertexBuffer;
|
import com.jme3.scene.VertexBuffer;
|
||||||
|
import com.jme3.scene.control.BillboardControl;
|
||||||
|
|
||||||
import java.nio.FloatBuffer;
|
import java.nio.FloatBuffer;
|
||||||
|
|
||||||
@@ -17,7 +18,13 @@ public final class ModelExportUtil {
|
|||||||
private ModelExportUtil() {}
|
private ModelExportUtil() {}
|
||||||
|
|
||||||
public static void stripControls(Spatial s) {
|
public static void stripControls(Spatial s) {
|
||||||
while (s.getNumControls() > 0) s.removeControl(s.getControl(0));
|
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) {
|
if (s instanceof Node n) {
|
||||||
n.getChildren().forEach(ModelExportUtil::stripControls);
|
n.getChildren().forEach(ModelExportUtil::stripControls);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.jme3.system.lwjgl.LwjglWindow;
|
|||||||
import de.blight.game.BlightGame;
|
import de.blight.game.BlightGame;
|
||||||
import de.blight.game.state.TerrainChunkState;
|
import de.blight.game.state.TerrainChunkState;
|
||||||
import de.blight.game.state.WeatherState;
|
import de.blight.game.state.WeatherState;
|
||||||
|
import de.blight.game.state.WorldObjectsState;
|
||||||
import de.blight.lang.TextResolver;
|
import de.blight.lang.TextResolver;
|
||||||
import org.lwjgl.glfw.GLFW;
|
import org.lwjgl.glfw.GLFW;
|
||||||
import org.lwjgl.glfw.GLFWVidMode;
|
import org.lwjgl.glfw.GLFWVidMode;
|
||||||
@@ -205,6 +206,9 @@ public class GraphicsScreen extends MenuScreen {
|
|||||||
TerrainChunkState tcs = getApplication().getStateManager().getState(TerrainChunkState.class);
|
TerrainChunkState tcs = getApplication().getStateManager().getState(TerrainChunkState.class);
|
||||||
if (tcs != null) tcs.setLodRanges(live.viewDistance.lod0Range, live.viewDistance.lod1Range);
|
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();
|
close();
|
||||||
|
|
||||||
if (needsRestart) {
|
if (needsRestart) {
|
||||||
|
|||||||
@@ -39,9 +39,9 @@ public class GraphicsSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public enum ViewDistance {
|
public enum ViewDistance {
|
||||||
SHORT (0.55f, 0, 2),
|
SHORT (1.00f, 1, 3),
|
||||||
NORMAL(1.00f, 1, 3),
|
NORMAL(1.60f, 2, 5),
|
||||||
FAR (1.60f, 2, 5);
|
FAR (2.50f, 3, 7);
|
||||||
|
|
||||||
public final float fogFactor;
|
public final float fogFactor;
|
||||||
public final int lod0Range;
|
public final int lod0Range;
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ public class WorldScene extends BaseAppState {
|
|||||||
BlightGame.status("Lade Welt-Objekte...");
|
BlightGame.status("Lade Welt-Objekte...");
|
||||||
app.getStateManager().attach(new RiverState());
|
app.getStateManager().attach(new RiverState());
|
||||||
WorldObjectsState worldObjects = new WorldObjectsState();
|
WorldObjectsState worldObjects = new WorldObjectsState();
|
||||||
worldObjects.setLodFactor(graphicsSettings.viewDistance.fogFactor);
|
worldObjects.setLodFactor((float) graphicsSettings.viewDistance.lod0Range);
|
||||||
app.getStateManager().attach(worldObjects);
|
app.getStateManager().attach(worldObjects);
|
||||||
app.getStateManager().attach(new WorldLightState(sharedFPP));
|
app.getStateManager().attach(new WorldLightState(sharedFPP));
|
||||||
app.getStateManager().attach(new StoneWorldState());
|
app.getStateManager().attach(new StoneWorldState());
|
||||||
|
|||||||
@@ -43,10 +43,9 @@ public class ImpostorViewControl extends AbstractControl {
|
|||||||
float angle = (float) Math.atan2(dx, dz);
|
float angle = (float) Math.atan2(dx, dz);
|
||||||
float t = angle * dirs / FastMath.TWO_PI;
|
float t = angle * dirs / FastMath.TWO_PI;
|
||||||
int best = ((int) Math.round(t) % dirs + dirs) % dirs;
|
int best = ((int) Math.round(t) % dirs + dirs) % dirs;
|
||||||
int opp = (best + dirs / 2) % dirs;
|
|
||||||
|
|
||||||
for (int i = 0; i < dirs; i++) {
|
for (int i = 0; i < dirs; i++) {
|
||||||
quads.get(i).setCullHint(i == best || i == opp
|
quads.get(i).setCullHint(i == best
|
||||||
? Spatial.CullHint.Inherit
|
? Spatial.CullHint.Inherit
|
||||||
: Spatial.CullHint.Always);
|
: Spatial.CullHint.Always);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,8 +34,7 @@ public class ModelLodControl extends AbstractControl {
|
|||||||
private final Camera cam;
|
private final Camera cam;
|
||||||
private final float lod1DistSq;
|
private final float lod1DistSq;
|
||||||
private final float lod2DistSq;
|
private final float lod2DistSq;
|
||||||
private final float cullDistSq;
|
private int currentSlot = 0; // 0=lod0, 1=lod1, 2=lod2, -1=ausgeblendet
|
||||||
private int currentSlot = 0; // 0=lod0, 1=lod1, 2=lod2, -1=culled
|
|
||||||
|
|
||||||
// ── Embedded-LOD-Modus ────────────────────────────────────────────────────
|
// ── Embedded-LOD-Modus ────────────────────────────────────────────────────
|
||||||
private final Spatial embLod0;
|
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
|
* Embedded-LOD-Modus: Die j3o-Nodes lod0/lod1/lod2 existieren bereits als
|
||||||
* Child-Nodes im Spatial, an dem dieser Control befestigt wird.
|
* 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,
|
public ModelLodControl(Camera cam,
|
||||||
Spatial lod0, Spatial lod1, Spatial lod2,
|
Spatial lod0, Spatial lod1, Spatial lod2,
|
||||||
float lod1Distance, float lod2Distance, float cullDistance) {
|
float lod1Distance, float lod2Distance) {
|
||||||
this.cam = cam;
|
this.cam = cam;
|
||||||
this.embLod0 = lod0;
|
this.embLod0 = lod0;
|
||||||
this.embLod1 = lod1;
|
this.embLod1 = lod1;
|
||||||
this.embLod2 = lod2;
|
this.embLod2 = lod2;
|
||||||
this.lod1DistSq = lod1 != null ? lod1Distance * lod1Distance : Float.MAX_VALUE;
|
this.lod1DistSq = lod1Distance > 0f ? lod1Distance * lod1Distance : Float.MAX_VALUE;
|
||||||
this.lod2DistSq = lod2 != null ? lod2Distance * lod2Distance : Float.MAX_VALUE;
|
this.lod2DistSq = lod2Distance > 0f ? lod2Distance * lod2Distance : Float.MAX_VALUE;
|
||||||
this.cullDistSq = cullDistance * cullDistance;
|
|
||||||
this.assets = null;
|
this.assets = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,14 +74,13 @@ public class ModelLodControl extends AbstractControl {
|
|||||||
*/
|
*/
|
||||||
public ModelLodControl(AssetManager assets, Camera cam,
|
public ModelLodControl(AssetManager assets, Camera cam,
|
||||||
String lod1Path, String lod2Path,
|
String lod1Path, String lod2Path,
|
||||||
float lod1Distance, float lod2Distance, float cullDistance) {
|
float lod1Distance, float lod2Distance) {
|
||||||
this.assets = assets;
|
this.assets = assets;
|
||||||
this.cam = cam;
|
this.cam = cam;
|
||||||
this.lod1Path = (lod1Path != null && !lod1Path.isBlank()) ? lod1Path : null;
|
this.lod1Path = (lod1Path != null && !lod1Path.isBlank()) ? lod1Path : null;
|
||||||
this.lod2Path = (lod2Path != null && !lod2Path.isBlank()) ? lod2Path : null;
|
this.lod2Path = (lod2Path != null && !lod2Path.isBlank()) ? lod2Path : null;
|
||||||
this.lod1DistSq = lod1Distance * lod1Distance;
|
this.lod1DistSq = lod1Distance > 0f ? lod1Distance * lod1Distance : Float.MAX_VALUE;
|
||||||
this.lod2DistSq = lod2Distance * lod2Distance;
|
this.lod2DistSq = lod2Distance > 0f ? lod2Distance * lod2Distance : Float.MAX_VALUE;
|
||||||
this.cullDistSq = cullDistance * cullDistance;
|
|
||||||
this.embLod0 = null;
|
this.embLod0 = null;
|
||||||
this.embLod1 = null;
|
this.embLod1 = null;
|
||||||
this.embLod2 = null;
|
this.embLod2 = null;
|
||||||
@@ -115,21 +112,25 @@ public class ModelLodControl extends AbstractControl {
|
|||||||
|
|
||||||
boolean embedded = embLod0 != null;
|
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;
|
int targetSlot;
|
||||||
if (distSq >= cullDistSq) {
|
if (distSq >= lod2DistSq) {
|
||||||
targetSlot = -1;
|
targetSlot = hasLod2 ? 2 : -1;
|
||||||
} else if (embedded) {
|
} else if (distSq >= lod1DistSq) {
|
||||||
if (embLod2 != null && distSq >= lod2DistSq) targetSlot = 2;
|
if (hasLod1) targetSlot = 1;
|
||||||
else if (embLod1 != null && distSq >= lod1DistSq) targetSlot = 1;
|
else if (hasLod2) targetSlot = 2; // kein LOD1, aber LOD2 vorhanden → Impostor früher
|
||||||
else targetSlot = 0;
|
else targetSlot = -1; // weder LOD1 noch LOD2 → ausblenden
|
||||||
} else {
|
} else {
|
||||||
if (lod2Path != null && distSq >= lod2DistSq) targetSlot = 2;
|
targetSlot = 0;
|
||||||
else if (lod1Path != null && distSq >= lod1DistSq) targetSlot = 1;
|
|
||||||
else targetSlot = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetSlot == currentSlot) return;
|
if (targetSlot == currentSlot) return;
|
||||||
log.debug("[LOD] {} Slot {} → {} (dist={}m)",
|
log.info("[LOD] {} Slot {} → {} (dist={}m)",
|
||||||
spatial.getName(), currentSlot, targetSlot,
|
spatial.getName(), currentSlot, targetSlot,
|
||||||
String.format("%.1f", Math.sqrt(distSq)));
|
String.format("%.1f", Math.sqrt(distSq)));
|
||||||
currentSlot = targetSlot;
|
currentSlot = targetSlot;
|
||||||
|
|||||||
@@ -226,9 +226,9 @@ public class WorldItemsState extends BaseAppState {
|
|||||||
float hMargin = cam.getWidth() * 0.10f;
|
float hMargin = cam.getWidth() * 0.10f;
|
||||||
float screenH = cam.getHeight();
|
float screenH = cam.getHeight();
|
||||||
|
|
||||||
boolean doLog = hoverLogTimer >= 1f;
|
boolean doLog = hoverLogTimer >= 1f && !items.isEmpty();
|
||||||
|
if (hoverLogTimer >= 1f) hoverLogTimer = 0f;
|
||||||
if (doLog) {
|
if (doLog) {
|
||||||
hoverLogTimer = 0f;
|
|
||||||
log.info("[Hover] items={} screenW={} screenH={} centerX={} hMargin={}",
|
log.info("[Hover] items={} screenW={} screenH={} centerX={} hMargin={}",
|
||||||
items.size(), cam.getWidth(), cam.getHeight(), centerX, hMargin);
|
items.size(), cam.getWidth(), cam.getHeight(), centerX, hMargin);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,18 @@ import com.jme3.app.state.BaseAppState;
|
|||||||
import com.jme3.asset.AssetManager;
|
import com.jme3.asset.AssetManager;
|
||||||
import com.jme3.asset.plugins.FileLocator;
|
import com.jme3.asset.plugins.FileLocator;
|
||||||
import com.jme3.bullet.BulletAppState;
|
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.control.RigidBodyControl;
|
||||||
import com.jme3.bullet.util.CollisionShapeFactory;
|
import com.jme3.bullet.util.CollisionShapeFactory;
|
||||||
|
import com.jme3.bounding.BoundingBox;
|
||||||
import com.jme3.material.Material;
|
import com.jme3.material.Material;
|
||||||
import com.jme3.material.MatParam;
|
import com.jme3.material.MatParam;
|
||||||
import com.jme3.material.RenderState;
|
import com.jme3.material.RenderState;
|
||||||
import com.jme3.math.*;
|
import com.jme3.math.*;
|
||||||
import com.jme3.renderer.queue.RenderQueue;
|
import com.jme3.renderer.queue.RenderQueue;
|
||||||
import com.jme3.scene.*;
|
import com.jme3.scene.*;
|
||||||
|
import com.jme3.scene.control.BillboardControl;
|
||||||
import com.jme3.scene.shape.*;
|
import com.jme3.scene.shape.*;
|
||||||
import com.jme3.texture.Texture;
|
import com.jme3.texture.Texture;
|
||||||
import de.blight.common.PlacedModel;
|
import de.blight.common.PlacedModel;
|
||||||
@@ -37,6 +41,10 @@ public class WorldObjectsState extends BaseAppState {
|
|||||||
private final List<Material> sceneLitMaterials = new ArrayList<>();
|
private final List<Material> sceneLitMaterials = new ArrayList<>();
|
||||||
private final List<Material> windMaterials = new ArrayList<>();
|
private final List<Material> windMaterials = new ArrayList<>();
|
||||||
private final List<Material> pbrMaterials = new ArrayList<>();
|
private final List<Material> pbrMaterials = new ArrayList<>();
|
||||||
|
/** 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 debugTimer = 0f;
|
||||||
private float lodFactor = 1.0f;
|
private float lodFactor = 1.0f;
|
||||||
|
|
||||||
@@ -92,6 +100,30 @@ public class WorldObjectsState extends BaseAppState {
|
|||||||
else shadowMode = RenderQueue.ShadowMode.Off;
|
else shadowMode = RenderQueue.ShadowMode.Off;
|
||||||
s.setShadowMode(shadowMode);
|
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
|
// Physik-Kollision für solide Objekte
|
||||||
if (m.solid() && bulletAppState != null) {
|
if (m.solid() && bulletAppState != null) {
|
||||||
try {
|
try {
|
||||||
@@ -142,6 +174,17 @@ public class WorldObjectsState extends BaseAppState {
|
|||||||
for (Material mat : pbrMaterials) {
|
for (Material mat : pbrMaterials) {
|
||||||
mat.setColor("Emissive", new ColorRGBA(ac.r, ac.g, ac.b, 1f));
|
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;
|
debugTimer += tpf;
|
||||||
if (debugTimer >= 5f) {
|
if (debugTimer >= 5f) {
|
||||||
debugTimer = 0f;
|
debugTimer = 0f;
|
||||||
@@ -193,27 +236,54 @@ public class WorldObjectsState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
spatial.setName("obj_" + path);
|
spatial.setName("obj_" + path);
|
||||||
|
|
||||||
// LOD / Distance-Culling
|
// LOD — kein cullDistance mehr; Objekte ohne LOD-Stufe werden ab der jeweiligen Distanz ausgeblendet
|
||||||
if (m.cullDistance() > 0f) {
|
if (m.lod1Distance() > 0f || m.lod2Distance() > 0f) {
|
||||||
if (spatial instanceof Node treeNode) {
|
if (spatial instanceof Node treeNode) {
|
||||||
Spatial lod0 = treeNode.getChild("lod0");
|
Spatial lod0 = treeNode.getChild("lod0");
|
||||||
Spatial lod1 = treeNode.getChild("lod1");
|
Spatial lod1 = treeNode.getChild("lod1");
|
||||||
Spatial lod2 = treeNode.getChild("lod2");
|
Spatial lod2 = treeNode.getChild("lod2");
|
||||||
|
|
||||||
// Fall A: j3o hat explizite lod0/lod1/lod2 Child-Nodes.
|
// 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) {
|
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);
|
lod0.setCullHint(Spatial.CullHint.Inherit);
|
||||||
if (lod1 != null) lod1.setCullHint(Spatial.CullHint.Always);
|
if (lod1 != null) lod1.setCullHint(Spatial.CullHint.Always);
|
||||||
if (lod2 != null) lod2.setCullHint(Spatial.CullHint.Always);
|
if (lod2 != null) {
|
||||||
|
lod2.setCullHint(Spatial.CullHint.Always);
|
||||||
|
if (lod2 instanceof Node lod2Node && !lod2Node.getChildren().isEmpty()
|
||||||
|
&& isImpostorNode(lod2Node)) {
|
||||||
|
initImpostor(lod2Node);
|
||||||
|
}
|
||||||
|
}
|
||||||
treeNode.addControl(new ModelLodControl(
|
treeNode.addControl(new ModelLodControl(
|
||||||
app.getCamera(), lod0, lod1, lod2,
|
app.getCamera(), lod0, lod1, lod2,
|
||||||
m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor, m.cullDistance() * lodFactor));
|
m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor));
|
||||||
return treeNode;
|
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.
|
// 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.
|
// lod2 aus dem Root ausgliedern, damit beide unabhängig ein/ausgeblendet werden können.
|
||||||
if (lod2 != null) {
|
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);
|
treeNode.detachChild(lod2);
|
||||||
Node lodRoot = new Node("lodRoot_" + path);
|
Node lodRoot = new Node("lodRoot_" + path);
|
||||||
lodRoot.attachChild(treeNode); // index 0 = lod0 (volle Qualität)
|
lodRoot.attachChild(treeNode); // index 0 = lod0 (volle Qualität)
|
||||||
@@ -232,11 +302,14 @@ public class WorldObjectsState extends BaseAppState {
|
|||||||
lod2.setLocalTranslation(0f, -groundY, 0f);
|
lod2.setLocalTranslation(0f, -groundY, 0f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Impostor: wählt pro Frame das kamerawinkelrichtige Quad-Paar.
|
// Impostor: wählt pro Frame das kamerawinkelrichtige Quad.
|
||||||
lod2.addControl(new ImpostorViewControl(app.getCamera()));
|
if (lod2 instanceof Node lod2Node && !lod2Node.getChildren().isEmpty()
|
||||||
|
&& isImpostorNode(lod2Node)) {
|
||||||
|
initImpostor(lod2Node);
|
||||||
|
}
|
||||||
lodRoot.addControl(new ModelLodControl(
|
lodRoot.addControl(new ModelLodControl(
|
||||||
app.getCamera(), treeNode, null, lod2,
|
app.getCamera(), treeNode, null, lod2,
|
||||||
m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor, m.cullDistance() * lodFactor));
|
m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor));
|
||||||
return lodRoot;
|
return lodRoot;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -247,7 +320,7 @@ public class WorldObjectsState extends BaseAppState {
|
|||||||
ModelLodControl ctrl = new ModelLodControl(
|
ModelLodControl ctrl = new ModelLodControl(
|
||||||
assets, app.getCamera(),
|
assets, app.getCamera(),
|
||||||
m.lod1Path(), m.lod2Path(),
|
m.lod1Path(), m.lod2Path(),
|
||||||
m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor, m.cullDistance() * lodFactor);
|
m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor);
|
||||||
ctrl.setLodLoadCallback(lod -> {
|
ctrl.setLodLoadCallback(lod -> {
|
||||||
convertUnshadedToPbr(lod);
|
convertUnshadedToPbr(lod);
|
||||||
collectSceneLitMaterials(lod);
|
collectSceneLitMaterials(lod);
|
||||||
@@ -259,6 +332,83 @@ public class WorldObjectsState extends BaseAppState {
|
|||||||
return spatial;
|
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) {
|
private Spatial createPrimitive(String type) {
|
||||||
return switch (type) {
|
return switch (type) {
|
||||||
case "sphere" -> new Geometry("sphere", new Sphere(16, 16, 1f));
|
case "sphere" -> new Geometry("sphere", new Sphere(16, 16, 1f));
|
||||||
@@ -319,6 +469,11 @@ public class WorldObjectsState extends BaseAppState {
|
|||||||
mat.setVector3("AmbientColor", new Vector3f(0.04f, 0.05f, 0.08f));
|
mat.setVector3("AmbientColor", new Vector3f(0.04f, 0.05f, 0.08f));
|
||||||
mat.setVector3("LightDir", new Vector3f(0f, -1f, 0f));
|
mat.setVector3("LightDir", new Vector3f(0f, -1f, 0f));
|
||||||
sceneLitMaterials.add(mat);
|
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)) {
|
} else if ("Fern".equals(name)) {
|
||||||
windMaterials.add(mat);
|
windMaterials.add(mat);
|
||||||
} else if ("PBRLighting".equals(name)) {
|
} else if ("PBRLighting".equals(name)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user