Neuer Baum Generator für Trauerweiden
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 948 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
Binary file not shown.
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
}
|
||||
@@ -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 (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<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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user