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 2b9424f..fb0257d 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -3582,6 +3582,27 @@ public class EditorApp extends Application { bakeBarLabel.setVisible(false); bakeBarLabel.setManaged(false); + // ── Bake-Parameter ──────────────────────────────────────────────── + Label smoothLbl = new Label(String.format("Glättung: %.1f", input.bakeSmoothStrength)); + smoothLbl.setStyle("-fx-font-size: 11;"); + javafx.scene.control.Slider smoothSlider = + new javafx.scene.control.Slider(0, 1, input.bakeSmoothStrength); + smoothSlider.setShowTickMarks(false); + smoothSlider.valueProperty().addListener((obs, o, n) -> { + input.bakeSmoothStrength = n.floatValue(); + smoothLbl.setText(String.format("Glättung: %.1f", n.floatValue())); + }); + + Label cliffLbl = new Label(String.format("Klippen-Noise: %.1f", input.bakeCliffNoiseStrength)); + cliffLbl.setStyle("-fx-font-size: 11;"); + javafx.scene.control.Slider cliffSlider = + new javafx.scene.control.Slider(0, 1, input.bakeCliffNoiseStrength); + cliffSlider.setShowTickMarks(false); + cliffSlider.valueProperty().addListener((obs, o, n) -> { + input.bakeCliffNoiseStrength = n.floatValue(); + cliffLbl.setText(String.format("Klippen-Noise: %.1f", n.floatValue())); + }); + Button bakeBtn = new Button("Voxels backen (J3O)"); bakeBtn.setMaxWidth(Double.MAX_VALUE); bakeBtn.setStyle("-fx-background-color: #4a7eba; -fx-text-fill: white;"); @@ -3697,8 +3718,9 @@ public class EditorApp extends Application { cleanupPoller.setCycleCount(javafx.animation.Animation.INDEFINITE); cleanupPoller.play(); - panel.getChildren().addAll(new Separator(), bakeBtn, - bakeBar, bakeBarLabel, bakeStatus, + panel.getChildren().addAll(new Separator(), + smoothLbl, smoothSlider, cliffLbl, cliffSlider, + bakeBtn, bakeBar, bakeBarLabel, bakeStatus, new Separator(), cleanupBtn, cleanupStatus, new Separator(), selInfoLabel, selHintLabel, deleteBtn, revertBtn); } diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index c687031..a79b515 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -778,7 +778,11 @@ public class SharedInput { public volatile boolean voxelRedoRequested = false; /** JFX → JME: alle Voxel-Chunks als geglättete J3O-Meshes backen. */ - public volatile boolean bakeVoxelsRequested = false; + public volatile boolean bakeVoxelsRequested = false; + /** JFX → JME: Stärke des Post-Bake-Laplacian-Smooth (0 = kein Extra-Pass, 1 = stark). */ + public volatile float bakeSmoothStrength = 0.3f; + /** JFX → JME: Amplitude der horizontalen Klippen-Noise-Verschiebung (0 = kein, 1 = ~2 m). */ + public volatile float bakeCliffNoiseStrength = 0.2f; /** JME → JFX: Anzahl bereits gebackener Chunks (0 = nicht gestartet). */ public volatile int bakeDone = 0; /** JME → JFX: Gesamtzahl der zu backenden Chunks (0 = nicht gestartet). */ 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 4c4de6b..2f87744 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 @@ -2522,8 +2522,10 @@ public class VoxelEditorState extends BaseAppState { // Nur erfolgreich gebackene Chunks werden gelöscht List successfullyBaked = new java.util.ArrayList<>(); int baked = 0; + float bakeSmoothStr = input.bakeSmoothStrength; + float bakeCliffStr = input.bakeCliffNoiseStrength; for (VoxelChunk chunk : nonEmpty) { - if (bakeChunk(chunk, blurredMap)) { + if (bakeChunk(chunk, blurredMap, bakeSmoothStr, bakeCliffStr)) { successfullyBaked.add(chunk); } else { log.warn("Chunk ({},{},{}) nicht gebacken – Voxel-Daten bleiben erhalten.", @@ -2639,7 +2641,8 @@ public class VoxelEditorState extends BaseAppState { } /** Bäckt einen einzelnen Chunk. Gibt true zurück wenn erfolgreich, false bei Fehler. */ - private boolean bakeChunk(VoxelChunk original, Map blurredMap) { + private boolean bakeChunk(VoxelChunk original, Map blurredMap, + float smoothStrength, float cliffNoiseStrength) { try { VoxelChunk blurred = blurredMap.get(chunkKey(original.cx, original.cy, original.cz)); if (blurred == null) return false; @@ -2647,9 +2650,25 @@ public class VoxelEditorState extends BaseAppState { // Geblurrte Nachbarn für nahtlose Chunk-Grenzen im MC VoxelChunk[] nb = getNeighbors(original.cx, original.cy, original.cz, blurredMap); + float wx = (float)(original.cx * VoxelChunk.CELLS); + float wz = (float)(original.cz * VoxelChunk.CELLS); + + Mesh lod0 = MarchingCubes.smooth(MarchingCubes.build(blurred, 1, nb), 1, 0.3f); + if (smoothStrength > 0.01f) { + lod0 = MarchingCubes.smooth(lod0, 1, smoothStrength); + } + if (cliffNoiseStrength > 0.01f) { + lod0 = MarchingCubes.perturb(lod0, cliffNoiseStrength, wx, wz); + } + + Mesh lod1 = MarchingCubes.smooth(MarchingCubes.build(blurred, 4, nb), 3, 0.4f); + if (cliffNoiseStrength > 0.01f) { + lod1 = MarchingCubes.perturb(lod1, cliffNoiseStrength * 0.6f, wx, wz); + } + Mesh[] meshes = { - MarchingCubes.smooth(MarchingCubes.build(blurred, 1, nb), 1, 0.3f), - MarchingCubes.smooth(MarchingCubes.build(blurred, 4, nb), 3, 0.4f), + lod0, + lod1, MarchingCubes.smooth(MarchingCubes.build(blurred, 16, nb), 2, 0.4f), }; diff --git a/blight-game/src/main/java/de/blight/game/state/MarchingCubes.java b/blight-game/src/main/java/de/blight/game/state/MarchingCubes.java index 5ee1c52..d9fb3c8 100644 --- a/blight-game/src/main/java/de/blight/game/state/MarchingCubes.java +++ b/blight-game/src/main/java/de/blight/game/state/MarchingCubes.java @@ -682,4 +682,172 @@ public final class MarchingCubes { return chunk.getDensity(x, y, z); } + /** + * Verschiebt Vertices auf steilen Flächen (normal.y < 0.707 ≈ 45°) horizontal (XZ) + * per kohärentem Value-Noise. Macht Klippen natürlicher und felsiger. + * Rand-Vertices (Chunk-Grenzen) bleiben fixiert, Nahtlosigkeit bleibt erhalten. + * + * @param strength Amplitude: 0 = kein Effekt, 1 = bis ~2 m maximale Verschiebung + * @param worldX Weltkoordinate X des Chunk-Ursprungs (cx * CELLS) + * @param worldZ Weltkoordinate Z des Chunk-Ursprungs (cz * CELLS) + */ + public static Mesh perturb(Mesh mesh, float strength, float worldX, float worldZ) { + if (mesh == null || strength < 0.001f) return mesh; + FloatBuffer posF = mesh.getFloatBuffer(VertexBuffer.Type.Position); + if (posF == null) return mesh; + int vertCount = posF.capacity() / 3; + if (vertCount < 3) return mesh; + int triCount = vertCount / 3; + + float[] pos = new float[vertCount * 3]; + posF.rewind(); posF.get(pos); + + // Vertex-Gruppen aufbauen (gleiche Position → gleiche Gruppe) + HashMap keyToGroup = new HashMap<>(vertCount / 3 + 16); + int[] vertGroup = new int[vertCount]; + int[] groupFirst = new int[vertCount]; + int groupCount = 0; + for (int v = 0; v < vertCount; v++) { + long key = posKey(pos, v); + Integer g = keyToGroup.get(key); + if (g == null) { + keyToGroup.put(key, groupCount); + groupFirst[groupCount] = v; + vertGroup[v] = groupCount++; + } else { + vertGroup[v] = g; + } + } + + float[] gx = new float[groupCount]; + float[] gy = new float[groupCount]; + float[] gz = new float[groupCount]; + for (int g = 0; g < groupCount; g++) { + int v = groupFirst[g]; + gx[g] = pos[v*3]; gy[g] = pos[v*3+1]; gz[g] = pos[v*3+2]; + } + + // Rand-Vertices einfrieren (Chunk-Nahtlosigkeit) + float bound = VoxelChunk.CELLS; + boolean[] pinned = new boolean[groupCount]; + for (int g = 0; g < groupCount; g++) { + float x = gx[g], y = gy[g], z = gz[g]; + if (x < 0.01f || x > bound - 0.01f || + y < 0.01f || y > bound - 0.01f || + z < 0.01f || z > bound - 0.01f) { + pinned[g] = true; + } + } + + // Flächennormalen berechnen und pro Gruppe akkumulieren + float[] gnx = new float[groupCount]; + float[] gny = new float[groupCount]; + float[] gnz = new float[groupCount]; + for (int t = 0; t < triCount; t++) { + int g0=vertGroup[t*3], g1=vertGroup[t*3+1], g2=vertGroup[t*3+2]; + float p0x=pos[t*9], p0y=pos[t*9+1], p0z=pos[t*9+2]; + float p1x=pos[t*9+3], p1y=pos[t*9+4], p1z=pos[t*9+5]; + float p2x=pos[t*9+6], p2y=pos[t*9+7], p2z=pos[t*9+8]; + float ex=p1x-p0x, ey=p1y-p0y, ez=p1z-p0z; + float fx=p2x-p0x, fy=p2y-p0y, fz=p2z-p0z; + float nx=ey*fz-ez*fy, ny=ez*fx-ex*fz, nz=ex*fy-ey*fx; + gnx[g0]+=nx; gny[g0]+=ny; gnz[g0]+=nz; + gnx[g1]+=nx; gny[g1]+=ny; gnz[g1]+=nz; + gnx[g2]+=nx; gny[g2]+=ny; gnz[g2]+=nz; + } + for (int g = 0; g < groupCount; g++) { + float len=(float)Math.sqrt(gnx[g]*gnx[g]+gny[g]*gny[g]+gnz[g]*gnz[g]); + if (len > 1e-6f) { gnx[g]/=len; gny[g]/=len; gnz[g]/=len; } + else { gny[g] = 1f; } + } + + // Horizontale Verschiebung auf steile Gruppen anwenden. + // fBm (Fractal Brownian Motion) mit 4 Oktaven: gleiche Frequenz auf allen + // drei Achsen → jedes Höhenniveau bekommt eigene, unabhängige Verschiebung. + // Zwei unkorrelierte fBm-Felder für X und Z (unterschiedliche Offsets). + final float STEEP = 0.707f; // cos(45°) + final float MAX_D = 4.0f; // m maximale Verschiebung bei strength=1, senkrechter Fläche + + for (int g = 0; g < groupCount; g++) { + if (pinned[g]) continue; + float ny = gny[g]; + if (ny >= STEEP) continue; + float steepness = 1f - ny / STEEP; // 0 bei 45°, 1 bei senkrecht + float wx = worldX + gx[g]; + float wz = worldZ + gz[g]; + float wy = gy[g]; + float noiseX = perturbFbm(wx * 0.08f, wy * 0.08f, wz * 0.08f); + float noiseZ = perturbFbm(wx * 0.08f + 31.7f, wy * 0.08f + 11.3f, wz * 0.08f + 67.1f); + float disp = steepness * strength * MAX_D; + gx[g] += noiseX * disp; + gz[g] += noiseZ * disp; + } + + // Positionen in Buffer schreiben + for (int v = 0; v < vertCount; v++) { + int g = vertGroup[v]; + pos[v*3] = gx[g]; pos[v*3+2] = gz[g]; + } + posF.rewind(); posF.put(pos); posF.rewind(); + + // Normalen aus geänderter Geometrie neu berechnen + java.util.Arrays.fill(gnx, 0f); java.util.Arrays.fill(gny, 0f); java.util.Arrays.fill(gnz, 0f); + for (int t = 0; t < triCount; t++) { + int g0=vertGroup[t*3], g1=vertGroup[t*3+1], g2=vertGroup[t*3+2]; + float p0x=pos[t*9], p0y=pos[t*9+1], p0z=pos[t*9+2]; + float p1x=pos[t*9+3], p1y=pos[t*9+4], p1z=pos[t*9+5]; + float p2x=pos[t*9+6], p2y=pos[t*9+7], p2z=pos[t*9+8]; + float ex=p1x-p0x, ey=p1y-p0y, ez=p1z-p0z; + float fx=p2x-p0x, fy=p2y-p0y, fz=p2z-p0z; + float nx=ey*fz-ez*fy, ny=ez*fx-ex*fz, nz=ex*fy-ey*fx; + gnx[g0]+=nx; gny[g0]+=ny; gnz[g0]+=nz; + gnx[g1]+=nx; gny[g1]+=ny; gnz[g1]+=nz; + gnx[g2]+=nx; gny[g2]+=ny; gnz[g2]+=nz; + } + FloatBuffer normF = mesh.getFloatBuffer(VertexBuffer.Type.Normal); + if (normF != null) { + normF.rewind(); + for (int v = 0; v < vertCount; v++) { + int g = vertGroup[v]; + float len=(float)Math.sqrt(gnx[g]*gnx[g]+gny[g]*gny[g]+gnz[g]*gnz[g]); + if (len > 1e-6f) normF.put(gnx[g]/len).put(gny[g]/len).put(gnz[g]/len); + else normF.put(0f).put(1f).put(0f); + } + normF.rewind(); + } + + mesh.updateBound(); + return mesh; + } + + /** fBm (Fractal Brownian Motion), 4 Oktaven, Ergebnis in [-1, 1]. */ + private static float perturbFbm(float x, float y, float z) { + float v = 0f, amp = 1f, sumAmp = 0f; + for (int i = 0; i < 4; i++) { + v += perturbNoise(x, y, z) * amp; + sumAmp += amp; + x *= 2.1f; y *= 2.1f; z *= 2.1f; + amp *= 0.5f; + } + return v / sumAmp; + } + + /** Trilinear interpoliertes Value-Noise in [-1, 1]. Kohärent, deterministisch. */ + private static float perturbNoise(float x, float y, float z) { + int ix=(int)Math.floor(x), iy=(int)Math.floor(y), iz=(int)Math.floor(z); + float fx=x-ix, fy=y-iy, fz=z-iz; + fx=fx*fx*(3-2*fx); fy=fy*fy*(3-2*fy); fz=fz*fz*(3-2*fz); + float v00 = perturbHash(ix,iy,iz) + fx*(perturbHash(ix+1,iy,iz) - perturbHash(ix,iy,iz)); + float v10 = perturbHash(ix,iy+1,iz) + fx*(perturbHash(ix+1,iy+1,iz) - perturbHash(ix,iy+1,iz)); + float v01 = perturbHash(ix,iy,iz+1) + fx*(perturbHash(ix+1,iy,iz+1) - perturbHash(ix,iy,iz+1)); + float v11 = perturbHash(ix,iy+1,iz+1)+fx*(perturbHash(ix+1,iy+1,iz+1)-perturbHash(ix,iy+1,iz+1)); + return (v00 + fy*(v10-v00)) + fz*((v01 + fy*(v11-v01)) - (v00 + fy*(v10-v00))); + } + + private static float perturbHash(int x, int y, int z) { + int h = x * 374761393 + y * 1103515245 + z * -2012135261; + h ^= (h >>> 15); h *= 0x9e3779b9; h ^= (h >>> 12); + return ((h >>> 1) & 0xFFFF) * (2f / 65535f) - 1f; + } + } diff --git a/blight-map/src/main/map/blight_map.blm.tmp b/blight-map/src/main/map/blight_map.blm.tmp deleted file mode 100644 index 3143159..0000000 Binary files a/blight-map/src/main/map/blight_map.blm.tmp and /dev/null 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 c9a380e..d3ac789 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 014a901..21923ea 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