Stein Tool ein wenig überarbeitet

This commit is contained in:
2026-10-05 22:14:54 +02:00
parent 7069c5f632
commit c3e028b9f8
47 changed files with 1049 additions and 145 deletions

View File

@@ -9,7 +9,8 @@ MaterialDef Grass {
Vector2 WindDir : 0.0 1.0 Vector2 WindDir : 0.0 1.0
Vector3 SunDir : 0.55 0.80 0.35 Vector3 SunDir : 0.55 0.80 0.35
Color SunColor : 1.0 1.0 0.95 1.0 Color SunColor : 1.0 1.0 0.95 1.0
Float Wetness : 0.0 Float Wetness : 0.0
Boolean GammaCorrect : false
} }
Technique { Technique {
@@ -28,8 +29,9 @@ MaterialDef Grass {
} }
Defines { Defines {
HAS_COLORMAP : ColorMap HAS_COLORMAP : ColorMap
HAS_NORMALMAP : NormalMap HAS_NORMALMAP : NormalMap
GAMMA_CORRECT : GammaCorrect
} }
} }
} }

View File

@@ -39,5 +39,9 @@ void main() {
color.rgb *= (1.0 - 0.18 * m_Wetness); color.rgb *= (1.0 - 0.18 * m_Wetness);
color.rgb *= lit; color.rgb *= lit;
float shine = pow(diffuse, 12.0) * m_Wetness * 0.25; 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);
} }

View File

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

Before

Width:  |  Height:  |  Size: 468 KiB

After

Width:  |  Height:  |  Size: 468 KiB

View File

@@ -11,7 +11,9 @@ public record PlacedStone(
float x, float x,
float z, float z,
float radius, float radius,
float rotX,
float rotY, float rotY,
float rotZ,
int textureSlot, int textureSlot,
float sinkFraction, float sinkFraction,
int noiseSeed int noiseSeed

View File

@@ -9,19 +9,20 @@ import java.util.zip.*;
* Liest und schreibt platzierte Steine als komprimierte Binärdatei * Liest und schreibt platzierte Steine als komprimierte Binärdatei
* ({@code blight_stones.bls}) neben der Kartendatei. * ({@code blight_stones.bls}) neben der Kartendatei.
* *
* Format v1: * Format v3:
* int MAGIC 0x53544E53 ("STNS") * int MAGIC 0x53544E53 ("STNS")
* int VERSION 1 * int VERSION 3
* int SLOT_COUNT 3 * int SLOT_COUNT 3
* 3× UTF Texturpfad pro Slot ("" = kein) * 3× UTF Texturpfad pro Slot ("" = kein)
* float roughness (v2+; 0..1)
* int stoneCount * 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 * byte textureSlot, float sinkFraction, int noiseSeed
*/ */
public final class PlacedStoneIO { public final class PlacedStoneIO {
private static final int MAGIC = 0x53544E53; 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; public static final int SLOT_COUNT = 3;
private PlacedStoneIO() {} private PlacedStoneIO() {}
@@ -30,7 +31,7 @@ public final class PlacedStoneIO {
return MapIO.getMapPath().resolveSibling("blight_stones.bls"); return MapIO.getMapPath().resolveSibling("blight_stones.bls");
} }
public record StoneData(String[] slotPaths, List<PlacedStone> stones) {} public record StoneData(String[] slotPaths, float roughness, List<PlacedStone> stones) {}
public static void save(StoneData data) throws IOException { public static void save(StoneData data) throws IOException {
Path p = getPath(); Path p = getPath();
@@ -45,13 +46,16 @@ public final class PlacedStoneIO {
String s = (i < paths.length && paths[i] != null) ? paths[i] : ""; String s = (i < paths.length && paths[i] != null) ? paths[i] : "";
out.writeUTF(s); out.writeUTF(s);
} }
out.writeFloat(data.roughness());
List<PlacedStone> stones = data.stones() != null ? data.stones() : List.of(); List<PlacedStone> stones = data.stones() != null ? data.stones() : List.of();
out.writeInt(stones.size()); out.writeInt(stones.size());
for (PlacedStone s : stones) { for (PlacedStone s : stones) {
out.writeFloat(s.x()); out.writeFloat(s.x());
out.writeFloat(s.z()); out.writeFloat(s.z());
out.writeFloat(s.radius()); out.writeFloat(s.radius());
out.writeFloat(s.rotX());
out.writeFloat(s.rotY()); out.writeFloat(s.rotY());
out.writeFloat(s.rotZ());
out.writeByte(s.textureSlot()); out.writeByte(s.textureSlot());
out.writeFloat(s.sinkFraction()); out.writeFloat(s.sinkFraction());
out.writeInt(s.noiseSeed()); out.writeInt(s.noiseSeed());
@@ -74,19 +78,22 @@ public final class PlacedStoneIO {
String s = in.readUTF(); String s = in.readUTF();
if (i < SLOT_COUNT) paths[i] = s; if (i < SLOT_COUNT) paths[i] = s;
} }
float roughness = (ver >= 2) ? in.readFloat() : 0.40f;
int count = in.readInt(); int count = in.readInt();
List<PlacedStone> stones = new ArrayList<>(count); List<PlacedStone> stones = new ArrayList<>(count);
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
float x = in.readFloat(); float x = in.readFloat();
float z = in.readFloat(); float z = in.readFloat();
float r = in.readFloat(); float r = in.readFloat();
float rotX = (ver >= 3) ? in.readFloat() : 0f;
float rotY = in.readFloat(); float rotY = in.readFloat();
float rotZ = (ver >= 3) ? in.readFloat() : 0f;
int slot = in.readUnsignedByte(); int slot = in.readUnsignedByte();
float sink = in.readFloat(); float sink = in.readFloat();
int seed = in.readInt(); 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);
} }
} }
} }

