Weiter an den Grafik Effekten gearbeitet
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user