Wasserfälle weiter verveinert
This commit is contained in:
@@ -744,6 +744,11 @@ public class EditorApp extends Application {
|
|||||||
updateWaterfallPanel(input.selectedWaterfallInfo);
|
updateWaterfallPanel(input.selectedWaterfallInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.refreshWorldTree) {
|
||||||
|
input.refreshWorldTree = false;
|
||||||
|
mapObjectsView.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
if (input.waterHeightChanged) {
|
if (input.waterHeightChanged) {
|
||||||
input.waterHeightChanged = false;
|
input.waterHeightChanged = false;
|
||||||
updateWaterHeightDisplay(input.waterCurrentHeight);
|
updateWaterHeightDisplay(input.waterCurrentHeight);
|
||||||
|
|||||||
@@ -376,6 +376,7 @@ public class SharedInput {
|
|||||||
public volatile boolean reloadPlacedModels = false;
|
public volatile boolean reloadPlacedModels = false;
|
||||||
public volatile boolean reloadPlacedItems = false;
|
public volatile boolean reloadPlacedItems = false;
|
||||||
public volatile boolean reloadPlacedOther = false; // Lichter, Emitter, Wasser, Bereiche, Zonen
|
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. */
|
/** Nur Lichter + Emitter neu laden (kein Zonen-Reload), gesetzt von SceneObjectState. */
|
||||||
public volatile boolean reloadLightsEmitters = false;
|
public volatile boolean reloadLightsEmitters = false;
|
||||||
/** Yaw in Grad: 0° = Süden (−Z), 90° = Westen (−X), ±180° = Norden (+Z). */
|
/** Yaw in Grad: 0° = Süden (−Z), 90° = Westen (−X), ±180° = Norden (+Z). */
|
||||||
|
|||||||
@@ -110,7 +110,8 @@ public class TerrainEditorState extends BaseAppState {
|
|||||||
private AreaState areaState;
|
private AreaState areaState;
|
||||||
private LocationZoneState locationZoneState;
|
private LocationZoneState locationZoneState;
|
||||||
private VoxelCliffEditorState voxelCliffEditorState;
|
private VoxelCliffEditorState voxelCliffEditorState;
|
||||||
private RiverEditorState riverEditorState;
|
private RiverEditorState riverEditorState;
|
||||||
|
private WaterfallEditorState waterfallEditorState;
|
||||||
private MapData loadedMapData;
|
private MapData loadedMapData;
|
||||||
private Node axesGizmo;
|
private Node axesGizmo;
|
||||||
private boolean wireframeMode = false;
|
private boolean wireframeMode = false;
|
||||||
@@ -390,7 +391,7 @@ public class TerrainEditorState extends BaseAppState {
|
|||||||
riverEditorState.setTerrain(terrain);
|
riverEditorState.setTerrain(terrain);
|
||||||
}
|
}
|
||||||
|
|
||||||
WaterfallEditorState waterfallEditorState = app.getStateManager().getState(WaterfallEditorState.class);
|
waterfallEditorState = app.getStateManager().getState(WaterfallEditorState.class);
|
||||||
if (waterfallEditorState != null) {
|
if (waterfallEditorState != null) {
|
||||||
waterfallEditorState.setTerrain(terrain);
|
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 (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 (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 (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 (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 (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) {
|
if (input.saveRequested) {
|
||||||
|
|||||||
@@ -83,6 +83,18 @@ public class WaterfallEditorState extends BaseAppState {
|
|||||||
|
|
||||||
public void setTerrain(TerrainQuad t) { this.terrain = t; }
|
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<PlacedWaterfall> 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
|
@Override
|
||||||
protected void initialize(Application app) {
|
protected void initialize(Application app) {
|
||||||
this.app = (SimpleApplication) app;
|
this.app = (SimpleApplication) app;
|
||||||
@@ -179,11 +191,18 @@ public class WaterfallEditorState extends BaseAppState {
|
|||||||
if (h >= 0) { startDrag(h); return; }
|
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);
|
Ray ray = buildRay(jmeX, jmeY);
|
||||||
Vector3f pt = raycastAll(ray);
|
Vector3f pt;
|
||||||
Vector3f snapped = snapToWaterVertex(jmeX, jmeY);
|
if (isBottomCorner) {
|
||||||
if (snapped != null) pt = snapped;
|
pt = raycastAll(ray, true);
|
||||||
|
} else {
|
||||||
|
pt = raycastAll(ray, false);
|
||||||
|
Vector3f snapped = snapToWaterVertex(jmeX, jmeY);
|
||||||
|
if (snapped != null) pt = snapped;
|
||||||
|
}
|
||||||
if (pt == null) return;
|
if (pt == null) return;
|
||||||
|
|
||||||
if (mode == Mode.PLACING) {
|
if (mode == Mode.PLACING) {
|
||||||
@@ -217,6 +236,7 @@ public class WaterfallEditorState extends BaseAppState {
|
|||||||
waterfalls.add(wf);
|
waterfalls.add(wf);
|
||||||
wfNodes.add(buildWfNode(wf, false));
|
wfNodes.add(buildWfNode(wf, false));
|
||||||
save();
|
save();
|
||||||
|
input.refreshWorldTree = true;
|
||||||
selectWf(waterfalls.size() - 1);
|
selectWf(waterfalls.size() - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,8 +260,6 @@ public class WaterfallEditorState extends BaseAppState {
|
|||||||
mode = (idx >= 0) ? Mode.EDITING : Mode.IDLE;
|
mode = (idx >= 0) ? Mode.EDITING : Mode.IDLE;
|
||||||
|
|
||||||
if (idx >= 0 && idx < waterfalls.size()) {
|
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);
|
PlacedWaterfall wf = waterfalls.get(idx);
|
||||||
liveCorners = new Vector3f[]{
|
liveCorners = new Vector3f[]{
|
||||||
new Vector3f(wf.ax(), wf.ay(), wf.az()),
|
new Vector3f(wf.ax(), wf.ay(), wf.az()),
|
||||||
@@ -256,7 +274,8 @@ public class WaterfallEditorState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void deselect() {
|
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);
|
wfNodes.get(selectedIdx).setCullHint(Spatial.CullHint.Inherit);
|
||||||
}
|
}
|
||||||
selectedIdx = -1;
|
selectedIdx = -1;
|
||||||
@@ -280,15 +299,26 @@ public class WaterfallEditorState extends BaseAppState {
|
|||||||
private void startDrag(int h) {
|
private void startDrag(int h) {
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
dragHandle = h;
|
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) {
|
private void processDragMove(float screenX, float screenY) {
|
||||||
if (!isDragging || selectedIdx < 0 || liveCorners == null) return;
|
if (!isDragging || selectedIdx < 0 || liveCorners == null) return;
|
||||||
float jmeX = screenX * (float) input.viewportScaleX;
|
float jmeX = screenX * (float) input.viewportScaleX;
|
||||||
float jmeY = cam.getHeight() - screenY * (float) input.viewportScaleY;
|
float jmeY = cam.getHeight() - screenY * (float) input.viewportScaleY;
|
||||||
Vector3f pt = raycastAll(buildRay(jmeX, jmeY));
|
// Handles 2/3 (C/D) = untere Ecken → nur Wasser-Kollision, kein Kanten-Snap
|
||||||
Vector3f snapped = snapToWaterVertex(jmeX, jmeY);
|
// Handles 0/1 (A/B) = obere Ecken → Terrain + Kanten-Snap, kein Wasser
|
||||||
if (snapped != null) pt = snapped;
|
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;
|
if (pt == null) return;
|
||||||
liveCorners[dragHandle].set(pt);
|
liveCorners[dragHandle].set(pt);
|
||||||
rebuildHandlePositions();
|
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,
|
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());
|
old.speed(), old.transparency(), old.colorR(), old.colorG(), old.colorB());
|
||||||
waterfalls.set(selectedIdx, updated);
|
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);
|
Node old2 = wfNodes.get(selectedIdx);
|
||||||
old2.removeFromParent();
|
old2.removeFromParent();
|
||||||
Node fresh = buildWfNode(updated, false);
|
Node fresh = buildWfNode(updated, false);
|
||||||
fresh.setCullHint(Spatial.CullHint.Always); // bleibt verborgen, solange selektiert
|
|
||||||
wfNodes.set(selectedIdx, fresh);
|
wfNodes.set(selectedIdx, fresh);
|
||||||
save();
|
save();
|
||||||
publishSelection(selectedIdx);
|
publishSelection(selectedIdx);
|
||||||
@@ -403,6 +432,10 @@ public class WaterfallEditorState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Vector3f raycastAll(Ray ray) {
|
private Vector3f raycastAll(Ray ray) {
|
||||||
|
return raycastAll(ray, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector3f raycastAll(Ray ray, boolean collisionWithWater) {
|
||||||
Vector3f best = null;
|
Vector3f best = null;
|
||||||
float bestDistSq = Float.MAX_VALUE;
|
float bestDistSq = Float.MAX_VALUE;
|
||||||
|
|
||||||
@@ -430,13 +463,57 @@ public class WaterfallEditorState extends BaseAppState {
|
|||||||
Vector3f sp = smes.raycastGeometry(ray);
|
Vector3f sp = smes.raycastGeometry(ray);
|
||||||
if (sp != null) {
|
if (sp != null) {
|
||||||
float d = ray.origin.distanceSquared(sp);
|
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;
|
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 ───────────────────────────────────────────────────────────────
|
// ── Visuals ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private int findNearestWf(Vector3f pos, float maxDist) {
|
private int findNearestWf(Vector3f pos, float maxDist) {
|
||||||
@@ -562,12 +639,7 @@ public class WaterfallEditorState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setAllCull(Spatial.CullHint hint) {
|
private void setAllCull(Spatial.CullHint hint) {
|
||||||
for (int i = 0; i < wfNodes.size(); i++) {
|
for (Node n : wfNodes) n.setCullHint(hint);
|
||||||
// Selektierter bleibt Always wenn hint=Always, sonst Inherit
|
|
||||||
if (hint == Spatial.CullHint.Always || i != selectedIdx) {
|
|
||||||
wfNodes.get(i).setCullHint(hint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (placingNode != null) placingNode.setCullHint(hint);
|
if (placingNode != null) placingNode.setCullHint(hint);
|
||||||
for (Geometry g : handleGeos) g.setCullHint(hint);
|
for (Geometry g : handleGeos) g.setCullHint(hint);
|
||||||
if (selOutline != null) selOutline.setCullHint(hint);
|
if (selOutline != null) selOutline.setCullHint(hint);
|
||||||
@@ -708,7 +780,8 @@ public class WaterfallEditorState extends BaseAppState {
|
|||||||
Node fresh = buildWfNode(upd, false);
|
Node fresh = buildWfNode(upd, false);
|
||||||
wfNodes.add(i, fresh);
|
wfNodes.add(i, fresh);
|
||||||
if (isSelected) {
|
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) {
|
if (liveCorners != null) {
|
||||||
liveCorners[0].set(upd.ax(), upd.ay(), upd.az());
|
liveCorners[0].set(upd.ax(), upd.ay(), upd.az());
|
||||||
liveCorners[1].set(upd.bx(), upd.by(), upd.bz());
|
liveCorners[1].set(upd.bx(), upd.by(), upd.bz());
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import com.jme3.app.Application;
|
|||||||
import com.jme3.app.SimpleApplication;
|
import com.jme3.app.SimpleApplication;
|
||||||
import com.jme3.app.state.BaseAppState;
|
import com.jme3.app.state.BaseAppState;
|
||||||
import com.jme3.asset.AssetManager;
|
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.Material;
|
||||||
import com.jme3.material.RenderState;
|
import com.jme3.material.RenderState;
|
||||||
import com.jme3.math.ColorRGBA;
|
import com.jme3.math.ColorRGBA;
|
||||||
@@ -13,7 +16,9 @@ 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.PlacedWaterfall;
|
import de.blight.common.PlacedWaterfall;
|
||||||
import de.blight.common.WaterfallIO;
|
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 ROWS = 24; // Tessellierungs-Zeilen vertikal
|
||||||
private static final int COLS = 6; // Tessellierungs-Spalten horizontal
|
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 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 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 Node rootNode;
|
||||||
private final List<Geometry> geos = new ArrayList<>();
|
private final List<Geometry> geos = new ArrayList<>();
|
||||||
private final List<Material> materials = new ArrayList<>();
|
private final List<Material> materials = new ArrayList<>();
|
||||||
private float time = 0f;
|
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
|
@Override
|
||||||
protected void initialize(Application app) {
|
protected void initialize(Application app) {
|
||||||
@@ -74,14 +84,23 @@ public class WaterfallState extends BaseAppState {
|
|||||||
for (Material m : materials) {
|
for (Material m : materials) {
|
||||||
m.setFloat("Time", time);
|
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
|
@Override
|
||||||
protected void cleanup(Application app) {
|
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();
|
geos.clear();
|
||||||
materials.clear();
|
materials.clear();
|
||||||
|
foams.clear();
|
||||||
|
foamMaterials.clear();
|
||||||
|
mists.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override protected void onEnable() {}
|
@Override protected void onEnable() {}
|
||||||
@@ -97,6 +116,154 @@ public class WaterfallState extends BaseAppState {
|
|||||||
rootNode.attachChild(geo);
|
rootNode.attachChild(geo);
|
||||||
geos.add(geo);
|
geos.add(geo);
|
||||||
materials.add(mat);
|
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 vertCount = vRows * vCols;
|
||||||
int triCount = ROWS * COLS * 2;
|
int triCount = ROWS * COLS * 2;
|
||||||
|
|
||||||
// Referenz-Normale (nach außen zeigend) aus Winding A top-left, B top-right, D bottom-left
|
// Outward-Richtung für die Wölbung und die Normalen-Referenz
|
||||||
Vector3f n1 = d.subtract(a).cross(b.subtract(a)).normalizeLocal();
|
Vector3f outward = computeOutward(a, b, c, d);
|
||||||
Vector3f n2 = b.subtract(c).cross(d.subtract(c)).normalizeLocal();
|
Vector3f avgNorm = outward; // für den Vorzeichen-Check der Normalen ausreichend
|
||||||
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();
|
|
||||||
|
|
||||||
// Pass 1: alle Vertex-Positionen mit Bulge berechnen und zwischenspeichern
|
// Pass 1: alle Vertex-Positionen mit Bulge berechnen und zwischenspeichern
|
||||||
Vector3f[][] positions = new Vector3f[vRows][vCols];
|
Vector3f[][] positions = new Vector3f[vRows][vCols];
|
||||||
@@ -228,4 +388,30 @@ public class WaterfallState extends BaseAppState {
|
|||||||
if (fallback == null) return null;
|
if (fallback == null) return null;
|
||||||
try { return assets.loadTexture(fallback); } catch (Exception ignored) { 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -1,2 +1,2 @@
|
|||||||
# ax,ay,az bx,by,bz cx,cy,cz dx,dy,dz speed transparency r g b
|
# 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
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user