Weiter das Palmen Tool verfeinert
4
.gitignore
vendored
@@ -1,6 +1,10 @@
|
|||||||
/.metadata/
|
/.metadata/
|
||||||
/.gradle/
|
/.gradle/
|
||||||
|
|
||||||
|
# Gradle build output
|
||||||
|
**/build/
|
||||||
|
**/config/
|
||||||
|
|
||||||
# Eclipse project files
|
# Eclipse project files
|
||||||
.project
|
.project
|
||||||
.classpath
|
.classpath
|
||||||
|
|||||||
BIN
blight-assets/src/main/resources/Textures/bark/Bark_Palm.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
BIN
blight-assets/src/main/resources/Textures/bark/Bark_Palm2.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 799 KiB After Width: | Height: | Size: 799 KiB |
BIN
blight-assets/src/main/resources/Textures/leaves/palmcrown.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
blight-editor/editor-assets/textures/impostor_Baum1.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
@@ -71,6 +71,11 @@ public class EditorApp extends Application {
|
|||||||
private PalmOptions palmOptions = new PalmOptions();
|
private PalmOptions palmOptions = new PalmOptions();
|
||||||
private final TextField palmNameField = new TextField("Palme1");
|
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
|
// Asset-Panel: Pfad-Map + DnD-Zustand
|
||||||
private final Map<TreeItem<String>, Path> itemPaths = new HashMap<>();
|
private final Map<TreeItem<String>, Path> itemPaths = new HashMap<>();
|
||||||
private TreeItem<String> draggedItem = null;
|
private TreeItem<String> draggedItem = null;
|
||||||
@@ -182,24 +187,28 @@ public class EditorApp extends Application {
|
|||||||
// ── Modus-Wechsel ────────────────────────────────────────────────────────
|
// ── Modus-Wechsel ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private void switchToTreeGenerator() {
|
private void switchToTreeGenerator() {
|
||||||
|
currentTool = "tree";
|
||||||
topBar.getChildren().set(1, buildTreeToolBar());
|
topBar.getChildren().set(1, buildTreeToolBar());
|
||||||
root.setCenter(buildTreePreviewPanel());
|
root.setCenter(buildTreePreviewPanel());
|
||||||
root.setRight(buildTreeParamsPanel());
|
root.setRight(buildTreeParamsPanel());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void switchToWorldEditor() {
|
private void switchToWorldEditor() {
|
||||||
|
currentTool = "world";
|
||||||
topBar.getChildren().set(1, worldToolBar);
|
topBar.getChildren().set(1, worldToolBar);
|
||||||
root.setCenter(worldViewport);
|
root.setCenter(worldViewport);
|
||||||
root.setRight(toolPanel);
|
root.setRight(toolPanel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void switchToEzTree() {
|
private void switchToEzTree() {
|
||||||
|
currentTool = "eztree";
|
||||||
topBar.getChildren().set(1, buildEzTreeToolBar());
|
topBar.getChildren().set(1, buildEzTreeToolBar());
|
||||||
root.setCenter(buildTreePreviewPanel()); // teilt Vorschau-Infrastruktur
|
root.setCenter(buildTreePreviewPanel());
|
||||||
root.setRight(buildEzTreeParamsPanel());
|
root.setRight(buildEzTreeParamsPanel());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void switchToPalm() {
|
private void switchToPalm() {
|
||||||
|
currentTool = "palm";
|
||||||
topBar.getChildren().set(1, buildPalmToolBar());
|
topBar.getChildren().set(1, buildPalmToolBar());
|
||||||
root.setCenter(buildTreePreviewPanel());
|
root.setCenter(buildTreePreviewPanel());
|
||||||
root.setRight(buildPalmParamsPanel());
|
root.setRight(buildPalmParamsPanel());
|
||||||
@@ -329,13 +338,14 @@ public class EditorApp extends Application {
|
|||||||
nameLabel.setStyle("-fx-font-weight: bold;");
|
nameLabel.setStyle("-fx-font-weight: bold;");
|
||||||
treeNameField.setPrefWidth(130);
|
treeNameField.setPrefWidth(130);
|
||||||
|
|
||||||
Button previewBtn = new Button("▶ Vorschau");
|
onF5 = () -> {
|
||||||
previewBtn.setStyle("-fx-font-weight: bold;");
|
|
||||||
previewBtn.setOnAction(e -> {
|
|
||||||
input.treeGenQueue.offer(
|
input.treeGenQueue.offer(
|
||||||
new SharedInput.TreeGenRequest(treeParams.copy(), false, treeNameField.getText().trim()));
|
new SharedInput.TreeGenRequest(treeParams.copy(), false, treeNameField.getText().trim()));
|
||||||
setStatus("Generiere Vorschau…");
|
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");
|
Button exportBtn = new Button("💾 Exportieren als .j3o");
|
||||||
exportBtn.setOnAction(e -> {
|
exportBtn.setOnAction(e -> {
|
||||||
@@ -414,7 +424,17 @@ public class EditorApp extends Application {
|
|||||||
seedSpinner.setEditable(true);
|
seedSpinner.setEditable(true);
|
||||||
seedSpinner.setMaxWidth(Double.MAX_VALUE);
|
seedSpinner.setMaxWidth(Double.MAX_VALUE);
|
||||||
seedSpinner.valueProperty().addListener((o, a, b) -> treeParams.seed = b);
|
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
|
// Ast-Tiefe
|
||||||
inner.getChildren().add(bold("Ast-Tiefe:"));
|
inner.getChildren().add(bold("Ast-Tiefe:"));
|
||||||
@@ -525,14 +545,15 @@ public class EditorApp extends Application {
|
|||||||
nameLabel.setStyle("-fx-font-weight: bold;");
|
nameLabel.setStyle("-fx-font-weight: bold;");
|
||||||
ezTreeNameField.setPrefWidth(130);
|
ezTreeNameField.setPrefWidth(130);
|
||||||
|
|
||||||
Button previewBtn = new Button("▶ Vorschau");
|
onF5 = () -> {
|
||||||
previewBtn.setStyle("-fx-font-weight: bold;");
|
|
||||||
previewBtn.setOnAction(e -> {
|
|
||||||
input.ezTreeGenQueue.offer(
|
input.ezTreeGenQueue.offer(
|
||||||
new SharedInput.EzTreeGenRequest(
|
new SharedInput.EzTreeGenRequest(
|
||||||
ezTreeOptions.copy(), false, ezTreeNameField.getText().trim()));
|
ezTreeOptions.copy(), false, ezTreeNameField.getText().trim()));
|
||||||
setStatus("EZ-Tree: generiere Vorschau…");
|
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");
|
Button exportBtn = new Button("💾 Export .j3o");
|
||||||
exportBtn.setOnAction(e -> {
|
exportBtn.setOnAction(e -> {
|
||||||
@@ -564,7 +585,17 @@ public class EditorApp extends Application {
|
|||||||
inner.getChildren().add(bold("Zufallssamen:"));
|
inner.getChildren().add(bold("Zufallssamen:"));
|
||||||
Spinner<Integer> seedSp = intSpinner(0, 999999, ezTreeOptions.seed);
|
Spinner<Integer> seedSp = intSpinner(0, 999999, ezTreeOptions.seed);
|
||||||
seedSp.valueProperty().addListener((o, a, b) -> ezTreeOptions.seed = b);
|
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:"));
|
inner.getChildren().add(bold("Typ:"));
|
||||||
ChoiceBox<String> typeCB = new ChoiceBox<>();
|
ChoiceBox<String> typeCB = new ChoiceBox<>();
|
||||||
@@ -728,14 +759,15 @@ public class EditorApp extends Application {
|
|||||||
nameLabel.setStyle("-fx-font-weight: bold;");
|
nameLabel.setStyle("-fx-font-weight: bold;");
|
||||||
palmNameField.setPrefWidth(130);
|
palmNameField.setPrefWidth(130);
|
||||||
|
|
||||||
Button previewBtn = new Button("▶ Vorschau");
|
onF5 = () -> {
|
||||||
previewBtn.setStyle("-fx-font-weight: bold;");
|
|
||||||
previewBtn.setOnAction(e -> {
|
|
||||||
input.palmGenQueue.offer(
|
input.palmGenQueue.offer(
|
||||||
new SharedInput.PalmGenRequest(
|
new SharedInput.PalmGenRequest(
|
||||||
palmOptions.copy(), false, palmNameField.getText().trim()));
|
palmOptions.copy(), false, palmNameField.getText().trim()));
|
||||||
setStatus("Palme: generiere Vorschau…");
|
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");
|
Button exportBtn = new Button("💾 Export .j3o");
|
||||||
exportBtn.setOnAction(e -> {
|
exportBtn.setOnAction(e -> {
|
||||||
@@ -763,37 +795,93 @@ public class EditorApp extends Application {
|
|||||||
inner.getChildren().add(bold("Zufallssamen:"));
|
inner.getChildren().add(bold("Zufallssamen:"));
|
||||||
Spinner<Integer> seedSp = intSpinner(0, 999999, palmOptions.seed);
|
Spinner<Integer> seedSp = intSpinner(0, 999999, palmOptions.seed);
|
||||||
seedSp.valueProperty().addListener((o, a, b) -> palmOptions.seed = b);
|
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().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,
|
inner.getChildren().add(ezFloat("Höhe:", 5, 25, palmOptions.trunkHeight,
|
||||||
v -> palmOptions.trunkHeight = v));
|
v -> palmOptions.trunkHeight = v));
|
||||||
inner.getChildren().add(ezFloat("Radius unten:", 0.1, 1.0, palmOptions.trunkRadiusBottom,
|
inner.getChildren().add(ezFloat("Radius unten:", 0.1, 1.0, palmOptions.trunkRadiusBottom,
|
||||||
v -> palmOptions.trunkRadiusBottom = v));
|
v -> palmOptions.trunkRadiusBottom = v));
|
||||||
inner.getChildren().add(ezFloat("Radius oben:", 0.05, 0.9, palmOptions.trunkRadiusTop,
|
inner.getChildren().add(ezFloat("Radius oben:", 0.05, 0.9, palmOptions.trunkRadiusTop,
|
||||||
v -> palmOptions.trunkRadiusTop = v));
|
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().addAll(sectionTitle("Wedel"), new Separator());
|
||||||
inner.getChildren().add(bold("Anzahl:"));
|
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);
|
frondCountSp.valueProperty().addListener((o, a, b) -> palmOptions.frondCount = b);
|
||||||
inner.getChildren().add(frondCountSp);
|
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));
|
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));
|
v -> palmOptions.frondAngleMax = v));
|
||||||
inner.getChildren().add(ezFloat("Länge:", 2, 12, palmOptions.frondLength,
|
inner.getChildren().add(ezFloat("Länge:", 2, 12, palmOptions.frondLength,
|
||||||
v -> palmOptions.frondLength = v));
|
v -> palmOptions.frondLength = v));
|
||||||
|
|
||||||
inner.getChildren().addAll(sectionTitle("Blätter (Fiederblättchen)"), new Separator());
|
inner.getChildren().addAll(sectionTitle("Blätter"), new Separator());
|
||||||
inner.getChildren().add(bold("Paare pro Wedel:"));
|
inner.getChildren().add(ezFloat("Schwerkraft:", 0.0, 2.0, palmOptions.gravity,
|
||||||
Spinner<Integer> pairsSp = intSpinner(3, 16, palmOptions.frondLeafletPairs);
|
v -> palmOptions.gravity = v));
|
||||||
pairsSp.valueProperty().addListener((o, a, b) -> palmOptions.frondLeafletPairs = b);
|
inner.getChildren().add(ezFloat("Größe oben (relativ):", 0.0, 1.0, palmOptions.frondMinSizeRatio,
|
||||||
inner.getChildren().add(pairsSp);
|
v -> palmOptions.frondMinSizeRatio = v));
|
||||||
inner.getChildren().add(ezFloat("Wedel-Breite:", 0.2, 4.0, palmOptions.frondWidth,
|
|
||||||
v -> palmOptions.frondWidth = v));
|
|
||||||
|
|
||||||
inner.getChildren().addAll(sectionTitle("Farben"), new Separator());
|
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 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 G:", 0, 1, palmOptions.barkG, v -> palmOptions.barkG = v));
|
||||||
inner.getChildren().add(ezFloat("Stamm B:", 0, 1, palmOptions.barkB, v -> palmOptions.barkB = 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 D -> input.right = pressed;
|
||||||
case Q -> input.up = pressed;
|
case Q -> input.up = pressed;
|
||||||
case E -> input.down = pressed;
|
case E -> input.down = pressed;
|
||||||
|
case F5 -> { if (pressed) onF5.run(); }
|
||||||
|
case F6 -> { if (pressed) onF6.run(); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import com.jme3.scene.Node;
|
|||||||
import com.jme3.scene.Spatial;
|
import com.jme3.scene.Spatial;
|
||||||
import com.jme3.texture.FrameBuffer;
|
import com.jme3.texture.FrameBuffer;
|
||||||
import com.jme3.texture.Image;
|
import com.jme3.texture.Image;
|
||||||
|
import com.jme3.texture.Texture;
|
||||||
import com.jme3.texture.Texture2D;
|
import com.jme3.texture.Texture2D;
|
||||||
import com.jme3.util.BufferUtils;
|
import com.jme3.util.BufferUtils;
|
||||||
import de.blight.editor.SharedInput;
|
import de.blight.editor.SharedInput;
|
||||||
@@ -195,7 +196,9 @@ public class EzTreeState extends BaseAppState {
|
|||||||
mat.setFloat("WindSpeed", 0.5f);
|
mat.setFloat("WindSpeed", 0.5f);
|
||||||
if (opts.bark.textureFile != null) {
|
if (opts.bark.textureFile != null) {
|
||||||
try {
|
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);
|
mat.setBoolean("HasBarkMap", true);
|
||||||
} catch (Exception ignored) {}
|
} catch (Exception ignored) {}
|
||||||
}
|
}
|
||||||
@@ -249,8 +252,8 @@ public class EzTreeState extends BaseAppState {
|
|||||||
|
|
||||||
Node scene = new Node("ezCapScene");
|
Node scene = new Node("ezCapScene");
|
||||||
scene.addLight(new DirectionalLight(
|
scene.addLight(new DirectionalLight(
|
||||||
new Vector3f(-0.4f, -1f, -0.5f).normalizeLocal(), ColorRGBA.White));
|
new Vector3f(-0.4f, -1f, -0.5f).normalizeLocal(), new ColorRGBA(2.0f, 1.85f, 1.5f, 1f)));
|
||||||
scene.addLight(new AmbientLight(new ColorRGBA(0.35f, 0.35f, 0.35f, 1f)));
|
scene.addLight(new AmbientLight(new ColorRGBA(0.60f, 0.60f, 0.60f, 1f)));
|
||||||
scene.attachChild(cloneForCapture(src));
|
scene.attachChild(cloneForCapture(src));
|
||||||
vp.attachScene(scene);
|
vp.attachScene(scene);
|
||||||
scene.updateGeometricState();
|
scene.updateGeometricState();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.jme3.bounding.BoundingBox;
|
|||||||
import com.jme3.export.binary.BinaryExporter;
|
import com.jme3.export.binary.BinaryExporter;
|
||||||
import com.jme3.material.Material;
|
import com.jme3.material.Material;
|
||||||
import com.jme3.material.RenderState;
|
import com.jme3.material.RenderState;
|
||||||
|
import com.jme3.texture.Texture;
|
||||||
import com.jme3.math.ColorRGBA;
|
import com.jme3.math.ColorRGBA;
|
||||||
import com.jme3.math.Vector3f;
|
import com.jme3.math.Vector3f;
|
||||||
import com.jme3.renderer.queue.RenderQueue;
|
import com.jme3.renderer.queue.RenderQueue;
|
||||||
@@ -55,8 +56,19 @@ public class PalmGeneratorState extends BaseAppState {
|
|||||||
SharedInput.PalmGenRequest req = input.palmGenQueue.poll();
|
SharedInput.PalmGenRequest req = input.palmGenQueue.poll();
|
||||||
if (req == null) return;
|
if (req == null) return;
|
||||||
|
|
||||||
Node palm = PalmMeshBuilder.build(req.options());
|
PalmOptions opts = req.options();
|
||||||
applyMaterials(palm, 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();
|
palm.updateGeometricState();
|
||||||
|
|
||||||
BoundingBox bb = palm.getWorldBound() instanceof BoundingBox b ? b : null;
|
BoundingBox bb = palm.getWorldBound() instanceof BoundingBox b ? b : null;
|
||||||
@@ -92,6 +104,10 @@ public class PalmGeneratorState extends BaseAppState {
|
|||||||
g.setMaterial(buildBarkMat(opts));
|
g.setMaterial(buildBarkMat(opts));
|
||||||
g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
|
g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
|
||||||
}
|
}
|
||||||
|
case "crown" -> {
|
||||||
|
g.setMaterial(buildCrownMat(opts));
|
||||||
|
g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
|
||||||
|
}
|
||||||
case "leaves" -> {
|
case "leaves" -> {
|
||||||
g.setMaterial(buildLeafMat(opts));
|
g.setMaterial(buildLeafMat(opts));
|
||||||
g.setQueueBucket(RenderQueue.Bucket.Transparent);
|
g.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||||
@@ -109,7 +125,9 @@ public class PalmGeneratorState extends BaseAppState {
|
|||||||
mat.setFloat("WindSpeed", 0.4f);
|
mat.setFloat("WindSpeed", 0.4f);
|
||||||
if (opts.barkTexture != null) {
|
if (opts.barkTexture != null) {
|
||||||
try {
|
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);
|
mat.setBoolean("HasBarkMap", true);
|
||||||
} catch (Exception ignored) {}
|
} 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) {
|
private void exportPalm(Node palmNode, String name) {
|
||||||
try {
|
try {
|
||||||
Path modelDir = ASSET_ROOT.resolve("models");
|
Path modelDir = ASSET_ROOT.resolve("models");
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ public class TreeGeneratorState extends BaseAppState {
|
|||||||
|
|
||||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
@Override
|
@Override
|
||||||
protected void initialize(Application app) {
|
protected void initialize(Application app) {
|
||||||
this.app = (SimpleApplication) app;
|
this.app = (SimpleApplication) app;
|
||||||
@@ -124,12 +123,12 @@ public class TreeGeneratorState extends BaseAppState {
|
|||||||
previewScene = new Node("previewScene");
|
previewScene = new Node("previewScene");
|
||||||
previewSunLight = new DirectionalLight(
|
previewSunLight = new DirectionalLight(
|
||||||
new Vector3f(-0.45f, -1.0f, -0.3f).normalizeLocal(),
|
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(previewSunLight);
|
||||||
previewScene.addLight(new DirectionalLight(
|
previewScene.addLight(new DirectionalLight(
|
||||||
new Vector3f(0.4f, -0.6f, -0.8f).normalizeLocal(),
|
new Vector3f(0.4f, -0.6f, -0.8f).normalizeLocal(),
|
||||||
new ColorRGBA(0.35f, 0.40f, 0.55f, 1f)));
|
new ColorRGBA(0.70f, 0.75f, 1.0f, 1f)));
|
||||||
previewScene.addLight(new AmbientLight(new ColorRGBA(0.25f, 0.25f, 0.30f, 1f)));
|
previewScene.addLight(new AmbientLight(new ColorRGBA(0.55f, 0.55f, 0.60f, 1f)));
|
||||||
previewTreeHolder = new Node("treeHolder");
|
previewTreeHolder = new Node("treeHolder");
|
||||||
previewScene.attachChild(previewTreeHolder);
|
previewScene.attachChild(previewTreeHolder);
|
||||||
previewScene.attachChild(buildPreviewGround());
|
previewScene.attachChild(buildPreviewGround());
|
||||||
@@ -140,7 +139,7 @@ public class TreeGeneratorState extends BaseAppState {
|
|||||||
new DirectionalLightShadowRenderer(assets, 2048, 1);
|
new DirectionalLightShadowRenderer(assets, 2048, 1);
|
||||||
shadowRenderer.setLight(previewSunLight);
|
shadowRenderer.setLight(previewSunLight);
|
||||||
shadowRenderer.setEdgeFilteringMode(EdgeFilteringMode.PCF4);
|
shadowRenderer.setEdgeFilteringMode(EdgeFilteringMode.PCF4);
|
||||||
shadowRenderer.setShadowIntensity(0.55f);
|
shadowRenderer.setShadowIntensity(0.25f);
|
||||||
shadowRenderer.setShadowZExtend(80f);
|
shadowRenderer.setShadowZExtend(80f);
|
||||||
previewVP.addProcessor(shadowRenderer);
|
previewVP.addProcessor(shadowRenderer);
|
||||||
previewTransfer = new FrameTransfer(input.treePreviewImage);
|
previewTransfer = new FrameTransfer(input.treePreviewImage);
|
||||||
@@ -386,7 +385,9 @@ public class TreeGeneratorState extends BaseAppState {
|
|||||||
mat.setFloat("WindSpeed", 0.5f);
|
mat.setFloat("WindSpeed", 0.5f);
|
||||||
if (p.barkTexture != null) {
|
if (p.barkTexture != null) {
|
||||||
try {
|
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);
|
mat.setBoolean("HasBarkMap", true);
|
||||||
} catch (Exception tex) {
|
} catch (Exception tex) {
|
||||||
System.err.println("[TreeGenerator] Bark-Textur nicht gefunden: " + p.barkTexture);
|
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
|
// Capture-Szene: Kopien der Geometrien + Beleuchtung
|
||||||
Node scene = new Node("captureScene");
|
Node scene = new Node("captureScene");
|
||||||
scene.addLight(new DirectionalLight(
|
scene.addLight(new DirectionalLight(
|
||||||
new Vector3f(-0.4f, -1f, -0.5f).normalizeLocal(), ColorRGBA.White));
|
new Vector3f(-0.4f, -1f, -0.5f).normalizeLocal(), new ColorRGBA(2.0f, 1.85f, 1.5f, 1f)));
|
||||||
scene.addLight(new AmbientLight(new ColorRGBA(0.35f, 0.35f, 0.35f, 1f)));
|
scene.addLight(new AmbientLight(new ColorRGBA(0.60f, 0.60f, 0.60f, 1f)));
|
||||||
|
|
||||||
Node capTree = cloneForCapture(treeNode);
|
Node capTree = cloneForCapture(treeNode);
|
||||||
scene.attachChild(capTree);
|
scene.attachChild(capTree);
|
||||||
|
|||||||
@@ -23,26 +23,99 @@ import java.util.Random;
|
|||||||
public class PalmMeshBuilder {
|
public class PalmMeshBuilder {
|
||||||
|
|
||||||
public static Node build(PalmOptions opts) {
|
public static Node build(PalmOptions opts) {
|
||||||
Random rng = new Random(opts.seed);
|
int count = Math.max(1, Math.min(3, opts.palmCount));
|
||||||
float[] azimuths = computeAzimuths(opts, rng);
|
float maxH = FastMath.tan(opts.lean * FastMath.DEG_TO_RAD);
|
||||||
float[] elevations = computeElevations(opts, rng);
|
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");
|
Node palm = new Node("palm");
|
||||||
palm.attachChild(new Geometry("bark", buildBarkMesh(opts)));
|
palm.attachChild(new Geometry("bark", barkAcc.toMesh()));
|
||||||
palm.attachChild(new Geometry("leaves", buildLeafMesh(opts, azimuths, elevations)));
|
palm.attachChild(new Geometry("crown", crownAcc.toMesh()));
|
||||||
|
palm.attachChild(new Geometry("leaves", leafAcc.toMesh()));
|
||||||
return palm;
|
return palm;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Bark mesh: trunk only ─────────────────────────────────────────────────
|
// ── Bark mesh: trunk only ─────────────────────────────────────────────────
|
||||||
|
|
||||||
private static Mesh buildBarkMesh(PalmOptions opts) {
|
private static void addTrunk(Accum acc, PalmOptions opts,
|
||||||
Accum acc = new Accum();
|
float[] cx, float[] cz, int M, float stepH) {
|
||||||
addTrunk(acc, opts);
|
|
||||||
return acc.toMesh();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void addTrunk(Accum acc, PalmOptions opts) {
|
|
||||||
int M = opts.trunkSections;
|
|
||||||
int N = opts.trunkSegments;
|
int N = opts.trunkSegments;
|
||||||
float H = opts.trunkHeight;
|
float H = opts.trunkHeight;
|
||||||
float r0 = opts.trunkRadiusBottom;
|
float r0 = opts.trunkRadiusBottom;
|
||||||
@@ -53,14 +126,14 @@ public class PalmMeshBuilder {
|
|||||||
for (int i = 0; i <= M; i++) {
|
for (int i = 0; i <= M; i++) {
|
||||||
float t = (float) i / M;
|
float t = (float) i / M;
|
||||||
float r = r0 + (r1 - r0) * t;
|
float r = r0 + (r1 - r0) * t;
|
||||||
float y = H * t;
|
float y = stepH * i;
|
||||||
float wind = t * 0.4f; // trunk barely sways at root, moderate at crown
|
float wind = t * 0.4f;
|
||||||
|
|
||||||
for (int j = 0; j <= N; j++) {
|
for (int j = 0; j <= N; j++) {
|
||||||
float angle = FastMath.TWO_PI * j / N;
|
float angle = FastMath.TWO_PI * j / N;
|
||||||
float nx = FastMath.cos(angle);
|
float nx = FastMath.cos(angle);
|
||||||
float nz = FastMath.sin(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) {
|
private static void addCrown(Accum acc, PalmOptions opts, float topX, float topZ) {
|
||||||
Accum acc = new Accum();
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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++) {
|
for (int f = 0; f < opts.frondCount; f++) {
|
||||||
addFrondLeaflets(acc, opts, azimuths[f], elevations[f]);
|
addFrond(acc, opts, azimuths[f], elevations[f], yOffsets[f], baseX, baseZ);
|
||||||
}
|
}
|
||||||
return acc.toMesh();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void addFrondLeaflets(Accum acc, PalmOptions opts, float azimuth, float elevation) {
|
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 sinE = FastMath.sin(elevation);
|
||||||
float cosE = FastMath.cos(elevation);
|
float cosE = FastMath.cos(elevation);
|
||||||
float cosA = FastMath.cos(azimuth);
|
float cosA = FastMath.cos(azimuth);
|
||||||
float sinA = FastMath.sin(azimuth);
|
float sinA = FastMath.sin(azimuth);
|
||||||
|
|
||||||
float dx = sinE * cosA; // frond direction vector
|
float dx = sinE * cosA;
|
||||||
float dy = cosE;
|
float dy = cosE;
|
||||||
float dz = sinE * sinA;
|
float dz = sinE * sinA;
|
||||||
|
|
||||||
// Horizontal projection for leaflet orientation (keeps leaflets truly flat)
|
|
||||||
float hLen = FastMath.sqrt(dx * dx + dz * dz);
|
float hLen = FastMath.sqrt(dx * dx + dz * dz);
|
||||||
if (hLen < 1e-5f) hLen = 1e-5f;
|
if (hLen < 1e-5f) hLen = 1e-5f;
|
||||||
float hx = dx / hLen;
|
float sx = -dz / hLen;
|
||||||
float hz = dz / hLen;
|
float sz = dx / hLen;
|
||||||
|
|
||||||
// Side direction perpendicular to frond in horizontal plane:
|
// Wedel starten an der Kronenspitze (XZ = Stammspitze + Lean-Offset)
|
||||||
// (hx,0,hz) × (0,1,0) = (-hz, 0, hx)
|
float baseY = opts.trunkHeight + opts.crownHeight + yOffset;
|
||||||
float sx = -hz;
|
|
||||||
float sz = hx;
|
|
||||||
|
|
||||||
int K = opts.frondLeafletPairs;
|
// Größe abhängig vom Elevationswinkel: aufwärts = kleiner, hängend = volle Größe
|
||||||
float step = opts.frondLength / K;
|
float minRad = opts.frondAngleMin * FastMath.DEG_TO_RAD;
|
||||||
float halfW = step * 0.30f; // leaflet thickness along frond axis
|
float maxRad = opts.frondAngleMax * FastMath.DEG_TO_RAD;
|
||||||
float baseY = opts.trunkHeight;
|
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 scaledLength = opts.frondLength * sizeScale;
|
||||||
float sizeBase = opts.frondWidth;
|
float halfW = opts.leafTextureAspect > 0f
|
||||||
float sizeTip = opts.frondWidth * 0.18f;
|
? scaledLength * opts.leafTextureAspect * 0.5f
|
||||||
|
: opts.frondWidth * sizeScale * 0.5f;
|
||||||
for (int k = 0; k < K; k++) {
|
boolean stemLeft = opts.leafTexture != null && opts.leafTexture.contains("palm2");
|
||||||
float tPos = (float) k / Math.max(1, K - 1); // 0→1 along frond, starts at trunk
|
float g = opts.gravity;
|
||||||
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;
|
|
||||||
|
|
||||||
int base = acc.vertexCount;
|
int base = acc.vertexCount;
|
||||||
|
|
||||||
// p0, p1 = inner edge (attached to frond), p2, p3 = outer tip
|
// 7 Reihen × 3 Spalten
|
||||||
acc.add(p0x, ay, p0z, 0f, 1f, 0f, 0f, 0f, windInner);
|
for (int row = 0; row <= SEGS; row++) {
|
||||||
acc.add(p1x, ay, p1z, 0f, 1f, 0f, 1f, 0f, windInner);
|
float t = (float) row / SEGS; // 0 = Stamm, 1 = Spitze
|
||||||
acc.add(p2x, ay, p2z, 0f, 1f, 0f, 1f, 1f, windOuter);
|
|
||||||
acc.add(p3x, ay, p3z, 0f, 1f, 0f, 0f, 1f, windOuter);
|
|
||||||
|
|
||||||
acc.tri(base, base + 1, base + 2);
|
// Position entlang Frond-Achse (Startpunkt = Lean-Offset der Stammspitze)
|
||||||
acc.tri(base, base + 2, base + 3);
|
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 ────────────────────────────────────────────────
|
// ── 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) {
|
private static float[] computeAzimuths(PalmOptions opts, Random rng) {
|
||||||
float[] a = new float[opts.frondCount];
|
float[] a = new float[opts.frondCount];
|
||||||
for (int f = 0; f < opts.frondCount; f++) {
|
for (int f = 0; f < opts.frondCount; f++) {
|
||||||
|
|||||||
@@ -7,25 +7,36 @@ public class PalmOptions {
|
|||||||
// Trunk
|
// Trunk
|
||||||
public float trunkHeight = 12f;
|
public float trunkHeight = 12f;
|
||||||
public float trunkRadiusBottom = 0.35f;
|
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 trunkSections = 10;
|
||||||
public int trunkSegments = 8;
|
public int trunkSegments = 8;
|
||||||
|
public float lean = 15f; // max. Neigungswinkel in Grad (0 = gerade, 50 = stark)
|
||||||
|
public int palmCount = 1; // 1–3 Stämme vom gleichen Ursprung
|
||||||
|
|
||||||
// Fronds
|
// Fronds
|
||||||
public int frondCount = 10;
|
public int frondCount = 80;
|
||||||
public float frondAngleMin = 70f; // degrees from vertical (Y-up)
|
public float frondAngleMin = 45f; // degrees from vertical (Y-up)
|
||||||
public float frondAngleMax = 110f;
|
public float frondAngleMax = 105f;
|
||||||
public float frondLength = 6.5f;
|
public float frondLength = 6.5f;
|
||||||
public int frondLeafletPairs = 8;
|
public float frondWidth = 1.4f;
|
||||||
public float frondWidth = 1.4f; // max leaflet width at frond base
|
public float frondMinSizeRatio = 0.4f; // Größe der aufwärtszeigenden Wedel relativ zu den hängenden (0–1)
|
||||||
|
|
||||||
// Colors
|
// Colors
|
||||||
public float barkR = 0.68f, barkG = 0.54f, barkB = 0.34f;
|
public float barkR = 0.68f, barkG = 0.54f, barkB = 0.34f;
|
||||||
public float leafR = 0.22f, leafG = 0.65f, leafB = 0.14f;
|
public float leafR = 0.22f, leafG = 0.65f, leafB = 0.14f;
|
||||||
|
|
||||||
// Textures
|
// Textures
|
||||||
public String barkTexture = "Textures/bark/Bark008_Color.jpg";
|
public String barkTexture = "Textures/bark/Bark_Palm.png";
|
||||||
public String leafTexture = "Textures/leaves/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() {
|
public PalmOptions copy() {
|
||||||
PalmOptions c = new PalmOptions();
|
PalmOptions c = new PalmOptions();
|
||||||
@@ -35,16 +46,24 @@ public class PalmOptions {
|
|||||||
c.trunkRadiusTop = trunkRadiusTop;
|
c.trunkRadiusTop = trunkRadiusTop;
|
||||||
c.trunkSections = trunkSections;
|
c.trunkSections = trunkSections;
|
||||||
c.trunkSegments = trunkSegments;
|
c.trunkSegments = trunkSegments;
|
||||||
|
c.lean = lean;
|
||||||
|
c.palmCount = palmCount;
|
||||||
c.frondCount = frondCount;
|
c.frondCount = frondCount;
|
||||||
c.frondAngleMin = frondAngleMin;
|
c.frondAngleMin = frondAngleMin;
|
||||||
c.frondAngleMax = frondAngleMax;
|
c.frondAngleMax = frondAngleMax;
|
||||||
c.frondLength = frondLength;
|
c.frondLength = frondLength;
|
||||||
c.frondLeafletPairs = frondLeafletPairs;
|
|
||||||
c.frondWidth = frondWidth;
|
c.frondWidth = frondWidth;
|
||||||
|
c.frondMinSizeRatio = frondMinSizeRatio;
|
||||||
c.barkR = barkR; c.barkG = barkG; c.barkB = barkB;
|
c.barkR = barkR; c.barkG = barkG; c.barkB = barkB;
|
||||||
c.leafR = leafR; c.leafG = leafG; c.leafB = leafB;
|
c.leafR = leafR; c.leafG = leafG; c.leafB = leafB;
|
||||||
c.barkTexture = barkTexture;
|
c.barkTexture = barkTexture;
|
||||||
c.leafTexture = leafTexture;
|
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;
|
return c;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
jmeVersion = '3.7.0-stable'
|
jmeVersion = '3.9.0-stable'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ subprojects {
|
|||||||
apply plugin: 'java'
|
apply plugin: 'java'
|
||||||
|
|
||||||
java {
|
java {
|
||||||
sourceCompatibility = JavaVersion.VERSION_21
|
sourceCompatibility = JavaVersion.VERSION_26
|
||||||
targetCompatibility = JavaVersion.VERSION_21
|
targetCompatibility = JavaVersion.VERSION_26
|
||||||
}
|
}
|
||||||
|
|
||||||
compileJava.options.encoding = 'UTF-8'
|
compileJava.options.encoding = 'UTF-8'
|
||||||
|
|||||||