Neuer Baum Generator für Trauerweiden

This commit is contained in:
2026-08-23 10:48:25 +02:00
parent fdd8eb60be
commit d65ebbfeb6
14 changed files with 1329 additions and 9 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

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

View File

@@ -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<String> 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<String> 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<String> 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() {

View File

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

View File

@@ -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<PalmGenRequest> palmGenQueue = new ConcurrentLinkedQueue<>();
// ── Trauerweide-Generator ─────────────────────────────────────────────────
public record WillowGenRequest(WillowOptions options, boolean exportAfter) {}
public final ConcurrentLinkedQueue<WillowGenRequest> willowGenQueue = new ConcurrentLinkedQueue<>();
// ── Objekt-Werkzeug ──────────────────────────────────────────────────────
/** activeLayer==5 → Objekte platzieren */
public static final int LAYER_OBJECTS = 5;

View File

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

View File

@@ -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);
lod2.setCullHint(dSq >= d12sq ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
}
@Override protected void controlRender(RenderManager rm, ViewPort vp) {}
}
}

View File

@@ -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<SecPt> 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 (LMitte)
col.tri(pb, pb+1, cb);
col.tri(pb+1, cb+1, cb);
// Rechtes Panel (MitteR)
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<Float> pos = new ArrayList<>();
final List<Float> norm = new ArrayList<>();
final List<Float> uv = new ArrayList<>();
final List<Float> col = new ArrayList<>();
final List<Integer> 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;
}
}
}

View File

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

View File

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

View File

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