Browse Source

Redesign river banks with multi-ring cross-section and volumetric geometry

Co-authored-by: djeada <[email protected]>
copilot-swe-agent[bot] 2 months ago
parent
commit
713624ef19
1 changed files with 250 additions and 112 deletions
  1. 250 112
      render/ground/riverbank_renderer.cpp

+ 250 - 112
render/ground/riverbank_renderer.cpp

@@ -103,8 +103,11 @@ void RiverbankRenderer::build_meshes() {
     QVector3D const perpendicular(-dir.z(), 0.0F, dir.x());
     float const half_width = segment.width * 0.5F;
 
-    float const bank_width = 0.8F;
-
+    // Multi-ring cross-section for volumetric bank geometry
+    // Each ring has a distance from water edge and height profile
+    constexpr int k_rings_per_side = 5; // water_edge, inner_mid, crest, outer_mid, terrain_blend
+    constexpr int k_total_rings = k_rings_per_side * 2; // both sides
+    
     int length_steps =
         static_cast<int>(std::ceil(length / (m_tile_size * 0.5F))) + 1;
     length_steps = std::max(length_steps, 8);
@@ -139,119 +142,254 @@ void RiverbankRenderer::build_meshes() {
       QVector3D const center_offset = perpendicular * meander;
       center_pos += center_offset;
 
-      QVector3D const inner_left =
-          center_pos - perpendicular * (half_width + width_variation);
-      QVector3D const inner_right =
-          center_pos + perpendicular * (half_width + width_variation);
-      samples.push_back(inner_left);
-      samples.push_back(inner_right);
-
-      float const outer_variation =
-          noise(center_pos.x() * 8.0F, center_pos.z() * 8.0F) * 0.5F;
-      QVector3D const outer_left =
-          inner_left - perpendicular * (bank_width + outer_variation);
-      QVector3D const outer_right =
-          inner_right + perpendicular * (bank_width + outer_variation);
-
-      Vertex left_inner;
-      Vertex left_outer;
-      float const height_inner_left =
-          sample_height(inner_left.x(), inner_left.z());
-      float const height_outer_left =
-          sample_height(outer_left.x(), outer_left.z());
-
-      // Create a valley/trough effect: outer edges at terrain level, inner edges below
-      // This creates a two-sided hill that slopes DOWN toward the river from both sides
-      // Water sits in this depression, hidden from horizontal viewing angles
-      float const bank_height_inner = -0.35F;  // Below terrain (near water)
-      float const bank_height_outer = 0.05F;   // At or slightly above terrain
-
-      left_inner.position[0] = inner_left.x();
-      left_inner.position[1] = height_inner_left + bank_height_inner;
-      left_inner.position[2] = inner_left.z();
+      // Define cross-section profile for both sides
+      // Heights: water edge slightly above water, crest raised, outer blends to terrain
+      struct RingProfile {
+        float distance_from_water; // perpendicular distance from water edge
+        float height_offset;        // height above/below terrain
+      };
+      
+      constexpr RingProfile k_left_rings[k_rings_per_side] = {
+        {0.0F, 0.05F},    // water edge - just above water
+        {0.25F, 0.35F},   // inner midslope
+        {0.5F, 0.6F},     // crest - peak of bank
+        {0.75F, 0.25F},   // outer midslope
+        {1.0F, 0.0F}      // terrain blend
+      };
       
-      left_outer.position[0] = outer_left.x();
-      left_outer.position[1] = height_outer_left + bank_height_outer;
-      left_outer.position[2] = outer_left.z();
-
-      // Calculate slope normal for the bank (valley geometry slopes down toward water)
-      // Convert vertex positions to QVector3D for easier manipulation
-      QVector3D left_inner_pos(left_inner.position[0], left_inner.position[1], left_inner.position[2]);
-      QVector3D left_outer_pos(left_outer.position[0], left_outer.position[1], left_outer.position[2]);
-      QVector3D bank_left_vec = left_outer_pos - left_inner_pos;
-      QVector3D tangent = dir;
-      // Left bank: cross product gives inward-facing normal (toward river due to valley)
-      QVector3D slope_normal = QVector3D::crossProduct(bank_left_vec, tangent).normalized();
-
-      left_inner.normal[0] = slope_normal.x();
-      left_inner.normal[1] = slope_normal.y();
-      left_inner.normal[2] = slope_normal.z();
-      left_inner.tex_coord[0] = 0.0F;
-      left_inner.tex_coord[1] = t;
-      vertices.push_back(left_inner);
-
-      left_outer.normal[0] = slope_normal.x();
-      left_outer.normal[1] = slope_normal.y();
-      left_outer.normal[2] = slope_normal.z();
-      left_outer.tex_coord[0] = 1.0F;
-      left_outer.tex_coord[1] = t;
-      vertices.push_back(left_outer);
-
-      Vertex right_inner;
-      Vertex right_outer;
-      float const height_inner_right =
-          sample_height(inner_right.x(), inner_right.z());
-      float const height_outer_right =
-          sample_height(outer_right.x(), outer_right.z());
-
-      right_inner.position[0] = inner_right.x();
-      right_inner.position[1] = height_inner_right + bank_height_inner;
-      right_inner.position[2] = inner_right.z();
-
-      right_outer.position[0] = outer_right.x();
-      right_outer.position[1] = height_outer_right + bank_height_outer;
-      right_outer.position[2] = outer_right.z();
-
-      // Calculate slope normal for the right bank (valley geometry slopes down toward water)
-      // Convert vertex positions to QVector3D for easier manipulation
-      QVector3D right_inner_pos(right_inner.position[0], right_inner.position[1], right_inner.position[2]);
-      QVector3D right_outer_pos(right_outer.position[0], right_outer.position[1], right_outer.position[2]);
-      QVector3D bank_right_vec = right_outer_pos - right_inner_pos;
-      // Right bank: reversed cross product gives inward-facing normal (toward river, opposite side)
-      QVector3D slope_normal_right = QVector3D::crossProduct(tangent, bank_right_vec).normalized();
-
-      right_inner.normal[0] = slope_normal_right.x();
-      right_inner.normal[1] = slope_normal_right.y();
-      right_inner.normal[2] = slope_normal_right.z();
-      right_inner.tex_coord[0] = 0.0F;
-      right_inner.tex_coord[1] = t;
-      vertices.push_back(right_inner);
-
-      right_outer.normal[0] = slope_normal_right.x();
-      right_outer.normal[1] = slope_normal_right.y();
-      right_outer.normal[2] = slope_normal_right.z();
-      right_outer.tex_coord[0] = 1.0F;
-      right_outer.tex_coord[1] = t;
-      vertices.push_back(right_outer);
+      // Add some noise-based width variation to ring distances
+      float const ring_noise = noise(center_pos.x() * 3.0F, center_pos.z() * 3.0F) * 0.15F;
+      float const base_bank_width = 1.0F + ring_noise;
 
+      // Build vertices for left bank rings
+      unsigned int const ring_start_idx = static_cast<unsigned int>(vertices.size());
+      
+      for (int ring = 0; ring < k_rings_per_side; ++ring) {
+        float const ring_dist = k_left_rings[ring].distance_from_water * base_bank_width;
+        float const ring_height = k_left_rings[ring].height_offset;
+        
+        QVector3D const ring_pos = center_pos - perpendicular * (half_width + width_variation + ring_dist);
+        float const terrain_height = sample_height(ring_pos.x(), ring_pos.z());
+        
+        if (ring == 0) { // water edge
+          samples.push_back(ring_pos);
+        }
+        
+        Vertex vtx{};
+        vtx.position[0] = ring_pos.x();
+        vtx.position[1] = terrain_height + ring_height;
+        vtx.position[2] = ring_pos.z();
+        
+        // Calculate normal based on slope between rings
+        QVector3D normal;
+        if (ring == 0) {
+          // Water edge: use normal pointing toward next ring
+          QVector3D const next_ring_pos = center_pos - perpendicular * 
+            (half_width + width_variation + k_left_rings[1].distance_from_water * base_bank_width);
+          float const next_terrain = sample_height(next_ring_pos.x(), next_ring_pos.z());
+          QVector3D slope_vec(
+            next_ring_pos.x() - ring_pos.x(),
+            (next_terrain + k_left_rings[1].height_offset) - (terrain_height + ring_height),
+            next_ring_pos.z() - ring_pos.z()
+          );
+          normal = QVector3D::crossProduct(slope_vec, dir).normalized();
+        } else if (ring == k_rings_per_side - 1) {
+          // Outer edge: use normal from previous ring
+          unsigned int prev_idx = ring_start_idx + ring - 1;
+          QVector3D prev_pos(vertices[prev_idx].position[0], 
+                            vertices[prev_idx].position[1], 
+                            vertices[prev_idx].position[2]);
+          QVector3D slope_vec(ring_pos.x() - prev_pos.x(),
+                             (terrain_height + ring_height) - prev_pos.y(),
+                             ring_pos.z() - prev_pos.z());
+          normal = QVector3D::crossProduct(slope_vec, dir).normalized();
+        } else {
+          // Middle rings: average of adjacent slopes
+          unsigned int prev_idx = ring_start_idx + ring - 1;
+          QVector3D prev_pos(vertices[prev_idx].position[0], 
+                            vertices[prev_idx].position[1], 
+                            vertices[prev_idx].position[2]);
+          
+          QVector3D const next_ring_pos = center_pos - perpendicular * 
+            (half_width + width_variation + k_left_rings[ring + 1].distance_from_water * base_bank_width);
+          float const next_terrain = sample_height(next_ring_pos.x(), next_ring_pos.z());
+          
+          QVector3D slope_from_prev(ring_pos.x() - prev_pos.x(),
+                                   (terrain_height + ring_height) - prev_pos.y(),
+                                   ring_pos.z() - prev_pos.z());
+          QVector3D slope_to_next(next_ring_pos.x() - ring_pos.x(),
+                                 (next_terrain + k_left_rings[ring + 1].height_offset) - (terrain_height + ring_height),
+                                 next_ring_pos.z() - ring_pos.z());
+          
+          QVector3D n1 = QVector3D::crossProduct(slope_from_prev, dir).normalized();
+          QVector3D n2 = QVector3D::crossProduct(slope_to_next, dir).normalized();
+          normal = ((n1 + n2) * 0.5F).normalized();
+        }
+        
+        vtx.normal[0] = normal.x();
+        vtx.normal[1] = normal.y();
+        vtx.normal[2] = normal.z();
+        vtx.tex_coord[0] = static_cast<float>(ring) / (k_rings_per_side - 1);
+        vtx.tex_coord[1] = t;
+        vertices.push_back(vtx);
+      }
+      
+      // Build vertices for right bank rings (mirror of left)
+      for (int ring = 0; ring < k_rings_per_side; ++ring) {
+        float const ring_dist = k_left_rings[ring].distance_from_water * base_bank_width;
+        float const ring_height = k_left_rings[ring].height_offset;
+        
+        QVector3D const ring_pos = center_pos + perpendicular * (half_width + width_variation + ring_dist);
+        float const terrain_height = sample_height(ring_pos.x(), ring_pos.z());
+        
+        if (ring == 0) { // water edge
+          samples.push_back(ring_pos);
+        }
+        
+        Vertex vtx{};
+        vtx.position[0] = ring_pos.x();
+        vtx.position[1] = terrain_height + ring_height;
+        vtx.position[2] = ring_pos.z();
+        
+        // Calculate normal (mirror logic from left side)
+        QVector3D normal;
+        if (ring == 0) {
+          QVector3D const next_ring_pos = center_pos + perpendicular * 
+            (half_width + width_variation + k_left_rings[1].distance_from_water * base_bank_width);
+          float const next_terrain = sample_height(next_ring_pos.x(), next_ring_pos.z());
+          QVector3D slope_vec(
+            next_ring_pos.x() - ring_pos.x(),
+            (next_terrain + k_left_rings[1].height_offset) - (terrain_height + ring_height),
+            next_ring_pos.z() - ring_pos.z()
+          );
+          normal = QVector3D::crossProduct(dir, slope_vec).normalized();
+        } else if (ring == k_rings_per_side - 1) {
+          unsigned int prev_idx = ring_start_idx + k_rings_per_side + ring - 1;
+          QVector3D prev_pos(vertices[prev_idx].position[0], 
+                            vertices[prev_idx].position[1], 
+                            vertices[prev_idx].position[2]);
+          QVector3D slope_vec(ring_pos.x() - prev_pos.x(),
+                             (terrain_height + ring_height) - prev_pos.y(),
+                             ring_pos.z() - prev_pos.z());
+          normal = QVector3D::crossProduct(dir, slope_vec).normalized();
+        } else {
+          unsigned int prev_idx = ring_start_idx + k_rings_per_side + ring - 1;
+          QVector3D prev_pos(vertices[prev_idx].position[0], 
+                            vertices[prev_idx].position[1], 
+                            vertices[prev_idx].position[2]);
+          
+          QVector3D const next_ring_pos = center_pos + perpendicular * 
+            (half_width + width_variation + k_left_rings[ring + 1].distance_from_water * base_bank_width);
+          float const next_terrain = sample_height(next_ring_pos.x(), next_ring_pos.z());
+          
+          QVector3D slope_from_prev(ring_pos.x() - prev_pos.x(),
+                                   (terrain_height + ring_height) - prev_pos.y(),
+                                   ring_pos.z() - prev_pos.z());
+          QVector3D slope_to_next(next_ring_pos.x() - ring_pos.x(),
+                                 (next_terrain + k_left_rings[ring + 1].height_offset) - (terrain_height + ring_height),
+                                 next_ring_pos.z() - ring_pos.z());
+          
+          QVector3D n1 = QVector3D::crossProduct(dir, slope_from_prev).normalized();
+          QVector3D n2 = QVector3D::crossProduct(dir, slope_to_next).normalized();
+          normal = ((n1 + n2) * 0.5F).normalized();
+        }
+        
+        vtx.normal[0] = normal.x();
+        vtx.normal[1] = normal.y();
+        vtx.normal[2] = normal.z();
+        vtx.tex_coord[0] = static_cast<float>(ring) / (k_rings_per_side - 1);
+        vtx.tex_coord[1] = t;
+        vertices.push_back(vtx);
+      }
+      
+      // Add water-level skirts (vertical geometry down to water surface)
+      // Left water skirt
+      {
+        Vertex const &water_edge_left = vertices[ring_start_idx];
+        Vertex skirt_vtx = water_edge_left;
+        skirt_vtx.position[1] = -0.05F; // Water surface level
+        skirt_vtx.normal[0] = -perpendicular.x();
+        skirt_vtx.normal[1] = 0.0F;
+        skirt_vtx.normal[2] = -perpendicular.z();
+        vertices.push_back(skirt_vtx);
+      }
+      
+      // Right water skirt
+      {
+        Vertex const &water_edge_right = vertices[ring_start_idx + k_rings_per_side];
+        Vertex skirt_vtx = water_edge_right;
+        skirt_vtx.position[1] = -0.05F; // Water surface level
+        skirt_vtx.normal[0] = perpendicular.x();
+        skirt_vtx.normal[1] = 0.0F;
+        skirt_vtx.normal[2] = perpendicular.z();
+        vertices.push_back(skirt_vtx);
+      }
+      
+      // Connect rings with triangle strips
       if (i < length_steps - 1) {
-        unsigned int const idx0 = i * 4;
-
-        indices.push_back(idx0 + 0);
-        indices.push_back(idx0 + 4);
-        indices.push_back(idx0 + 1);
-
-        indices.push_back(idx0 + 1);
-        indices.push_back(idx0 + 4);
-        indices.push_back(idx0 + 5);
-
-        indices.push_back(idx0 + 2);
-        indices.push_back(idx0 + 3);
-        indices.push_back(idx0 + 6);
-
-        indices.push_back(idx0 + 3);
-        indices.push_back(idx0 + 7);
-        indices.push_back(idx0 + 6);
+        unsigned int const base_idx = ring_start_idx;
+        unsigned int const next_base_idx = base_idx + k_total_rings + 2; // +2 for skirts
+        
+        // Left bank strips
+        for (int ring = 0; ring < k_rings_per_side - 1; ++ring) {
+          unsigned int idx0 = base_idx + ring;
+          unsigned int idx1 = base_idx + ring + 1;
+          unsigned int idx2 = next_base_idx + ring;
+          unsigned int idx3 = next_base_idx + ring + 1;
+          
+          indices.push_back(idx0);
+          indices.push_back(idx2);
+          indices.push_back(idx1);
+          
+          indices.push_back(idx1);
+          indices.push_back(idx2);
+          indices.push_back(idx3);
+        }
+        
+        // Right bank strips
+        for (int ring = 0; ring < k_rings_per_side - 1; ++ring) {
+          unsigned int idx0 = base_idx + k_rings_per_side + ring;
+          unsigned int idx1 = base_idx + k_rings_per_side + ring + 1;
+          unsigned int idx2 = next_base_idx + k_rings_per_side + ring;
+          unsigned int idx3 = next_base_idx + k_rings_per_side + ring + 1;
+          
+          indices.push_back(idx0);
+          indices.push_back(idx1);
+          indices.push_back(idx2);
+          
+          indices.push_back(idx1);
+          indices.push_back(idx3);
+          indices.push_back(idx2);
+        }
+        
+        // Water skirt strips
+        {
+          unsigned int left_top = base_idx;
+          unsigned int left_bottom = base_idx + k_total_rings;
+          unsigned int left_top_next = next_base_idx;
+          unsigned int left_bottom_next = next_base_idx + k_total_rings;
+          
+          indices.push_back(left_top);
+          indices.push_back(left_bottom);
+          indices.push_back(left_top_next);
+          
+          indices.push_back(left_bottom);
+          indices.push_back(left_bottom_next);
+          indices.push_back(left_top_next);
+          
+          unsigned int right_top = base_idx + k_rings_per_side;
+          unsigned int right_bottom = base_idx + k_total_rings + 1;
+          unsigned int right_top_next = next_base_idx + k_rings_per_side;
+          unsigned int right_bottom_next = next_base_idx + k_total_rings + 1;
+          
+          indices.push_back(right_top);
+          indices.push_back(right_top_next);
+          indices.push_back(right_bottom);
+          
+          indices.push_back(right_bottom);
+          indices.push_back(right_top_next);
+          indices.push_back(right_bottom_next);
+        }
       }
     }