Malen von Gras erweitert

This commit is contained in:
2026-08-21 15:03:23 +02:00
parent 5d8e4db468
commit 03dcfb4187
15 changed files with 1055 additions and 41 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

View File

@@ -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;
}
}
}

View File

@@ -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(); }); }
} }
} }

View File

@@ -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;

View File

@@ -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);
// 050 % Trockenheit: Grün → Goldgelb // 050 % 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;

View File

@@ -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;
}
}

View File

@@ -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) {

View File

@@ -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);
}
}

View File

@@ -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);
// 050 % Trockenheit: Grün → Goldgelb // 050 % 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;

View File

@@ -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.