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 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 ────────────────────────────────────
|
||||
{
|
||||
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++) {
|
||||
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);
|
||||
int wiyBase = c.cy * C;
|
||||
|
||||
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 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++) {
|
||||
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
|
||||
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
|
||||
|
||||
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++;
|
||||
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));
|
||||
}
|
||||
next[c.idx(bx, by, bz)] = vSum / cnt;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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/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/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.
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.
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_13_0_06.blvc.prebake
Normal file
BIN
blight-map/src/main/map/chunks/voxel_13_0_06.blvc.prebake
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_13_0_09.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_13_0_09.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_14_0_05.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_14_0_05.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_14_0_06.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_14_0_06.blvc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_14_0_07.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_14_0_07.blvc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_15_0_05.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_15_0_05.blvc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_15_0_07.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_15_0_07.blvc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_15_0_08.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_15_0_08.blvc
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_16_0_06.blvc.prebake
Normal file
BIN
blight-map/src/main/map/chunks/voxel_16_0_06.blvc.prebake
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_16_0_07.blvc.prebake
Normal file
BIN
blight-map/src/main/map/chunks/voxel_16_0_07.blvc.prebake
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_16_0_08.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_16_0_08.blvc
Normal file
Binary file not shown.
Reference in New Issue
Block a user