Malen von Gras erweitert
This commit is contained in:
@@ -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<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;
|
||||
|
||||
@@ -98,6 +98,8 @@ public class TerrainChunkState extends BaseAppState {
|
||||
this.mapData = mapData;
|
||||
}
|
||||
|
||||
public MapData getMapData() { return mapData; }
|
||||
|
||||
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
||||
|
||||
public void loadChunkHeights() {
|
||||
|
||||
Reference in New Issue
Block a user