Weiter am Voxel/Gebirgsterrain gearbeitet
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user