Browse Source

apply format

djeada 1 month ago
parent
commit
678105c32a

+ 1 - 1
app/core/game_engine.cpp

@@ -98,7 +98,7 @@ GameEngine::GameEngine() {
   m_pine = std::make_unique<Render::GL::PineRenderer>();
 
   m_passes = {m_ground.get(), m_terrain.get(), m_biome.get(), m_stone.get(),
-              m_plant.get(), m_pine.get(), m_fog.get()};
+              m_plant.get(),  m_pine.get(),    m_fog.get()};
 
   std::unique_ptr<Engine::Core::System> arrowSys =
       std::make_unique<Game::Systems::ArrowSystem>();

+ 52 - 58
assets/shaders/pine_instanced.frag

@@ -17,65 +17,59 @@ const float TWO_PI = 6.28318530718;
 
 // Hash-based noise (cheap, repeatable)
 float hash(vec2 p) {
-    return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
+  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
 }
 
 void main() {
-    // --- Lighting ---
-    vec3 n = normalize(vNormal);
-    vec3 l = normalize(uLightDirection);
-    float diffuse = max(dot(n, l), 0.0);
-    float ambient = 0.4;
-    float lighting = ambient + diffuse * 0.7;
-
-    // --- Foliage (needles) variation ---
-    float needleNoise = hash(vec2(
-        vTexCoord.x * 28.0 + vNeedleSeed * 7.1,
-        vTexCoord.y * 24.0 + vNeedleSeed * 5.3
-    ));
-
-    // vertical streaking; quantize Y to bands
-    float needleStreak = hash(vec2(
-        vTexCoord.x * 12.0 + vNeedleSeed * 3.7,
-        floor(vTexCoord.y * 6.0 + vNeedleSeed * 2.0)
-    ));
-
-    vec3 needleColor = vColor * (0.78 + needleNoise * 0.28);
-    needleColor += vec3(0.02, 0.05, 0.02) * needleStreak;
-
-    // Subtle brightening toward tips (top of UVs)
-    float tipBlend = smoothstep(0.82, 1.02, vTexCoord.y);
-    needleColor = mix(needleColor, needleColor * vec3(1.08, 1.04, 1.10), tipBlend);
-
-    // --- Bark variation: stripes + noise ---
-    float barkStripe = sin(vTexCoord.y * 45.0 + vBarkSeed * TWO_PI) * 0.1 + 0.9;
-    float barkNoise  = hash(vec2(
-        vTexCoord.x * 18.0 + vBarkSeed * 4.3,
-        vTexCoord.y * 10.0 + vBarkSeed * 7.7
-    ));
-
-    vec3 trunkBase  = vec3(0.32, 0.24, 0.16) * barkStripe;
-    vec3 trunkColor = trunkBase * (0.85 + barkNoise * 0.35);
-
-    // --- Final color ---
-    vec3 baseColor = mix(trunkColor, needleColor, vFoliageMask);
-    vec3 color = baseColor * lighting;
-
-    // --- Alpha shaping (silhouette for foliage + bottom fade) ---
-    float silhouetteNoise = hash(vec2(
-        vTexCoord.x * 30.0 + vNeedleSeed * 9.0,
-        vTexCoord.y * 40.0 + vNeedleSeed * 5.5
-    ));
-
-    float alphaFoliage = 0.70 + silhouetteNoise * 0.25;
-    float alpha = mix(1.0, alphaFoliage, vFoliageMask);
-
-    // fade near bottom (ground contact)
-    alpha *= smoothstep(0.00, 0.05, vTexCoord.y);
-
-    // clamp and alpha test
-    alpha = clamp(alpha, 0.0, 1.0);
-    if (alpha < 0.06) discard;
-
-    FragColor = vec4(color, alpha);
+  // --- Lighting ---
+  vec3 n = normalize(vNormal);
+  vec3 l = normalize(uLightDirection);
+  float diffuse = max(dot(n, l), 0.0);
+  float ambient = 0.4;
+  float lighting = ambient + diffuse * 0.7;
+
+  // --- Foliage (needles) variation ---
+  float needleNoise = hash(vec2(vTexCoord.x * 28.0 + vNeedleSeed * 7.1,
+                                vTexCoord.y * 24.0 + vNeedleSeed * 5.3));
+
+  // vertical streaking; quantize Y to bands
+  float needleStreak = hash(vec2(vTexCoord.x * 12.0 + vNeedleSeed * 3.7,
+                                 floor(vTexCoord.y * 6.0 + vNeedleSeed * 2.0)));
+
+  vec3 needleColor = vColor * (0.78 + needleNoise * 0.28);
+  needleColor += vec3(0.02, 0.05, 0.02) * needleStreak;
+
+  // Subtle brightening toward tips (top of UVs)
+  float tipBlend = smoothstep(0.82, 1.02, vTexCoord.y);
+  needleColor =
+      mix(needleColor, needleColor * vec3(1.08, 1.04, 1.10), tipBlend);
+
+  // --- Bark variation: stripes + noise ---
+  float barkStripe = sin(vTexCoord.y * 45.0 + vBarkSeed * TWO_PI) * 0.1 + 0.9;
+  float barkNoise = hash(vec2(vTexCoord.x * 18.0 + vBarkSeed * 4.3,
+                              vTexCoord.y * 10.0 + vBarkSeed * 7.7));
+
+  vec3 trunkBase = vec3(0.32, 0.24, 0.16) * barkStripe;
+  vec3 trunkColor = trunkBase * (0.85 + barkNoise * 0.35);
+
+  // --- Final color ---
+  vec3 baseColor = mix(trunkColor, needleColor, vFoliageMask);
+  vec3 color = baseColor * lighting;
+
+  // --- Alpha shaping (silhouette for foliage + bottom fade) ---
+  float silhouetteNoise = hash(vec2(vTexCoord.x * 30.0 + vNeedleSeed * 9.0,
+                                    vTexCoord.y * 40.0 + vNeedleSeed * 5.5));
+
+  float alphaFoliage = 0.70 + silhouetteNoise * 0.25;
+  float alpha = mix(1.0, alphaFoliage, vFoliageMask);
+
+  // fade near bottom (ground contact)
+  alpha *= smoothstep(0.00, 0.05, vTexCoord.y);
+
+  // clamp and alpha test
+  alpha = clamp(alpha, 0.0, 1.0);
+  if (alpha < 0.06)
+    discard;
+
+  FragColor = vec4(color, alpha);
 }

+ 79 - 84
assets/shaders/pine_instanced.vert

@@ -6,9 +6,10 @@
 layout(location = 0) in vec3 aPos;
 layout(location = 1) in vec2 aTexCoord;
 layout(location = 2) in vec3 aNormal;
-layout(location = 3) in vec4 aPosScale;   // instance: xyz = world pos, w = scale
-layout(location = 4) in vec4 aColorSway;  // instance: rgb = tint, a = sway phase
-layout(location = 5) in vec4 aRotation;   // instance: x = Y-axis rotation, yzw = seeds
+layout(location = 3) in vec4 aPosScale;  // instance: xyz = world pos, w = scale
+layout(location = 4) in vec4 aColorSway; // instance: rgb = tint, a = sway phase
+layout(location =
+           5) in vec4 aRotation; // instance: x = Y-axis rotation, yzw = seeds
 
 // ─────────────────────────────────────────────────────────────
 // Uniforms
