Zwischenstand gesichert bei den voxel poxoxeln

This commit is contained in:
2026-08-09 21:48:23 +02:00
parent 936efb7bcc
commit 00a7ccbe47
11 changed files with 135 additions and 53 deletions

View File

@@ -58,11 +58,10 @@ void main() {
vec2 uvY = vWorldPos.xz / m_TexScale; vec2 uvY = vWorldPos.xz / m_TexScale;
vec2 uvZ = vWorldPos.xy / m_TexScale; vec2 uvZ = vWorldPos.xy / m_TexScale;
// Flach bis ~22,5° Gefälle (deckt sich mit der Bake-Schwelle in VoxelEditorState, // Flach bis ~45° Gefälle. LOD0-Smoothing (3 Passes, 0.3 Stärke) kann die Normalen
// die Rampen bis 22,5° glättet vorher lag diese Grenze bei ~15° und sorgte dafür, // an Rampenfüßen (14°-Rampe, ideal normal.y≈0.97) auf 0.70-0.85 herunterziehen.
// dass geometrisch glatt gebackene, aber 15-22,5° steile Rampen trotzdem komplett // Untere Grenze 0.70 (≈45°) echte Klippen (normal.y<0.70) zeigen weiter Gebirge.
// mit der Klippen-Textur eingefärbt wurden statt mit der Flach-Textur. float flatBlend = smoothstep(0.70, 0.90, vNormal.y);
float flatBlend = smoothstep(0.90, 0.95, vNormal.y);
float steepBlend = 1.0 - flatBlend; float steepBlend = 1.0 - flatBlend;
// Flat: reines XZ-UV wie das Terrain (uvY = worldPos.xz / texScale), kein Triplanar. // Flat: reines XZ-UV wie das Terrain (uvY = worldPos.xz / texScale), kein Triplanar.

View File

@@ -2111,78 +2111,160 @@ public class VoxelEditorState extends BaseAppState {
// Fix v6 (aktuell): Steigungs-Fortsetzung komplett gestrichen. Reiner Distanz-Blend // Fix v6 (aktuell): Steigungs-Fortsetzung komplett gestrichen. Reiner Distanz-Blend
// von der (ring-gemittelten) Kanten-Höhe zur tatsächlichen Basisterrain-Höhe. Folgt // 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. // nicht mehr exakt dem Rampen-Winkel, ist aber garantiert monoton/glatt.
// 4:1-Abwärtsrampe bei Stufenabbrüchen (inter-terrace + Basisterrain-Übergang). // 4:1-Abwärtsrampe bei Stufenabbrüchen.
// //
// Schritt 2 lässt Spalten mit hUp=null flach (smoothH=h0), auch wenn direkt // Zwei getrennte Passes wegen unterschiedlicher Schutz-Logik:
// daneben eine niedrigere Terasse oder das Basisterrain-Tal liegt. Das erzeugt
// vertikale Klippen an allen Plateau-Kanten.
// //
// Fix: Für jede Spalte den nächsten Nachbarn mit Höhe < h0 suchen // Pass A Basisterrain-Übergang:
// (Voxel-Spalte aus hfMap ODER Basisterrain außerhalb). Wenn gefunden bei // Sucht nur Nicht-hfMap-Zellen (echtes Basisterrain). Voxel-Spalten in
// Chebyshev-Distanz d mit mittlerer Höhe avgLow: // hfMap blocken die Sicht natürlich (Kliffwände sind in hfMap → werden
// rampH = avgLow + d / 4.0 // übersprungen). MAX_TERRAIN_DIFF verhindert tiefe Klippen.
// Spalte wird auf min(smoothH, rampH) abgesenkt nie angehoben. // Kein Ring-Vorfilter nötig der Schutz kommt durch hfMap-Blocking.
// Entspricht 4 Schritte vor / 1 Schritt runter, deckt beide Bedingungen ab: //
// „Tal darunter" (avgLow << h0) und „<1m über Basisterrain" (kleines dH). // Pass B Voxel-zu-Voxel-Übergang:
// Ringsuche je Spalte unabhängig von anderen Schritt-2b-Ergebnissen → // Sucht nur hfMap-Zellen. Ring-1-Vorfilter: alle Voxel auf gleicher Höhe
// keine Kettenfortpflanzung, keine Divergenz. // → Plateau-Innenzelle → überspringen.
{ {
final float RAMP_RATIO = 4.0f; final float RAMP_RATIO = 4.0f;
final int MAX_RAMP_R = 20; final int MAX_RAMP_R = 20;
final int MAX_RAMP_R_TERRAIN = 4;
final float MAX_TERRAIN_DIFF = 1.0f;
final float MAX_VOXEL_DIFF = 1.05f;
// Terrain-Cache: vermeidet wiederholte terrainH()-Aufrufe bei Überlappungen
Map<Long, Float> terrainCache2b = new HashMap<>(); Map<Long, Float> terrainCache2b = new HashMap<>();
for (Map.Entry<Long, Float> entry : hfMap.entrySet()) { for (Map.Entry<Long, Float> entry : hfMap.entrySet()) {
long hk0 = entry.getKey(); long hk0 = entry.getKey();
float h0raw = entry.getValue(); // Original-Voxelhöhe float h0raw = entry.getValue();
float h0sm = smoothMap.getOrDefault(hk0, h0raw); // Schritt-2-Ausgabe float h0sm = smoothMap.getOrDefault(hk0, h0raw);
int colX2b = (int)(hk0 / 60001L) - 30000; int colX2b = (int)(hk0 / 60001L) - 30000;
int colZ2b = (int)(hk0 % 60001L) - 30000; int colZ2b = (int)(hk0 % 60001L) - 30000;
// Ringsuche nach nächstem niedrigeren Nachbarn (Voxel oder Terrain) float minRampH = h0sm;
float sumLowH = 0f; // Terrain ÜBER dem Voxel: Voxel nach oben auf Terrain-Niveau heben.
int lowCount = 0; // Kein +0.1f-Offset an Ring-1 → Ring-1-Oberfläche liegt exakt auf Terrain
int lowDist = -1; // (nahtloser Übergang). Innere Ringe steigen per 4:1-Rampe weiter an.
float minRaiseH = Float.POSITIVE_INFINITY;
outer2b: // ── Pass A: Basisterrain ──────────────────────────────────────────
for (int r = 1; r <= MAX_RAMP_R; r++) { // Pro Ring: höchstes qualifizierendes th (maxThAtR) → Abwärts-Kandidat.
// Gegenfall: Terrain ÜBER h0sm (maxThAboveAtR) → Aufwärts-Kandidat.
float passA_maxThAbove = Float.NEGATIVE_INFINITY; // Terrain ÜBER h0sm (r=1, Diagnose)
for (int r = 1; r <= MAX_RAMP_R_TERRAIN; r++) {
float maxThAtR = Float.NEGATIVE_INFINITY;
float maxThAboveAtR = Float.NEGATIVE_INFINITY;
for (int dz2b = -r; dz2b <= r; dz2b++) { for (int dz2b = -r; dz2b <= r; dz2b++) {
for (int dx2b = -r; dx2b <= r; dx2b++) { for (int dx2b = -r; dx2b <= r; dx2b++) {
if (Math.max(Math.abs(dx2b), Math.abs(dz2b)) != r) continue; if (Math.max(Math.abs(dx2b), Math.abs(dz2b)) != r) continue;
long hkN = (long)(colX2b + dx2b + 30000) * 60001L long hkN = (long)(colX2b + dx2b + 30000) * 60001L
+ (colZ2b + dz2b + 30000); + (colZ2b + dz2b + 30000);
Float hN = hfMap.get(hkN); if (hfMap.containsKey(hkN)) continue;
float neighborH; Float cached = terrainCache2b.get(hkN);
if (hN != null) { if (cached == null) {
neighborH = hN; // Voxel-Spalte: Originalhöhe cached = terrainH((float)(colX2b + dx2b),
} else { (float)(colZ2b + dz2b));
Float cached = terrainCache2b.get(hkN); terrainCache2b.put(hkN, cached);
if (cached == null) {
cached = terrainH((float)(colX2b + dx2b),
(float)(colZ2b + dz2b));
terrainCache2b.put(hkN, cached);
}
neighborH = cached; // Basisterrain
} }
if (neighborH < h0raw) { float th = cached;
sumLowH += neighborH; if (th >= h0sm) {
lowCount++; if (r == 1 && th > passA_maxThAbove) {
passA_maxThAbove = th;
}
if ((th - h0sm) <= MAX_TERRAIN_DIFF && th > maxThAboveAtR) {
maxThAboveAtR = th;
}
continue;
}
float diff = h0sm - th;
if (diff > MAX_TERRAIN_DIFF) continue;
if (th > maxThAtR) {
maxThAtR = th;
} }
} }
} }
if (lowCount > 0) { if (maxThAtR > Float.NEGATIVE_INFINITY) {
lowDist = r; float candidate = maxThAtR + 0.1f + (r - 1) / RAMP_RATIO;
break outer2b; if (candidate < minRampH) {
minRampH = candidate;
}
}
// Aufwärts-Kandidat: gleiche Formel wie Abwärts (symmetrisch)
if (maxThAboveAtR > Float.NEGATIVE_INFINITY) {
float raiseCandidate = maxThAboveAtR + (r - 1) / RAMP_RATIO;
if (raiseCandidate < minRaiseH) {
minRaiseH = raiseCandidate;
}
}
}
// Diagnose
if (log.isTraceEnabled() && passA_maxThAbove > Float.NEGATIVE_INFINITY
&& minRampH >= h0sm) {
if (minRaiseH < Float.POSITIVE_INFINITY) {
log.trace(String.format(
"PassA-RAISE (%d,%d) h=%.2f → %.2f (terrainR1=%.2f)",
colX2b, colZ2b, h0sm, minRaiseH, passA_maxThAbove));
} else {
log.trace(String.format(
"PassA-UNTEN (%d,%d) h0sm=%.3f h0raw=%.3f terrainR1=%.3f Terrain zu weit oben (>MAX_TERRAIN_DIFF), kein Kandidat",
colX2b, colZ2b, h0sm, h0raw, passA_maxThAbove));
} }
} }
if (lowDist < 0) continue; // kein niedrigerer Nachbar im Suchradius // ── Pass B: Voxel-zu-Voxel ────────────────────────────────────────
// Nur Terrassen-Zellen (h0raw ≤ 4.25m) Bergterrain wird nicht angefasst.
// Pass B als if-Block (kein continue): smoothMap.put() am Ende bleibt für
// Pass-A-Ergebnisse aller Zellen erreichbar.
// Cascade-Schutz: alle Vergleiche ausschließlich mit RAW-Höhen (hN, h0raw),
// nie mit smoothMap gerampte Kliffränder behalten raw ihre Originalhöhe
// und werden nicht fälschlicherweise als niedrigere Nachbarn erkannt.
if (h0raw <= 4.25f) {
boolean hadAnyLower = false;
boolean hadNonVoxelR1R3 = false;
voxelPassB:
for (int r = 1; r <= MAX_RAMP_R; r++) {
for (int dz2b = -r; dz2b <= r; dz2b++) {
for (int dx2b = -r; dx2b <= r; dx2b++) {
if (Math.max(Math.abs(dx2b), Math.abs(dz2b)) != r) continue;
long hkN = (long)(colX2b + dx2b + 30000) * 60001L
+ (colZ2b + dz2b + 30000);
Float hN = hfMap.get(hkN);
if (hN == null) {
if (r <= 3) { hadNonVoxelR1R3 = true; }
continue;
}
if (hN >= h0raw) continue;
if ((h0raw - hN) >= MAX_VOXEL_DIFF) continue;
hadAnyLower = true;
if ((h0raw - hN) > (float) r) continue;
float candidate = hN + r / RAMP_RATIO;
if (candidate < minRampH) {
minRampH = candidate;
}
}
}
// Nach Ring-3: kein gültiger niedrigerer Voxel + kein Terrain → Plateau-Innenzelle
if (r == 3 && !hadAnyLower && !hadNonVoxelR1R3) {
break voxelPassB;
}
if (hadAnyLower) {
break voxelPassB;
}
}
}
float avgLowH = sumLowH / lowCount; if (minRampH < h0sm) {
float rampH = avgLowH + lowDist / RAMP_RATIO; if (log.isTraceEnabled()) {
if (rampH < h0sm) { log.trace(String.format("Schritt2b (%d,%d) h=%.2f → %.2f",
smoothMap.put(hk0, rampH); colX2b, colZ2b, h0sm, minRampH));
}
smoothMap.put(hk0, minRampH);
} else if (minRaiseH > h0sm && minRaiseH < Float.POSITIVE_INFINITY) {
// Voxel lag unter dem Terrain → auf Terrain-Niveau anheben
if (log.isTraceEnabled()) {
log.trace(String.format("Schritt2b-RAISE (%d,%d) h=%.2f → %.2f",
colX2b, colZ2b, h0sm, minRaiseH));
}
smoothMap.put(hk0, minRaiseH);
} }
} }
} }

View File

@@ -26,6 +26,7 @@
<!-- Material warnt bei linear-color-space Texturen ohne passenden Parameter bekannt, kein Fehler --> <!-- Material warnt bei linear-color-space Texturen ohne passenden Parameter bekannt, kein Fehler -->
<logger name="com.jme3.material.Material" level="ERROR"/> <logger name="com.jme3.material.Material" level="ERROR"/>
<logger name="de.blight.game.animation.FootIKControl" level="DEBUG"/> <logger name="de.blight.game.animation.FootIKControl" level="DEBUG"/>
<logger name="de.blight.editor.state.VoxelEditorState" level="TRACE"/>
<root level="INFO"> <root level="INFO">
<appender-ref ref="CONSOLE"/> <appender-ref ref="CONSOLE"/>