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 7a60ec5..b87334a 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -7655,13 +7655,15 @@ public class EditorApp extends Application { // auch wenn Overlay-Nodes (Tab-Button, Asset-Panel) das Event abfangen würden. pane.addEventFilter(javafx.scene.input.MouseEvent.MOUSE_PRESSED, e -> { if (e.getButton() == MouseButton.MIDDLE - || (e.isPrimaryButtonDown() && e.isSecondaryButtonDown())) { + || (e.isPrimaryButtonDown() && e.isSecondaryButtonDown()) + || (e.getButton() == MouseButton.PRIMARY && e.isAltDown())) { prevDragX = e.getX(); prevDragY = e.getY(); } }); pane.addEventFilter(javafx.scene.input.MouseEvent.MOUSE_DRAGGED, e -> { boolean bothDown = e.isPrimaryButtonDown() && e.isSecondaryButtonDown(); - if (e.isMiddleButtonDown() || bothDown) { + boolean altLeft = e.isPrimaryButtonDown() && e.isAltDown(); + if (e.isMiddleButtonDown() || bothDown || altLeft) { double dx = e.getX() - prevDragX; double dy = e.getY() - prevDragY; input.addMouseDelta((int) dx, (int) dy); @@ -7730,7 +7732,7 @@ public class EditorApp extends Application { } } else if (bothDown) { stopEditTimer(); - } else if (e.getButton() == MouseButton.PRIMARY) { + } else if (e.getButton() == MouseButton.PRIMARY && !e.isAltDown()) { if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL) { // Einzel-Klick ohne Edit-Timer (verhindert Dauer-Spam) input.playToolClickQueue.offer( @@ -7750,7 +7752,7 @@ public class EditorApp extends Application { viewport.setOnMouseDragged(e -> { if (isObjectMode()) { if (objDragging && e.isPrimaryButtonDown() - && !e.isSecondaryButtonDown() && !e.isMiddleButtonDown()) { + && !e.isSecondaryButtonDown() && !e.isMiddleButtonDown() && !e.isAltDown()) { float dx = (float)(e.getX() - objDragPrevX); float dy = (float)(e.getY() - objDragPrevY); input.objectDragQueue.offer(new SharedInput.ObjectDrag(dx, dy)); 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 9e79f05..f4fa4f3 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 @@ -596,9 +596,8 @@ public class VoxelEditorState extends BaseAppState { Vector3f bestPos = null; Vector3f bestNorm = new Vector3f(0, 1, 0); - // Im Voxel-Layer nur Voxel-Geometrie und die Basis-Referenzebene treffen, - // nicht das Heightmap-Terrain (das würde Voxel auf der falschen Höhe erzeugen). - if (terrainNode != null && input.activeLayer != SharedInput.LAYER_VOXEL) { + // Terrain immer treffen (auch im Voxel-Layer); Voxel/Baked-Meshes gewinnen per bestDist. + if (terrainNode != null) { terrainNode.collideWith(ray, results); if (results.size() > 0) { CollisionResult cr = results.getClosestCollision(); @@ -695,8 +694,6 @@ public class VoxelEditorState extends BaseAppState { input.markedDeleteBakedKeys.clear(); input.markedBakedSelectionDirty = true; } - input.voxelTool.mode.setSelectedIndex(de.blight.editor.tool.VoxelTool.MODE_SINUS); - input.voxelTool.modeChanged = true; return; } @@ -714,18 +711,14 @@ public class VoxelEditorState extends BaseAppState { if (nhLen < 0.1f) return; } - // Plateau-Rechtsklick (nur vertikal): Voxel- und Terrain-Höhe sampeln + // Plateau-Rechtsklick (nur vertikal): Raycast-Treffer als Ziel-Höhe verwenden. + // wy kommt bereits vom zentralen raycastHit(), der Voxel, Terrain und + // gebakte Meshes nach Distanz priorisiert – immer der korrekte Oberflächenpunkt. if (!isHorizontal && isColumn && modeIdx == de.blight.editor.tool.VoxelTool.MODE_PLATEAU && lower) { - float h = columnTopWorldY(wx, wz); - TerrainEditorState tes = getStateManager().getState(TerrainEditorState.class); - if (tes != null) { - float th = tes.sampleTerrainHeight(new com.jme3.math.Vector3f(wx, wy, wz)); - if (Float.isFinite(th)) h = Float.isFinite(h) ? Math.max(h, th) : th; - } - if (Float.isFinite(h)) { - input.voxelTool.plateauTarget.setValue(h); + if (Float.isFinite(wy)) { + input.voxelTool.plateauTarget.setValue(wy); input.voxelTool.plateauTargetChanged = true; - input.heightTool.plateauHeight.setValue(h); + input.heightTool.plateauHeight.setValue(wy); input.heightTool.plateauHeightChanged = true; } return; @@ -752,7 +745,8 @@ public class VoxelEditorState extends BaseAppState { // Wenn der Nutzer auf die Basis-Ebene (y=-10) klickt, liegt der Terrain-Ankerpunkt // (terrainH ≥ 0) in einem anderen cy als der Hit-Punkt → Voxel würden sonst nie gesetzt. if (isColumn && !isHorizontal && !isCave) { - float anchorY = Math.max(terrainH(wx, wz), wy); + float th = terrainH(wx, wz); + float anchorY = hasTerrainMesh() ? Math.max(th, wy) : wy; int anchorCy = VoxelChunk.worldYToCy(anchorY); cyMin = Math.min(cyMin, anchorCy); cyMax = Math.max(cyMax, anchorCy); @@ -802,10 +796,10 @@ public class VoxelEditorState extends BaseAppState { applyColumnToTarget(chunk, cx, cy, cz, wx, wz, radius, strength, coord -> target); } else if (modeIdx == de.blight.editor.tool.VoxelTool.MODE_SMOOTH) { if (lower) { - // RMB: Durchschnitt-Smooth (wie vormals LMB) + // RMB: Durchschnitt-Smooth applySmoothColumn(chunk, cx, cy, cz, wx, wz, radius, strength, slopeParams); } else { - // LMB: gleichmäßiger Slope vom höchsten Außenring-Punkt zum Gegenpunkt + // LMB: Slope vom höchsten Außenring-Punkt zum Gegenpunkt applySlopeColumn(chunk, cx, cy, cz, wx, wz, radius, strength, slopeParams); } } else { @@ -915,8 +909,9 @@ public class VoxelEditorState extends BaseAppState { int z1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lzC + radius)); float r2 = radius * radius; - // Schritt pro Event: strength bestimmt die Wachstumsgeschwindigkeit - float stepBase = Math.max(1f, strength / 8f); + // Mindestens 6 Voxel am Zentrum, damit die Falloff-Kurve diskret sichtbar wird. + // Analogie zum Terrain-Tool: delta*falloff direkt addieren, ohne Integer-Minimum. + float stepBase = Math.max(6f, strength / 5f); for (int lz = z0; lz <= z1; lz++) { float wz = VoxelChunk.toWorldZ(cz, lz); @@ -929,8 +924,11 @@ public class VoxelEditorState extends BaseAppState { float t = (float) Math.sqrt(d2) / radius; float falloff = computeFalloff(mode, t); - // Mindestens 1, damit Kanten-Spalten nicht komplett übersprungen werden - int colStep = Math.max(1, (int)(stepBase * falloff)); + // Spalten mit vernachlässigbarem falloff überspringen (erzeugt Profil-Form statt Plateau). + // Analog zum Terrain-Tool, das ebenfalls kein Minimum anwendet. + float colStepF = stepBase * falloff; + if (colStepF < 0.5f) continue; + int colStep = Math.round(colStepF); if (!lower) { // Höchsten Solid-Voxel in dieser Spalte suchen @@ -939,11 +937,10 @@ public class VoxelEditorState extends BaseAppState { if (chunk.getDensity(lx, ly, lz) > 0) { currentTop = ly; break; } } if (currentTop < 0) { - // Ankerpunkt: Terrain-Oberfläche ODER gebackenes Terrain (brushHitWY), - // je nachdem was höher liegt. So starten neue Voxel sichtbar über - // bestehendem gebackenem Terrain statt darunter. + // Ankerpunkt: Terrain-Oberfläche ODER Klickpunkt (brushHitWY). + // Ohne Terrain-Mesh: direkt brushHitWY nutzen (terrainH liefert nur 0 als Fallback). float th = terrainH(wx, wz); - float anchor = Math.max(th, brushHitWY); + float anchor = hasTerrainMesh() ? Math.max(th, brushHitWY) : brushHitWY; int aCy = VoxelChunk.worldYToCy(anchor); if (cy == aCy) { currentTop = Math.max(0, Math.min(VoxelChunk.SIZE - 1, @@ -1115,6 +1112,10 @@ public class VoxelEditorState extends BaseAppState { return 0f; } + private boolean hasTerrainMesh() { + return terrainEditorState != null || terrainQuad != null; + } + // ── Smooth-Modus ────────────────────────────────────────────────────────── /** @@ -1298,19 +1299,76 @@ public class VoxelEditorState extends BaseAppState { } /** - * Smooth-Pinsel (Linksklick): bewegt alle Spalten im Pinselbereich auf die - * Durchschnittshöhe aller Spalten zu (sp[6]). - * - * Spikes (Spalten über dem Durchschnitt) werden abgebaut, tiefe Stellen - * leicht angehoben. Dadurch entsteht eine gleichmäßig geebnte Fläche, - * ohne dass neues Material an Stellen aufgebaut wird, die bereits flach sind. + * Smooth-Pinsel (RMB): exakter Port von TerrainEditorState.smoothHeight(). + * blend = clamp((1-t) * strength/5, 0, 1) — linearer Falloff. + * Faktor 5 statt 50 (wie Terrain): Voxel-Höhen sind Integer, d.h. 0,2-Deltas + * würden auf 0 runden → Tool tut nichts. 10× stärker = gleiches Verhalten. + * delta = (avg - top) * blend — proportionale Bewegung zur Durchschnittshöhe. + * Für leere Spalten wird terrainH als Baseline verwendet (gleich wie computeSlopeParams). */ private void applySmoothColumn(VoxelChunk chunk, int cx, int cy, int cz, float brushWX, float brushWZ, float radius, float strength, float[] sp) { if (sp == null || Float.isNaN(sp[6])) return; - final float targetH = sp[6]; - applyColumnToTarget(chunk, cx, cy, cz, brushWX, brushWZ, radius, strength, coord -> targetH); + final float avgH = sp[6]; + + float lxC = VoxelChunk.worldXToLocal(brushWX, cx); + float lzC = VoxelChunk.worldZToLocal(brushWZ, cz); + int x0 = Math.max(0, (int)(lxC - radius)); + int x1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lxC + radius)); + int z0 = Math.max(0, (int)(lzC - radius)); + int z1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lzC + radius)); + float r2 = radius * radius; + + for (int lz = z0; lz <= z1; lz++) { + float wz = VoxelChunk.toWorldZ(cz, lz); + float dz = wz - brushWZ; + for (int lx = x0; lx <= x1; lx++) { + float wx = VoxelChunk.toWorldX(cx, lx); + float dx = wx - brushWX; + float d2 = dx*dx + dz*dz; + if (d2 > r2) continue; + + float t = (float) Math.sqrt(d2) / radius; + float blend = Math.max(0f, Math.min(1f, (1f - t) * (strength / 5f))); + if (blend < 0.001f) continue; + + int currentTopLY = -1; + for (int ly = VoxelChunk.SIZE - 1; ly >= 0; ly--) { + if (chunk.getDensity(lx, ly, lz) > 0) { currentTopLY = ly; break; } + } + + float currentTopWY; + int startLY; + if (currentTopLY >= 0) { + currentTopWY = VoxelChunk.toWorldY(cy, currentTopLY); + startLY = currentTopLY; + } else { + // Kein Voxel: Basis-Terrain als Höhenreferenz + float th = terrainH(wx, wz); + if (!Float.isFinite(th)) continue; + int thCy = VoxelChunk.worldYToCy(th); + if (cy != thCy) continue; // Terrain-Oberfläche in anderem Chunk + currentTopWY = th; + startLY = Math.max(0, Math.min(VoxelChunk.SIZE - 1, + (int)(th - cy * (float) VoxelChunk.CELLS))); + } + + int step = (int) Math.round((avgH - currentTopWY) * blend); + if (step == 0) continue; + + if (step > 0) { + int newTop = Math.min(VoxelChunk.SIZE - 1, startLY + step); + for (int ly = startLY; ly <= newTop; ly++) + chunk.setDensity(lx, ly, lz, (byte) 127); + } else { + if (currentTopLY < 0) continue; // nix zum Abbauen + int newTop = Math.max(0, currentTopLY + step); + for (int ly = newTop + 1; ly <= currentTopLY; ly++) + chunk.setDensity(lx, ly, lz, Byte.MIN_VALUE); + } + } + } } /** @@ -1350,8 +1408,9 @@ public class VoxelEditorState extends BaseAppState { } /** - * Slope-Pinsel (Smooth-Linksklick): gleichmäßige Neigung vom höchsten Außenring-Punkt - * zum geometrisch gegenüberliegenden Punkt. + * Slope-Pinsel (LMB): port von TerrainEditorState.slopeHeight(). + * delta = (target - top) * clamp(cosine_falloff * strength/50, 0, 1). + * Ziel-Höhe per linearer Interpolation entlang der Neigungsachse. */ private void applySlopeColumn(VoxelChunk chunk, int cx, int cy, int cz, float brushWX, float brushWZ, @@ -1362,11 +1421,67 @@ public class VoxelEditorState extends BaseAppState { final float projRange = projHigh - projOpp; if (projRange < 0.5f) return; - applyColumnToTarget(chunk, cx, cy, cz, brushWX, brushWZ, radius, strength, coord -> { - float proj = coord[0] * dirX + coord[1] * dirZ; - float t = Math.max(0f, Math.min(1f, (proj - projOpp) / projRange)); - return oppH + t * (highH - oppH); - }); + float lxC = VoxelChunk.worldXToLocal(brushWX, cx); + float lzC = VoxelChunk.worldZToLocal(brushWZ, cz); + int x0 = Math.max(0, (int)(lxC - radius)); + int x1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lxC + radius)); + int z0 = Math.max(0, (int)(lzC - radius)); + int z1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lzC + radius)); + float r2 = radius * radius; + + for (int lz = z0; lz <= z1; lz++) { + float wz = VoxelChunk.toWorldZ(cz, lz); + float dz = wz - brushWZ; + for (int lx = x0; lx <= x1; lx++) { + float wx = VoxelChunk.toWorldX(cx, lx); + float dx = wx - brushWX; + float d2 = dx*dx + dz*dz; + if (d2 > r2) continue; + + float t = (float) Math.sqrt(d2) / radius; + float falloff = (float)(0.5 * (1.0 + Math.cos(t * Math.PI))); + float blend = Math.max(0f, Math.min(1f, falloff * (strength / 5f))); + if (blend < 0.001f) continue; + + float proj = dx * dirX + dz * dirZ; + float slopeT = Math.max(0f, Math.min(1f, (proj - projOpp) / projRange)); + float targetH = oppH + slopeT * (highH - oppH); + + int currentTopLY = -1; + for (int ly = VoxelChunk.SIZE - 1; ly >= 0; ly--) { + if (chunk.getDensity(lx, ly, lz) > 0) { currentTopLY = ly; break; } + } + + float currentTopWY; + int startLY; + if (currentTopLY >= 0) { + currentTopWY = VoxelChunk.toWorldY(cy, currentTopLY); + startLY = currentTopLY; + } else { + float th = terrainH(wx, wz); + if (!Float.isFinite(th)) continue; + int thCy = VoxelChunk.worldYToCy(th); + if (cy != thCy) continue; + currentTopWY = th; + startLY = Math.max(0, Math.min(VoxelChunk.SIZE - 1, + (int)(th - cy * (float) VoxelChunk.CELLS))); + } + + int step = (int) Math.round((targetH - currentTopWY) * blend); + if (step == 0) continue; + + if (step > 0) { + int newTop = Math.min(VoxelChunk.SIZE - 1, startLY + step); + for (int ly = startLY; ly <= newTop; ly++) + chunk.setDensity(lx, ly, lz, (byte) 127); + } else { + if (currentTopLY < 0) continue; + int newTop = Math.max(0, currentTopLY + step); + for (int ly = newTop + 1; ly <= currentTopLY; ly++) + chunk.setDensity(lx, ly, lz, Byte.MIN_VALUE); + } + } + } } // ── Intern: Voxel-Bake ──────────────────────────────────────────────────── @@ -1485,54 +1600,142 @@ public class VoxelEditorState extends BaseAppState { input.blurIterDone = iter + 1; } - // ── Flachbereich-Glättung: 3 reine Gauß-Passes nur nahe der Isofläche ────── - // Der bilaterale Filter bewahrt scharfe Kanten (Klippen), glättet jedoch die - // Dichte-Sprünge an der Isofläche (127 → -128) kaum, da die Differenz (255) das - // bilaterale Gewicht auf ~0 senkt. Auf flachen Flächen entstehen dadurch Stufen. - // Diese reinen Gauß-Passes (keine bilaterale Gewichtung) glätten gezielt Voxel, - // die (a) nahe der Oberfläche liegen und (b) eine überwiegend vertikale Normale haben. + // ── Neigungsadaptive Oberflächenverarbeitung (Regressions-Ansatz) ──────── + // + // Warum XZ-Blur nicht funktioniert: + // XZ-Blur verschiebt Dichten lateral, aber die Marching-Cubes-Isofläche + // liegt immer zwischen zwei Voxeln, wo einer positiv und einer negativ ist. + // Stufenfronten bestehen aus Voxeln, die auf beiden Seiten der Stufe je + // einheitlich positiv bzw. negativ sind → die Isofläche bleibt an exakt + // denselben ganzzahligen Positionen, egal wie oft man XZ-blurt. + // + // Richtige Lösung: density = (smoothH − worldY) × 127 einschreiben. + // Damit kann MC die Isofläche auf sub-voxel-genaue Positionen legen. + // + // Klassifikation via linearer Regression (2D-Ebene durch ±W-Fenster): + // • totalSlope < MIN_SLOPE (≈ 3°): Flachfläche – keine Korrektur nötig. + // • MIN_SLOPE ≤ totalSlope < TAN_22,5°: Sanfte Steigung → Dichte schreiben. + // • totalSlope > TAN_45°: Steile Klippe → Noise. + // • |smoothH − h0| > 0,6: Hügelkuppe / Senke – überspringen + // (verhindert, dass lokale Extrema durch weit entfernte Werte abgezogen + // werden; Regressions-Abweichung an Stufenfronten liegt bei ≤ 0,46). { - final float SURF_BAND = 80f; // |dichte| < SURF_BAND → nahe der Isofläche - final float COS_30 = 0.866f; // cos(30°) – Grenze "flache Fläche" - for (int flatPass = 0; flatPass < 3; flatPass++) { + final float TAN_22_5 = 0.414f; // tan(22,5°) + final float TAN_45 = 1.000f; // tan(45°) + final float MIN_SLOPE = 0.05f; // < ~3° → komplett flach, nicht anfassen + java.util.Random rng = new java.util.Random(42); + final int C = VoxelChunk.CELLS; + final int W = 5; // Regressions-Halbfenster (±5 Spalten) + + // ── Schritt 1: Höhenfeld ────────────────────────────────────────── + // Key-Schema: (cx*C + bx + 30000) * 60001 + (cz*C + bz + 30000) + // Überlapp-Voxel (bx = CELLS) werden ausgelassen. + Map hfMap = new HashMap<>(); + for (VoxelChunk c : nonEmpty) { + long k = chunkKey(c.cx, c.cy, c.cz); + float[] buf = curBufs.get(k); + for (int bz = 0; bz < C; bz++) { + for (int bx = 0; bx < C; bx++) { + for (int by = blurN - 1; by >= 0; by--) { + if (buf[c.idx(bx, by, bz)] > 0) { + int wiy = c.cy * C + by; + long hk = (long)(c.cx * C + bx + 30000) * 60001L + + (c.cz * C + bz + 30000); + hfMap.merge(hk, wiy, Math::max); + break; + } + } + } + } + } + + // ── Schritt 2: Lineare 2D-Regression je Spalte ─────────────────── + // Fit: H(dx,dz) = a + b·dx + c·dz (zentriert auf Spaltenmitte). + // a = meanH → sub-voxel-genaue Ziel-Höhe (smoothH) + // b = slopeX, c = slopeZ + // totalSlope = sqrt(b²+c²) → Gesamt-Neigung des Ebenen-Fits + // + // Warum Regression besser als Max-Slope: + // Max-Slope wird von den UNMITTELBAREN Nachbarn dominiert (die bei + // einer Stufe ±1 Voxel haben → slope = 1,0 > TAN_22,5°). + // Die Regression sieht das gesamte ±5-Fenster und klassifiziert + // die Stufe korrekt als sanfte Steigung (slope ≈ 0,14 für 3er-Stufen). + // + // regMap[0] = smoothH, regMap[1] = totalSlope + Map regMap = new HashMap<>(hfMap.size()); + for (Map.Entry entry : hfMap.entrySet()) { + long hk0 = entry.getKey(); + int h0 = entry.getValue(); + int colZ = (int)(hk0 % 60001L) - 30000; + int colX = (int)(hk0 / 60001L) - 30000; + + float sumH = 0f, sumDxH = 0f, sumDzH = 0f; + float sumDx2 = 0f, sumDz2 = 0f; + int cnt = 0; + for (int dz = -W; dz <= W; dz++) { + for (int dx = -W; dx <= W; dx++) { + long hkN = (long)(colX + dx + 30000) * 60001L + (colZ + dz + 30000); + Integer hN = hfMap.get(hkN); + if (hN == null) continue; + sumH += hN; + sumDxH += (float)dx * hN; + sumDzH += (float)dz * hN; + sumDx2 += dx * dx; + sumDz2 += dz * dz; + cnt++; + } + } + if (cnt < 4) { + regMap.put(hk0, new float[]{ h0, 99f }); + continue; + } + float meanH = sumH / cnt; + float slopeX = (sumDx2 > 0f) ? sumDxH / sumDx2 : 0f; + float slopeZ = (sumDz2 > 0f) ? sumDzH / sumDz2 : 0f; + float totalSlope = (float) Math.sqrt(slopeX * slopeX + slopeZ * slopeZ); + regMap.put(hk0, new float[]{ meanH, totalSlope }); + } + + // ── Schritt 3: Dichte anpassen ──────────────────────────────────── + { Map nextBufs = new HashMap<>(); for (VoxelChunk c : nonEmpty) { long k = chunkKey(c.cx, c.cy, c.cz); float[] cur = curBufs.get(k); float[] next = Arrays.copyOf(cur, cur.length); - for (int by = 0; by < blurN; by++) { - for (int bz = 0; bz < blurN; bz++) { - for (int bx = 0; bx < blurN; bx++) { - float d = cur[c.idx(bx, by, bz)]; - if (Math.abs(d) >= SURF_BAND) continue; + int wiyBase = c.cy * C; - // Gradient über zentrale Differenzen - float gx = getBlurBuf(curBufs, allOriginal, c, bx+1, by, bz) - - getBlurBuf(curBufs, allOriginal, c, bx-1, by, bz); - float gy = getBlurBuf(curBufs, allOriginal, c, bx, by+1, bz) - - getBlurBuf(curBufs, allOriginal, c, bx, by-1, bz); - float gz = getBlurBuf(curBufs, allOriginal, c, bx, by, bz+1) - - getBlurBuf(curBufs, allOriginal, c, bx, by, bz-1); - float gLen = (float) Math.sqrt(gx*gx + gy*gy + gz*gz); - if (gLen < 1f) continue; - float normY = Math.abs(gy) / gLen; - if (normY < COS_30) continue; // steile Fläche → nicht glätten + for (int bz = 0; bz < C; bz++) { + int colZ = c.cz * C + bz; + for (int bx = 0; bx < C; bx++) { + int colX = c.cx * C + bx; + long hk0 = (long)(colX + 30000) * 60001L + (colZ + 30000); + Integer h0 = hfMap.get(hk0); + float[] reg = regMap.get(hk0); + if (h0 == null || reg == null) continue; - float vSum = 0f; - int cnt = 0; - for (int dy = -1; dy <= 1; dy++) - for (int dz = -1; dz <= 1; dz++) - for (int dx = -1; dx <= 1; dx++) { - int sx = bx+dx, sy = by+dy, sz = bz+dz; - float nb; - if (sx >= 0 && sx < blurN && sy >= 0 && sy < blurN && sz >= 0 && sz < blurN) - nb = cur[c.idx(sx, sy, sz)]; - else - nb = getBlurBuf(curBufs, allOriginal, c, sx, sy, sz); - vSum += nb; - cnt++; - } - next[c.idx(bx, by, bz)] = vSum / cnt; + float smoothH = reg[0]; + float totalSlope = reg[1]; + float diff = smoothH - h0; + + if (totalSlope >= MIN_SLOPE && totalSlope < TAN_22_5 + && Math.abs(diff) <= 0.6f) { + // Sanfte Steigung: sub-voxel-genauen Dichte-Gradienten schreiben. + // MC interpoliert die Isofläche dann auf die genaue smoothH-Höhe. + for (int by = 0; by < blurN; by++) { + float d = (smoothH - (wiyBase + by)) * 127f; + next[c.idx(bx, by, bz)] = Math.max(-128f, Math.min(127f, d)); + } + } else if (totalSlope > TAN_45) { + // Steile Klippe: Noise für organische Optik + int surfBy = h0 - wiyBase; + if (surfBy < 0 || surfBy >= blurN) continue; + for (int by = Math.max(0, surfBy - 2); + by <= Math.min(blurN - 1, surfBy + 2); by++) { + float noise = (float)(rng.nextGaussian() * 20.0); + float val = cur[c.idx(bx, by, bz)] + noise; + next[c.idx(bx, by, bz)] = Math.max(-128f, Math.min(127f, val)); + } } } } @@ -1540,6 +1743,28 @@ public class VoxelEditorState extends BaseAppState { } curBufs = nextBufs; } + + // ── Schritt 4: Überlapp-Voxel synchronisieren ──────────────────── + for (VoxelChunk c : nonEmpty) { + long ck = chunkKey(c.cx, c.cy, c.cz); + float[] cBuf = curBufs.get(ck); + float[] rBuf = curBufs.get(chunkKey(c.cx + 1, c.cy, c.cz)); + if (rBuf != null) { + for (int by = 0; by < blurN; by++) { + for (int bz = 0; bz < blurN; bz++) { + cBuf[c.idx(VoxelChunk.CELLS, by, bz)] = rBuf[c.idx(0, by, bz)]; + } + } + } + float[] fBuf = curBufs.get(chunkKey(c.cx, c.cy, c.cz + 1)); + if (fBuf != null) { + for (int by = 0; by < blurN; by++) { + for (int bx = 0; bx < blurN; bx++) { + cBuf[c.idx(bx, by, VoxelChunk.CELLS)] = fBuf[c.idx(bx, by, 0)]; + } + } + } + } } // Blur-Ergebnisse in VoxelChunks umwandeln @@ -1906,8 +2131,10 @@ public class VoxelEditorState extends BaseAppState { private float computeFalloff(int mode, float t) { return switch (mode) { - case de.blight.editor.tool.VoxelTool.MODE_SINUS -> (float) Math.cos(t * Math.PI / 2); - case de.blight.editor.tool.VoxelTool.MODE_SPIKE -> (1f - t) * (1f - t); + // identisch mit dem Terrain-Tool: (1+cos(π·t))/2 → weiches Glöckchen + case de.blight.editor.tool.VoxelTool.MODE_SINUS -> (float)(0.5 * (1 + Math.cos(Math.PI * t))); + // identisch mit dem Terrain-Tool: (1-t)^4 → steiler Kegel + case de.blight.editor.tool.VoxelTool.MODE_SPIKE -> { float u = 1f - t; yield u * u * u * u; } case de.blight.editor.tool.VoxelTool.MODE_SMOOTH -> (float)(0.5 * (1 + Math.cos(t * Math.PI))); default -> 1f; // PLATEAU }; diff --git a/blight-map/src/main/map/blight_grass_vertex.blgv b/blight-map/src/main/map/blight_grass_vertex.blgv index 7b76825..320c872 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 5f6db40..dfe2096 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_objects.blo b/blight-map/src/main/map/blight_objects.blo index b9789b5..ea898e7 100644 --- a/blight-map/src/main/map/blight_objects.blo +++ b/blight-map/src/main/map/blight_objects.blo @@ -4,4 +4,4 @@ Models/trees/pine/medium/pine_medium_20260706_190947.j3o -6.42357 1.22971 -1318. Models/trees/pine/medium/pine_medium_20260706_190953.j3o -17.84356 1.24567 -1316.45410 0.26793 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 Models/plants/misc/kaktusfeige.j3o -6.18754 1.22995 -1320.34875 0.00000 2.50000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 Models/imported/alter_steg.j3o -8.42317 -0.11015 -1359.15662 3.14159 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 -Models/trees/grapevine/grapevine_20260725_195635.j3o -85.78869 1.32939 -1297.17883 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 +Models/trees/grapevine/grapevine_20260725_195635.j3o -85.78869 1.32641 -1297.17883 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 diff --git a/blight-map/src/main/map/chunks/chunk_14_06.blc b/blight-map/src/main/map/chunks/chunk_14_06.blc index f33b6c2..42e7cfb 100644 Binary files a/blight-map/src/main/map/chunks/chunk_14_06.blc and b/blight-map/src/main/map/chunks/chunk_14_06.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_14_07.blc b/blight-map/src/main/map/chunks/chunk_14_07.blc index 5a96cb2..768b2d5 100644 Binary files a/blight-map/src/main/map/chunks/chunk_14_07.blc and b/blight-map/src/main/map/chunks/chunk_14_07.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_14_08.blc b/blight-map/src/main/map/chunks/chunk_14_08.blc index 70efc43..c6d4153 100644 Binary files a/blight-map/src/main/map/chunks/chunk_14_08.blc and b/blight-map/src/main/map/chunks/chunk_14_08.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_14_09.blc b/blight-map/src/main/map/chunks/chunk_14_09.blc index 599d104..0717feb 100644 Binary files a/blight-map/src/main/map/chunks/chunk_14_09.blc and b/blight-map/src/main/map/chunks/chunk_14_09.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_15_05.blc b/blight-map/src/main/map/chunks/chunk_15_05.blc index 3fb3c21..687145a 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_05.blc and b/blight-map/src/main/map/chunks/chunk_15_05.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_15_06.blc b/blight-map/src/main/map/chunks/chunk_15_06.blc index e7c30c9..9afc004 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_06.blc and b/blight-map/src/main/map/chunks/chunk_15_06.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_15_07.blc b/blight-map/src/main/map/chunks/chunk_15_07.blc index d48e407..2e53ad6 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_07.blc and b/blight-map/src/main/map/chunks/chunk_15_07.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_15_08.blc b/blight-map/src/main/map/chunks/chunk_15_08.blc index d9a3bd5..dd3f911 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_08.blc and b/blight-map/src/main/map/chunks/chunk_15_08.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_15_09.blc b/blight-map/src/main/map/chunks/chunk_15_09.blc index 4dddeb0..3d78799 100644 Binary files a/blight-map/src/main/map/chunks/chunk_15_09.blc and b/blight-map/src/main/map/chunks/chunk_15_09.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_16_05.blc b/blight-map/src/main/map/chunks/chunk_16_05.blc index 068a0aa..25e2669 100644 Binary files a/blight-map/src/main/map/chunks/chunk_16_05.blc and b/blight-map/src/main/map/chunks/chunk_16_05.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_16_06.blc b/blight-map/src/main/map/chunks/chunk_16_06.blc index edc0d11..2c6a397 100644 Binary files a/blight-map/src/main/map/chunks/chunk_16_06.blc and b/blight-map/src/main/map/chunks/chunk_16_06.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_16_07.blc b/blight-map/src/main/map/chunks/chunk_16_07.blc index bfa0bb0..414c236 100644 Binary files a/blight-map/src/main/map/chunks/chunk_16_07.blc and b/blight-map/src/main/map/chunks/chunk_16_07.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_16_08.blc b/blight-map/src/main/map/chunks/chunk_16_08.blc index ccef952..de34c75 100644 Binary files a/blight-map/src/main/map/chunks/chunk_16_08.blc and b/blight-map/src/main/map/chunks/chunk_16_08.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_17_05.blc b/blight-map/src/main/map/chunks/chunk_17_05.blc index 84e5b55..c1c6c45 100644 Binary files a/blight-map/src/main/map/chunks/chunk_17_05.blc and b/blight-map/src/main/map/chunks/chunk_17_05.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_17_06.blc b/blight-map/src/main/map/chunks/chunk_17_06.blc index 668b881..9c996bc 100644 Binary files a/blight-map/src/main/map/chunks/chunk_17_06.blc and b/blight-map/src/main/map/chunks/chunk_17_06.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_17_07.blc b/blight-map/src/main/map/chunks/chunk_17_07.blc index 400f066..92813e4 100644 Binary files a/blight-map/src/main/map/chunks/chunk_17_07.blc and b/blight-map/src/main/map/chunks/chunk_17_07.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_17_08.blc b/blight-map/src/main/map/chunks/chunk_17_08.blc index ed0a954..06ad23d 100644 Binary files a/blight-map/src/main/map/chunks/chunk_17_08.blc and b/blight-map/src/main/map/chunks/chunk_17_08.blc differ diff --git a/blight-map/src/main/map/chunks/voxel_13_0_06.blvc.prebake b/blight-map/src/main/map/chunks/voxel_13_0_06.blvc.prebake new file mode 100644 index 0000000..af9118b Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_13_0_06.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_07.blvc.prebake b/blight-map/src/main/map/chunks/voxel_13_0_07.blvc.prebake similarity index 51% rename from blight-map/src/main/map/chunks/voxel_14_0_07.blvc.prebake rename to blight-map/src/main/map/chunks/voxel_13_0_07.blvc.prebake index fa3cdd3..b39702f 100644 Binary files a/blight-map/src/main/map/chunks/voxel_14_0_07.blvc.prebake and b/blight-map/src/main/map/chunks/voxel_13_0_07.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_13_0_06.blvc b/blight-map/src/main/map/chunks/voxel_13_0_08.blvc similarity index 96% rename from blight-map/src/main/map/chunks/voxel_13_0_06.blvc rename to blight-map/src/main/map/chunks/voxel_13_0_08.blvc index 69a6119..dfcb6a1 100644 Binary files a/blight-map/src/main/map/chunks/voxel_13_0_06.blvc and b/blight-map/src/main/map/chunks/voxel_13_0_08.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_13_0_09.blvc b/blight-map/src/main/map/chunks/voxel_13_0_09.blvc new file mode 100644 index 0000000..e7cd959 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_13_0_09.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_05.blvc b/blight-map/src/main/map/chunks/voxel_14_0_05.blvc new file mode 100644 index 0000000..989b304 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_14_0_05.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_06.blvc b/blight-map/src/main/map/chunks/voxel_14_0_06.blvc new file mode 100644 index 0000000..23594af Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_14_0_06.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_06.blvc.prebake b/blight-map/src/main/map/chunks/voxel_14_0_06.blvc.prebake deleted file mode 100644 index 0a0848f..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_14_0_06.blvc.prebake and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_06_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_14_0_06_baked_lod0.j3o deleted file mode 100644 index ee44555..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_14_0_06_baked_lod0.j3o and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_06_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_14_0_06_baked_lod1.j3o deleted file mode 100644 index 35e084c..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_14_0_06_baked_lod1.j3o and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_07.blvc b/blight-map/src/main/map/chunks/voxel_14_0_07.blvc new file mode 100644 index 0000000..e1bd7e0 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_14_0_07.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_07_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_14_0_07_baked_lod0.j3o deleted file mode 100644 index 248ee25..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_14_0_07_baked_lod0.j3o and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_07_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_14_0_07_baked_lod1.j3o deleted file mode 100644 index 027763b..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_14_0_07_baked_lod1.j3o and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_05.blvc b/blight-map/src/main/map/chunks/voxel_15_0_05.blvc new file mode 100644 index 0000000..0589843 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_0_05.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_06.blvc b/blight-map/src/main/map/chunks/voxel_15_0_06.blvc index d6315c5..193597c 100644 Binary files a/blight-map/src/main/map/chunks/voxel_15_0_06.blvc and b/blight-map/src/main/map/chunks/voxel_15_0_06.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_06.blvc.prebake b/blight-map/src/main/map/chunks/voxel_15_0_06.blvc.prebake deleted file mode 100644 index 424cf01..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_15_0_06.blvc.prebake and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_06_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_15_0_06_baked_lod0.j3o deleted file mode 100644 index a9ffc54..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_15_0_06_baked_lod0.j3o and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_06_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_15_0_06_baked_lod1.j3o deleted file mode 100644 index 006c906..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_15_0_06_baked_lod1.j3o and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_07.blvc b/blight-map/src/main/map/chunks/voxel_15_0_07.blvc new file mode 100644 index 0000000..3655dda Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_0_07.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_07.blvc.prebake b/blight-map/src/main/map/chunks/voxel_15_0_07.blvc.prebake deleted file mode 100644 index b0d474a..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_15_0_07.blvc.prebake and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_07_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_15_0_07_baked_lod0.j3o deleted file mode 100644 index 1128fed..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_15_0_07_baked_lod0.j3o and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_07_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_15_0_07_baked_lod1.j3o deleted file mode 100644 index c2d18fa..0000000 Binary files a/blight-map/src/main/map/chunks/voxel_15_0_07_baked_lod1.j3o and /dev/null differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_08.blvc b/blight-map/src/main/map/chunks/voxel_15_0_08.blvc new file mode 100644 index 0000000..7f9aaa9 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_0_08.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_06.blvc.prebake b/blight-map/src/main/map/chunks/voxel_16_0_06.blvc.prebake new file mode 100644 index 0000000..27a9040 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_06.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_07.blvc.prebake b/blight-map/src/main/map/chunks/voxel_16_0_07.blvc.prebake new file mode 100644 index 0000000..ed99e4d Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_07.blvc.prebake differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_08.blvc b/blight-map/src/main/map/chunks/voxel_16_0_08.blvc new file mode 100644 index 0000000..3df8a02 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_16_0_08.blvc differ