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

View File

@@ -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);
// 050 % 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<GrassVertexBlade> 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;

View File

@@ -98,6 +98,8 @@ public class TerrainChunkState extends BaseAppState {
this.mapData = mapData;
}
public MapData getMapData() { return mapData; }
// ── Lifecycle ─────────────────────────────────────────────────────────────
public void loadChunkHeights() {