Weiter an den Grafik Effekten gearbeitet

This commit is contained in:
2026-07-24 16:28:35 +02:00
parent 919f70bb44
commit 574bad3d67
17 changed files with 539 additions and 23 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

View File

@@ -31,7 +31,8 @@ public class GraphicsScreen extends MenuScreen {
private static final int ROW_VIEWDIST = 4; private static final int ROW_VIEWDIST = 4;
private static final int ROW_SHADOW = 5; private static final int ROW_SHADOW = 5;
private static final int ROW_POSTFX = 6; private static final int ROW_POSTFX = 6;
private static final int ROW_COUNT = 7; private static final int ROW_PARTICLES = 7;
private static final int ROW_COUNT = 8;
private final GraphicsSettings live; private final GraphicsSettings live;
private GraphicsSettings edit; private GraphicsSettings edit;
@@ -62,9 +63,10 @@ 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; edit.viewDistance = live.viewDistance;
edit.shadowQuality = live.shadowQuality; edit.shadowQuality = live.shadowQuality;
edit.postFxQuality = live.postFxQuality; edit.postFxQuality = live.postFxQuality;
edit.particleQuality = live.particleQuality;
resIdx = 0; resIdx = 0;
for (int i = 0; i < resolutions.length; i++) { for (int i = 0; i < resolutions.length; i++) {
@@ -89,7 +91,8 @@ public class GraphicsScreen extends MenuScreen {
"menu.graphics.row.aa", "menu.graphics.row.aa",
"menu.graphics.row.viewdist", "menu.graphics.row.viewdist",
"menu.graphics.row.shadow", "menu.graphics.row.shadow",
"menu.graphics.row.postfx" "menu.graphics.row.postfx",
"menu.graphics.row.particles"
}; };
float lblX = CONT_X + 30f; float lblX = CONT_X + 30f;
@@ -153,6 +156,11 @@ public class GraphicsScreen extends MenuScreen {
case MEDIUM -> t("menu.graphics.val.shadow.medium"); case MEDIUM -> t("menu.graphics.val.shadow.medium");
case HIGH -> t("menu.graphics.val.shadow.high"); case HIGH -> t("menu.graphics.val.shadow.high");
}; };
case ROW_PARTICLES -> switch (edit.particleQuality) {
case OFF -> t("menu.graphics.val.off");
case MEDIUM -> t("menu.graphics.val.shadow.medium");
case HIGH -> t("menu.graphics.val.shadow.high");
};
default -> ""; default -> "";
}; };
BitmapText vt = valTexts[row]; BitmapText vt = valTexts[row];
@@ -190,29 +198,36 @@ public class GraphicsScreen extends MenuScreen {
GraphicsSettings.PostFxQuality[] pfqs = GraphicsSettings.PostFxQuality.values(); GraphicsSettings.PostFxQuality[] pfqs = GraphicsSettings.PostFxQuality.values();
edit.postFxQuality = pfqs[(edit.postFxQuality.ordinal() + dir + pfqs.length) % pfqs.length]; edit.postFxQuality = pfqs[(edit.postFxQuality.ordinal() + dir + pfqs.length) % pfqs.length];
break; break;
case ROW_PARTICLES:
GraphicsSettings.ParticleQuality[] pqs = GraphicsSettings.ParticleQuality.values();
edit.particleQuality = pqs[(edit.particleQuality.ordinal() + dir + pqs.length) % pqs.length];
break;
} }
refreshText(row); refreshText(row);
} }
private void applyAndSave() { private void applyAndSave() {
boolean shadowChanged = live.shadowQuality != edit.shadowQuality; boolean shadowChanged = live.shadowQuality != edit.shadowQuality;
boolean postFxChanged = live.postFxQuality != edit.postFxQuality; boolean postFxChanged = live.postFxQuality != edit.postFxQuality;
boolean particleChanged = live.particleQuality != edit.particleQuality;
boolean needsRestart = live.width != edit.width boolean needsRestart = live.width != edit.width
|| live.height != edit.height || 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
|| shadowChanged || shadowChanged
|| postFxChanged; || postFxChanged
|| particleChanged;
live.width = edit.width; live.width = edit.width;
live.height = edit.height; 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; live.viewDistance = edit.viewDistance;
live.shadowQuality = edit.shadowQuality; live.shadowQuality = edit.shadowQuality;
live.postFxQuality = edit.postFxQuality; live.postFxQuality = edit.postFxQuality;
live.particleQuality = edit.particleQuality;
GraphicsStore.save(live); GraphicsStore.save(live);
WeatherState ws = getApplication().getStateManager().getState(WeatherState.class); WeatherState ws = getApplication().getStateManager().getState(WeatherState.class);

View File

@@ -1,16 +1,17 @@
package de.blight.game.config; package de.blight.game.config;
public class GraphicsSettings { public class GraphicsSettings {
public int width = 1280; public int width = 1280;
public int height = 720; public int height = 720;
public boolean fullscreen = false; public boolean fullscreen = false;
public boolean vsync = false; public boolean vsync = false;
public int samples = 4; public int samples = 4;
public ViewDistance viewDistance = ViewDistance.NORMAL; public ViewDistance viewDistance = ViewDistance.NORMAL;
public ShadowQuality shadowQuality = ShadowQuality.MEDIUM; public ShadowQuality shadowQuality = ShadowQuality.MEDIUM;
/** Verstecktes Debug-Flag: erzwingt LOD2 (Impostor) für alle Objekte. Nur per Konsole setzbar. */ /** Verstecktes Debug-Flag: erzwingt LOD2 (Impostor) für alle Objekte. Nur per Konsole setzbar. */
public boolean debugLod2 = false; public boolean debugLod2 = false;
public PostFxQuality postFxQuality = PostFxQuality.HIGH; public PostFxQuality postFxQuality = PostFxQuality.HIGH;
public ParticleQuality particleQuality = ParticleQuality.HIGH;
public enum PostFxQuality { public enum PostFxQuality {
// fxaa toneMap bloomI bloomExp godRays // fxaa toneMap bloomI bloomExp godRays
@@ -68,6 +69,10 @@ public class GraphicsSettings {
} }
} }
public enum ParticleQuality {
OFF, MEDIUM, HIGH
}
public enum ViewDistance { public enum ViewDistance {
SHORT (1.00f, 1, 3), SHORT (1.00f, 1, 3),
NORMAL(1.60f, 2, 5), NORMAL(1.60f, 2, 5),

View File

@@ -813,6 +813,10 @@ public class WorldScene extends BaseAppState {
weather.setDayTime(dayNight.getDayTime()); weather.setDayTime(dayNight.getDayTime());
app.getStateManager().attach(weather); app.getStateManager().attach(weather);
app.getStateManager().attach(new de.blight.game.state.RainState()); app.getStateManager().attach(new de.blight.game.state.RainState());
if (graphicsSettings.particleQuality != GraphicsSettings.ParticleQuality.OFF) {
app.getStateManager().attach(
new de.blight.game.state.AmbientParticlesState(graphicsSettings));
}
} catch (Exception e) { } catch (Exception e) {
log.warn("[WorldScene] Post-Processing nicht verfügbar: {}", e.getMessage()); log.warn("[WorldScene] Post-Processing nicht verfügbar: {}", e.getMessage());
} }

View File

@@ -0,0 +1,386 @@
package de.blight.game.state;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState;
import com.jme3.asset.AssetManager;
import com.jme3.effect.ParticleEmitter;
import com.jme3.effect.ParticleMesh;
import com.jme3.effect.shapes.EmitterBoxShape;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.jme3.texture.Image;
import com.jme3.texture.Texture2D;
import com.jme3.texture.image.ColorSpace;
import de.blight.common.PlacedModel;
import de.blight.common.PlacedModelIO;
import de.blight.game.config.GraphicsSettings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
/**
* Ambient-Partikeleffekte:
* - Blätter (Kiefer/Eiche/Esche) bei Wind in Waldnähe
* - Staubwirbel bei starkem Wind im offenen Gelände
* - Regen-Bodensplitzer (ab ParticleQuality.MEDIUM)
*
* Aktiviert sich nur bei ParticleQuality != OFF.
*/
public class AmbientParticlesState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(AmbientParticlesState.class);
// ── Konstanten ────────────────────────────────────────────────────────────
private static final float SCAN_INTERVAL = 6f;
private static final float TREE_RADIUS = 40f;
private static final float TREE_DENSITY_MAX = 8f;
private static final float EMITTER_RADIUS = 35f;
private static final float SPLASH_RADIUS = 40f;
private static final float CHAR_HEIGHT = 1.7f; // Kamera-Auge über Boden
/** Ab dieser Windstärke beginnen Blätter zu fliegen. */
private static final float LEAF_MIN_WIND = 6f;
/** Ab dieser Windstärke beginnt Staubaufwirbelung. */
private static final float DUST_MIN_WIND = 20f;
// ── State ─────────────────────────────────────────────────────────────────
private final GraphicsSettings settings;
private final List<Vector3f> treePositions = new ArrayList<>();
private float treeDensityFactor = 0f;
private float scanTimer = 0f;
private static final String[] LEAF_TEX_PATHS = {
"Textures/internal/leavesfilter/leaf1.png",
"Textures/internal/leavesfilter/leaf2.png",
"Textures/internal/leavesfilter/leaf3.png",
"Textures/internal/leavesfilter/leaf4.png",
"Textures/internal/leavesfilter/leaf5.png",
"Textures/internal/leavesfilter/leaf6.png",
"Textures/internal/leavesfilter/leaf7.png",
"Textures/internal/leavesfilter/leaf8.png",
};
@SuppressWarnings("deprecation") private ParticleEmitter[] leafEmitters;
@SuppressWarnings("deprecation") private ParticleEmitter debrisEmitter;
@SuppressWarnings("deprecation") private ParticleEmitter splashEmitter;
private Node leafNode;
private Node debrisNode;
private Node splashNode;
private float leafIntensity = 0f;
private float debrisIntensity = 0f;
private float splashIntensity = 0f;
// ── Konstruktor ───────────────────────────────────────────────────────────
public AmbientParticlesState(GraphicsSettings settings) {
this.settings = settings;
}
// ── Lifecycle ─────────────────────────────────────────────────────────────
@Override
@SuppressWarnings("deprecation")
protected void initialize(Application app) {
// Baumpositionen einmalig laden; Palmen ausschließen
try {
for (PlacedModel m : PlacedModelIO.load()) {
String path = (m.meshFile() != null && !m.meshFile().isBlank())
? m.meshFile() : (m.modelPath() != null ? m.modelPath() : "");
if (path.contains("Models/trees/") && !path.contains("/palm/")) {
treePositions.add(new Vector3f(m.x(), m.y(), m.z()));
}
}
log.info("[AmbientParticles] {} Baumpositionen (ohne Palmen).", treePositions.size());
} catch (Exception e) {
log.warn("[AmbientParticles] Baumpositionen nicht ladbar: {}", e.getMessage());
}
boolean high = settings.particleQuality == GraphicsSettings.ParticleQuality.HIGH;
SimpleApplication sa = (SimpleApplication) app;
Node root = sa.getRootNode();
AssetManager assets = app.getAssetManager();
// ── Blatt-Emitter (1 pro Textur → zufällige Blattformen im Umlauf) ────
Texture2D[] leafTextures = loadLeafTextures(assets);
int leafTexCount = leafTextures.length;
int poolPerEmitter = Math.max(1, (high ? 300 : 150) / leafTexCount);
leafEmitters = new ParticleEmitter[leafTexCount];
leafNode = new Node("leafNode");
for (int li = 0; li < leafTexCount; li++) {
ParticleEmitter le = new ParticleEmitter("leaves_" + li,
ParticleMesh.Type.Triangle, poolPerEmitter);
le.setShape(new EmitterBoxShape(
new Vector3f(-EMITTER_RADIUS, -1f, -EMITTER_RADIUS),
new Vector3f( EMITTER_RADIUS, 4f, EMITTER_RADIUS)));
le.setGravity(0f, -0.3f, 0f);
le.setParticlesPerSec(0f);
le.setLowLife(4f);
le.setHighLife(9f);
le.setStartSize(0.16f);
le.setEndSize(0.15f);
le.setStartColor(new ColorRGBA(0.38f, 0.58f, 0.22f, 0.88f));
le.setEndColor( new ColorRGBA(0.50f, 0.35f, 0.10f, 0.00f));
le.setInitialVelocity(new Vector3f(0f, -0.3f, 0f));
le.setVelocityVariation(0.9f);
le.setRandomAngle(true);
le.setRotateSpeed(FastMath.PI * 1.5f);
com.jme3.material.Material lm =
new com.jme3.material.Material(assets, "Common/MatDefs/Misc/Particle.j3md");
lm.setTexture("Texture", leafTextures[li]);
lm.getAdditionalRenderState().setBlendMode(com.jme3.material.RenderState.BlendMode.Alpha);
lm.getAdditionalRenderState().setDepthWrite(false);
le.setMaterial(lm);
leafEmitters[li] = le;
leafNode.attachChild(le);
}
root.attachChild(leafNode);
// ── Staub-Emitter (offenes Gelände) ───────────────────────────────────
debrisEmitter = new ParticleEmitter("debris", ParticleMesh.Type.Triangle, high ? 200 : 100);
debrisEmitter.setShape(new EmitterBoxShape(
new Vector3f(-EMITTER_RADIUS, -1f, -EMITTER_RADIUS),
new Vector3f( EMITTER_RADIUS, 4f, EMITTER_RADIUS)));
debrisEmitter.setGravity(0f, 0f, 0f);
debrisEmitter.setParticlesPerSec(0f);
debrisEmitter.setLowLife(3f);
debrisEmitter.setHighLife(6f);
debrisEmitter.setStartSize(0.30f);
debrisEmitter.setEndSize(0.80f);
debrisEmitter.setStartColor(new ColorRGBA(0.68f, 0.60f, 0.50f, 0.30f));
debrisEmitter.setEndColor( new ColorRGBA(0.68f, 0.60f, 0.50f, 0.00f));
debrisEmitter.setInitialVelocity(new Vector3f(0f, 0f, 0f));
debrisEmitter.setVelocityVariation(0.4f);
com.jme3.material.Material dm =
new com.jme3.material.Material(assets, "Common/MatDefs/Misc/Particle.j3md");
dm.setTexture("Texture", createSoftCircleTexture(32, 173, 153, 128, 140));
dm.getAdditionalRenderState().setBlendMode(com.jme3.material.RenderState.BlendMode.Alpha);
dm.getAdditionalRenderState().setDepthWrite(false);
debrisEmitter.setMaterial(dm);
debrisNode = new Node("debrisNode");
debrisNode.attachChild(debrisEmitter);
root.attachChild(debrisNode);
// ── Regen-Bodensplitzer (ab MEDIUM) ──────────────────────────────────
splashEmitter = new ParticleEmitter("splash", ParticleMesh.Type.Triangle, high ? 2000 : 1000);
splashEmitter.setShape(new EmitterBoxShape(
new Vector3f(-SPLASH_RADIUS, -0.02f, -SPLASH_RADIUS),
new Vector3f( SPLASH_RADIUS, 0.02f, SPLASH_RADIUS)));
// Starke Schwerkraft: Spritzer schießen kurz hoch und fallen sofort zurück
splashEmitter.setGravity(0f, -18f, 0f);
splashEmitter.setParticlesPerSec(0f);
splashEmitter.setLowLife(0.05f);
splashEmitter.setHighLife(0.14f);
splashEmitter.setStartSize(0.01f);
splashEmitter.setEndSize(0.03f);
splashEmitter.setStartColor(new ColorRGBA(0.82f, 0.91f, 1.00f, 0.60f));
splashEmitter.setEndColor( new ColorRGBA(0.82f, 0.91f, 1.00f, 0.00f));
// Velocity in ALLE Richtungen → Variation wirkt auf x, y UND z → echter Spritz-Effekt
splashEmitter.setInitialVelocity(new Vector3f(0.5f, 0.8f, 0.5f));
splashEmitter.setVelocityVariation(1.0f);
com.jme3.material.Material sm =
new com.jme3.material.Material(assets, "Common/MatDefs/Misc/Particle.j3md");
sm.setTexture("Texture", createSoftCircleTexture(16, 210, 230, 255, 200));
sm.getAdditionalRenderState().setBlendMode(com.jme3.material.RenderState.BlendMode.Alpha);
sm.getAdditionalRenderState().setDepthWrite(false);
splashEmitter.setMaterial(sm);
splashNode = new Node("splashNode");
splashNode.attachChild(splashEmitter);
root.attachChild(splashNode);
}
@Override
@SuppressWarnings("deprecation")
public void update(float tpf) {
WeatherState ws = getApplication().getStateManager().getState(WeatherState.class);
if (ws == null) { return; }
Vector3f cam = getApplication().getCamera().getLocation();
boolean high = settings.particleQuality == GraphicsSettings.ParticleQuality.HIGH;
// ── Periodischer Baum-Dichte-Scan ─────────────────────────────────────
scanTimer -= tpf;
if (scanTimer <= 0f) {
scanTimer = SCAN_INTERVAL;
float r2 = TREE_RADIUS * TREE_RADIUS;
int count = 0;
for (Vector3f tp : treePositions) {
float dx = tp.x - cam.x, dz = tp.z - cam.z;
if (dx * dx + dz * dz < r2) { count++; }
}
treeDensityFactor = FastMath.clamp(count / TREE_DENSITY_MAX, 0f, 1f);
}
float wind = ws.getWindSpeed();
Vector3f wDir = ws.getWindDirection();
// ── Blätter ───────────────────────────────────────────────────────────
float leafTarget = 0f;
if (wind > LEAF_MIN_WIND && treeDensityFactor > 0.20f) {
leafTarget = FastMath.clamp(
(wind - LEAF_MIN_WIND) / 55f * treeDensityFactor, 0f, 1f);
}
leafIntensity += (leafTarget - leafIntensity) * FastMath.clamp(tpf * 0.5f, 0f, 1f);
leafNode.setLocalTranslation(cam.x, cam.y, cam.z);
if (leafIntensity > 0.01f) {
float ws2 = wind * 0.15f;
Vector3f leafVel = new Vector3f(wDir.x * ws2, -0.05f, wDir.z * ws2);
float ratePerEmitter = leafIntensity * (high ? 40f : 20f) / leafEmitters.length;
for (ParticleEmitter le : leafEmitters) {
le.getParticleInfluencer().setInitialVelocity(leafVel);
le.setParticlesPerSec(ratePerEmitter);
}
} else {
for (ParticleEmitter le : leafEmitters) { le.setParticlesPerSec(0f); }
}
// ── Staub / Schutt ────────────────────────────────────────────────────
float debrisTarget = 0f;
if (wind > DUST_MIN_WIND && treeDensityFactor < 0.5f) {
debrisTarget = FastMath.clamp(
(wind - DUST_MIN_WIND) / 50f * (1f - treeDensityFactor), 0f, 1f);
}
debrisIntensity += (debrisTarget - debrisIntensity) * FastMath.clamp(tpf * 0.5f, 0f, 1f);
debrisNode.setLocalTranslation(cam.x, cam.y, cam.z);
if (debrisIntensity > 0.01f) {
float ws2 = wind * 0.05f;
debrisEmitter.getParticleInfluencer()
.setInitialVelocity(new Vector3f(wDir.x * ws2, 0.05f, wDir.z * ws2));
debrisEmitter.setParticlesPerSec(debrisIntensity * (high ? 30f : 15f));
} else {
debrisEmitter.setParticlesPerSec(0f);
}
// ── Regen-Bodensplitzer ───────────────────────────────────────────────
float rainIntensity = ws.getRainIntensity();
float splashTarget = rainIntensity;
splashIntensity += (splashTarget - splashIntensity) * FastMath.clamp(tpf * 2f, 0f, 1f);
// Splashknoten am Boden unter der Kamera positionieren
splashNode.setLocalTranslation(cam.x, cam.y - CHAR_HEIGHT, cam.z);
if (splashIntensity > 0.01f) {
// Wind bläst Spritzer leicht schräg — aber y muss immer >> 0 bleiben
// damit Variation auf alle Achsen greift (VelocityVariation * component)
float wn = wind * 0.01f;
splashEmitter.getParticleInfluencer()
.setInitialVelocity(new Vector3f(
0.5f + wDir.x * wn,
0.8f,
0.5f + wDir.z * wn));
splashEmitter.setParticlesPerSec(splashIntensity * (high ? 700f : 350f));
} else {
splashEmitter.setParticlesPerSec(0f);
}
}
@Override
protected void onDisable() {
if (leafEmitters != null) {
for (ParticleEmitter le : leafEmitters) { le.setParticlesPerSec(0f); }
}
if (debrisEmitter != null) { debrisEmitter.setParticlesPerSec(0f); }
if (splashEmitter != null) { splashEmitter.setParticlesPerSec(0f); }
}
@Override protected void onEnable() {}
@Override
protected void cleanup(Application app) {
Node root = ((SimpleApplication) app).getRootNode();
if (leafNode != null) { root.detachChild(leafNode); }
if (debrisNode != null) { root.detachChild(debrisNode); }
if (splashNode != null) { root.detachChild(splashNode); }
}
// ── Texturen ─────────────────────────────────────────────────────────────
/**
* Lädt alle Blatt-Texturen aus LEAF_TEX_PATHS.
* Fehlende Einträge werden übersprungen; ist das Array leer, wird die prozedurale Fallback-Textur verwendet.
*/
private static Texture2D[] loadLeafTextures(AssetManager assets) {
List<Texture2D> loaded = new ArrayList<>();
for (String path : LEAF_TEX_PATHS) {
try {
com.jme3.texture.Texture t = assets.loadTexture(path);
if (t instanceof Texture2D) {
loaded.add((Texture2D) t);
}
} catch (Exception ignored) {}
}
if (loaded.isEmpty()) {
log.warn("[AmbientParticles] Keine Blatt-Texturen gefunden — prozeduraler Fallback.");
loaded.add(createLeafTexture());
} else {
log.info("[AmbientParticles] {} Blatt-Texturen geladen.", loaded.size());
}
return loaded.toArray(new Texture2D[0]);
}
/**
* Prozedurale weiße Blattsilhouette (lang-oval mit Mittelrippe).
* Weiß → Partikel-StartColor/EndColor bestimmt die Farbe unverfälscht.
*/
private static Texture2D createLeafTexture() {
int w = 32, h = 64;
byte[] pixels = new byte[w * h * 4];
float cx = (w - 1) * 0.5f, cy = (h - 1) * 0.5f;
float rx = w * 0.40f, ry = h * 0.46f;
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
float nx = (x - cx) / rx;
float ny = (y - cy) / ry;
// Leichte Asymmetrie für natürlichere Blattspitze
float nyd = ny + 0.18f * nx * nx;
float d = nx * nx + nyd * nyd;
float alpha = FastMath.clamp((1f - d) * 3.5f, 0f, 1f);
// Sanfte Abdunkelung zur Blattmitte hin (Mittelrippen-Effekt)
float vein = 1f - 0.30f * (float) Math.exp(-nx * nx * 30f);
int brightness = (int)(vein * 255f);
int i = (y * w + x) * 4;
pixels[i ] = (byte) brightness;
pixels[i+1] = (byte) brightness;
pixels[i+2] = (byte) brightness;
pixels[i+3] = (byte) (int)(alpha * 240f);
}
}
ByteBuffer buf = ByteBuffer.allocateDirect(pixels.length);
buf.put(pixels).flip();
return new Texture2D(new Image(Image.Format.RGBA8, w, h, buf, ColorSpace.Linear));
}
/** Weiches rundes Sprite — für Staub und Regen-Splitzer. */
private static Texture2D createSoftCircleTexture(int size, int r, int g, int b, int maxAlpha) {
byte[] pixels = new byte[size * size * 4];
float c = (size - 1) * 0.5f;
for (int y = 0; y < size; y++) {
for (int x = 0; x < size; x++) {
float dx = (x - c) / c, dy = (y - c) / c;
float alpha = FastMath.clamp(1f - (dx * dx + dy * dy) * 2f, 0f, 1f);
int i = (y * size + x) * 4;
pixels[i ] = (byte) r;
pixels[i+1] = (byte) g;
pixels[i+2] = (byte) b;
pixels[i+3] = (byte) (int)(alpha * maxAlpha);
}
}
ByteBuffer buf = ByteBuffer.allocateDirect(pixels.length);
buf.put(pixels).flip();
return new Texture2D(new Image(Image.Format.RGBA8, size, size, buf, ColorSpace.Linear));
}
}

View File

@@ -75,6 +75,8 @@ public class DayNightState extends BaseAppState implements TimeListener {
private float lastSunOcc = 0f; private float lastSunOcc = 0f;
/** 1.0 = klar, sinkt mit zunehmender Wolkendecke (min 0.20). */ /** 1.0 = klar, sinkt mit zunehmender Wolkendecke (min 0.20). */
private float cloudDimFactor = 1f; private float cloudDimFactor = 1f;
/** Aktuelle Sonnenhöhe (-1=Nadir, 0=Horizont, +1=Zenit) — gecacht aus onTimeChanged(). */
private float sunElevation = 0f;
private ColorRGBA customAmbient = new ColorRGBA(); private ColorRGBA customAmbient = new ColorRGBA();
/** true = Spiel-Beleuchtungswerte, false = Editor-Werte. Standardmäßig = withShadows. */ /** true = Spiel-Beleuchtungswerte, false = Editor-Werte. Standardmäßig = withShadows. */
private boolean gameMode; private boolean gameMode;
@@ -204,6 +206,11 @@ public class DayNightState extends BaseAppState implements TimeListener {
/** Ambient-Farbe für eigene Shader-Uniforms (AmbientColor). Kleiner als AmbientLight. */ /** Ambient-Farbe für eigene Shader-Uniforms (AmbientColor). Kleiner als AmbientLight. */
public ColorRGBA getCustomAmbient() { return customAmbient; } public ColorRGBA getCustomAmbient() { return customAmbient; }
/** Letzter sunOcclusionByCloud()-Wert (0=frei, 1=verdeckt) — für WeatherState. */
public float getLastSunOcc() { return lastSunOcc; }
/** Cached Sonnenhöhe aus letztem onTimeChanged()-Aufruf (-1..+1). */
public float getSunElevation() { return sunElevation; }
@Override @Override
protected void cleanup(Application app) { protected void cleanup(Application app) {
dayTime.removeListener(this); dayTime.removeListener(this);
@@ -389,6 +396,7 @@ public class DayNightState extends BaseAppState implements TimeListener {
skyControl.getSunAndStars().setHour(t * 24f); skyControl.getSunAndStars().setHour(t * 24f);
Vector3f toSun = skyControl.getSunAndStars().sunDirection(null); Vector3f toSun = skyControl.getSunAndStars().sunDirection(null);
float elevation = toSun.y; // +1 = Zenit, 0 = Horizont, -1 = Nadir float elevation = toSun.y; // +1 = Zenit, 0 = Horizont, -1 = Nadir
this.sunElevation = elevation;
float elevC = FastMath.clamp(elevation, 0f, 1f); float elevC = FastMath.clamp(elevation, 0f, 1f);
// Spiel vs. Editor: unterschiedliche Helligkeitswerte aus LightingConfig // Spiel vs. Editor: unterschiedliche Helligkeitswerte aus LightingConfig
@@ -417,8 +425,20 @@ public class DayNightState extends BaseAppState implements TimeListener {
ColorRGBA custDayTinted = custDay.mult(sunTint); ColorRGBA custDayTinted = custDay.mult(sunTint);
float ambFactor = FastMath.clamp( float ambFactor = FastMath.clamp(
(elevation - LightingConfig.AMB_ELEV_LOW) / LightingConfig.AMB_ELEV_RANGE, 0f, 1f); (elevation - LightingConfig.AMB_ELEV_LOW) / LightingConfig.AMB_ELEV_RANGE, 0f, 1f);
ambient.setColor(ambNight.clone().interpolateLocal(ambDayTinted, ambFactor)); // Mondlicht: Vollmond hellt die Nacht bläulich-weiß auf (0=Neumond, 1=Vollmond)
customAmbient.set(custNight.clone().interpolateLocal(custDayTinted, ambFactor)); float moonIllum = skyControl.getMoonIllumination();
ColorRGBA ambNightMoon = ambNight.clone();
ColorRGBA custNightMoon = custNight.clone();
if (moonIllum > 0f) {
ambNightMoon.r = FastMath.clamp(ambNightMoon.r + 0.030f * moonIllum, 0f, 1f);
ambNightMoon.g = FastMath.clamp(ambNightMoon.g + 0.040f * moonIllum, 0f, 1f);
ambNightMoon.b = FastMath.clamp(ambNightMoon.b + 0.070f * moonIllum, 0f, 1f);
custNightMoon.r = FastMath.clamp(custNightMoon.r + 0.015f * moonIllum, 0f, 1f);
custNightMoon.g = FastMath.clamp(custNightMoon.g + 0.020f * moonIllum, 0f, 1f);
custNightMoon.b = FastMath.clamp(custNightMoon.b + 0.040f * moonIllum, 0f, 1f);
}
ambient.setColor(ambNightMoon.interpolateLocal(ambDayTinted, ambFactor));
customAmbient.set(custNightMoon.interpolateLocal(custDayTinted, ambFactor));
// Basis-Schattenintensität: nur von Sonnenhöhe abhängig. // Basis-Schattenintensität: nur von Sonnenhöhe abhängig.
// Wolken-Okklusion wird jedes Frame separat in update() berechnet. // Wolken-Okklusion wird jedes Frame separat in update() berechnet.

View File

@@ -47,6 +47,12 @@ public class RainState extends BaseAppState {
private ParticleEmitter rainEmitter; private ParticleEmitter rainEmitter;
private Node rainNode; private Node rainNode;
// ── Calima-Staub ──────────────────────────────────────────────────────────
@SuppressWarnings("deprecation")
private ParticleEmitter dustEmitter;
private Node dustNode;
private float dustIntensity = 0f;
// ── Audio ───────────────────────────────────────────────────────────────── // ── Audio ─────────────────────────────────────────────────────────────────
private AudioNode lightRainAudio; private AudioNode lightRainAudio;
private AudioNode heavyRainAudio; private AudioNode heavyRainAudio;
@@ -109,6 +115,33 @@ public class RainState extends BaseAppState {
rainNode.attachChild(rainEmitter); rainNode.attachChild(rainEmitter);
root.attachChild(rainNode); root.attachChild(rainNode);
// Calima-Staubpartikel — große, langsame, horizontale Schwebepartikel
dustEmitter = new ParticleEmitter("dust", ParticleMesh.Type.Triangle, 1500);
dustEmitter.setShape(new EmitterBoxShape(
new Vector3f(-EMITTER_RADIUS, -6f, -EMITTER_RADIUS),
new Vector3f( EMITTER_RADIUS, 4f, EMITTER_RADIUS)));
dustEmitter.setGravity(0, 0, 0);
dustEmitter.setParticlesPerSec(0);
dustEmitter.setLowLife(5f);
dustEmitter.setHighLife(9f);
dustEmitter.setStartSize(0.50f);
dustEmitter.setEndSize(0.90f);
dustEmitter.setStartColor(new ColorRGBA(0.88f, 0.72f, 0.38f, 0.22f));
dustEmitter.setEndColor( new ColorRGBA(0.88f, 0.72f, 0.38f, 0.00f));
dustEmitter.setInitialVelocity(new Vector3f(0f, 0f, 0f));
dustEmitter.setVelocityVariation(0.4f);
com.jme3.material.Material dm =
new com.jme3.material.Material(assets, "Common/MatDefs/Misc/Particle.j3md");
dm.setTexture("Texture", createDustTexture());
dm.getAdditionalRenderState()
.setBlendMode(com.jme3.material.RenderState.BlendMode.Alpha);
dm.getAdditionalRenderState().setDepthWrite(false);
dustEmitter.setMaterial(dm);
dustNode = new Node("dustNode");
dustNode.attachChild(dustEmitter);
root.attachChild(dustNode);
// Regen-Audio (gestreamt, da lange Schleifen) // Regen-Audio (gestreamt, da lange Schleifen)
lightRainAudio = streamAudio(assets, "audio/ambient/weather/leichter_regen.ogg"); lightRainAudio = streamAudio(assets, "audio/ambient/weather/leichter_regen.ogg");
heavyRainAudio = streamAudio(assets, "audio/ambient/weather/starker_regen.ogg"); heavyRainAudio = streamAudio(assets, "audio/ambient/weather/starker_regen.ogg");
@@ -159,6 +192,19 @@ public class RainState extends BaseAppState {
rainEmitter.setParticlesPerSec(intensity * 2500f); rainEmitter.setParticlesPerSec(intensity * 2500f);
// ── Calima-Staub ──────────────────────────────────────────────────────
boolean isCalima = weather == WeatherState.Weather.CALIMA;
dustIntensity += ((isCalima ? 1f : 0f) - dustIntensity) * FastMath.clamp(tpf * 0.3f, 0f, 1f);
if (dustIntensity > 0.01f) {
float dustWs = ws.getWindSpeed() * 0.06f;
dustEmitter.getParticleInfluencer()
.setInitialVelocity(new Vector3f(wind.x * dustWs, 0.10f, wind.z * dustWs));
dustEmitter.setParticlesPerSec(dustIntensity * 70f);
dustNode.setLocalTranslation(cam.x, cam.y, cam.z);
} else {
dustEmitter.setParticlesPerSec(0f);
}
// ── Audio ───────────────────────────────────────────────────────────── // ── Audio ─────────────────────────────────────────────────────────────
float tgtLight = intensity > 0.01f ? FastMath.clamp(1f - intensity * 1.2f, 0f, 0.225f) : 0f; float tgtLight = intensity > 0.01f ? FastMath.clamp(1f - intensity * 1.2f, 0f, 0.225f) : 0f;
float tgtHeavy = FastMath.clamp((intensity - 0.20f) * 1.35f, 0f, 0.25f); float tgtHeavy = FastMath.clamp((intensity - 0.20f) * 1.35f, 0f, 0.25f);
@@ -222,6 +268,7 @@ public class RainState extends BaseAppState {
@Override @Override
protected void onDisable() { protected void onDisable() {
if (rainEmitter != null) rainEmitter.setParticlesPerSec(0); if (rainEmitter != null) rainEmitter.setParticlesPerSec(0);
if (dustEmitter != null) dustEmitter.setParticlesPerSec(0);
if (lightRainAudio != null) lightRainAudio.setVolume(0); if (lightRainAudio != null) lightRainAudio.setVolume(0);
if (heavyRainAudio != null) heavyRainAudio.setVolume(0); if (heavyRainAudio != null) heavyRainAudio.setVolume(0);
} }
@@ -234,6 +281,7 @@ public class RainState extends BaseAppState {
Node root = ((SimpleApplication) app).getRootNode(); Node root = ((SimpleApplication) app).getRootNode();
if (lightningLight != null) root.removeLight(lightningLight); if (lightningLight != null) root.removeLight(lightningLight);
if (rainNode != null) root.detachChild(rainNode); if (rainNode != null) root.detachChild(rainNode);
if (dustNode != null) root.detachChild(dustNode);
if (lightRainAudio != null) { lightRainAudio.stop(); root.detachChild(lightRainAudio); } if (lightRainAudio != null) { lightRainAudio.stop(); root.detachChild(lightRainAudio); }
if (heavyRainAudio != null) { heavyRainAudio.stop(); root.detachChild(heavyRainAudio); } if (heavyRainAudio != null) { heavyRainAudio.stop(); root.detachChild(heavyRainAudio); }
for (AudioNode tn : thunderAudio) { if (tn != null) root.detachChild(tn); } for (AudioNode tn : thunderAudio) { if (tn != null) root.detachChild(tn); }
@@ -292,6 +340,27 @@ public class RainState extends BaseAppState {
return new Texture2D(new Image(Image.Format.RGBA8, w, h, buf, ColorSpace.Linear)); return new Texture2D(new Image(Image.Format.RGBA8, w, h, buf, ColorSpace.Linear));
} }
private static Texture2D createDustTexture() {
int w = 32, h = 32;
ByteBuffer buf = ByteBuffer.allocateDirect(w * h * 4);
float cx = (w - 1) * 0.5f, cy = (h - 1) * 0.5f;
float maxR = w * 0.5f;
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
float dx = (x - cx) / maxR;
float dy = (y - cy) / maxR;
float r = (float) Math.sqrt(dx * dx + dy * dy);
float alpha = FastMath.clamp(1f - r * r * 2.5f, 0f, 1f);
buf.put((byte) 224); // R — gelblich
buf.put((byte) 184); // G
buf.put((byte) 96); // B — warm-bräunlich
buf.put((byte) (int) (alpha * 160));
}
}
buf.flip();
return new Texture2D(new Image(Image.Format.RGBA8, w, h, buf, ColorSpace.Linear));
}
private static AudioNode streamAudio(AssetManager assets, String path) { private static AudioNode streamAudio(AssetManager assets, String path) {
AudioNode n = new AudioNode(assets, path, AudioData.DataType.Stream); AudioNode n = new AudioNode(assets, path, AudioData.DataType.Stream);
n.setPositional(false); n.setPositional(false);

View File

@@ -327,9 +327,20 @@ public class WeatherState extends BaseAppState {
deepWaterColor.interpolateLocal(DEEP_WATER_COLOR[i], tpf * 0.020f); deepWaterColor.interpolateLocal(DEEP_WATER_COLOR[i], tpf * 0.020f);
if (fogFilter != null) { if (fogFilter != null) {
// Abendrot/Morgenröte: orange-rötlicher Dunst wenn Sonne nahe am Horizont
ColorRGBA tintedFog = fogColor.clone();
DayNightState dns = getApplication().getStateManager().getState(DayNightState.class);
if (dns != null) {
float elev = dns.getSunElevation();
float dawnT = FastMath.clamp(1f - Math.abs(elev - 0.08f) / 0.18f, 0f, 1f);
dawnT = dawnT * dawnT; // weichere Kurve
tintedFog.r = FastMath.clamp(tintedFog.r + 0.28f * dawnT, 0f, 1f);
tintedFog.g = FastMath.clamp(tintedFog.g - 0.06f * dawnT, 0f, 1f);
tintedFog.b = FastMath.clamp(tintedFog.b - 0.14f * dawnT, 0f, 1f);
}
fogFilter.setFogDensity(fogDensity); fogFilter.setFogDensity(fogDensity);
fogFilter.setFogDistance(fogDistance); fogFilter.setFogDistance(fogDistance);
fogFilter.setFogColor(fogColor.clone()); fogFilter.setFogColor(tintedFog);
} }
if (waterFilter != null) { if (waterFilter != null) {
waterFilter.setSpeed(waveSpeed); waterFilter.setSpeed(waveSpeed);
@@ -345,6 +356,10 @@ public class WeatherState extends BaseAppState {
if (lightScatterFilter != null) { if (lightScatterFilter != null) {
// Max 0.08 um Gras-Streifen zu vermeiden; Nebel/Wolken reduzieren weiter // Max 0.08 um Gras-Streifen zu vermeiden; Nebel/Wolken reduzieren weiter
float ld = Math.max(0f, 0.08f - fogDensity * 0.3f); float ld = Math.max(0f, 0.08f - fogDensity * 0.3f);
// God Rays verschwinden wenn Sonne hinter Wolken steckt
DayNightState dns = getApplication().getStateManager().getState(DayNightState.class);
float sunOcc = dns != null ? dns.getLastSunOcc() : 0f;
ld *= FastMath.clamp(1f - sunOcc * 0.95f, 0.05f, 1f);
lightScatterFilter.setLightDensity(ld); lightScatterFilter.setLightDensity(ld);
} }
} }

View File

@@ -59,6 +59,7 @@ menu.graphics.val.shadow.low=Niedrig
menu.graphics.val.shadow.medium=Mittel menu.graphics.val.shadow.medium=Mittel
menu.graphics.val.shadow.high=Hoch menu.graphics.val.shadow.high=Hoch
menu.graphics.row.postfx=Post-Effekte menu.graphics.row.postfx=Post-Effekte
menu.graphics.row.particles=Partikeleffekte
menu.graphics.val.resolution.native=Nativ menu.graphics.val.resolution.native=Nativ
menu.graphics.btn.apply=Übernehmen menu.graphics.btn.apply=Übernehmen
menu.graphics.btn.cancel=Abbrechen menu.graphics.btn.cancel=Abbrechen

View File

@@ -62,6 +62,7 @@ menu.graphics.val.shadow.low=Low
menu.graphics.val.shadow.medium=Medium menu.graphics.val.shadow.medium=Medium
menu.graphics.val.shadow.high=High menu.graphics.val.shadow.high=High
menu.graphics.row.postfx=Post Effects menu.graphics.row.postfx=Post Effects
menu.graphics.row.particles=Particle Effects
menu.graphics.val.resolution.native=Native menu.graphics.val.resolution.native=Native
menu.graphics.btn.apply=Apply menu.graphics.btn.apply=Apply
menu.graphics.btn.cancel=Cancel menu.graphics.btn.cancel=Cancel