Bugfixes, weitere Vegetationsgeneratoren hinzugefügt

This commit is contained in:
2026-07-25 20:16:13 +02:00
parent 574bad3d67
commit d7aeb890c9
66 changed files with 2379 additions and 173 deletions

View File

@@ -7,9 +7,9 @@ MaterialDef Tree {
Vector2 WindDir : 0.0 1.0 Vector2 WindDir : 0.0 1.0
Texture2D BarkMap Texture2D BarkMap
Boolean HasBarkMap : false Boolean HasBarkMap : false
Vector3 LightDir : 0.6 -0.8 0.4 Vector3 LightDir : 0.45 1.0 0.3
Vector3 SunColor : 0.68 0.65 0.60 Vector3 SunColor : 1.4 1.3 1.1
Vector3 AmbientColor : 0.08 0.10 0.16 Vector3 AmbientColor : 0.18 0.18 0.22
// Vom Shadow-Renderer befüllt (PostShadow-Pass) // Vom Shadow-Renderer befüllt (PostShadow-Pass)
Int BoundDrawBuffer Int BoundDrawBuffer

View File

@@ -29,9 +29,9 @@ MaterialDef TreeLeaf {
Matrix4 LightViewProjectionMatrix4 Matrix4 LightViewProjectionMatrix4
Matrix4 LightViewProjectionMatrix5 Matrix4 LightViewProjectionMatrix5
Vector3 LightPos Vector3 LightPos
Vector3 LightDir : 0.6 -0.8 0.4 Vector3 LightDir : 0.45 1.0 0.3
Vector3 SunColor : 0.68 0.65 0.60 Vector3 SunColor : 1.4 1.3 1.1
Vector3 AmbientColor : 0.08 0.10 0.16 Vector3 AmbientColor : 0.18 0.18 0.22
Float PCFEdge Float PCFEdge
Float ShadowMapSize Float ShadowMapSize
Boolean BackfaceShadows : false Boolean BackfaceShadows : false

View File

@@ -2,6 +2,10 @@ MaterialDef Voxel {
MaterialParameters { MaterialParameters {
Texture2D TexFlat Texture2D TexFlat
Texture2D TexFlat2
Texture2D TexFlat3
Texture2D TexFlat4
Texture2D SplatMap
Texture2D TexSteep Texture2D TexSteep
Texture2D NormalMapFlat Texture2D NormalMapFlat
Texture2D NormalMapSteep Texture2D NormalMapSteep
@@ -32,6 +36,7 @@ MaterialDef Voxel {
HAS_LIGHTDIR : LightDir HAS_LIGHTDIR : LightDir
HAS_SCENE_LIGHT : SunColor HAS_SCENE_LIGHT : SunColor
DEBUG_NO_LIGHT : DebugNoLight DEBUG_NO_LIGHT : DebugNoLight
HAS_SPLAT : SplatMap
} }
RenderState { RenderState {
@@ -60,6 +65,7 @@ MaterialDef Voxel {
HAS_DISP_STEEP : DisplacementMapSteep HAS_DISP_STEEP : DisplacementMapSteep
HAS_LIGHTDIR : LightDir HAS_LIGHTDIR : LightDir
HAS_SCENE_LIGHT : SunColor HAS_SCENE_LIGHT : SunColor
HAS_SPLAT : SplatMap
} }
RenderState { RenderState {

View File

@@ -0,0 +1,28 @@
#Fri Jul 24 16:47:20 CEST 2026
attachedEmitters.count=0
attachedLights.count=0
castShadow=true
category=
cullDistance=120.0
footstepSurface=
interactableOffsetX=0.0
interactableOffsetY=0.5
interactableOffsetZ=0.0
interactableRotY=0.0
interactableType=NONE
lod1Distance=30.0
lod1Path=
lod2Distance=80.0
lod2Path=
name=bridge1
pivotOffsetY=0.0
placementOffsetY=0.0
randomScaleMax=1.0
randomScaleMin=1.0
receiveShadow=true
scaleX=1.0
scaleY=1.0
scaleZ=1.0
solid=true
tags=
uniformScale=true

View File

@@ -25,14 +25,20 @@ void main() {
vec2 windN = (dot(m_WindDir, m_WindDir) > 0.001) ? normalize(m_WindDir) : vec2(0.0, 1.0); vec2 windN = (dot(m_WindDir, m_WindDir) > 0.001) ? normalize(m_WindDir) : vec2(0.0, 1.0);
vec2 perpN = vec2(-windN.y, windN.x); vec2 perpN = vec2(-windN.y, windN.x);
float wavePhase = dot(worldXZ, windN); float wavePhase = dot(worldXZ, windN);
float randPhase = fract(sin(dot(worldXZ, vec2(127.1, 311.7))) * 43758.5453) * 6.2832; // 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);
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; float mainSway = sin(t + wavePhase * 0.08 + randPhase) * windW * m_WindStrength;
float crossSway = cos(t * 0.73 + wavePhase * 0.06 + randPhase) * windW * m_WindStrength * 0.25; float crossSway = cos(t * 0.73 + wavePhase * 0.06 + randPhase) * windW * m_WindStrength * 0.25;
// Y-Kompression: Äste neigen sich statt zu strecken → verhindert Breiterwerden der Spitzen
float sway2 = mainSway * mainSway + crossSway * crossSway;
vec3 animPos = inPosition + vec3( vec3 animPos = inPosition + vec3(
windN.x * mainSway + perpN.x * crossSway, windN.x * mainSway + perpN.x * crossSway,
0.0, -sway2 * 0.5,
windN.y * mainSway + perpN.y * crossSway windN.y * mainSway + perpN.y * crossSway
); );

View File

@@ -1,5 +1,12 @@
uniform sampler2D m_TexFlat; uniform sampler2D m_TexFlat;
uniform sampler2D m_TexSteep; uniform sampler2D m_TexSteep;
#ifdef HAS_SPLAT
uniform sampler2D m_SplatMap;
uniform sampler2D m_TexFlat2;
uniform sampler2D m_TexFlat3;
uniform sampler2D m_TexFlat4;
#endif
uniform float m_TexScale; uniform float m_TexScale;
#ifdef HAS_NM_FLAT #ifdef HAS_NM_FLAT
@@ -57,7 +64,22 @@ void main() {
// Flat: reines XZ-UV wie das Terrain (uvY = worldPos.xz / texScale), kein Triplanar. // Flat: reines XZ-UV wie das Terrain (uvY = worldPos.xz / texScale), kein Triplanar.
// Steep: Triplanar für alle nicht-flachen Flächen inkl. Decken und Tunnelwände. // Steep: Triplanar für alle nicht-flachen Flächen inkl. Decken und Tunnelwände.
vec4 col = texture(m_TexFlat, uvY) * flatBlend #ifdef HAS_SPLAT
// Splatmap-UV: worldPos.xz linear auf [0,1] (kein Flip nötig Konvention im Editor: pz=0 → worldZ=-2048)
vec2 splatUV = (vWorldPos.xz + 2048.0) / 4096.0;
vec4 splat = texture(m_SplatMap, splatUV);
float w2 = splat.g;
float w3 = splat.b;
float w4 = splat.a;
float w1 = max(0.0, 1.0 - w2 - w3 - w4);
vec4 flatCol = texture(m_TexFlat, uvY) * w1
+ texture(m_TexFlat2, uvY) * w2
+ texture(m_TexFlat3, uvY) * w3
+ texture(m_TexFlat4, uvY) * w4;
#else
vec4 flatCol = texture(m_TexFlat, uvY);
#endif
vec4 col = flatCol * flatBlend
+ triplanar(m_TexSteep, uvX, uvY, uvZ, bw) * steepBlend; + triplanar(m_TexSteep, uvX, uvY, uvZ, bw) * steepBlend;
// Geometrie-Normale für Beleuchtung, ggf. durch Normal-Map ersetzt. // Geometrie-Normale für Beleuchtung, ggf. durch Normal-Map ersetzt.

View File

Before

Width:  |  Height:  |  Size: 177 KiB

After

Width:  |  Height:  |  Size: 177 KiB

View File

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 KiB

View File

Before

Width:  |  Height:  |  Size: 232 KiB

After

Width:  |  Height:  |  Size: 232 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

Before

Width:  |  Height:  |  Size: 799 KiB

After

Width:  |  Height:  |  Size: 799 KiB

View File

Before

Width:  |  Height:  |  Size: 297 KiB

After

Width:  |  Height:  |  Size: 297 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

View File

@@ -105,6 +105,14 @@ public class EditorApp extends Application {
// Farn-Generator-Zustand // Farn-Generator-Zustand
private de.blight.editor.tree.FernOptions fernOptions = new de.blight.editor.tree.FernOptions(); private de.blight.editor.tree.FernOptions fernOptions = new de.blight.editor.tree.FernOptions();
// Fruchtbusch-Generator-Zustand
private de.blight.editor.tree.FruitBushOptions fruitBushOptions =
de.blight.editor.tree.FruitBushOptions.orange();
private String fruitBushVariant = "Orange";
// Weinpflanzen-Generator-Zustand
private de.blight.editor.tree.GrapevineOptions grapevineOptions = new de.blight.editor.tree.GrapevineOptions();
// Vegetations-Generator-Zustand // Vegetations-Generator-Zustand
private String vegetationType = "Baum (Eiche)"; private String vegetationType = "Baum (Eiche)";
@@ -321,8 +329,8 @@ public class EditorApp extends Application {
private ToggleButton playToolBtn; private ToggleButton playToolBtn;
private ToggleButton voxelBtn; private ToggleButton voxelBtn;
private ToggleButton voxelCliffBtn; private ToggleButton voxelCliffBtn;
private ToggleButton camOrbitBtn; private RadioMenuItem camOrbitItem;
private ToggleButton camFreeBtn; private RadioMenuItem camFreeItem;
// "Objekt"-Button in der Selektionsleiste (zum Zurückschalten bei importierten Objekten) // "Objekt"-Button in der Selektionsleiste (zum Zurückschalten bei importierten Objekten)
private ToggleButton selModeObjectBtn; private ToggleButton selModeObjectBtn;
@@ -410,7 +418,20 @@ public class EditorApp extends Application {
Scene scene = new Scene(root, 1280, 760); Scene scene = new Scene(root, 1280, 760);
java.net.URL cssUrl = getClass().getResource("/editor.css"); java.net.URL cssUrl = getClass().getResource("/editor.css");
if (cssUrl != null) scene.getStylesheets().add(cssUrl.toExternalForm()); if (cssUrl != null) scene.getStylesheets().add(cssUrl.toExternalForm());
scene.addEventFilter(javafx.scene.input.KeyEvent.KEY_PRESSED, e -> handleKeyPress(e.getCode(), true)); scene.addEventFilter(javafx.scene.input.KeyEvent.KEY_PRESSED, e -> {
if (e.getCode() == KeyCode.C && e.isAltDown()) {
e.consume();
if (input.camMode == SharedInput.CAM_ORBIT) {
input.camMode = SharedInput.CAM_FREEFLY;
if (camFreeItem != null) { camFreeItem.setSelected(true); }
} else {
input.camMode = SharedInput.CAM_ORBIT;
if (camOrbitItem != null) { camOrbitItem.setSelected(true); }
}
return;
}
handleKeyPress(e.getCode(), true);
});
scene.addEventFilter(javafx.scene.input.KeyEvent.KEY_RELEASED, e -> handleKeyPress(e.getCode(), false)); scene.addEventFilter(javafx.scene.input.KeyEvent.KEY_RELEASED, e -> handleKeyPress(e.getCode(), false));
scene.setOnKeyTyped(e -> { scene.setOnKeyTyped(e -> {
if (!input.consoleIsOpen) return; if (!input.consoleIsOpen) return;
@@ -962,7 +983,7 @@ public class EditorApp extends Application {
ComboBox<String> typeBox = new ComboBox<>(); ComboBox<String> typeBox = new ComboBox<>();
typeBox.getItems().addAll( typeBox.getItems().addAll(
"Baum (Eiche)", "Baum (Birke)", "Baum (Kiefer)", "Baum (Weide)", "Baum (Busch)", "Baum (Eiche)", "Baum (Birke)", "Baum (Kiefer)", "Baum (Weide)", "Baum (Busch)",
"Farn", "Palme"); "Farn", "Palme", "Fruchtbusch", "Weinpflanze");
typeBox.setValue(vegetationType); typeBox.setValue(vegetationType);
typeBox.setOnAction(e -> { typeBox.setOnAction(e -> {
vegetationType = typeBox.getValue(); vegetationType = typeBox.getValue();
@@ -986,6 +1007,10 @@ public class EditorApp extends Application {
treeParams.copy(), true, treeTypeFromPreset(currentTreePreset))); treeParams.copy(), true, treeTypeFromPreset(currentTreePreset)));
} else if ("Farn".equals(vegetationType)) { } else if ("Farn".equals(vegetationType)) {
input.fernGenQueue.offer(new SharedInput.FernGenRequest(fernOptions.copy(), true)); input.fernGenQueue.offer(new SharedInput.FernGenRequest(fernOptions.copy(), true));
} else if ("Fruchtbusch".equals(vegetationType)) {
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 { } else {
input.palmGenQueue.offer(new SharedInput.PalmGenRequest(palmOptions.copy(), true)); input.palmGenQueue.offer(new SharedInput.PalmGenRequest(palmOptions.copy(), true));
} }
@@ -1027,6 +1052,28 @@ public class EditorApp extends Application {
root.setRight(buildVegetationParamsPanel()); root.setRight(buildVegetationParamsPanel());
onF5.run(); onF5.run();
}; };
} else if ("Fruchtbusch".equals(vegetationType)) {
onF5 = () -> {
input.fruitBushGenQueue.offer(
new SharedInput.FruitBushGenRequest(fruitBushOptions.copy(), fruitBushVariant, false));
setStatus("Fruchtbusch: generiere Vorschau…");
};
onF6 = () -> {
fruitBushOptions.seed = new java.util.Random().nextInt(1000000);
root.setRight(buildVegetationParamsPanel());
onF5.run();
};
} else if ("Weinpflanze".equals(vegetationType)) {
onF5 = () -> {
input.grapevineGenQueue.offer(
new SharedInput.GrapevineGenRequest(grapevineOptions.copy(), false));
setStatus("Weinpflanze: generiere Vorschau…");
};
onF6 = () -> {
grapevineOptions.seed = new java.util.Random().nextInt(1000000);
root.setRight(buildVegetationParamsPanel());
onF5.run();
};
} else { } else {
onF5 = () -> { onF5 = () -> {
input.palmGenQueue.offer(new SharedInput.PalmGenRequest(palmOptions.copy(), false)); input.palmGenQueue.offer(new SharedInput.PalmGenRequest(palmOptions.copy(), false));
@@ -1045,6 +1092,10 @@ public class EditorApp extends Application {
return buildTreeParamsPanel(); return buildTreeParamsPanel();
} else if ("Farn".equals(vegetationType)) { } else if ("Farn".equals(vegetationType)) {
return buildFernParamsPanel(); return buildFernParamsPanel();
} else if ("Fruchtbusch".equals(vegetationType)) {
return buildFruitBushParamsPanel();
} else if ("Weinpflanze".equals(vegetationType)) {
return buildGrapevineParamsPanel();
} else { } else {
return buildPalmParamsPanel(); return buildPalmParamsPanel();
} }
@@ -1114,6 +1165,113 @@ public class EditorApp extends Application {
return panel; return panel;
} }
private VBox buildFruitBushParamsPanel() {
VBox inner = new VBox(8);
inner.setPadding(new Insets(10));
// ── Variante ──────────────────────────────────────────────────────────
inner.getChildren().addAll(sectionTitle("Fruchtbusch-Typ"), new Separator());
ComboBox<String> variantBox = new ComboBox<>();
variantBox.getItems().addAll("Orange", "Zitrone", "Aprikose");
variantBox.setValue(fruitBushVariant);
variantBox.setMaxWidth(Double.MAX_VALUE);
variantBox.setOnAction(e -> {
fruitBushVariant = variantBox.getValue();
int oldSeed = fruitBushOptions.seed;
fruitBushOptions = switch (fruitBushVariant) {
case "Zitrone" -> de.blight.editor.tree.FruitBushOptions.lemon();
case "Aprikose" -> de.blight.editor.tree.FruitBushOptions.apricot();
default -> de.blight.editor.tree.FruitBushOptions.orange();
};
fruitBushOptions.seed = oldSeed;
root.setRight(buildVegetationParamsPanel());
if (onF5 != null) onF5.run();
});
inner.getChildren().add(variantBox);
// ── Allgemein ─────────────────────────────────────────────────────────
inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator());
inner.getChildren().add(bold("Zufallssamen:"));
Spinner<Integer> seedSp = intSpinner(0, 999999, fruitBushOptions.seed);
seedSp.valueProperty().addListener((o, a, b) -> fruitBushOptions.seed = b);
Button rndSeedBtn = new Button("🎲");
rndSeedBtn.setOnAction(e -> {
int s = new java.util.Random().nextInt(1000000);
fruitBushOptions.seed = s;
seedSp.getValueFactory().setValue(s);
if (onF5 != null) onF5.run();
});
HBox seedRow = new HBox(4, seedSp, rndSeedBtn);
HBox.setHgrow(seedSp, Priority.ALWAYS);
inner.getChildren().add(seedRow);
// ── Stamm ─────────────────────────────────────────────────────────────
inner.getChildren().addAll(sectionTitle("Stamm"), new Separator());
inner.getChildren().add(ezFloat("Höhe (m):", 0.2, 1.5, fruitBushOptions.trunkHeight,
v -> fruitBushOptions.trunkHeight = v));
inner.getChildren().add(ezFloat("Radius (m):", 0.01, 0.15, fruitBushOptions.trunkRadius,
v -> fruitBushOptions.trunkRadius = v));
// ── Äste ──────────────────────────────────────────────────────────────
inner.getChildren().addAll(sectionTitle("Äste"), new Separator());
inner.getChildren().add(bold("Anzahl Primäräste:"));
Spinner<Integer> branchCountSp = intSpinner(2, 10, fruitBushOptions.branchCount);
branchCountSp.valueProperty().addListener((o, a, b) -> fruitBushOptions.branchCount = b);
inner.getChildren().add(branchCountSp);
inner.getChildren().add(ezFloat("Winkel (°):", 10, 80, fruitBushOptions.branchAngle,
v -> fruitBushOptions.branchAngle = v));
inner.getChildren().add(ezFloat("Länge (m):", 0.2, 2.0, fruitBushOptions.branchLength,
v -> fruitBushOptions.branchLength = v));
inner.getChildren().add(bold("Reiser pro Ast:"));
Spinner<Integer> twigCountSp = intSpinner(1, 8, fruitBushOptions.twigCount);
twigCountSp.valueProperty().addListener((o, a, b) -> fruitBushOptions.twigCount = b);
inner.getChildren().add(twigCountSp);
inner.getChildren().add(ezFloat("Reiser-Winkel (°):", 10, 80, fruitBushOptions.twigAngle,
v -> fruitBushOptions.twigAngle = v));
inner.getChildren().add(ezFloat("Reiser-Länge (m):", 0.1, 1.2, fruitBushOptions.twigLength,
v -> fruitBushOptions.twigLength = v));
// ── Blätter & Früchte ─────────────────────────────────────────────────
inner.getChildren().addAll(sectionTitle("Blätter & Früchte"), new Separator());
inner.getChildren().add(ezFloat("Blattgröße (m):", 0.05, 0.6, fruitBushOptions.leafSize,
v -> fruitBushOptions.leafSize = v));
inner.getChildren().add(bold("Blätter pro Reiser:"));
Spinner<Integer> leafCountSp = intSpinner(2, 20, fruitBushOptions.leafCount);
leafCountSp.valueProperty().addListener((o, a, b) -> fruitBushOptions.leafCount = b);
inner.getChildren().add(leafCountSp);
inner.getChildren().add(ezFloat("Frucht-Dichte [01]:", 0.0, 1.0, fruitBushOptions.fruitDensity,
v -> fruitBushOptions.fruitDensity = v));
inner.getChildren().add(ezFloat("Frucht-Radius (m):", 0.02, 0.15, fruitBushOptions.fruitRadius,
v -> fruitBushOptions.fruitRadius = v));
// ── Wind ──────────────────────────────────────────────────────────────
inner.getChildren().addAll(sectionTitle("Wind"), new Separator());
inner.getChildren().add(ezFloat("Windstärke:", 0.0, 0.5, fruitBushOptions.windStrength,
v -> fruitBushOptions.windStrength = v));
inner.getChildren().add(ezFloat("Windgeschwindigkeit:", 0.1, 2.0, fruitBushOptions.windSpeed,
v -> fruitBushOptions.windSpeed = v));
Button previewBtn = new Button("▶ Vorschau");
previewBtn.setMaxWidth(Double.MAX_VALUE);
previewBtn.setStyle("-fx-background-color:#2d8a3e;-fx-text-fill:white;-fx-font-weight:bold;-fx-padding:6 12 6 12;");
previewBtn.setOnAction(e -> { if (onF5 != null) onF5.run(); });
inner.getChildren().addAll(new Separator(), previewBtn);
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;
}
private void switchToTripo() { private void switchToTripo() {
currentTool = "tripo"; currentTool = "tripo";
topBar.getChildren().set(1, buildTripoToolBar()); topBar.getChildren().set(1, buildTripoToolBar());
@@ -1209,8 +1367,19 @@ public class EditorApp extends Application {
}); });
viewTopologyItem.setOnAction(e -> viewTopologyItem.setOnAction(e ->
input.topologyRequest = viewTopologyItem.isSelected() ? 1 : 2); input.topologyRequest = viewTopologyItem.isSelected() ? 1 : 2);
ToggleGroup camMenuGroup = new ToggleGroup();
camOrbitItem = new RadioMenuItem("⊙ Orbit-Kamera (Alt+C)");
camFreeItem = new RadioMenuItem("✈ FreeFly-Kamera (Alt+C)");
camOrbitItem.setToggleGroup(camMenuGroup);
camFreeItem.setToggleGroup(camMenuGroup);
camOrbitItem.setSelected(true);
camOrbitItem.setOnAction(e -> input.camMode = SharedInput.CAM_ORBIT);
camFreeItem.setOnAction(e -> input.camMode = SharedInput.CAM_FREEFLY);
viewMenu.getItems().addAll(resetCam, new SeparatorMenuItem(), viewTexture, viewWireframe, viewMenu.getItems().addAll(resetCam, new SeparatorMenuItem(), viewTexture, viewWireframe,
new SeparatorMenuItem(), viewTopologyItem); new SeparatorMenuItem(), viewTopologyItem,
new SeparatorMenuItem(), camOrbitItem, camFreeItem);
Menu zeitMenu = new Menu("Zeit"); Menu zeitMenu = new Menu("Zeit");
ToggleGroup zeitGroup = new ToggleGroup(); ToggleGroup zeitGroup = new ToggleGroup();
@@ -1240,11 +1409,9 @@ public class EditorApp extends Application {
areaBtn = new ToggleButton("🗺 Bereiche"); areaBtn = new ToggleButton("🗺 Bereiche");
locationZoneBtn = new ToggleButton("📍 Locations"); locationZoneBtn = new ToggleButton("📍 Locations");
playToolBtn = new ToggleButton("🎮 Spielen"); playToolBtn = new ToggleButton("🎮 Spielen");
voxelBtn = new ToggleButton("⬡ Voxel"); voxelBtn = new ToggleButton("⬡ Voxel");
voxelCliffBtn = new ToggleButton("⛰ Klippe"); voxelCliffBtn = new ToggleButton("⛰ Klippe");
stoneBtn = new ToggleButton("🪨 Steine"); stoneBtn = new ToggleButton("🪨 Steine");
camOrbitBtn = new ToggleButton("⊙ Orbit");
camFreeBtn = new ToggleButton("✈ FreeFly");
baseBtn.setStyle("-fx-font-weight:bold;"); baseBtn.setStyle("-fx-font-weight:bold;");
grassBtn.setStyle("-fx-font-weight:bold;"); grassBtn.setStyle("-fx-font-weight:bold;");
grassVertexBtn.setStyle("-fx-font-weight:bold;"); grassVertexBtn.setStyle("-fx-font-weight:bold;");
@@ -1262,8 +1429,6 @@ public class EditorApp extends Application {
voxelBtn.setStyle("-fx-font-weight:bold;"); voxelBtn.setStyle("-fx-font-weight:bold;");
voxelCliffBtn.setStyle("-fx-font-weight:bold;"); voxelCliffBtn.setStyle("-fx-font-weight:bold;");
stoneBtn.setStyle("-fx-font-weight:bold;"); stoneBtn.setStyle("-fx-font-weight:bold;");
camOrbitBtn.setStyle("-fx-font-weight:bold;");
camFreeBtn.setStyle("-fx-font-weight:bold;");
ToggleGroup layerGroup = new ToggleGroup(); ToggleGroup layerGroup = new ToggleGroup();
baseBtn.setToggleGroup(layerGroup); baseBtn.setToggleGroup(layerGroup);
@@ -1286,6 +1451,9 @@ public class EditorApp extends Application {
baseBtn.setSelected(true); baseBtn.setSelected(true);
baseBtn.setOnAction(e -> { baseBtn.setOnAction(e -> {
input.heightTool.brushRadius.setValue(input.voxelTool.brushRadius.getValue());
input.heightTool.brushStrength.setValue(input.voxelTool.brushStrength.getValue());
input.heightTool.plateauHeight.setValue(input.voxelTool.plateauTarget.getValue());
input.activeLayer = 0; input.activeTool = input.heightTool; input.activeLayer = 0; input.activeTool = input.heightTool;
root.setRight(toolPanel); root.setRight(toolPanel);
showToolParameters(toolPanel, input.activeTool); showToolParameters(toolPanel, input.activeTool);
@@ -1352,6 +1520,9 @@ public class EditorApp extends Application {
root.setRight(buildPlayToolPanel()); root.setRight(buildPlayToolPanel());
}); });
voxelBtn.setOnAction(e -> { voxelBtn.setOnAction(e -> {
input.voxelTool.brushRadius.setValue(input.heightTool.brushRadius.getValue());
input.voxelTool.brushStrength.setValue(input.heightTool.brushStrength.getValue());
input.voxelTool.plateauTarget.setValue(input.heightTool.plateauHeight.getValue());
input.activeLayer = SharedInput.LAYER_VOXEL; input.activeLayer = SharedInput.LAYER_VOXEL;
input.activeTool = input.voxelTool; input.activeTool = input.voxelTool;
root.setRight(toolPanel); root.setRight(toolPanel);
@@ -1368,13 +1539,6 @@ public class EditorApp extends Application {
showToolParameters(toolPanel, input.activeTool); showToolParameters(toolPanel, input.activeTool);
}); });
ToggleGroup camModeGroup = new ToggleGroup();
camOrbitBtn.setToggleGroup(camModeGroup);
camFreeBtn.setToggleGroup(camModeGroup);
camOrbitBtn.setSelected(true);
camOrbitBtn.setOnAction(e -> input.camMode = SharedInput.CAM_ORBIT);
camFreeBtn.setOnAction(e -> input.camMode = SharedInput.CAM_FREEFLY);
Button camResetBtn = new Button("⌂ Reset"); Button camResetBtn = new Button("⌂ Reset");
camResetBtn.setStyle("-fx-font-weight:bold;"); camResetBtn.setStyle("-fx-font-weight:bold;");
camResetBtn.setTooltip(new javafx.scene.control.Tooltip("Kamera auf x=0, z=0, y=Terrain+10m zurücksetzen")); camResetBtn.setTooltip(new javafx.scene.control.Tooltip("Kamera auf x=0, z=0, y=Terrain+10m zurücksetzen"));
@@ -1393,7 +1557,7 @@ public class EditorApp extends Application {
new Separator(Orientation.VERTICAL), playToolBtn, new Separator(Orientation.VERTICAL), playToolBtn,
new Separator(Orientation.VERTICAL), voxelBtn, voxelCliffBtn, new Separator(Orientation.VERTICAL), voxelBtn, voxelCliffBtn,
new Separator(Orientation.VERTICAL), stoneBtn, new Separator(Orientation.VERTICAL), stoneBtn,
new Separator(Orientation.VERTICAL), camOrbitBtn, camFreeBtn, camResetBtn, new Separator(Orientation.VERTICAL), camResetBtn,
new Separator(Orientation.VERTICAL), hint); new Separator(Orientation.VERTICAL), hint);
worldToolBar = toolBar; worldToolBar = toolBar;
@@ -1449,6 +1613,76 @@ public class EditorApp extends Application {
return plantPreviewPanel; return plantPreviewPanel;
} }
// ── Weinpflanzen-Generator rechtes Parameter-Panel ─────────────────────
private VBox buildGrapevineParamsPanel() {
VBox inner = new VBox(8);
inner.setPadding(new Insets(10));
inner.getChildren().addAll(sectionTitle("Allgemein"), new Separator());
inner.getChildren().add(bold("Zufallssamen:"));
Spinner<Integer> seedSp = intSpinner(0, 999999, grapevineOptions.seed);
seedSp.valueProperty().addListener((o, a, b) -> grapevineOptions.seed = b);
Button rndSeedBtn = new Button("🎲");
rndSeedBtn.setOnAction(e -> {
int s = new java.util.Random().nextInt(1000000);
grapevineOptions.seed = s;
seedSp.getValueFactory().setValue(s);
if (onF5 != null) onF5.run();
});
HBox seedRow = new HBox(4, seedSp, rndSeedBtn);
HBox.setHgrow(seedSp, Priority.ALWAYS);
inner.getChildren().add(seedRow);
inner.getChildren().addAll(sectionTitle("Rahmen"), new Separator());
inner.getChildren().add(ezFloat("Gesamtbreite (m):", 2.0, 10.0, grapevineOptions.totalWidth,
v -> grapevineOptions.totalWidth = v));
inner.getChildren().add(ezFloat("Gesamthöhe (m):", 1.5, 4.0, grapevineOptions.totalHeight,
v -> grapevineOptions.totalHeight = v));
inner.getChildren().add(ezFloat("1. Draht-Höhe (m):", 0.5, 2.0, grapevineOptions.firstWireH,
v -> grapevineOptions.firstWireH = v));
inner.getChildren().add(bold("Draht-Anzahl:"));
Spinner<Integer> wireCountSp = intSpinner(2, 6, grapevineOptions.wireCount);
wireCountSp.valueProperty().addListener((o, a, b) -> grapevineOptions.wireCount = b);
inner.getChildren().add(wireCountSp);
inner.getChildren().addAll(sectionTitle("Triebe & Blätter"), new Separator());
inner.getChildren().add(bold("Triebe pro Seite:"));
Spinner<Integer> shootCountSp = intSpinner(2, 14, grapevineOptions.shootCount);
shootCountSp.valueProperty().addListener((o, a, b) -> grapevineOptions.shootCount = b);
inner.getChildren().add(shootCountSp);
inner.getChildren().add(ezFloat("Blattgröße (m):", 0.10, 0.60, grapevineOptions.leafSize,
v -> grapevineOptions.leafSize = v));
inner.getChildren().add(bold("Blätter pro Trieb:"));
Spinner<Integer> leafCountSp = intSpinner(2, 128, grapevineOptions.leafCount);
leafCountSp.valueProperty().addListener((o, a, b) -> grapevineOptions.leafCount = b);
inner.getChildren().add(leafCountSp);
inner.getChildren().addAll(sectionTitle("Wind"), new Separator());
inner.getChildren().add(ezFloat("Windstärke:", 0.0, 0.5, grapevineOptions.windStrength,
v -> grapevineOptions.windStrength = v));
inner.getChildren().add(ezFloat("Windgeschwindigkeit:", 0.1, 2.0, grapevineOptions.windSpeed,
v -> grapevineOptions.windSpeed = v));
Button previewBtn = new Button("▶ Vorschau");
previewBtn.setMaxWidth(Double.MAX_VALUE);
previewBtn.setStyle("-fx-background-color:#2d8a3e;-fx-text-fill:white;-fx-font-weight:bold;-fx-padding:6 12 6 12;");
previewBtn.setOnAction(e -> { if (onF5 != null) onF5.run(); });
inner.getChildren().add(new Separator());
inner.getChildren().add(previewBtn);
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;
}
// ── Baum-Generator rechtes Parameter-Panel ───────────────────────────── // ── Baum-Generator rechtes Parameter-Panel ─────────────────────────────
private VBox buildTreeParamsPanel() { private VBox buildTreeParamsPanel() {
@@ -1846,7 +2080,7 @@ public class EditorApp extends Application {
&& palmOptions.leafTexture.contains("palm2") ? "palm2.png" : "palm.png"; && palmOptions.leafTexture.contains("palm2") ? "palm2.png" : "palm.png";
leafTexBox.setValue(currentLeaf); leafTexBox.setValue(currentLeaf);
leafTexBox.setMaxWidth(Double.MAX_VALUE); leafTexBox.setMaxWidth(Double.MAX_VALUE);
leafTexBox.setOnAction(e -> palmOptions.leafTexture = "Textures/internal/leaves/" + leafTexBox.getValue()); leafTexBox.setOnAction(e -> palmOptions.leafTexture = "Textures/internal/foliage/" + leafTexBox.getValue());
inner.getChildren().add(new VBox(2, leafTexLabel, leafTexBox)); inner.getChildren().add(new VBox(2, leafTexLabel, leafTexBox));
// Rinden-Textur-Auswahl (alle Texturen aus dem bark-Ordner) // Rinden-Textur-Auswahl (alle Texturen aus dem bark-Ordner)
@@ -3260,7 +3494,9 @@ public class EditorApp extends Application {
new javafx.scene.control.Separator(), new javafx.scene.control.Separator(),
scaleLbl, globalScaleSpin, scaleLbl, globalScaleSpin,
new javafx.scene.control.Separator(), new javafx.scene.control.Separator(),
slotsScroll); slotsScroll,
new javafx.scene.control.Separator(),
buildVoxelSplatSectionUI());
} }
// Textur-Picker (nur beim GrassTool) // Textur-Picker (nur beim GrassTool)
@@ -3714,6 +3950,39 @@ public class EditorApp extends Application {
return box; return box;
} }
private javafx.scene.Node buildVoxelSplatSectionUI() {
VBox box = new VBox(4);
Label title = new Label("Voxel-Splatmap (Slots 24)");
title.setStyle("-fx-font-weight: bold; -fx-text-fill: #111; -fx-font-size: 11;");
Label hint = new Label("Slot 1 (Basis) = Voxel-Flat-Slot oben");
hint.setStyle("-fx-text-fill: #888; -fx-font-size: 10;");
box.getChildren().addAll(title, hint);
String[] slotLabels = {"Slot 2 (G)", "Slot 3 (B)", "Slot 4 (A)"};
for (int i = 1; i <= 3; i++) {
final int si = i;
String[] paths = input.voxelSplatTexturePaths;
String cur = (paths != null && i < paths.length && paths[i] != null) ? paths[i] : "";
Label nameLbl = new Label(slotLabels[i - 1] + ": " + labelFromPath(cur));
nameLbl.setStyle("-fx-font-size: 10; -fx-text-fill: #444;");
nameLbl.setMaxWidth(180);
Button chooseBtn = new Button("...");
chooseBtn.setOnAction(e -> {
de.blight.editor.ui.TextureChooser chooser =
new de.blight.editor.ui.TextureChooser(ASSET_ROOT, false);
chooser.showAndWait().ifPresent(chosenPath -> {
String[] copy = input.voxelSplatTexturePaths.clone();
copy[si] = chosenPath;
input.voxelSplatTexturePaths = copy;
input.voxelSplatTexturesChanged = true;
nameLbl.setText(slotLabels[si - 1] + ": " + labelFromPath(chosenPath));
});
});
box.getChildren().add(new HBox(4, nameLbl, chooseBtn));
}
return box;
}
private javafx.scene.Node buildVoxelTextureChoiceUI(ChoiceToolParameter param) { private javafx.scene.Node buildVoxelTextureChoiceUI(ChoiceToolParameter param) {
ToggleGroup tg = new ToggleGroup(); ToggleGroup tg = new ToggleGroup();
javafx.scene.layout.TilePane tile = new javafx.scene.layout.TilePane(); javafx.scene.layout.TilePane tile = new javafx.scene.layout.TilePane();
@@ -6624,7 +6893,8 @@ public class EditorApp extends Application {
input.modelEditorScaleY = 1f; input.modelEditorScaleY = 1f;
input.modelEditorScaleZ = 1f; input.modelEditorScaleZ = 1f;
input.modelEditorPivotY = 0f; input.modelEditorPivotY = 0f;
input.modelEditorOpenPath = relPath; input.modelEditorOpenPath = relPath;
input.modelImportIntermediatePath = relPath;
root.setRight(buildModelImportPanel(relPath, suggestedName)); root.setRight(buildModelImportPanel(relPath, suggestedName));
setStatus("Modell-Import: " + relPath); setStatus("Modell-Import: " + relPath);
@@ -7549,7 +7819,11 @@ public class EditorApp extends Application {
case 3 -> input.grassEditQueue.offer(new SharedInput.GrassEdit((float) x, (float) y, action)); case 3 -> input.grassEditQueue.offer(new SharedInput.GrassEdit((float) x, (float) y, action));
case SharedInput.LAYER_GRASS_VERTEX -> case SharedInput.LAYER_GRASS_VERTEX ->
input.grassVertexEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action)); input.grassVertexEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action));
case 4 -> input.textureEditQueue.offer(new SharedInput.TextureEdit((float) x, (float) y, action)); case 4 -> {
SharedInput.TextureEdit te = new SharedInput.TextureEdit((float) x, (float) y, action);
input.textureEditQueue.offer(te);
input.voxelTextureEditQueue.offer(te);
}
case SharedInput.LAYER_LIGHTS -> case SharedInput.LAYER_LIGHTS ->
input.lightClickQueue.offer(new SharedInput.LightClick((float) x, (float) y, action < 0)); input.lightClickQueue.offer(new SharedInput.LightClick((float) x, (float) y, action < 0));
case SharedInput.LAYER_EMITTERS -> case SharedInput.LAYER_EMITTERS ->

View File

@@ -30,6 +30,8 @@ import de.blight.editor.state.WaterBodyState;
import de.blight.editor.state.EzTreeState; import de.blight.editor.state.EzTreeState;
import de.blight.editor.state.LightState; import de.blight.editor.state.LightState;
import de.blight.editor.state.FernGeneratorState; 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.PalmGeneratorState;
import de.blight.editor.state.SceneObjectState; import de.blight.editor.state.SceneObjectState;
import de.blight.editor.state.TerrainEditorState; import de.blight.editor.state.TerrainEditorState;
@@ -189,6 +191,8 @@ public class JmeEditorApp extends SimpleApplication {
stateManager.attach(new EzTreeState(input)); stateManager.attach(new EzTreeState(input));
stateManager.attach(new PalmGeneratorState(input)); stateManager.attach(new PalmGeneratorState(input));
stateManager.attach(new FernGeneratorState(input)); stateManager.attach(new FernGeneratorState(input));
stateManager.attach(new FruitBushGeneratorState(input));
stateManager.attach(new GrapevineGeneratorState(input));
stateManager.attach(new LightState(input)); stateManager.attach(new LightState(input));
stateManager.attach(new EmitterState(input)); stateManager.attach(new EmitterState(input));
stateManager.attach(new WaterBodyState(input)); stateManager.attach(new WaterBodyState(input));

View File

@@ -22,11 +22,12 @@ import java.util.Map;
public class LegacyAssetRedirectLocator implements AssetLocator { public class LegacyAssetRedirectLocator implements AssetLocator {
private static final Map<String, String> PREFIXES = Map.of( private static final Map<String, String> PREFIXES = Map.of(
"Textures/bark/", "Textures/internal/bark/", "Textures/bark/", "Textures/internal/bark/",
"Textures/leaves/", "Textures/internal/leaves/", "Textures/leaves/", "Textures/internal/foliage/",
"Textures/fern/", "Textures/internal/fern/", "Textures/internal/leaves/", "Textures/internal/foliage/",
"Textures/water/", "Textures/internal/water/", "Textures/fern/", "Textures/internal/fern/",
"Textures/Water/", "Textures/internal/water/" "Textures/water/", "Textures/internal/water/",
"Textures/Water/", "Textures/internal/water/"
); );
private Path root; private Path root;

View File

@@ -100,6 +100,14 @@ public class SharedInput {
public record FernGenRequest(de.blight.editor.tree.FernOptions options, boolean exportAfter) {} public record FernGenRequest(de.blight.editor.tree.FernOptions options, boolean exportAfter) {}
public final ConcurrentLinkedQueue<FernGenRequest> fernGenQueue = new ConcurrentLinkedQueue<>(); public final ConcurrentLinkedQueue<FernGenRequest> fernGenQueue = new ConcurrentLinkedQueue<>();
// ── Fruchtbusch-Generator ─────────────────────────────────────────────────
public record FruitBushGenRequest(de.blight.editor.tree.FruitBushOptions options, String presetName, boolean exportAfter) {}
public final ConcurrentLinkedQueue<FruitBushGenRequest> fruitBushGenQueue = new ConcurrentLinkedQueue<>();
// ── Weinpflanzen-Generator ────────────────────────────────────────────────
public record GrapevineGenRequest(de.blight.editor.tree.GrapevineOptions options, boolean exportAfter) {}
public final ConcurrentLinkedQueue<GrapevineGenRequest> grapevineGenQueue = new ConcurrentLinkedQueue<>();
// ── Gras-Einstellungen (JavaFX → JME3) ─────────────────────────────────── // ── Gras-Einstellungen (JavaFX → JME3) ───────────────────────────────────
/** Relativer Asset-Pfad der Gras-Textur ("" = Standardfarbe). */ /** Relativer Asset-Pfad der Gras-Textur ("" = Standardfarbe). */
public volatile String grassTexturePath = ""; public volatile String grassTexturePath = "";
@@ -868,6 +876,8 @@ public class SharedInput {
public volatile String modelImportExportName = null; public volatile String modelImportExportName = null;
/** JME → JFX: Status-Meldung nach dem Export (relativer Pfad oder "FEHLER: …"). */ /** JME → JFX: Status-Meldung nach dem Export (relativer Pfad oder "FEHLER: …"). */
public volatile String modelImportExportStatus = null; public volatile String modelImportExportStatus = null;
/** JFX: Pfad der Zwischen-.j3o-Datei (relativ zu ASSET_ROOT). Wird von ModelImportState nach Export gelöscht, nicht von ModelEditorState. */
public volatile String modelImportIntermediatePath = null;
// ── Mesh-Sculpting ─────────────────────────────────────────────────────── // ── Mesh-Sculpting ───────────────────────────────────────────────────────
/** activeLayer==24 → gebackene Voxel-Meshes direkt sculpten */ /** activeLayer==24 → gebackene Voxel-Meshes direkt sculpten */
@@ -1040,4 +1050,14 @@ public class SharedInput {
public volatile float cliffRoughnessAmp = 4.0f; public volatile float cliffRoughnessAmp = 4.0f;
/** 3D-Noise-Frequenz für die Kliff-Oberfläche (höher = feiner Detail). */ /** 3D-Noise-Frequenz für die Kliff-Oberfläche (höher = feiner Detail). */
public volatile float cliffRoughnessScale = 0.12f; public volatile float cliffRoughnessScale = 0.12f;
// ── Voxel-Textur-Malen ────────────────────────────────────────────────────
/** Parallel-Queue zu textureEditQueue wird von submitEdit(layer=4) mitbefüllt,
* damit VoxelEditorState unabhängig vom Terrain seine Splatmap beschreiben kann. */
public final ConcurrentLinkedQueue<TextureEdit> voxelTextureEditQueue = new ConcurrentLinkedQueue<>();
/** Pfade der 4 Voxel-Splatmap-Slots (Slot 0 = Basis/TexFlat, Slots 1-3 = G/B/A). */
public volatile String[] voxelSplatTexturePaths = new String[]{"", "", "", ""};
/** JFX setzt true wenn Voxel-Splat-Texturen geändert wurden; JME liest + resettet. */
public volatile boolean voxelSplatTexturesChanged = false;
} }

View File

@@ -449,6 +449,16 @@ public class EzTreeState extends BaseAppState {
Node lodRoot = assembleLodNode(req.presetName(), hdNode, ld1Node, bb, atlasT2d); Node lodRoot = assembleLodNode(req.presetName(), hdNode, ld1Node, bb, atlasT2d);
exportTree(lodRoot, exportName, subPath); exportTree(lodRoot, exportName, subPath);
// assembleLodNode hat hdNode aus previewTreeHolder entnommen (JME3: nur ein Parent).
// Nach dem Export die Vorschau wiederherstellen.
float camDist = bb != null
? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3.2f
: 20f;
Vector3f target = bb != null
? new Vector3f(0f, bb.getCenter().y, 0f)
: new Vector3f(0f, 5f, 0f);
previewHost.setPreviewContent(hdNode, camDist, target);
} }
// ── Material-Aufbau ─────────────────────────────────────────────────────── // ── Material-Aufbau ───────────────────────────────────────────────────────
@@ -466,6 +476,7 @@ public class EzTreeState extends BaseAppState {
g.setQueueBucket(RenderQueue.Bucket.Transparent); g.setQueueBucket(RenderQueue.Bucket.Transparent);
g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
} }
} }
} else if (child instanceof Node trellis) { } else if (child instanceof Node trellis) {
Material mat = buildBarkMat(opts); Material mat = buildBarkMat(opts);

View File

@@ -0,0 +1,199 @@
package de.blight.editor.state;
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.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.texture.Texture;
import de.blight.editor.SharedInput;
import de.blight.editor.tree.FruitBushMeshBuilder;
import de.blight.editor.tree.FruitBushOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class FruitBushGeneratorState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(FruitBushGeneratorState.class);
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;
public FruitBushGeneratorState(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;
}
SharedInput.FruitBushGenRequest req = input.fruitBushGenQueue.poll();
if (req == null) return;
FruitBushOptions opts = req.options();
FruitBushMeshBuilder.MeshResult result = FruitBushMeshBuilder.build(opts);
Node bush = assembleBushNode(result, opts);
bush.updateGeometricState();
BoundingBox bb = bush.getWorldBound() instanceof BoundingBox b ? b : null;
float dist = bb != null
? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 4f
: 3f;
Vector3f target = bb != null
? new Vector3f(0f, bb.getCenter().y, 0f)
: new Vector3f(0f, 1f, 0f);
final Node finalBush = bush;
final float finalDist = dist;
final Vector3f finalTarget = target;
final boolean doExport = req.exportAfter();
final String presetName = req.presetName();
app.enqueue(() -> {
previewHost.setPreviewContent(finalBush, finalDist, finalTarget);
if (doExport) {
exportBush(finalBush, presetName);
}
});
input.treeGenStatusMsg = doExport ? "Fruchtbusch: exportiere…" : "Fruchtbusch: Vorschau";
}
private Node assembleBushNode(FruitBushMeshBuilder.MeshResult result, FruitBushOptions opts) {
Node node = new Node("fruchtbusch");
Geometry bark = new Geometry("bark", result.bark());
bark.setMaterial(buildBarkMat(opts));
bark.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
node.attachChild(bark);
if (result.leaves().getVertexCount() > 0) {
Geometry leaves = new Geometry("leaves", result.leaves());
leaves.setMaterial(buildLeafMat(opts));
leaves.setQueueBucket(RenderQueue.Bucket.Transparent);
leaves.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
node.attachChild(leaves);
}
if (opts.fruitsEnabled && result.fruits().getVertexCount() > 0) {
Geometry fruits = new Geometry("fruits", result.fruits());
fruits.setMaterial(buildFruitMat(opts));
fruits.setQueueBucket(RenderQueue.Bucket.Transparent);
fruits.setShadowMode(RenderQueue.ShadowMode.Off);
node.attachChild(fruits);
}
return node;
}
private Material buildBarkMat(FruitBushOptions opts) {
try {
Material mat = new Material(assets, "MatDefs/Tree.j3md");
mat.setColor("Diffuse", new ColorRGBA(0.72f, 0.60f, 0.45f, 1f));
mat.setFloat("WindStrength", opts.windStrength * 0.4f);
mat.setFloat("WindSpeed", opts.windSpeed);
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));
try {
Texture barkTex = assets.loadTexture("Textures/internal/bark/Bark001_Color.jpg");
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(0.45f, 0.32f, 0.18f, 1f));
return mat;
}
}
private Material buildLeafMat(FruitBushOptions 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", opts.windStrength);
mat.setFloat("WindSpeed", opts.windSpeed);
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;
}
}
private Material buildFruitMat(FruitBushOptions opts) {
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(opts.fruitR, opts.fruitG, opts.fruitB, 1f));
if (opts.fruitTexture != null) {
try {
Texture tex = assets.loadTexture(opts.fruitTexture);
tex.setWrap(Texture.WrapMode.Clamp);
mat.setTexture("ColorMap", tex);
} catch (Exception ignored) {}
}
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
return mat;
}
private void exportBush(Node bush, String presetName) {
try {
String folder = presetName == null ? "fruchtbusch"
: presetName.toLowerCase().replace("ä", "ae").replace("ö", "oe")
.replace("ü", "ue").replace(" ", "_");
String ts = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
String name = folder + "_" + ts;
Path dir = ASSET_ROOT.resolve("Models").resolve("trees").resolve(folder);
Files.createDirectories(dir);
Path out = dir.resolve(name + ".j3o");
BinaryExporter.getInstance().save(bush, out.toFile());
log.info("[Fruchtbusch] Gespeichert: {}", out);
input.treeGenStatusMsg = "Gespeichert: Models/trees/" + folder + "/" + name + ".j3o";
input.refreshAssets = true;
} catch (IOException e) {
log.error("[Fruchtbusch] Export-Fehler: {}", e.getMessage());
input.treeGenStatusMsg = "Fruchtbusch Export-Fehler: " + e.getMessage();
}
}
}

View File

@@ -0,0 +1,205 @@
package de.blight.editor.state;
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.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.texture.Texture;
import de.blight.editor.SharedInput;
import de.blight.editor.tree.GrapevineMeshBuilder;
import de.blight.editor.tree.GrapevineOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class GrapevineGeneratorState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(GrapevineGeneratorState.class);
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;
public GrapevineGeneratorState(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;
}
SharedInput.GrapevineGenRequest req = input.grapevineGenQueue.poll();
if (req == null) return;
GrapevineOptions opts = req.options();
GrapevineMeshBuilder.MeshResult result = GrapevineMeshBuilder.build(opts);
Node vine = assembleVineNode(result, opts);
vine.updateGeometricState();
BoundingBox bb = vine.getWorldBound() instanceof BoundingBox b ? b : null;
float dist = bb != null
? Math.max(bb.getXExtent(), Math.max(bb.getYExtent(), bb.getZExtent())) * 3.0f
: 5f;
Vector3f target = bb != null
? new Vector3f(0f, bb.getCenter().y, 0f)
: new Vector3f(0f, 1.4f, 0f);
final Node finalVine = vine;
final float finalDist = dist;
final Vector3f finalTarget = target;
final boolean doExport = req.exportAfter();
app.enqueue(() -> {
previewHost.setPreviewContent(finalVine, finalDist, finalTarget);
if (doExport) {
exportVine(finalVine);
}
});
input.treeGenStatusMsg = doExport ? "Weinpflanze: exportiere…" : "Weinpflanze: Vorschau";
}
private Node assembleVineNode(GrapevineMeshBuilder.MeshResult result, GrapevineOptions opts) {
Node node = new Node("weinpflanze");
Geometry bark = new Geometry("bark", result.bark());
bark.setMaterial(buildBarkMat(opts));
bark.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
node.attachChild(bark);
if (result.wires().getVertexCount() > 0) {
Geometry wires = new Geometry("wires", result.wires());
wires.setMaterial(buildWireMat());
wires.setShadowMode(RenderQueue.ShadowMode.Off);
node.attachChild(wires);
}
if (result.leaves().getVertexCount() > 0) {
Geometry leaves = new Geometry("leaves", result.leaves());
leaves.setMaterial(buildLeafMat(opts));
leaves.setQueueBucket(RenderQueue.Bucket.Transparent);
leaves.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
node.attachChild(leaves);
}
if (result.fruits().getVertexCount() > 0) {
Geometry fruits = new Geometry("grapes", result.fruits());
fruits.setMaterial(buildGrapeMat(opts));
fruits.setQueueBucket(RenderQueue.Bucket.Transparent);
fruits.setShadowMode(RenderQueue.ShadowMode.Off);
node.attachChild(fruits);
}
return node;
}
private Material buildBarkMat(GrapevineOptions opts) {
try {
Material mat = new Material(assets, "MatDefs/Tree.j3md");
mat.setColor("Diffuse", new ColorRGBA(0.65f, 0.52f, 0.38f, 1f));
mat.setFloat("WindStrength", opts.windStrength * 0.3f);
mat.setFloat("WindSpeed", opts.windSpeed);
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));
try {
Texture barkTex = assets.loadTexture("Textures/internal/bark/Bark001_Color.jpg");
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(0.45f, 0.32f, 0.18f, 1f));
return mat;
}
}
private Material buildWireMat() {
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(0.22f, 0.20f, 0.18f, 1f));
return mat;
}
private Material buildLeafMat(GrapevineOptions 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", opts.windStrength);
mat.setFloat("WindSpeed", opts.windSpeed);
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);
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;
}
}
private Material buildGrapeMat(GrapevineOptions opts) {
Material mat = new Material(assets, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", new ColorRGBA(0.42f, 0.18f, 0.55f, 1f));
if (opts.grapeTexture != null) {
try {
Texture tex = assets.loadTexture(opts.grapeTexture);
tex.setWrap(Texture.WrapMode.Clamp);
mat.setTexture("ColorMap", tex);
} catch (Exception ignored) {}
}
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
return mat;
}
private void exportVine(Node vine) {
try {
String ts = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(LocalDateTime.now());
String name = "grapevine_" + ts;
Path dir = ASSET_ROOT.resolve("Models").resolve("trees").resolve("grapevine");
Files.createDirectories(dir);
Path out = dir.resolve(name + ".j3o");
BinaryExporter.getInstance().save(vine, out.toFile());
log.info("[Grapevine] Gespeichert: {}", out);
input.treeGenStatusMsg = "Gespeichert: Models/trees/grapevine/" + name + ".j3o";
input.refreshAssets = true;
} catch (IOException e) {
log.error("[Grapevine] Export-Fehler: {}", e.getMessage());
input.treeGenStatusMsg = "Grapevine Export-Fehler: " + e.getMessage();
}
}
}

View File

@@ -443,11 +443,12 @@ public class ModelImportState extends BaseAppState {
// nach einem Editor-Neustart oder im Spiel (dort kein Cache vorhanden). // nach einem Editor-Neustart oder im Spiel (dort kein Cache vorhanden).
assets.deleteFromCache(new com.jme3.asset.ModelKey(relPath)); assets.deleteFromCache(new com.jme3.asset.ModelKey(relPath));
// Zwischendatei löschen wenn der Nutzer einen anderen Dateinamen gewählt hat. // Zwischendatei immer löschen — sie dient nur der Vorschau und soll nicht
// SceneObjectState hat die Quelldatei unter dem originalen Dateinamen als .j3o // im Asset-Baum bleiben. modelImportIntermediatePath wird in activateModelImport
// gespeichert; erst performExport() bäckt Scale + Pivot in die Ziel-Datei. // gesetzt und nicht von ModelEditorState geleert (im Gegensatz zu modelEditorOpenPath).
String intermediateRel = input.modelEditorOpenPath != null String intermediateRel = input.modelImportIntermediatePath != null
? input.modelEditorOpenPath.replace('\\', '/') : null; ? input.modelImportIntermediatePath.replace('\\', '/') : null;
input.modelImportIntermediatePath = null;
if (intermediateRel != null && !intermediateRel.equals(relPath)) { if (intermediateRel != null && !intermediateRel.equals(relPath)) {
Path intermediateAbs = ASSET_ROOT.resolve(intermediateRel); Path intermediateAbs = ASSET_ROOT.resolve(intermediateRel);
try { try {
@@ -459,6 +460,8 @@ public class ModelImportState extends BaseAppState {
} }
} }
// refreshAssets erst NACH dem Löschen der Zwischendatei setzen,
// damit der Asset-Baum nur die finale Datei anzeigt.
input.modelImportExportStatus = "Gespeichert: " + relPath; input.modelImportExportStatus = "Gespeichert: " + relPath;
input.refreshAssets = true; input.refreshAssets = true;

View File

@@ -404,6 +404,9 @@ public class PalmGeneratorState extends BaseAppState {
mat.setColor("Diffuse", new ColorRGBA(opts.barkR, opts.barkG, opts.barkB, 1f)); mat.setColor("Diffuse", new ColorRGBA(opts.barkR, opts.barkG, opts.barkB, 1f));
mat.setFloat("WindStrength", 0.08f); mat.setFloat("WindStrength", 0.08f);
mat.setFloat("WindSpeed", 0.4f); mat.setFloat("WindSpeed", 0.4f);
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) { if (opts.barkTexture != null) {
try { try {
Texture barkTex = assets.loadTexture(opts.barkTexture); Texture barkTex = assets.loadTexture(opts.barkTexture);
@@ -426,6 +429,9 @@ public class PalmGeneratorState extends BaseAppState {
mat.setColor("Diffuse", new ColorRGBA(opts.leafR, opts.leafG, opts.leafB, 1f)); mat.setColor("Diffuse", new ColorRGBA(opts.leafR, opts.leafG, opts.leafB, 1f));
mat.setFloat("WindStrength", 0.20f); mat.setFloat("WindStrength", 0.20f);
mat.setFloat("WindSpeed", 0.5f); mat.setFloat("WindSpeed", 0.5f);
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); mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
if (opts.leafTexture != null) { if (opts.leafTexture != null) {
try { try {
@@ -449,6 +455,9 @@ public class PalmGeneratorState extends BaseAppState {
mat.setColor("Diffuse", color); mat.setColor("Diffuse", color);
mat.setFloat("WindStrength", 0.04f); mat.setFloat("WindStrength", 0.04f);
mat.setFloat("WindSpeed", 0.3f); mat.setFloat("WindSpeed", 0.3f);
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.crownTexture != null) { if (opts.crownTexture != null) {
try { try {
Texture tex = assets.loadTexture(opts.crownTexture); Texture tex = assets.loadTexture(opts.crownTexture);

View File

@@ -1408,45 +1408,80 @@ public class TerrainEditorState extends BaseAppState {
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f); Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
com.jme3.math.Ray ray = new com.jme3.math.Ray(near, far.subtract(near).normalizeLocal()); com.jme3.math.Ray ray = new com.jme3.math.Ray(near, far.subtract(near).normalizeLocal());
int mode = input.heightTool.mode.getSelectedIndex();
// Plateau-RMB: vor dem Terrain-Raycast prüfen, damit Kliff-Flächen funktionieren
if (mode == HeightTool.MODE_PLATEAU && edit.action() < 0) {
CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits);
Vector3f contact = hits.size() > 0 ? hits.getClosestCollision().getContactPoint() : null;
sampleAndSetPlateauHeight(ray, contact);
continue;
}
CollisionResults hits = new CollisionResults(); CollisionResults hits = new CollisionResults();
terrain.collideWith(ray, hits); terrain.collideWith(ray, hits);
if (hits.size() == 0) continue; if (hits.size() == 0) continue;
Vector3f contact = hits.getClosestCollision().getContactPoint(); Vector3f contact = hits.getClosestCollision().getContactPoint();
int mode = input.heightTool.mode.getSelectedIndex();
boolean terrainChanged = true;
if (mode == HeightTool.MODE_SMOOTH) { if (mode == HeightTool.MODE_SMOOTH) {
smoothHeight(contact); if (edit.action() > 0) {
} else if (mode == HeightTool.MODE_PLATEAU) { slopeHeight(contact);
if (edit.action() < 0) {
// Rechtsklick: Terrain- und Voxel-Höhe sampeln, Maximum als Plateau-Ziel
float h = sampleTerrainHeight(contact);
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
if (ves != null) {
float vh = ves.columnTopWorldY(contact.x, contact.z);
if (Float.isFinite(vh)) h = Float.isFinite(h) ? Math.max(h, vh) : vh;
}
if (Float.isFinite(h)) {
input.heightTool.plateauHeight.setValue(h);
input.heightTool.plateauHeightChanged = true;
}
terrainChanged = false;
} else { } else {
// Linksklick: Terrain schrittweise auf Plateau-Höhe angleichen smoothHeight(contact);
flattenToPlateauHeight(contact);
} }
} else if (mode == HeightTool.MODE_PLATEAU) {
flattenToPlateauHeight(contact);
} else { } else {
float delta = (float) input.heightTool.brushStrength.getValue() * edit.action(); float delta = (float) input.heightTool.brushStrength.getValue() * edit.action();
modifyHeight(contact, delta, mode); modifyHeight(contact, delta, mode);
} }
if (terrainChanged) { float br = (float) input.heightTool.brushRadius.getValue();
float br = (float) input.heightTool.brushRadius.getValue(); input.terrainEditedAreas.offer(new float[]{contact.x, contact.z, br});
input.terrainEditedAreas.offer(new float[]{contact.x, contact.z, br});
}
} }
if (processed > 0) terrain.updateModelBound(); if (processed > 0) terrain.updateModelBound();
} }
private void sampleAndSetPlateauHeight(com.jme3.math.Ray ray, Vector3f terrainContact) {
float h = Float.NaN;
if (terrainContact != null) {
h = sampleTerrainHeight(terrainContact);
}
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
if (ves != null) {
// 1. Versuch: Voxel-Geometrie direkt raycasten (Kliff-Fläche)
Vector3f voxelContact = ves.raycastVoxelGeometry(ray);
if (voxelContact != null) {
float vh = ves.columnTopWorldY(voxelContact.x, voxelContact.z);
if (Float.isFinite(vh)) h = Float.isFinite(h) ? Math.max(h, vh) : vh;
}
// 2. Fallback: XZ aus Terrain-Kontakt
if (terrainContact != null) {
float vh = ves.columnTopWorldY(terrainContact.x, terrainContact.z);
if (Float.isFinite(vh)) h = Float.isFinite(h) ? Math.max(h, vh) : vh;
}
// 3. Fallback: Ray auf y=0 Ebene projizieren → columnTopWorldY findet die Voxel-Säule
if (!Float.isFinite(h) || voxelContact == null) {
float dy = ray.getDirection().y;
if (Math.abs(dy) > 0.001f) {
float t = -ray.getOrigin().y / dy;
if (t > 0.5f) {
float fx = ray.getOrigin().x + t * ray.getDirection().x;
float fz = ray.getOrigin().z + t * ray.getDirection().z;
float vh = ves.columnTopWorldY(fx, fz);
if (Float.isFinite(vh)) h = Float.isFinite(h) ? Math.max(h, vh) : vh;
}
}
}
}
if (Float.isFinite(h)) {
input.heightTool.plateauHeight.setValue(h);
input.heightTool.plateauHeightChanged = true;
input.voxelTool.plateauTarget.setValue(h);
input.voxelTool.plateauTargetChanged = true;
}
}
// ── Höhen-Werkzeug ──────────────────────────────────────────────────────── // ── Höhen-Werkzeug ────────────────────────────────────────────────────────
private void modifyHeight(Vector3f worldContact, float delta, int mode) { private void modifyHeight(Vector3f worldContact, float delta, int mode) {
@@ -1552,6 +1587,102 @@ public class TerrainEditorState extends BaseAppState {
if (grassVertexState != null) grassVertexState.adjustBladeHeights(worldContact, radius); if (grassVertexState != null) grassVertexState.adjustBladeHeights(worldContact, radius);
} }
private void slopeHeight(Vector3f worldContact) {
float radius = (float) input.heightTool.brushRadius.getValue();
float strength = (float) input.heightTool.brushStrength.getValue();
int r = (int) Math.ceil(radius / VERTEX_SPACING);
int cx = Math.round((worldContact.x + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING);
int cz = Math.round((worldContact.z + TERRAIN_SIZE * 0.5f) / VERTEX_SPACING);
if (cachedHeightMap == null) return;
float[] hmap = cachedHeightMap;
float outerMin = 0.7f * radius;
// 1. Höchsten Punkt auf dem äußeren Ring suchen
float maxH = Float.NEGATIVE_INFINITY;
float maxWX = 0, maxWZ = 0;
for (int dz = -r; dz <= r; dz++) {
for (int dx = -r; dx <= r; dx++) {
int vx = cx + dx, vz = cz + dz;
if (vx < 0 || vx >= TOTAL_SIZE || vz < 0 || vz >= TOTAL_SIZE) continue;
float dist = FastMath.sqrt(dx * dx + dz * dz) * VERTEX_SPACING;
if (dist < outerMin || dist >= radius) continue;
float h = hmap[vz * TOTAL_SIZE + vx];
if (!Float.isFinite(h)) continue;
if (h > maxH) { maxH = h; maxWX = dx * VERTEX_SPACING; maxWZ = dz * VERTEX_SPACING; }
}
}
if (!Float.isFinite(maxH)) return;
// 2. Richtung vom Zentrum zum Hochpunkt → Slope-Achse
float highDist = FastMath.sqrt(maxWX * maxWX + maxWZ * maxWZ);
if (highDist < 0.001f) return;
float dirX = maxWX / highDist;
float dirZ = maxWZ / highDist;
// 3. Gegenüberliegenden Punkt finden: Außenring-Vertex mit kleinstem Proj-Wert (= entgegengesetzt zu dir)
float oppH = Float.NaN;
float oppWX = -maxWX, oppWZ = -maxWZ; // Initiales Ideal-Gegenteil
float minProj = Float.MAX_VALUE;
for (int dz = -r; dz <= r; dz++) {
for (int dx = -r; dx <= r; dx++) {
int vx = cx + dx, vz = cz + dz;
if (vx < 0 || vx >= TOTAL_SIZE || vz < 0 || vz >= TOTAL_SIZE) continue;
float dist = FastMath.sqrt(dx * dx + dz * dz) * VERTEX_SPACING;
if (dist < outerMin || dist >= radius) continue;
float h = hmap[vz * TOTAL_SIZE + vx];
if (!Float.isFinite(h)) continue;
float wx = dx * VERTEX_SPACING, wz = dz * VERTEX_SPACING;
float proj = wx * dirX + wz * dirZ;
if (proj < minProj) { minProj = proj; oppH = h; oppWX = wx; oppWZ = wz; }
}
}
if (!Float.isFinite(oppH)) return;
// 4. Projektionsachse: von Gegenpunkt (t=0) zu Hochpunkt (t=1)
float projHigh = maxWX * dirX + maxWZ * dirZ;
float projOpp = oppWX * dirX + oppWZ * dirZ;
float projRange = projHigh - projOpp;
if (Math.abs(projRange) < 0.001f) return;
// 5. Alle Brush-Vertices zur Ziel-Ebene hinziehen
List<Vector2f> locs = new ArrayList<>();
List<Float> deltas = new ArrayList<>();
for (int dz = -r; dz <= r; dz++) {
for (int dx = -r; dx <= r; dx++) {
int vx = cx + dx, vz = cz + dz;
if (vx < 0 || vx >= TOTAL_SIZE || vz < 0 || vz >= TOTAL_SIZE) continue;
float dist = FastMath.sqrt(dx * dx + dz * dz) * VERTEX_SPACING;
if (dist >= radius) continue;
float curH = hmap[vz * TOTAL_SIZE + vx];
if (!Float.isFinite(curH)) continue;
float proj = (dx * VERTEX_SPACING) * dirX + (dz * VERTEX_SPACING) * dirZ;
float t = FastMath.clamp((proj - projOpp) / projRange, 0f, 1f);
float target = oppH + t * (maxH - oppH);
float falloff = (1f + FastMath.cos(FastMath.PI * dist / radius)) * 0.5f;
float blend = FastMath.clamp(falloff * (strength / 50f), 0f, 1f);
float delta = (target - curH) * blend;
if (delta == 0f) continue;
locs.add(new Vector2f(vx * VERTEX_SPACING - TERRAIN_SIZE * 0.5f,
vz * VERTEX_SPACING - TERRAIN_SIZE * 0.5f));
deltas.add(delta);
}
}
if (!locs.isEmpty()) {
terrain.adjustHeight(locs, deltas);
syncHeightCache(locs, deltas);
if (placedObjectState != null) placedObjectState.adjustObjectHeights(locs, deltas);
if (sceneObjState != null) sceneObjState.snapToTerrain(worldContact.x, worldContact.z, radius);
if (grassVertexState != null) grassVertexState.adjustBladeHeights(worldContact, radius);
}
}
/** Liest die Terrain-Höhe am nächstgelegenen Vertex zum Kontaktpunkt. */ /** Liest die Terrain-Höhe am nächstgelegenen Vertex zum Kontaktpunkt. */
public float sampleTerrainHeight(Vector3f worldContact) { public float sampleTerrainHeight(Vector3f worldContact) {
if (cachedHeightMap == null) return Float.NaN; if (cachedHeightMap == null) return Float.NaN;

View File

@@ -35,8 +35,6 @@ import com.jme3.scene.Spatial;
import com.jme3.scene.VertexBuffer; import com.jme3.scene.VertexBuffer;
import com.jme3.scene.control.AbstractControl; import com.jme3.scene.control.AbstractControl;
import com.jme3.scene.shape.Sphere; import com.jme3.scene.shape.Sphere;
import com.jme3.shadow.DirectionalLightShadowRenderer;
import com.jme3.shadow.EdgeFilteringMode;
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.Texture;
@@ -138,13 +136,13 @@ public class TreeGeneratorState extends BaseAppState {
previewScene.attachChild(buildPreviewGrid()); previewScene.attachChild(buildPreviewGrid());
previewVP.attachScene(previewScene); previewVP.attachScene(previewScene);
DirectionalLightShadowRenderer shadowRenderer = // Shadow-Renderer absichtlich NICHT im Preview-Viewport:
new DirectionalLightShadowRenderer(assets, 2048, 1); // Tree.j3md / TreeLeaf.j3md haben eine PostShadow-Technik mit Blend Modulate.
shadowRenderer.setLight(previewSunLight); // Wird ShadowIntensity nicht korrekt auf 0.25 propagiert (JME3-Bug mit
shadowRenderer.setEdgeFilteringMode(EdgeFilteringMode.PCF4); // Custom-PostShadow + PCF4), bleibt der j3md-Default 1.0 aktiv →
shadowRenderer.setShadowIntensity(0.25f); // lit = 0 → Blend Modulate → komplettes Schwarz.
shadowRenderer.setShadowZExtend(80f); // Die Preview-Beleuchtung kommt ohnehin aus den Custom-Uniforms
previewVP.addProcessor(shadowRenderer); // (LightDir / SunColor / AmbientColor) und braucht keinen Shadow-Renderer.
// Stellt sicher, dass previewScene direkt vor dem Rendern immer aktuell ist // Stellt sicher, dass previewScene direkt vor dem Rendern immer aktuell ist
// unabhängig davon, welche AppStates nach TreeGeneratorState die Szene noch ändern. // unabhängig davon, welche AppStates nach TreeGeneratorState die Szene noch ändern.
previewVP.addProcessor(new SceneProcessor() { previewVP.addProcessor(new SceneProcessor() {
@@ -169,7 +167,10 @@ public class TreeGeneratorState extends BaseAppState {
public void setPreviewContent(com.jme3.scene.Node node, float camDist, public void setPreviewContent(com.jme3.scene.Node node, float camDist,
com.jme3.math.Vector3f target) { com.jme3.math.Vector3f target) {
previewTreeHolder.detachAllChildren(); previewTreeHolder.detachAllChildren();
if (node != null) previewTreeHolder.attachChild(node); if (node != null) {
ImpostorUtil.applyTreeLighting(node, previewSunLight.getDirection().negate());
previewTreeHolder.attachChild(node);
}
this.previewCamDist = camDist; this.previewCamDist = camDist;
this.previewTarget.set(target); this.previewTarget.set(target);
} }
@@ -315,6 +316,7 @@ public class TreeGeneratorState extends BaseAppState {
Node previewTree = makeTreeNode(pendingHdResult, Node previewTree = makeTreeNode(pendingHdResult,
pendingBarkMat.clone(), pendingLeafMat.clone(), "prev"); pendingBarkMat.clone(), pendingLeafMat.clone(), "prev");
ImpostorUtil.applyTreeLighting(previewTree, previewSunLight.getDirection().negate());
previewTreeHolder.detachAllChildren(); previewTreeHolder.detachAllChildren();
previewTreeHolder.attachChild(previewTree); previewTreeHolder.attachChild(previewTree);
@@ -380,6 +382,9 @@ public class TreeGeneratorState extends BaseAppState {
mat.setColor("Diffuse", new ColorRGBA(0.42f, 0.26f, 0.10f, 1f)); mat.setColor("Diffuse", new ColorRGBA(0.42f, 0.26f, 0.10f, 1f));
mat.setFloat("WindStrength", 0.15f); mat.setFloat("WindStrength", 0.15f);
mat.setFloat("WindSpeed", 0.5f); mat.setFloat("WindSpeed", 0.5f);
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 (p.barkTexture != null) { if (p.barkTexture != null) {
try { try {
Texture barkTex = assets.loadTexture(p.barkTexture); Texture barkTex = assets.loadTexture(p.barkTexture);
@@ -404,6 +409,9 @@ public class TreeGeneratorState extends BaseAppState {
mat.setColor("Diffuse", new ColorRGBA(0.18f, 0.60f, 0.10f, 1f)); mat.setColor("Diffuse", new ColorRGBA(0.18f, 0.60f, 0.10f, 1f));
mat.setFloat("WindStrength", 0.30f); mat.setFloat("WindStrength", 0.30f);
mat.setFloat("WindSpeed", 0.7f); mat.setFloat("WindSpeed", 0.7f);
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); mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha); mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
if (p.leafTexture != null) { if (p.leafTexture != null) {

View File

@@ -23,7 +23,10 @@ import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Quad; import com.jme3.scene.shape.Quad;
import com.jme3.scene.VertexBuffer; import com.jme3.scene.VertexBuffer;
import com.jme3.terrain.geomipmap.TerrainQuad; import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.texture.Image;
import com.jme3.texture.Texture; import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D;
import com.jme3.texture.image.ColorSpace;
import com.jme3.util.BufferUtils; import com.jme3.util.BufferUtils;
import de.blight.common.VoxelChunk; import de.blight.common.VoxelChunk;
import de.blight.common.VoxelChunkIO; import de.blight.common.VoxelChunkIO;
@@ -35,11 +38,18 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.jme3.export.binary.BinaryExporter; import com.jme3.export.binary.BinaryExporter;
import java.io.IOException; import de.blight.common.MapData;
import de.blight.common.MapIO;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer; import java.nio.FloatBuffer;
import java.nio.IntBuffer; import java.nio.IntBuffer;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Arrays;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import java.util.*; import java.util.*;
import java.util.concurrent.*; import java.util.concurrent.*;
import java.util.ArrayDeque; import java.util.ArrayDeque;
@@ -145,6 +155,19 @@ public class VoxelEditorState extends BaseAppState {
/** Vorheriger Layer-Zustand für Einstieg/Ausstieg-Erkennung. */ /** Vorheriger Layer-Zustand für Einstieg/Ausstieg-Erkennung. */
private boolean prevLayerWasVoxel = false; private boolean prevLayerWasVoxel = false;
// ── Voxel-Splatmap ───────────────────────────────────────────────────────
private static final int VOX_SPLAT_SIZE = MapData.SPLAT_SIZE; // 2049
private static final float VOX_WORLD_HALF = 2048f;
private static final float VOX_SPLAT_WE_PER_PX =
(VOX_WORLD_HALF * 2f) / (VOX_SPLAT_SIZE - 1); // ~2 WE/px
private byte[] voxSplatR, voxSplatG, voxSplatB, voxSplatA;
private ByteBuffer voxSplatBuf;
private Image voxSplatImage;
private Texture2D voxSplatTex;
private boolean splatDirty = false;
// ── LOD-Rebuild-Queue ───────────────────────────────────────────────────── // ── LOD-Rebuild-Queue ─────────────────────────────────────────────────────
/** Chunks, die LOD1/2 neu brauchen. Wird im Hintergrund-Thread abgearbeitet. */ /** Chunks, die LOD1/2 neu brauchen. Wird im Hintergrund-Thread abgearbeitet. */
@@ -188,6 +211,7 @@ public class VoxelEditorState extends BaseAppState {
} }
voxelMaterial = buildMaterial(); voxelMaterial = buildMaterial();
initVoxelSplat();
brushIndicator = buildBrushIndicator(); brushIndicator = buildBrushIndicator();
app.getRootNode().attachChild(brushIndicator); app.getRootNode().attachChild(brushIndicator);
@@ -289,11 +313,16 @@ public class VoxelEditorState extends BaseAppState {
clearMarkedOverlays(); clearMarkedOverlays();
} }
// Voxel-Texturen aktualisiert? // Voxel-Texturen (Slot/Normal-Map) aktualisiert?
if (input.voxelTexturesChanged) { if (input.voxelTexturesChanged) {
input.voxelTexturesChanged = false; input.voxelTexturesChanged = false;
applyTextures(voxelMaterial); applyTextures(voxelMaterial);
} }
// Voxel-Splat-Texturen (TexFlat2/3/4) aktualisiert?
if (input.voxelSplatTexturesChanged) {
input.voxelSplatTexturesChanged = false;
applyVoxelSplatTextures(voxelMaterial);
}
// Layer-Wechsel erkennen → Referenzebene steuern (kein automatischer Wireframe-Wechsel) // Layer-Wechsel erkennen → Referenzebene steuern (kein automatischer Wireframe-Wechsel)
boolean isVoxelLayer = input.activeLayer == SharedInput.LAYER_VOXEL; boolean isVoxelLayer = input.activeLayer == SharedInput.LAYER_VOXEL;
@@ -307,6 +336,14 @@ public class VoxelEditorState extends BaseAppState {
applyWireframe(input.voxelWireframeEnabled); applyWireframe(input.voxelWireframeEnabled);
} }
// Voxel-Textur-Malen: parallel zum TerrainEditorState, wenn der Textur-Layer aktiv ist
if (input.activeLayer == 4) {
processVoxelTextureEdits();
idleSinceSave += tpf;
checkAutoSave();
return;
}
// Nur aktiv wenn LAYER_VOXEL gesetzt // Nur aktiv wenn LAYER_VOXEL gesetzt
if (input.activeLayer != SharedInput.LAYER_VOXEL) { if (input.activeLayer != SharedInput.LAYER_VOXEL) {
idleSinceEdit = 0f; idleSinceEdit = 0f;
@@ -603,6 +640,17 @@ public class VoxelEditorState extends BaseAppState {
bestPos = cr.getContactPoint(); bestPos = cr.getContactPoint();
bestNorm = new Vector3f(0, 1, 0); bestNorm = new Vector3f(0, 1, 0);
} }
results.clear();
}
}
// Fallback auf Basis-Terrain wenn noch keine Voxel vorhanden sind,
// damit Pinsel-Indikator und erster Pinsel-Tick korrekt auf dem Terrain landen.
if (bestPos == null && terrainNode != null) {
terrainNode.collideWith(ray, results);
if (results.size() > 0) {
bestPos = results.getClosestCollision().getContactPoint();
bestNorm = new Vector3f(0, 1, 0);
} }
} }
@@ -657,6 +705,8 @@ public class VoxelEditorState extends BaseAppState {
if (Float.isFinite(h)) { if (Float.isFinite(h)) {
input.voxelTool.plateauTarget.setValue(h); input.voxelTool.plateauTarget.setValue(h);
input.voxelTool.plateauTargetChanged = true; input.voxelTool.plateauTargetChanged = true;
input.heightTool.plateauHeight.setValue(h);
input.heightTool.plateauHeightChanged = true;
} }
return; return;
} }
@@ -722,9 +772,11 @@ public class VoxelEditorState extends BaseAppState {
applyColumnToTarget(chunk, cx, cy, cz, wx, wz, radius, strength, coord -> target); applyColumnToTarget(chunk, cx, cy, cz, wx, wz, radius, strength, coord -> target);
} else if (modeIdx == de.blight.editor.tool.VoxelTool.MODE_SMOOTH) { } else if (modeIdx == de.blight.editor.tool.VoxelTool.MODE_SMOOTH) {
if (lower) { if (lower) {
applyCliffColumn(chunk, cx, cy, cz, wx, wz, radius, strength, slopeParams); // RMB: Durchschnitt-Smooth (wie vormals LMB)
} else {
applySmoothColumn(chunk, cx, cy, cz, wx, wz, radius, strength, slopeParams); applySmoothColumn(chunk, cx, cy, cz, wx, wz, radius, strength, slopeParams);
} else {
// LMB: gleichmäßiger Slope vom höchsten Außenring-Punkt zum Gegenpunkt
applySlopeColumn(chunk, cx, cy, cz, wx, wz, radius, strength, slopeParams);
} }
} else { } else {
applyColumnBrush(chunk, cx, cy, cz, wx, wz, radius, strength, modeIdx, lower); applyColumnBrush(chunk, cx, cy, cz, wx, wz, radius, strength, modeIdx, lower);
@@ -1065,40 +1117,79 @@ public class VoxelEditorState extends BaseAppState {
* projLow = Projektion des Tiefpunkts auf die Achse (immer ≤ projHigh) * projLow = Projektion des Tiefpunkts auf die Achse (immer ≤ projHigh)
* avgH = Durchschnittshöhe aller Spalten im Pinselbereich (für Smooth-Modus) * avgH = Durchschnittshöhe aller Spalten im Pinselbereich (für Smooth-Modus)
*/ */
/**
* Slope-Parameter für den Smooth-LMB-Modus.
* Rückgabe: [0]=maxH, [1]=oppH, [2]=dirX, [3]=dirZ, [4]=projHigh, [5]=projOpp, [6]=avgH
* Achse zeigt vom Zentrum zum höchsten Außenring-Punkt; oppH ist der geometrisch
* gegenüberliegende Außenring-Punkt (kleinste Projektion auf diese Achse).
*/
private float[] computeSlopeParams(float brushWX, float brushWZ, float radius) { private float[] computeSlopeParams(float brushWX, float brushWZ, float radius) {
float r2 = radius * radius; float r2 = radius * radius;
float outerMin = 0.7f * radius;
float outerMin2 = outerMin * outerMin;
int xMin = (int)(brushWX - radius), xMax = (int) Math.ceil(brushWX + radius); int xMin = (int)(brushWX - radius), xMax = (int) Math.ceil(brushWX + radius);
int zMin = (int)(brushWZ - radius), zMax = (int) Math.ceil(brushWZ + radius); int zMin = (int)(brushWZ - radius), zMax = (int) Math.ceil(brushWZ + radius);
float maxH = Float.NEGATIVE_INFINITY, minH = Float.POSITIVE_INFINITY; // Höchsten Außenring-Punkt finden + Durchschnitt über den gesamten Pinsel
float maxX = brushWX, maxZ = brushWZ, minX = brushWX, minZ = brushWZ; float maxH = Float.NEGATIVE_INFINITY;
float sum = 0f; float maxOX = 0, maxOZ = 0;
float sum = 0f;
int count = 0; int count = 0;
for (int xi = xMin; xi <= xMax; xi++) { for (int xi = xMin; xi <= xMax; xi++) {
float dx = xi - brushWX; float dx = xi - brushWX;
for (int zi = zMin; zi <= zMax; zi++) { for (int zi = zMin; zi <= zMax; zi++) {
float dz = zi - brushWZ; float dz = zi - brushWZ;
if (dx*dx + dz*dz > r2) continue; float d2 = dx*dx + dz*dz;
if (d2 > r2) continue;
float h = columnTopWorldY(xi, zi); float h = columnTopWorldY(xi, zi);
if (h > maxH) { maxH = h; maxX = xi; maxZ = zi; }
if (h < minH) { minH = h; minX = xi; minZ = zi; }
sum += h; sum += h;
count++; count++;
if (d2 >= outerMin2 && h > maxH) { maxH = h; maxOX = dx; maxOZ = dz; }
} }
} }
if (maxH == Float.NEGATIVE_INFINITY) return new float[]{ Float.NaN, Float.NaN, 0, 0, 0, 0, Float.NaN }; if (maxH == Float.NEGATIVE_INFINITY || count == 0)
return new float[]{ Float.NaN, Float.NaN, 0, 0, 0, 0, Float.NaN };
// Richtung vom tiefsten zum höchsten Punkt — garantiert projHigh - projLow = |maxPos - minPos| // Achse: Zentrum → Hochpunkt
float ddx = maxX - minX, ddz = maxZ - minZ; float highDist = (float) Math.sqrt(maxOX*maxOX + maxOZ*maxOZ);
float len = (float) Math.sqrt(ddx*ddx + ddz*ddz); float dirX, dirZ;
if (len < 1e-4f) { ddx = 1f; ddz = 0f; } else { ddx /= len; ddz /= len; } if (highDist < 1e-4f) { dirX = 1f; dirZ = 0f; }
else { dirX = maxOX / highDist; dirZ = maxOZ / highDist; }
float projHigh = (maxX - brushWX) * ddx + (maxZ - brushWZ) * ddz; // Gegenüberliegender Punkt: Außenring-Vertex mit kleinstem Projektons-Wert
float projLow = (minX - brushWX) * ddx + (minZ - brushWZ) * ddz; float oppH = Float.NaN;
float oppOX = 0, oppOZ = 0;
float minProj = Float.MAX_VALUE;
for (int xi = xMin; xi <= xMax; xi++) {
float dx = xi - brushWX;
for (int zi = zMin; zi <= zMax; zi++) {
float dz = zi - brushWZ;
float d2 = dx*dx + dz*dz;
if (d2 < outerMin2 || d2 > r2) continue;
float proj = dx * dirX + dz * dirZ;
if (proj < minProj) {
minProj = proj;
oppH = columnTopWorldY(xi, zi);
oppOX = dx; oppOZ = dz;
}
}
}
if (Float.isNaN(oppH)) oppH = sum / count;
float projHigh = maxOX * dirX + maxOZ * dirZ;
float projOpp = oppOX * dirX + oppOZ * dirZ;
float avgH = sum / count; float avgH = sum / count;
return new float[]{ maxH, minH, ddx, ddz, projHigh, projLow, avgH }; return new float[]{ maxH, oppH, dirX, dirZ, projHigh, projOpp, avgH };
}
/** Raycast gegen reine Voxel-Geometrie; gibt nächsten Treffpunkt oder null zurück. */
public Vector3f raycastVoxelGeometry(com.jme3.math.Ray ray) {
CollisionResults results = new CollisionResults();
voxelRoot.collideWith(ray, results);
if (results.size() == 0) return null;
return results.getClosestCollision().getContactPoint();
} }
/** Welt-Y des höchsten Solid-Voxels an (worldX, worldZ), oder Terrain-Höhe wenn keine Voxel. */ /** Welt-Y des höchsten Solid-Voxels an (worldX, worldZ), oder Terrain-Höhe wenn keine Voxel. */
@@ -1251,27 +1342,22 @@ public class VoxelEditorState extends BaseAppState {
} }
/** /**
* Klippe-Pinsel (Smooth-Rechtsklick): scharfe Terrassenstufe mittig zwischen Hoch- und Tiefpunkt. * Slope-Pinsel (Smooth-Linksklick): gleichmäßige Neigung vom höchsten Außenring-Punkt
* zum geometrisch gegenüberliegenden Punkt.
*/ */
private void applyCliffColumn(VoxelChunk chunk, int cx, int cy, int cz, private void applySlopeColumn(VoxelChunk chunk, int cx, int cy, int cz,
float brushWX, float brushWZ, float brushWX, float brushWZ,
float radius, float strength, float[] sp) { float radius, float strength, float[] sp) {
if (sp == null || Float.isNaN(sp[0])) return; if (sp == null || Float.isNaN(sp[0]) || Float.isNaN(sp[1])) return;
final float highH = sp[0], lowH = sp[1], dirX = sp[2], dirZ = sp[3]; final float highH = sp[0], oppH = sp[1], dirX = sp[2], dirZ = sp[3];
final float projHigh = sp[4], projLow = sp[5]; final float projHigh = sp[4], projOpp = sp[5];
final float projRange = projHigh - projLow; final float projRange = projHigh - projOpp;
if (projRange < 0.5f) return; if (projRange < 0.5f) return;
final float projMid = (projHigh + projLow) * 0.5f;
final float blendHalf = Math.max(0.3f, projRange * 0.1f);
applyColumnToTarget(chunk, cx, cy, cz, brushWX, brushWZ, radius, strength, coord -> { applyColumnToTarget(chunk, cx, cy, cz, brushWX, brushWZ, radius, strength, coord -> {
float proj = coord[0] * dirX + coord[1] * dirZ; float proj = coord[0] * dirX + coord[1] * dirZ;
float rel = proj - projMid; float t = Math.max(0f, Math.min(1f, (proj - projOpp) / projRange));
if (rel > blendHalf) return highH; return oppH + t * (highH - oppH);
if (rel < -blendHalf) return lowH;
float t = (rel / blendHalf + 1f) * 0.5f;
return lowH + (highH - lowH) * t;
}); });
} }
@@ -1744,11 +1830,18 @@ public class VoxelEditorState extends BaseAppState {
private void checkAutoSave() { private void checkAutoSave() {
if (idleSinceSave < AUTO_SAVE_IDLE_S) return; if (idleSinceSave < AUTO_SAVE_IDLE_S) return;
boolean anyDirty = false; boolean anyDirty = splatDirty;
for (VoxelChunk c : chunks.values()) { if (c.dirty) { anyDirty = true; break; } } for (VoxelChunk c : chunks.values()) { if (c.dirty) { anyDirty = true; break; } }
if (!anyDirty) return; if (!anyDirty) return;
idleSinceSave = 0f; idleSinceSave = 0f;
final boolean saveSplat = splatDirty;
if (saveSplat) splatDirty = false;
final byte[] snapR = saveSplat ? voxSplatR.clone() : null;
final byte[] snapG = saveSplat ? voxSplatG.clone() : null;
final byte[] snapB = saveSplat ? voxSplatB.clone() : null;
final byte[] snapA = saveSplat ? voxSplatA.clone() : null;
executor.submit(() -> { executor.submit(() -> {
for (VoxelChunk chunk : chunks.values()) { for (VoxelChunk chunk : chunks.values()) {
if (!chunk.dirty) continue; if (!chunk.dirty) continue;
@@ -1759,9 +1852,212 @@ public class VoxelEditorState extends BaseAppState {
chunk.cx, chunk.cy, chunk.cz, e.getMessage()); chunk.cx, chunk.cy, chunk.cz, e.getMessage());
} }
} }
if (saveSplat) {
try {
saveVoxelSplat(snapR, snapG, snapB, snapA);
} catch (IOException e) {
log.error("Auto-Save Voxel-Splatmap: {}", e.getMessage());
}
}
}); });
} }
// ── Voxel-Splatmap: Init / Speichern / Laden ─────────────────────────────
private void initVoxelSplat() {
voxSplatR = new byte[VOX_SPLAT_SIZE * VOX_SPLAT_SIZE];
voxSplatG = new byte[VOX_SPLAT_SIZE * VOX_SPLAT_SIZE];
voxSplatB = new byte[VOX_SPLAT_SIZE * VOX_SPLAT_SIZE];
voxSplatA = new byte[VOX_SPLAT_SIZE * VOX_SPLAT_SIZE];
Arrays.fill(voxSplatR, (byte) 255);
// Vorhandene Datei laden (falls vorhanden)
Path file = voxelSplatFile();
if (Files.exists(file)) {
try {
loadVoxelSplat(file);
} catch (IOException e) {
log.warn("Voxel-Splatmap nicht ladbar, wird neu initialisiert: {}", e.getMessage());
Arrays.fill(voxSplatR, (byte) 255);
Arrays.fill(voxSplatG, (byte) 0);
Arrays.fill(voxSplatB, (byte) 0);
Arrays.fill(voxSplatA, (byte) 0);
}
}
voxSplatBuf = BufferUtils.createByteBuffer(VOX_SPLAT_SIZE * VOX_SPLAT_SIZE * 4);
for (int i = 0; i < VOX_SPLAT_SIZE * VOX_SPLAT_SIZE; i++) {
voxSplatBuf.put(voxSplatR[i]).put(voxSplatG[i]).put(voxSplatB[i]).put(voxSplatA[i]);
}
voxSplatBuf.flip();
voxSplatImage = new Image(Image.Format.RGBA8, VOX_SPLAT_SIZE, VOX_SPLAT_SIZE,
voxSplatBuf, ColorSpace.Linear);
voxSplatTex = new Texture2D(voxSplatImage);
voxelMaterial.setTexture("SplatMap", voxSplatTex);
applyVoxelSplatTextures(voxelMaterial);
}
private Path voxelSplatFile() {
return MapIO.getMapPath().resolveSibling("blight_voxel_splat.bin");
}
private void loadVoxelSplat(Path file) throws IOException {
try (DataInputStream in = new DataInputStream(
new BufferedInputStream(new GZIPInputStream(Files.newInputStream(file))))) {
in.readFully(voxSplatR);
in.readFully(voxSplatG);
in.readFully(voxSplatB);
in.readFully(voxSplatA);
// Lade Texturpfade falls vorhanden
try {
String[] paths = new String[4];
for (int i = 0; i < 4; i++) paths[i] = in.readUTF();
input.voxelSplatTexturePaths = paths;
} catch (EOFException ignored) {}
}
}
private void saveVoxelSplat(byte[] snapR, byte[] snapG, byte[] snapB, byte[] snapA)
throws IOException {
Path file = voxelSplatFile();
Path tmp = file.resolveSibling(file.getFileName() + ".tmp");
Files.createDirectories(file.getParent());
try (DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(new GZIPOutputStream(Files.newOutputStream(tmp))))) {
out.write(snapR);
out.write(snapG);
out.write(snapB);
out.write(snapA);
String[] paths = input.voxelSplatTexturePaths;
for (int i = 0; i < 4; i++) {
String p = (paths != null && i < paths.length && paths[i] != null) ? paths[i] : "";
out.writeUTF(p);
}
}
try {
Files.move(tmp, file, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (java.nio.file.AtomicMoveNotSupportedException e) {
Files.move(tmp, file, StandardCopyOption.REPLACE_EXISTING);
}
log.info("Voxel-Splatmap gespeichert: {}", file);
}
// ── Voxel-Textur-Malen ────────────────────────────────────────────────────
private void processVoxelTextureEdits() {
SharedInput.TextureEdit edit;
int processed = 0;
while ((edit = input.voxelTextureEditQueue.poll()) != null && processed < MAX_EDITS_PER_FRAME) {
processed++;
float jmeX = (float)(edit.screenX() * input.viewportScaleX);
float jmeY = cam.getHeight() - (float)(edit.screenY() * input.viewportScaleY);
Vector3f near = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 0f);
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
com.jme3.math.Ray ray = new com.jme3.math.Ray(near, far.subtract(near).normalizeLocal());
// Raycast gegen Voxel-Geometrie
CollisionResults hits = new CollisionResults();
voxelRoot.collideWith(ray, hits);
Vector3f contact = null;
if (hits.size() > 0) {
contact = hits.getClosestCollision().getContactPoint();
}
// Fallback auf Basis-Terrain
if (contact == null && terrainNode != null) {
CollisionResults terrHits = new CollisionResults();
terrainNode.collideWith(ray, terrHits);
if (terrHits.size() > 0) contact = terrHits.getClosestCollision().getContactPoint();
}
if (contact == null) continue;
int selIdx = input.textureTool.textureIndex.getSelectedIndex();
float str = (float) input.textureTool.brushStrength.getValue();
if (edit.action() > 0) {
applyVoxelTexturePaint(contact, str, selIdx);
} else {
applyVoxelTexturePaint(contact, str, 0);
}
}
if (processed > 0) {
idleSinceSave = 0f;
}
}
private void applyVoxelTexturePaint(Vector3f contact, float strength, int textureIndex) {
float radius = (float) input.textureTool.brushRadius.getValue();
int centerPX = Math.round((contact.x + VOX_WORLD_HALF) / VOX_SPLAT_WE_PER_PX);
int centerPZ = Math.round((contact.z + VOX_WORLD_HALF) / VOX_SPLAT_WE_PER_PX);
int pixR = (int) Math.ceil(radius / VOX_SPLAT_WE_PER_PX);
boolean changed = false;
for (int dz = -pixR; dz <= pixR; dz++) {
int pz = centerPZ + dz;
if (pz < 0 || pz >= VOX_SPLAT_SIZE) continue;
for (int dx = -pixR; dx <= pixR; dx++) {
int px = centerPX + dx;
if (px < 0 || px >= VOX_SPLAT_SIZE) continue;
float distWE = FastMath.sqrt(dx * dx + dz * dz) * VOX_SPLAT_WE_PER_PX;
if (distWE >= radius) continue;
float t = distWE / radius;
float falloff = (1f + FastMath.cos(FastMath.PI * t)) * 0.5f;
float blend = (textureIndex == 0) ? falloff : strength * falloff;
int idx = pz * VOX_SPLAT_SIZE + px;
float curG = (voxSplatG[idx] & 0xFF) / 255f;
float curB = (voxSplatB[idx] & 0xFF) / 255f;
float curA = (voxSplatA[idx] & 0xFF) / 255f;
float tgG = (textureIndex == 1) ? 1f : 0f;
float tgB = (textureIndex == 2) ? 1f : 0f;
float tgA = (textureIndex == 3) ? 1f : 0f;
voxSplatR[idx] = (byte) 255;
voxSplatG[idx] = (byte) Math.round((curG + (tgG - curG) * blend) * 255f);
voxSplatB[idx] = (byte) Math.round((curB + (tgB - curB) * blend) * 255f);
voxSplatA[idx] = (byte) Math.round((curA + (tgA - curA) * blend) * 255f);
int bi = idx * 4;
voxSplatBuf.put(bi, voxSplatR[idx]);
voxSplatBuf.put(bi + 1, voxSplatG[idx]);
voxSplatBuf.put(bi + 2, voxSplatB[idx]);
voxSplatBuf.put(bi + 3, voxSplatA[idx]);
changed = true;
}
}
if (changed) {
voxSplatBuf.rewind();
voxSplatImage.setUpdateNeeded();
splatDirty = true;
}
}
private void applyVoxelSplatTextures(Material mat) {
String[] paths = input.voxelSplatTexturePaths;
String[] slots = {"TexFlat2", "TexFlat3", "TexFlat4"};
int[][] fallbacks = {{100, 130, 60}, {110, 100, 90}, {80, 80, 100}};
for (int i = 0; i < 3; i++) {
String p = (paths != null && (i + 1) < paths.length) ? paths[i + 1] : "";
if (p != null && !p.isEmpty()) {
try {
Texture t = assets.loadTexture(p);
t.getImage().setColorSpace(ColorSpace.Linear);
t.setWrap(Texture.WrapMode.Repeat);
mat.setTexture(slots[i], t);
} catch (Exception e) {
log.warn("Voxel-Splat-Textur {} nicht ladbar: {}", p, e.getMessage());
mat.setTexture(slots[i], solidColorTexture(fallbacks[i]));
}
} else {
mat.setTexture(slots[i], solidColorTexture(fallbacks[i]));
}
}
}
// ── Intern: Material ───────────────────────────────────────────────────── // ── Intern: Material ─────────────────────────────────────────────────────
private Material buildMaterial() { private Material buildMaterial() {
@@ -1877,7 +2173,9 @@ public class VoxelEditorState extends BaseAppState {
private void updateBrushIndicator() { private void updateBrushIndicator() {
if (brushIndicator == null) return; if (brushIndicator == null) return;
if (input.activeLayer != SharedInput.LAYER_VOXEL) { boolean isVoxelOrTex = input.activeLayer == SharedInput.LAYER_VOXEL
|| input.activeLayer == 4;
if (!isVoxelOrTex) {
brushIndicator.setCullHint(Spatial.CullHint.Always); brushIndicator.setCullHint(Spatial.CullHint.Always);
return; return;
} }
@@ -1891,7 +2189,9 @@ public class VoxelEditorState extends BaseAppState {
float jmeY = cam.getHeight() - my * (float) input.viewportScaleY; float jmeY = cam.getHeight() - my * (float) input.viewportScaleY;
Hit hit = raycastHit(jmeX, jmeY); Hit hit = raycastHit(jmeX, jmeY);
if (hit != null) { if (hit != null) {
float r = (float) input.voxelTool.brushRadius.getValue(); float r = (input.activeLayer == 4)
? (float) input.textureTool.brushRadius.getValue()
: (float) input.voxelTool.brushRadius.getValue();
// Leicht entlang der Flächen-Normalen versetzt, um Z-Fighting zu vermeiden // Leicht entlang der Flächen-Normalen versetzt, um Z-Fighting zu vermeiden
brushIndicator.setLocalTranslation(hit.pos.add(hit.normal().mult(0.05f))); brushIndicator.setLocalTranslation(hit.pos.add(hit.normal().mult(0.05f)));

View File

@@ -0,0 +1,321 @@
package de.blight.editor.tree;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
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;
import java.util.Random;
/**
* Prozeduraler Fruchtbusch-Generator.
*
* Erzeugt drei Kinder-Nodes:
* "bark" — Stamm + Äste als Zylinder-Geometrie
* "leaves" — Blatt-Quads (Cross-Billboard) an den Astspitzen
* "fruits" — kleine Kugel-Approx. (2 gekreuzte Quads) an Blattclustern
*
* Vertex-Farbe R = Wind-Gewicht (0 = Stammbasis, 1 = Astspitze).
*/
public class FruitBushMeshBuilder {
public record MeshResult(Mesh bark, Mesh leaves, Mesh fruits) {}
public static MeshResult build(FruitBushOptions o) {
Random rng = new Random(o.seed);
VertexCollector barkCol = new VertexCollector();
VertexCollector leafCol = new VertexCollector();
VertexCollector fruitCol = new VertexCollector();
// ── Stamm ────────────────────────────────────────────────────────────
int trunkSegs = 7;
Vector3f trunkBot = new Vector3f(0f, 0f, 0f);
Vector3f trunkTop = new Vector3f(0f, o.trunkHeight, 0f);
addCylinder(barkCol, trunkBot, trunkTop, o.trunkRadius, o.trunkRadius * 0.70f,
0f, 0.15f, trunkSegs);
// ── Primäräste ───────────────────────────────────────────────────────
float branchAngleRad = o.branchAngle * FastMath.DEG_TO_RAD;
float twigAngleRad = o.twigAngle * FastMath.DEG_TO_RAD;
for (int b = 0; b < o.branchCount; b++) {
float azimuth = b * FastMath.TWO_PI / o.branchCount
+ rng.nextFloat() * 0.4f - 0.2f;
float lenScale = 0.80f + rng.nextFloat() * 0.40f;
float bLen = o.branchLength * lenScale;
// Richtung: um branchAngle° geneigt, azimuth° um Y gedreht
Vector3f bDir = branchDir(Vector3f.UNIT_Y, azimuth, branchAngleRad);
// Startet etwas über dem Stammende mit leichter Versetzung
float startT = 0.6f + rng.nextFloat() * 0.35f;
Vector3f bBase = trunkTop.mult(startT);
Vector3f bTip = bBase.add(bDir.mult(bLen));
float branchBaseRad = o.trunkRadius * 0.45f;
float windBase = 0.15f + startT * 0.20f;
float windTip = 0.60f;
addCylinder(barkCol, bBase, bTip, branchBaseRad, branchBaseRad * 0.22f,
windBase, windTip, 5);
// ── Sekundäräste (Reiser) ─────────────────────────────────────
for (int t = 0; t < o.twigCount; t++) {
float tAz = t * FastMath.TWO_PI / o.twigCount
+ rng.nextFloat() * 0.5f - 0.25f;
float tLenScl = 0.75f + rng.nextFloat() * 0.50f;
float tLen = o.twigLength * tLenScl;
Vector3f tDir = branchDir(bDir, tAz, twigAngleRad);
float tStart = 0.35f + rng.nextFloat() * 0.50f;
Vector3f tBase = bBase.add(bDir.mult(bLen * tStart));
Vector3f tTip = tBase.add(tDir.mult(tLen));
float windTBase = windBase + (windTip - windBase) * tStart;
addCylinder(barkCol, tBase, tTip, o.twigRadius, o.twigRadius * 0.10f,
windTBase, 0.90f, 4);
// Blätter entlang des gesamten Reisers alle ~12 cm ein Cluster
int twigClusters = Math.max(2, Math.round(tLen / 0.12f));
int leafsPerCluster = Math.max(3, o.leafCount / 2);
for (int lc = 0; lc < twigClusters; lc++) {
float lT = FastMath.clamp(
(lc + 0.5f + rng.nextFloat() * 0.4f - 0.2f) / twigClusters,
0.05f, 1.0f);
Vector3f lPos = tBase.add(tDir.mult(tLen * lT));
float wL = windTBase + (0.90f - windTBase) * lT;
addLeafCluster(leafCol, lPos, wL, o.leafSize, leafsPerCluster, rng);
}
// Früchte an der Astgabel (Gabelpunkt Primärast → Reiser)
if (o.fruitsEnabled && rng.nextFloat() < o.fruitDensity) {
addFruitSphere(fruitCol, tBase, windTBase, o.fruitRadius, rng);
}
}
// Blätter auch entlang des Primärasts (zwischen den Gabeln)
int branchClusters = Math.max(1, Math.round(bLen / 0.22f));
for (int lc = 0; lc < branchClusters; lc++) {
float lT = (lc + 0.5f + rng.nextFloat() * 0.3f - 0.15f) / branchClusters;
lT = FastMath.clamp(lT, 0.05f, 1.0f);
Vector3f lPos = bBase.add(bDir.mult(bLen * lT));
float wL = windBase + (windTip - windBase) * lT;
addLeafCluster(leafCol, lPos, wL, o.leafSize * 0.80f,
Math.max(2, o.leafCount / 3), rng);
}
}
return new MeshResult(barkCol.toMesh(), leafCol.toMesh(), fruitCol.toMesh());
}
// ── Zylinder-Segment ─────────────────────────────────────────────────────
private static void addCylinder(VertexCollector col,
Vector3f start, Vector3f end,
float rBot, float rTop,
float windBot, float windTop, int N) {
Vector3f axis = end.subtract(start);
if (axis.lengthSquared() < 1e-8f) return;
axis.normalizeLocal();
Vector3f perp1 = (Math.abs(axis.y) < 0.9f)
? axis.cross(Vector3f.UNIT_Y).normalizeLocal()
: axis.cross(Vector3f.UNIT_X).normalizeLocal();
Vector3f perp2 = axis.cross(perp1).normalizeLocal();
int base = col.vertexCount;
int N1 = N + 1;
for (int ring = 0; ring < 2; ring++) {
Vector3f center = (ring == 0) ? start : end;
float r = (ring == 0) ? rBot : rTop;
float wind = (ring == 0) ? windBot : windTop;
float vCoord = ring;
for (int i = 0; i <= N; i++) {
float theta = FastMath.TWO_PI * i / N;
float cos = FastMath.cos(theta);
float sin = FastMath.sin(theta);
float nx = cos * perp1.x + sin * perp2.x;
float ny = cos * perp1.y + sin * perp2.y;
float nz = cos * perp1.z + sin * perp2.z;
col.add(center.x + nx * r, center.y + ny * r, center.z + nz * r,
nx, ny, nz, (float) i / N, vCoord, wind);
}
}
for (int i = 0; i < N; i++) {
int b0 = base + i, b1 = base + i + 1;
int t0 = base + N1 + i, t1 = base + N1 + i + 1;
col.tri(b0, b1, t1);
col.tri(b0, t1, t0);
}
}
// ── Blatt-Cluster ────────────────────────────────────────────────────────
private static void addLeafCluster(VertexCollector col, Vector3f tip,
float wind, float scale, int count, Random rng) {
for (int i = 0; i < count; i++) {
float ox = rng.nextFloat() * scale * 0.20f - scale * 0.10f;
float oy = rng.nextFloat() * scale * 0.10f - scale * 0.05f;
float oz = rng.nextFloat() * scale * 0.20f - scale * 0.10f;
float s = scale * (0.70f + rng.nextFloat() * 0.60f);
// Volle 3D-Richtung: zufälliger Azimuth + zufällige Neigung (volle Kugel)
float yaw = rng.nextFloat() * FastMath.TWO_PI;
float pitch = rng.nextFloat() * FastMath.PI;
float sinP = FastMath.sin(pitch);
float gx = sinP * FastMath.cos(yaw);
float gy = FastMath.cos(pitch);
float gz = sinP * FastMath.sin(yaw);
addLeafQuad(col, tip.x + ox, tip.y + oy, tip.z + oz, s, wind, gx, gy, gz);
}
}
private static void addLeafQuad(VertexCollector col,
float cx, float cy, float cz,
float s, float wind,
float gx, float gy, float gz) {
// Rechtsvektor senkrecht zur Wachstumsrichtung
float rx, ry, rz;
if (Math.abs(gy) < 0.95f) {
rx = gz; ry = 0f; rz = -gx; // cross(UNIT_Y, grow)
} else {
rx = 0f; ry = -gz; rz = gy; // cross(UNIT_X, grow)
}
float rLen = FastMath.sqrt(rx * rx + ry * ry + rz * rz);
if (rLen > 1e-5f) { rx /= rLen; ry /= rLen; rz /= rLen; }
float hw = s * 0.5f;
float hh = s * 1.3f;
// Normalvektor: right × grow
float nx = ry * gz - rz * gy;
float ny = rz * gx - rx * gz;
float nz = rx * gy - ry * gx;
float windTip = Math.min(1.0f, wind + 0.25f);
int base = col.vertexCount;
// Untere Kante am Ast (verankert)
col.add(cx - rx * hw, cy - ry * hw, cz - rz * hw,
nx, ny, nz, 0f, 0f, wind);
col.add(cx + rx * hw, cy + ry * hw, cz + rz * hw,
nx, ny, nz, 1f, 0f, wind);
// Obere Kante frei (schwingt im Wind)
col.add(cx + rx * hw + gx * hh, cy + ry * hw + gy * hh, cz + rz * hw + gz * hh,
nx, ny, nz, 1f, 1f, windTip);
col.add(cx - rx * hw + gx * hh, cy - ry * hw + gy * hh, cz - rz * hw + gz * hh,
nx, ny, nz, 0f, 1f, windTip);
col.tri(base, base + 1, base + 2);
col.tri(base, base + 2, base + 3);
col.tri(base + 2, base + 1, base);
col.tri(base + 3, base + 2, base);
}
// ── Frucht-Kugel (2 gekreuzte Quads) ─────────────────────────────────────
private static void addFruitSphere(VertexCollector col, Vector3f tip,
float wind, float radius, Random rng) {
float ox = rng.nextFloat() * radius * 0.8f - radius * 0.4f;
// Frucht hängt unterhalb der Gabel: Zentrum ca. 1.8× Radius unter Gabelposition
float oy = -radius * 1.8f + rng.nextFloat() * radius * 0.4f - radius * 0.2f;
float oz = rng.nextFloat() * radius * 0.8f - radius * 0.4f;
float cx = tip.x + ox, cy = tip.y + oy, cz = tip.z + oz;
addFruitQuad(col, cx, cy, cz, radius, wind, 0f);
addFruitQuad(col, cx, cy, cz, radius, wind, FastMath.HALF_PI);
}
private static void addFruitQuad(VertexCollector col,
float cx, float cy, float cz,
float r, float wind, float yaw) {
float cosY = FastMath.cos(yaw), sinY = FastMath.sin(yaw);
float[] xs = { cx - cosY * r, cx + cosY * r, cx + cosY * r, cx - cosY * r };
float[] ys = { cy - r, cy - r, cy + r, cy + r };
float[] zs = { cz + sinY * r, cz - sinY * r, cz - sinY * r, cz + sinY * r };
float nnx = sinY, nnz = cosY;
int base = col.vertexCount;
col.add(xs[0], ys[0], zs[0], nnx, 0, nnz, 0, 0, wind);
col.add(xs[1], ys[1], zs[1], nnx, 0, nnz, 1, 0, wind);
col.add(xs[2], ys[2], zs[2], nnx, 0, nnz, 1, 1, wind);
col.add(xs[3], ys[3], zs[3], nnx, 0, nnz, 0, 1, wind);
col.tri(base, base+1, base+2);
col.tri(base, base+2, base+3);
col.tri(base+2, base+1, base);
col.tri(base+3, base+2, base);
}
// ── Ast-Richtung ─────────────────────────────────────────────────────────
private static Vector3f branchDir(Vector3f parent, float yaw, float tiltRad) {
Vector3f perp = (Math.abs(parent.y) < 0.9f)
? parent.cross(Vector3f.UNIT_Y).normalizeLocal()
: parent.cross(Vector3f.UNIT_X).normalizeLocal();
Quaternion tilt = new Quaternion().fromAngleAxis(tiltRad, perp);
Quaternion spin = new Quaternion().fromAngleAxis(yaw, parent);
return spin.mult(tilt.mult(parent)).normalizeLocal();
}
// ── Vertex-Sammler ────────────────────────────────────────────────────────
private 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> wind = 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 w) {
pos.add(x); pos.add(y); pos.add(z);
norm.add(nx); norm.add(ny); norm.add(nz);
uv.add(u); uv.add(v);
wind.add(w);
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 : wind) { colB.put(f); colB.put(0f); colB.put(0f); colB.put(1f); }
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();
mesh.updateCounts();
return mesh;
}
}
}

View File

@@ -0,0 +1,145 @@
package de.blight.editor.tree;
/** Parameter für den prozeduralen Fruchtbusch-Generator. */
public class FruitBushOptions {
public int seed = 55123;
/** Höhe des Stamms in Metern. */
public float trunkHeight = 1.20f;
/** Radius des Stamms an der Basis. */
public float trunkRadius = 0.10f;
/** Anzahl Primäräste (direkt vom Stamm). */
public int branchCount = 5;
/** Winkel der Primäräste zur Vertikalen (Grad). */
public float branchAngle = 50f;
/** Länge der Primäräste (m). */
public float branchLength = 1.60f;
/** Anzahl Sekundäräste pro Primärast. */
public int twigCount = 4;
/** Winkel der Sekundäräste zur Elternrichtung (Grad). */
public float twigAngle = 45f;
/** Länge der Sekundäräste (m). */
public float twigLength = 0.90f;
/** Radius der Endäste. */
public float twigRadius = 0.024f;
/** Blatt-Größe (Quadrat-Seite, m). */
public float leafSize = 0.44f;
/** Blätter pro Sekundärast. */
public int leafCount = 16;
/** Blatt-Textur (relativ zum Asset-Root). */
public String leafTexture = "Textures/internal/foliage/fruittree.png";
/** Blatt-Tint R. */
public float leafR = 0.50f, leafG = 0.80f, leafB = 0.22f;
/** Früchte aktivieren. */
public boolean fruitsEnabled = true;
/** Wahrscheinlichkeit [01], dass ein Blattcluster eine Frucht bekommt. */
public float fruitDensity = 0.35f;
/** Frucht-Radius (m). */
public float fruitRadius = 0.08f;
/** Frucht-Farbe R. */
public float fruitR = 1.0f, fruitG = 0.45f, fruitB = 0.0f;
/** Frucht-Textur (relativ zum Asset-Root, RGBA mit Alpha-Maske). */
public String fruitTexture = "Textures/internal/fruits/orange.png";
public float windStrength = 0.20f;
public float windSpeed = 0.60f;
// ── Presets ──────────────────────────────────────────────────────────────
public static FruitBushOptions orange() {
FruitBushOptions o = new FruitBushOptions();
o.seed = 55123;
o.trunkHeight = 1.30f;
o.trunkRadius = 0.11f;
o.branchCount = 5;
o.branchAngle = 50f;
o.branchLength = 1.80f;
o.twigCount = 4;
o.twigAngle = 44f;
o.twigLength = 1.00f;
o.twigRadius = 0.026f;
o.leafSize = 0.48f;
o.leafCount = 18;
o.leafR = 0.50f; o.leafG = 0.82f; o.leafB = 0.22f;
o.fruitsEnabled = true;
o.fruitDensity = 0.32f;
o.fruitRadius = 0.08f;
o.fruitR = 1.00f; o.fruitG = 0.45f; o.fruitB = 0.00f;
o.fruitTexture = "Textures/internal/fruits/orange.png";
return o;
}
public static FruitBushOptions lemon() {
FruitBushOptions o = new FruitBushOptions();
o.seed = 63241;
o.trunkHeight = 1.10f;
o.trunkRadius = 0.096f;
o.branchCount = 6;
o.branchAngle = 46f;
o.branchLength = 1.50f;
o.twigCount = 3;
o.twigAngle = 40f;
o.twigLength = 0.84f;
o.twigRadius = 0.024f;
o.leafSize = 0.44f;
o.leafCount = 16;
o.leafR = 0.48f; o.leafG = 0.80f; o.leafB = 0.25f;
o.fruitsEnabled = true;
o.fruitDensity = 0.30f;
o.fruitRadius = 0.08f;
o.fruitR = 0.92f; o.fruitG = 0.88f; o.fruitB = 0.12f;
o.fruitTexture = "Textures/internal/fruits/lemon.png";
return o;
}
public static FruitBushOptions apricot() {
FruitBushOptions o = new FruitBushOptions();
o.seed = 47892;
o.trunkHeight = 1.00f;
o.trunkRadius = 0.084f;
o.branchCount = 4;
o.branchAngle = 54f;
o.branchLength = 1.40f;
o.twigCount = 4;
o.twigAngle = 48f;
o.twigLength = 0.80f;
o.twigRadius = 0.022f;
o.leafSize = 0.40f;
o.leafCount = 16;
o.leafR = 0.52f; o.leafG = 0.78f; o.leafB = 0.20f;
o.fruitsEnabled = true;
o.fruitDensity = 0.68f;
o.fruitRadius = 0.04f;
o.fruitR = 0.95f; o.fruitG = 0.62f; o.fruitB = 0.24f;
o.fruitTexture = "Textures/internal/fruits/apricot.png";
return o;
}
public FruitBushOptions copy() {
FruitBushOptions c = new FruitBushOptions();
c.seed = seed;
c.trunkHeight = trunkHeight;
c.trunkRadius = trunkRadius;
c.branchCount = branchCount;
c.branchAngle = branchAngle;
c.branchLength = branchLength;
c.twigCount = twigCount;
c.twigAngle = twigAngle;
c.twigLength = twigLength;
c.twigRadius = twigRadius;
c.leafSize = leafSize;
c.leafCount = leafCount;
c.leafTexture = leafTexture;
c.leafR = leafR; c.leafG = leafG; c.leafB = leafB;
c.fruitsEnabled = fruitsEnabled;
c.fruitDensity = fruitDensity;
c.fruitRadius = fruitRadius;
c.fruitR = fruitR; c.fruitG = fruitG; c.fruitB = fruitB;
c.fruitTexture = fruitTexture;
c.windStrength = windStrength;
c.windSpeed = windSpeed;
return c;
}
}

View File

@@ -0,0 +1,421 @@
package de.blight.editor.tree;
import com.jme3.math.FastMath;
import com.jme3.math.Vector3f;
import com.jme3.scene.Mesh;
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;
import java.util.Random;
public class GrapevineMeshBuilder {
public record MeshResult(Mesh bark, Mesh wires, Mesh leaves, Mesh fruits) {}
public static MeshResult build(GrapevineOptions o) {
Random rng = new Random(o.seed);
float halfW = o.totalWidth * 0.5f;
VertexCollector barkCol = new VertexCollector();
VertexCollector wireCol = new VertexCollector();
VertexCollector leafCol = new VertexCollector();
VertexCollector fruitCol = new VertexCollector();
// ── Pfosten ───────────────────────────────────────────────────────────
addCylinder(barkCol,
new Vector3f(-halfW, 0, 0), new Vector3f(-halfW, o.totalHeight + 0.15f, 0),
o.postRadius, o.postRadius * 0.80f, 0f, 0f, 7);
addCylinder(barkCol,
new Vector3f(halfW, 0, 0), new Vector3f(halfW, o.totalHeight + 0.15f, 0),
o.postRadius, o.postRadius * 0.80f, 0f, 0f, 7);
// ── Drähte ────────────────────────────────────────────────────────────
float wireR = 0.006f;
for (int w = 0; w < o.wireCount; w++) {
float wh = o.firstWireH
+ (o.totalHeight - o.firstWireH) * (float) w / Math.max(1, o.wireCount - 1);
addCylinder(wireCol,
new Vector3f(-halfW, wh, 0), new Vector3f(halfW, wh, 0),
wireR, wireR, 0f, 0f, 4);
}
// ── Stamm (gewunden) ──────────────────────────────────────────────────
addBentPath(barkCol,
new Vector3f(0, 0, 0), new Vector3f(0, o.firstWireH, 0),
o.trunkRadius, o.trunkRadius * 0.70f, 0f, 0f,
4, 6, 0.10f, rng);
// ── Trieb-t-Werte vorab berechnen → Armlänge = Position des letzten Triebs
float[] leftTs = new float[o.shootCount];
float[] rightTs = new float[o.shootCount];
float leftMaxT = 0f, rightMaxT = 0f;
for (int s = 0; s < o.shootCount; s++) {
leftTs[s] = FastMath.clamp((s + 0.5f + rng.nextFloat() * 0.5f - 0.25f) / o.shootCount, 0.08f, 0.97f);
rightTs[s] = FastMath.clamp((s + 0.5f + rng.nextFloat() * 0.5f - 0.25f) / o.shootCount, 0.08f, 0.97f);
leftMaxT = Math.max(leftMaxT, leftTs[s]);
rightMaxT = Math.max(rightMaxT, rightTs[s]);
}
// ── Arme (enden genau beim letzten Trieb) ────────────────────────────
Vector3f armCenter = new Vector3f(0, o.firstWireH, 0);
float leftArmEnd = halfW * leftMaxT;
float rightArmEnd = halfW * rightMaxT;
Vector3f[] leftArmPts = bentPathPoints(armCenter, new Vector3f(-leftArmEnd, o.firstWireH, 0), 5, 0.07f, rng);
Vector3f[] rightArmPts = bentPathPoints(armCenter, new Vector3f( rightArmEnd, o.firstWireH, 0), 5, 0.07f, rng);
addPolylineCylinder(barkCol, leftArmPts, o.trunkRadius * 0.65f, o.trunkRadius * 0.38f, 0f, 0f, 5);
addPolylineCylinder(barkCol, rightArmPts, o.trunkRadius * 0.65f, o.trunkRadius * 0.38f, 0f, 0f, 5);
// ── Triebe + Blätter + Trauben ────────────────────────────────────────
float shootH = o.totalHeight - o.firstWireH;
for (int side = -1; side <= 1; side += 2) {
float[] ts = (side == -1) ? leftTs : rightTs;
float maxT = (side == -1) ? leftMaxT : rightMaxT;
Vector3f[] armPts = (side == -1) ? leftArmPts : rightArmPts;
for (int s = 0; s < o.shootCount; s++) {
float t = ts[s];
// Startpunkt exakt auf dem gebogenen Arm (t/maxT → 0..1 entlang des Pfads)
Vector3f sBase = interpPath(armPts, t / maxT);
float tiltX = rng.nextFloat() * 0.12f - 0.06f;
float tiltZ = rng.nextFloat() * 0.12f - 0.06f;
Vector3f sDir = new Vector3f(tiltX, 1f, tiltZ).normalizeLocal();
Vector3f sTip = sBase.add(sDir.mult(shootH));
Vector3f[] shootPts = bentPathPoints(sBase, sTip, 3, 0.12f, rng);
addPolylineCylinder(barkCol, shootPts,
o.shootRadius, o.shootRadius * 0.20f, 0f, 0f, 4);
// Blätter im oberen 65%
int clusters = Math.max(2, o.leafCount / 2);
for (int lc = 0; lc < clusters; lc++) {
float lt = FastMath.clamp(
0.35f + (lc + 0.5f + rng.nextFloat() * 0.3f - 0.15f) / clusters * 0.65f,
0.30f, 1.0f);
Vector3f lp = interpPath(shootPts, lt);
float wl = 0.55f + 0.45f * lt;
addLeafCluster(leafCol, lp, wl, o.leafSize,
Math.max(2, o.leafCount / clusters), rng);
}
// 2 Trauben immer, 3. mit 50 % Wahrscheinlichkeit weiter oben.
// Unterste Traube: Oberkante auf Höhe des niedersten Blattes (t≈0.35).
int grapeCount = rng.nextFloat() < 0.50f ? 3 : 2;
float[] grapeTs = new float[]{ 0.33f + rng.nextFloat() * 0.06f, // 0.330.39
0.48f + rng.nextFloat() * 0.10f, // 0.480.58
0.65f + rng.nextFloat() * 0.12f }; // 0.650.77
for (int g = 0; g < grapeCount; g++) {
Vector3f ga = interpPath(shootPts, grapeTs[g]);
addGrapeBunch(fruitCol, ga.x, ga.y, ga.z, 0.12f, 0.25f);
}
}
}
return new MeshResult(barkCol.toMesh(), wireCol.toMesh(), leafCol.toMesh(), fruitCol.toMesh());
}
// ── Blatt-Cluster / Quad ──────────────────────────────────────────────────
private static void addLeafCluster(VertexCollector col, Vector3f tip,
float wind, float scale, int count, Random rng) {
for (int i = 0; i < count; i++) {
float ox = rng.nextFloat() * scale * 0.20f - scale * 0.10f;
float oy = rng.nextFloat() * scale * 0.10f - scale * 0.05f;
float oz = rng.nextFloat() * scale * 0.20f - scale * 0.10f;
float s = scale * (0.70f + rng.nextFloat() * 0.60f);
float yaw = rng.nextFloat() * FastMath.TWO_PI;
float pitch = rng.nextFloat() * FastMath.PI;
float sinP = FastMath.sin(pitch);
float gx = sinP * FastMath.cos(yaw);
float gy = FastMath.cos(pitch);
float gz = sinP * FastMath.sin(yaw);
addLeafQuad(col, tip.x + ox, tip.y + oy, tip.z + oz, s, wind, gx, gy, gz);
}
}
private static void addLeafQuad(VertexCollector col,
float cx, float cy, float cz,
float s, float wind,
float gx, float gy, float gz) {
float rx, ry, rz;
if (Math.abs(gy) < 0.95f) {
rx = gz; ry = 0f; rz = -gx;
} else {
rx = 0f; ry = -gz; rz = gy;
}
float rLen = FastMath.sqrt(rx * rx + ry * ry + rz * rz);
if (rLen > 1e-5f) { rx /= rLen; ry /= rLen; rz /= rLen; }
float hw = s * 0.5f;
float hh = s * 1.3f;
float nx = ry * gz - rz * gy;
float ny = rz * gx - rx * gz;
float nz = rx * gy - ry * gx;
float windTip = Math.min(1.0f, wind + 0.25f);
int base = col.vertexCount;
col.add(cx - rx * hw, cy - ry * hw, cz - rz * hw, nx, ny, nz, 0f, 0f, wind);
col.add(cx + rx * hw, cy + ry * hw, cz + rz * hw, nx, ny, nz, 1f, 0f, wind);
col.add(cx + rx * hw + gx * hh, cy + ry * hw + gy * hh, cz + rz * hw + gz * hh, nx, ny, nz, 1f, 1f, windTip);
col.add(cx - rx * hw + gx * hh, cy - ry * hw + gy * hh, cz - rz * hw + gz * hh, nx, ny, nz, 0f, 1f, windTip);
col.tri(base, base + 1, base + 2);
col.tri(base, base + 2, base + 3);
col.tri(base + 2, base + 1, base);
col.tri(base + 3, base + 2, base);
}
// ── Trauben-Billboard ─────────────────────────────────────────────────────
/** Zwei gekreuzte Quads für eine ganze Traube (hängt nach unten vom Aufhängepunkt). */
private static void addGrapeBunch(VertexCollector col,
float cx, float cy, float cz,
float w, float h) {
// Quad 1 — X/Y-Ebene
int base = col.vertexCount;
col.add(cx - w, cy, cz, 0, 0, 1, 0, 1, 0f);
col.add(cx + w, cy, cz, 0, 0, 1, 1, 1, 0f);
col.add(cx + w, cy - h, cz, 0, 0, 1, 1, 0, 0f);
col.add(cx - w, cy - h, cz, 0, 0, 1, 0, 0, 0f);
col.tri(base, base+1, base+2); col.tri(base, base+2, base+3);
col.tri(base+2, base+1, base); col.tri(base+3, base+2, base);
// Quad 2 — Z/Y-Ebene
base = col.vertexCount;
col.add(cx, cy, cz - w, 1, 0, 0, 0, 1, 0f);
col.add(cx, cy, cz + w, 1, 0, 0, 1, 1, 0f);
col.add(cx, cy - h, cz + w, 1, 0, 0, 1, 0, 0f);
col.add(cx, cy - h, cz - w, 1, 0, 0, 0, 0, 0f);
col.tri(base, base+1, base+2); col.tri(base, base+2, base+3);
col.tri(base+2, base+1, base); col.tri(base+3, base+2, base);
}
// ── Zylinder ──────────────────────────────────────────────────────────────
private static void addCylinder(VertexCollector col,
Vector3f start, Vector3f end,
float rBot, float rTop,
float windBot, float windTop, int N) {
Vector3f axis = end.subtract(start);
if (axis.lengthSquared() < 1e-8f) return;
axis.normalizeLocal();
Vector3f perp1 = (Math.abs(axis.y) < 0.9f)
? axis.cross(Vector3f.UNIT_Y).normalizeLocal()
: axis.cross(Vector3f.UNIT_X).normalizeLocal();
Vector3f perp2 = axis.cross(perp1).normalizeLocal();
int base = col.vertexCount;
int N1 = N + 1;
for (int ring = 0; ring < 2; ring++) {
Vector3f center = (ring == 0) ? start : end;
float r = (ring == 0) ? rBot : rTop;
float wind = (ring == 0) ? windBot : windTop;
float vCoord = ring;
for (int i = 0; i <= N; i++) {
float theta = FastMath.TWO_PI * i / N;
float cos = FastMath.cos(theta);
float sin = FastMath.sin(theta);
float nx = cos * perp1.x + sin * perp2.x;
float ny = cos * perp1.y + sin * perp2.y;
float nz = cos * perp1.z + sin * perp2.z;
col.add(center.x + nx * r, center.y + ny * r, center.z + nz * r,
nx, ny, nz, (float) i / N, vCoord, wind);
}
}
for (int i = 0; i < N; i++) {
int b0 = base + i, b1 = base + i + 1;
int t0 = base + N1 + i, t1 = base + N1 + i + 1;
col.tri(b0, b1, t1);
col.tri(b0, t1, t0);
}
}
// ── Gebogene Pfade ────────────────────────────────────────────────────────
/**
* Berechnet Pfadpunkte von start→end mit zufälligen Versätzen senkrecht zur
* Hauptrichtung. Die Hüllkurve (sin) stellt sicher, dass Anfang und Ende exakt
* auf der Geraden liegen.
*
* @param segments Anzahl Zwischenpunkte + 1 (Segmente); Gesamt-Punkte = segments+1
* @param bendAmt Maximale Auslenkung relativ zur Länge (z.B. 0.10 = 10 %)
*/
private static Vector3f[] bentPathPoints(Vector3f start, Vector3f end,
int segments, float bendAmt, Random rng) {
Vector3f[] pts = new Vector3f[segments + 1];
pts[0] = start.clone();
pts[segments] = end.clone();
Vector3f main = end.subtract(start);
float len = main.length();
if (len < 1e-6f) {
for (int i = 1; i < segments; i++) pts[i] = start.clone();
return pts;
}
main.divideLocal(len);
Vector3f p1 = (Math.abs(main.y) < 0.9f)
? main.cross(Vector3f.UNIT_Y).normalizeLocal()
: main.cross(Vector3f.UNIT_X).normalizeLocal();
Vector3f p2 = main.cross(p1).normalizeLocal();
for (int i = 1; i < segments; i++) {
float t = (float) i / segments;
float envelope = FastMath.sin(t * FastMath.PI);
float off1 = (rng.nextFloat() * 2f - 1f) * bendAmt * len * envelope;
float off2 = (rng.nextFloat() * 2f - 1f) * bendAmt * len * envelope;
pts[i] = start.add(main.mult(len * t))
.addLocal(p1.mult(off1))
.addLocal(p2.mult(off2));
}
return pts;
}
/** Linearer Interpolationspfad entlang eines Punkt-Arrays, t ∈ [0,1]. */
private static Vector3f interpPath(Vector3f[] pts, float t) {
if (pts.length == 1) return pts[0].clone();
float scaled = t * (pts.length - 1);
int lo = (int) scaled;
float frac = scaled - lo;
if (lo >= pts.length - 1) return pts[pts.length - 1].clone();
return pts[lo].clone().interpolateLocal(pts[lo + 1], frac);
}
/** Convenience-Wrapper: berechnet Punkte und rendert nahtlosen Polylinien-Zylinder. */
private static void addBentPath(VertexCollector col,
Vector3f start, Vector3f end,
float rBot, float rTop,
float windBot, float windTop,
int segments, int ringN,
float bendAmt, Random rng) {
Vector3f[] pts = bentPathPoints(start, end, segments, bendAmt, rng);
addPolylineCylinder(col, pts, rBot, rTop, windBot, windTop, ringN);
}
/**
* Erzeugt einen nahtlosen Zylinder entlang beliebig vieler Pfadpunkte.
* Parallel-Transport verhindert Twist und Lücken an den Übergängen.
*/
private static void addPolylineCylinder(VertexCollector col, Vector3f[] pts,
float rBot, float rTop,
float windBot, float windTop, int N) {
int numPts = pts.length;
if (numPts < 2) return;
int N1 = N + 1;
// Tangentenrichtung an jedem Punkt
Vector3f[] tang = new Vector3f[numPts];
for (int p = 0; p < numPts; p++) {
if (p == 0) {
tang[p] = pts[1].subtract(pts[0]).normalizeLocal();
} else if (p == numPts - 1) {
tang[p] = pts[p].subtract(pts[p - 1]).normalizeLocal();
} else {
Vector3f a = pts[p].subtract(pts[p - 1]).normalizeLocal();
Vector3f b = pts[p + 1].subtract(pts[p]).normalizeLocal();
tang[p] = a.add(b).normalizeLocal();
}
}
// Parallel-Transport: perp1 des ersten Rings auf alle weiteren projizieren
Vector3f[] perp1 = new Vector3f[numPts];
perp1[0] = (Math.abs(tang[0].y) < 0.9f)
? tang[0].cross(Vector3f.UNIT_Y).normalizeLocal()
: tang[0].cross(Vector3f.UNIT_X).normalizeLocal();
for (int p = 1; p < numPts; p++) {
float dot = tang[p].dot(perp1[p - 1]);
Vector3f v = perp1[p - 1].subtract(tang[p].mult(dot));
perp1[p] = (v.lengthSquared() > 1e-8f) ? v.normalizeLocal() : perp1[p - 1].clone();
}
int baseVtx = col.vertexCount;
// Ringe erzeugen
for (int p = 0; p < numPts; p++) {
float t = (float) p / (numPts - 1);
float r = rBot + (rTop - rBot) * t;
float wind = windBot + (windTop - windBot) * t;
Vector3f p2 = tang[p].cross(perp1[p]).normalizeLocal();
for (int i = 0; i <= N; i++) {
float theta = FastMath.TWO_PI * i / N;
float cos = FastMath.cos(theta);
float sin = FastMath.sin(theta);
float nx = cos * perp1[p].x + sin * p2.x;
float ny = cos * perp1[p].y + sin * p2.y;
float nz = cos * perp1[p].z + sin * p2.z;
col.add(pts[p].x + nx * r, pts[p].y + ny * r, pts[p].z + nz * r,
nx, ny, nz, (float) i / N, t, wind);
}
}
// Ringe verbinden
for (int p = 0; p < numPts - 1; p++) {
for (int i = 0; i < N; i++) {
int b0 = baseVtx + p * N1 + i;
int b1 = baseVtx + p * N1 + i + 1;
int t0 = baseVtx + (p + 1) * N1 + i;
int t1 = baseVtx + (p + 1) * N1 + i + 1;
col.tri(b0, b1, t1);
col.tri(b0, t1, t0);
}
}
}
// ── Vertex-Sammler ────────────────────────────────────────────────────────
private 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> wind = 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 w) {
pos.add(x); pos.add(y); pos.add(z);
norm.add(nx); norm.add(ny); norm.add(nz);
uv.add(u); uv.add(v);
wind.add(w);
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 : wind) { colB.put(f); colB.put(0f); colB.put(0f); colB.put(1f); }
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();
mesh.updateCounts();
return mesh;
}
}
}

View File

@@ -0,0 +1,42 @@
package de.blight.editor.tree;
public class GrapevineOptions {
public int seed = 77541;
public float totalWidth = 6.0f; // Pfosten-zu-Pfosten
public float totalHeight = 2.8f;
public float firstWireH = 0.90f; // Stammhöhe = erster Draht
public int wireCount = 4;
public float postRadius = 0.065f;
public float trunkRadius = 0.040f;
public float shootRadius = 0.016f;
public int shootCount = 7; // pro Arm
public float leafSize = 0.30f;
public int leafCount = 96;
public String leafTexture = "Textures/internal/foliage/wine.png";
public float leafR = 0.35f, leafG = 0.72f, leafB = 0.18f;
public String grapeTexture = "Textures/internal/fruits/wine.png";
public float windStrength = 0.12f;
public float windSpeed = 0.45f;
public GrapevineOptions copy() {
GrapevineOptions c = new GrapevineOptions();
c.seed = seed;
c.totalWidth = totalWidth;
c.totalHeight = totalHeight;
c.firstWireH = firstWireH;
c.wireCount = wireCount;
c.postRadius = postRadius;
c.trunkRadius = trunkRadius;
c.shootRadius = shootRadius;
c.shootCount = shootCount;
c.leafSize = leafSize;
c.leafCount = leafCount;
c.leafTexture = leafTexture;
c.leafR = leafR; c.leafG = leafG; c.leafB = leafB;
c.grapeTexture = grapeTexture;
c.windStrength = windStrength;
c.windSpeed = windSpeed;
return c;
}
}

View File

@@ -27,7 +27,7 @@ public class PalmOptions {
// Textures // Textures
public String barkTexture = "Textures/internal/bark/Bark_Palm.png"; public String barkTexture = "Textures/internal/bark/Bark_Palm.png";
public String leafTexture = "Textures/internal/leaves/palm.png"; public String leafTexture = "Textures/internal/foliage/palm.png";
public float leafTextureAspect = 0f; // width/length-Verhältnis der Textur, 0 = frondWidth nutzen 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 public float gravity = 0.3f; // Durchhang: 0 = gerade, höher = stärker hängend
@@ -35,7 +35,7 @@ public class PalmOptions {
public float crownHeight = 1.2f; public float crownHeight = 1.2f;
public float crownFlare = 1.6f; // max. Aufweitung = trunkRadiusTop × crownFlare public float crownFlare = 1.6f; // max. Aufweitung = trunkRadiusTop × crownFlare
public float crownR = 0.22f, crownG = 0.58f, crownB = 0.14f; public float crownR = 0.22f, crownG = 0.58f, crownB = 0.14f;
public String crownTexture = "Textures/internal/leaves/palmcrown.png"; public String crownTexture = "Textures/internal/foliage/palmcrown.png";
public PalmOptions copy() { public PalmOptions copy() {

View File

@@ -223,47 +223,62 @@ public class TreeMeshBuilder {
float wind, float scale, int count, float wind, float scale, int count,
float angleDeg, Rng rng) { float angleDeg, Rng rng) {
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
float ox = rng.range(-scale * 0.5f, scale * 0.5f); float ox = rng.range(-scale * 0.20f, scale * 0.20f);
float oy = rng.range(-scale * 0.25f, scale * 0.25f); float oy = rng.range(-scale * 0.10f, scale * 0.10f);
float oz = rng.range(-scale * 0.5f, scale * 0.5f); float oz = rng.range(-scale * 0.20f, scale * 0.20f);
float s = scale * (0.7f + rng.range(0f, 0.6f)); float s = scale * (0.7f + rng.range(0f, 0.6f));
float tilt = angleDeg * FastMath.DEG_TO_RAD;
addLeafQuad(col, tip.x + ox, tip.y + oy, tip.z + oz, // Volle 3D-Richtung: zufälliger Azimuth + zufällige Neigung (volle Kugel)
s, wind, rng.range(0f, FastMath.TWO_PI), tilt); float yaw = rng.range(0f, FastMath.TWO_PI);
float pitch = rng.range(0f, FastMath.PI);
float sinP = FastMath.sin(pitch);
float gx = sinP * FastMath.cos(yaw);
float gy = FastMath.cos(pitch);
float gz = sinP * FastMath.sin(yaw);
addLeafQuad(col, tip.x + ox, tip.y + oy, tip.z + oz, s, wind, gx, gy, gz);
} }
} }
private static void addLeafQuad(VertexCollector col, private static void addLeafQuad(VertexCollector col,
float cx, float cy, float cz, float cx, float cy, float cz,
float s, float wind, float yRot, float tilt) { float s, float wind,
float cosY = FastMath.cos(yRot), sinY = FastMath.sin(yRot); float gx, float gy, float gz) {
float cosT = FastMath.cos(tilt), sinT = FastMath.sin(tilt); // Rechtsvektor senkrecht zur Wachstumsrichtung
float hw = s * 0.5f; float rx, ry, rz;
float hh = s * 0.65f; if (Math.abs(gy) < 0.95f) {
rx = gz; ry = 0f; rz = -gx; // cross(UNIT_Y, grow)
// Quad A } else {
for (int q = 0; q < 2; q++) { rx = 0f; ry = -gz; rz = gy; // cross(UNIT_X, grow)
// second quad perpendicular (yRot + PI/2)
float cy2 = (q == 0) ? cosY : -sinY;
float sz2 = (q == 0) ? sinY : cosY;
int base = col.vertexCount;
// 4 Ecken: unten-links, unten-rechts, oben-rechts, oben-links
// "oben" ist um tilt-Grad nach vorne/hinten geneigt
float[] xs = {-hw * cy2, hw * cy2, hw * cy2, -hw * cy2};
float[] zs = {-hw * sz2, hw * sz2, hw * sz2, -hw * sz2};
float[] ys = {-hh * cosT, -hh * cosT, hh * cosT, hh * cosT};
// Face normal = right × up = (cy2,0,sz2) × (0,1,0) = (-sz2, 0, cy2)
float nnx = -sz2, nnz = cy2;
col.add(cx + xs[0], cy + ys[0], cz + zs[0], nnx, 0, nnz, 0, 0, wind);
col.add(cx + xs[1], cy + ys[1], cz + zs[1], nnx, 0, nnz, 1, 0, wind);
col.add(cx + xs[2], cy + ys[2], cz + zs[2], nnx, 0, nnz, 1, 1, wind);
col.add(cx + xs[3], cy + ys[3], cz + zs[3], nnx, 0, nnz, 0, 1, wind);
col.tri(base, base+1, base+2);
col.tri(base, base+2, base+3);
col.tri(base+2, base+1, base); // back face
col.tri(base+3, base+2, base);
} }
float rLen = FastMath.sqrt(rx * rx + ry * ry + rz * rz);
if (rLen > 1e-5f) { rx /= rLen; ry /= rLen; rz /= rLen; }
float hw = s * 0.5f;
float hh = s * 1.3f;
// Normalvektor: right × grow
float nx = ry * gz - rz * gy;
float ny = rz * gx - rx * gz;
float nz = rx * gy - ry * gx;
float windTip = Math.min(1.0f, wind + 0.25f);
int base = col.vertexCount;
// Untere Kante am Ast (verankert)
col.add(cx - rx * hw, cy - ry * hw, cz - rz * hw,
nx, ny, nz, 0f, 0f, wind);
col.add(cx + rx * hw, cy + ry * hw, cz + rz * hw,
nx, ny, nz, 1f, 0f, wind);
// Obere Kante frei (schwingt im Wind)
col.add(cx + rx * hw + gx * hh, cy + ry * hw + gy * hh, cz + rz * hw + gz * hh,
nx, ny, nz, 1f, 1f, windTip);
col.add(cx - rx * hw + gx * hh, cy - ry * hw + gy * hh, cz - rz * hw + gz * hh,
nx, ny, nz, 0f, 1f, windTip);
col.tri(base, base + 1, base + 2);
col.tri(base, base + 2, base + 3);
col.tri(base + 2, base + 1, base);
col.tri(base + 3, base + 2, base);
} }
// ── Ast-Richtung berechnen ──────────────────────────────────────────────── // ── Ast-Richtung berechnen ────────────────────────────────────────────────

View File

@@ -43,7 +43,7 @@ public class TreeParams {
// ── Texturen (relative Pfade für den Asset-Manager) ────────────────────── // ── Texturen (relative Pfade für den Asset-Manager) ──────────────────────
public String barkTexture = null; // z.B. "Textures/internal/bark/Bark001_Color.jpg" public String barkTexture = null; // z.B. "Textures/internal/bark/Bark001_Color.jpg"
public String leafTexture = null; // z.B. "Textures/internal/leaves/oak.png" public String leafTexture = null; // z.B. "Textures/internal/foliage/oak.png"
// ── Wind ───────────────────────────────────────────────────────────────── // ── Wind ─────────────────────────────────────────────────────────────────
public float trunkFlexibility = 0.05f; public float trunkFlexibility = 0.05f;
@@ -68,7 +68,7 @@ public class TreeParams {
p.leafScale = 1.4f; p.leafCount = 6; p.leafAngle = 42f; p.leafBranchings = 2; p.leafScale = 1.4f; p.leafCount = 6; p.leafAngle = 42f; p.leafBranchings = 2;
p.trunkFlexibility = 0.04f; p.branchFlexibility = 0.85f; p.trunkFlexibility = 0.04f; p.branchFlexibility = 0.85f;
p.barkTexture = "Textures/internal/bark/Bark001_Color.jpg"; p.barkTexture = "Textures/internal/bark/Bark001_Color.jpg";
p.leafTexture = "Textures/internal/leaves/oak.png"; p.leafTexture = "Textures/internal/foliage/oak.png";
return p; return p;
} }
@@ -89,7 +89,7 @@ public class TreeParams {
p.leafScale = 0.9f; p.leafCount = 4; p.leafAngle = 38f; p.leafBranchings = 1; p.leafScale = 0.9f; p.leafCount = 4; p.leafAngle = 38f; p.leafBranchings = 1;
p.trunkFlexibility = 0.03f; p.branchFlexibility = 0.95f; p.trunkFlexibility = 0.03f; p.branchFlexibility = 0.95f;
p.barkTexture = "Textures/internal/bark/Bark002_Color.jpg"; p.barkTexture = "Textures/internal/bark/Bark002_Color.jpg";
p.leafTexture = "Textures/internal/leaves/aspen.png"; p.leafTexture = "Textures/internal/foliage/aspen.png";
return p; return p;
} }
@@ -110,7 +110,7 @@ public class TreeParams {
p.leafScale = 0.7f; p.leafCount = 8; p.leafAngle = 70f; p.leafBranchings = 1; p.leafScale = 0.7f; p.leafCount = 8; p.leafAngle = 70f; p.leafBranchings = 1;
p.trunkFlexibility = 0.03f; p.branchFlexibility = 0.70f; p.trunkFlexibility = 0.03f; p.branchFlexibility = 0.70f;
p.barkTexture = "Textures/internal/bark/Bark003_Color.jpg"; p.barkTexture = "Textures/internal/bark/Bark003_Color.jpg";
p.leafTexture = "Textures/internal/leaves/pine.png"; p.leafTexture = "Textures/internal/foliage/pine.png";
return p; return p;
} }
@@ -131,7 +131,7 @@ public class TreeParams {
p.leafScale = 1.5f; p.leafCount = 7; p.leafAngle = 55f; p.leafBranchings = 2; p.leafScale = 1.5f; p.leafCount = 7; p.leafAngle = 55f; p.leafBranchings = 2;
p.trunkFlexibility = 0.06f; p.branchFlexibility = 0.98f; p.trunkFlexibility = 0.06f; p.branchFlexibility = 0.98f;
p.barkTexture = "Textures/internal/bark/Bark001_Color.jpg"; p.barkTexture = "Textures/internal/bark/Bark001_Color.jpg";
p.leafTexture = "Textures/internal/leaves/ash.png"; p.leafTexture = "Textures/internal/foliage/ash.png";
return p; return p;
} }
@@ -152,7 +152,7 @@ public class TreeParams {
p.leafScale = 1.0f; p.leafCount = 5; p.leafAngle = 50f; p.leafBranchings = 2; p.leafScale = 1.0f; p.leafCount = 5; p.leafAngle = 50f; p.leafBranchings = 2;
p.trunkFlexibility = 0.05f; p.branchFlexibility = 0.90f; p.trunkFlexibility = 0.05f; p.branchFlexibility = 0.90f;
p.barkTexture = "Textures/internal/bark/Bark001_Color.jpg"; p.barkTexture = "Textures/internal/bark/Bark001_Color.jpg";
p.leafTexture = "Textures/internal/leaves/ash.png"; p.leafTexture = "Textures/internal/foliage/ash.png";
return p; return p;
} }

View File

@@ -386,10 +386,14 @@ public final class ImpostorUtil {
for (Spatial s : root.getChildren()) { for (Spatial s : root.getChildren()) {
if (s instanceof Geometry g) { if (s instanceof Geometry g) {
Material mat = g.getMaterial(); Material mat = g.getMaterial();
if (mat.getMaterialDef().getMaterialParam("LightDir") != null) { if (mat == null) { continue; }
mat.setVector3("LightDir", lightDir); com.jme3.material.MaterialDef def = mat.getMaterialDef();
mat.setVector3("SunColor", new Vector3f(1.3f, 1.2f, 1.0f)); if (def.getMaterialParam("LightDir") != null) {
mat.setVector3("AmbientColor", new Vector3f(0.55f, 0.55f, 0.57f)); mat.setVector3("LightDir", lightDir);
}
if (def.getMaterialParam("SunColor") != null) {
mat.setVector3("SunColor", new Vector3f(1.4f, 1.3f, 1.1f));
mat.setVector3("AmbientColor", new Vector3f(0.18f, 0.18f, 0.22f));
} }
} else if (s instanceof Node n) { } else if (s instanceof Node n) {
applyTreeLighting(n, lightDir); applyTreeLighting(n, lightDir);

View File

@@ -59,14 +59,14 @@ public class AmbientParticlesState extends BaseAppState {
private float scanTimer = 0f; private float scanTimer = 0f;
private static final String[] LEAF_TEX_PATHS = { private static final String[] LEAF_TEX_PATHS = {
"Textures/internal/leavesfilter/leaf1.png", "Textures/internal/leafFilter/leaf1.png",
"Textures/internal/leavesfilter/leaf2.png", "Textures/internal/leafFilter/leaf2.png",
"Textures/internal/leavesfilter/leaf3.png", "Textures/internal/leafFilter/leaf3.png",
"Textures/internal/leavesfilter/leaf4.png", "Textures/internal/leafFilter/leaf4.png",
"Textures/internal/leavesfilter/leaf5.png", "Textures/internal/leafFilter/leaf5.png",
"Textures/internal/leavesfilter/leaf6.png", "Textures/internal/leafFilter/leaf6.png",
"Textures/internal/leavesfilter/leaf7.png", "Textures/internal/leafFilter/leaf7.png",
"Textures/internal/leavesfilter/leaf8.png", "Textures/internal/leafFilter/leaf8.png",
}; };
@SuppressWarnings("deprecation") private ParticleEmitter[] leafEmitters; @SuppressWarnings("deprecation") private ParticleEmitter[] leafEmitters;

View File

@@ -8,6 +8,7 @@ import com.jme3.asset.plugins.FileLocator;
import com.jme3.bullet.BulletAppState; import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.shapes.CapsuleCollisionShape; import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;
import com.jme3.bullet.collision.shapes.CompoundCollisionShape; import com.jme3.bullet.collision.shapes.CompoundCollisionShape;
import com.jme3.bullet.collision.shapes.MeshCollisionShape;
import com.jme3.bullet.control.RigidBodyControl; import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.bullet.util.CollisionShapeFactory; import com.jme3.bullet.util.CollisionShapeFactory;
import com.jme3.bounding.BoundingBox; import com.jme3.bounding.BoundingBox;
@@ -108,7 +109,22 @@ public class WorldObjectsState extends BaseAppState {
// Baum-Stammkollision: Capsule statt Vollmesh (Blätter ausgeschlossen) // Baum-Stammkollision: Capsule statt Vollmesh (Blätter ausgeschlossen)
String objPath = m.modelPath() != null ? m.modelPath() : ""; String objPath = m.modelPath() != null ? m.modelPath() : "";
if (objPath.startsWith("Models/trees/") && bulletAppState != null) { if (objPath.contains("/grapevine/") && bulletAppState != null) {
try {
s.updateGeometricState();
if (s instanceof Node vineNode) {
CompoundCollisionShape compound = new CompoundCollisionShape();
collectGrapevineShapes(vineNode, compound);
if (!compound.getChildren().isEmpty()) {
RigidBodyControl rbc = new RigidBodyControl(compound, 0f);
s.addControl(rbc);
bulletAppState.getPhysicsSpace().add(rbc);
}
}
} catch (Exception pe) {
log.warn("[WorldObjects] Grapevine-Physik für '{}' nicht erzeugbar: {}", m.modelPath(), pe.getMessage());
}
} else if (objPath.startsWith("Models/trees/") && bulletAppState != null) {
try { try {
s.updateGeometricState(); s.updateGeometricState();
float treeHeight = 6f; float treeHeight = 6f;
@@ -549,4 +565,18 @@ public class WorldObjectsState extends BaseAppState {
mat.setFloat("Wetness", w); mat.setFloat("Wetness", w);
} }
} }
/** Sammelt Bark- und Wire-Geometrien der Grapevine als exakte Mesh-Shapes. */
private static void collectGrapevineShapes(Node node, CompoundCollisionShape compound) {
for (Spatial child : node.getChildren()) {
if (child instanceof Geometry geo) {
String n = geo.getName();
if ("bark".equals(n) || "wires".equals(n)) {
compound.addChildShape(new MeshCollisionShape(geo.getMesh()), Vector3f.ZERO);
}
} else if (child instanceof Node sub) {
collectGrapevineShapes(sub, compound);
}
}
}
} }

View File

@@ -4,3 +4,4 @@ Models/trees/pine/medium/pine_medium_20260706_190947.j3o -6.42357 1.22971 -1318.
Models/trees/pine/medium/pine_medium_20260706_190953.j3o -17.84356 1.24567 -1316.45410 0.26793 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000 Models/trees/pine/medium/pine_medium_20260706_190953.j3o -17.84356 1.24567 -1316.45410 0.26793 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000
Models/plants/misc/kaktusfeige.j3o -6.18754 1.22995 -1320.34875 0.00000 2.50000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 Models/plants/misc/kaktusfeige.j3o -6.18754 1.22995 -1320.34875 0.00000 2.50000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000
Models/imported/alter_steg.j3o -8.42317 -0.11015 -1359.15662 3.14159 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000 Models/imported/alter_steg.j3o -8.42317 -0.11015 -1359.15662 3.14159 1.00000 0.00000 0.00000 true true true 30.00000 80.00000 120.00000
Models/trees/grapevine/grapevine_20260725_195635.j3o -85.78869 1.32939 -1297.17883 0.00000 1.00000 0.00000 0.00000 false true true 30.00000 80.00000 120.00000

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -20,10 +20,10 @@ public final class TreePresets {
private static final String BARK2 = "Textures/internal/bark/Bark002_Color.jpg"; private static final String BARK2 = "Textures/internal/bark/Bark002_Color.jpg";
private static final String BARK3 = "Textures/internal/bark/Bark003_Color.jpg"; private static final String BARK3 = "Textures/internal/bark/Bark003_Color.jpg";
private static final String LEAF_OAK = "Textures/internal/leaves/oak.png"; private static final String LEAF_OAK = "Textures/internal/foliage/oak.png";
private static final String LEAF_ASH = "Textures/internal/leaves/ash.png"; private static final String LEAF_ASH = "Textures/internal/foliage/ash.png";
private static final String LEAF_ASPEN = "Textures/internal/leaves/aspen.png"; private static final String LEAF_ASPEN = "Textures/internal/foliage/aspen.png";
private static final String LEAF_PINE = "Textures/internal/leaves/pine.png"; private static final String LEAF_PINE = "Textures/internal/foliage/pine.png";
// ════════════════════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════════════════════
// OAK // OAK
@@ -501,9 +501,9 @@ public final class TreePresets {
case "pine small" -> pineSmall(); case "pine small" -> pineSmall();
case "pine medium" -> pineMedium(); case "pine medium" -> pineMedium();
case "pine large" -> pineLarge(); case "pine large" -> pineLarge();
case "bush 1" -> bush1(); case "bush 1" -> bush1();
case "bush 2" -> bush2(); case "bush 2" -> bush2();
case "bush 3" -> bush3(); case "bush 3" -> bush3();
case "trellis" -> trellis(); case "trellis" -> trellis();
default -> oakMedium(); default -> oakMedium();
}; };

BIN
doc/Endgame.odt Normal file

Binary file not shown.

BIN
doc/Freibeuter.odt Normal file

Binary file not shown.