@@ -33,85 +34,79 @@ out float vBarkSeed;
 // Main Shader Logic
 // ─────────────────────────────────────────────────────────────
 void main() {
-    const float TWO_PI = 6.2831853;
-
-    // Instance data unpacking
-    float scale          = aPosScale.w;
-    vec3  worldPos       = aPosScale.xyz;
-    float swayPhase      = aColorSway.a;
-    float rotation       = aRotation.x;
-    float silhouetteSeed = aRotation.y;
-    float needleSeed     = aRotation.z;
-    float barkSeed       = aRotation.w;
-
-    vec3 modelPos = aPos;
-
-    // ── Foliage and tip region masks ──────────────────────────
-    float foliageMask = smoothstep(0.34, 0.42, aTexCoord.y);
-    float tipMask     = smoothstep(0.88, 1.02, aTexCoord.y);
-    float angle       = aTexCoord.x * TWO_PI;
-
-    // ── Irregular silhouette shaping ──────────────────────────
-    float irregularBase = sin(angle * 3.0 + silhouetteSeed * TWO_PI);
-    float irregularFine = sin(angle * 5.0 + silhouetteSeed * TWO_PI * 2.0);
-    float irregular = (irregularBase * 0.11 + irregularFine * 0.05) *
-                      foliageMask * (1.0 - tipMask * 0.6);
-
-    modelPos.xz *= (1.0 + irregular);
-
-    // Slight droop on foliage
-    float droop = foliageMask * (1.0 - tipMask) * 0.08;
-    modelPos.y -= droop;
-
-    // Height-based influence
-    float heightFactor = clamp(modelPos.y, 0.0, 1.1);
-    vec3  localPos = modelPos * scale;
-
-    // ── Wind sway (stronger at upper regions) ─────────────────
-    float sway = sin(uTime * uWindSpeed * 0.5 + swayPhase) *
-                 uWindStrength * 0.8 * heightFactor * heightFactor;
-
-    float swayInfluence = mix(0.04, 0.12, foliageMask);
-    localPos.x += sway * swayInfluence;
-
-    // Slight forward bend for branches
-    localPos.y -= sway * 0.02 * foliageMask;
-
-    // ── Adjust normals for irregular foliage ──────────────────
-    vec3 localNormal = aNormal;
-    if (foliageMask > 0.0) {
-        float normalScale = 1.0 + irregular;
-        localNormal = normalize(vec3(
-            localNormal.x * normalScale,
-            localNormal.y - foliageMask * 0.2,
-            localNormal.z * normalScale
-        ));
-    }
-
-    // ── Instance rotation about Y-axis ────────────────────────
-    float cosR = cos(rotation);
-    float sinR = sin(rotation);
-    mat2 rot = mat2(cosR, -sinR,
-                    sinR,  cosR);
-
-    vec2 rotatedXZ = rot * localPos.xz;
-    localPos = vec3(rotatedXZ.x, localPos.y, rotatedXZ.y);
-
-    vec2 rotatedNormalXZ = rot * localNormal.xz;
-    vec3 finalNormal = normalize(vec3(
-        rotatedNormalXZ.x,
-        localNormal.y,
-        rotatedNormalXZ.y
-    ));
-
-    // ── Outputs ───────────────────────────────────────────────
-    vWorldPos    = localPos + worldPos;
-    vNormal      = finalNormal;
-    vColor       = aColorSway.rgb;
-    vTexCoord    = aTexCoord;
-    vFoliageMask = foliageMask;
-    vNeedleSeed  = needleSeed;
-    vBarkSeed    = barkSeed;
-
-    gl_Position = uViewProj * vec4(vWorldPos, 1.0);
+  const float TWO_PI = 6.2831853;
+
+  // Instance data unpacking
+  float scale = aPosScale.w;
+  vec3 worldPos = aPosScale.xyz;
+  float swayPhase = aColorSway.a;
+  float rotation = aRotation.x;
+  float silhouetteSeed = aRotation.y;
+  float needleSeed = aRotation.z;
+  float barkSeed = aRotation.w;
+
+  vec3 modelPos = aPos;
+
+  // ── Foliage and tip region masks ──────────────────────────
+  float foliageMask = smoothstep(0.34, 0.42, aTexCoord.y);
+  float tipMask = smoothstep(0.88, 1.02, aTexCoord.y);
+  float angle = aTexCoord.x * TWO_PI;
+
+  // ── Irregular silhouette shaping ──────────────────────────
+  float irregularBase = sin(angle * 3.0 + silhouetteSeed * TWO_PI);
+  float irregularFine = sin(angle * 5.0 + silhouetteSeed * TWO_PI * 2.0);
+  float irregular = (irregularBase * 0.11 + irregularFine * 0.05) *
+                    foliageMask * (1.0 - tipMask * 0.6);
+
+  modelPos.xz *= (1.0 + irregular);
+
+  // Slight droop on foliage
+  float droop = foliageMask * (1.0 - tipMask) * 0.08;
+  modelPos.y -= droop;
+
+  // Height-based influence
+  float heightFactor = clamp(modelPos.y, 0.0, 1.1);
+  vec3 localPos = modelPos * scale;
+
+  // ── Wind sway (stronger at upper regions) ─────────────────
+  float sway = sin(uTime * uWindSpeed * 0.5 + swayPhase) * uWindStrength * 0.8 *
+               heightFactor * heightFactor;
+
+  float swayInfluence = mix(0.04, 0.12, foliageMask);
+  localPos.x += sway * swayInfluence;
+
+  // Slight forward bend for branches
+  localPos.y -= sway * 0.02 * foliageMask;
+
+  // ── Adjust normals for irregular foliage ──────────────────
+  vec3 localNormal = aNormal;
+  if (foliageMask > 0.0) {
+    float normalScale = 1.0 + irregular;
+    localNormal = normalize(vec3(localNormal.x * normalScale,
+                                 localNormal.y - foliageMask * 0.2,
+                                 localNormal.z * normalScale));
+  }
+
+  // ── Instance rotation about Y-axis ────────────────────────
+  float cosR = cos(rotation);
+  float sinR = sin(rotation);
+  mat2 rot = mat2(cosR, -sinR, sinR, cosR);
+
+  vec2 rotatedXZ = rot * localPos.xz;
+  localPos = vec3(rotatedXZ.x, localPos.y, rotatedXZ.y);
+
+  vec2 rotatedNormalXZ = rot * localNormal.xz;
+  vec3 finalNormal =
+      normalize(vec3(rotatedNormalXZ.x, localNormal.y, rotatedNormalXZ.y));
+
+  // ── Outputs ───────────────────────────────────────────────
+  vWorldPos = localPos + worldPos;
+  vNormal = finalNormal;
+  vColor = aColorSway.rgb;
+  vTexCoord = aTexCoord;
+  vFoliageMask = foliageMask;
+  vNeedleSeed = needleSeed;
+  vBarkSeed = barkSeed;
+
+  gl_Position = uViewProj * vec4(vWorldPos, 1.0);
 }

+ 138 - 132
assets/shaders/plant_instanced.frag

@@ -1,15 +1,15 @@
 #version 330 core
 
-in vec3  vWorldPos;
-in vec3  vNormal;
-in vec3  vColor;
-in vec2  vTexCoord;
-in float vAlpha;     // (unused)
+in vec3 vWorldPos;
+in vec3 vNormal;
+in vec3 vColor;
+in vec2 vTexCoord;
+in float vAlpha; // (unused)
 in float vHeight;
 in float vSeed;
 in float vType;
-in vec3  vTangent;
-in vec3  vBitangent;
+in vec3 vTangent;
+in vec3 vBitangent;
 
 uniform vec3 uLightDirection;
 
@@ -19,9 +19,10 @@ out vec4 FragColor;
 // Toggles
 // ─────────────────────────────────────────────────────────────
 // Use when distant shimmer persists and you want depth-stable masking.
-//#define USE_HASHED_ALPHA 1
-// If your renderer uses TAA, screen-anchor the dither; otherwise leave off for world-anchored.
-//#define DITHER_SCREEN_ANCHORED 1
+// #define USE_HASHED_ALPHA 1
+// If your renderer uses TAA, screen-anchor the dither; otherwise leave off for
+// world-anchored.
+// #define DITHER_SCREEN_ANCHORED 1
 
 // ─────────────────────────────────────────────────────────────
 // Helpers
@@ -30,176 +31,181 @@ float h11(float n) { return fract(sin(n) * 43758.5453123); }
 
 // Stable, screen-space UV footprint with power-of-two quantization.
 float aawidthUV(vec2 uv) {
-    vec2 dx = dFdx(uv), dy = dFdy(uv);
-    float w = 0.5 * (length(dx) + length(dy));
-    float q = exp2(floor(log2(max(w, 1e-6)) + 0.5)); // quantize to PoT buckets
-    return clamp(q, 0.0015, 0.0060);
+  vec2 dx = dFdx(uv), dy = dFdy(uv);
+  float w = 0.5 * (length(dx) + length(dy));
+  float q = exp2(floor(log2(max(w, 1e-6)) + 0.5)); // quantize to PoT buckets
+  return clamp(q, 0.0015, 0.0060);
 }
 
 // Conservative, quantized alpha from SDF + UV footprint.
 float sdf_to_alpha_uv_stable(float sdf, vec2 uv) {
-    float w = aawidthUV(uv);
+  float w = aawidthUV(uv);
 
-    // Slight inward bias stabilizes coverage at threshold.
-    sdf -= 0.25 * w;
+  // Slight inward bias stabilizes coverage at threshold.
+  sdf -= 0.25 * w;
 
-    // Analytic coverage.
-    float a = 1.0 - smoothstep(-w, w, sdf);
+  // Analytic coverage.
+  float a = 1.0 - smoothstep(-w, w, sdf);
 
-    // Mild alpha quantization (finer as alpha grows) to reduce sub-8bit flicker.
-    float steps = mix(24.0, 64.0, a);
-    a = floor(a * steps + 0.5) / steps;
+  // Mild alpha quantization (finer as alpha grows) to reduce sub-8bit flicker.
+  float steps = mix(24.0, 64.0, a);
+  a = floor(a * steps + 0.5) / steps;
 
-    return a;
+  return a;
 }
 
 // Deterministic noise; anchor chosen below.
 float interleavedGradientNoise(vec2 p) {
-    float f = dot(p, vec2(0.06711056, 0.00583715));
-    return fract(52.9829189 * fract(f));
+  float f = dot(p, vec2(0.06711056, 0.00583715));
+  return fract(52.9829189 * fract(f));
 }
 
 float stableDither(float seed) {
 #ifdef DITHER_SCREEN_ANCHORED
-    return interleavedGradientNoise(gl_FragCoord.xy);
+  return interleavedGradientNoise(gl_FragCoord.xy);
 #else
-    return interleavedGradientNoise(floor(vWorldPos.xz * 4.0 + seed * 17.0));
+  return interleavedGradientNoise(floor(vWorldPos.xz * 4.0 + seed * 17.0));
 #endif
 }
 
 // Quantized step for stable finite differences.