View File

@@ -92,6 +92,7 @@ public class EditorApp extends Application {
private de.blight.editor.toolset.water.WaterController waterController; private de.blight.editor.toolset.water.WaterController waterController;
private de.blight.editor.toolset.voxel.VoxelController voxelController; private de.blight.editor.toolset.voxel.VoxelController voxelController;
private de.blight.editor.toolset.grass.GrassController grassController; 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.content.ContentController contentController;
private de.blight.editor.toolset.tripo.TripoController tripoController; private de.blight.editor.toolset.tripo.TripoController tripoController;
private de.blight.editor.toolset.game.GameController gameController; private de.blight.editor.toolset.game.GameController gameController;
@@ -193,6 +194,7 @@ public class EditorApp extends Application {
viewportController.getViewport(), terrainController::showToolParameters, viewportController.getViewport(), terrainController::showToolParameters,
colorAbsPath -> { de.blight.editor.TextureMapData d = terrainController.loadTexMeta(colorAbsPath); return d.normalMap != null ? d.normalMap : ""; }); colorAbsPath -> { de.blight.editor.TextureMapData d = terrainController.loadTexMeta(colorAbsPath); return d.normalMap != null ? d.normalMap : ""; });
terrainController.setGrassController(grassController); terrainController.setGrassController(grassController);
stoneController = new de.blight.editor.toolset.stone.StoneController(toolCtx);
contentController = new de.blight.editor.toolset.content.ContentController(toolCtx); contentController = new de.blight.editor.toolset.content.ContentController(toolCtx);
contentController.setOnEnterContentMode(() -> { contentController.setOnEnterContentMode(() -> {
if (assetController.isOverlayOpen()) assetController.closeOverlay(); if (assetController.isOverlayOpen()) assetController.closeOverlay();
@@ -227,7 +229,7 @@ public class EditorApp extends Application {
modelEditorController, assetController, animSetController, modelEditorController, assetController, animSetController,
contentController, worldPanelController, voxelController, contentController, worldPanelController, voxelController,
grassController, atmosphereController, waterController, grassController, atmosphereController, waterController,
gameController, terrainController, objectController, gameController, terrainController, objectController, stoneController,
this::switchToVegetationGenerator, this::switchToEzTree, this::switchToVegetationGenerator, this::switchToEzTree,
this::switchToTripo, this::openCharacterEditor, this::switchToTripo, this::openCharacterEditor,
this::switchToThumbnailManager, this::takeEditorScreenshot, this::openSettings this::switchToThumbnailManager, this::takeEditorScreenshot, this::openSettings
@@ -375,6 +377,12 @@ public class EditorApp extends Application {
atmosphereController.onFrame(); atmosphereController.onFrame();
waterController.onFrame(); waterController.onFrame();
voxelController.onFrame(); voxelController.onFrame();
stoneController.onFrame();
if (input.stoneAutoActivateEditMode) {
input.stoneAutoActivateEditMode = false;
stoneController.activateEditMode();
topBarController.selectStoneBtnSilently();
}
java.util.Set<String> skeletal = input.skeletalPaths.getAndSet(null); java.util.Set<String> skeletal = input.skeletalPaths.getAndSet(null);
if (skeletal != null) { if (skeletal != null) {

View File

@@ -1023,6 +1023,8 @@ public class SharedInput {
// ── Steine ──────────────────────────────────────────────────────────────── // ── Steine ────────────────────────────────────────────────────────────────
/** activeLayer==23 → Steine setzen/entfernen */ /** activeLayer==23 → Steine setzen/entfernen */
public static final int LAYER_STONE = 23; 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 record StoneEdit(float screenX, float screenY, int action) {}
public final ConcurrentLinkedQueue<StoneEdit> stoneEditQueue = new ConcurrentLinkedQueue<>(); public final ConcurrentLinkedQueue<StoneEdit> stoneEditQueue = new ConcurrentLinkedQueue<>();
@@ -1033,6 +1035,24 @@ public class SharedInput {
/** JFX → JME: Reload der Materialien auslösen. */ /** JFX → JME: Reload der Materialien auslösen. */
public volatile boolean stoneTexturesChanged = false; 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<ObjectDrag> 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<StoneEdit> 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}. */ /** JME → JME: Terrain-Edits, die Stein-Höhen neu berechnen müssen. float[3] = {wx, wz, radius}. */
public final ConcurrentLinkedQueue<float[]> terrainEditedAreas = new ConcurrentLinkedQueue<>(); public final ConcurrentLinkedQueue<float[]> terrainEditedAreas = new ConcurrentLinkedQueue<>();

View File

@@ -220,6 +220,7 @@ public class PlacedObjectState extends BaseAppState {
mat.setFloat("WindStrength", 0.12f); mat.setFloat("WindStrength", 0.12f);
mat.setVector3("SunDir", new Vector3f(0.55f, 0.80f, 0.35f)); mat.setVector3("SunDir", new Vector3f(0.55f, 0.80f, 0.35f));
mat.setColor("SunColor", ColorRGBA.White); mat.setColor("SunColor", ColorRGBA.White);
mat.setBoolean("GammaCorrect", true);
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
return mat; return mat;
} catch (Exception e) { } catch (Exception e) {
@@ -444,8 +445,7 @@ public class PlacedObjectState extends BaseAppState {
int vi = 0; int vi = 0;
for (float[] blade : blades) { for (float[] blade : blades) {
float x = blade[0], y = blade[1], z = blade[2], h = blade[3]; 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.001f, h * ar * 0.5f);
float w = Math.max(0.05f, h * ar * 0.5f);
// Quad 1: liegt in XY-Ebene (konstantes z) – Normal zeigt +Z, Tangent zeigt +X // 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(0).put(0);
pos.put(x+w).put(y ).put(z); uv.put(1).put(0); pos.put(x+w).put(y ).put(z); uv.put(1).put(0);

View File

@@ -4,6 +4,7 @@ import com.jme3.app.Application;
import com.jme3.app.SimpleApplication; import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState; import com.jme3.app.state.BaseAppState;
import com.jme3.asset.AssetManager; import com.jme3.asset.AssetManager;
import com.jme3.bounding.BoundingVolume;
import com.jme3.collision.CollisionResults; import com.jme3.collision.CollisionResults;
import com.jme3.material.Material; import com.jme3.material.Material;
import com.jme3.material.RenderState; import com.jme3.material.RenderState;
@@ -12,6 +13,8 @@ import com.jme3.renderer.Camera;
import com.jme3.renderer.queue.RenderQueue; import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.*; import com.jme3.scene.*;
import com.jme3.scene.VertexBuffer.Type; 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.terrain.geomipmap.TerrainQuad;
import com.jme3.util.BufferUtils; import com.jme3.util.BufferUtils;
import de.blight.common.PlacedStone; import de.blight.common.PlacedStone;
@@ -68,11 +71,38 @@ public class StoneEditorState extends BaseAppState {
private final Material[] slotMat = new Material[PlacedStoneIO.SLOT_COUNT]; private final Material[] slotMat = new Material[PlacedStoneIO.SLOT_COUNT];
private Material defaultMat; private Material defaultMat;
private Geometry brushIndicator; private Geometry brushIndicator;
private Geometry selHighlight;
private Geometry previewGeom; private Geometry previewGeom;
private PlacedStone pendingStone; private PlacedStone pendingStone;
private final Random random = new Random(); private final Random random = new Random();
private boolean modified = false; 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<String, int[]> geomNameToRef = new HashMap<>();
public StoneEditorState(SharedInput input) { public StoneEditorState(SharedInput input) {
this.input = input; this.input = input;
@@ -99,6 +129,21 @@ public class StoneEditorState extends BaseAppState {
previewGeom = buildPreviewGeom(); previewGeom = buildPreviewGeom();
rootNode.attachChild(previewGeom); 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 @Override
@@ -107,6 +152,8 @@ public class StoneEditorState extends BaseAppState {
rootNode.detachChild(stoneRoot); rootNode.detachChild(stoneRoot);
rootNode.detachChild(brushIndicator); rootNode.detachChild(brushIndicator);
rootNode.detachChild(previewGeom); rootNode.detachChild(previewGeom);
rootNode.detachChild(gizmoNode);
rootNode.detachChild(selHighlight);
} }
@Override protected void onEnable() { stoneRoot.setCullHint(Spatial.CullHint.Inherit); } @Override protected void onEnable() { stoneRoot.setCullHint(Spatial.CullHint.Inherit); }
@@ -122,16 +169,81 @@ public class StoneEditorState extends BaseAppState {
Arrays.fill(dirtyChunks, true); 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(); updateBrushIndicator();
updatePreviewStone(); updatePreviewStone();
processEdits(); processEdits();
processAuswahlClicks();
processTerrainUpdates(); processTerrainUpdates();
rebuildDirtyChunks(); rebuildDirtyChunks();
updateChunkLOD(); 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 ───────────────────────────────────────────────────── // ── 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() { private void processTerrainUpdates() {
float[] area; float[] area;
while ((area = input.terrainEditedAreas.poll()) != null) { while ((area = input.terrainEditedAreas.poll()) != null) {
@@ -150,15 +262,22 @@ public class StoneEditorState extends BaseAppState {
while ((edit = input.stoneEditQueue.poll()) != null) { while ((edit = input.stoneEditQueue.poll()) != null) {
float jx = edit.screenX() * (float) input.viewportScaleX; float jx = edit.screenX() * (float) input.viewportScaleX;
float jy = cam.getHeight() - edit.screenY() * (float) input.viewportScaleY; 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); Vector3f hit = raycastTerrain(jx, jy);
if (hit == null) continue; if (hit == null) continue;
if (isSingleStoneMode()) { 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); placePendingStoneAt(hit.x, hit.z);
generatePendingStone(); generatePendingStone();
} else { // Rechtsklick: nur neuen Zufallsstein zum Platzieren bereitstellen
generatePendingStone();
} }
// Linksklick ignoriert in Single-Stone-Modus
} else { } else {
if (edit.action() > 0) addStonesAt(hit.x, hit.z); if (edit.action() > 0) addStonesAt(hit.x, hit.z);
else removeStonesAt(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 slot = random.nextInt(activeSlots);
int seed = random.nextInt(); 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.setMesh(mesh);
previewGeom.setLocalRotation(new Quaternion().fromAngles(0f, rotY * FastMath.DEG_TO_RAD, 0f)); previewGeom.setLocalRotation(new Quaternion().fromAngles(0f, rotY * FastMath.DEG_TO_RAD, 0f));
previewGeom.setMaterial(materialForSlot(slot));
} }
private void placePendingStoneAt(float wx, float wz) { private void placePendingStoneAt(float wx, float wz) {
if (pendingStone == null) { return; } if (pendingStone == null) { return; }
if (!Float.isFinite(getHeightAt(wx, wz))) { 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()); pendingStone.textureSlot(), pendingStone.sinkFraction(), pendingStone.noiseSeed());
int ci = chunkIndex(wx, wz); int ci = chunkIndex(wx, wz);
if (ci >= 0) { if (ci >= 0) {
@@ -254,7 +376,7 @@ public class StoneEditorState extends BaseAppState {
int slot = random.nextInt(activeSlots); int slot = random.nextInt(activeSlots);
int seed = random.nextInt(); 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); int ci = chunkIndex(sx, sz);
if (ci >= 0) { if (ci >= 0) {
chunkStones[ci].add(stone); chunkStones[ci].add(stone);
@@ -292,7 +414,9 @@ public class StoneEditorState extends BaseAppState {
} }
private void rebuildChunk(int ci) { 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 (lod0Nodes[ci] != null) { lod0Nodes[ci].detachAllChildren(); stoneRoot.detachChild(lod0Nodes[ci]); }
if (lod1Nodes[ci] != null) { lod1Nodes[ci].detachAllChildren(); stoneRoot.detachChild(lod1Nodes[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 n0 = new Node("stone_lod0_" + ci);
Node n1 = new Node("stone_lod1_" + 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); 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); n0.attachChild(g0);
geomNameToRef.put(name, new int[]{ci, si});
if (s.radius() * 2f > 1f) { // Durchmesser > 1m → LOD1 if (s.radius() * 2f > 1f) {
Geometry g1 = buildStoneGeom(s, y, 1); // LOD1: 1 Subdiv Geometry g1 = buildStoneGeom(s, y, Math.max(1, sd0 - 2), roughness, name + "_lod1");
n1.attachChild(g1); n1.attachChild(g1);
} }
} }
stoneRoot.attachChild(n0); stoneRoot.attachChild(n0);
stoneRoot.attachChild(n1);
lod0Nodes[ci] = n0; 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) { private Geometry buildStoneGeom(PlacedStone s, float worldY, int subdivisions, float roughness) {
Mesh mesh = buildStoneMesh(s.radius(), s.noiseSeed(), subdivisions); return buildStoneGeom(s, worldY, subdivisions, roughness, "stone");
// 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, String name) {
Mesh mesh = buildStoneMesh(s.radius(), s.noiseSeed(), subdivisions, roughness);
float yCenter = worldY + s.radius() * (1f - 2f * s.sinkFraction()); 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.setMaterial(materialForSlot(s.textureSlot()));
g.setLocalTranslation(s.x(), yCenter, s.z()); 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; return g;
} }
@@ -344,9 +484,10 @@ public class StoneEditorState extends BaseAppState {
if (dist > CULL_DIST) { if (dist > CULL_DIST) {
lod0Nodes[ci].setCullHint(Spatial.CullHint.Always); lod0Nodes[ci].setCullHint(Spatial.CullHint.Always);
if (lod1Nodes[ci] != null) lod1Nodes[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); lod0Nodes[ci].setCullHint(Spatial.CullHint.Always);
if (lod1Nodes[ci] != null) lod1Nodes[ci].setCullHint(Spatial.CullHint.Inherit); lod1Nodes[ci].setCullHint(Spatial.CullHint.Inherit);
} else { } else {
lod0Nodes[ci].setCullHint(Spatial.CullHint.Inherit); lod0Nodes[ci].setCullHint(Spatial.CullHint.Inherit);
if (lod1Nodes[ci] != null) lod1Nodes[ci].setCullHint(Spatial.CullHint.Always); 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 "<name>_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 ─────────────────────────────────────────── // ── Mesh-Generierung: Icosphere ───────────────────────────────────────────
private static final float PHI = (1f + (float) Math.sqrt(5)) / 2f; 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. * Erzeugt einen Stein-Mesh mit der gegebenen Anzahl an Icosphere-Unterteilungen.
* Noise-Verformung wird durch noiseSeed deterministisch gesteuert. * 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 // 1. Icosphere aufbauen und unterteilen
List<float[]> verts = new ArrayList<>(Arrays.asList(ICO_V)); List<float[]> verts = new ArrayList<>(Arrays.asList(ICO_V));
List<int[]> faces = new ArrayList<>(Arrays.asList(ICO_F)); List<int[]> faces = new ArrayList<>(Arrays.asList(ICO_F));
@@ -410,57 +854,111 @@ public class StoneEditorState extends BaseAppState {
verts = nv; faces = nf; verts = nv; faces = nf;
} }
// 2. Noise-Verformung (entlang Vertex-Normal = Einheitskugel-Richtung) // 2. Variantenreiche Verformung: individuelle Achsenskalierung + 3 Oktaven
Random rng = new Random(noiseSeed); Random rng = new Random(noiseSeed);
float ox = rng.nextFloat() * 50f, oy = rng.nextFloat() * 50f, oz = rng.nextFloat() * 50f; float ox = rng.nextFloat() * 50f;
float freq = 2.5f + (Math.abs(noiseSeed) % 3); // 2.5–4.5 float oy = rng.nextFloat() * 50f;
float amp = 0.22f; // ±22 % Verformung 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(); int numVerts = verts.size();
float[] pos = new float[nv * 3]; float[] def = new float[numVerts * 3];
float[] nor = new float[nv * 3]; for (int i = 0; i < numVerts; i++) {
float[] uv = new float[nv * 2]; float[] u = verts.get(i);
float n1 = smoothNoise3D(u[0]*freq+ox, u[1]*freq+oy, u[2]*freq+oz);
for (int i = 0; i < nv; i++) { 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[] unit = verts.get(i); 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 n = smoothNoise3D(unit[0] * freq + ox, unit[1] * freq + oy, unit[2] * freq + oz); float r = radius * (1f + (n1 * 0.55f + n2 * 0.30f + n3 * 0.15f) * amp);
float r = radius * (1f + n * amp); def[i*3] = u[0] * r * scaleX;
pos[i*3] = unit[0] * r; def[i*3+1] = u[1] * r * flattenY;
pos[i*3+1] = unit[1] * r; def[i*3+2] = u[2] * r * scaleZ;
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);
} }
// 3. Vertex-Normalen aus Dreiecken akkumulieren // 3. Taubin-Glättung (lambda-mu, kein Mesh-Schrumpfen)
for (int[] f : faces) { {
int ai = f[0]*3, bi = f[1]*3, ci = f[2]*3; List<List<Integer>> adj = new ArrayList<>(numVerts);
float ax = pos[ai], ay = pos[ai+1], az = pos[ai+2]; for (int i = 0; i < numVerts; i++) adj.add(new ArrayList<>(6));
float bx = pos[bi], by = pos[bi+1], bz = pos[bi+2]; for (int[] f : faces) {
float cx2= pos[ci], cy = pos[ci+1], cz = pos[ci+2]; for (int i = 0; i < 3; i++) {
float nx = (by-ay)*(cz-az) - (bz-az)*(cy-ay); int a = f[i], b = f[(i + 1) % 3];
float ny = (bz-az)*(cx2-ax) - (bx-ax)*(cz-az); List<Integer> la = adj.get(a), lb = adj.get(b);
float nz = (bx-ax)*(cy-ay) - (by-ay)*(cx2-ax); if (!la.contains(b)) la.add(b);
for (int vi : f) { nor[vi*3] += nx; nor[vi*3+1] += ny; nor[vi*3+2] += nz; } if (!lb.contains(a)) lb.add(a);
} }
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]); float lambda = 0.5f, mu = -0.53f;
if (len > 0) { nor[i*3] /= len; nor[i*3+1] /= len; nor[i*3+2] /= len; } 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<Integer> 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 // 4. Flat-Shading: jedes Dreieck bekommt eigene Vertices mit Flächennormale + Box-Map UV
FloatBuffer pb = BufferUtils.createFloatBuffer(pos); int fc = faces.size();
FloatBuffer nb = BufferUtils.createFloatBuffer(nor); float[] pos = new float[fc * 9];
FloatBuffer ub = BufferUtils.createFloatBuffer(uv); float[] nor = new float[fc * 9];
java.nio.IntBuffer ib = BufferUtils.createIntBuffer(faces.size() * 3); float[] uv = new float[fc * 6];
for (int[] f : faces) ib.put(f[0]).put(f[1]).put(f[2]); java.nio.IntBuffer ib = BufferUtils.createIntBuffer(fc * 3);
for (int i = 0; i < fc * 3; i++) ib.put(i);
ib.flip(); 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 mesh = new Mesh();
mesh.setBuffer(Type.Position, 3, pb); mesh.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(pos));
mesh.setBuffer(Type.Normal, 3, nb); mesh.setBuffer(Type.Normal, 3, BufferUtils.createFloatBuffer(nor));
mesh.setBuffer(Type.TexCoord, 2, ub); mesh.setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(uv));
mesh.setBuffer(Type.Index, 3, ib); mesh.setBuffer(Type.Index, 3, ib);
mesh.updateBound(); mesh.updateBound();
return mesh; 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; } 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 ─────────────────────────────────────────────────────────── // ── Materialien ───────────────────────────────────────────────────────────
private void reloadMaterials() { private void reloadMaterials() {
@@ -534,8 +1039,6 @@ public class StoneEditorState extends BaseAppState {
return defaultMat; return defaultMat;
} }
// ── Brush-Indicator ───────────────────────────────────────────────────────
private void updateBrushIndicator() { private void updateBrushIndicator() {
if (input.activeLayer != SharedInput.LAYER_STONE || input.mouseScreenX < 0 if (input.activeLayer != SharedInput.LAYER_STONE || input.mouseScreenX < 0
|| isSingleStoneMode()) { || isSingleStoneMode()) {
@@ -573,7 +1076,6 @@ public class StoneEditorState extends BaseAppState {
mesh.setBuffer(Type.Position, 3, pos); mesh.setBuffer(Type.Position, 3, pos);
mesh.setBuffer(VertexBuffer.Type.Index, 3, idx); mesh.setBuffer(VertexBuffer.Type.Index, 3, idx);
mesh.updateBound(); mesh.updateBound();
Geometry geo = new Geometry("stoneBrushIndicator", mesh); Geometry geo = new Geometry("stoneBrushIndicator", mesh);
Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(0.9f, 0.5f, 0.1f, 0.4f)); 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() { 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); Geometry g = new Geometry("stonePreview", mesh);
Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); g.setMaterial(defaultMat);
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.setCullHint(Spatial.CullHint.Always); g.setCullHint(Spatial.CullHint.Always);
return g; return g;
} }
@@ -705,6 +1202,8 @@ public class StoneEditorState extends BaseAppState {
input.stoneTool.texturePaths[i] = paths[i] != null ? paths[i] : ""; input.stoneTool.texturePaths[i] = paths[i] != null ? paths[i] : "";
reloadMaterials(); reloadMaterials();
} }
input.stoneTool.roughness.setValue(data.roughness());
lastRoughness = data.roughness();
for (PlacedStone s : data.stones()) { for (PlacedStone s : data.stones()) {
int ci = chunkIndex(s.x(), s.z()); int ci = chunkIndex(s.x(), s.z());
if (ci >= 0) { chunkStones[ci].add(s); dirtyChunks[ci] = true; } if (ci >= 0) { chunkStones[ci].add(s); dirtyChunks[ci] = true; }
@@ -720,7 +1219,8 @@ public class StoneEditorState extends BaseAppState {
try { try {
List<PlacedStone> all = new ArrayList<>(); List<PlacedStone> all = new ArrayList<>();
for (List<PlacedStone> list : chunkStones) all.addAll(list); for (List<PlacedStone> 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; modified = false;
log.info("[StoneEditorState] {} Steine gespeichert.", all.size()); log.info("[StoneEditorState] {} Steine gespeichert.", all.size());
} catch (Exception e) { } catch (Exception e) {

View File

@@ -1738,6 +1738,7 @@ public class TerrainEditorState extends BaseAppState {
|| layer == SharedInput.LAYER_LOCATION_ZONES || layer == SharedInput.LAYER_LOCATION_ZONES
|| layer == SharedInput.LAYER_PLAY_TOOL || layer == SharedInput.LAYER_PLAY_TOOL
|| layer == SharedInput.LAYER_VOXEL || layer == SharedInput.LAYER_STONE || layer == SharedInput.LAYER_VOXEL || layer == SharedInput.LAYER_STONE
|| layer == SharedInput.LAYER_STONE_EDIT
|| layer == SharedInput.LAYER_BED_LIEGE || layer == SharedInput.LAYER_BED_LIEGE
|| layer == SharedInput.LAYER_BENCH_SITZ || layer == SharedInput.LAYER_BENCH_SITZ
|| layer == SharedInput.LAYER_AUSWAHL || layer == SharedInput.LAYER_AUSWAHL

View File

@@ -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 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 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 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). */ /** Texturpfade für bis zu 3 Slots; "" = kein Texture (Fallback: Grau). */
public volatile String[] texturePaths = new String[]{"", "", ""}; public volatile String[] texturePaths = new String[]{"", "", ""};
@@ -20,6 +21,6 @@ public class StoneTool extends EditorTool {
@Override public ToolParameter getBrushRadius() { return brushRadius; } @Override public ToolParameter getBrushRadius() { return brushRadius; }
@Override public List<ToolParameter> getParameters() { @Override public List<ToolParameter> getParameters() {
return List.of(brushRadius, minSize, maxSize, density); return List.of(brushRadius, minSize, maxSize, density, roughness);
} }
} }

View File

@@ -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;
}
}

View File

@@ -28,6 +28,7 @@ public class TopBarController {
de.blight.editor.toolset.game.GameController gameController, de.blight.editor.toolset.game.GameController gameController,
de.blight.editor.toolset.terrain.TerrainPanelController terrainController, de.blight.editor.toolset.terrain.TerrainPanelController terrainController,
de.blight.editor.toolset.object.ObjectController objectController, de.blight.editor.toolset.object.ObjectController objectController,
de.blight.editor.toolset.stone.StoneController stoneController,
Runnable switchToVegetationGenerator, Runnable switchToVegetationGenerator,
Runnable switchToEzTree, Runnable switchToEzTree,
Runnable switchToTripo, Runnable switchToTripo,
@@ -39,7 +40,8 @@ public class TopBarController {
private final SharedInput input; private final SharedInput input;
private ToggleGroup layerGroup; 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 // ToggleButtons – pre-created so getters work before buildTop() is called
private final ToggleButton auswahlBtn = new ToggleButton("[k] Auswahl"); private final ToggleButton auswahlBtn = new ToggleButton("[k] Auswahl");
@@ -117,6 +119,7 @@ public class TopBarController {
public ToggleButton getTextureBtn() { return textureBtn; } public ToggleButton getTextureBtn() { return textureBtn; }
public ToggleButton getGrassBtn() { return grassBtn; } public ToggleButton getGrassBtn() { return grassBtn; }
public ToggleButton getStoneBtn() { return stoneBtn; } public ToggleButton getStoneBtn() { return stoneBtn; }
public void selectStoneBtnSilently() { suppressStonePanelBuild = true; stoneBtn.setSelected(true); }
public ToggleButton getObjPlaceBtn() { return objPlaceBtn; } public ToggleButton getObjPlaceBtn() { return objPlaceBtn; }
public ToggleButton getAtmosphaereBtn() { return atmosphaereBtn; } public ToggleButton getAtmosphaereBtn() { return atmosphaereBtn; }
public ToggleButton getWasserMergedBtn(){ return wasserMergedBtn; } public ToggleButton getWasserMergedBtn(){ return wasserMergedBtn; }
@@ -293,12 +296,10 @@ public class TopBarController {
}, ownerStage)); }, ownerStage));
grassBtn.setOnAction(e -> guardVoxelExit( grassBtn.setOnAction(e -> guardVoxelExit(
() -> ctx.setRightPanel().accept(deps.grassController().buildPanel()), ownerStage)); () -> ctx.setRightPanel().accept(deps.grassController().buildPanel()), ownerStage));
stoneBtn.setOnAction(e -> guardVoxelExit(() -> { stoneBtn.setOnAction(e -> {
input.activeLayer = SharedInput.LAYER_STONE; if (suppressStonePanelBuild) { suppressStonePanelBuild = false; return; }
input.activeTool = input.stoneTool; guardVoxelExit(() -> ctx.setRightPanel().accept(deps.stoneController().buildPanel()), ownerStage);
ctx.setRightPanel().accept(deps.toolPanel()); });
deps.terrainController().showToolParameters(deps.toolPanel(), input.activeTool);
}, ownerStage));
objPlaceBtn.setOnAction(e -> guardVoxelExit(() -> { objPlaceBtn.setOnAction(e -> guardVoxelExit(() -> {
deps.assetController().expandModelsNode(); deps.assetController().expandModelsNode();
deps.assetController().openOverlay(); deps.assetController().openOverlay();

View File

@@ -20,6 +20,8 @@ public class ViewportController {
private double prevDragX, prevDragY; private double prevDragX, prevDragY;
private double objDragPrevX, objDragPrevY; private double objDragPrevX, objDragPrevY;
private boolean objDragging = false; private boolean objDragging = false;
private double stoneDragPrevX, stoneDragPrevY;
private boolean stoneDragging = false;
private double editPressX, editPressY; private double editPressX, editPressY;
private int editPressAction; private int editPressAction;
private javafx.animation.Timeline editTimer; private javafx.animation.Timeline editTimer;
@@ -107,6 +109,9 @@ public class ViewportController {
if (e.getButton() == MouseButton.PRIMARY && !bothDown) { if (e.getButton() == MouseButton.PRIMARY && !bothDown) {
input.objectClickQueue.offer( input.objectClickQueue.offer(
new SharedInput.ObjectClick((float)e.getX(), (float)e.getY(), false, e.isShiftDown())); 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(); objDragPrevX = e.getX(); objDragPrevY = e.getY();
objDragging = true; objDragging = true;
} else if (e.getButton() == MouseButton.SECONDARY && !bothDown) { } else if (e.getButton() == MouseButton.SECONDARY && !bothDown) {
@@ -173,12 +178,28 @@ public class ViewportController {
return; 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 if (input.activeLayer == SharedInput.LAYER_STONE
&& input.stoneTool.brushRadius.getValue() == 1.0) { && input.stoneTool.brushRadius.getValue() == 1.0) {
// Single-Stone-Modus: nur Rechtsklick, einmalig ohne Timer // Single-Stone-Modus: Linksklick = platzieren, Rechtsklick = neuen Stein würfeln
if (e.getButton() == MouseButton.SECONDARY && !bothDown) { if (!bothDown) {
input.stoneEditQueue.offer( if (e.getButton() == MouseButton.PRIMARY && !e.isAltDown()) {
new SharedInput.StoneEdit((float) e.getX(), (float) e.getY(), -1)); 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) { } else if (bothDown) {
stopEditTimer(); stopEditTimer();
@@ -213,6 +234,17 @@ public class ViewportController {
}); });
viewport.setOnMouseDragged(e -> { 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 (isObjectMode()) {
if (objDragging && e.isPrimaryButtonDown() if (objDragging && e.isPrimaryButtonDown()
&& !e.isSecondaryButtonDown() && !e.isMiddleButtonDown() && !e.isAltDown()) { && !e.isSecondaryButtonDown() && !e.isMiddleButtonDown() && !e.isAltDown()) {
@@ -302,6 +334,7 @@ public class ViewportController {
viewport.setOnMouseReleased(e -> { viewport.setOnMouseReleased(e -> {
objDragging = false; objDragging = false;
stoneDragging = false;
if (!isObjectMode()) { stopEditTimer(); } if (!isObjectMode()) { stopEditTimer(); }
if (input.activeLayer == SharedInput.LAYER_PATH_NETWORK) { if (input.activeLayer == SharedInput.LAYER_PATH_NETWORK) {
if (pathNetDragging) { if (pathNetDragging) {

View File

@@ -323,7 +323,7 @@ public class GrassState extends BaseAppState
int vi = 0; int vi = 0;
for (float[] blade : blades) { for (float[] blade : blades) {
float x = blade[0], y = blade[1], z = blade[2], h = blade[3]; 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(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); pos.put(x+w).put(y ).put(z); uv.put(1).put(0); nrm.put(0).put(1).put(0);

View File

@@ -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[] ax = new float[groupCount];
float[] ay = new float[groupCount]; float[] ay = new float[groupCount];
float[] az = 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; } if (len>1e-6f) { gnx[g]/=len; gny[g]/=len; gnz[g]/=len; }
else { gny[g]=1f; } 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 // 3. Vertex-Normalen schreiben
FloatBuffer normF = mesh.getFloatBuffer(VertexBuffer.Type.Normal); FloatBuffer normF = mesh.getFloatBuffer(VertexBuffer.Type.Normal);
normF.rewind(); normF.rewind();

View File

@@ -18,7 +18,6 @@ import de.blight.common.PlacedStoneIO;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.nio.FloatBuffer;
import java.util.*; import java.util.*;
public class StoneWorldState extends BaseAppState { public class StoneWorldState extends BaseAppState {
@@ -68,13 +67,13 @@ public class StoneWorldState extends BaseAppState {
: 0f; : 0f;
float yCenter = worldY + s.radius() * (1f - 2f * s.sinkFraction()); 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); Geometry geo = new Geometry("stone", mesh);
Material mat = (s.textureSlot() >= 0 && s.textureSlot() < slotMat.length && slotMat[s.textureSlot()] != null) Material mat = (s.textureSlot() >= 0 && s.textureSlot() < slotMat.length && slotMat[s.textureSlot()] != null)
? slotMat[s.textureSlot()] : defMat; ? slotMat[s.textureSlot()] : defMat;
geo.setMaterial(mat); geo.setMaterial(mat);
geo.setLocalTranslation(s.x(), yCenter, s.z()); 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); geo.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
if (bulletAppState != null) { if (bulletAppState != null) {
@@ -173,7 +172,7 @@ public class StoneWorldState extends BaseAppState {
return new float[]{v[0]/len, v[1]/len, v[2]/len}; 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<float[]> verts = new ArrayList<>(Arrays.asList(ICO_V)); List<float[]> verts = new ArrayList<>(Arrays.asList(ICO_V));
List<int[]> faces = new ArrayList<>(Arrays.asList(ICO_F)); List<int[]> faces = new ArrayList<>(Arrays.asList(ICO_F));
@@ -194,53 +193,111 @@ public class StoneWorldState extends BaseAppState {
verts = nv; faces = nf; verts = nv; faces = nf;
} }
Random rng = new Random(noiseSeed); // Variantenreiche Verformung: individuelle Achsenskalierung + 3 Oktaven
float ox = rng.nextFloat() * 50f, oy = rng.nextFloat() * 50f, oz = rng.nextFloat() * 50f; Random rng = new Random(noiseSeed);
float freq = 2.5f + (Math.abs(noiseSeed) % 3); float ox = rng.nextFloat() * 50f;
float amp = 0.22f; 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(); int numVerts = verts.size();
float[] pos = new float[nv2 * 3]; float[] def = new float[numVerts * 3];
float[] nor = new float[nv2 * 3]; for (int i = 0; i < numVerts; i++) {
float[] uv = new float[nv2 * 2]; float[] u = verts.get(i);
float n1 = smoothNoise3D(u[0]*freq+ox, u[1]*freq+oy, u[2]*freq+oz);
for (int i = 0; i < nv2; i++) { 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[] unit = verts.get(i); 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 n = smoothNoise3D(unit[0] * freq + ox, unit[1] * freq + oy, unit[2] * freq + oz); float r = radius * (1f + (n1 * 0.55f + n2 * 0.30f + n3 * 0.15f) * amp);
float r = radius * (1f + n * amp); def[i*3] = u[0] * r * scaleX;
pos[i*3] = unit[0] * r; def[i*3+1] = u[1] * r * flattenY;
pos[i*3+1] = unit[1] * r; def[i*3+2] = u[2] * r * scaleZ;
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);
} }
for (int[] f : faces) { // Taubin-Glättung (lambda-mu, kein Mesh-Schrumpfen)
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]; List<List<Integer>> adj = new ArrayList<>(numVerts);
float bx = pos[bi], by = pos[bi+1], bz = pos[bi+2]; for (int i = 0; i < numVerts; i++) adj.add(new ArrayList<>(6));
float cx = pos[ci], cy = pos[ci+1], cz = pos[ci+2]; for (int[] f : faces) {
float nx = (by-ay)*(cz-az) - (bz-az)*(cy-ay); for (int i = 0; i < 3; i++) {
float ny = (bz-az)*(cx-ax) - (bx-ax)*(cz-az); int a = f[i], b = f[(i + 1) % 3];
float nz = (bx-ax)*(cy-ay) - (by-ay)*(cx-ax); List<Integer> la = adj.get(a), lb = adj.get(b);
for (int vi : f) { nor[vi*3] += nx; nor[vi*3+1] += ny; nor[vi*3+2] += nz; } if (!la.contains(b)) la.add(b);
} if (!lb.contains(a)) lb.add(a);
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; } 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<Integer> 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); // Flat-Shading: jedes Dreieck bekommt eigene Vertices mit Flächennormale + Box-Map UV
FloatBuffer nb = BufferUtils.createFloatBuffer(nor); int fc = faces.size();
FloatBuffer ub = BufferUtils.createFloatBuffer(uv); float[] pos = new float[fc * 9];
java.nio.IntBuffer ib = BufferUtils.createIntBuffer(faces.size() * 3); float[] nor = new float[fc * 9];
for (int[] f : faces) ib.put(f[0]).put(f[1]).put(f[2]); 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(); 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 mesh = new Mesh();
mesh.setBuffer(Type.Position, 3, pb); mesh.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(pos));
mesh.setBuffer(Type.Normal, 3, nb); mesh.setBuffer(Type.Normal, 3, BufferUtils.createFloatBuffer(nor));
mesh.setBuffer(Type.TexCoord, 2, ub); mesh.setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(uv));
mesh.setBuffer(Type.Index, 3, ib); mesh.setBuffer(Type.Index, 3, ib);
mesh.updateBound(); mesh.updateBound();
return mesh; 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 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));
}
} }

Binary file not shown.