Commit vor großem Terrain refactoring
This commit is contained in:
@@ -14,6 +14,7 @@ import de.blight.game.scene.WorldScene;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -35,6 +36,12 @@ public class BlightGame extends SimpleApplication {
|
||||
|
||||
private JWindow splashWindow;
|
||||
|
||||
// ── Splash-Status (von JME-Thread geschrieben, EDT liest) ─────────────────
|
||||
static volatile String loadingStatus = "Initialisiere...";
|
||||
static volatile boolean gameReady = false;
|
||||
private static JLabel splashLabel;
|
||||
private static Timer splashTimer;
|
||||
|
||||
public static void main(String[] args) {
|
||||
SLF4JBridgeHandler.removeHandlersForRootLogger();
|
||||
SLF4JBridgeHandler.install();
|
||||
@@ -42,6 +49,7 @@ public class BlightGame extends SimpleApplication {
|
||||
BlightGame app = new BlightGame();
|
||||
app.splashWindow = showSplash();
|
||||
|
||||
status("Lade Grafikeinstellungen...");
|
||||
GraphicsSettings gs = GraphicsStore.load();
|
||||
AppSettings settings = new AppSettings(true);
|
||||
settings.setTitle("Blight");
|
||||
@@ -53,21 +61,61 @@ public class BlightGame extends SimpleApplication {
|
||||
settings.setVSync(gs.vsync);
|
||||
settings.setSamples(gs.samples);
|
||||
|
||||
status("Initialisiere Renderer...");
|
||||
app.setSettings(settings);
|
||||
app.setShowSettings(false);
|
||||
app.start();
|
||||
}
|
||||
|
||||
/** Setzt den Ladetext, der im Splash-Screen angezeigt wird. Thread-sicher. */
|
||||
public static void status(String msg) {
|
||||
loadingStatus = msg;
|
||||
}
|
||||
|
||||
private static JWindow showSplash() {
|
||||
try {
|
||||
BufferedImage img = ImageIO.read(BlightGame.class.getResourceAsStream("/logo.png"));
|
||||
BufferedImage icon = ImageIO.read(BlightGame.class.getResourceAsStream("/icon.png"));
|
||||
// Logo skaliert auf 700 px Breite (Seitenverhältnis beibehalten)
|
||||
BufferedImage img = ImageIO.read(BlightGame.class.getResourceAsStream("/logo.png"));
|
||||
int tw = 700;
|
||||
int th = (int) (img.getHeight() * (tw / (double) img.getWidth()));
|
||||
Image scaled = img.getScaledInstance(tw, th, Image.SCALE_SMOOTH);
|
||||
|
||||
// Dunkles Panel
|
||||
JPanel panel = new JPanel(new BorderLayout());
|
||||
panel.setBackground(new Color(0x1c1c1c));
|
||||
panel.setOpaque(true);
|
||||
panel.add(new JLabel(new ImageIcon(scaled)), BorderLayout.CENTER);
|
||||
|
||||
splashLabel = new JLabel(loadingStatus);
|
||||
splashLabel.setForeground(new Color(0xbb, 0xbb, 0xbb));
|
||||
splashLabel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
|
||||
splashLabel.setBorder(BorderFactory.createEmptyBorder(8, 16, 12, 16));
|
||||
splashLabel.setOpaque(false);
|
||||
panel.add(splashLabel, BorderLayout.SOUTH);
|
||||
|
||||
JWindow win = new JWindow();
|
||||
if (icon != null) win.setIconImages(java.util.List.of(icon));
|
||||
win.getContentPane().add(new JLabel(new ImageIcon(img)));
|
||||
try {
|
||||
BufferedImage icon = ImageIO.read(BlightGame.class.getResourceAsStream("/icon.png"));
|
||||
if (icon != null) win.setIconImages(java.util.List.of(icon));
|
||||
} catch (Exception ignored) {}
|
||||
win.setContentPane(panel);
|
||||
win.pack();
|
||||
win.setLocationRelativeTo(null);
|
||||
win.setVisible(true);
|
||||
|
||||
// Pollt loadingStatus alle 100 ms – analog zum Editor-Splash
|
||||
splashTimer = new Timer(100, null);
|
||||
splashTimer.addActionListener(e -> {
|
||||
splashLabel.setText(loadingStatus);
|
||||
if (gameReady) {
|
||||
splashTimer.stop();
|
||||
Timer closeDelay = new Timer(400, ev -> win.dispose());
|
||||
closeDelay.setRepeats(false);
|
||||
closeDelay.start();
|
||||
}
|
||||
});
|
||||
splashTimer.start();
|
||||
|
||||
return win;
|
||||
} catch (IOException | NullPointerException ignored) {
|
||||
return null;
|
||||
@@ -76,16 +124,14 @@ public class BlightGame extends SimpleApplication {
|
||||
|
||||
@Override
|
||||
public void simpleInitApp() {
|
||||
if (splashWindow != null) {
|
||||
SwingUtilities.invokeLater(() -> { splashWindow.dispose(); splashWindow = null; });
|
||||
}
|
||||
|
||||
status("Lade Tastenbelegung...");
|
||||
flyCam.setEnabled(false);
|
||||
inputManager.deleteMapping(INPUT_MAPPING_EXIT);
|
||||
|
||||
keyBindings = KeyBindingStore.load();
|
||||
graphicsSettings = GraphicsStore.load();
|
||||
|
||||
status("Baue Spielwelt...");
|
||||
worldScene = new WorldScene(keyBindings);
|
||||
stateManager.attach(worldScene);
|
||||
|
||||
@@ -147,7 +193,12 @@ public class BlightGame extends SimpleApplication {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void simpleUpdate(float tpf) {}
|
||||
public void simpleUpdate(float tpf) {
|
||||
if (!gameReady) {
|
||||
gameReady = true; // erstes gerendtertes Frame → Splash schließen
|
||||
status("Bereit");
|
||||
}
|
||||
}
|
||||
|
||||
private static Path findProjectRoot() {
|
||||
String prop = System.getProperty("blight.project.root");
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.bullet.BulletAppState;
|
||||
import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;
|
||||
import com.jme3.bullet.collision.shapes.HeightfieldCollisionShape;
|
||||
import com.jme3.bullet.control.CharacterControl;
|
||||
import com.jme3.bullet.control.RigidBodyControl;
|
||||
import com.jme3.bullet.util.CollisionShapeFactory;
|
||||
@@ -28,6 +29,7 @@ import de.blight.common.MapIO;
|
||||
import de.blight.common.model.CharacterIO;
|
||||
import de.blight.common.model.GameCharacter;
|
||||
import de.blight.common.model.MainCharacter;
|
||||
import de.blight.game.BlightGame;
|
||||
import de.blight.game.animation.AnimationLibrary;
|
||||
import de.blight.game.config.KeyBindings;
|
||||
import de.blight.game.control.PlayerInputControl;
|
||||
@@ -36,6 +38,8 @@ import com.jme3.post.FilterPostProcessor;
|
||||
import com.jme3.post.filters.FogFilter;
|
||||
import com.jme3.water.WaterFilter;
|
||||
import de.blight.game.state.GrassState;
|
||||
import de.blight.game.state.GrassVertexRenderState;
|
||||
import de.blight.game.state.LocationState;
|
||||
import de.blight.game.state.RiverState;
|
||||
import de.blight.game.state.WaterBodyState;
|
||||
import de.blight.game.state.WeatherState;
|
||||
@@ -91,6 +95,7 @@ public class WorldScene extends BaseAppState {
|
||||
this.rootNode = this.app.getRootNode();
|
||||
this.assetManager = app.getAssetManager();
|
||||
|
||||
BlightGame.status("Initialisiere Physik-Engine...");
|
||||
bulletAppState = new BulletAppState();
|
||||
app.getStateManager().attach(bulletAppState);
|
||||
|
||||
@@ -100,17 +105,28 @@ public class WorldScene extends BaseAppState {
|
||||
|
||||
@Override
|
||||
protected void onEnable() {
|
||||
BlightGame.status("Baue Beleuchtung und Himmel...");
|
||||
buildLighting();
|
||||
|
||||
BlightGame.status("Lade Terrain...");
|
||||
TerrainQuad terrain = buildTerrain();
|
||||
|
||||
BlightGame.status("Lade Vegetation...");
|
||||
app.getStateManager().attach(new GrassState(terrain));
|
||||
app.getStateManager().attach(new WaterBodyState(terrain, sharedFPP));
|
||||
app.getStateManager().attach(new GrassVertexRenderState());
|
||||
|
||||
BlightGame.status("Lade Wasserflächen...");
|
||||
app.getStateManager().attach(new WaterBodyState(sharedFPP));
|
||||
|
||||
BlightGame.status("Lade Welt-Objekte...");
|
||||
app.getStateManager().attach(new RiverState());
|
||||
app.getStateManager().attach(new WorldObjectsState());
|
||||
|
||||
BlightGame.status("Lade Charakter...");
|
||||
character = loadOrBuildCharacter();
|
||||
rootNode.attachChild(character);
|
||||
|
||||
BlightGame.status("Initialisiere Spieler-Physik...");
|
||||
// Bullet-Charakter: Kapsel 0.4 Radius, 1.0 Höhe, Y-Achse (1)
|
||||
CapsuleCollisionShape capsule = new CapsuleCollisionShape(0.4f, 1.0f, 1);
|
||||
physicsChar = new CharacterControl(capsule, 0.05f);
|
||||
@@ -128,6 +144,11 @@ public class WorldScene extends BaseAppState {
|
||||
thirdPersonCam = new ThirdPersonCamera(app.getCamera(), app.getInputManager());
|
||||
thirdPersonCam.setTarget(character);
|
||||
|
||||
MainCharacter mc = findMainCharacter();
|
||||
if (mc != null) {
|
||||
app.getStateManager().attach(new LocationState(mc, character));
|
||||
}
|
||||
|
||||
// Maus einfangen – keine Klick-Pflicht für Kamerasteuerung
|
||||
app.getInputManager().setCursorVisible(false);
|
||||
}
|
||||
@@ -324,15 +345,17 @@ public class WorldScene extends BaseAppState {
|
||||
|
||||
/**
|
||||
* Erstellt ein Terrain aus der gespeicherten {@link MapData}.
|
||||
* 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.
|
||||
* Visuell: 4097×4097 Vertices (jeder 4. Editor-Vertex), Scale (1, 1, 1) = 1 m/Vertex.
|
||||
* Physik: 513×513 Vertices (jeder 32. Editor-Vertex), Scale (8, 1, 8) – unsichtbar,
|
||||
* nur für Bullet-Kollision (33,5M Dreiecke bei 4097² wären zu viel für Bullet).
|
||||
*/
|
||||
private TerrainQuad buildTerrainFromMap(MapData map) {
|
||||
final int GAME_VERTS = 513; // 512 Zellen à 8 WE = 4096 WE
|
||||
final int STEP = (MapData.TERRAIN_VERTS - 1) / (GAME_VERTS - 1); // 32
|
||||
final int SRC_VERTS = MapData.TERRAIN_VERTS; // 16385
|
||||
|
||||
// ── Visuelles Terrain (4097 Vertices, 1 m/Vertex) ────────────────────
|
||||
final int GAME_VERTS = 4097;
|
||||
final int STEP = (SRC_VERTS - 1) / (GAME_VERTS - 1); // 4
|
||||
|
||||
float[] heights = new float[GAME_VERTS * GAME_VERTS];
|
||||
for (int gz = 0; gz < GAME_VERTS; gz++) {
|
||||
int sz = gz * STEP;
|
||||
@@ -350,13 +373,13 @@ public class WorldScene extends BaseAppState {
|
||||
+ " → X=" + spawnX + " Z=" + spawnZ);
|
||||
|
||||
TerrainQuad terrain = new TerrainQuad("terrain", 65, GAME_VERTS, heights);
|
||||
terrain.setLocalScale(8f, 1f, 8f);
|
||||
terrain.setLocalScale(1f, 1f, 1f);
|
||||
terrain.setShadowMode(RenderQueue.ShadowMode.Receive);
|
||||
applyTerrainMaterial(terrain, map);
|
||||
rootNode.attachChild(terrain);
|
||||
|
||||
// Terrain-Höhe am Spawnpunkt: lokale Koordinaten = weltXZ / scaleXZ
|
||||
float terrainH = terrain.getHeight(new Vector2f(spawnX / 8f, spawnZ / 8f));
|
||||
// Terrain-Höhe am Spawnpunkt (scale=1 → lokale Koordinaten = Weltkoordinaten)
|
||||
float terrainH = terrain.getHeight(new Vector2f(spawnX, spawnZ));
|
||||
if (Float.isNaN(terrainH)) {
|
||||
float maxH = -Float.MAX_VALUE;
|
||||
for (float h : heights) { if (h > maxH) maxH = h; }
|
||||
@@ -364,8 +387,12 @@ public class WorldScene extends BaseAppState {
|
||||
}
|
||||
spawnY = terrainH + 10f;
|
||||
|
||||
// ── Physik-Terrain: HeightfieldCollisionShape mit identischem heights[]-Array ─
|
||||
// Gleiche 4097×4097-Daten wie das visuelle Terrain → pixel-genaue Übereinstimmung.
|
||||
// HeightfieldCollisionShape ist für Terrain optimiert (kein BVH, O(log n) Queries)
|
||||
// und braucht keine separate TerrainQuad-Instanz.
|
||||
RigidBodyControl terrainPhysics = new RigidBodyControl(
|
||||
CollisionShapeFactory.createMeshShape(terrain), 0f);
|
||||
new HeightfieldCollisionShape(heights, terrain.getLocalScale()), 0f);
|
||||
terrain.addControl(terrainPhysics);
|
||||
bulletAppState.getPhysicsSpace().add(terrainPhysics);
|
||||
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
package de.blight.game.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.renderer.Camera;
|
||||
import com.jme3.renderer.RenderManager;
|
||||
import com.jme3.renderer.ViewPort;
|
||||
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.scene.control.AbstractControl;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.common.GrassVertexBlade;
|
||||
import de.blight.common.GrassVertexIO;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Rendert Vertex-Gras-Büschel im Spiel: 3 geneigte, verjüngte Halme pro Büschel,
|
||||
* beleuchtet über NormalBuffer + Custom-Shader, chunk-basiertes Lazy-Loading.
|
||||
*/
|
||||
public class GrassVertexRenderState extends BaseAppState {
|
||||
|
||||
// ── Chunks ────────────────────────────────────────────────────────────────
|
||||
private static final int TERRAIN_HALF = 2048;
|
||||
private static final int CHUNK_SIZE = 128;
|
||||
private static final int CHUNKS_PER_AXIS = (TERRAIN_HALF * 2) / CHUNK_SIZE;
|
||||
private static final int CHUNK_COUNT = CHUNKS_PER_AXIS * CHUNKS_PER_AXIS;
|
||||
private static final int INIT_PER_FRAME = 4;
|
||||
private static final float FAR_DIST_SQ = 200f * 200f;
|
||||
|
||||
// ── Geometrie (identisch zu GrassVertexState) ─────────────────────────────
|
||||
private static final int BLADES_PER_TUFT = 3;
|
||||
private static final int SEGMENTS = 5;
|
||||
private static final float WIDTH_FACTOR = 0.05f;
|
||||
private static final float BEND_FACTOR = 0.15f;
|
||||
private static final ColorRGBA ROOT_COLOR = new ColorRGBA(0.08f, 0.34f, 0.04f, 1f);
|
||||
private static final ColorRGBA TIP_COLOR = new ColorRGBA(0.26f, 0.72f, 0.11f, 1f);
|
||||
private static final ColorRGBA DRY_ROOT_COLOR = new ColorRGBA(0.45f, 0.35f, 0.08f, 1f);
|
||||
private static final ColorRGBA DRY_TIP_COLOR = new ColorRGBA(0.82f, 0.74f, 0.16f, 1f);
|
||||
|
||||
// ── Zustand ───────────────────────────────────────────────────────────────
|
||||
private Camera cam;
|
||||
private AssetManager assetManager;
|
||||
private Node grassNode;
|
||||
private Material material;
|
||||
private int nextChunk = 0;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private final List<GrassVertexBlade>[] chunkBlades = new List[CHUNK_COUNT];
|
||||
private final Node[] chunkNodes = new Node[CHUNK_COUNT];
|
||||
|
||||
@Override
|
||||
protected void initialize(Application app) {
|
||||
this.cam = app.getCamera();
|
||||
this.assetManager = app.getAssetManager();
|
||||
grassNode = new Node("grassVertexNode");
|
||||
((SimpleApplication) app).getRootNode().attachChild(grassNode);
|
||||
|
||||
for (int i = 0; i < CHUNK_COUNT; i++) chunkBlades[i] = new ArrayList<>();
|
||||
|
||||
try {
|
||||
for (GrassVertexBlade b : GrassVertexIO.load()) {
|
||||
int ci = chunkIndex(b.x(), b.z());
|
||||
if (ci >= 0) chunkBlades[ci].add(b);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("[GrassVertexRenderState] Daten nicht ladbar: " + e.getMessage());
|
||||
}
|
||||
|
||||
material = buildMaterial();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application app) {
|
||||
((SimpleApplication) app).getRootNode().detachChild(grassNode);
|
||||
}
|
||||
|
||||
@Override protected void onEnable() { grassNode.setCullHint(Spatial.CullHint.Inherit); }
|
||||
@Override protected void onDisable() { grassNode.setCullHint(Spatial.CullHint.Always); }
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
int built = 0;
|
||||
while (nextChunk < CHUNK_COUNT && built < INIT_PER_FRAME) {
|
||||
buildChunk(nextChunk++);
|
||||
built++;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Material ──────────────────────────────────────────────────────────────
|
||||
|
||||
private Material buildMaterial() {
|
||||
try {
|
||||
Material mat = new Material(assetManager, "MatDefs/GrassVertex.j3md");
|
||||
mat.setFloat("WindSpeed", 1.0f);
|
||||
mat.setFloat("WindStrength", 0.15f);
|
||||
mat.setVector3("SunDir", new Vector3f(0.35f, 0.8f, 0.45f).normalizeLocal());
|
||||
mat.setColor("SunColor", new ColorRGBA(0.95f, 0.90f, 0.75f, 1.0f));
|
||||
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
||||
return mat;
|
||||
} catch (Exception e) {
|
||||
System.err.println("[GrassVertexRenderState] Material nicht ladbar: " + e.getMessage());
|
||||
Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", new ColorRGBA(0.22f, 0.68f, 0.12f, 1f));
|
||||
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
||||
return mat;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Chunk-Verwaltung ──────────────────────────────────────────────────────
|
||||
|
||||
private int chunkIndex(float x, float z) {
|
||||
int cx = (int) ((x + TERRAIN_HALF) / CHUNK_SIZE);
|
||||
int cz = (int) ((z + TERRAIN_HALF) / CHUNK_SIZE);
|
||||
if (cx < 0 || cx >= CHUNKS_PER_AXIS || cz < 0 || cz >= CHUNKS_PER_AXIS) return -1;
|
||||
return cz * CHUNKS_PER_AXIS + cx;
|
||||
}
|
||||
|
||||
private void buildChunk(int ci) {
|
||||
List<GrassVertexBlade> blades = chunkBlades[ci];
|
||||
if (blades.isEmpty()) return;
|
||||
|
||||
int bladeCount = blades.size();
|
||||
int vertCount = bladeCount * BLADES_PER_TUFT * (SEGMENTS + 1) * 2;
|
||||
int indexCount = bladeCount * BLADES_PER_TUFT * SEGMENTS * 6;
|
||||
|
||||
float[] positions = new float[vertCount * 3];
|
||||
float[] normals = new float[vertCount * 3];
|
||||
float[] colors = new float[vertCount * 4];
|
||||
float[] texCoords = new float[vertCount * 2];
|
||||
int[] indices = new int[indexCount];
|
||||
|
||||
int vi = 0, ii = 0;
|
||||
for (GrassVertexBlade blade : blades) {
|
||||
buildTuft(positions, normals, colors, texCoords, indices, vi, ii, blade);
|
||||
vi += BLADES_PER_TUFT * (SEGMENTS + 1) * 2;
|
||||
ii += BLADES_PER_TUFT * SEGMENTS * 6;
|
||||
}
|
||||
|
||||
Mesh mesh = new Mesh();
|
||||
mesh.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(positions));
|
||||
mesh.setBuffer(VertexBuffer.Type.Normal, 3, BufferUtils.createFloatBuffer(normals));
|
||||
mesh.setBuffer(VertexBuffer.Type.Color, 4, BufferUtils.createFloatBuffer(colors));
|
||||
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, BufferUtils.createFloatBuffer(texCoords));
|
||||
mesh.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createIntBuffer(indices));
|
||||
mesh.updateBound();
|
||||
|
||||
Geometry geo = new Geometry("gv_" + ci, mesh);
|
||||
geo.setMaterial(material);
|
||||
|
||||
int cx = ci % CHUNKS_PER_AXIS;
|
||||
int cz = ci / CHUNKS_PER_AXIS;
|
||||
float chunkCX = -TERRAIN_HALF + cx * CHUNK_SIZE + CHUNK_SIZE * 0.5f;
|
||||
float chunkCZ = -TERRAIN_HALF + cz * CHUNK_SIZE + CHUNK_SIZE * 0.5f;
|
||||
|
||||
Node node = new Node("gvc_" + ci);
|
||||
node.attachChild(geo);
|
||||
node.addControl(new VisibilityControl(cam, new Vector3f(chunkCX, 0f, chunkCZ)));
|
||||
chunkNodes[ci] = node;
|
||||
grassNode.attachChild(node);
|
||||
}
|
||||
|
||||
// ── Mesh-Logik (gespiegelt zu GrassVertexState) ───────────────────────────
|
||||
|
||||
private static void buildTuft(float[] pos, float[] nrm, float[] col, float[] tex, int[] idx,
|
||||
int vi, int ii, GrassVertexBlade blade) {
|
||||
float x = blade.x(), y = blade.y(), z = blade.z(), h = blade.height();
|
||||
float baseHW = h * WIDTH_FACTOR * 0.5f;
|
||||
float tAngle = (float) (((x * 127.1f + z * 311.7f) % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2));
|
||||
|
||||
for (int b = 0; b < BLADES_PER_TUFT; b++) {
|
||||
float bf = b;
|
||||
|
||||
float jitter = (hash(x + bf * 13.7f, z + bf * 19.3f) - 0.5f) * 0.4f;
|
||||
float bladeAngle = tAngle + b * (float) (Math.PI * 2.0 / BLADES_PER_TUFT) + jitter;
|
||||
float leanMag = 0.25f + hash(x + bf * 7.3f, z + bf * 3.1f) * 0.55f;
|
||||
float leanDir = bladeAngle + (float) (Math.PI * 0.5) * (b % 2 == 0 ? 1f : -1f);
|
||||
|
||||
float offR = hash(x + bf * 2.9f, z + bf * 5.3f) * 0.28f;
|
||||
float offA = hash(x + bf * 3.7f, z + bf * 1.7f) * (float) (Math.PI * 2);
|
||||
float bx = x + offR * (float) Math.cos(offA);
|
||||
float bz = z + offR * (float) Math.sin(offA);
|
||||
|
||||
float cosA = (float) Math.cos(bladeAngle);
|
||||
float sinA = (float) Math.sin(bladeAngle);
|
||||
|
||||
float lnX = (float) (Math.sin(leanMag) * Math.cos(leanDir));
|
||||
float lnY = (float) Math.cos(leanMag);
|
||||
float lnZ = (float) (Math.sin(leanMag) * Math.sin(leanDir));
|
||||
|
||||
float bendStr = (hash(x + bf * 5.1f, z + bf * 8.7f) - 0.5f) * 2f * BEND_FACTOR * h;
|
||||
float bendX = -sinA * bendStr;
|
||||
float bendZ = cosA * bendStr;
|
||||
|
||||
int bladeVi = vi + b * (SEGMENTS + 1) * 2;
|
||||
int bladeIi = ii + b * SEGMENTS * 6;
|
||||
|
||||
for (int s = 0; s <= SEGMENTS; s++) {
|
||||
float t = (float) s / SEGMENTS;
|
||||
float hw = baseHW * (float) Math.pow(1.0 - t, 1.4);
|
||||
|
||||
float spX = bx + lnX * h * t + bendX * t * t;
|
||||
float spY = y + lnY * h * t;
|
||||
float spZ = bz + lnZ * h * t + bendZ * t * t;
|
||||
|
||||
float tgX = lnX + 2f * bendX * t;
|
||||
float tgY = lnY;
|
||||
float tgZ = lnZ + 2f * bendZ * t;
|
||||
float tgLen = (float) Math.sqrt(tgX*tgX + tgY*tgY + tgZ*tgZ);
|
||||
if (tgLen > 1e-6f) { tgX /= tgLen; tgY /= tgLen; tgZ /= tgLen; }
|
||||
|
||||
float nx = -sinA * tgY;
|
||||
float ny = sinA * tgX - cosA * tgZ;
|
||||
float nz = cosA * tgY;
|
||||
float nLen = (float) Math.sqrt(nx*nx + ny*ny + nz*nz);
|
||||
if (nLen > 1e-6f) { nx /= nLen; ny /= nLen; nz /= nLen; }
|
||||
|
||||
float blend = 0.30f;
|
||||
nx *= (1f - blend);
|
||||
ny = ny * (1f - blend) + blend;
|
||||
nz *= (1f - blend);
|
||||
nLen = (float) Math.sqrt(nx*nx + ny*ny + nz*nz);
|
||||
if (nLen > 1e-6f) { nx /= nLen; ny /= nLen; nz /= nLen; }
|
||||
|
||||
int svi = bladeVi + s * 2;
|
||||
float dry = blade.dryness();
|
||||
setV(pos, nrm, col, tex, svi, spX - cosA * hw, spY, spZ - sinA * hw, nx, ny, nz, t, dry);
|
||||
setV(pos, nrm, col, tex, svi+1, spX + cosA * hw, spY, spZ + sinA * hw, nx, ny, nz, t, dry);
|
||||
|
||||
if (s < SEGMENTS) {
|
||||
int sii = bladeIi + s * 6;
|
||||
idx[sii] = svi; idx[sii+1] = svi+1; idx[sii+2] = svi+3;
|
||||
idx[sii+3] = svi; idx[sii+4] = svi+3; idx[sii+5] = svi+2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static float hash(float a, float b) {
|
||||
int ia = Float.floatToRawIntBits(a);
|
||||
int ib = Float.floatToRawIntBits(b);
|
||||
int h = ia ^ (ib * 0x9e3779b9);
|
||||
h ^= h >>> 16;
|
||||
h *= 0x45d9f3b;
|
||||
h ^= h >>> 16;
|
||||
return (h & 0x7FFFFFFF) / (float) 0x7FFFFFFF;
|
||||
}
|
||||
|
||||
private static void setV(float[] pos, float[] nrm, float[] col, float[] tex, int vi,
|
||||
float x, float y, float z, float nx, float ny, float nz,
|
||||
float wf, float dryness) {
|
||||
int pi = vi * 3;
|
||||
pos[pi] = x; pos[pi+1] = y; pos[pi+2] = z;
|
||||
|
||||
int ni = vi * 3;
|
||||
nrm[ni] = nx; nrm[ni+1] = ny; nrm[ni+2] = nz;
|
||||
|
||||
int ci = vi * 4;
|
||||
float gr = ROOT_COLOR.r + (TIP_COLOR.r - ROOT_COLOR.r) * wf;
|
||||
float gg = ROOT_COLOR.g + (TIP_COLOR.g - ROOT_COLOR.g) * wf;
|
||||
float gb = ROOT_COLOR.b + (TIP_COLOR.b - ROOT_COLOR.b) * wf;
|
||||
float dr = DRY_ROOT_COLOR.r + (DRY_TIP_COLOR.r - DRY_ROOT_COLOR.r) * wf;
|
||||
float dg = DRY_ROOT_COLOR.g + (DRY_TIP_COLOR.g - DRY_ROOT_COLOR.g) * wf;
|
||||
float db = DRY_ROOT_COLOR.b + (DRY_TIP_COLOR.b - DRY_ROOT_COLOR.b) * wf;
|
||||
col[ci] = gr + (dr - gr) * dryness;
|
||||
col[ci+1] = gg + (dg - gg) * dryness;
|
||||
col[ci+2] = gb + (db - gb) * dryness;
|
||||
col[ci+3] = 1f;
|
||||
|
||||
int ti = vi * 2;
|
||||
tex[ti] = wf; tex[ti+1] = 0f;
|
||||
}
|
||||
|
||||
// ── LOD-Culling ───────────────────────────────────────────────────────────
|
||||
|
||||
private static final class VisibilityControl extends AbstractControl {
|
||||
private final Camera cam;
|
||||
private final Vector3f center;
|
||||
|
||||
VisibilityControl(Camera cam, Vector3f center) {
|
||||
this.cam = cam;
|
||||
this.center = center;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void controlUpdate(float tpf) {
|
||||
float dx = cam.getLocation().x - center.x;
|
||||
float dz = cam.getLocation().z - center.z;
|
||||
spatial.setCullHint(dx*dx + dz*dz <= FAR_DIST_SQ
|
||||
? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||
}
|
||||
|
||||
@Override protected void controlRender(RenderManager rm, ViewPort vp) {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package de.blight.game.state;
|
||||
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.math.Vector3f;
|
||||
import de.blight.common.LocationIO;
|
||||
import de.blight.common.model.Location;
|
||||
import de.blight.common.model.MainCharacter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Verfolgt die Spieler-Position und feuert Location-Trigger wenn der Charakter
|
||||
* eine Location betritt (Übergang outside→inside).
|
||||
*/
|
||||
public class LocationState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LocationState.class);
|
||||
|
||||
private final MainCharacter character;
|
||||
private List<Location> locations;
|
||||
private final Set<String> active = new HashSet<>();
|
||||
|
||||
/** Referenz auf die Node/Control, von der wir die Spieler-Position lesen. */
|
||||
private final com.jme3.scene.Node playerNode;
|
||||
|
||||
public LocationState(MainCharacter character, com.jme3.scene.Node playerNode) {
|
||||
this.character = character;
|
||||
this.playerNode = playerNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialize(Application app) {
|
||||
try {
|
||||
locations = LocationIO.load();
|
||||
log.info("{} Location(s) geladen.", locations.size());
|
||||
} catch (Exception e) {
|
||||
log.error("Locations nicht ladbar", e);
|
||||
locations = List.of();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
if (locations.isEmpty() || playerNode == null) return;
|
||||
Vector3f pos = playerNode.getWorldTranslation();
|
||||
float px = pos.x, pz = pos.z;
|
||||
|
||||
for (Location loc : locations) {
|
||||
boolean inside = loc.contains(px, pz);
|
||||
boolean wasInside = active.contains(loc.getId());
|
||||
if (inside && !wasInside) {
|
||||
active.add(loc.getId());
|
||||
loc.entered(character);
|
||||
log.debug("Location betreten: {}", loc.getId());
|
||||
} else if (!inside) {
|
||||
active.remove(loc.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override protected void cleanup(Application app) {}
|
||||
@Override protected void onEnable() {}
|
||||
@Override protected void onDisable() {}
|
||||
}
|
||||
@@ -8,8 +8,8 @@ import com.jme3.audio.AudioData;
|
||||
import com.jme3.audio.AudioNode;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.scene.Node;
|
||||
import de.blight.common.MusicAreaIO;
|
||||
import de.blight.common.PlacedMusicArea;
|
||||
import de.blight.common.AreaIO;
|
||||
import de.blight.common.PlacedArea;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -36,7 +36,7 @@ public class MusicSystem extends BaseAppState {
|
||||
private AssetManager assets;
|
||||
private Node rootNode;
|
||||
|
||||
private final List<PlacedMusicArea> data = new ArrayList<>();
|
||||
private final List<PlacedArea> data = new ArrayList<>();
|
||||
// three nodes per area: [0]=day, [1]=night, [2]=combat; element may be null
|
||||
private final List<AudioNode[]> tracks = new ArrayList<>();
|
||||
private final List<FadeState> fadeStates = new ArrayList<>();
|
||||
@@ -51,7 +51,7 @@ public class MusicSystem extends BaseAppState {
|
||||
rootNode = app.getRootNode();
|
||||
|
||||
try {
|
||||
for (PlacedMusicArea area : MusicAreaIO.load()) {
|
||||
for (PlacedArea area : AreaIO.load()) {
|
||||
AudioNode[] arr = {
|
||||
loadAmbient(area.dayTrack()),
|
||||
loadAmbient(area.nightTrack()),
|
||||
@@ -131,7 +131,7 @@ public class MusicSystem extends BaseAppState {
|
||||
checkTimer = 0f;
|
||||
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
PlacedMusicArea area = data.get(i);
|
||||
PlacedArea area = data.get(i);
|
||||
boolean inside = pointInPolygon(playerPos.x, playerPos.z, area.pointsX(), area.pointsZ());
|
||||
FadeState fs = fadeStates.get(i);
|
||||
|
||||
|
||||
@@ -3,57 +3,38 @@ package de.blight.game.state;
|
||||
import com.jme3.app.Application;
|
||||
import com.jme3.app.SimpleApplication;
|
||||
import com.jme3.app.state.BaseAppState;
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.material.RenderState;
|
||||
import com.jme3.math.*;
|
||||
import com.jme3.math.ColorRGBA;
|
||||
import com.jme3.math.Vector2f;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.post.FilterPostProcessor;
|
||||
import com.jme3.renderer.queue.RenderQueue;
|
||||
import com.jme3.scene.*;
|
||||
import com.jme3.scene.VertexBuffer;
|
||||
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import com.jme3.water.WaterFilter;
|
||||
import de.blight.common.PlacedWater;
|
||||
import de.blight.common.WaterBodyIO;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.FloatBuffer;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Rendert im Editor platzierte Wasserflächen per WaterFilter (visuelle Qualität).
|
||||
* Ein unsichtbares Komplementär-Mesh (nur Tiefenpuffer) verhindert, dass der
|
||||
* WaterFilter außerhalb der Flood-Fill-Form Wasser rendert.
|
||||
* Rendert Polygon-Wasserflächen per WaterPolygonFilter (ein Filter pro Fläche).
|
||||
* Identisches Aussehen wie WaterFilter, aber auf das eingezeichnete Polygon beschränkt.
|
||||
*/
|
||||
public class WaterBodyState extends BaseAppState {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(WaterBodyState.class);
|
||||
private static final int WATER_GRID = 2049;
|
||||
private static final int STEP = 2;
|
||||
private static final int WORLD_HALF = 2048;
|
||||
private static final int MAX_CELLS = 500_000;
|
||||
private static final float MASK_MARGIN = 20f; // Puffer um den Becken-Radius
|
||||
|
||||
private final TerrainQuad terrain;
|
||||
private final FilterPostProcessor fpp;
|
||||
private Node waterNode;
|
||||
private final List<WaterFilter> waterFilters = new ArrayList<>();
|
||||
private static final Vector3f SUN_DIR = new Vector3f(-0.5f, -1f, -0.5f).normalizeLocal();
|
||||
|
||||
public WaterBodyState(TerrainQuad terrain, FilterPostProcessor fpp) {
|
||||
this.terrain = terrain;
|
||||
this.fpp = fpp;
|
||||
private final FilterPostProcessor fpp;
|
||||
private final List<WaterPolygonFilter> filters = new ArrayList<>();
|
||||
|
||||
public WaterBodyState(FilterPostProcessor fpp) {
|
||||
this.fpp = fpp;
|
||||
}
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
@Override
|
||||
protected void initialize(Application app) {
|
||||
SimpleApplication sa = (SimpleApplication) app;
|
||||
waterNode = new Node("waterBodies");
|
||||
sa.getRootNode().attachChild(waterNode);
|
||||
|
||||
List<PlacedWater> bodies;
|
||||
try {
|
||||
@@ -64,214 +45,37 @@ public class WaterBodyState extends BaseAppState {
|
||||
}
|
||||
if (bodies.isEmpty()) return;
|
||||
|
||||
Vector3f sunDir = new Vector3f(-0.5f, -1f, -0.5f).normalizeLocal();
|
||||
|
||||
for (PlacedWater body : bodies) {
|
||||
float[] xs = body.pointsX();
|
||||
float[] zs = body.pointsZ();
|
||||
if (xs.length < 3) continue;
|
||||
try {
|
||||
Set<Integer> cells = floodFill(body.seedX(), body.seedZ(), body.waterHeight());
|
||||
if (cells == null || cells.isEmpty()) {
|
||||
log.warn("Becken nicht rekonstruierbar: {}/{}", body.seedX(), body.seedZ());
|
||||
continue;
|
||||
}
|
||||
|
||||
float[] cr = computeCentroidAndRadius(cells);
|
||||
float cx = cr[0], cz = cr[1];
|
||||
float filterRadius = cr[2] + MASK_MARGIN;
|
||||
|
||||
WaterFilter wf = new WaterFilter(sa.getRootNode(), sunDir);
|
||||
wf.setWaterHeight(body.waterHeight());
|
||||
wf.setCenter(new Vector3f(cx, body.waterHeight(), cz));
|
||||
wf.setRadius(filterRadius);
|
||||
wf.setShapeType(WaterFilter.AreaShape.Circular);
|
||||
wf.setWaterColor(new ColorRGBA(0.05f, 0.25f, 0.55f, 1f));
|
||||
wf.setDeepWaterColor(new ColorRGBA(0.02f, 0.12f, 0.30f, 1f));
|
||||
wf.setWaterTransparency(0.15f);
|
||||
wf.setMaxAmplitude(0.3f);
|
||||
wf.setWaveScale(0.008f);
|
||||
wf.setSpeed(0.5f);
|
||||
fpp.addFilter(wf);
|
||||
waterFilters.add(wf);
|
||||
|
||||
// Tiefenpuffer-Maske: alle Zellen im Filterkreis außerhalb des Beckens
|
||||
Geometry mask = buildDepthMask(cells, cx, cz, filterRadius, body.waterHeight(),
|
||||
app.getAssetManager());
|
||||
if (mask != null) waterNode.attachChild(mask);
|
||||
|
||||
log.info("Becken: cells={} h={} center=({},{}) r={}",
|
||||
cells.size(), body.waterHeight(), cx, cz, filterRadius);
|
||||
WaterPolygonFilter f = new WaterPolygonFilter(
|
||||
sa.getRootNode(), SUN_DIR, body.waterHeight(), xs, zs);
|
||||
f.setWaterColor(new ColorRGBA(0.05f, 0.25f, 0.55f, 1f));
|
||||
f.setDeepWaterColor(new ColorRGBA(0.02f, 0.12f, 0.30f, 1f));
|
||||
f.setWaterTransparency(0.15f);
|
||||
f.setMaxAmplitude(0.1f); // reduced to keep visual height close to waterHeight
|
||||
f.setWaveScale(0.008f);
|
||||
f.setSpeed(0.5f);
|
||||
float rad = (float) Math.toRadians(body.flowDegrees());
|
||||
f.setWindDirection(new Vector2f((float) Math.sin(rad), (float) Math.cos(rad)));
|
||||
fpp.addFilter(f);
|
||||
filters.add(f);
|
||||
log.info("Wasserfläche geladen: {} Punkte, h={}", xs.length, body.waterHeight());
|
||||
} catch (Exception e) {
|
||||
log.error("Fehler bei Becken {}/{}", body.seedX(), body.seedZ(), e);
|
||||
log.error("Fehler beim Laden einer Wasserfläche", e);
|
||||
}
|
||||
}
|
||||
log.info("{}/{} Wasserfläche(n) geladen.", waterFilters.size(), bodies.size());
|
||||
log.info("{}/{} Wasserfläche(n) geladen.", filters.size(), bodies.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void cleanup(Application app) {
|
||||
for (WaterFilter wf : waterFilters) fpp.removeFilter(wf);
|
||||
waterFilters.clear();
|
||||
if (waterNode != null)
|
||||
((SimpleApplication) app).getRootNode().detachChild(waterNode);
|
||||
for (WaterPolygonFilter f : filters) fpp.removeFilter(f);
|
||||
filters.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onEnable() {
|
||||
if (waterNode != null) waterNode.setCullHint(Spatial.CullHint.Inherit);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDisable() {
|
||||
if (waterNode != null) waterNode.setCullHint(Spatial.CullHint.Always);
|
||||
}
|
||||
|
||||
// ── Flood-Fill ────────────────────────────────────────────────────────────
|
||||
|
||||
private Set<Integer> floodFill(float seedWorldX, float seedWorldZ, float waterHeight) {
|
||||
int seedPX = Math.round((seedWorldX + WORLD_HALF) / (float) STEP);
|
||||
int seedPZ = Math.round((seedWorldZ + WORLD_HALF) / (float) STEP);
|
||||
seedPX = Math.max(0, Math.min(WATER_GRID - 1, seedPX));
|
||||
seedPZ = Math.max(0, Math.min(WATER_GRID - 1, seedPZ));
|
||||
|
||||
Map<Integer, Float> heightCache = new HashMap<>();
|
||||
float seedH = sampleHeight(seedPX, seedPZ, heightCache);
|
||||
if (seedH > waterHeight + 0.5f) {
|
||||
log.warn("Seed-Höhe {} über waterHeight {} – Becken nicht rekonstruierbar", seedH, waterHeight);
|
||||
return null;
|
||||
}
|
||||
|
||||
Set<Integer> visited = new HashSet<>();
|
||||
Deque<int[]> queue = new ArrayDeque<>();
|
||||
visited.add(seedPZ * WATER_GRID + seedPX);
|
||||
queue.add(new int[]{seedPX, seedPZ});
|
||||
|
||||
final int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
|
||||
|
||||
while (!queue.isEmpty()) {
|
||||
int[] c = queue.poll();
|
||||
int px = c[0], pz = c[1];
|
||||
|
||||
if (px == 0 || px == WATER_GRID - 1 || pz == 0 || pz == WATER_GRID - 1)
|
||||
return null;
|
||||
|
||||
for (int[] d : dirs) {
|
||||
int nx = px + d[0], nz = pz + d[1];
|
||||
int nIdx = nz * WATER_GRID + nx;
|
||||
if (visited.contains(nIdx)) continue;
|
||||
if (sampleHeight(nx, nz, heightCache) <= waterHeight) {
|
||||
visited.add(nIdx);
|
||||
if (visited.size() > MAX_CELLS) return null;
|
||||
queue.add(new int[]{nx, nz});
|
||||
}
|
||||
}
|
||||
}
|
||||
return visited.isEmpty() ? null : visited;
|
||||
}
|
||||
|
||||
private float sampleHeight(int px, int pz, Map<Integer, Float> cache) {
|
||||
int key = pz * WATER_GRID + px;
|
||||
Float cached = cache.get(key);
|
||||
if (cached != null) return cached;
|
||||
float worldX = (float)(px * STEP) - WORLD_HALF;
|
||||
float worldZ = (float)(pz * STEP) - WORLD_HALF;
|
||||
Float h = terrain.getHeight(new Vector2f(worldX, worldZ));
|
||||
float height = (h != null && !Float.isNaN(h)) ? h : Float.MAX_VALUE;
|
||||
cache.put(key, height);
|
||||
return height;
|
||||
}
|
||||
|
||||
// ── Geometrie-Hilfsmethoden ───────────────────────────────────────────────
|
||||
|
||||
private static float[] computeCentroidAndRadius(Set<Integer> cells) {
|
||||
double sumX = 0, sumZ = 0;
|
||||
for (int cell : cells) {
|
||||
sumX += (double)((cell % WATER_GRID) * STEP) - WORLD_HALF;
|
||||
sumZ += (double)((cell / WATER_GRID) * STEP) - WORLD_HALF;
|
||||
}
|
||||
float cx = (float)(sumX / cells.size());
|
||||
float cz = (float)(sumZ / cells.size());
|
||||
float maxR = 0f;
|
||||
for (int cell : cells) {
|
||||
float wx = (float)((cell % WATER_GRID) * STEP) - WORLD_HALF;
|
||||
float wz = (float)((cell / WATER_GRID) * STEP) - WORLD_HALF;
|
||||
float dx = wx - cx, dz = wz - cz;
|
||||
float r = FastMath.sqrt(dx * dx + dz * dz);
|
||||
if (r > maxR) maxR = r;
|
||||
}
|
||||
return new float[]{cx, cz, maxR};
|
||||
}
|
||||
|
||||
/**
|
||||
* Erstellt ein unsichtbares Mesh bei waterHeight+0.01 für alle Zellen im
|
||||
* Filterkreis, die NICHT zum Becken gehören.
|
||||
*
|
||||
* Das Mesh schreibt nur in den Tiefenpuffer (ColorWrite=false, DepthWrite=true).
|
||||
* Vom WaterFilter aus gesehen liegt diese "Fläche" über dem Terrain (höheres Y =
|
||||
* näher zur Kamera von oben) und blockiert damit das Wasser-Rendering außerhalb
|
||||
* des Beckens.
|
||||
*/
|
||||
private static Geometry buildDepthMask(Set<Integer> basinCells, float cx, float cz,
|
||||
float radius, float waterHeight,
|
||||
AssetManager assets) {
|
||||
float h = waterHeight + 0.01f;
|
||||
float r2 = radius * radius;
|
||||
|
||||
int minPX = Math.max(0, (int) Math.floor(((cx - radius) + WORLD_HALF) / STEP) - 1);
|
||||
int maxPX = Math.min(WATER_GRID - 1, (int) Math.ceil (((cx + radius) + WORLD_HALF) / STEP) + 1);
|
||||
int minPZ = Math.max(0, (int) Math.floor(((cz - radius) + WORLD_HALF) / STEP) - 1);
|
||||
int maxPZ = Math.min(WATER_GRID - 1, (int) Math.ceil (((cz + radius) + WORLD_HALF) / STEP) + 1);
|
||||
|
||||
List<Integer> maskCells = new ArrayList<>();
|
||||
for (int pz = minPZ; pz <= maxPZ; pz++) {
|
||||
for (int px = minPX; px <= maxPX; px++) {
|
||||
int cellIdx = pz * WATER_GRID + px;
|
||||
if (basinCells.contains(cellIdx)) continue;
|
||||
float wx = (float)(px * STEP) - WORLD_HALF;
|
||||
float wz = (float)(pz * STEP) - WORLD_HALF;
|
||||
float dx = wx - cx, dz = wz - cz;
|
||||
if (dx * dx + dz * dz <= r2) maskCells.add(cellIdx);
|
||||
}
|
||||
}
|
||||
if (maskCells.isEmpty()) return null;
|
||||
|
||||
int n = maskCells.size();
|
||||
FloatBuffer pos = BufferUtils.createFloatBuffer(n * 4 * 3);
|
||||
IntBuffer idx = BufferUtils.createIntBuffer(n * 6);
|
||||
int vi = 0;
|
||||
for (int cell : maskCells) {
|
||||
int pz = cell / WATER_GRID;
|
||||
int px = cell % WATER_GRID;
|
||||
float wx = (float)(px * STEP) - WORLD_HALF;
|
||||
float wz = (float)(pz * STEP) - WORLD_HALF;
|
||||
pos.put(wx ).put(h).put(wz );
|
||||
pos.put(wx + STEP).put(h).put(wz );
|
||||
pos.put(wx + STEP).put(h).put(wz + STEP);
|
||||
pos.put(wx ).put(h).put(wz + STEP);
|
||||
// CCW von oben → Normalen zeigen +Y
|
||||
idx.put(vi).put(vi+2).put(vi+1);
|
||||
idx.put(vi).put(vi+3).put(vi+2);
|
||||
vi += 4;
|
||||
}
|
||||
|
||||
pos.rewind(); idx.rewind();
|
||||
|
||||
Mesh mesh = new Mesh();
|
||||
mesh.setBuffer(VertexBuffer.Type.Position, 3, pos);
|
||||
mesh.setBuffer(VertexBuffer.Type.Index, 3, idx);
|
||||
mesh.updateBound();
|
||||
mesh.updateCounts();
|
||||
|
||||
Geometry geo = new Geometry("water_mask", mesh);
|
||||
geo.setShadowMode(RenderQueue.ShadowMode.Off);
|
||||
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||
mat.setColor("Color", ColorRGBA.Black);
|
||||
mat.getAdditionalRenderState().setColorWrite(false);
|
||||
mat.getAdditionalRenderState().setDepthWrite(true);
|
||||
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
||||
geo.setMaterial(mat);
|
||||
// Transparent: renders after Opaque terrain, so terrain depth is already in buffer.
|
||||
// The mask (closer to camera = higher Y) passes depth test and overwrites it,
|
||||
// blocking WaterFilter from reading terrain Y < waterHeight at non-basin pixels.
|
||||
geo.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
return geo;
|
||||
}
|
||||
@Override protected void onEnable() {}
|
||||
@Override protected void onDisable() {}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package de.blight.game.state;
|
||||
|
||||
import com.jme3.asset.AssetManager;
|
||||
import com.jme3.material.MatParam;
|
||||
import com.jme3.material.Material;
|
||||
import com.jme3.math.Vector2f;
|
||||
import com.jme3.math.Vector3f;
|
||||
import com.jme3.renderer.RenderManager;
|
||||
import com.jme3.renderer.ViewPort;
|
||||
import com.jme3.scene.Node;
|
||||
import com.jme3.shader.VarType;
|
||||
import com.jme3.water.WaterFilter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* WaterFilter mit Polygon-Beschränkung.
|
||||
* Verwendet exakt denselben Shader wie WaterFilter, erweitert um einen
|
||||
* GLSL-Point-in-Polygon-Test der das Wasser auf den eingezeichneten Bereich begrenzt.
|
||||
*/
|
||||
public class WaterPolygonFilter extends WaterFilter {
|
||||
|
||||
private static final int MAX_POINTS = 64;
|
||||
|
||||
private final Vector2f[] polygonPoints;
|
||||
|
||||
public WaterPolygonFilter(Node scene, Vector3f sunDir,
|
||||
float waterHeight, float[] pointsX, float[] pointsZ) {
|
||||
super(scene, sunDir);
|
||||
setWaterHeight(waterHeight);
|
||||
int n = Math.min(pointsX.length, MAX_POINTS);
|
||||
polygonPoints = new Vector2f[n];
|
||||
for (int i = 0; i < n; i++)
|
||||
polygonPoints[i] = new Vector2f(pointsX[i], pointsZ[i]);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initFilter(AssetManager manager, RenderManager renderManager,
|
||||
ViewPort vp, int w, int h) {
|
||||
super.initFilter(manager, renderManager, vp, w, h);
|
||||
|
||||
// WaterFilter set up `material` from Water.j3md — swap to our extended version
|
||||
// that adds POLYGON_AREA check in the fragment shader.
|
||||
Material newMat = new Material(manager, "MatDefs/WaterPolygon.j3md");
|
||||
|
||||
// Copy every parameter WaterFilter set (textures, floats, vectors, booleans…)
|
||||
for (MatParam p : new ArrayList<>(material.getParams())) {
|
||||
try {
|
||||
newMat.setParam(p.getName(), p.getVarType(), p.getValue());
|
||||
} catch (Exception ignored) {
|
||||
// Skip params the new j3md doesn't know about
|
||||
}
|
||||
}
|
||||
|
||||
// Inject polygon points
|
||||
newMat.setParam("Points", VarType.Vector2Array, polygonPoints);
|
||||
newMat.setInt("NumPoints", polygonPoints.length);
|
||||
|
||||
// Replace the protected field — all subsequent WaterFilter setters use this field directly
|
||||
material = newMat;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user