diff --git a/blight-assets/src/main/resources/Textures/ground/vegetation/rohrkolben.png b/blight-assets/src/main/resources/Textures/ground/vegetation/rohrkolben.png new file mode 100644 index 0000000..aaf3b1c Binary files /dev/null and b/blight-assets/src/main/resources/Textures/ground/vegetation/rohrkolben.png differ diff --git a/blight-common/src/main/java/de/blight/common/TerrainColorGrassIO.java b/blight-common/src/main/java/de/blight/common/TerrainColorGrassIO.java new file mode 100644 index 0000000..cdb23c7 --- /dev/null +++ b/blight-common/src/main/java/de/blight/common/TerrainColorGrassIO.java @@ -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 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 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 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; + } + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/EditorApp.java b/blight-editor/src/main/java/de/blight/editor/EditorApp.java index c620c62..af23aed 100644 --- a/blight-editor/src/main/java/de/blight/editor/EditorApp.java +++ b/blight-editor/src/main/java/de/blight/editor/EditorApp.java @@ -364,8 +364,9 @@ public class EditorApp extends Application { // Toolbar-Buttons (müssen vom Status-Poller erreichbar sein) private ToggleButton baseBtn; private ToggleButton grassBtn; // einziger Toolbar-Button für beide Gras-Tools - private ToggleButton grassSubTexBtn; // Sub-Toggle im Panel: Textur-Gras - private ToggleButton grassSubVertBtn; // Sub-Toggle im Panel: Vertex-Gras + private ToggleButton grassSubTexBtn; // Sub-Toggle im Panel: Textur-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 stoneBtn; private ToggleButton objPlaceBtn; // Unified Objekte-Button (Place + Edit) @@ -3571,28 +3572,33 @@ public class EditorApp extends Application { panel.getChildren().addAll(title, new Separator()); // Sub-Tool-Auswahl - grassSubTexBtn = new ToggleButton(":: Gras (Textur)"); - grassSubVertBtn = new ToggleButton("|| Gras (Vertices)"); + grassSubTexBtn = new ToggleButton(":: Textur"); + grassSubVertBtn = new ToggleButton("|| Vertex"); + grassSubTerrainBtn = new ToggleButton("~~ Terrain"); grassSubTexBtn.setMaxWidth(Double.MAX_VALUE); grassSubVertBtn.setMaxWidth(Double.MAX_VALUE); + grassSubTerrainBtn.setMaxWidth(Double.MAX_VALUE); ToggleGroup subGroup = new ToggleGroup(); grassSubTexBtn.setToggleGroup(subGroup); grassSubVertBtn.setToggleGroup(subGroup); + grassSubTerrainBtn.setToggleGroup(subGroup); subGroup.selectedToggleProperty().addListener((obs, o, n) -> { if (n == null) subGroup.selectToggle(o); }); - HBox subRow = new HBox(4, grassSubTexBtn, grassSubVertBtn); - HBox.setHgrow(grassSubTexBtn, Priority.ALWAYS); - HBox.setHgrow(grassSubVertBtn, Priority.ALWAYS); + HBox subRow = new HBox(4, grassSubTexBtn, grassSubVertBtn, grassSubTerrainBtn); + HBox.setHgrow(grassSubTexBtn, Priority.ALWAYS); + HBox.setHgrow(grassSubVertBtn, Priority.ALWAYS); + HBox.setHgrow(grassSubTerrainBtn, Priority.ALWAYS); panel.getChildren().add(subRow); // Parameter-Bereich (wird beim Sub-Tool-Wechsel ersetzt) VBox paramsBox = new VBox(10); panel.getChildren().add(paramsBox); - // Globale Farben - panel.getChildren().add(new Separator()); + // Globale Farben – nur für Textur-Gras und Vertex-Gras sichtbar + VBox colorSection = new VBox(6); + colorSection.getChildren().add(new Separator()); Label colorTitle = new Label("Globale Farben"); 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.paint.Color.color(input.grassFreshR, input.grassFreshG, input.grassFreshB)); @@ -3616,9 +3622,36 @@ public class EditorApp extends Application { input.grassColorsChanged = true; }); - panel.getChildren().addAll( - new Label("Unvertrocknet:"), freshPicker, - new Label("Vertrocknet:"), dryPicker); + Button pickFromTerrainBtn = new Button("[ ] Farbe vom Boden picken"); + pickFromTerrainBtn.setMaxWidth(Double.MAX_VALUE); + 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 handler = + new javafx.event.EventHandler() { + @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 Runnable activateTex = () -> { @@ -3628,6 +3661,8 @@ public class EditorApp extends Application { VBox inner = new VBox(10); showToolParameters(inner, input.activeTool); paramsBox.getChildren().add(inner); + colorSection.setVisible(true); + colorSection.setManaged(true); }; Runnable activateVert = () -> { input.activeLayer = SharedInput.LAYER_GRASS_VERTEX; @@ -3636,15 +3671,31 @@ public class EditorApp extends Application { VBox inner = new VBox(10); showToolParameters(inner, input.activeTool); 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()); - grassSubVertBtn.setOnAction(e -> activateVert.run()); + grassSubTexBtn.setOnAction(e -> activateTex.run()); + grassSubVertBtn.setOnAction(e -> activateVert.run()); + grassSubTerrainBtn.setOnAction(e -> activateTerrain.run()); // Standard: zuletzt aktives Sub-Tool if (input.activeLayer == SharedInput.LAYER_GRASS_VERTEX) { grassSubVertBtn.setSelected(true); activateVert.run(); + } else if (input.activeLayer == SharedInput.LAYER_TERRAIN_GRASS) { + grassSubTerrainBtn.setSelected(true); + activateTerrain.run(); } else { grassSubTexBtn.setSelected(true); activateTex.run(); @@ -8822,8 +8873,14 @@ public class EditorApp extends Application { switch (input.activeLayer) { 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 SharedInput.LAYER_GRASS_VERTEX -> + case SharedInput.LAYER_GRASS_VERTEX -> { 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 -> { SharedInput.TextureEdit te = new SharedInput.TextureEdit((float) x, (float) y, action); 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 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 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(); }); } } } diff --git a/blight-editor/src/main/java/de/blight/editor/SharedInput.java b/blight-editor/src/main/java/de/blight/editor/SharedInput.java index 7e6cfa7..1adba0c 100644 --- a/blight-editor/src/main/java/de/blight/editor/SharedInput.java +++ b/blight-editor/src/main/java/de/blight/editor/SharedInput.java @@ -3,6 +3,7 @@ package de.blight.editor; import de.blight.editor.tool.EditorTool; import de.blight.editor.tool.GrassTool; import de.blight.editor.tool.GrassVertexTool; +import de.blight.editor.tool.TerrainColorGrassTool; import de.blight.editor.tool.HeightTool; import de.blight.editor.tool.HoleTool; import de.blight.editor.tool.StoneTool; @@ -25,7 +26,8 @@ public class SharedInput { public final HeightTool heightTool = new HeightTool(); public final UpperHeightTool upperHeightTool = new UpperHeightTool(); 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 HoleTool holeTool = new HoleTool(); public final VoxelTool voxelTool = new VoxelTool(); @@ -92,10 +94,16 @@ public class SharedInput { // ── Gras (Vertices) ─────────────────────────────────────────────────────── /** 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 final ConcurrentLinkedQueue grassVertexEditQueue = new ConcurrentLinkedQueue<>(); + public final ConcurrentLinkedQueue grassVertexEditQueue = new ConcurrentLinkedQueue<>(); + public final ConcurrentLinkedQueue terrainGrassEditQueue = new ConcurrentLinkedQueue<>(); + /** Einzelner Klick zum Terrain-Farb-Sampling (Dropper) für Vertex-Gras Tool 1 */ + public final ConcurrentLinkedQueue grassTerrainPickQueue = new ConcurrentLinkedQueue<>(); + /** Callback: JME-Thread setzt Farbe, EditorApp leitet sie via Platform.runLater an den Picker weiter */ + public volatile java.util.function.Consumer grassTerrainPickCallback = null; /** Globale Grasfarbe – frisches Gras (Spitze; Wurzel wird intern abgeleitet). Defaults: TIP_COLOR */ public volatile float grassFreshR = 0.26f, grassFreshG = 0.72f, grassFreshB = 0.11f; diff --git a/blight-editor/src/main/java/de/blight/editor/state/GrassVertexState.java b/blight-editor/src/main/java/de/blight/editor/state/GrassVertexState.java index 140aee4..9e28b1d 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/GrassVertexState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/GrassVertexState.java @@ -22,6 +22,7 @@ import com.jme3.texture.Texture; import com.jme3.util.BufferUtils; import de.blight.common.GrassVertexBlade; import de.blight.common.GrassVertexIO; +import de.blight.common.MapData; import de.blight.editor.SharedInput; 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 SEGMENTS = 5; // Segmente pro Halm (5 → 6 Reihen) 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 TIP_COLOR = new ColorRGBA(0.26f, 0.72f, 0.11f, 1f); // 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 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) { this.input = input; 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 List getAllBlades() { @@ -417,7 +421,7 @@ public class GrassVertexState extends BaseAppState { for (GrassVertexBlade blade : blades) { buildTuft(positions, normals, colors, texCoords, indices, vi, ii, blade, 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; ii += BLADES_PER_TUFT * SEGMENTS * 6; } @@ -580,7 +584,8 @@ public class GrassVertexState extends BaseAppState { float frshRootR, float frshRootG, float frshRootB, float frshTipR, float frshTipG, float frshTipB, 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 baseHW = h * WIDTH_FACTOR * 0.5f; 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(); setV(pos, nrm, col, tex, svi, spX - cosA * hw, spY, spZ - sinA * hw, nx, ny, nz, t, dry, 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, frshRootR, frshRootG, frshRootB, frshTipR, frshTipG, frshTipB, - dryRootR, dryRootG, dryRootB, dryTipR, dryTipG, dryTipB); + dryRootR, dryRootG, dryRootB, dryTipR, dryTipG, dryTipB, tint); if (s < SEGMENTS) { int sii = bladeIi + s * 6; @@ -679,7 +684,8 @@ public class GrassVertexState extends BaseAppState { float frshRootR, float frshRootG, float frshRootB, float frshTipR, float frshTipG, float frshTipB, float dryRootR, float dryRootG, float dryRootB, - float dryTipR, float dryTipG, float dryTipB) { + float dryTipR, float dryTipG, float dryTipB, + float[] tint) { int pi = vi * 3; 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 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 effectiveDryness = Math.min(1f, dryness + tint[4]); float fr, fg, fb; - if (dryness <= 0.5f) { - float t = dryness * 2f; + if (effectiveDryness <= 0.5f) { + float t = effectiveDryness * 2f; fr = gr + (dr - gr) * t; fg = gg + (dg - gg) * t; fb = gb + (db - gb) * t; } else { - float t = (dryness - 0.5f) * 2f; + float t = (effectiveDryness - 0.5f) * 2f; fr = dr + (vr - dr) * t; fg = dg + (vg - dg) * 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; int ti = vi * 2; diff --git a/blight-editor/src/main/java/de/blight/editor/state/TerrainColorGrassState.java b/blight-editor/src/main/java/de/blight/editor/state/TerrainColorGrassState.java new file mode 100644 index 0000000..441d225 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/state/TerrainColorGrassState.java @@ -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[] 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 getAllBlades() { + List all = new ArrayList<>(); + for (List 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 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 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 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 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 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 seededAll = new ArrayList<>(); + Map> 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> 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 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 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; + } +} diff --git a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java index 8f47a5b..2478827 100644 --- a/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java +++ b/blight-editor/src/main/java/de/blight/editor/state/TerrainEditorState.java @@ -98,7 +98,8 @@ public class TerrainEditorState extends BaseAppState { private Geometry brushIndicator; private Geometry waterGeo; private PlacedObjectState placedObjectState; - private GrassVertexState grassVertexState; + private GrassVertexState grassVertexState; + private TerrainColorGrassState terrainColorGrassState; private StoneEditorState stoneEditorState; private SceneObjectState sceneObjState; private ItemPlacementState itemPlacementState; @@ -253,6 +254,7 @@ public class TerrainEditorState extends BaseAppState { // ── Szene aufbauen ──────────────────────────────────────────────────────── private void buildScene() { + MapData mapDataForGrass = loadedMapData; // sichern bevor buildTerrain()/initSplatmap() es auf null setzt input.loadingStatus = "Lade Terrain..."; terrain = buildTerrain(); cachedHeightMap = terrain.getHeightMap(); @@ -266,8 +268,15 @@ public class TerrainEditorState extends BaseAppState { input.loadingStatus = "Lade Vertex-Gras..."; grassVertexState = new GrassVertexState(input); grassVertexState.setTerrain(terrain); + grassVertexState.setMapData(mapDataForGrass); 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..."; stoneEditorState = new StoneEditorState(input); stoneEditorState.setTerrain(terrain); @@ -1222,6 +1231,7 @@ public class TerrainEditorState extends BaseAppState { processEdits(); processTextureEdits(); updateBrushIndicator(); + processGrassTerrainPick(); updateAxesGizmo(); // Debug: Strg+F8 — Raw-Texture-Modus (kein Lighting) ein/aus if (input.debugNoLightToggle) { @@ -1411,6 +1421,8 @@ public class TerrainEditorState extends BaseAppState { /** Gibt den TerrainQuad-Node zurück (z.B. für Voxel-Raycasts). */ public TerrainQuad getTerrainNode() { return terrain; } + public MapData getMapData() { return loadedMapData; } + /** Gibt das Terrain-Material zurück (für BakedTerrain-Material-Aufbau in anderen States). */ public com.jme3.material.Material getTerrainMaterial() { return terrainMat; } @@ -1534,7 +1546,8 @@ public class TerrainEditorState extends BaseAppState { final GrassTuftIO.GrassData grassData = placedObjectState != null ? new GrassTuftIO.GrassData(placedObjectState.getSlotPaths(), placedObjectState.getAllTufts()) : 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 models = sceneObjState != null ? sceneObjState.getPlacedModels() : null; final List lights = lightState != null ? lightState.getPlacedLights() : null; final List emitters = emitterState != null ? emitterState.getPlacedEmitters() : null; @@ -1635,6 +1648,10 @@ public class TerrainEditorState extends BaseAppState { try { GrassVertexIO.save(grassVertexBlades); } 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); if (heightSnap != null) { try { @@ -1661,6 +1678,30 @@ public class TerrainEditorState extends BaseAppState { // ── 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 cb = input.grassTerrainPickCallback; + if (cb != null) { + input.grassTerrainPickCallback = null; + javafx.application.Platform.runLater(() -> cb.accept(color)); + } + } + } + private void updateBrushIndicator() { float mx = input.mouseScreenX; float my = input.mouseScreenY; @@ -1720,6 +1761,15 @@ public class TerrainEditorState extends BaseAppState { 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) { diff --git a/blight-editor/src/main/java/de/blight/editor/tool/TerrainColorGrassTool.java b/blight-editor/src/main/java/de/blight/editor/tool/TerrainColorGrassTool.java new file mode 100644 index 0000000..b1abe64 --- /dev/null +++ b/blight-editor/src/main/java/de/blight/editor/tool/TerrainColorGrassTool.java @@ -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 getChoiceParameters() { return List.of(); } + + @Override public List getParameters() { + return List.of(brushRadius, bladeHeight, density, uniformity, seedDensity); + } +} diff --git a/blight-game/src/main/java/de/blight/game/state/GrassVertexRenderState.java b/blight-game/src/main/java/de/blight/game/state/GrassVertexRenderState.java index 1bbd62d..23b6ebc 100644 --- a/blight-game/src/main/java/de/blight/game/state/GrassVertexRenderState.java +++ b/blight-game/src/main/java/de/blight/game/state/GrassVertexRenderState.java @@ -20,6 +20,7 @@ import com.jme3.texture.Texture; import com.jme3.util.BufferUtils; import de.blight.common.GrassVertexBlade; import de.blight.common.GrassVertexIO; +import de.blight.common.MapData; import org.slf4j.Logger; 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 SEGMENTS = 5; 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 TIP_COLOR = new ColorRGBA(0.26f, 0.72f, 0.11f, 1f); // 0–50 % Trockenheit: Grün → Goldgelb @@ -69,6 +71,7 @@ public class GrassVertexRenderState extends BaseAppState // ── Zustand ─────────────────────────────────────────────────────────────── private final TerrainChunkState terrainChunkState; private AssetManager assetManager; + private float[][] slotAvgColors; // [12][3] — gemittelte RGB (0..1) pro Terrain-Textur-Slot private Node grassNode; private Material material; private Material[] seedMaterials = new Material[0]; @@ -104,6 +107,7 @@ public class GrassVertexRenderState extends BaseAppState material = buildMaterial(); seedMaterials = loadSeedMaterials(); seedStalkMaterial = buildSeedStalkMaterial(); + initSlotColors(); } private Material buildSeedStalkMaterial() { @@ -238,6 +242,131 @@ public class GrassVertexRenderState extends BaseAppState 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) { List blades = chunkBlades[ci]; if (blades.isEmpty()) return; @@ -254,7 +383,10 @@ public class GrassVertexRenderState extends BaseAppState int vi = 0, ii = 0; 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; ii += BLADES_PER_TUFT * SEGMENTS * 6; } @@ -434,7 +566,7 @@ public class GrassVertexRenderState extends BaseAppState // ── Mesh-Logik (gespiegelt zu GrassVertexState) ─────────────────────────── 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 baseHW = h * WIDTH_FACTOR * 0.5f; 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; float dry = blade.dryness(); 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+1, 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, tint); if (s < SEGMENTS) { 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, 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; 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; 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 gg = ROOT_COLOR.g + (TIP_COLOR.g - ROOT_COLOR.g) * 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; // Zwei-Segment-Gradient: 0→0.5 = grün→goldgelb, 0.5→1.0 = goldgelb→dunkelbraun float fr, fg, fb; - if (dryness <= 0.5f) { - float t = dryness * 2f; + if (effectiveDryness <= 0.5f) { + float t = effectiveDryness * 2f; fr = gr + (dr - gr) * t; fg = gg + (dg - gg) * t; fb = gb + (db - gb) * t; } else { - float t = (dryness - 0.5f) * 2f; + float t = (effectiveDryness - 0.5f) * 2f; fr = dr + (vr - dr) * t; fg = dg + (vg - dg) * 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; int ti = vi * 2; diff --git a/blight-game/src/main/java/de/blight/game/state/TerrainChunkState.java b/blight-game/src/main/java/de/blight/game/state/TerrainChunkState.java index 5b0193a..cfa2109 100644 --- a/blight-game/src/main/java/de/blight/game/state/TerrainChunkState.java +++ b/blight-game/src/main/java/de/blight/game/state/TerrainChunkState.java @@ -98,6 +98,8 @@ public class TerrainChunkState extends BaseAppState { this.mapData = mapData; } + public MapData getMapData() { return mapData; } + // ── Lifecycle ───────────────────────────────────────────────────────────── public void loadChunkHeights() { diff --git a/blight-map/src/main/map/blight_grass.blg b/blight-map/src/main/map/blight_grass.blg index 940b068..534956b 100644 Binary files a/blight-map/src/main/map/blight_grass.blg and b/blight-map/src/main/map/blight_grass.blg differ diff --git a/blight-map/src/main/map/blight_grass_terrain.blgv b/blight-map/src/main/map/blight_grass_terrain.blgv new file mode 100644 index 0000000..468ee0c Binary files /dev/null and b/blight-map/src/main/map/blight_grass_terrain.blgv differ diff --git a/blight-map/src/main/map/blight_grass_vertex.blgv b/blight-map/src/main/map/blight_grass_vertex.blgv index 66b9cfe..0155c6e 100644 Binary files a/blight-map/src/main/map/blight_grass_vertex.blgv and b/blight-map/src/main/map/blight_grass_vertex.blgv differ diff --git a/blight-map/src/main/map/blight_map.blm b/blight-map/src/main/map/blight_map.blm index 44be15d..f97fd36 100644 Binary files a/blight-map/src/main/map/blight_map.blm and b/blight-map/src/main/map/blight_map.blm differ diff --git a/blight-map/src/main/map/blight_voxel_splat.bin b/blight-map/src/main/map/blight_voxel_splat.bin index 6d258c0..adce6f4 100644 Binary files a/blight-map/src/main/map/blight_voxel_splat.bin and b/blight-map/src/main/map/blight_voxel_splat.bin differ