diff --git a/blight-assets/src/main/resources/MatDefs/Grass.j3md b/blight-assets/src/main/resources/MatDefs/Grass.j3md index 329c52d..3ba8513 100644 --- a/blight-assets/src/main/resources/MatDefs/Grass.j3md +++ b/blight-assets/src/main/resources/MatDefs/Grass.j3md @@ -9,7 +9,8 @@ MaterialDef Grass { Vector2 WindDir : 0.0 1.0 Vector3 SunDir : 0.55 0.80 0.35 Color SunColor : 1.0 1.0 0.95 1.0 - Float Wetness : 0.0 + Float Wetness : 0.0 + Boolean GammaCorrect : false } Technique { @@ -28,8 +29,9 @@ MaterialDef Grass { } Defines { - HAS_COLORMAP : ColorMap - HAS_NORMALMAP : NormalMap + HAS_COLORMAP : ColorMap + HAS_NORMALMAP : NormalMap + GAMMA_CORRECT : GammaCorrect } } } diff --git a/blight-assets/src/main/resources/Shaders/Grass.frag b/blight-assets/src/main/resources/Shaders/Grass.frag index ab2cdd8..282d0a5 100644 --- a/blight-assets/src/main/resources/Shaders/Grass.frag +++ b/blight-assets/src/main/resources/Shaders/Grass.frag @@ -39,5 +39,9 @@ void main() { color.rgb *= (1.0 - 0.18 * m_Wetness); color.rgb *= lit; float shine = pow(diffuse, 12.0) * m_Wetness * 0.25; - outFragColor = vec4(color.rgb + vec3(shine), color.a); + vec3 result = color.rgb + vec3(shine); +#ifdef GAMMA_CORRECT + result = pow(max(result, vec3(0.0)), vec3(1.0 / 2.2)); +#endif + outFragColor = vec4(result, color.a); } diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/gras1.png b/blight-assets/src/main/resources/Textures/vegetation/gras1.png similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/gras1.png rename to blight-assets/src/main/resources/Textures/vegetation/gras1.png diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/gras2.png b/blight-assets/src/main/resources/Textures/vegetation/gras2.png similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/gras2.png rename to blight-assets/src/main/resources/Textures/vegetation/gras2.png diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant1/plant1_Color.png b/blight-assets/src/main/resources/Textures/vegetation/plant1/plant1_Color.png similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/plant1/plant1_Color.png rename to blight-assets/src/main/resources/Textures/vegetation/plant1/plant1_Color.png diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant1/plant1_NormalGL.png b/blight-assets/src/main/resources/Textures/vegetation/plant1/plant1_NormalGL.png similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/plant1/plant1_NormalGL.png rename to blight-assets/src/main/resources/Textures/vegetation/plant1/plant1_NormalGL.png diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant1/plant1_Roughness.png b/blight-assets/src/main/resources/Textures/vegetation/plant1/plant1_Roughness.png similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/plant1/plant1_Roughness.png rename to blight-assets/src/main/resources/Textures/vegetation/plant1/plant1_Roughness.png diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant1/textureset.json b/blight-assets/src/main/resources/Textures/vegetation/plant1/textureset.json similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/plant1/textureset.json rename to blight-assets/src/main/resources/Textures/vegetation/plant1/textureset.json diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant2/plant2_Color.png b/blight-assets/src/main/resources/Textures/vegetation/plant2/plant2_Color.png similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/plant2/plant2_Color.png rename to blight-assets/src/main/resources/Textures/vegetation/plant2/plant2_Color.png diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant2/plant2_NormalGL.png b/blight-assets/src/main/resources/Textures/vegetation/plant2/plant2_NormalGL.png similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/plant2/plant2_NormalGL.png rename to blight-assets/src/main/resources/Textures/vegetation/plant2/plant2_NormalGL.png diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant2/plant2_Roughness.png b/blight-assets/src/main/resources/Textures/vegetation/plant2/plant2_Roughness.png similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/plant2/plant2_Roughness.png rename to blight-assets/src/main/resources/Textures/vegetation/plant2/plant2_Roughness.png diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant2/textureset.json b/blight-assets/src/main/resources/Textures/vegetation/plant2/textureset.json similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/plant2/textureset.json rename to blight-assets/src/main/resources/Textures/vegetation/plant2/textureset.json diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant3/plant3_Color.png b/blight-assets/src/main/resources/Textures/vegetation/plant3/plant3_Color.png similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/plant3/plant3_Color.png rename to blight-assets/src/main/resources/Textures/vegetation/plant3/plant3_Color.png diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant3/plant3_NormalGL.png b/blight-assets/src/main/resources/Textures/vegetation/plant3/plant3_NormalGL.png similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/plant3/plant3_NormalGL.png rename to blight-assets/src/main/resources/Textures/vegetation/plant3/plant3_NormalGL.png diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant3/plant3_Roughness.png b/blight-assets/src/main/resources/Textures/vegetation/plant3/plant3_Roughness.png similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/plant3/plant3_Roughness.png rename to blight-assets/src/main/resources/Textures/vegetation/plant3/plant3_Roughness.png diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant3/textureset.json b/blight-assets/src/main/resources/Textures/vegetation/plant3/textureset.json similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/plant3/textureset.json rename to blight-assets/src/main/resources/Textures/vegetation/plant3/textureset.json diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant4/plant4_Color.png b/blight-assets/src/main/resources/Textures/vegetation/plant4/plant4_Color.png similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/plant4/plant4_Color.png rename to blight-assets/src/main/resources/Textures/vegetation/plant4/plant4_Color.png diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant4/plant4_NormalGL.png b/blight-assets/src/main/resources/Textures/vegetation/plant4/plant4_NormalGL.png similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/plant4/plant4_NormalGL.png rename to blight-assets/src/main/resources/Textures/vegetation/plant4/plant4_NormalGL.png diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant4/plant4_Roughness.png b/blight-assets/src/main/resources/Textures/vegetation/plant4/plant4_Roughness.png similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/plant4/plant4_Roughness.png rename to blight-assets/src/main/resources/Textures/vegetation/plant4/plant4_Roughness.png diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant4/textureset.json b/blight-assets/src/main/resources/Textures/vegetation/plant4/textureset.json similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/plant4/textureset.json rename to blight-assets/src/main/resources/Textures/vegetation/plant4/textureset.json diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant5/plant5_Color.png b/blight-assets/src/main/resources/Textures/vegetation/plant5/plant5_Color.png similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/plant5/plant5_Color.png rename to blight-assets/src/main/resources/Textures/vegetation/plant5/plant5_Color.png diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant5/plant5_NormalGL.png b/blight-assets/src/main/resources/Textures/vegetation/plant5/plant5_NormalGL.png similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/plant5/plant5_NormalGL.png rename to blight-assets/src/main/resources/Textures/vegetation/plant5/plant5_NormalGL.png diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant5/plant5_Roughness.png b/blight-assets/src/main/resources/Textures/vegetation/plant5/plant5_Roughness.png similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/plant5/plant5_Roughness.png rename to blight-assets/src/main/resources/Textures/vegetation/plant5/plant5_Roughness.png diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/plant5/textureset.json b/blight-assets/src/main/resources/Textures/vegetation/plant5/textureset.json similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/plant5/textureset.json rename to blight-assets/src/main/resources/Textures/vegetation/plant5/textureset.json diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/rohrkolben.png b/blight-assets/src/main/resources/Textures/vegetation/rohrkolben.png similarity index 100% rename from blight-assets/src/main/resources/Textures/ground/vegetation/rohrkolben.png rename to blight-assets/src/main/resources/Textures/vegetation/rohrkolben.png diff --git a/blight-common/src/main/java/de/blight/common/PlacedStone.java b/blight-common/src/main/java/de/blight/common/PlacedStone.java index d7237fd..1267085 100644 --- a/blight-common/src/main/java/de/blight/common/PlacedStone.java +++ b/blight-common/src/main/java/de/blight/common/PlacedStone.java @@ -11,7 +11,9 @@ public record PlacedStone( float x, float z, float radius, + float rotX, float rotY, + float rotZ, int textureSlot, float sinkFraction, int noiseSeed diff --git a/blight-common/src/main/java/de/blight/common/PlacedStoneIO.java b/blight-common/src/main/java/de/blight/common/PlacedStoneIO.java index 0033f6b..babc1aa 100644 --- a/blight-common/src/main/java/de/blight/common/PlacedStoneIO.java +++ b/blight-common/src/main/java/de/blight/common/PlacedStoneIO.java @@ -9,19 +9,20 @@ import java.util.zip.*; * Liest und schreibt platzierte Steine als komprimierte Binärdatei * ({@code blight_stones.bls}) neben der Kartendatei. * - * Format v1: + * Format v3: * int MAGIC 0x53544E53 ("STNS") - * int VERSION 1 + * int VERSION 3 * int SLOT_COUNT 3 * 3× UTF Texturpfad pro Slot ("" = kein) + * float roughness (v2+; 0..1) * int stoneCount - * N× float x, float z, float radius, float rotY, + * N× float x, float z, float radius, float rotX, float rotY, float rotZ, * byte textureSlot, float sinkFraction, int noiseSeed */ public final class PlacedStoneIO { private static final int MAGIC = 0x53544E53; - private static final int VERSION = 1; + private static final int VERSION = 3; public static final int SLOT_COUNT = 3; private PlacedStoneIO() {} @@ -30,7 +31,7 @@ public final class PlacedStoneIO { return MapIO.getMapPath().resolveSibling("blight_stones.bls"); } - public record StoneData(String[] slotPaths, List stones) {} + public record StoneData(String[] slotPaths, float roughness, List stones) {} public static void save(StoneData data) throws IOException { Path p = getPath(); @@ -45,13 +46,16 @@ public final class PlacedStoneIO { String s = (i < paths.length && paths[i] != null) ? paths[i] : ""; out.writeUTF(s); } + out.writeFloat(data.roughness()); List stones = data.stones() != null ? data.stones() : List.of(); out.writeInt(stones.size()); for (PlacedStone s : stones) { out.writeFloat(s.x()); out.writeFloat(s.z()); out.writeFloat(s.radius()); + out.writeFloat(s.rotX()); out.writeFloat(s.rotY()); + out.writeFloat(s.rotZ()); out.writeByte(s.textureSlot()); out.writeFloat(s.sinkFraction()); out.writeInt(s.noiseSeed()); @@ -74,19 +78,22 @@ public final class PlacedStoneIO { String s = in.readUTF(); if (i < SLOT_COUNT) paths[i] = s; } + float roughness = (ver >= 2) ? in.readFloat() : 0.40f; int count = in.readInt(); List stones = new ArrayList<>(count); for (int i = 0; i < count; i++) { float x = in.readFloat(); float z = in.readFloat(); float r = in.readFloat(); + float rotX = (ver >= 3) ? in.readFloat() : 0f; float rotY = in.readFloat(); + float rotZ = (ver >= 3) ? in.readFloat() : 0f; int slot = in.readUnsignedByte(); float sink = in.readFloat(); int seed = in.readInt(); - stones.add(new PlacedStone(x, z, r, rotY, slot, sink, seed)); + stones.add(new PlacedStone(x, z, r, rotX, rotY, rotZ, slot, sink, seed)); } - return new StoneData(paths, stones); + return new StoneData(paths, roughness, stones); } } } diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index ed2d2fa..6a76c7c 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -92,6 +92,7 @@ public class EditorApp extends Application { private de.blight.editor.toolset.water.WaterController waterController; private de.blight.editor.toolset.voxel.VoxelController voxelController; private de.blight.editor.toolset.grass.GrassController grassController; + private de.blight.editor.toolset.stone.StoneController stoneController; private de.blight.editor.toolset.content.ContentController contentController; private de.blight.editor.toolset.tripo.TripoController tripoController; private de.blight.editor.toolset.game.GameController gameController; @@ -193,6 +194,7 @@ public class EditorApp extends Application { viewportController.getViewport(), terrainController::showToolParameters, colorAbsPath -> { de.blight.editor.TextureMapData d = terrainController.loadTexMeta(colorAbsPath); return d.normalMap != null ? d.normalMap : ""; }); terrainController.setGrassController(grassController); + stoneController = new de.blight.editor.toolset.stone.StoneController(toolCtx); contentController = new de.blight.editor.toolset.content.ContentController(toolCtx); contentController.setOnEnterContentMode(() -> { if (assetController.isOverlayOpen()) assetController.closeOverlay(); @@ -227,7 +229,7 @@ public class EditorApp extends Application { modelEditorController, assetController, animSetController, contentController, worldPanelController, voxelController, grassController, atmosphereController, waterController, - gameController, terrainController, objectController, + gameController, terrainController, objectController, stoneController, this::switchToVegetationGenerator, this::switchToEzTree, this::switchToTripo, this::openCharacterEditor, this::switchToThumbnailManager, this::takeEditorScreenshot, this::openSettings @@ -375,6 +377,12 @@ public class EditorApp extends Application { atmosphereController.onFrame(); waterController.onFrame(); voxelController.onFrame(); + stoneController.onFrame(); + if (input.stoneAutoActivateEditMode) { + input.stoneAutoActivateEditMode = false; + stoneController.activateEditMode(); + topBarController.selectStoneBtnSilently(); + } java.util.Set skeletal = input.skeletalPaths.getAndSet(null); if (skeletal != null) { diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index b98ee88..5527952 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -1023,6 +1023,8 @@ public class SharedInput { // ── Steine ──────────────────────────────────────────────────────────────── /** activeLayer==23 → Steine setzen/entfernen */ public static final int LAYER_STONE = 23; + /** activeLayer==37 → Stein selektieren / Gizmo bedienen */ + public static final int LAYER_STONE_EDIT = 37; public record StoneEdit(float screenX, float screenY, int action) {} public final ConcurrentLinkedQueue stoneEditQueue = new ConcurrentLinkedQueue<>(); @@ -1033,6 +1035,24 @@ public class SharedInput { /** JFX → JME: Reload der Materialien auslösen. */ public volatile boolean stoneTexturesChanged = false; + /** JME → JFX: Info über selektierten Stein ("x|z|rotX|rotY|rotZ|radius" oder null). */ + public volatile String stoneSelectedInfo = null; + public volatile boolean stoneSelectionChanged = false; + + /** JFX → JME: aktives Gizmo-Werkzeug im Bearbeiten-Modus (0=Bewegen, 1=Rotieren). */ + public volatile int stoneEditTool = EDIT_TOOL_MOVE; + + /** Drag-Ereignisse aus ViewportController für Stein-Gizmo. */ + public final ConcurrentLinkedQueue stoneDragQueue = new ConcurrentLinkedQueue<>(); + + /** JFX → JME: Selektierten Stein löschen. */ + public volatile boolean stoneDeleteRequested = false; + + /** Klicks aus LAYER_AUSWAHL – StoneEditorState testet parallel auf Stein-Treffer. */ + public final ConcurrentLinkedQueue stoneAuswahlQueue = new ConcurrentLinkedQueue<>(); + /** JME → JFX: Stein wurde per Auswahl-Tool selektiert → automatisch Edit-Panel öffnen. */ + public volatile boolean stoneAutoActivateEditMode = false; + /** JME → JME: Terrain-Edits, die Stein-Höhen neu berechnen müssen. float[3] = {wx, wz, radius}. */ public final ConcurrentLinkedQueue terrainEditedAreas = new ConcurrentLinkedQueue<>(); diff --git a/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.java b/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.java index d99d3b6..997c9da 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/PlacedObjectState.java @@ -220,6 +220,7 @@ public class PlacedObjectState extends BaseAppState { mat.setFloat("WindStrength", 0.12f); mat.setVector3("SunDir", new Vector3f(0.55f, 0.80f, 0.35f)); mat.setColor("SunColor", ColorRGBA.White); + mat.setBoolean("GammaCorrect", true); mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); return mat; } catch (Exception e) { @@ -444,8 +445,7 @@ public class PlacedObjectState extends BaseAppState { int vi = 0; for (float[] blade : blades) { float x = blade[0], y = blade[1], z = blade[2], h = blade[3]; - // Klingenbreite: Hälfte der Gesamtbreite (2w × h = Quad-Seitenverhältnis ar) - float w = Math.max(0.05f, h * ar * 0.5f); + float w = Math.max(0.001f, h * ar * 0.5f); // Quad 1: liegt in XY-Ebene (konstantes z) – Normal zeigt +Z, Tangent zeigt +X pos.put(x-w).put(y ).put(z); uv.put(0).put(0); pos.put(x+w).put(y ).put(z); uv.put(1).put(0); diff --git a/blight-editor/src/main/java/de/blight/editor/state/StoneEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/StoneEditorState.java index 86caccc..d2013f2 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/StoneEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/StoneEditorState.java @@ -4,6 +4,7 @@ import com.jme3.app.Application; import com.jme3.app.SimpleApplication; import com.jme3.app.state.BaseAppState; import com.jme3.asset.AssetManager; +import com.jme3.bounding.BoundingVolume; import com.jme3.collision.CollisionResults; import com.jme3.material.Material; import com.jme3.material.RenderState; @@ -12,6 +13,8 @@ import com.jme3.renderer.Camera; import com.jme3.renderer.queue.RenderQueue; import com.jme3.scene.*; import com.jme3.scene.VertexBuffer.Type; +import com.jme3.scene.shape.Cylinder; +import com.jme3.scene.shape.Torus; import com.jme3.terrain.geomipmap.TerrainQuad; import com.jme3.util.BufferUtils; import de.blight.common.PlacedStone; @@ -68,11 +71,38 @@ public class StoneEditorState extends BaseAppState { private final Material[] slotMat = new Material[PlacedStoneIO.SLOT_COUNT]; private Material defaultMat; private Geometry brushIndicator; + private Geometry selHighlight; private Geometry previewGeom; private PlacedStone pendingStone; private final Random random = new Random(); private boolean modified = false; + private double lastRoughness = -1; + + // ── Selektion & Gizmo ──────────────────────────────────────────────────── + private static final ColorRGBA COL_X = new ColorRGBA(0.92f, 0.12f, 0.12f, 1f); + private static final ColorRGBA COL_Y = new ColorRGBA(0.12f, 0.88f, 0.12f, 1f); + private static final ColorRGBA COL_Z = new ColorRGBA(0.12f, 0.35f, 1.00f, 1f); + private static final float SHAFT_LEN = 3.2f; + private static final float HEAD_LEN = 0.9f; + private static final float SHAFT_RADIUS = 0.06f; + private static final float HEAD_RADIUS = 0.22f; + private static final float RING_RADIUS = 4.5f; + private static final float RING_TUBE = 0.25f; + private static final float PX_PER_WE = 0.15f; + private static final float DEG_PER_PX = 0.8f; + + private Node gizmoNode; + private Node arrowX, arrowY, arrowZ; + private Node ringX, ringY, ringZ; + private int activeGizmo = -1; + private int activeRing = -1; + private int lastStoneEditTool = -1; + + private int selChunk = -1; + private int selIdx = -1; + /** Rückwärts-Lookup: Geometrie-Name → {chunkIdx, stoneIdx} */ + private final Map geomNameToRef = new HashMap<>(); public StoneEditorState(SharedInput input) { this.input = input; @@ -99,6 +129,21 @@ public class StoneEditorState extends BaseAppState { previewGeom = buildPreviewGeom(); rootNode.attachChild(previewGeom); + + gizmoNode = new Node("stoneGizmo"); + buildGizmo(); + gizmoNode.setCullHint(Spatial.CullHint.Always); + rootNode.attachChild(gizmoNode); + + Material hlMat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + hlMat.setColor("Color", new ColorRGBA(0.3f, 0.8f, 1.0f, 1f)); + hlMat.getAdditionalRenderState().setWireframe(true); + hlMat.getAdditionalRenderState().setDepthTest(false); + selHighlight = new Geometry("stoneSelHighlight", new Mesh()); + selHighlight.setMaterial(hlMat); + selHighlight.setQueueBucket(RenderQueue.Bucket.Transparent); + selHighlight.setCullHint(Spatial.CullHint.Always); + rootNode.attachChild(selHighlight); } @Override @@ -107,6 +152,8 @@ public class StoneEditorState extends BaseAppState { rootNode.detachChild(stoneRoot); rootNode.detachChild(brushIndicator); rootNode.detachChild(previewGeom); + rootNode.detachChild(gizmoNode); + rootNode.detachChild(selHighlight); } @Override protected void onEnable() { stoneRoot.setCullHint(Spatial.CullHint.Inherit); } @@ -122,16 +169,81 @@ public class StoneEditorState extends BaseAppState { Arrays.fill(dirtyChunks, true); } + double curRoughness = input.stoneTool.roughness.getValue(); + if (curRoughness != lastRoughness) { + lastRoughness = curRoughness; + Arrays.fill(dirtyChunks, true); + pendingStone = null; + } + + if (input.stoneEditTool != lastStoneEditTool) { + lastStoneEditTool = input.stoneEditTool; + activeGizmo = -1; + activeRing = -1; + } + updateBrushIndicator(); updatePreviewStone(); processEdits(); + processAuswahlClicks(); processTerrainUpdates(); rebuildDirtyChunks(); updateChunkLOD(); + + if (input.activeLayer == SharedInput.LAYER_STONE_EDIT) { + brushIndicator.setCullHint(Spatial.CullHint.Always); + SharedInput.ObjectDrag drag; + while ((drag = input.stoneDragQueue.poll()) != null) { + if (activeGizmo >= 0) handleMoveDrag(drag); + else if (activeRing >= 0) handleRotateDrag(drag); + } + if (input.stoneDeleteRequested) { + input.stoneDeleteRequested = false; + deleteSelectedStone(); + } + if (selChunk >= 0) { + selHighlight.setCullHint(Spatial.CullHint.Inherit); + updateGizmoPosition(); + updateGizmoVisibility(); + } else { + gizmoNode.setCullHint(Spatial.CullHint.Always); + selHighlight.setCullHint(Spatial.CullHint.Always); + } + } else { + while (input.stoneDragQueue.poll() != null) { /* drain */ } + activeGizmo = -1; + activeRing = -1; + gizmoNode.setCullHint(Spatial.CullHint.Always); + selHighlight.setCullHint(Spatial.CullHint.Always); + } } // ── Edit-Verarbeitung ───────────────────────────────────────────────────── + private void processAuswahlClicks() { + SharedInput.StoneEdit click; + while ((click = input.stoneAuswahlQueue.poll()) != null) { + if (input.activeLayer != SharedInput.LAYER_AUSWAHL) continue; + float jx = click.screenX() * (float) input.viewportScaleX; + float jy = cam.getHeight() - click.screenY() * (float) input.viewportScaleY; + Vector3f origin = cam.getWorldCoordinates(new Vector2f(jx, jy), 0f); + Vector3f target = cam.getWorldCoordinates(new Vector2f(jx, jy), 1f); + Ray ray = new Ray(origin, target.subtractLocal(origin).normalizeLocal()); + CollisionResults res = new CollisionResults(); + stoneRoot.collideWith(ray, res); + if (res.size() > 0) { + String rawName = res.getClosestCollision().getGeometry().getName(); + String name = rawName.endsWith("_lod1") + ? rawName.substring(0, rawName.length() - 5) : rawName; + int[] ref = geomNameToRef.get(name); + if (ref != null) { + selectStone(ref[0], ref[1]); + input.stoneAutoActivateEditMode = true; + } + } + } + } + private void processTerrainUpdates() { float[] area; while ((area = input.terrainEditedAreas.poll()) != null) { @@ -150,15 +262,22 @@ public class StoneEditorState extends BaseAppState { while ((edit = input.stoneEditQueue.poll()) != null) { float jx = edit.screenX() * (float) input.viewportScaleX; float jy = cam.getHeight() - edit.screenY() * (float) input.viewportScaleY; + + if (input.activeLayer == SharedInput.LAYER_STONE_EDIT) { + if (edit.action() > 0) processEditModeClick(jx, jy); + continue; + } + Vector3f hit = raycastTerrain(jx, jy); if (hit == null) continue; if (isSingleStoneMode()) { - if (edit.action() < 0) { // Rechtsklick: platzierten Stein + neuen Preview + if (edit.action() > 0) { // Linksklick: Stein platzieren + neuen Preview erzeugen placePendingStoneAt(hit.x, hit.z); generatePendingStone(); + } else { // Rechtsklick: nur neuen Zufallsstein zum Platzieren bereitstellen + generatePendingStone(); } - // Linksklick ignoriert in Single-Stone-Modus } else { if (edit.action() > 0) addStonesAt(hit.x, hit.z); else removeStonesAt(hit.x, hit.z); @@ -187,17 +306,20 @@ public class StoneEditorState extends BaseAppState { int slot = random.nextInt(activeSlots); int seed = random.nextInt(); - pendingStone = new PlacedStone(0f, 0f, radius, rotY, slot, sinkFrac, seed); + pendingStone = new PlacedStone(0f, 0f, radius, 0f, rotY, 0f, slot, sinkFrac, seed); - Mesh mesh = buildStoneMesh(radius, seed, 2); + float roughness = (float) input.stoneTool.roughness.getValue(); + Mesh mesh = buildStoneMesh(radius, seed, subdivForRadius(radius), roughness); previewGeom.setMesh(mesh); previewGeom.setLocalRotation(new Quaternion().fromAngles(0f, rotY * FastMath.DEG_TO_RAD, 0f)); + previewGeom.setMaterial(materialForSlot(slot)); } private void placePendingStoneAt(float wx, float wz) { if (pendingStone == null) { return; } if (!Float.isFinite(getHeightAt(wx, wz))) { return; } - PlacedStone stone = new PlacedStone(wx, wz, pendingStone.radius(), pendingStone.rotY(), + PlacedStone stone = new PlacedStone(wx, wz, pendingStone.radius(), + pendingStone.rotX(), pendingStone.rotY(), pendingStone.rotZ(), pendingStone.textureSlot(), pendingStone.sinkFraction(), pendingStone.noiseSeed()); int ci = chunkIndex(wx, wz); if (ci >= 0) { @@ -254,7 +376,7 @@ public class StoneEditorState extends BaseAppState { int slot = random.nextInt(activeSlots); int seed = random.nextInt(); - PlacedStone stone = new PlacedStone(sx, sz, radius, rotY, slot, sinkFrac, seed); + PlacedStone stone = new PlacedStone(sx, sz, radius, 0f, rotY, 0f, slot, sinkFrac, seed); int ci = chunkIndex(sx, sz); if (ci >= 0) { chunkStones[ci].add(stone); @@ -292,7 +414,9 @@ public class StoneEditorState extends BaseAppState { } private void rebuildChunk(int ci) { - // Alte Nodes entfernen + // Rückwärts-Lookup für diesen Chunk bereinigen + geomNameToRef.entrySet().removeIf(e -> e.getValue()[0] == ci); + if (lod0Nodes[ci] != null) { lod0Nodes[ci].detachAllChildren(); stoneRoot.detachChild(lod0Nodes[ci]); } if (lod1Nodes[ci] != null) { lod1Nodes[ci].detachAllChildren(); stoneRoot.detachChild(lod1Nodes[ci]); } @@ -302,32 +426,48 @@ public class StoneEditorState extends BaseAppState { Node n0 = new Node("stone_lod0_" + ci); Node n1 = new Node("stone_lod1_" + ci); - for (PlacedStone s : stones) { + float roughness = (float) input.stoneTool.roughness.getValue(); + for (int si = 0; si < stones.size(); si++) { + PlacedStone s = stones.get(si); float y = stoneWorldY(s); + String name = "stone_ci" + ci + "_si" + si; - Geometry g0 = buildStoneGeom(s, y, 2); // LOD0: 2 Subdiv + int sd0 = subdivForRadius(s.radius()); + Geometry g0 = buildStoneGeom(s, y, sd0, roughness, name); n0.attachChild(g0); + geomNameToRef.put(name, new int[]{ci, si}); - if (s.radius() * 2f > 1f) { // Durchmesser > 1m → LOD1 - Geometry g1 = buildStoneGeom(s, y, 1); // LOD1: 1 Subdiv + if (s.radius() * 2f > 1f) { + Geometry g1 = buildStoneGeom(s, y, Math.max(1, sd0 - 2), roughness, name + "_lod1"); n1.attachChild(g1); } } stoneRoot.attachChild(n0); - stoneRoot.attachChild(n1); lod0Nodes[ci] = n0; - lod1Nodes[ci] = n1; + // Nur wenn LOD1-Geometrien vorhanden – sonst immer LOD0 benutzen + if (n1.getQuantity() > 0) { + stoneRoot.attachChild(n1); + lod1Nodes[ci] = n1; + } else { + lod1Nodes[ci] = null; + } + } - private Geometry buildStoneGeom(PlacedStone s, float worldY, int subdivisions) { - Mesh mesh = buildStoneMesh(s.radius(), s.noiseSeed(), subdivisions); - // Einsinken: Zentrum liegt bei terrain - sinkFraction * radius * 2 + radius (= terrain + radius*(1 - 2*sinkFraction)) + private Geometry buildStoneGeom(PlacedStone s, float worldY, int subdivisions, float roughness) { + return buildStoneGeom(s, worldY, subdivisions, roughness, "stone"); + } + + private Geometry buildStoneGeom(PlacedStone s, float worldY, int subdivisions, float roughness, String name) { + Mesh mesh = buildStoneMesh(s.radius(), s.noiseSeed(), subdivisions, roughness); float yCenter = worldY + s.radius() * (1f - 2f * s.sinkFraction()); - Geometry g = new Geometry("stone", mesh); + Geometry g = new Geometry(name, mesh); g.setMaterial(materialForSlot(s.textureSlot())); g.setLocalTranslation(s.x(), yCenter, s.z()); - g.rotate(0f, s.rotY() * FastMath.DEG_TO_RAD, 0f); + Quaternion rot = new Quaternion(); + rot.fromAngles(s.rotX(), s.rotY() * FastMath.DEG_TO_RAD, s.rotZ()); + g.setLocalRotation(rot); return g; } @@ -344,9 +484,10 @@ public class StoneEditorState extends BaseAppState { if (dist > CULL_DIST) { lod0Nodes[ci].setCullHint(Spatial.CullHint.Always); if (lod1Nodes[ci] != null) lod1Nodes[ci].setCullHint(Spatial.CullHint.Always); - } else if (dist > LOD1_DIST) { + } else if (dist > LOD1_DIST && lod1Nodes[ci] != null) { + // Nur auf LOD1 wechseln wenn LOD1 tatsächlich Geometrien hat lod0Nodes[ci].setCullHint(Spatial.CullHint.Always); - if (lod1Nodes[ci] != null) lod1Nodes[ci].setCullHint(Spatial.CullHint.Inherit); + lod1Nodes[ci].setCullHint(Spatial.CullHint.Inherit); } else { lod0Nodes[ci].setCullHint(Spatial.CullHint.Inherit); if (lod1Nodes[ci] != null) lod1Nodes[ci].setCullHint(Spatial.CullHint.Always); @@ -354,6 +495,309 @@ public class StoneEditorState extends BaseAppState { } } + // ── Gizmo-System ───────────────────────────────────────────────────────── + + // JME3-3.9: Cylinder-Achse ist +Z. geomRot dreht Z auf die Zielachse. + private void buildGizmo() { + arrowX = buildArrowNode(COL_X, "stoneArrowX", + new Vector3f(1, 0, 0), + new Quaternion().fromAngleAxis( FastMath.HALF_PI, Vector3f.UNIT_Y)); // Z→+X + arrowY = buildArrowNode(COL_Y, "stoneArrowY", + new Vector3f(0, 1, 0), + new Quaternion().fromAngleAxis(-FastMath.HALF_PI, Vector3f.UNIT_X)); // Z→+Y + arrowZ = buildArrowNode(COL_Z, "stoneArrowZ", + new Vector3f(0, 0, -1), + new Quaternion()); // Z→-Z + gizmoNode.attachChild(arrowX); + gizmoNode.attachChild(arrowY); + gizmoNode.attachChild(arrowZ); + + // Torus liegt default in XY-Ebene (Symmetrieachse=Z). Node-Rotation dreht in Zielebene. + ringX = buildRingNode(COL_X, "stoneRingX", + new Quaternion().fromAngleAxis( FastMath.HALF_PI, Vector3f.UNIT_Y)); // XY→YZ (dreht um X) + ringY = buildRingNode(COL_Y, "stoneRingY", + new Quaternion().fromAngleAxis( FastMath.HALF_PI, Vector3f.UNIT_X)); // XY→XZ (dreht um Y) + ringZ = buildRingNode(COL_Z, "stoneRingZ", + new Quaternion()); // XY-Ebene (dreht um Z) + gizmoNode.attachChild(ringX); + gizmoNode.attachChild(ringY); + gizmoNode.attachChild(ringZ); + } + + private Node buildArrowNode(ColorRGBA color, String name, Vector3f dir, Quaternion geomRot) { + Material mat = gizmoMat(color); + + Cylinder shaftMesh = new Cylinder(2, 10, SHAFT_RADIUS, SHAFT_LEN, false); + Geometry shaft = new Geometry(name + "_shaft", shaftMesh); + shaft.setLocalRotation(geomRot); + shaft.setLocalTranslation(dir.mult(SHAFT_LEN * 0.5f)); + shaft.setMaterial(mat); + shaft.setQueueBucket(RenderQueue.Bucket.Translucent); + + Cylinder headMesh = new Cylinder(2, 10, HEAD_RADIUS, HEAD_LEN, true); + Geometry head = new Geometry(name + "_head", headMesh); + head.setLocalRotation(geomRot); + head.setLocalTranslation(dir.mult(SHAFT_LEN + HEAD_LEN * 0.5f)); + head.setMaterial(mat); + head.setQueueBucket(RenderQueue.Bucket.Translucent); + + Node node = new Node(name); + node.attachChild(shaft); + node.attachChild(head); + return node; + } + + private Node buildRingNode(ColorRGBA color, String name, Quaternion nodeRot) { + Material mat = gizmoMat(color); + Geometry ring = new Geometry(name + "_ring", new Torus(48, 10, RING_TUBE, RING_RADIUS)); + ring.setMaterial(mat); + ring.setQueueBucket(RenderQueue.Bucket.Translucent); + Node node = new Node(name); + node.setLocalRotation(nodeRot); + node.attachChild(ring); + return node; + } + + private Material gizmoMat(ColorRGBA color) { + Material m = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + m.setColor("Color", color); + m.getAdditionalRenderState().setDepthTest(false); + return m; + } + + private int pickGizmo(Ray ray) { + CollisionResults hits = new CollisionResults(); + arrowX.collideWith(ray, hits); if (hits.size() > 0) return 0; + hits.clear(); arrowY.collideWith(ray, hits); if (hits.size() > 0) return 1; + hits.clear(); arrowZ.collideWith(ray, hits); if (hits.size() > 0) return 2; + return -1; + } + + private int pickRing(Ray ray) { + CollisionResults hits = new CollisionResults(); + ringX.collideWith(ray, hits); if (hits.size() > 0) return 0; + hits.clear(); ringY.collideWith(ray, hits); if (hits.size() > 0) return 1; + hits.clear(); ringZ.collideWith(ray, hits); if (hits.size() > 0) return 2; + return -1; + } + + private void updateGizmoPosition() { + if (selChunk < 0 || selIdx < 0) return; + PlacedStone s = chunkStones[selChunk].get(selIdx); + float worldY = stoneWorldY(s); + float yCenter = worldY + s.radius() * (1f - 2f * s.sinkFraction()); + Vector3f pos = new Vector3f(s.x(), yCenter, s.z()); + gizmoNode.setLocalTranslation(pos); + // Skalierung: Stein-Radius / RING_RADIUS — exakt wie SceneObjectState (maxR / RING_RADIUS) + gizmoNode.setLocalScale(Math.max(0.01f, s.radius() / RING_RADIUS)); + } + + private void updateGizmoVisibility() { + boolean move = (selChunk >= 0) && (input.stoneEditTool == SharedInput.EDIT_TOOL_MOVE); + boolean rot = (selChunk >= 0) && (input.stoneEditTool == SharedInput.EDIT_TOOL_ROTATE); + arrowX.setCullHint(move ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + arrowY.setCullHint(move ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + arrowZ.setCullHint(move ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + ringX.setCullHint(rot ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + ringY.setCullHint(rot ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + ringZ.setCullHint(rot ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + gizmoNode.setCullHint((move || rot) ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + } + + private static Vector3f arrowWorldDir(int idx) { + return switch (idx) { + case 0 -> new Vector3f(1, 0, 0); + case 1 -> new Vector3f(0, 1, 0); + default -> new Vector3f(0, 0, -1); + }; + } + + private void selectStone(int ci, int si) { + selChunk = ci; + selIdx = si; + PlacedStone s = chunkStones[ci].get(si); + float worldY = stoneWorldY(s); + float yCenter = worldY + s.radius() * (1f - 2f * s.sinkFraction()); + selHighlight.setMesh(buildStoneMesh(s.radius(), s.noiseSeed(), + subdivForRadius(s.radius()), (float) input.stoneTool.roughness.getValue())); + selHighlight.setLocalTranslation(s.x(), yCenter, s.z()); + selHighlight.setLocalRotation(new Quaternion().fromAngles( + s.rotX(), s.rotY() * FastMath.DEG_TO_RAD, s.rotZ())); + selHighlight.setLocalScale(1.02f); + selHighlight.setCullHint(Spatial.CullHint.Inherit); + updateGizmoPosition(); + updateGizmoVisibility(); + broadcastSelection(); + } + + private void deselectStone() { + selChunk = -1; + selIdx = -1; + activeGizmo = -1; + activeRing = -1; + selHighlight.setCullHint(Spatial.CullHint.Always); + gizmoNode.setCullHint(Spatial.CullHint.Always); + broadcastSelection(); + } + + private void broadcastSelection() { + if (selChunk < 0 || selIdx < 0) { + input.stoneSelectedInfo = null; + } else { + PlacedStone s = chunkStones[selChunk].get(selIdx); + input.stoneSelectedInfo = s.x() + "|" + s.z() + "|" + + s.rotX() + "|" + s.rotY() + "|" + s.rotZ() + "|" + s.radius(); + } + input.stoneSelectionChanged = true; + } + + private void processEditModeClick(float jx, float jy) { + Vector3f origin = cam.getWorldCoordinates(new Vector2f(jx, jy), 0f); + Vector3f target = cam.getWorldCoordinates(new Vector2f(jx, jy), 1f); + Ray ray = new Ray(origin, target.subtractLocal(origin).normalizeLocal()); + + // 1. Gizmo-Pick (nur wenn etwas selektiert ist und Gizmo sichtbar) + if (selChunk >= 0) { + if (input.stoneEditTool == SharedInput.EDIT_TOOL_MOVE) { + int g = pickGizmo(ray); + if (g >= 0) { activeGizmo = g; return; } + } else { + int r = pickRing(ray); + if (r >= 0) { activeRing = r; return; } + } + } + activeGizmo = -1; + activeRing = -1; + + // 2. Stein-Pick + CollisionResults res = new CollisionResults(); + stoneRoot.collideWith(ray, res); + if (res.size() > 0) { + String rawName = res.getClosestCollision().getGeometry().getName(); + // LOD1-Geometrien heißen "_lod1" – Suffix abschneiden für Lookup + String name = rawName.endsWith("_lod1") + ? rawName.substring(0, rawName.length() - 5) : rawName; + int[] ref = geomNameToRef.get(name); + if (ref != null) { selectStone(ref[0], ref[1]); return; } + } + + // 3. Kein Treffer → Selektion aufheben + deselectStone(); + } + + private void handleMoveDrag(SharedInput.ObjectDrag drag) { + if (selChunk < 0 || selIdx < 0 || activeGizmo < 0) return; + + PlacedStone s = chunkStones[selChunk].get(selIdx); + + // Pfeilrichtung auf Bildschirm projizieren (exakt wie SceneObjectState) + Vector3f arrowDir = arrowWorldDir(activeGizmo); + Vector3f origin = gizmoNode.getWorldTranslation(); + Vector3f scrO = cam.getScreenCoordinates(origin); + Vector3f scrT_ = cam.getScreenCoordinates(origin.add(arrowDir)); + float sdx = scrT_.x - scrO.x; + float sdy = -(scrT_.y - scrO.y); // JME Y↑, Drag Y↓ → invertieren + float screenLen2 = sdx * sdx + sdy * sdy; + + float magnitude; + if (screenLen2 < 0.01f) { + magnitude = -drag.dy() * PX_PER_WE; + } else { + magnitude = (drag.dx() * sdx + drag.dy() * sdy) / screenLen2; + } + + Vector3f worldDelta = arrowDir.mult(magnitude); + float newX = s.x() + worldDelta.x; + float newZ = s.z() + worldDelta.z; + if (!Float.isFinite(getHeightAt(newX, newZ))) return; + + // Y-Achse: sinkFraction anpassen (nach oben = weniger einsinken) + float newSinkFraction = s.sinkFraction() - worldDelta.y / (2f * s.radius()); + + PlacedStone updated = new PlacedStone(newX, newZ, s.radius(), + s.rotX(), s.rotY(), s.rotZ(), s.textureSlot(), newSinkFraction, s.noiseSeed()); + + int newCi = chunkIndex(newX, newZ); + if (newCi < 0) return; + + if (newCi != selChunk) { + chunkStones[selChunk].remove(selIdx); + dirtyChunks[selChunk] = true; + chunkStones[newCi].add(updated); + dirtyChunks[newCi] = true; + selChunk = newCi; + selIdx = chunkStones[newCi].size() - 1; + } else { + chunkStones[selChunk].set(selIdx, updated); + dirtyChunks[selChunk] = true; + } + modified = true; + + float nY = stoneWorldY(updated); + float nCenter = nY + updated.radius() * (1f - 2f * updated.sinkFraction()); + selHighlight.setLocalTranslation(updated.x(), nCenter, updated.z()); + broadcastSelection(); + } + + private void handleRotateDrag(SharedInput.ObjectDrag drag) { + if (selChunk < 0 || selIdx < 0 || activeRing < 0) return; + PlacedStone s = chunkStones[selChunk].get(selIdx); + + // Tangent-basierte Winkel-Projektion (exakt wie SceneObjectState) + Vector3f rotAxis = switch (activeRing) { + case 0 -> new Vector3f(1, 0, 0); // Welt-X + case 1 -> new Vector3f(0, 1, 0); // Welt-Y + default -> new Vector3f(0, 0, 1); // Welt-Z + }; + Vector3f center = gizmoNode.getWorldTranslation(); + Vector3f toCam = cam.getLocation().subtract(center).normalizeLocal(); + Vector3f tangent = rotAxis.cross(toCam); + float tLen = tangent.length(); + float angle; + if (tLen < 1e-4f) { + angle = drag.dx() * DEG_PER_PX * FastMath.DEG_TO_RAD; + } else { + tangent.divideLocal(tLen); + Vector3f scrC = cam.getScreenCoordinates(center); + Vector3f scrT = cam.getScreenCoordinates(center.add(tangent)); + float sdx = scrT.x - scrC.x; + float sdy = -(scrT.y - scrC.y); + float sl2 = sdx * sdx + sdy * sdy; + if (sl2 < 0.5f) { + angle = drag.dx() * DEG_PER_PX * FastMath.DEG_TO_RAD; + } else { + float proj = (drag.dx() * sdx + drag.dy() * sdy) / sl2; + angle = proj / RING_RADIUS; + } + } + + float newRotX = s.rotX(); + float newRotY = s.rotY(); + float newRotZ = s.rotZ(); + switch (activeRing) { + case 0 -> newRotX += angle; + case 1 -> newRotY += angle * FastMath.RAD_TO_DEG; // rotY in Grad gespeichert + case 2 -> newRotZ += angle; + } + + PlacedStone updated = new PlacedStone(s.x(), s.z(), s.radius(), + newRotX, newRotY, newRotZ, s.textureSlot(), s.sinkFraction(), s.noiseSeed()); + chunkStones[selChunk].set(selIdx, updated); + dirtyChunks[selChunk] = true; + modified = true; + selHighlight.setLocalRotation(new Quaternion().fromAngles( + newRotX, newRotY * FastMath.DEG_TO_RAD, newRotZ)); + broadcastSelection(); + } + + private void deleteSelectedStone() { + if (selChunk < 0 || selIdx < 0) return; + chunkStones[selChunk].remove(selIdx); + dirtyChunks[selChunk] = true; + modified = true; + deselectStone(); + } + // ── Mesh-Generierung: Icosphere ─────────────────────────────────────────── private static final float PHI = (1f + (float) Math.sqrt(5)) / 2f; @@ -388,7 +832,7 @@ public class StoneEditorState extends BaseAppState { * Erzeugt einen Stein-Mesh mit der gegebenen Anzahl an Icosphere-Unterteilungen. * Noise-Verformung wird durch noiseSeed deterministisch gesteuert. */ - private static Mesh buildStoneMesh(float radius, int noiseSeed, int subdivisions) { + private static Mesh buildStoneMesh(float radius, int noiseSeed, int subdivisions, float roughness) { // 1. Icosphere aufbauen und unterteilen List verts = new ArrayList<>(Arrays.asList(ICO_V)); List faces = new ArrayList<>(Arrays.asList(ICO_F)); @@ -410,57 +854,111 @@ public class StoneEditorState extends BaseAppState { verts = nv; faces = nf; } - // 2. Noise-Verformung (entlang Vertex-Normal = Einheitskugel-Richtung) - Random rng = new Random(noiseSeed); - float ox = rng.nextFloat() * 50f, oy = rng.nextFloat() * 50f, oz = rng.nextFloat() * 50f; - float freq = 2.5f + (Math.abs(noiseSeed) % 3); // 2.5–4.5 - float amp = 0.22f; // ±22 % Verformung + // 2. Variantenreiche Verformung: individuelle Achsenskalierung + 3 Oktaven + Random rng = new Random(noiseSeed); + float ox = rng.nextFloat() * 50f; + float oy = rng.nextFloat() * 50f; + float oz = rng.nextFloat() * 50f; + float flattenY = 0.50f + rng.nextFloat() * 0.45f; + float scaleX = 0.80f + rng.nextFloat() * 0.70f; + float scaleZ = 0.80f + rng.nextFloat() * 0.70f; + float freq = 1.5f + (Math.abs(noiseSeed) % 4); + float amp = roughness * 0.60f; - int nv = verts.size(); - float[] pos = new float[nv * 3]; - float[] nor = new float[nv * 3]; - float[] uv = new float[nv * 2]; - - for (int i = 0; i < nv; i++) { - float[] unit = verts.get(i); - float n = smoothNoise3D(unit[0] * freq + ox, unit[1] * freq + oy, unit[2] * freq + oz); - float r = radius * (1f + n * amp); - pos[i*3] = unit[0] * r; - pos[i*3+1] = unit[1] * r; - pos[i*3+2] = unit[2] * r; - // UV: sphärisch - uv[i*2] = (float)(Math.atan2(unit[2], unit[0]) / (2 * Math.PI) + 0.5); - uv[i*2+1] = (float)(Math.asin(Math.max(-1, Math.min(1, unit[1]))) / Math.PI + 0.5); + int numVerts = verts.size(); + float[] def = new float[numVerts * 3]; + for (int i = 0; i < numVerts; i++) { + float[] u = verts.get(i); + float n1 = smoothNoise3D(u[0]*freq+ox, u[1]*freq+oy, u[2]*freq+oz); + float n2 = smoothNoise3D(u[0]*freq*2.5f+ox+11f, u[1]*freq*2.5f+oy+11f, u[2]*freq*2.5f+oz+11f); + float n3 = smoothNoise3D(u[0]*freq*5.5f+ox+23f, u[1]*freq*5.5f+oy+23f, u[2]*freq*5.5f+oz+23f); + float r = radius * (1f + (n1 * 0.55f + n2 * 0.30f + n3 * 0.15f) * amp); + def[i*3] = u[0] * r * scaleX; + def[i*3+1] = u[1] * r * flattenY; + def[i*3+2] = u[2] * r * scaleZ; } - // 3. Vertex-Normalen aus Dreiecken akkumulieren - for (int[] f : faces) { - int ai = f[0]*3, bi = f[1]*3, ci = f[2]*3; - float ax = pos[ai], ay = pos[ai+1], az = pos[ai+2]; - float bx = pos[bi], by = pos[bi+1], bz = pos[bi+2]; - float cx2= pos[ci], cy = pos[ci+1], cz = pos[ci+2]; - float nx = (by-ay)*(cz-az) - (bz-az)*(cy-ay); - float ny = (bz-az)*(cx2-ax) - (bx-ax)*(cz-az); - float nz = (bx-ax)*(cy-ay) - (by-ay)*(cx2-ax); - for (int vi : f) { nor[vi*3] += nx; nor[vi*3+1] += ny; nor[vi*3+2] += nz; } - } - for (int i = 0; i < nv; i++) { - float len = (float) Math.sqrt(nor[i*3]*nor[i*3] + nor[i*3+1]*nor[i*3+1] + nor[i*3+2]*nor[i*3+2]); - if (len > 0) { nor[i*3] /= len; nor[i*3+1] /= len; nor[i*3+2] /= len; } + // 3. Taubin-Glättung (lambda-mu, kein Mesh-Schrumpfen) + { + List> adj = new ArrayList<>(numVerts); + for (int i = 0; i < numVerts; i++) adj.add(new ArrayList<>(6)); + for (int[] f : faces) { + for (int i = 0; i < 3; i++) { + int a = f[i], b = f[(i + 1) % 3]; + List la = adj.get(a), lb = adj.get(b); + if (!la.contains(b)) la.add(b); + if (!lb.contains(a)) lb.add(a); + } + } + float lambda = 0.5f, mu = -0.53f; + for (int iter = 0; iter < 6; iter++) { + float step = (iter % 2 == 0) ? lambda : mu; + float[] next = new float[numVerts * 3]; + for (int i = 0; i < numVerts; i++) { + List nb = adj.get(i); + float sx = 0, sy = 0, sz = 0; + for (int n : nb) { sx += def[n*3]; sy += def[n*3+1]; sz += def[n*3+2]; } + float inv = nb.isEmpty() ? 1f : 1f / nb.size(); + next[i*3] = def[i*3] + step * (sx*inv - def[i*3]); + next[i*3+1] = def[i*3+1] + step * (sy*inv - def[i*3+1]); + next[i*3+2] = def[i*3+2] + step * (sz*inv - def[i*3+2]); + } + def = next; + } } - // 4. JME3-Mesh zusammenbauen - FloatBuffer pb = BufferUtils.createFloatBuffer(pos); - FloatBuffer nb = BufferUtils.createFloatBuffer(nor); - FloatBuffer ub = BufferUtils.createFloatBuffer(uv); - java.nio.IntBuffer ib = BufferUtils.createIntBuffer(faces.size() * 3); - for (int[] f : faces) ib.put(f[0]).put(f[1]).put(f[2]); + // 4. Flat-Shading: jedes Dreieck bekommt eigene Vertices mit Flächennormale + Box-Map UV + int fc = faces.size(); + float[] pos = new float[fc * 9]; + float[] nor = new float[fc * 9]; + float[] uv = new float[fc * 6]; + java.nio.IntBuffer ib = BufferUtils.createIntBuffer(fc * 3); + for (int i = 0; i < fc * 3; i++) ib.put(i); ib.flip(); + float uvScale = 0.5f / radius; + for (int fi = 0; fi < fc; fi++) { + int[] f = faces.get(fi); + float ax = def[f[0]*3], ay = def[f[0]*3+1], az = def[f[0]*3+2]; + float bx = def[f[1]*3], by = def[f[1]*3+1], bz = def[f[1]*3+2]; + float cx = def[f[2]*3], cy = def[f[2]*3+1], cz = def[f[2]*3+2]; + float ex = bx-ax, ey = by-ay, ez = bz-az; + float gx = cx-ax, gy = cy-ay, gz = cz-az; + float nx = ey*gz-ez*gy, ny = ez*gx-ex*gz, nz = ex*gy-ey*gx; + float nlen = (float) Math.sqrt(nx*nx+ny*ny+nz*nz); + if (nlen > 0) { nx/=nlen; ny/=nlen; nz/=nlen; } + + float anx = Math.abs(nx), any = Math.abs(ny), anz = Math.abs(nz); + float uA, vA, uB, vB, uC, vC; + if (anx >= any && anx >= anz) { + uA=az*uvScale+0.5f; vA=ay*uvScale+0.5f; + uB=bz*uvScale+0.5f; vB=by*uvScale+0.5f; + uC=cz*uvScale+0.5f; vC=cy*uvScale+0.5f; + } else if (any >= anx && any >= anz) { + uA=ax*uvScale+0.5f; vA=az*uvScale+0.5f; + uB=bx*uvScale+0.5f; vB=bz*uvScale+0.5f; + uC=cx*uvScale+0.5f; vC=cz*uvScale+0.5f; + } else { + uA=ax*uvScale+0.5f; vA=ay*uvScale+0.5f; + uB=bx*uvScale+0.5f; vB=by*uvScale+0.5f; + uC=cx*uvScale+0.5f; vC=cy*uvScale+0.5f; + } + int p3 = fi*9, u2 = fi*6; + pos[p3]=ax; pos[p3+1]=ay; pos[p3+2]=az; + pos[p3+3]=bx; pos[p3+4]=by; pos[p3+5]=bz; + pos[p3+6]=cx; pos[p3+7]=cy; pos[p3+8]=cz; + nor[p3]=nx; nor[p3+1]=ny; nor[p3+2]=nz; + nor[p3+3]=nx; nor[p3+4]=ny; nor[p3+5]=nz; + nor[p3+6]=nx; nor[p3+7]=ny; nor[p3+8]=nz; + uv[u2]=uA; uv[u2+1]=vA; + uv[u2+2]=uB; uv[u2+3]=vB; + uv[u2+4]=uC; uv[u2+5]=vC; + } + Mesh mesh = new Mesh(); - mesh.setBuffer(Type.Position, 3, pb); - mesh.setBuffer(Type.Normal, 3, nb); - mesh.setBuffer(Type.TexCoord, 2, ub); + mesh.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(pos)); + mesh.setBuffer(Type.Normal, 3, BufferUtils.createFloatBuffer(nor)); + mesh.setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(uv)); mesh.setBuffer(Type.Index, 3, ib); mesh.updateBound(); return mesh; @@ -498,6 +996,13 @@ public class StoneEditorState extends BaseAppState { private static float lerp(float a, float b, float t) { return a + (b-a)*t; } + /** Unterteilungsstufe proportional zum Steinradius: kleiner Kiesel → weniger Faces, großer Boulder → mehr. */ + private static int subdivForRadius(float radius) { + // r≈0.10m→2 (320), r≈0.25m→3 (1280), r≈0.50m→4 (5120), r≥1m→5 (20480) + double logR = Math.log(Math.max(0.01, radius) / 0.1) / Math.log(2.0); + return Math.max(1, Math.min(5, (int) Math.round(logR) + 2)); + } + // ── Materialien ─────────────────────────────────────────────────────────── private void reloadMaterials() { @@ -534,8 +1039,6 @@ public class StoneEditorState extends BaseAppState { return defaultMat; } - // ── Brush-Indicator ─────────────────────────────────────────────────────── - private void updateBrushIndicator() { if (input.activeLayer != SharedInput.LAYER_STONE || input.mouseScreenX < 0 || isSingleStoneMode()) { @@ -573,7 +1076,6 @@ public class StoneEditorState extends BaseAppState { mesh.setBuffer(Type.Position, 3, pos); mesh.setBuffer(VertexBuffer.Type.Index, 3, idx); mesh.updateBound(); - Geometry geo = new Geometry("stoneBrushIndicator", mesh); Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); mat.setColor("Color", new ColorRGBA(0.9f, 0.5f, 0.1f, 0.4f)); @@ -587,14 +1089,9 @@ public class StoneEditorState extends BaseAppState { } private Geometry buildPreviewGeom() { - Mesh mesh = buildStoneMesh(0.5f, 0, 2); + Mesh mesh = buildStoneMesh(0.5f, 0, subdivForRadius(0.5f), 0.40f); Geometry g = new Geometry("stonePreview", mesh); - Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); - mat.setColor("Color", new ColorRGBA(0.75f, 0.82f, 0.95f, 0.5f)); - mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); - mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); - g.setQueueBucket(RenderQueue.Bucket.Transparent); - g.setMaterial(mat); + g.setMaterial(defaultMat); g.setCullHint(Spatial.CullHint.Always); return g; } @@ -705,6 +1202,8 @@ public class StoneEditorState extends BaseAppState { input.stoneTool.texturePaths[i] = paths[i] != null ? paths[i] : ""; reloadMaterials(); } + input.stoneTool.roughness.setValue(data.roughness()); + lastRoughness = data.roughness(); for (PlacedStone s : data.stones()) { int ci = chunkIndex(s.x(), s.z()); if (ci >= 0) { chunkStones[ci].add(s); dirtyChunks[ci] = true; } @@ -720,7 +1219,8 @@ public class StoneEditorState extends BaseAppState { try { List all = new ArrayList<>(); for (List list : chunkStones) all.addAll(list); - PlacedStoneIO.save(new PlacedStoneIO.StoneData(input.stoneTool.texturePaths, all)); + float roughness = (float) input.stoneTool.roughness.getValue(); + PlacedStoneIO.save(new PlacedStoneIO.StoneData(input.stoneTool.texturePaths, roughness, all)); modified = false; log.info("[StoneEditorState] {} Steine gespeichert.", all.size()); } catch (Exception e) { diff --git a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java index ee9db4a..2e2ff70 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java @@ -1738,6 +1738,7 @@ public class TerrainEditorState extends BaseAppState { || layer == SharedInput.LAYER_LOCATION_ZONES || layer == SharedInput.LAYER_PLAY_TOOL || layer == SharedInput.LAYER_VOXEL || layer == SharedInput.LAYER_STONE + || layer == SharedInput.LAYER_STONE_EDIT || layer == SharedInput.LAYER_BED_LIEGE || layer == SharedInput.LAYER_BENCH_SITZ || layer == SharedInput.LAYER_AUSWAHL diff --git a/blight-editor/src/main/java/de/blight/editor/tool/StoneTool.java b/blight-editor/src/main/java/de/blight/editor/tool/StoneTool.java index 10e3ef2..2e2e678 100644 --- a/blight-editor/src/main/java/de/blight/editor/tool/StoneTool.java +++ b/blight-editor/src/main/java/de/blight/editor/tool/StoneTool.java @@ -8,6 +8,7 @@ public class StoneTool extends EditorTool { public final ToolParameter minSize = new ToolParameter("Min-Größe (m)", 0.3, 0.1, 5.0); public final ToolParameter maxSize = new ToolParameter("Max-Größe (m)", 1.5, 0.2, 10.0); public final ToolParameter density = new ToolParameter("Dichte (Klick)", 4.0, 1.0, 30.0); + public final ToolParameter roughness = new ToolParameter("Rauigkeit", 0.40, 0.0, 1.0); /** Texturpfade für bis zu 3 Slots; "" = kein Texture (Fallback: Grau). */ public volatile String[] texturePaths = new String[]{"", "", ""}; @@ -20,6 +21,6 @@ public class StoneTool extends EditorTool { @Override public ToolParameter getBrushRadius() { return brushRadius; } @Override public List getParameters() { - return List.of(brushRadius, minSize, maxSize, density); + return List.of(brushRadius, minSize, maxSize, density, roughness); } } diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/stone/StoneController.java b/blight-editor/src/main/java/de/blight/editor/toolset/stone/StoneController.java new file mode 100644 index 0000000..ac29dd6 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/toolset/stone/StoneController.java @@ -0,0 +1,234 @@ +package de.blight.editor.toolset.stone; + +import de.blight.common.PlacedStoneIO; +import de.blight.editor.SharedInput; +import de.blight.editor.ToolContext; +import de.blight.editor.toolset.UiHelper; +import de.blight.editor.ui.TextureChooser; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.scene.control.*; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.*; + +import java.io.File; + +public class StoneController { + + private final ToolContext ctx; + private final SharedInput input; + + // "Bearbeiten"-Tab Widgets – aktualisiert von onFrame() + private Label selInfoLabel; + private Label selPosLabel; + private Label selRotLabel; + private Button deleteBtn; + + public StoneController(ToolContext ctx) { + this.ctx = ctx; + this.input = ctx.input(); + } + + public void onFrame() { + if (!input.stoneSelectionChanged) return; + input.stoneSelectionChanged = false; + Platform.runLater(this::refreshSelectionUI); + } + + /** Malen-Panel; setzt activeLayer=LAYER_STONE. */ + public VBox buildPanel() { + input.activeLayer = SharedInput.LAYER_STONE; + input.activeTool = input.stoneTool; + + VBox panel = new VBox(8); + panel.setPadding(new Insets(10)); + panel.setPrefWidth(270); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + + Label title = new Label("Steine"); + title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111;"); + panel.getChildren().addAll(title, new Separator()); + panel.getChildren().add(buildMalenPanel()); + + return panel; + } + + /** Bearbeiten-Panel via F1; setzt activeLayer=LAYER_STONE_EDIT. */ + public void activateEditMode() { + input.activeLayer = SharedInput.LAYER_STONE_EDIT; + + VBox panel = new VBox(8); + panel.setPadding(new Insets(10)); + panel.setPrefWidth(270); + panel.setStyle("-fx-background-color: #f0f0f0; -fx-border-color: #ccc; -fx-border-width: 0 0 0 1;"); + + Label title = new Label("Steine bearbeiten"); + title.setStyle("-fx-font-weight: bold; -fx-font-size: 13; -fx-text-fill: #111;"); + panel.getChildren().addAll(title, new Separator()); + panel.getChildren().add(buildBearbeitenPanel()); + refreshSelectionUI(); + + ctx.setRightPanel().accept(panel); + } + + private VBox buildMalenPanel() { + VBox box = new VBox(6); + + // Parameter-Slider + for (var p : input.stoneTool.getParameters()) { + box.getChildren().add(UiHelper.paramSlider(p.getName(), p.getMin(), p.getMax(), p.getValue(), + v -> p.setValue(v))); + } + box.getChildren().add(new Separator()); + + // Textur-Slots 0-2 + Label texTitle = new Label("Texturen"); + texTitle.setStyle("-fx-font-weight: bold; -fx-text-fill: #111;"); + box.getChildren().add(texTitle); + + for (int i = 0; i < PlacedStoneIO.SLOT_COUNT; i++) { + box.getChildren().add(buildTextureSlotRow(i)); + } + + return box; + } + + private HBox buildTextureSlotRow(int slot) { + ImageView thumb = new ImageView(); + thumb.setFitWidth(36); + thumb.setFitHeight(36); + thumb.setPreserveRatio(true); + refreshThumb(thumb, slot); + + Label pathLabel = new Label(shortName(input.stoneTool.texturePaths[slot])); + pathLabel.setStyle("-fx-text-fill: #555; -fx-font-size: 10;"); + pathLabel.setMaxWidth(120); + pathLabel.setEllipsisString("..."); + + Button chooseBtn = new Button("..."); + chooseBtn.setTooltip(new Tooltip("Textur wählen (Slot " + (slot + 1) + ")")); + chooseBtn.setOnAction(e -> { + TextureChooser chooser = new TextureChooser(ctx.assetRoot(), true); + chooser.initOwner(ctx.stageSupplier().get()); + chooser.showAndWait().ifPresent(path -> { + input.stoneTool.texturePaths[slot] = path; + input.stoneTexturesChanged = true; + pathLabel.setText(shortName(path)); + refreshThumb(thumb, slot); + }); + }); + + Button clearBtn = new Button("X"); + clearBtn.setTooltip(new Tooltip("Slot leeren")); + clearBtn.setOnAction(e -> { + input.stoneTool.texturePaths[slot] = ""; + input.stoneTexturesChanged = true; + pathLabel.setText(""); + thumb.setImage(null); + }); + + Label slotLabel = new Label("Slot " + (slot + 1)); + slotLabel.setStyle("-fx-font-size: 10; -fx-text-fill: #333;"); + VBox info = new VBox(2, slotLabel, pathLabel); + HBox row = new HBox(6, thumb, info, new Region(), chooseBtn, clearBtn); + HBox.setHgrow(info, Priority.ALWAYS); + row.setAlignment(javafx.geometry.Pos.CENTER_LEFT); + return row; + } + + private VBox buildBearbeitenPanel() { + VBox box = new VBox(8); + + // Werkzeug-Toggle: Bewegen / Rotieren + ToggleButton btnBewegen = new ToggleButton("Bewegen"); + ToggleButton btnRotieren = new ToggleButton("Rotieren"); + btnBewegen.setMaxWidth(Double.MAX_VALUE); + btnRotieren.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(btnBewegen, Priority.ALWAYS); + HBox.setHgrow(btnRotieren, Priority.ALWAYS); + ToggleGroup tg = new ToggleGroup(); + btnBewegen.setToggleGroup(tg); + btnRotieren.setToggleGroup(tg); + if (input.stoneEditTool == SharedInput.EDIT_TOOL_MOVE) btnBewegen.setSelected(true); + else btnRotieren.setSelected(true); + btnBewegen.setOnAction(e -> { + if (!btnBewegen.isSelected()) { btnBewegen.setSelected(true); return; } + input.stoneEditTool = SharedInput.EDIT_TOOL_MOVE; + }); + btnRotieren.setOnAction(e -> { + if (!btnRotieren.isSelected()) { btnRotieren.setSelected(true); return; } + input.stoneEditTool = SharedInput.EDIT_TOOL_ROTATE; + }); + HBox toolBar = new HBox(4, btnBewegen, btnRotieren); + toolBar.setMaxWidth(Double.MAX_VALUE); + + box.getChildren().add(toolBar); + box.getChildren().add(new Separator()); + + // Selektion-Info + selInfoLabel = new Label("Kein Stein selektiert"); + selInfoLabel.setStyle("-fx-text-fill: #555; -fx-font-size: 11;"); + selPosLabel = new Label(""); + selPosLabel.setStyle("-fx-text-fill: #333; -fx-font-size: 11;"); + selRotLabel = new Label(""); + selRotLabel.setStyle("-fx-text-fill: #333; -fx-font-size: 11;"); + box.getChildren().addAll(selInfoLabel, selPosLabel, selRotLabel); + + box.getChildren().add(new Separator()); + + // Löschen + deleteBtn = new Button("Stein loeschen"); + deleteBtn.setMaxWidth(Double.MAX_VALUE); + deleteBtn.setDisable(true); + deleteBtn.setOnAction(e -> input.stoneDeleteRequested = true); + box.getChildren().add(deleteBtn); + + Label hint = new Label("Klick = Stein auswaehlen\nZiehen an Pfeil/Ring = verschieben/drehen"); + hint.setStyle("-fx-text-fill: #888; -fx-font-size: 10;"); + box.getChildren().add(hint); + + return box; + } + + private void refreshSelectionUI() { + String info = input.stoneSelectedInfo; + if (selInfoLabel == null) return; + if (info == null) { + selInfoLabel.setText("Kein Stein selektiert"); + selPosLabel.setText(""); + selRotLabel.setText(""); + if (deleteBtn != null) deleteBtn.setDisable(true); + } else { + String[] parts = info.split("\\|"); + if (parts.length >= 6) { + float x = Float.parseFloat(parts[0]); + float z = Float.parseFloat(parts[1]); + float rotX = Float.parseFloat(parts[2]); + float rotY = Float.parseFloat(parts[3]); + float rotZ = Float.parseFloat(parts[4]); + float r = Float.parseFloat(parts[5]); + selInfoLabel.setText(String.format("Radius: %.2f m", r)); + selPosLabel.setText(String.format("X: %.1f Z: %.1f", x, z)); + selRotLabel.setText(String.format("rotX: %.1f rotY: %.1f rotZ: %.1f", + (float)Math.toDegrees(rotX), rotY, (float)Math.toDegrees(rotZ))); + } + if (deleteBtn != null) deleteBtn.setDisable(false); + } + } + + private void refreshThumb(ImageView iv, int slot) { + String path = input.stoneTool.texturePaths[slot]; + if (path == null || path.isEmpty()) { iv.setImage(null); return; } + try { + File f = ctx.assetRoot().resolve(path).toFile(); + if (f.exists()) iv.setImage(new Image(f.toURI().toString(), 36, 36, true, true)); + } catch (Exception ex) { iv.setImage(null); } + } + + private static String shortName(String path) { + if (path == null || path.isEmpty()) return ""; + int sl = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')); + return sl >= 0 ? path.substring(sl + 1) : path; + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/topbar/TopBarController.java b/blight-editor/src/main/java/de/blight/editor/toolset/topbar/TopBarController.java index 6599937..7f73452 100644 --- a/blight-editor/src/main/java/de/blight/editor/toolset/topbar/TopBarController.java +++ b/blight-editor/src/main/java/de/blight/editor/toolset/topbar/TopBarController.java @@ -28,6 +28,7 @@ public class TopBarController { de.blight.editor.toolset.game.GameController gameController, de.blight.editor.toolset.terrain.TerrainPanelController terrainController, de.blight.editor.toolset.object.ObjectController objectController, + de.blight.editor.toolset.stone.StoneController stoneController, Runnable switchToVegetationGenerator, Runnable switchToEzTree, Runnable switchToTripo, @@ -39,7 +40,8 @@ public class TopBarController { private final SharedInput input; private ToggleGroup layerGroup; - private Runnable pendingTabSwitchAction = null; + private Runnable pendingTabSwitchAction = null; + private boolean suppressStonePanelBuild = false; // ToggleButtons – pre-created so getters work before buildTop() is called private final ToggleButton auswahlBtn = new ToggleButton("[k] Auswahl"); @@ -117,6 +119,7 @@ public class TopBarController { public ToggleButton getTextureBtn() { return textureBtn; } public ToggleButton getGrassBtn() { return grassBtn; } public ToggleButton getStoneBtn() { return stoneBtn; } + public void selectStoneBtnSilently() { suppressStonePanelBuild = true; stoneBtn.setSelected(true); } public ToggleButton getObjPlaceBtn() { return objPlaceBtn; } public ToggleButton getAtmosphaereBtn() { return atmosphaereBtn; } public ToggleButton getWasserMergedBtn(){ return wasserMergedBtn; } @@ -293,12 +296,10 @@ public class TopBarController { }, ownerStage)); grassBtn.setOnAction(e -> guardVoxelExit( () -> ctx.setRightPanel().accept(deps.grassController().buildPanel()), ownerStage)); - stoneBtn.setOnAction(e -> guardVoxelExit(() -> { - input.activeLayer = SharedInput.LAYER_STONE; - input.activeTool = input.stoneTool; - ctx.setRightPanel().accept(deps.toolPanel()); - deps.terrainController().showToolParameters(deps.toolPanel(), input.activeTool); - }, ownerStage)); + stoneBtn.setOnAction(e -> { + if (suppressStonePanelBuild) { suppressStonePanelBuild = false; return; } + guardVoxelExit(() -> ctx.setRightPanel().accept(deps.stoneController().buildPanel()), ownerStage); + }); objPlaceBtn.setOnAction(e -> guardVoxelExit(() -> { deps.assetController().expandModelsNode(); deps.assetController().openOverlay(); diff --git a/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java b/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java index 7afec11..eb68c66 100644 --- a/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java +++ b/blight-editor/src/main/java/de/blight/editor/toolset/viewport/ViewportController.java @@ -20,6 +20,8 @@ public class ViewportController { private double prevDragX, prevDragY; private double objDragPrevX, objDragPrevY; private boolean objDragging = false; + private double stoneDragPrevX, stoneDragPrevY; + private boolean stoneDragging = false; private double editPressX, editPressY; private int editPressAction; private javafx.animation.Timeline editTimer; @@ -107,6 +109,9 @@ public class ViewportController { if (e.getButton() == MouseButton.PRIMARY && !bothDown) { input.objectClickQueue.offer( new SharedInput.ObjectClick((float)e.getX(), (float)e.getY(), false, e.isShiftDown())); + // Parallel auf Steine testen (StoneEditorState liest diesen Queue) + input.stoneAuswahlQueue.offer( + new SharedInput.StoneEdit((float)e.getX(), (float)e.getY(), +1)); objDragPrevX = e.getX(); objDragPrevY = e.getY(); objDragging = true; } else if (e.getButton() == MouseButton.SECONDARY && !bothDown) { @@ -173,12 +178,28 @@ public class ViewportController { return; } + if (input.activeLayer == SharedInput.LAYER_STONE_EDIT) { + // Bearbeiten-Modus: Linksklick = Gizmo-Pick oder Stein selektieren + if (!bothDown && e.getButton() == MouseButton.PRIMARY && !e.isAltDown()) { + input.stoneEditQueue.offer( + new SharedInput.StoneEdit((float) e.getX(), (float) e.getY(), +1)); + stoneDragPrevX = e.getX(); stoneDragPrevY = e.getY(); + stoneDragging = true; + } + return; + } + if (input.activeLayer == SharedInput.LAYER_STONE && input.stoneTool.brushRadius.getValue() == 1.0) { - // Single-Stone-Modus: nur Rechtsklick, einmalig ohne Timer - if (e.getButton() == MouseButton.SECONDARY && !bothDown) { - input.stoneEditQueue.offer( - new SharedInput.StoneEdit((float) e.getX(), (float) e.getY(), -1)); + // Single-Stone-Modus: Linksklick = platzieren, Rechtsklick = neuen Stein würfeln + if (!bothDown) { + if (e.getButton() == MouseButton.PRIMARY && !e.isAltDown()) { + input.stoneEditQueue.offer( + new SharedInput.StoneEdit((float) e.getX(), (float) e.getY(), +1)); + } else if (e.getButton() == MouseButton.SECONDARY) { + input.stoneEditQueue.offer( + new SharedInput.StoneEdit((float) e.getX(), (float) e.getY(), -1)); + } } } else if (bothDown) { stopEditTimer(); @@ -213,6 +234,17 @@ public class ViewportController { }); viewport.setOnMouseDragged(e -> { + if (input.activeLayer == SharedInput.LAYER_STONE_EDIT) { + if (stoneDragging && e.isPrimaryButtonDown() + && !e.isSecondaryButtonDown() && !e.isMiddleButtonDown() && !e.isAltDown()) { + float dx = (float)(e.getX() - stoneDragPrevX); + float dy = (float)(e.getY() - stoneDragPrevY); + input.stoneDragQueue.offer(new SharedInput.ObjectDrag(dx, dy)); + stoneDragPrevX = e.getX(); stoneDragPrevY = e.getY(); + } + return; + } + if (isObjectMode()) { if (objDragging && e.isPrimaryButtonDown() && !e.isSecondaryButtonDown() && !e.isMiddleButtonDown() && !e.isAltDown()) { @@ -302,6 +334,7 @@ public class ViewportController { viewport.setOnMouseReleased(e -> { objDragging = false; + stoneDragging = false; if (!isObjectMode()) { stopEditTimer(); } if (input.activeLayer == SharedInput.LAYER_PATH_NETWORK) { if (pathNetDragging) { diff --git a/blight-game/src/main/java/de/blight/game/state/GrassState.java b/blight-game/src/main/java/de/blight/game/state/GrassState.java index 7bf282c..0184060 100644 --- a/blight-game/src/main/java/de/blight/game/state/GrassState.java +++ b/blight-game/src/main/java/de/blight/game/state/GrassState.java @@ -323,7 +323,7 @@ public class GrassState extends BaseAppState int vi = 0; for (float[] blade : blades) { float x = blade[0], y = blade[1], z = blade[2], h = blade[3]; - float w = Math.max(0.05f, h * ar * 0.5f); + float w = Math.max(0.001f, h * ar * 0.5f); pos.put(x-w).put(y ).put(z); uv.put(0).put(0); nrm.put(0).put(1).put(0); pos.put(x+w).put(y ).put(z); uv.put(1).put(0); nrm.put(0).put(1).put(0); diff --git a/blight-game/src/main/java/de/blight/game/state/MarchingCubes.java b/blight-game/src/main/java/de/blight/game/state/MarchingCubes.java index d9fb3c8..e5ea77d 100644 --- a/blight-game/src/main/java/de/blight/game/state/MarchingCubes.java +++ b/blight-game/src/main/java/de/blight/game/state/MarchingCubes.java @@ -561,6 +561,18 @@ public final class MarchingCubes { } } + // Originale build()-Normalen der Border-Gruppen retten; build() hat sie + // via getDensityWithNeighbors() chunk-übergreifend korrekt berechnet. + // Nach smooth() würden sie durch rein chunk-interne Mittelung überschrieben + // → sichtbare Kante. Deshalb nach der Normal-Neuberechnung wiederherstellen. + FloatBuffer normFOrig = mesh.getFloatBuffer(VertexBuffer.Type.Normal); + float[] origNorms = null; + if (normFOrig != null) { + origNorms = new float[vertCount * 3]; + normFOrig.rewind(); + normFOrig.get(origNorms); + } + float[] ax = new float[groupCount]; float[] ay = new float[groupCount]; float[] az = new float[groupCount]; @@ -623,6 +635,23 @@ public final class MarchingCubes { if (len>1e-6f) { gnx[g]/=len; gny[g]/=len; gnz[g]/=len; } else { gny[g]=1f; } } + // Pinned Border-Gruppen: originale (chunk-übergreifend berechnete) Normalen zurückschreiben. + if (origNorms != null) { + float[] pgx = new float[groupCount], pgy = new float[groupCount], pgz = new float[groupCount]; + int[] pgc = new int[groupCount]; + for (int v = 0; v < vertCount; v++) { + int g = vertGroup[v]; + if (pinned[g]) { + pgx[g] += origNorms[v*3]; pgy[g] += origNorms[v*3+1]; pgz[g] += origNorms[v*3+2]; + pgc[g]++; + } + } + for (int g = 0; g < groupCount; g++) { + if (!pinned[g] || pgc[g] == 0) continue; + float len = (float)Math.sqrt(pgx[g]*pgx[g]+pgy[g]*pgy[g]+pgz[g]*pgz[g]); + if (len > 1e-6f) { gnx[g]=pgx[g]/len; gny[g]=pgy[g]/len; gnz[g]=pgz[g]/len; } + } + } // 3. Vertex-Normalen schreiben FloatBuffer normF = mesh.getFloatBuffer(VertexBuffer.Type.Normal); normF.rewind(); diff --git a/blight-game/src/main/java/de/blight/game/state/StoneWorldState.java b/blight-game/src/main/java/de/blight/game/state/StoneWorldState.java index 7d8e5e0..25ec02f 100644 --- a/blight-game/src/main/java/de/blight/game/state/StoneWorldState.java +++ b/blight-game/src/main/java/de/blight/game/state/StoneWorldState.java @@ -18,7 +18,6 @@ import de.blight.common.PlacedStoneIO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.nio.FloatBuffer; import java.util.*; public class StoneWorldState extends BaseAppState { @@ -68,13 +67,13 @@ public class StoneWorldState extends BaseAppState { : 0f; float yCenter = worldY + s.radius() * (1f - 2f * s.sinkFraction()); - Mesh mesh = buildStoneMesh(s.radius(), s.noiseSeed(), 2); + Mesh mesh = buildStoneMesh(s.radius(), s.noiseSeed(), subdivForRadius(s.radius()), data.roughness()); Geometry geo = new Geometry("stone", mesh); Material mat = (s.textureSlot() >= 0 && s.textureSlot() < slotMat.length && slotMat[s.textureSlot()] != null) ? slotMat[s.textureSlot()] : defMat; geo.setMaterial(mat); geo.setLocalTranslation(s.x(), yCenter, s.z()); - geo.rotate(0f, s.rotY() * FastMath.DEG_TO_RAD, 0f); + geo.setLocalRotation(new com.jme3.math.Quaternion().fromAngles(s.rotX(), s.rotY() * FastMath.DEG_TO_RAD, s.rotZ())); geo.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); if (bulletAppState != null) { @@ -173,7 +172,7 @@ public class StoneWorldState extends BaseAppState { return new float[]{v[0]/len, v[1]/len, v[2]/len}; } - private static Mesh buildStoneMesh(float radius, int noiseSeed, int subdivisions) { + private static Mesh buildStoneMesh(float radius, int noiseSeed, int subdivisions, float roughness) { List verts = new ArrayList<>(Arrays.asList(ICO_V)); List faces = new ArrayList<>(Arrays.asList(ICO_F)); @@ -194,53 +193,111 @@ public class StoneWorldState extends BaseAppState { verts = nv; faces = nf; } - Random rng = new Random(noiseSeed); - float ox = rng.nextFloat() * 50f, oy = rng.nextFloat() * 50f, oz = rng.nextFloat() * 50f; - float freq = 2.5f + (Math.abs(noiseSeed) % 3); - float amp = 0.22f; + // Variantenreiche Verformung: individuelle Achsenskalierung + 3 Oktaven + Random rng = new Random(noiseSeed); + float ox = rng.nextFloat() * 50f; + float oy = rng.nextFloat() * 50f; + float oz = rng.nextFloat() * 50f; + float flattenY = 0.50f + rng.nextFloat() * 0.45f; + float scaleX = 0.80f + rng.nextFloat() * 0.70f; + float scaleZ = 0.80f + rng.nextFloat() * 0.70f; + float freq = 1.5f + (Math.abs(noiseSeed) % 4); + float amp = roughness * 0.60f; - int nv2 = verts.size(); - float[] pos = new float[nv2 * 3]; - float[] nor = new float[nv2 * 3]; - float[] uv = new float[nv2 * 2]; - - for (int i = 0; i < nv2; i++) { - float[] unit = verts.get(i); - float n = smoothNoise3D(unit[0] * freq + ox, unit[1] * freq + oy, unit[2] * freq + oz); - float r = radius * (1f + n * amp); - pos[i*3] = unit[0] * r; - pos[i*3+1] = unit[1] * r; - pos[i*3+2] = unit[2] * r; - uv[i*2] = (float)(Math.atan2(unit[2], unit[0]) / (2 * Math.PI) + 0.5); - uv[i*2+1] = (float)(Math.asin(Math.max(-1, Math.min(1, unit[1]))) / Math.PI + 0.5); + int numVerts = verts.size(); + float[] def = new float[numVerts * 3]; + for (int i = 0; i < numVerts; i++) { + float[] u = verts.get(i); + float n1 = smoothNoise3D(u[0]*freq+ox, u[1]*freq+oy, u[2]*freq+oz); + float n2 = smoothNoise3D(u[0]*freq*2.5f+ox+11f, u[1]*freq*2.5f+oy+11f, u[2]*freq*2.5f+oz+11f); + float n3 = smoothNoise3D(u[0]*freq*5.5f+ox+23f, u[1]*freq*5.5f+oy+23f, u[2]*freq*5.5f+oz+23f); + float r = radius * (1f + (n1 * 0.55f + n2 * 0.30f + n3 * 0.15f) * amp); + def[i*3] = u[0] * r * scaleX; + def[i*3+1] = u[1] * r * flattenY; + def[i*3+2] = u[2] * r * scaleZ; } - for (int[] f : faces) { - int ai = f[0]*3, bi = f[1]*3, ci = f[2]*3; - float ax = pos[ai], ay = pos[ai+1], az = pos[ai+2]; - float bx = pos[bi], by = pos[bi+1], bz = pos[bi+2]; - float cx = pos[ci], cy = pos[ci+1], cz = pos[ci+2]; - float nx = (by-ay)*(cz-az) - (bz-az)*(cy-ay); - float ny = (bz-az)*(cx-ax) - (bx-ax)*(cz-az); - float nz = (bx-ax)*(cy-ay) - (by-ay)*(cx-ax); - for (int vi : f) { nor[vi*3] += nx; nor[vi*3+1] += ny; nor[vi*3+2] += nz; } - } - for (int i = 0; i < nv2; i++) { - float len = (float) Math.sqrt(nor[i*3]*nor[i*3] + nor[i*3+1]*nor[i*3+1] + nor[i*3+2]*nor[i*3+2]); - if (len > 0) { nor[i*3] /= len; nor[i*3+1] /= len; nor[i*3+2] /= len; } + // Taubin-Glättung (lambda-mu, kein Mesh-Schrumpfen) + { + List> adj = new ArrayList<>(numVerts); + for (int i = 0; i < numVerts; i++) adj.add(new ArrayList<>(6)); + for (int[] f : faces) { + for (int i = 0; i < 3; i++) { + int a = f[i], b = f[(i + 1) % 3]; + List la = adj.get(a), lb = adj.get(b); + if (!la.contains(b)) la.add(b); + if (!lb.contains(a)) lb.add(a); + } + } + float lambda = 0.5f, mu = -0.53f; + for (int iter = 0; iter < 6; iter++) { + float step = (iter % 2 == 0) ? lambda : mu; + float[] next = new float[numVerts * 3]; + for (int i = 0; i < numVerts; i++) { + List nb = adj.get(i); + float sx = 0, sy = 0, sz = 0; + for (int n : nb) { sx += def[n*3]; sy += def[n*3+1]; sz += def[n*3+2]; } + float inv = nb.isEmpty() ? 1f : 1f / nb.size(); + next[i*3] = def[i*3] + step * (sx*inv - def[i*3]); + next[i*3+1] = def[i*3+1] + step * (sy*inv - def[i*3+1]); + next[i*3+2] = def[i*3+2] + step * (sz*inv - def[i*3+2]); + } + def = next; + } } - FloatBuffer pb = BufferUtils.createFloatBuffer(pos); - FloatBuffer nb = BufferUtils.createFloatBuffer(nor); - FloatBuffer ub = BufferUtils.createFloatBuffer(uv); - java.nio.IntBuffer ib = BufferUtils.createIntBuffer(faces.size() * 3); - for (int[] f : faces) ib.put(f[0]).put(f[1]).put(f[2]); + // Flat-Shading: jedes Dreieck bekommt eigene Vertices mit Flächennormale + Box-Map UV + int fc = faces.size(); + float[] pos = new float[fc * 9]; + float[] nor = new float[fc * 9]; + float[] uv = new float[fc * 6]; + java.nio.IntBuffer ib = BufferUtils.createIntBuffer(fc * 3); + for (int i = 0; i < fc * 3; i++) ib.put(i); ib.flip(); + float uvScale = 0.5f / radius; + for (int fi = 0; fi < fc; fi++) { + int[] f = faces.get(fi); + float ax = def[f[0]*3], ay = def[f[0]*3+1], az = def[f[0]*3+2]; + float bx = def[f[1]*3], by = def[f[1]*3+1], bz = def[f[1]*3+2]; + float cx = def[f[2]*3], cy = def[f[2]*3+1], cz = def[f[2]*3+2]; + float ex = bx-ax, ey = by-ay, ez = bz-az; + float gx = cx-ax, gy = cy-ay, gz = cz-az; + float nx = ey*gz-ez*gy, ny = ez*gx-ex*gz, nz = ex*gy-ey*gx; + float nlen = (float) Math.sqrt(nx*nx+ny*ny+nz*nz); + if (nlen > 0) { nx/=nlen; ny/=nlen; nz/=nlen; } + + float anx = Math.abs(nx), any = Math.abs(ny), anz = Math.abs(nz); + float uA, vA, uB, vB, uC, vC; + if (anx >= any && anx >= anz) { + uA=az*uvScale+0.5f; vA=ay*uvScale+0.5f; + uB=bz*uvScale+0.5f; vB=by*uvScale+0.5f; + uC=cz*uvScale+0.5f; vC=cy*uvScale+0.5f; + } else if (any >= anx && any >= anz) { + uA=ax*uvScale+0.5f; vA=az*uvScale+0.5f; + uB=bx*uvScale+0.5f; vB=bz*uvScale+0.5f; + uC=cx*uvScale+0.5f; vC=cz*uvScale+0.5f; + } else { + uA=ax*uvScale+0.5f; vA=ay*uvScale+0.5f; + uB=bx*uvScale+0.5f; vB=by*uvScale+0.5f; + uC=cx*uvScale+0.5f; vC=cy*uvScale+0.5f; + } + int p3 = fi*9, u2 = fi*6; + pos[p3]=ax; pos[p3+1]=ay; pos[p3+2]=az; + pos[p3+3]=bx; pos[p3+4]=by; pos[p3+5]=bz; + pos[p3+6]=cx; pos[p3+7]=cy; pos[p3+8]=cz; + nor[p3]=nx; nor[p3+1]=ny; nor[p3+2]=nz; + nor[p3+3]=nx; nor[p3+4]=ny; nor[p3+5]=nz; + nor[p3+6]=nx; nor[p3+7]=ny; nor[p3+8]=nz; + uv[u2]=uA; uv[u2+1]=vA; + uv[u2+2]=uB; uv[u2+3]=vB; + uv[u2+4]=uC; uv[u2+5]=vC; + } + Mesh mesh = new Mesh(); - mesh.setBuffer(Type.Position, 3, pb); - mesh.setBuffer(Type.Normal, 3, nb); - mesh.setBuffer(Type.TexCoord, 2, ub); + mesh.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(pos)); + mesh.setBuffer(Type.Normal, 3, BufferUtils.createFloatBuffer(nor)); + mesh.setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(uv)); mesh.setBuffer(Type.Index, 3, ib); mesh.updateBound(); return mesh; @@ -275,4 +332,9 @@ public class StoneWorldState extends BaseAppState { } private static float lerp(float a, float b, float t) { return a + (b-a)*t; } + + private static int subdivForRadius(float radius) { + double logR = Math.log(Math.max(0.01, radius) / 0.1) / Math.log(2.0); + return Math.max(1, Math.min(5, (int) Math.round(logR) + 2)); + } } diff --git a/blight-map/src/main/map/blight_grass.blg b/blight-map/src/main/map/blight_grass.blg index 14f3df9..a4e8c50 100644 Binary files a/blight-map/src/main/map/blight_grass.blg and b/blight-map/src/main/map/blight_grass.blg differ diff --git a/blight-map/src/main/map/blight_grass_vertex.blgv b/blight-map/src/main/map/blight_grass_vertex.blgv index 487b489..07c9bbd 100644 Binary files a/blight-map/src/main/map/blight_grass_vertex.blgv and b/blight-map/src/main/map/blight_grass_vertex.blgv differ diff --git a/blight-map/src/main/map/blight_stones.bls b/blight-map/src/main/map/blight_stones.bls new file mode 100644 index 0000000..c723b38 Binary files /dev/null and b/blight-map/src/main/map/blight_stones.bls differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_15.blvc b/blight-map/src/main/map/chunks/voxel_16_0_15.blvc index 65668c1..cacd42a 100644 Binary files a/blight-map/src/main/map/chunks/voxel_16_0_15.blvc and b/blight-map/src/main/map/chunks/voxel_16_0_15.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_16.blvc b/blight-map/src/main/map/chunks/voxel_16_0_16.blvc index 780bf81..0603c92 100644 Binary files a/blight-map/src/main/map/chunks/voxel_16_0_16.blvc and b/blight-map/src/main/map/chunks/voxel_16_0_16.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod0.j3o b/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod0.j3o index e63f054..fd230fa 100644 Binary files a/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod0.j3o and b/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod0.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod1.j3o b/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod1.j3o index 445ddc4..c355610 100644 Binary files a/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod1.j3o and b/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod1.j3o differ diff --git a/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod2.j3o b/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod2.j3o index d2beff9..1a3545f 100644 Binary files a/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod2.j3o and b/blight-map/src/main/map/chunks/voxel_16_0_16_baked_lod2.j3o differ