Weiter am Voxel kram gebastelt

This commit is contained in:
2026-08-06 22:48:54 +02:00
parent 0329390d31
commit 8feaf24490
39 changed files with 359 additions and 140 deletions

View File

@@ -782,8 +782,6 @@ public class SharedInput {
public volatile int bakeTotal = 0;
/** JME → JFX: Status-Meldung nach Abschluss des Backens oder Löschens. */
public volatile String bakeStatusMsg = null;
/** JME → JFX: Aktuell abgeschlossene Blur-Iteration (0-7). */
public volatile int blurIterDone = 0;
/**
* Per BFS selektierte gebackene Chunks (chunkKey-kodiert).

View File

@@ -547,6 +547,11 @@ public class TerrainEditorState extends BaseAppState {
thirdSplatTex.setWrap(Texture.WrapMode.EdgeClamp);
thirdSplatTex.setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
thirdSplatTex.setMagFilter(Texture.MagFilter.Bilinear);
// loadedMapData wird ab hier nicht mehr gebraucht (letzter Verbraucher war
// dieser Merge). Freigeben, weil terrainHeight allein ~1 GB (16385² floats)
// belegt und sonst die ganze Session über unnötig im Heap hängt.
loadedMapData = null;
}
// Standard-Texturpfade (Fallback wenn kein benutzerdefinierter Pfad gesetzt)

View File

@@ -1558,47 +1558,10 @@ public class VoxelEditorState extends BaseAppState {
cBuf[c.idx(bx, VoxelChunk.CELLS, bz)] = tBuf[c.idx(bx, 0, bz)];
}
input.blurIterDone = 0;
// Bilateraler Filter: Nachbarn mit großem Dichtesprung (Klippen, Felskanten)
// bekommen fast Null-Gewicht → scharfe Übergänge bleiben erhalten.
// Flache Bereiche (geringe Differenz) werden voll geglättet wie bisher.
// sigma_r = 30 → diff=20: w≈0.80 (flach, volles Blur);
// diff=80: w≈0.04 (Klippe, kaum Blur).
final float BILATERAL_INV_2S2 = 1f / (2f * 30f * 30f);
for (int iter = 0; iter < 7; iter++) {
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 = new float[blurN * blurN * blurN];
for (int by = 0; by < blurN; by++) {
for (int bz = 0; bz < blurN; bz++) {
for (int bx = 0; bx < blurN; bx++) {
float center = cur[c.idx(bx, by, bz)];
float wSum = 0f, vSum = 0f;
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);
float diff = nb - center;
float w = (float) Math.exp(-diff * diff * BILATERAL_INV_2S2);
vSum += w * nb;
wSum += w;
}
next[c.idx(bx, by, bz)] = vSum / wSum;
}
}
}
nextBufs.put(k, next);
}
curBufs = nextBufs;
input.blurIterDone = iter + 1;
}
// KEIN Blur mehr vor der Rampen-Berechnung (entfernt 2026-08-06): war ein Rest
// der alten Pipeline, den ich beim "fang bei 0 an"-Rewrite versehentlich stehen
// gelassen hatte. hfMap wird jetzt direkt aus curBufs (= unveränderte Rohdichte,
// nur Rand-synchronisiert) gebaut, nicht aus geblurrten Daten.
// ── Neigungsadaptive Oberflächenverarbeitung (Regressions-Ansatz) ────────
//
@@ -1620,12 +1583,14 @@ public class VoxelEditorState extends BaseAppState {
// (verhindert, dass lokale Extrema durch weit entfernte Werte abgezogen
// werden; Regressions-Abweichung an Stufenfronten liegt bei ≤ 0,46).
{
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)
// RADIUS in Voxel-Zellen. 1 Zelle = 1 Weltmeter (siehe VoxelChunk.toWorldX:
// Koeffizient 1, kein Skalierungsfaktor). 10 m gewählt, weil ein kleinerer
// Deckel bei konstanter Steigung (langer, flacher Hang) wieder künstliche
// Stufen einführen würde, sobald die Suche vor dem echten Kontur-Nachbarn
// abbricht (vom Nutzer bestätigt: "nicht so niedrig, dass wir bei einer
// konstanten Steigung wieder Stufen reinbekommen").
final int RADIUS = 10;
// ── Schritt 1: Höhenfeld ──────────────────────────────────────────
// Key-Schema: (cx*C + bx + 30000) * 60001 + (cz*C + bz + 30000)
@@ -1649,54 +1614,268 @@ public class VoxelEditorState extends BaseAppState {
}
}
// ── 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
// Nachbar-Chunks (nur XZ, gleiches cy) rein lesend nachladen, damit die
// Ringsuche in Schritt 2 nicht an der Batch-Grenze abreißt (RADIUS=10 kann
// problemlos in einen nicht mitgebackenen Nachbar-Chunk hineinreichen).
// Diese Chunks werden NICHT gebacken/verändert, nur ihr Höhenfeld gelesen.
for (VoxelChunk c : nonEmpty) {
for (int dcz = -1; dcz <= 1; dcz++) {
for (int dcx = -1; dcx <= 1; dcx++) {
if (dcx == 0 && dcz == 0) continue;
int ncx = c.cx + dcx, ncz = c.cz + dcz, ncy = c.cy;
long nk = chunkKey(ncx, ncy, ncz);
if (allOriginal.containsKey(nk)) continue; // schon im Batch enthalten
try {
if (!VoxelChunkIO.exists(ncx, ncy, ncz)) continue;
VoxelChunk nc = VoxelChunkIO.load(ncx, ncy, ncz);
for (int bz = 0; bz < C; bz++) {
for (int bx = 0; bx < C; bx++) {
for (int by = VoxelChunk.SIZE - 1; by >= 0; by--) {
if (nc.getDensity(bx, by, bz) > 0) {
int wiy = ncy * C + by;
long hk = (long)(ncx * C + bx + 30000) * 60001L
+ (ncz * C + bz + 30000);
hfMap.merge(hk, wiy, Math::max);
break;
}
}
}
}
} catch (IOException e) {
log.warn("Nachbar-Chunk ({},{},{}) konnte für Rampen-Suche nicht geladen werden", ncx, ncy, ncz, e);
}
}
}
}
// ── Schritt 2: Waypoint-Distanz-Interpolation ────────────────────
// 1:1-Umsetzung der Nutzer-Vorgabe vom 2026-08-05/06:
// Für jede Spalte (Rohhöhe h0) isotrope Ringsuche (Chebyshev-Distanz
// vermeidet Zickzack-Artefakte einer Achsen-Suche) nach dem nächsten
// Nachbarn mit Höhe > h0 (d_up, h_up = TATSÄCHLICHE gefundene Höhe,
// kein Cap auf h0+1 mehrstufige Sprünge werden laut Nutzer als EINE
// durchgehende Fläche überbrückt statt Stufe für Stufe) und dem
// nächsten Nachbarn mit Höhe < h0 (d_downRaw).
// smoothH = (h0·d_up + h_up·d_down) / (d_up + d_down)
// d_down = d_downRaw 1 (Trailing-Edge-Korrektur: die eigene
// Kante liegt einen Schritt vor der ersten tatsächlich
// niedrigeren Zelle siehe diag_waypoint*.jsh)
// Kein höherer Nachbar gefunden → Plateau → smoothH = h0 (flach).
// Kein niedrigerer Nachbar in der Voxel-Struktur gefunden → Basisterrain
// wird als impliziter unterer Nachbar mitgesucht (grobe Version, per
// Nutzer-Feedback erstmal ohne <1m-Schwelle "passt so, sonst gehen
// wir das später an"). Nur falls selbst das nicht greift (echtes
// Innen-Plateau weit von jeder Kante) bleibt smoothH = h0.
//
// 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).
// Validiert in jshell VOR dem Einbau (siehe scratchpad diag_waypoint2.jsh,
// diag_waypoint2d_v2.jsh, diag_waypoint2d_v3.jsh):
// - 1D-Vorgabe 3,2,2,2,2,1,1,1,0 → exakt erwartete Werte inkl. Basisterrain-Tail
// - 2D-Rampen-Extrusion → identisch zum 1D-Fall
// - Kuppel/Krümmungstest (der ursprüngliche Graben-Auslöser dieser
// ganzen Debug-Session) → 0 spurious dips, weil die Formel auf reiner
// Nachbar-Distanz basiert statt auf einer Fenster-Mittelung (Jensen's-
// Inequality-Problem der alten Regressions-/Blur-Ansätze entfällt).
// - Mehrstufen-Sprung 3,3,3,1,1,0 → 3,3,3,2,1,0 (Ebene 2 übersprungen,
// eine durchgehende Fläche statt Stufen).
// - Isolierter Spike auf großem Plateau → bleibt isolierte Säule
// (kein niedrigerer Nachbar in der Umgebung → korrekt kein Ramping).
//
// regMap[0] = smoothH, regMap[1] = totalSlope
Map<Long, float[]> regMap = new HashMap<>(hfMap.size());
// NACHTRAG 2026-08-06 (echte Chunks 14/15_0_06/07): erster Bake zeigte kleine
// aufgeschüttete Wälle an alten Voxel-Kanten. Ursache in echten (nicht rein
// synthetischen) Daten gefunden: bei gleicher Ring-Distanz r existieren oft
// MEHRERE Nachbarn mit unterschiedlicher Höhe (z.B. 8 im Westen, 9 im Norden,
// beide r=1). Die ursprüngliche Version nahm den ERSTEN Treffer der dz/dx-
// Schleife statt des nächstgelegenen tatsächlichen Stufen-Nachbarn → zufällige
// Sprünge zu einer zu weit entfernten Höhe. Fix: pro Ring den Nachbarn mit der
// kleinsten Höhe > h0 bzw. größten Höhe < h0 wählen. Betraf 17 von 10002 Spalten
// in den Test-Chunks, max. Abweichung 1,0 (diag_realwall*.jsh im scratchpad).
Map<Long, Float> smoothMap = 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++;
// WICHTIG: Bei gleicher Ring-Distanz r können mehrere Nachbarn mit
// unterschiedlicher Höhe existieren (z.B. Höhe 8 im Westen UND Höhe 9
// im Norden, beide bei r=1). Naiv den ERSTEN Treffer der Schleifen-
// Reihenfolge zu nehmen wählt praktisch zufällig zwischen ihnen (abhängig
// von der dz/dx-Iterationsreihenfolge) und erzeugte an echten Kanten
// willkürliche Sprünge zum "falschen" (zu weit entfernten) Nachbarn
// sichtbar als kleine aufgeschüttete Wälle. Fix: pro Ring den Nachbarn
// mit der KLEINSTEN Höhe > h0 (bzw. GRÖSSTEN Höhe < h0) wählen, also den
// nächstgelegenen tatsächlichen Stufen-Nachbarn, nicht irgendeinen weiter
// entfernten. Validiert gegen reale Chunk-Daten (diag_realwall*.jsh).
Integer hUp = null; int dUp = -1;
Integer hDown = null; int dDownRaw = -1;
for (int r = 1; r <= RADIUS && (hUp == null || hDown == null); r++) {
Integer bestUpThisRing = null, bestDownThisRing = null;
for (int dz = -r; dz <= r; dz++) {
for (int dx = -r; dx <= r; dx++) {
if (Math.max(Math.abs(dx), Math.abs(dz)) != r) continue;
long hkN = (long)(colX + dx + 30000) * 60001L + (colZ + dz + 30000);
Integer hN = hfMap.get(hkN);
if (hN == null) continue;
if (hN > h0 && (bestUpThisRing == null || hN < bestUpThisRing)) bestUpThisRing = hN;
if (hN < h0 && (bestDownThisRing == null || hN > bestDownThisRing)) bestDownThisRing = hN;
}
}
if (hUp == null && bestUpThisRing != null) { hUp = bestUpThisRing; dUp = r; }
if (hDown == null && bestDownThisRing != null) { hDown = bestDownThisRing; dDownRaw = r; }
}
// KORREKTUR 2026-08-06 (echter Bake-Test, Screenshot mit 1m-Stufe vor langer
// flacher Fläche): wenn kein niedrigerer Voxel-Nachbar gefunden wird, hatte
// ich hier fälschlich "smoothH=h0" (flach, keine Rampe) verwendet. Das
// widerspricht der Original-Vorgabe: "wenn wir keinen niedrigeren Partner
// finden, dann haben wir die Höhe des Basisterrains erreicht und es soll die
// OBERE GERADE WEITERGEZOGEN werden" bereits im 1D-Fall exakt so validiert
// (Tail-Extrapolation, siehe diag_waypoint2.jsh). Die 2D-Generalisierung hatte
// das fälschlich auf "flach" vereinfacht, wodurch Spalten mit einem gültigen
// hUp aber ohne erreichbaren hDown (z.B. eine 1m-Stufe vor einer sehr weiten
// Ebene) GAR NICHT geramped wurden sichtbar als scharfe Stufe.
// Fix: fehlendes hDown wird als "gerade noch nicht gefunden, aber bei RADIUS
// angenommen" behandelt statt als "keine Rampe". Das erzeugt eine sanft zum
// Rand hin auslaufende Rampe über die vollen RADIUS Meter, statt eines
// abrupten Sprungs zwischen "geramped" und "unverändert flach".
float smoothH;
if (hUp == null) {
smoothH = h0; // Plateau: kein höherer Nachbar -> bewusst flach (Originalregel)
} else {
float dDown = (hDown != null) ? (dDownRaw - 1) : (RADIUS - 1);
smoothH = (dDown <= 0f) ? h0 : (h0 * dUp + hUp * dDown) / (dUp + dDown);
}
smoothMap.put(hk0, smoothH);
}
// ── Schritt 2b: Perimeter-Übergang zum Basisterrain ──────────────
// Umsetzung v3 (2026-08-06) der Nutzer-Vorgabe: "berechne auch die Schnittpunkte
// mit dem Basisterrain einmal außen herum... baue hier die Rampe weiter anstelle
// einer Klippe", 10m Reichweite, Fallback-Szenario (nur wenn Schritt 2 die Struktur
// nicht schon selbst bis auf Basisterrain-Niveau geführt hat).
//
// v1 (Höhen-Schwellwert pro Spalte) erzeugte an der Schwelle selbst neue Sprünge.
// v2 (unabhängige Strahlen pro Rand-Spalte/Richtung) überschrieben sich gegenseitig
// unkoordiniert an Überlappungen → Flickenteppich aus Dreiecken (Screenshot 21-56-58).
// v3 (reine Nachbar-Mittelwert-Ausbreitung ohne Steigung) verlor den Winkel des
// Randes komplett → 10m flaches Plateau statt Rampe (Nutzer-Feedback).
// v4 (Höhe+Steigung Zelle-für-Zelle weiterreichen/mitteln, BFS) divergierte: die
// Rückkopplung über viele Zellen hinweg schaukelte sich zu chaotischen, spitzen
// Glitches auf (Screenshot 22-23-06) klassisches Instabilitätsproblem iterativer
// Fortpflanzung ohne Dämpfung.
// Fix v5: KEINE Kettenfortpflanzung mehr. Jede leere Zelle im 10m-Streifen wird
// unabhängig berechnet exakt wie Schritt 2 selbst (dort funktioniert das robuste
// "jede Spalte für sich per Ringsuche" bereits nachweislich gut): isotrope Ringsuche
// zur nächsten Struktur-Zelle, deren lokale Steigung einmalig (nicht iterativ)
// fortgesetzt, mit wachsender Distanz zunehmend zur echten Basisterrain-Höhe hin
// eingeblendet. Keine Zelle hängt von einer anderen NEU BERECHNETEN Zelle ab →
// keine Rückkopplung, keine Divergenz möglich. War in der Tat stabil, ABER: edgeH
// selbst schwankte zwischen benachbarten Zielzellen stark (3,9 bis 5,33 nur 4
// Zellen auseinander), weil jede Zelle nur die EINE zuerst gefundene Struktur-
// Zelle nahm. Fix v5b: über ALLE Struktur-Zellen im nächstgelegenen Ring mitteln.
// Trotzdem blieb ein welliges/oszillierendes Band über eigentlich flachen Flächen
// sichtbar (Screenshot 22-43-57, aus Seitenansicht bestätigt: keine Höhle/Überhang,
// echte Welligkeit) die Steigungs-Fortsetzung (auch gedämpft, siehe rawH) war
// schlicht zu rauschempfindlich.
// Fix v6 (aktuell): Steigungs-Fortsetzung komplett gestrichen. Reiner Distanz-Blend
// von der (ring-gemittelten) Kanten-Höhe zur tatsächlichen Basisterrain-Höhe. Folgt
// nicht mehr exakt dem Rampen-Winkel, ist aber garantiert monoton/glatt.
int[][] DIRS8 = {{1,0},{-1,0},{0,1},{0,-1},{1,1},{1,-1},{-1,1},{-1,-1}};
// Kandidaten sammeln: alle leeren Zellen innerhalb RADIUS einer Rand-Spalte.
java.util.Set<Long> candidates = new java.util.HashSet<>();
for (long hk0 : hfMap.keySet()) {
int colZ = (int)(hk0 % 60001L) - 30000;
int colX = (int)(hk0 / 60001L) - 30000;
for (int dz = -RADIUS; dz <= RADIUS; dz++) {
for (int dx = -RADIUS; dx <= RADIUS; dx++) {
if (Math.max(Math.abs(dx), Math.abs(dz)) > RADIUS) continue;
long hkC = (long)(colX + dx + 30000) * 60001L + (colZ + dz + 30000);
if (!hfMap.containsKey(hkC)) candidates.add(hkC);
}
}
if (cnt < 4) {
regMap.put(hk0, new float[]{ h0, 99f });
continue;
}
Map<Long, Float> extensionMap = new HashMap<>();
int extCount = 0, extLogged = 0;
for (long hkC : candidates) {
int colZ = (int)(hkC % 60001L) - 30000;
int colX = (int)(hkC / 60001L) - 30000;
// Nächsten Ring mit Struktur-Zellen isotrop suchen (gleiches Vorgehen wie
// Schritt 2), dann ALLE Struktur-Zellen in diesem Ring mitteln nicht nur die
// erste gefundene. Grund (Log-Befund): benachbarte Zielzellen fanden oft
// unterschiedliche einzelne "nächste" Zellen mit stark abweichender Höhe
// (edgeH schwankte 3,9 bis 5,33 nur 4 Zellen auseinander) → wildes Springen.
// Mittelung über den ganzen Ring glättet das.
int nearestDist = -1;
java.util.List<long[]> ringHits = new java.util.ArrayList<>(); // {colX,colZ}
outer:
for (int r = 1; r <= RADIUS; r++) {
for (int dz = -r; dz <= r; dz++) {
for (int dx = -r; dx <= r; dx++) {
if (Math.max(Math.abs(dx), Math.abs(dz)) != r) continue;
long hkN = (long)(colX + dx + 30000) * 60001L + (colZ + dz + 30000);
if (hfMap.containsKey(hkN)) ringHits.add(new long[]{colX + dx, colZ + dz});
}
}
if (!ringHits.isEmpty()) { nearestDist = r; break outer; }
}
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 });
if (nearestDist < 0) continue; // außerhalb der Reichweite, bleibt Klippe
float sumEdgeH = 0f; int hitCnt = 0;
for (long[] hit : ringHits) {
int hColX = (int) hit[0], hColZ = (int) hit[1];
long hKey = (long)(hColX + 30000) * 60001L + (hColZ + 30000);
Float hH = smoothMap.get(hKey);
if (hH == null) continue;
sumEdgeH += hH; hitCnt++;
}
if (hitCnt == 0) continue;
float edgeH = sumEdgeH / hitCnt;
// KEINE Steigungs-Extrapolation mehr (v4/v5 hatten das versucht eine an nur
// wenigen Pixeln gemessene Steigung ist zu verrauscht, verstärkt sich über die
// Distanz und erzeugte ein welliges/oszillierendes Band über eigentlich flachen
// Flächen, siehe Screenshot 22-43-57). Reiner, robuster Distanz-Blend von der
// (gemittelten) Kanten-Höhe zur tatsächlichen Basisterrain-Höhe: nah am Rand
// dominiert die Kante, weiter draußen die Terrainhöhe. Folgt nicht mehr exakt
// dem Winkel der letzten Rampe, ist aber garantiert monoton und ohne Rauschen.
float wx = colX - 2048f, wz = colZ - 2048f;
float th = terrainH(wx, wz);
float finalH = edgeH;
if (Float.isFinite(th)) {
float t = nearestDist / (float) RADIUS; // 0 am Rand, 1 bei RADIUS
finalH = edgeH * (1f - t) + th * t;
}
extensionMap.put(hkC, finalH);
extCount++;
if (extLogged < 20) {
log.info("Perimeter-Rampe: colX={} colZ={} dist={} edgeH={} th={} finalH={}",
colX, colZ, nearestDist, edgeH, th, finalH);
extLogged++;
}
}
log.info("Perimeter-Übergang: {} Erweiterungs-Spalten jenseits des Strukturrands erzeugt.", extCount);
// Erweiterungs-Spalten in hfMap/smoothMap einspeisen, damit Schritt 3 sie mitschreibt.
for (Map.Entry<Long, Float> e : extensionMap.entrySet()) {
long hk = e.getKey();
if (hfMap.containsKey(hk)) continue;
hfMap.put(hk, Math.round(e.getValue()));
smoothMap.put(hk, e.getValue());
}
// ── Schritt 3: Dichte anpassen ────────────────────────────────────
// ALLE Spalten aus hfMap bekommen den Dichte-Gradienten geschrieben, auch
// Plateau-Spalten (smoothH==h0), nicht nur echte Rampen. Grund (gefunden
// 2026-08-06 per Debug-Dump-Vergleich gegen das echte Mesh): eine unveränderte
// Spalte (volle Dichte +127 bei h0, leer bei h0+1) hat ihre MC-Isofläche NICHT
// exakt bei h0, sondern bei ≈ h0 + 127/255 ≈ h0+0.498 (lineare Nullstellen-
// Interpolation). Wurde nur bei Rampen-Spalten explizit auf die exakte smoothH
// geschrieben, blieben Plateau-Spalten bei ihrer natürlichen +0,5-Position →
// an JEDER Grenze zwischen Plateau und Rampe ein ~0,5 Einheiten hoher Sprung.
// Das war die eigentliche Ursache der "aufgeschütteten Wälle" (betraf 60% der
// Spalten in den Test-Chunks). Fix: einheitliche Konvention für alle Spalten.
{
Map<Long, float[]> nextBufs = new HashMap<>();
for (VoxelChunk c : nonEmpty) {
@@ -1710,32 +1889,15 @@ public class VoxelEditorState extends BaseAppState {
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;
Integer h0 = hfMap.get(hk0);
Float smoothH = smoothMap.get(hk0);
if (h0 == null || smoothH == null) continue;
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));
}
// Sub-voxel-genauen Dichte-Gradienten schreiben, damit MC die
// Isofläche auf die exakte smoothH-Höhe interpoliert.
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));
}
}
}
@@ -2095,32 +2257,7 @@ public class VoxelEditorState extends BaseAppState {
}
}
/**
* Cross-boundary Lookup für den kooperativen Blur:
* Liest aus dem aktuellen Blur-Buffer des Nachbar-Chunks, oder falls der
* Nachbar nicht in bufs ist direkt aus dessen Original-Daten.
* Verwendet {@code src.idx()} da SIZE für alle Chunks identisch ist.
*/
private float getBlurBuf(Map<Long, float[]> bufs, Map<Long, VoxelChunk> originals,
VoxelChunk src, int x, int y, int z) {
int n = VoxelChunk.SIZE;
int cells = VoxelChunk.CELLS;
int ncx = src.cx, ncy = src.cy, ncz = src.cz;
int lx = x, ly = y, lz = z;
if (x < 0) { ncx--; lx = cells + x; }
else if (x >= n) { ncx++; lx = x - cells; }
if (y < 0) { ncy--; ly = cells + y; }
else if (y >= n) { ncy++; ly = y - cells; }
if (z < 0) { ncz--; lz = cells + z; }
else if (z >= n) { ncz++; lz = z - cells; }
long nk = chunkKey(ncx, ncy, ncz);
float[] buf = bufs.get(nk);
if (buf != null) return buf[src.idx(lx, ly, lz)];
VoxelChunk nb = originals.get(nk);
return nb != null ? nb.getDensity(lx, ly, lz) : Byte.MIN_VALUE;
}
/** Liest Dichte aus einem geblurrten Chunk, clamped an Chunk-Grenzen. Nur für Gradienten-Berechnung. */
/** Liest Dichte aus einem Chunk, clamped an Chunk-Grenzen. Nur für Gradienten-Berechnung. */
private static float sampleBlurred(VoxelChunk chunk, int x, int y, int z) {
int n = VoxelChunk.SIZE;
return chunk.getDensity(