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

@@ -58,8 +58,11 @@ void main() {
vec2 uvY = vWorldPos.xz / m_TexScale;
vec2 uvZ = vWorldPos.xy / m_TexScale;
// Flach ab ~11° Gefälle (20% grade, normal.y≈0.98); alles andere Fels.
float flatBlend = smoothstep(0.94, 0.99, vNormal.y);
// Flach bis ~22,5° Gefälle (deckt sich mit der Bake-Schwelle in VoxelEditorState,
// die Rampen bis 22,5° glättet vorher lag diese Grenze bei ~15° und sorgte dafür,
// dass geometrisch glatt gebackene, aber 15-22,5° steile Rampen trotzdem komplett
// mit der Klippen-Textur eingefärbt wurden statt mit der Flach-Textur.
float flatBlend = smoothstep(0.90, 0.95, vNormal.y);
float steepBlend = 1.0 - flatBlend;
// Flat: reines XZ-UV wie das Terrain (uvY = worldPos.xz / texScale), kein Triplanar.

View File

@@ -10,8 +10,8 @@ import java.util.List;
*/
public class DayTime {
/** Standard-Tagesdauer in Echtzeit-Sekunden (15 Minuten). */
public static final float DEFAULT_DAY_DURATION = 900f;
/** Standard-Tagesdauer in Echtzeit-Sekunden (96 Minuten). */
public static final float DEFAULT_DAY_DURATION = 5760f;
private float timeOfDay;
private float timeScale;
@@ -25,7 +25,7 @@ public class DayTime {
/** @param dayDuration Echtzeit-Sekunden für einen kompletten Spieltag */
public DayTime(float dayDuration) {
this.timeScale = 1f / dayDuration;
this.timeOfDay = 0.25f; // Start: Sonnenaufgang
this.timeOfDay = 0.5f; // Start: Mittag (bessere Sicht zum Testen als Sonnenaufgang)
}
public void update(float tpf) {

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(

View File

@@ -51,6 +51,9 @@ tasks.register('run', JavaExec) {
description = 'Startet das Spiel'
mainClass = mainClassName
classpath = sourceSets.main.runtimeClasspath
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(26)
}
jvmArgs = [
'--add-opens', 'java.base/java.lang=ALL-UNNAMED',
'--add-opens', 'java.desktop/sun.awt=ALL-UNNAMED',

View File

@@ -62,6 +62,15 @@ public class BlightGame extends SimpleApplication {
private long startTimeMs = 0;
private long lastFullscreenFix = 0;
private long pendingFullscreenFixAt = Long.MAX_VALUE;
// Cinnamons Display-Daemon interpretiert den glfwSetWindowMonitor()-Toggle in
// fixFullscreenMonitor() offenbar als Monitor-Neukonfiguration und setzt dabei
// "scaling-factor" (org.cinnamon.desktop.interface) auf 0 (=Auto) zurück, auch wenn
// der Nutzer explizit 100% (Wert 1) eingestellt hatte das bleibt dann auch nach dem
// Spielende so stehen. Wir sichern den Wert vor dem Fix und schreiben ihn zeitversetzt
// zurück (Cinnamon braucht etwas, bis es reagiert sofortiges Zurückschreiben würde
// von Cinnamons eigenem Reset kurz danach wieder überschrieben).
private String savedScalingFactor = null;
private long pendingScalingRestoreAt = Long.MAX_VALUE;
private boolean firstLaunch = true; // false nach dem ersten simpleInitApp()
private boolean initialFixDone = false; // Initial-Fix bei 0.5s bereits gelaufen
private boolean pendingMenuRebuild = false; // nur true nach explizitem app.restart()
@@ -202,6 +211,7 @@ public class BlightGame extends SimpleApplication {
audioSettings = AudioSettingsStore.load();
de.blight.game.state.ModelLodControl.debugLod2 = graphicsSettings.debugLod2;
captureScalingFactor();
startTimeMs = System.currentTimeMillis();
initialFixDone = false;
@@ -226,6 +236,16 @@ public class BlightGame extends SimpleApplication {
// Fullscreen-Zustand war (wie beim 5s-Watchdog). Aus dem Hidden-Zustand behandelt
// Cinnamon den nächsten Fullscreen-Übergang als Neuerstplatzierung und ignoriert
// den Ziel-Monitor zugunsten des aktiven Workspaces (Editor-Monitor).
//
// mainMenuState wird weiter unten in dieser Methode gebaut mit der JETZT
// aktuellen (noch nicht fullscreen-korrigierten) Kamera-Größe. Der Vollbild-Fix
// läuft aber erst ~200ms später in simpleUpdate() und löst dann reshape() aus.
// Ohne pendingMenuRebuild würde rebuildScreenDependentStates() dabei NUR HUD/
// Hotbar/LoadingScreen neu aufbauen (siehe dortige Kommentare: das ist normalerweise
// extra vor app.restart() gesetzt) das Menü bliebe dauerhaft auf der falschen,
// zu kleinen Ausgangsgröße stehen und würde nur einen Bildschirm-Ausschnitt
// (unten links) füllen, obwohl das Fenster längst auf volle Auflösung steht.
pendingMenuRebuild = true;
}
// Restart nach Einstellungsänderung: pendingFullscreenFixAt (scheduleFullscreenFix)
// bleibt gesetzt und übernimmt den Fix nach 1 s.
@@ -493,6 +513,11 @@ public class BlightGame extends SimpleApplication {
fixFullscreenMonitor();
}
if (now >= pendingScalingRestoreAt) {
pendingScalingRestoreAt = Long.MAX_VALUE;
restoreScalingFactor();
}
// Watchdog: Vollbild-Verlust durch GNOME/WM (erkennbar an Auflösungs-Mismatch).
if (graphicsSettings != null && graphicsSettings.fullscreen) {
if (now - startTimeMs > 5_000
@@ -798,8 +823,56 @@ public class BlightGame extends SimpleApplication {
glfwSleep(200);
GLFW.glfwFocusWindow(win);
// scaling-factor zeitversetzt zurückschreiben, siehe Feld-Kommentar oben bei
// savedScalingFactor Cinnamon reagiert erst mit etwas Verzögerung auf den
// Monitor-Toggle, ein sofortiger Restore würde von Cinnamons eigenem Reset
// wieder überschrieben.
pendingScalingRestoreAt = System.currentTimeMillis() + 2_000;
} catch (Throwable e) {
log.warn("[Grafik] Vollbild-Fix fehlgeschlagen: {}", e.getMessage(), e);
}
}
/** Liest den aktuellen Cinnamon-Skalierungsfaktor, um ihn nach dem Vollbild-Fix
* wiederherzustellen (siehe Feld-Kommentar bei savedScalingFactor). */
private void captureScalingFactor() {
try {
Process p = new ProcessBuilder(
"gsettings", "get", "org.cinnamon.desktop.interface", "scaling-factor")
.redirectErrorStream(true).start();
String out = new String(p.getInputStream().readAllBytes()).trim();
p.waitFor();
// Format ist z.B. "uint32 1" letztes Token ist der Wert. Nicht per
// replaceAll("[^0-9]","") Ziffern rausfiltern: "uint32" enthält selbst
// die Ziffern 3 und 2, das hängt sie vor den echten Wert (1 -> "321",
// 321 -> "32321", ...) und schaukelt sich exponentiell hoch.
String[] parts = out.split("\\s+");
String value = parts[parts.length - 1];
savedScalingFactor = value.matches("[0-9]+") ? value : null;
log.info("[Grafik] scaling-factor gesichert: {}", savedScalingFactor);
} catch (Exception e) {
log.debug("[Grafik] scaling-factor konnte nicht gelesen werden (evtl. kein Cinnamon): {}", e.getMessage());
savedScalingFactor = null;
}
}
/** Schreibt den zuvor gesicherten Cinnamon-Skalierungsfaktor zurück. */
private void restoreScalingFactor() {
if (savedScalingFactor == null) return;
try {
new ProcessBuilder(
"gsettings", "set", "org.cinnamon.desktop.interface", "scaling-factor", savedScalingFactor)
.start().waitFor();
log.info("[Grafik] scaling-factor wiederhergestellt: {}", savedScalingFactor);
} catch (Exception e) {
log.warn("[Grafik] scaling-factor konnte nicht wiederhergestellt werden: {}", e.getMessage());
}
}
@Override
public void stop() {
restoreScalingFactor();
super.stop();
}
}

View File

@@ -373,7 +373,7 @@ public class WorldScene extends BaseAppState {
dayNight.getDayTime().setTimeOfDay(0.5f);
} else if (envCamFrames == 30) {
log.info("[WorldScene] Starte LightProbe-Backen bei Mittag (Frame {})...", envCamFrames);
final float restoreTime = 0.25f;
final float restoreTime = 0.5f; // Spielstart-Uhrzeit, siehe DayTime-Default
LightProbeFactory.updateProbe(probe, envCam, rootNode,
new JobProgressAdapter<LightProbe>() {
@Override public void done(LightProbe result) {

View File

@@ -87,7 +87,7 @@ public class DayNightState extends BaseAppState implements TimeListener {
// ── Konstruktoren ─────────────────────────────────────────────────────────
/** Game-Modus: Schatten aktiv, 5-Minuten-Tag. */
/** Game-Modus: Schatten aktiv, 96-Minuten-Tag (siehe DayTime.DEFAULT_DAY_DURATION). */
public DayNightState() {
this(new DayTime(), true);
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.