diff --git a/assets/imported/models/wooden suspension bridge 3d model.glb b/assets/imported/models/wooden suspension bridge 3d model.glb new file mode 100644 index 0000000..f1e235f Binary files /dev/null and b/assets/imported/models/wooden suspension bridge 3d model.glb differ diff --git a/blight-assets/src/main/resources/MatDefs/Tree.j3md b/blight-assets/src/main/resources/MatDefs/Tree.j3md index b03c915..bca8461 100644 --- a/blight-assets/src/main/resources/MatDefs/Tree.j3md +++ b/blight-assets/src/main/resources/MatDefs/Tree.j3md @@ -7,9 +7,9 @@ MaterialDef Tree { Vector2 WindDir : 0.0 1.0 Texture2D BarkMap Boolean HasBarkMap : false - Vector3 LightDir : 0.6 -0.8 0.4 - Vector3 SunColor : 0.68 0.65 0.60 - Vector3 AmbientColor : 0.08 0.10 0.16 + Vector3 LightDir : 0.45 1.0 0.3 + Vector3 SunColor : 1.4 1.3 1.1 + Vector3 AmbientColor : 0.18 0.18 0.22 // Vom Shadow-Renderer befüllt (PostShadow-Pass) Int BoundDrawBuffer diff --git a/blight-assets/src/main/resources/MatDefs/TreeLeaf.j3md b/blight-assets/src/main/resources/MatDefs/TreeLeaf.j3md index 13ac48e..1186703 100644 --- a/blight-assets/src/main/resources/MatDefs/TreeLeaf.j3md +++ b/blight-assets/src/main/resources/MatDefs/TreeLeaf.j3md @@ -29,9 +29,9 @@ MaterialDef TreeLeaf { Matrix4 LightViewProjectionMatrix4 Matrix4 LightViewProjectionMatrix5 Vector3 LightPos - Vector3 LightDir : 0.6 -0.8 0.4 - Vector3 SunColor : 0.68 0.65 0.60 - Vector3 AmbientColor : 0.08 0.10 0.16 + Vector3 LightDir : 0.45 1.0 0.3 + Vector3 SunColor : 1.4 1.3 1.1 + Vector3 AmbientColor : 0.18 0.18 0.22 Float PCFEdge Float ShadowMapSize Boolean BackfaceShadows : false diff --git a/blight-assets/src/main/resources/MatDefs/Voxel.j3md b/blight-assets/src/main/resources/MatDefs/Voxel.j3md index c4dcb99..e4fe198 100644 --- a/blight-assets/src/main/resources/MatDefs/Voxel.j3md +++ b/blight-assets/src/main/resources/MatDefs/Voxel.j3md @@ -2,6 +2,10 @@ MaterialDef Voxel { MaterialParameters { Texture2D TexFlat + Texture2D TexFlat2 + Texture2D TexFlat3 + Texture2D TexFlat4 + Texture2D SplatMap Texture2D TexSteep Texture2D NormalMapFlat Texture2D NormalMapSteep @@ -32,6 +36,7 @@ MaterialDef Voxel { HAS_LIGHTDIR : LightDir HAS_SCENE_LIGHT : SunColor DEBUG_NO_LIGHT : DebugNoLight + HAS_SPLAT : SplatMap } RenderState { @@ -60,6 +65,7 @@ MaterialDef Voxel { HAS_DISP_STEEP : DisplacementMapSteep HAS_LIGHTDIR : LightDir HAS_SCENE_LIGHT : SunColor + HAS_SPLAT : SplatMap } RenderState { diff --git a/blight-assets/src/main/resources/Models/imported/bridge1.j3o b/blight-assets/src/main/resources/Models/imported/bridge1.j3o new file mode 100644 index 0000000..77edc6e Binary files /dev/null and b/blight-assets/src/main/resources/Models/imported/bridge1.j3o differ diff --git a/blight-assets/src/main/resources/Models/imported/bridge1.j3o.meta b/blight-assets/src/main/resources/Models/imported/bridge1.j3o.meta new file mode 100644 index 0000000..94f9916 --- /dev/null +++ b/blight-assets/src/main/resources/Models/imported/bridge1.j3o.meta @@ -0,0 +1,28 @@ +#Fri Jul 24 16:47:20 CEST 2026 +attachedEmitters.count=0 +attachedLights.count=0 +castShadow=true +category= +cullDistance=120.0 +footstepSurface= +interactableOffsetX=0.0 +interactableOffsetY=0.5 +interactableOffsetZ=0.0 +interactableRotY=0.0 +interactableType=NONE +lod1Distance=30.0 +lod1Path= +lod2Distance=80.0 +lod2Path= +name=bridge1 +pivotOffsetY=0.0 +placementOffsetY=0.0 +randomScaleMax=1.0 +randomScaleMin=1.0 +receiveShadow=true +scaleX=1.0 +scaleY=1.0 +scaleZ=1.0 +solid=true +tags= +uniformScale=true diff --git a/blight-assets/src/main/resources/Models/trees/grapevine/grapevine_20260725_195635.j3o b/blight-assets/src/main/resources/Models/trees/grapevine/grapevine_20260725_195635.j3o new file mode 100644 index 0000000..bae2afa Binary files /dev/null and b/blight-assets/src/main/resources/Models/trees/grapevine/grapevine_20260725_195635.j3o differ diff --git a/blight-assets/src/main/resources/Models/trees/grapevine/grapevine_20260725_195638.j3o b/blight-assets/src/main/resources/Models/trees/grapevine/grapevine_20260725_195638.j3o new file mode 100644 index 0000000..7d0e153 Binary files /dev/null and b/blight-assets/src/main/resources/Models/trees/grapevine/grapevine_20260725_195638.j3o differ diff --git a/blight-assets/src/main/resources/Shaders/Tree.vert b/blight-assets/src/main/resources/Shaders/Tree.vert index cbfd787..fbb460c 100644 --- a/blight-assets/src/main/resources/Shaders/Tree.vert +++ b/blight-assets/src/main/resources/Shaders/Tree.vert @@ -25,14 +25,20 @@ void main() { vec2 windN = (dot(m_WindDir, m_WindDir) > 0.001) ? normalize(m_WindDir) : vec2(0.0, 1.0); vec2 perpN = vec2(-windN.y, windN.x); float wavePhase = dot(worldXZ, windN); - float randPhase = fract(sin(dot(worldXZ, vec2(127.1, 311.7))) * 43758.5453) * 6.2832; + // 1m-Raster für den Phase-Hash: benachbarte Vertices (Ast + Blatt-Basis) landen im + // gleichen Rasterfeld → identische randPhase → Blatt-Basis schwebt nicht mehr + vec2 hashPos = floor(worldXZ); + float randPhase = fract(sin(dot(hashPos, vec2(127.1, 311.7))) * 43758.5453) * 6.2832; float mainSway = sin(t + wavePhase * 0.08 + randPhase) * windW * m_WindStrength; float crossSway = cos(t * 0.73 + wavePhase * 0.06 + randPhase) * windW * m_WindStrength * 0.25; + // Y-Kompression: Äste neigen sich statt zu strecken → verhindert Breiterwerden der Spitzen + float sway2 = mainSway * mainSway + crossSway * crossSway; + vec3 animPos = inPosition + vec3( windN.x * mainSway + perpN.x * crossSway, - 0.0, + -sway2 * 0.5, windN.y * mainSway + perpN.y * crossSway ); diff --git a/blight-assets/src/main/resources/Shaders/Voxel.frag b/blight-assets/src/main/resources/Shaders/Voxel.frag index 8e04c4c..beae359 100644 --- a/blight-assets/src/main/resources/Shaders/Voxel.frag +++ b/blight-assets/src/main/resources/Shaders/Voxel.frag @@ -1,5 +1,12 @@ uniform sampler2D m_TexFlat; uniform sampler2D m_TexSteep; + +#ifdef HAS_SPLAT +uniform sampler2D m_SplatMap; +uniform sampler2D m_TexFlat2; +uniform sampler2D m_TexFlat3; +uniform sampler2D m_TexFlat4; +#endif uniform float m_TexScale; #ifdef HAS_NM_FLAT @@ -57,7 +64,22 @@ void main() { // Flat: reines XZ-UV wie das Terrain (uvY = worldPos.xz / texScale), kein Triplanar. // Steep: Triplanar für alle nicht-flachen Flächen inkl. Decken und Tunnelwände. - vec4 col = texture(m_TexFlat, uvY) * flatBlend +#ifdef HAS_SPLAT + // Splatmap-UV: worldPos.xz linear auf [0,1] (kein Flip nötig – Konvention im Editor: pz=0 → worldZ=-2048) + vec2 splatUV = (vWorldPos.xz + 2048.0) / 4096.0; + vec4 splat = texture(m_SplatMap, splatUV); + float w2 = splat.g; + float w3 = splat.b; + float w4 = splat.a; + float w1 = max(0.0, 1.0 - w2 - w3 - w4); + vec4 flatCol = texture(m_TexFlat, uvY) * w1 + + texture(m_TexFlat2, uvY) * w2 + + texture(m_TexFlat3, uvY) * w3 + + texture(m_TexFlat4, uvY) * w4; +#else + vec4 flatCol = texture(m_TexFlat, uvY); +#endif + vec4 col = flatCol * flatBlend + triplanar(m_TexSteep, uvX, uvY, uvZ, bw) * steepBlend; // Geometrie-Normale für Beleuchtung, ggf. durch Normal-Map ersetzt. diff --git a/blight-assets/src/main/resources/Textures/internal/leaves/ash.png b/blight-assets/src/main/resources/Textures/internal/foliage/ash.png similarity index 100% rename from blight-assets/src/main/resources/Textures/internal/leaves/ash.png rename to blight-assets/src/main/resources/Textures/internal/foliage/ash.png diff --git a/blight-assets/src/main/resources/Textures/internal/leaves/aspen.png b/blight-assets/src/main/resources/Textures/internal/foliage/aspen.png similarity index 100% rename from blight-assets/src/main/resources/Textures/internal/leaves/aspen.png rename to blight-assets/src/main/resources/Textures/internal/foliage/aspen.png diff --git a/blight-assets/src/main/resources/Textures/internal/foliage/fruittree.png b/blight-assets/src/main/resources/Textures/internal/foliage/fruittree.png new file mode 100644 index 0000000..d4e4d43 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/internal/foliage/fruittree.png differ diff --git a/blight-assets/src/main/resources/Textures/internal/leaves/oak.png b/blight-assets/src/main/resources/Textures/internal/foliage/oak.png similarity index 100% rename from blight-assets/src/main/resources/Textures/internal/leaves/oak.png rename to blight-assets/src/main/resources/Textures/internal/foliage/oak.png diff --git a/blight-assets/src/main/resources/Textures/internal/leaves/palm.png b/blight-assets/src/main/resources/Textures/internal/foliage/palm.png similarity index 100% rename from blight-assets/src/main/resources/Textures/internal/leaves/palm.png rename to blight-assets/src/main/resources/Textures/internal/foliage/palm.png diff --git a/blight-assets/src/main/resources/Textures/internal/leaves/palm2.png b/blight-assets/src/main/resources/Textures/internal/foliage/palm2.png similarity index 100% rename from blight-assets/src/main/resources/Textures/internal/leaves/palm2.png rename to blight-assets/src/main/resources/Textures/internal/foliage/palm2.png diff --git a/blight-assets/src/main/resources/Textures/internal/leaves/palmcrown.png b/blight-assets/src/main/resources/Textures/internal/foliage/palmcrown.png similarity index 100% rename from blight-assets/src/main/resources/Textures/internal/leaves/palmcrown.png rename to blight-assets/src/main/resources/Textures/internal/foliage/palmcrown.png diff --git a/blight-assets/src/main/resources/Textures/internal/leaves/pine.png b/blight-assets/src/main/resources/Textures/internal/foliage/pine.png similarity index 100% rename from blight-assets/src/main/resources/Textures/internal/leaves/pine.png rename to blight-assets/src/main/resources/Textures/internal/foliage/pine.png diff --git a/blight-assets/src/main/resources/Textures/internal/foliage/wine.png b/blight-assets/src/main/resources/Textures/internal/foliage/wine.png new file mode 100644 index 0000000..479735d Binary files /dev/null and b/blight-assets/src/main/resources/Textures/internal/foliage/wine.png differ diff --git a/blight-assets/src/main/resources/Textures/internal/fruits/apricot.png b/blight-assets/src/main/resources/Textures/internal/fruits/apricot.png new file mode 100644 index 0000000..7e9885f Binary files /dev/null and b/blight-assets/src/main/resources/Textures/internal/fruits/apricot.png differ diff --git a/blight-assets/src/main/resources/Textures/internal/fruits/lemon.png b/blight-assets/src/main/resources/Textures/internal/fruits/lemon.png new file mode 100644 index 0000000..b207151 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/internal/fruits/lemon.png differ diff --git a/blight-assets/src/main/resources/Textures/internal/fruits/orange.png b/blight-assets/src/main/resources/Textures/internal/fruits/orange.png new file mode 100644 index 0000000..6077427 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/internal/fruits/orange.png differ diff --git a/blight-assets/src/main/resources/Textures/internal/fruits/wine.png b/blight-assets/src/main/resources/Textures/internal/fruits/wine.png new file mode 100644 index 0000000..822407c Binary files /dev/null and b/blight-assets/src/main/resources/Textures/internal/fruits/wine.png differ diff --git a/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf1.png b/blight-assets/src/main/resources/Textures/internal/leafFilter/leaf1.png similarity index 100% rename from blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf1.png rename to blight-assets/src/main/resources/Textures/internal/leafFilter/leaf1.png diff --git a/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf2.png b/blight-assets/src/main/resources/Textures/internal/leafFilter/leaf2.png similarity index 100% rename from blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf2.png rename to blight-assets/src/main/resources/Textures/internal/leafFilter/leaf2.png diff --git a/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf3.png b/blight-assets/src/main/resources/Textures/internal/leafFilter/leaf3.png similarity index 100% rename from blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf3.png rename to blight-assets/src/main/resources/Textures/internal/leafFilter/leaf3.png diff --git a/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf4.png b/blight-assets/src/main/resources/Textures/internal/leafFilter/leaf4.png similarity index 100% rename from blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf4.png rename to blight-assets/src/main/resources/Textures/internal/leafFilter/leaf4.png diff --git a/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf5.png b/blight-assets/src/main/resources/Textures/internal/leafFilter/leaf5.png similarity index 100% rename from blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf5.png rename to blight-assets/src/main/resources/Textures/internal/leafFilter/leaf5.png diff --git a/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf6.png b/blight-assets/src/main/resources/Textures/internal/leafFilter/leaf6.png similarity index 100% rename from blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf6.png rename to blight-assets/src/main/resources/Textures/internal/leafFilter/leaf6.png diff --git a/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf7.png b/blight-assets/src/main/resources/Textures/internal/leafFilter/leaf7.png similarity index 100% rename from blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf7.png rename to blight-assets/src/main/resources/Textures/internal/leafFilter/leaf7.png diff --git a/blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf8.png b/blight-assets/src/main/resources/Textures/internal/leafFilter/leaf8.png similarity index 100% rename from blight-assets/src/main/resources/Textures/internal/leavesfilter/leaf8.png rename to blight-assets/src/main/resources/Textures/internal/leafFilter/leaf8.png 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 d6b1a6d..8106565 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -105,6 +105,14 @@ public class EditorApp extends Application { // Farn-Generator-Zustand private de.blight.editor.tree.FernOptions fernOptions = new de.blight.editor.tree.FernOptions(); + // Fruchtbusch-Generator-Zustand + private de.blight.editor.tree.FruitBushOptions fruitBushOptions = + de.blight.editor.tree.FruitBushOptions.orange(); + private String fruitBushVariant = "Orange"; + + // Weinpflanzen-Generator-Zustand + private de.blight.editor.tree.GrapevineOptions grapevineOptions = new de.blight.editor.tree.GrapevineOptions(); + // Vegetations-Generator-Zustand private String vegetationType = "Baum (Eiche)"; @@ -321,8 +329,8 @@ public class EditorApp extends Application { private ToggleButton playToolBtn; private ToggleButton voxelBtn; private ToggleButton voxelCliffBtn; - private ToggleButton camOrbitBtn; - private ToggleButton camFreeBtn; + private RadioMenuItem camOrbitItem; + private RadioMenuItem camFreeItem; // "Objekt"-Button in der Selektionsleiste (zum Zurückschalten bei importierten Objekten) private ToggleButton selModeObjectBtn; @@ -410,7 +418,20 @@ public class EditorApp extends Application { Scene scene = new Scene(root, 1280, 760); java.net.URL cssUrl = getClass().getResource("/editor.css"); if (cssUrl != null) scene.getStylesheets().add(cssUrl.toExternalForm()); - scene.addEventFilter(javafx.scene.input.KeyEvent.KEY_PRESSED, e -> handleKeyPress(e.getCode(), true)); + scene.addEventFilter(javafx.scene.input.KeyEvent.KEY_PRESSED, e -> { + if (e.getCode() == KeyCode.C && e.isAltDown()) { + e.consume(); + if (input.camMode == SharedInput.CAM_ORBIT) { + input.camMode = SharedInput.CAM_FREEFLY; + if (camFreeItem != null) { camFreeItem.setSelected(true); } + } else { + input.camMode = SharedInput.CAM_ORBIT; + if (camOrbitItem != null) { camOrbitItem.setSelected(true); } + } + return; + } + handleKeyPress(e.getCode(), true); + }); scene.addEventFilter(javafx.scene.input.KeyEvent.KEY_RELEASED, e -> handleKeyPress(e.getCode(), false)); scene.setOnKeyTyped(e -> { if (!input.consoleIsOpen) return; @@ -962,7 +983,7 @@ public class EditorApp extends Application { ComboBox typeBox = new ComboBox<>(); typeBox.getItems().addAll( "Baum (Eiche)", "Baum (Birke)", "Baum (Kiefer)", "Baum (Weide)", "Baum (Busch)", - "Farn", "Palme"); + "Farn", "Palme", "Fruchtbusch", "Weinpflanze"); typeBox.setValue(vegetationType); typeBox.setOnAction(e -> { vegetationType = typeBox.getValue(); @@ -986,6 +1007,10 @@ public class EditorApp extends Application { treeParams.copy(), true, treeTypeFromPreset(currentTreePreset))); } else if ("Farn".equals(vegetationType)) { input.fernGenQueue.offer(new SharedInput.FernGenRequest(fernOptions.copy(), true)); + } else if ("Fruchtbusch".equals(vegetationType)) { + input.fruitBushGenQueue.offer(new SharedInput.FruitBushGenRequest(fruitBushOptions.copy(), fruitBushVariant, true)); + } else if ("Weinpflanze".equals(vegetationType)) { + input.grapevineGenQueue.offer(new SharedInput.GrapevineGenRequest(grapevineOptions.copy(), true)); } else { input.palmGenQueue.offer(new SharedInput.PalmGenRequest(palmOptions.copy(), true)); } @@ -1027,6 +1052,28 @@ public class EditorApp extends Application { root.setRight(buildVegetationParamsPanel()); onF5.run(); }; + } else if ("Fruchtbusch".equals(vegetationType)) { + onF5 = () -> { + input.fruitBushGenQueue.offer( + new SharedInput.FruitBushGenRequest(fruitBushOptions.copy(), fruitBushVariant, false)); + setStatus("Fruchtbusch: generiere Vorschau…"); + }; + onF6 = () -> { + fruitBushOptions.seed = new java.util.Random().nextInt(1000000); + root.setRight(buildVegetationParamsPanel()); + onF5.run(); + }; + } else if ("Weinpflanze".equals(vegetationType)) { + onF5 = () -> { + input.grapevineGenQueue.offer( + new SharedInput.GrapevineGenRequest(grapevineOptions.copy(), false)); + setStatus("Weinpflanze: generiere Vorschau…"); + }; + onF6 = () -> { + grapevineOptions.seed = new java.util.Random().nextInt(1000000); + root.setRight(buildVegetationParamsPanel()); + onF5.run(); + }; } else { onF5 = () -> { input.palmGenQueue.offer(new SharedInput.PalmGenRequest(palmOptions.copy(), false)); @@ -1045,6 +1092,10 @@ public class EditorApp extends Application { return buildTreeParamsPanel(); } else if ("Farn".equals(vegetationType)) { return buildFernParamsPanel(); + } else if ("Fruchtbusch".equals(vegetationType)) { + return buildFruitBushParamsPanel(); + } else if ("Weinpflanze".equals(vegetationType)) { + return buildGrapevineParamsPanel(); } else { return buildPalmParamsPanel(); } @@ -1114,6 +1165,113 @@ public class EditorApp extends Application { return panel; } + private VBox buildFruitBushParamsPanel() { + VBox inner = new VBox(8); + inner.setPadding(new Insets(10)); + + // ── Variante ────────────────────────────────────────────────────────── + inner.getChildren().addAll(sectionTitle("Fruchtbusch-Typ"), new Separator()); + ComboBox variantBox = new ComboBox<>(); + variantBox.getItems().addAll("Orange", "Zitrone", "Aprikose"); + variantBox.setValue(fruitBushVariant); + variantBox.setMaxWidth(Double.MAX_VALUE); + variantBox.setOnAction(e -> { + fruitBushVariant = variantBox.getValue(); + int oldSeed = fruitBushOptions.seed; + fruitBushOptions = switch (fruitBushVariant) { + case "Zitrone" -> de.blight.editor.tree.FruitBushOptions.lemon(); + case "Aprikose" -> de.blight.editor.tree.FruitBushOptions.apricot(); + default -> de.blight.editor.tree.FruitBushOptions.orange(); + }; + fruitBushOptions.seed = oldSeed; + root.setRight(buildVegetationParamsPanel()); + if (onF5 != null) onF5.run(); + }); + inner.getChildren().add(variantBox); + + // ── Allgemein ───────────────────────────────────────────────────────── + inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator()); + inner.getChildren().add(bold("Zufallssamen:")); + Spinner seedSp = intSpinner(0, 999999, fruitBushOptions.seed); + seedSp.valueProperty().addListener((o, a, b) -> fruitBushOptions.seed = b); + Button rndSeedBtn = new Button("🎲"); + rndSeedBtn.setOnAction(e -> { + int s = new java.util.Random().nextInt(1000000); + fruitBushOptions.seed = s; + seedSp.getValueFactory().setValue(s); + if (onF5 != null) onF5.run(); + }); + HBox seedRow = new HBox(4, seedSp, rndSeedBtn); + HBox.setHgrow(seedSp, Priority.ALWAYS); + inner.getChildren().add(seedRow); + + // ── Stamm ───────────────────────────────────────────────────────────── + inner.getChildren().addAll(sectionTitle("Stamm"), new Separator()); + inner.getChildren().add(ezFloat("Höhe (m):", 0.2, 1.5, fruitBushOptions.trunkHeight, + v -> fruitBushOptions.trunkHeight = v)); + inner.getChildren().add(ezFloat("Radius (m):", 0.01, 0.15, fruitBushOptions.trunkRadius, + v -> fruitBushOptions.trunkRadius = v)); + + // ── Äste ────────────────────────────────────────────────────────────── + inner.getChildren().addAll(sectionTitle("Äste"), new Separator()); + inner.getChildren().add(bold("Anzahl Primäräste:")); + Spinner branchCountSp = intSpinner(2, 10, fruitBushOptions.branchCount); + branchCountSp.valueProperty().addListener((o, a, b) -> fruitBushOptions.branchCount = b); + inner.getChildren().add(branchCountSp); + + inner.getChildren().add(ezFloat("Winkel (°):", 10, 80, fruitBushOptions.branchAngle, + v -> fruitBushOptions.branchAngle = v)); + inner.getChildren().add(ezFloat("Länge (m):", 0.2, 2.0, fruitBushOptions.branchLength, + v -> fruitBushOptions.branchLength = v)); + + inner.getChildren().add(bold("Reiser pro Ast:")); + Spinner twigCountSp = intSpinner(1, 8, fruitBushOptions.twigCount); + twigCountSp.valueProperty().addListener((o, a, b) -> fruitBushOptions.twigCount = b); + inner.getChildren().add(twigCountSp); + + inner.getChildren().add(ezFloat("Reiser-Winkel (°):", 10, 80, fruitBushOptions.twigAngle, + v -> fruitBushOptions.twigAngle = v)); + inner.getChildren().add(ezFloat("Reiser-Länge (m):", 0.1, 1.2, fruitBushOptions.twigLength, + v -> fruitBushOptions.twigLength = v)); + + // ── Blätter & Früchte ───────────────────────────────────────────────── + inner.getChildren().addAll(sectionTitle("Blätter & Früchte"), new Separator()); + inner.getChildren().add(ezFloat("Blattgröße (m):", 0.05, 0.6, fruitBushOptions.leafSize, + v -> fruitBushOptions.leafSize = v)); + inner.getChildren().add(bold("Blätter pro Reiser:")); + Spinner leafCountSp = intSpinner(2, 20, fruitBushOptions.leafCount); + leafCountSp.valueProperty().addListener((o, a, b) -> fruitBushOptions.leafCount = b); + inner.getChildren().add(leafCountSp); + + inner.getChildren().add(ezFloat("Frucht-Dichte [0–1]:", 0.0, 1.0, fruitBushOptions.fruitDensity, + v -> fruitBushOptions.fruitDensity = v)); + inner.getChildren().add(ezFloat("Frucht-Radius (m):", 0.02, 0.15, fruitBushOptions.fruitRadius, + v -> fruitBushOptions.fruitRadius = v)); + + // ── Wind ────────────────────────────────────────────────────────────── + inner.getChildren().addAll(sectionTitle("Wind"), new Separator()); + inner.getChildren().add(ezFloat("Windstärke:", 0.0, 0.5, fruitBushOptions.windStrength, + v -> fruitBushOptions.windStrength = v)); + inner.getChildren().add(ezFloat("Windgeschwindigkeit:", 0.1, 2.0, fruitBushOptions.windSpeed, + v -> fruitBushOptions.windSpeed = v)); + + Button previewBtn = new Button("▶ Vorschau"); + previewBtn.setMaxWidth(Double.MAX_VALUE); + previewBtn.setStyle("-fx-background-color:#2d8a3e;-fx-text-fill:white;-fx-font-weight:bold;-fx-padding:6 12 6 12;"); + previewBtn.setOnAction(e -> { if (onF5 != null) onF5.run(); }); + inner.getChildren().addAll(new Separator(), previewBtn); + + ScrollPane scroll = new ScrollPane(inner); + scroll.setFitToWidth(true); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setStyle("-fx-background-color:transparent;-fx-background:transparent;"); + VBox panel = new VBox(scroll); + VBox.setVgrow(scroll, Priority.ALWAYS); + panel.setPrefWidth(270); + panel.setStyle("-fx-background-color:#f0f0f0;-fx-border-color:#ccc;-fx-border-width:0 0 0 1;"); + return panel; + } + private void switchToTripo() { currentTool = "tripo"; topBar.getChildren().set(1, buildTripoToolBar()); @@ -1209,8 +1367,19 @@ public class EditorApp extends Application { }); viewTopologyItem.setOnAction(e -> input.topologyRequest = viewTopologyItem.isSelected() ? 1 : 2); + + ToggleGroup camMenuGroup = new ToggleGroup(); + camOrbitItem = new RadioMenuItem("⊙ Orbit-Kamera (Alt+C)"); + camFreeItem = new RadioMenuItem("✈ FreeFly-Kamera (Alt+C)"); + camOrbitItem.setToggleGroup(camMenuGroup); + camFreeItem.setToggleGroup(camMenuGroup); + camOrbitItem.setSelected(true); + camOrbitItem.setOnAction(e -> input.camMode = SharedInput.CAM_ORBIT); + camFreeItem.setOnAction(e -> input.camMode = SharedInput.CAM_FREEFLY); + viewMenu.getItems().addAll(resetCam, new SeparatorMenuItem(), viewTexture, viewWireframe, - new SeparatorMenuItem(), viewTopologyItem); + new SeparatorMenuItem(), viewTopologyItem, + new SeparatorMenuItem(), camOrbitItem, camFreeItem); Menu zeitMenu = new Menu("Zeit"); ToggleGroup zeitGroup = new ToggleGroup(); @@ -1240,11 +1409,9 @@ public class EditorApp extends Application { areaBtn = new ToggleButton("🗺 Bereiche"); locationZoneBtn = new ToggleButton("📍 Locations"); playToolBtn = new ToggleButton("🎮 Spielen"); - voxelBtn = new ToggleButton("⬡ Voxel"); - voxelCliffBtn = new ToggleButton("⛰ Klippe"); + voxelBtn = new ToggleButton("⬡ Voxel"); + voxelCliffBtn = new ToggleButton("⛰ Klippe"); stoneBtn = new ToggleButton("🪨 Steine"); - camOrbitBtn = new ToggleButton("⊙ Orbit"); - camFreeBtn = new ToggleButton("✈ FreeFly"); baseBtn.setStyle("-fx-font-weight:bold;"); grassBtn.setStyle("-fx-font-weight:bold;"); grassVertexBtn.setStyle("-fx-font-weight:bold;"); @@ -1262,8 +1429,6 @@ public class EditorApp extends Application { voxelBtn.setStyle("-fx-font-weight:bold;"); voxelCliffBtn.setStyle("-fx-font-weight:bold;"); stoneBtn.setStyle("-fx-font-weight:bold;"); - camOrbitBtn.setStyle("-fx-font-weight:bold;"); - camFreeBtn.setStyle("-fx-font-weight:bold;"); ToggleGroup layerGroup = new ToggleGroup(); baseBtn.setToggleGroup(layerGroup); @@ -1286,6 +1451,9 @@ public class EditorApp extends Application { baseBtn.setSelected(true); baseBtn.setOnAction(e -> { + input.heightTool.brushRadius.setValue(input.voxelTool.brushRadius.getValue()); + input.heightTool.brushStrength.setValue(input.voxelTool.brushStrength.getValue()); + input.heightTool.plateauHeight.setValue(input.voxelTool.plateauTarget.getValue()); input.activeLayer = 0; input.activeTool = input.heightTool; root.setRight(toolPanel); showToolParameters(toolPanel, input.activeTool); @@ -1352,6 +1520,9 @@ public class EditorApp extends Application { root.setRight(buildPlayToolPanel()); }); voxelBtn.setOnAction(e -> { + input.voxelTool.brushRadius.setValue(input.heightTool.brushRadius.getValue()); + input.voxelTool.brushStrength.setValue(input.heightTool.brushStrength.getValue()); + input.voxelTool.plateauTarget.setValue(input.heightTool.plateauHeight.getValue()); input.activeLayer = SharedInput.LAYER_VOXEL; input.activeTool = input.voxelTool; root.setRight(toolPanel); @@ -1368,13 +1539,6 @@ public class EditorApp extends Application { showToolParameters(toolPanel, input.activeTool); }); - ToggleGroup camModeGroup = new ToggleGroup(); - camOrbitBtn.setToggleGroup(camModeGroup); - camFreeBtn.setToggleGroup(camModeGroup); - camOrbitBtn.setSelected(true); - camOrbitBtn.setOnAction(e -> input.camMode = SharedInput.CAM_ORBIT); - camFreeBtn.setOnAction(e -> input.camMode = SharedInput.CAM_FREEFLY); - Button camResetBtn = new Button("⌂ Reset"); camResetBtn.setStyle("-fx-font-weight:bold;"); camResetBtn.setTooltip(new javafx.scene.control.Tooltip("Kamera auf x=0, z=0, y=Terrain+10m zurücksetzen")); @@ -1393,7 +1557,7 @@ public class EditorApp extends Application { new Separator(Orientation.VERTICAL), playToolBtn, new Separator(Orientation.VERTICAL), voxelBtn, voxelCliffBtn, new Separator(Orientation.VERTICAL), stoneBtn, - new Separator(Orientation.VERTICAL), camOrbitBtn, camFreeBtn, camResetBtn, + new Separator(Orientation.VERTICAL), camResetBtn, new Separator(Orientation.VERTICAL), hint); worldToolBar = toolBar; @@ -1449,6 +1613,76 @@ public class EditorApp extends Application { return plantPreviewPanel; } + // ── Weinpflanzen-Generator – rechtes Parameter-Panel ───────────────────── + + private VBox buildGrapevineParamsPanel() { + VBox inner = new VBox(8); + inner.setPadding(new Insets(10)); + + inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator()); + inner.getChildren().add(bold("Zufallssamen:")); + Spinner seedSp = intSpinner(0, 999999, grapevineOptions.seed); + seedSp.valueProperty().addListener((o, a, b) -> grapevineOptions.seed = b); + Button rndSeedBtn = new Button("🎲"); + rndSeedBtn.setOnAction(e -> { + int s = new java.util.Random().nextInt(1000000); + grapevineOptions.seed = s; + seedSp.getValueFactory().setValue(s); + if (onF5 != null) onF5.run(); + }); + HBox seedRow = new HBox(4, seedSp, rndSeedBtn); + HBox.setHgrow(seedSp, Priority.ALWAYS); + inner.getChildren().add(seedRow); + + inner.getChildren().addAll(sectionTitle("Rahmen"), new Separator()); + inner.getChildren().add(ezFloat("Gesamtbreite (m):", 2.0, 10.0, grapevineOptions.totalWidth, + v -> grapevineOptions.totalWidth = v)); + inner.getChildren().add(ezFloat("Gesamthöhe (m):", 1.5, 4.0, grapevineOptions.totalHeight, + v -> grapevineOptions.totalHeight = v)); + inner.getChildren().add(ezFloat("1. Draht-Höhe (m):", 0.5, 2.0, grapevineOptions.firstWireH, + v -> grapevineOptions.firstWireH = v)); + inner.getChildren().add(bold("Draht-Anzahl:")); + Spinner wireCountSp = intSpinner(2, 6, grapevineOptions.wireCount); + wireCountSp.valueProperty().addListener((o, a, b) -> grapevineOptions.wireCount = b); + inner.getChildren().add(wireCountSp); + + inner.getChildren().addAll(sectionTitle("Triebe & Blätter"), new Separator()); + inner.getChildren().add(bold("Triebe pro Seite:")); + Spinner shootCountSp = intSpinner(2, 14, grapevineOptions.shootCount); + shootCountSp.valueProperty().addListener((o, a, b) -> grapevineOptions.shootCount = b); + inner.getChildren().add(shootCountSp); + inner.getChildren().add(ezFloat("Blattgröße (m):", 0.10, 0.60, grapevineOptions.leafSize, + v -> grapevineOptions.leafSize = v)); + inner.getChildren().add(bold("Blätter pro Trieb:")); + Spinner leafCountSp = intSpinner(2, 128, grapevineOptions.leafCount); + leafCountSp.valueProperty().addListener((o, a, b) -> grapevineOptions.leafCount = b); + inner.getChildren().add(leafCountSp); + + inner.getChildren().addAll(sectionTitle("Wind"), new Separator()); + inner.getChildren().add(ezFloat("Windstärke:", 0.0, 0.5, grapevineOptions.windStrength, + v -> grapevineOptions.windStrength = v)); + inner.getChildren().add(ezFloat("Windgeschwindigkeit:", 0.1, 2.0, grapevineOptions.windSpeed, + v -> grapevineOptions.windSpeed = v)); + + Button previewBtn = new Button("▶ Vorschau"); + previewBtn.setMaxWidth(Double.MAX_VALUE); + previewBtn.setStyle("-fx-background-color:#2d8a3e;-fx-text-fill:white;-fx-font-weight:bold;-fx-padding:6 12 6 12;"); + previewBtn.setOnAction(e -> { if (onF5 != null) onF5.run(); }); + + inner.getChildren().add(new Separator()); + inner.getChildren().add(previewBtn); + + ScrollPane scroll = new ScrollPane(inner); + scroll.setFitToWidth(true); + scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scroll.setStyle("-fx-background-color:transparent;-fx-background:transparent;"); + VBox panel = new VBox(scroll); + VBox.setVgrow(scroll, Priority.ALWAYS); + panel.setPrefWidth(270); + panel.setStyle("-fx-background-color:#f0f0f0;-fx-border-color:#ccc;-fx-border-width:0 0 0 1;"); + return panel; + } + // ── Baum-Generator – rechtes Parameter-Panel ───────────────────────────── private VBox buildTreeParamsPanel() { @@ -1846,7 +2080,7 @@ public class EditorApp extends Application { && palmOptions.leafTexture.contains("palm2") ? "palm2.png" : "palm.png"; leafTexBox.setValue(currentLeaf); leafTexBox.setMaxWidth(Double.MAX_VALUE); - leafTexBox.setOnAction(e -> palmOptions.leafTexture = "Textures/internal/leaves/" + leafTexBox.getValue()); + leafTexBox.setOnAction(e -> palmOptions.leafTexture = "Textures/internal/foliage/" + leafTexBox.getValue()); inner.getChildren().add(new VBox(2, leafTexLabel, leafTexBox)); // Rinden-Textur-Auswahl (alle Texturen aus dem bark-Ordner) @@ -3260,7 +3494,9 @@ public class EditorApp extends Application { new javafx.scene.control.Separator(), scaleLbl, globalScaleSpin, new javafx.scene.control.Separator(), - slotsScroll); + slotsScroll, + new javafx.scene.control.Separator(), + buildVoxelSplatSectionUI()); } // Textur-Picker (nur beim GrassTool) @@ -3714,6 +3950,39 @@ public class EditorApp extends Application { return box; } + private javafx.scene.Node buildVoxelSplatSectionUI() { + VBox box = new VBox(4); + Label title = new Label("Voxel-Splatmap (Slots 2–4)"); + title.setStyle("-fx-font-weight: bold; -fx-text-fill: #111; -fx-font-size: 11;"); + Label hint = new Label("Slot 1 (Basis) = Voxel-Flat-Slot oben"); + hint.setStyle("-fx-text-fill: #888; -fx-font-size: 10;"); + box.getChildren().addAll(title, hint); + + String[] slotLabels = {"Slot 2 (G)", "Slot 3 (B)", "Slot 4 (A)"}; + for (int i = 1; i <= 3; i++) { + final int si = i; + String[] paths = input.voxelSplatTexturePaths; + String cur = (paths != null && i < paths.length && paths[i] != null) ? paths[i] : ""; + Label nameLbl = new Label(slotLabels[i - 1] + ": " + labelFromPath(cur)); + nameLbl.setStyle("-fx-font-size: 10; -fx-text-fill: #444;"); + nameLbl.setMaxWidth(180); + Button chooseBtn = new Button("..."); + chooseBtn.setOnAction(e -> { + de.blight.editor.ui.TextureChooser chooser = + new de.blight.editor.ui.TextureChooser(ASSET_ROOT, false); + chooser.showAndWait().ifPresent(chosenPath -> { + String[] copy = input.voxelSplatTexturePaths.clone(); + copy[si] = chosenPath; + input.voxelSplatTexturePaths = copy; + input.voxelSplatTexturesChanged = true; + nameLbl.setText(slotLabels[si - 1] + ": " + labelFromPath(chosenPath)); + }); + }); + box.getChildren().add(new HBox(4, nameLbl, chooseBtn)); + } + return box; + } + private javafx.scene.Node buildVoxelTextureChoiceUI(ChoiceToolParameter param) { ToggleGroup tg = new ToggleGroup(); javafx.scene.layout.TilePane tile = new javafx.scene.layout.TilePane(); @@ -6624,7 +6893,8 @@ public class EditorApp extends Application { input.modelEditorScaleY = 1f; input.modelEditorScaleZ = 1f; input.modelEditorPivotY = 0f; - input.modelEditorOpenPath = relPath; + input.modelEditorOpenPath = relPath; + input.modelImportIntermediatePath = relPath; root.setRight(buildModelImportPanel(relPath, suggestedName)); setStatus("Modell-Import: " + relPath); @@ -7549,7 +7819,11 @@ public class EditorApp extends Application { case 3 -> input.grassEditQueue.offer(new SharedInput.GrassEdit((float) x, (float) y, action)); case SharedInput.LAYER_GRASS_VERTEX -> input.grassVertexEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action)); - case 4 -> input.textureEditQueue.offer(new SharedInput.TextureEdit((float) x, (float) y, action)); + case 4 -> { + SharedInput.TextureEdit te = new SharedInput.TextureEdit((float) x, (float) y, action); + input.textureEditQueue.offer(te); + input.voxelTextureEditQueue.offer(te); + } case SharedInput.LAYER_LIGHTS -> input.lightClickQueue.offer(new SharedInput.LightClick((float) x, (float) y, action < 0)); case SharedInput.LAYER_EMITTERS -> diff --git a/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java b/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java index ce9715a..ce28296 100644 --- a/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java @@ -30,6 +30,8 @@ import de.blight.editor.state.WaterBodyState; import de.blight.editor.state.EzTreeState; import de.blight.editor.state.LightState; import de.blight.editor.state.FernGeneratorState; +import de.blight.editor.state.FruitBushGeneratorState; +import de.blight.editor.state.GrapevineGeneratorState; import de.blight.editor.state.PalmGeneratorState; import de.blight.editor.state.SceneObjectState; import de.blight.editor.state.TerrainEditorState; @@ -189,6 +191,8 @@ public class JmeEditorApp extends SimpleApplication { stateManager.attach(new EzTreeState(input)); stateManager.attach(new PalmGeneratorState(input)); stateManager.attach(new FernGeneratorState(input)); + stateManager.attach(new FruitBushGeneratorState(input)); + stateManager.attach(new GrapevineGeneratorState(input)); stateManager.attach(new LightState(input)); stateManager.attach(new EmitterState(input)); stateManager.attach(new WaterBodyState(input)); diff --git a/blight-editor/src/main/java/de/blight/editor/LegacyAssetRedirectLocator.java b/blight-editor/src/main/java/de/blight/editor/LegacyAssetRedirectLocator.java index 57c48a8..2a28a09 100644 --- a/blight-editor/src/main/java/de/blight/editor/LegacyAssetRedirectLocator.java +++ b/blight-editor/src/main/java/de/blight/editor/LegacyAssetRedirectLocator.java @@ -22,11 +22,12 @@ import java.util.Map; public class LegacyAssetRedirectLocator implements AssetLocator { private static final Map PREFIXES = Map.of( - "Textures/bark/", "Textures/internal/bark/", - "Textures/leaves/", "Textures/internal/leaves/", - "Textures/fern/", "Textures/internal/fern/", - "Textures/water/", "Textures/internal/water/", - "Textures/Water/", "Textures/internal/water/" + "Textures/bark/", "Textures/internal/bark/", + "Textures/leaves/", "Textures/internal/foliage/", + "Textures/internal/leaves/", "Textures/internal/foliage/", + "Textures/fern/", "Textures/internal/fern/", + "Textures/water/", "Textures/internal/water/", + "Textures/Water/", "Textures/internal/water/" ); private Path root; 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 fa9ddb1..7866e80 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -100,6 +100,14 @@ public class SharedInput { public record FernGenRequest(de.blight.editor.tree.FernOptions options, boolean exportAfter) {} public final ConcurrentLinkedQueue fernGenQueue = new ConcurrentLinkedQueue<>(); + // ── Fruchtbusch-Generator ───────────────────────────────────────────────── + public record FruitBushGenRequest(de.blight.editor.tree.FruitBushOptions options, String presetName, boolean exportAfter) {} + public final ConcurrentLinkedQueue fruitBushGenQueue = new ConcurrentLinkedQueue<>(); + + // ── Weinpflanzen-Generator ──────────────────────────────────────────────── + public record GrapevineGenRequest(de.blight.editor.tree.GrapevineOptions options, boolean exportAfter) {} + public final ConcurrentLinkedQueue grapevineGenQueue = new ConcurrentLinkedQueue<>(); + // ── Gras-Einstellungen (JavaFX → JME3) ─────────────────────────────────── /** Relativer Asset-Pfad der Gras-Textur ("" = Standardfarbe). */ public volatile String grassTexturePath = ""; @@ -868,6 +876,8 @@ public class SharedInput { public volatile String modelImportExportName = null; /** JME → JFX: Status-Meldung nach dem Export (relativer Pfad oder "FEHLER: …"). */ public volatile String modelImportExportStatus = null; + /** JFX: Pfad der Zwischen-.j3o-Datei (relativ zu ASSET_ROOT). Wird von ModelImportState nach Export gelöscht, nicht von ModelEditorState. */ + public volatile String modelImportIntermediatePath = null; // ── Mesh-Sculpting ─────────────────────────────────────────────────────── /** activeLayer==24 → gebackene Voxel-Meshes direkt sculpten */ @@ -1040,4 +1050,14 @@ public class SharedInput { public volatile float cliffRoughnessAmp = 4.0f; /** 3D-Noise-Frequenz für die Kliff-Oberfläche (höher = feiner Detail). */ public volatile float cliffRoughnessScale = 0.12f; + + // ── Voxel-Textur-Malen ──────────────────────────────────────────────────── + /** Parallel-Queue zu textureEditQueue – wird von submitEdit(layer=4) mitbefüllt, + * damit VoxelEditorState unabhängig vom Terrain seine Splatmap beschreiben kann. */ + public final ConcurrentLinkedQueue voxelTextureEditQueue = new ConcurrentLinkedQueue<>(); + + /** Pfade der 4 Voxel-Splatmap-Slots (Slot 0 = Basis/TexFlat, Slots 1-3 = G/B/A). */ + public volatile String[] voxelSplatTexturePaths = new String[]{"", "", "", ""}; + /** JFX setzt true wenn Voxel-Splat-Texturen geändert wurden; JME liest + resettet. */ + public volatile boolean voxelSplatTexturesChanged = false; } diff --git a/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java b/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java index c60c3e9..62c224b 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/EzTreeState.java @@ -449,6 +449,16 @@ public class EzTreeState extends BaseAppState { Node lodRoot = assembleLodNode(req.presetName(), hdNode, ld1Node, bb, atlasT2d); exportTree(lodRoot, exportName, subPath); + + // assembleLodNode hat hdNode aus previewTreeHolder entnommen (JME3: nur ein Parent). + // Nach dem Export die Vorschau wiederherstellen. + float camDist = bb != null + ? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3.2f + : 20f; + Vector3f target = bb != null + ? new Vector3f(0f, bb.getCenter().y, 0f) + : new Vector3f(0f, 5f, 0f); + previewHost.setPreviewContent(hdNode, camDist, target); } // ── Material-Aufbau ─────────────────────────────────────────────────────── @@ -466,6 +476,7 @@ public class EzTreeState extends BaseAppState { g.setQueueBucket(RenderQueue.Bucket.Transparent); g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); } + } } else if (child instanceof Node trellis) { Material mat = buildBarkMat(opts); diff --git a/blight-editor/src/main/java/de/blight/editor/state/FruitBushGeneratorState.java b/blight-editor/src/main/java/de/blight/editor/state/FruitBushGeneratorState.java new file mode 100644 index 0000000..8b02131 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/state/FruitBushGeneratorState.java @@ -0,0 +1,199 @@ +package de.blight.editor.state; + +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.BoundingBox; +import com.jme3.export.binary.BinaryExporter; +import com.jme3.material.Material; +import com.jme3.material.RenderState; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector3f; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.Node; +import com.jme3.texture.Texture; +import de.blight.editor.SharedInput; +import de.blight.editor.tree.FruitBushMeshBuilder; +import de.blight.editor.tree.FruitBushOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class FruitBushGeneratorState extends BaseAppState { + + private static final Logger log = LoggerFactory.getLogger(FruitBushGeneratorState.class); + private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve( + "blight-assets", "src", "main", "resources"); + + private final SharedInput input; + private SimpleApplication app; + private AssetManager assets; + private TreeGeneratorState previewHost; + + public FruitBushGeneratorState(SharedInput input) { this.input = input; } + + @Override + protected void initialize(Application app) { + this.app = (SimpleApplication) app; + this.assets = app.getAssetManager(); + } + @Override protected void cleanup(Application app) {} + @Override protected void onEnable() {} + @Override protected void onDisable() {} + + @Override + public void update(float tpf) { + if (previewHost == null) { + previewHost = getStateManager().getState(TreeGeneratorState.class); + if (previewHost == null) return; + } + + SharedInput.FruitBushGenRequest req = input.fruitBushGenQueue.poll(); + if (req == null) return; + + FruitBushOptions opts = req.options(); + FruitBushMeshBuilder.MeshResult result = FruitBushMeshBuilder.build(opts); + Node bush = assembleBushNode(result, opts); + bush.updateGeometricState(); + + BoundingBox bb = bush.getWorldBound() instanceof BoundingBox b ? b : null; + float dist = bb != null + ? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 4f + : 3f; + Vector3f target = bb != null + ? new Vector3f(0f, bb.getCenter().y, 0f) + : new Vector3f(0f, 1f, 0f); + + final Node finalBush = bush; + final float finalDist = dist; + final Vector3f finalTarget = target; + final boolean doExport = req.exportAfter(); + final String presetName = req.presetName(); + + app.enqueue(() -> { + previewHost.setPreviewContent(finalBush, finalDist, finalTarget); + if (doExport) { + exportBush(finalBush, presetName); + } + }); + input.treeGenStatusMsg = doExport ? "Fruchtbusch: exportiere…" : "Fruchtbusch: Vorschau"; + } + + private Node assembleBushNode(FruitBushMeshBuilder.MeshResult result, FruitBushOptions opts) { + Node node = new Node("fruchtbusch"); + + Geometry bark = new Geometry("bark", result.bark()); + bark.setMaterial(buildBarkMat(opts)); + bark.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); + node.attachChild(bark); + + if (result.leaves().getVertexCount() > 0) { + Geometry leaves = new Geometry("leaves", result.leaves()); + leaves.setMaterial(buildLeafMat(opts)); + leaves.setQueueBucket(RenderQueue.Bucket.Transparent); + leaves.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); + node.attachChild(leaves); + } + + if (opts.fruitsEnabled && result.fruits().getVertexCount() > 0) { + Geometry fruits = new Geometry("fruits", result.fruits()); + fruits.setMaterial(buildFruitMat(opts)); + fruits.setQueueBucket(RenderQueue.Bucket.Transparent); + fruits.setShadowMode(RenderQueue.ShadowMode.Off); + node.attachChild(fruits); + } + + return node; + } + + private Material buildBarkMat(FruitBushOptions opts) { + try { + Material mat = new Material(assets, "MatDefs/Tree.j3md"); + mat.setColor("Diffuse", new ColorRGBA(0.72f, 0.60f, 0.45f, 1f)); + mat.setFloat("WindStrength", opts.windStrength * 0.4f); + mat.setFloat("WindSpeed", opts.windSpeed); + mat.setVector3("LightDir", new Vector3f(0.45f, 1.0f, 0.3f).normalizeLocal()); + mat.setVector3("SunColor", new Vector3f(1.4f, 1.3f, 1.1f)); + mat.setVector3("AmbientColor", new Vector3f(0.18f, 0.18f, 0.22f)); + try { + Texture barkTex = assets.loadTexture("Textures/internal/bark/Bark001_Color.jpg"); + barkTex.setWrap(Texture.WrapMode.Repeat); + mat.setTexture("BarkMap", barkTex); + mat.setBoolean("HasBarkMap", true); + } catch (Exception ignored) {} + return mat; + } catch (Exception e) { + Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", new ColorRGBA(0.45f, 0.32f, 0.18f, 1f)); + return mat; + } + } + + private Material buildLeafMat(FruitBushOptions opts) { + try { + Material mat = new Material(assets, "MatDefs/TreeLeaf.j3md"); + mat.setColor("Diffuse", new ColorRGBA(opts.leafR, opts.leafG, opts.leafB, 1f)); + mat.setFloat("WindStrength", opts.windStrength); + mat.setFloat("WindSpeed", opts.windSpeed); + mat.setVector3("LightDir", new Vector3f(0.45f, 1.0f, 0.3f).normalizeLocal()); + mat.setVector3("SunColor", new Vector3f(1.4f, 1.3f, 1.1f)); + mat.setVector3("AmbientColor", new Vector3f(0.18f, 0.18f, 0.22f)); + mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); + if (opts.leafTexture != null) { + try { + mat.setTexture("LeafMap", assets.loadTexture(opts.leafTexture)); + mat.setBoolean("HasLeafMap", true); + } catch (Exception ignored) {} + } + return mat; + } catch (Exception e) { + Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", new ColorRGBA(opts.leafR, opts.leafG, opts.leafB, 1f)); + mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); + return mat; + } + } + + private Material buildFruitMat(FruitBushOptions opts) { + Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", new ColorRGBA(opts.fruitR, opts.fruitG, opts.fruitB, 1f)); + if (opts.fruitTexture != null) { + try { + Texture tex = assets.loadTexture(opts.fruitTexture); + tex.setWrap(Texture.WrapMode.Clamp); + mat.setTexture("ColorMap", tex); + } catch (Exception ignored) {} + } + mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); + return mat; + } + + private void exportBush(Node bush, String presetName) { + try { + String folder = presetName == null ? "fruchtbusch" + : presetName.toLowerCase().replace("ä", "ae").replace("ö", "oe") + .replace("ü", "ue").replace(" ", "_"); + String ts = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now()); + String name = folder + "_" + ts; + Path dir = ASSET_ROOT.resolve("Models").resolve("trees").resolve(folder); + Files.createDirectories(dir); + Path out = dir.resolve(name + ".j3o"); + BinaryExporter.getInstance().save(bush, out.toFile()); + log.info("[Fruchtbusch] Gespeichert: {}", out); + input.treeGenStatusMsg = "Gespeichert: Models/trees/" + folder + "/" + name + ".j3o"; + input.refreshAssets = true; + } catch (IOException e) { + log.error("[Fruchtbusch] Export-Fehler: {}", e.getMessage()); + input.treeGenStatusMsg = "Fruchtbusch Export-Fehler: " + e.getMessage(); + } + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/state/GrapevineGeneratorState.java b/blight-editor/src/main/java/de/blight/editor/state/GrapevineGeneratorState.java new file mode 100644 index 0000000..eb2f467 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/state/GrapevineGeneratorState.java @@ -0,0 +1,205 @@ +package de.blight.editor.state; + +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.BoundingBox; +import com.jme3.export.binary.BinaryExporter; +import com.jme3.material.Material; +import com.jme3.material.RenderState; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector3f; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.texture.Texture; +import de.blight.editor.SharedInput; +import de.blight.editor.tree.GrapevineMeshBuilder; +import de.blight.editor.tree.GrapevineOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class GrapevineGeneratorState extends BaseAppState { + + private static final Logger log = LoggerFactory.getLogger(GrapevineGeneratorState.class); + private static final Path ASSET_ROOT = de.blight.editor.ProjectRoot.resolve( + "blight-assets", "src", "main", "resources"); + + private final SharedInput input; + private SimpleApplication app; + private AssetManager assets; + private TreeGeneratorState previewHost; + + public GrapevineGeneratorState(SharedInput input) { this.input = input; } + + @Override + protected void initialize(Application app) { + this.app = (SimpleApplication) app; + this.assets = app.getAssetManager(); + } + @Override protected void cleanup(Application app) {} + @Override protected void onEnable() {} + @Override protected void onDisable() {} + + @Override + public void update(float tpf) { + if (previewHost == null) { + previewHost = getStateManager().getState(TreeGeneratorState.class); + if (previewHost == null) return; + } + + SharedInput.GrapevineGenRequest req = input.grapevineGenQueue.poll(); + if (req == null) return; + + GrapevineOptions opts = req.options(); + GrapevineMeshBuilder.MeshResult result = GrapevineMeshBuilder.build(opts); + Node vine = assembleVineNode(result, opts); + vine.updateGeometricState(); + + BoundingBox bb = vine.getWorldBound() instanceof BoundingBox b ? b : null; + float dist = bb != null + ? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3.0f + : 5f; + Vector3f target = bb != null + ? new Vector3f(0f, bb.getCenter().y, 0f) + : new Vector3f(0f, 1.4f, 0f); + + final Node finalVine = vine; + final float finalDist = dist; + final Vector3f finalTarget = target; + final boolean doExport = req.exportAfter(); + + app.enqueue(() -> { + previewHost.setPreviewContent(finalVine, finalDist, finalTarget); + if (doExport) { + exportVine(finalVine); + } + }); + input.treeGenStatusMsg = doExport ? "Weinpflanze: exportiere…" : "Weinpflanze: Vorschau"; + } + + private Node assembleVineNode(GrapevineMeshBuilder.MeshResult result, GrapevineOptions opts) { + Node node = new Node("weinpflanze"); + + Geometry bark = new Geometry("bark", result.bark()); + bark.setMaterial(buildBarkMat(opts)); + bark.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); + node.attachChild(bark); + + if (result.wires().getVertexCount() > 0) { + Geometry wires = new Geometry("wires", result.wires()); + wires.setMaterial(buildWireMat()); + wires.setShadowMode(RenderQueue.ShadowMode.Off); + node.attachChild(wires); + } + + if (result.leaves().getVertexCount() > 0) { + Geometry leaves = new Geometry("leaves", result.leaves()); + leaves.setMaterial(buildLeafMat(opts)); + leaves.setQueueBucket(RenderQueue.Bucket.Transparent); + leaves.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); + node.attachChild(leaves); + } + + if (result.fruits().getVertexCount() > 0) { + Geometry fruits = new Geometry("grapes", result.fruits()); + fruits.setMaterial(buildGrapeMat(opts)); + fruits.setQueueBucket(RenderQueue.Bucket.Transparent); + fruits.setShadowMode(RenderQueue.ShadowMode.Off); + node.attachChild(fruits); + } + + return node; + } + + private Material buildBarkMat(GrapevineOptions opts) { + try { + Material mat = new Material(assets, "MatDefs/Tree.j3md"); + mat.setColor("Diffuse", new ColorRGBA(0.65f, 0.52f, 0.38f, 1f)); + mat.setFloat("WindStrength", opts.windStrength * 0.3f); + mat.setFloat("WindSpeed", opts.windSpeed); + mat.setVector3("LightDir", new Vector3f(0.45f, 1.0f, 0.3f).normalizeLocal()); + mat.setVector3("SunColor", new Vector3f(1.4f, 1.3f, 1.1f)); + mat.setVector3("AmbientColor", new Vector3f(0.18f, 0.18f, 0.22f)); + try { + Texture barkTex = assets.loadTexture("Textures/internal/bark/Bark001_Color.jpg"); + barkTex.setWrap(Texture.WrapMode.Repeat); + mat.setTexture("BarkMap", barkTex); + mat.setBoolean("HasBarkMap", true); + } catch (Exception ignored) {} + return mat; + } catch (Exception e) { + Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", new ColorRGBA(0.45f, 0.32f, 0.18f, 1f)); + return mat; + } + } + + private Material buildWireMat() { + Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", new ColorRGBA(0.22f, 0.20f, 0.18f, 1f)); + return mat; + } + + private Material buildLeafMat(GrapevineOptions opts) { + try { + Material mat = new Material(assets, "MatDefs/TreeLeaf.j3md"); + mat.setColor("Diffuse", new ColorRGBA(opts.leafR, opts.leafG, opts.leafB, 1f)); + mat.setFloat("WindStrength", opts.windStrength); + mat.setFloat("WindSpeed", opts.windSpeed); + mat.setVector3("LightDir", new Vector3f(0.45f, 1.0f, 0.3f).normalizeLocal()); + mat.setVector3("SunColor", new Vector3f(1.4f, 1.3f, 1.1f)); + mat.setVector3("AmbientColor", new Vector3f(0.18f, 0.18f, 0.22f)); + mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); + try { + mat.setTexture("LeafMap", assets.loadTexture(opts.leafTexture)); + mat.setBoolean("HasLeafMap", true); + } catch (Exception ignored) {} + return mat; + } catch (Exception e) { + Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", new ColorRGBA(opts.leafR, opts.leafG, opts.leafB, 1f)); + mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); + return mat; + } + } + + private Material buildGrapeMat(GrapevineOptions opts) { + Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", new ColorRGBA(0.42f, 0.18f, 0.55f, 1f)); + if (opts.grapeTexture != null) { + try { + Texture tex = assets.loadTexture(opts.grapeTexture); + tex.setWrap(Texture.WrapMode.Clamp); + mat.setTexture("ColorMap", tex); + } catch (Exception ignored) {} + } + mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); + return mat; + } + + private void exportVine(Node vine) { + try { + String ts = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now()); + String name = "grapevine_" + ts; + Path dir = ASSET_ROOT.resolve("Models").resolve("trees").resolve("grapevine"); + Files.createDirectories(dir); + Path out = dir.resolve(name + ".j3o"); + BinaryExporter.getInstance().save(vine, out.toFile()); + log.info("[Grapevine] Gespeichert: {}", out); + input.treeGenStatusMsg = "Gespeichert: Models/trees/grapevine/" + name + ".j3o"; + input.refreshAssets = true; + } catch (IOException e) { + log.error("[Grapevine] Export-Fehler: {}", e.getMessage()); + input.treeGenStatusMsg = "Grapevine Export-Fehler: " + e.getMessage(); + } + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java b/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java index b512505..65a9001 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/ModelImportState.java @@ -443,11 +443,12 @@ public class ModelImportState extends BaseAppState { // nach einem Editor-Neustart oder im Spiel (dort kein Cache vorhanden). assets.deleteFromCache(new com.jme3.asset.ModelKey(relPath)); - // Zwischendatei löschen wenn der Nutzer einen anderen Dateinamen gewählt hat. - // SceneObjectState hat die Quelldatei unter dem originalen Dateinamen als .j3o - // gespeichert; erst performExport() bäckt Scale + Pivot in die Ziel-Datei. - String intermediateRel = input.modelEditorOpenPath != null - ? input.modelEditorOpenPath.replace('\\', '/') : null; + // Zwischendatei immer löschen — sie dient nur der Vorschau und soll nicht + // im Asset-Baum bleiben. modelImportIntermediatePath wird in activateModelImport + // gesetzt und nicht von ModelEditorState geleert (im Gegensatz zu modelEditorOpenPath). + String intermediateRel = input.modelImportIntermediatePath != null + ? input.modelImportIntermediatePath.replace('\\', '/') : null; + input.modelImportIntermediatePath = null; if (intermediateRel != null && !intermediateRel.equals(relPath)) { Path intermediateAbs = ASSET_ROOT.resolve(intermediateRel); try { @@ -459,6 +460,8 @@ public class ModelImportState extends BaseAppState { } } + // refreshAssets erst NACH dem Löschen der Zwischendatei setzen, + // damit der Asset-Baum nur die finale Datei anzeigt. input.modelImportExportStatus = "Gespeichert: " + relPath; input.refreshAssets = true; diff --git a/blight-editor/src/main/java/de/blight/editor/state/PalmGeneratorState.java b/blight-editor/src/main/java/de/blight/editor/state/PalmGeneratorState.java index 40f38b1..56c8832 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/PalmGeneratorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/PalmGeneratorState.java @@ -404,6 +404,9 @@ public class PalmGeneratorState extends BaseAppState { mat.setColor("Diffuse", new ColorRGBA(opts.barkR, opts.barkG, opts.barkB, 1f)); mat.setFloat("WindStrength", 0.08f); mat.setFloat("WindSpeed", 0.4f); + mat.setVector3("LightDir", new Vector3f(0.45f, 1.0f, 0.3f).normalizeLocal()); + mat.setVector3("SunColor", new Vector3f(1.4f, 1.3f, 1.1f)); + mat.setVector3("AmbientColor", new Vector3f(0.18f, 0.18f, 0.22f)); if (opts.barkTexture != null) { try { Texture barkTex = assets.loadTexture(opts.barkTexture); @@ -426,6 +429,9 @@ public class PalmGeneratorState extends BaseAppState { mat.setColor("Diffuse", new ColorRGBA(opts.leafR, opts.leafG, opts.leafB, 1f)); mat.setFloat("WindStrength", 0.20f); mat.setFloat("WindSpeed", 0.5f); + mat.setVector3("LightDir", new Vector3f(0.45f, 1.0f, 0.3f).normalizeLocal()); + mat.setVector3("SunColor", new Vector3f(1.4f, 1.3f, 1.1f)); + mat.setVector3("AmbientColor", new Vector3f(0.18f, 0.18f, 0.22f)); mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); if (opts.leafTexture != null) { try { @@ -449,6 +455,9 @@ public class PalmGeneratorState extends BaseAppState { mat.setColor("Diffuse", color); mat.setFloat("WindStrength", 0.04f); mat.setFloat("WindSpeed", 0.3f); + mat.setVector3("LightDir", new Vector3f(0.45f, 1.0f, 0.3f).normalizeLocal()); + mat.setVector3("SunColor", new Vector3f(1.4f, 1.3f, 1.1f)); + mat.setVector3("AmbientColor", new Vector3f(0.18f, 0.18f, 0.22f)); if (opts.crownTexture != null) { try { Texture tex = assets.loadTexture(opts.crownTexture); 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 fbcef70..0a3c146 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 @@ -1408,45 +1408,80 @@ public class TerrainEditorState extends BaseAppState { Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f); com.jme3.math.Ray ray = new com.jme3.math.Ray(near, far.subtract(near).normalizeLocal()); + int mode = input.heightTool.mode.getSelectedIndex(); + + // Plateau-RMB: vor dem Terrain-Raycast prüfen, damit Kliff-Flächen funktionieren + if (mode == HeightTool.MODE_PLATEAU && edit.action() < 0) { + CollisionResults hits = new CollisionResults(); + terrain.collideWith(ray, hits); + Vector3f contact = hits.size() > 0 ? hits.getClosestCollision().getContactPoint() : null; + sampleAndSetPlateauHeight(ray, contact); + continue; + } + CollisionResults hits = new CollisionResults(); terrain.collideWith(ray, hits); if (hits.size() == 0) continue; Vector3f contact = hits.getClosestCollision().getContactPoint(); - int mode = input.heightTool.mode.getSelectedIndex(); - boolean terrainChanged = true; if (mode == HeightTool.MODE_SMOOTH) { - smoothHeight(contact); - } else if (mode == HeightTool.MODE_PLATEAU) { - if (edit.action() < 0) { - // Rechtsklick: Terrain- und Voxel-Höhe sampeln, Maximum als Plateau-Ziel - float h = sampleTerrainHeight(contact); - VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class); - if (ves != null) { - float vh = ves.columnTopWorldY(contact.x, contact.z); - if (Float.isFinite(vh)) h = Float.isFinite(h) ? Math.max(h, vh) : vh; - } - if (Float.isFinite(h)) { - input.heightTool.plateauHeight.setValue(h); - input.heightTool.plateauHeightChanged = true; - } - terrainChanged = false; + if (edit.action() > 0) { + slopeHeight(contact); } else { - // Linksklick: Terrain schrittweise auf Plateau-Höhe angleichen - flattenToPlateauHeight(contact); + smoothHeight(contact); } + } else if (mode == HeightTool.MODE_PLATEAU) { + flattenToPlateauHeight(contact); } else { float delta = (float) input.heightTool.brushStrength.getValue() * edit.action(); modifyHeight(contact, delta, mode); } - if (terrainChanged) { - float br = (float) input.heightTool.brushRadius.getValue(); - input.terrainEditedAreas.offer(new float[]{contact.x, contact.z, br}); - } + float br = (float) input.heightTool.brushRadius.getValue(); + input.terrainEditedAreas.offer(new float[]{contact.x, contact.z, br}); } if (processed > 0) terrain.updateModelBound(); } + private void sampleAndSetPlateauHeight(com.jme3.math.Ray ray, Vector3f terrainContact) { + float h = Float.NaN; + if (terrainContact != null) { + h = sampleTerrainHeight(terrainContact); + } + VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class); + if (ves != null) { + // 1. Versuch: Voxel-Geometrie direkt raycasten (Kliff-Fläche) + Vector3f voxelContact = ves.raycastVoxelGeometry(ray); + if (voxelContact != null) { + float vh = ves.columnTopWorldY(voxelContact.x, voxelContact.z); + if (Float.isFinite(vh)) h = Float.isFinite(h) ? Math.max(h, vh) : vh; + } + // 2. Fallback: XZ aus Terrain-Kontakt + if (terrainContact != null) { + float vh = ves.columnTopWorldY(terrainContact.x, terrainContact.z); + if (Float.isFinite(vh)) h = Float.isFinite(h) ? Math.max(h, vh) : vh; + } + // 3. Fallback: Ray auf y=0 Ebene projizieren → columnTopWorldY findet die Voxel-Säule + if (!Float.isFinite(h) || voxelContact == null) { + float dy = ray.getDirection().y; + if (Math.abs(dy) > 0.001f) { + float t = -ray.getOrigin().y / dy; + if (t > 0.5f) { + float fx = ray.getOrigin().x + t * ray.getDirection().x; + float fz = ray.getOrigin().z + t * ray.getDirection().z; + float vh = ves.columnTopWorldY(fx, fz); + if (Float.isFinite(vh)) h = Float.isFinite(h) ? Math.max(h, vh) : vh; + } + } + } + } + if (Float.isFinite(h)) { + input.heightTool.plateauHeight.setValue(h); + input.heightTool.plateauHeightChanged = true; + input.voxelTool.plateauTarget.setValue(h); + input.voxelTool.plateauTargetChanged = true; + } + } + // ── Höhen-Werkzeug ──────────────────────────────────────────────────────── private void modifyHeight(Vector3f worldContact, float delta, int mode) { @@ -1552,6 +1587,102 @@ public class TerrainEditorState extends BaseAppState { if (grassVertexState != null) grassVertexState.adjustBladeHeights(worldContact, radius); } + private void slopeHeight(Vector3f worldContact) { + float radius = (float) input.heightTool.brushRadius.getValue(); + float strength = (float) input.heightTool.brushStrength.getValue(); + int r = (int) Math.ceil(radius / VERTEX_SPACING); + int cx = Math.round((worldContact.x + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING); + int cz = Math.round((worldContact.z + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING); + + if (cachedHeightMap == null) return; + float[] hmap = cachedHeightMap; + + float outerMin = 0.7f * radius; + + // 1. Höchsten Punkt auf dem äußeren Ring suchen + float maxH = Float.NEGATIVE_INFINITY; + float maxWX = 0, maxWZ = 0; + for (int dz = -r; dz <= r; dz++) { + for (int dx = -r; dx <= r; dx++) { + int vx = cx + dx, vz = cz + dz; + if (vx < 0 || vx >= TOTAL_SIZE || vz < 0 || vz >= TOTAL_SIZE) continue; + float dist = FastMath.sqrt(dx * dx + dz * dz) * VERTEX_SPACING; + if (dist < outerMin || dist >= radius) continue; + float h = hmap[vz * TOTAL_SIZE + vx]; + if (!Float.isFinite(h)) continue; + if (h > maxH) { maxH = h; maxWX = dx * VERTEX_SPACING; maxWZ = dz * VERTEX_SPACING; } + } + } + if (!Float.isFinite(maxH)) return; + + // 2. Richtung vom Zentrum zum Hochpunkt → Slope-Achse + float highDist = FastMath.sqrt(maxWX * maxWX + maxWZ * maxWZ); + if (highDist < 0.001f) return; + float dirX = maxWX / highDist; + float dirZ = maxWZ / highDist; + + // 3. Gegenüberliegenden Punkt finden: Außenring-Vertex mit kleinstem Proj-Wert (= entgegengesetzt zu dir) + float oppH = Float.NaN; + float oppWX = -maxWX, oppWZ = -maxWZ; // Initiales Ideal-Gegenteil + float minProj = Float.MAX_VALUE; + for (int dz = -r; dz <= r; dz++) { + for (int dx = -r; dx <= r; dx++) { + int vx = cx + dx, vz = cz + dz; + if (vx < 0 || vx >= TOTAL_SIZE || vz < 0 || vz >= TOTAL_SIZE) continue; + float dist = FastMath.sqrt(dx * dx + dz * dz) * VERTEX_SPACING; + if (dist < outerMin || dist >= radius) continue; + float h = hmap[vz * TOTAL_SIZE + vx]; + if (!Float.isFinite(h)) continue; + float wx = dx * VERTEX_SPACING, wz = dz * VERTEX_SPACING; + float proj = wx * dirX + wz * dirZ; + if (proj < minProj) { minProj = proj; oppH = h; oppWX = wx; oppWZ = wz; } + } + } + if (!Float.isFinite(oppH)) return; + + // 4. Projektionsachse: von Gegenpunkt (t=0) zu Hochpunkt (t=1) + float projHigh = maxWX * dirX + maxWZ * dirZ; + float projOpp = oppWX * dirX + oppWZ * dirZ; + float projRange = projHigh - projOpp; + if (Math.abs(projRange) < 0.001f) return; + + // 5. Alle Brush-Vertices zur Ziel-Ebene hinziehen + List locs = new ArrayList<>(); + List deltas = new ArrayList<>(); + + for (int dz = -r; dz <= r; dz++) { + for (int dx = -r; dx <= r; dx++) { + int vx = cx + dx, vz = cz + dz; + if (vx < 0 || vx >= TOTAL_SIZE || vz < 0 || vz >= TOTAL_SIZE) continue; + float dist = FastMath.sqrt(dx * dx + dz * dz) * VERTEX_SPACING; + if (dist >= radius) continue; + float curH = hmap[vz * TOTAL_SIZE + vx]; + if (!Float.isFinite(curH)) continue; + + float proj = (dx * VERTEX_SPACING) * dirX + (dz * VERTEX_SPACING) * dirZ; + float t = FastMath.clamp((proj - projOpp) / projRange, 0f, 1f); + float target = oppH + t * (maxH - oppH); + + float falloff = (1f + FastMath.cos(FastMath.PI * dist / radius)) * 0.5f; + float blend = FastMath.clamp(falloff * (strength / 50f), 0f, 1f); + float delta = (target - curH) * blend; + if (delta == 0f) continue; + + locs.add(new Vector2f(vx * VERTEX_SPACING - TERRAIN_SIZE * 0.5f, + vz * VERTEX_SPACING - TERRAIN_SIZE * 0.5f)); + deltas.add(delta); + } + } + + if (!locs.isEmpty()) { + terrain.adjustHeight(locs, deltas); + syncHeightCache(locs, deltas); + if (placedObjectState != null) placedObjectState.adjustObjectHeights(locs, deltas); + if (sceneObjState != null) sceneObjState.snapToTerrain(worldContact.x, worldContact.z, radius); + if (grassVertexState != null) grassVertexState.adjustBladeHeights(worldContact, radius); + } + } + /** Liest die Terrain-Höhe am nächstgelegenen Vertex zum Kontaktpunkt. */ public float sampleTerrainHeight(Vector3f worldContact) { if (cachedHeightMap == null) return Float.NaN; diff --git a/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java index 5379eeb..9746fb6 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/TreeGeneratorState.java @@ -35,8 +35,6 @@ import com.jme3.scene.Spatial; import com.jme3.scene.VertexBuffer; import com.jme3.scene.control.AbstractControl; import com.jme3.scene.shape.Sphere; -import com.jme3.shadow.DirectionalLightShadowRenderer; -import com.jme3.shadow.EdgeFilteringMode; import com.jme3.texture.FrameBuffer; import com.jme3.texture.Image; import com.jme3.texture.Texture; @@ -138,13 +136,13 @@ public class TreeGeneratorState extends BaseAppState { previewScene.attachChild(buildPreviewGrid()); previewVP.attachScene(previewScene); - DirectionalLightShadowRenderer shadowRenderer = - new DirectionalLightShadowRenderer(assets, 2048, 1); - shadowRenderer.setLight(previewSunLight); - shadowRenderer.setEdgeFilteringMode(EdgeFilteringMode.PCF4); - shadowRenderer.setShadowIntensity(0.25f); - shadowRenderer.setShadowZExtend(80f); - previewVP.addProcessor(shadowRenderer); + // Shadow-Renderer absichtlich NICHT im Preview-Viewport: + // Tree.j3md / TreeLeaf.j3md haben eine PostShadow-Technik mit Blend Modulate. + // Wird ShadowIntensity nicht korrekt auf 0.25 propagiert (JME3-Bug mit + // Custom-PostShadow + PCF4), bleibt der j3md-Default 1.0 aktiv → + // lit = 0 → Blend Modulate → komplettes Schwarz. + // Die Preview-Beleuchtung kommt ohnehin aus den Custom-Uniforms + // (LightDir / SunColor / AmbientColor) und braucht keinen Shadow-Renderer. // Stellt sicher, dass previewScene direkt vor dem Rendern immer aktuell ist – // unabhängig davon, welche AppStates nach TreeGeneratorState die Szene noch ändern. previewVP.addProcessor(new SceneProcessor() { @@ -169,7 +167,10 @@ public class TreeGeneratorState extends BaseAppState { public void setPreviewContent(com.jme3.scene.Node node, float camDist, com.jme3.math.Vector3f target) { previewTreeHolder.detachAllChildren(); - if (node != null) previewTreeHolder.attachChild(node); + if (node != null) { + ImpostorUtil.applyTreeLighting(node, previewSunLight.getDirection().negate()); + previewTreeHolder.attachChild(node); + } this.previewCamDist = camDist; this.previewTarget.set(target); } @@ -315,6 +316,7 @@ public class TreeGeneratorState extends BaseAppState { Node previewTree = makeTreeNode(pendingHdResult, pendingBarkMat.clone(), pendingLeafMat.clone(), "prev"); + ImpostorUtil.applyTreeLighting(previewTree, previewSunLight.getDirection().negate()); previewTreeHolder.detachAllChildren(); previewTreeHolder.attachChild(previewTree); @@ -380,6 +382,9 @@ public class TreeGeneratorState extends BaseAppState { mat.setColor("Diffuse", new ColorRGBA(0.42f, 0.26f, 0.10f, 1f)); mat.setFloat("WindStrength", 0.15f); mat.setFloat("WindSpeed", 0.5f); + mat.setVector3("LightDir", new Vector3f(0.45f, 1.0f, 0.3f).normalizeLocal()); + mat.setVector3("SunColor", new Vector3f(1.4f, 1.3f, 1.1f)); + mat.setVector3("AmbientColor", new Vector3f(0.18f, 0.18f, 0.22f)); if (p.barkTexture != null) { try { Texture barkTex = assets.loadTexture(p.barkTexture); @@ -404,6 +409,9 @@ public class TreeGeneratorState extends BaseAppState { mat.setColor("Diffuse", new ColorRGBA(0.18f, 0.60f, 0.10f, 1f)); mat.setFloat("WindStrength", 0.30f); mat.setFloat("WindSpeed", 0.7f); + mat.setVector3("LightDir", new Vector3f(0.45f, 1.0f, 0.3f).normalizeLocal()); + mat.setVector3("SunColor", new Vector3f(1.4f, 1.3f, 1.1f)); + mat.setVector3("AmbientColor", new Vector3f(0.18f, 0.18f, 0.22f)); mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); if (p.leafTexture != null) { diff --git a/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java index 6c34bd3..2cbc969 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/VoxelEditorState.java @@ -23,7 +23,10 @@ import com.jme3.scene.Spatial; import com.jme3.scene.shape.Quad; import com.jme3.scene.VertexBuffer; import com.jme3.terrain.geomipmap.TerrainQuad; +import com.jme3.texture.Image; import com.jme3.texture.Texture; +import com.jme3.texture.Texture2D; +import com.jme3.texture.image.ColorSpace; import com.jme3.util.BufferUtils; import de.blight.common.VoxelChunk; import de.blight.common.VoxelChunkIO; @@ -35,11 +38,18 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.jme3.export.binary.BinaryExporter; -import java.io.IOException; +import de.blight.common.MapData; +import de.blight.common.MapIO; +import java.io.*; +import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.nio.IntBuffer; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Arrays; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; import java.util.*; import java.util.concurrent.*; import java.util.ArrayDeque; @@ -145,6 +155,19 @@ public class VoxelEditorState extends BaseAppState { /** Vorheriger Layer-Zustand für Einstieg/Ausstieg-Erkennung. */ private boolean prevLayerWasVoxel = false; + // ── Voxel-Splatmap ─────────────────────────────────────────────────────── + + private static final int VOX_SPLAT_SIZE = MapData.SPLAT_SIZE; // 2049 + private static final float VOX_WORLD_HALF = 2048f; + private static final float VOX_SPLAT_WE_PER_PX = + (VOX_WORLD_HALF * 2f) / (VOX_SPLAT_SIZE - 1); // ~2 WE/px + + private byte[] voxSplatR, voxSplatG, voxSplatB, voxSplatA; + private ByteBuffer voxSplatBuf; + private Image voxSplatImage; + private Texture2D voxSplatTex; + private boolean splatDirty = false; + // ── LOD-Rebuild-Queue ───────────────────────────────────────────────────── /** Chunks, die LOD1/2 neu brauchen. Wird im Hintergrund-Thread abgearbeitet. */ @@ -188,6 +211,7 @@ public class VoxelEditorState extends BaseAppState { } voxelMaterial = buildMaterial(); + initVoxelSplat(); brushIndicator = buildBrushIndicator(); app.getRootNode().attachChild(brushIndicator); @@ -289,11 +313,16 @@ public class VoxelEditorState extends BaseAppState { clearMarkedOverlays(); } - // Voxel-Texturen aktualisiert? + // Voxel-Texturen (Slot/Normal-Map) aktualisiert? if (input.voxelTexturesChanged) { input.voxelTexturesChanged = false; applyTextures(voxelMaterial); } + // Voxel-Splat-Texturen (TexFlat2/3/4) aktualisiert? + if (input.voxelSplatTexturesChanged) { + input.voxelSplatTexturesChanged = false; + applyVoxelSplatTextures(voxelMaterial); + } // Layer-Wechsel erkennen → Referenzebene steuern (kein automatischer Wireframe-Wechsel) boolean isVoxelLayer = input.activeLayer == SharedInput.LAYER_VOXEL; @@ -307,6 +336,14 @@ public class VoxelEditorState extends BaseAppState { applyWireframe(input.voxelWireframeEnabled); } + // Voxel-Textur-Malen: parallel zum TerrainEditorState, wenn der Textur-Layer aktiv ist + if (input.activeLayer == 4) { + processVoxelTextureEdits(); + idleSinceSave += tpf; + checkAutoSave(); + return; + } + // Nur aktiv wenn LAYER_VOXEL gesetzt if (input.activeLayer != SharedInput.LAYER_VOXEL) { idleSinceEdit = 0f; @@ -603,6 +640,17 @@ public class VoxelEditorState extends BaseAppState { bestPos = cr.getContactPoint(); bestNorm = new Vector3f(0, 1, 0); } + results.clear(); + } + } + + // Fallback auf Basis-Terrain wenn noch keine Voxel vorhanden sind, + // damit Pinsel-Indikator und erster Pinsel-Tick korrekt auf dem Terrain landen. + if (bestPos == null && terrainNode != null) { + terrainNode.collideWith(ray, results); + if (results.size() > 0) { + bestPos = results.getClosestCollision().getContactPoint(); + bestNorm = new Vector3f(0, 1, 0); } } @@ -657,6 +705,8 @@ public class VoxelEditorState extends BaseAppState { if (Float.isFinite(h)) { input.voxelTool.plateauTarget.setValue(h); input.voxelTool.plateauTargetChanged = true; + input.heightTool.plateauHeight.setValue(h); + input.heightTool.plateauHeightChanged = true; } return; } @@ -722,9 +772,11 @@ public class VoxelEditorState extends BaseAppState { applyColumnToTarget(chunk, cx, cy, cz, wx, wz, radius, strength, coord -> target); } else if (modeIdx == de.blight.editor.tool.VoxelTool.MODE_SMOOTH) { if (lower) { - applyCliffColumn(chunk, cx, cy, cz, wx, wz, radius, strength, slopeParams); - } else { + // RMB: Durchschnitt-Smooth (wie vormals LMB) applySmoothColumn(chunk, cx, cy, cz, wx, wz, radius, strength, slopeParams); + } else { + // LMB: gleichmäßiger Slope vom höchsten Außenring-Punkt zum Gegenpunkt + applySlopeColumn(chunk, cx, cy, cz, wx, wz, radius, strength, slopeParams); } } else { applyColumnBrush(chunk, cx, cy, cz, wx, wz, radius, strength, modeIdx, lower); @@ -1065,40 +1117,79 @@ public class VoxelEditorState extends BaseAppState { * projLow = Projektion des Tiefpunkts auf die Achse (immer ≤ projHigh) * avgH = Durchschnittshöhe aller Spalten im Pinselbereich (für Smooth-Modus) */ + /** + * Slope-Parameter für den Smooth-LMB-Modus. + * Rückgabe: [0]=maxH, [1]=oppH, [2]=dirX, [3]=dirZ, [4]=projHigh, [5]=projOpp, [6]=avgH + * Achse zeigt vom Zentrum zum höchsten Außenring-Punkt; oppH ist der geometrisch + * gegenüberliegende Außenring-Punkt (kleinste Projektion auf diese Achse). + */ private float[] computeSlopeParams(float brushWX, float brushWZ, float radius) { - float r2 = radius * radius; + float r2 = radius * radius; + float outerMin = 0.7f * radius; + float outerMin2 = outerMin * outerMin; int xMin = (int)(brushWX - radius), xMax = (int) Math.ceil(brushWX + radius); int zMin = (int)(brushWZ - radius), zMax = (int) Math.ceil(brushWZ + radius); - float maxH = Float.NEGATIVE_INFINITY, minH = Float.POSITIVE_INFINITY; - float maxX = brushWX, maxZ = brushWZ, minX = brushWX, minZ = brushWZ; - float sum = 0f; + // Höchsten Außenring-Punkt finden + Durchschnitt über den gesamten Pinsel + float maxH = Float.NEGATIVE_INFINITY; + float maxOX = 0, maxOZ = 0; + float sum = 0f; int count = 0; for (int xi = xMin; xi <= xMax; xi++) { float dx = xi - brushWX; for (int zi = zMin; zi <= zMax; zi++) { float dz = zi - brushWZ; - if (dx*dx + dz*dz > r2) continue; + float d2 = dx*dx + dz*dz; + if (d2 > r2) continue; float h = columnTopWorldY(xi, zi); - if (h > maxH) { maxH = h; maxX = xi; maxZ = zi; } - if (h < minH) { minH = h; minX = xi; minZ = zi; } sum += h; count++; + if (d2 >= outerMin2 && h > maxH) { maxH = h; maxOX = dx; maxOZ = dz; } } } - if (maxH == Float.NEGATIVE_INFINITY) return new float[]{ Float.NaN, Float.NaN, 0, 0, 0, 0, Float.NaN }; + if (maxH == Float.NEGATIVE_INFINITY || count == 0) + return new float[]{ Float.NaN, Float.NaN, 0, 0, 0, 0, Float.NaN }; - // Richtung vom tiefsten zum höchsten Punkt — garantiert projHigh - projLow = |maxPos - minPos| - float ddx = maxX - minX, ddz = maxZ - minZ; - float len = (float) Math.sqrt(ddx*ddx + ddz*ddz); - if (len < 1e-4f) { ddx = 1f; ddz = 0f; } else { ddx /= len; ddz /= len; } + // Achse: Zentrum → Hochpunkt + float highDist = (float) Math.sqrt(maxOX*maxOX + maxOZ*maxOZ); + float dirX, dirZ; + if (highDist < 1e-4f) { dirX = 1f; dirZ = 0f; } + else { dirX = maxOX / highDist; dirZ = maxOZ / highDist; } - float projHigh = (maxX - brushWX) * ddx + (maxZ - brushWZ) * ddz; - float projLow = (minX - brushWX) * ddx + (minZ - brushWZ) * ddz; + // Gegenüberliegender Punkt: Außenring-Vertex mit kleinstem Projektons-Wert + float oppH = Float.NaN; + float oppOX = 0, oppOZ = 0; + float minProj = Float.MAX_VALUE; + for (int xi = xMin; xi <= xMax; xi++) { + float dx = xi - brushWX; + for (int zi = zMin; zi <= zMax; zi++) { + float dz = zi - brushWZ; + float d2 = dx*dx + dz*dz; + if (d2 < outerMin2 || d2 > r2) continue; + float proj = dx * dirX + dz * dirZ; + if (proj < minProj) { + minProj = proj; + oppH = columnTopWorldY(xi, zi); + oppOX = dx; oppOZ = dz; + } + } + } + if (Float.isNaN(oppH)) oppH = sum / count; + + float projHigh = maxOX * dirX + maxOZ * dirZ; + float projOpp = oppOX * dirX + oppOZ * dirZ; float avgH = sum / count; - return new float[]{ maxH, minH, ddx, ddz, projHigh, projLow, avgH }; + return new float[]{ maxH, oppH, dirX, dirZ, projHigh, projOpp, avgH }; + } + + /** Raycast gegen reine Voxel-Geometrie; gibt nächsten Treffpunkt oder null zurück. */ + public Vector3f raycastVoxelGeometry(com.jme3.math.Ray ray) { + CollisionResults results = new CollisionResults(); + voxelRoot.collideWith(ray, results); + if (results.size() == 0) return null; + return results.getClosestCollision().getContactPoint(); } /** Welt-Y des höchsten Solid-Voxels an (worldX, worldZ), oder Terrain-Höhe wenn keine Voxel. */ @@ -1251,27 +1342,22 @@ public class VoxelEditorState extends BaseAppState { } /** - * Klippe-Pinsel (Smooth-Rechtsklick): scharfe Terrassenstufe mittig zwischen Hoch- und Tiefpunkt. + * Slope-Pinsel (Smooth-Linksklick): gleichmäßige Neigung vom höchsten Außenring-Punkt + * zum geometrisch gegenüberliegenden Punkt. */ - private void applyCliffColumn(VoxelChunk chunk, int cx, int cy, int cz, + private void applySlopeColumn(VoxelChunk chunk, int cx, int cy, int cz, float brushWX, float brushWZ, float radius, float strength, float[] sp) { - if (sp == null || Float.isNaN(sp[0])) return; - final float highH = sp[0], lowH = sp[1], dirX = sp[2], dirZ = sp[3]; - final float projHigh = sp[4], projLow = sp[5]; - final float projRange = projHigh - projLow; + if (sp == null || Float.isNaN(sp[0]) || Float.isNaN(sp[1])) return; + final float highH = sp[0], oppH = sp[1], dirX = sp[2], dirZ = sp[3]; + final float projHigh = sp[4], projOpp = sp[5]; + final float projRange = projHigh - projOpp; if (projRange < 0.5f) return; - final float projMid = (projHigh + projLow) * 0.5f; - final float blendHalf = Math.max(0.3f, projRange * 0.1f); - applyColumnToTarget(chunk, cx, cy, cz, brushWX, brushWZ, radius, strength, coord -> { float proj = coord[0] * dirX + coord[1] * dirZ; - float rel = proj - projMid; - if (rel > blendHalf) return highH; - if (rel < -blendHalf) return lowH; - float t = (rel / blendHalf + 1f) * 0.5f; - return lowH + (highH - lowH) * t; + float t = Math.max(0f, Math.min(1f, (proj - projOpp) / projRange)); + return oppH + t * (highH - oppH); }); } @@ -1744,11 +1830,18 @@ public class VoxelEditorState extends BaseAppState { private void checkAutoSave() { if (idleSinceSave < AUTO_SAVE_IDLE_S) return; - boolean anyDirty = false; + boolean anyDirty = splatDirty; for (VoxelChunk c : chunks.values()) { if (c.dirty) { anyDirty = true; break; } } if (!anyDirty) return; idleSinceSave = 0f; + final boolean saveSplat = splatDirty; + if (saveSplat) splatDirty = false; + final byte[] snapR = saveSplat ? voxSplatR.clone() : null; + final byte[] snapG = saveSplat ? voxSplatG.clone() : null; + final byte[] snapB = saveSplat ? voxSplatB.clone() : null; + final byte[] snapA = saveSplat ? voxSplatA.clone() : null; + executor.submit(() -> { for (VoxelChunk chunk : chunks.values()) { if (!chunk.dirty) continue; @@ -1759,9 +1852,212 @@ public class VoxelEditorState extends BaseAppState { chunk.cx, chunk.cy, chunk.cz, e.getMessage()); } } + if (saveSplat) { + try { + saveVoxelSplat(snapR, snapG, snapB, snapA); + } catch (IOException e) { + log.error("Auto-Save Voxel-Splatmap: {}", e.getMessage()); + } + } }); } + // ── Voxel-Splatmap: Init / Speichern / Laden ───────────────────────────── + + private void initVoxelSplat() { + voxSplatR = new byte[VOX_SPLAT_SIZE * VOX_SPLAT_SIZE]; + voxSplatG = new byte[VOX_SPLAT_SIZE * VOX_SPLAT_SIZE]; + voxSplatB = new byte[VOX_SPLAT_SIZE * VOX_SPLAT_SIZE]; + voxSplatA = new byte[VOX_SPLAT_SIZE * VOX_SPLAT_SIZE]; + Arrays.fill(voxSplatR, (byte) 255); + + // Vorhandene Datei laden (falls vorhanden) + Path file = voxelSplatFile(); + if (Files.exists(file)) { + try { + loadVoxelSplat(file); + } catch (IOException e) { + log.warn("Voxel-Splatmap nicht ladbar, wird neu initialisiert: {}", e.getMessage()); + Arrays.fill(voxSplatR, (byte) 255); + Arrays.fill(voxSplatG, (byte) 0); + Arrays.fill(voxSplatB, (byte) 0); + Arrays.fill(voxSplatA, (byte) 0); + } + } + + voxSplatBuf = BufferUtils.createByteBuffer(VOX_SPLAT_SIZE * VOX_SPLAT_SIZE * 4); + for (int i = 0; i < VOX_SPLAT_SIZE * VOX_SPLAT_SIZE; i++) { + voxSplatBuf.put(voxSplatR[i]).put(voxSplatG[i]).put(voxSplatB[i]).put(voxSplatA[i]); + } + voxSplatBuf.flip(); + voxSplatImage = new Image(Image.Format.RGBA8, VOX_SPLAT_SIZE, VOX_SPLAT_SIZE, + voxSplatBuf, ColorSpace.Linear); + voxSplatTex = new Texture2D(voxSplatImage); + voxelMaterial.setTexture("SplatMap", voxSplatTex); + applyVoxelSplatTextures(voxelMaterial); + } + + private Path voxelSplatFile() { + return MapIO.getMapPath().resolveSibling("blight_voxel_splat.bin"); + } + + private void loadVoxelSplat(Path file) throws IOException { + try (DataInputStream in = new DataInputStream( + new BufferedInputStream(new GZIPInputStream(Files.newInputStream(file))))) { + in.readFully(voxSplatR); + in.readFully(voxSplatG); + in.readFully(voxSplatB); + in.readFully(voxSplatA); + // Lade Texturpfade falls vorhanden + try { + String[] paths = new String[4]; + for (int i = 0; i < 4; i++) paths[i] = in.readUTF(); + input.voxelSplatTexturePaths = paths; + } catch (EOFException ignored) {} + } + } + + private void saveVoxelSplat(byte[] snapR, byte[] snapG, byte[] snapB, byte[] snapA) + throws IOException { + Path file = voxelSplatFile(); + Path tmp = file.resolveSibling(file.getFileName() + ".tmp"); + Files.createDirectories(file.getParent()); + try (DataOutputStream out = new DataOutputStream( + new BufferedOutputStream(new GZIPOutputStream(Files.newOutputStream(tmp))))) { + out.write(snapR); + out.write(snapG); + out.write(snapB); + out.write(snapA); + String[] paths = input.voxelSplatTexturePaths; + for (int i = 0; i < 4; i++) { + String p = (paths != null && i < paths.length && paths[i] != null) ? paths[i] : ""; + out.writeUTF(p); + } + } + try { + Files.move(tmp, file, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (java.nio.file.AtomicMoveNotSupportedException e) { + Files.move(tmp, file, StandardCopyOption.REPLACE_EXISTING); + } + log.info("Voxel-Splatmap gespeichert: {}", file); + } + + // ── Voxel-Textur-Malen ──────────────────────────────────────────────────── + + private void processVoxelTextureEdits() { + SharedInput.TextureEdit edit; + int processed = 0; + while ((edit = input.voxelTextureEditQueue.poll()) != null && processed < MAX_EDITS_PER_FRAME) { + processed++; + float jmeX = (float)(edit.screenX() * input.viewportScaleX); + float jmeY = cam.getHeight() - (float)(edit.screenY() * input.viewportScaleY); + + Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f); + Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f); + com.jme3.math.Ray ray = new com.jme3.math.Ray(near, far.subtract(near).normalizeLocal()); + + // Raycast gegen Voxel-Geometrie + CollisionResults hits = new CollisionResults(); + voxelRoot.collideWith(ray, hits); + Vector3f contact = null; + if (hits.size() > 0) { + contact = hits.getClosestCollision().getContactPoint(); + } + // Fallback auf Basis-Terrain + if (contact == null && terrainNode != null) { + CollisionResults terrHits = new CollisionResults(); + terrainNode.collideWith(ray, terrHits); + if (terrHits.size() > 0) contact = terrHits.getClosestCollision().getContactPoint(); + } + if (contact == null) continue; + + int selIdx = input.textureTool.textureIndex.getSelectedIndex(); + float str = (float) input.textureTool.brushStrength.getValue(); + + if (edit.action() > 0) { + applyVoxelTexturePaint(contact, str, selIdx); + } else { + applyVoxelTexturePaint(contact, str, 0); + } + } + if (processed > 0) { + idleSinceSave = 0f; + } + } + + private void applyVoxelTexturePaint(Vector3f contact, float strength, int textureIndex) { + float radius = (float) input.textureTool.brushRadius.getValue(); + + int centerPX = Math.round((contact.x + VOX_WORLD_HALF) / VOX_SPLAT_WE_PER_PX); + int centerPZ = Math.round((contact.z + VOX_WORLD_HALF) / VOX_SPLAT_WE_PER_PX); + int pixR = (int) Math.ceil(radius / VOX_SPLAT_WE_PER_PX); + + boolean changed = false; + for (int dz = -pixR; dz <= pixR; dz++) { + int pz = centerPZ + dz; + if (pz < 0 || pz >= VOX_SPLAT_SIZE) continue; + for (int dx = -pixR; dx <= pixR; dx++) { + int px = centerPX + dx; + if (px < 0 || px >= VOX_SPLAT_SIZE) continue; + + float distWE = FastMath.sqrt(dx * dx + dz * dz) * VOX_SPLAT_WE_PER_PX; + if (distWE >= radius) continue; + + float t = distWE / radius; + float falloff = (1f + FastMath.cos(FastMath.PI * t)) * 0.5f; + float blend = (textureIndex == 0) ? falloff : strength * falloff; + + int idx = pz * VOX_SPLAT_SIZE + px; + float curG = (voxSplatG[idx] & 0xFF) / 255f; + float curB = (voxSplatB[idx] & 0xFF) / 255f; + float curA = (voxSplatA[idx] & 0xFF) / 255f; + + float tgG = (textureIndex == 1) ? 1f : 0f; + float tgB = (textureIndex == 2) ? 1f : 0f; + float tgA = (textureIndex == 3) ? 1f : 0f; + + voxSplatR[idx] = (byte) 255; + voxSplatG[idx] = (byte) Math.round((curG + (tgG - curG) * blend) * 255f); + voxSplatB[idx] = (byte) Math.round((curB + (tgB - curB) * blend) * 255f); + voxSplatA[idx] = (byte) Math.round((curA + (tgA - curA) * blend) * 255f); + + int bi = idx * 4; + voxSplatBuf.put(bi, voxSplatR[idx]); + voxSplatBuf.put(bi + 1, voxSplatG[idx]); + voxSplatBuf.put(bi + 2, voxSplatB[idx]); + voxSplatBuf.put(bi + 3, voxSplatA[idx]); + changed = true; + } + } + if (changed) { + voxSplatBuf.rewind(); + voxSplatImage.setUpdateNeeded(); + splatDirty = true; + } + } + + private void applyVoxelSplatTextures(Material mat) { + String[] paths = input.voxelSplatTexturePaths; + String[] slots = {"TexFlat2", "TexFlat3", "TexFlat4"}; + int[][] fallbacks = {{100, 130, 60}, {110, 100, 90}, {80, 80, 100}}; + for (int i = 0; i < 3; i++) { + String p = (paths != null && (i + 1) < paths.length) ? paths[i + 1] : ""; + if (p != null && !p.isEmpty()) { + try { + Texture t = assets.loadTexture(p); + t.getImage().setColorSpace(ColorSpace.Linear); + t.setWrap(Texture.WrapMode.Repeat); + mat.setTexture(slots[i], t); + } catch (Exception e) { + log.warn("Voxel-Splat-Textur {} nicht ladbar: {}", p, e.getMessage()); + mat.setTexture(slots[i], solidColorTexture(fallbacks[i])); + } + } else { + mat.setTexture(slots[i], solidColorTexture(fallbacks[i])); + } + } + } + // ── Intern: Material ───────────────────────────────────────────────────── private Material buildMaterial() { @@ -1877,7 +2173,9 @@ public class VoxelEditorState extends BaseAppState { private void updateBrushIndicator() { if (brushIndicator == null) return; - if (input.activeLayer != SharedInput.LAYER_VOXEL) { + boolean isVoxelOrTex = input.activeLayer == SharedInput.LAYER_VOXEL + || input.activeLayer == 4; + if (!isVoxelOrTex) { brushIndicator.setCullHint(Spatial.CullHint.Always); return; } @@ -1891,7 +2189,9 @@ public class VoxelEditorState extends BaseAppState { float jmeY = cam.getHeight() - my * (float) input.viewportScaleY; Hit hit = raycastHit(jmeX, jmeY); if (hit != null) { - float r = (float) input.voxelTool.brushRadius.getValue(); + float r = (input.activeLayer == 4) + ? (float) input.textureTool.brushRadius.getValue() + : (float) input.voxelTool.brushRadius.getValue(); // Leicht entlang der Flächen-Normalen versetzt, um Z-Fighting zu vermeiden brushIndicator.setLocalTranslation(hit.pos.add(hit.normal().mult(0.05f))); diff --git a/blight-editor/src/main/java/de/blight/editor/tree/FruitBushMeshBuilder.java b/blight-editor/src/main/java/de/blight/editor/tree/FruitBushMeshBuilder.java new file mode 100644 index 0000000..5bb83ff --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/tree/FruitBushMeshBuilder.java @@ -0,0 +1,321 @@ +package de.blight.editor.tree; + +import com.jme3.math.FastMath; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.Node; +import com.jme3.scene.VertexBuffer; +import com.jme3.util.BufferUtils; + +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +/** + * Prozeduraler Fruchtbusch-Generator. + * + * Erzeugt drei Kinder-Nodes: + * "bark" — Stamm + Äste als Zylinder-Geometrie + * "leaves" — Blatt-Quads (Cross-Billboard) an den Astspitzen + * "fruits" — kleine Kugel-Approx. (2 gekreuzte Quads) an Blattclustern + * + * Vertex-Farbe R = Wind-Gewicht (0 = Stammbasis, 1 = Astspitze). + */ +public class FruitBushMeshBuilder { + + public record MeshResult(Mesh bark, Mesh leaves, Mesh fruits) {} + + public static MeshResult build(FruitBushOptions o) { + Random rng = new Random(o.seed); + + VertexCollector barkCol = new VertexCollector(); + VertexCollector leafCol = new VertexCollector(); + VertexCollector fruitCol = new VertexCollector(); + + // ── Stamm ──────────────────────────────────────────────────────────── + int trunkSegs = 7; + Vector3f trunkBot = new Vector3f(0f, 0f, 0f); + Vector3f trunkTop = new Vector3f(0f, o.trunkHeight, 0f); + addCylinder(barkCol, trunkBot, trunkTop, o.trunkRadius, o.trunkRadius * 0.70f, + 0f, 0.15f, trunkSegs); + + // ── Primäräste ─────────────────────────────────────────────────────── + float branchAngleRad = o.branchAngle * FastMath.DEG_TO_RAD; + float twigAngleRad = o.twigAngle * FastMath.DEG_TO_RAD; + + for (int b = 0; b < o.branchCount; b++) { + float azimuth = b * FastMath.TWO_PI / o.branchCount + + rng.nextFloat() * 0.4f - 0.2f; + float lenScale = 0.80f + rng.nextFloat() * 0.40f; + float bLen = o.branchLength * lenScale; + + // Richtung: um branchAngle° geneigt, azimuth° um Y gedreht + Vector3f bDir = branchDir(Vector3f.UNIT_Y, azimuth, branchAngleRad); + + // Startet etwas über dem Stammende mit leichter Versetzung + float startT = 0.6f + rng.nextFloat() * 0.35f; + Vector3f bBase = trunkTop.mult(startT); + Vector3f bTip = bBase.add(bDir.mult(bLen)); + + float branchBaseRad = o.trunkRadius * 0.45f; + float windBase = 0.15f + startT * 0.20f; + float windTip = 0.60f; + + addCylinder(barkCol, bBase, bTip, branchBaseRad, branchBaseRad * 0.22f, + windBase, windTip, 5); + + // ── Sekundäräste (Reiser) ───────────────────────────────────── + for (int t = 0; t < o.twigCount; t++) { + float tAz = t * FastMath.TWO_PI / o.twigCount + + rng.nextFloat() * 0.5f - 0.25f; + float tLenScl = 0.75f + rng.nextFloat() * 0.50f; + float tLen = o.twigLength * tLenScl; + Vector3f tDir = branchDir(bDir, tAz, twigAngleRad); + + float tStart = 0.35f + rng.nextFloat() * 0.50f; + Vector3f tBase = bBase.add(bDir.mult(bLen * tStart)); + Vector3f tTip = tBase.add(tDir.mult(tLen)); + + float windTBase = windBase + (windTip - windBase) * tStart; + addCylinder(barkCol, tBase, tTip, o.twigRadius, o.twigRadius * 0.10f, + windTBase, 0.90f, 4); + + // Blätter entlang des gesamten Reisers – alle ~12 cm ein Cluster + int twigClusters = Math.max(2, Math.round(tLen / 0.12f)); + int leafsPerCluster = Math.max(3, o.leafCount / 2); + for (int lc = 0; lc < twigClusters; lc++) { + float lT = FastMath.clamp( + (lc + 0.5f + rng.nextFloat() * 0.4f - 0.2f) / twigClusters, + 0.05f, 1.0f); + Vector3f lPos = tBase.add(tDir.mult(tLen * lT)); + float wL = windTBase + (0.90f - windTBase) * lT; + addLeafCluster(leafCol, lPos, wL, o.leafSize, leafsPerCluster, rng); + } + + // Früchte an der Astgabel (Gabelpunkt Primärast → Reiser) + if (o.fruitsEnabled && rng.nextFloat() < o.fruitDensity) { + addFruitSphere(fruitCol, tBase, windTBase, o.fruitRadius, rng); + } + } + + // Blätter auch entlang des Primärasts (zwischen den Gabeln) + int branchClusters = Math.max(1, Math.round(bLen / 0.22f)); + for (int lc = 0; lc < branchClusters; lc++) { + float lT = (lc + 0.5f + rng.nextFloat() * 0.3f - 0.15f) / branchClusters; + lT = FastMath.clamp(lT, 0.05f, 1.0f); + Vector3f lPos = bBase.add(bDir.mult(bLen * lT)); + float wL = windBase + (windTip - windBase) * lT; + addLeafCluster(leafCol, lPos, wL, o.leafSize * 0.80f, + Math.max(2, o.leafCount / 3), rng); + } + + } + + return new MeshResult(barkCol.toMesh(), leafCol.toMesh(), fruitCol.toMesh()); + } + + // ── Zylinder-Segment ───────────────────────────────────────────────────── + + private static void addCylinder(VertexCollector col, + Vector3f start, Vector3f end, + float rBot, float rTop, + float windBot, float windTop, int N) { + Vector3f axis = end.subtract(start); + if (axis.lengthSquared() < 1e-8f) return; + axis.normalizeLocal(); + + Vector3f perp1 = (Math.abs(axis.y) < 0.9f) + ? axis.cross(Vector3f.UNIT_Y).normalizeLocal() + : axis.cross(Vector3f.UNIT_X).normalizeLocal(); + Vector3f perp2 = axis.cross(perp1).normalizeLocal(); + + int base = col.vertexCount; + int N1 = N + 1; + + for (int ring = 0; ring < 2; ring++) { + Vector3f center = (ring == 0) ? start : end; + float r = (ring == 0) ? rBot : rTop; + float wind = (ring == 0) ? windBot : windTop; + float vCoord = ring; + for (int i = 0; i <= N; i++) { + float theta = FastMath.TWO_PI * i / N; + float cos = FastMath.cos(theta); + float sin = FastMath.sin(theta); + float nx = cos * perp1.x + sin * perp2.x; + float ny = cos * perp1.y + sin * perp2.y; + float nz = cos * perp1.z + sin * perp2.z; + col.add(center.x + nx * r, center.y + ny * r, center.z + nz * r, + nx, ny, nz, (float) i / N, vCoord, wind); + } + } + + for (int i = 0; i < N; i++) { + int b0 = base + i, b1 = base + i + 1; + int t0 = base + N1 + i, t1 = base + N1 + i + 1; + col.tri(b0, b1, t1); + col.tri(b0, t1, t0); + } + } + + // ── Blatt-Cluster ──────────────────────────────────────────────────────── + + private static void addLeafCluster(VertexCollector col, Vector3f tip, + float wind, float scale, int count, Random rng) { + for (int i = 0; i < count; i++) { + float ox = rng.nextFloat() * scale * 0.20f - scale * 0.10f; + float oy = rng.nextFloat() * scale * 0.10f - scale * 0.05f; + float oz = rng.nextFloat() * scale * 0.20f - scale * 0.10f; + float s = scale * (0.70f + rng.nextFloat() * 0.60f); + + // Volle 3D-Richtung: zufälliger Azimuth + zufällige Neigung (volle Kugel) + float yaw = rng.nextFloat() * FastMath.TWO_PI; + float pitch = rng.nextFloat() * FastMath.PI; + float sinP = FastMath.sin(pitch); + float gx = sinP * FastMath.cos(yaw); + float gy = FastMath.cos(pitch); + float gz = sinP * FastMath.sin(yaw); + + addLeafQuad(col, tip.x + ox, tip.y + oy, tip.z + oz, s, wind, gx, gy, gz); + } + } + + private static void addLeafQuad(VertexCollector col, + float cx, float cy, float cz, + float s, float wind, + float gx, float gy, float gz) { + // Rechtsvektor senkrecht zur Wachstumsrichtung + float rx, ry, rz; + if (Math.abs(gy) < 0.95f) { + rx = gz; ry = 0f; rz = -gx; // cross(UNIT_Y, grow) + } else { + rx = 0f; ry = -gz; rz = gy; // cross(UNIT_X, grow) + } + float rLen = FastMath.sqrt(rx * rx + ry * ry + rz * rz); + if (rLen > 1e-5f) { rx /= rLen; ry /= rLen; rz /= rLen; } + + float hw = s * 0.5f; + float hh = s * 1.3f; + + // Normalvektor: right × grow + float nx = ry * gz - rz * gy; + float ny = rz * gx - rx * gz; + float nz = rx * gy - ry * gx; + + float windTip = Math.min(1.0f, wind + 0.25f); + int base = col.vertexCount; + // Untere Kante am Ast (verankert) + col.add(cx - rx * hw, cy - ry * hw, cz - rz * hw, + nx, ny, nz, 0f, 0f, wind); + col.add(cx + rx * hw, cy + ry * hw, cz + rz * hw, + nx, ny, nz, 1f, 0f, wind); + // Obere Kante frei (schwingt im Wind) + col.add(cx + rx * hw + gx * hh, cy + ry * hw + gy * hh, cz + rz * hw + gz * hh, + nx, ny, nz, 1f, 1f, windTip); + col.add(cx - rx * hw + gx * hh, cy - ry * hw + gy * hh, cz - rz * hw + gz * hh, + nx, ny, nz, 0f, 1f, windTip); + + col.tri(base, base + 1, base + 2); + col.tri(base, base + 2, base + 3); + col.tri(base + 2, base + 1, base); + col.tri(base + 3, base + 2, base); + } + + // ── Frucht-Kugel (2 gekreuzte Quads) ───────────────────────────────────── + + private static void addFruitSphere(VertexCollector col, Vector3f tip, + float wind, float radius, Random rng) { + float ox = rng.nextFloat() * radius * 0.8f - radius * 0.4f; + // Frucht hängt unterhalb der Gabel: Zentrum ca. 1.8× Radius unter Gabelposition + float oy = -radius * 1.8f + rng.nextFloat() * radius * 0.4f - radius * 0.2f; + float oz = rng.nextFloat() * radius * 0.8f - radius * 0.4f; + float cx = tip.x + ox, cy = tip.y + oy, cz = tip.z + oz; + + addFruitQuad(col, cx, cy, cz, radius, wind, 0f); + addFruitQuad(col, cx, cy, cz, radius, wind, FastMath.HALF_PI); + } + + private static void addFruitQuad(VertexCollector col, + float cx, float cy, float cz, + float r, float wind, float yaw) { + float cosY = FastMath.cos(yaw), sinY = FastMath.sin(yaw); + + float[] xs = { cx - cosY * r, cx + cosY * r, cx + cosY * r, cx - cosY * r }; + float[] ys = { cy - r, cy - r, cy + r, cy + r }; + float[] zs = { cz + sinY * r, cz - sinY * r, cz - sinY * r, cz + sinY * r }; + + float nnx = sinY, nnz = cosY; + int base = col.vertexCount; + col.add(xs[0], ys[0], zs[0], nnx, 0, nnz, 0, 0, wind); + col.add(xs[1], ys[1], zs[1], nnx, 0, nnz, 1, 0, wind); + col.add(xs[2], ys[2], zs[2], nnx, 0, nnz, 1, 1, wind); + col.add(xs[3], ys[3], zs[3], nnx, 0, nnz, 0, 1, wind); + col.tri(base, base+1, base+2); + col.tri(base, base+2, base+3); + col.tri(base+2, base+1, base); + col.tri(base+3, base+2, base); + } + + // ── Ast-Richtung ───────────────────────────────────────────────────────── + + private static Vector3f branchDir(Vector3f parent, float yaw, float tiltRad) { + Vector3f perp = (Math.abs(parent.y) < 0.9f) + ? parent.cross(Vector3f.UNIT_Y).normalizeLocal() + : parent.cross(Vector3f.UNIT_X).normalizeLocal(); + Quaternion tilt = new Quaternion().fromAngleAxis(tiltRad, perp); + Quaternion spin = new Quaternion().fromAngleAxis(yaw, parent); + return spin.mult(tilt.mult(parent)).normalizeLocal(); + } + + // ── Vertex-Sammler ──────────────────────────────────────────────────────── + + private static final class VertexCollector { + final List pos = new ArrayList<>(); + final List norm = new ArrayList<>(); + final List uv = new ArrayList<>(); + final List wind = new ArrayList<>(); + final List idx = new ArrayList<>(); + int vertexCount = 0; + + void add(float x, float y, float z, + float nx, float ny, float nz, + float u, float v, float w) { + pos.add(x); pos.add(y); pos.add(z); + norm.add(nx); norm.add(ny); norm.add(nz); + uv.add(u); uv.add(v); + wind.add(w); + vertexCount++; + } + + void tri(int a, int b, int c) { idx.add(a); idx.add(b); idx.add(c); } + + Mesh toMesh() { + if (vertexCount == 0) return new Mesh(); + int n = vertexCount; + FloatBuffer posB = BufferUtils.createFloatBuffer(n * 3); + FloatBuffer normB = BufferUtils.createFloatBuffer(n * 3); + FloatBuffer uvB = BufferUtils.createFloatBuffer(n * 2); + FloatBuffer colB = BufferUtils.createFloatBuffer(n * 4); + IntBuffer idxB = BufferUtils.createIntBuffer(idx.size()); + + for (Float f : pos) posB.put(f); + for (Float f : norm) normB.put(f); + for (Float f : uv) uvB.put(f); + for (Float f : wind) { colB.put(f); colB.put(0f); colB.put(0f); colB.put(1f); } + for (Integer i : idx) idxB.put(i); + + Mesh mesh = new Mesh(); + mesh.setBuffer(VertexBuffer.Type.Position, 3, posB); + mesh.setBuffer(VertexBuffer.Type.Normal, 3, normB); + mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, uvB); + mesh.setBuffer(VertexBuffer.Type.Color, 4, colB); + mesh.setBuffer(VertexBuffer.Type.Index, 3, idxB); + mesh.updateBound(); + mesh.updateCounts(); + return mesh; + } + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/tree/FruitBushOptions.java b/blight-editor/src/main/java/de/blight/editor/tree/FruitBushOptions.java new file mode 100644 index 0000000..393ec38 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/tree/FruitBushOptions.java @@ -0,0 +1,145 @@ +package de.blight.editor.tree; + +/** Parameter für den prozeduralen Fruchtbusch-Generator. */ +public class FruitBushOptions { + + public int seed = 55123; + /** Höhe des Stamms in Metern. */ + public float trunkHeight = 1.20f; + /** Radius des Stamms an der Basis. */ + public float trunkRadius = 0.10f; + /** Anzahl Primäräste (direkt vom Stamm). */ + public int branchCount = 5; + /** Winkel der Primäräste zur Vertikalen (Grad). */ + public float branchAngle = 50f; + /** Länge der Primäräste (m). */ + public float branchLength = 1.60f; + /** Anzahl Sekundäräste pro Primärast. */ + public int twigCount = 4; + /** Winkel der Sekundäräste zur Elternrichtung (Grad). */ + public float twigAngle = 45f; + /** Länge der Sekundäräste (m). */ + public float twigLength = 0.90f; + /** Radius der Endäste. */ + public float twigRadius = 0.024f; + + /** Blatt-Größe (Quadrat-Seite, m). */ + public float leafSize = 0.44f; + /** Blätter pro Sekundärast. */ + public int leafCount = 16; + /** Blatt-Textur (relativ zum Asset-Root). */ + public String leafTexture = "Textures/internal/foliage/fruittree.png"; + /** Blatt-Tint R. */ + public float leafR = 0.50f, leafG = 0.80f, leafB = 0.22f; + + /** Früchte aktivieren. */ + public boolean fruitsEnabled = true; + /** Wahrscheinlichkeit [0–1], dass ein Blattcluster eine Frucht bekommt. */ + public float fruitDensity = 0.35f; + /** Frucht-Radius (m). */ + public float fruitRadius = 0.08f; + /** Frucht-Farbe R. */ + public float fruitR = 1.0f, fruitG = 0.45f, fruitB = 0.0f; + /** Frucht-Textur (relativ zum Asset-Root, RGBA mit Alpha-Maske). */ + public String fruitTexture = "Textures/internal/fruits/orange.png"; + + public float windStrength = 0.20f; + public float windSpeed = 0.60f; + + // ── Presets ────────────────────────────────────────────────────────────── + + public static FruitBushOptions orange() { + FruitBushOptions o = new FruitBushOptions(); + o.seed = 55123; + o.trunkHeight = 1.30f; + o.trunkRadius = 0.11f; + o.branchCount = 5; + o.branchAngle = 50f; + o.branchLength = 1.80f; + o.twigCount = 4; + o.twigAngle = 44f; + o.twigLength = 1.00f; + o.twigRadius = 0.026f; + o.leafSize = 0.48f; + o.leafCount = 18; + o.leafR = 0.50f; o.leafG = 0.82f; o.leafB = 0.22f; + o.fruitsEnabled = true; + o.fruitDensity = 0.32f; + o.fruitRadius = 0.08f; + o.fruitR = 1.00f; o.fruitG = 0.45f; o.fruitB = 0.00f; + o.fruitTexture = "Textures/internal/fruits/orange.png"; + return o; + } + + public static FruitBushOptions lemon() { + FruitBushOptions o = new FruitBushOptions(); + o.seed = 63241; + o.trunkHeight = 1.10f; + o.trunkRadius = 0.096f; + o.branchCount = 6; + o.branchAngle = 46f; + o.branchLength = 1.50f; + o.twigCount = 3; + o.twigAngle = 40f; + o.twigLength = 0.84f; + o.twigRadius = 0.024f; + o.leafSize = 0.44f; + o.leafCount = 16; + o.leafR = 0.48f; o.leafG = 0.80f; o.leafB = 0.25f; + o.fruitsEnabled = true; + o.fruitDensity = 0.30f; + o.fruitRadius = 0.08f; + o.fruitR = 0.92f; o.fruitG = 0.88f; o.fruitB = 0.12f; + o.fruitTexture = "Textures/internal/fruits/lemon.png"; + return o; + } + + public static FruitBushOptions apricot() { + FruitBushOptions o = new FruitBushOptions(); + o.seed = 47892; + o.trunkHeight = 1.00f; + o.trunkRadius = 0.084f; + o.branchCount = 4; + o.branchAngle = 54f; + o.branchLength = 1.40f; + o.twigCount = 4; + o.twigAngle = 48f; + o.twigLength = 0.80f; + o.twigRadius = 0.022f; + o.leafSize = 0.40f; + o.leafCount = 16; + o.leafR = 0.52f; o.leafG = 0.78f; o.leafB = 0.20f; + o.fruitsEnabled = true; + o.fruitDensity = 0.68f; + o.fruitRadius = 0.04f; + o.fruitR = 0.95f; o.fruitG = 0.62f; o.fruitB = 0.24f; + o.fruitTexture = "Textures/internal/fruits/apricot.png"; + return o; + } + + public FruitBushOptions copy() { + FruitBushOptions c = new FruitBushOptions(); + c.seed = seed; + c.trunkHeight = trunkHeight; + c.trunkRadius = trunkRadius; + c.branchCount = branchCount; + c.branchAngle = branchAngle; + c.branchLength = branchLength; + c.twigCount = twigCount; + c.twigAngle = twigAngle; + c.twigLength = twigLength; + c.twigRadius = twigRadius; + c.leafSize = leafSize; + c.leafCount = leafCount; + c.leafTexture = leafTexture; + c.leafR = leafR; c.leafG = leafG; c.leafB = leafB; + c.fruitsEnabled = fruitsEnabled; + c.fruitDensity = fruitDensity; + c.fruitRadius = fruitRadius; + c.fruitR = fruitR; c.fruitG = fruitG; c.fruitB = fruitB; + c.fruitTexture = fruitTexture; + c.windStrength = windStrength; + c.windSpeed = windSpeed; + return c; + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/tree/GrapevineMeshBuilder.java b/blight-editor/src/main/java/de/blight/editor/tree/GrapevineMeshBuilder.java new file mode 100644 index 0000000..549d57b --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/tree/GrapevineMeshBuilder.java @@ -0,0 +1,421 @@ +package de.blight.editor.tree; + +import com.jme3.math.FastMath; +import com.jme3.math.Vector3f; +import com.jme3.scene.Mesh; +import com.jme3.scene.VertexBuffer; +import com.jme3.util.BufferUtils; + +import java.nio.FloatBuffer; +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class GrapevineMeshBuilder { + + public record MeshResult(Mesh bark, Mesh wires, Mesh leaves, Mesh fruits) {} + + public static MeshResult build(GrapevineOptions o) { + Random rng = new Random(o.seed); + float halfW = o.totalWidth * 0.5f; + + VertexCollector barkCol = new VertexCollector(); + VertexCollector wireCol = new VertexCollector(); + VertexCollector leafCol = new VertexCollector(); + VertexCollector fruitCol = new VertexCollector(); + + // ── Pfosten ─────────────────────────────────────────────────────────── + addCylinder(barkCol, + new Vector3f(-halfW, 0, 0), new Vector3f(-halfW, o.totalHeight + 0.15f, 0), + o.postRadius, o.postRadius * 0.80f, 0f, 0f, 7); + addCylinder(barkCol, + new Vector3f(halfW, 0, 0), new Vector3f(halfW, o.totalHeight + 0.15f, 0), + o.postRadius, o.postRadius * 0.80f, 0f, 0f, 7); + + // ── Drähte ──────────────────────────────────────────────────────────── + float wireR = 0.006f; + for (int w = 0; w < o.wireCount; w++) { + float wh = o.firstWireH + + (o.totalHeight - o.firstWireH) * (float) w / Math.max(1, o.wireCount - 1); + addCylinder(wireCol, + new Vector3f(-halfW, wh, 0), new Vector3f(halfW, wh, 0), + wireR, wireR, 0f, 0f, 4); + } + + // ── Stamm (gewunden) ────────────────────────────────────────────────── + addBentPath(barkCol, + new Vector3f(0, 0, 0), new Vector3f(0, o.firstWireH, 0), + o.trunkRadius, o.trunkRadius * 0.70f, 0f, 0f, + 4, 6, 0.10f, rng); + + // ── Trieb-t-Werte vorab berechnen → Armlänge = Position des letzten Triebs + float[] leftTs = new float[o.shootCount]; + float[] rightTs = new float[o.shootCount]; + float leftMaxT = 0f, rightMaxT = 0f; + for (int s = 0; s < o.shootCount; s++) { + leftTs[s] = FastMath.clamp((s + 0.5f + rng.nextFloat() * 0.5f - 0.25f) / o.shootCount, 0.08f, 0.97f); + rightTs[s] = FastMath.clamp((s + 0.5f + rng.nextFloat() * 0.5f - 0.25f) / o.shootCount, 0.08f, 0.97f); + leftMaxT = Math.max(leftMaxT, leftTs[s]); + rightMaxT = Math.max(rightMaxT, rightTs[s]); + } + + // ── Arme (enden genau beim letzten Trieb) ──────────────────────────── + Vector3f armCenter = new Vector3f(0, o.firstWireH, 0); + float leftArmEnd = halfW * leftMaxT; + float rightArmEnd = halfW * rightMaxT; + Vector3f[] leftArmPts = bentPathPoints(armCenter, new Vector3f(-leftArmEnd, o.firstWireH, 0), 5, 0.07f, rng); + Vector3f[] rightArmPts = bentPathPoints(armCenter, new Vector3f( rightArmEnd, o.firstWireH, 0), 5, 0.07f, rng); + addPolylineCylinder(barkCol, leftArmPts, o.trunkRadius * 0.65f, o.trunkRadius * 0.38f, 0f, 0f, 5); + addPolylineCylinder(barkCol, rightArmPts, o.trunkRadius * 0.65f, o.trunkRadius * 0.38f, 0f, 0f, 5); + + // ── Triebe + Blätter + Trauben ──────────────────────────────────────── + float shootH = o.totalHeight - o.firstWireH; + for (int side = -1; side <= 1; side += 2) { + float[] ts = (side == -1) ? leftTs : rightTs; + float maxT = (side == -1) ? leftMaxT : rightMaxT; + Vector3f[] armPts = (side == -1) ? leftArmPts : rightArmPts; + + for (int s = 0; s < o.shootCount; s++) { + float t = ts[s]; + // Startpunkt exakt auf dem gebogenen Arm (t/maxT → 0..1 entlang des Pfads) + Vector3f sBase = interpPath(armPts, t / maxT); + + float tiltX = rng.nextFloat() * 0.12f - 0.06f; + float tiltZ = rng.nextFloat() * 0.12f - 0.06f; + Vector3f sDir = new Vector3f(tiltX, 1f, tiltZ).normalizeLocal(); + Vector3f sTip = sBase.add(sDir.mult(shootH)); + + Vector3f[] shootPts = bentPathPoints(sBase, sTip, 3, 0.12f, rng); + addPolylineCylinder(barkCol, shootPts, + o.shootRadius, o.shootRadius * 0.20f, 0f, 0f, 4); + + // Blätter im oberen 65% + int clusters = Math.max(2, o.leafCount / 2); + for (int lc = 0; lc < clusters; lc++) { + float lt = FastMath.clamp( + 0.35f + (lc + 0.5f + rng.nextFloat() * 0.3f - 0.15f) / clusters * 0.65f, + 0.30f, 1.0f); + Vector3f lp = interpPath(shootPts, lt); + float wl = 0.55f + 0.45f * lt; + addLeafCluster(leafCol, lp, wl, o.leafSize, + Math.max(2, o.leafCount / clusters), rng); + } + + // 2 Trauben immer, 3. mit 50 % Wahrscheinlichkeit weiter oben. + // Unterste Traube: Oberkante auf Höhe des niedersten Blattes (t≈0.35). + int grapeCount = rng.nextFloat() < 0.50f ? 3 : 2; + float[] grapeTs = new float[]{ 0.33f + rng.nextFloat() * 0.06f, // 0.33–0.39 + 0.48f + rng.nextFloat() * 0.10f, // 0.48–0.58 + 0.65f + rng.nextFloat() * 0.12f }; // 0.65–0.77 + for (int g = 0; g < grapeCount; g++) { + Vector3f ga = interpPath(shootPts, grapeTs[g]); + addGrapeBunch(fruitCol, ga.x, ga.y, ga.z, 0.12f, 0.25f); + } + } + } + + return new MeshResult(barkCol.toMesh(), wireCol.toMesh(), leafCol.toMesh(), fruitCol.toMesh()); + } + + // ── Blatt-Cluster / Quad ────────────────────────────────────────────────── + + private static void addLeafCluster(VertexCollector col, Vector3f tip, + float wind, float scale, int count, Random rng) { + for (int i = 0; i < count; i++) { + float ox = rng.nextFloat() * scale * 0.20f - scale * 0.10f; + float oy = rng.nextFloat() * scale * 0.10f - scale * 0.05f; + float oz = rng.nextFloat() * scale * 0.20f - scale * 0.10f; + float s = scale * (0.70f + rng.nextFloat() * 0.60f); + + float yaw = rng.nextFloat() * FastMath.TWO_PI; + float pitch = rng.nextFloat() * FastMath.PI; + float sinP = FastMath.sin(pitch); + float gx = sinP * FastMath.cos(yaw); + float gy = FastMath.cos(pitch); + float gz = sinP * FastMath.sin(yaw); + + addLeafQuad(col, tip.x + ox, tip.y + oy, tip.z + oz, s, wind, gx, gy, gz); + } + } + + private static void addLeafQuad(VertexCollector col, + float cx, float cy, float cz, + float s, float wind, + float gx, float gy, float gz) { + float rx, ry, rz; + if (Math.abs(gy) < 0.95f) { + rx = gz; ry = 0f; rz = -gx; + } else { + rx = 0f; ry = -gz; rz = gy; + } + float rLen = FastMath.sqrt(rx * rx + ry * ry + rz * rz); + if (rLen > 1e-5f) { rx /= rLen; ry /= rLen; rz /= rLen; } + + float hw = s * 0.5f; + float hh = s * 1.3f; + + float nx = ry * gz - rz * gy; + float ny = rz * gx - rx * gz; + float nz = rx * gy - ry * gx; + + float windTip = Math.min(1.0f, wind + 0.25f); + int base = col.vertexCount; + col.add(cx - rx * hw, cy - ry * hw, cz - rz * hw, nx, ny, nz, 0f, 0f, wind); + col.add(cx + rx * hw, cy + ry * hw, cz + rz * hw, nx, ny, nz, 1f, 0f, wind); + col.add(cx + rx * hw + gx * hh, cy + ry * hw + gy * hh, cz + rz * hw + gz * hh, nx, ny, nz, 1f, 1f, windTip); + col.add(cx - rx * hw + gx * hh, cy - ry * hw + gy * hh, cz - rz * hw + gz * hh, nx, ny, nz, 0f, 1f, windTip); + col.tri(base, base + 1, base + 2); + col.tri(base, base + 2, base + 3); + col.tri(base + 2, base + 1, base); + col.tri(base + 3, base + 2, base); + } + + // ── Trauben-Billboard ───────────────────────────────────────────────────── + + /** Zwei gekreuzte Quads für eine ganze Traube (hängt nach unten vom Aufhängepunkt). */ + private static void addGrapeBunch(VertexCollector col, + float cx, float cy, float cz, + float w, float h) { + // Quad 1 — X/Y-Ebene + int base = col.vertexCount; + col.add(cx - w, cy, cz, 0, 0, 1, 0, 1, 0f); + col.add(cx + w, cy, cz, 0, 0, 1, 1, 1, 0f); + col.add(cx + w, cy - h, cz, 0, 0, 1, 1, 0, 0f); + col.add(cx - w, cy - h, cz, 0, 0, 1, 0, 0, 0f); + col.tri(base, base+1, base+2); col.tri(base, base+2, base+3); + col.tri(base+2, base+1, base); col.tri(base+3, base+2, base); + // Quad 2 — Z/Y-Ebene + base = col.vertexCount; + col.add(cx, cy, cz - w, 1, 0, 0, 0, 1, 0f); + col.add(cx, cy, cz + w, 1, 0, 0, 1, 1, 0f); + col.add(cx, cy - h, cz + w, 1, 0, 0, 1, 0, 0f); + col.add(cx, cy - h, cz - w, 1, 0, 0, 0, 0, 0f); + col.tri(base, base+1, base+2); col.tri(base, base+2, base+3); + col.tri(base+2, base+1, base); col.tri(base+3, base+2, base); + } + + // ── Zylinder ────────────────────────────────────────────────────────────── + + private static void addCylinder(VertexCollector col, + Vector3f start, Vector3f end, + float rBot, float rTop, + float windBot, float windTop, int N) { + Vector3f axis = end.subtract(start); + if (axis.lengthSquared() < 1e-8f) return; + axis.normalizeLocal(); + + Vector3f perp1 = (Math.abs(axis.y) < 0.9f) + ? axis.cross(Vector3f.UNIT_Y).normalizeLocal() + : axis.cross(Vector3f.UNIT_X).normalizeLocal(); + Vector3f perp2 = axis.cross(perp1).normalizeLocal(); + + int base = col.vertexCount; + int N1 = N + 1; + + for (int ring = 0; ring < 2; ring++) { + Vector3f center = (ring == 0) ? start : end; + float r = (ring == 0) ? rBot : rTop; + float wind = (ring == 0) ? windBot : windTop; + float vCoord = ring; + for (int i = 0; i <= N; i++) { + float theta = FastMath.TWO_PI * i / N; + float cos = FastMath.cos(theta); + float sin = FastMath.sin(theta); + float nx = cos * perp1.x + sin * perp2.x; + float ny = cos * perp1.y + sin * perp2.y; + float nz = cos * perp1.z + sin * perp2.z; + col.add(center.x + nx * r, center.y + ny * r, center.z + nz * r, + nx, ny, nz, (float) i / N, vCoord, wind); + } + } + + for (int i = 0; i < N; i++) { + int b0 = base + i, b1 = base + i + 1; + int t0 = base + N1 + i, t1 = base + N1 + i + 1; + col.tri(b0, b1, t1); + col.tri(b0, t1, t0); + } + } + + // ── Gebogene Pfade ──────────────────────────────────────────────────────── + + /** + * Berechnet Pfadpunkte von start→end mit zufälligen Versätzen senkrecht zur + * Hauptrichtung. Die Hüllkurve (sin) stellt sicher, dass Anfang und Ende exakt + * auf der Geraden liegen. + * + * @param segments Anzahl Zwischenpunkte + 1 (Segmente); Gesamt-Punkte = segments+1 + * @param bendAmt Maximale Auslenkung relativ zur Länge (z.B. 0.10 = 10 %) + */ + private static Vector3f[] bentPathPoints(Vector3f start, Vector3f end, + int segments, float bendAmt, Random rng) { + Vector3f[] pts = new Vector3f[segments + 1]; + pts[0] = start.clone(); + pts[segments] = end.clone(); + + Vector3f main = end.subtract(start); + float len = main.length(); + if (len < 1e-6f) { + for (int i = 1; i < segments; i++) pts[i] = start.clone(); + return pts; + } + main.divideLocal(len); + + Vector3f p1 = (Math.abs(main.y) < 0.9f) + ? main.cross(Vector3f.UNIT_Y).normalizeLocal() + : main.cross(Vector3f.UNIT_X).normalizeLocal(); + Vector3f p2 = main.cross(p1).normalizeLocal(); + + for (int i = 1; i < segments; i++) { + float t = (float) i / segments; + float envelope = FastMath.sin(t * FastMath.PI); + float off1 = (rng.nextFloat() * 2f - 1f) * bendAmt * len * envelope; + float off2 = (rng.nextFloat() * 2f - 1f) * bendAmt * len * envelope; + pts[i] = start.add(main.mult(len * t)) + .addLocal(p1.mult(off1)) + .addLocal(p2.mult(off2)); + } + return pts; + } + + /** Linearer Interpolationspfad entlang eines Punkt-Arrays, t ∈ [0,1]. */ + private static Vector3f interpPath(Vector3f[] pts, float t) { + if (pts.length == 1) return pts[0].clone(); + float scaled = t * (pts.length - 1); + int lo = (int) scaled; + float frac = scaled - lo; + if (lo >= pts.length - 1) return pts[pts.length - 1].clone(); + return pts[lo].clone().interpolateLocal(pts[lo + 1], frac); + } + + /** Convenience-Wrapper: berechnet Punkte und rendert nahtlosen Polylinien-Zylinder. */ + private static void addBentPath(VertexCollector col, + Vector3f start, Vector3f end, + float rBot, float rTop, + float windBot, float windTop, + int segments, int ringN, + float bendAmt, Random rng) { + Vector3f[] pts = bentPathPoints(start, end, segments, bendAmt, rng); + addPolylineCylinder(col, pts, rBot, rTop, windBot, windTop, ringN); + } + + /** + * Erzeugt einen nahtlosen Zylinder entlang beliebig vieler Pfadpunkte. + * Parallel-Transport verhindert Twist und Lücken an den Übergängen. + */ + private static void addPolylineCylinder(VertexCollector col, Vector3f[] pts, + float rBot, float rTop, + float windBot, float windTop, int N) { + int numPts = pts.length; + if (numPts < 2) return; + int N1 = N + 1; + + // Tangentenrichtung an jedem Punkt + Vector3f[] tang = new Vector3f[numPts]; + for (int p = 0; p < numPts; p++) { + if (p == 0) { + tang[p] = pts[1].subtract(pts[0]).normalizeLocal(); + } else if (p == numPts - 1) { + tang[p] = pts[p].subtract(pts[p - 1]).normalizeLocal(); + } else { + Vector3f a = pts[p].subtract(pts[p - 1]).normalizeLocal(); + Vector3f b = pts[p + 1].subtract(pts[p]).normalizeLocal(); + tang[p] = a.add(b).normalizeLocal(); + } + } + + // Parallel-Transport: perp1 des ersten Rings auf alle weiteren projizieren + Vector3f[] perp1 = new Vector3f[numPts]; + perp1[0] = (Math.abs(tang[0].y) < 0.9f) + ? tang[0].cross(Vector3f.UNIT_Y).normalizeLocal() + : tang[0].cross(Vector3f.UNIT_X).normalizeLocal(); + for (int p = 1; p < numPts; p++) { + float dot = tang[p].dot(perp1[p - 1]); + Vector3f v = perp1[p - 1].subtract(tang[p].mult(dot)); + perp1[p] = (v.lengthSquared() > 1e-8f) ? v.normalizeLocal() : perp1[p - 1].clone(); + } + + int baseVtx = col.vertexCount; + + // Ringe erzeugen + for (int p = 0; p < numPts; p++) { + float t = (float) p / (numPts - 1); + float r = rBot + (rTop - rBot) * t; + float wind = windBot + (windTop - windBot) * t; + Vector3f p2 = tang[p].cross(perp1[p]).normalizeLocal(); + + for (int i = 0; i <= N; i++) { + float theta = FastMath.TWO_PI * i / N; + float cos = FastMath.cos(theta); + float sin = FastMath.sin(theta); + float nx = cos * perp1[p].x + sin * p2.x; + float ny = cos * perp1[p].y + sin * p2.y; + float nz = cos * perp1[p].z + sin * p2.z; + col.add(pts[p].x + nx * r, pts[p].y + ny * r, pts[p].z + nz * r, + nx, ny, nz, (float) i / N, t, wind); + } + } + + // Ringe verbinden + for (int p = 0; p < numPts - 1; p++) { + for (int i = 0; i < N; i++) { + int b0 = baseVtx + p * N1 + i; + int b1 = baseVtx + p * N1 + i + 1; + int t0 = baseVtx + (p + 1) * N1 + i; + int t1 = baseVtx + (p + 1) * N1 + i + 1; + col.tri(b0, b1, t1); + col.tri(b0, t1, t0); + } + } + } + + // ── Vertex-Sammler ──────────────────────────────────────────────────────── + + private static final class VertexCollector { + final List pos = new ArrayList<>(); + final List norm = new ArrayList<>(); + final List uv = new ArrayList<>(); + final List wind = new ArrayList<>(); + final List idx = new ArrayList<>(); + int vertexCount = 0; + + void add(float x, float y, float z, + float nx, float ny, float nz, + float u, float v, float w) { + pos.add(x); pos.add(y); pos.add(z); + norm.add(nx); norm.add(ny); norm.add(nz); + uv.add(u); uv.add(v); + wind.add(w); + vertexCount++; + } + + void tri(int a, int b, int c) { idx.add(a); idx.add(b); idx.add(c); } + + Mesh toMesh() { + if (vertexCount == 0) return new Mesh(); + int n = vertexCount; + FloatBuffer posB = BufferUtils.createFloatBuffer(n * 3); + FloatBuffer normB = BufferUtils.createFloatBuffer(n * 3); + FloatBuffer uvB = BufferUtils.createFloatBuffer(n * 2); + FloatBuffer colB = BufferUtils.createFloatBuffer(n * 4); + IntBuffer idxB = BufferUtils.createIntBuffer(idx.size()); + + for (Float f : pos) posB.put(f); + for (Float f : norm) normB.put(f); + for (Float f : uv) uvB.put(f); + for (Float f : wind) { colB.put(f); colB.put(0f); colB.put(0f); colB.put(1f); } + for (Integer i : idx) idxB.put(i); + + Mesh mesh = new Mesh(); + mesh.setBuffer(VertexBuffer.Type.Position, 3, posB); + mesh.setBuffer(VertexBuffer.Type.Normal, 3, normB); + mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, uvB); + mesh.setBuffer(VertexBuffer.Type.Color, 4, colB); + mesh.setBuffer(VertexBuffer.Type.Index, 3, idxB); + mesh.updateBound(); + mesh.updateCounts(); + return mesh; + } + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/tree/GrapevineOptions.java b/blight-editor/src/main/java/de/blight/editor/tree/GrapevineOptions.java new file mode 100644 index 0000000..65ee6b8 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/tree/GrapevineOptions.java @@ -0,0 +1,42 @@ +package de.blight.editor.tree; + +public class GrapevineOptions { + + public int seed = 77541; + public float totalWidth = 6.0f; // Pfosten-zu-Pfosten + public float totalHeight = 2.8f; + public float firstWireH = 0.90f; // Stammhöhe = erster Draht + public int wireCount = 4; + public float postRadius = 0.065f; + public float trunkRadius = 0.040f; + public float shootRadius = 0.016f; + public int shootCount = 7; // pro Arm + public float leafSize = 0.30f; + public int leafCount = 96; + public String leafTexture = "Textures/internal/foliage/wine.png"; + public float leafR = 0.35f, leafG = 0.72f, leafB = 0.18f; + public String grapeTexture = "Textures/internal/fruits/wine.png"; + public float windStrength = 0.12f; + public float windSpeed = 0.45f; + + public GrapevineOptions copy() { + GrapevineOptions c = new GrapevineOptions(); + c.seed = seed; + c.totalWidth = totalWidth; + c.totalHeight = totalHeight; + c.firstWireH = firstWireH; + c.wireCount = wireCount; + c.postRadius = postRadius; + c.trunkRadius = trunkRadius; + c.shootRadius = shootRadius; + c.shootCount = shootCount; + c.leafSize = leafSize; + c.leafCount = leafCount; + c.leafTexture = leafTexture; + c.leafR = leafR; c.leafG = leafG; c.leafB = leafB; + c.grapeTexture = grapeTexture; + c.windStrength = windStrength; + c.windSpeed = windSpeed; + return c; + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/tree/PalmOptions.java b/blight-editor/src/main/java/de/blight/editor/tree/PalmOptions.java index 4140d81..8fbc59b 100644 --- a/blight-editor/src/main/java/de/blight/editor/tree/PalmOptions.java +++ b/blight-editor/src/main/java/de/blight/editor/tree/PalmOptions.java @@ -27,7 +27,7 @@ public class PalmOptions { // Textures public String barkTexture = "Textures/internal/bark/Bark_Palm.png"; - public String leafTexture = "Textures/internal/leaves/palm.png"; + public String leafTexture = "Textures/internal/foliage/palm.png"; public float leafTextureAspect = 0f; // width/length-Verhältnis der Textur, 0 = frondWidth nutzen public float gravity = 0.3f; // Durchhang: 0 = gerade, höher = stärker hängend @@ -35,7 +35,7 @@ public class PalmOptions { public float crownHeight = 1.2f; public float crownFlare = 1.6f; // max. Aufweitung = trunkRadiusTop × crownFlare public float crownR = 0.22f, crownG = 0.58f, crownB = 0.14f; - public String crownTexture = "Textures/internal/leaves/palmcrown.png"; + public String crownTexture = "Textures/internal/foliage/palmcrown.png"; public PalmOptions copy() { diff --git a/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder.java b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder.java index 8e4851c..af9bd81 100644 --- a/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder.java +++ b/blight-editor/src/main/java/de/blight/editor/tree/TreeMeshBuilder.java @@ -223,47 +223,62 @@ public class TreeMeshBuilder { float wind, float scale, int count, float angleDeg, Rng rng) { for (int i = 0; i < count; i++) { - float ox = rng.range(-scale * 0.5f, scale * 0.5f); - float oy = rng.range(-scale * 0.25f, scale * 0.25f); - float oz = rng.range(-scale * 0.5f, scale * 0.5f); + float ox = rng.range(-scale * 0.20f, scale * 0.20f); + float oy = rng.range(-scale * 0.10f, scale * 0.10f); + float oz = rng.range(-scale * 0.20f, scale * 0.20f); float s = scale * (0.7f + rng.range(0f, 0.6f)); - float tilt = angleDeg * FastMath.DEG_TO_RAD; - addLeafQuad(col, tip.x + ox, tip.y + oy, tip.z + oz, - s, wind, rng.range(0f, FastMath.TWO_PI), tilt); + + // Volle 3D-Richtung: zufälliger Azimuth + zufällige Neigung (volle Kugel) + float yaw = rng.range(0f, FastMath.TWO_PI); + float pitch = rng.range(0f, FastMath.PI); + float sinP = FastMath.sin(pitch); + float gx = sinP * FastMath.cos(yaw); + float gy = FastMath.cos(pitch); + float gz = sinP * FastMath.sin(yaw); + + addLeafQuad(col, tip.x + ox, tip.y + oy, tip.z + oz, s, wind, gx, gy, gz); } } private static void addLeafQuad(VertexCollector col, float cx, float cy, float cz, - float s, float wind, float yRot, float tilt) { - float cosY = FastMath.cos(yRot), sinY = FastMath.sin(yRot); - float cosT = FastMath.cos(tilt), sinT = FastMath.sin(tilt); - float hw = s * 0.5f; - float hh = s * 0.65f; - - // Quad A - for (int q = 0; q < 2; q++) { - // second quad perpendicular (yRot + PI/2) - float cy2 = (q == 0) ? cosY : -sinY; - float sz2 = (q == 0) ? sinY : cosY; - int base = col.vertexCount; - // 4 Ecken: unten-links, unten-rechts, oben-rechts, oben-links - // "oben" ist um tilt-Grad nach vorne/hinten geneigt - float[] xs = {-hw * cy2, hw * cy2, hw * cy2, -hw * cy2}; - float[] zs = {-hw * sz2, hw * sz2, hw * sz2, -hw * sz2}; - float[] ys = {-hh * cosT, -hh * cosT, hh * cosT, hh * cosT}; - - // Face normal = right × up = (cy2,0,sz2) × (0,1,0) = (-sz2, 0, cy2) - float nnx = -sz2, nnz = cy2; - col.add(cx + xs[0], cy + ys[0], cz + zs[0], nnx, 0, nnz, 0, 0, wind); - col.add(cx + xs[1], cy + ys[1], cz + zs[1], nnx, 0, nnz, 1, 0, wind); - col.add(cx + xs[2], cy + ys[2], cz + zs[2], nnx, 0, nnz, 1, 1, wind); - col.add(cx + xs[3], cy + ys[3], cz + zs[3], nnx, 0, nnz, 0, 1, wind); - col.tri(base, base+1, base+2); - col.tri(base, base+2, base+3); - col.tri(base+2, base+1, base); // back face - col.tri(base+3, base+2, base); + float s, float wind, + float gx, float gy, float gz) { + // Rechtsvektor senkrecht zur Wachstumsrichtung + float rx, ry, rz; + if (Math.abs(gy) < 0.95f) { + rx = gz; ry = 0f; rz = -gx; // cross(UNIT_Y, grow) + } else { + rx = 0f; ry = -gz; rz = gy; // cross(UNIT_X, grow) } + float rLen = FastMath.sqrt(rx * rx + ry * ry + rz * rz); + if (rLen > 1e-5f) { rx /= rLen; ry /= rLen; rz /= rLen; } + + float hw = s * 0.5f; + float hh = s * 1.3f; + + // Normalvektor: right × grow + float nx = ry * gz - rz * gy; + float ny = rz * gx - rx * gz; + float nz = rx * gy - ry * gx; + + float windTip = Math.min(1.0f, wind + 0.25f); + int base = col.vertexCount; + // Untere Kante am Ast (verankert) + col.add(cx - rx * hw, cy - ry * hw, cz - rz * hw, + nx, ny, nz, 0f, 0f, wind); + col.add(cx + rx * hw, cy + ry * hw, cz + rz * hw, + nx, ny, nz, 1f, 0f, wind); + // Obere Kante frei (schwingt im Wind) + col.add(cx + rx * hw + gx * hh, cy + ry * hw + gy * hh, cz + rz * hw + gz * hh, + nx, ny, nz, 1f, 1f, windTip); + col.add(cx - rx * hw + gx * hh, cy - ry * hw + gy * hh, cz - rz * hw + gz * hh, + nx, ny, nz, 0f, 1f, windTip); + + col.tri(base, base + 1, base + 2); + col.tri(base, base + 2, base + 3); + col.tri(base + 2, base + 1, base); + col.tri(base + 3, base + 2, base); } // ── Ast-Richtung berechnen ──────────────────────────────────────────────── diff --git a/blight-editor/src/main/java/de/blight/editor/tree/TreeParams.java b/blight-editor/src/main/java/de/blight/editor/tree/TreeParams.java index a35c89d..18c02e9 100644 --- a/blight-editor/src/main/java/de/blight/editor/tree/TreeParams.java +++ b/blight-editor/src/main/java/de/blight/editor/tree/TreeParams.java @@ -43,7 +43,7 @@ public class TreeParams { // ── Texturen (relative Pfade für den Asset-Manager) ────────────────────── public String barkTexture = null; // z.B. "Textures/internal/bark/Bark001_Color.jpg" - public String leafTexture = null; // z.B. "Textures/internal/leaves/oak.png" + public String leafTexture = null; // z.B. "Textures/internal/foliage/oak.png" // ── Wind ───────────────────────────────────────────────────────────────── public float trunkFlexibility = 0.05f; @@ -68,7 +68,7 @@ public class TreeParams { p.leafScale = 1.4f; p.leafCount = 6; p.leafAngle = 42f; p.leafBranchings = 2; p.trunkFlexibility = 0.04f; p.branchFlexibility = 0.85f; p.barkTexture = "Textures/internal/bark/Bark001_Color.jpg"; - p.leafTexture = "Textures/internal/leaves/oak.png"; + p.leafTexture = "Textures/internal/foliage/oak.png"; return p; } @@ -89,7 +89,7 @@ public class TreeParams { p.leafScale = 0.9f; p.leafCount = 4; p.leafAngle = 38f; p.leafBranchings = 1; p.trunkFlexibility = 0.03f; p.branchFlexibility = 0.95f; p.barkTexture = "Textures/internal/bark/Bark002_Color.jpg"; - p.leafTexture = "Textures/internal/leaves/aspen.png"; + p.leafTexture = "Textures/internal/foliage/aspen.png"; return p; } @@ -110,7 +110,7 @@ public class TreeParams { p.leafScale = 0.7f; p.leafCount = 8; p.leafAngle = 70f; p.leafBranchings = 1; p.trunkFlexibility = 0.03f; p.branchFlexibility = 0.70f; p.barkTexture = "Textures/internal/bark/Bark003_Color.jpg"; - p.leafTexture = "Textures/internal/leaves/pine.png"; + p.leafTexture = "Textures/internal/foliage/pine.png"; return p; } @@ -131,7 +131,7 @@ public class TreeParams { p.leafScale = 1.5f; p.leafCount = 7; p.leafAngle = 55f; p.leafBranchings = 2; p.trunkFlexibility = 0.06f; p.branchFlexibility = 0.98f; p.barkTexture = "Textures/internal/bark/Bark001_Color.jpg"; - p.leafTexture = "Textures/internal/leaves/ash.png"; + p.leafTexture = "Textures/internal/foliage/ash.png"; return p; } @@ -152,7 +152,7 @@ public class TreeParams { p.leafScale = 1.0f; p.leafCount = 5; p.leafAngle = 50f; p.leafBranchings = 2; p.trunkFlexibility = 0.05f; p.branchFlexibility = 0.90f; p.barkTexture = "Textures/internal/bark/Bark001_Color.jpg"; - p.leafTexture = "Textures/internal/leaves/ash.png"; + p.leafTexture = "Textures/internal/foliage/ash.png"; return p; } diff --git a/blight-editor/src/main/java/de/blight/editor/util/ImpostorUtil.java b/blight-editor/src/main/java/de/blight/editor/util/ImpostorUtil.java index d06fc17..9d9ae85 100644 --- a/blight-editor/src/main/java/de/blight/editor/util/ImpostorUtil.java +++ b/blight-editor/src/main/java/de/blight/editor/util/ImpostorUtil.java @@ -386,10 +386,14 @@ public final class ImpostorUtil { for (Spatial s : root.getChildren()) { if (s instanceof Geometry g) { Material mat = g.getMaterial(); - if (mat.getMaterialDef().getMaterialParam("LightDir") != null) { - mat.setVector3("LightDir", lightDir); - mat.setVector3("SunColor", new Vector3f(1.3f, 1.2f, 1.0f)); - mat.setVector3("AmbientColor", new Vector3f(0.55f, 0.55f, 0.57f)); + if (mat == null) { continue; } + com.jme3.material.MaterialDef def = mat.getMaterialDef(); + if (def.getMaterialParam("LightDir") != null) { + mat.setVector3("LightDir", lightDir); + } + if (def.getMaterialParam("SunColor") != null) { + mat.setVector3("SunColor", new Vector3f(1.4f, 1.3f, 1.1f)); + mat.setVector3("AmbientColor", new Vector3f(0.18f, 0.18f, 0.22f)); } } else if (s instanceof Node n) { applyTreeLighting(n, lightDir); diff --git a/blight-game/src/main/java/de/blight/game/state/AmbientParticlesState.java b/blight-game/src/main/java/de/blight/game/state/AmbientParticlesState.java index 8fb19cc..537e651 100644 --- a/blight-game/src/main/java/de/blight/game/state/AmbientParticlesState.java +++ b/blight-game/src/main/java/de/blight/game/state/AmbientParticlesState.java @@ -59,14 +59,14 @@ public class AmbientParticlesState extends BaseAppState { private float scanTimer = 0f; private static final String[] LEAF_TEX_PATHS = { - "Textures/internal/leavesfilter/leaf1.png", - "Textures/internal/leavesfilter/leaf2.png", - "Textures/internal/leavesfilter/leaf3.png", - "Textures/internal/leavesfilter/leaf4.png", - "Textures/internal/leavesfilter/leaf5.png", - "Textures/internal/leavesfilter/leaf6.png", - "Textures/internal/leavesfilter/leaf7.png", - "Textures/internal/leavesfilter/leaf8.png", + "Textures/internal/leafFilter/leaf1.png", + "Textures/internal/leafFilter/leaf2.png", + "Textures/internal/leafFilter/leaf3.png", + "Textures/internal/leafFilter/leaf4.png", + "Textures/internal/leafFilter/leaf5.png", + "Textures/internal/leafFilter/leaf6.png", + "Textures/internal/leafFilter/leaf7.png", + "Textures/internal/leafFilter/leaf8.png", }; @SuppressWarnings("deprecation") private ParticleEmitter[] leafEmitters; diff --git a/blight-game/src/main/java/de/blight/game/state/WorldObjectsState.java b/blight-game/src/main/java/de/blight/game/state/WorldObjectsState.java index 229fc54..28190cb 100644 --- a/blight-game/src/main/java/de/blight/game/state/WorldObjectsState.java +++ b/blight-game/src/main/java/de/blight/game/state/WorldObjectsState.java @@ -8,6 +8,7 @@ import com.jme3.asset.plugins.FileLocator; import com.jme3.bullet.BulletAppState; import com.jme3.bullet.collision.shapes.CapsuleCollisionShape; import com.jme3.bullet.collision.shapes.CompoundCollisionShape; +import com.jme3.bullet.collision.shapes.MeshCollisionShape; import com.jme3.bullet.control.RigidBodyControl; import com.jme3.bullet.util.CollisionShapeFactory; import com.jme3.bounding.BoundingBox; @@ -108,7 +109,22 @@ public class WorldObjectsState extends BaseAppState { // Baum-Stammkollision: Capsule statt Vollmesh (Blätter ausgeschlossen) String objPath = m.modelPath() != null ? m.modelPath() : ""; - if (objPath.startsWith("Models/trees/") && bulletAppState != null) { + if (objPath.contains("/grapevine/") && bulletAppState != null) { + try { + s.updateGeometricState(); + if (s instanceof Node vineNode) { + CompoundCollisionShape compound = new CompoundCollisionShape(); + collectGrapevineShapes(vineNode, compound); + if (!compound.getChildren().isEmpty()) { + RigidBodyControl rbc = new RigidBodyControl(compound, 0f); + s.addControl(rbc); + bulletAppState.getPhysicsSpace().add(rbc); + } + } + } catch (Exception pe) { + log.warn("[WorldObjects] Grapevine-Physik für '{}' nicht erzeugbar: {}", m.modelPath(), pe.getMessage()); + } + } else if (objPath.startsWith("Models/trees/") && bulletAppState != null) { try { s.updateGeometricState(); float treeHeight = 6f; @@ -549,4 +565,18 @@ public class WorldObjectsState extends BaseAppState { mat.setFloat("Wetness", w); } } + + /** Sammelt Bark- und Wire-Geometrien der Grapevine als exakte Mesh-Shapes. */ + private static void collectGrapevineShapes(Node node, CompoundCollisionShape compound) { + for (Spatial child : node.getChildren()) { + if (child instanceof Geometry geo) { + String n = geo.getName(); + if ("bark".equals(n) || "wires".equals(n)) { + compound.addChildShape(new MeshCollisionShape(geo.getMesh()), Vector3f.ZERO); + } + } else if (child instanceof Node sub) { + collectGrapevineShapes(sub, compound); + } + } + } } diff --git a/blight-map/src/main/map/blight_map.blm b/blight-map/src/main/map/blight_map.blm index c3e2bf5..4fde61f 100644 Binary files a/blight-map/src/main/map/blight_map.blm and b/blight-map/src/main/map/blight_map.blm differ diff --git a/blight-map/src/main/map/blight_objects.blo b/blight-map/src/main/map/blight_objects.blo index dae7eec..b9789b5 100644 --- a/blight-map/src/main/map/blight_objects.blo +++ b/blight-map/src/main/map/blight_objects.blo @@ -4,3 +4,4 @@ Models/trees/pine/medium/pine_medium_20260706_190947.j3o -6.42357 1.22971 -1318. Models/trees/pine/medium/pine_medium_20260706_190953.j3o -17.84356 1.24567 -1316.45410 0.26793 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 Models/plants/misc/kaktusfeige.j3o -6.18754 1.22995 -1320.34875 0.00000 2.50000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 Models/imported/alter_steg.j3o -8.42317 -0.11015 -1359.15662 3.14159 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 +Models/trees/grapevine/grapevine_20260725_195635.j3o -85.78869 1.32939 -1297.17883 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 diff --git a/blight-map/src/main/map/blight_voxel_splat.bin b/blight-map/src/main/map/blight_voxel_splat.bin new file mode 100644 index 0000000..59fe886 Binary files /dev/null and b/blight-map/src/main/map/blight_voxel_splat.bin differ diff --git a/blight-map/src/main/map/chunks/chunk_13_05.blc b/blight-map/src/main/map/chunks/chunk_13_05.blc index 1f970d0..6ea2d9c 100644 Binary files a/blight-map/src/main/map/chunks/chunk_13_05.blc and b/blight-map/src/main/map/chunks/chunk_13_05.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_14_04.blc b/blight-map/src/main/map/chunks/chunk_14_04.blc index a3915a0..7510cff 100644 Binary files a/blight-map/src/main/map/chunks/chunk_14_04.blc and b/blight-map/src/main/map/chunks/chunk_14_04.blc differ diff --git a/blight-map/src/main/map/chunks/chunk_14_05.blc b/blight-map/src/main/map/chunks/chunk_14_05.blc index 0d1299f..69692dd 100644 Binary files a/blight-map/src/main/map/chunks/chunk_14_05.blc and b/blight-map/src/main/map/chunks/chunk_14_05.blc differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_05.blvc b/blight-map/src/main/map/chunks/voxel_14_0_05.blvc new file mode 100644 index 0000000..7e1d89b Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_14_0_05.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_14_0_06.blvc b/blight-map/src/main/map/chunks/voxel_14_0_06.blvc index 1676381..48deeeb 100644 Binary files a/blight-map/src/main/map/chunks/voxel_14_0_06.blvc and b/blight-map/src/main/map/chunks/voxel_14_0_06.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_05.blvc b/blight-map/src/main/map/chunks/voxel_15_0_05.blvc new file mode 100644 index 0000000..abc1f52 Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_0_05.blvc differ diff --git a/blight-map/src/main/map/chunks/voxel_15_0_06.blvc b/blight-map/src/main/map/chunks/voxel_15_0_06.blvc new file mode 100644 index 0000000..cf33ddd Binary files /dev/null and b/blight-map/src/main/map/chunks/voxel_15_0_06.blvc differ diff --git a/blight-vegetation-generator/src/main/java/de/blight/eztree/TreePresets.java b/blight-vegetation-generator/src/main/java/de/blight/eztree/TreePresets.java index af7ddae..ddb26d8 100644 --- a/blight-vegetation-generator/src/main/java/de/blight/eztree/TreePresets.java +++ b/blight-vegetation-generator/src/main/java/de/blight/eztree/TreePresets.java @@ -20,10 +20,10 @@ public final class TreePresets { private static final String BARK2 = "Textures/internal/bark/Bark002_Color.jpg"; private static final String BARK3 = "Textures/internal/bark/Bark003_Color.jpg"; - private static final String LEAF_OAK = "Textures/internal/leaves/oak.png"; - private static final String LEAF_ASH = "Textures/internal/leaves/ash.png"; - private static final String LEAF_ASPEN = "Textures/internal/leaves/aspen.png"; - private static final String LEAF_PINE = "Textures/internal/leaves/pine.png"; + private static final String LEAF_OAK = "Textures/internal/foliage/oak.png"; + private static final String LEAF_ASH = "Textures/internal/foliage/ash.png"; + private static final String LEAF_ASPEN = "Textures/internal/foliage/aspen.png"; + private static final String LEAF_PINE = "Textures/internal/foliage/pine.png"; // ════════════════════════════════════════════════════════════════════════ // OAK @@ -501,9 +501,9 @@ public final class TreePresets { case "pine small" -> pineSmall(); case "pine medium" -> pineMedium(); case "pine large" -> pineLarge(); - case "bush 1" -> bush1(); - case "bush 2" -> bush2(); - case "bush 3" -> bush3(); + case "bush 1" -> bush1(); + case "bush 2" -> bush2(); + case "bush 3" -> bush3(); case "trellis" -> trellis(); default -> oakMedium(); }; diff --git a/doc/Endgame.odt b/doc/Endgame.odt new file mode 100644 index 0000000..61e9068 Binary files /dev/null and b/doc/Endgame.odt differ diff --git a/doc/Freibeuter.odt b/doc/Freibeuter.odt new file mode 100644 index 0000000..06b36cf Binary files /dev/null and b/doc/Freibeuter.odt differ