diff --git a/blight-assets/src/main/resources/.thumbnails/Models/trees/willow/willow_20260823_101850.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/trees/willow/willow_20260823_101850.j3o.thumb.png new file mode 100644 index 0000000..f66e1d4 Binary files /dev/null and b/blight-assets/src/main/resources/.thumbnails/Models/trees/willow/willow_20260823_101850.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/.thumbnails/Models/trees/willow/willow_20260823_101856.j3o.thumb.png b/blight-assets/src/main/resources/.thumbnails/Models/trees/willow/willow_20260823_101856.j3o.thumb.png new file mode 100644 index 0000000..301c955 Binary files /dev/null and b/blight-assets/src/main/resources/.thumbnails/Models/trees/willow/willow_20260823_101856.j3o.thumb.png differ diff --git a/blight-assets/src/main/resources/Models/trees/willow/willow_20260823_101850.j3o b/blight-assets/src/main/resources/Models/trees/willow/willow_20260823_101850.j3o new file mode 100644 index 0000000..0cf2b55 Binary files /dev/null and b/blight-assets/src/main/resources/Models/trees/willow/willow_20260823_101850.j3o differ diff --git a/blight-assets/src/main/resources/Models/trees/willow/willow_20260823_101856.j3o b/blight-assets/src/main/resources/Models/trees/willow/willow_20260823_101856.j3o new file mode 100644 index 0000000..4a3489a Binary files /dev/null and b/blight-assets/src/main/resources/Models/trees/willow/willow_20260823_101856.j3o differ diff --git a/blight-assets/src/main/resources/Shaders/Tree.vert b/blight-assets/src/main/resources/Shaders/Tree.vert index fbb460c..47b4280 100644 --- a/blight-assets/src/main/resources/Shaders/Tree.vert +++ b/blight-assets/src/main/resources/Shaders/Tree.vert @@ -25,9 +25,9 @@ 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); - // 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); + // Objekt-Ursprung als Hash-Basis: alle Vertices desselben Baums bekommen identische + // randPhase → kein Phasensprung innerhalb eines Blattstreifens → kein Zickzack + vec2 hashPos = floor(vec2(g_WorldMatrix[3][0], g_WorldMatrix[3][2])); 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; 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 50b7c67..1c7a4b7 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -151,6 +151,9 @@ public class EditorApp extends Application { // Weinpflanzen-Generator-Zustand private de.blight.editor.tree.GrapevineOptions grapevineOptions = new de.blight.editor.tree.GrapevineOptions(); + // Trauerweide-Generator-Zustand + private de.blight.editor.tree.WillowOptions willowOptions = new de.blight.editor.tree.WillowOptions(); + // Vegetations-Generator-Zustand private String vegetationType = "Baum (Eiche)"; @@ -1208,11 +1211,12 @@ public class EditorApp extends Application { ComboBox typeBox = new ComboBox<>(); typeBox.getItems().addAll( "Baum (Eiche)", "Baum (Birke)", "Baum (Kiefer)", "Baum (Weide)", "Baum (Busch)", + "Baum (Trauerweide)", "Farn", "Palme", "Fruchtbusch", "Weinpflanze"); typeBox.setValue(vegetationType); typeBox.setOnAction(e -> { vegetationType = typeBox.getValue(); - if (vegetationType.startsWith("Baum (")) { + if (vegetationType.startsWith("Baum (") && !"Baum (Trauerweide)".equals(vegetationType)) { currentTreePreset = vegetationType.substring(6, vegetationType.length() - 1); treeParams = presetFromName(currentTreePreset); } @@ -1227,7 +1231,7 @@ public class EditorApp extends Application { Button exportBtn = new Button("💾 Export .j3o"); exportBtn.setOnAction(e -> { - if (vegetationType.startsWith("Baum (")) { + if (vegetationType.startsWith("Baum (") && !"Baum (Trauerweide)".equals(vegetationType)) { input.treeGenQueue.offer(new SharedInput.TreeGenRequest( treeParams.copy(), true, treeTypeFromPreset(currentTreePreset))); } else if ("Farn".equals(vegetationType)) { @@ -1236,6 +1240,8 @@ public class EditorApp extends Application { 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 if ("Baum (Trauerweide)".equals(vegetationType)) { + input.willowGenQueue.offer(new SharedInput.WillowGenRequest(willowOptions.copy(), true)); } else { input.palmGenQueue.offer(new SharedInput.PalmGenRequest(palmOptions.copy(), true)); } @@ -1256,7 +1262,18 @@ public class EditorApp extends Application { } private void updateVegetationOnF5() { - if (vegetationType.startsWith("Baum (")) { + if ("Baum (Trauerweide)".equals(vegetationType)) { + onF5 = () -> { + input.willowGenQueue.offer( + new SharedInput.WillowGenRequest(willowOptions.copy(), false)); + setStatus("Trauerweide: generiere Vorschau…"); + }; + onF6 = () -> { + willowOptions.seed = new java.util.Random().nextInt(1000000); + root.setRight(buildVegetationParamsPanel()); + onF5.run(); + }; + } else if (vegetationType.startsWith("Baum (")) { onF5 = () -> { input.treeGenQueue.offer(new SharedInput.TreeGenRequest( treeParams.copy(), false, treeTypeFromPreset(currentTreePreset))); @@ -1313,7 +1330,9 @@ public class EditorApp extends Application { } private javafx.scene.Node buildVegetationParamsPanel() { - if (vegetationType.startsWith("Baum (")) { + if ("Baum (Trauerweide)".equals(vegetationType)) { + return buildWillowParamsPanel(); + } else if (vegetationType.startsWith("Baum (")) { return buildTreeParamsPanel(); } else if ("Farn".equals(vegetationType)) { return buildFernParamsPanel(); @@ -1797,9 +1816,18 @@ public class EditorApp extends Application { } }); pane.setOnScroll(e -> { + if (e.getDeltaY() == 0) return; float factor = e.getDeltaY() > 0 ? 0.88f : 1.14f; input.treePreviewZoom = (float) Math.max(0.25, Math.min(4.0, input.treePreviewZoom * factor)); + e.consume(); + }); + pane.setFocusTraversable(true); + pane.setOnMouseClicked(e -> pane.requestFocus()); + pane.setOnKeyPressed(e -> { + if (e.getCode() == javafx.scene.input.KeyCode.W) { + input.treePreviewWireframe = !input.treePreviewWireframe; + } }); plantPreviewPanel = pane; @@ -2201,6 +2229,127 @@ public class EditorApp extends Application { return panel; } + // ── Trauerweide-Generator – Parameter-Panel ────────────────────────────── + + private VBox buildWillowParamsPanel() { + VBox inner = new VBox(6); + inner.setPadding(new Insets(10)); + + inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator()); + inner.getChildren().add(bold("Zufallssamen:")); + Spinner seedSp = intSpinner(0, 999999, willowOptions.seed); + seedSp.valueProperty().addListener((o, a, b) -> willowOptions.seed = b); + Button rndSeed = new Button("🎲"); + onF6 = () -> { + int s = new java.util.Random().nextInt(1000000); + willowOptions.seed = s; + seedSp.getValueFactory().setValue(s); + onF5.run(); + }; + rndSeed.setOnAction(e -> onF6.run()); + HBox seedRow = new HBox(4, seedSp, rndSeed); + HBox.setHgrow(seedSp, Priority.ALWAYS); + inner.getChildren().add(seedRow); + + inner.getChildren().addAll(sectionTitle("Stamm"), new Separator()); + inner.getChildren().add(ezFloat("Höhe:", 4, 25, willowOptions.trunkHeight, + v -> willowOptions.trunkHeight = v)); + inner.getChildren().add(ezFloat("Radius:", 0.1, 1.0, willowOptions.trunkRadius, + v -> willowOptions.trunkRadius = v)); + + inner.getChildren().addAll(sectionTitle("Hauptäste"), new Separator()); + inner.getChildren().add(bold("Anzahl Äste:")); + Spinner branchCountSp = intSpinner(3, 16, willowOptions.branchCount); + branchCountSp.valueProperty().addListener((o, a, b) -> willowOptions.branchCount = b); + inner.getChildren().add(branchCountSp); + inner.getChildren().add(ezFloat("Startwinkel (°):", 10, 80, willowOptions.branchStartAngle, + v -> willowOptions.branchStartAngle = v)); + inner.getChildren().add(ezFloat("Endwinkel / Hänge (°):", 90, 170, willowOptions.branchEndAngle, + v -> willowOptions.branchEndAngle = v)); + inner.getChildren().add(ezFloat("Winkel-Varianz (°):", 0, 40, willowOptions.branchVariance, + v -> willowOptions.branchVariance = v)); + inner.getChildren().add(ezFloat("Länge:", 3, 18, willowOptions.branchLength, + v -> willowOptions.branchLength = v)); + inner.getChildren().add(ezFloat("Radius:", 0.05, 0.5, willowOptions.branchRadius, + v -> willowOptions.branchRadius = v)); + + inner.getChildren().addAll(sectionTitle("Hängeäste"), new Separator()); + inner.getChildren().add(bold("Anzahl pro Sektion:")); + Spinner subCountSp = intSpinner(1, 8, willowOptions.subBranchCount); + subCountSp.valueProperty().addListener((o, a, b) -> willowOptions.subBranchCount = b); + inner.getChildren().add(subCountSp); + inner.getChildren().add(ezFloat("Länge:", 1, 10, willowOptions.subBranchLength, + v -> willowOptions.subBranchLength = v)); + inner.getChildren().add(ezFloat("Startwinkel (Grad):", 10, 90, willowOptions.subBranchStartAngle, + v -> willowOptions.subBranchStartAngle = v)); + inner.getChildren().add(ezFloat("Endwinkel (Grad):", 91, 180, willowOptions.subBranchEndAngle, + v -> willowOptions.subBranchEndAngle = v)); + + inner.getChildren().addAll(sectionTitle("Blätter"), new Separator()); + inner.getChildren().add(ezFloat("Größe:", 0.3, 3.0, willowOptions.leafScale, + v -> willowOptions.leafScale = v)); + inner.getChildren().add(bold("Anzahl pro Cluster:")); + Spinner leafCountSp = intSpinner(1, 12, willowOptions.leafCount); + leafCountSp.valueProperty().addListener((o, a, b) -> willowOptions.leafCount = b); + inner.getChildren().add(leafCountSp); + inner.getChildren().add(ezFloat("Blatt-Hänge:", 0.0, 1.0, willowOptions.leafDroop, + v -> willowOptions.leafDroop = v)); + inner.getChildren().add(ezFloat("Blatt-Startwinkel min (°):", 0, 89, willowOptions.leafAngleMin, + v -> willowOptions.leafAngleMin = v)); + inner.getChildren().add(ezFloat("Blatt-Startwinkel max (°):", 1, 90, willowOptions.leafAngleMax, + v -> willowOptions.leafAngleMax = v)); + + inner.getChildren().addAll(sectionTitle("Farben & Texturen"), new Separator()); + + Label leafTexLabel = new Label("Blatt-Textur:"); + leafTexLabel.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;"); + ComboBox leafTexBox = new ComboBox<>(); + leafTexBox.getItems().addAll("weeping_willow.png", "ash.png", "oak.png"); + String curLeaf = willowOptions.leafTexture != null + ? willowOptions.leafTexture.substring(willowOptions.leafTexture.lastIndexOf('/') + 1) + : "weeping_willow.png"; + leafTexBox.setValue(leafTexBox.getItems().contains(curLeaf) ? curLeaf : "weeping_willow.png"); + leafTexBox.setMaxWidth(Double.MAX_VALUE); + leafTexBox.setOnAction(e -> willowOptions.leafTexture = + "Textures/internal/foliage/" + leafTexBox.getValue()); + inner.getChildren().add(new VBox(2, leafTexLabel, leafTexBox)); + + Label barkTexLabel = new Label("Rinden-Textur:"); + barkTexLabel.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;"); + ComboBox barkTexBox = new ComboBox<>(); + barkTexBox.getItems().addAll( + "Bark001_Color.jpg", "Bark002_Color.jpg", "Bark003_Color.jpg", "Bark008_Color.jpg"); + String curBark = willowOptions.barkTexture != null + ? willowOptions.barkTexture.substring(willowOptions.barkTexture.lastIndexOf('/') + 1) + : "Bark001_Color.jpg"; + barkTexBox.setValue(barkTexBox.getItems().contains(curBark) ? curBark : "Bark001_Color.jpg"); + barkTexBox.setMaxWidth(Double.MAX_VALUE); + barkTexBox.setOnAction(e -> { + if (barkTexBox.getValue() != null) { + willowOptions.barkTexture = "Textures/internal/bark/" + barkTexBox.getValue(); + } + }); + inner.getChildren().add(new VBox(2, barkTexLabel, barkTexBox)); + + inner.getChildren().add(ezFloat("Rinde R:", 0, 1, willowOptions.barkR, v -> willowOptions.barkR = v)); + inner.getChildren().add(ezFloat("Rinde G:", 0, 1, willowOptions.barkG, v -> willowOptions.barkG = v)); + inner.getChildren().add(ezFloat("Rinde B:", 0, 1, willowOptions.barkB, v -> willowOptions.barkB = v)); + inner.getChildren().add(ezFloat("Blatt R:", 0, 1, willowOptions.leafR, v -> willowOptions.leafR = v)); + inner.getChildren().add(ezFloat("Blatt G:", 0, 1, willowOptions.leafG, v -> willowOptions.leafG = v)); + inner.getChildren().add(ezFloat("Blatt B:", 0, 1, willowOptions.leafB, v -> willowOptions.leafB = v)); + + 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; + } + // ── Palmen-Generator – Parameter-Panel ────────────────────────────────── private VBox buildPalmParamsPanel() { 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 a78bd81..9b73a79 100644 --- a/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/JmeEditorApp.java @@ -34,6 +34,7 @@ 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.WillowGeneratorState; import de.blight.editor.state.SceneObjectState; import de.blight.editor.state.TerrainEditorState; import de.blight.editor.state.TreeGeneratorState; @@ -200,6 +201,7 @@ public class JmeEditorApp extends SimpleApplication { stateManager.attach(new FernGeneratorState(input)); stateManager.attach(new FruitBushGeneratorState(input)); stateManager.attach(new GrapevineGeneratorState(input)); + stateManager.attach(new WillowGeneratorState(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/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index 3563446..e799785 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -13,6 +13,7 @@ import de.blight.editor.tool.SculptMeshTool; import de.blight.editor.tool.VoxelTool; import de.blight.editor.tree.PalmOptions; import de.blight.editor.tree.TreeParams; +import de.blight.editor.tree.WillowOptions; import javafx.scene.image.WritableImage; import java.util.concurrent.ConcurrentLinkedQueue; @@ -232,6 +233,8 @@ public class SharedInput { */ public volatile WritableImage treePreviewImage = new WritableImage(1024, 1024); public volatile boolean treePreviewResized = false; + /** W-Taste in der Vorschau toggelt Wireframe-Debug. */ + public volatile boolean treePreviewWireframe = false; // ── Baum-Generator ─────────────────────────────────────────────────────── public record TreeGenRequest(TreeParams params, boolean exportAfter, String treeType) {} @@ -245,6 +248,10 @@ public class SharedInput { public record PalmGenRequest(PalmOptions options, boolean exportAfter) {} public final ConcurrentLinkedQueue palmGenQueue = new ConcurrentLinkedQueue<>(); + // ── Trauerweide-Generator ───────────────────────────────────────────────── + public record WillowGenRequest(WillowOptions options, boolean exportAfter) {} + public final ConcurrentLinkedQueue willowGenQueue = new ConcurrentLinkedQueue<>(); + // ── Objekt-Werkzeug ────────────────────────────────────────────────────── /** activeLayer==5 → Objekte platzieren */ public static final int LAYER_OBJECTS = 5; 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 fef1780..fd47458 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 @@ -96,6 +96,7 @@ public class TreeGeneratorState extends BaseAppState { private FrameBuffer captureFB = null; private Texture2D captureTex = null; private boolean[] captureReady = new boolean[]{false}; + private boolean lastWireframe = false; private int capturePass = 0; private ByteBuffer[] capturePixels = new ByteBuffer[ImpostorUtil.DIRS]; @@ -205,7 +206,18 @@ public class TreeGeneratorState extends BaseAppState { resizePreviewViewport(reqW, reqH); } - // 3. Kamera-Orbit – updateGeometricState wird jetzt per preFrame-SceneProcessor + // 3. Wireframe-Debug-Toggle (W-Taste in der Vorschau) + boolean wf = input.treePreviewWireframe; + if (wf != lastWireframe) { + lastWireframe = wf; + previewTreeHolder.depthFirstTraversal(s -> { + if (s instanceof com.jme3.scene.Geometry g && g.getMaterial() != null) { + g.getMaterial().getAdditionalRenderState().setWireframe(wf); + } + }); + } + + // 4. Kamera-Orbit – updateGeometricState wird jetzt per preFrame-SceneProcessor // direkt vor dem Rendern des previewVP aufgerufen (nach allen State-Updates). if (previewVP != null) { float rotY = input.treePreviewRotY * FastMath.DEG_TO_RAD; diff --git a/blight-editor/src/main/java/de/blight/editor/state/WillowGeneratorState.java b/blight-editor/src/main/java/de/blight/editor/state/WillowGeneratorState.java new file mode 100644 index 0000000..588373f --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/state/WillowGeneratorState.java @@ -0,0 +1,479 @@ +package de.blight.editor.state; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +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.light.AmbientLight; +import com.jme3.light.DirectionalLight; +import com.jme3.material.Material; +import com.jme3.material.RenderState; +import com.jme3.math.ColorRGBA; +import com.jme3.math.FastMath; +import com.jme3.math.Vector3f; +import com.jme3.post.SceneProcessor; +import com.jme3.profile.AppProfiler; +import com.jme3.renderer.Camera; +import com.jme3.renderer.RenderManager; +import com.jme3.renderer.ViewPort; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.scene.VertexBuffer; +import com.jme3.scene.control.AbstractControl; +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; +import de.blight.editor.tree.WillowMeshBuilder; +import de.blight.editor.tree.WillowOptions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WillowGeneratorState extends BaseAppState { + + private static final Logger log = LoggerFactory.getLogger(WillowGeneratorState.class); + + private static final int IMPOSTOR_SIZE = 512; + private static final int ATLAS_DIRS = 4; + private static final int ATLAS_W = IMPOSTOR_SIZE * ATLAS_DIRS; + private static final int ATLAS_H = IMPOSTOR_SIZE; + 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; + + // ── Capture-Kontext ─────────────────────────────────────────────────────── + private SharedInput.WillowGenRequest pendingRequest = null; + private Node pendingLod0 = null; + private Node pendingLod1 = null; + private BoundingBox pendingBb = null; + private String pendingFileName = null; + private ViewPort captureVP = null; + private FrameBuffer captureFB = null; + private volatile boolean captureReady = false; + private int capturePass = 0; + private ByteBuffer[] capturePixels = new ByteBuffer[ATLAS_DIRS]; + + public WillowGeneratorState(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; + } + + if (pendingRequest != null && captureReady) { + finishCapture(); + return; + } + + if (pendingRequest != null) return; + + SharedInput.WillowGenRequest req = input.willowGenQueue.poll(); + if (req != null) startGeneration(req); + } + + // ── Phase 1: Generierung ────────────────────────────────────────────────── + + private void startGeneration(SharedInput.WillowGenRequest req) { + cleanupCapture(); + + WillowOptions opts = req.options(); + Node lod0 = WillowMeshBuilder.build(opts); + applyMaterials(lod0, opts); + lod0.updateGeometricState(); + + BoundingBox bb = lod0.getWorldBound() instanceof BoundingBox b ? b : null; + if (bb == null) bb = new BoundingBox(Vector3f.ZERO, 5f, 8f, 5f); + + float dist = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3f; + Vector3f target = new Vector3f(0f, bb.getCenter().y, 0f); + + previewHost.setPreviewContent(lod0, dist, target); + + String timestamp = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now()); + + pendingRequest = req; + pendingLod0 = lod0; + pendingLod1 = buildLod1(opts); + pendingBb = bb; + pendingFileName = "willow_" + timestamp; + capturePass = 0; + capturePixels = new ByteBuffer[ATLAS_DIRS]; + + startCapturePass(0); + input.treeGenStatusMsg = "Trauerweide: Rendere Impostor (1/" + ATLAS_DIRS + ")…"; + } + + private Node buildLod1(WillowOptions opts) { + WillowOptions ld = opts.copy(); + ld.trunkSegments = Math.max(4, opts.trunkSegments / 2); + ld.trunkSections = Math.max(4, opts.trunkSections / 2); + ld.branchSections = Math.max(4, opts.branchSections / 2); + ld.subBranchSections = Math.max(3, opts.subBranchSections / 2); + ld.subBranchCount = Math.max(1, opts.subBranchCount / 2); + Node n = WillowMeshBuilder.build(ld); + applyMaterials(n, ld); + return n; + } + + // ── Phase 2: Capture ───────────────────────────────────────────────────── + + @SuppressWarnings("deprecation") + private void startCapturePass(int pass) { + Texture2D capTex = new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.RGBA8); + captureFB = new FrameBuffer(IMPOSTOR_SIZE, IMPOSTOR_SIZE, 1); + captureFB.addColorTexture(capTex); + captureFB.setDepthTexture(new Texture2D(IMPOSTOR_SIZE, IMPOSTOR_SIZE, Image.Format.Depth)); + float angle = pass * FastMath.HALF_PI; + captureVP = buildCaptureViewPort(pendingLod0, pendingBb, captureFB, angle); + captureReady = false; + } + + private void finishCapture() { + ByteBuffer pixels = BufferUtils.createByteBuffer(IMPOSTOR_SIZE * IMPOSTOR_SIZE * 4); + app.getRenderer().readFrameBuffer(captureFB, pixels); + capturePixels[capturePass] = pixels; + cleanupCapture(); + + if (capturePass < ATLAS_DIRS - 1) { + capturePass++; + input.treeGenStatusMsg = "Trauerweide: Rendere Impostor (" + (capturePass + 1) + "/" + ATLAS_DIRS + ")…"; + startCapturePass(capturePass); + return; + } + + String impostorName = "willow_impostor_" + pendingFileName.substring("willow_".length()); + ByteBuffer atlas = combineAtlas(capturePixels); + Texture2D impTex = saveImpostor(atlas, impostorName, ATLAS_W, ATLAS_H); + + if (pendingRequest.exportAfter()) { + Node lodNode = assembleLodNode(impTex); + exportWillow(lodNode, pendingFileName); + } else { + input.treeGenStatusMsg = "Trauerweide: Vorschau"; + } + + pendingRequest = null; + pendingLod0 = null; + pendingLod1 = null; + pendingBb = null; + pendingFileName = null; + capturePixels = new ByteBuffer[ATLAS_DIRS]; + } + + // ── LOD-Aufbau ──────────────────────────────────────────────────────────── + + private Node assembleLodNode(Texture2D impostorTex) { + Node root = new Node("willow"); + root.attachChild(pendingLod0); + root.attachChild(pendingLod1); + + Node lod2 = makeImpostorNode(pendingBb, impostorTex); + root.attachChild(lod2); + + pendingLod1.setCullHint(Spatial.CullHint.Always); + lod2.setCullHint(Spatial.CullHint.Always); + lod2.setShadowMode(RenderQueue.ShadowMode.Off); + + root.addControl(new WillowLodControl(app.getCamera(), + pendingLod0, pendingLod1, lod2, 50f, 150f)); + return root; + } + + private Node makeImpostorNode(BoundingBox bb, Texture2D tex) { + float h = bb.getYExtent() * 2f; + float w = Math.max(bb.getXExtent(), bb.getZExtent()) * 2f; + float size = Math.max(h, w); + float yOff = bb.getCenter().y + 2f; + + Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md"); + if (tex != null) mat.setTexture("ColorMap", tex); + else mat.setColor("Color", new ColorRGBA(0.22f, 0.62f, 0.14f, 0.9f)); + mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); + mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off); + + Node n = new Node("lod2"); + for (int d = 0; d < ATLAS_DIRS; d++) { + float uMin = (float) d / ATLAS_DIRS; + float uMax = (float)(d + 1) / ATLAS_DIRS; + n.attachChild(buildBillboardQuad("quad_" + d, d * FastMath.HALF_PI, + yOff, size, mat.clone(), uMin, uMax)); + } + n.setQueueBucket(RenderQueue.Bucket.Transparent); + return n; + } + + private Geometry buildBillboardQuad(String name, float yRot, float yCent, + float size, Material mat, float uMin, float uMax) { + float hw = size * 0.5f; + float hh = size * 0.5f; + float cos = FastMath.cos(yRot); + float sin = FastMath.sin(yRot); + + Mesh mesh = new Mesh(); + mesh.setBuffer(VertexBuffer.Type.Position, 3, new float[]{ + -hw*cos, yCent-hh, -hw*sin, + hw*cos, yCent-hh, hw*sin, + hw*cos, yCent+hh, hw*sin, + -hw*cos, yCent+hh, -hw*sin + }); + mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, new float[]{ + uMin, 0, uMax, 0, uMax, 1, uMin, 1 + }); + mesh.setBuffer(VertexBuffer.Type.Index, 3, new int[]{0,1,2, 0,2,3, 2,1,0, 3,2,0}); + mesh.updateBound(); + + Geometry g = new Geometry(name, mesh); + g.setMaterial(mat); + return g; + } + + // ── Offscreen-ViewPort ──────────────────────────────────────────────────── + + private ViewPort buildCaptureViewPort(Node willowNode, BoundingBox bb, FrameBuffer fb, float angle) { + Camera cam = new Camera(IMPOSTOR_SIZE, IMPOSTOR_SIZE); + Vector3f center = bb.getCenter().add(0f, 2f, 0f); + float extent = Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())); + float dist = extent * 3.0f; + + float camX = FastMath.sin(angle) * dist; + float camZ = FastMath.cos(angle) * dist; + cam.setLocation(center.add(camX, 0f, camZ)); + cam.lookAt(center, Vector3f.UNIT_Y); + cam.setFrustumPerspective(35f, 1f, 0.1f, dist * 4f); + + ViewPort vp = app.getRenderManager().createPostView("willowCap_" + System.nanoTime(), cam); + vp.setOutputFrameBuffer(fb); + vp.setBackgroundColor(new ColorRGBA(0f, 0f, 0f, 0f)); + vp.setClearFlags(true, true, true); + + Node scene = new Node("willowCapScene"); + scene.addLight(new DirectionalLight( + 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(willowNode)); + vp.attachScene(scene); + scene.updateGeometricState(); + + vp.addProcessor(new SceneProcessor() { + @Override public void initialize(RenderManager rm, ViewPort v) {} + @Override public void reshape(ViewPort v, int w, int h) {} + @Override public boolean isInitialized() { return true; } + @Override public void preFrame(float t) {} + @Override public void postQueue(RenderQueue rq) {} + @Override public void cleanup() {} + @Override public void setProfiler(AppProfiler profiler) {} + @Override public void postFrame(FrameBuffer out) { + vp.removeProcessor(this); + captureReady = true; + } + }); + + return vp; + } + + private Node cloneForCapture(Node src) { + Node copy = new Node(src.getName() + "_cap"); + for (Spatial child : src.getChildren()) { + if (child instanceof Geometry g) { + Geometry gc = new Geometry(g.getName() + "_c", g.getMesh()); + gc.setMaterial(g.getMaterial().clone()); + copy.attachChild(gc); + } + } + return copy; + } + + // ── Atlas kombinieren ───────────────────────────────────────────────────── + + private ByteBuffer combineAtlas(ByteBuffer[] passes) { + ByteBuffer atlas = BufferUtils.createByteBuffer(ATLAS_W * ATLAS_H * 4); + for (int d = 0; d < ATLAS_DIRS; d++) { + ByteBuffer src = passes[d]; + src.rewind(); + for (int y = 0; y < IMPOSTOR_SIZE; y++) { + for (int x = 0; x < IMPOSTOR_SIZE; x++) { + int srcOff = (y * IMPOSTOR_SIZE + x) * 4; + int dstOff = (y * ATLAS_W + d * IMPOSTOR_SIZE + x) * 4; + atlas.put(dstOff, src.get(srcOff)); + atlas.put(dstOff + 1, src.get(srcOff + 1)); + atlas.put(dstOff + 2, src.get(srcOff + 2)); + atlas.put(dstOff + 3, src.get(srcOff + 3)); + } + } + } + return atlas; + } + + private Texture2D saveImpostor(ByteBuffer pixels, String name, int width, int height) { + pixels.rewind(); + Image jmeImg = new Image(Image.Format.RGBA8, width, height, + pixels, null, com.jme3.texture.image.ColorSpace.sRGB); + return new Texture2D(jmeImg); + } + + // ── Export ──────────────────────────────────────────────────────────────── + + private void exportWillow(Node lodNode, String fileName) { + try { + Path modelDir = ASSET_ROOT.resolve("Models").resolve("trees").resolve("willow"); + Files.createDirectories(modelDir); + File out = modelDir.resolve(fileName + ".j3o").toFile(); + while (lodNode.getNumControls() > 0) + lodNode.removeControl(lodNode.getControl(0)); + byte[] thumb = null; + try { + thumb = ThumbnailRenderer.render(lodNode.clone(), app.getRenderManager(), app.getRenderer()); + if (thumb != null) ThumbnailRenderer.embed(lodNode, thumb); + } catch (Exception te) { log.warn("[Trauerweide] Thumbnail-Fehler: {}", te.getMessage()); } + BinaryExporter.getInstance().save(lodNode, out); + if (thumb != null) ThumbnailRenderer.saveSidecar(thumb, out.toPath(), ASSET_ROOT); + log.info("[Trauerweide] Gespeichert: {}", out.getAbsolutePath()); + input.treeGenStatusMsg = "Gespeichert: Models/trees/willow/" + fileName + ".j3o"; + input.refreshAssets = true; + } catch (IOException e) { + log.error("[Trauerweide] Export-Fehler: {}", e.getMessage()); + input.treeGenStatusMsg = "Trauerweide Export-Fehler: " + e.getMessage(); + } + } + + // ── Materialien ─────────────────────────────────────────────────────────── + + private void applyMaterials(Node willow, WillowOptions opts) { + for (Spatial child : willow.getChildren()) { + if (!(child instanceof Geometry g)) continue; + switch (g.getName()) { + case "bark" -> { + g.setMaterial(buildBarkMat(opts)); + g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); + } + case "leaves" -> { + g.setMaterial(buildLeafMat(opts)); + g.setQueueBucket(RenderQueue.Bucket.Transparent); + g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); + } + } + } + } + + private Material buildBarkMat(WillowOptions opts) { + try { + Material mat = new Material(assets, "MatDefs/Tree.j3md"); + mat.setColor("Diffuse", new ColorRGBA(opts.barkR, opts.barkG, opts.barkB, 1f)); + mat.setFloat("WindStrength", 0.06f); + mat.setFloat("WindSpeed", 0.35f); + 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); + 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(opts.barkR, opts.barkG, opts.barkB, 1f)); + return mat; + } + } + + private Material buildLeafMat(WillowOptions 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", 0.22f); + mat.setFloat("WindSpeed", 0.55f); + 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; + } + } + + // ── Aufräumen ───────────────────────────────────────────────────────────── + + private void cleanupCapture() { + if (captureVP != null) { + app.getRenderManager().removePostView(captureVP); + captureVP = null; + } + if (captureFB != null) { + try { captureFB.dispose(); } catch (Exception ignored) {} + captureFB = null; + } + captureReady = false; + } + + // ── LOD-Control ─────────────────────────────────────────────────────────── + + private static final class WillowLodControl extends AbstractControl { + private final Camera cam; + private final Node lod0, lod1, lod2; + private final float d01sq, d12sq; + + WillowLodControl(Camera cam, Node l0, Node l1, Node l2, float d01, float d12) { + this.cam = cam; + this.lod0 = l0; this.lod1 = l1; this.lod2 = l2; + this.d01sq = d01 * d01; + this.d12sq = d12 * d12; + } + + @Override + protected void controlUpdate(float tpf) { + float dSq = cam.getLocation().distanceSquared(spatial.getWorldTranslation()); + lod0.setCullHint(dSq < d01sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + lod1.setCullHint(dSq>=d01sq && dSq= d12sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always); + } + + @Override protected void controlRender(RenderManager rm, ViewPort vp) {} + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/tree/WillowMeshBuilder.java b/blight-editor/src/main/java/de/blight/editor/tree/WillowMeshBuilder.java new file mode 100644 index 0000000..1193068 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/tree/WillowMeshBuilder.java @@ -0,0 +1,564 @@ +package de.blight.editor.tree; + +import com.jme3.bounding.BoundingBox; +import com.jme3.math.FastMath; +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; + +/** + * Prozeduraler Trauerweide-Generator. + * + * Geometrie-Konzept: + * 1. Stamm: leicht gnarled, von unten nach oben. + * 2. Hauptäste: starten ab der halben Stammhöhe. Jeder Ast macht einen + * steuerbaren Bogen von branchStartAngle (steil, z.B. 45°) bis + * branchEndAngle (hängend, z.B. 135°). Die Varianzen der Winkel sind + * pro Ast individuell zufällig. + * 3. Hängeäste: an jedem Sektionspunkt der Hauptäste. Richtung zufällig, + * Neigung durch subBranchDroop gesteuert (0=waagrecht, 1=senkrecht). + * 4. Blätter: an den Hängeästen. leafDroop steuert, wie stark sie hängen. + * + * Das Color-Buffer-R-Kanal enthält den Wind-Gewichtsfaktor (0=statisch, 1=max). + */ +public class WillowMeshBuilder { + + public record MeshResult(Mesh bark, Mesh leaves, BoundingBox bounds) {} + + // ── Einstieg ────────────────────────────────────────────────────────────── + + public static Node build(WillowOptions o) { + Rng rng = new Rng(o.seed); + VertexCollector barkCol = new VertexCollector(); + VertexCollector leafCol = new VertexCollector(); + + Vector3f[] trunkPts = buildTrunk(barkCol, o, rng); + buildMainBranches(barkCol, leafCol, o, rng, trunkPts); + + Node node = new Node("willow"); + node.attachChild(toGeom("bark", barkCol.toMesh())); + node.attachChild(toGeom("leaves", leafCol.toMesh())); + return node; + } + + private static Geometry toGeom(String name, Mesh mesh) { + return new Geometry(name, mesh); + } + + // ── 1. Stamm ────────────────────────────────────────────────────────────── + + private static Vector3f[] buildTrunk(VertexCollector col, WillowOptions o, Rng rng) { + float segLen = o.trunkHeight / o.trunkSections; + int nPts = o.trunkSections + 1; + Vector3f[] pts = new Vector3f[nPts]; + float[] radii = new float[nPts]; + float[] winds = new float[nPts]; + + Vector3f pos = new Vector3f(0, 0, 0); + Vector3f dir = new Vector3f(0, 1, 0); + pts[0] = pos.clone(); + radii[0] = o.trunkRadius; + winds[0] = 0f; + + for (int s = 0; s < o.trunkSections; s++) { + float t0 = (float) s / o.trunkSections; + float t1 = (float)(s + 1) / o.trunkSections; + + float g = 0.06f + t0 * 0.04f; + dir.x += rng.range(-g, g); + dir.z += rng.range(-g, g); + dir.normalizeLocal(); + + pos = pos.add(dir.mult(segLen)); + pts[s + 1] = pos.clone(); + radii[s + 1] = o.trunkRadius * lerp(1f, 0.01f, t1 * t1); + winds[s + 1] = o.trunkFlexibility * t1; + } + buildTube(col, pts, radii, winds, o.trunkSegments); + return pts; + } + + // ── 2. Hauptäste ───────────────────────────────────────────────────────── + + private static void buildMainBranches(VertexCollector barkCol, VertexCollector leafCol, + WillowOptions o, Rng rng, Vector3f[] trunkPts) { + int n = o.branchCount; + float halfTrunk = o.trunkHeight * 0.5f; + + for (int i = 0; i < n; i++) { + // Azimut gleichmäßig verteilt + kleiner Zufall + float azimuth = i * FastMath.TWO_PI / n + rng.range(-0.5f, 0.5f); + float perpX = FastMath.sin(azimuth); + float perpZ = FastMath.cos(azimuth); + + // Individuelle Winkel-Varianz + float variance = rng.range(-o.branchVariance, o.branchVariance); + float startAngRad = (o.branchStartAngle + variance) * FastMath.DEG_TO_RAD; + float endAngRad = (o.branchEndAngle + variance) * FastMath.DEG_TO_RAD; + + // Startpunkt stratifiziert entlang der oberen Stammhälfte + float frac = FastMath.clamp((i + rng.range(-0.3f, 0.3f)) / n, 0.05f, 0.95f); + float startY = halfTrunk + frac * halfTrunk; + float trunkR = o.trunkRadius * lerp(1f, 0.01f, startY / o.trunkHeight); + + // Astradius darf den Stammradius an der Abgangsstelle nicht überschreiten + float baseBranchR = Math.min(o.branchRadius, trunkR); + + // Wind-Basiswert an diesem Ansatzpunkt + float windBase = lerp(o.trunkFlexibility, o.branchFlexibility * 0.5f, + startY / o.trunkHeight); + + // Tatsächliche Stammposition an startY interpolieren + float trunkFrac = (startY / o.trunkHeight) * o.trunkSections; + int tIdx = Math.min((int) trunkFrac, o.trunkSections - 1); + float tT = trunkFrac - tIdx; + Vector3f trunkCenter = trunkPts[tIdx].add( + trunkPts[tIdx + 1].subtract(trunkPts[tIdx]).mult(tT)); + + // Branchursprung am Stammumfang (radial vom echten Stammmittelpunkt) + Vector3f origin = trunkCenter.add(perpX * trunkR, 0f, perpZ * trunkR); + + // Ast-Bogen aufbauen – akkumulierte Richtung für organische Kurve + float segLen = o.branchLength / o.branchSections; + Vector3f pos = origin.clone(); + + // Startrichtung aus dem Bogen + float sinS = FastMath.sin(startAngRad); + float cosS = FastMath.cos(startAngRad); + float ddx = sinS * perpX, ddy = cosS, ddz = sinS * perpZ; + float dn0 = FastMath.sqrt(ddx*ddx + ddy*ddy + ddz*ddz); + if (dn0 > 1e-5f) { ddx /= dn0; ddy /= dn0; ddz /= dn0; } + + // Seitenvektor senkrecht zur Astebene + float sideX = -perpZ; + float sideZ = perpX; + + // Sektionspunkte sammeln für Hängeäste + record SecPt(Vector3f pos, float wind, float radius) {} + List secPts = new ArrayList<>(o.branchSections + 1); + secPts.add(new SecPt(pos.clone(), windBase, baseBranchR)); + + for (int s = 0; s < o.branchSections; s++) { + float t0 = (float) s / o.branchSections; + float t1 = (float)(s + 1) / o.branchSections; + + // Zielrichtung aus dem Bogen + float angle = lerp(startAngRad, endAngRad, t0); + float sinA = FastMath.sin(angle); + float cosA = FastMath.cos(angle); + float tx = sinA * perpX, ty = cosA, tz = sinA * perpZ; + float tl = FastMath.sqrt(tx*tx + ty*ty + tz*tz); + if (tl > 1e-5f) { tx /= tl; ty /= tl; tz /= tl; } + + // Akkumulierte Richtung sanft zum Bogen-Ziel ziehen + Wobble + float pull = 0.25f; + ddx += (tx - ddx) * pull + rng.range(-0.14f, 0.14f) * sideX; + ddy += (ty - ddy) * pull + rng.range(-0.06f, 0.06f); + ddz += (tz - ddz) * pull + rng.range(-0.14f, 0.14f) * sideZ; + float dn = FastMath.sqrt(ddx*ddx + ddy*ddy + ddz*ddz); + if (dn > 1e-5f) { ddx /= dn; ddy /= dn; ddz /= dn; } + + Vector3f end = pos.add(ddx * segLen, ddy * segLen, ddz * segLen); + + float r1 = baseBranchR * lerp(1f, 0.01f, t1 * t1); + float w1 = lerp(windBase, o.branchFlexibility, t1); + + secPts.add(new SecPt(end.clone(), w1, r1)); + pos = end; + } + + // Ast als nahtlose Röhre bauen + { + int nB = secPts.size(); + Vector3f[] bPts = new Vector3f[nB]; + float[] bRad = new float[nB]; + float[] bWind = new float[nB]; + for (int k = 0; k < nB; k++) { + bPts[k] = secPts.get(k).pos(); + bRad[k] = secPts.get(k).radius(); + bWind[k] = secPts.get(k).wind(); + } + buildTube(barkCol, bPts, bRad, bWind, o.branchSegments); + } + + // Hängeäste an jedem Sektionspunkt (nicht am allerersten) + for (int si = 1; si < secPts.size(); si++) { + SecPt sp = secPts.get(si); + for (int sb = 0; sb < o.subBranchCount; sb++) { + buildSubBranch(barkCol, leafCol, o, rng, sp.pos(), sp.wind(), sp.radius()); + } + } + } + } + + // ── 3. Hängeäste ───────────────────────────────────────────────────────── + + private static void buildSubBranch(VertexCollector barkCol, VertexCollector leafCol, + WillowOptions o, Rng rng, + Vector3f startPos, float windBase, float maxRadius) { + // Azimut: weg vom Stamm (Stamm liegt bei x=0, z=0 im Lokalraum). + // ±120° um die Outward-Richtung → Richtung zum Stamm praktisch ausgeschlossen. + float outLen = FastMath.sqrt(startPos.x * startPos.x + startPos.z * startPos.z); + float outYaw = outLen > 1e-4f + ? FastMath.atan2(startPos.x, startPos.z) + : rng.range(0f, FastMath.TWO_PI); + float yaw = outYaw + rng.range(-120f, 120f) * FastMath.DEG_TO_RAD; + float perpX = FastMath.sin(yaw); + float perpZ = FastMath.cos(yaw); + float sideX = -perpZ; + float sideZ = perpX; + + // Individuelle Varianz (halb so groß wie bei Hauptästen) + float variance = rng.range(-o.branchVariance * 0.5f, o.branchVariance * 0.5f); + float startAngRad = (o.subBranchStartAngle + variance) * FastMath.DEG_TO_RAD; + float endAngRad = (o.subBranchEndAngle + variance) * FastMath.DEG_TO_RAD; + + // Astradius darf Hauptastradius an Abgangsstelle nicht überschreiten + float baseR = Math.min(o.subBranchRadius, maxRadius); + if (baseR < 0.01f) return; // unter 2 cm Durchmesser → kein Ast, keine Blätter + + // Startrichtung aus dem Bogen + float sinS = FastMath.sin(startAngRad); + float cosS = FastMath.cos(startAngRad); + float ddx = sinS * perpX, ddy = cosS, ddz = sinS * perpZ; + float dn0 = FastMath.sqrt(ddx*ddx + ddy*ddy + ddz*ddz); + if (dn0 < 1e-5f) return; + ddx /= dn0; ddy /= dn0; ddz /= dn0; + + int nPts = o.subBranchSections + 1; + Vector3f[] sPts = new Vector3f[nPts]; + float[] sRad = new float[nPts]; + float[] sWind = new float[nPts]; + sPts[0] = startPos.clone(); + sRad[0] = baseR; + sWind[0] = windBase; + + float segLen = o.subBranchLength / o.subBranchSections; + Vector3f pos = startPos.clone(); + + for (int s = 0; s < o.subBranchSections; s++) { + float t0 = (float) s / o.subBranchSections; + float t1 = (float)(s + 1) / o.subBranchSections; + + // Zielrichtung aus dem Bogen + float angle = lerp(startAngRad, endAngRad, t0); + float sinA = FastMath.sin(angle); + float cosA = FastMath.cos(angle); + float tx = sinA * perpX, ty = cosA, tz = sinA * perpZ; + float tl = FastMath.sqrt(tx*tx + ty*ty + tz*tz); + if (tl > 1e-5f) { tx /= tl; ty /= tl; tz /= tl; } + + // Akkumulierte Richtung sanft zum Bogen-Ziel ziehen + Wobble + float pull = 0.30f; + ddx += (tx - ddx) * pull + rng.range(-0.10f, 0.10f) * sideX; + ddy += (ty - ddy) * pull + rng.range(-0.05f, 0.05f); + ddz += (tz - ddz) * pull + rng.range(-0.10f, 0.10f) * sideZ; + float dn = FastMath.sqrt(ddx*ddx + ddy*ddy + ddz*ddz); + if (dn > 1e-5f) { ddx /= dn; ddy /= dn; ddz /= dn; } + + float w0 = lerp(windBase, o.branchFlexibility, t0); + float w1 = lerp(windBase, o.branchFlexibility, t1); + + Vector3f end = pos.add(ddx * segLen, ddy * segLen, ddz * segLen); + sPts[s + 1] = end.clone(); + sRad[s + 1] = baseR * lerp(1f, 0.01f, t1 * t1); + sWind[s + 1] = w1; + + // Blätter nur wo Ast-Durchmesser ≥ 2 cm + float radHere = baseR * lerp(1f, 0.01f, t0 * t0); + if (radHere >= 0.01f) { + int midLeaves = Math.max(1, o.leafCount * 2 / 3); + addLeafCluster(leafCol, pos, w0, o.leafScale * 0.7f, midLeaves, o.leafDroop, + o.leafAngleMin, o.leafAngleMax, rng); + } + + pos = end; + } + buildTube(barkCol, sPts, sRad, sWind, o.subBranchSegments); + + // Blatt-Cluster an der Spitze (nur wenn Spitze noch ≥ 2 cm Durchmesser) + if (baseR * lerp(1f, 0.01f, 1f) >= 0.01f) { + addLeafCluster(leafCol, pos, o.branchFlexibility, o.leafScale, o.leafCount, o.leafDroop, + o.leafAngleMin, o.leafAngleMax, rng); + } + } + + // ── 4. Blatt-Cluster ───────────────────────────────────────────────────── + + private static void addLeafCluster(VertexCollector col, Vector3f tip, + float wind, float scale, int count, + float droop, float angleMin, float angleMax, Rng rng) { + for (int i = 0; i < count; i++) { + float ox = rng.range(-scale * 0.15f, scale * 0.15f); + float oy = rng.range(-scale * 0.10f, scale * 0.10f); + float oz = rng.range(-scale * 0.15f, scale * 0.15f); + float s = scale * (0.7f + rng.range(0f, 0.6f)); + + float yaw = rng.range(0f, FastMath.TWO_PI); + float angFromDown = rng.range(angleMin, angleMax) * FastMath.DEG_TO_RAD; + float sinA = FastMath.sin(angFromDown); + float cosA = FastMath.cos(angFromDown); + float cosY = FastMath.cos(yaw), sinY = FastMath.sin(yaw); + float gx = sinA * cosY; + float gy = -cosA; + float gz = sinA * sinY; + + // W1: horizontal senkrecht zur Wachstumsrichtung (Farn-Ansatz) + float w1x = sinY, w1y = 0f, w1z = -cosY; + // W2 = g × W1: senkrecht zu g und W1, zeigt schräg aufwärts (klassische X-Form) + float w2x = cosA * cosY, w2y = sinA, w2z = cosA * sinY; + + // Zwei senkrechte Fronds (X-Form) – jeder ein sauberer flacher Wedel + addLeafFrond(col, tip.x + ox, tip.y + oy, tip.z + oz, + s, wind, gx, gy, gz, w1x, w1y, w1z, droop); + addLeafFrond(col, tip.x + ox, tip.y + oy, tip.z + oz, + s, wind, gx, gy, gz, w2x, w2y, w2z, droop); + } + } + + private static final int LEAF_SECS = 6; + + // Einzelner flacher Blatt-Wedel nach dem Farn-Ansatz: + // 3 Vertex-Spalten (links / Mittelrippe / rechts), Normale = T × W. + // Bézier-Mittellinie für glatten Droop ohne Knicke. + // (wx,wy,wz) = Breiten-Richtung als Einheitsvektor, direkt übergeben. + private static void addLeafFrond(VertexCollector col, + float cx, float cy, float cz, + float s, float wind, + float gx, float gy, float gz, + float wx, float wy, float wz, + float droop) { + float L = s * 3.9f; + + // Bézier-Kontrollpunkte + float p1x = gx * L * 0.5f, p1y = gy * L * 0.5f, p1z = gz * L * 0.5f; + float edX = lerp(gx, 0f, droop), edY = lerp(gy, -1f, droop), edZ = lerp(gz, 0f, droop); + float edLen = FastMath.sqrt(edX*edX + edY*edY + edZ*edZ); + if (edLen > 1e-5f) { edX /= edLen; edY /= edLen; edZ /= edLen; } + else { edX = 0; edY = -1; edZ = 0; } + float p2x = p1x + edX * L * 0.5f; + float p2y = p1y + edY * L * 0.5f; + float p2z = p1z + edZ * L * 0.5f; + + int base = col.vertexCount; + + for (int i = 0; i <= LEAF_SECS; i++) { + float t = (float) i / LEAF_SECS; + float mt = 1f - t; + + // Bézier-Position + float posX = cx + 2*t*mt*p1x + t*t*p2x; + float posY = cy + 2*t*mt*p1y + t*t*p2y; + float posZ = cz + 2*t*mt*p1z + t*t*p2z; + + // Bézier-Tangente (normiert) + float dX = 2*(1 - 2*t)*p1x + 2*t*p2x; + float dY = 2*(1 - 2*t)*p1y + 2*t*p2y; + float dZ = 2*(1 - 2*t)*p1z + 2*t*p2z; + float dLen = FastMath.sqrt(dX*dX + dY*dY + dZ*dZ); + if (dLen > 1e-5f) { dX /= dLen; dY /= dLen; dZ /= dLen; } + else { dX = 0; dY = -1; dZ = 0; } + + // Normale N = T × W (allgemeine Form, korrekte Flächennormale) + float nx = dY*wz - dZ*wy; + float ny = dZ*wx - dX*wz; + float nz = dX*wy - dY*wx; + float nLen = FastMath.sqrt(nx*nx + ny*ny + nz*nz); + if (nLen > 1e-5f) { nx /= nLen; ny /= nLen; nz /= nLen; } + else { nx = 0; ny = 1; nz = 0; } + + float hw = s * 1.5f * (1f - t * 0.75f); + float wCur = t; // 0 an der Basis (Ast-Ansatz), 1 an der Spitze + + // 3 Vertex-Spalten: links (u=0), Mittelrippe (u=0.5), rechts (u=1) + col.add(posX + hw*wx, posY + hw*wy, posZ + hw*wz, nx, ny, nz, 0f, t, wCur); + col.add(posX, posY, posZ, nx, ny, nz, 0.5f, t, wCur); + col.add(posX - hw*wx, posY - hw*wy, posZ - hw*wz, nx, ny, nz, 1f, t, wCur); + + if (i > 0) { + int pb = base + (i - 1) * 3; + int cb = base + i * 3; + // Linkes Panel (L–Mitte) + col.tri(pb, pb+1, cb); + col.tri(pb+1, cb+1, cb); + // Rechtes Panel (Mitte–R) + col.tri(pb+1, pb+2, cb+1); + col.tri(pb+2, cb+2, cb+1); + } + } + } + + // ── Nahtlose Röhre entlang eines Pfades ────────────────────────────────── + // Bisektor-Achsen an Gelenken + Parallel-Transport verhindern Lücken und Torsion. + + private static void buildTube(VertexCollector col, + Vector3f[] pts, float[] radii, float[] winds, int N) { + int nPts = pts.length; + if (nPts < 2) return; + + // Segmentrichtungen + Vector3f[] segDir = new Vector3f[nPts - 1]; + for (int i = 0; i < nPts - 1; i++) { + segDir[i] = pts[i + 1].subtract(pts[i]); + float len = segDir[i].length(); + if (len > 1e-6f) segDir[i].divideLocal(len); else segDir[i].set(0, 1, 0); + } + + // Ringachsen: Bisektor an inneren Gelenken, Segmentrichtung an den Enden + Vector3f[] ringAxis = new Vector3f[nPts]; + ringAxis[0] = segDir[0].clone(); + ringAxis[nPts - 1] = segDir[nPts - 2].clone(); + for (int i = 1; i < nPts - 1; i++) { + ringAxis[i] = segDir[i - 1].add(segDir[i]); + float len = ringAxis[i].length(); + if (len > 1e-6f) ringAxis[i].divideLocal(len); else ringAxis[i] = segDir[i].clone(); + } + + // Startperpendicular + Vector3f axis0 = ringAxis[0]; + Vector3f perp = (Math.abs(axis0.y) < 0.9f) + ? axis0.cross(Vector3f.UNIT_Y).normalizeLocal() + : axis0.cross(Vector3f.UNIT_X).normalizeLocal(); + + int N1 = N + 1; + int base = col.vertexCount; + + for (int i = 0; i < nPts; i++) { + Vector3f axis = ringAxis[i]; + + // Parallel-Transport: perp auf die zur axis senkrechte Ebene projizieren + if (i > 0) { + float dot = perp.dot(axis); + perp = perp.subtract(axis.mult(dot)); + float len = perp.length(); + if (len > 1e-6f) perp.divideLocal(len); + else perp = (Math.abs(axis.y) < 0.9f) + ? axis.cross(Vector3f.UNIT_Y).normalizeLocal() + : axis.cross(Vector3f.UNIT_X).normalizeLocal(); + } + + Vector3f perp2 = axis.cross(perp).normalizeLocal(); + float r = radii[i]; + float w = winds[i]; + float v = (float) i / (nPts - 1); + + for (int j = 0; j <= N; j++) { + float theta = FastMath.TWO_PI * j / N; + float cosT = FastMath.cos(theta); + float sinT = FastMath.sin(theta); + float nx = cosT * perp.x + sinT * perp2.x; + float ny = cosT * perp.y + sinT * perp2.y; + float nz = cosT * perp.z + sinT * perp2.z; + col.add(pts[i].x + nx * r, pts[i].y + ny * r, pts[i].z + nz * r, + nx, ny, nz, (float) j / N, v, w); + } + + if (i > 0) { + int pb = base + (i - 1) * N1; + int cb = base + i * N1; + for (int j = 0; j < N; j++) { + col.tri(pb + j, pb + j + 1, cb + j + 1); + col.tri(pb + j, cb + j + 1, cb + j); + } + } + } + } + + // ── Hilfsfunktionen ─────────────────────────────────────────────────────── + + private static float lerp(float a, float b, float t) { return a + (b - a) * t; } + + // ── BoundingBox ─────────────────────────────────────────────────────────── + + static BoundingBox computeBounds(VertexCollector col) { + if (col.pos.isEmpty()) return new BoundingBox(); + float minX = Float.MAX_VALUE, minY = Float.MAX_VALUE, minZ = Float.MAX_VALUE; + float maxX = -Float.MAX_VALUE, maxY = -Float.MAX_VALUE, maxZ = -Float.MAX_VALUE; + for (int i = 0; i < col.pos.size(); i += 3) { + float x = col.pos.get(i), y = col.pos.get(i+1), z = col.pos.get(i+2); + if (x < minX) minX = x; if (x > maxX) maxX = x; + if (y < minY) minY = y; if (y > maxY) maxY = y; + if (z < minZ) minZ = z; if (z > maxZ) maxZ = z; + } + return new BoundingBox( + new Vector3f((minX+maxX)*0.5f, (minY+maxY)*0.5f, (minZ+maxZ)*0.5f), + (maxX-minX)*0.5f, (maxY-minY)*0.5f, (maxZ-minZ)*0.5f); + } + + // ── MWC-RNG ─────────────────────────────────────────────────────────────── + + static final class Rng { + private long w, z; + + Rng(int seed) { + w = (123456789L + seed) & 0xFFFFFFFFL; + z = (987654321L - seed) & 0xFFFFFFFFL; + } + + float next() { + z = (36969L*(z & 65535L) + (z >> 16)) & 0xFFFFFFFFL; + w = (18000L*(w & 65535L) + (w >> 16)) & 0xFFFFFFFFL; + long r = ((z << 16) + (w & 65535L)) & 0xFFFFFFFFL; + return (float) r / 4294967296f; + } + + float range(float lo, float hi) { return lo + (hi - lo) * next(); } + } + + // ── Vertex-Sammler ──────────────────────────────────────────────────────── + + static final class VertexCollector { + final List pos = new ArrayList<>(); + final List norm = new ArrayList<>(); + final List uv = new ArrayList<>(); + final List col = 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 wind) { + pos.add(x); pos.add(y); pos.add(z); + norm.add(nx); norm.add(ny); norm.add(nz); + uv.add(u); uv.add(v); + col.add(wind); col.add(0f); col.add(0f); col.add(1f); + 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 : col) colB.put(f); + 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(); + return mesh; + } + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/tree/WillowOptions.java b/blight-editor/src/main/java/de/blight/editor/tree/WillowOptions.java new file mode 100644 index 0000000..66c39b4 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/tree/WillowOptions.java @@ -0,0 +1,105 @@ +package de.blight.editor.tree; + +/** + * Parameter für den prozeduralen Trauerweide-Generator. + * + * Astform: Äste starten bei ~45° vom Stamm (steil nach oben) und biegen sich + * im Bogen bis ~135° (hängend nach unten). Von den Hauptästen hängen dünne + * Hängeäste ab, die mit leafDroop gesteuert hauptsächlich senkrecht fallen. + */ +public class WillowOptions { + + public int seed = 77042; + + // ── Stamm ───────────────────────────────────────────────────────────────── + public float trunkHeight = 12f; + public float trunkRadius = 0.45f; + public int trunkSections = 8; + public int trunkSegments = 8; + + // ── Hauptäste (ab trunkHeight * 0.5 aufwärts) ─────────────────────────── + /** Anzahl Hauptäste um den Stamm. */ + public int branchCount = 7; + /** Startwinkel vom Stamm in Grad (0=oben, 90=waagrecht, 180=unten). */ + public float branchStartAngle = 60f; + /** Endwinkel am Astende (nach der Kurve). */ + public float branchEndAngle = 150f; + /** Zufällige ±Varianz auf Start- und Endwinkel in Grad. */ + public float branchVariance = 18f; + public float branchLength = 9f; + public float branchRadius = 0.18f; + public int branchSections = 8; + public int branchSegments = 5; + + // ── Hängeäste (von den Hauptästen) ─────────────────────────────────────── + /** Anzahl Hängeäste pro Hauptast-Sektionspunkt. */ + public int subBranchCount = 3; + public float subBranchLength = 5f; + public float subBranchRadius = 0.07f; + /** Startwinkel vom Stamm in Grad (wie branchStartAngle). */ + public float subBranchStartAngle = 80f; + /** Endwinkel nach dem Bogen in Grad. */ + public float subBranchEndAngle = 160f; + public int subBranchSections = 5; + public int subBranchSegments = 4; + + // ── Blätter ─────────────────────────────────────────────────────────────── + public float leafScale = 2.0f; + public int leafCount = 5; + /** 0=zufällige Richtung, 1=hängen senkrecht. */ + public float leafDroop = 0.80f; + /** Minimaler Startwinkel der Blätter von der Senkrechten nach unten (0°=senkrecht, 90°=waagrecht). */ + public float leafAngleMin = 10f; + /** Maximaler Startwinkel der Blätter von der Senkrechten nach unten. */ + public float leafAngleMax = 55f; + + // ── Texturen ────────────────────────────────────────────────────────────── + public String barkTexture = "Textures/internal/bark/Bark001_Color.jpg"; + public String leafTexture = "Textures/internal/foliage/weeping_willow.png"; + + // ── Farben ──────────────────────────────────────────────────────────────── + public float barkR = 0.52f, barkG = 0.43f, barkB = 0.28f; + public float leafR = 0.22f, leafG = 0.60f, leafB = 0.12f; + + // ── Wind ────────────────────────────────────────────────────────────────── + public float trunkFlexibility = 0.05f; + public float branchFlexibility = 0.92f; + + // ── copy ────────────────────────────────────────────────────────────────── + + public WillowOptions copy() { + WillowOptions c = new WillowOptions(); + c.seed = seed; + c.trunkHeight = trunkHeight; + c.trunkRadius = trunkRadius; + c.trunkSections = trunkSections; + c.trunkSegments = trunkSegments; + c.branchCount = branchCount; + c.branchStartAngle = branchStartAngle; + c.branchEndAngle = branchEndAngle; + c.branchVariance = branchVariance; + c.branchLength = branchLength; + c.branchRadius = branchRadius; + c.branchSections = branchSections; + c.branchSegments = branchSegments; + c.subBranchCount = subBranchCount; + c.subBranchLength = subBranchLength; + c.subBranchRadius = subBranchRadius; + c.subBranchStartAngle = subBranchStartAngle; + c.subBranchEndAngle = subBranchEndAngle; + c.subBranchSections = subBranchSections; + c.subBranchSegments = subBranchSegments; + c.leafScale = leafScale; + c.leafCount = leafCount; + c.leafDroop = leafDroop; + c.leafAngleMin = leafAngleMin; + c.leafAngleMax = leafAngleMax; + c.barkTexture = barkTexture; + c.leafTexture = leafTexture; + c.barkR = barkR; c.barkG = barkG; c.barkB = barkB; + c.leafR = leafR; c.leafG = leafG; c.leafB = leafB; + c.trunkFlexibility = trunkFlexibility; + c.branchFlexibility = branchFlexibility; + return c; + } +} 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 28190cb..c20cdd8 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 @@ -330,7 +330,7 @@ public class WorldObjectsState extends BaseAppState { lodRoot.updateGeometricState(); if (treeNode.getWorldBound() instanceof com.jme3.bounding.BoundingBox treeBb) { float groundY = treeBb.getCenter().y - treeBb.getYExtent(); - if (Math.abs(groundY) > 0.01f) { + if (groundY > 0.01f) { treeNode.setLocalTranslation(0f, -groundY, 0f); lod2.setLocalTranslation(0f, -groundY, 0f); } diff --git a/blight-map/src/main/map/blight_objects.blo b/blight-map/src/main/map/blight_objects.blo index 1936d9a..900ed33 100644 --- a/blight-map/src/main/map/blight_objects.blo +++ b/blight-map/src/main/map/blight_objects.blo @@ -9,3 +9,5 @@ Models/trees/palm/palm_20260816_213341.j3o 247.05431 6.44288 -888.47949 1.23606 Models/trees/palm/palm_20260816_213341.j3o 239.43253 0.94988 -886.23218 -2.49448 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000 Models/trees/palm/palm_20260816_213341.j3o 270.73062 3.06689 -913.67090 -1.45828 1.00000 -0.00000 0.00000 false true true 30.00000 80.00000 120.00000 Models/imported/bank1.j3o 236.63928 -6.31074 -888.17450 -3.22597 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 BENCH 9ac9943d-0e12-4d5c-8323-0e2b92eebdec +Models/trees/willow/willow_20260823_101850.j3o 152.40488 11.49070 -888.86847 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 +Models/trees/willow/willow_20260823_101856.j3o 170.68971 11.48961 -873.92236 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000