diff --git a/blight-editor/src/main/java/de/blight/editor/state/GrassVertexState.java b/blight-editor/src/main/java/de/blight/editor/state/GrassVertexState.java index 3cad917..3e84d9e 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/GrassVertexState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/GrassVertexState.java @@ -218,6 +218,8 @@ public class GrassVertexState extends BaseAppState { float jmeY = (float) (edit.screenY() * input.viewportScaleY); Vector3f hit = raycastSurface(jmeX, jmeY, getApplication().getCamera()); if (hit == null) continue; + VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class); + if (ves != null && ves.terrainTypeAt(hit.x, hit.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) continue; if (edit.action() > 0) addBlades(hit); else removeBlades(hit); } @@ -294,14 +296,22 @@ public class GrassVertexState extends BaseAppState { if (changed) dirtyChunks[ci] = true; } + VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class); + // Neue Halme mit Falloff und Gleichmäßigkeits-Variation setzen for (int i = 0; i < density; i++) { float angle = rng.nextFloat() * (float) (Math.PI * 2); float r = rng.nextFloat() * radius; float bx = center.x + (float) Math.cos(angle) * r; float bz = center.z + (float) Math.sin(angle) * r; - float by = terrain.getHeight(new Vector2f(bx, bz)); - if (Float.isNaN(by)) continue; + float by; + if (ves != null) { + by = ves.heightAt(bx, bz); + if (ves.isTooSteep(bx, bz)) continue; + } else { + by = terrain.getHeight(new Vector2f(bx, bz)); + if (Float.isNaN(by)) continue; + } float distRatio = r / radius; float h = height * brushFalloff(distRatio) * (1f + variation * (rng.nextFloat() * 2f - 1f)); diff --git a/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.java b/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.java index c22150f..a5fcf85 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.java @@ -255,6 +255,8 @@ public class PlacedObjectState extends BaseAppState { Ray ray = new Ray(near, far.subtract(near).normalizeLocal()); Vector3f contact = raycastSurface(ray); if (contact == null) continue; + VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class); + if (ves != null && ves.terrainTypeAt(contact.x, contact.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) continue; float radius = (float) input.grassTool.brushRadius.getValue(); if (edit.action() > 0) paintGrass(contact.x, contact.z, radius); else eraseGrass(contact.x, contact.z, radius); @@ -292,6 +294,7 @@ public class PlacedObjectState extends BaseAppState { float baseH = (float) input.grassTool.grassHeight.getValue(); int slot = input.grassActiveSlot; Random rng = new Random(); + VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class); for (int i = 0; i < n; i++) { float angle = rng.nextFloat() * FastMath.TWO_PI; float dist = FastMath.sqrt(rng.nextFloat()) * radius; @@ -299,6 +302,7 @@ public class PlacedObjectState extends BaseAppState { float bz = cz + dist * FastMath.sin(angle); if (bx < -TERRAIN_HALF || bx > TERRAIN_HALF || bz < -TERRAIN_HALF || bz > TERRAIN_HALF) continue; + if (ves != null && ves.isTooSteep(bx, bz)) continue; float h = baseH * (0.7f + rng.nextFloat() * 0.6f); int ci = chunkIndex(bx, bz); if (ci >= 0) { @@ -356,6 +360,8 @@ public class PlacedObjectState extends BaseAppState { } if (chunkTufts[idx].isEmpty()) return; + VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class); + Map> bySlot = new LinkedHashMap<>(); for (GrassTuft t : chunkTufts[idx]) { long seed = (long) Float.floatToRawIntBits(t.x()) * 0x9E3779B9L @@ -367,8 +373,13 @@ public class PlacedObjectState extends BaseAppState { float oz = (rng.nextFloat() - 0.5f) * TUFT_SPREAD * 2f; float bx = t.x() + ox; float bz = t.z() + oz; - float th = terrain.getHeight(new Vector2f(bx, bz)); - if (Float.isNaN(th)) continue; + float th; + if (ves != null) { + th = ves.heightAt(bx, bz); + } else { + th = terrain.getHeight(new Vector2f(bx, bz)); + if (Float.isNaN(th)) continue; + } float bh = t.height() * (0.7f + rng.nextFloat() * 0.6f); blades.add(new float[]{bx, th, bz, bh}); } 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 26874c5..f5d739b 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 @@ -1392,11 +1392,19 @@ public class TerrainEditorState extends BaseAppState { contactPoint = raycastSurface(ray); if (contactPoint != null) { brushRadius = (float) input.grassTool.brushRadius.getValue(); + VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class); + if (ves != null && ves.terrainTypeAt(contactPoint.x, contactPoint.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) { + contactPoint = null; + } } } else if (layer == SharedInput.LAYER_GRASS_VERTEX) { contactPoint = raycastSurface(ray); if (contactPoint != null) { brushRadius = (float) input.grassVertexTool.brushRadius.getValue(); + VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class); + if (ves != null && ves.terrainTypeAt(contactPoint.x, contactPoint.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) { + contactPoint = null; + } } } diff --git a/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java index 2f87744..05ebf19 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java @@ -1154,6 +1154,87 @@ public class VoxelEditorState extends BaseAppState { return 0f; } + // ── Zentrale Terrain-Abfragen ───────────────────────────────────────────── + + /** Terrain-Typ an einer Weltposition. */ + public enum TerrainType { BASE, VOXEL_BAKED, VOXEL_UNBAKED } + + /** + * Liefert den Terrain-Typ an einer Weltposition. + * BASE = nur Basis-Terrain; VOXEL_BAKED = mind. ein gebackener Chunk in der Spalte; + * VOXEL_UNBAKED = Voxel-Daten vorhanden, aber kein Bake-Ergebnis auf der Festplatte. + */ + public TerrainType terrainTypeAt(float worldX, float worldZ) { + int cx = VoxelChunk.worldXToCx(worldX); + int cz = VoxelChunk.worldZToCz(worldZ); + boolean hasVoxel = false; + boolean hasBaked = false; + for (int cy = -2; cy <= 10; cy++) { + VoxelChunk chunk = chunks.get(chunkKey(cx, cy, cz)); + if (chunk != null && !chunk.isEmpty()) { + hasVoxel = true; + if (VoxelChunkIO.bakedExists(cx, cy, cz)) { + hasBaked = true; + } + } + } + if (!hasVoxel) return TerrainType.BASE; + return hasBaked ? TerrainType.VOXEL_BAKED : TerrainType.VOXEL_UNBAKED; + } + + /** + * Nächster Oberflächentreffer für den gegebenen Ray (Terrain + Voxel-Geo + Baked Sculpt). + * Zentrale Alternative zu den lokalen raycastSurface()-Methoden in den einzelnen States. + */ + public Vector3f clickAt(Ray ray) { + Vector3f best = null; + float bestDistSq = Float.MAX_VALUE; + + if (terrainNode != null) { + CollisionResults hits = new CollisionResults(); + terrainNode.collideWith(ray, hits); + if (hits.size() > 0) { + best = hits.getClosestCollision().getContactPoint(); + bestDistSq = ray.getOrigin().distanceSquared(best); + } + } + + Vector3f vp = raycastVoxelGeometry(ray); + if (vp != null) { + float d = ray.getOrigin().distanceSquared(vp); + if (d < bestDistSq) { best = vp; bestDistSq = d; } + } + + SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class); + if (smes != null) { + Vector3f sp = smes.raycastGeometry(ray); + if (sp != null) { + float d = ray.getOrigin().distanceSquared(sp); + if (d < bestDistSq) { best = sp; } + } + } + + return best; + } + + /** true wenn das Gefälle an (worldX, worldZ) 45° überschreitet. */ + public boolean isTooSteep(float worldX, float worldZ) { + // Gebackenes Mesh: Flächennormale direkt aus dem Mesh — genauer als Höhen-Sampling. + SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class); + if (smes != null) { + Ray r = new Ray(new Vector3f(worldX, 500f, worldZ), new Vector3f(0f, -1f, 0f)); + Vector3f[] hit = smes.raycastGeometryWithNormal(r); + if (hit != null) { + return hit[1].y < 0.7071f; // cos(45°) + } + } + // Fallback für Basis-Terrain und rohes Voxel: Höhen-Sampling mit 1 WE Abstand. + float h0 = heightAt(worldX, worldZ); + float dhx = heightAt(worldX + 1f, worldZ) - h0; + float dhz = heightAt(worldX, worldZ + 1f) - h0; + return dhx * dhx + dhz * dhz > 1.0f; // tan²(45°) = 1 + } + /** Höchste bekannte Oberfläche an (worldX, worldZ): max(Terrain, Voxel, Baked). */ public float heightAt(float worldX, float worldZ) { float h = terrainH(worldX, worldZ); diff --git a/blight-map/src/main/map/blight_grass_vertex.blgv b/blight-map/src/main/map/blight_grass_vertex.blgv index 320c872..7f99945 100644 Binary files a/blight-map/src/main/map/blight_grass_vertex.blgv and b/blight-map/src/main/map/blight_grass_vertex.blgv differ diff --git a/blight-map/src/main/map/blight_map.blm b/blight-map/src/main/map/blight_map.blm index 09ca7bf..76466b9 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/chunks/voxel_16_0_07_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_16_0_07_baked_lod0.j3o index d3ac789..2dd0594 100644 Binary files a/blight-map/src/main/map/chunks/voxel_16_0_07_baked_lod0.j3o and b/blight-map/src/main/map/chunks/voxel_16_0_07_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_07_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_16_0_07_baked_lod1.j3o index 21923ea..abd53cd 100644 Binary files a/blight-map/src/main/map/chunks/voxel_16_0_07_baked_lod1.j3o and b/blight-map/src/main/map/chunks/voxel_16_0_07_baked_lod1.j3o differ