Malen von Gras erweitert
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
@@ -0,0 +1,66 @@
|
|||||||
|
package de.blight.common;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.file.*;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.zip.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liest und schreibt Terrain-Farb-Gras-Halme als komprimierte Binärdatei
|
||||||
|
* ({@code blight_grass_terrain.blgv}) neben der Kartendatei.
|
||||||
|
*
|
||||||
|
* Format v1: int MAGIC, int VERSION, int count, N × (float x, float y, float z, float height, byte seedIdx)
|
||||||
|
* Kein Trockenheitswert – Farbe kommt ausschließlich vom Terrain.
|
||||||
|
*/
|
||||||
|
public final class TerrainColorGrassIO {
|
||||||
|
|
||||||
|
private static final int MAGIC = 0x54434758; // "TCGX"
|
||||||
|
private static final int VERSION = 1;
|
||||||
|
|
||||||
|
private TerrainColorGrassIO() {}
|
||||||
|
|
||||||
|
public static Path getPath() {
|
||||||
|
return MapIO.getMapPath().resolveSibling("blight_grass_terrain.blgv");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void save(List<GrassVertexBlade> blades) throws IOException {
|
||||||
|
Path p = getPath();
|
||||||
|
Files.createDirectories(p.getParent());
|
||||||
|
try (DataOutputStream out = new DataOutputStream(
|
||||||
|
new BufferedOutputStream(new GZIPOutputStream(Files.newOutputStream(p))))) {
|
||||||
|
out.writeInt(MAGIC);
|
||||||
|
out.writeInt(VERSION);
|
||||||
|
out.writeInt(blades.size());
|
||||||
|
for (GrassVertexBlade b : blades) {
|
||||||
|
out.writeFloat(b.x());
|
||||||
|
out.writeFloat(b.y());
|
||||||
|
out.writeFloat(b.z());
|
||||||
|
out.writeFloat(b.height());
|
||||||
|
out.writeByte(Math.max(-1, Math.min(126, b.seedIdx())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<GrassVertexBlade> load() throws IOException {
|
||||||
|
Path p = getPath();
|
||||||
|
if (!Files.exists(p)) return List.of();
|
||||||
|
try (DataInputStream in = new DataInputStream(
|
||||||
|
new BufferedInputStream(new GZIPInputStream(Files.newInputStream(p))))) {
|
||||||
|
int magic = in.readInt();
|
||||||
|
if (magic != MAGIC) throw new IOException("Ungültiges Dateiformat (MAGIC)");
|
||||||
|
int version = in.readInt();
|
||||||
|
if (version < 1 || version > VERSION) throw new IOException("Unbekannte Version: " + version);
|
||||||
|
int count = in.readInt();
|
||||||
|
List<GrassVertexBlade> list = new ArrayList<>(count);
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
float x = in.readFloat();
|
||||||
|
float y = in.readFloat();
|
||||||
|
float z = in.readFloat();
|
||||||
|
float h = in.readFloat();
|
||||||
|
int si = in.readByte();
|
||||||
|
list.add(new GrassVertexBlade(x, y, z, h, 0f, si));
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -364,8 +364,9 @@ public class EditorApp extends Application {
|
|||||||
// Toolbar-Buttons (müssen vom Status-Poller erreichbar sein)
|
// Toolbar-Buttons (müssen vom Status-Poller erreichbar sein)
|
||||||
private ToggleButton baseBtn;
|
private ToggleButton baseBtn;
|
||||||
private ToggleButton grassBtn; // einziger Toolbar-Button für beide Gras-Tools
|
private ToggleButton grassBtn; // einziger Toolbar-Button für beide Gras-Tools
|
||||||
private ToggleButton grassSubTexBtn; // Sub-Toggle im Panel: Textur-Gras
|
private ToggleButton grassSubTexBtn; // Sub-Toggle im Panel: Textur-Gras
|
||||||
private ToggleButton grassSubVertBtn; // Sub-Toggle im Panel: Vertex-Gras
|
private ToggleButton grassSubVertBtn; // Sub-Toggle im Panel: Vertex-Gras
|
||||||
|
private ToggleButton grassSubTerrainBtn; // Sub-Toggle im Panel: Terrain-Farb-Gras
|
||||||
private ToggleButton textureBtn;
|
private ToggleButton textureBtn;
|
||||||
private ToggleButton stoneBtn;
|
private ToggleButton stoneBtn;
|
||||||
private ToggleButton objPlaceBtn; // Unified Objekte-Button (Place + Edit)
|
private ToggleButton objPlaceBtn; // Unified Objekte-Button (Place + Edit)
|
||||||
@@ -3571,28 +3572,33 @@ public class EditorApp extends Application {
|
|||||||
panel.getChildren().addAll(title, new Separator());
|
panel.getChildren().addAll(title, new Separator());
|
||||||
|
|
||||||
// Sub-Tool-Auswahl
|
// Sub-Tool-Auswahl
|
||||||
grassSubTexBtn = new ToggleButton(":: Gras (Textur)");
|
grassSubTexBtn = new ToggleButton(":: Textur");
|
||||||
grassSubVertBtn = new ToggleButton("|| Gras (Vertices)");
|
grassSubVertBtn = new ToggleButton("|| Vertex");
|
||||||
|
grassSubTerrainBtn = new ToggleButton("~~ Terrain");
|
||||||
grassSubTexBtn.setMaxWidth(Double.MAX_VALUE);
|
grassSubTexBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
grassSubVertBtn.setMaxWidth(Double.MAX_VALUE);
|
grassSubVertBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
grassSubTerrainBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
ToggleGroup subGroup = new ToggleGroup();
|
ToggleGroup subGroup = new ToggleGroup();
|
||||||
grassSubTexBtn.setToggleGroup(subGroup);
|
grassSubTexBtn.setToggleGroup(subGroup);
|
||||||
grassSubVertBtn.setToggleGroup(subGroup);
|
grassSubVertBtn.setToggleGroup(subGroup);
|
||||||
|
grassSubTerrainBtn.setToggleGroup(subGroup);
|
||||||
subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); });
|
subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); });
|
||||||
HBox subRow = new HBox(4, grassSubTexBtn, grassSubVertBtn);
|
HBox subRow = new HBox(4, grassSubTexBtn, grassSubVertBtn, grassSubTerrainBtn);
|
||||||
HBox.setHgrow(grassSubTexBtn, Priority.ALWAYS);
|
HBox.setHgrow(grassSubTexBtn, Priority.ALWAYS);
|
||||||
HBox.setHgrow(grassSubVertBtn, Priority.ALWAYS);
|
HBox.setHgrow(grassSubVertBtn, Priority.ALWAYS);
|
||||||
|
HBox.setHgrow(grassSubTerrainBtn, Priority.ALWAYS);
|
||||||
panel.getChildren().add(subRow);
|
panel.getChildren().add(subRow);
|
||||||
|
|
||||||
// Parameter-Bereich (wird beim Sub-Tool-Wechsel ersetzt)
|
// Parameter-Bereich (wird beim Sub-Tool-Wechsel ersetzt)
|
||||||
VBox paramsBox = new VBox(10);
|
VBox paramsBox = new VBox(10);
|
||||||
panel.getChildren().add(paramsBox);
|
panel.getChildren().add(paramsBox);
|
||||||
|
|
||||||
// Globale Farben
|
// Globale Farben – nur für Textur-Gras und Vertex-Gras sichtbar
|
||||||
panel.getChildren().add(new Separator());
|
VBox colorSection = new VBox(6);
|
||||||
|
colorSection.getChildren().add(new Separator());
|
||||||
Label colorTitle = new Label("Globale Farben");
|
Label colorTitle = new Label("Globale Farben");
|
||||||
colorTitle.setStyle("-fx-font-weight: bold; -fx-font-size: 11; -fx-text-fill: #333;");
|
colorTitle.setStyle("-fx-font-weight: bold; -fx-font-size: 11; -fx-text-fill: #333;");
|
||||||
panel.getChildren().add(colorTitle);
|
colorSection.getChildren().add(colorTitle);
|
||||||
|
|
||||||
javafx.scene.control.ColorPicker freshPicker = new javafx.scene.control.ColorPicker(
|
javafx.scene.control.ColorPicker freshPicker = new javafx.scene.control.ColorPicker(
|
||||||
javafx.scene.paint.Color.color(input.grassFreshR, input.grassFreshG, input.grassFreshB));
|
javafx.scene.paint.Color.color(input.grassFreshR, input.grassFreshG, input.grassFreshB));
|
||||||
@@ -3616,9 +3622,36 @@ public class EditorApp extends Application {
|
|||||||
input.grassColorsChanged = true;
|
input.grassColorsChanged = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
panel.getChildren().addAll(
|
Button pickFromTerrainBtn = new Button("[ ] Farbe vom Boden picken");
|
||||||
new Label("Unvertrocknet:"), freshPicker,
|
pickFromTerrainBtn.setMaxWidth(Double.MAX_VALUE);
|
||||||
new Label("Vertrocknet:"), dryPicker);
|
pickFromTerrainBtn.setOnAction(e -> {
|
||||||
|
pickFromTerrainBtn.setText("[ ] Boden anklicken...");
|
||||||
|
pickFromTerrainBtn.setDisable(true);
|
||||||
|
input.grassTerrainPickCallback = color -> {
|
||||||
|
freshPicker.setValue(javafx.scene.paint.Color.color(
|
||||||
|
Math.min(1.0, color[0]), Math.min(1.0, color[1]), Math.min(1.0, color[2])));
|
||||||
|
pickFromTerrainBtn.setText("[ ] Farbe vom Boden picken");
|
||||||
|
pickFromTerrainBtn.setDisable(false);
|
||||||
|
};
|
||||||
|
// Nächsten Klick auf die JME-View abfangen
|
||||||
|
javafx.event.EventHandler<javafx.scene.input.MouseEvent> handler =
|
||||||
|
new javafx.event.EventHandler<javafx.scene.input.MouseEvent>() {
|
||||||
|
@Override public void handle(javafx.scene.input.MouseEvent ev) {
|
||||||
|
if (ev.getButton() == javafx.scene.input.MouseButton.PRIMARY) {
|
||||||
|
input.grassTerrainPickQueue.offer(
|
||||||
|
new SharedInput.GrassVertexEdit((float) ev.getX(), (float) ev.getY(), +1));
|
||||||
|
viewport.removeEventHandler(javafx.scene.input.MouseEvent.MOUSE_PRESSED, this);
|
||||||
|
ev.consume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
viewport.addEventHandler(javafx.scene.input.MouseEvent.MOUSE_PRESSED, handler);
|
||||||
|
});
|
||||||
|
|
||||||
|
colorSection.getChildren().addAll(new Label("Unvertrocknet:"), freshPicker,
|
||||||
|
pickFromTerrainBtn,
|
||||||
|
new Label("Vertrocknet:"), dryPicker);
|
||||||
|
panel.getChildren().add(colorSection);
|
||||||
|
|
||||||
// Sub-Tool-Handler
|
// Sub-Tool-Handler
|
||||||
Runnable activateTex = () -> {
|
Runnable activateTex = () -> {
|
||||||
@@ -3628,6 +3661,8 @@ public class EditorApp extends Application {
|
|||||||
VBox inner = new VBox(10);
|
VBox inner = new VBox(10);
|
||||||
showToolParameters(inner, input.activeTool);
|
showToolParameters(inner, input.activeTool);
|
||||||
paramsBox.getChildren().add(inner);
|
paramsBox.getChildren().add(inner);
|
||||||
|
colorSection.setVisible(true);
|
||||||
|
colorSection.setManaged(true);
|
||||||
};
|
};
|
||||||
Runnable activateVert = () -> {
|
Runnable activateVert = () -> {
|
||||||
input.activeLayer = SharedInput.LAYER_GRASS_VERTEX;
|
input.activeLayer = SharedInput.LAYER_GRASS_VERTEX;
|
||||||
@@ -3636,15 +3671,31 @@ public class EditorApp extends Application {
|
|||||||
VBox inner = new VBox(10);
|
VBox inner = new VBox(10);
|
||||||
showToolParameters(inner, input.activeTool);
|
showToolParameters(inner, input.activeTool);
|
||||||
paramsBox.getChildren().add(inner);
|
paramsBox.getChildren().add(inner);
|
||||||
|
colorSection.setVisible(true);
|
||||||
|
colorSection.setManaged(true);
|
||||||
|
};
|
||||||
|
Runnable activateTerrain = () -> {
|
||||||
|
input.activeLayer = SharedInput.LAYER_TERRAIN_GRASS;
|
||||||
|
input.activeTool = input.terrainGrassTool;
|
||||||
|
paramsBox.getChildren().clear();
|
||||||
|
VBox inner = new VBox(10);
|
||||||
|
showToolParameters(inner, input.activeTool);
|
||||||
|
paramsBox.getChildren().add(inner);
|
||||||
|
colorSection.setVisible(false);
|
||||||
|
colorSection.setManaged(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
grassSubTexBtn.setOnAction(e -> activateTex.run());
|
grassSubTexBtn.setOnAction(e -> activateTex.run());
|
||||||
grassSubVertBtn.setOnAction(e -> activateVert.run());
|
grassSubVertBtn.setOnAction(e -> activateVert.run());
|
||||||
|
grassSubTerrainBtn.setOnAction(e -> activateTerrain.run());
|
||||||
|
|
||||||
// Standard: zuletzt aktives Sub-Tool
|
// Standard: zuletzt aktives Sub-Tool
|
||||||
if (input.activeLayer == SharedInput.LAYER_GRASS_VERTEX) {
|
if (input.activeLayer == SharedInput.LAYER_GRASS_VERTEX) {
|
||||||
grassSubVertBtn.setSelected(true);
|
grassSubVertBtn.setSelected(true);
|
||||||
activateVert.run();
|
activateVert.run();
|
||||||
|
} else if (input.activeLayer == SharedInput.LAYER_TERRAIN_GRASS) {
|
||||||
|
grassSubTerrainBtn.setSelected(true);
|
||||||
|
activateTerrain.run();
|
||||||
} else {
|
} else {
|
||||||
grassSubTexBtn.setSelected(true);
|
grassSubTexBtn.setSelected(true);
|
||||||
activateTex.run();
|
activateTex.run();
|
||||||
@@ -8822,8 +8873,14 @@ public class EditorApp extends Application {
|
|||||||
switch (input.activeLayer) {
|
switch (input.activeLayer) {
|
||||||
case 0 -> input.editQueue.offer(new SharedInput.TerrainEdit((float) x, (float) y, action));
|
case 0 -> input.editQueue.offer(new SharedInput.TerrainEdit((float) x, (float) y, action));
|
||||||
case 3 -> input.grassEditQueue.offer(new SharedInput.GrassEdit((float) x, (float) y, action));
|
case 3 -> input.grassEditQueue.offer(new SharedInput.GrassEdit((float) x, (float) y, action));
|
||||||
case SharedInput.LAYER_GRASS_VERTEX ->
|
case SharedInput.LAYER_GRASS_VERTEX -> {
|
||||||
input.grassVertexEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action));
|
input.grassVertexEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action));
|
||||||
|
if (action < 0) input.terrainGrassEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action));
|
||||||
|
}
|
||||||
|
case SharedInput.LAYER_TERRAIN_GRASS -> {
|
||||||
|
input.terrainGrassEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action));
|
||||||
|
if (action < 0) input.grassVertexEditQueue.offer(new SharedInput.GrassVertexEdit((float) x, (float) y, action));
|
||||||
|
}
|
||||||
case 4 -> {
|
case 4 -> {
|
||||||
SharedInput.TextureEdit te = new SharedInput.TextureEdit((float) x, (float) y, action);
|
SharedInput.TextureEdit te = new SharedInput.TextureEdit((float) x, (float) y, action);
|
||||||
input.textureEditQueue.offer(te);
|
input.textureEditQueue.offer(te);
|
||||||
@@ -9434,7 +9491,7 @@ public class EditorApp extends Application {
|
|||||||
case F9 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (wasserMergedBtn != null) wasserMergedBtn.fire(); }); }
|
case F9 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (wasserMergedBtn != null) wasserMergedBtn.fire(); }); }
|
||||||
case F10 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (wegeBtn != null) wegeBtn.fire(); }); }
|
case F10 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (wegeBtn != null) wegeBtn.fire(); }); }
|
||||||
case F11 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (zonenBtn != null) zonenBtn.fire(); }); }
|
case F11 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (zonenBtn != null) zonenBtn.fire(); }); }
|
||||||
case F12 -> { if (pressed && "world".equals(currentTool)) Platform.runLater(() -> { if (playToolBtn != null) playToolBtn.fire(); }); }
|
case F12 -> { if (pressed && !input.ctrlHeld && "world".equals(currentTool)) Platform.runLater(() -> { if (playToolBtn != null) playToolBtn.fire(); }); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package de.blight.editor;
|
|||||||
import de.blight.editor.tool.EditorTool;
|
import de.blight.editor.tool.EditorTool;
|
||||||
import de.blight.editor.tool.GrassTool;
|
import de.blight.editor.tool.GrassTool;
|
||||||
import de.blight.editor.tool.GrassVertexTool;
|
import de.blight.editor.tool.GrassVertexTool;
|
||||||
|
import de.blight.editor.tool.TerrainColorGrassTool;
|
||||||
import de.blight.editor.tool.HeightTool;
|
import de.blight.editor.tool.HeightTool;
|
||||||
import de.blight.editor.tool.HoleTool;
|
import de.blight.editor.tool.HoleTool;
|
||||||
import de.blight.editor.tool.StoneTool;
|
import de.blight.editor.tool.StoneTool;
|
||||||
@@ -25,7 +26,8 @@ public class SharedInput {
|
|||||||
public final HeightTool heightTool = new HeightTool();
|
public final HeightTool heightTool = new HeightTool();
|
||||||
public final UpperHeightTool upperHeightTool = new UpperHeightTool();
|
public final UpperHeightTool upperHeightTool = new UpperHeightTool();
|
||||||
public final GrassTool grassTool = new GrassTool();
|
public final GrassTool grassTool = new GrassTool();
|
||||||
public final GrassVertexTool grassVertexTool = new GrassVertexTool();
|
public final GrassVertexTool grassVertexTool = new GrassVertexTool();
|
||||||
|
public final TerrainColorGrassTool terrainGrassTool = new TerrainColorGrassTool();
|
||||||
public final TextureTool textureTool = new TextureTool();
|
public final TextureTool textureTool = new TextureTool();
|
||||||
public final HoleTool holeTool = new HoleTool();
|
public final HoleTool holeTool = new HoleTool();
|
||||||
public final VoxelTool voxelTool = new VoxelTool();
|
public final VoxelTool voxelTool = new VoxelTool();
|
||||||
@@ -92,10 +94,16 @@ public class SharedInput {
|
|||||||
|
|
||||||
// ── Gras (Vertices) ───────────────────────────────────────────────────────
|
// ── Gras (Vertices) ───────────────────────────────────────────────────────
|
||||||
/** activeLayer==15 → Vertex-Gras (X-Quad-Halme) platzieren und entfernen */
|
/** activeLayer==15 → Vertex-Gras (X-Quad-Halme) platzieren und entfernen */
|
||||||
public static final int LAYER_GRASS_VERTEX = 15;
|
public static final int LAYER_GRASS_VERTEX = 15;
|
||||||
|
public static final int LAYER_TERRAIN_GRASS = 34;
|
||||||
|
|
||||||
public record GrassVertexEdit(float screenX, float screenY, int action) {}
|
public record GrassVertexEdit(float screenX, float screenY, int action) {}
|
||||||
public final ConcurrentLinkedQueue<GrassVertexEdit> grassVertexEditQueue = new ConcurrentLinkedQueue<>();
|
public final ConcurrentLinkedQueue<GrassVertexEdit> grassVertexEditQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
public final ConcurrentLinkedQueue<GrassVertexEdit> terrainGrassEditQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
/** Einzelner Klick zum Terrain-Farb-Sampling (Dropper) für Vertex-Gras Tool 1 */
|
||||||
|
public final ConcurrentLinkedQueue<GrassVertexEdit> grassTerrainPickQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
/** Callback: JME-Thread setzt Farbe, EditorApp leitet sie via Platform.runLater an den Picker weiter */
|
||||||
|
public volatile java.util.function.Consumer<float[]> grassTerrainPickCallback = null;
|
||||||
|
|
||||||
/** Globale Grasfarbe – frisches Gras (Spitze; Wurzel wird intern abgeleitet). Defaults: TIP_COLOR */
|
/** Globale Grasfarbe – frisches Gras (Spitze; Wurzel wird intern abgeleitet). Defaults: TIP_COLOR */
|
||||||
public volatile float grassFreshR = 0.26f, grassFreshG = 0.72f, grassFreshB = 0.11f;
|
public volatile float grassFreshR = 0.26f, grassFreshG = 0.72f, grassFreshB = 0.11f;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import com.jme3.texture.Texture;
|
|||||||
import com.jme3.util.BufferUtils;
|
import com.jme3.util.BufferUtils;
|
||||||
import de.blight.common.GrassVertexBlade;
|
import de.blight.common.GrassVertexBlade;
|
||||||
import de.blight.common.GrassVertexIO;
|
import de.blight.common.GrassVertexIO;
|
||||||
|
import de.blight.common.MapData;
|
||||||
import de.blight.editor.SharedInput;
|
import de.blight.editor.SharedInput;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -52,8 +53,7 @@ public class GrassVertexState extends BaseAppState {
|
|||||||
static final int BLADES_PER_TUFT = 3; // Halme pro Büschel
|
static final int BLADES_PER_TUFT = 3; // Halme pro Büschel
|
||||||
static final int SEGMENTS = 5; // Segmente pro Halm (5 → 6 Reihen)
|
static final int SEGMENTS = 5; // Segmente pro Halm (5 → 6 Reihen)
|
||||||
static final float WIDTH_FACTOR = 0.10f; // Basis-Halbbreite = Höhe × WIDTH_FACTOR
|
static final float WIDTH_FACTOR = 0.10f; // Basis-Halbbreite = Höhe × WIDTH_FACTOR
|
||||||
static final float BEND_FACTOR = 0.15f; // max. Krümmungsversatz an der Spitze
|
static final float BEND_FACTOR = 0.15f;
|
||||||
|
|
||||||
static final ColorRGBA ROOT_COLOR = new ColorRGBA(0.08f, 0.34f, 0.04f, 1f);
|
static final ColorRGBA ROOT_COLOR = new ColorRGBA(0.08f, 0.34f, 0.04f, 1f);
|
||||||
static final ColorRGBA TIP_COLOR = new ColorRGBA(0.26f, 0.72f, 0.11f, 1f);
|
static final ColorRGBA TIP_COLOR = new ColorRGBA(0.26f, 0.72f, 0.11f, 1f);
|
||||||
// 0–50 % Trockenheit: Grün → Goldgelb
|
// 0–50 % Trockenheit: Grün → Goldgelb
|
||||||
@@ -95,11 +95,15 @@ public class GrassVertexState extends BaseAppState {
|
|||||||
private float freshTipR = TIP_COLOR.r, freshTipG = TIP_COLOR.g, freshTipB = TIP_COLOR.b;
|
private float freshTipR = TIP_COLOR.r, freshTipG = TIP_COLOR.g, freshTipB = TIP_COLOR.b;
|
||||||
private float dryTipR = DRY_TIP_COLOR.r, dryTipG = DRY_TIP_COLOR.g, dryTipB = DRY_TIP_COLOR.b;
|
private float dryTipR = DRY_TIP_COLOR.r, dryTipG = DRY_TIP_COLOR.g, dryTipB = DRY_TIP_COLOR.b;
|
||||||
|
|
||||||
|
private static final float[] NO_TINT = {0f, 0f, 0f, 0f, 0f};
|
||||||
|
|
||||||
public GrassVertexState(SharedInput input) {
|
public GrassVertexState(SharedInput input) {
|
||||||
this.input = input;
|
this.input = input;
|
||||||
for (int i = 0; i < CHUNK_COUNT; i++) chunkBlades[i] = new ArrayList<>();
|
for (int i = 0; i < CHUNK_COUNT; i++) chunkBlades[i] = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setMapData(MapData mapData) { /* Farbe kommt nicht aus Terrain – kein Terrain-Tint für Tool 1 */ }
|
||||||
|
|
||||||
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; }
|
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; }
|
||||||
|
|
||||||
public List<GrassVertexBlade> getAllBlades() {
|
public List<GrassVertexBlade> getAllBlades() {
|
||||||
@@ -417,7 +421,7 @@ public class GrassVertexState extends BaseAppState {
|
|||||||
for (GrassVertexBlade blade : blades) {
|
for (GrassVertexBlade blade : blades) {
|
||||||
buildTuft(positions, normals, colors, texCoords, indices, vi, ii, blade,
|
buildTuft(positions, normals, colors, texCoords, indices, vi, ii, blade,
|
||||||
frR, frG, frB, freshTipR, freshTipG, freshTipB,
|
frR, frG, frB, freshTipR, freshTipG, freshTipB,
|
||||||
drR, drG, drB, dryTipR, dryTipG, dryTipB);
|
drR, drG, drB, dryTipR, dryTipG, dryTipB, NO_TINT);
|
||||||
vi += BLADES_PER_TUFT * (SEGMENTS + 1) * 2;
|
vi += BLADES_PER_TUFT * (SEGMENTS + 1) * 2;
|
||||||
ii += BLADES_PER_TUFT * SEGMENTS * 6;
|
ii += BLADES_PER_TUFT * SEGMENTS * 6;
|
||||||
}
|
}
|
||||||
@@ -580,7 +584,8 @@ public class GrassVertexState extends BaseAppState {
|
|||||||
float frshRootR, float frshRootG, float frshRootB,
|
float frshRootR, float frshRootG, float frshRootB,
|
||||||
float frshTipR, float frshTipG, float frshTipB,
|
float frshTipR, float frshTipG, float frshTipB,
|
||||||
float dryRootR, float dryRootG, float dryRootB,
|
float dryRootR, float dryRootG, float dryRootB,
|
||||||
float dryTipR, float dryTipG, float dryTipB) {
|
float dryTipR, float dryTipG, float dryTipB,
|
||||||
|
float[] tint) {
|
||||||
float x = blade.x(), y = blade.y(), z = blade.z(), h = blade.height();
|
float x = blade.x(), y = blade.y(), z = blade.z(), h = blade.height();
|
||||||
float baseHW = h * WIDTH_FACTOR * 0.5f;
|
float baseHW = h * WIDTH_FACTOR * 0.5f;
|
||||||
float tAngle = (float) (((x * 127.1f + z * 311.7f) % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2));
|
float tAngle = (float) (((x * 127.1f + z * 311.7f) % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2));
|
||||||
@@ -648,10 +653,10 @@ public class GrassVertexState extends BaseAppState {
|
|||||||
float dry = blade.dryness();
|
float dry = blade.dryness();
|
||||||
setV(pos, nrm, col, tex, svi, spX - cosA * hw, spY, spZ - sinA * hw, nx, ny, nz, t, dry,
|
setV(pos, nrm, col, tex, svi, spX - cosA * hw, spY, spZ - sinA * hw, nx, ny, nz, t, dry,
|
||||||
frshRootR, frshRootG, frshRootB, frshTipR, frshTipG, frshTipB,
|
frshRootR, frshRootG, frshRootB, frshTipR, frshTipG, frshTipB,
|
||||||
dryRootR, dryRootG, dryRootB, dryTipR, dryTipG, dryTipB);
|
dryRootR, dryRootG, dryRootB, dryTipR, dryTipG, dryTipB, tint);
|
||||||
setV(pos, nrm, col, tex, svi+1, spX + cosA * hw, spY, spZ + sinA * hw, nx, ny, nz, t, dry,
|
setV(pos, nrm, col, tex, svi+1, spX + cosA * hw, spY, spZ + sinA * hw, nx, ny, nz, t, dry,
|
||||||
frshRootR, frshRootG, frshRootB, frshTipR, frshTipG, frshTipB,
|
frshRootR, frshRootG, frshRootB, frshTipR, frshTipG, frshTipB,
|
||||||
dryRootR, dryRootG, dryRootB, dryTipR, dryTipG, dryTipB);
|
dryRootR, dryRootG, dryRootB, dryTipR, dryTipG, dryTipB, tint);
|
||||||
|
|
||||||
if (s < SEGMENTS) {
|
if (s < SEGMENTS) {
|
||||||
int sii = bladeIi + s * 6;
|
int sii = bladeIi + s * 6;
|
||||||
@@ -679,7 +684,8 @@ public class GrassVertexState extends BaseAppState {
|
|||||||
float frshRootR, float frshRootG, float frshRootB,
|
float frshRootR, float frshRootG, float frshRootB,
|
||||||
float frshTipR, float frshTipG, float frshTipB,
|
float frshTipR, float frshTipG, float frshTipB,
|
||||||
float dryRootR, float dryRootG, float dryRootB,
|
float dryRootR, float dryRootG, float dryRootB,
|
||||||
float dryTipR, float dryTipG, float dryTipB) {
|
float dryTipR, float dryTipG, float dryTipB,
|
||||||
|
float[] tint) {
|
||||||
int pi = vi * 3;
|
int pi = vi * 3;
|
||||||
pos[pi] = x; pos[pi+1] = y; pos[pi+2] = z;
|
pos[pi] = x; pos[pi+1] = y; pos[pi+2] = z;
|
||||||
|
|
||||||
@@ -696,18 +702,29 @@ public class GrassVertexState extends BaseAppState {
|
|||||||
float vr = VERY_DRY_ROOT_COLOR.r + (VERY_DRY_TIP_COLOR.r - VERY_DRY_ROOT_COLOR.r) * wf;
|
float vr = VERY_DRY_ROOT_COLOR.r + (VERY_DRY_TIP_COLOR.r - VERY_DRY_ROOT_COLOR.r) * wf;
|
||||||
float vg = VERY_DRY_ROOT_COLOR.g + (VERY_DRY_TIP_COLOR.g - VERY_DRY_ROOT_COLOR.g) * wf;
|
float vg = VERY_DRY_ROOT_COLOR.g + (VERY_DRY_TIP_COLOR.g - VERY_DRY_ROOT_COLOR.g) * wf;
|
||||||
float vb = VERY_DRY_ROOT_COLOR.b + (VERY_DRY_TIP_COLOR.b - VERY_DRY_ROOT_COLOR.b) * wf;
|
float vb = VERY_DRY_ROOT_COLOR.b + (VERY_DRY_TIP_COLOR.b - VERY_DRY_ROOT_COLOR.b) * wf;
|
||||||
|
float effectiveDryness = Math.min(1f, dryness + tint[4]);
|
||||||
float fr, fg, fb;
|
float fr, fg, fb;
|
||||||
if (dryness <= 0.5f) {
|
if (effectiveDryness <= 0.5f) {
|
||||||
float t = dryness * 2f;
|
float t = effectiveDryness * 2f;
|
||||||
fr = gr + (dr - gr) * t;
|
fr = gr + (dr - gr) * t;
|
||||||
fg = gg + (dg - gg) * t;
|
fg = gg + (dg - gg) * t;
|
||||||
fb = gb + (db - gb) * t;
|
fb = gb + (db - gb) * t;
|
||||||
} else {
|
} else {
|
||||||
float t = (dryness - 0.5f) * 2f;
|
float t = (effectiveDryness - 0.5f) * 2f;
|
||||||
fr = dr + (vr - dr) * t;
|
fr = dr + (vr - dr) * t;
|
||||||
fg = dg + (vg - dg) * t;
|
fg = dg + (vg - dg) * t;
|
||||||
fb = db + (vb - db) * t;
|
fb = db + (vb - db) * t;
|
||||||
}
|
}
|
||||||
|
if (tint[3] > 0f) {
|
||||||
|
// Terrain-Farbe vollständig übernehmen; Gradient: 40% an Wurzel, 115% an Spitze
|
||||||
|
float factor = 0.40f + wf * 0.75f;
|
||||||
|
fr = Math.min(1f, tint[0] * factor);
|
||||||
|
fg = Math.min(1f, tint[1] * factor);
|
||||||
|
fb = Math.min(1f, tint[2] * factor);
|
||||||
|
}
|
||||||
|
fr = Math.max(0f, Math.min(1f, fr));
|
||||||
|
fg = Math.max(0f, Math.min(1f, fg));
|
||||||
|
fb = Math.max(0f, Math.min(1f, fb));
|
||||||
col[ci] = fr; col[ci+1] = fg; col[ci+2] = fb; col[ci+3] = 1f;
|
col[ci] = fr; col[ci+1] = fg; col[ci+2] = fb; col[ci+3] = 1f;
|
||||||
|
|
||||||
int ti = vi * 2;
|
int ti = vi * 2;
|
||||||
|
|||||||
@@ -0,0 +1,650 @@
|
|||||||
|
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.collision.CollisionResults;
|
||||||
|
import com.jme3.material.Material;
|
||||||
|
import com.jme3.material.RenderState;
|
||||||
|
import com.jme3.math.ColorRGBA;
|
||||||
|
import com.jme3.math.Ray;
|
||||||
|
import com.jme3.math.Vector2f;
|
||||||
|
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.scene.Spatial;
|
||||||
|
import com.jme3.scene.VertexBuffer;
|
||||||
|
import com.jme3.terrain.geomipmap.TerrainQuad;
|
||||||
|
import com.jme3.texture.Texture;
|
||||||
|
import com.jme3.util.BufferUtils;
|
||||||
|
import de.blight.common.GrassVertexBlade;
|
||||||
|
import de.blight.common.MapData;
|
||||||
|
import de.blight.common.TerrainColorGrassIO;
|
||||||
|
import de.blight.editor.SharedInput;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Random;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rendert Terrain-Farb-Gras im Editor: Halmfarbe kommt pixel-genau von der
|
||||||
|
* Terrain-Textur an der Bladeposition. Keine Nutzer-Farbauswahl, kein Trockenheitsgrad.
|
||||||
|
*/
|
||||||
|
public class TerrainColorGrassState extends BaseAppState {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(TerrainColorGrassState.class);
|
||||||
|
|
||||||
|
// ── Chunks ────────────────────────────────────────────────────────────────
|
||||||
|
private static final int TERRAIN_HALF = 2048;
|
||||||
|
private static final int CHUNK_SIZE = 128;
|
||||||
|
private static final int CHUNKS_PER_AXIS = (TERRAIN_HALF * 2) / CHUNK_SIZE;
|
||||||
|
private static final int CHUNK_COUNT = CHUNKS_PER_AXIS * CHUNKS_PER_AXIS;
|
||||||
|
private static final int MAX_REBUILDS_PER_FRAME = 3;
|
||||||
|
|
||||||
|
// ── Geometrie (identisch zu GrassVertexState) ─────────────────────────────
|
||||||
|
static final int BLADES_PER_TUFT = 3;
|
||||||
|
static final int SEGMENTS = 5;
|
||||||
|
static final float WIDTH_FACTOR = 0.10f;
|
||||||
|
static final float BEND_FACTOR = 0.15f;
|
||||||
|
|
||||||
|
// ── LOD ───────────────────────────────────────────────────────────────────
|
||||||
|
private static final float CULL_DIST = 150f;
|
||||||
|
private static final float CULL_DIST_SQ = CULL_DIST * CULL_DIST;
|
||||||
|
|
||||||
|
// ── Samen ─────────────────────────────────────────────────────────────────
|
||||||
|
private static final String SEED_TEX_BASE = "Textures/internal/gras/seeds/seeds";
|
||||||
|
private static final String SEED_TEX_EXT = ".png";
|
||||||
|
private static final float SEED_SIZE_FACTOR = 0.45f;
|
||||||
|
private static final float SEED_Y_FACTOR = 0.78f;
|
||||||
|
|
||||||
|
// ── Zustand ───────────────────────────────────────────────────────────────
|
||||||
|
private final SharedInput input;
|
||||||
|
private AssetManager assetManager;
|
||||||
|
private com.jme3.renderer.Camera cam;
|
||||||
|
private TerrainQuad terrain;
|
||||||
|
private Node grassNode;
|
||||||
|
private Material material;
|
||||||
|
private Material[] seedMaterials = new Material[0];
|
||||||
|
private Material seedStalkMaterial;
|
||||||
|
|
||||||
|
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||||
|
private final List<GrassVertexBlade>[] chunkBlades = new List[CHUNK_COUNT];
|
||||||
|
private final Node[] chunkNodes = new Node[CHUNK_COUNT];
|
||||||
|
private final boolean[] dirtyChunks = new boolean[CHUNK_COUNT];
|
||||||
|
|
||||||
|
// ── Terrain-Farb-Sampling ─────────────────────────────────────────────────
|
||||||
|
private BufferedImage[] slotImages; // [12] – geladene Texturbilder pro Slot
|
||||||
|
private MapData cachedMapData;
|
||||||
|
|
||||||
|
public TerrainColorGrassState(SharedInput input) {
|
||||||
|
this.input = input;
|
||||||
|
for (int i = 0; i < CHUNK_COUNT; i++) chunkBlades[i] = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTerrain(TerrainQuad terrain) { this.terrain = terrain; }
|
||||||
|
|
||||||
|
public void setMapData(MapData mapData) {
|
||||||
|
this.cachedMapData = mapData;
|
||||||
|
this.slotImages = null;
|
||||||
|
initSlotImages();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<GrassVertexBlade> getAllBlades() {
|
||||||
|
List<GrassVertexBlade> all = new ArrayList<>();
|
||||||
|
for (List<GrassVertexBlade> list : chunkBlades) all.addAll(list);
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initialize(Application app) {
|
||||||
|
this.assetManager = app.getAssetManager();
|
||||||
|
this.cam = app.getCamera();
|
||||||
|
grassNode = new Node("terrainColorGrassNode");
|
||||||
|
((SimpleApplication) app).getRootNode().attachChild(grassNode);
|
||||||
|
material = buildMaterial();
|
||||||
|
seedMaterials = loadSeedMaterials();
|
||||||
|
seedStalkMaterial = buildSeedStalkMaterial();
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (GrassVertexBlade b : TerrainColorGrassIO.load()) {
|
||||||
|
int ci = chunkIndex(b.x(), b.z());
|
||||||
|
if (ci >= 0) { chunkBlades[ci].add(b); dirtyChunks[ci] = true; }
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[TerrainColorGrassState] Daten nicht ladbar: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void cleanup(Application app) {
|
||||||
|
((SimpleApplication) app).getRootNode().detachChild(grassNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void onEnable() { grassNode.setCullHint(Spatial.CullHint.Inherit); }
|
||||||
|
@Override protected void onDisable() { grassNode.setCullHint(Spatial.CullHint.Always); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void update(float tpf) {
|
||||||
|
if (slotImages == null && cachedMapData != null) initSlotImages();
|
||||||
|
processBrushEdits();
|
||||||
|
rebuildDirtyChunks();
|
||||||
|
updateChunkVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Terrain-Farb-Sampling ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void initSlotImages() {
|
||||||
|
if (cachedMapData == null) return;
|
||||||
|
slotImages = new BufferedImage[12];
|
||||||
|
String[] paths = new String[12];
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
paths[i] = (cachedMapData.terrainTextures != null && cachedMapData.terrainTextures.length > i) ? cachedMapData.terrainTextures[i] : "";
|
||||||
|
paths[i+4] = (cachedMapData.upperTextures != null && cachedMapData.upperTextures.length > i) ? cachedMapData.upperTextures[i] : "";
|
||||||
|
paths[i+8] = (cachedMapData.thirdTextures != null && cachedMapData.thirdTextures.length > i) ? cachedMapData.thirdTextures[i] : "";
|
||||||
|
}
|
||||||
|
for (int s = 0; s < 12; s++) {
|
||||||
|
if (paths[s] == null || paths[s].isEmpty()) continue;
|
||||||
|
try (java.io.InputStream is = getClass().getClassLoader().getResourceAsStream(paths[s])) {
|
||||||
|
if (is == null) continue;
|
||||||
|
slotImages[s] = javax.imageio.ImageIO.read(is);
|
||||||
|
if (slotImages[s] != null) {
|
||||||
|
log.info("[TerrainColorGrassState] Slot {} geladen: {} ({}x{})", s, paths[s],
|
||||||
|
slotImages[s].getWidth(), slotImages[s].getHeight());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[TerrainColorGrassState] Slot {} nicht ladbar '{}': {}", s, paths[s], e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int ci = 0; ci < CHUNK_COUNT; ci++) {
|
||||||
|
if (!chunkBlades[ci].isEmpty()) dirtyChunks[ci] = true;
|
||||||
|
}
|
||||||
|
log.info("[TerrainColorGrassState] Slot-Bilder geladen, Chunks als dirty markiert");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gibt die pixel-genaue Terrain-Texturfarbe an der Weltposition zurück.
|
||||||
|
* UV = worldXZ / diffuseScale (identisch zum BakedTerrain-Shader).
|
||||||
|
*/
|
||||||
|
public float[] sampleTerrainColorAt(float worldX, float worldZ) {
|
||||||
|
return sampleTerrainColor(worldX, worldZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
private float[] sampleTerrainColor(float worldX, float worldZ) {
|
||||||
|
float[] fallback = {0.2f, 0.5f, 0.1f};
|
||||||
|
if (cachedMapData == null || slotImages == null) return fallback;
|
||||||
|
|
||||||
|
MapData md = cachedMapData;
|
||||||
|
int size = MapData.SPLAT_SIZE;
|
||||||
|
int px = Math.max(0, Math.min(size - 1, Math.round((worldX + 1024f) / 2f)));
|
||||||
|
int pz = Math.max(0, Math.min(size - 1, (size - 1) - Math.round((worldZ + 1024f) / 2f)));
|
||||||
|
int idx = pz * size + px;
|
||||||
|
|
||||||
|
int maxOverlay = Math.max(
|
||||||
|
Math.max(md.upperSplatR[idx] & 0xFF, md.upperSplatG[idx] & 0xFF),
|
||||||
|
Math.max(
|
||||||
|
Math.max(md.upperSplatB[idx] & 0xFF, md.upperSplatA[idx] & 0xFF),
|
||||||
|
Math.max(
|
||||||
|
Math.max(md.thirdSplatR[idx] & 0xFF, md.thirdSplatG[idx] & 0xFF),
|
||||||
|
Math.max(md.thirdSplatB[idx] & 0xFF, md.thirdSplatA[idx] & 0xFF)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
float baseScale = Math.max(0f, (255 - maxOverlay) / 255f);
|
||||||
|
int splatR = md.splatR[idx] & 0xFF;
|
||||||
|
if (splatR == 0) splatR = 255;
|
||||||
|
|
||||||
|
float[] slotW = {
|
||||||
|
splatR * baseScale / 255f,
|
||||||
|
(md.splatG[idx] & 0xFF) * baseScale / 255f,
|
||||||
|
(md.splatB[idx] & 0xFF) * baseScale / 255f,
|
||||||
|
(md.splatA[idx] & 0xFF) * baseScale / 255f,
|
||||||
|
(md.upperSplatR[idx] & 0xFF) / 255f,
|
||||||
|
(md.upperSplatG[idx] & 0xFF) / 255f,
|
||||||
|
(md.upperSplatB[idx] & 0xFF) / 255f,
|
||||||
|
(md.upperSplatA[idx] & 0xFF) / 255f,
|
||||||
|
(md.thirdSplatR[idx] & 0xFF) / 255f,
|
||||||
|
(md.thirdSplatG[idx] & 0xFF) / 255f,
|
||||||
|
(md.thirdSplatB[idx] & 0xFF) / 255f,
|
||||||
|
(md.thirdSplatA[idx] & 0xFF) / 255f,
|
||||||
|
};
|
||||||
|
|
||||||
|
float[] scales = input.diffuseScales; // volatile – Shader-UV: worldXZ / scale
|
||||||
|
float rSum = 0, gSum = 0, bSum = 0, wSum = 0;
|
||||||
|
for (int s = 0; s < 12; s++) {
|
||||||
|
if (slotW[s] <= 0f || slotImages[s] == null) continue;
|
||||||
|
float sc = (scales != null && s < scales.length && scales[s] > 0f) ? scales[s] : 8f;
|
||||||
|
float u = (worldX / sc) % 1f; if (u < 0f) u += 1f;
|
||||||
|
float v = (worldZ / sc) % 1f; if (v < 0f) v += 1f;
|
||||||
|
int imgW = slotImages[s].getWidth();
|
||||||
|
int imgH = slotImages[s].getHeight();
|
||||||
|
int texPx = Math.min(imgW - 1, (int)(u * imgW));
|
||||||
|
int texPz = Math.min(imgH - 1, (int)(v * imgH));
|
||||||
|
int rgb = slotImages[s].getRGB(texPx, texPz);
|
||||||
|
float r = ((rgb >> 16) & 0xFF) / 255f;
|
||||||
|
float g = ((rgb >> 8) & 0xFF) / 255f;
|
||||||
|
float b = ( rgb & 0xFF) / 255f;
|
||||||
|
rSum += r * slotW[s];
|
||||||
|
gSum += g * slotW[s];
|
||||||
|
bSum += b * slotW[s];
|
||||||
|
wSum += slotW[s];
|
||||||
|
}
|
||||||
|
if (wSum <= 0f) return fallback;
|
||||||
|
return new float[]{rSum / wSum, gSum / wSum, bSum / wSum};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Material ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private Material buildMaterial() {
|
||||||
|
try {
|
||||||
|
Material mat = new Material(assetManager, "MatDefs/GrassVertex.j3md");
|
||||||
|
mat.setFloat("WindSpeed", 1.0f);
|
||||||
|
mat.setFloat("WindStrength", 0.15f);
|
||||||
|
mat.setVector3("SunDir", new Vector3f(0.35f, 0.8f, 0.45f).normalizeLocal());
|
||||||
|
mat.setColor("SunColor", new ColorRGBA(0.95f, 0.90f, 0.75f, 1.0f));
|
||||||
|
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
||||||
|
return mat;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
|
||||||
|
mat.setColor("Color", new ColorRGBA(0.22f, 0.68f, 0.12f, 1f));
|
||||||
|
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
||||||
|
return mat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Material buildSeedStalkMaterial() {
|
||||||
|
Material mat = new Material(assetManager, "MatDefs/GrassVertex.j3md");
|
||||||
|
mat.setFloat("WindSpeed", 1.0f);
|
||||||
|
mat.setFloat("WindStrength", 0.15f);
|
||||||
|
mat.setVector3("SunDir", new Vector3f(0.35f, 0.8f, 0.45f).normalizeLocal());
|
||||||
|
mat.setColor("SunColor", new ColorRGBA(0.95f, 0.90f, 0.75f, 1.0f));
|
||||||
|
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
||||||
|
return mat;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Material[] loadSeedMaterials() {
|
||||||
|
List<Material> mats = new ArrayList<>();
|
||||||
|
for (int i = 1; i <= 99; i++) {
|
||||||
|
String path = SEED_TEX_BASE + i + SEED_TEX_EXT;
|
||||||
|
try {
|
||||||
|
Texture tex = assetManager.loadTexture(path);
|
||||||
|
Material mat = new Material(assetManager, "MatDefs/GrassSeed.j3md");
|
||||||
|
mat.setTexture("ColorMap", tex);
|
||||||
|
mat.setFloat("WindSpeed", 1.0f);
|
||||||
|
mat.setFloat("WindStrength", 0.15f);
|
||||||
|
mat.setVector3("SunDir", new Vector3f(0.35f, 0.8f, 0.45f).normalizeLocal());
|
||||||
|
mat.setColor("SunColor", new ColorRGBA(0.95f, 0.90f, 0.75f, 1.0f));
|
||||||
|
mat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
|
||||||
|
mats.add(mat);
|
||||||
|
} catch (Exception e) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mats.toArray(new Material[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pinsel-Interaktion ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void processBrushEdits() {
|
||||||
|
SharedInput.GrassVertexEdit edit;
|
||||||
|
while ((edit = input.terrainGrassEditQueue.poll()) != null) {
|
||||||
|
if (terrain == null) continue;
|
||||||
|
float jmeX = (float)(edit.screenX() * input.viewportScaleX);
|
||||||
|
float jmeY = (float)(edit.screenY() * input.viewportScaleY);
|
||||||
|
Vector3f hit = raycastSurface(jmeX, jmeY, getApplication().getCamera());
|
||||||
|
if (hit == null) continue;
|
||||||
|
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||||
|
if (ves != null && ves.terrainTypeAt(hit.x, hit.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) continue;
|
||||||
|
if (edit.action() > 0) addBlades(hit);
|
||||||
|
else removeBlades(hit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Vector3f raycastSurface(float screenX, float screenY, com.jme3.renderer.Camera cam) {
|
||||||
|
float flippedY = cam.getHeight() - screenY;
|
||||||
|
Vector3f origin = cam.getWorldCoordinates(new Vector2f(screenX, flippedY), 0f);
|
||||||
|
Vector3f dir = cam.getWorldCoordinates(new Vector2f(screenX, flippedY), 1f).subtractLocal(origin).normalizeLocal();
|
||||||
|
Ray ray = new Ray(origin, dir);
|
||||||
|
|
||||||
|
Vector3f best = null;
|
||||||
|
float bestDistSq = Float.MAX_VALUE;
|
||||||
|
|
||||||
|
CollisionResults cr = new CollisionResults();
|
||||||
|
terrain.collideWith(ray, cr);
|
||||||
|
if (cr.size() > 0) {
|
||||||
|
best = cr.getClosestCollision().getContactPoint().clone();
|
||||||
|
bestDistSq = origin.distanceSquared(best);
|
||||||
|
}
|
||||||
|
|
||||||
|
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||||
|
if (ves != null) {
|
||||||
|
Vector3f vp = ves.raycastVoxelGeometry(ray);
|
||||||
|
if (vp != null) {
|
||||||
|
float d = origin.distanceSquared(vp);
|
||||||
|
if (d < bestDistSq) { best = vp; bestDistSq = d; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SculptedMeshEditorState smes = getStateManager().getState(SculptedMeshEditorState.class);
|
||||||
|
if (smes != null) {
|
||||||
|
Vector3f sp = smes.raycastGeometry(ray);
|
||||||
|
if (sp != null) {
|
||||||
|
float d = origin.distanceSquared(sp);
|
||||||
|
if (d < bestDistSq) { best = sp; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Random rng = new Random();
|
||||||
|
|
||||||
|
private static float brushFalloff(float distRatio) {
|
||||||
|
return 1f - 0.5f * distRatio * distRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addBlades(Vector3f center) {
|
||||||
|
float radius = (float) input.terrainGrassTool.brushRadius.getValue();
|
||||||
|
float height = (float) input.terrainGrassTool.bladeHeight.getValue();
|
||||||
|
int density = (int) input.terrainGrassTool.density.getValue();
|
||||||
|
float uniformity = (float) input.terrainGrassTool.uniformity.getValue();
|
||||||
|
float variation = (1f - uniformity) * 0.25f;
|
||||||
|
float radSq = radius * radius;
|
||||||
|
float seedPct = (float) input.terrainGrassTool.seedDensity.getValue() / 100f;
|
||||||
|
|
||||||
|
// Zu kleine Halme im Pinselbereich durch größere ersetzen
|
||||||
|
for (int ci = 0; ci < CHUNK_COUNT; ci++) {
|
||||||
|
List<GrassVertexBlade> list = chunkBlades[ci];
|
||||||
|
boolean changed = false;
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
GrassVertexBlade b = list.get(i);
|
||||||
|
float dx = b.x() - center.x, dz = b.z() - center.z;
|
||||||
|
float distSq = dx*dx + dz*dz;
|
||||||
|
if (distSq > radSq) continue;
|
||||||
|
float distRatio = (float) Math.sqrt(distSq) / radius;
|
||||||
|
float idealH = height * brushFalloff(distRatio);
|
||||||
|
if (b.height() < idealH * 0.88f) {
|
||||||
|
float newH = idealH * (1f + variation * (rng.nextFloat() * 2f - 1f));
|
||||||
|
list.set(i, new GrassVertexBlade(b.x(), b.y(), b.z(), Math.max(0.05f, newH), 0f, b.seedIdx()));
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) dirtyChunks[ci] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||||
|
|
||||||
|
for (int i = 0; i < density; i++) {
|
||||||
|
float angle = rng.nextFloat() * (float)(Math.PI * 2);
|
||||||
|
float r = rng.nextFloat() * radius;
|
||||||
|
float bx = center.x + (float) Math.cos(angle) * r;
|
||||||
|
float bz = center.z + (float) Math.sin(angle) * r;
|
||||||
|
float by;
|
||||||
|
if (ves != null) {
|
||||||
|
by = ves.heightAt(bx, bz);
|
||||||
|
if (ves.isTooSteep(bx, bz)) continue;
|
||||||
|
} else {
|
||||||
|
by = terrain.getHeight(new Vector2f(bx, bz));
|
||||||
|
if (Float.isNaN(by)) continue;
|
||||||
|
}
|
||||||
|
float distRatio = r / radius;
|
||||||
|
float h = height * brushFalloff(distRatio) * (1f + variation * (rng.nextFloat() * 2f - 1f));
|
||||||
|
h = Math.max(0.05f, h);
|
||||||
|
int seedIdx = rng.nextFloat() < seedPct ? rng.nextInt(Math.max(1, seedMaterials.length)) : -1;
|
||||||
|
int ci = chunkIndex(bx, bz);
|
||||||
|
if (ci >= 0) {
|
||||||
|
chunkBlades[ci].add(new GrassVertexBlade(bx, by, bz, h, 0f, seedIdx));
|
||||||
|
dirtyChunks[ci] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void adjustBladeHeights(Vector3f center, float radius) {
|
||||||
|
if (terrain == null) return;
|
||||||
|
float radSq = radius * radius;
|
||||||
|
for (int ci = 0; ci < CHUNK_COUNT; ci++) {
|
||||||
|
List<GrassVertexBlade> list = chunkBlades[ci];
|
||||||
|
boolean changed = false;
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
GrassVertexBlade b = list.get(i);
|
||||||
|
float dx = b.x() - center.x, dz = b.z() - center.z;
|
||||||
|
if (dx*dx + dz*dz > radSq) continue;
|
||||||
|
float newY = terrain.getHeight(new Vector2f(b.x(), b.z()));
|
||||||
|
if (!Float.isNaN(newY)) {
|
||||||
|
list.set(i, new GrassVertexBlade(b.x(), newY, b.z(), b.height(), 0f, b.seedIdx()));
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) dirtyChunks[ci] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeBlades(Vector3f center) {
|
||||||
|
float radSq = (float) Math.pow(input.terrainGrassTool.brushRadius.getValue(), 2);
|
||||||
|
for (int ci = 0; ci < CHUNK_COUNT; ci++) {
|
||||||
|
List<GrassVertexBlade> list = chunkBlades[ci];
|
||||||
|
int before = list.size();
|
||||||
|
list.removeIf(b -> {
|
||||||
|
float dx = b.x() - center.x, dz = b.z() - center.z;
|
||||||
|
return dx*dx + dz*dz <= radSq;
|
||||||
|
});
|
||||||
|
if (list.size() != before) dirtyChunks[ci] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Chunk-Verwaltung ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private int chunkIndex(float x, float z) {
|
||||||
|
int cx = (int)((x + TERRAIN_HALF) / CHUNK_SIZE);
|
||||||
|
int cz = (int)((z + TERRAIN_HALF) / CHUNK_SIZE);
|
||||||
|
if (cx < 0 || cx >= CHUNKS_PER_AXIS || cz < 0 || cz >= CHUNKS_PER_AXIS) return -1;
|
||||||
|
return cz * CHUNKS_PER_AXIS + cx;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rebuildDirtyChunks() {
|
||||||
|
int rebuilt = 0;
|
||||||
|
for (int ci = 0; ci < CHUNK_COUNT && rebuilt < MAX_REBUILDS_PER_FRAME; ci++) {
|
||||||
|
if (!dirtyChunks[ci]) continue;
|
||||||
|
dirtyChunks[ci] = false;
|
||||||
|
rebuilt++;
|
||||||
|
rebuildChunk(ci);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rebuildChunk(int ci) {
|
||||||
|
if (chunkNodes[ci] != null) {
|
||||||
|
grassNode.detachChild(chunkNodes[ci]);
|
||||||
|
chunkNodes[ci] = null;
|
||||||
|
}
|
||||||
|
List<GrassVertexBlade> blades = chunkBlades[ci];
|
||||||
|
if (blades.isEmpty()) return;
|
||||||
|
|
||||||
|
int bladeCount = blades.size();
|
||||||
|
int vertCount = bladeCount * BLADES_PER_TUFT * (SEGMENTS + 1) * 2;
|
||||||
|
int indexCount = bladeCount * BLADES_PER_TUFT * SEGMENTS * 6;
|
||||||
|
|
||||||
|
float[] positions = new float[vertCount * 3];
|
||||||
|
float[] normals = new float[vertCount * 3];
|
||||||
|
float[] colors = new float[vertCount * 4];
|
||||||
|
float[] texCoords = new float[vertCount * 2];
|
||||||
|
int[] indices = new int[indexCount];
|
||||||
|
|
||||||
|
int vi = 0, ii = 0;
|
||||||
|
for (GrassVertexBlade blade : blades) {
|
||||||
|
// Pixel-genaue Terrain-Farbe an der Halmposition
|
||||||
|
float[] col = sampleTerrainColor(blade.x(), blade.z());
|
||||||
|
// Als vollständiger Tint übergeben (tint[3]=1.0 → GrassVertexState.setV übernimmt Farbe komplett)
|
||||||
|
float[] tint = {col[0], col[1], col[2], 1.0f, 0f};
|
||||||
|
// Farbparameter sind egal – werden durch tint[3]=1.0 komplett überschrieben
|
||||||
|
GrassVertexState.buildTuft(positions, normals, colors, texCoords, indices, vi, ii, blade,
|
||||||
|
0f, 0f, 0f, 0f, 0f, 0f,
|
||||||
|
0f, 0f, 0f, 0f, 0f, 0f, tint);
|
||||||
|
vi += BLADES_PER_TUFT * (SEGMENTS + 1) * 2;
|
||||||
|
ii += BLADES_PER_TUFT * SEGMENTS * 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
Mesh mesh = new Mesh();
|
||||||
|
mesh.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(positions));
|
||||||
|
mesh.setBuffer(VertexBuffer.Type.Normal, 3, BufferUtils.createFloatBuffer(normals));
|
||||||
|
mesh.setBuffer(VertexBuffer.Type.Color, 4, BufferUtils.createFloatBuffer(colors));
|
||||||
|
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2, BufferUtils.createFloatBuffer(texCoords));
|
||||||
|
mesh.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createIntBuffer(indices));
|
||||||
|
mesh.updateBound();
|
||||||
|
|
||||||
|
Geometry geo = new Geometry("tcg_" + ci, mesh);
|
||||||
|
geo.setMaterial(material);
|
||||||
|
|
||||||
|
Node node = new Node("tcgc_" + ci);
|
||||||
|
node.attachChild(geo);
|
||||||
|
|
||||||
|
// Samen
|
||||||
|
if (seedMaterials.length > 0) {
|
||||||
|
List<GrassVertexBlade> seededAll = new ArrayList<>();
|
||||||
|
Map<Integer, List<GrassVertexBlade>> byTex = new HashMap<>();
|
||||||
|
for (GrassVertexBlade b : blades) {
|
||||||
|
if (b.seedIdx() >= 0 && b.seedIdx() < seedMaterials.length) {
|
||||||
|
seededAll.add(b);
|
||||||
|
byTex.computeIfAbsent(b.seedIdx(), k -> new ArrayList<>()).add(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!seededAll.isEmpty()) {
|
||||||
|
Geometry stalkGeo = buildSeedStalkMesh("stalk_" + ci, seededAll);
|
||||||
|
stalkGeo.setMaterial(seedStalkMaterial);
|
||||||
|
node.attachChild(stalkGeo);
|
||||||
|
}
|
||||||
|
for (Map.Entry<Integer, List<GrassVertexBlade>> e : byTex.entrySet()) {
|
||||||
|
Geometry seedGeo = buildSeedCrossMesh("seed_" + ci + "_" + e.getKey(), e.getValue());
|
||||||
|
seedGeo.setMaterial(seedMaterials[e.getKey()]);
|
||||||
|
node.attachChild(seedGeo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chunkNodes[ci] = node;
|
||||||
|
grassNode.attachChild(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateChunkVisibility() {
|
||||||
|
if (cam == null) return;
|
||||||
|
Vector3f camPos = cam.getLocation();
|
||||||
|
for (int ci = 0; ci < CHUNK_COUNT; ci++) {
|
||||||
|
if (chunkNodes[ci] == null) continue;
|
||||||
|
int cx = ci % CHUNKS_PER_AXIS;
|
||||||
|
int cz = ci / CHUNKS_PER_AXIS;
|
||||||
|
float wx = cx * CHUNK_SIZE - TERRAIN_HALF + CHUNK_SIZE * 0.5f;
|
||||||
|
float wz = cz * CHUNK_SIZE - TERRAIN_HALF + CHUNK_SIZE * 0.5f;
|
||||||
|
float dx = camPos.x - wx, dz = camPos.z - wz;
|
||||||
|
boolean visible = dx*dx + dz*dz <= CULL_DIST_SQ;
|
||||||
|
chunkNodes[ci].setCullHint(visible ? Spatial.CullHint.Inherit : Spatial.CullHint.Always);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Samen-Mesh-Generierung (analog zu GrassVertexState) ───────────────────
|
||||||
|
|
||||||
|
private static Geometry buildSeedStalkMesh(String name, List<GrassVertexBlade> blades) {
|
||||||
|
final int SEG = 4;
|
||||||
|
final float R = 0xbb / 255f, G = 0x90 / 255f, B = 0x59 / 255f;
|
||||||
|
int n = blades.size();
|
||||||
|
int vTotal = n * (SEG + 1) * 2;
|
||||||
|
float[] pos = new float[vTotal * 3];
|
||||||
|
float[] nrm = new float[vTotal * 3];
|
||||||
|
float[] col = new float[vTotal * 4];
|
||||||
|
float[] tex = new float[vTotal * 2];
|
||||||
|
int[] idx = new int [n * SEG * 6];
|
||||||
|
|
||||||
|
int vi = 0, ii = 0;
|
||||||
|
for (GrassVertexBlade blade : blades) {
|
||||||
|
float x = blade.x(), y = blade.y(), z = blade.z(), h = blade.height();
|
||||||
|
float hw = h * 0.018f;
|
||||||
|
float ang = (float)(((x * 127.1f + z * 311.7f) % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2));
|
||||||
|
float cA = (float) Math.cos(ang), sA = (float) Math.sin(ang);
|
||||||
|
float nx = -sA, ny = 0f, nz = cA;
|
||||||
|
float blend = 0.30f;
|
||||||
|
nx *= (1f - blend); ny = blend; nz *= (1f - blend);
|
||||||
|
float nLen = (float) Math.sqrt(nx*nx + ny*ny + nz*nz);
|
||||||
|
nx /= nLen; ny /= nLen; nz /= nLen;
|
||||||
|
|
||||||
|
for (int s = 0; s <= SEG; s++) {
|
||||||
|
float t = (float) s / SEG;
|
||||||
|
float curHW = hw * (float) Math.pow(1.0 - t, 1.4);
|
||||||
|
float py = y + h * t;
|
||||||
|
int sviL = vi + s * 2;
|
||||||
|
int sviR = sviL + 1;
|
||||||
|
pos[sviL*3] = x - cA*curHW; pos[sviL*3+1] = py; pos[sviL*3+2] = z - sA*curHW;
|
||||||
|
nrm[sviL*3] = nx; nrm[sviL*3+1] = ny; nrm[sviL*3+2] = nz;
|
||||||
|
col[sviL*4] = R; col[sviL*4+1] = G; col[sviL*4+2] = B; col[sviL*4+3] = 1f;
|
||||||
|
tex[sviL*2] = t; tex[sviL*2+1] = 0f;
|
||||||
|
pos[sviR*3] = x + cA*curHW; pos[sviR*3+1] = py; pos[sviR*3+2] = z + sA*curHW;
|
||||||
|
nrm[sviR*3] = nx; nrm[sviR*3+1] = ny; nrm[sviR*3+2] = nz;
|
||||||
|
col[sviR*4] = R; col[sviR*4+1] = G; col[sviR*4+2] = B; col[sviR*4+3] = 1f;
|
||||||
|
tex[sviR*2] = t; tex[sviR*2+1] = 0f;
|
||||||
|
}
|
||||||
|
for (int s = 0; s < SEG; s++) {
|
||||||
|
int b0 = vi + s * 2;
|
||||||
|
idx[ii] = b0; idx[ii+1] = b0+1; idx[ii+2] = b0+3;
|
||||||
|
idx[ii+3] = b0; idx[ii+4] = b0+3; idx[ii+5] = b0+2;
|
||||||
|
ii += 6;
|
||||||
|
}
|
||||||
|
vi += (SEG + 1) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
Mesh m = new Mesh();
|
||||||
|
m.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(pos));
|
||||||
|
m.setBuffer(VertexBuffer.Type.Normal, 3, BufferUtils.createFloatBuffer(nrm));
|
||||||
|
m.setBuffer(VertexBuffer.Type.Color, 4, BufferUtils.createFloatBuffer(col));
|
||||||
|
m.setBuffer(VertexBuffer.Type.TexCoord, 2, BufferUtils.createFloatBuffer(tex));
|
||||||
|
m.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createIntBuffer(idx));
|
||||||
|
m.updateBound();
|
||||||
|
return new Geometry(name, m);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Geometry buildSeedCrossMesh(String name, List<GrassVertexBlade> blades) {
|
||||||
|
int n = blades.size();
|
||||||
|
float[] pos = new float[n * 8 * 3];
|
||||||
|
float[] tex = new float[n * 8 * 2];
|
||||||
|
int[] idx = new int [n * 12];
|
||||||
|
|
||||||
|
int vi = 0, ii = 0;
|
||||||
|
for (GrassVertexBlade b : blades) {
|
||||||
|
float x = b.x();
|
||||||
|
float yBot = b.y() + b.height() * SEED_Y_FACTOR;
|
||||||
|
float z = b.z();
|
||||||
|
float size = b.height() * SEED_SIZE_FACTOR;
|
||||||
|
float hw = size * 0.5f;
|
||||||
|
setSeedV(pos, tex, vi+0, x-hw, yBot, z, 0,0);
|
||||||
|
setSeedV(pos, tex, vi+1, x+hw, yBot, z, 1,0);
|
||||||
|
setSeedV(pos, tex, vi+2, x+hw, yBot+size, z, 1,1);
|
||||||
|
setSeedV(pos, tex, vi+3, x-hw, yBot+size, z, 0,1);
|
||||||
|
setSeedV(pos, tex, vi+4, x, yBot, z-hw, 0,0);
|
||||||
|
setSeedV(pos, tex, vi+5, x, yBot, z+hw, 1,0);
|
||||||
|
setSeedV(pos, tex, vi+6, x, yBot+size, z+hw, 1,1);
|
||||||
|
setSeedV(pos, tex, vi+7, x, yBot+size, z-hw, 0,1);
|
||||||
|
idx[ii] = vi; idx[ii+1] = vi+1; idx[ii+2] = vi+2;
|
||||||
|
idx[ii+3] = vi; idx[ii+4] = vi+2; idx[ii+5] = vi+3;
|
||||||
|
idx[ii+6] = vi+4; idx[ii+7] = vi+5; idx[ii+8] = vi+6;
|
||||||
|
idx[ii+9] = vi+4; idx[ii+10] = vi+6; idx[ii+11] = vi+7;
|
||||||
|
vi += 8; ii += 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
Mesh m = new Mesh();
|
||||||
|
m.setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(pos));
|
||||||
|
m.setBuffer(VertexBuffer.Type.TexCoord, 2, BufferUtils.createFloatBuffer(tex));
|
||||||
|
m.setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createIntBuffer(idx));
|
||||||
|
m.updateBound();
|
||||||
|
return new Geometry(name, m);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setSeedV(float[] pos, float[] tex, int vi,
|
||||||
|
float x, float y, float z, float u, float v) {
|
||||||
|
pos[vi*3] = x; pos[vi*3+1] = y; pos[vi*3+2] = z;
|
||||||
|
tex[vi*2] = u; tex[vi*2+1] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,7 +98,8 @@ public class TerrainEditorState extends BaseAppState {
|
|||||||
private Geometry brushIndicator;
|
private Geometry brushIndicator;
|
||||||
private Geometry waterGeo;
|
private Geometry waterGeo;
|
||||||
private PlacedObjectState placedObjectState;
|
private PlacedObjectState placedObjectState;
|
||||||
private GrassVertexState grassVertexState;
|
private GrassVertexState grassVertexState;
|
||||||
|
private TerrainColorGrassState terrainColorGrassState;
|
||||||
private StoneEditorState stoneEditorState;
|
private StoneEditorState stoneEditorState;
|
||||||
private SceneObjectState sceneObjState;
|
private SceneObjectState sceneObjState;
|
||||||
private ItemPlacementState itemPlacementState;
|
private ItemPlacementState itemPlacementState;
|
||||||
@@ -253,6 +254,7 @@ public class TerrainEditorState extends BaseAppState {
|
|||||||
// ── Szene aufbauen ────────────────────────────────────────────────────────
|
// ── Szene aufbauen ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private void buildScene() {
|
private void buildScene() {
|
||||||
|
MapData mapDataForGrass = loadedMapData; // sichern bevor buildTerrain()/initSplatmap() es auf null setzt
|
||||||
input.loadingStatus = "Lade Terrain...";
|
input.loadingStatus = "Lade Terrain...";
|
||||||
terrain = buildTerrain();
|
terrain = buildTerrain();
|
||||||
cachedHeightMap = terrain.getHeightMap();
|
cachedHeightMap = terrain.getHeightMap();
|
||||||
@@ -266,8 +268,15 @@ public class TerrainEditorState extends BaseAppState {
|
|||||||
input.loadingStatus = "Lade Vertex-Gras...";
|
input.loadingStatus = "Lade Vertex-Gras...";
|
||||||
grassVertexState = new GrassVertexState(input);
|
grassVertexState = new GrassVertexState(input);
|
||||||
grassVertexState.setTerrain(terrain);
|
grassVertexState.setTerrain(terrain);
|
||||||
|
grassVertexState.setMapData(mapDataForGrass);
|
||||||
app.getStateManager().attach(grassVertexState);
|
app.getStateManager().attach(grassVertexState);
|
||||||
|
|
||||||
|
input.loadingStatus = "Lade Terrain-Gras...";
|
||||||
|
terrainColorGrassState = new TerrainColorGrassState(input);
|
||||||
|
terrainColorGrassState.setTerrain(terrain);
|
||||||
|
terrainColorGrassState.setMapData(mapDataForGrass);
|
||||||
|
app.getStateManager().attach(terrainColorGrassState);
|
||||||
|
|
||||||
input.loadingStatus = "Lade Steine...";
|
input.loadingStatus = "Lade Steine...";
|
||||||
stoneEditorState = new StoneEditorState(input);
|
stoneEditorState = new StoneEditorState(input);
|
||||||
stoneEditorState.setTerrain(terrain);
|
stoneEditorState.setTerrain(terrain);
|
||||||
@@ -1222,6 +1231,7 @@ public class TerrainEditorState extends BaseAppState {
|
|||||||
processEdits();
|
processEdits();
|
||||||
processTextureEdits();
|
processTextureEdits();
|
||||||
updateBrushIndicator();
|
updateBrushIndicator();
|
||||||
|
processGrassTerrainPick();
|
||||||
updateAxesGizmo();
|
updateAxesGizmo();
|
||||||
// Debug: Strg+F8 — Raw-Texture-Modus (kein Lighting) ein/aus
|
// Debug: Strg+F8 — Raw-Texture-Modus (kein Lighting) ein/aus
|
||||||
if (input.debugNoLightToggle) {
|
if (input.debugNoLightToggle) {
|
||||||
@@ -1411,6 +1421,8 @@ public class TerrainEditorState extends BaseAppState {
|
|||||||
/** Gibt den TerrainQuad-Node zurück (z.B. für Voxel-Raycasts). */
|
/** Gibt den TerrainQuad-Node zurück (z.B. für Voxel-Raycasts). */
|
||||||
public TerrainQuad getTerrainNode() { return terrain; }
|
public TerrainQuad getTerrainNode() { return terrain; }
|
||||||
|
|
||||||
|
public MapData getMapData() { return loadedMapData; }
|
||||||
|
|
||||||
/** Gibt das Terrain-Material zurück (für BakedTerrain-Material-Aufbau in anderen States). */
|
/** Gibt das Terrain-Material zurück (für BakedTerrain-Material-Aufbau in anderen States). */
|
||||||
public com.jme3.material.Material getTerrainMaterial() { return terrainMat; }
|
public com.jme3.material.Material getTerrainMaterial() { return terrainMat; }
|
||||||
|
|
||||||
@@ -1534,7 +1546,8 @@ public class TerrainEditorState extends BaseAppState {
|
|||||||
final GrassTuftIO.GrassData grassData = placedObjectState != null
|
final GrassTuftIO.GrassData grassData = placedObjectState != null
|
||||||
? new GrassTuftIO.GrassData(placedObjectState.getSlotPaths(), placedObjectState.getAllTufts())
|
? new GrassTuftIO.GrassData(placedObjectState.getSlotPaths(), placedObjectState.getAllTufts())
|
||||||
: null;
|
: null;
|
||||||
final var grassVertexBlades = grassVertexState != null ? grassVertexState.getAllBlades() : null;
|
final var grassVertexBlades = grassVertexState != null ? grassVertexState.getAllBlades() : null;
|
||||||
|
final var terrainColorGrassBlades = terrainColorGrassState != null ? terrainColorGrassState.getAllBlades() : null;
|
||||||
final List<PlacedModel> models = sceneObjState != null ? sceneObjState.getPlacedModels() : null;
|
final List<PlacedModel> models = sceneObjState != null ? sceneObjState.getPlacedModels() : null;
|
||||||
final List<PlacedLight> lights = lightState != null ? lightState.getPlacedLights() : null;
|
final List<PlacedLight> lights = lightState != null ? lightState.getPlacedLights() : null;
|
||||||
final List<PlacedEmitter> emitters = emitterState != null ? emitterState.getPlacedEmitters() : null;
|
final List<PlacedEmitter> emitters = emitterState != null ? emitterState.getPlacedEmitters() : null;
|
||||||
@@ -1635,6 +1648,10 @@ public class TerrainEditorState extends BaseAppState {
|
|||||||
try { GrassVertexIO.save(grassVertexBlades); }
|
try { GrassVertexIO.save(grassVertexBlades); }
|
||||||
catch (IOException e) { log.error("Vertex-Gras nicht speicherbar", e); }
|
catch (IOException e) { log.error("Vertex-Gras nicht speicherbar", e); }
|
||||||
}
|
}
|
||||||
|
if (terrainColorGrassBlades != null) {
|
||||||
|
try { de.blight.common.TerrainColorGrassIO.save(terrainColorGrassBlades); }
|
||||||
|
catch (IOException e) { log.error("Terrain-Gras nicht speicherbar", e); }
|
||||||
|
}
|
||||||
MapIO.save(data);
|
MapIO.save(data);
|
||||||
if (heightSnap != null) {
|
if (heightSnap != null) {
|
||||||
try {
|
try {
|
||||||
@@ -1661,6 +1678,30 @@ public class TerrainEditorState extends BaseAppState {
|
|||||||
|
|
||||||
// ── Brush-Indikator ───────────────────────────────────────────────────────
|
// ── Brush-Indikator ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void processGrassTerrainPick() {
|
||||||
|
SharedInput.GrassVertexEdit req;
|
||||||
|
while ((req = input.grassTerrainPickQueue.poll()) != null) {
|
||||||
|
if (terrainColorGrassState == null) continue;
|
||||||
|
float jmeX = req.screenX() * (float) input.viewportScaleX;
|
||||||
|
float jmeY = cam.getHeight() - req.screenY() * (float) input.viewportScaleY;
|
||||||
|
Vector3f near = cam.getWorldCoordinates(new com.jme3.math.Vector2f(jmeX, jmeY), 0f);
|
||||||
|
Vector3f far = cam.getWorldCoordinates(new com.jme3.math.Vector2f(jmeX, jmeY), 1f);
|
||||||
|
com.jme3.math.Ray ray = new com.jme3.math.Ray(near, far.subtract(near).normalizeLocal());
|
||||||
|
Vector3f hit = raycastSurface(ray);
|
||||||
|
if (hit == null) continue;
|
||||||
|
float[] color = terrainColorGrassState.sampleTerrainColorAt(hit.x, hit.z);
|
||||||
|
input.grassFreshR = color[0];
|
||||||
|
input.grassFreshG = color[1];
|
||||||
|
input.grassFreshB = color[2];
|
||||||
|
input.grassColorsChanged = true;
|
||||||
|
java.util.function.Consumer<float[]> cb = input.grassTerrainPickCallback;
|
||||||
|
if (cb != null) {
|
||||||
|
input.grassTerrainPickCallback = null;
|
||||||
|
javafx.application.Platform.runLater(() -> cb.accept(color));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void updateBrushIndicator() {
|
private void updateBrushIndicator() {
|
||||||
float mx = input.mouseScreenX;
|
float mx = input.mouseScreenX;
|
||||||
float my = input.mouseScreenY;
|
float my = input.mouseScreenY;
|
||||||
@@ -1720,6 +1761,15 @@ public class TerrainEditorState extends BaseAppState {
|
|||||||
contactPoint = null;
|
contactPoint = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (layer == SharedInput.LAYER_TERRAIN_GRASS) {
|
||||||
|
contactPoint = raycastSurface(ray);
|
||||||
|
if (contactPoint != null) {
|
||||||
|
brushRadius = (float) input.terrainGrassTool.brushRadius.getValue();
|
||||||
|
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||||
|
if (ves != null && ves.terrainTypeAt(contactPoint.x, contactPoint.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) {
|
||||||
|
contactPoint = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contactPoint != null) {
|
if (contactPoint != null) {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package de.blight.editor.tool;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class TerrainColorGrassTool extends EditorTool {
|
||||||
|
|
||||||
|
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 5.0, 1.0, 50.0);
|
||||||
|
public final ToolParameter bladeHeight = new ToolParameter("Halmhöhe", 0.6, 0.1, 2.0);
|
||||||
|
public final ToolParameter density = new ToolParameter("Dichte", 50.0, 1.0, 100.0);
|
||||||
|
public final ToolParameter uniformity = new ToolParameter("Gleichmäßigkeit", 1.0, 0.0, 1.0);
|
||||||
|
public final ToolParameter seedDensity = new ToolParameter("Samen %", 0.0, 0.0, 100.0);
|
||||||
|
|
||||||
|
@Override public String getName() { return "Terrain-Gras"; }
|
||||||
|
|
||||||
|
@Override public List<ChoiceToolParameter> getChoiceParameters() { return List.of(); }
|
||||||
|
|
||||||
|
@Override public List<ToolParameter> getParameters() {
|
||||||
|
return List.of(brushRadius, bladeHeight, density, uniformity, seedDensity);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import com.jme3.texture.Texture;
|
|||||||
import com.jme3.util.BufferUtils;
|
import com.jme3.util.BufferUtils;
|
||||||
import de.blight.common.GrassVertexBlade;
|
import de.blight.common.GrassVertexBlade;
|
||||||
import de.blight.common.GrassVertexIO;
|
import de.blight.common.GrassVertexIO;
|
||||||
|
import de.blight.common.MapData;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -50,7 +51,8 @@ public class GrassVertexRenderState extends BaseAppState
|
|||||||
private static final int BLADES_PER_TUFT = 3;
|
private static final int BLADES_PER_TUFT = 3;
|
||||||
private static final int SEGMENTS = 5;
|
private static final int SEGMENTS = 5;
|
||||||
private static final float WIDTH_FACTOR = 0.10f;
|
private static final float WIDTH_FACTOR = 0.10f;
|
||||||
private static final float BEND_FACTOR = 0.15f;
|
private static final float BEND_FACTOR = 0.15f;
|
||||||
|
private static final float TERRAIN_DRYNESS_MAX = 0.7f; // max Dryness-Boost bei nicht-grünem Untergrund
|
||||||
private static final ColorRGBA ROOT_COLOR = new ColorRGBA(0.08f, 0.34f, 0.04f, 1f);
|
private static final ColorRGBA ROOT_COLOR = new ColorRGBA(0.08f, 0.34f, 0.04f, 1f);
|
||||||
private static final ColorRGBA TIP_COLOR = new ColorRGBA(0.26f, 0.72f, 0.11f, 1f);
|
private static final ColorRGBA TIP_COLOR = new ColorRGBA(0.26f, 0.72f, 0.11f, 1f);
|
||||||
// 0–50 % Trockenheit: Grün → Goldgelb
|
// 0–50 % Trockenheit: Grün → Goldgelb
|
||||||
@@ -69,6 +71,7 @@ public class GrassVertexRenderState extends BaseAppState
|
|||||||
// ── Zustand ───────────────────────────────────────────────────────────────
|
// ── Zustand ───────────────────────────────────────────────────────────────
|
||||||
private final TerrainChunkState terrainChunkState;
|
private final TerrainChunkState terrainChunkState;
|
||||||
private AssetManager assetManager;
|
private AssetManager assetManager;
|
||||||
|
private float[][] slotAvgColors; // [12][3] — gemittelte RGB (0..1) pro Terrain-Textur-Slot
|
||||||
private Node grassNode;
|
private Node grassNode;
|
||||||
private Material material;
|
private Material material;
|
||||||
private Material[] seedMaterials = new Material[0];
|
private Material[] seedMaterials = new Material[0];
|
||||||
@@ -104,6 +107,7 @@ public class GrassVertexRenderState extends BaseAppState
|
|||||||
material = buildMaterial();
|
material = buildMaterial();
|
||||||
seedMaterials = loadSeedMaterials();
|
seedMaterials = loadSeedMaterials();
|
||||||
seedStalkMaterial = buildSeedStalkMaterial();
|
seedStalkMaterial = buildSeedStalkMaterial();
|
||||||
|
initSlotColors();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Material buildSeedStalkMaterial() {
|
private Material buildSeedStalkMaterial() {
|
||||||
@@ -238,6 +242,131 @@ public class GrassVertexRenderState extends BaseAppState
|
|||||||
return cz * CHUNKS_PER_AXIS + cx;
|
return cz * CHUNKS_PER_AXIS + cx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Terrain-Tint ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private void initSlotColors() {
|
||||||
|
slotAvgColors = new float[12][3];
|
||||||
|
MapData md = terrainChunkState.getMapData();
|
||||||
|
if (md == null) {
|
||||||
|
for (float[] c : slotAvgColors) { c[0] = 1f; c[1] = 1f; c[2] = 1f; }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String[] paths = new String[12];
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
paths[i] = (md.terrainTextures != null && md.terrainTextures.length > i) ? md.terrainTextures[i] : "";
|
||||||
|
paths[i+4] = (md.upperTextures != null && md.upperTextures.length > i) ? md.upperTextures[i] : "";
|
||||||
|
paths[i+8] = (md.thirdTextures != null && md.thirdTextures.length > i) ? md.thirdTextures[i] : "";
|
||||||
|
}
|
||||||
|
for (int s = 0; s < 12; s++) {
|
||||||
|
slotAvgColors[s] = sampleAvgColor(paths[s]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float[] sampleAvgColor(String path) {
|
||||||
|
float[] neutral = {1f, 1f, 1f};
|
||||||
|
if (path == null || path.isEmpty()) return neutral;
|
||||||
|
try (java.io.InputStream is = getClass().getClassLoader().getResourceAsStream(path)) {
|
||||||
|
if (is == null) return neutral;
|
||||||
|
java.awt.image.BufferedImage bi = javax.imageio.ImageIO.read(is);
|
||||||
|
if (bi == null) return neutral;
|
||||||
|
int w = bi.getWidth(), h = bi.getHeight();
|
||||||
|
float rSum = 0, gSum = 0, bSum = 0;
|
||||||
|
int samples = 0;
|
||||||
|
for (int gx = 0; gx < 4; gx++) {
|
||||||
|
for (int gz = 0; gz < 4; gz++) {
|
||||||
|
int px = (int)((gx + 0.5f) / 4f * w);
|
||||||
|
int pz = (int)((gz + 0.5f) / 4f * h);
|
||||||
|
int rgb = bi.getRGB(px, pz);
|
||||||
|
rSum += ((rgb >> 16) & 0xFF) / 255f;
|
||||||
|
gSum += ((rgb >> 8) & 0xFF) / 255f;
|
||||||
|
bSum += ( rgb & 0xFF) / 255f;
|
||||||
|
samples++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (samples == 0) return neutral;
|
||||||
|
return new float[]{rSum / samples, gSum / samples, bSum / samples};
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[GrassVertexRenderState] Textur-Sampling fehlgeschlagen für '{}': {}", path, e.getMessage());
|
||||||
|
return neutral;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float[] computeTerrainTint(float worldX, float worldZ) {
|
||||||
|
// tint[5]: [r, g, b, colorBlend, drynessBoost]
|
||||||
|
// colorBlend > 0: grüner Untergrund → Farbton übernehmen
|
||||||
|
// drynessBoost > 0: nicht-grüner Untergrund → Gras trockener darstellen
|
||||||
|
float[] noTint = {0f, 0f, 0f, 0f, 0f};
|
||||||
|
MapData md = terrainChunkState.getMapData();
|
||||||
|
if (md == null) return noTint;
|
||||||
|
int size = MapData.SPLAT_SIZE;
|
||||||
|
int px = Math.max(0, Math.min(size - 1, Math.round((worldX + 1024f) / 2f)));
|
||||||
|
int pz = Math.max(0, Math.min(size - 1, (size - 1) - Math.round((worldZ + 1024f) / 2f)));
|
||||||
|
int idx = pz * size + px;
|
||||||
|
|
||||||
|
int maxOverlay = Math.max(
|
||||||
|
Math.max(md.upperSplatR[idx] & 0xFF, md.upperSplatG[idx] & 0xFF),
|
||||||
|
Math.max(
|
||||||
|
Math.max(md.upperSplatB[idx] & 0xFF, md.upperSplatA[idx] & 0xFF),
|
||||||
|
Math.max(
|
||||||
|
Math.max(md.thirdSplatR[idx] & 0xFF, md.thirdSplatG[idx] & 0xFF),
|
||||||
|
Math.max(md.thirdSplatB[idx] & 0xFF, md.thirdSplatA[idx] & 0xFF)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
float baseScale = Math.max(0f, (255 - maxOverlay) / 255f);
|
||||||
|
int splatR = md.splatR[idx] & 0xFF;
|
||||||
|
if (splatR == 0) splatR = 255;
|
||||||
|
|
||||||
|
float[] slotW = {
|
||||||
|
splatR * baseScale / 255f,
|
||||||
|
(md.splatG[idx] & 0xFF) * baseScale / 255f,
|
||||||
|
(md.splatB[idx] & 0xFF) * baseScale / 255f,
|
||||||
|
(md.splatA[idx] & 0xFF) * baseScale / 255f,
|
||||||
|
(md.upperSplatR[idx] & 0xFF) / 255f,
|
||||||
|
(md.upperSplatG[idx] & 0xFF) / 255f,
|
||||||
|
(md.upperSplatB[idx] & 0xFF) / 255f,
|
||||||
|
(md.upperSplatA[idx] & 0xFF) / 255f,
|
||||||
|
(md.thirdSplatR[idx] & 0xFF) / 255f,
|
||||||
|
(md.thirdSplatG[idx] & 0xFF) / 255f,
|
||||||
|
(md.thirdSplatB[idx] & 0xFF) / 255f,
|
||||||
|
(md.thirdSplatA[idx] & 0xFF) / 255f,
|
||||||
|
};
|
||||||
|
|
||||||
|
float rSum = 0, gSum = 0, bSum = 0, wSum = 0;
|
||||||
|
for (int s = 0; s < 12; s++) {
|
||||||
|
if (slotW[s] <= 0f) continue;
|
||||||
|
rSum += slotAvgColors[s][0] * slotW[s];
|
||||||
|
gSum += slotAvgColors[s][1] * slotW[s];
|
||||||
|
bSum += slotAvgColors[s][2] * slotW[s];
|
||||||
|
wSum += slotW[s];
|
||||||
|
}
|
||||||
|
if (wSum <= 0f) return noTint;
|
||||||
|
float r = rSum / wSum, g = gSum / wSum, b = bSum / wSum;
|
||||||
|
|
||||||
|
// Hue berechnen
|
||||||
|
float cmax = Math.max(r, Math.max(g, b));
|
||||||
|
float cmin = Math.min(r, Math.min(g, b));
|
||||||
|
float delta = cmax - cmin;
|
||||||
|
if (delta < 1e-6f || cmax < 0.05f) return noTint;
|
||||||
|
float sat = delta / cmax;
|
||||||
|
if (sat < 0.08f) return noTint;
|
||||||
|
float hue;
|
||||||
|
if (cmax == r) hue = 60f * (((g - b) / delta) % 6f);
|
||||||
|
else if (cmax == g) hue = 60f * ((b - r) / delta + 2f);
|
||||||
|
else hue = 60f * ((r - g) / delta + 4f);
|
||||||
|
if (hue < 0f) hue += 360f;
|
||||||
|
|
||||||
|
if (hue >= 80f && hue <= 155f) {
|
||||||
|
// Grüner Untergrund → Farbton des Terrains übernehmen
|
||||||
|
return new float[]{r, g, b, 0.45f, 0f};
|
||||||
|
} else if (hue >= 20f && hue < 80f) {
|
||||||
|
// Gelb-/Braunton → je weiter von Grün entfernt, desto trockener
|
||||||
|
float drynessBoost = (80f - hue) / 60f * TERRAIN_DRYNESS_MAX;
|
||||||
|
return new float[]{0f, 0f, 0f, 0f, drynessBoost};
|
||||||
|
}
|
||||||
|
return noTint;
|
||||||
|
}
|
||||||
|
|
||||||
private void buildChunk(int ci) {
|
private void buildChunk(int ci) {
|
||||||
List<GrassVertexBlade> blades = chunkBlades[ci];
|
List<GrassVertexBlade> blades = chunkBlades[ci];
|
||||||
if (blades.isEmpty()) return;
|
if (blades.isEmpty()) return;
|
||||||
@@ -254,7 +383,10 @@ public class GrassVertexRenderState extends BaseAppState
|
|||||||
|
|
||||||
int vi = 0, ii = 0;
|
int vi = 0, ii = 0;
|
||||||
for (GrassVertexBlade blade : blades) {
|
for (GrassVertexBlade blade : blades) {
|
||||||
buildTuft(positions, normals, colors, texCoords, indices, vi, ii, blade);
|
float[] tint = (slotAvgColors != null)
|
||||||
|
? computeTerrainTint(blade.x(), blade.z())
|
||||||
|
: new float[]{0f, 0f, 0f, 0f, 0f};
|
||||||
|
buildTuft(positions, normals, colors, texCoords, indices, vi, ii, blade, tint);
|
||||||
vi += BLADES_PER_TUFT * (SEGMENTS + 1) * 2;
|
vi += BLADES_PER_TUFT * (SEGMENTS + 1) * 2;
|
||||||
ii += BLADES_PER_TUFT * SEGMENTS * 6;
|
ii += BLADES_PER_TUFT * SEGMENTS * 6;
|
||||||
}
|
}
|
||||||
@@ -434,7 +566,7 @@ public class GrassVertexRenderState extends BaseAppState
|
|||||||
// ── Mesh-Logik (gespiegelt zu GrassVertexState) ───────────────────────────
|
// ── Mesh-Logik (gespiegelt zu GrassVertexState) ───────────────────────────
|
||||||
|
|
||||||
private static void buildTuft(float[] pos, float[] nrm, float[] col, float[] tex,
|
private static void buildTuft(float[] pos, float[] nrm, float[] col, float[] tex,
|
||||||
int[] idx, int vi, int ii, GrassVertexBlade blade) {
|
int[] idx, int vi, int ii, GrassVertexBlade blade, float[] tint) {
|
||||||
float x = blade.x(), y = blade.y(), z = blade.z(), h = blade.height();
|
float x = blade.x(), y = blade.y(), z = blade.z(), h = blade.height();
|
||||||
float baseHW = h * WIDTH_FACTOR * 0.5f;
|
float baseHW = h * WIDTH_FACTOR * 0.5f;
|
||||||
float tAngle = (float) (((x * 127.1f + z * 311.7f) % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2));
|
float tAngle = (float) (((x * 127.1f + z * 311.7f) % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2));
|
||||||
@@ -496,8 +628,8 @@ public class GrassVertexRenderState extends BaseAppState
|
|||||||
int svi = bladeVi + s * 2;
|
int svi = bladeVi + s * 2;
|
||||||
float dry = blade.dryness();
|
float dry = blade.dryness();
|
||||||
float bladePhase = hash(bx, bz) * 6.2832f;
|
float bladePhase = hash(bx, bz) * 6.2832f;
|
||||||
setV(pos, nrm, col, tex, svi, spX - cosA * hw, spY, spZ - sinA * hw, nx, ny, nz, t, bladePhase, dry);
|
setV(pos, nrm, col, tex, svi, spX - cosA * hw, spY, spZ - sinA * hw, nx, ny, nz, t, bladePhase, dry, tint);
|
||||||
setV(pos, nrm, col, tex, svi+1, spX + cosA * hw, spY, spZ + sinA * hw, nx, ny, nz, t, bladePhase, dry);
|
setV(pos, nrm, col, tex, svi+1, spX + cosA * hw, spY, spZ + sinA * hw, nx, ny, nz, t, bladePhase, dry, tint);
|
||||||
|
|
||||||
if (s < SEGMENTS) {
|
if (s < SEGMENTS) {
|
||||||
int sii = bladeIi + s * 6;
|
int sii = bladeIi + s * 6;
|
||||||
@@ -520,7 +652,7 @@ public class GrassVertexRenderState extends BaseAppState
|
|||||||
|
|
||||||
private static void setV(float[] pos, float[] nrm, float[] col, float[] tex, int vi,
|
private static void setV(float[] pos, float[] nrm, float[] col, float[] tex, int vi,
|
||||||
float x, float y, float z, float nx, float ny, float nz,
|
float x, float y, float z, float nx, float ny, float nz,
|
||||||
float wf, float bladePhase, float dryness) {
|
float wf, float bladePhase, float dryness, float[] tint) {
|
||||||
int pi = vi * 3;
|
int pi = vi * 3;
|
||||||
pos[pi] = x; pos[pi+1] = y; pos[pi+2] = z;
|
pos[pi] = x; pos[pi+1] = y; pos[pi+2] = z;
|
||||||
|
|
||||||
@@ -528,6 +660,8 @@ public class GrassVertexRenderState extends BaseAppState
|
|||||||
nrm[ni] = nx; nrm[ni+1] = ny; nrm[ni+2] = nz;
|
nrm[ni] = nx; nrm[ni+1] = ny; nrm[ni+2] = nz;
|
||||||
|
|
||||||
int ci = vi * 4;
|
int ci = vi * 4;
|
||||||
|
// Effektive Dryness: manueller Wert + Terrain-Boost (nicht-grüner Untergrund)
|
||||||
|
float effectiveDryness = Math.min(1f, dryness + tint[4]);
|
||||||
float gr = ROOT_COLOR.r + (TIP_COLOR.r - ROOT_COLOR.r) * wf;
|
float gr = ROOT_COLOR.r + (TIP_COLOR.r - ROOT_COLOR.r) * wf;
|
||||||
float gg = ROOT_COLOR.g + (TIP_COLOR.g - ROOT_COLOR.g) * wf;
|
float gg = ROOT_COLOR.g + (TIP_COLOR.g - ROOT_COLOR.g) * wf;
|
||||||
float gb = ROOT_COLOR.b + (TIP_COLOR.b - ROOT_COLOR.b) * wf;
|
float gb = ROOT_COLOR.b + (TIP_COLOR.b - ROOT_COLOR.b) * wf;
|
||||||
@@ -539,17 +673,27 @@ public class GrassVertexRenderState extends BaseAppState
|
|||||||
float vb = VERY_DRY_ROOT_COLOR.b + (VERY_DRY_TIP_COLOR.b - VERY_DRY_ROOT_COLOR.b) * wf;
|
float vb = VERY_DRY_ROOT_COLOR.b + (VERY_DRY_TIP_COLOR.b - VERY_DRY_ROOT_COLOR.b) * wf;
|
||||||
// Zwei-Segment-Gradient: 0→0.5 = grün→goldgelb, 0.5→1.0 = goldgelb→dunkelbraun
|
// Zwei-Segment-Gradient: 0→0.5 = grün→goldgelb, 0.5→1.0 = goldgelb→dunkelbraun
|
||||||
float fr, fg, fb;
|
float fr, fg, fb;
|
||||||
if (dryness <= 0.5f) {
|
if (effectiveDryness <= 0.5f) {
|
||||||
float t = dryness * 2f;
|
float t = effectiveDryness * 2f;
|
||||||
fr = gr + (dr - gr) * t;
|
fr = gr + (dr - gr) * t;
|
||||||
fg = gg + (dg - gg) * t;
|
fg = gg + (dg - gg) * t;
|
||||||
fb = gb + (db - gb) * t;
|
fb = gb + (db - gb) * t;
|
||||||
} else {
|
} else {
|
||||||
float t = (dryness - 0.5f) * 2f;
|
float t = (effectiveDryness - 0.5f) * 2f;
|
||||||
fr = dr + (vr - dr) * t;
|
fr = dr + (vr - dr) * t;
|
||||||
fg = dg + (vg - dg) * t;
|
fg = dg + (vg - dg) * t;
|
||||||
fb = db + (vb - db) * t;
|
fb = db + (vb - db) * t;
|
||||||
}
|
}
|
||||||
|
// Grüner Untergrund: Farbton des Terrains übernehmen (stärker an Wurzel)
|
||||||
|
if (tint[3] > 0f) {
|
||||||
|
float ts = tint[3] * (1f - wf * 0.5f);
|
||||||
|
fr = fr + (tint[0] - fr) * ts;
|
||||||
|
fg = fg + (tint[1] - fg) * ts;
|
||||||
|
fb = fb + (tint[2] - fb) * ts;
|
||||||
|
}
|
||||||
|
fr = Math.max(0f, Math.min(1f, fr));
|
||||||
|
fg = Math.max(0f, Math.min(1f, fg));
|
||||||
|
fb = Math.max(0f, Math.min(1f, fb));
|
||||||
col[ci] = fr; col[ci+1] = fg; col[ci+2] = fb; col[ci+3] = 1f;
|
col[ci] = fr; col[ci+1] = fg; col[ci+2] = fb; col[ci+3] = 1f;
|
||||||
|
|
||||||
int ti = vi * 2;
|
int ti = vi * 2;
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ public class TerrainChunkState extends BaseAppState {
|
|||||||
this.mapData = mapData;
|
this.mapData = mapData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MapData getMapData() { return mapData; }
|
||||||
|
|
||||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public void loadChunkHeights() {
|
public void loadChunkHeights() {
|
||||||
|
|||||||
Binary file not shown.
BIN
blight-map/src/main/map/blight_grass_terrain.blgv
Normal file
BIN
blight-map/src/main/map/blight_grass_terrain.blgv
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user