Browse Source

Improve terrain rendering with enhanced shaders for realism (#680)

* Initial plan

* Enhance hill rendering for more natural and realistic appearance

- Improve hill color blending with smoother transitions using smoothstep interpolation
- Add multi-stage grass color blending from low to high elevations
- Reduce rock exposure on hills (0.06 vs 0.08) for gentler appearance
- Lower slope mix factor (0.55 vs 0.60) for less aggressive rock transitions
- Add height noise variation (1.12x multiplier) for natural terrain texture
- Adjust ambient lighting (0.97x) for better hill illumination
- Fine-tune ridge detection with adjusted convexity threshold (-0.02 to 0.18)
- Reduce soil height offset (0.04 vs 0.06) for more vegetation coverage
- Lower sharpness multiplier (1.15 vs 1.25) for softer slope transitions

Co-authored-by: djeada <[email protected]>

* Enhance terrain shaders for higher realism and visual quality

Fragment Shader Improvements:
- Enhanced FBM with 5 octaves (up from 4) for smoother noise patterns
- Added fbmDetail function with 6 octaves for fine surface variation
- Implemented micro-variation layer for subtle grass color differences
- Added height-based grass dryness gradient for natural elevation effects
- Enhanced rock color with detail variation using fbmDetail
- Improved micro-normal calculation with finer offset (0.008 vs 0.01)
- Added secondary fine normal perturbation for rock surface detail
- Implemented curvature-based ambient occlusion for better depth
- Enhanced specular lighting with Blinn-Phong model and view-dependent highlights
- Increased micro-detail amplitude (0.18 vs 0.15) for more pronounced surface features
- Adjusted ambient lighting (0.32 with AO) for more realistic shadowing
- Refined surface roughness for better specular response
- Increased diffuse contribution (0.78 vs 0.75) for brighter terrain

Vertex Shader Improvements:
- Enhanced FBM to 3 octaves (up from 2) with refined frequency constants
- Implemented dual-layer height displacement for more natural terrain variation
- Added second displacement layer at 2.7x frequency mixed at 35%
- Improved displacement factor with squared flatness for smoother transitions
- Adjusted base displacement factor (0.35 vs 0.4) for subtler steep-slope effects

Visual Impact:
- More realistic surface micro-detail and texture variation
- Better lighting with proper ambient occlusion in concave areas
- Enhanced specular highlights on wet or smooth surfaces
- Natural height-based color transitions on hills
- Improved rock surface appearance with varied detail
- Smoother terrain displacement with multi-octave noise

Co-authored-by: djeada <[email protected]>

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: djeada <[email protected]>
Copilot 2 months ago
parent
commit
de367d61eb

+ 48 - 12
assets/shaders/terrain_chunk.frag

@@ -54,7 +54,7 @@ float noise21(vec2 p) {
 
 float fbm(vec2 p) {
   float v = 0.0, a = 0.5;
-  for (int i = 0; i < 4; ++i) {
+  for (int i = 0; i < 5; ++i) {
     v += noise21(p) * a;
     p = p * 2.07 + 13.17;
     a *= 0.5;
@@ -62,6 +62,16 @@ float fbm(vec2 p) {
   return v;
 }
 
+float fbmDetail(vec2 p) {
+  float v = 0.0, a = 0.5;
+  for (int i = 0; i < 6; ++i) {
+    v += noise21(p) * a;
+    p = p * 2.13 + 7.89;
+    a *= 0.52;
+  }
+  return v;
+}
+
 vec3 triplanarWeights(vec3 n) {
   vec3 b = abs(n);
   b = pow(b, vec3(4.0));
@@ -145,6 +155,7 @@ void main() {
       triplanarNoise(v_worldPos, u_detailNoiseScale * 2.5 / tileScale);
   float erosionNoise =
       triplanarNoise(v_worldPos, u_detailNoiseScale * 4.0 / tileScale + 17.0);
+  float microVariation = fbmDetail(world_coord * u_macroNoiseScale * 8.0);
 
   float patchNoise = fbm(world_coord * u_macroNoiseScale * 0.4);
   float moistureVar = smoothstep(0.3, 0.7, patchNoise);
@@ -153,7 +164,11 @@ void main() {
   vec3 lushGrass = mix(u_grassPrimary, u_grassSecondary, lushFactor);
   float dryness = clamp(0.55 * slope + 0.45 * detailNoise, 0.0, 1.0);
   dryness += moistureVar * 0.15;
-  vec3 grassColor = mix(lushGrass, u_grassDry, dryness);
+  
+  float heightFade = smoothstep(0.0, 2.5, v_worldPos.y);
+  float drynessByHeight = mix(dryness, dryness * 1.15, heightFade * 0.4);
+  vec3 grassColor = mix(lushGrass, u_grassDry, drynessByHeight);
+  grassColor *= (1.0 + microVariation * 0.08);
 
   float soilWidth = max(0.01, 1.0 / max(u_soilBlendSharpness, 0.001));
 
@@ -209,12 +224,15 @@ void main() {
 
   float rockLerp = clamp(0.35 + detailNoise * 0.65, 0.0, 1.0);
   vec3 rockColor = mix(u_rockLow, u_rockHigh, rockLerp);
-  rockColor = mix(rockColor, rockColor * 1.15,
-                  clamp(u_rockDetailStrength * 1.4, 0.0, 1.0));
+  
+  float rockDetailVariation = fbmDetail(world_coord * 0.15) * 0.5 + 0.5;
+  rockColor *= mix(0.92, 1.08, rockDetailVariation);
+  rockColor = mix(rockColor, rockColor * 1.12,
+                  clamp(u_rockDetailStrength * 1.3, 0.0, 1.0));
 
   vec3 microNormal = normal;
   float microDetailScale = u_detailNoiseScale * 8.0 / tileScale;
-  vec2 microOffset = vec2(0.01, 0.0);
+  vec2 microOffset = vec2(0.008, 0.0);
   float h0 = triplanarNoise(v_worldPos, microDetailScale);
   float hx = triplanarNoise(v_worldPos + vec3(microOffset.x, 0.0, 0.0),
                             microDetailScale);
@@ -222,8 +240,16 @@ void main() {
                             microDetailScale);
   vec3 microGrad =
       vec3((hx - h0) / microOffset.x, 0.0, (hz - h0) / microOffset.x);
-  float microAmp = 0.15 * u_rockDetailStrength * (0.2 + 0.8 * slope);
+  float microAmp = 0.18 * u_rockDetailStrength * (0.15 + 0.85 * slope);
   microNormal = normalize(normal + microGrad * microAmp);
+  
+  float fineDetail = triplanarNoise(v_worldPos, microDetailScale * 2.5);
+  vec3 fineNormalPerturb = vec3(
+    (fineDetail - 0.5) * 0.03,
+    0.0,
+    (triplanarNoise(v_worldPos + vec3(0.1, 0.0, 0.0), microDetailScale * 2.5) - 0.5) * 0.03
+  );
+  microNormal = normalize(microNormal + fineNormalPerturb * (0.3 + 0.7 * rockMask));
 
   float isFlat = 1.0 - smoothstep(0.10, 0.25, slope);
   float isHigh = smoothstep(u_soilBlendHeight + 0.5, u_soilBlendHeight + 1.5,
@@ -284,17 +310,27 @@ void main() {
 
   vec3 L = normalize(u_lightDir);
   float ndl = max(dot(microNormal, L), 0.0);
-  float ambient = 0.35;
+  
+  float skyOcclusion = smoothstep(-0.03, 0.01, -curvature);
+  float ao = mix(1.0, 0.75, skyOcclusion * (1.0 - slope * 0.5));
+  
+  float ambient = 0.32 * ao;
   float fresnel =
-      pow(1.0 - max(dot(microNormal, vec3(0.0, 1.0, 0.0)), 0.0), 2.0);
+      pow(1.0 - max(dot(microNormal, vec3(0.0, 1.0, 0.0)), 0.0), 2.2);
 
   float surfaceRoughness = mix(0.65, 0.95, u_soilRoughness);
-  surfaceRoughness = mix(surfaceRoughness, 0.45, u_moistureLevel * 0.5);
+  surfaceRoughness = mix(surfaceRoughness, 0.42, u_moistureLevel * 0.5);
+  
+  vec3 viewDir = normalize(vec3(0.0, 1.0, -0.5));
+  vec3 halfDir = normalize(L + viewDir);
+  float specAngle = max(dot(microNormal, halfDir), 0.0);
+  float specular = pow(specAngle, mix(4.0, 32.0, 1.0 - surfaceRoughness));
+  
   float specContrib =
-      fresnel * 0.12 * (1.0 - surfaceRoughness) * (1.0 - rockMask);
+      fresnel * 0.14 * (1.0 - surfaceRoughness) * (1.0 - rockMask) * specular;
 
-  specContrib += u_moistureLevel * 0.08 * fresnel * (1.0 - rockMask);
-  float shade = ambient + ndl * 0.75 + specContrib;
+  specContrib += u_moistureLevel * 0.10 * fresnel * (1.0 - rockMask);
+  float shade = ambient + ndl * 0.78 + specContrib;
 
   float plateauBrightness = 1.0 + plateauFactor * 0.05;
   float gullyDarkness = 1.0 - isGully * 0.04;

+ 8 - 6
assets/shaders/terrain_chunk.vert

@@ -32,13 +32,13 @@ float noise21(vec2 p) {
   return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
 }
 
-float fbm2(vec2 p) {
+float fbm3(vec2 p) {
   float value = 0.0;
   float amplitude = 0.5;
-  for (int i = 0; i < 2; ++i) {
+  for (int i = 0; i < 3; ++i) {
     value += noise21(p) * amplitude;
-    p = p * 2.07 + 13.17;
-    amplitude *= 0.5;
+    p = p * 2.13 + 11.47;
+    amplitude *= 0.48;
   }
   return value;
 }
@@ -60,11 +60,13 @@ void main() {
 
   vec2 uv = rot2(angle) * (wp.xz + u_noiseOffset);
 
-  float h = fbm2(uv * u_heightNoiseFrequency) * 2.0 - 1.0;
+  float h1 = fbm3(uv * u_heightNoiseFrequency) * 2.0 - 1.0;
+  float h2 = fbm3(uv * u_heightNoiseFrequency * 2.7) * 2.0 - 1.0;
+  float h = mix(h1, h2, 0.35);
 
   float flatness = clamp(worldNormal.y, 0.0, 1.0);
 
-  float displacementFactor = mix(0.4, 1.0, flatness);
+  float displacementFactor = mix(0.35, 1.0, flatness * flatness);
 
   float heightAmp = clamp(u_heightNoiseStrength, 0.0, 0.20);
 

+ 26 - 11
render/ground/terrain_renderer.cpp

@@ -263,7 +263,7 @@ void TerrainRenderer::build_meshes() {
         const float slope = 1.0F - std::clamp(n0.y(), 0.0F, 1.0F);
 
         const float ridge_s = smooth(0.35F, 0.70F, slope);
-        const float ridge_c = smooth(0.00F, 0.20F, convexity);
+        const float ridge_c = smooth(-0.02F, 0.18F, convexity);
         const float ridge_factor =
             std::clamp(0.5F * ridge_s + 0.5F * ridge_c, 0.0F, 1.0F);
         const float base_boost = 0.6F * (1.0F - nh);
@@ -557,7 +557,7 @@ void TerrainRenderer::build_meshes() {
         float slope_mix = std::clamp(
             avg_slope * ((chunk.type == Game::Map::TerrainType::Flat) ? 0.30F
                          : (chunk.type == Game::Map::TerrainType::Hill)
-                             ? 0.60F
+                             ? 0.55F
                              : 0.90F),
             0.0F, 1.0F);
 
@@ -627,8 +627,8 @@ void TerrainRenderer::build_meshes() {
         float slope_threshold = m_biome_settings.terrain_rock_threshold;
         float sharpness_mul = 1.0F;
         if (chunk.type == Game::Map::TerrainType::Hill) {
-          slope_threshold -= 0.08F;
-          sharpness_mul = 1.25F;
+          slope_threshold -= 0.06F;
+          sharpness_mul = 1.15F;
         } else if (chunk.type == Game::Map::TerrainType::Mountain) {
           slope_threshold -= 0.16F;
           sharpness_mul = 1.60F;
@@ -645,7 +645,7 @@ void TerrainRenderer::build_meshes() {
 
         float soil_height = m_biome_settings.terrain_soil_height;
         if (chunk.type == Game::Map::TerrainType::Hill) {
-          soil_height -= 0.06F;
+          soil_height -= 0.04F;
         } else if (chunk.type == Game::Map::TerrainType::Mountain) {
           soil_height -= 0.12F;
         }
@@ -670,7 +670,9 @@ void TerrainRenderer::build_meshes() {
         float base_amp =
             m_biome_settings.height_noise_amplitude *
             (0.7F + 0.3F * std::clamp(roughness * 0.6F, 0.0F, 1.0F));
-        if (chunk.type == Game::Map::TerrainType::Mountain) {
+        if (chunk.type == Game::Map::TerrainType::Hill) {
+          base_amp *= 1.12F;
+        } else if (chunk.type == Game::Map::TerrainType::Mountain) {
           base_amp *= 1.25F;
         }
         base_amp *= (1.0F + 0.10F * edge_factor - 0.08F * plateau_factor -
@@ -680,7 +682,9 @@ void TerrainRenderer::build_meshes() {
 
         params.ambient_boost =
             m_biome_settings.terrain_ambient_boost *
-            ((chunk.type == Game::Map::TerrainType::Mountain) ? 0.90F : 0.95F);
+            ((chunk.type == Game::Map::TerrainType::Hill) ? 0.97F
+             : (chunk.type == Game::Map::TerrainType::Mountain) ? 0.90F 
+             : 0.95F);
         params.rock_detail_strength =
             m_biome_settings.terrain_rock_detail_strength *
             (0.75F + 0.35F * std::clamp(avg_slope * 1.2F, 0.0F, 1.0F) +
@@ -709,11 +713,22 @@ auto TerrainRenderer::getTerrainColor(Game::Map::TerrainType type,
     return m_biome_settings.rock_low;
   case Game::Map::TerrainType::Hill: {
     float const t = std::clamp(height / 3.0F, 0.0F, 1.0F);
-    QVector3D const grass = m_biome_settings.grass_secondary * (1.0F - t) +
-                            m_biome_settings.grass_dry * t;
+    float const t_smooth = t * t * (3.0F - 2.0F * t);
+    
+    QVector3D const grass_low = m_biome_settings.grass_primary * 0.3F + 
+                                m_biome_settings.grass_secondary * 0.7F;
+    QVector3D const grass_high = m_biome_settings.grass_secondary * 0.6F +
+                                 m_biome_settings.grass_dry * 0.4F;
+    QVector3D const grass = grass_low * (1.0F - t_smooth) + grass_high * t_smooth;
+    
     QVector3D const rock =
-        m_biome_settings.rock_low * (1.0F - t) + m_biome_settings.rock_high * t;
-    float const rock_blend = std::clamp(0.25F + 0.5F * t, 0.0F, 0.75F);
+        m_biome_settings.rock_low * (1.0F - t_smooth) + 
+        m_biome_settings.rock_high * t_smooth;
+    
+    float const rock_blend_base = 0.15F + 0.45F * t_smooth;
+    float const height_factor = std::clamp((height - 0.5F) * 0.3F, 0.0F, 0.25F);
+    float const rock_blend = std::clamp(rock_blend_base + height_factor, 0.0F, 0.70F);
+    
     return grass * (1.0F - rock_blend) + rock * rock_blend;
   }
   case Game::Map::TerrainType::Flat: