Bugfixes, weitere Vegetationsgeneratoren hinzugefügt
BIN
assets/imported/models/wooden suspension bridge 3d model.glb
Normal file
@@ -7,9 +7,9 @@ MaterialDef Tree {
|
||||
Vector2 WindDir : 0.0 1.0
|
||||
Texture2D BarkMap
|
||||
Boolean HasBarkMap : false
|
||||
Vector3 LightDir : 0.6 -0.8 0.4
|
||||
Vector3 SunColor : 0.68 0.65 0.60
|
||||
Vector3 AmbientColor : 0.08 0.10 0.16
|
||||
Vector3 LightDir : 0.45 1.0 0.3
|
||||
Vector3 SunColor : 1.4 1.3 1.1
|
||||
Vector3 AmbientColor : 0.18 0.18 0.22
|
||||
|
||||
// Vom Shadow-Renderer befüllt (PostShadow-Pass)
|
||||
Int BoundDrawBuffer
|
||||
|
||||
@@ -29,9 +29,9 @@ MaterialDef TreeLeaf {
|
||||
Matrix4 LightViewProjectionMatrix4
|
||||
Matrix4 LightViewProjectionMatrix5
|
||||
Vector3 LightPos
|
||||
Vector3 LightDir : 0.6 -0.8 0.4
|
||||
Vector3 SunColor : 0.68 0.65 0.60
|
||||
Vector3 AmbientColor : 0.08 0.10 0.16
|
||||
Vector3 LightDir : 0.45 1.0 0.3
|
||||
Vector3 SunColor : 1.4 1.3 1.1
|
||||
Vector3 AmbientColor : 0.18 0.18 0.22
|
||||
Float PCFEdge
|
||||
Float ShadowMapSize
|
||||
Boolean BackfaceShadows : false
|
||||
|
||||
@@ -2,6 +2,10 @@ MaterialDef Voxel {
|
||||
|
||||
MaterialParameters {
|
||||
Texture2D TexFlat
|
||||
Texture2D TexFlat2
|
||||
Texture2D TexFlat3
|
||||
Texture2D TexFlat4
|
||||
Texture2D SplatMap
|
||||
Texture2D TexSteep
|
||||
Texture2D NormalMapFlat
|
||||
Texture2D NormalMapSteep
|
||||
@@ -32,6 +36,7 @@ MaterialDef Voxel {
|
||||
HAS_LIGHTDIR : LightDir
|
||||
HAS_SCENE_LIGHT : SunColor
|
||||
DEBUG_NO_LIGHT : DebugNoLight
|
||||
HAS_SPLAT : SplatMap
|
||||
}
|
||||
|
||||
RenderState {
|
||||
@@ -60,6 +65,7 @@ MaterialDef Voxel {
|
||||
HAS_DISP_STEEP : DisplacementMapSteep
|
||||
HAS_LIGHTDIR : LightDir
|
||||
HAS_SCENE_LIGHT : SunColor
|
||||
HAS_SPLAT : SplatMap
|
||||
}
|
||||
|
||||
RenderState {
|
||||
|
||||
BIN
blight-assets/src/main/resources/Models/imported/bridge1.j3o
Normal 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
|
||||
@@ -25,14 +25,20 @@ void main() {
|
||||
vec2 windN = (dot(m_WindDir, m_WindDir) > 0.001) ? normalize(m_WindDir) : vec2(0.0, 1.0);
|
||||
vec2 perpN = vec2(-windN.y, windN.x);
|
||||
float wavePhase = dot(worldXZ, windN);
|
||||
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 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(
|
||||
windN.x * mainSway + perpN.x * crossSway,
|
||||
0.0,
|
||||
-sway2 * 0.5,
|
||||
windN.y * mainSway + perpN.y * crossSway
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
uniform sampler2D m_TexFlat;
|
||||
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;
|
||||
|
||||
#ifdef HAS_NM_FLAT
|
||||
@@ -57,7 +64,22 @@ void main() {
|
||||
|
||||
// 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.
|
||||
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;
|
||||
|
||||
// Geometrie-Normale für Beleuchtung, ggf. durch Normal-Map ersetzt.
|
||||
|
||||
|
Before Width: | Height: | Size: 177 KiB After Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 419 KiB |
|
Before Width: | Height: | Size: 232 KiB After Width: | Height: | Size: 232 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 799 KiB After Width: | Height: | Size: 799 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 297 KiB After Width: | Height: | Size: 297 KiB |
|
After Width: | Height: | Size: 537 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 272 KiB After Width: | Height: | Size: 272 KiB |
|
Before Width: | Height: | Size: 324 KiB After Width: | Height: | Size: 324 KiB |
|
Before Width: | Height: | Size: 306 KiB After Width: | Height: | Size: 306 KiB |
|
Before Width: | Height: | Size: 303 KiB After Width: | Height: | Size: 303 KiB |
|
Before Width: | Height: | Size: 209 KiB After Width: | Height: | Size: 209 KiB |
|
Before Width: | Height: | Size: 237 KiB After Width: | Height: | Size: 237 KiB |
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 188 KiB |
|
Before Width: | Height: | Size: 336 KiB After Width: | Height: | Size: 336 KiB |
@@ -105,6 +105,14 @@ public class EditorApp extends Application {
|
||||
// Farn-Generator-Zustand
|
||||
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
|
||||
private String vegetationType = "Baum (Eiche)";
|
||||
|
||||
@@ -321,8 +329,8 @@ public class EditorApp extends Application {
|
||||
private ToggleButton playToolBtn;
|
||||
private ToggleButton voxelBtn;
|
||||
private ToggleButton voxelCliffBtn;
|
||||
private ToggleButton camOrbitBtn;
|
||||
private ToggleButton camFreeBtn;
|
||||
private RadioMenuItem camOrbitItem;
|
||||
private RadioMenuItem camFreeItem;
|
||||
|
||||
// "Objekt"-Button in der Selektionsleiste (zum Zurückschalten bei importierten Objekten)
|
||||
private ToggleButton selModeObjectBtn;
|
||||
@@ -410,7 +418,20 @@ public class EditorApp extends Application {
|
||||
Scene scene = new Scene(root, 1280, 760);
|
||||
java.net.URL cssUrl = getClass().getResource("/editor.css");
|
||||
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.setOnKeyTyped(e -> {
|
||||
if (!input.consoleIsOpen) return;
|
||||
@@ -962,7 +983,7 @@ public class EditorApp extends Application {
|
||||
ComboBox<String> typeBox = new ComboBox<>();
|
||||
typeBox.getItems().addAll(
|
||||
"Baum (Eiche)", "Baum (Birke)", "Baum (Kiefer)", "Baum (Weide)", "Baum (Busch)",
|
||||
"Farn", "Palme");
|
||||
"Farn", "Palme", "Fruchtbusch", "Weinpflanze");
|
||||
typeBox.setValue(vegetationType);
|
||||
typeBox.setOnAction(e -> {
|
||||
vegetationType = typeBox.getValue();
|
||||
@@ -986,6 +1007,10 @@ public class EditorApp extends Application {
|
||||
treeParams.copy(), true, treeTypeFromPreset(currentTreePreset)));
|
||||
} else if ("Farn".equals(vegetationType)) {
|
||||
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 {
|
||||
input.palmGenQueue.offer(new SharedInput.PalmGenRequest(palmOptions.copy(), true));
|
||||
}
|
||||
@@ -1027,6 +1052,28 @@ public class EditorApp extends Application {
|
||||
root.setRight(buildVegetationParamsPanel());
|
||||
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 {
|
||||
onF5 = () -> {
|
||||
input.palmGenQueue.offer(new SharedInput.PalmGenRequest(palmOptions.copy(), false));
|
||||
@@ -1045,6 +1092,10 @@ public class EditorApp extends Application {
|
||||
return buildTreeParamsPanel();
|
||||
} else if ("Farn".equals(vegetationType)) {
|
||||
return buildFernParamsPanel();
|
||||
} else if ("Fruchtbusch".equals(vegetationType)) {
|
||||
return buildFruitBushParamsPanel();
|
||||
} else if ("Weinpflanze".equals(vegetationType)) {
|
||||
return buildGrapevineParamsPanel();
|
||||
} else {
|
||||
return buildPalmParamsPanel();
|
||||
}
|
||||
@@ -1114,6 +1165,113 @@ public class EditorApp extends Application {
|
||||
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 [0–1]:", 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() {
|
||||
currentTool = "tripo";
|
||||
topBar.getChildren().set(1, buildTripoToolBar());
|
||||
@@ -1209,8 +1367,19 @@ public class EditorApp extends Application {
|
||||
});
|
||||
viewTopologyItem.setOnAction(e ->
|
||||
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,
|
||||
new SeparatorMenuItem(), viewTopologyItem);
|
||||
new SeparatorMenuItem(), viewTopologyItem,
|
||||
new SeparatorMenuItem(), camOrbitItem, camFreeItem);
|
||||
|
||||
Menu zeitMenu = new Menu("Zeit");
|
||||
ToggleGroup zeitGroup = new ToggleGroup();
|
||||
@@ -1240,11 +1409,9 @@ public class EditorApp extends Application {
|
||||
areaBtn = new ToggleButton("🗺 Bereiche");
|
||||
locationZoneBtn = new ToggleButton("📍 Locations");
|
||||
playToolBtn = new ToggleButton("🎮 Spielen");
|
||||
voxelBtn = new ToggleButton("⬡ Voxel");
|
||||
voxelCliffBtn = new ToggleButton("⛰ Klippe");
|
||||
voxelBtn = new ToggleButton("⬡ Voxel");
|
||||
voxelCliffBtn = new ToggleButton("⛰ Klippe");
|
||||
stoneBtn = new ToggleButton("🪨 Steine");
|
||||
camOrbitBtn = new ToggleButton("⊙ Orbit");
|
||||
camFreeBtn = new ToggleButton("✈ FreeFly");
|
||||
baseBtn.setStyle("-fx-font-weight:bold;");
|
||||
grassBtn.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;");
|
||||
voxelCliffBtn.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();
|
||||
baseBtn.setToggleGroup(layerGroup);
|
||||
@@ -1286,6 +1451,9 @@ public class EditorApp extends Application {
|
||||
baseBtn.setSelected(true);
|
||||
|
||||
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;
|
||||
root.setRight(toolPanel);
|
||||
showToolParameters(toolPanel, input.activeTool);
|
||||
@@ -1352,6 +1520,9 @@ public class EditorApp extends Application {
|
||||
root.setRight(buildPlayToolPanel());
|
||||
});
|
||||
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.activeTool = input.voxelTool;
|
||||
root.setRight(toolPanel);
|
||||
@@ -1368,13 +1539,6 @@ public class EditorApp extends Application {
|
||||
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");
|
||||
camResetBtn.setStyle("-fx-font-weight:bold;");
|
||||
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), voxelBtn, voxelCliffBtn,
|
||||
new Separator(Orientation.VERTICAL), stoneBtn,
|
||||
new Separator(Orientation.VERTICAL), camOrbitBtn, camFreeBtn, camResetBtn,
|
||||
new Separator(Orientation.VERTICAL), camResetBtn,
|
||||
new Separator(Orientation.VERTICAL), hint);
|
||||
|
||||
worldToolBar = toolBar;
|
||||
@@ -1449,6 +1613,76 @@ public class EditorApp extends Application {
|
||||
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 ─────────────────────────────
|
||||
|
||||
private VBox buildTreeParamsPanel() {
|
||||
@@ -1846,7 +2080,7 @@ public class EditorApp extends Application {
|
||||
&& palmOptions.leafTexture.contains("palm2") ? "palm2.png" : "palm.png";
|
||||
leafTexBox.setValue(currentLeaf);
|
||||
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));
|
||||
|
||||
// Rinden-Textur-Auswahl (alle Texturen aus dem bark-Ordner)
|
||||
@@ -3260,7 +3494,9 @@ public class EditorApp extends Application {
|
||||
new javafx.scene.control.Separator(),
|
||||
scaleLbl, globalScaleSpin,
|
||||
new javafx.scene.control.Separator(),
|
||||
slotsScroll);
|
||||
slotsScroll,
|
||||
new javafx.scene.control.Separator(),
|
||||
buildVoxelSplatSectionUI());
|
||||
}
|
||||
|
||||
// Textur-Picker (nur beim GrassTool)
|
||||
@@ -3714,6 +3950,39 @@ public class EditorApp extends Application {
|
||||
return box;
|
||||
}
|
||||
|
||||
private javafx.scene.Node buildVoxelSplatSectionUI() {
|
||||
VBox box = new VBox(4);
|
||||
Label title = new Label("Voxel-Splatmap (Slots 2–4)");
|
||||
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) {
|
||||
ToggleGroup tg = new ToggleGroup();
|
||||
javafx.scene.layout.TilePane tile = new javafx.scene.layout.TilePane();
|
||||
@@ -6624,7 +6893,8 @@ public class EditorApp extends Application {
|
||||
input.modelEditorScaleY = 1f;
|
||||
input.modelEditorScaleZ = 1f;
|
||||
input.modelEditorPivotY = 0f;
|
||||
input.modelEditorOpenPath = relPath;
|
||||
input.modelEditorOpenPath = relPath;
|
||||
input.modelImportIntermediatePath = relPath;
|
||||
|
||||
root.setRight(buildModelImportPanel(relPath, suggestedName));
|
||||
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 SharedInput.LAYER_GRASS_VERTEX ->
|
||||
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 ->
|
||||
input.lightClickQueue.offer(new SharedInput.LightClick((float) x, (float) y, action < 0));
|
||||
case SharedInput.LAYER_EMITTERS ->
|
||||
|
||||
@@ -30,6 +30,8 @@ import de.blight.editor.state.WaterBodyState;
|
||||
import de.blight.editor.state.EzTreeState;
|
||||
import de.blight.editor.state.LightState;
|
||||
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.SceneObjectState;
|
||||
import de.blight.editor.state.TerrainEditorState;
|
||||
@@ -189,6 +191,8 @@ public class JmeEditorApp extends SimpleApplication {
|
||||
stateManager.attach(new EzTreeState(input));
|
||||
stateManager.attach(new PalmGeneratorState(input));
|
||||
stateManager.attach(new FernGeneratorState(input));
|
||||
stateManager.attach(new FruitBushGeneratorState(input));
|
||||
stateManager.attach(new GrapevineGeneratorState(input));
|
||||
stateManager.attach(new LightState(input));
|
||||
stateManager.attach(new EmitterState(input));
|
||||
stateManager.attach(new WaterBodyState(input));
|
||||
|
||||
@@ -22,11 +22,12 @@ import java.util.Map;
|
||||
public class LegacyAssetRedirectLocator implements AssetLocator {
|
||||
|
||||
private static final Map<String, String> PREFIXES = Map.of(
|
||||
"Textures/bark/", "Textures/internal/bark/",
|
||||
"Textures/leaves/", "Textures/internal/leaves/",
|
||||
"Textures/fern/", "Textures/internal/fern/",
|
||||
"Textures/water/", "Textures/internal/water/",
|
||||
"Textures/Water/", "Textures/internal/water/"
|
||||
"Textures/bark/", "Textures/internal/bark/",
|
||||
"Textures/leaves/", "Textures/internal/foliage/",
|
||||
"Textures/internal/leaves/", "Textures/internal/foliage/",
|
||||
"Textures/fern/", "Textures/internal/fern/",
|
||||
"Textures/water/", "Textures/internal/water/",
|
||||
"Textures/Water/", "Textures/internal/water/"
|
||||
);
|
||||
|
||||
private Path root;
|
||||
|
||||
@@ -100,6 +100,14 @@ public class SharedInput {
|
||||
public record FernGenRequest(de.blight.editor.tree.FernOptions options, boolean exportAfter) {}
|
||||
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) ───────────────────────────────────
|
||||
/** Relativer Asset-Pfad der Gras-Textur ("" = Standardfarbe). */
|
||||
public volatile String grassTexturePath = "";
|
||||
@@ -868,6 +876,8 @@ public class SharedInput {
|
||||
public volatile String modelImportExportName = null;
|
||||
/** JME → JFX: Status-Meldung nach dem Export (relativer Pfad oder "FEHLER: …"). */
|
||||
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 ───────────────────────────────────────────────────────
|
||||
/** activeLayer==24 → gebackene Voxel-Meshes direkt sculpten */
|
||||
@@ -1040,4 +1050,14 @@ public class SharedInput {
|
||||
public volatile float cliffRoughnessAmp = 4.0f;
|
||||
/** 3D-Noise-Frequenz für die Kliff-Oberfläche (höher = feiner Detail). */
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -449,6 +449,16 @@ public class EzTreeState extends BaseAppState {
|
||||
|
||||
Node lodRoot = assembleLodNode(req.presetName(), hdNode, ld1Node, bb, atlasT2d);
|
||||
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 ───────────────────────────────────────────────────────
|
||||
@@ -466,6 +476,7 @@ public class EzTreeState extends BaseAppState {
|
||||
g.setQueueBucket(RenderQueue.Bucket.Transparent);
|
||||
g.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
|
||||
}
|
||||
|
||||
}
|
||||
} else if (child instanceof Node trellis) {
|
||||
Material mat = buildBarkMat(opts);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -443,11 +443,12 @@ public class ModelImportState extends BaseAppState {
|
||||
// nach einem Editor-Neustart oder im Spiel (dort kein Cache vorhanden).
|
||||
assets.deleteFromCache(new com.jme3.asset.ModelKey(relPath));
|
||||
|
||||
// Zwischendatei löschen wenn der Nutzer einen anderen Dateinamen gewählt hat.
|
||||
// SceneObjectState hat die Quelldatei unter dem originalen Dateinamen als .j3o
|
||||
// gespeichert; erst performExport() bäckt Scale + Pivot in die Ziel-Datei.
|
||||
String intermediateRel = input.modelEditorOpenPath != null
|
||||
? input.modelEditorOpenPath.replace('\\', '/') : null;
|
||||
// Zwischendatei immer löschen — sie dient nur der Vorschau und soll nicht
|
||||
// im Asset-Baum bleiben. modelImportIntermediatePath wird in activateModelImport
|
||||
// gesetzt und nicht von ModelEditorState geleert (im Gegensatz zu modelEditorOpenPath).
|
||||
String intermediateRel = input.modelImportIntermediatePath != null
|
||||
? input.modelImportIntermediatePath.replace('\\', '/') : null;
|
||||
input.modelImportIntermediatePath = null;
|
||||
if (intermediateRel != null && !intermediateRel.equals(relPath)) {
|
||||
Path intermediateAbs = ASSET_ROOT.resolve(intermediateRel);
|
||||
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.refreshAssets = true;
|
||||
|
||||
|
||||
@@ -404,6 +404,9 @@ public class PalmGeneratorState extends BaseAppState {
|
||||
mat.setColor("Diffuse", new ColorRGBA(opts.barkR, opts.barkG, opts.barkB, 1f));
|
||||
mat.setFloat("WindStrength", 0.08f);
|
||||
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) {
|
||||
try {
|
||||
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.setFloat("WindStrength", 0.20f);
|
||||
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);
|
||||
if (opts.leafTexture != null) {
|
||||
try {
|
||||
@@ -449,6 +455,9 @@ public class PalmGeneratorState extends BaseAppState {
|
||||
mat.setColor("Diffuse", color);
|
||||
mat.setFloat("WindStrength", 0.04f);
|
||||
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) {
|
||||
try {
|
||||
Texture tex = assets.loadTexture(opts.crownTexture);
|
||||
|
||||
@@ -1408,45 +1408,80 @@ public class TerrainEditorState extends BaseAppState {
|
||||
Vector3f far = cam.getWorldCoordinates(new Vector2f(jmeX, jmeY), 1f);
|
||||
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();
|
||||
terrain.collideWith(ray, hits);
|
||||
if (hits.size() == 0) continue;
|
||||
|
||||
Vector3f contact = hits.getClosestCollision().getContactPoint();
|
||||
int mode = input.heightTool.mode.getSelectedIndex();
|
||||
boolean terrainChanged = true;
|
||||
if (mode == HeightTool.MODE_SMOOTH) {
|
||||
smoothHeight(contact);
|
||||
} else if (mode == HeightTool.MODE_PLATEAU) {
|
||||
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;
|
||||
if (edit.action() > 0) {
|
||||
slopeHeight(contact);
|
||||
} else {
|
||||
// Linksklick: Terrain schrittweise auf Plateau-Höhe angleichen
|
||||
flattenToPlateauHeight(contact);
|
||||
smoothHeight(contact);
|
||||
}
|
||||
} else if (mode == HeightTool.MODE_PLATEAU) {
|
||||
flattenToPlateauHeight(contact);
|
||||
} else {
|
||||
float delta = (float) input.heightTool.brushStrength.getValue() * edit.action();
|
||||
modifyHeight(contact, delta, mode);
|
||||
}
|
||||
if (terrainChanged) {
|
||||
float br = (float) input.heightTool.brushRadius.getValue();
|
||||
input.terrainEditedAreas.offer(new float[]{contact.x, contact.z, br});
|
||||
}
|
||||
float br = (float) input.heightTool.brushRadius.getValue();
|
||||
input.terrainEditedAreas.offer(new float[]{contact.x, contact.z, br});
|
||||
}
|
||||
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 ────────────────────────────────────────────────────────
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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. */
|
||||
public float sampleTerrainHeight(Vector3f worldContact) {
|
||||
if (cachedHeightMap == null) return Float.NaN;
|
||||
|
||||
@@ -35,8 +35,6 @@ import com.jme3.scene.Spatial;
|
||||
import com.jme3.scene.VertexBuffer;
|
||||
import com.jme3.scene.control.AbstractControl;
|
||||
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.Image;
|
||||
import com.jme3.texture.Texture;
|
||||
@@ -138,13 +136,13 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
previewScene.attachChild(buildPreviewGrid());
|
||||
previewVP.attachScene(previewScene);
|
||||
|
||||
DirectionalLightShadowRenderer shadowRenderer =
|
||||
new DirectionalLightShadowRenderer(assets, 2048, 1);
|
||||
shadowRenderer.setLight(previewSunLight);
|
||||
shadowRenderer.setEdgeFilteringMode(EdgeFilteringMode.PCF4);
|
||||
shadowRenderer.setShadowIntensity(0.25f);
|
||||
shadowRenderer.setShadowZExtend(80f);
|
||||
previewVP.addProcessor(shadowRenderer);
|
||||
// Shadow-Renderer absichtlich NICHT im Preview-Viewport:
|
||||
// Tree.j3md / TreeLeaf.j3md haben eine PostShadow-Technik mit Blend Modulate.
|
||||
// Wird ShadowIntensity nicht korrekt auf 0.25 propagiert (JME3-Bug mit
|
||||
// Custom-PostShadow + PCF4), bleibt der j3md-Default 1.0 aktiv →
|
||||
// lit = 0 → Blend Modulate → komplettes Schwarz.
|
||||
// Die Preview-Beleuchtung kommt ohnehin aus den Custom-Uniforms
|
||||
// (LightDir / SunColor / AmbientColor) und braucht keinen Shadow-Renderer.
|
||||
// Stellt sicher, dass previewScene direkt vor dem Rendern immer aktuell ist –
|
||||
// unabhängig davon, welche AppStates nach TreeGeneratorState die Szene noch ändern.
|
||||
previewVP.addProcessor(new SceneProcessor() {
|
||||
@@ -169,7 +167,10 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
public void setPreviewContent(com.jme3.scene.Node node, float camDist,
|
||||
com.jme3.math.Vector3f target) {
|
||||
previewTreeHolder.detachAllChildren();
|
||||
if (node != null) previewTreeHolder.attachChild(node);
|
||||
if (node != null) {
|
||||
ImpostorUtil.applyTreeLighting(node, previewSunLight.getDirection().negate());
|
||||
previewTreeHolder.attachChild(node);
|
||||
}
|
||||
this.previewCamDist = camDist;
|
||||
this.previewTarget.set(target);
|
||||
}
|
||||
@@ -315,6 +316,7 @@ public class TreeGeneratorState extends BaseAppState {
|
||||
|
||||
Node previewTree = makeTreeNode(pendingHdResult,
|
||||
pendingBarkMat.clone(), pendingLeafMat.clone(), "prev");
|
||||
ImpostorUtil.applyTreeLighting(previewTree, previewSunLight.getDirection().negate());
|
||||
previewTreeHolder.detachAllChildren();
|
||||
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.setFloat("WindStrength", 0.15f);
|
||||
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) {
|
||||
try {
|
||||
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.setFloat("WindStrength", 0.30f);
|
||||
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().setBlendMode(RenderState.BlendMode.Alpha);
|
||||
if (p.leafTexture != null) {
|
||||
|
||||
@@ -23,7 +23,10 @@ import com.jme3.scene.Spatial;
|
||||
import com.jme3.scene.shape.Quad;
|
||||
import com.jme3.scene.VertexBuffer;
|
||||
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||
import com.jme3.texture.Image;
|
||||
import com.jme3.texture.Texture;
|
||||
import com.jme3.texture.Texture2D;
|
||||
import com.jme3.texture.image.ColorSpace;
|
||||
import com.jme3.util.BufferUtils;
|
||||
import de.blight.common.VoxelChunk;
|
||||
import de.blight.common.VoxelChunkIO;
|
||||
@@ -35,11 +38,18 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
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.IntBuffer;
|
||||
import java.nio.file.Files;
|
||||
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.concurrent.*;
|
||||
import java.util.ArrayDeque;
|
||||
@@ -145,6 +155,19 @@ public class VoxelEditorState extends BaseAppState {
|
||||
/** Vorheriger Layer-Zustand für Einstieg/Ausstieg-Erkennung. */
|
||||
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 ─────────────────────────────────────────────────────
|
||||
|
||||
/** Chunks, die LOD1/2 neu brauchen. Wird im Hintergrund-Thread abgearbeitet. */
|
||||
@@ -188,6 +211,7 @@ public class VoxelEditorState extends BaseAppState {
|
||||
}
|
||||
|
||||
voxelMaterial = buildMaterial();
|
||||
initVoxelSplat();
|
||||
|
||||
brushIndicator = buildBrushIndicator();
|
||||
app.getRootNode().attachChild(brushIndicator);
|
||||
@@ -289,11 +313,16 @@ public class VoxelEditorState extends BaseAppState {
|
||||
clearMarkedOverlays();
|
||||
}
|
||||
|
||||
// Voxel-Texturen aktualisiert?
|
||||
// Voxel-Texturen (Slot/Normal-Map) aktualisiert?
|
||||
if (input.voxelTexturesChanged) {
|
||||
input.voxelTexturesChanged = false;
|
||||
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)
|
||||
boolean isVoxelLayer = input.activeLayer == SharedInput.LAYER_VOXEL;
|
||||
@@ -307,6 +336,14 @@ public class VoxelEditorState extends BaseAppState {
|
||||
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
|
||||
if (input.activeLayer != SharedInput.LAYER_VOXEL) {
|
||||
idleSinceEdit = 0f;
|
||||
@@ -603,6 +640,17 @@ public class VoxelEditorState extends BaseAppState {
|
||||
bestPos = cr.getContactPoint();
|
||||
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)) {
|
||||
input.voxelTool.plateauTarget.setValue(h);
|
||||
input.voxelTool.plateauTargetChanged = true;
|
||||
input.heightTool.plateauHeight.setValue(h);
|
||||
input.heightTool.plateauHeightChanged = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -722,9 +772,11 @@ public class VoxelEditorState extends BaseAppState {
|
||||
applyColumnToTarget(chunk, cx, cy, cz, wx, wz, radius, strength, coord -> target);
|
||||
} else if (modeIdx == de.blight.editor.tool.VoxelTool.MODE_SMOOTH) {
|
||||
if (lower) {
|
||||
applyCliffColumn(chunk, cx, cy, cz, wx, wz, radius, strength, slopeParams);
|
||||
} else {
|
||||
// RMB: Durchschnitt-Smooth (wie vormals LMB)
|
||||
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 {
|
||||
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)
|
||||
* 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) {
|
||||
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 zMin = (int)(brushWZ - radius), zMax = (int) Math.ceil(brushWZ + radius);
|
||||
|
||||
float maxH = Float.NEGATIVE_INFINITY, minH = Float.POSITIVE_INFINITY;
|
||||
float maxX = brushWX, maxZ = brushWZ, minX = brushWX, minZ = brushWZ;
|
||||
float sum = 0f;
|
||||
// Höchsten Außenring-Punkt finden + Durchschnitt über den gesamten Pinsel
|
||||
float maxH = Float.NEGATIVE_INFINITY;
|
||||
float maxOX = 0, maxOZ = 0;
|
||||
float sum = 0f;
|
||||
int count = 0;
|
||||
|
||||
for (int xi = xMin; xi <= xMax; xi++) {
|
||||
float dx = xi - brushWX;
|
||||
for (int zi = zMin; zi <= zMax; zi++) {
|
||||
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);
|
||||
if (h > maxH) { maxH = h; maxX = xi; maxZ = zi; }
|
||||
if (h < minH) { minH = h; minX = xi; minZ = zi; }
|
||||
sum += h;
|
||||
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|
|
||||
float ddx = maxX - minX, ddz = maxZ - minZ;
|
||||
float len = (float) Math.sqrt(ddx*ddx + ddz*ddz);
|
||||
if (len < 1e-4f) { ddx = 1f; ddz = 0f; } else { ddx /= len; ddz /= len; }
|
||||
// Achse: Zentrum → Hochpunkt
|
||||
float highDist = (float) Math.sqrt(maxOX*maxOX + maxOZ*maxOZ);
|
||||
float dirX, dirZ;
|
||||
if (highDist < 1e-4f) { dirX = 1f; dirZ = 0f; }
|
||||
else { dirX = maxOX / highDist; dirZ = maxOZ / highDist; }
|
||||
|
||||
float projHigh = (maxX - brushWX) * ddx + (maxZ - brushWZ) * ddz;
|
||||
float projLow = (minX - brushWX) * ddx + (minZ - brushWZ) * ddz;
|
||||
// Gegenüberliegender Punkt: Außenring-Vertex mit kleinstem Projektons-Wert
|
||||
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;
|
||||
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. */
|
||||
@@ -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 radius, float strength, float[] sp) {
|
||||
if (sp == null || Float.isNaN(sp[0])) return;
|
||||
final float highH = sp[0], lowH = sp[1], dirX = sp[2], dirZ = sp[3];
|
||||
final float projHigh = sp[4], projLow = sp[5];
|
||||
final float projRange = projHigh - projLow;
|
||||
if (sp == null || Float.isNaN(sp[0]) || Float.isNaN(sp[1])) return;
|
||||
final float highH = sp[0], oppH = sp[1], dirX = sp[2], dirZ = sp[3];
|
||||
final float projHigh = sp[4], projOpp = sp[5];
|
||||
final float projRange = projHigh - projOpp;
|
||||
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 -> {
|
||||
float proj = coord[0] * dirX + coord[1] * dirZ;
|
||||
float rel = proj - projMid;
|
||||
if (rel > blendHalf) return highH;
|
||||
if (rel < -blendHalf) return lowH;
|
||||
float t = (rel / blendHalf + 1f) * 0.5f;
|
||||
return lowH + (highH - lowH) * t;
|
||||
float t = Math.max(0f, Math.min(1f, (proj - projOpp) / projRange));
|
||||
return oppH + t * (highH - oppH);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1744,11 +1830,18 @@ public class VoxelEditorState extends BaseAppState {
|
||||
|
||||
private void checkAutoSave() {
|
||||
if (idleSinceSave < AUTO_SAVE_IDLE_S) return;
|
||||
boolean anyDirty = false;
|
||||
boolean anyDirty = splatDirty;
|
||||
for (VoxelChunk c : chunks.values()) { if (c.dirty) { anyDirty = true; break; } }
|
||||
if (!anyDirty) return;
|
||||
|
||||
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(() -> {
|
||||
for (VoxelChunk chunk : chunks.values()) {
|
||||
if (!chunk.dirty) continue;
|
||||
@@ -1759,9 +1852,212 @@ public class VoxelEditorState extends BaseAppState {
|
||||
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 ─────────────────────────────────────────────────────
|
||||
|
||||
private Material buildMaterial() {
|
||||
@@ -1877,7 +2173,9 @@ public class VoxelEditorState extends BaseAppState {
|
||||
|
||||
private void updateBrushIndicator() {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
@@ -1891,7 +2189,9 @@ public class VoxelEditorState extends BaseAppState {
|
||||
float jmeY = cam.getHeight() - my * (float) input.viewportScaleY;
|
||||
Hit hit = raycastHit(jmeX, jmeY);
|
||||
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
|
||||
brushIndicator.setLocalTranslation(hit.pos.add(hit.normal().mult(0.05f)));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 [0–1], 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;
|
||||
}
|
||||
}
|
||||
@@ -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.33–0.39
|
||||
0.48f + rng.nextFloat() * 0.10f, // 0.48–0.58
|
||||
0.65f + rng.nextFloat() * 0.12f }; // 0.65–0.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ public class PalmOptions {
|
||||
|
||||
// Textures
|
||||
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 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 crownFlare = 1.6f; // max. Aufweitung = trunkRadiusTop × crownFlare
|
||||
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() {
|
||||
|
||||
@@ -223,47 +223,62 @@ public class TreeMeshBuilder {
|
||||
float wind, float scale, int count,
|
||||
float angleDeg, Rng rng) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
float ox = rng.range(-scale * 0.5f, scale * 0.5f);
|
||||
float oy = rng.range(-scale * 0.25f, scale * 0.25f);
|
||||
float oz = rng.range(-scale * 0.5f, scale * 0.5f);
|
||||
float ox = rng.range(-scale * 0.20f, scale * 0.20f);
|
||||
float oy = rng.range(-scale * 0.10f, scale * 0.10f);
|
||||
float oz = rng.range(-scale * 0.20f, scale * 0.20f);
|
||||
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,
|
||||
s, wind, rng.range(0f, FastMath.TWO_PI), tilt);
|
||||
|
||||
// Volle 3D-Richtung: zufälliger Azimuth + zufällige Neigung (volle Kugel)
|
||||
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,
|
||||
float cx, float cy, float cz,
|
||||
float s, float wind, float yRot, float tilt) {
|
||||
float cosY = FastMath.cos(yRot), sinY = FastMath.sin(yRot);
|
||||
float cosT = FastMath.cos(tilt), sinT = FastMath.sin(tilt);
|
||||
float hw = s * 0.5f;
|
||||
float hh = s * 0.65f;
|
||||
|
||||
// Quad A
|
||||
for (int q = 0; q < 2; q++) {
|
||||
// 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 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);
|
||||
}
|
||||
|
||||
// ── Ast-Richtung berechnen ────────────────────────────────────────────────
|
||||
|
||||
@@ -43,7 +43,7 @@ public class TreeParams {
|
||||
|
||||
// ── Texturen (relative Pfade für den Asset-Manager) ──────────────────────
|
||||
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 ─────────────────────────────────────────────────────────────────
|
||||
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.trunkFlexibility = 0.04f; p.branchFlexibility = 0.85f;
|
||||
p.barkTexture = "Textures/internal/bark/Bark001_Color.jpg";
|
||||
p.leafTexture = "Textures/internal/leaves/oak.png";
|
||||
p.leafTexture = "Textures/internal/foliage/oak.png";
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ public class TreeParams {
|
||||
p.leafScale = 0.9f; p.leafCount = 4; p.leafAngle = 38f; p.leafBranchings = 1;
|
||||
p.trunkFlexibility = 0.03f; p.branchFlexibility = 0.95f;
|
||||
p.barkTexture = "Textures/internal/bark/Bark002_Color.jpg";
|
||||
p.leafTexture = "Textures/internal/leaves/aspen.png";
|
||||
p.leafTexture = "Textures/internal/foliage/aspen.png";
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ public class TreeParams {
|
||||
p.leafScale = 0.7f; p.leafCount = 8; p.leafAngle = 70f; p.leafBranchings = 1;
|
||||
p.trunkFlexibility = 0.03f; p.branchFlexibility = 0.70f;
|
||||
p.barkTexture = "Textures/internal/bark/Bark003_Color.jpg";
|
||||
p.leafTexture = "Textures/internal/leaves/pine.png";
|
||||
p.leafTexture = "Textures/internal/foliage/pine.png";
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ public class TreeParams {
|
||||
p.leafScale = 1.5f; p.leafCount = 7; p.leafAngle = 55f; p.leafBranchings = 2;
|
||||
p.trunkFlexibility = 0.06f; p.branchFlexibility = 0.98f;
|
||||
p.barkTexture = "Textures/internal/bark/Bark001_Color.jpg";
|
||||
p.leafTexture = "Textures/internal/leaves/ash.png";
|
||||
p.leafTexture = "Textures/internal/foliage/ash.png";
|
||||
return p;
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ public class TreeParams {
|
||||
p.leafScale = 1.0f; p.leafCount = 5; p.leafAngle = 50f; p.leafBranchings = 2;
|
||||
p.trunkFlexibility = 0.05f; p.branchFlexibility = 0.90f;
|
||||
p.barkTexture = "Textures/internal/bark/Bark001_Color.jpg";
|
||||
p.leafTexture = "Textures/internal/leaves/ash.png";
|
||||
p.leafTexture = "Textures/internal/foliage/ash.png";
|
||||
return p;
|
||||
}
|
||||
|
||||
|
||||
@@ -386,10 +386,14 @@ public final class ImpostorUtil {
|
||||
for (Spatial s : root.getChildren()) {
|
||||
if (s instanceof Geometry g) {
|
||||
Material mat = g.getMaterial();
|
||||
if (mat.getMaterialDef().getMaterialParam("LightDir") != null) {
|
||||
mat.setVector3("LightDir", lightDir);
|
||||
mat.setVector3("SunColor", new Vector3f(1.3f, 1.2f, 1.0f));
|
||||
mat.setVector3("AmbientColor", new Vector3f(0.55f, 0.55f, 0.57f));
|
||||
if (mat == null) { continue; }
|
||||
com.jme3.material.MaterialDef def = mat.getMaterialDef();
|
||||
if (def.getMaterialParam("LightDir") != null) {
|
||||
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) {
|
||||
applyTreeLighting(n, lightDir);
|
||||
|
||||
@@ -59,14 +59,14 @@ public class AmbientParticlesState extends BaseAppState {
|
||||
private float scanTimer = 0f;
|
||||
|
||||
private static final String[] LEAF_TEX_PATHS = {
|
||||
"Textures/internal/leavesfilter/leaf1.png",
|
||||
"Textures/internal/leavesfilter/leaf2.png",
|
||||
"Textures/internal/leavesfilter/leaf3.png",
|
||||
"Textures/internal/leavesfilter/leaf4.png",
|
||||
"Textures/internal/leavesfilter/leaf5.png",
|
||||
"Textures/internal/leavesfilter/leaf6.png",
|
||||
"Textures/internal/leavesfilter/leaf7.png",
|
||||
"Textures/internal/leavesfilter/leaf8.png",
|
||||
"Textures/internal/leafFilter/leaf1.png",
|
||||
"Textures/internal/leafFilter/leaf2.png",
|
||||
"Textures/internal/leafFilter/leaf3.png",
|
||||
"Textures/internal/leafFilter/leaf4.png",
|
||||
"Textures/internal/leafFilter/leaf5.png",
|
||||
"Textures/internal/leafFilter/leaf6.png",
|
||||
"Textures/internal/leafFilter/leaf7.png",
|
||||
"Textures/internal/leafFilter/leaf8.png",
|
||||
};
|
||||
|
||||
@SuppressWarnings("deprecation") private ParticleEmitter[] leafEmitters;
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.jme3.asset.plugins.FileLocator;
|
||||
import com.jme3.bullet.BulletAppState;
|
||||
import com.jme3.bullet.collision.shapes.CapsuleCollisionShape;
|
||||
import com.jme3.bullet.collision.shapes.CompoundCollisionShape;
|
||||
import com.jme3.bullet.collision.shapes.MeshCollisionShape;
|
||||
import com.jme3.bullet.control.RigidBodyControl;
|
||||
import com.jme3.bullet.util.CollisionShapeFactory;
|
||||
import com.jme3.bounding.BoundingBox;
|
||||
@@ -108,7 +109,22 @@ public class WorldObjectsState extends BaseAppState {
|
||||
|
||||
// Baum-Stammkollision: Capsule statt Vollmesh (Blätter ausgeschlossen)
|
||||
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 {
|
||||
s.updateGeometricState();
|
||||
float treeHeight = 6f;
|
||||
@@ -549,4 +565,18 @@ public class WorldObjectsState extends BaseAppState {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/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/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
|
||||
|
||||
BIN
blight-map/src/main/map/blight_voxel_splat.bin
Normal file
BIN
blight-map/src/main/map/chunks/voxel_14_0_05.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_15_0_05.blvc
Normal file
BIN
blight-map/src/main/map/chunks/voxel_15_0_06.blvc
Normal file
@@ -20,10 +20,10 @@ public final class TreePresets {
|
||||
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 LEAF_OAK = "Textures/internal/leaves/oak.png";
|
||||
private static final String LEAF_ASH = "Textures/internal/leaves/ash.png";
|
||||
private static final String LEAF_ASPEN = "Textures/internal/leaves/aspen.png";
|
||||
private static final String LEAF_PINE = "Textures/internal/leaves/pine.png";
|
||||
private static final String LEAF_OAK = "Textures/internal/foliage/oak.png";
|
||||
private static final String LEAF_ASH = "Textures/internal/foliage/ash.png";
|
||||
private static final String LEAF_ASPEN = "Textures/internal/foliage/aspen.png";
|
||||
private static final String LEAF_PINE = "Textures/internal/foliage/pine.png";
|
||||
|
||||
// ════════════════════════════════════════════════════════════════════════
|
||||
// OAK
|
||||
@@ -501,9 +501,9 @@ public final class TreePresets {
|
||||
case "pine small" -> pineSmall();
|
||||
case "pine medium" -> pineMedium();
|
||||
case "pine large" -> pineLarge();
|
||||
case "bush 1" -> bush1();
|
||||
case "bush 2" -> bush2();
|
||||
case "bush 3" -> bush3();
|
||||
case "bush 1" -> bush1();
|
||||
case "bush 2" -> bush2();
|
||||
case "bush 3" -> bush3();
|
||||
case "trellis" -> trellis();
|
||||
default -> oakMedium();
|
||||
};
|
||||
|
||||