diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index 8657f16..01f905b 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -744,6 +744,11 @@ public class EditorApp extends Application { updateWaterfallPanel(input.selectedWaterfallInfo); } + if (input.refreshWorldTree) { + input.refreshWorldTree = false; + mapObjectsView.refresh(); + } + if (input.waterHeightChanged) { input.waterHeightChanged = false; updateWaterHeightDisplay(input.waterCurrentHeight); diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index cf61b7d..3563446 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -376,6 +376,7 @@ public class SharedInput { public volatile boolean reloadPlacedModels = false; public volatile boolean reloadPlacedItems = false; public volatile boolean reloadPlacedOther = false; // Lichter, Emitter, Wasser, Bereiche, Zonen + public volatile boolean refreshWorldTree = false; // Weltbaum (MapObjectsView) neu laden /** Nur Lichter + Emitter neu laden (kein Zonen-Reload), gesetzt von SceneObjectState. */ public volatile boolean reloadLightsEmitters = false; /** Yaw in Grad: 0° = Süden (−Z), 90° = Westen (−X), ±180° = Norden (+Z). */ diff --git a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java index 1cdf03f..b174b30 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java @@ -110,7 +110,8 @@ public class TerrainEditorState extends BaseAppState { private AreaState areaState; private LocationZoneState locationZoneState; private VoxelCliffEditorState voxelCliffEditorState; - private RiverEditorState riverEditorState; + private RiverEditorState riverEditorState; + private WaterfallEditorState waterfallEditorState; private MapData loadedMapData; private Node axesGizmo; private boolean wireframeMode = false; @@ -390,7 +391,7 @@ public class TerrainEditorState extends BaseAppState { riverEditorState.setTerrain(terrain); } - WaterfallEditorState waterfallEditorState = app.getStateManager().getState(WaterfallEditorState.class); + waterfallEditorState = app.getStateManager().getState(WaterfallEditorState.class); if (waterfallEditorState != null) { waterfallEditorState.setTerrain(terrain); } @@ -1314,8 +1315,9 @@ public class TerrainEditorState extends BaseAppState { try { if (soundAreaState != null) soundAreaState.loadAreas(de.blight.common.SoundAreaIO.load()); } catch (Exception ignored) {} try { if (areaState != null) areaState.loadAreas(de.blight.common.AreaIO.load()); } catch (Exception ignored) {} try { if (locationZoneState != null) locationZoneState.loadZones(de.blight.common.LocationZoneIO.load()); } catch (Exception ignored) {} - try { if (riverEditorState != null) riverEditorState.loadPlacedRivers(de.blight.common.RiverIO.load()); } catch (Exception ignored) {} - try { if (voxelCliffEditorState != null) voxelCliffEditorState.loadZones(de.blight.common.VoxelCliffZoneIO.load()); } catch (Exception ignored) {} + try { if (riverEditorState != null) riverEditorState.loadPlacedRivers(de.blight.common.RiverIO.load()); } catch (Exception ignored) {} + try { if (waterfallEditorState != null) waterfallEditorState.loadPlacedWaterfalls(de.blight.common.WaterfallIO.load()); } catch (Exception ignored) {} + try { if (voxelCliffEditorState != null) voxelCliffEditorState.loadZones(de.blight.common.VoxelCliffZoneIO.load()); } catch (Exception ignored) {} } if (input.saveRequested) { diff --git a/blight-editor/src/main/java/de/blight/editor/state/WaterfallEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/WaterfallEditorState.java index 883042a..ca646cc 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/WaterfallEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/WaterfallEditorState.java @@ -83,6 +83,18 @@ public class WaterfallEditorState extends BaseAppState { public void setTerrain(TerrainQuad t) { this.terrain = t; } + /** Wird von TerrainEditorState bei reloadPlacedOther (z.B. Löschen im Weltbaum) aufgerufen. */ + public void loadPlacedWaterfalls(java.util.List list) { + deselect(); + for (Node n : wfNodes) n.removeFromParent(); + wfNodes.clear(); + waterfalls.clear(); + for (PlacedWaterfall wf : list) { + waterfalls.add(wf); + wfNodes.add(buildWfNode(wf, false)); + } + } + @Override protected void initialize(Application app) { this.app = (SimpleApplication) app; @@ -179,11 +191,18 @@ public class WaterfallEditorState extends BaseAppState { if (h >= 0) { startDrag(h); return; } } - // Raycast + See-Snapping + // Obere Ecken (A=0, B=1): nur auf Terrain/Voxel + Snap auf Seekanten + // Untere Ecken (C=2, D=3): nur auf Wasserflächen (Meer/Seen), kein Kanten-Snap + boolean isBottomCorner = (mode == Mode.PLACING && placingCorners.size() >= 2); Ray ray = buildRay(jmeX, jmeY); - Vector3f pt = raycastAll(ray); - Vector3f snapped = snapToWaterVertex(jmeX, jmeY); - if (snapped != null) pt = snapped; + Vector3f pt; + if (isBottomCorner) { + pt = raycastAll(ray, true); + } else { + pt = raycastAll(ray, false); + Vector3f snapped = snapToWaterVertex(jmeX, jmeY); + if (snapped != null) pt = snapped; + } if (pt == null) return; if (mode == Mode.PLACING) { @@ -217,6 +236,7 @@ public class WaterfallEditorState extends BaseAppState { waterfalls.add(wf); wfNodes.add(buildWfNode(wf, false)); save(); + input.refreshWorldTree = true; selectWf(waterfalls.size() - 1); } @@ -240,8 +260,6 @@ public class WaterfallEditorState extends BaseAppState { mode = (idx >= 0) ? Mode.EDITING : Mode.IDLE; if (idx >= 0 && idx < waterfalls.size()) { - // wfNode ausblenden — Handles + selOutline übernehmen die Darstellung - wfNodes.get(idx).setCullHint(Spatial.CullHint.Always); PlacedWaterfall wf = waterfalls.get(idx); liveCorners = new Vector3f[]{ new Vector3f(wf.ax(), wf.ay(), wf.az()), @@ -256,7 +274,8 @@ public class WaterfallEditorState extends BaseAppState { } private void deselect() { - if (selectedIdx >= 0 && selectedIdx < wfNodes.size()) { + // wfNode war nur während Drag verborgen → Inherit wiederherstellen falls nötig + if (isDragging && selectedIdx >= 0 && selectedIdx < wfNodes.size()) { wfNodes.get(selectedIdx).setCullHint(Spatial.CullHint.Inherit); } selectedIdx = -1; @@ -280,15 +299,26 @@ public class WaterfallEditorState extends BaseAppState { private void startDrag(int h) { isDragging = true; dragHandle = h; + // wfNode während Drag verbergen — live Positionen zeigt selOutline + if (selectedIdx >= 0 && selectedIdx < wfNodes.size()) { + wfNodes.get(selectedIdx).setCullHint(Spatial.CullHint.Always); + } } private void processDragMove(float screenX, float screenY) { if (!isDragging || selectedIdx < 0 || liveCorners == null) return; float jmeX = screenX * (float) input.viewportScaleX; float jmeY = cam.getHeight() - screenY * (float) input.viewportScaleY; - Vector3f pt = raycastAll(buildRay(jmeX, jmeY)); - Vector3f snapped = snapToWaterVertex(jmeX, jmeY); - if (snapped != null) pt = snapped; + // Handles 2/3 (C/D) = untere Ecken → nur Wasser-Kollision, kein Kanten-Snap + // Handles 0/1 (A/B) = obere Ecken → Terrain + Kanten-Snap, kein Wasser + Vector3f pt; + if (dragHandle >= 2) { + pt = raycastAll(buildRay(jmeX, jmeY), true); + } else { + pt = raycastAll(buildRay(jmeX, jmeY), false); + Vector3f snapped = snapToWaterVertex(jmeX, jmeY); + if (snapped != null) pt = snapped; + } if (pt == null) return; liveCorners[dragHandle].set(pt); rebuildHandlePositions(); @@ -305,11 +335,10 @@ public class WaterfallEditorState extends BaseAppState { a.x, a.y, a.z, b.x, b.y, b.z, c.x, c.y, c.z, d.x, d.y, d.z, old.speed(), old.transparency(), old.colorR(), old.colorG(), old.colorB()); waterfalls.set(selectedIdx, updated); - // wfNode (verborgen) auf neue Positionen updaten, damit er beim Abwählen stimmt + // wfNode mit neuen Positionen neu bauen und wieder anzeigen Node old2 = wfNodes.get(selectedIdx); old2.removeFromParent(); Node fresh = buildWfNode(updated, false); - fresh.setCullHint(Spatial.CullHint.Always); // bleibt verborgen, solange selektiert wfNodes.set(selectedIdx, fresh); save(); publishSelection(selectedIdx); @@ -403,6 +432,10 @@ public class WaterfallEditorState extends BaseAppState { } private Vector3f raycastAll(Ray ray) { + return raycastAll(ray, false); + } + + private Vector3f raycastAll(Ray ray, boolean collisionWithWater) { Vector3f best = null; float bestDistSq = Float.MAX_VALUE; @@ -430,13 +463,57 @@ public class WaterfallEditorState extends BaseAppState { Vector3f sp = smes.raycastGeometry(ray); if (sp != null) { float d = ray.origin.distanceSquared(sp); - if (d < bestDistSq) { best = sp; } + if (d < bestDistSq) { best = sp; bestDistSq = d; } + } + } + + if (collisionWithWater) { + // Meer bei Y=0 + Vector3f seaHit = rayHitsHorizontalPlane(ray, 0f); + if (seaHit != null) { + float d = ray.origin.distanceSquared(seaHit); + if (d < bestDistSq) { best = seaHit; bestDistSq = d; } + } + + // Definierte Seen (PlacedWater) + WaterBodyState wbs = getStateManager().getState(WaterBodyState.class); + if (wbs != null) { + for (PlacedWater body : wbs.getPlacedBodies()) { + float[] xs = body.pointsX(), ys = body.pointsY(), zs = body.pointsZ(); + float avgY = 0f; + for (float y : ys) avgY += y; + avgY /= ys.length; + Vector3f hit = rayHitsHorizontalPlane(ray, avgY); + if (hit != null && pointInPolygon(hit.x, hit.z, xs, zs)) { + float d = ray.origin.distanceSquared(hit); + if (d < bestDistSq) { best = hit; bestDistSq = d; } + } + } } } return best; } + private static Vector3f rayHitsHorizontalPlane(Ray ray, float planeY) { + if (Math.abs(ray.direction.y) < 1e-6f) return null; + float t = (planeY - ray.origin.y) / ray.direction.y; + if (t <= 0f) return null; + return ray.origin.add(ray.direction.mult(t)); + } + + private static boolean pointInPolygon(float px, float pz, float[] xs, float[] zs) { + int n = xs.length; + boolean inside = false; + 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; + } + // ── Visuals ─────────────────────────────────────────────────────────────── private int findNearestWf(Vector3f pos, float maxDist) { @@ -562,12 +639,7 @@ public class WaterfallEditorState extends BaseAppState { } private void setAllCull(Spatial.CullHint hint) { - for (int i = 0; i < wfNodes.size(); i++) { - // Selektierter bleibt Always wenn hint=Always, sonst Inherit - if (hint == Spatial.CullHint.Always || i != selectedIdx) { - wfNodes.get(i).setCullHint(hint); - } - } + for (Node n : wfNodes) n.setCullHint(hint); if (placingNode != null) placingNode.setCullHint(hint); for (Geometry g : handleGeos) g.setCullHint(hint); if (selOutline != null) selOutline.setCullHint(hint); @@ -708,7 +780,8 @@ public class WaterfallEditorState extends BaseAppState { Node fresh = buildWfNode(upd, false); wfNodes.add(i, fresh); if (isSelected) { - fresh.setCullHint(Spatial.CullHint.Always); + // wfNode bleibt sichtbar; nur bei aktivem Drag verbergen + if (isDragging) fresh.setCullHint(Spatial.CullHint.Always); if (liveCorners != null) { liveCorners[0].set(upd.ax(), upd.ay(), upd.az()); liveCorners[1].set(upd.bx(), upd.by(), upd.bz()); diff --git a/blight-game/src/main/java/de/blight/game/state/WaterfallState.java b/blight-game/src/main/java/de/blight/game/state/WaterfallState.java index 69859ae..486cf83 100644 --- a/blight-game/src/main/java/de/blight/game/state/WaterfallState.java +++ b/blight-game/src/main/java/de/blight/game/state/WaterfallState.java @@ -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 geos = new ArrayList<>(); - private final List materials = new ArrayList<>(); - private float time = 0f; + private final List geos = new ArrayList<>(); + private final List materials = new ArrayList<>(); + private final List foams = new ArrayList<>(); + private final List foamMaterials = new ArrayList<>(); + private final List 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; + } } diff --git a/blight-map/src/main/map/blight_map.blm b/blight-map/src/main/map/blight_map.blm index 7b3bd5d..4d5ee22 100644 Binary files a/blight-map/src/main/map/blight_map.blm and b/blight-map/src/main/map/blight_map.blm differ diff --git a/blight-map/src/main/map/blight_waterfall.blwf b/blight-map/src/main/map/blight_waterfall.blwf index 69396b9..7629823 100644 --- a/blight-map/src/main/map/blight_waterfall.blwf +++ b/blight-map/src/main/map/blight_waterfall.blwf @@ -1,2 +1,2 @@ # ax,ay,az bx,by,bz cx,cy,cz dx,dy,dz speed transparency r g b -139.68401,11.11647,-897.94312 133.19110,11.13542,-897.81921 132.98654,-2.73925,-898.99731 138.49196,-1.53031,-899.29150 1.5000 0.75000 0.35000 0.55000 0.75000 +139.60182,11.05989,-897.79767 133.36690,11.03875,-897.70581 133.57416,0.00000,-901.02240 139.35196,0.00000,-900.97418 1.5000 0.75000 0.35000 0.55000 0.75000 diff --git a/blight-map/src/main/map/chunks/chunk_08_00.blc b/blight-map/src/main/map/chunks/chunk_08_00.blc index 407df62..0cae612 100644 Binary files a/blight-map/src/main/map/chunks/chunk_08_00.blc and b/blight-map/src/main/map/chunks/chunk_08_00.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_09_00.blc b/blight-map/src/main/map/chunks/chunk_09_00.blc index e00274d..495cdf4 100644 Binary files a/blight-map/src/main/map/chunks/chunk_09_00.blc and b/blight-map/src/main/map/chunks/chunk_09_00.blc differ