diff --git a/blight-assets/src/main/resources/audio/River Loop.ogg b/blight-assets/src/main/resources/audio/River Loop.ogg new file mode 100644 index 0000000..46f73c1 Binary files /dev/null and b/blight-assets/src/main/resources/audio/River Loop.ogg differ diff --git a/blight-assets/src/main/resources/audio/River Stream Loop.ogg b/blight-assets/src/main/resources/audio/River Stream Loop.ogg new file mode 100644 index 0000000..745bec1 Binary files /dev/null and b/blight-assets/src/main/resources/audio/River Stream Loop.ogg differ diff --git a/blight-assets/src/main/resources/audio/Waterfall Loop.ogg b/blight-assets/src/main/resources/audio/Waterfall Loop.ogg new file mode 100644 index 0000000..2416d79 Binary files /dev/null and b/blight-assets/src/main/resources/audio/Waterfall Loop.ogg differ diff --git a/blight-game/src/main/java/de/blight/game/state/WaterBodyState.java b/blight-game/src/main/java/de/blight/game/state/WaterBodyState.java index 63e2f9f..c502916 100644 --- a/blight-game/src/main/java/de/blight/game/state/WaterBodyState.java +++ b/blight-game/src/main/java/de/blight/game/state/WaterBodyState.java @@ -15,13 +15,18 @@ 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.PlacedModel; +import de.blight.common.PlacedModelIO; import de.blight.common.PlacedWater; import de.blight.common.WaterBodyIO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.util.ArrayList; @@ -47,6 +52,9 @@ public class WaterBodyState extends BaseAppState { private final List filters = new ArrayList<>(); private final List inclinedGeos = new ArrayList<>(); private final List animatedMaterials = new ArrayList<>(); + private final List objectFoams = new ArrayList<>(); + private final List objectFoamMats = new ArrayList<>(); + private Texture2D foamRingTex = null; private float time = 0f; private Node rootNode; @@ -85,15 +93,20 @@ public class WaterBodyState extends BaseAppState { } } log.info("{} flache + {} abschüssige Wasserfläche(n) geladen.", flat, inclined); + // buildObjectFoam(assets, bodies); // TODO: Objekt-Schaum deaktiviert } @Override public void update(float tpf) { - if (!animatedMaterials.isEmpty()) { + if (!animatedMaterials.isEmpty() || !objectFoamMats.isEmpty()) { time += tpf; for (Material m : animatedMaterials) { m.setFloat("Time", time); } + for (int i = 0; i < objectFoamMats.size(); i++) { + float a = 0.22f + 0.12f * (float) Math.sin(time * 2.2f + i * 1.3f); + objectFoamMats.get(i).setColor("Color", new ColorRGBA(1f, 1f, 1f, a)); + } } } @@ -104,6 +117,9 @@ public class WaterBodyState extends BaseAppState { for (Geometry g : inclinedGeos) rootNode.detachChild(g); inclinedGeos.clear(); animatedMaterials.clear(); + for (Geometry g : objectFoams) rootNode.detachChild(g); + objectFoams.clear(); + objectFoamMats.clear(); } @Override protected void onEnable() {} @@ -144,6 +160,109 @@ public class WaterBodyState extends BaseAppState { } } + // ── Objekt-Schaum ───────────────────────────────────────────────────────── + + /** Meereshöhe für den globalen DynamicWaterFilter (kein PlacedWater-Body). */ + private static final float SEA_LEVEL = 0f; + + private void buildObjectFoam(AssetManager assets, List bodies) { + List flatBodies = bodies.stream().filter(PlacedWater::isFlat).toList(); + + List models; + try { + models = PlacedModelIO.load(); + } catch (Exception e) { + log.warn("[WaterBodyState] Objekte für Objekt-Schaum nicht ladbar: {}", e.getMessage()); + return; + } + + int count = 0; + for (PlacedModel m : models) { + boolean added = false; + + // 1. Definierte flache Wasserkörper (Seen, Teiche …) + for (PlacedWater body : flatBodies) { + if (!pointInPolygon(m.x(), m.z(), body.pointsX(), body.pointsZ())) continue; + if (m.y() > body.waterHeight() + 1.0f) continue; + if (m.y() < body.waterHeight() - 6.0f) continue; + + attachObjectFoam(new Vector3f(m.x(), body.waterHeight() + 0.04f, m.z()), + m.scale(), assets); + count++; + added = true; + break; + } + + // 2. Meereshöhe (globaler DynamicWaterFilter, kein Polygon nötig) + if (!added && m.y() <= SEA_LEVEL + 1.0f && m.y() >= SEA_LEVEL - 6.0f) { + attachObjectFoam(new Vector3f(m.x(), SEA_LEVEL + 0.04f, m.z()), + m.scale(), assets); + count++; + } + } + log.info("[WaterBodyState] {} Objekt-Schaum-Scheibe(n) platziert.", count); + } + + private void attachObjectFoam(Vector3f center, float scale, AssetManager assets) { + float r = com.jme3.math.FastMath.clamp(scale * 0.5f, 0.4f, 2.0f); + // Horizontales Quad in der XZ-Ebene, texturiert mit Ring-Textur + FloatBuffer pos = BufferUtils.createFloatBuffer(new float[]{ -r,0,-r, r,0,-r, r,0,r, -r,0,r }); + FloatBuffer uv = BufferUtils.createFloatBuffer(new float[]{ 0,0, 1,0, 1,1, 0,1 }); + FloatBuffer norm = BufferUtils.createFloatBuffer(new float[]{ 0,1,0, 0,1,0, 0,1,0, 0,1,0 }); + IntBuffer idx = BufferUtils.createIntBuffer(new int[]{ 0,1,2, 0,2,3 }); + Mesh mesh = new Mesh(); + mesh.setBuffer(VertexBuffer.Type.Position, 3, pos); + mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, uv); + mesh.setBuffer(VertexBuffer.Type.Normal, 3, norm); + mesh.setBuffer(VertexBuffer.Type.Index, 3, idx); + mesh.updateBound(); mesh.updateCounts(); + + Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setTexture("ColorMap", getFoamRingTex()); + mat.setColor("Color", new ColorRGBA(1f, 1f, 1f, 0.28f)); + mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); + + Geometry geo = new Geometry("obj_foam", mesh); + geo.setMaterial(mat); + geo.setLocalTranslation(center); + geo.setQueueBucket(RenderQueue.Bucket.Transparent); + rootNode.attachChild(geo); + objectFoams.add(geo); + objectFoamMats.add(mat); + } + + /** Ring-Textur: transparent innen + außen, opak in der Mitte — sin(π*d)-Profil. */ + private Texture2D getFoamRingTex() { + if (foamRingTex != null) return foamRingTex; + int size = 64; + ByteBuffer buf = 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 d = (float) Math.sqrt(dx * dx + dy * dy); // 0=Mitte, 1=Rand + float a = (d < 1f) ? (float) Math.sin(Math.PI * d) : 0f; + buf.put((byte) 255).put((byte) 255).put((byte) 255).put((byte)(a * 255)); + } + } + buf.flip(); + foamRingTex = new Texture2D(new Image(Image.Format.RGBA8, size, size, buf)); + return foamRingTex; + } + + private static boolean pointInPolygon(float px, float pz, float[] xs, float[] zs) { + boolean inside = false; + int n = xs.length; + for (int i = 0, j = n - 1; i < n; j = i++) { + if (((zs[i] > pz) != (zs[j] > pz)) && + (px < (xs[j] - xs[i]) * (pz - zs[i]) / (zs[j] - zs[i]) + xs[i])) { + inside = !inside; + } + } + return inside; + } + /** * Baut das Wasserfall-Mesh aus den gespeicherten Eckpunkt-Koordinaten. * Y wird zwischen Nachbar-Ecken linear interpoliert – kein Terrain-Raycast, diff --git a/blight-game/src/main/java/de/blight/game/state/WaterInteractionState.java b/blight-game/src/main/java/de/blight/game/state/WaterInteractionState.java index 9fc7c5c..1756a78 100644 --- a/blight-game/src/main/java/de/blight/game/state/WaterInteractionState.java +++ b/blight-game/src/main/java/de/blight/game/state/WaterInteractionState.java @@ -1,11 +1,22 @@ package de.blight.game.state; import com.jme3.app.Application; +import com.jme3.app.SimpleApplication; import com.jme3.app.state.BaseAppState; import com.jme3.bullet.control.CharacterControl; +import com.jme3.material.Material; +import com.jme3.material.RenderState; +import com.jme3.math.ColorRGBA; import com.jme3.math.FastMath; import com.jme3.math.Vector2f; import com.jme3.math.Vector3f; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.scene.VertexBuffer; +import com.jme3.util.BufferUtils; import com.jme3.texture.Image; import com.jme3.texture.Texture; import com.jme3.texture.Texture2D; @@ -15,6 +26,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.nio.IntBuffer; import java.util.Arrays; /** @@ -37,9 +50,9 @@ public class WaterInteractionState extends BaseAppState { private static final int N = 256; private static final float HALF_EXTENT = 64f; // half of 128m world coverage private static final float INV_EXTENT = 1.0f / (HALF_EXTENT * 2f); - private static final float WAVE_K = 0.25f; // wave eq. coefficient (stability: ≤ 0.5) - private static final float DAMPING = 0.995f; - private static final float WAVE_STRENGTH = 6.0f; // normal perturbation scale in shader + private static final float WAVE_K = 0.004f; // wave eq. coefficient — speed = sqrt(K)*0.5m*fps ≈ sqrt(0.004)*30 ≈ 1.9 m/s + private static final float DAMPING = 0.985f; // stärkere Dämpfung da langsame Wellen länger verweilen + private static final float WAVE_STRENGTH = 1.5f; // normal perturbation scale in shader private static final float BLOB_RADIUS_TEX = 3.0f; // blob radius in texels private static final float BLOB_COOLDOWN = 0.07f; // seconds between blob injections private static final float RECENTER_THRESH = HALF_EXTENT * 0.45f; // ~29m @@ -66,6 +79,12 @@ public class WaterInteractionState extends BaseAppState { private float waterHeight = 0f; private final Vector3f prevPos = new Vector3f(Float.NaN, 0f, Float.NaN); + // ── Player foam ────────────────────────────────────────────────────────── + + private Geometry foamGeo; + private Material foamMat; + private float foamTime = 0f; + public WaterInteractionState(DynamicWaterFilter filter) { this.dynFilter = filter; } @@ -100,6 +119,8 @@ public class WaterInteractionState extends BaseAppState { waveTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps); waveTex.setWrap(Texture.WrapMode.EdgeClamp); + // buildPlayerFoam(app); // TODO: Spieler-Schaum deaktiviert + log.debug("[WaterInteraction] Simulation initialized ({}×{}, {}m coverage)", N, N, HALF_EXTENT * 2f); } @@ -109,6 +130,7 @@ public class WaterInteractionState extends BaseAppState { waveBuf = null; waveImg = null; waveTex = null; + if (foamGeo != null) foamGeo.removeFromParent(); } @Override protected void onEnable() {} @@ -125,6 +147,7 @@ public class WaterInteractionState extends BaseAppState { paramsApplied = true; } + boolean playerInWater = false; if (playerControl != null) { Vector3f pos = playerControl.getPhysicsLocation(); @@ -132,11 +155,12 @@ public class WaterInteractionState extends BaseAppState { float feetY = pos.y - 0.9f; // CharacterControl position = capsule center, ~0.9m above feet if (feetY < waterHeight + 0.4f) { // player in or just above water + playerInWater = true; float speed = Float.isNaN(prevPos.x) ? 0f : pos.distance(prevPos) / tpf; if (speed > 0.3f) { // ignore sub-threshold shuffling blobTimer -= tpf; if (blobTimer <= 0f) { - float strength = FastMath.clamp(speed / 8.0f, 0.1f, 1.0f); + float strength = FastMath.clamp(speed / 8.0f, 0.05f, 0.3f); addBlob(pos.x, pos.z, strength); blobTimer = BLOB_COOLDOWN; } @@ -145,6 +169,8 @@ public class WaterInteractionState extends BaseAppState { prevPos.set(pos); } + // updatePlayerFoam(tpf, playerInWater); // TODO: Spieler-Schaum deaktiviert + runSimulation(); uploadTexture(); } @@ -210,4 +236,63 @@ public class WaterInteractionState extends BaseAppState { waveBuf.rewind(); waveImg.setUpdateNeeded(); } + + // ── Player foam ────────────────────────────────────────────────────────── + + private void buildPlayerFoam(Application app) { + Node root = ((SimpleApplication) app).getRootNode(); + float r = 0.38f; + FloatBuffer pos = BufferUtils.createFloatBuffer(new float[]{ -r,0,-r, r,0,-r, r,0,r, -r,0,r }); + FloatBuffer uv = BufferUtils.createFloatBuffer(new float[]{ 0,0, 1,0, 1,1, 0,1 }); + FloatBuffer norm = BufferUtils.createFloatBuffer(new float[]{ 0,1,0, 0,1,0, 0,1,0, 0,1,0 }); + IntBuffer idx = BufferUtils.createIntBuffer(new int[]{ 0,1,2, 0,2,3 }); + Mesh mesh = new Mesh(); + mesh.setBuffer(VertexBuffer.Type.Position, 3, pos); + mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, uv); + mesh.setBuffer(VertexBuffer.Type.Normal, 3, norm); + mesh.setBuffer(VertexBuffer.Type.Index, 3, idx); + mesh.updateBound(); mesh.updateCounts(); + + foamMat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md"); + foamMat.setTexture("ColorMap", buildFoamRingTex()); + foamMat.setColor("Color", new ColorRGBA(1f, 1f, 1f, 0.30f)); + foamMat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + foamMat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); + + foamGeo = new Geometry("player_foam", mesh); + foamGeo.setMaterial(foamMat); + foamGeo.setQueueBucket(RenderQueue.Bucket.Transparent); + foamGeo.setCullHint(Spatial.CullHint.Always); + root.attachChild(foamGeo); + } + + private void updatePlayerFoam(float tpf, boolean inWater) { + if (foamGeo == null || playerControl == null) return; + if (inWater) { + Vector3f pos = playerControl.getPhysicsLocation(); + foamGeo.setLocalTranslation(pos.x, waterHeight + 0.04f, pos.z); + foamGeo.setCullHint(Spatial.CullHint.Inherit); + foamTime += tpf; + float a = 0.20f + 0.12f * FastMath.sin(foamTime * 2.5f); + foamMat.setColor("Color", new ColorRGBA(1f, 1f, 1f, a)); + } else { + foamGeo.setCullHint(Spatial.CullHint.Always); + } + } + + private static Texture2D buildFoamRingTex() { + int size = 64; + ByteBuffer buf = 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 d = (float) Math.sqrt(dx * dx + dy * dy); + float a = (d < 1f) ? (float) Math.sin(Math.PI * d) : 0f; + buf.put((byte) 255).put((byte) 255).put((byte) 255).put((byte)(a * 255)); + } + } + buf.flip(); + return new Texture2D(new Image(Image.Format.RGBA8, size, size, buf)); + } }