Wasserfälle weiter verveinert

This commit is contained in:
2026-08-22 13:16:40 +02:00
parent 6ee404fc2c
commit 95552196b0
9 changed files with 308 additions and 41 deletions

View File

@@ -4,6 +4,9 @@ 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.EmitterSphereShape;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA;
@@ -13,7 +16,9 @@ import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.VertexBuffer;
import com.jme3.texture.Image;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D;
import com.jme3.util.BufferUtils;
import de.blight.common.PlacedWaterfall;
import de.blight.common.WaterfallIO;
@@ -35,13 +40,18 @@ public class WaterfallState extends BaseAppState {
private static final int ROWS = 24; // Tessellierungs-Zeilen vertikal
private static final int COLS = 6; // Tessellierungs-Spalten horizontal
private static final float BULGE_MAX = 1.2f; // Maximale horizontale Vorwölbung am oberen Rand (m)
private static final float V_RANGE = 0.30f; // Anteil der Wasserfallhöhe für den Bogen (0..1)
private static final float BULGE_MAX = 1.2f; // Maximale horizontale Vorwölbung am oberen Rand (m)
private static final float V_RANGE = 0.30f; // Anteil der Wasserfallhöhe für den Bogen (0..1)
private static final float SPLASH_HEIGHT = 0.9f; // Höhe des vertikalen Schaum-Vorhangs (m)
private Node rootNode;
private final List<Geometry> geos = new ArrayList<>();
private final List<Material> materials = new ArrayList<>();
private float time = 0f;
private final List<Geometry> geos = new ArrayList<>();
private final List<Material> materials = new ArrayList<>();
private final List<Geometry> foams = new ArrayList<>();
private final List<Material> foamMaterials = new ArrayList<>();
private final List<ParticleEmitter> mists = new ArrayList<>();
private float time = 0f;
private Texture2D softCircleTex = null; // lazily built, shared by alle Emitter
@Override
protected void initialize(Application app) {
@@ -74,14 +84,23 @@ public class WaterfallState extends BaseAppState {
for (Material m : materials) {
m.setFloat("Time", time);
}
for (int i = 0; i < foamMaterials.size(); i++) {
float alpha = 0.65f + 0.25f * (float) Math.sin(time * 2.8f + i * 1.1f);
foamMaterials.get(i).setColor("Color", new ColorRGBA(1f, 1f, 1f, alpha));
}
}
}
@Override
protected void cleanup(Application app) {
for (Geometry g : geos) g.removeFromParent();
for (Geometry g : geos) g.removeFromParent();
for (Geometry g : foams) g.removeFromParent();
for (ParticleEmitter e : mists) e.removeFromParent();
geos.clear();
materials.clear();
foams.clear();
foamMaterials.clear();
mists.clear();
}
@Override protected void onEnable() {}
@@ -97,6 +116,154 @@ public class WaterfallState extends BaseAppState {
rootNode.attachChild(geo);
geos.add(geo);
materials.add(mat);
buildFoam(wf, assets);
buildMist(wf, assets);
}
private void buildFoam(PlacedWaterfall wf, AssetManager assets) {
Vector3f a = new Vector3f(wf.ax(), wf.ay(), wf.az());
Vector3f b = new Vector3f(wf.bx(), wf.by(), wf.bz());
Vector3f c = new Vector3f(wf.cx(), wf.cy(), wf.cz());
Vector3f d = new Vector3f(wf.dx(), wf.dy(), wf.dz());
Vector3f outward = computeOutward(a, b, c, d);
float waterY = (wf.cy() + wf.dy()) * 0.5f;
float width = c.distance(d);
// Horizontale Scheibe: sichtbar von oben (Schaum auf der Wasseroberfläche)
Vector3f discCenter = c.add(d).multLocal(0.5f).addLocal(outward.mult(BULGE_MAX));
discCenter.y = waterY + 0.03f;
attachFoam(buildDiscMesh(width * 0.65f, 28), discCenter, assets);
// Vertikaler Vorhang: sichtbar von vorne/seitlich (Spritzwasser am Auftreffpunkt)
attachFoam(buildCurtainMesh(c, d, outward, waterY), null, assets);
}
private void attachFoam(Mesh mesh, Vector3f pos, AssetManager assets) {
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setBoolean("VertexColor", true);
mat.setColor("Color", new ColorRGBA(1f, 1f, 1f, 0.8f));
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
mat.getAdditionalRenderState().setDepthWrite(false);
Geometry geo = new Geometry("waterfall_foam", mesh);
geo.setMaterial(mat);
if (pos != null) geo.setLocalTranslation(pos);
geo.setQueueBucket(RenderQueue.Bucket.Transparent);
rootNode.attachChild(geo);
foams.add(geo);
foamMaterials.add(mat);
}
private void buildMist(PlacedWaterfall wf, AssetManager assets) {
Vector3f a = new Vector3f(wf.ax(), wf.ay(), wf.az());
Vector3f b = new Vector3f(wf.bx(), wf.by(), wf.bz());
Vector3f c = new Vector3f(wf.cx(), wf.cy(), wf.cz());
Vector3f d = new Vector3f(wf.dx(), wf.dy(), wf.dz());
Vector3f outward = computeOutward(a, b, c, d);
float waterY = (wf.cy() + wf.dy()) * 0.5f;
float width = c.distance(d);
// Emitter-Position: Auftreffpunkt des Wasserfalls auf der Wasserfläche
Vector3f pos = c.add(d).multLocal(0.5f).addLocal(outward.mult(BULGE_MAX));
pos.y = waterY;
ParticleEmitter mist = new ParticleEmitter("waterfall_mist", ParticleMesh.Type.Triangle, 60);
Material mat = new Material(assets, "Common/MatDefs/Misc/Particle.j3md");
mat.setTexture("Texture", getSoftCircleTex());
mist.setMaterial(mat);
// Partikel über die Wasserfallbreite verteilen
mist.setShape(new EmitterSphereShape(Vector3f.ZERO, width * 0.4f));
mist.setParticlesPerSec(14);
mist.setGravity(0f, 0f, 0f); // kein Schwerkrafteinfluss → Partikel steigen
mist.setLowLife(2.5f);
mist.setHighLife(5.0f);
mist.getParticleInfluencer().setInitialVelocity(new Vector3f(0f, 1.1f, 0f));
mist.getParticleInfluencer().setVelocityVariation(0.45f);
mist.setStartSize(0.25f);
mist.setEndSize(1.6f);
mist.setStartColor(new ColorRGBA(1f, 1f, 1f, 0.55f));
mist.setEndColor(new ColorRGBA(1f, 1f, 1f, 0f));
mist.setQueueBucket(RenderQueue.Bucket.Transparent);
mist.setLocalTranslation(pos);
rootNode.attachChild(mist);
mists.add(mist);
}
/** Senkrechter Schaum-Vorhang am Auftreffpunkt — unten opak, oben transparent. */
private static Mesh buildCurtainMesh(Vector3f c, Vector3f d, Vector3f outward, float waterY) {
float ox = outward.x * BULGE_MAX, oz = outward.z * BULGE_MAX;
// Eckpunkte: 0=BL, 1=BR, 2=TL, 3=TR (B=bottom, T=top, L=d-Seite, R=c-Seite)
float[] vx = { d.x + ox, c.x + ox, d.x + ox, c.x + ox };
float[] vy = { waterY, waterY, waterY + SPLASH_HEIGHT, waterY + SPLASH_HEIGHT };
float[] vz = { d.z + oz, c.z + oz, d.z + oz, c.z + oz };
float[] va = { 0.9f, 0.9f, 0f, 0f }; // unten opak, oben transparent
FloatBuffer pos = BufferUtils.createFloatBuffer(12);
FloatBuffer colors = BufferUtils.createFloatBuffer(16);
IntBuffer idx = BufferUtils.createIntBuffer(6);
for (int i = 0; i < 4; i++) {
pos.put(vx[i]).put(vy[i]).put(vz[i]);
colors.put(1f).put(1f).put(1f).put(va[i]);
}
idx.put(0).put(1).put(2).put(1).put(3).put(2);
pos.rewind(); colors.rewind(); idx.rewind();
Mesh mesh = new Mesh();
mesh.setBuffer(VertexBuffer.Type.Position, 3, pos);
mesh.setBuffer(VertexBuffer.Type.Color, 4, colors);
mesh.setBuffer(VertexBuffer.Type.Index, 3, idx);
mesh.updateBound();
mesh.updateCounts();
return mesh;
}
private static Vector3f computeOutward(Vector3f a, Vector3f b, Vector3f c, Vector3f d) {
Vector3f n1 = d.subtract(a).cross(b.subtract(a)).normalizeLocal();
Vector3f n2 = b.subtract(c).cross(d.subtract(c)).normalizeLocal();
Vector3f avg = n1.add(n2).normalizeLocal();
if (avg.lengthSquared() < 1e-4f) avg.set(0f, 0f, 1f);
Vector3f out = new Vector3f(avg.x, 0f, avg.z);
if (out.lengthSquared() < 1e-4f) out.set(avg);
else out.normalizeLocal();
return out;
}
/** Flache Kreisscheibe in der XZ-Ebene mit radialem Alpha-Gradient (Mitte opak → Rand transparent). */
private static Mesh buildDiscMesh(float radius, int segments) {
int vCount = segments + 1; // Mittelpunkt + Rand
FloatBuffer pos = BufferUtils.createFloatBuffer(vCount * 3);
FloatBuffer colors = BufferUtils.createFloatBuffer(vCount * 4);
IntBuffer idx = BufferUtils.createIntBuffer(segments * 3);
// Mittelpunkt
pos.put(0f).put(0f).put(0f);
colors.put(1f).put(1f).put(1f).put(1f);
// Randpunkte
for (int i = 0; i < segments; i++) {
float angle = (float) (2 * Math.PI * i / segments);
pos.put((float) Math.cos(angle) * radius).put(0f).put((float) Math.sin(angle) * radius);
colors.put(1f).put(1f).put(1f).put(0f); // Rand transparent
}
// Dreiecke: Fächer vom Mittelpunkt
for (int i = 0; i < segments; i++) {
idx.put(0).put(i + 1).put((i + 1) % segments + 1);
}
pos.rewind(); colors.rewind(); idx.rewind();
Mesh mesh = new Mesh();
mesh.setBuffer(VertexBuffer.Type.Position, 3, pos);
mesh.setBuffer(VertexBuffer.Type.Color, 4, colors);
mesh.setBuffer(VertexBuffer.Type.Index, 3, idx);
mesh.updateBound();
mesh.updateCounts();
return mesh;
}
/**
@@ -116,16 +283,9 @@ public class WaterfallState extends BaseAppState {
int vertCount = vRows * vCols;
int triCount = ROWS * COLS * 2;
// Referenz-Normale (nach außen zeigend) aus Winding A top-left, B top-right, D bottom-left
Vector3f n1 = d.subtract(a).cross(b.subtract(a)).normalizeLocal();
Vector3f n2 = b.subtract(c).cross(d.subtract(c)).normalizeLocal();
Vector3f avgNorm = n1.add(n2).normalizeLocal();
if (avgNorm.lengthSquared() < 1e-4f) avgNorm.set(0f, 0f, 1f);
// Horizontale Outward-Richtung für die Wölbung (Y=0, normiert)
Vector3f outward = new Vector3f(avgNorm.x, 0f, avgNorm.z);
if (outward.lengthSquared() < 1e-4f) outward.set(avgNorm); // Fallback für horizontale Fläche
else outward.normalizeLocal();
// Outward-Richtung für die Wölbung und die Normalen-Referenz
Vector3f outward = computeOutward(a, b, c, d);
Vector3f avgNorm = outward; // für den Vorzeichen-Check der Normalen ausreichend
// Pass 1: alle Vertex-Positionen mit Bulge berechnen und zwischenspeichern
Vector3f[][] positions = new Vector3f[vRows][vCols];
@@ -228,4 +388,30 @@ public class WaterfallState extends BaseAppState {
if (fallback == null) return null;
try { return assets.loadTexture(fallback); } catch (Exception ignored) { return null; }
}
/**
* Weiche Kreisblob-Textur (64×64 RGBA), procedural erzeugt.
* Mitte weiß-opak, Rand vollständig transparent — quadratischer Abfall.
* Wird einmal gebaut und für alle Emitter geteilt.
*/
private Texture2D getSoftCircleTex() {
if (softCircleTex != null) return softCircleTex;
int size = 64;
java.nio.ByteBuffer buf = java.nio.ByteBuffer.allocateDirect(size * size * 4);
float c = (size - 1) / 2f;
for (int y = 0; y < size; y++) {
for (int x = 0; x < size; x++) {
float dx = (x - c) / c, dy = (y - c) / c;
float d2 = dx * dx + dy * dy;
float alpha = (d2 < 1f) ? (1f - d2) * (1f - d2) : 0f; // glatter Rand
buf.put((byte) 255); // R
buf.put((byte) 255); // G
buf.put((byte) 255); // B
buf.put((byte) Math.round(alpha * 255)); // A
}
}
buf.flip();
softCircleTex = new Texture2D(new Image(Image.Format.RGBA8, size, size, buf));
return softCircleTex;
}
}