Weiter das Palmen Tool verfeinert

This commit is contained in:
2026-05-19 21:54:04 +02:00
parent 4f48834e2c
commit f9a77cc321
15 changed files with 439 additions and 129 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

View File

@@ -71,6 +71,11 @@ public class EditorApp extends Application {
private PalmOptions palmOptions = new PalmOptions();
private final TextField palmNameField = new TextField("Palme1");
// Aktives Tool + Tastenkürzel-Callbacks
private String currentTool = "world";
private Runnable onF5 = () -> {}; // Vorschau
private Runnable onF6 = () -> {}; // Zufälligen Seed wählen
// Asset-Panel: Pfad-Map + DnD-Zustand
private final Map<TreeItem<String>, Path> itemPaths = new HashMap<>();
private TreeItem<String> draggedItem = null;
@@ -182,24 +187,28 @@ public class EditorApp extends Application {
// ── Modus-Wechsel ────────────────────────────────────────────────────────
private void switchToTreeGenerator() {
currentTool = "tree";
topBar.getChildren().set(1, buildTreeToolBar());
root.setCenter(buildTreePreviewPanel());
root.setRight(buildTreeParamsPanel());
}
private void switchToWorldEditor() {
currentTool = "world";
topBar.getChildren().set(1, worldToolBar);
root.setCenter(worldViewport);
root.setRight(toolPanel);
}
private void switchToEzTree() {
currentTool = "eztree";
topBar.getChildren().set(1, buildEzTreeToolBar());
root.setCenter(buildTreePreviewPanel()); // teilt Vorschau-Infrastruktur
root.setCenter(buildTreePreviewPanel());
root.setRight(buildEzTreeParamsPanel());
}
private void switchToPalm() {
currentTool = "palm";
topBar.getChildren().set(1, buildPalmToolBar());
root.setCenter(buildTreePreviewPanel());
root.setRight(buildPalmParamsPanel());
@@ -329,13 +338,14 @@ public class EditorApp extends Application {
nameLabel.setStyle("-fx-font-weight: bold;");
treeNameField.setPrefWidth(130);
Button previewBtn = new Button("▶ Vorschau");
previewBtn.setStyle("-fx-font-weight: bold;");
previewBtn.setOnAction(e -> {
onF5 = () -> {
input.treeGenQueue.offer(
new SharedInput.TreeGenRequest(treeParams.copy(), false, treeNameField.getText().trim()));
setStatus("Generiere Vorschau…");
});
};
Button previewBtn = new Button("▶ Vorschau [F5]");
previewBtn.setStyle("-fx-font-weight: bold;");
previewBtn.setOnAction(e -> onF5.run());
Button exportBtn = new Button("💾 Exportieren als .j3o");
exportBtn.setOnAction(e -> {
@@ -414,7 +424,17 @@ public class EditorApp extends Application {
seedSpinner.setEditable(true);
seedSpinner.setMaxWidth(Double.MAX_VALUE);
seedSpinner.valueProperty().addListener((o, a, b) -> treeParams.seed = b);
inner.getChildren().add(seedSpinner);
Button treeRndSeed = new Button("🎲");
onF6 = () -> {
int s = new java.util.Random().nextInt(100000);
treeParams.seed = s;
seedSpinner.getValueFactory().setValue(s);
onF5.run();
};
treeRndSeed.setOnAction(e -> onF6.run());
HBox treeSeedRow = new HBox(4, seedSpinner, treeRndSeed);
HBox.setHgrow(seedSpinner, Priority.ALWAYS);
inner.getChildren().add(treeSeedRow);
// Ast-Tiefe
inner.getChildren().add(bold("Ast-Tiefe:"));
@@ -525,14 +545,15 @@ public class EditorApp extends Application {
nameLabel.setStyle("-fx-font-weight: bold;");
ezTreeNameField.setPrefWidth(130);
Button previewBtn = new Button("▶ Vorschau");
previewBtn.setStyle("-fx-font-weight: bold;");
previewBtn.setOnAction(e -> {
onF5 = () -> {
input.ezTreeGenQueue.offer(
new SharedInput.EzTreeGenRequest(
ezTreeOptions.copy(), false, ezTreeNameField.getText().trim()));
setStatus("EZ-Tree: generiere Vorschau…");
});
};
Button previewBtn = new Button("▶ Vorschau [F5]");
previewBtn.setStyle("-fx-font-weight: bold;");
previewBtn.setOnAction(e -> onF5.run());
Button exportBtn = new Button("💾 Export .j3o");
exportBtn.setOnAction(e -> {
@@ -564,7 +585,17 @@ public class EditorApp extends Application {
inner.getChildren().add(bold("Zufallssamen:"));
Spinner<Integer> seedSp = intSpinner(0, 999999, ezTreeOptions.seed);
seedSp.valueProperty().addListener((o, a, b) -> ezTreeOptions.seed = b);
inner.getChildren().add(seedSp);
Button ezRndSeed = new Button("🎲");
onF6 = () -> {
int s = new java.util.Random().nextInt(1000000);
ezTreeOptions.seed = s;
seedSp.getValueFactory().setValue(s);
onF5.run();
};
ezRndSeed.setOnAction(e -> onF6.run());
HBox ezSeedRow = new HBox(4, seedSp, ezRndSeed);
HBox.setHgrow(seedSp, Priority.ALWAYS);
inner.getChildren().add(ezSeedRow);
inner.getChildren().add(bold("Typ:"));
ChoiceBox<String> typeCB = new ChoiceBox<>();
@@ -728,14 +759,15 @@ public class EditorApp extends Application {
nameLabel.setStyle("-fx-font-weight: bold;");
palmNameField.setPrefWidth(130);
Button previewBtn = new Button("▶ Vorschau");
previewBtn.setStyle("-fx-font-weight: bold;");
previewBtn.setOnAction(e -> {
onF5 = () -> {
input.palmGenQueue.offer(
new SharedInput.PalmGenRequest(
palmOptions.copy(), false, palmNameField.getText().trim()));
setStatus("Palme: generiere Vorschau…");
});
};
Button previewBtn = new Button("▶ Vorschau [F5]");
previewBtn.setStyle("-fx-font-weight: bold;");
previewBtn.setOnAction(e -> onF5.run());
Button exportBtn = new Button("💾 Export .j3o");
exportBtn.setOnAction(e -> {
@@ -763,37 +795,93 @@ public class EditorApp extends Application {
inner.getChildren().add(bold("Zufallssamen:"));
Spinner<Integer> seedSp = intSpinner(0, 999999, palmOptions.seed);
seedSp.valueProperty().addListener((o, a, b) -> palmOptions.seed = b);
inner.getChildren().add(seedSp);
Button palmRndSeed = new Button("🎲");
onF6 = () -> {
int s = new java.util.Random().nextInt(1000000);
palmOptions.seed = s;
seedSp.getValueFactory().setValue(s);
onF5.run();
};
palmRndSeed.setOnAction(e -> onF6.run());
HBox palmSeedRow = new HBox(4, seedSp, palmRndSeed);
HBox.setHgrow(seedSp, Priority.ALWAYS);
inner.getChildren().add(palmSeedRow);
inner.getChildren().addAll(sectionTitle("Stamm"), new Separator());
inner.getChildren().add(bold("Anzahl Stämme:"));
Spinner<Integer> palmCountSp = intSpinner(1, 3, palmOptions.palmCount);
palmCountSp.valueProperty().addListener((o, a, b) -> palmOptions.palmCount = b);
inner.getChildren().add(palmCountSp);
inner.getChildren().add(ezFloat("Höhe:", 5, 25, palmOptions.trunkHeight,
v -> palmOptions.trunkHeight = v));
inner.getChildren().add(ezFloat("Radius unten:", 0.1, 1.0, palmOptions.trunkRadiusBottom,
v -> palmOptions.trunkRadiusBottom = v));
inner.getChildren().add(ezFloat("Radius oben:", 0.05, 0.9, palmOptions.trunkRadiusTop,
v -> palmOptions.trunkRadiusTop = v));
inner.getChildren().add(ezFloat("Neigung (°):", 0.0, 50.0, palmOptions.lean,
v -> palmOptions.lean = v));
inner.getChildren().addAll(sectionTitle("Kronenschaft"), new Separator());
inner.getChildren().add(ezFloat("Höhe:", 0.2, 4.0, palmOptions.crownHeight,
v -> palmOptions.crownHeight = v));
inner.getChildren().add(ezFloat("Aufweitung:", 1.0, 3.0, palmOptions.crownFlare,
v -> palmOptions.crownFlare = v));
inner.getChildren().add(ezFloat("Farbe R:", 0, 1, palmOptions.crownR, v -> palmOptions.crownR = v));
inner.getChildren().add(ezFloat("Farbe G:", 0, 1, palmOptions.crownG, v -> palmOptions.crownG = v));
inner.getChildren().add(ezFloat("Farbe B:", 0, 1, palmOptions.crownB, v -> palmOptions.crownB = v));
inner.getChildren().addAll(sectionTitle("Wedel"), new Separator());
inner.getChildren().add(bold("Anzahl:"));
Spinner<Integer> frondCountSp = intSpinner(3, 20, palmOptions.frondCount);
Spinner<Integer> frondCountSp = intSpinner(12, 120, palmOptions.frondCount);
frondCountSp.valueProperty().addListener((o, a, b) -> palmOptions.frondCount = b);
inner.getChildren().add(frondCountSp);
inner.getChildren().add(ezFloat("Winkel min (° von oben):", 60, 95, palmOptions.frondAngleMin,
inner.getChildren().add(ezFloat("Winkel min (° von oben):", 10, 90, palmOptions.frondAngleMin,
v -> palmOptions.frondAngleMin = v));
inner.getChildren().add(ezFloat("Winkel max (° von oben):", 85, 130, palmOptions.frondAngleMax,
inner.getChildren().add(ezFloat("Winkel max (° von oben):", 90, 170, palmOptions.frondAngleMax,
v -> palmOptions.frondAngleMax = v));
inner.getChildren().add(ezFloat("Länge:", 2, 12, palmOptions.frondLength,
v -> palmOptions.frondLength = v));
inner.getChildren().addAll(sectionTitle("Blätter (Fiederblättchen)"), new Separator());
inner.getChildren().add(bold("Paare pro Wedel:"));
Spinner<Integer> pairsSp = intSpinner(3, 16, palmOptions.frondLeafletPairs);
pairsSp.valueProperty().addListener((o, a, b) -> palmOptions.frondLeafletPairs = b);
inner.getChildren().add(pairsSp);
inner.getChildren().add(ezFloat("Wedel-Breite:", 0.2, 4.0, palmOptions.frondWidth,
v -> palmOptions.frondWidth = v));
inner.getChildren().addAll(sectionTitle("Blätter"), new Separator());
inner.getChildren().add(ezFloat("Schwerkraft:", 0.0, 2.0, palmOptions.gravity,
v -> palmOptions.gravity = v));
inner.getChildren().add(ezFloat("Größe oben (relativ):", 0.0, 1.0, palmOptions.frondMinSizeRatio,
v -> palmOptions.frondMinSizeRatio = v));
inner.getChildren().addAll(sectionTitle("Farben"), new Separator());
// Blatt-Textur-Auswahl
Label leafTexLabel = new Label("Blatt-Textur:");
leafTexLabel.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
ComboBox<String> leafTexBox = new ComboBox<>();
leafTexBox.getItems().addAll("palm.png", "palm2.png");
String currentLeaf = palmOptions.leafTexture != null
&& palmOptions.leafTexture.contains("palm2") ? "palm2.png" : "palm.png";
leafTexBox.setValue(currentLeaf);
leafTexBox.setMaxWidth(Double.MAX_VALUE);
leafTexBox.setOnAction(e -> palmOptions.leafTexture = "Textures/leaves/" + leafTexBox.getValue());
inner.getChildren().add(new VBox(2, leafTexLabel, leafTexBox));
// Rinden-Textur-Auswahl (alle Texturen aus dem bark-Ordner)
Label barkTexLabel = new Label("Rinden-Textur:");
barkTexLabel.setStyle("-fx-font-weight: bold; -fx-text-fill: #111111;");
ComboBox<String> barkTexBox = new ComboBox<>();
barkTexBox.getItems().addAll(
"Bark_Palm.png", "Bark_Palm2.png",
"Bark001_Color.jpg", "Bark002_Color.jpg",
"Bark003_Color.jpg", "Bark008_Color.jpg"
);
String currentBark = palmOptions.barkTexture != null
? palmOptions.barkTexture.substring(palmOptions.barkTexture.lastIndexOf('/') + 1)
: "Bark_Palm.png";
barkTexBox.setValue(barkTexBox.getItems().contains(currentBark)
? currentBark : barkTexBox.getItems().get(0));
barkTexBox.setMaxWidth(Double.MAX_VALUE);
barkTexBox.setOnAction(e -> {
if (barkTexBox.getValue() != null)
palmOptions.barkTexture = "Textures/bark/" + barkTexBox.getValue();
});
inner.getChildren().add(new VBox(2, barkTexLabel, barkTexBox));
inner.getChildren().add(ezFloat("Stamm R:", 0, 1, palmOptions.barkR, v -> palmOptions.barkR = v));
inner.getChildren().add(ezFloat("Stamm G:", 0, 1, palmOptions.barkG, v -> palmOptions.barkG = v));
inner.getChildren().add(ezFloat("Stamm B:", 0, 1, palmOptions.barkB, v -> palmOptions.barkB = v));
@@ -1754,6 +1842,8 @@ public class EditorApp extends Application {
case D -> input.right = pressed;
case Q -> input.up = pressed;
case E -> input.down = pressed;
case F5 -> { if (pressed) onF5.run(); }
case F6 -> { if (pressed) onF6.run(); }
}
}
}

View File

@@ -23,6 +23,7 @@ import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.texture.FrameBuffer;
import com.jme3.texture.Image;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D;
import com.jme3.util.BufferUtils;
import de.blight.editor.SharedInput;
@@ -195,7 +196,9 @@ public class EzTreeState extends BaseAppState {
mat.setFloat("WindSpeed", 0.5f);
if (opts.bark.textureFile != null) {
try {
mat.setTexture("BarkMap", assets.loadTexture(opts.bark.textureFile));
Texture barkTex = assets.loadTexture(opts.bark.textureFile);
barkTex.setWrap(Texture.WrapMode.Repeat);
mat.setTexture("BarkMap", barkTex);
mat.setBoolean("HasBarkMap", true);
} catch (Exception ignored) {}
}
@@ -249,8 +252,8 @@ public class EzTreeState extends BaseAppState {
Node scene = new Node("ezCapScene");
scene.addLight(new DirectionalLight(
new Vector3f(-0.4f, -1f, -0.5f).normalizeLocal(), ColorRGBA.White));
scene.addLight(new AmbientLight(new ColorRGBA(0.35f, 0.35f, 0.35f, 1f)));
new Vector3f(-0.4f, -1f, -0.5f).normalizeLocal(), new ColorRGBA(2.0f, 1.85f, 1.5f, 1f)));
scene.addLight(new AmbientLight(new ColorRGBA(0.60f, 0.60f, 0.60f, 1f)));
scene.attachChild(cloneForCapture(src));
vp.attachScene(scene);
scene.updateGeometricState();

View File

@@ -8,6 +8,7 @@ import com.jme3.bounding.BoundingBox;
import com.jme3.export.binary.BinaryExporter;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.texture.Texture;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue;
@@ -55,8 +56,19 @@ public class PalmGeneratorState extends BaseAppState {
SharedInput.PalmGenRequest req = input.palmGenQueue.poll();
if (req == null) return;
Node palm = PalmMeshBuilder.build(req.options());
applyMaterials(palm, req.options());
PalmOptions opts = req.options();
if (opts.leafTexture != null) {
try {
Texture t = assets.loadTexture(opts.leafTexture);
int w = t.getImage().getWidth();
int h = t.getImage().getHeight();
boolean stemLeft = opts.leafTexture.contains("palm2");
opts.leafTextureAspect = stemLeft ? (float) h / w : (float) w / h;
} catch (Exception ignored) {}
}
Node palm = PalmMeshBuilder.build(opts);
applyMaterials(palm, opts);
palm.updateGeometricState();
BoundingBox bb = palm.getWorldBound() instanceof BoundingBox b ? b : null;
@@ -92,6 +104,10 @@ public class PalmGeneratorState extends BaseAppState {
g.setMaterial(buildBarkMat(opts));
g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
}
case "crown" -> {
g.setMaterial(buildCrownMat(opts));
g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
}
case "leaves" -> {
g.setMaterial(buildLeafMat(opts));
g.setQueueBucket(RenderQueue.Bucket.Transparent);
@@ -109,7 +125,9 @@ public class PalmGeneratorState extends BaseAppState {
mat.setFloat("WindSpeed", 0.4f);
if (opts.barkTexture != null) {
try {
mat.setTexture("BarkMap", assets.loadTexture(opts.barkTexture));
Texture barkTex = assets.loadTexture(opts.barkTexture);
barkTex.setWrap(Texture.WrapMode.Repeat);
mat.setTexture("BarkMap", barkTex);
mat.setBoolean("HasBarkMap", true);
} catch (Exception ignored) {}
}
@@ -143,6 +161,29 @@ public class PalmGeneratorState extends BaseAppState {
}
}
private Material buildCrownMat(PalmOptions opts) {
ColorRGBA color = new ColorRGBA(opts.crownR, opts.crownG, opts.crownB, 1f);
try {
Material mat = new Material(assets, "MatDefs/TreeLeaf.j3md");
mat.setColor("Diffuse", color);
mat.setFloat("WindStrength", 0.04f);
mat.setFloat("WindSpeed", 0.3f);
if (opts.crownTexture != null) {
try {
Texture tex = assets.loadTexture(opts.crownTexture);
tex.setWrap(Texture.WrapMode.Repeat);
mat.setTexture("LeafMap", tex);
mat.setBoolean("HasLeafMap", true);
} catch (Exception ignored) {}
}
return mat;
} catch (Exception e) {
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", color);
return mat;
}
}
private void exportPalm(Node palmNode, String name) {
try {
Path modelDir = ASSET_ROOT.resolve("models");

View File

@@ -100,7 +100,6 @@ public class TreeGeneratorState extends BaseAppState {
// ── Lifecycle ─────────────────────────────────────────────────────────────
@SuppressWarnings("deprecation")
@Override
protected void initialize(Application app) {
this.app = (SimpleApplication) app;
@@ -124,12 +123,12 @@ public class TreeGeneratorState extends BaseAppState {
previewScene = new Node("previewScene");
previewSunLight = new DirectionalLight(
new Vector3f(-0.45f, -1.0f, -0.3f).normalizeLocal(),
new ColorRGBA(1.2f, 1.1f, 0.9f, 1f));
new ColorRGBA(2.0f, 1.85f, 1.5f, 1f));
previewScene.addLight(previewSunLight);
previewScene.addLight(new DirectionalLight(
new Vector3f(0.4f, -0.6f, -0.8f).normalizeLocal(),
new ColorRGBA(0.35f, 0.40f, 0.55f, 1f)));
previewScene.addLight(new AmbientLight(new ColorRGBA(0.25f, 0.25f, 0.30f, 1f)));
new ColorRGBA(0.70f, 0.75f, 1.0f, 1f)));
previewScene.addLight(new AmbientLight(new ColorRGBA(0.55f, 0.55f, 0.60f, 1f)));
previewTreeHolder = new Node("treeHolder");
previewScene.attachChild(previewTreeHolder);
previewScene.attachChild(buildPreviewGround());
@@ -140,7 +139,7 @@ public class TreeGeneratorState extends BaseAppState {
new DirectionalLightShadowRenderer(assets, 2048, 1);
shadowRenderer.setLight(previewSunLight);
shadowRenderer.setEdgeFilteringMode(EdgeFilteringMode.PCF4);
shadowRenderer.setShadowIntensity(0.55f);
shadowRenderer.setShadowIntensity(0.25f);
shadowRenderer.setShadowZExtend(80f);
previewVP.addProcessor(shadowRenderer);
previewTransfer = new FrameTransfer(input.treePreviewImage);
@@ -386,7 +385,9 @@ public class TreeGeneratorState extends BaseAppState {
mat.setFloat("WindSpeed", 0.5f);
if (p.barkTexture != null) {
try {
mat.setTexture("BarkMap", assets.loadTexture(p.barkTexture));
Texture barkTex = assets.loadTexture(p.barkTexture);
barkTex.setWrap(Texture.WrapMode.Repeat);
mat.setTexture("BarkMap", barkTex);
mat.setBoolean("HasBarkMap", true);
} catch (Exception tex) {
System.err.println("[TreeGenerator] Bark-Textur nicht gefunden: " + p.barkTexture);
@@ -467,8 +468,8 @@ public class TreeGeneratorState extends BaseAppState {
// Capture-Szene: Kopien der Geometrien + Beleuchtung
Node scene = new Node("captureScene");
scene.addLight(new DirectionalLight(
new Vector3f(-0.4f, -1f, -0.5f).normalizeLocal(), ColorRGBA.White));
scene.addLight(new AmbientLight(new ColorRGBA(0.35f, 0.35f, 0.35f, 1f)));
new Vector3f(-0.4f, -1f, -0.5f).normalizeLocal(), new ColorRGBA(2.0f, 1.85f, 1.5f, 1f)));
scene.addLight(new AmbientLight(new ColorRGBA(0.60f, 0.60f, 0.60f, 1f)));
Node capTree = cloneForCapture(treeNode);
scene.attachChild(capTree);

View File

@@ -23,26 +23,99 @@ import java.util.Random;
public class PalmMeshBuilder {
public static Node build(PalmOptions opts) {
Random rng = new Random(opts.seed);
float[] azimuths = computeAzimuths(opts, rng);
float[] elevations = computeElevations(opts, rng);
int count = Math.max(1, Math.min(3, opts.palmCount));
float maxH = FastMath.tan(opts.lean * FastMath.DEG_TO_RAD);
float minH = count > 1 ? Math.max(maxH * 0.45f, FastMath.tan(10f * FastMath.DEG_TO_RAD)) : 0f;
float segSize = 0.5f;
int M = Math.max(4, Math.round(opts.trunkHeight / segSize));
float stepH = opts.trunkHeight / M;
// Eigener RNG pro Stamm
Random[] rngs = new Random[count];
for (int p = 0; p < count; p++)
rngs[p] = new Random(opts.seed + (long) p * 73856093L);
// Azimut-Sektoren + Startrichtungen
float[] sectorAz = new float[count];
float[] dX = new float[count];
float[] dZ = new float[count];
for (int p = 0; p < count; p++) {
sectorAz[p] = FastMath.TWO_PI * p / count
+ (rngs[p].nextFloat() - 0.5f) * (FastMath.TWO_PI / count) * 0.15f;
float initH = minH + rngs[p].nextFloat() * Math.max(0f, maxH - minH);
dX[p] = FastMath.cos(sectorAz[p]) * initH;
dZ[p] = FastMath.sin(sectorAz[p]) * initH;
}
// Alle Stämme gleichzeitig simulieren → ermöglicht Abstoßung
float[][] cx = new float[count][M + 1];
float[][] cz = new float[count][M + 1];
// Schwächerer Aufwärtsdrang bei Mehrstamm (höhere Dämpfung = mehr Trägheit der Richtung)
float damping = count > 1 ? 0.88f : 0.72f;
// Abstoßungskraft skaliert mit Neigungsstärke und Segmentgröße
float repStrength = count > 1 ? maxH * stepH * 0.40f : 0f;
for (int i = 1; i <= M; i++) {
for (int p = 0; p < count; p++) {
// Zufallswanderung mit reduzierterer Dämpfung
dX[p] = dX[p] * damping + (rngs[p].nextFloat() - 0.5f) * maxH * 0.9f;
dZ[p] = dZ[p] * damping + (rngs[p].nextFloat() - 0.5f) * maxH * 0.9f;
// Sektor-Bias (leichter als zuvor, Abstoßung übernimmt die Spreizung)
if (count > 1) {
dX[p] += FastMath.cos(sectorAz[p]) * minH * 0.10f;
dZ[p] += FastMath.sin(sectorAz[p]) * minH * 0.10f;
}
// Abstoßung von anderen Stämmen (Positionen des vorherigen Schritts)
for (int q = 0; q < count; q++) {
if (q == p) continue;
float rx = cx[p][i - 1] - cx[q][i - 1];
float rz = cz[p][i - 1] - cz[q][i - 1];
float d2 = Math.max(0.09f, rx * rx + rz * rz); // min. 0.3m Abstand
float d = FastMath.sqrt(d2);
float f = repStrength / d2;
dX[p] += (rx / d) * f;
dZ[p] += (rz / d) * f;
}
// Geschwindigkeit begrenzen (mehr Spielraum bei Mehrstamm)
float velMax = count > 1 ? maxH * 2.0f : maxH;
float hLen = FastMath.sqrt(dX[p] * dX[p] + dZ[p] * dZ[p]);
if (hLen > velMax) { dX[p] *= velMax / hLen; dZ[p] *= velMax / hLen; }
cx[p][i] = cx[p][i - 1] + dX[p] * stepH;
cz[p][i] = cz[p][i - 1] + dZ[p] * stepH;
}
}
Accum barkAcc = new Accum();
Accum crownAcc = new Accum();
Accum leafAcc = new Accum();
for (int p = 0; p < count; p++) {
float topX = cx[p][M], topZ = cz[p][M];
float[] azimuths = computeAzimuths(opts, rngs[p]);
float[] elevations = computeElevations(opts, rngs[p]);
float[] yOffsets = computeYOffsets(opts, elevations, rngs[p]);
addTrunk(barkAcc, opts, cx[p], cz[p], M, stepH);
addCrown(crownAcc, opts, topX, topZ);
addLeaves(leafAcc, opts, azimuths, elevations, yOffsets, topX, topZ);
}
Node palm = new Node("palm");
palm.attachChild(new Geometry("bark", buildBarkMesh(opts)));
palm.attachChild(new Geometry("leaves", buildLeafMesh(opts, azimuths, elevations)));
palm.attachChild(new Geometry("bark", barkAcc.toMesh()));
palm.attachChild(new Geometry("crown", crownAcc.toMesh()));
palm.attachChild(new Geometry("leaves", leafAcc.toMesh()));
return palm;
}
// ── Bark mesh: trunk only ─────────────────────────────────────────────────
private static Mesh buildBarkMesh(PalmOptions opts) {
Accum acc = new Accum();
addTrunk(acc, opts);
return acc.toMesh();
}
private static void addTrunk(Accum acc, PalmOptions opts) {
int M = opts.trunkSections;
private static void addTrunk(Accum acc, PalmOptions opts,
float[] cx, float[] cz, int M, float stepH) {
int N = opts.trunkSegments;
float H = opts.trunkHeight;
float r0 = opts.trunkRadiusBottom;
@@ -53,14 +126,14 @@ public class PalmMeshBuilder {
for (int i = 0; i <= M; i++) {
float t = (float) i / M;
float r = r0 + (r1 - r0) * t;
float y = H * t;
float wind = t * 0.4f; // trunk barely sways at root, moderate at crown
float y = stepH * i;
float wind = t * 0.4f;
for (int j = 0; j <= N; j++) {
float angle = FastMath.TWO_PI * j / N;
float nx = FastMath.cos(angle);
float nz = FastMath.sin(angle);
acc.add(nx * r, y, nz * r, nx, 0f, nz, (float) j / N, t, wind);
acc.add(cx[i] + nx * r, y, cz[i] + nz * r, nx, 0f, nz, (float) j / N, t, wind);
}
}
@@ -74,89 +147,168 @@ public class PalmMeshBuilder {
}
}
// ── Leaf mesh: horizontal bilateral leaflets (Wedel) ─────────────────────
// ── Kronenschaft: zuerst breiter, dann oben enger (Bauchprofil) ──────────
private static Mesh buildLeafMesh(PalmOptions opts, float[] azimuths, float[] elevations) {
Accum acc = new Accum();
for (int f = 0; f < opts.frondCount; f++) {
addFrondLeaflets(acc, opts, azimuths[f], elevations[f]);
private static void addCrown(Accum acc, PalmOptions opts, float topX, float topZ) {
int N = opts.trunkSegments;
int M = 8;
float yBot = opts.trunkHeight;
float yTop = opts.trunkHeight + opts.crownHeight;
float rBot = opts.trunkRadiusTop; // Übergang zum Stamm (unten)
float rMax = opts.trunkRadiusTop * opts.crownFlare; // Bauch
float rTop = opts.trunkRadiusTop * 0.55f; // Spitze oben (deutlich schmaler)
int base = acc.vertexCount;
for (int i = 0; i <= M; i++) {
float t = (float) i / M;
float y = yBot + (yTop - yBot) * t;
float r = crownRadius(t, rBot, rMax, rTop);
float wind = t * 0.12f;
for (int j = 0; j <= N; j++) {
float angle = FastMath.TWO_PI * j / N;
float nx = FastMath.cos(angle);
float nz = FastMath.sin(angle);
acc.add(topX + nx * r, y, topZ + nz * r, nx, 0f, nz, (float) j / N, t, wind);
}
}
for (int i = 0; i < M; i++) {
for (int j = 0; j < N; j++) {
int v00 = base + i * (N + 1) + j;
int v10 = base + (i + 1) * (N + 1) + j;
int v01 = base + i * (N + 1) + j + 1;
int v11 = base + (i + 1) * (N + 1) + j + 1;
acc.tri(v00, v10, v11);
acc.tri(v00, v11, v01);
}
}
return acc.toMesh();
}
private static void addFrondLeaflets(Accum acc, PalmOptions opts, float azimuth, float elevation) {
/** Bauchprofil: rBot unten → rMax bei ~40 % → rTop oben, sine-geglättet. */
private static float crownRadius(float t, float rBot, float rMax, float rTop) {
float peakT = 0.40f;
if (t <= peakT) {
return rBot + (rMax - rBot) * FastMath.sin(FastMath.HALF_PI * t / peakT);
} else {
return rTop + (rMax - rTop) * FastMath.sin(FastMath.HALF_PI * (1f - t) / (1f - peakT));
}
}
// ── Leaf mesh: ein Quad pro Wedel (Textur = vollständiger Wedel) ──────────
private static void addLeaves(Accum acc, PalmOptions opts, float[] azimuths,
float[] elevations, float[] yOffsets, float baseX, float baseZ) {
for (int f = 0; f < opts.frondCount; f++) {
addFrond(acc, opts, azimuths[f], elevations[f], yOffsets[f], baseX, baseZ);
}
}
private static final int SEGS = 6; // Längsunterteilungen
private static final int COLS = 3; // Spalten: linke Kante, Mitte, rechte Kante
private static void addFrond(Accum acc, PalmOptions opts, float azimuth, float elevation,
float yOffset, float baseX, float baseZ) {
float sinE = FastMath.sin(elevation);
float cosE = FastMath.cos(elevation);
float cosA = FastMath.cos(azimuth);
float sinA = FastMath.sin(azimuth);
float dx = sinE * cosA; // frond direction vector
float dx = sinE * cosA;
float dy = cosE;
float dz = sinE * sinA;
// Horizontal projection for leaflet orientation (keeps leaflets truly flat)
float hLen = FastMath.sqrt(dx * dx + dz * dz);
if (hLen < 1e-5f) hLen = 1e-5f;
float hx = dx / hLen;
float hz = dz / hLen;
float sx = -dz / hLen;
float sz = dx / hLen;
// Side direction perpendicular to frond in horizontal plane:
// (hx,0,hz) × (0,1,0) = (-hz, 0, hx)
float sx = -hz;
float sz = hx;
// Wedel starten an der Kronenspitze (XZ = Stammspitze + Lean-Offset)
float baseY = opts.trunkHeight + opts.crownHeight + yOffset;
int K = opts.frondLeafletPairs;
float step = opts.frondLength / K;
float halfW = step * 0.30f; // leaflet thickness along frond axis
float baseY = opts.trunkHeight;
// Größe abhängig vom Elevationswinkel: aufwärts = kleiner, hängend = volle Größe
float minRad = opts.frondAngleMin * FastMath.DEG_TO_RAD;
float maxRad = opts.frondAngleMax * FastMath.DEG_TO_RAD;
float droopT = (maxRad - minRad) > 1e-5f
? FastMath.clamp((elevation - minRad) / (maxRad - minRad), 0f, 1f)
: 1f;
float sizeScale = opts.frondMinSizeRatio + (1f - opts.frondMinSizeRatio) * droopT;
// Tip width is a fixed fraction of base width — gives natural taper
float sizeBase = opts.frondWidth;
float sizeTip = opts.frondWidth * 0.18f;
for (int k = 0; k < K; k++) {
float tPos = (float) k / Math.max(1, K - 1); // 0→1 along frond, starts at trunk
float leafSize = sizeBase + (sizeTip - sizeBase) * tPos;
float ax = dx * opts.frondLength * tPos;
float ay = baseY + dy * opts.frondLength * tPos;
float az = dz * opts.frondLength * tPos;
// Wind: 0 near trunk tip, more at frond tip; extra delta for outer leaflet edge
float windBase = tPos * 0.65f; // inner edge of leaflet
float windTip = tPos * 0.65f + 0.35f; // outer edge of leaflet (leaf tip)
addLeafletQuad(acc, ax, ay, az, hx, hz, sx, sz, leafSize, halfW, +1f, windBase, windTip);
addLeafletQuad(acc, ax, ay, az, hx, hz, sx, sz, leafSize, halfW, -1f, windBase, windTip);
}
}
private static void addLeafletQuad(Accum acc,
float ax, float ay, float az,
float hx, float hz,
float sx, float sz,
float leafSize, float halfW, float side,
float windInner, float windOuter) {
// All 4 vertices at Y = ay (truly horizontal, normal = +Y)
float p0x = ax - hx * halfW, p0z = az - hz * halfW;
float p1x = ax + hx * halfW, p1z = az + hz * halfW;
float p2x = p1x + sx * side * leafSize, p2z = p1z + sz * side * leafSize;
float p3x = p0x + sx * side * leafSize, p3z = p0z + sz * side * leafSize;
float scaledLength = opts.frondLength * sizeScale;
float halfW = opts.leafTextureAspect > 0f
? scaledLength * opts.leafTextureAspect * 0.5f
: opts.frondWidth * sizeScale * 0.5f;
boolean stemLeft = opts.leafTexture != null && opts.leafTexture.contains("palm2");
float g = opts.gravity;
int base = acc.vertexCount;
// p0, p1 = inner edge (attached to frond), p2, p3 = outer tip
acc.add(p0x, ay, p0z, 0f, 1f, 0f, 0f, 0f, windInner);
acc.add(p1x, ay, p1z, 0f, 1f, 0f, 1f, 0f, windInner);
acc.add(p2x, ay, p2z, 0f, 1f, 0f, 1f, 1f, windOuter);
acc.add(p3x, ay, p3z, 0f, 1f, 0f, 0f, 1f, windOuter);
// 7 Reihen × 3 Spalten
for (int row = 0; row <= SEGS; row++) {
float t = (float) row / SEGS; // 0 = Stamm, 1 = Spitze
acc.tri(base, base + 1, base + 2);
acc.tri(base, base + 2, base + 3);
// Position entlang Frond-Achse (Startpunkt = Lean-Offset der Stammspitze)
float fx = baseX + dx * scaledLength * t;
float fy = baseY + dy * scaledLength * t;
float fz = baseZ + dz * scaledLength * t;
// Längsdurchhang: quadratisch, wächst zur Spitze
float droopLen = g * t * t * scaledLength;
for (int col = 0; col < COLS; col++) {
float s = (float) col / (COLS - 1); // 0 = links, 1 = rechts
float side = s * 2f - 1f; // -1, 0, +1
// Seitenkanten hängen zusätzlich durch (nimmt zur Spitze zu)
float droopSide = g * FastMath.abs(side) * halfW * t;
// Breite konvergiert zur Stammspitze: bei t=0 alle Vertices im Zentrum
float x = fx + sx * halfW * side * t;
float y = fy - droopLen - droopSide;
float z = fz + sz * halfW * side * t;
float u = stemLeft ? t : s;
float v = stemLeft ? s : t;
acc.add(x, y, z, 0f, 1f, 0f, u, v, t * 0.8f);
}
}
// Dreiecke für 6×2 Zellen
for (int row = 0; row < SEGS; row++) {
for (int col = 0; col < COLS - 1; col++) {
int v00 = base + row * COLS + col;
int v10 = base + (row + 1) * COLS + col;
int v01 = base + row * COLS + col + 1;
int v11 = base + (row + 1) * COLS + col + 1;
acc.tri(v00, v10, v11);
acc.tri(v00, v11, v01);
}
}
}
// ── Random frond placement ────────────────────────────────────────────────
/**
* Vertikaler Versatz: Minimum (steil aufwärts) = Stammspitze (Offset 0),
* höhere Werte (stärker hängend) = unterhalb der Stammspitze (negativer Offset).
*/
private static float[] computeYOffsets(PalmOptions opts, float[] elevations, Random rng) {
float minRad = opts.frondAngleMin * FastMath.DEG_TO_RAD;
float maxRad = opts.frondAngleMax * FastMath.DEG_TO_RAD;
float maxDrop = opts.crownHeight * 0.6f; // Wedel bleiben innerhalb des Kronenschafts
float[] result = new float[opts.frondCount];
for (int f = 0; f < opts.frondCount; f++) {
float elevNorm = (maxRad - minRad) > 1e-5f
? FastMath.clamp((elevations[f] - minRad) / (maxRad - minRad), 0f, 1f)
: 0.5f;
// elevNorm 0 = aufwärts → Offset 0 (Stammspitze)
// elevNorm 1 = hängend → Offset -maxDrop (unterhalb der Stammspitze)
float base = -elevNorm * maxDrop;
float noise = (rng.nextFloat() - 0.5f) * opts.trunkRadiusTop * 0.5f;
result[f] = Math.min(0f, base + noise); // nie über Stammspitze hinaus
}
return result;
}
private static float[] computeAzimuths(PalmOptions opts, Random rng) {
float[] a = new float[opts.frondCount];
for (int f = 0; f < opts.frondCount; f++) {

View File

@@ -7,25 +7,36 @@ public class PalmOptions {
// Trunk
public float trunkHeight = 12f;
public float trunkRadiusBottom = 0.35f;
public float trunkRadiusTop = 0.30f; // tapers but stays thick
public float trunkRadiusTop = 0.175f; // default = trunkRadiusBottom / 2
public int trunkSections = 10;
public int trunkSegments = 8;
public float lean = 15f; // max. Neigungswinkel in Grad (0 = gerade, 50 = stark)
public int palmCount = 1; // 13 Stämme vom gleichen Ursprung
// Fronds
public int frondCount = 10;
public float frondAngleMin = 70f; // degrees from vertical (Y-up)
public float frondAngleMax = 110f;
public float frondLength = 6.5f;
public int frondLeafletPairs = 8;
public float frondWidth = 1.4f; // max leaflet width at frond base
public int frondCount = 80;
public float frondAngleMin = 45f; // degrees from vertical (Y-up)
public float frondAngleMax = 105f;
public float frondLength = 6.5f;
public float frondWidth = 1.4f;
public float frondMinSizeRatio = 0.4f; // Größe der aufwärtszeigenden Wedel relativ zu den hängenden (01)
// Colors
public float barkR = 0.68f, barkG = 0.54f, barkB = 0.34f;
public float leafR = 0.22f, leafG = 0.65f, leafB = 0.14f;
// Textures
public String barkTexture = "Textures/bark/Bark008_Color.jpg";
public String leafTexture = "Textures/leaves/palm.png";
public String barkTexture = "Textures/bark/Bark_Palm.png";
public String leafTexture = "Textures/leaves/palm.png";
public float leafTextureAspect = 0f; // width/length-Verhältnis der Textur, 0 = frondWidth nutzen
public float gravity = 0.3f; // Durchhang: 0 = gerade, höher = stärker hängend
// Kronenschaft
public float crownHeight = 1.2f;
public float crownFlare = 1.6f; // max. Aufweitung = trunkRadiusTop × crownFlare
public float crownR = 0.22f, crownG = 0.58f, crownB = 0.14f;
public String crownTexture = "Textures/leaves/palmcrown.png";
public PalmOptions copy() {
PalmOptions c = new PalmOptions();
@@ -35,16 +46,24 @@ public class PalmOptions {
c.trunkRadiusTop = trunkRadiusTop;
c.trunkSections = trunkSections;
c.trunkSegments = trunkSegments;
c.lean = lean;
c.palmCount = palmCount;
c.frondCount = frondCount;
c.frondAngleMin = frondAngleMin;
c.frondAngleMax = frondAngleMax;
c.frondLength = frondLength;
c.frondLeafletPairs = frondLeafletPairs;
c.frondWidth = frondWidth;
c.frondMinSizeRatio = frondMinSizeRatio;
c.barkR = barkR; c.barkG = barkG; c.barkB = barkB;
c.leafR = leafR; c.leafG = leafG; c.leafB = leafB;
c.barkTexture = barkTexture;
c.leafTexture = leafTexture;
c.barkTexture = barkTexture;
c.leafTexture = leafTexture;
c.leafTextureAspect = leafTextureAspect;
c.gravity = gravity;
c.crownHeight = crownHeight;
c.crownFlare = crownFlare;
c.crownR = crownR; c.crownG = crownG; c.crownB = crownB;
c.crownTexture = crownTexture;
return c;
}
}