Einmal den Fortschritt mit dem Wasser sichern

This commit is contained in:
2026-06-06 09:05:25 +02:00
parent d56f2ea41f
commit 7faed35287
21 changed files with 853 additions and 152 deletions

View File

@@ -324,14 +324,14 @@ public class WorldScene extends BaseAppState {
/**
* Erstellt ein Terrain aus der gespeicherten {@link MapData}.
* Die 4097×4097 Editor-Daten werden auf 513×513 heruntergesampelt
* (jeder 8. Vertex), mit Scale (8, 1, 8) auf die gleiche Weltgröße
* Die 16385×16385 Editor-Daten werden auf 513×513 heruntergesampelt
* (jeder 32. Vertex), mit Scale (8, 1, 8) auf die gleiche Weltgröße
* 4096 × 4096 WE gebracht.
*/
private TerrainQuad buildTerrainFromMap(MapData map) {
final int GAME_VERTS = 513; // 512 Zellen à 8 WE = 4096 WE
final int STEP = 8; // 4096 / 512 = 8 Vertices überspringen
final int SRC_VERTS = MapData.TERRAIN_VERTS; // 4097
final int STEP = (MapData.TERRAIN_VERTS - 1) / (GAME_VERTS - 1); // 32
final int SRC_VERTS = MapData.TERRAIN_VERTS; // 16385
float[] heights = new float[GAME_VERTS * GAME_VERTS];
for (int gz = 0; gz < GAME_VERTS; gz++) {

View File

@@ -40,7 +40,8 @@ import java.util.Random;
public class RiverState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(RiverState.class);
private static final float UV_SCALE = 6.0f;
private static final float UV_SCALE = 6.0f;
private static final float WATER_SINK = 0.5f; // Wasser liegt 0,5m unter den Kontrollpunkten
private Node riverNode;
private AssetManager assets;
@@ -118,7 +119,7 @@ public class RiverState extends BaseAppState {
List<RiverPoint> run = RiverSpline.subdivide(pts.subList(i, j + 1));
if (run.size() >= 2) {
buildRibbonSection(run, wf);
if (wf) buildWaterfallParticles(run.get(run.size() - 1));
if (wf) buildWaterfallParticles(run.get(run.size() - 1), computeFlowDir(run));
}
i = j;
}
@@ -175,7 +176,7 @@ public class RiverState extends BaseAppState {
if (right.lengthSquared() < 1e-6f) right.set(1f, 0f, 0f);
float halfW = pt.width() * 0.5f;
float px = pt.x(), py = pt.y(), pz = pt.z();
float px = pt.x(), py = pt.y() - WATER_SINK, pz = pt.z();
float vCoord = arcLen[i] / UV_SCALE;
// Linker Rand (U=0), rechter Rand (U=1)
@@ -208,28 +209,44 @@ public class RiverState extends BaseAppState {
private Material buildMaterial(boolean isWaterfall) {
ColorRGBA tint = isWaterfall
? new ColorRGBA(0.65f, 0.82f, 0.95f, 0.80f)
: new ColorRGBA(0.10f, 0.30f, 0.62f, 0.85f);
? new ColorRGBA(0.65f, 0.82f, 0.95f, 0.75f)
: new ColorRGBA(0.10f, 0.30f, 0.62f, 0.68f);
Material mat;
try {
mat = new Material(assets, "MatDefs/FlowingWater.j3md");
try {
Texture nm = assets.loadTexture(
"Common/MatDefs/Water/Textures/water_normalmap.png");
// Normal-Map: erst eigene Texturen versuchen, dann JME-Fallback
Texture nm = loadTextureOr(
isWaterfall ? "Textures/water/waterfall_normal.png"
: "Textures/water/river_normal.jpg",
"Common/MatDefs/Water/Textures/water_normalmap.png");
if (nm != null) {
nm.setWrap(Texture.WrapMode.Repeat);
mat.setTexture("NormalMap", nm);
} catch (Exception e) {
log.warn("Normal-Map nicht ladbar, wird ohne Wellenstruktur gerendert");
} else {
log.warn("Keine Normal-Map geladen Mesh ohne Wellenstruktur");
}
if (foamTexture != null) {
mat.setTexture("FoamMap", foamTexture);
}
mat.setColor("Tint", tint);
mat.setFloat("UVScale", UV_SCALE);
mat.setFloat("FlowSpeed", isWaterfall ? RiverPoint.WATERFALL_SPEED
: RiverPoint.RIVER_SPEED);
mat.setFloat("FoamAmount", isWaterfall ? 1.0f : 0.0f);
// Diffuse-Map: river.jpg für Fluss (Farbmodulation), waterfall_diffuse für Gischt
String diffPath = isWaterfall ? "Textures/water/waterfall_diffuse.png"
: "Textures/water/river.jpg";
Texture diff = loadTextureOr(diffPath, null);
if (diff != null) {
diff.setWrap(Texture.WrapMode.Repeat);
mat.setTexture("DiffuseMap", diff);
}
mat.setColor("Tint", tint);
mat.setFloat("UVScale", UV_SCALE);
mat.setFloat("NormalUVScale", 0.5f); // 2x größeres Wellenmuster
mat.setFloat("FlowSpeed", isWaterfall ? RiverPoint.WATERFALL_SPEED
: RiverPoint.RIVER_SPEED);
mat.setFloat("FoamAmount", isWaterfall ? 1.0f : 0.0f);
} catch (Exception e) {
log.warn("FlowingWater-Material nicht ladbar, Fallback auf Unshaded", e);
mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
@@ -242,6 +259,20 @@ public class RiverState extends BaseAppState {
return mat;
}
/** Versucht primaryPath zu laden; schlägt das fehl, wird fallbackPath versucht (null = kein Fallback). */
private Texture loadTextureOr(String primaryPath, String fallbackPath) {
try {
return assets.loadTexture(primaryPath);
} catch (Exception ignored) {}
if (fallbackPath == null) return null;
try {
return assets.loadTexture(fallbackPath);
} catch (Exception e) {
log.warn("Textur nicht ladbar: {} und {}", primaryPath, fallbackPath);
return null;
}
}
// ── Worley-Noise Schaum-Textur ────────────────────────────────────────────
private Texture2D generateFoamTexture() {
@@ -286,29 +317,58 @@ public class RiverState extends BaseAppState {
// ── Partikel-Emitter ──────────────────────────────────────────────────────
private void buildWaterfallParticles(RiverPoint base) {
/**
* Gischt-Partikel am Fuß des Wasserfalls.
* Die Spray-Richtung ist das Spiegelbild der horizontalen Fließrichtung
* (Wasser prallt auf und spritzt zurück + hoch).
*/
private void buildWaterfallParticles(RiverPoint base, Vector3f flowDir) {
ParticleEmitter emitter = new ParticleEmitter(
"waterfall_particles", ParticleMesh.Type.Triangle, 30);
"waterfall_particles", ParticleMesh.Type.Triangle, 60);
Material pMat = new Material(assets, "Common/MatDefs/Misc/Particle.j3md");
try {
pMat.setTexture("Texture", assets.loadTexture("Effects/Smoke/Smoke.png"));
} catch (Exception e) {
log.warn("Partikel-Textur nicht ladbar", e);
}
Texture pTex = loadTextureOr("Textures/Water/spray.png", "Effects/Smoke/Smoke.png");
if (pTex != null) pMat.setTexture("Texture", pTex);
pMat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.AlphaAdditive);
emitter.setMaterial(pMat);
emitter.setImagesX(1);
emitter.setImagesY(1);
emitter.setStartColor(new ColorRGBA(1f, 1f, 1f, 0.5f));
emitter.setEndColor(new ColorRGBA(1f, 1f, 1f, 0f));
emitter.setStartSize(1.2f);
emitter.setEndSize(2.5f);
emitter.setGravity(0f, -0.5f, 0f);
emitter.setLowLife(0.8f);
emitter.setHighLife(1.2f);
emitter.setInitialVelocity(new Vector3f(0f, 3f, 0f));
emitter.setVelocityVariation(0.6f);
emitter.setParticlesPerSec(15);
// Hellweiße, leicht bläuliche Gischt startet fast opak, verblasst komplett
emitter.setStartColor(new ColorRGBA(0.90f, 0.95f, 1.00f, 0.55f));
emitter.setEndColor (new ColorRGBA(1.00f, 1.00f, 1.00f, 0.00f));
// Partikel starten klein, werden als Nebelwolke größer
emitter.setStartSize(0.4f);
emitter.setEndSize (2.8f);
// Leichte Schwerkraft damit Gischt wieder fällt
emitter.setGravity(0f, -1.2f, 0f);
emitter.setLowLife (0.7f);
emitter.setHighLife(1.6f);
// Spray-Richtung: nach oben + horizontal entgegen der Fließrichtung (Aufprall-Bounce)
// flowDir zeigt bergab/in Fließrichtung; horizontale Umkehrung = Rückspritzer
Vector3f sprayVel = new Vector3f(
-flowDir.x * 1.8f,
4.5f,
-flowDir.z * 1.8f);
emitter.setInitialVelocity(sprayVel);
emitter.setVelocityVariation(0.75f); // hohe Variation → aufgefächerte Gischt-Wolke
emitter.setParticlesPerSec(35);
emitter.setLocalTranslation(base.x(), base.y(), base.z());
riverNode.attachChild(emitter);
}
/** Fließrichtung am Ende eines Abschnitts aus den letzten Stützpunkten. */
private Vector3f computeFlowDir(List<RiverPoint> pts) {
int n = pts.size();
RiverPoint a = pts.get(Math.max(0, n - 3));
RiverPoint b = pts.get(n - 1);
Vector3f dir = new Vector3f(b.x() - a.x(), b.y() - a.y(), b.z() - a.z());
if (dir.lengthSquared() < 1e-6f) return new Vector3f(0f, -1f, 0f);
return dir.normalizeLocal();
}
}