Back Effekte hinzugefügt

This commit is contained in:
2026-08-09 22:22:44 +02:00
parent 46be9ffe57
commit 0a657a5831
7 changed files with 220 additions and 7 deletions

View File

@@ -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);
}

View File

@@ -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). */

View File

@@ -2522,8 +2522,10 @@ public class VoxelEditorState extends BaseAppState {
// Nur erfolgreich gebackene Chunks werden gelöscht
List<VoxelChunk> 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<Long, VoxelChunk> blurredMap) {
private boolean bakeChunk(VoxelChunk original, Map<Long, VoxelChunk> 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),
};

View File

@@ -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<Long, Integer> 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;
}
}