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. // auch wenn Overlay-Nodes (Tab-Button, Asset-Panel) das Event abfangen würden.
pane.addEventFilter(javafx.scene.input.MouseEvent.MOUSE_PRESSED, e -> { pane.addEventFilter(javafx.scene.input.MouseEvent.MOUSE_PRESSED, e -> {
if (e.getButton() == MouseButton.MIDDLE 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(); prevDragX = e.getX(); prevDragY = e.getY();
} }
}); });
pane.addEventFilter(javafx.scene.input.MouseEvent.MOUSE_DRAGGED, e -> { pane.addEventFilter(javafx.scene.input.MouseEvent.MOUSE_DRAGGED, e -> {
boolean bothDown = e.isPrimaryButtonDown() && e.isSecondaryButtonDown(); 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 dx = e.getX() - prevDragX;
double dy = e.getY() - prevDragY; double dy = e.getY() - prevDragY;
input.addMouseDelta((int) dx, (int) dy); input.addMouseDelta((int) dx, (int) dy);
@@ -7730,7 +7732,7 @@ public class EditorApp extends Application {
} }
} else if (bothDown) { } else if (bothDown) {
stopEditTimer(); stopEditTimer();
} else if (e.getButton() == MouseButton.PRIMARY) { } else if (e.getButton() == MouseButton.PRIMARY && !e.isAltDown()) {
if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL) { if (input.activeLayer == SharedInput.LAYER_PLAY_TOOL) {
// Einzel-Klick ohne Edit-Timer (verhindert Dauer-Spam) // Einzel-Klick ohne Edit-Timer (verhindert Dauer-Spam)
input.playToolClickQueue.offer( input.playToolClickQueue.offer(
@@ -7750,7 +7752,7 @@ public class EditorApp extends Application {
viewport.setOnMouseDragged(e -> { viewport.setOnMouseDragged(e -> {
if (isObjectMode()) { if (isObjectMode()) {
if (objDragging && e.isPrimaryButtonDown() if (objDragging && e.isPrimaryButtonDown()
&& !e.isSecondaryButtonDown() && !e.isMiddleButtonDown()) { && !e.isSecondaryButtonDown() && !e.isMiddleButtonDown() && !e.isAltDown()) {
float dx = (float)(e.getX() - objDragPrevX); float dx = (float)(e.getX() - objDragPrevX);
float dy = (float)(e.getY() - objDragPrevY); float dy = (float)(e.getY() - objDragPrevY);
input.objectDragQueue.offer(new SharedInput.ObjectDrag(dx, dy)); input.objectDragQueue.offer(new SharedInput.ObjectDrag(dx, dy));

View File

@@ -596,9 +596,8 @@ public class VoxelEditorState extends BaseAppState {
Vector3f bestPos = null; Vector3f bestPos = null;
Vector3f bestNorm = new Vector3f(0, 1, 0); Vector3f bestNorm = new Vector3f(0, 1, 0);
// Im Voxel-Layer nur Voxel-Geometrie und die Basis-Referenzebene treffen, // Terrain immer treffen (auch im Voxel-Layer); Voxel/Baked-Meshes gewinnen per bestDist.
// nicht das Heightmap-Terrain (das würde Voxel auf der falschen Höhe erzeugen). if (terrainNode != null) {
if (terrainNode != null && input.activeLayer != SharedInput.LAYER_VOXEL) {
terrainNode.collideWith(ray, results); terrainNode.collideWith(ray, results);
if (results.size() > 0) { if (results.size() > 0) {
CollisionResult cr = results.getClosestCollision(); CollisionResult cr = results.getClosestCollision();
@@ -695,8 +694,6 @@ public class VoxelEditorState extends BaseAppState {
input.markedDeleteBakedKeys.clear(); input.markedDeleteBakedKeys.clear();
input.markedBakedSelectionDirty = true; input.markedBakedSelectionDirty = true;
} }
input.voxelTool.mode.setSelectedIndex(de.blight.editor.tool.VoxelTool.MODE_SINUS);
input.voxelTool.modeChanged = true;
return; return;
} }
@@ -714,18 +711,14 @@ public class VoxelEditorState extends BaseAppState {
if (nhLen < 0.1f) return; 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) { if (!isHorizontal && isColumn && modeIdx == de.blight.editor.tool.VoxelTool.MODE_PLATEAU && lower) {
float h = columnTopWorldY(wx, wz); if (Float.isFinite(wy)) {
TerrainEditorState tes = getStateManager().getState(TerrainEditorState.class); input.voxelTool.plateauTarget.setValue(wy);
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);
input.voxelTool.plateauTargetChanged = true; input.voxelTool.plateauTargetChanged = true;
input.heightTool.plateauHeight.setValue(h); input.heightTool.plateauHeight.setValue(wy);
input.heightTool.plateauHeightChanged = true; input.heightTool.plateauHeightChanged = true;
} }
return; return;
@@ -752,7 +745,8 @@ public class VoxelEditorState extends BaseAppState {
// Wenn der Nutzer auf die Basis-Ebene (y=-10) klickt, liegt der Terrain-Ankerpunkt // 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. // (terrainH ≥ 0) in einem anderen cy als der Hit-Punkt → Voxel würden sonst nie gesetzt.
if (isColumn && !isHorizontal && !isCave) { 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); int anchorCy = VoxelChunk.worldYToCy(anchorY);
cyMin = Math.min(cyMin, anchorCy); cyMin = Math.min(cyMin, anchorCy);
cyMax = Math.max(cyMax, 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); applyColumnToTarget(chunk, cx, cy, cz, wx, wz, radius, strength, coord -> target);
} else if (modeIdx == de.blight.editor.tool.VoxelTool.MODE_SMOOTH) { } else if (modeIdx == de.blight.editor.tool.VoxelTool.MODE_SMOOTH) {
if (lower) { if (lower) {
// RMB: Durchschnitt-Smooth (wie vormals LMB) // RMB: Durchschnitt-Smooth
applySmoothColumn(chunk, cx, cy, cz, wx, wz, radius, strength, slopeParams); applySmoothColumn(chunk, cx, cy, cz, wx, wz, radius, strength, slopeParams);
} else { } 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); applySlopeColumn(chunk, cx, cy, cz, wx, wz, radius, strength, slopeParams);
} }
} else { } else {
@@ -915,8 +909,9 @@ public class VoxelEditorState extends BaseAppState {
int z1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lzC + radius)); int z1 = Math.min(VoxelChunk.SIZE - 1, (int) Math.ceil(lzC + radius));
float r2 = radius * radius; float r2 = radius * radius;
// Schritt pro Event: strength bestimmt die Wachstumsgeschwindigkeit // Mindestens 6 Voxel am Zentrum, damit die Falloff-Kurve diskret sichtbar wird.
float stepBase = Math.max(1f, strength / 8f); // 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++) { for (int lz = z0; lz <= z1; lz++) {
float wz = VoxelChunk.toWorldZ(cz, lz); float wz = VoxelChunk.toWorldZ(cz, lz);
@@ -929,8 +924,11 @@ public class VoxelEditorState extends BaseAppState {
float t = (float) Math.sqrt(d2) / radius; float t = (float) Math.sqrt(d2) / radius;
float falloff = computeFalloff(mode, t); float falloff = computeFalloff(mode, t);
// Mindestens 1, damit Kanten-Spalten nicht komplett übersprungen werden // Spalten mit vernachlässigbarem falloff überspringen (erzeugt Profil-Form statt Plateau).
int colStep = Math.max(1, (int)(stepBase * falloff)); // 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) { if (!lower) {
// Höchsten Solid-Voxel in dieser Spalte suchen // 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 (chunk.getDensity(lx, ly, lz) > 0) { currentTop = ly; break; }
} }
if (currentTop < 0) { if (currentTop < 0) {
// Ankerpunkt: Terrain-Oberfläche ODER gebackenes Terrain (brushHitWY), // Ankerpunkt: Terrain-Oberfläche ODER Klickpunkt (brushHitWY).
// je nachdem was höher liegt. So starten neue Voxel sichtbar über // Ohne Terrain-Mesh: direkt brushHitWY nutzen (terrainH liefert nur 0 als Fallback).
// bestehendem gebackenem Terrain statt darunter.
float th = terrainH(wx, wz); float th = terrainH(wx, wz);
float anchor = Math.max(th, brushHitWY); float anchor = hasTerrainMesh() ? Math.max(th, brushHitWY) : brushHitWY;
int aCy = VoxelChunk.worldYToCy(anchor); int aCy = VoxelChunk.worldYToCy(anchor);
if (cy == aCy) { if (cy == aCy) {
currentTop = Math.max(0, Math.min(VoxelChunk.SIZE - 1, currentTop = Math.max(0, Math.min(VoxelChunk.SIZE - 1,
@@ -1115,6 +1112,10 @@ public class VoxelEditorState extends BaseAppState {
return 0f; return 0f;
} }
private boolean hasTerrainMesh() {
return terrainEditorState != null || terrainQuad != null;
}
// ── Smooth-Modus ────────────────────────────────────────────────────────── // ── Smooth-Modus ──────────────────────────────────────────────────────────
/** /**
@@ -1298,19 +1299,76 @@ public class VoxelEditorState extends BaseAppState {
} }
/** /**
* Smooth-Pinsel (Linksklick): bewegt alle Spalten im Pinselbereich auf die * Smooth-Pinsel (RMB): exakter Port von TerrainEditorState.smoothHeight().
* Durchschnittshöhe aller Spalten zu (sp[6]). * 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
* Spikes (Spalten über dem Durchschnitt) werden abgebaut, tiefe Stellen * würden auf 0 runden → Tool tut nichts. 10× stärker = gleiches Verhalten.
* leicht angehoben. Dadurch entsteht eine gleichmäßig geebnte Fläche, * delta = (avg - top) * blend — proportionale Bewegung zur Durchschnittshöhe.
* ohne dass neues Material an Stellen aufgebaut wird, die bereits flach sind. * Für leere Spalten wird terrainH als Baseline verwendet (gleich wie computeSlopeParams).
*/ */
private void applySmoothColumn(VoxelChunk chunk, int cx, int cy, int cz, private void applySmoothColumn(VoxelChunk chunk, int cx, int cy, int cz,
float brushWX, float brushWZ, float brushWX, float brushWZ,
float radius, float strength, float[] sp) { float radius, float strength, float[] sp) {
if (sp == null || Float.isNaN(sp[6])) return; if (sp == null || Float.isNaN(sp[6])) return;
final float targetH = sp[6]; final float avgH = sp[6];
applyColumnToTarget(chunk, cx, cy, cz, brushWX, brushWZ, radius, strength, coord -> targetH);
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 * Slope-Pinsel (LMB): port von TerrainEditorState.slopeHeight().
* zum geometrisch gegenüberliegenden Punkt. * 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, private void applySlopeColumn(VoxelChunk chunk, int cx, int cy, int cz,
float brushWX, float brushWZ, float brushWX, float brushWZ,
@@ -1362,11 +1421,67 @@ public class VoxelEditorState extends BaseAppState {
final float projRange = projHigh - projOpp; final float projRange = projHigh - projOpp;
if (projRange < 0.5f) return; if (projRange < 0.5f) return;
applyColumnToTarget(chunk, cx, cy, cz, brushWX, brushWZ, radius, strength, coord -> { float lxC = VoxelChunk.worldXToLocal(brushWX, cx);
float proj = coord[0] * dirX + coord[1] * dirZ; float lzC = VoxelChunk.worldZToLocal(brushWZ, cz);
float t = Math.max(0f, Math.min(1f, (proj - projOpp) / projRange)); int x0 = Math.max(0, (int)(lxC - radius));
return oppH + t * (highH - oppH); 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 ──────────────────────────────────────────────────── // ── Intern: Voxel-Bake ────────────────────────────────────────────────────
@@ -1485,54 +1600,142 @@ public class VoxelEditorState extends BaseAppState {
input.blurIterDone = iter + 1; input.blurIterDone = iter + 1;
} }
// ── Flachbereich-Glättung: 3 reine Gauß-Passes nur nahe der Isofläche ────── // ── Neigungsadaptive Oberflächenverarbeitung (Regressions-Ansatz) ────────
// 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 // Warum XZ-Blur nicht funktioniert:
// bilaterale Gewicht auf ~0 senkt. Auf flachen Flächen entstehen dadurch Stufen. // XZ-Blur verschiebt Dichten lateral, aber die Marching-Cubes-Isofläche
// Diese reinen Gauß-Passes (keine bilaterale Gewichtung) glätten gezielt Voxel, // liegt immer zwischen zwei Voxeln, wo einer positiv und einer negativ ist.
// die (a) nahe der Oberfläche liegen und (b) eine überwiegend vertikale Normale haben. // 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 TAN_22_5 = 0.414f; // tan(22,5°)
final float COS_30 = 0.866f; // cos(30°) Grenze "flache Fläche" final float TAN_45 = 1.000f; // tan(45°)
for (int flatPass = 0; flatPass < 3; flatPass++) { 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<>(); Map<Long, float[]> nextBufs = new HashMap<>();
for (VoxelChunk c : nonEmpty) { for (VoxelChunk c : nonEmpty) {
long k = chunkKey(c.cx, c.cy, c.cz); long k = chunkKey(c.cx, c.cy, c.cz);
float[] cur = curBufs.get(k); float[] cur = curBufs.get(k);
float[] next = Arrays.copyOf(cur, cur.length); float[] next = Arrays.copyOf(cur, cur.length);
for (int by = 0; by < blurN; by++) { int wiyBase = c.cy * C;
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;
// Gradient über zentrale Differenzen for (int bz = 0; bz < C; bz++) {
float gx = getBlurBuf(curBufs, allOriginal, c, bx+1, by, bz) int colZ = c.cz * C + bz;
- getBlurBuf(curBufs, allOriginal, c, bx-1, by, bz); for (int bx = 0; bx < C; bx++) {
float gy = getBlurBuf(curBufs, allOriginal, c, bx, by+1, bz) int colX = c.cx * C + bx;
- getBlurBuf(curBufs, allOriginal, c, bx, by-1, bz); long hk0 = (long)(colX + 30000) * 60001L + (colZ + 30000);
float gz = getBlurBuf(curBufs, allOriginal, c, bx, by, bz+1) Integer h0 = hfMap.get(hk0);
- getBlurBuf(curBufs, allOriginal, c, bx, by, bz-1); float[] reg = regMap.get(hk0);
float gLen = (float) Math.sqrt(gx*gx + gy*gy + gz*gz); if (h0 == null || reg == null) continue;
if (gLen < 1f) continue;
float normY = Math.abs(gy) / gLen;
if (normY < COS_30) continue; // steile Fläche → nicht glätten
float vSum = 0f; float smoothH = reg[0];
int cnt = 0; float totalSlope = reg[1];
for (int dy = -1; dy <= 1; dy++) float diff = smoothH - h0;
for (int dz = -1; dz <= 1; dz++)
for (int dx = -1; dx <= 1; dx++) { if (totalSlope >= MIN_SLOPE && totalSlope < TAN_22_5
int sx = bx+dx, sy = by+dy, sz = bz+dz; && Math.abs(diff) <= 0.6f) {
float nb; // Sanfte Steigung: sub-voxel-genauen Dichte-Gradienten schreiben.
if (sx >= 0 && sx < blurN && sy >= 0 && sy < blurN && sz >= 0 && sz < blurN) // MC interpoliert die Isofläche dann auf die genaue smoothH-Höhe.
nb = cur[c.idx(sx, sy, sz)]; for (int by = 0; by < blurN; by++) {
else float d = (smoothH - (wiyBase + by)) * 127f;
nb = getBlurBuf(curBufs, allOriginal, c, sx, sy, sz); next[c.idx(bx, by, bz)] = Math.max(-128f, Math.min(127f, d));
vSum += nb; }
cnt++; } else if (totalSlope > TAN_45) {
} // Steile Klippe: Noise für organische Optik
next[c.idx(bx, by, bz)] = vSum / cnt; 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; 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 // Blur-Ergebnisse in VoxelChunks umwandeln
@@ -1906,8 +2131,10 @@ public class VoxelEditorState extends BaseAppState {
private float computeFalloff(int mode, float t) { private float computeFalloff(int mode, float t) {
return switch (mode) { return switch (mode) {
case de.blight.editor.tool.VoxelTool.MODE_SINUS -> (float) Math.cos(t * Math.PI / 2); // identisch mit dem Terrain-Tool: (1+cos(π·t))/2 → weiches Glöckchen
case de.blight.editor.tool.VoxelTool.MODE_SPIKE -> (1f - t) * (1f - t); 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))); case de.blight.editor.tool.VoxelTool.MODE_SMOOTH -> (float)(0.5 * (1 + Math.cos(t * Math.PI)));
default -> 1f; // PLATEAU default -> 1f; // PLATEAU
}; };

View File

@@ -4,4 +4,4 @@ Models/trees/pine/medium/pine_medium_20260706_190947.j3o -6.42357 1.22971 -1318.
Models/trees/pine/medium/pine_medium_20260706_190953.j3o -17.84356 1.24567 -1316.45410 0.26793 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 Models/trees/pine/medium/pine_medium_20260706_190953.j3o -17.84356 1.24567 -1316.45410 0.26793 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/plants/misc/kaktusfeige.j3o -6.18754 1.22995 -1320.34875 0.00000 2.50000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 Models/plants/misc/kaktusfeige.j3o -6.18754 1.22995 -1320.34875 0.00000 2.50000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000
Models/imported/alter_steg.j3o -8.42317 -0.11015 -1359.15662 3.14159 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 Models/imported/alter_steg.j3o -8.42317 -0.11015 -1359.15662 3.14159 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000
Models/trees/grapevine/grapevine_20260725_195635.j3o -85.78869 1.32939 -1297.17883 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 Models/trees/grapevine/grapevine_20260725_195635.j3o -85.78869 1.32641 -1297.17883 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.