-float quantStep(float w) {
-    return exp2(floor(log2(max(w, 1e-6)) + 0.5));
-}
+float quantStep(float w) { return exp2(floor(log2(max(w, 1e-6)) + 0.5)); }
 
 // ─────────────────────────────────────────────────────────────
 // SDF Silhouettes (non-destructive)
 // ─────────────────────────────────────────────────────────────
 float bushSDF(vec2 uv, float seed) {
-    vec2 p = (uv - 0.5) * vec2(1.08, 0.96);
-    float sdf = 1e9;
-    for (int i = 0; i < 5; i++) {
-        float fi  = float(i);
-        float ang = fi * 1.25663706 + seed * 3.7;
-        vec2 c    = vec2(cos(ang), sin(ang)) * (0.18 + h11(seed * 7.9 + fi) * 0.05);
-        float r   = 0.30 + h11(seed * 5.7 + fi) * 0.06;
-        float d   = length(p - c) - r;
-        sdf = min(sdf, d);
-    }
-    return sdf - 0.007;
+  vec2 p = (uv - 0.5) * vec2(1.08, 0.96);
+  float sdf = 1e9;
+  for (int i = 0; i < 5; i++) {
+    float fi = float(i);
+    float ang = fi * 1.25663706 + seed * 3.7;
+    vec2 c = vec2(cos(ang), sin(ang)) * (0.18 + h11(seed * 7.9 + fi) * 0.05);
+    float r = 0.30 + h11(seed * 5.7 + fi) * 0.06;
+    float d = length(p - c) - r;
+    sdf = min(sdf, d);
+  }
+  return sdf - 0.007;
 }
 
 float rosetteSDF(vec2 uv, float seed) {
-    vec2 p = uv - 0.5;
-    float a = atan(p.y, p.x);
-    float r = length(p);
-    float petals = mix(10.0, 16.0, h11(seed * 2.7));
-    float wave   = 0.20 + 0.06 * sin(a * petals + seed * 5.1);
-    return (r - wave) - 0.006;
+  vec2 p = uv - 0.5;
+  float a = atan(p.y, p.x);
+  float r = length(p);
+  float petals = mix(10.0, 16.0, h11(seed * 2.7));
+  float wave = 0.20 + 0.06 * sin(a * petals + seed * 5.1);
+  return (r - wave) - 0.006;
 }
 
 float cactusSDF(vec2 uv, float seed) {
-    vec2 p = (uv - 0.5) * vec2(0.92, 1.08);
-    float sdf = length(p) - 0.48; // body
-    for (int i = 0; i < 3; i++) {
-        float fi  = float(i);
-        float ang = mix(-1.6, 1.6, h11(seed * 3.3 + fi));
-        vec2 c    = vec2(0.22 * cos(ang), 0.12 + 0.25 * abs(sin(ang)));
-        vec2 e    = vec2(0.22, 0.30) * mix(0.7, 1.1, h11(seed * 6.1 + fi));
-        float d   = length((p - c) / e) - 1.0;
-        sdf = min(sdf, d);
-    }
-    return sdf - 0.006;
+  vec2 p = (uv - 0.5) * vec2(0.92, 1.08);
+  float sdf = length(p) - 0.48; // body
+  for (int i = 0; i < 3; i++) {
+    float fi = float(i);
+    float ang = mix(-1.6, 1.6, h11(seed * 3.3 + fi));
+    vec2 c = vec2(0.22 * cos(ang), 0.12 + 0.25 * abs(sin(ang)));
+    vec2 e = vec2(0.22, 0.30) * mix(0.7, 1.1, h11(seed * 6.1 + fi));
+    float d = length((p - c) / e) - 1.0;
+    sdf = min(sdf, d);
+  }
+  return sdf - 0.006;
 }
 
 float plantSDF(vec2 uv, float typeVal, float seed) {
-    if (typeVal < 0.45) return bushSDF(uv, seed);
-    if (typeVal < 0.80) return rosetteSDF(uv, seed);
-    return cactusSDF(uv, seed);
+  if (typeVal < 0.45)
+    return bushSDF(uv, seed);
+  if (typeVal < 0.80)
+    return rosetteSDF(uv, seed);
+  return cactusSDF(uv, seed);
 }
 
