Compare commits
4 Commits
936efb7bcc
...
accdd2fa75
| Author | SHA1 | Date | |
|---|---|---|---|
| accdd2fa75 | |||
| 0a657a5831 | |||
| 46be9ffe57 | |||
| 00a7ccbe47 |
@@ -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.
|
||||||
|
|||||||
@@ -3582,6 +3582,27 @@ public class EditorApp extends Application {
|
|||||||
bakeBarLabel.setVisible(false);
|
bakeBarLabel.setVisible(false);
|
||||||
bakeBarLabel.setManaged(false);
|
bakeBarLabel.setManaged(false);
|
||||||
|
|
||||||
|
// ── Bake-Parameter ────────────────────────────────────────────────
|
||||||
|
Label smoothLbl = new Label(String.format("Glättung: %.1f", input.bakeSmoothStrength));
|
||||||
|
smoothLbl.setStyle("-fx-font-size: 11;");
|
||||||
|
javafx.scene.control.Slider smoothSlider =
|
||||||
|
new javafx.scene.control.Slider(0, 1, input.bakeSmoothStrength);
|
||||||
|
smoothSlider.setShowTickMarks(false);
|
||||||
|
smoothSlider.valueProperty().addListener((obs, o, n) -> {
|
||||||
|
input.bakeSmoothStrength = n.floatValue();
|
||||||
|
smoothLbl.setText(String.format("Glättung: %.1f", n.floatValue()));
|
||||||
|
});
|
||||||
|
|
||||||
|
Label cliffLbl = new Label(String.format("Klippen-Noise: %.1f", input.bakeCliffNoiseStrength));
|
||||||
|
cliffLbl.setStyle("-fx-font-size: 11;");
|
||||||
|
javafx.scene.control.Slider cliffSlider =
|
||||||
|
new javafx.scene.control.Slider(0, 1, input.bakeCliffNoiseStrength);
|
||||||
|
cliffSlider.setShowTickMarks(false);
|
||||||
|
cliffSlider.valueProperty().addListener((obs, o, n) -> {
|
||||||
|
input.bakeCliffNoiseStrength = n.floatValue();
|
||||||
|
cliffLbl.setText(String.format("Klippen-Noise: %.1f", n.floatValue()));
|
||||||
|
});
|
||||||
|
|
||||||
Button bakeBtn = new Button("Voxels backen (J3O)");
|
Button bakeBtn = new Button("Voxels backen (J3O)");
|
||||||
bakeBtn.setMaxWidth(Double.MAX_VALUE);
|
bakeBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
bakeBtn.setStyle("-fx-background-color: #4a7eba; -fx-text-fill: white;");
|
bakeBtn.setStyle("-fx-background-color: #4a7eba; -fx-text-fill: white;");
|
||||||
@@ -3697,8 +3718,9 @@ public class EditorApp extends Application {
|
|||||||
cleanupPoller.setCycleCount(javafx.animation.Animation.INDEFINITE);
|
cleanupPoller.setCycleCount(javafx.animation.Animation.INDEFINITE);
|
||||||
cleanupPoller.play();
|
cleanupPoller.play();
|
||||||
|
|
||||||
panel.getChildren().addAll(new Separator(), bakeBtn,
|
panel.getChildren().addAll(new Separator(),
|
||||||
bakeBar, bakeBarLabel, bakeStatus,
|
smoothLbl, smoothSlider, cliffLbl, cliffSlider,
|
||||||
|
bakeBtn, bakeBar, bakeBarLabel, bakeStatus,
|
||||||
new Separator(), cleanupBtn, cleanupStatus,
|
new Separator(), cleanupBtn, cleanupStatus,
|
||||||
new Separator(), selInfoLabel, selHintLabel, deleteBtn, revertBtn);
|
new Separator(), selInfoLabel, selHintLabel, deleteBtn, revertBtn);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -778,7 +778,11 @@ public class SharedInput {
|
|||||||
public volatile boolean voxelRedoRequested = false;
|
public volatile boolean voxelRedoRequested = false;
|
||||||
|
|
||||||
/** JFX → JME: alle Voxel-Chunks als geglättete J3O-Meshes backen. */
|
/** JFX → JME: alle Voxel-Chunks als geglättete J3O-Meshes backen. */
|
||||||
public volatile boolean bakeVoxelsRequested = false;
|
public volatile boolean bakeVoxelsRequested = false;
|
||||||
|
/** JFX → JME: Stärke des Post-Bake-Laplacian-Smooth (0 = kein Extra-Pass, 1 = stark). */
|
||||||
|
public volatile float bakeSmoothStrength = 0.3f;
|
||||||
|
/** JFX → JME: Amplitude der horizontalen Klippen-Noise-Verschiebung (0 = kein, 1 = ~2 m). */
|
||||||
|
public volatile float bakeCliffNoiseStrength = 0.2f;
|
||||||
/** JME → JFX: Anzahl bereits gebackener Chunks (0 = nicht gestartet). */
|
/** JME → JFX: Anzahl bereits gebackener Chunks (0 = nicht gestartet). */
|
||||||
public volatile int bakeDone = 0;
|
public volatile int bakeDone = 0;
|
||||||
/** JME → JFX: Gesamtzahl der zu backenden Chunks (0 = nicht gestartet). */
|
/** JME → JFX: Gesamtzahl der zu backenden Chunks (0 = nicht gestartet). */
|
||||||
|
|||||||
@@ -218,6 +218,8 @@ public class GrassVertexState extends BaseAppState {
|
|||||||
float jmeY = (float) (edit.screenY() * input.viewportScaleY);
|
float jmeY = (float) (edit.screenY() * input.viewportScaleY);
|
||||||
Vector3f hit = raycastSurface(jmeX, jmeY, getApplication().getCamera());
|
Vector3f hit = raycastSurface(jmeX, jmeY, getApplication().getCamera());
|
||||||
if (hit == null) continue;
|
if (hit == null) continue;
|
||||||
|
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||||
|
if (ves != null && ves.terrainTypeAt(hit.x, hit.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) continue;
|
||||||
if (edit.action() > 0) addBlades(hit);
|
if (edit.action() > 0) addBlades(hit);
|
||||||
else removeBlades(hit);
|
else removeBlades(hit);
|
||||||
}
|
}
|
||||||
@@ -294,14 +296,22 @@ public class GrassVertexState extends BaseAppState {
|
|||||||
if (changed) dirtyChunks[ci] = true;
|
if (changed) dirtyChunks[ci] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||||
|
|
||||||
// Neue Halme mit Falloff und Gleichmäßigkeits-Variation setzen
|
// Neue Halme mit Falloff und Gleichmäßigkeits-Variation setzen
|
||||||
for (int i = 0; i < density; i++) {
|
for (int i = 0; i < density; i++) {
|
||||||
float angle = rng.nextFloat() * (float) (Math.PI * 2);
|
float angle = rng.nextFloat() * (float) (Math.PI * 2);
|
||||||
float r = rng.nextFloat() * radius;
|
float r = rng.nextFloat() * radius;
|
||||||
float bx = center.x + (float) Math.cos(angle) * r;
|
float bx = center.x + (float) Math.cos(angle) * r;
|
||||||
float bz = center.z + (float) Math.sin(angle) * r;
|
float bz = center.z + (float) Math.sin(angle) * r;
|
||||||
float by = terrain.getHeight(new Vector2f(bx, bz));
|
float by;
|
||||||
if (Float.isNaN(by)) continue;
|
if (ves != null) {
|
||||||
|
by = ves.heightAt(bx, bz);
|
||||||
|
if (ves.isTooSteep(bx, bz)) continue;
|
||||||
|
} else {
|
||||||
|
by = terrain.getHeight(new Vector2f(bx, bz));
|
||||||
|
if (Float.isNaN(by)) continue;
|
||||||
|
}
|
||||||
float distRatio = r / radius;
|
float distRatio = r / radius;
|
||||||
float h = height * brushFalloff(distRatio)
|
float h = height * brushFalloff(distRatio)
|
||||||
* (1f + variation * (rng.nextFloat() * 2f - 1f));
|
* (1f + variation * (rng.nextFloat() * 2f - 1f));
|
||||||
|
|||||||
@@ -255,6 +255,8 @@ public class PlacedObjectState extends BaseAppState {
|
|||||||
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
Ray ray = new Ray(near, far.subtract(near).normalizeLocal());
|
||||||
Vector3f contact = raycastSurface(ray);
|
Vector3f contact = raycastSurface(ray);
|
||||||
if (contact == null) continue;
|
if (contact == null) continue;
|
||||||
|
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||||
|
if (ves != null && ves.terrainTypeAt(contact.x, contact.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) continue;
|
||||||
float radius = (float) input.grassTool.brushRadius.getValue();
|
float radius = (float) input.grassTool.brushRadius.getValue();
|
||||||
if (edit.action() > 0) paintGrass(contact.x, contact.z, radius);
|
if (edit.action() > 0) paintGrass(contact.x, contact.z, radius);
|
||||||
else eraseGrass(contact.x, contact.z, radius);
|
else eraseGrass(contact.x, contact.z, radius);
|
||||||
@@ -292,6 +294,7 @@ public class PlacedObjectState extends BaseAppState {
|
|||||||
float baseH = (float) input.grassTool.grassHeight.getValue();
|
float baseH = (float) input.grassTool.grassHeight.getValue();
|
||||||
int slot = input.grassActiveSlot;
|
int slot = input.grassActiveSlot;
|
||||||
Random rng = new Random();
|
Random rng = new Random();
|
||||||
|
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||||
for (int i = 0; i < n; i++) {
|
for (int i = 0; i < n; i++) {
|
||||||
float angle = rng.nextFloat() * FastMath.TWO_PI;
|
float angle = rng.nextFloat() * FastMath.TWO_PI;
|
||||||
float dist = FastMath.sqrt(rng.nextFloat()) * radius;
|
float dist = FastMath.sqrt(rng.nextFloat()) * radius;
|
||||||
@@ -299,6 +302,7 @@ public class PlacedObjectState extends BaseAppState {
|
|||||||
float bz = cz + dist * FastMath.sin(angle);
|
float bz = cz + dist * FastMath.sin(angle);
|
||||||
if (bx < -TERRAIN_HALF || bx > TERRAIN_HALF
|
if (bx < -TERRAIN_HALF || bx > TERRAIN_HALF
|
||||||
|| bz < -TERRAIN_HALF || bz > TERRAIN_HALF) continue;
|
|| bz < -TERRAIN_HALF || bz > TERRAIN_HALF) continue;
|
||||||
|
if (ves != null && ves.isTooSteep(bx, bz)) continue;
|
||||||
float h = baseH * (0.7f + rng.nextFloat() * 0.6f);
|
float h = baseH * (0.7f + rng.nextFloat() * 0.6f);
|
||||||
int ci = chunkIndex(bx, bz);
|
int ci = chunkIndex(bx, bz);
|
||||||
if (ci >= 0) {
|
if (ci >= 0) {
|
||||||
@@ -356,6 +360,8 @@ public class PlacedObjectState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
if (chunkTufts[idx].isEmpty()) return;
|
if (chunkTufts[idx].isEmpty()) return;
|
||||||
|
|
||||||
|
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||||
|
|
||||||
Map<Integer, List<float[]>> bySlot = new LinkedHashMap<>();
|
Map<Integer, List<float[]>> bySlot = new LinkedHashMap<>();
|
||||||
for (GrassTuft t : chunkTufts[idx]) {
|
for (GrassTuft t : chunkTufts[idx]) {
|
||||||
long seed = (long) Float.floatToRawIntBits(t.x()) * 0x9E3779B9L
|
long seed = (long) Float.floatToRawIntBits(t.x()) * 0x9E3779B9L
|
||||||
@@ -367,8 +373,13 @@ public class PlacedObjectState extends BaseAppState {
|
|||||||
float oz = (rng.nextFloat() - 0.5f) * TUFT_SPREAD * 2f;
|
float oz = (rng.nextFloat() - 0.5f) * TUFT_SPREAD * 2f;
|
||||||
float bx = t.x() + ox;
|
float bx = t.x() + ox;
|
||||||
float bz = t.z() + oz;
|
float bz = t.z() + oz;
|
||||||
float th = terrain.getHeight(new Vector2f(bx, bz));
|
float th;
|
||||||
if (Float.isNaN(th)) continue;
|
if (ves != null) {
|
||||||
|
th = ves.heightAt(bx, bz);
|
||||||
|
} else {
|
||||||
|
th = terrain.getHeight(new Vector2f(bx, bz));
|
||||||
|
if (Float.isNaN(th)) continue;
|
||||||
|
}
|
||||||
float bh = t.height() * (0.7f + rng.nextFloat() * 0.6f);
|
float bh = t.height() * (0.7f + rng.nextFloat() * 0.6f);
|
||||||
blades.add(new float[]{bx, th, bz, bh});
|
blades.add(new float[]{bx, th, bz, bh});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1392,11 +1392,19 @@ public class TerrainEditorState extends BaseAppState {
|
|||||||
contactPoint = raycastSurface(ray);
|
contactPoint = raycastSurface(ray);
|
||||||
if (contactPoint != null) {
|
if (contactPoint != null) {
|
||||||
brushRadius = (float) input.grassTool.brushRadius.getValue();
|
brushRadius = (float) input.grassTool.brushRadius.getValue();
|
||||||
|
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||||
|
if (ves != null && ves.terrainTypeAt(contactPoint.x, contactPoint.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) {
|
||||||
|
contactPoint = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (layer == SharedInput.LAYER_GRASS_VERTEX) {
|
} else if (layer == SharedInput.LAYER_GRASS_VERTEX) {
|
||||||
contactPoint = raycastSurface(ray);
|
contactPoint = raycastSurface(ray);
|
||||||
if (contactPoint != null) {
|
if (contactPoint != null) {
|
||||||
brushRadius = (float) input.grassVertexTool.brushRadius.getValue();
|
brushRadius = (float) input.grassVertexTool.brushRadius.getValue();
|
||||||
|
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||||
|
if (ves != null && ves.terrainTypeAt(contactPoint.x, contactPoint.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) {
|
||||||
|
contactPoint = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1154,6 +1154,87 @@ public class VoxelEditorState extends BaseAppState {
|
|||||||
return 0f;
|
return 0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Zentrale Terrain-Abfragen ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Terrain-Typ an einer Weltposition. */
|
||||||
|
public enum TerrainType { BASE, VOXEL_BAKED, VOXEL_UNBAKED }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liefert den Terrain-Typ an einer Weltposition.
|
||||||
|
* BASE = nur Basis-Terrain; VOXEL_BAKED = mind. ein gebackener Chunk in der Spalte;
|
||||||
|
* VOXEL_UNBAKED = Voxel-Daten vorhanden, aber kein Bake-Ergebnis auf der Festplatte.
|
||||||
|
*/
|
||||||
|
public TerrainType terrainTypeAt(float worldX, float worldZ) {
|
||||||
|
int cx = VoxelChunk.worldXToCx(worldX);
|
||||||
|
int cz = VoxelChunk.worldZToCz(worldZ);
|
||||||
|
boolean hasVoxel = false;
|
||||||
|
boolean hasBaked = false;
|
||||||
|
for (int cy = -2; cy <= 10; cy++) {
|
||||||
|
VoxelChunk chunk = chunks.get(chunkKey(cx, cy, cz));
|
||||||
|
if (chunk != null && !chunk.isEmpty()) {
|
||||||
|
hasVoxel = true;
|
||||||
|
if (VoxelChunkIO.bakedExists(cx, cy, cz)) {
|
||||||
|
hasBaked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasVoxel) return TerrainType.BASE;
|
||||||
|
return hasBaked ? TerrainType.VOXEL_BAKED : TerrainType.VOXEL_UNBAKED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nächster Oberflächentreffer für den gegebenen Ray (Terrain + Voxel-Geo + Baked Sculpt).
|
||||||
|
* Zentrale Alternative zu den lokalen raycastSurface()-Methoden in den einzelnen States.
|
||||||
|
*/
|
||||||
|
public Vector3f clickAt(Ray ray) {
|
||||||
|
Vector3f best = null;
|
||||||
|
float bestDistSq = Float.MAX_VALUE;
|
||||||
|
|
||||||
|
if (terrainNode != null) {
|
||||||
|
CollisionResults hits = new CollisionResults();
|
||||||
|
terrainNode.collideWith(ray, hits);
|
||||||
|
if (hits.size() > 0) {
|
||||||
|
best = hits.getClosestCollision().getContactPoint();
|
||||||
|
bestDistSq = ray.getOrigin().distanceSquared(best);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3f vp = raycastVoxelGeometry(ray);
|
||||||
|
if (vp != null) {
|
||||||
|
float d = ray.getOrigin().distanceSquared(vp);
|
||||||
|
if (d < bestDistSq) { best = vp; bestDistSq = d; }
|
||||||
|
}
|
||||||
|
|
||||||
|
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
|
||||||
|
if (smes != null) {
|
||||||
|
Vector3f sp = smes.raycastGeometry(ray);
|
||||||
|
if (sp != null) {
|
||||||
|
float d = ray.getOrigin().distanceSquared(sp);
|
||||||
|
if (d < bestDistSq) { best = sp; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** true wenn das Gefälle an (worldX, worldZ) 45° überschreitet. */
|
||||||
|
public boolean isTooSteep(float worldX, float worldZ) {
|
||||||
|
// Gebackenes Mesh: Flächennormale direkt aus dem Mesh — genauer als Höhen-Sampling.
|
||||||
|
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
|
||||||
|
if (smes != null) {
|
||||||
|
Ray r = new Ray(new Vector3f(worldX, 500f, worldZ), new Vector3f(0f, -1f, 0f));
|
||||||
|
Vector3f[] hit = smes.raycastGeometryWithNormal(r);
|
||||||
|
if (hit != null) {
|
||||||
|
return hit[1].y < 0.7071f; // cos(45°)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback für Basis-Terrain und rohes Voxel: Höhen-Sampling mit 1 WE Abstand.
|
||||||
|
float h0 = heightAt(worldX, worldZ);
|
||||||
|
float dhx = heightAt(worldX + 1f, worldZ) - h0;
|
||||||
|
float dhz = heightAt(worldX, worldZ + 1f) - h0;
|
||||||
|
return dhx * dhx + dhz * dhz > 1.0f; // tan²(45°) = 1
|
||||||
|
}
|
||||||
|
|
||||||
/** Höchste bekannte Oberfläche an (worldX, worldZ): max(Terrain, Voxel, Baked). */
|
/** Höchste bekannte Oberfläche an (worldX, worldZ): max(Terrain, Voxel, Baked). */
|
||||||
public float heightAt(float worldX, float worldZ) {
|
public float heightAt(float worldX, float worldZ) {
|
||||||
float h = terrainH(worldX, worldZ);
|
float h = terrainH(worldX, worldZ);
|
||||||
@@ -2111,78 +2192,168 @@ 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<>();
|
||||||
|
|
||||||
|
// ── Loop 1: Pass A – Basisterrain (LOWER + RAISE) ───────────────────
|
||||||
|
// Muss als eigenständiger Loop VOR Pass B laufen: Pass B liest smoothMap
|
||||||
|
// der Nachbarn. Wären beide Passes im selben Loop, sähe Pass B manchmal
|
||||||
|
// noch den vergrabenen h0raw einer Nachbarzelle, die von PassA-RAISE noch
|
||||||
|
// nicht angehoben wurde (HashMap-Reihenfolge unbestimmt) → Graben.
|
||||||
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;
|
float minRaiseH = Float.POSITIVE_INFINITY;
|
||||||
int lowCount = 0;
|
float passA_maxThAbove = Float.NEGATIVE_INFINITY;
|
||||||
int lowDist = -1;
|
|
||||||
|
|
||||||
outer2b:
|
for (int r = 1; r <= MAX_RAMP_R_TERRAIN; r++) {
|
||||||
for (int r = 1; r <= MAX_RAMP_R; 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (maxThAboveAtR > Float.NEGATIVE_INFINITY) {
|
||||||
|
float raiseCandidate = maxThAboveAtR + (r - 1) / RAMP_RATIO;
|
||||||
|
if (raiseCandidate < minRaiseH) {
|
||||||
|
minRaiseH = raiseCandidate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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",
|
||||||
|
colX2b, colZ2b, h0sm, h0raw, passA_maxThAbove));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (minRampH < h0sm) {
|
||||||
|
if (log.isTraceEnabled()) {
|
||||||
|
log.trace(String.format("Schritt2b-A (%d,%d) h=%.2f → %.2f",
|
||||||
|
colX2b, colZ2b, h0sm, minRampH));
|
||||||
|
}
|
||||||
|
smoothMap.put(hk0, minRampH);
|
||||||
|
} else if (minRaiseH > h0sm && minRaiseH < Float.POSITIVE_INFINITY) {
|
||||||
|
if (log.isTraceEnabled()) {
|
||||||
|
log.trace(String.format("Schritt2b-RAISE (%d,%d) h=%.2f → %.2f",
|
||||||
|
colX2b, colZ2b, h0sm, minRaiseH));
|
||||||
|
}
|
||||||
|
smoothMap.put(hk0, minRaiseH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (lowDist < 0) continue; // kein niedrigerer Nachbar im Suchradius
|
// ── Loop 2: Pass B – Voxel-zu-Voxel ─────────────────────────────────
|
||||||
|
// Liest smoothMap aus Loop 1 → RAISE-Zellen haben ihre angehobene Höhe.
|
||||||
|
// Effektive Nachbarhöhe: max(raw, smooth).
|
||||||
|
// RAISE-Nachbar (smooth > raw): smooth gewinnt → kein Kandidat unter Terrain.
|
||||||
|
// LOWER-Nachbar (smooth < raw): raw gewinnt → Kaskaden-Schutz bleibt.
|
||||||
|
for (Map.Entry<Long, Float> entry : hfMap.entrySet()) {
|
||||||
|
long hk0 = entry.getKey();
|
||||||
|
float h0raw = entry.getValue();
|
||||||
|
if (h0raw > 4.25f) continue;
|
||||||
|
float h0sm = smoothMap.getOrDefault(hk0, h0raw);
|
||||||
|
int colX2b = (int)(hk0 / 60001L) - 30000;
|
||||||
|
int colZ2b = (int)(hk0 % 60001L) - 30000;
|
||||||
|
|
||||||
float avgLowH = sumLowH / lowCount;
|
float minRampH = h0sm;
|
||||||
float rampH = avgLowH + lowDist / RAMP_RATIO;
|
boolean hadAnyLower = false;
|
||||||
if (rampH < h0sm) {
|
boolean hadNonVoxelR1R3 = false;
|
||||||
smoothMap.put(hk0, rampH);
|
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 hNraw = hfMap.get(hkN);
|
||||||
|
if (hNraw == null) {
|
||||||
|
if (r <= 3) { hadNonVoxelR1R3 = true; }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
float hN = Math.max(hNraw, smoothMap.getOrDefault(hkN, hNraw));
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (r == 3 && !hadAnyLower && !hadNonVoxelR1R3) {
|
||||||
|
break voxelPassB;
|
||||||
|
}
|
||||||
|
if (hadAnyLower) {
|
||||||
|
break voxelPassB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (minRampH < h0sm) {
|
||||||
|
if (log.isTraceEnabled()) {
|
||||||
|
log.trace(String.format("Schritt2b-B (%d,%d) h=%.2f → %.2f",
|
||||||
|
colX2b, colZ2b, h0sm, minRampH));
|
||||||
|
}
|
||||||
|
smoothMap.put(hk0, minRampH);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2432,8 +2603,10 @@ public class VoxelEditorState extends BaseAppState {
|
|||||||
// Nur erfolgreich gebackene Chunks werden gelöscht
|
// Nur erfolgreich gebackene Chunks werden gelöscht
|
||||||
List<VoxelChunk> successfullyBaked = new java.util.ArrayList<>();
|
List<VoxelChunk> successfullyBaked = new java.util.ArrayList<>();
|
||||||
int baked = 0;
|
int baked = 0;
|
||||||
|
float bakeSmoothStr = input.bakeSmoothStrength;
|
||||||
|
float bakeCliffStr = input.bakeCliffNoiseStrength;
|
||||||
for (VoxelChunk chunk : nonEmpty) {
|
for (VoxelChunk chunk : nonEmpty) {
|
||||||
if (bakeChunk(chunk, blurredMap)) {
|
if (bakeChunk(chunk, blurredMap, bakeSmoothStr, bakeCliffStr)) {
|
||||||
successfullyBaked.add(chunk);
|
successfullyBaked.add(chunk);
|
||||||
} else {
|
} else {
|
||||||
log.warn("Chunk ({},{},{}) nicht gebacken – Voxel-Daten bleiben erhalten.",
|
log.warn("Chunk ({},{},{}) nicht gebacken – Voxel-Daten bleiben erhalten.",
|
||||||
@@ -2549,7 +2722,8 @@ public class VoxelEditorState extends BaseAppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Bäckt einen einzelnen Chunk. Gibt true zurück wenn erfolgreich, false bei Fehler. */
|
/** Bäckt einen einzelnen Chunk. Gibt true zurück wenn erfolgreich, false bei Fehler. */
|
||||||
private boolean bakeChunk(VoxelChunk original, Map<Long, VoxelChunk> blurredMap) {
|
private boolean bakeChunk(VoxelChunk original, Map<Long, VoxelChunk> blurredMap,
|
||||||
|
float smoothStrength, float cliffNoiseStrength) {
|
||||||
try {
|
try {
|
||||||
VoxelChunk blurred = blurredMap.get(chunkKey(original.cx, original.cy, original.cz));
|
VoxelChunk blurred = blurredMap.get(chunkKey(original.cx, original.cy, original.cz));
|
||||||
if (blurred == null) return false;
|
if (blurred == null) return false;
|
||||||
@@ -2557,9 +2731,25 @@ public class VoxelEditorState extends BaseAppState {
|
|||||||
// Geblurrte Nachbarn für nahtlose Chunk-Grenzen im MC
|
// Geblurrte Nachbarn für nahtlose Chunk-Grenzen im MC
|
||||||
VoxelChunk[] nb = getNeighbors(original.cx, original.cy, original.cz, blurredMap);
|
VoxelChunk[] nb = getNeighbors(original.cx, original.cy, original.cz, blurredMap);
|
||||||
|
|
||||||
|
float wx = (float)(original.cx * VoxelChunk.CELLS);
|
||||||
|
float wz = (float)(original.cz * VoxelChunk.CELLS);
|
||||||
|
|
||||||
|
Mesh lod0 = MarchingCubes.smooth(MarchingCubes.build(blurred, 1, nb), 1, 0.3f);
|
||||||
|
if (smoothStrength > 0.01f) {
|
||||||
|
lod0 = MarchingCubes.smooth(lod0, 1, smoothStrength);
|
||||||
|
}
|
||||||
|
if (cliffNoiseStrength > 0.01f) {
|
||||||
|
lod0 = MarchingCubes.perturb(lod0, cliffNoiseStrength, wx, wz);
|
||||||
|
}
|
||||||
|
|
||||||
|
Mesh lod1 = MarchingCubes.smooth(MarchingCubes.build(blurred, 4, nb), 3, 0.4f);
|
||||||
|
if (cliffNoiseStrength > 0.01f) {
|
||||||
|
lod1 = MarchingCubes.perturb(lod1, cliffNoiseStrength * 0.6f, wx, wz);
|
||||||
|
}
|
||||||
|
|
||||||
Mesh[] meshes = {
|
Mesh[] meshes = {
|
||||||
MarchingCubes.smooth(MarchingCubes.build(blurred, 1, nb), 1, 0.3f),
|
lod0,
|
||||||
MarchingCubes.smooth(MarchingCubes.build(blurred, 4, nb), 3, 0.4f),
|
lod1,
|
||||||
MarchingCubes.smooth(MarchingCubes.build(blurred, 16, nb), 2, 0.4f),
|
MarchingCubes.smooth(MarchingCubes.build(blurred, 16, nb), 2, 0.4f),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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"/>
|
||||||
|
|||||||
@@ -682,4 +682,172 @@ public final class MarchingCubes {
|
|||||||
return chunk.getDensity(x, y, z);
|
return chunk.getDensity(x, y, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verschiebt Vertices auf steilen Flächen (normal.y < 0.707 ≈ 45°) horizontal (XZ)
|
||||||
|
* per kohärentem Value-Noise. Macht Klippen natürlicher und felsiger.
|
||||||
|
* Rand-Vertices (Chunk-Grenzen) bleiben fixiert, Nahtlosigkeit bleibt erhalten.
|
||||||
|
*
|
||||||
|
* @param strength Amplitude: 0 = kein Effekt, 1 = bis ~2 m maximale Verschiebung
|
||||||
|
* @param worldX Weltkoordinate X des Chunk-Ursprungs (cx * CELLS)
|
||||||
|
* @param worldZ Weltkoordinate Z des Chunk-Ursprungs (cz * CELLS)
|
||||||
|
*/
|
||||||
|
public static Mesh perturb(Mesh mesh, float strength, float worldX, float worldZ) {
|
||||||
|
if (mesh == null || strength < 0.001f) return mesh;
|
||||||
|
FloatBuffer posF = mesh.getFloatBuffer(VertexBuffer.Type.Position);
|
||||||
|
if (posF == null) return mesh;
|
||||||
|
int vertCount = posF.capacity() / 3;
|
||||||
|
if (vertCount < 3) return mesh;
|
||||||
|
int triCount = vertCount / 3;
|
||||||
|
|
||||||
|
float[] pos = new float[vertCount * 3];
|
||||||
|
posF.rewind(); posF.get(pos);
|
||||||
|
|
||||||
|
// Vertex-Gruppen aufbauen (gleiche Position → gleiche Gruppe)
|
||||||
|
HashMap<Long, Integer> keyToGroup = new HashMap<>(vertCount / 3 + 16);
|
||||||
|
int[] vertGroup = new int[vertCount];
|
||||||
|
int[] groupFirst = new int[vertCount];
|
||||||
|
int groupCount = 0;
|
||||||
|
for (int v = 0; v < vertCount; v++) {
|
||||||
|
long key = posKey(pos, v);
|
||||||
|
Integer g = keyToGroup.get(key);
|
||||||
|
if (g == null) {
|
||||||
|
keyToGroup.put(key, groupCount);
|
||||||
|
groupFirst[groupCount] = v;
|
||||||
|
vertGroup[v] = groupCount++;
|
||||||
|
} else {
|
||||||
|
vertGroup[v] = g;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
float[] gx = new float[groupCount];
|
||||||
|
float[] gy = new float[groupCount];
|
||||||
|
float[] gz = new float[groupCount];
|
||||||
|
for (int g = 0; g < groupCount; g++) {
|
||||||
|
int v = groupFirst[g];
|
||||||
|
gx[g] = pos[v*3]; gy[g] = pos[v*3+1]; gz[g] = pos[v*3+2];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rand-Vertices einfrieren (Chunk-Nahtlosigkeit)
|
||||||
|
float bound = VoxelChunk.CELLS;
|
||||||
|
boolean[] pinned = new boolean[groupCount];
|
||||||
|
for (int g = 0; g < groupCount; g++) {
|
||||||
|
float x = gx[g], y = gy[g], z = gz[g];
|
||||||
|
if (x < 0.01f || x > bound - 0.01f ||
|
||||||
|
y < 0.01f || y > bound - 0.01f ||
|
||||||
|
z < 0.01f || z > bound - 0.01f) {
|
||||||
|
pinned[g] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flächennormalen berechnen und pro Gruppe akkumulieren
|
||||||
|
float[] gnx = new float[groupCount];
|
||||||
|
float[] gny = new float[groupCount];
|
||||||
|
float[] gnz = new float[groupCount];
|
||||||
|
for (int t = 0; t < triCount; t++) {
|
||||||
|
int g0=vertGroup[t*3], g1=vertGroup[t*3+1], g2=vertGroup[t*3+2];
|
||||||
|
float p0x=pos[t*9], p0y=pos[t*9+1], p0z=pos[t*9+2];
|
||||||
|
float p1x=pos[t*9+3], p1y=pos[t*9+4], p1z=pos[t*9+5];
|
||||||
|
float p2x=pos[t*9+6], p2y=pos[t*9+7], p2z=pos[t*9+8];
|
||||||
|
float ex=p1x-p0x, ey=p1y-p0y, ez=p1z-p0z;
|
||||||
|
float fx=p2x-p0x, fy=p2y-p0y, fz=p2z-p0z;
|
||||||
|
float nx=ey*fz-ez*fy, ny=ez*fx-ex*fz, nz=ex*fy-ey*fx;
|
||||||
|
gnx[g0]+=nx; gny[g0]+=ny; gnz[g0]+=nz;
|
||||||
|
gnx[g1]+=nx; gny[g1]+=ny; gnz[g1]+=nz;
|
||||||
|
gnx[g2]+=nx; gny[g2]+=ny; gnz[g2]+=nz;
|
||||||
|
}
|
||||||
|
for (int g = 0; g < groupCount; g++) {
|
||||||
|
float len=(float)Math.sqrt(gnx[g]*gnx[g]+gny[g]*gny[g]+gnz[g]*gnz[g]);
|
||||||
|
if (len > 1e-6f) { gnx[g]/=len; gny[g]/=len; gnz[g]/=len; }
|
||||||
|
else { gny[g] = 1f; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontale Verschiebung auf steile Gruppen anwenden.
|
||||||
|
// fBm (Fractal Brownian Motion) mit 4 Oktaven: gleiche Frequenz auf allen
|
||||||
|
// drei Achsen → jedes Höhenniveau bekommt eigene, unabhängige Verschiebung.
|
||||||
|
// Zwei unkorrelierte fBm-Felder für X und Z (unterschiedliche Offsets).
|
||||||
|
final float STEEP = 0.707f; // cos(45°)
|
||||||
|
final float MAX_D = 4.0f; // m maximale Verschiebung bei strength=1, senkrechter Fläche
|
||||||
|
|
||||||
|
for (int g = 0; g < groupCount; g++) {
|
||||||
|
if (pinned[g]) continue;
|
||||||
|
float ny = gny[g];
|
||||||
|
if (ny >= STEEP) continue;
|
||||||
|
float steepness = 1f - ny / STEEP; // 0 bei 45°, 1 bei senkrecht
|
||||||
|
float wx = worldX + gx[g];
|
||||||
|
float wz = worldZ + gz[g];
|
||||||
|
float wy = gy[g];
|
||||||
|
float noiseX = perturbFbm(wx * 0.08f, wy * 0.08f, wz * 0.08f);
|
||||||
|
float noiseZ = perturbFbm(wx * 0.08f + 31.7f, wy * 0.08f + 11.3f, wz * 0.08f + 67.1f);
|
||||||
|
float disp = steepness * strength * MAX_D;
|
||||||
|
gx[g] += noiseX * disp;
|
||||||
|
gz[g] += noiseZ * disp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Positionen in Buffer schreiben
|
||||||
|
for (int v = 0; v < vertCount; v++) {
|
||||||
|
int g = vertGroup[v];
|
||||||
|
pos[v*3] = gx[g]; pos[v*3+2] = gz[g];
|
||||||
|
}
|
||||||
|
posF.rewind(); posF.put(pos); posF.rewind();
|
||||||
|
|
||||||
|
// Normalen aus geänderter Geometrie neu berechnen
|
||||||
|
java.util.Arrays.fill(gnx, 0f); java.util.Arrays.fill(gny, 0f); java.util.Arrays.fill(gnz, 0f);
|
||||||
|
for (int t = 0; t < triCount; t++) {
|
||||||
|
int g0=vertGroup[t*3], g1=vertGroup[t*3+1], g2=vertGroup[t*3+2];
|
||||||
|
float p0x=pos[t*9], p0y=pos[t*9+1], p0z=pos[t*9+2];
|
||||||
|
float p1x=pos[t*9+3], p1y=pos[t*9+4], p1z=pos[t*9+5];
|
||||||
|
float p2x=pos[t*9+6], p2y=pos[t*9+7], p2z=pos[t*9+8];
|
||||||
|
float ex=p1x-p0x, ey=p1y-p0y, ez=p1z-p0z;
|
||||||
|
float fx=p2x-p0x, fy=p2y-p0y, fz=p2z-p0z;
|
||||||
|
float nx=ey*fz-ez*fy, ny=ez*fx-ex*fz, nz=ex*fy-ey*fx;
|
||||||
|
gnx[g0]+=nx; gny[g0]+=ny; gnz[g0]+=nz;
|
||||||
|
gnx[g1]+=nx; gny[g1]+=ny; gnz[g1]+=nz;
|
||||||
|
gnx[g2]+=nx; gny[g2]+=ny; gnz[g2]+=nz;
|
||||||
|
}
|
||||||
|
FloatBuffer normF = mesh.getFloatBuffer(VertexBuffer.Type.Normal);
|
||||||
|
if (normF != null) {
|
||||||
|
normF.rewind();
|
||||||
|
for (int v = 0; v < vertCount; v++) {
|
||||||
|
int g = vertGroup[v];
|
||||||
|
float len=(float)Math.sqrt(gnx[g]*gnx[g]+gny[g]*gny[g]+gnz[g]*gnz[g]);
|
||||||
|
if (len > 1e-6f) normF.put(gnx[g]/len).put(gny[g]/len).put(gnz[g]/len);
|
||||||
|
else normF.put(0f).put(1f).put(0f);
|
||||||
|
}
|
||||||
|
normF.rewind();
|
||||||
|
}
|
||||||
|
|
||||||
|
mesh.updateBound();
|
||||||
|
return mesh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** fBm (Fractal Brownian Motion), 4 Oktaven, Ergebnis in [-1, 1]. */
|
||||||
|
private static float perturbFbm(float x, float y, float z) {
|
||||||
|
float v = 0f, amp = 1f, sumAmp = 0f;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
v += perturbNoise(x, y, z) * amp;
|
||||||
|
sumAmp += amp;
|
||||||
|
x *= 2.1f; y *= 2.1f; z *= 2.1f;
|
||||||
|
amp *= 0.5f;
|
||||||
|
}
|
||||||
|
return v / sumAmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Trilinear interpoliertes Value-Noise in [-1, 1]. Kohärent, deterministisch. */
|
||||||
|
private static float perturbNoise(float x, float y, float z) {
|
||||||
|
int ix=(int)Math.floor(x), iy=(int)Math.floor(y), iz=(int)Math.floor(z);
|
||||||
|
float fx=x-ix, fy=y-iy, fz=z-iz;
|
||||||
|
fx=fx*fx*(3-2*fx); fy=fy*fy*(3-2*fy); fz=fz*fz*(3-2*fz);
|
||||||
|
float v00 = perturbHash(ix,iy,iz) + fx*(perturbHash(ix+1,iy,iz) - perturbHash(ix,iy,iz));
|
||||||
|
float v10 = perturbHash(ix,iy+1,iz) + fx*(perturbHash(ix+1,iy+1,iz) - perturbHash(ix,iy+1,iz));
|
||||||
|
float v01 = perturbHash(ix,iy,iz+1) + fx*(perturbHash(ix+1,iy,iz+1) - perturbHash(ix,iy,iz+1));
|
||||||
|
float v11 = perturbHash(ix,iy+1,iz+1)+fx*(perturbHash(ix+1,iy+1,iz+1)-perturbHash(ix,iy+1,iz+1));
|
||||||
|
return (v00 + fy*(v10-v00)) + fz*((v01 + fy*(v11-v01)) - (v00 + fy*(v10-v00)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float perturbHash(int x, int y, int z) {
|
||||||
|
int h = x * 374761393 + y * 1103515245 + z * -2012135261;
|
||||||
|
h ^= (h >>> 15); h *= 0x9e3779b9; h ^= (h >>> 12);
|
||||||
|
return ((h >>> 1) & 0xFFFF) * (2f / 65535f) - 1f;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
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_16_0_07_baked_lod0.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_16_0_07_baked_lod0.j3o
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_16_0_07_baked_lod1.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_16_0_07_baked_lod1.j3o
Normal file
Binary file not shown.
BIN
blight-map/src/main/map/chunks/voxel_16_0_07_baked_lod2.j3o
Normal file
BIN
blight-map/src/main/map/chunks/voxel_16_0_07_baked_lod2.j3o
Normal file
Binary file not shown.
Reference in New Issue
Block a user