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)
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) {
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)
status("Initialisiere Welt...");
worldScene = new WorldScene(keyBindings);
worldScene = new WorldScene(keyBindings, graphicsSettings);
worldScene.setEnabled(false);
stateManager.attach(worldScene);
@@ -268,6 +268,13 @@ public class BlightGame extends SimpleApplication {
});
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) ──────────────────────────────────────────
inputManager.addMapping("DebugNoLight", new KeyTrigger(KeyInput.KEY_F8));
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.scene.Node;
import com.jme3.system.AppSettings;
import de.blight.game.state.TerrainChunkState;
import de.blight.game.state.WeatherState;
import de.blight.lang.TextResolver;
public class GraphicsScreen extends MenuScreen {
@@ -13,11 +15,12 @@ public class GraphicsScreen extends MenuScreen {
};
private static final int[] SAMPLES = {0, 2, 4, 8};
private static final int ROW_RES = 0;
private static final int ROW_FULL = 1;
private static final int ROW_VSYNC = 2;
private static final int ROW_AA = 3;
private static final int ROW_COUNT = 4;
private static final int ROW_RES = 0;
private static final int ROW_FULL = 1;
private static final int ROW_VSYNC = 2;
private static final int ROW_AA = 3;
private static final int ROW_VIEWDIST = 4;
private static final int ROW_COUNT = 5;
private final GraphicsSettings live;
private GraphicsSettings edit;
@@ -42,9 +45,10 @@ public class GraphicsScreen extends MenuScreen {
protected void onEnableExtras() {
edit = new GraphicsSettings();
edit.width = live.width; edit.height = live.height;
edit.fullscreen = live.fullscreen;
edit.vsync = live.vsync;
edit.samples = live.samples;
edit.fullscreen = live.fullscreen;
edit.vsync = live.vsync;
edit.samples = live.samples;
edit.viewDistance = live.viewDistance;
resIdx = 0;
for (int i = 0; i < RESOLUTIONS.length; i++) {
@@ -66,7 +70,8 @@ public class GraphicsScreen extends MenuScreen {
"menu.graphics.row.resolution",
"menu.graphics.row.fullscreen",
"menu.graphics.row.vsync",
"menu.graphics.row.aa"
"menu.graphics.row.aa",
"menu.graphics.row.viewdist"
};
float lblX = CONT_X + 30f;
@@ -110,11 +115,16 @@ public class GraphicsScreen extends MenuScreen {
private void refreshText(int row) {
String val = switch (row) {
case ROW_RES -> RESOLUTIONS[resIdx][0] + "x" + RESOLUTIONS[resIdx][1];
case ROW_FULL -> edit.fullscreen ? t("menu.graphics.val.on") : t("menu.graphics.val.off");
case ROW_VSYNC -> edit.vsync ? t("menu.graphics.val.on") : t("menu.graphics.val.off");
case ROW_AA -> SAMPLES[samplesIdx] == 0
? t("menu.graphics.val.off") : SAMPLES[samplesIdx] + "x MSAA";
case ROW_RES -> RESOLUTIONS[resIdx][0] + "x" + RESOLUTIONS[resIdx][1];
case ROW_FULL -> edit.fullscreen ? t("menu.graphics.val.on") : t("menu.graphics.val.off");
case ROW_VSYNC -> edit.vsync ? t("menu.graphics.val.on") : t("menu.graphics.val.off");
case ROW_AA -> SAMPLES[samplesIdx] == 0
? 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 -> "";
};
BitmapText vt = valTexts[row];
@@ -140,27 +150,47 @@ public class GraphicsScreen extends MenuScreen {
samplesIdx = (samplesIdx + dir + SAMPLES.length) % SAMPLES.length;
edit.samples = SAMPLES[samplesIdx];
break;
case ROW_VIEWDIST:
GraphicsSettings.ViewDistance[] vds = GraphicsSettings.ViewDistance.values();
edit.viewDistance = vds[(edit.viewDistance.ordinal() + dir + vds.length) % vds.length];
break;
}
refreshText(row);
}
private void applyAndSave() {
live.width = edit.width; live.height = edit.height;
live.fullscreen = edit.fullscreen;
live.vsync = edit.vsync;
live.samples = edit.samples;
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.vsync = edit.vsync;
live.samples = edit.samples;
live.viewDistance = edit.viewDistance;
GraphicsStore.save(live);
AppSettings s = app.getContext().getSettings();
s.setResolution(live.width, live.height);
s.setFullscreen(live.fullscreen);
s.setBitsPerPixel(32);
s.setVSync(live.vsync);
s.setSamples(live.samples);
app.setSettings(s);
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();
app.restart();
if (needsRestart) {
AppSettings s = app.getContext().getSettings();
s.setResolution(live.width, live.height);
s.setFullscreen(live.fullscreen);
s.setBitsPerPixel(32);
s.setVSync(live.vsync);
s.setSamples(live.samples);
app.setSettings(s);
app.restart();
}
}
private void close() {

View File

@@ -1,9 +1,26 @@
package de.blight.game.config;
public class GraphicsSettings {
public int width = 1280;
public int height = 720;
public boolean fullscreen = false;
public boolean vsync = false;
public int samples = 4;
public int width = 1280;
public int height = 720;
public boolean fullscreen = false;
public boolean vsync = false;
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.game.BlightGame;
import de.blight.game.animation.AnimationLibrary;
import de.blight.game.config.GraphicsSettings;
import de.blight.game.config.KeyBindings;
import de.blight.game.control.PlayerInputControl;
import de.blight.game.control.ThirdPersonCamera;
import com.jme3.post.FilterPostProcessor;
import com.jme3.post.filters.FogFilter;
import de.blight.game.post.BlightFogFilter;
import com.jme3.water.WaterFilter;
import de.blight.game.state.DynamicWaterFilter;
import de.blight.game.state.WaterInteractionState;
@@ -78,10 +79,12 @@ public class WorldScene extends BaseAppState {
private BulletAppState bulletAppState;
private MapData loadedMapData;
private FilterPostProcessor sharedFPP;
private BlightFogFilter fogFilter;
private TerrainChunkState terrainChunkState;
private Material terrainMaterial;
private final KeyBindings keyBindings;
private final KeyBindings keyBindings;
private final GraphicsSettings graphicsSettings;
private ThirdPersonCamera thirdPersonCam;
private PlayerInputControl playerInput;
private InventoryState inventoryState;
@@ -101,8 +104,9 @@ public class WorldScene extends BaseAppState {
private de.blight.game.state.DrownState drownState;
private WaterInteractionState waterInteractionState;
public WorldScene(KeyBindings keyBindings) {
this.keyBindings = keyBindings;
public WorldScene(KeyBindings keyBindings, GraphicsSettings graphicsSettings) {
this.keyBindings = keyBindings;
this.graphicsSettings = graphicsSettings;
}
public InventoryState getInventoryState() { return inventoryState; }
@@ -246,7 +250,9 @@ public class WorldScene extends BaseAppState {
BlightGame.status("Lade Welt-Objekte...");
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 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 onDisable() {}
@@ -712,13 +725,14 @@ public class WorldScene extends BaseAppState {
WeatherState weather = new WeatherState();
weather.setWaterFilter(waterFilter);
FogFilter fogFilter = new FogFilter();
fogFilter = new BlightFogFilter();
fogFilter.setFogColor(new ColorRGBA(0.75f, 0.80f, 0.88f, 1f));
fogFilter.setFogDensity(0.0f);
fogFilter.setFogDensity(0.40f);
fogFilter.setFogDistance(600f);
fpp.addFilter(fogFilter);
weather.setFogFilter(fogFilter);
weather.setViewDistanceFactor(graphicsSettings.viewDistance.fogFactor);
weather.setSkyControl(dayNight.getSkyControl());
app.getStateManager().attach(weather);
} catch (Exception e) {
@@ -775,6 +789,9 @@ public class WorldScene extends BaseAppState {
terrainMaterial = buildTerrainMaterial(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
terrainChunkState.loadChunkHeights();
app.getStateManager().attach(terrainChunkState);

View File

@@ -10,48 +10,59 @@ import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Quad;
import com.jme3.scene.VertexBuffer;
import com.jme3.util.BufferUtils;
/**
* Kompass unten links. Zeigt N/O/S/W-Labels, die um das Kompasszentrum
* rotieren wenn sich die Kamera dreht. Eine feste Markierung oben
* zeigt immer die aktuelle Blickrichtung an.
* Kompass unten links runde rotierende Rose, identisch mit dem Editor-Kompass.
*
* 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 {
// ── Layout ────────────────────────────────────────────────────────────────
private static final float SIZE = 76f; // Kompass-Durchmesser (Pixel)
private static final float RADIUS = 26f; // Label-Radius vom Mittelpunkt
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 float SIZE = 100f; // Canvas-Größe (px)
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 ColorRGBA COL_BG = new ColorRGBA(0.06f, 0.06f, 0.10f, 0.88f);
private static final ColorRGBA COL_BORDER = new ColorRGBA(0.30f, 0.30f, 0.44f, 1.00f);
private static final ColorRGBA COL_N = new ColorRGBA(0.90f, 0.20f, 0.20f, 1.00f);
private static final ColorRGBA COL_CARD = new ColorRGBA(0.75f, 0.75f, 0.78f, 1.00f);
private static final ColorRGBA COL_MARKER = new ColorRGBA(1.00f, 0.90f, 0.20f, 1.00f);
// ── Farben (matching EditorApp.drawCompass) ───────────────────────────────
private static final ColorRGBA COL_BG = new ColorRGBA(0.047f, 0.047f, 0.094f, 0.82f);
private static final ColorRGBA COL_BORDER = new ColorRGBA(0.392f, 0.392f, 0.627f, 0.85f);
private static final ColorRGBA COL_TICK_MJ = new ColorRGBA(0.627f, 0.627f, 0.784f, 0.65f);
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) ─────────────────
private static final String[] LABELS = {"N", "O", "S", "W"};
private static final float[] OFFSETS = {0f, FastMath.HALF_PI, FastMath.PI, -FastMath.HALF_PI};
// 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 float[] LABEL_DEG = {0f, 90f, 180f, 270f};
private SimpleApplication app;
private Camera cam;
private AssetManager assets;
private BitmapFont font;
private Node compassNode;
private float cx, cy; // Mittelpunkt (Bildschirmkoordinaten)
private SimpleApplication app;
private Camera cam;
private AssetManager assets;
private BitmapFont font;
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 ─────────────────────────────────────────────────────────────
@Override
protected void initialize(Application application) {
app = (SimpleApplication) application;
@@ -70,6 +81,7 @@ public class CompassHudState extends BaseAppState {
if (compassNode != null) {
app.getGuiNode().detachChild(compassNode);
compassNode = null;
roseNode = null;
}
}
@@ -77,80 +89,173 @@ public class CompassHudState extends BaseAppState {
protected void cleanup(Application application) {}
// ── Update ────────────────────────────────────────────────────────────────
@Override
public void update(float tpf) {
if (compassNode == null) { return; }
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);
for (int i = 0; i < 4; i++) {
// Winkel des Labels im Kompass-Raum (von Oben CW = von +Y Achse)
float a = -yaw + OFFSETS[i];
float lx = cx + RADIUS * FastMath.sin(a);
float ly = cy + RADIUS * FastMath.cos(a);
BitmapText lbl = dirLabels[i];
lbl.setLocalTranslation(lx - lbl.getLineWidth() * 0.5f, ly + lbl.getLineHeight() * 0.4f, LABEL_Z);
// Rose dreht sich so, dass Nord immer in der echten Nord-Richtung liegt
roseRot.fromAngleAxis(-yaw, Vector3f.UNIT_Z);
roseNode.setLocalRotation(roseRot);
// Buchstaben konter-rotieren → bleiben immer aufrecht
labelRot.fromAngleAxis(yaw, Vector3f.UNIT_Z);
for (Node ln : labelNodes) {
ln.setLocalRotation(labelRot);
}
}
// ── Aufbau ───────────────────────────────────────────────────────────────
private void buildCompass() {
float sw = cam.getWidth();
float sh = cam.getHeight();
// ── Aufbau ───────────────────────────────────────────────────────────────
// Kompass unten links
float ox = MARGIN;
// Direkt über der Hotbar: Hotbar.MARGIN_BOT + SLOT_SIZE + Gap
private void buildCompass() {
float oy = HotbarState.MARGIN_BOT + HotbarState.SLOT_SIZE + 8f;
cx = ox + SIZE / 2f;
cy = oy + SIZE / 2f;
float cx = MARGIN + SIZE / 2f;
float cy = oy + SIZE / 2f;
compassNode = new Node("compass");
compassNode.setLocalTranslation(cx, cy, 0f);
// Rahmen
compassNode.attachChild(makeQuad(ox, oy, SIZE, SIZE, COL_BORDER, 1f));
// Hintergrund
compassNode.attachChild(makeQuad(ox + 1, oy + 1, SIZE - 2, SIZE - 2, COL_BG, 2f));
// Rahmen (etwas größerer Kreis) + Hintergrund
compassNode.attachChild(makeCircle(RADIUS + 1.5f, 64, COL_BORDER, 1f));
compassNode.attachChild(makeCircle(RADIUS, 64, COL_BG, 2f));
// Vorwärts-Marker: kleine goldene Raute oben in der Mitte
compassNode.attachChild(makeQuad(cx - DOT_SZ * 0.5f, cy + RADIUS - DOT_SZ * 0.5f,
DOT_SZ, DOT_SZ, COL_MARKER, 3f));
// Rotierende Rose
roseNode = new Node("rose");
compassNode.attachChild(roseNode);
// Himmelsrichtungs-Labels (Positionen werden in update() gesetzt)
for (int i = 0; i < 4; i++) {
ColorRGBA col = i == 0 ? COL_N : COL_CARD;
BitmapText lbl = txt(LABELS[i], 13, col);
lbl.setLocalTranslation(cx, cy, LABEL_Z);
dirLabels[i] = lbl;
compassNode.attachChild(lbl);
// 8 Tick-Striche (major = Hauptrichtungen, minor = Zwischenrichtungen)
for (int i = 0; i < 8; i++) {
float deg = i * 45f;
boolean maj = (i % 2 == 0);
float innerR = RADIUS - (maj ? 9f : 5f);
roseNode.attachChild(makeTickQuad(deg, innerR, maj ? COL_TICK_MJ : COL_TICK_MN, 3f));
}
// 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);
}
// ── Hilfsmethoden ────────────────────────────────────────────────────────
private Geometry makeQuad(float x, float y, float w, float h, ColorRGBA col, float z) {
Geometry g = new Geometry("q", new Quad(w, h));
// ── Geometry-Helfer ───────────────────────────────────────────────────────
/** 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");
m.setColor("Color", col.clone());
if (col.a < 1f) {
m.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
g.setQueueBucket(RenderQueue.Bucket.Transparent);
} else {
g.setQueueBucket(RenderQueue.Bucket.Gui);
}
g.setMaterial(m);
g.setLocalTranslation(x, y, z);
return g;
return m;
}
private BitmapText txt(String s, int size, ColorRGBA col) {
private BitmapText makeBitmapText(String text, int size, ColorRGBA col) {
BitmapText t = new BitmapText(font);
t.setSize(size);
t.setColor(col);
t.setText(s);
t.setColor(col.clone());
t.setText(text);
t.setQueueBucket(RenderQueue.Bucket.Gui);
return t;
}

View File

@@ -1,12 +1,15 @@
package de.blight.game.state;
import com.jme3.asset.AssetManager;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 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 {
private static final Logger log = LoggerFactory.getLogger(ModelLodControl.class);
private final Camera cam;
private final float lod1DistSq;
private final float lod2DistSq;
@@ -93,9 +98,19 @@ public class ModelLodControl extends AbstractControl {
@Override
protected void controlUpdate(float tpf) {
float dx = cam.getLocation().x - spatial.getWorldTranslation().x;
float dy = cam.getLocation().y - spatial.getWorldTranslation().y;
float dz = cam.getLocation().z - spatial.getWorldTranslation().z;
// getWorldTranslation() liefert auf dem ersten Frame (0,0,0) solange updateGeometricState()
// noch nicht gelaufen ist. Deshalb: wenn Translation noch (0,0,0) ist und der spatial
// 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;
boolean embedded = embLod0 != null;
@@ -114,6 +129,9 @@ public class ModelLodControl extends AbstractControl {
}
if (targetSlot == currentSlot) return;
log.debug("[LOD] {} Slot {} → {} (dist={}m)",
spatial.getName(), currentSlot, targetSlot,
String.format("%.1f", Math.sqrt(distSq)));
currentSlot = targetSlot;
if (targetSlot == -1) {

View File

@@ -72,9 +72,19 @@ public class TerrainChunkState extends BaseAppState {
private final RigidBodyControl[] physics = new RigidBodyControl[TOTAL];
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 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). */
private float spawnHintX = Float.NaN;
private float spawnHintZ = Float.NaN;
@@ -184,7 +194,7 @@ public class TerrainChunkState extends BaseAppState {
for (int cx = 0; cx < N; cx++) {
int ci = ChunkTerrainIO.chunkIndex(cx, cz);
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];
}
}

View File

@@ -6,7 +6,7 @@ import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.post.filters.FogFilter;
import de.blight.game.post.BlightFogFilter;
import com.jme3.water.WaterFilter;
import jme3utilities.sky.SkyControl;
import org.slf4j.Logger;
@@ -16,37 +16,40 @@ public class WeatherState extends BaseAppState {
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_DISTANCE = { 600f, 350f, 140f, 50f };
private static final float[] WIND_SPEED = { 4f, 14f, 26f, 55f };
private static final float[] WAVE_SPEED = { 0.5f, 1.0f, 1.5f, 3.2f };
private static final float[] WAVE_AMP = { 0.3f, 0.5f, 0.8f, 1.8f };
private static final float[] WAVE_SCALE = { 0.008f, 0.007f, 0.006f, 0.005f};
private static final float[] WATER_TRANS = { 0.15f, 0.10f, 0.07f, 0.02f };
private static final float[] FOAM_INTENSITY= { 0.0f, 0.20f, 0.45f, 0.90f };
private static final float[] CLOUD_OPACITY = { 0.0f, 0.40f, 0.80f, 1.00f };
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, 80f };
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, 0.4f };
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, 0.009f};
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, 0.0f };
private static final float[] CLOUD_OPACITY = { 0.0f, 0.40f, 0.80f, 1.00f, 0.95f };
private static final ColorRGBA[] FOG_COLOR = {
new ColorRGBA(0.75f, 0.80f, 0.88f, 1f),
new ColorRGBA(0.62f, 0.65f, 0.70f, 1f),
new ColorRGBA(0.42f, 0.43f, 0.46f, 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 = {
new ColorRGBA(0.05f, 0.25f, 0.55f, 1f),
new ColorRGBA(0.04f, 0.18f, 0.42f, 1f),
new ColorRGBA(0.03f, 0.12f, 0.28f, 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 = {
new ColorRGBA(0.02f, 0.12f, 0.30f, 1f),
new ColorRGBA(0.01f, 0.08f, 0.20f, 1f),
new ColorRGBA(0.01f, 0.04f, 0.12f, 1f),
new ColorRGBA(0.00f, 0.02f, 0.06f, 1f),
new ColorRGBA(0.01f, 0.09f, 0.23f, 1f),
};
// ── State ────────────────────────────────────────────────────────────────
@@ -54,7 +57,7 @@ public class WeatherState extends BaseAppState {
private Weather active = Weather.SUNNY;
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 windSpeed = 4f;
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 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 ────────────────────────────────────────────────────
private FogFilter fogFilter;
private WaterFilter waterFilter;
private SkyControl skyControl;
private BlightFogFilter fogFilter;
private WaterFilter waterFilter;
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 setSkyControl(SkyControl sc) { this.skyControl = sc; }
@@ -114,8 +123,8 @@ public class WeatherState extends BaseAppState {
int i = active.ordinal();
fogDensity = approach(fogDensity, FOG_DENSITY[i], tpf * 0.025f);
fogDistance = approach(fogDistance, FOG_DISTANCE[i], tpf * 0.025f);
fogDensity = approach(fogDensity, FOG_DENSITY[i], tpf * 0.025f);
fogDistance = approach(fogDistance, FOG_DISTANCE[i] * viewDistanceFactor, tpf * 0.025f);
windSpeed = approach(windSpeed, WIND_SPEED[i], tpf * 0.040f);
waveSpeed = approach(waveSpeed, WAVE_SPEED[i], tpf * 0.030f);
waveAmp = approach(waveAmp, WAVE_AMP[i], tpf * 0.025f);
@@ -156,10 +165,11 @@ public class WeatherState extends BaseAppState {
private void pickNextWeather() {
float r = FastMath.nextRandomFloat();
Weather next = r < 0.40f ? Weather.SUNNY
: r < 0.70f ? Weather.CLOUDY
: r < 0.88f ? Weather.OVERCAST
: Weather.STORM;
Weather next = r < 0.38f ? Weather.SUNNY
: r < 0.65f ? Weather.CLOUDY
: r < 0.82f ? Weather.OVERCAST
: r < 0.92f ? Weather.STORM
: Weather.FOG;
windAngleTgt = FastMath.nextRandomFloat() * FastMath.TWO_PI;
if (next != active) {
active = next;

View File

@@ -14,6 +14,7 @@ import com.jme3.material.RenderState;
import com.jme3.math.*;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.*;
import com.jme3.scene.control.BillboardControl;
import com.jme3.scene.shape.*;
import com.jme3.texture.Texture;
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> pbrMaterials = new ArrayList<>();
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. */
private final Map<String, RigidBodyControl> interactableRbcs = new HashMap<>();
@@ -192,22 +196,41 @@ public class WorldObjectsState extends BaseAppState {
// LOD / Distance-Culling
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) {
Spatial lod0 = treeNode.getChild("lod0");
Spatial lod1 = treeNode.getChild("lod1");
Spatial lod2 = treeNode.getChild("lod2");
// Fall A: j3o hat explizite lod0/lod1/lod2 Child-Nodes.
if (lod0 != null) {
lod0.setCullHint(Spatial.CullHint.Inherit);
if (lod1 != null) lod1.setCullHint(Spatial.CullHint.Always);
if (lod2 != null) lod2.setCullHint(Spatial.CullHint.Always);
treeNode.addControl(new ModelLodControl(
app.getCamera(), lod0, lod1, lod2,
m.lod1Distance(), m.lod2Distance(), m.cullDistance()));
m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor, m.cullDistance() * lodFactor));
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)
@@ -216,7 +239,7 @@ public class WorldObjectsState extends BaseAppState {
ModelLodControl ctrl = new ModelLodControl(
assets, app.getCamera(),
m.lod1Path(), m.lod2Path(),
m.lod1Distance(), m.lod2Distance(), m.cullDistance());
m.lod1Distance() * lodFactor, m.lod2Distance() * lodFactor, m.cullDistance() * lodFactor);
ctrl.setLodLoadCallback(lod -> {
convertUnshadedToPbr(lod);
collectSceneLitMaterials(lod);

View File

@@ -48,8 +48,12 @@ menu.graphics.row.resolution=Auflösung
menu.graphics.row.fullscreen=Vollbild
menu.graphics.row.vsync=VSync
menu.graphics.row.aa=Kantenglättung
menu.graphics.row.viewdist=Sichtweite
menu.graphics.val.on=An
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.cancel=Abbrechen

View File

@@ -51,8 +51,12 @@ menu.graphics.row.resolution=Resolution
menu.graphics.row.fullscreen=Fullscreen
menu.graphics.row.vsync=VSync
menu.graphics.row.aa=Anti-aliasing
menu.graphics.row.viewdist=View Distance
menu.graphics.val.on=On
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.cancel=Cancel