Bewegung im Wasser bearbeitet

This commit is contained in:
2026-08-22 18:39:19 +02:00
parent 95552196b0
commit f26a15b3b0
5 changed files with 209 additions and 5 deletions

Binary file not shown.

View File

@@ -15,13 +15,18 @@ import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh; import com.jme3.scene.Mesh;
import com.jme3.scene.Node; import com.jme3.scene.Node;
import com.jme3.scene.VertexBuffer; import com.jme3.scene.VertexBuffer;
import com.jme3.texture.Image;
import com.jme3.texture.Texture; import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D;
import com.jme3.util.BufferUtils; import com.jme3.util.BufferUtils;
import de.blight.common.PlacedModel;
import de.blight.common.PlacedModelIO;
import de.blight.common.PlacedWater; import de.blight.common.PlacedWater;
import de.blight.common.WaterBodyIO; import de.blight.common.WaterBodyIO;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer; import java.nio.FloatBuffer;
import java.nio.IntBuffer; import java.nio.IntBuffer;
import java.util.ArrayList; import java.util.ArrayList;
@@ -47,6 +52,9 @@ public class WaterBodyState extends BaseAppState {
private final List<WaterPolygonFilter> filters = new ArrayList<>(); private final List<WaterPolygonFilter> filters = new ArrayList<>();
private final List<Geometry> inclinedGeos = new ArrayList<>(); private final List<Geometry> inclinedGeos = new ArrayList<>();
private final List<Material> animatedMaterials = new ArrayList<>(); private final List<Material> animatedMaterials = new ArrayList<>();
private final List<Geometry> objectFoams = new ArrayList<>();
private final List<Material> objectFoamMats = new ArrayList<>();
private Texture2D foamRingTex = null;
private float time = 0f; private float time = 0f;
private Node rootNode; private Node rootNode;
@@ -85,15 +93,20 @@ public class WaterBodyState extends BaseAppState {
} }
} }
log.info("{} flache + {} abschüssige Wasserfläche(n) geladen.", flat, inclined); log.info("{} flache + {} abschüssige Wasserfläche(n) geladen.", flat, inclined);
// buildObjectFoam(assets, bodies); // TODO: Objekt-Schaum deaktiviert
} }
@Override @Override
public void update(float tpf) { public void update(float tpf) {
if (!animatedMaterials.isEmpty()) { if (!animatedMaterials.isEmpty() || !objectFoamMats.isEmpty()) {
time += tpf; time += tpf;
for (Material m : animatedMaterials) { for (Material m : animatedMaterials) {
m.setFloat("Time", time); 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); for (Geometry g : inclinedGeos) rootNode.detachChild(g);
inclinedGeos.clear(); inclinedGeos.clear();
animatedMaterials.clear(); animatedMaterials.clear();
for (Geometry g : objectFoams) rootNode.detachChild(g);
objectFoams.clear();
objectFoamMats.clear();
} }
@Override protected void onEnable() {} @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<PlacedWater> bodies) {
List<PlacedWater> flatBodies = bodies.stream().filter(PlacedWater::isFlat).toList();
List<PlacedModel> 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. * Baut das Wasserfall-Mesh aus den gespeicherten Eckpunkt-Koordinaten.
* Y wird zwischen Nachbar-Ecken linear interpoliert kein Terrain-Raycast, * Y wird zwischen Nachbar-Ecken linear interpoliert kein Terrain-Raycast,

View File

@@ -1,11 +1,22 @@
package de.blight.game.state; package de.blight.game.state;
import com.jme3.app.Application; import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState; import com.jme3.app.state.BaseAppState;
import com.jme3.bullet.control.CharacterControl; 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.FastMath;
import com.jme3.math.Vector2f; import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f; 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.Image;
import com.jme3.texture.Texture; import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D; import com.jme3.texture.Texture2D;
@@ -15,6 +26,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.Arrays; import java.util.Arrays;
/** /**
@@ -37,9 +50,9 @@ public class WaterInteractionState extends BaseAppState {
private static final int N = 256; private static final int N = 256;
private static final float HALF_EXTENT = 64f; // half of 128m world coverage 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 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 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.995f; private static final float DAMPING = 0.985f; // stärkere Dämpfung da langsame Wellen länger verweilen
private static final float WAVE_STRENGTH = 6.0f; // normal perturbation scale in shader 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_RADIUS_TEX = 3.0f; // blob radius in texels
private static final float BLOB_COOLDOWN = 0.07f; // seconds between blob injections private static final float BLOB_COOLDOWN = 0.07f; // seconds between blob injections
private static final float RECENTER_THRESH = HALF_EXTENT * 0.45f; // ~29m 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 float waterHeight = 0f;
private final Vector3f prevPos = new Vector3f(Float.NaN, 0f, Float.NaN); 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) { public WaterInteractionState(DynamicWaterFilter filter) {
this.dynFilter = filter; this.dynFilter = filter;
} }
@@ -100,6 +119,8 @@ public class WaterInteractionState extends BaseAppState {
waveTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps); waveTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
waveTex.setWrap(Texture.WrapMode.EdgeClamp); waveTex.setWrap(Texture.WrapMode.EdgeClamp);
// buildPlayerFoam(app); // TODO: Spieler-Schaum deaktiviert
log.debug("[WaterInteraction] Simulation initialized ({}×{}, {}m coverage)", N, N, HALF_EXTENT * 2f); log.debug("[WaterInteraction] Simulation initialized ({}×{}, {}m coverage)", N, N, HALF_EXTENT * 2f);
} }
@@ -109,6 +130,7 @@ public class WaterInteractionState extends BaseAppState {
waveBuf = null; waveBuf = null;
waveImg = null; waveImg = null;
waveTex = null; waveTex = null;
if (foamGeo != null) foamGeo.removeFromParent();
} }
@Override protected void onEnable() {} @Override protected void onEnable() {}
@@ -125,6 +147,7 @@ public class WaterInteractionState extends BaseAppState {
paramsApplied = true; paramsApplied = true;
} }
boolean playerInWater = false;
if (playerControl != null) { if (playerControl != null) {
Vector3f pos = playerControl.getPhysicsLocation(); 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 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 if (feetY < waterHeight + 0.4f) { // player in or just above water
playerInWater = true;
float speed = Float.isNaN(prevPos.x) ? 0f : pos.distance(prevPos) / tpf; float speed = Float.isNaN(prevPos.x) ? 0f : pos.distance(prevPos) / tpf;
if (speed > 0.3f) { // ignore sub-threshold shuffling if (speed > 0.3f) { // ignore sub-threshold shuffling
blobTimer -= tpf; blobTimer -= tpf;
if (blobTimer <= 0f) { 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); addBlob(pos.x, pos.z, strength);
blobTimer = BLOB_COOLDOWN; blobTimer = BLOB_COOLDOWN;
} }
@@ -145,6 +169,8 @@ public class WaterInteractionState extends BaseAppState {
prevPos.set(pos); prevPos.set(pos);
} }
// updatePlayerFoam(tpf, playerInWater); // TODO: Spieler-Schaum deaktiviert
runSimulation(); runSimulation();
uploadTexture(); uploadTexture();
} }
@@ -210,4 +236,63 @@ public class WaterInteractionState extends BaseAppState {
waveBuf.rewind(); waveBuf.rewind();
waveImg.setUpdateNeeded(); 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));
}
} }