-// Finite-difference SDF gradient in UV (step tied & quantized to pixel footprint).
+// Finite-difference SDF gradient in UV (step tied & quantized to pixel
+// footprint).
 vec2 sdfGrad(vec2 uv, float typeVal, float seed, float stepUV) {
-    stepUV = quantStep(stepUV);
-    vec2 e = vec2(stepUV, 0.0);
-    float sx1 = plantSDF(uv + e.xy, typeVal, seed);
-    float sx2 = plantSDF(uv - e.xy, typeVal, seed);
-    float sy1 = plantSDF(uv + e.yx, typeVal, seed);
-    float sy2 = plantSDF(uv - e.yx, typeVal, seed);
-    return vec2(sx1 - sx2, sy1 - sy2) * (0.5 / stepUV);
+  stepUV = quantStep(stepUV);
+  vec2 e = vec2(stepUV, 0.0);
+  float sx1 = plantSDF(uv + e.xy, typeVal, seed);
+  float sx2 = plantSDF(uv - e.xy, typeVal, seed);
+  float sy1 = plantSDF(uv + e.yx, typeVal, seed);
+  float sy2 = plantSDF(uv - e.yx, typeVal, seed);
+  return vec2(sx1 - sx2, sy1 - sy2) * (0.5 / stepUV);
 }
 
 // ─────────────────────────────────────────────────────────────
 // Shading
 // ─────────────────────────────────────────────────────────────
 void main() {
-    // Base palette
-    float dryness = mix(0.35, 0.92, h11(vSeed * 2.7 + vType * 0.73));
-    vec3 lush = vec3(0.17, 0.32, 0.19);
-    vec3 dry  = vec3(0.46, 0.44, 0.28);
-    vec3 base = mix(lush, dry, dryness);
-    base = mix(base, vColor, 0.40);
-    base *= 0.88;
-
-    // Bulged normals for shrub fullness
-    vec2 uv2 = (vTexCoord - 0.5) * 2.0;
-    float r2 = clamp(dot(uv2, uv2), 0.0, 1.0);
-    float z  = sqrt(max(1.0 - r2, 0.0));
-    vec3 Nbulge = normalize(vTangent * uv2.x + vBitangent * uv2.y + vNormal * (z * 1.8));
-    vec3 N      = normalize(mix(vNormal, Nbulge, 0.85));
-
-    // SDF & stable alpha
-    float typeVal = fract(vType);
-    float sdf     = plantSDF(vTexCoord, typeVal, vSeed);
-    float alpha   = sdf_to_alpha_uv_stable(sdf, vTexCoord);
-
-    if (alpha <= 0.002) discard;
+  // Base palette
+  float dryness = mix(0.35, 0.92, h11(vSeed * 2.7 + vType * 0.73));
+  vec3 lush = vec3(0.17, 0.32, 0.19);
+  vec3 dry = vec3(0.46, 0.44, 0.28);
+  vec3 base = mix(lush, dry, dryness);
+  base = mix(base, vColor, 0.40);
+  base *= 0.88;
+
+  // Bulged normals for shrub fullness
+  vec2 uv2 = (vTexCoord - 0.5) * 2.0;
+  float r2 = clamp(dot(uv2, uv2), 0.0, 1.0);
+  float z = sqrt(max(1.0 - r2, 0.0));
+  vec3 Nbulge =
+      normalize(vTangent * uv2.x + vBitangent * uv2.y + vNormal * (z * 1.8));
+  vec3 N = normalize(mix(vNormal, Nbulge, 0.85));
+
+  // SDF & stable alpha
+  float typeVal = fract(vType);
+  float sdf = plantSDF(vTexCoord, typeVal, vSeed);
+  float alpha = sdf_to_alpha_uv_stable(sdf, vTexCoord);
+
+  if (alpha <= 0.002)
+    discard;
 
     // Optional hashed alpha for ultra-thin coverage → stable depth
 #ifdef USE_HASHED_ALPHA
-    {
-        float w    = aawidthUV(vTexCoord);
-        float thin = smoothstep(0.0, 2.0 * w, alpha);
-        if (thin < 0.98) {
-            if (thin < stableDither(vSeed)) discard;
-            alpha = 1.0; // write solid depth for kept pixels
-        }
+  {
+    float w = aawidthUV(vTexCoord);
+    float thin = smoothstep(0.0, 2.0 * w, alpha);
+    if (thin < 0.98) {
+      if (thin < stableDither(vSeed))
+        discard;
+      alpha = 1.0; // write solid depth for kept pixels
     }
+  }
 #endif
 
-    // Shape-space normal near silhouettes (with stabilized FD step)
-    float stepUV = aawidthUV(vTexCoord);
-    vec2 g = sdfGrad(vTexCoord, typeVal, vSeed, stepUV);
-    vec3 Nshape = normalize(vTangent * (-g.x) + vBitangent * (-g.y) + vNormal * 3.0);
-    float edgeMix = smoothstep(0.30, 0.0, sdf); // only inside & near edge
-    vec3 Ntemp = normalize(mix(N, Nshape, 0.6 * edgeMix));
-
-    // Edge attenuation to avoid sparkling highlights & reduce normal swapping
-    float wAA     = aawidthUV(vTexCoord);
-    float edge1   = 1.0 - smoothstep(-wAA, wAA, sdf);         // 1 at edge, 0 inside
-    N             = normalize(mix(Ntemp, vNormal, edge1 * 0.5));
-    float edgeAtten = mix(0.6, 1.0, pow(1.0 - edge1, 1.5));   // 0.6 at very edge
-
-    // Lighting
-    vec3 L = normalize(uLightDirection);
-    float nl          = max(dot(N, L), 0.0);
-    float halfLambert = nl * 0.5 + 0.5;
-    float wrap        = clamp((dot(N, L) + 0.20) / 1.20, 0.0, 1.0);
-    float diffuse     = mix(halfLambert, wrap, 0.30) * edgeAtten;
-    float sss         = pow(clamp(dot(-N, L), 0.0, 1.0), 2.2) * 0.22 * edgeAtten;
-    float ambient     = 0.16;
-
-    float aoStem = mix(0.50, 1.0, smoothstep(0.0, 0.55, vHeight));
-
-    // Subtle tip brightening & inner-edge occlusion (view-stable)
-    float tip   = smoothstep(0.25, 1.0, r2);
-    float inner = smoothstep(-2.0 * wAA, -0.2 * wAA, sdf);
-    vec3 albedo = base;
-    albedo *= mix(1.0, 1.08, tip);
-    albedo *= mix(0.95, 1.0, inner);
-
-    vec3 color = albedo * (ambient + diffuse * aoStem)
-               + albedo * sss * vec3(1.0, 0.95, 0.85);
-
-    FragColor = vec4(color, alpha);
+  // Shape-space normal near silhouettes (with stabilized FD step)
+  float stepUV = aawidthUV(vTexCoord);
+  vec2 g = sdfGrad(vTexCoord, typeVal, vSeed, stepUV);
+  vec3 Nshape =
+      normalize(vTangent * (-g.x) + vBitangent * (-g.y) + vNormal * 3.0);
+  float edgeMix = smoothstep(0.30, 0.0, sdf); // only inside & near edge
+  vec3 Ntemp = normalize(mix(N, Nshape, 0.6 * edgeMix));
+
+  // Edge attenuation to avoid sparkling highlights & reduce normal swapping
+  float wAA = aawidthUV(vTexCoord);
+  float edge1 = 1.0 - smoothstep(-wAA, wAA, sdf); // 1 at edge, 0 inside
+  N = normalize(mix(Ntemp, vNormal, edge1 * 0.5));
+  float edgeAtten = mix(0.6, 1.0, pow(1.0 - edge1, 1.5)); // 0.6 at very edge
+
+  // Lighting
+  vec3 L = normalize(uLightDirection);
+  float nl = max(dot(N, L), 0.0);
+  float halfLambert = nl * 0.5 + 0.5;
+  float wrap = clamp((dot(N, L) + 0.20) / 1.20, 0.0, 1.0);
+  float diffuse = mix(halfLambert, wrap, 0.30) * edgeAtten;
+  float sss = pow(clamp(dot(-N, L), 0.0, 1.0), 2.2) * 0.22 * edgeAtten;
+  float ambient = 0.16;
+
+  float aoStem = mix(0.50, 1.0, smoothstep(0.0, 0.55, vHeight));
+
+  // Subtle tip brightening & inner-edge occlusion (view-stable)
+  float tip = smoothstep(0.25, 1.0, r2);
+  float inner = smoothstep(-2.0 * wAA, -0.2 * wAA, sdf);
+  vec3 albedo = base;
+  albedo *= mix(1.0, 1.08, tip);
+  albedo *= mix(0.95, 1.0, inner);
+
+  vec3 color = albedo * (ambient + diffuse * aoStem) +
+               albedo * sss * vec3(1.0, 0.95, 0.85);
+
+  FragColor = vec4(color, alpha);
 }

+ 80 - 81
assets/shaders/plant_instanced.vert

@@ -7,9 +7,10 @@
 layout(location = 0) in vec3 aPos;
 layout(location = 1) in vec2 aTexCoord;
 layout(location = 2) in vec3 aNormal;
-layout(location = 3) in vec4 aPosScale;     // xyz = world pos, w = scale
-layout(location = 4) in vec4 aColorSway;    // rgb = tint, a = sway phase
-layout(location = 5) in vec4 aTypeParams;   // x = type, y = rotation, z = sway strength, w = sway speed
+layout(location = 3) in vec4 aPosScale;  // xyz = world pos, w = scale
+layout(location = 4) in vec4 aColorSway; // rgb = tint, a = sway phase
+layout(location = 5) in vec4
+    aTypeParams; // x = type, y = rotation, z = sway strength, w = sway speed
 
 uniform mat4 uViewProj;
 uniform float uTime;
@@ -27,87 +28,85 @@ out float vType;
 out vec3 vTangent;
 out vec3 vBitangent;
 
-float h11(float n) {
-    return fract(sin(n) * 43758.5453123);
-}
+float h11(float n) { return fract(sin(n) * 43758.5453123); }
 
 float h31(vec3 p) {
-    return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453);
+  return fract(sin(dot(p, vec3(127.1, 311.7, 74.7))) * 43758.5453);
 }
 
 void main() {
-    float scale = aPosScale.w;
-    vec3 worldOrigin = aPosScale.xyz;
-    float rotation = aTypeParams.y;
-    float swayStrength = aTypeParams.z;
-    float swaySpeed = aTypeParams.w;
-    float swayPhase = aColorSway.a;
-
-    // stable per-instance seed
-    float seed = h31(worldOrigin * 0.173 + vec3(0.27, 0.49, 0.19));
-
-    // bigger plants + size jitter
-    const float SIZE_MULT = 1.85; // ← bump this if still small
-    float sizeJitter = mix(0.90, 1.45, h11(seed * 3.9));
-    float finalScale = scale * SIZE_MULT * sizeJitter;
-
-    vec3 localPos = aPos * finalScale;
-    float h = clamp(aPos.y, 0.0, 1.0);
-
-    // irregular lean (looks like full shrubs, not thin leaves)
-    float leanAngle = (h11(seed * 2.1) - 0.5) * 0.18; // ±10°
-    float leanYaw = h11(seed * 3.7) * 6.28318;
-    vec2 leanDir = vec2(cos(leanYaw), sin(leanYaw));
-    localPos.xz += leanDir * (h * h) * tan(leanAngle) * finalScale;
-
-    // wind: gentle gusts with per-instance direction
-    float gust = sin(uTime * 0.35 + seed * 6.0) * 0.5 + 0.5;
-    float sway = sin(uTime * swaySpeed * uWindSpeed + swayPhase + seed * 4.0);
-    sway *= (0.22 + 0.55 * gust) * swayStrength * uWindStrength * pow(h, 1.25);
-
-    float windYaw = seed * 9.0;
-    vec2 windDir = normalize(vec2(cos(windYaw), sin(windYaw)) + vec2(0.6, 0.8));
-    localPos.xz += windDir * (0.10 * sway);
-
-    // mild twist toward the tip for volume
-    float twist = (h11(seed * 5.5) - 0.5) * 0.30; // ±17°
-    float twistAngle = twist * h;
-    mat2 tw = mat2(cos(twistAngle), -sin(twistAngle),
-                   sin(twistAngle),  cos(twistAngle));
-    localPos.xz = tw * localPos.xz;
-
-    // instance rotation about Y
-    float cs = cos(rotation), sn = sin(rotation);
-    mat2 rot = mat2(cs, -sn, sn, cs);
-    localPos.xz = rot * localPos.xz;
-
-    vWorldPos = localPos + worldOrigin;
-
-    // rotate normal the same way
-    vec3 n = aNormal;
-    n.xz = tw * n.xz;
-    n.xz = rot * n.xz;
-    vNormal = normalize(n);
-
-    // world-space tangents (card X/Z) for bulge normal in FS
-    vec3 t = vec3(1.0, 0.0, 0.0);
-    vec3 b = vec3(0.0, 0.0, 1.0);
-    t.xz = tw * t.xz;
-    b.xz = tw * b.xz;
-    t.xz = rot * t.xz;
-    b.xz = rot * b.xz;
-
-    vTangent = normalize(t);
-    vBitangent = normalize(b);
-
-    vHeight = h;
-    vSeed = seed;
-    vType = aTypeParams.x;
-    vColor = aColorSway.rgb;
-    vTexCoord = aTexCoord;
-
-    // fuller base alpha (final silhouette handled in FS)
-    vAlpha = 1.0 - smoothstep(0.49, 0.56, abs(aTexCoord.x - 0.5));
-
-    gl_Position = uViewProj * vec4(vWorldPos, 1.0);
+  float scale = aPosScale.w;
+  vec3 worldOrigin = aPosScale.xyz;
+  float rotation = aTypeParams.y;
+  float swayStrength = aTypeParams.z;
+  float swaySpeed = aTypeParams.w;
+  float swayPhase = aColorSway.a;
+
+  // stable per-instance seed
+  float seed = h31(worldOrigin * 0.173 + vec3(0.27, 0.49, 0.19));
+
+  // bigger plants + size jitter
+  const float SIZE_MULT = 1.85; // ← bump this if still small
+  float sizeJitter = mix(0.90, 1.45, h11(seed * 3.9));
+  float finalScale = scale * SIZE_MULT * sizeJitter;
+
+  vec3 localPos = aPos * finalScale;
+  float h = clamp(aPos.y, 0.0, 1.0);
+
+  // irregular lean (looks like full shrubs, not thin leaves)
+  float leanAngle = (h11(seed * 2.1) - 0.5) * 0.18; // ±10°
+  float leanYaw = h11(seed * 3.7) * 6.28318;
+  vec2 leanDir = vec2(cos(leanYaw), sin(leanYaw));
+  localPos.xz += leanDir * (h * h) * tan(leanAngle) * finalScale;
+
+  // wind: gentle gusts with per-instance direction
+  float gust = sin(uTime * 0.35 + seed * 6.0) * 0.5 + 0.5;
+  float sway = sin(uTime * swaySpeed * uWindSpeed + swayPhase + seed * 4.0);
+  sway *= (0.22 + 0.55 * gust) * swayStrength * uWindStrength * pow(h, 1.25);
+
+  float windYaw = seed * 9.0;
+  vec2 windDir = normalize(vec2(cos(windYaw), sin(windYaw)) + vec2(0.6, 0.8));
+  localPos.xz += windDir * (0.10 * sway);
+
+  // mild twist toward the tip for volume
+  float twist = (h11(seed * 5.5) - 0.5) * 0.30; // ±17°
+  float twistAngle = twist * h;
+  mat2 tw =
+      mat2(cos(twistAngle), -sin(twistAngle), sin(twistAngle), cos(twistAngle));
+  localPos.xz = tw * localPos.xz;
+
+  // instance rotation about Y
+  float cs = cos(rotation), sn = sin(rotation);
+  mat2 rot = mat2(cs, -sn, sn, cs);
+  localPos.xz = rot * localPos.xz;
+
+  vWorldPos = localPos + worldOrigin;
+
+  // rotate normal the same way
+  vec3 n = aNormal;
+  n.xz = tw * n.xz;
+  n.xz = rot * n.xz;
+  vNormal = normalize(n);
+
+  // world-space tangents (card X/Z) for bulge normal in FS
+  vec3 t = vec3(1.0, 0.0, 0.0);
+  vec3 b = vec3(0.0, 0.0, 1.0);
+  t.xz = tw * t.xz;
+  b.xz = tw * b.xz;
+  t.xz = rot * t.xz;
+  b.xz = rot * b.xz;
+
+  vTangent = normalize(t);
+  vBitangent = normalize(b);
+
+  vHeight = h;
+  vSeed = seed;
+  vType = aTypeParams.x;
+  vColor = aColorSway.rgb;
+  vTexCoord = aTexCoord;
+
+  // fuller base alpha (final silhouette handled in FS)
+  vAlpha = 1.0 - smoothstep(0.49, 0.56, abs(aTexCoord.x - 0.5));
+
+  gl_Position = uViewProj * vec4(vWorldPos, 1.0);
 }

