|
|
@@ -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);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|