Sichtweite und Nebel hinzugefügt

This commit is contained in:
2026-07-17 22:20:44 +02:00
parent 5ed7b4017c
commit 0b1cb4d0e9
16 changed files with 540 additions and 140 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,4 +6,21 @@ public class GraphicsSettings {
public boolean fullscreen = false; public boolean fullscreen = false;
public boolean vsync = false; public boolean vsync = false;
public int samples = 4; public int samples = 4;
public ViewDistance viewDistance = ViewDistance.NORMAL;
public enum ViewDistance {
SHORT (0.55f, 0, 2),
NORMAL(1.00f, 1, 3),
FAR (1.60f, 2, 5);
public final float fogFactor;
public final int lod0Range;
public final int lod1Range;
ViewDistance(float fogFactor, int lod0Range, int lod1Range) {
this.fogFactor = fogFactor;
this.lod0Range = lod0Range;
this.lod1Range = lod1Range;
}
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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