+ 5 - 4
game/systems/combat_system.cpp

@@ -110,9 +110,9 @@ void CombatSystem::processAttacks(Engine::Core::World *world, float deltaTime) {
       damage = attackerAtk->getCurrentDamage();
       cooldown = attackerAtk->getCurrentCooldown();
 
-      auto *holdMode = attacker->getComponent<Engine::Core::HoldModeComponent>();
-      if (holdMode && holdMode->active &&
-          attackerUnit->unitType == "archer") {
+      auto *holdMode =
+          attacker->getComponent<Engine::Core::HoldModeComponent>();
+      if (holdMode && holdMode->active && attackerUnit->unitType == "archer") {
         range *= 1.5f;
         damage = static_cast<int>(damage * 1.3f);
       }
@@ -167,7 +167,8 @@ void CombatSystem::processAttacks(Engine::Core::World *world, float deltaTime) {
                 attacker->getComponent<Engine::Core::HoldModeComponent>();
             if (holdMode && holdMode->active) {
               if (!isInRange(attacker, target, range)) {
-                attacker->removeComponent<Engine::Core::AttackTargetComponent>();
+                attacker
+                    ->removeComponent<Engine::Core::AttackTargetComponent>();
               }
               continue;
             }

+ 2 - 1
game/systems/movement_system.cpp

@@ -108,7 +108,8 @@ void MovementSystem::moveUnit(Engine::Core::Entity *entity,
   auto *holdMode = entity->getComponent<Engine::Core::HoldModeComponent>();
   if (holdMode) {
     if (holdMode->exitCooldown > 0.0f) {
-      holdMode->exitCooldown = std::max(0.0f, holdMode->exitCooldown - deltaTime);
+      holdMode->exitCooldown =
+          std::max(0.0f, holdMode->exitCooldown - deltaTime);
     }
 
     if (holdMode->active) {

+ 30 - 35
render/draw_queue.h

@@ -110,10 +110,10 @@ struct SelectionSmokeCmd {
   float baseAlpha = 0.15f;
 };
 
-using DrawCmd = std::variant<GridCmd, SelectionRingCmd, SelectionSmokeCmd,
-                             CylinderCmd, MeshCmd, FogBatchCmd, GrassBatchCmd,
-                             StoneBatchCmd, PlantBatchCmd, PineBatchCmd,
-                             TerrainChunkCmd>;
+using DrawCmd =
+    std::variant<GridCmd, SelectionRingCmd, SelectionSmokeCmd, CylinderCmd,
+                 MeshCmd, FogBatchCmd, GrassBatchCmd, StoneBatchCmd,
+                 PlantBatchCmd, PineBatchCmd, TerrainChunkCmd>;
 
 enum class DrawCmdType : std::uint8_t {
   Grid = 0,
@@ -248,38 +248,33 @@ private:
   }
 
   uint64_t computeSortKey(const DrawCmd &cmd) const {
-    // Rendering order (lower order value = earlier in frame):
-    // TerrainChunk (0) → GrassBatch (1) → StoneBatch (2) → PlantBatch (3) → 
-    // PineBatch (4) → FogBatch (5) → Mesh (6) → Cylinder (7) → SelectionSmoke (8) → SelectionRing (9) → Grid (10)
-    
+
     enum class RenderOrder : uint8_t {
-      TerrainChunk = 0,    // Opaque ground base (renders first)
-      GrassBatch = 1,      // Grass on terrain
-      StoneBatch = 2,      // Stones on terrain
-      PlantBatch = 3,      // Plants on terrain (3rd biome layer)
-      PineBatch = 4,       // Pine trees on terrain (4th biome layer)
-      FogBatch = 5,        // Fog of war (covers all biome layers)
-      Mesh = 6,            // Units and objects
-      Cylinder = 7,        // Unit body parts
-      SelectionSmoke = 8,  // Selection effects
-      SelectionRing = 9,   // Selection UI
-      Grid = 10            // Debug grid (renders last)
+      TerrainChunk = 0,
+      GrassBatch = 1,
+      StoneBatch = 2,
+      PlantBatch = 3,
+      PineBatch = 4,
+      FogBatch = 5,
+      Mesh = 6,
+      Cylinder = 7,
+      SelectionSmoke = 8,
+      SelectionRing = 9,
+      Grid = 10
     };
-    
-    // Map variant index to render order using enum values
+
     static constexpr uint8_t kTypeOrder[] = {
-      static_cast<uint8_t>(RenderOrder::Grid),           // Grid (variant index 0)
-      static_cast<uint8_t>(RenderOrder::SelectionRing),  // SelectionRing (variant index 1)
-      static_cast<uint8_t>(RenderOrder::SelectionSmoke), // SelectionSmoke (variant index 2)
-      static_cast<uint8_t>(RenderOrder::Cylinder),       // Cylinder (variant index 3)
-      static_cast<uint8_t>(RenderOrder::Mesh),           // Mesh (variant index 4)
-      static_cast<uint8_t>(RenderOrder::FogBatch),       // FogBatch (variant index 5)
-      static_cast<uint8_t>(RenderOrder::GrassBatch),     // GrassBatch (variant index 6)
-      static_cast<uint8_t>(RenderOrder::StoneBatch),     // StoneBatch (variant index 7)
-      static_cast<uint8_t>(RenderOrder::PlantBatch),     // PlantBatch (variant index 8)
-      static_cast<uint8_t>(RenderOrder::PineBatch),      // PineBatch (variant index 9)
-      static_cast<uint8_t>(RenderOrder::TerrainChunk)    // TerrainChunk (variant index 10)
-    };
+        static_cast<uint8_t>(RenderOrder::Grid),
+        static_cast<uint8_t>(RenderOrder::SelectionRing),
+        static_cast<uint8_t>(RenderOrder::SelectionSmoke),
+        static_cast<uint8_t>(RenderOrder::Cylinder),
+        static_cast<uint8_t>(RenderOrder::Mesh),
+        static_cast<uint8_t>(RenderOrder::FogBatch),
+        static_cast<uint8_t>(RenderOrder::GrassBatch),
+        static_cast<uint8_t>(RenderOrder::StoneBatch),
+        static_cast<uint8_t>(RenderOrder::PlantBatch),
+        static_cast<uint8_t>(RenderOrder::PineBatch),
+        static_cast<uint8_t>(RenderOrder::TerrainChunk)};
 
     const std::size_t typeIndex = cmd.index();
     constexpr std::size_t typeCount =
@@ -313,8 +308,8 @@ private:
       key |= bufferPtr;
     } else if (cmd.index() == PineBatchCmdIndex) {
       const auto &pine = std::get<PineBatchCmdIndex>(cmd);
-      uint64_t bufferPtr = reinterpret_cast<uintptr_t>(pine.instanceBuffer) &
-                           0x0000FFFFFFFFFFFF;
+      uint64_t bufferPtr =
+          reinterpret_cast<uintptr_t>(pine.instanceBuffer) & 0x0000FFFFFFFFFFFF;
       key |= bufferPtr;
     } else if (cmd.index() == TerrainChunkCmdIndex) {
       const auto &terrain = std::get<TerrainChunkCmdIndex>(cmd);

+ 45 - 61
render/gl/backend.cpp

@@ -72,9 +72,11 @@ void Backend::initialize() {
   if (!m_stoneShader)
     qWarning() << "Backend: stone shader missing";
   if (!m_plantShader)
-    qWarning() << "Backend: plant shader missing - check plant_instanced.vert/frag";
+    qWarning()
+        << "Backend: plant shader missing - check plant_instanced.vert/frag";
   if (!m_pineShader)
-    qWarning() << "Backend: pine shader missing - check pine_instanced.vert/frag";
+    qWarning()
+        << "Backend: pine shader missing - check pine_instanced.vert/frag";
   if (!m_groundShader)
     qWarning() << "Backend: ground_plane shader missing";
   if (!m_terrainShader)
@@ -329,15 +331,14 @@ void Backend::execute(const DrawQueue &queue, const Camera &cam) {
     }
     case PlantBatchCmdIndex: {
       const auto &plant = std::get<PlantBatchCmdIndex>(cmd);
-      
+
       if (!plant.instanceBuffer || plant.instanceCount == 0 || !m_plantShader ||
           !m_plantVao || m_plantIndexCount == 0) {
         break;
       }
 
-      // IMPORTANT: Plants need depth testing ENABLED to render behind units
-      DepthMaskScope depthMask(false); // Don't write to depth buffer
-      // But still TEST against it
+      DepthMaskScope depthMask(false);
+
       glEnable(GL_DEPTH_TEST);
       BlendScope blend(true);
       glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
@@ -398,14 +399,13 @@ void Backend::execute(const DrawQueue &queue, const Camera &cam) {
     }
     case PineBatchCmdIndex: {
       const auto &pine = std::get<PineBatchCmdIndex>(cmd);
-      
+
       if (!pine.instanceBuffer || pine.instanceCount == 0 || !m_pineShader ||
           !m_pineVao || m_pineIndexCount == 0) {
         break;
       }
 
-      // Pine trees: similar rendering to plants but taller geometry
-      DepthMaskScope depthMask(false); // Don't write to depth buffer
+      DepthMaskScope depthMask(false);
       glEnable(GL_DEPTH_TEST);
       BlendScope blend(true);
       glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
@@ -427,11 +427,11 @@ void Backend::execute(const DrawQueue &queue, const Camera &cam) {
       }
       if (m_pineUniforms.windStrength != Shader::InvalidUniform) {
         m_pineShader->setUniform(m_pineUniforms.windStrength,
-                                  pine.params.windStrength);
+                                 pine.params.windStrength);
       }
       if (m_pineUniforms.windSpeed != Shader::InvalidUniform) {
         m_pineShader->setUniform(m_pineUniforms.windSpeed,
-                                  pine.params.windSpeed);
+                                 pine.params.windSpeed);
       }
       if (m_pineUniforms.lightDirection != Shader::InvalidUniform) {
         QVector3D lightDir = pine.params.lightDirection;
@@ -454,8 +454,8 @@ void Backend::execute(const DrawQueue &queue, const Camera &cam) {
           reinterpret_cast<void *>(offsetof(PineInstanceGpu, rotation)));
       pine.instanceBuffer->unbind();
 
-      glDrawElementsInstanced(GL_TRIANGLES, m_pineIndexCount,
-                              GL_UNSIGNED_SHORT, nullptr,
+      glDrawElementsInstanced(GL_TRIANGLES, m_pineIndexCount, GL_UNSIGNED_SHORT,
+                              nullptr,
                               static_cast<GLsizei>(pine.instanceCount));
       glBindVertexArray(0);
 
@@ -1370,8 +1370,7 @@ void Backend::cachePineUniforms() {
   if (m_pineShader) {
     m_pineUniforms.viewProj = m_pineShader->uniformHandle("uViewProj");
     m_pineUniforms.time = m_pineShader->uniformHandle("uTime");
-    m_pineUniforms.windStrength =
-        m_pineShader->uniformHandle("uWindStrength");
+    m_pineUniforms.windStrength = m_pineShader->uniformHandle("uWindStrength");
     m_pineUniforms.windSpeed = m_pineShader->uniformHandle("uWindSpeed");
     m_pineUniforms.lightDirection =
         m_pineShader->uniformHandle("uLightDirection");
@@ -1388,28 +1387,23 @@ void Backend::initializePlantPipeline() {
     QVector3D normal;
   };
 
-  // Cross-quad billboard for realistic 3D plant appearance (not grass!)
-  // Creates an X-shape when viewed from above, visible from all angles
   const PlantVertex plantVertices[] = {
-      // First quad (front-back aligned)
-      {{-0.5f, 0.0f, 0.0f}, {0.0f, 0.0f}, {0.0f, 0.0f, 1.0f}},  // bottom left
-      {{0.5f, 0.0f, 0.0f}, {1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}},   // bottom right
-      {{0.5f, 1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 0.0f, 1.0f}},   // top right
-      {{-0.5f, 1.0f, 0.0f}, {0.0f, 1.0f}, {0.0f, 0.0f, 1.0f}},  // top left
-      
-      // First quad back face
+
+      {{-0.5f, 0.0f, 0.0f}, {0.0f, 0.0f}, {0.0f, 0.0f, 1.0f}},
+      {{0.5f, 0.0f, 0.0f}, {1.0f, 0.0f}, {0.0f, 0.0f, 1.0f}},
+      {{0.5f, 1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 0.0f, 1.0f}},
+      {{-0.5f, 1.0f, 0.0f}, {0.0f, 1.0f}, {0.0f, 0.0f, 1.0f}},
+
       {{0.5f, 0.0f, 0.0f}, {0.0f, 0.0f}, {0.0f, 0.0f, -1.0f}},
       {{-0.5f, 0.0f, 0.0f}, {1.0f, 0.0f}, {0.0f, 0.0f, -1.0f}},
       {{-0.5f, 1.0f, 0.0f}, {1.0f, 1.0f}, {0.0f, 0.0f, -1.0f}},
       {{0.5f, 1.0f, 0.0f}, {0.0f, 1.0f}, {0.0f, 0.0f, -1.0f}},
-      
-      // Second quad (perpendicular, left-right aligned for X shape)
-      {{0.0f, 0.0f, -0.5f}, {0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}},  // bottom left
-      {{0.0f, 0.0f, 0.5f}, {1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}},   // bottom right
-      {{0.0f, 1.0f, 0.5f}, {1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}},   // top right
-      {{0.0f, 1.0f, -0.5f}, {0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}},  // top left
-      
-      // Second quad back face
+
+      {{0.0f, 0.0f, -0.5f}, {0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}},
+      {{0.0f, 0.0f, 0.5f}, {1.0f, 0.0f}, {1.0f, 0.0f, 0.0f}},
+      {{0.0f, 1.0f, 0.5f}, {1.0f, 1.0f}, {1.0f, 0.0f, 0.0f}},
+      {{0.0f, 1.0f, -0.5f}, {0.0f, 1.0f}, {1.0f, 0.0f, 0.0f}},
+
       {{0.0f, 0.0f, 0.5f}, {0.0f, 0.0f}, {-1.0f, 0.0f, 0.0f}},
       {{0.0f, 0.0f, -0.5f}, {1.0f, 0.0f}, {-1.0f, 0.0f, 0.0f}},
       {{0.0f, 1.0f, -0.5f}, {1.0f, 1.0f}, {-1.0f, 0.0f, 0.0f}},
@@ -1417,13 +1411,13 @@ void Backend::initializePlantPipeline() {
   };
 
   const unsigned short plantIndices[] = {
-      // First quad front
-      0, 1, 2, 0, 2, 3,
-      // First quad back
-      4, 5, 6, 4, 6, 7,
-      // Second quad front
-      8, 9, 10, 8, 10, 11,
-      // Second quad back
+
+      0,  1,  2,  0,  2,  3,
+
+      4,  5,  6,  4,  6,  7,
+
+      8,  9,  10, 8,  10, 11,
+
       12, 13, 14, 12, 14, 15,
   };
 
@@ -1434,32 +1428,29 @@ void Backend::initializePlantPipeline() {
   glBindBuffer(GL_ARRAY_BUFFER, m_plantVertexBuffer);
   glBufferData(GL_ARRAY_BUFFER, sizeof(plantVertices), plantVertices,
                GL_STATIC_DRAW);
-  m_plantVertexCount = 16;  // 16 vertices for cross-quad
+  m_plantVertexCount = 16;
 
-  // Position attribute
   glEnableVertexAttribArray(0);
   glVertexAttribPointer(
       0, 3, GL_FLOAT, GL_FALSE, sizeof(PlantVertex),
       reinterpret_cast<void *>(offsetof(PlantVertex, position)));
-  // TexCoord attribute
+
   glEnableVertexAttribArray(1);
   glVertexAttribPointer(
       1, 2, GL_FLOAT, GL_FALSE, sizeof(PlantVertex),
       reinterpret_cast<void *>(offsetof(PlantVertex, texCoord)));
-  // Normal attribute
+
   glEnableVertexAttribArray(2);
   glVertexAttribPointer(
       2, 3, GL_FLOAT, GL_FALSE, sizeof(PlantVertex),
       reinterpret_cast<void *>(offsetof(PlantVertex, normal)));
 
-  // Index buffer
   glGenBuffers(1, &m_plantIndexBuffer);
   glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_plantIndexBuffer);
   glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(plantIndices), plantIndices,
                GL_STATIC_DRAW);
-  m_plantIndexCount = 24;  // 24 indices (4 quads × 6 indices)
+  m_plantIndexCount = 24;
 
-  // Instance attributes (will be set per-draw)
   glEnableVertexAttribArray(3);
   glVertexAttribDivisor(3, 1);
   glEnableVertexAttribArray(4);
@@ -1529,12 +1520,10 @@ void Backend::initializePinePipeline() {
   auto connectRings = [&](int lowerStart, int upperStart) {
     for (int i = 0; i < kSegments; ++i) {
       const int next = (i + 1) % kSegments;
-      const unsigned short lower0 =
-          static_cast<unsigned short>(lowerStart + i);
+      const unsigned short lower0 = static_cast<unsigned short>(lowerStart + i);
       const unsigned short lower1 =
           static_cast<unsigned short>(lowerStart + next);
-      const unsigned short upper0 =
-          static_cast<unsigned short>(upperStart + i);
+      const unsigned short upper0 = static_cast<unsigned short>(upperStart + i);
       const unsigned short upper1 =
           static_cast<unsigned short>(upperStart + next);
 
@@ -1573,8 +1562,7 @@ void Backend::initializePinePipeline() {
     indices.push_back(trunkCapIndex);
   }
 
-  const unsigned short apexIndex =
-      static_cast<unsigned short>(vertices.size());
+  const unsigned short apexIndex = static_cast<unsigned short>(vertices.size());
   vertices.push_back({QVector3D(0.0f, 1.18f, 0.0f), QVector2D(0.5f, 1.0f),
                       QVector3D(0.0f, 1.0f, 0.0f)});
   for (int i = 0; i < kSegments; ++i) {
@@ -1594,31 +1582,27 @@ void Backend::initializePinePipeline() {
                vertices.data(), GL_STATIC_DRAW);
   m_pineVertexCount = static_cast<GLsizei>(vertices.size());
 
-  // Position attribute
   glEnableVertexAttribArray(0);
   glVertexAttribPointer(
       0, 3, GL_FLOAT, GL_FALSE, sizeof(PineVertex),
       reinterpret_cast<void *>(offsetof(PineVertex, position)));
-  // TexCoord attribute
+
   glEnableVertexAttribArray(1);
   glVertexAttribPointer(
       1, 2, GL_FLOAT, GL_FALSE, sizeof(PineVertex),
       reinterpret_cast<void *>(offsetof(PineVertex, texCoord)));
-  // Normal attribute
+
   glEnableVertexAttribArray(2);
-  glVertexAttribPointer(
-      2, 3, GL_FLOAT, GL_FALSE, sizeof(PineVertex),
-      reinterpret_cast<void *>(offsetof(PineVertex, normal)));
+  glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, sizeof(PineVertex),
+                        reinterpret_cast<void *>(offsetof(PineVertex, normal)));
 
-  // Index buffer
   glGenBuffers(1, &m_pineIndexBuffer);
   glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_pineIndexBuffer);
   glBufferData(GL_ELEMENT_ARRAY_BUFFER,
                static_cast<GLsizeiptr>(indices.size() * sizeof(unsigned short)),
                indices.data(), GL_STATIC_DRAW);
-  m_pineIndexCount = static_cast<GLsizei>(indices.size());  // triangles * 3
+  m_pineIndexCount = static_cast<GLsizei>(indices.size());
 
-  // Instance attributes (will be set per-draw)
   glEnableVertexAttribArray(3);
   glVertexAttribDivisor(3, 1);
   glEnableVertexAttribArray(4);

+ 5 - 7
render/ground/pine_gpu.h

@@ -6,19 +6,17 @@
 
 namespace Render::GL {
 
-// GPU instance data for pine trees
 struct PineInstanceGpu {
-  QVector4D posScale;    // xyz = world position, w = scale multiplier
-  QVector4D colorSway;   // rgb = tint color, a = sway phase offset
-  QVector4D rotation;    // x = Y-axis rotation, yzw = reserved for future use
+  QVector4D posScale;
+  QVector4D colorSway;
+  QVector4D rotation;
 };
 
-// Parameters for pine tree batch rendering
 struct PineBatchParams {
   QVector3D lightDirection{0.35f, 0.8f, 0.45f};
   float time = 0.0f;
-  float windStrength = 0.3f;  // Gentler than plants
-  float windSpeed = 0.5f;     // Slower than plants
+  float windStrength = 0.3f;
+  float windSpeed = 0.5f;
 };
 
 } // namespace Render::GL

+ 18 - 37
render/ground/pine_renderer.cpp

@@ -60,7 +60,7 @@ PineRenderer::PineRenderer() = default;
 PineRenderer::~PineRenderer() = default;
 
 void PineRenderer::configure(const Game::Map::TerrainHeightMap &heightMap,
-                              const Game::Map::BiomeSettings &biomeSettings) {
+                             const Game::Map::BiomeSettings &biomeSettings) {
   m_width = heightMap.getWidth();
   m_height = heightMap.getHeight();
   m_tileSize = heightMap.getTileSize();
@@ -76,15 +76,15 @@ void PineRenderer::configure(const Game::Map::TerrainHeightMap &heightMap,
 
   m_pineParams.lightDirection = QVector3D(0.35f, 0.8f, 0.45f);
   m_pineParams.time = 0.0f;
-  m_pineParams.windStrength = 0.3f;  // Gentler than plants
-  m_pineParams.windSpeed = 0.5f;     // Slower than plants
+  m_pineParams.windStrength = 0.3f;
+  m_pineParams.windSpeed = 0.5f;
 
   generatePineInstances();
 }
 
 void PineRenderer::submit(Renderer &renderer, ResourceManager *resources) {
   (void)resources;
-  
+
   m_pineInstanceCount = static_cast<uint32_t>(m_pineInstances.size());
 
   if (m_pineInstanceCount > 0) {
@@ -103,8 +103,7 @@ void PineRenderer::submit(Renderer &renderer, ResourceManager *resources) {
   if (m_pineInstanceBuffer && m_pineInstanceCount > 0) {
     PineBatchParams params = m_pineParams;
     params.time = renderer.getAnimationTime();
-    renderer.pineBatch(m_pineInstanceBuffer.get(), m_pineInstanceCount,
-                        params);
+    renderer.pineBatch(m_pineInstanceBuffer.get(), m_pineInstanceCount, params);
   }
 }
 
@@ -126,14 +125,12 @@ void PineRenderer::generatePineInstances() {
   const float halfHeight = static_cast<float>(m_height) * 0.5f;
   const float tileSafe = std::max(0.1f, m_tileSize);
 
-  // Pine density from biome settings (default 0.2, much lower than plants)
   float pineDensity = 0.2f;
   if (m_biomeSettings.plantDensity > 0.0f) {
-    // Reuse plantDensity but at reduced rate for trees
+
     pineDensity = m_biomeSettings.plantDensity * 0.3f;
   }
 
-  // Pre-compute normals for slope calculation
   std::vector<QVector3D> normals(m_width * m_height, QVector3D(0, 1, 0));
   for (int z = 1; z < m_height - 1; ++z) {
     for (int x = 1; x < m_width - 1; ++x) {
@@ -161,12 +158,9 @@ void PineRenderer::generatePineInstances() {
     int iz = std::clamp(int(std::floor(sgz + 0.5f)), 0, m_height - 1);
     int normalIdx = iz * m_width + ix;
 
-    // Pines prefer hills and moderate terrain
-    // Avoid extremely steep slopes
     QVector3D normal = normals[normalIdx];
     float slope = 1.0f - std::clamp(normal.y(), 0.0f, 1.0f);
 
-    // Pines can handle more slope than plants but not extreme
     if (slope > 0.75f)
       return false;
 
@@ -180,48 +174,40 @@ void PineRenderer::generatePineInstances() {
       return false;
     }
 
-    // Pines are TALL trees (3-6 units instead of 0.3-0.8 for plants)
     float scale = remap(rand01(state), 3.0f, 6.0f) * tileSafe;
 
-    // Pine color variation (darker green/brown tones)
     float colorVar = remap(rand01(state), 0.0f, 1.0f);
-    QVector3D baseColor(0.15f, 0.35f, 0.20f);  // Dark pine green
-    QVector3D varColor(0.20f, 0.40f, 0.25f);   // Slightly lighter
+    QVector3D baseColor(0.15f, 0.35f, 0.20f);
+    QVector3D varColor(0.20f, 0.40f, 0.25f);
     QVector3D tintColor = baseColor * (1.0f - colorVar) + varColor * colorVar;
 
-    // Add brownish tint for trunk variation
     float brownMix = remap(rand01(state), 0.10f, 0.25f);
     QVector3D brownTint(0.35f, 0.30f, 0.20f);
     tintColor = tintColor * (1.0f - brownMix) + brownTint * brownMix;
 
-  // Sway parameters (gentler and slower than plants)
-  float swayPhase = rand01(state) * 6.2831853f;
+    float swayPhase = rand01(state) * 6.2831853f;
 
-  // Y-axis rotation for variety
-  float rotation = rand01(state) * 6.2831853f;
+    float rotation = rand01(state) * 6.2831853f;
 
-  // Additional per-instance variation for silhouette and shading
-  float silhouetteSeed = rand01(state);
-  float needleSeed = rand01(state);
-  float barkSeed = rand01(state);
+    float silhouetteSeed = rand01(state);
+    float needleSeed = rand01(state);
+    float barkSeed = rand01(state);
 
     PineInstanceGpu instance;
-    // Pine trees on ground level (not elevated like plants were)
+
     instance.posScale = QVector4D(worldX, worldY, worldZ, scale);
     instance.colorSway =
         QVector4D(tintColor.x(), tintColor.y(), tintColor.z(), swayPhase);
-  instance.rotation = QVector4D(rotation, silhouetteSeed, needleSeed, barkSeed);
+    instance.rotation =
+        QVector4D(rotation, silhouetteSeed, needleSeed, barkSeed);
     m_pineInstances.push_back(instance);
     return true;
   };
 
-  // Generate pines in a sparse grid pattern (every 6 tiles instead of 3)
-  // Lower density for trees compared to plants
   for (int z = 0; z < m_height; z += 6) {
     for (int x = 0; x < m_width; x += 6) {
       int idx = z * m_width + x;
 
-      // Check terrain slope - avoid steep areas
       QVector3D normal = normals[idx];
       float slope = 1.0f - std::clamp(normal.y(), 0.0f, 1.0f);
       if (slope > 0.75f)
@@ -233,30 +219,25 @@ void PineRenderer::generatePineInstances() {
       float worldX = (x - halfWidth) * m_tileSize;
       float worldZ = (z - halfHeight) * m_tileSize;
 
-      // Use forest/tree clustering noise
       float clusterNoise =
           valueNoise(worldX * 0.03f, worldZ * 0.03f, m_noiseSeed ^ 0x7F8E9D0Au);
 
-      // Pines cluster in forest areas
       if (clusterNoise < 0.35f)
         continue;
 
-      // Terrain-based density multiplier
       float densityMult = 1.0f;
       if (m_terrainTypes[idx] == Game::Map::TerrainType::Hill) {
-        densityMult = 1.2f; // More trees on hills
+        densityMult = 1.2f;
       } else if (m_terrainTypes[idx] == Game::Map::TerrainType::Mountain) {
-        densityMult = 0.4f; // Fewer on mountains
+        densityMult = 0.4f;
       }
 
-      // Calculate pine count (much lower than plants)
       float effectiveDensity = pineDensity * densityMult * 0.8f;
       int pineCount = static_cast<int>(std::floor(effectiveDensity));
       float frac = effectiveDensity - float(pineCount);
       if (rand01(state) < frac)
         pineCount += 1;
 
-      // Place pines in small clusters
       for (int i = 0; i < pineCount; ++i) {
         float gx = float(x) + rand01(state) * 6.0f;
         float gz = float(z) + rand01(state) * 6.0f;

+ 3 - 4
render/ground/plant_gpu.h

@@ -6,10 +6,9 @@
 namespace Render::GL {
 
 struct PlantInstanceGpu {
-  QVector4D posScale;   // xyz=world pos, w=scale
-  QVector4D colorSway;  // rgb=tint color, a=sway phase
-  QVector4D typeParams; // x=plant type, y=rotation, z=sway strength, w=sway
-                        // speed
+  QVector4D posScale;
+  QVector4D colorSway;
+  QVector4D typeParams;
 };
 
 struct PlantBatchParams {

+ 11 - 26
render/ground/plant_renderer.cpp

@@ -83,8 +83,8 @@ void PlantRenderer::configure(const Game::Map::TerrainHeightMap &heightMap,
 }
 
 void PlantRenderer::submit(Renderer &renderer, ResourceManager *resources) {
-  (void)resources; // Unused parameter
-  
+  (void)resources;
+
   m_plantInstanceCount = static_cast<uint32_t>(m_plantInstances.size());
 
   if (m_plantInstanceCount > 0) {
@@ -124,12 +124,11 @@ void PlantRenderer::generatePlantInstances() {
     return;
   }
 
-  // Use plant density from biome settings (default to moderate density)
   const float plantDensity =
       std::clamp(m_biomeSettings.plantDensity, 0.0f, 2.0f);
-  qDebug() << "PlantRenderer: plantDensity =" << plantDensity 
+  qDebug() << "PlantRenderer: plantDensity =" << plantDensity
            << "from biome settings";
-  
+
   if (plantDensity < 0.01f) {
     qDebug() << "PlantRenderer: plantDensity too low, skipping generation";
     m_plantInstanceCount = 0;
@@ -162,7 +161,6 @@ void PlantRenderer::generatePlantInstances() {
     return h0 * (1.0f - tz) + h1 * tz;
   };
 
-  // Compute normals for slope detection
   for (int z = 0; z < m_height; ++z) {
     for (int x = 0; x < m_width; ++x) {
       int idx = z * m_width + x;
@@ -196,14 +194,12 @@ void PlantRenderer::generatePlantInstances() {
     int iz = std::clamp(int(std::floor(sgz + 0.5f)), 0, m_height - 1);
     int normalIdx = iz * m_width + ix;
 
-    // Plants prefer flat terrain, avoid mountains
     if (m_terrainTypes[normalIdx] == Game::Map::TerrainType::Mountain)
       return false;
 
     QVector3D normal = normals[normalIdx];
     float slope = 1.0f - std::clamp(normal.y(), 0.0f, 1.0f);
 
-    // Avoid steep slopes
     if (slope > 0.65f)
       return false;
 
@@ -217,24 +213,19 @@ void PlantRenderer::generatePlantInstances() {
       return false;
     }
 
-    // Plants are taller than grass for visibility
     float scale = remap(rand01(state), 0.30f, 0.80f) * tileSafe;
 
-    // Plant type variation (0-3 for different plant models)
     float plantType = std::floor(rand01(state) * 4.0f);
 
-    // Plants use darker, more saturated colors than grass
     float colorVar = remap(rand01(state), 0.0f, 1.0f);
-    QVector3D baseColor = m_biomeSettings.grassPrimary * 0.7f; // Darker than grass
+    QVector3D baseColor = m_biomeSettings.grassPrimary * 0.7f;
     QVector3D varColor = m_biomeSettings.grassSecondary * 0.8f;
     QVector3D tintColor = baseColor * (1.0f - colorVar) + varColor * colorVar;
 
-    // Add brownish/woody tint for plant stems and variety
     float brownMix = remap(rand01(state), 0.15f, 0.35f);
     QVector3D brownTint(0.55f, 0.50f, 0.35f);
     tintColor = tintColor * (1.0f - brownMix) + brownTint * brownMix;
 
-    // Sway parameters
     float swayPhase = rand01(state) * 6.2831853f;
     float swayStrength = remap(rand01(state), 0.6f, 1.2f);
     float swaySpeed = remap(rand01(state), 0.8f, 1.3f);
@@ -242,26 +233,25 @@ void PlantRenderer::generatePlantInstances() {
     float rotation = rand01(state) * 6.2831853f;
 
     PlantInstanceGpu instance;
-    // Plants elevated slightly above terrain to avoid z-fighting
+
     instance.posScale = QVector4D(worldX, worldY + 0.05f, worldZ, scale);
     instance.colorSway =
         QVector4D(tintColor.x(), tintColor.y(), tintColor.z(), swayPhase);
-    instance.typeParams = QVector4D(plantType, rotation, swayStrength, swaySpeed);
+    instance.typeParams =
+        QVector4D(plantType, rotation, swayStrength, swaySpeed);
     m_plantInstances.push_back(instance);
     return true;
   };
 
-  // Generate plants in a grid pattern with clustering
   int cellsChecked = 0;
   int cellsPassed = 0;
   int plantsAdded = 0;
-  
+
   for (int z = 0; z < m_height; z += 3) {
     for (int x = 0; x < m_width; x += 3) {
       cellsChecked++;
       int idx = z * m_width + x;
 
-      // Skip mountains completely
       if (m_terrainTypes[idx] == Game::Map::TerrainType::Mountain)
         continue;
 
@@ -276,30 +266,25 @@ void PlantRenderer::generatePlantInstances() {
       float worldX = (x - halfWidth) * m_tileSize;
       float worldZ = (z - halfHeight) * m_tileSize;
 
-      // Use clustering noise to create natural-looking plant distribution
       float clusterNoise =
           valueNoise(worldX * 0.05f, worldZ * 0.05f, m_noiseSeed ^ 0x4B9D2F1Au);
 
-      // Plants cluster in certain areas
       if (clusterNoise < 0.45f)
         continue;
-      
+
       cellsPassed++;
 
-      // Adjust density based on terrain type
       float densityMult = 1.0f;
       if (m_terrainTypes[idx] == Game::Map::TerrainType::Hill) {
-        densityMult = 0.6f; // Fewer plants on hills
+        densityMult = 0.6f;
       }
 
-      // Calculate plant count (higher multiplier for visibility)
       float effectiveDensity = plantDensity * densityMult * 2.0f;
       int plantCount = static_cast<int>(std::floor(effectiveDensity));
       float frac = effectiveDensity - float(plantCount);
       if (rand01(state) < frac)
         plantCount += 1;
 
-      // Place plants in small clusters
       for (int i = 0; i < plantCount; ++i) {
         float gx = float(x) + rand01(state) * 3.0f;
         float gz = float(z) + rand01(state) * 3.0f;