Weiter am Voxel/Gebirgsterrain gearbeitet

This commit is contained in:
2026-07-26 22:01:09 +02:00
parent 2c88b7e313
commit 0329390d31
47 changed files with 317 additions and 88 deletions

View File

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

View File

@@ -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<Long, Integer> 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<Long, float[]> regMap = new HashMap<>(hfMap.size());
for (Map.Entry<Long, Integer> 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<Long, float[]> 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
};