diff --git a/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf1.png b/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf1.png new file mode 100644 index 0000000..e044a5f Binary files /dev/null and b/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf1.png differ diff --git a/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf2.png b/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf2.png new file mode 100644 index 0000000..726ced3 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf2.png differ diff --git a/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf3.png b/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf3.png new file mode 100644 index 0000000..a6bae54 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf3.png differ diff --git a/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf4.png b/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf4.png new file mode 100644 index 0000000..266f309 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf4.png differ diff --git a/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf5.png b/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf5.png new file mode 100644 index 0000000..78fdd92 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf5.png differ diff --git a/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf6.png b/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf6.png new file mode 100644 index 0000000..58255a2 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf6.png differ diff --git a/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf7.png b/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf7.png new file mode 100644 index 0000000..716f078 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf7.png differ diff --git a/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf8.png b/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf8.png new file mode 100644 index 0000000..79e4108 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf8.png differ diff --git a/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java b/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java index 0fcd1b8..86316af 100644 --- a/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java +++ b/blight-game/src/main/java/de/blight/game/config/GraphicsScreen.java @@ -31,7 +31,8 @@ public class GraphicsScreen extends MenuScreen { private static final int ROW_VIEWDIST = 4; private static final int ROW_SHADOW = 5; 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 GraphicsSettings edit; @@ -62,9 +63,10 @@ public class GraphicsScreen extends MenuScreen { edit.fullscreen = live.fullscreen; edit.vsync = live.vsync; edit.samples = live.samples; - edit.viewDistance = live.viewDistance; - edit.shadowQuality = live.shadowQuality; - edit.postFxQuality = live.postFxQuality; + edit.viewDistance = live.viewDistance; + edit.shadowQuality = live.shadowQuality; + edit.postFxQuality = live.postFxQuality; + edit.particleQuality = live.particleQuality; resIdx = 0; for (int i = 0; i < resolutions.length; i++) { @@ -89,7 +91,8 @@ public class GraphicsScreen extends MenuScreen { "menu.graphics.row.aa", "menu.graphics.row.viewdist", "menu.graphics.row.shadow", - "menu.graphics.row.postfx" + "menu.graphics.row.postfx", + "menu.graphics.row.particles" }; float lblX = CONT_X + 30f; @@ -153,6 +156,11 @@ public class GraphicsScreen extends MenuScreen { case MEDIUM -> t("menu.graphics.val.shadow.medium"); 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 -> ""; }; BitmapText vt = valTexts[row]; @@ -190,29 +198,36 @@ public class GraphicsScreen extends MenuScreen { GraphicsSettings.PostFxQuality[] pfqs = GraphicsSettings.PostFxQuality.values(); edit.postFxQuality = pfqs[(edit.postFxQuality.ordinal() + dir + pfqs.length) % pfqs.length]; break; + case ROW_PARTICLES: + GraphicsSettings.ParticleQuality[] pqs = GraphicsSettings.ParticleQuality.values(); + edit.particleQuality = pqs[(edit.particleQuality.ordinal() + dir + pqs.length) % pqs.length]; + break; } refreshText(row); } private void applyAndSave() { - boolean shadowChanged = live.shadowQuality != edit.shadowQuality; - boolean postFxChanged = live.postFxQuality != edit.postFxQuality; + boolean shadowChanged = live.shadowQuality != edit.shadowQuality; + boolean postFxChanged = live.postFxQuality != edit.postFxQuality; + boolean particleChanged = live.particleQuality != edit.particleQuality; boolean needsRestart = live.width != edit.width || live.height != edit.height || live.fullscreen != edit.fullscreen || live.vsync != edit.vsync || live.samples != edit.samples || shadowChanged - || postFxChanged; + || postFxChanged + || particleChanged; live.width = edit.width; live.height = edit.height; live.fullscreen = edit.fullscreen; live.vsync = edit.vsync; live.samples = edit.samples; - live.viewDistance = edit.viewDistance; - live.shadowQuality = edit.shadowQuality; - live.postFxQuality = edit.postFxQuality; + live.viewDistance = edit.viewDistance; + live.shadowQuality = edit.shadowQuality; + live.postFxQuality = edit.postFxQuality; + live.particleQuality = edit.particleQuality; GraphicsStore.save(live); WeatherState ws = getApplication().getStateManager().getState(WeatherState.class); diff --git a/blight-game/src/main/java/de/blight/game/config/GraphicsSettings.java b/blight-game/src/main/java/de/blight/game/config/GraphicsSettings.java index 5072877..afece6f 100644 --- a/blight-game/src/main/java/de/blight/game/config/GraphicsSettings.java +++ b/blight-game/src/main/java/de/blight/game/config/GraphicsSettings.java @@ -1,16 +1,17 @@ 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 ViewDistance viewDistance = ViewDistance.NORMAL; - public ShadowQuality shadowQuality = ShadowQuality.MEDIUM; + 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 ShadowQuality shadowQuality = ShadowQuality.MEDIUM; /** Verstecktes Debug-Flag: erzwingt LOD2 (Impostor) für alle Objekte. Nur per Konsole setzbar. */ - public boolean debugLod2 = false; - public PostFxQuality postFxQuality = PostFxQuality.HIGH; + public boolean debugLod2 = false; + public PostFxQuality postFxQuality = PostFxQuality.HIGH; + public ParticleQuality particleQuality = ParticleQuality.HIGH; public enum PostFxQuality { // fxaa toneMap bloomI bloomExp godRays @@ -68,6 +69,10 @@ public class GraphicsSettings { } } + public enum ParticleQuality { + OFF, MEDIUM, HIGH + } + public enum ViewDistance { SHORT (1.00f, 1, 3), NORMAL(1.60f, 2, 5), diff --git a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java index 688d8ff..cbf6e81 100644 --- a/blight-game/src/main/java/de/blight/game/scene/WorldScene.java +++ b/blight-game/src/main/java/de/blight/game/scene/WorldScene.java @@ -813,6 +813,10 @@ public class WorldScene extends BaseAppState { weather.setDayTime(dayNight.getDayTime()); app.getStateManager().attach(weather); 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) { log.warn("[WorldScene] Post-Processing nicht verfügbar: {}", e.getMessage()); } diff --git a/blight-game/src/main/java/de/blight/game/state/AmbientParticlesState.java b/blight-game/src/main/java/de/blight/game/state/AmbientParticlesState.java new file mode 100644 index 0000000..8fb19cc --- /dev/null +++ b/blight-game/src/main/java/de/blight/game/state/AmbientParticlesState.java @@ -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 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 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)); + } +} diff --git a/blight-game/src/main/java/de/blight/game/state/DayNightState.java b/blight-game/src/main/java/de/blight/game/state/DayNightState.java index 1fe4b70..94426f3 100644 --- a/blight-game/src/main/java/de/blight/game/state/DayNightState.java +++ b/blight-game/src/main/java/de/blight/game/state/DayNightState.java @@ -75,6 +75,8 @@ public class DayNightState extends BaseAppState implements TimeListener { private float lastSunOcc = 0f; /** 1.0 = klar, sinkt mit zunehmender Wolkendecke (min 0.20). */ 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(); /** true = Spiel-Beleuchtungswerte, false = Editor-Werte. Standardmäßig = withShadows. */ 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. */ 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 protected void cleanup(Application app) { dayTime.removeListener(this); @@ -389,6 +396,7 @@ public class DayNightState extends BaseAppState implements TimeListener { skyControl.getSunAndStars().setHour(t * 24f); Vector3f toSun = skyControl.getSunAndStars().sunDirection(null); float elevation = toSun.y; // +1 = Zenit, 0 = Horizont, -1 = Nadir + this.sunElevation = elevation; float elevC = FastMath.clamp(elevation, 0f, 1f); // Spiel vs. Editor: unterschiedliche Helligkeitswerte aus LightingConfig @@ -417,8 +425,20 @@ public class DayNightState extends BaseAppState implements TimeListener { ColorRGBA custDayTinted = custDay.mult(sunTint); float ambFactor = FastMath.clamp( (elevation - LightingConfig.AMB_ELEV_LOW) / LightingConfig.AMB_ELEV_RANGE, 0f, 1f); - ambient.setColor(ambNight.clone().interpolateLocal(ambDayTinted, ambFactor)); - customAmbient.set(custNight.clone().interpolateLocal(custDayTinted, ambFactor)); + // Mondlicht: Vollmond hellt die Nacht bläulich-weiß auf (0=Neumond, 1=Vollmond) + 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. // Wolken-Okklusion wird jedes Frame separat in update() berechnet. diff --git a/blight-game/src/main/java/de/blight/game/state/RainState.java b/blight-game/src/main/java/de/blight/game/state/RainState.java index ede5271..f0508e8 100644 --- a/blight-game/src/main/java/de/blight/game/state/RainState.java +++ b/blight-game/src/main/java/de/blight/game/state/RainState.java @@ -47,6 +47,12 @@ public class RainState extends BaseAppState { private ParticleEmitter rainEmitter; private Node rainNode; + // ── Calima-Staub ────────────────────────────────────────────────────────── + @SuppressWarnings("deprecation") + private ParticleEmitter dustEmitter; + private Node dustNode; + private float dustIntensity = 0f; + // ── Audio ───────────────────────────────────────────────────────────────── private AudioNode lightRainAudio; private AudioNode heavyRainAudio; @@ -109,6 +115,33 @@ public class RainState extends BaseAppState { rainNode.attachChild(rainEmitter); 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) lightRainAudio = streamAudio(assets, "audio/ambient/weather/leichter_regen.ogg"); heavyRainAudio = streamAudio(assets, "audio/ambient/weather/starker_regen.ogg"); @@ -159,6 +192,19 @@ public class RainState extends BaseAppState { 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 ───────────────────────────────────────────────────────────── 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); @@ -222,6 +268,7 @@ public class RainState extends BaseAppState { @Override protected void onDisable() { if (rainEmitter != null) rainEmitter.setParticlesPerSec(0); + if (dustEmitter != null) dustEmitter.setParticlesPerSec(0); if (lightRainAudio != null) lightRainAudio.setVolume(0); if (heavyRainAudio != null) heavyRainAudio.setVolume(0); } @@ -234,6 +281,7 @@ public class RainState extends BaseAppState { Node root = ((SimpleApplication) app).getRootNode(); if (lightningLight != null) root.removeLight(lightningLight); if (rainNode != null) root.detachChild(rainNode); + if (dustNode != null) root.detachChild(dustNode); if (lightRainAudio != null) { lightRainAudio.stop(); root.detachChild(lightRainAudio); } if (heavyRainAudio != null) { heavyRainAudio.stop(); root.detachChild(heavyRainAudio); } 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)); } + 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) { AudioNode n = new AudioNode(assets, path, AudioData.DataType.Stream); n.setPositional(false); diff --git a/blight-game/src/main/java/de/blight/game/state/WeatherState.java b/blight-game/src/main/java/de/blight/game/state/WeatherState.java index e173c82..6debc8e 100644 --- a/blight-game/src/main/java/de/blight/game/state/WeatherState.java +++ b/blight-game/src/main/java/de/blight/game/state/WeatherState.java @@ -327,9 +327,20 @@ public class WeatherState extends BaseAppState { deepWaterColor.interpolateLocal(DEEP_WATER_COLOR[i], tpf * 0.020f); 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.setFogDistance(fogDistance); - fogFilter.setFogColor(fogColor.clone()); + fogFilter.setFogColor(tintedFog); } if (waterFilter != null) { waterFilter.setSpeed(waveSpeed); @@ -345,6 +356,10 @@ public class WeatherState extends BaseAppState { if (lightScatterFilter != null) { // Max 0.08 um Gras-Streifen zu vermeiden; Nebel/Wolken reduzieren weiter 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); } } diff --git a/blight-lang/src/main/resources/lang/messages_de.properties b/blight-lang/src/main/resources/lang/messages_de.properties index 8f04a3b..ca7487d 100644 --- a/blight-lang/src/main/resources/lang/messages_de.properties +++ b/blight-lang/src/main/resources/lang/messages_de.properties @@ -59,6 +59,7 @@ menu.graphics.val.shadow.low=Niedrig menu.graphics.val.shadow.medium=Mittel menu.graphics.val.shadow.high=Hoch menu.graphics.row.postfx=Post-Effekte +menu.graphics.row.particles=Partikeleffekte menu.graphics.val.resolution.native=Nativ menu.graphics.btn.apply=Übernehmen menu.graphics.btn.cancel=Abbrechen diff --git a/blight-lang/src/main/resources/lang/messages_en.properties b/blight-lang/src/main/resources/lang/messages_en.properties index 11363ed..8eba328 100644 --- a/blight-lang/src/main/resources/lang/messages_en.properties +++ b/blight-lang/src/main/resources/lang/messages_en.properties @@ -62,6 +62,7 @@ menu.graphics.val.shadow.low=Low menu.graphics.val.shadow.medium=Medium menu.graphics.val.shadow.high=High menu.graphics.row.postfx=Post Effects +menu.graphics.row.particles=Particle Effects menu.graphics.val.resolution.native=Native menu.graphics.btn.apply=Apply menu.graphics.btn.cancel=Cancel