diff --git a/.gitignore b/.gitignore index 6d729f6..0442206 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,10 @@ /.metadata/ /.gradle/ +# Gradle build output +**/build/ +**/config/ + # Eclipse project files .project .classpath diff --git a/blight-assets/src/main/resources/Textures/bark/Bark_Palm.png b/blight-assets/src/main/resources/Textures/bark/Bark_Palm.png new file mode 100644 index 0000000..875a575 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/bark/Bark_Palm.png differ diff --git a/blight-assets/src/main/resources/Textures/bark/Bark_Palm2.png b/blight-assets/src/main/resources/Textures/bark/Bark_Palm2.png new file mode 100644 index 0000000..6a0eef8 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/bark/Bark_Palm2.png differ diff --git a/blight-assets/src/main/resources/Textures/leaves/palm.png b/blight-assets/src/main/resources/Textures/leaves/palm.png index b6db0bb..6409fad 100644 Binary files a/blight-assets/src/main/resources/Textures/leaves/palm.png and b/blight-assets/src/main/resources/Textures/leaves/palm.png differ diff --git a/blight-assets/src/main/resources/Textures/leaves/palm2.png b/blight-assets/src/main/resources/Textures/leaves/palm2.png index 3ca0657..8093782 100644 Binary files a/blight-assets/src/main/resources/Textures/leaves/palm2.png and b/blight-assets/src/main/resources/Textures/leaves/palm2.png differ diff --git a/blight-assets/src/main/resources/Textures/leaves/palmcrown.png b/blight-assets/src/main/resources/Textures/leaves/palmcrown.png new file mode 100644 index 0000000..1c6d4e4 Binary files /dev/null and b/blight-assets/src/main/resources/Textures/leaves/palmcrown.png differ diff --git a/blight-editor/editor-assets/textures/impostor_Baum1.png b/blight-editor/editor-assets/textures/impostor_Baum1.png new file mode 100644 index 0000000..700161e Binary files /dev/null and b/blight-editor/editor-assets/textures/impostor_Baum1.png differ 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 0ece3bd..7c4074c 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -71,6 +71,11 @@ public class EditorApp extends Application { private PalmOptions palmOptions = new PalmOptions(); private final TextField palmNameField = new TextField("Palme1"); + // Aktives Tool + Tastenkürzel-Callbacks + private String currentTool = "world"; + private Runnable onF5 = () -> {}; // Vorschau + private Runnable onF6 = () -> {}; // Zufälligen Seed wählen + // Asset-Panel: Pfad-Map + DnD-Zustand private final Map, Path> itemPaths = new HashMap<>(); private TreeItem draggedItem = null; @@ -182,24 +187,28 @@ public class EditorApp extends Application { // ── Modus-Wechsel ──────────────────────────────────────────────────────── private void switchToTreeGenerator() { + currentTool = "tree"; topBar.getChildren().set(1, buildTreeToolBar()); root.setCenter(buildTreePreviewPanel()); root.setRight(buildTreeParamsPanel()); } private void switchToWorldEditor() { + currentTool = "world"; topBar.getChildren().set(1, worldToolBar); root.setCenter(worldViewport); root.setRight(toolPanel); } private void switchToEzTree() { + currentTool = "eztree"; topBar.getChildren().set(1, buildEzTreeToolBar()); - root.setCenter(buildTreePreviewPanel()); // teilt Vorschau-Infrastruktur + root.setCenter(buildTreePreviewPanel()); root.setRight(buildEzTreeParamsPanel()); } private void switchToPalm() { + currentTool = "palm"; topBar.getChildren().set(1, buildPalmToolBar()); root.setCenter(buildTreePreviewPanel()); root.setRight(buildPalmParamsPanel()); @@ -329,13 +338,14 @@ public class EditorApp extends Application { nameLabel.setStyle("-fx-font-weight: bold;"); treeNameField.setPrefWidth(130); - Button previewBtn = new Button("▶ Vorschau"); - previewBtn.setStyle("-fx-font-weight: bold;"); - previewBtn.setOnAction(e -> { + onF5 = () -> { input.treeGenQueue.offer( new SharedInput.TreeGenRequest(treeParams.copy(), false, treeNameField.getText().trim())); setStatus("Generiere Vorschau…"); - }); + }; + Button previewBtn = new Button("▶ Vorschau [F5]"); + previewBtn.setStyle("-fx-font-weight: bold;"); + previewBtn.setOnAction(e -> onF5.run()); Button exportBtn = new Button("💾 Exportieren als .j3o"); exportBtn.setOnAction(e -> { @@ -414,7 +424,17 @@ public class EditorApp extends Application { seedSpinner.setEditable(true); seedSpinner.setMaxWidth(Double.MAX_VALUE); seedSpinner.valueProperty().addListener((o, a, b) -> treeParams.seed = b); - inner.getChildren().add(seedSpinner); + Button treeRndSeed = new Button("🎲"); + onF6 = () -> { + int s = new java.util.Random().nextInt(100000); + treeParams.seed = s; + seedSpinner.getValueFactory().setValue(s); + onF5.run(); + }; + treeRndSeed.setOnAction(e -> onF6.run()); + HBox treeSeedRow = new HBox(4, seedSpinner, treeRndSeed); + HBox.setHgrow(seedSpinner, Priority.ALWAYS); + inner.getChildren().add(treeSeedRow); // Ast-Tiefe inner.getChildren().add(bold("Ast-Tiefe:")); @@ -525,14 +545,15 @@ public class EditorApp extends Application { nameLabel.setStyle("-fx-font-weight: bold;"); ezTreeNameField.setPrefWidth(130); - Button previewBtn = new Button("▶ Vorschau"); - previewBtn.setStyle("-fx-font-weight: bold;"); - previewBtn.setOnAction(e -> { + onF5 = () -> { input.ezTreeGenQueue.offer( new SharedInput.EzTreeGenRequest( ezTreeOptions.copy(), false, ezTreeNameField.getText().trim())); setStatus("EZ-Tree: generiere Vorschau…"); - }); + }; + Button previewBtn = new Button("▶ Vorschau [F5]"); + previewBtn.setStyle("-fx-font-weight: bold;"); + previewBtn.setOnAction(e -> onF5.run()); Button exportBtn = new Button("💾 Export .j3o"); exportBtn.setOnAction(e -> { @@ -564,7 +585,17 @@ public class EditorApp extends Application { inner.getChildren().add(bold("Zufallssamen:")); Spinner seedSp = intSpinner(0, 999999, ezTreeOptions.seed); seedSp.valueProperty().addListener((o, a, b) -> ezTreeOptions.seed = b); - inner.getChildren().add(seedSp); + Button ezRndSeed = new Button("🎲"); + onF6 = () -> { + int s = new java.util.Random().nextInt(1000000); + ezTreeOptions.seed = s; + seedSp.getValueFactory().setValue(s); + onF5.run(); + }; + ezRndSeed.setOnAction(e -> onF6.run()); + HBox ezSeedRow = new HBox(4, seedSp, ezRndSeed); + HBox.setHgrow(seedSp, Priority.ALWAYS); + inner.getChildren().add(ezSeedRow); inner.getChildren().add(bold("Typ:")); ChoiceBox typeCB = new ChoiceBox<>(); @@ -728,14 +759,15 @@ public class EditorApp extends Application { nameLabel.setStyle("-fx-font-weight: bold;"); palmNameField.setPrefWidth(130); - Button previewBtn = new Button("▶ Vorschau"); - previewBtn.setStyle("-fx-font-weight: bold;"); - previewBtn.setOnAction(e -> { + onF5 = () -> { input.palmGenQueue.offer( new SharedInput.PalmGenRequest( palmOptions.copy(), false, palmNameField.getText().trim())); setStatus("Palme: generiere Vorschau…"); - }); + }; + Button previewBtn = new Button("▶ Vorschau [F5]"); + previewBtn.setStyle("-fx-font-weight: bold;"); + previewBtn.setOnAction(e -> onF5.run()); Button exportBtn = new Button("💾 Export .j3o"); exportBtn.setOnAction(e -> { @@ -763,37 +795,93 @@ public class EditorApp extends Application { inner.getChildren().add(bold("Zufallssamen:")); Spinner seedSp = intSpinner(0, 999999, palmOptions.seed); seedSp.valueProperty().addListener((o, a, b) -> palmOptions.seed = b); - inner.getChildren().add(seedSp); + Button palmRndSeed = new Button("🎲"); + onF6 = () -> { + int s = new java.util.Random().nextInt(1000000); + palmOptions.seed = s; + seedSp.getValueFactory().setValue(s); + onF5.run(); + }; + palmRndSeed.setOnAction(e -> onF6.run()); + HBox palmSeedRow = new HBox(4, seedSp, palmRndSeed); + HBox.setHgrow(seedSp, Priority.ALWAYS); + inner.getChildren().add(palmSeedRow); inner.getChildren().addAll(sectionTitle("Stamm"), new Separator()); + inner.getChildren().add(bold("Anzahl Stämme:")); + Spinner palmCountSp = intSpinner(1, 3, palmOptions.palmCount); + palmCountSp.valueProperty().addListener((o, a, b) -> palmOptions.palmCount = b); + inner.getChildren().add(palmCountSp); inner.getChildren().add(ezFloat("Höhe:", 5, 25, palmOptions.trunkHeight, v -> palmOptions.trunkHeight = v)); inner.getChildren().add(ezFloat("Radius unten:", 0.1, 1.0, palmOptions.trunkRadiusBottom, v -> palmOptions.trunkRadiusBottom = v)); inner.getChildren().add(ezFloat("Radius oben:", 0.05, 0.9, palmOptions.trunkRadiusTop, v -> palmOptions.trunkRadiusTop = v)); + inner.getChildren().add(ezFloat("Neigung (°):", 0.0, 50.0, palmOptions.lean, + v -> palmOptions.lean = v)); + + inner.getChildren().addAll(sectionTitle("Kronenschaft"), new Separator()); + inner.getChildren().add(ezFloat("Höhe:", 0.2, 4.0, palmOptions.crownHeight, + v -> palmOptions.crownHeight = v)); + inner.getChildren().add(ezFloat("Aufweitung:", 1.0, 3.0, palmOptions.crownFlare, + v -> palmOptions.crownFlare = v)); + inner.getChildren().add(ezFloat("Farbe R:", 0, 1, palmOptions.crownR, v -> palmOptions.crownR = v)); + inner.getChildren().add(ezFloat("Farbe G:", 0, 1, palmOptions.crownG, v -> palmOptions.crownG = v)); + inner.getChildren().add(ezFloat("Farbe B:", 0, 1, palmOptions.crownB, v -> palmOptions.crownB = v)); inner.getChildren().addAll(sectionTitle("Wedel"), new Separator()); inner.getChildren().add(bold("Anzahl:")); - Spinner frondCountSp = intSpinner(3, 20, palmOptions.frondCount); + Spinner frondCountSp = intSpinner(12, 120, palmOptions.frondCount); frondCountSp.valueProperty().addListener((o, a, b) -> palmOptions.frondCount = b); inner.getChildren().add(frondCountSp); - inner.getChildren().add(ezFloat("Winkel min (° von oben):", 60, 95, palmOptions.frondAngleMin, + inner.getChildren().add(ezFloat("Winkel min (° von oben):", 10, 90, palmOptions.frondAngleMin, v -> palmOptions.frondAngleMin = v)); - inner.getChildren().add(ezFloat("Winkel max (° von oben):", 85, 130, palmOptions.frondAngleMax, + inner.getChildren().add(ezFloat("Winkel max (° von oben):", 90, 170, palmOptions.frondAngleMax, v -> palmOptions.frondAngleMax = v)); inner.getChildren().add(ezFloat("Länge:", 2, 12, palmOptions.frondLength, v -> palmOptions.frondLength = v)); - inner.getChildren().addAll(sectionTitle("Blätter (Fiederblättchen)"), new Separator()); - inner.getChildren().add(bold("Paare pro Wedel:")); - Spinner pairsSp = intSpinner(3, 16, palmOptions.frondLeafletPairs); - pairsSp.valueProperty().addListener((o, a, b) -> palmOptions.frondLeafletPairs = b); - inner.getChildren().add(pairsSp); - inner.getChildren().add(ezFloat("Wedel-Breite:", 0.2, 4.0, palmOptions.frondWidth, - v -> palmOptions.frondWidth = v)); + inner.getChildren().addAll(sectionTitle("Blätter"), new Separator()); + inner.getChildren().add(ezFloat("Schwerkraft:", 0.0, 2.0, palmOptions.gravity, + v -> palmOptions.gravity = v)); + inner.getChildren().add(ezFloat("Größe oben (relativ):", 0.0, 1.0, palmOptions.frondMinSizeRatio, + v -> palmOptions.frondMinSizeRatio = v)); inner.getChildren().addAll(sectionTitle("Farben"), new Separator()); + // Blatt-Textur-Auswahl + Label leafTexLabel = new Label("Blatt-Textur:"); + leafTexLabel.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;"); + ComboBox leafTexBox = new ComboBox<>(); + leafTexBox.getItems().addAll("palm.png", "palm2.png"); + String currentLeaf = palmOptions.leafTexture != null + && palmOptions.leafTexture.contains("palm2") ? "palm2.png" : "palm.png"; + leafTexBox.setValue(currentLeaf); + leafTexBox.setMaxWidth(Double.MAX_VALUE); + leafTexBox.setOnAction(e -> palmOptions.leafTexture = "Textures/leaves/" + leafTexBox.getValue()); + inner.getChildren().add(new VBox(2, leafTexLabel, leafTexBox)); + + // Rinden-Textur-Auswahl (alle Texturen aus dem bark-Ordner) + Label barkTexLabel = new Label("Rinden-Textur:"); + barkTexLabel.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;"); + ComboBox barkTexBox = new ComboBox<>(); + barkTexBox.getItems().addAll( + "Bark_Palm.png", "Bark_Palm2.png", + "Bark001_Color.jpg", "Bark002_Color.jpg", + "Bark003_Color.jpg", "Bark008_Color.jpg" + ); + String currentBark = palmOptions.barkTexture != null + ? palmOptions.barkTexture.substring(palmOptions.barkTexture.lastIndexOf('/') + 1) + : "Bark_Palm.png"; + barkTexBox.setValue(barkTexBox.getItems().contains(currentBark) + ? currentBark : barkTexBox.getItems().get(0)); + barkTexBox.setMaxWidth(Double.MAX_VALUE); + barkTexBox.setOnAction(e -> { + if (barkTexBox.getValue() != null) + palmOptions.barkTexture = "Textures/bark/" + barkTexBox.getValue(); + }); + inner.getChildren().add(new VBox(2, barkTexLabel, barkTexBox)); + inner.getChildren().add(ezFloat("Stamm R:", 0, 1, palmOptions.barkR, v -> palmOptions.barkR = v)); inner.getChildren().add(ezFloat("Stamm G:", 0, 1, palmOptions.barkG, v -> palmOptions.barkG = v)); inner.getChildren().add(ezFloat("Stamm B:", 0, 1, palmOptions.barkB, v -> palmOptions.barkB = v)); @@ -1754,6 +1842,8 @@ public class EditorApp extends Application { case D -> input.right = pressed; case Q -> input.up = pressed; case E -> input.down = pressed; + case F5 -> { if (pressed) onF5.run(); } + case F6 -> { if (pressed) onF6.run(); } } } } 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 be3f10a..631a34b 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 @@ -23,6 +23,7 @@ import com.jme3.scene.Node; import com.jme3.scene.Spatial; import com.jme3.texture.FrameBuffer; import com.jme3.texture.Image; +import com.jme3.texture.Texture; import com.jme3.texture.Texture2D; import com.jme3.util.BufferUtils; import de.blight.editor.SharedInput; @@ -195,7 +196,9 @@ public class EzTreeState extends BaseAppState { mat.setFloat("WindSpeed", 0.5f); if (opts.bark.textureFile != null) { try { - mat.setTexture("BarkMap", assets.loadTexture(opts.bark.textureFile)); + Texture barkTex = assets.loadTexture(opts.bark.textureFile); + barkTex.setWrap(Texture.WrapMode.Repeat); + mat.setTexture("BarkMap", barkTex); mat.setBoolean("HasBarkMap", true); } catch (Exception ignored) {} } @@ -249,8 +252,8 @@ public class EzTreeState extends BaseAppState { Node scene = new Node("ezCapScene"); scene.addLight(new DirectionalLight( - new Vector3f(-0.4f, -1f, -0.5f).normalizeLocal(), ColorRGBA.White)); - scene.addLight(new AmbientLight(new ColorRGBA(0.35f, 0.35f, 0.35f, 1f))); + new Vector3f(-0.4f, -1f, -0.5f).normalizeLocal(), new ColorRGBA(2.0f, 1.85f, 1.5f, 1f))); + scene.addLight(new AmbientLight(new ColorRGBA(0.60f, 0.60f, 0.60f, 1f))); scene.attachChild(cloneForCapture(src)); vp.attachScene(scene); scene.updateGeometricState(); 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 196bcb2..7fffef1 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 @@ -8,6 +8,7 @@ import com.jme3.bounding.BoundingBox; import com.jme3.export.binary.BinaryExporter; import com.jme3.material.Material; import com.jme3.material.RenderState; +import com.jme3.texture.Texture; import com.jme3.math.ColorRGBA; import com.jme3.math.Vector3f; import com.jme3.renderer.queue.RenderQueue; @@ -55,8 +56,19 @@ public class PalmGeneratorState extends BaseAppState { SharedInput.PalmGenRequest req = input.palmGenQueue.poll(); if (req == null) return; - Node palm = PalmMeshBuilder.build(req.options()); - applyMaterials(palm, req.options()); + PalmOptions opts = req.options(); + if (opts.leafTexture != null) { + try { + Texture t = assets.loadTexture(opts.leafTexture); + int w = t.getImage().getWidth(); + int h = t.getImage().getHeight(); + boolean stemLeft = opts.leafTexture.contains("palm2"); + opts.leafTextureAspect = stemLeft ? (float) h / w : (float) w / h; + } catch (Exception ignored) {} + } + + Node palm = PalmMeshBuilder.build(opts); + applyMaterials(palm, opts); palm.updateGeometricState(); BoundingBox bb = palm.getWorldBound() instanceof BoundingBox b ? b : null; @@ -92,6 +104,10 @@ public class PalmGeneratorState extends BaseAppState { g.setMaterial(buildBarkMat(opts)); g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); } + case "crown" -> { + g.setMaterial(buildCrownMat(opts)); + g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); + } case "leaves" -> { g.setMaterial(buildLeafMat(opts)); g.setQueueBucket(RenderQueue.Bucket.Transparent); @@ -109,7 +125,9 @@ public class PalmGeneratorState extends BaseAppState { mat.setFloat("WindSpeed", 0.4f); if (opts.barkTexture != null) { try { - mat.setTexture("BarkMap", assets.loadTexture(opts.barkTexture)); + Texture barkTex = assets.loadTexture(opts.barkTexture); + barkTex.setWrap(Texture.WrapMode.Repeat); + mat.setTexture("BarkMap", barkTex); mat.setBoolean("HasBarkMap", true); } catch (Exception ignored) {} } @@ -143,6 +161,29 @@ public class PalmGeneratorState extends BaseAppState { } } + private Material buildCrownMat(PalmOptions opts) { + ColorRGBA color = new ColorRGBA(opts.crownR, opts.crownG, opts.crownB, 1f); + try { + Material mat = new Material(assets, "MatDefs/TreeLeaf.j3md"); + mat.setColor("Diffuse", color); + mat.setFloat("WindStrength", 0.04f); + mat.setFloat("WindSpeed", 0.3f); + if (opts.crownTexture != null) { + try { + Texture tex = assets.loadTexture(opts.crownTexture); + tex.setWrap(Texture.WrapMode.Repeat); + mat.setTexture("LeafMap", tex); + 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", color); + return mat; + } + } + private void exportPalm(Node palmNode, String name) { try { Path modelDir = ASSET_ROOT.resolve("models"); 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 afee952..1fb77f0 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 @@ -100,7 +100,6 @@ public class TreeGeneratorState extends BaseAppState { // ── Lifecycle ───────────────────────────────────────────────────────────── - @SuppressWarnings("deprecation") @Override protected void initialize(Application app) { this.app = (SimpleApplication) app; @@ -124,12 +123,12 @@ public class TreeGeneratorState extends BaseAppState { previewScene = new Node("previewScene"); previewSunLight = new DirectionalLight( new Vector3f(-0.45f, -1.0f, -0.3f).normalizeLocal(), - new ColorRGBA(1.2f, 1.1f, 0.9f, 1f)); + new ColorRGBA(2.0f, 1.85f, 1.5f, 1f)); previewScene.addLight(previewSunLight); previewScene.addLight(new DirectionalLight( new Vector3f(0.4f, -0.6f, -0.8f).normalizeLocal(), - new ColorRGBA(0.35f, 0.40f, 0.55f, 1f))); - previewScene.addLight(new AmbientLight(new ColorRGBA(0.25f, 0.25f, 0.30f, 1f))); + new ColorRGBA(0.70f, 0.75f, 1.0f, 1f))); + previewScene.addLight(new AmbientLight(new ColorRGBA(0.55f, 0.55f, 0.60f, 1f))); previewTreeHolder = new Node("treeHolder"); previewScene.attachChild(previewTreeHolder); previewScene.attachChild(buildPreviewGround()); @@ -140,7 +139,7 @@ public class TreeGeneratorState extends BaseAppState { new DirectionalLightShadowRenderer(assets, 2048, 1); shadowRenderer.setLight(previewSunLight); shadowRenderer.setEdgeFilteringMode(EdgeFilteringMode.PCF4); - shadowRenderer.setShadowIntensity(0.55f); + shadowRenderer.setShadowIntensity(0.25f); shadowRenderer.setShadowZExtend(80f); previewVP.addProcessor(shadowRenderer); previewTransfer = new FrameTransfer(input.treePreviewImage); @@ -386,7 +385,9 @@ public class TreeGeneratorState extends BaseAppState { mat.setFloat("WindSpeed", 0.5f); if (p.barkTexture != null) { try { - mat.setTexture("BarkMap", assets.loadTexture(p.barkTexture)); + Texture barkTex = assets.loadTexture(p.barkTexture); + barkTex.setWrap(Texture.WrapMode.Repeat); + mat.setTexture("BarkMap", barkTex); mat.setBoolean("HasBarkMap", true); } catch (Exception tex) { System.err.println("[TreeGenerator] Bark-Textur nicht gefunden: " + p.barkTexture); @@ -467,8 +468,8 @@ public class TreeGeneratorState extends BaseAppState { // Capture-Szene: Kopien der Geometrien + Beleuchtung Node scene = new Node("captureScene"); scene.addLight(new DirectionalLight( - new Vector3f(-0.4f, -1f, -0.5f).normalizeLocal(), ColorRGBA.White)); - scene.addLight(new AmbientLight(new ColorRGBA(0.35f, 0.35f, 0.35f, 1f))); + new Vector3f(-0.4f, -1f, -0.5f).normalizeLocal(), new ColorRGBA(2.0f, 1.85f, 1.5f, 1f))); + scene.addLight(new AmbientLight(new ColorRGBA(0.60f, 0.60f, 0.60f, 1f))); Node capTree = cloneForCapture(treeNode); scene.attachChild(capTree); diff --git a/blight-editor/src/main/java/de/blight/editor/tree/PalmMeshBuilder.java b/blight-editor/src/main/java/de/blight/editor/tree/PalmMeshBuilder.java index 80e5b88..536124d 100644 --- a/blight-editor/src/main/java/de/blight/editor/tree/PalmMeshBuilder.java +++ b/blight-editor/src/main/java/de/blight/editor/tree/PalmMeshBuilder.java @@ -23,26 +23,99 @@ import java.util.Random; public class PalmMeshBuilder { public static Node build(PalmOptions opts) { - Random rng = new Random(opts.seed); - float[] azimuths = computeAzimuths(opts, rng); - float[] elevations = computeElevations(opts, rng); + int count = Math.max(1, Math.min(3, opts.palmCount)); + float maxH = FastMath.tan(opts.lean * FastMath.DEG_TO_RAD); + float minH = count > 1 ? Math.max(maxH * 0.45f, FastMath.tan(10f * FastMath.DEG_TO_RAD)) : 0f; + + float segSize = 0.5f; + int M = Math.max(4, Math.round(opts.trunkHeight / segSize)); + float stepH = opts.trunkHeight / M; + + // Eigener RNG pro Stamm + Random[] rngs = new Random[count]; + for (int p = 0; p < count; p++) + rngs[p] = new Random(opts.seed + (long) p * 73856093L); + + // Azimut-Sektoren + Startrichtungen + float[] sectorAz = new float[count]; + float[] dX = new float[count]; + float[] dZ = new float[count]; + for (int p = 0; p < count; p++) { + sectorAz[p] = FastMath.TWO_PI * p / count + + (rngs[p].nextFloat() - 0.5f) * (FastMath.TWO_PI / count) * 0.15f; + float initH = minH + rngs[p].nextFloat() * Math.max(0f, maxH - minH); + dX[p] = FastMath.cos(sectorAz[p]) * initH; + dZ[p] = FastMath.sin(sectorAz[p]) * initH; + } + + // Alle Stämme gleichzeitig simulieren → ermöglicht Abstoßung + float[][] cx = new float[count][M + 1]; + float[][] cz = new float[count][M + 1]; + + // Schwächerer Aufwärtsdrang bei Mehrstamm (höhere Dämpfung = mehr Trägheit der Richtung) + float damping = count > 1 ? 0.88f : 0.72f; + // Abstoßungskraft skaliert mit Neigungsstärke und Segmentgröße + float repStrength = count > 1 ? maxH * stepH * 0.40f : 0f; + + for (int i = 1; i <= M; i++) { + for (int p = 0; p < count; p++) { + // Zufallswanderung mit reduzierterer Dämpfung + dX[p] = dX[p] * damping + (rngs[p].nextFloat() - 0.5f) * maxH * 0.9f; + dZ[p] = dZ[p] * damping + (rngs[p].nextFloat() - 0.5f) * maxH * 0.9f; + + // Sektor-Bias (leichter als zuvor, Abstoßung übernimmt die Spreizung) + if (count > 1) { + dX[p] += FastMath.cos(sectorAz[p]) * minH * 0.10f; + dZ[p] += FastMath.sin(sectorAz[p]) * minH * 0.10f; + } + + // Abstoßung von anderen Stämmen (Positionen des vorherigen Schritts) + for (int q = 0; q < count; q++) { + if (q == p) continue; + float rx = cx[p][i - 1] - cx[q][i - 1]; + float rz = cz[p][i - 1] - cz[q][i - 1]; + float d2 = Math.max(0.09f, rx * rx + rz * rz); // min. 0.3m Abstand + float d = FastMath.sqrt(d2); + float f = repStrength / d2; + dX[p] += (rx / d) * f; + dZ[p] += (rz / d) * f; + } + + // Geschwindigkeit begrenzen (mehr Spielraum bei Mehrstamm) + float velMax = count > 1 ? maxH * 2.0f : maxH; + float hLen = FastMath.sqrt(dX[p] * dX[p] + dZ[p] * dZ[p]); + if (hLen > velMax) { dX[p] *= velMax / hLen; dZ[p] *= velMax / hLen; } + + cx[p][i] = cx[p][i - 1] + dX[p] * stepH; + cz[p][i] = cz[p][i - 1] + dZ[p] * stepH; + } + } + + Accum barkAcc = new Accum(); + Accum crownAcc = new Accum(); + Accum leafAcc = new Accum(); + + for (int p = 0; p < count; p++) { + float topX = cx[p][M], topZ = cz[p][M]; + float[] azimuths = computeAzimuths(opts, rngs[p]); + float[] elevations = computeElevations(opts, rngs[p]); + float[] yOffsets = computeYOffsets(opts, elevations, rngs[p]); + addTrunk(barkAcc, opts, cx[p], cz[p], M, stepH); + addCrown(crownAcc, opts, topX, topZ); + addLeaves(leafAcc, opts, azimuths, elevations, yOffsets, topX, topZ); + } Node palm = new Node("palm"); - palm.attachChild(new Geometry("bark", buildBarkMesh(opts))); - palm.attachChild(new Geometry("leaves", buildLeafMesh(opts, azimuths, elevations))); + palm.attachChild(new Geometry("bark", barkAcc.toMesh())); + palm.attachChild(new Geometry("crown", crownAcc.toMesh())); + palm.attachChild(new Geometry("leaves", leafAcc.toMesh())); return palm; } // ── Bark mesh: trunk only ───────────────────────────────────────────────── - private static Mesh buildBarkMesh(PalmOptions opts) { - Accum acc = new Accum(); - addTrunk(acc, opts); - return acc.toMesh(); - } - - private static void addTrunk(Accum acc, PalmOptions opts) { - int M = opts.trunkSections; + private static void addTrunk(Accum acc, PalmOptions opts, + float[] cx, float[] cz, int M, float stepH) { int N = opts.trunkSegments; float H = opts.trunkHeight; float r0 = opts.trunkRadiusBottom; @@ -53,14 +126,14 @@ public class PalmMeshBuilder { for (int i = 0; i <= M; i++) { float t = (float) i / M; float r = r0 + (r1 - r0) * t; - float y = H * t; - float wind = t * 0.4f; // trunk barely sways at root, moderate at crown + float y = stepH * i; + float wind = t * 0.4f; for (int j = 0; j <= N; j++) { float angle = FastMath.TWO_PI * j / N; float nx = FastMath.cos(angle); float nz = FastMath.sin(angle); - acc.add(nx * r, y, nz * r, nx, 0f, nz, (float) j / N, t, wind); + acc.add(cx[i] + nx * r, y, cz[i] + nz * r, nx, 0f, nz, (float) j / N, t, wind); } } @@ -74,89 +147,168 @@ public class PalmMeshBuilder { } } - // ── Leaf mesh: horizontal bilateral leaflets (Wedel) ───────────────────── + // ── Kronenschaft: zuerst breiter, dann oben enger (Bauchprofil) ────────── - private static Mesh buildLeafMesh(PalmOptions opts, float[] azimuths, float[] elevations) { - Accum acc = new Accum(); - for (int f = 0; f < opts.frondCount; f++) { - addFrondLeaflets(acc, opts, azimuths[f], elevations[f]); + private static void addCrown(Accum acc, PalmOptions opts, float topX, float topZ) { + int N = opts.trunkSegments; + int M = 8; + float yBot = opts.trunkHeight; + float yTop = opts.trunkHeight + opts.crownHeight; + float rBot = opts.trunkRadiusTop; // Übergang zum Stamm (unten) + float rMax = opts.trunkRadiusTop * opts.crownFlare; // Bauch + float rTop = opts.trunkRadiusTop * 0.55f; // Spitze oben (deutlich schmaler) + int base = acc.vertexCount; + + for (int i = 0; i <= M; i++) { + float t = (float) i / M; + float y = yBot + (yTop - yBot) * t; + float r = crownRadius(t, rBot, rMax, rTop); + float wind = t * 0.12f; + for (int j = 0; j <= N; j++) { + float angle = FastMath.TWO_PI * j / N; + float nx = FastMath.cos(angle); + float nz = FastMath.sin(angle); + acc.add(topX + nx * r, y, topZ + nz * r, nx, 0f, nz, (float) j / N, t, wind); + } + } + for (int i = 0; i < M; i++) { + for (int j = 0; j < N; j++) { + int v00 = base + i * (N + 1) + j; + int v10 = base + (i + 1) * (N + 1) + j; + int v01 = base + i * (N + 1) + j + 1; + int v11 = base + (i + 1) * (N + 1) + j + 1; + acc.tri(v00, v10, v11); + acc.tri(v00, v11, v01); + } } - return acc.toMesh(); } - private static void addFrondLeaflets(Accum acc, PalmOptions opts, float azimuth, float elevation) { + /** Bauchprofil: rBot unten → rMax bei ~40 % → rTop oben, sine-geglättet. */ + private static float crownRadius(float t, float rBot, float rMax, float rTop) { + float peakT = 0.40f; + if (t <= peakT) { + return rBot + (rMax - rBot) * FastMath.sin(FastMath.HALF_PI * t / peakT); + } else { + return rTop + (rMax - rTop) * FastMath.sin(FastMath.HALF_PI * (1f - t) / (1f - peakT)); + } + } + + // ── Leaf mesh: ein Quad pro Wedel (Textur = vollständiger Wedel) ────────── + + private static void addLeaves(Accum acc, PalmOptions opts, float[] azimuths, + float[] elevations, float[] yOffsets, float baseX, float baseZ) { + for (int f = 0; f < opts.frondCount; f++) { + addFrond(acc, opts, azimuths[f], elevations[f], yOffsets[f], baseX, baseZ); + } + } + + private static final int SEGS = 6; // Längsunterteilungen + private static final int COLS = 3; // Spalten: linke Kante, Mitte, rechte Kante + + private static void addFrond(Accum acc, PalmOptions opts, float azimuth, float elevation, + float yOffset, float baseX, float baseZ) { float sinE = FastMath.sin(elevation); float cosE = FastMath.cos(elevation); float cosA = FastMath.cos(azimuth); float sinA = FastMath.sin(azimuth); - float dx = sinE * cosA; // frond direction vector + float dx = sinE * cosA; float dy = cosE; float dz = sinE * sinA; - // Horizontal projection for leaflet orientation (keeps leaflets truly flat) float hLen = FastMath.sqrt(dx * dx + dz * dz); if (hLen < 1e-5f) hLen = 1e-5f; - float hx = dx / hLen; - float hz = dz / hLen; + float sx = -dz / hLen; + float sz = dx / hLen; - // Side direction perpendicular to frond in horizontal plane: - // (hx,0,hz) × (0,1,0) = (-hz, 0, hx) - float sx = -hz; - float sz = hx; + // Wedel starten an der Kronenspitze (XZ = Stammspitze + Lean-Offset) + float baseY = opts.trunkHeight + opts.crownHeight + yOffset; - int K = opts.frondLeafletPairs; - float step = opts.frondLength / K; - float halfW = step * 0.30f; // leaflet thickness along frond axis - float baseY = opts.trunkHeight; + // Größe abhängig vom Elevationswinkel: aufwärts = kleiner, hängend = volle Größe + float minRad = opts.frondAngleMin * FastMath.DEG_TO_RAD; + float maxRad = opts.frondAngleMax * FastMath.DEG_TO_RAD; + float droopT = (maxRad - minRad) > 1e-5f + ? FastMath.clamp((elevation - minRad) / (maxRad - minRad), 0f, 1f) + : 1f; + float sizeScale = opts.frondMinSizeRatio + (1f - opts.frondMinSizeRatio) * droopT; - // Tip width is a fixed fraction of base width — gives natural taper - float sizeBase = opts.frondWidth; - float sizeTip = opts.frondWidth * 0.18f; - - for (int k = 0; k < K; k++) { - float tPos = (float) k / Math.max(1, K - 1); // 0→1 along frond, starts at trunk - float leafSize = sizeBase + (sizeTip - sizeBase) * tPos; - - float ax = dx * opts.frondLength * tPos; - float ay = baseY + dy * opts.frondLength * tPos; - float az = dz * opts.frondLength * tPos; - - // Wind: 0 near trunk tip, more at frond tip; extra delta for outer leaflet edge - float windBase = tPos * 0.65f; // inner edge of leaflet - float windTip = tPos * 0.65f + 0.35f; // outer edge of leaflet (leaf tip) - - addLeafletQuad(acc, ax, ay, az, hx, hz, sx, sz, leafSize, halfW, +1f, windBase, windTip); - addLeafletQuad(acc, ax, ay, az, hx, hz, sx, sz, leafSize, halfW, -1f, windBase, windTip); - } - } - - private static void addLeafletQuad(Accum acc, - float ax, float ay, float az, - float hx, float hz, - float sx, float sz, - float leafSize, float halfW, float side, - float windInner, float windOuter) { - // All 4 vertices at Y = ay (truly horizontal, normal = +Y) - float p0x = ax - hx * halfW, p0z = az - hz * halfW; - float p1x = ax + hx * halfW, p1z = az + hz * halfW; - float p2x = p1x + sx * side * leafSize, p2z = p1z + sz * side * leafSize; - float p3x = p0x + sx * side * leafSize, p3z = p0z + sz * side * leafSize; + float scaledLength = opts.frondLength * sizeScale; + float halfW = opts.leafTextureAspect > 0f + ? scaledLength * opts.leafTextureAspect * 0.5f + : opts.frondWidth * sizeScale * 0.5f; + boolean stemLeft = opts.leafTexture != null && opts.leafTexture.contains("palm2"); + float g = opts.gravity; int base = acc.vertexCount; - // p0, p1 = inner edge (attached to frond), p2, p3 = outer tip - acc.add(p0x, ay, p0z, 0f, 1f, 0f, 0f, 0f, windInner); - acc.add(p1x, ay, p1z, 0f, 1f, 0f, 1f, 0f, windInner); - acc.add(p2x, ay, p2z, 0f, 1f, 0f, 1f, 1f, windOuter); - acc.add(p3x, ay, p3z, 0f, 1f, 0f, 0f, 1f, windOuter); + // 7 Reihen × 3 Spalten + for (int row = 0; row <= SEGS; row++) { + float t = (float) row / SEGS; // 0 = Stamm, 1 = Spitze - acc.tri(base, base + 1, base + 2); - acc.tri(base, base + 2, base + 3); + // Position entlang Frond-Achse (Startpunkt = Lean-Offset der Stammspitze) + float fx = baseX + dx * scaledLength * t; + float fy = baseY + dy * scaledLength * t; + float fz = baseZ + dz * scaledLength * t; + + // Längsdurchhang: quadratisch, wächst zur Spitze + float droopLen = g * t * t * scaledLength; + + for (int col = 0; col < COLS; col++) { + float s = (float) col / (COLS - 1); // 0 = links, 1 = rechts + float side = s * 2f - 1f; // -1, 0, +1 + + // Seitenkanten hängen zusätzlich durch (nimmt zur Spitze zu) + float droopSide = g * FastMath.abs(side) * halfW * t; + + // Breite konvergiert zur Stammspitze: bei t=0 alle Vertices im Zentrum + float x = fx + sx * halfW * side * t; + float y = fy - droopLen - droopSide; + float z = fz + sz * halfW * side * t; + + float u = stemLeft ? t : s; + float v = stemLeft ? s : t; + + acc.add(x, y, z, 0f, 1f, 0f, u, v, t * 0.8f); + } + } + + // Dreiecke für 6×2 Zellen + for (int row = 0; row < SEGS; row++) { + for (int col = 0; col < COLS - 1; col++) { + int v00 = base + row * COLS + col; + int v10 = base + (row + 1) * COLS + col; + int v01 = base + row * COLS + col + 1; + int v11 = base + (row + 1) * COLS + col + 1; + acc.tri(v00, v10, v11); + acc.tri(v00, v11, v01); + } + } } // ── Random frond placement ──────────────────────────────────────────────── + /** + * Vertikaler Versatz: Minimum (steil aufwärts) = Stammspitze (Offset 0), + * höhere Werte (stärker hängend) = unterhalb der Stammspitze (negativer Offset). + */ + private static float[] computeYOffsets(PalmOptions opts, float[] elevations, Random rng) { + float minRad = opts.frondAngleMin * FastMath.DEG_TO_RAD; + float maxRad = opts.frondAngleMax * FastMath.DEG_TO_RAD; + float maxDrop = opts.crownHeight * 0.6f; // Wedel bleiben innerhalb des Kronenschafts + float[] result = new float[opts.frondCount]; + for (int f = 0; f < opts.frondCount; f++) { + float elevNorm = (maxRad - minRad) > 1e-5f + ? FastMath.clamp((elevations[f] - minRad) / (maxRad - minRad), 0f, 1f) + : 0.5f; + // elevNorm 0 = aufwärts → Offset 0 (Stammspitze) + // elevNorm 1 = hängend → Offset -maxDrop (unterhalb der Stammspitze) + float base = -elevNorm * maxDrop; + float noise = (rng.nextFloat() - 0.5f) * opts.trunkRadiusTop * 0.5f; + result[f] = Math.min(0f, base + noise); // nie über Stammspitze hinaus + } + return result; + } + private static float[] computeAzimuths(PalmOptions opts, Random rng) { float[] a = new float[opts.frondCount]; for (int f = 0; f < opts.frondCount; f++) { 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 90734f3..27d2cca 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 @@ -7,25 +7,36 @@ public class PalmOptions { // Trunk public float trunkHeight = 12f; public float trunkRadiusBottom = 0.35f; - public float trunkRadiusTop = 0.30f; // tapers but stays thick + public float trunkRadiusTop = 0.175f; // default = trunkRadiusBottom / 2 public int trunkSections = 10; public int trunkSegments = 8; + public float lean = 15f; // max. Neigungswinkel in Grad (0 = gerade, 50 = stark) + public int palmCount = 1; // 1–3 Stämme vom gleichen Ursprung // Fronds - public int frondCount = 10; - public float frondAngleMin = 70f; // degrees from vertical (Y-up) - public float frondAngleMax = 110f; - public float frondLength = 6.5f; - public int frondLeafletPairs = 8; - public float frondWidth = 1.4f; // max leaflet width at frond base + public int frondCount = 80; + public float frondAngleMin = 45f; // degrees from vertical (Y-up) + public float frondAngleMax = 105f; + public float frondLength = 6.5f; + public float frondWidth = 1.4f; + public float frondMinSizeRatio = 0.4f; // Größe der aufwärtszeigenden Wedel relativ zu den hängenden (0–1) // Colors public float barkR = 0.68f, barkG = 0.54f, barkB = 0.34f; public float leafR = 0.22f, leafG = 0.65f, leafB = 0.14f; // Textures - public String barkTexture = "Textures/bark/Bark008_Color.jpg"; - public String leafTexture = "Textures/leaves/palm.png"; + public String barkTexture = "Textures/bark/Bark_Palm.png"; + public String leafTexture = "Textures/leaves/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 + + // Kronenschaft + 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/leaves/palmcrown.png"; + public PalmOptions copy() { PalmOptions c = new PalmOptions(); @@ -35,16 +46,24 @@ public class PalmOptions { c.trunkRadiusTop = trunkRadiusTop; c.trunkSections = trunkSections; c.trunkSegments = trunkSegments; + c.lean = lean; + c.palmCount = palmCount; c.frondCount = frondCount; c.frondAngleMin = frondAngleMin; c.frondAngleMax = frondAngleMax; c.frondLength = frondLength; - c.frondLeafletPairs = frondLeafletPairs; c.frondWidth = frondWidth; + c.frondMinSizeRatio = frondMinSizeRatio; c.barkR = barkR; c.barkG = barkG; c.barkB = barkB; c.leafR = leafR; c.leafG = leafG; c.leafB = leafB; - c.barkTexture = barkTexture; - c.leafTexture = leafTexture; + c.barkTexture = barkTexture; + c.leafTexture = leafTexture; + c.leafTextureAspect = leafTextureAspect; + c.gravity = gravity; + c.crownHeight = crownHeight; + c.crownFlare = crownFlare; + c.crownR = crownR; c.crownG = crownG; c.crownB = crownB; + c.crownTexture = crownTexture; return c; } } diff --git a/blight-game/build.gradle b/blight-game/build.gradle index b78f1b1..05265f5 100644 --- a/blight-game/build.gradle +++ b/blight-game/build.gradle @@ -13,7 +13,7 @@ application { } ext { - jmeVersion = '3.7.0-stable' + jmeVersion = '3.9.0-stable' } dependencies { diff --git a/build.gradle b/build.gradle index efb7ad4..11cee9f 100644 --- a/build.gradle +++ b/build.gradle @@ -9,8 +9,8 @@ subprojects { apply plugin: 'java' java { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_26 + targetCompatibility = JavaVersion.VERSION_26 } compileJava.options.encoding = 'UTF-8'