Browse Source

fix river bank visibility

djeada 1 tuần trước cách đây
mục cha
commit
0cc42b512c

+ 24 - 0
assets/shaders/riverbank.frag

@@ -6,6 +6,12 @@ in vec3 WorldPos;
 in vec3 Normal;
 
 uniform float time;
+uniform sampler2D u_visibilityTex;
+uniform vec2 u_visibilitySize;
+uniform float u_visibilityTileSize;
+uniform float u_exploredAlpha;
+uniform int u_hasVisibility;
+uniform float u_segmentVisibility;
 
 float saturate(float x) { return clamp(x, 0.0, 1.0); }
 vec2 saturate(vec2 v) { return clamp(v, vec2(0.0), vec2(1.0)); }
@@ -42,6 +48,22 @@ void main() {
 
   vec2 uv = warp(WorldPos.xz * 0.45);
 
+  float visibilityFactor = 1.0;
+  if (u_hasVisibility == 1 && u_visibilitySize.x > 0.0 &&
+      u_visibilitySize.y > 0.0) {
+    float tileSize = max(u_visibilityTileSize, 0.0001);
+    vec2 grid = vec2(WorldPos.x / tileSize, WorldPos.z / tileSize);
+    grid += (u_visibilitySize * 0.5) - vec2(0.5);
+    vec2 visUV = (grid + vec2(0.5)) / u_visibilitySize;
+    float visSample = texture(u_visibilityTex, visUV).r;
+    if (visSample < 0.25) {
+      discard;
+    } else if (visSample < 0.75) {
+      visibilityFactor = u_exploredAlpha;
+    }
+  }
+  visibilityFactor *= u_segmentVisibility;
+
   vec3 wetSoil = vec3(0.20, 0.17, 0.14);
   vec3 dampSoil = vec3(0.32, 0.27, 0.22);
   vec3 drySoil = vec3(0.46, 0.40, 0.33);
@@ -101,5 +123,7 @@ void main() {
 
   color *= (1.0 - streaks * 0.07 * (0.3 + 0.7 * wetness));
 
+  color *= visibilityFactor;
+
   FragColor = vec4(saturate(color), 1.0);
 }

+ 2 - 4
game/systems/combat_system/attack_processor.cpp

@@ -386,10 +386,8 @@ void process_attacks(Engine::Core::World *world, float delta_time) {
               auto *target_transform =
                   target->get_component<Engine::Core::TransformComponent>();
               if (target_transform != nullptr) {
-                float const dx =
-                    target_transform->position.x - guard_x;
-                float const dz =
-                    target_transform->position.z - guard_z;
+                float const dx = target_transform->position.x - guard_x;
+                float const dz = target_transform->position.z - guard_z;
                 float const dist_sq = dx * dx + dz * dz;
                 float const guard_radius_sq =
                     guard_mode->guard_radius * guard_mode->guard_radius;

+ 2 - 1
game/systems/command_service.cpp

@@ -366,7 +366,8 @@ void CommandService::moveGroup(Engine::Core::World &world,
       hold_mode->exit_cooldown = hold_mode->stand_up_duration;
     }
 
-    auto *guard_mode = entity->get_component<Engine::Core::GuardModeComponent>();
+    auto *guard_mode =
+        entity->get_component<Engine::Core::GuardModeComponent>();
     if ((guard_mode != nullptr) && guard_mode->active &&
         !guard_mode->returning_to_guard_position) {
       guard_mode->active = false;

+ 2 - 2
game/systems/rain_manager.cpp

@@ -60,7 +60,7 @@ void RainManager::update(float delta_time) {
 
   if (target_state != m_state) {
     transition_to(target_state);
-    
+
     if (target_state == RainState::FadingIn) {
       m_state_time = m_cycle_time - rain_start;
     } else if (target_state == RainState::FadingOut) {
@@ -71,7 +71,7 @@ void RainManager::update(float delta_time) {
   } else {
     m_state_time += delta_time;
   }
-  
+
   update_intensity(delta_time);
 }
 

+ 2 - 2
render/equipment/weapons/shield_carthage.cpp

@@ -20,8 +20,8 @@ using Render::Geom::sphere_at;
 
 namespace {
 
-auto create_unit_hemisphere_mesh(int lat_segments = 12,
-                                 int lon_segments = 32) -> std::unique_ptr<Mesh> {
+auto create_unit_hemisphere_mesh(int lat_segments = 12, int lon_segments = 32)
+    -> std::unique_ptr<Mesh> {
   std::vector<Vertex> vertices;
   std::vector<unsigned int> indices;
   vertices.reserve((lat_segments + 1) * (lon_segments + 1));

+ 45 - 0
render/gl/backend.cpp

@@ -1278,6 +1278,51 @@ void Backend::execute(const DrawQueue &queue, const Camera &cam) {
         active_shader->set_uniform(m_waterPipeline->m_riverbankUniforms.time,
                                    m_animationTime);
 
+        if (m_waterPipeline->m_riverbankUniforms.segment_visibility !=
+            Shader::InvalidUniform) {
+          active_shader->set_uniform(
+              m_waterPipeline->m_riverbankUniforms.segment_visibility,
+              it.alpha);
+        }
+
+        if (m_waterPipeline->m_riverbankUniforms.has_visibility !=
+            Shader::InvalidUniform) {
+          int const has_vis = m_riverbankVisibility.enabled ? 1 : 0;
+          active_shader->set_uniform(
+              m_waterPipeline->m_riverbankUniforms.has_visibility, has_vis);
+        }
+
+        if (m_riverbankVisibility.enabled &&
+            m_riverbankVisibility.texture != nullptr) {
+          if (m_waterPipeline->m_riverbankUniforms.visibility_size !=
+              Shader::InvalidUniform) {
+            active_shader->set_uniform(
+                m_waterPipeline->m_riverbankUniforms.visibility_size,
+                m_riverbankVisibility.size);
+          }
+          if (m_waterPipeline->m_riverbankUniforms.visibility_tile_size !=
+              Shader::InvalidUniform) {
+            active_shader->set_uniform(
+                m_waterPipeline->m_riverbankUniforms.visibility_tile_size,
+                m_riverbankVisibility.tile_size);
+          }
+          if (m_waterPipeline->m_riverbankUniforms.explored_alpha !=
+              Shader::InvalidUniform) {
+            active_shader->set_uniform(
+                m_waterPipeline->m_riverbankUniforms.explored_alpha,
+                m_riverbankVisibility.explored_alpha);
+          }
+          constexpr int k_riverbank_vis_texture_unit = 7;
+          m_riverbankVisibility.texture->bind(k_riverbank_vis_texture_unit);
+          if (m_waterPipeline->m_riverbankUniforms.visibility_texture !=
+              Shader::InvalidUniform) {
+            active_shader->set_uniform(
+                m_waterPipeline->m_riverbankUniforms.visibility_texture,
+                k_riverbank_vis_texture_unit);
+          }
+          m_lastBoundTexture = m_riverbankVisibility.texture;
+        }
+
         it.mesh->draw();
         break;
       }

+ 18 - 0
render/gl/backend.h

@@ -122,6 +122,16 @@ public:
     glPolygonOffset(factor, units);
   }
 
+  void setRiverbankVisibility(bool enabled, Texture *texture,
+                              const QVector2D &size, float tile_size,
+                              float explored_alpha) {
+    m_riverbankVisibility.enabled = enabled && (texture != nullptr);
+    m_riverbankVisibility.texture = texture;
+    m_riverbankVisibility.size = size;
+    m_riverbankVisibility.tile_size = tile_size;
+    m_riverbankVisibility.explored_alpha = explored_alpha;
+  }
+
 private:
   int m_viewportWidth{0};
   int m_viewportHeight{0};
@@ -150,6 +160,14 @@ private:
   bool m_depth_testEnabled = true;
   bool m_blendEnabled = false;
   float m_animationTime = 0.0F;
+
+  struct {
+    Texture *texture = nullptr;
+    QVector2D size{0.0F, 0.0F};
+    float tile_size = 1.0F;
+    float explored_alpha = 0.6F;
+    bool enabled = false;
+  } m_riverbankVisibility;
 };
 
 } // namespace Render::GL

+ 12 - 0
render/gl/backend/water_pipeline.cpp

@@ -75,6 +75,18 @@ void WaterPipeline::cache_riverbank_uniforms() {
   m_riverbankUniforms.projection =
       m_riverbankShader->uniform_handle("projection");
   m_riverbankUniforms.time = m_riverbankShader->uniform_handle("time");
+  m_riverbankUniforms.visibility_texture =
+      m_riverbankShader->uniform_handle("u_visibilityTex");
+  m_riverbankUniforms.visibility_size =
+      m_riverbankShader->uniform_handle("u_visibilitySize");
+  m_riverbankUniforms.visibility_tile_size =
+      m_riverbankShader->uniform_handle("u_visibilityTileSize");
+  m_riverbankUniforms.explored_alpha =
+      m_riverbankShader->uniform_handle("u_exploredAlpha");
+  m_riverbankUniforms.has_visibility =
+      m_riverbankShader->uniform_handle("u_hasVisibility");
+  m_riverbankUniforms.segment_visibility =
+      m_riverbankShader->uniform_handle("u_segmentVisibility");
 }
 
 void WaterPipeline::cache_bridge_uniforms() {

+ 6 - 0
render/gl/backend/water_pipeline.h

@@ -32,6 +32,12 @@ public:
     GL::Shader::UniformHandle view{GL::Shader::InvalidUniform};
     GL::Shader::UniformHandle projection{GL::Shader::InvalidUniform};
     GL::Shader::UniformHandle time{GL::Shader::InvalidUniform};
+    GL::Shader::UniformHandle visibility_texture{GL::Shader::InvalidUniform};
+    GL::Shader::UniformHandle visibility_size{GL::Shader::InvalidUniform};
+    GL::Shader::UniformHandle visibility_tile_size{GL::Shader::InvalidUniform};
+    GL::Shader::UniformHandle explored_alpha{GL::Shader::InvalidUniform};
+    GL::Shader::UniformHandle has_visibility{GL::Shader::InvalidUniform};
+    GL::Shader::UniformHandle segment_visibility{GL::Shader::InvalidUniform};
   };
 
   struct BridgeUniforms {

+ 2 - 1
render/gl/mesh.cpp

@@ -116,7 +116,8 @@ auto createCubeMesh() -> std::unique_ptr<Mesh> {
   return std::make_unique<Mesh>(vertices, indices);
 }
 
-auto createPlaneMesh(float width, float height, int subdivisions) -> std::unique_ptr<Mesh> {
+auto createPlaneMesh(float width, float height,
+                     int subdivisions) -> std::unique_ptr<Mesh> {
   std::vector<Vertex> vertices;
   std::vector<unsigned int> indices;
 

+ 2 - 1
render/gl/mesh.h

@@ -71,6 +71,7 @@ private:
 
 auto createQuadMesh() -> std::unique_ptr<Mesh>;
 auto createCubeMesh() -> std::unique_ptr<Mesh>;
-auto createPlaneMesh(float width, float height, int subdivisions = 1) -> std::unique_ptr<Mesh>;
+auto createPlaneMesh(float width, float height,
+                     int subdivisions = 1) -> std::unique_ptr<Mesh>;
 
 } // namespace Render::GL

+ 13 - 22
render/gl/primitives.cpp

@@ -97,7 +97,8 @@ auto create_unit_cylinder_mesh(int radialSegments) -> std::unique_ptr<Mesh> {
   return std::make_unique<Mesh>(v, idx);
 }
 
-auto create_unit_sphere_mesh(int latSegments, int lonSegments) -> std::unique_ptr<Mesh> {
+auto create_unit_sphere_mesh(int latSegments,
+                             int lonSegments) -> std::unique_ptr<Mesh> {
   const float r = k_unit_radius;
   std::vector<Vertex> v;
   std::vector<unsigned int> idx;
@@ -188,7 +189,8 @@ auto create_unit_cone_mesh(int radialSegments) -> std::unique_ptr<Mesh> {
   return std::make_unique<Mesh>(v, idx);
 }
 
-auto createCapsuleMesh(int radialSegments, int heightSegments) -> std::unique_ptr<Mesh> {
+auto createCapsuleMesh(int radialSegments,
+                       int heightSegments) -> std::unique_ptr<Mesh> {
   constexpr float k_capsule_radius = 0.25F;
   const float radius = k_capsule_radius;
   const float half_h = k_half_scalar;
@@ -273,7 +275,8 @@ auto simple_hash(float seed) -> float {
   return x - std::floor(x);
 }
 
-auto create_unit_torso_mesh(int radialSegments, int heightSegments) -> std::unique_ptr<Mesh> {
+auto create_unit_torso_mesh(int radialSegments,
+                            int heightSegments) -> std::unique_ptr<Mesh> {
   const float half_h = k_half_scalar;
 
   constexpr float k_lower_extension = 0.05F;
@@ -352,28 +355,17 @@ auto create_unit_torso_mesh(int radialSegments, int heightSegments) -> std::uniq
     Axes A;
   };
 
-  // Torso profile keys - defines width (ax) and depth (az) at height t
-  // Profile is inverted: t=0 is shoulders/top, t=1 is pelvis/bottom
-  // Improved shoulder/trapezius shaping with gradual curves
   const Key keys[] = {
-      {0.00F, {0.72F, 0.65F}},  // Neck base - narrow
-      {0.08F, {0.88F, 0.82F}},  // Trapezius slope start
-      {0.15F, {1.02F, 0.95F}},  // Shoulder peak - widest point
-      {0.22F, {0.98F, 0.92F}},  // Below shoulders - slight taper
-      {0.45F, {0.76F, 0.70F}},  // Waist - narrowest
-      {0.65F, {1.12F, 1.06F}},  // Hips widening
-      {0.85F, {1.30F, 1.25F}},  // Upper pelvis
-      {1.02F, {1.48F, 1.20F}},  // Pelvis peak
-      {1.10F, {1.12F, 0.92F}},  // Lower pelvis taper
+      {0.00F, {0.72F, 0.65F}}, {0.08F, {0.88F, 0.82F}}, {0.15F, {1.02F, 0.95F}},
+      {0.22F, {0.98F, 0.92F}}, {0.45F, {0.76F, 0.70F}}, {0.65F, {1.12F, 1.06F}},
+      {0.85F, {1.30F, 1.25F}}, {1.02F, {1.48F, 1.20F}}, {1.10F, {1.12F, 0.92F}},
   };
   constexpr int key_count = sizeof(keys) / sizeof(keys[0]);
 
-  // Shoulder rounding parameters - adds lateral bulge at shoulder height
   constexpr float k_shoulder_bulge_amp = 0.08F;
   constexpr float k_shoulder_bulge_start = 0.10F;
   constexpr float k_shoulder_bulge_end = 0.22F;
 
-  // Trapezius rounding - adds smooth slope from neck to shoulders
   constexpr float k_trap_slope_amp = 0.04F;
   constexpr float k_trap_slope_start = 0.00F;
   constexpr float k_trap_slope_end = 0.12F;
@@ -453,16 +445,15 @@ auto create_unit_torso_mesh(int radialSegments, int heightSegments) -> std::uniq
     s += k_theta_cos_amp * smooth_band(t, k_theta_cos_start, k_theta_cos_end) *
          cos_a;
 
-    // Shoulder bulge - adds roundness at shoulder level for lateral angles
     float const shoulder_band =
         smooth_band(t, k_shoulder_bulge_start, k_shoulder_bulge_end);
-    // sin_a peaks at sides (±90°), so use abs(sin_a) for both shoulders
+
     float const lateral_factor = std::abs(sin_a);
     s += k_shoulder_bulge_amp * shoulder_band * lateral_factor;
 
-    // Trapezius slope - smooth curve from neck to shoulder
-    float const trap_band = smooth_band(t, k_trap_slope_start, k_trap_slope_end);
-    // Affects front-back more than sides for natural trap shape
+    float const trap_band =
+        smooth_band(t, k_trap_slope_start, k_trap_slope_end);
+
     float const trap_factor = (1.0F - std::abs(sin_a)) * 0.7F + 0.3F;
     s += k_trap_slope_amp * trap_band * trap_factor;
 

+ 253 - 134
render/ground/riverbank_renderer.cpp

@@ -5,6 +5,7 @@
 #include "../scene_renderer.h"
 #include "ground_utils.h"
 #include "map/terrain.h"
+#include <GL/gl.h>
 #include <QVector2D>
 #include <QVector3D>
 #include <algorithm>
@@ -30,6 +31,10 @@ void RiverbankRenderer::configure(
   m_grid_width = height_map.getWidth();
   m_grid_height = height_map.getHeight();
   m_heights = height_map.getHeightData();
+  m_visibilityTexture.reset();
+  m_cachedVisibilityVersion = 0;
+  m_visibilityWidth = 0;
+  m_visibilityHeight = 0;
   build_meshes();
 }
 
@@ -103,11 +108,9 @@ void RiverbankRenderer::build_meshes() {
     QVector3D const perpendicular(-dir.z(), 0.0F, dir.x());
     float const half_width = segment.width * 0.5F;
 
-    // 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
-    
+    constexpr int k_rings_per_side = 5;
+    constexpr int k_total_rings = k_rings_per_side * 2;
+
     int length_steps =
         static_cast<int>(std::ceil(length / (m_tile_size * 0.5F))) + 1;
     length_steps = std::max(length_steps, 8);
@@ -142,90 +145,100 @@ void RiverbankRenderer::build_meshes() {
       QVector3D const center_offset = perpendicular * meander;
       center_pos += center_offset;
 
-      // 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.02F},    // water edge - just above water (halved from 0.05)
-        {0.125F, 0.175F}, // inner midslope (halved from 0.25, 0.35)
-        {0.25F, 0.3F},    // crest - peak of bank (halved from 0.5, 0.6)
-        {0.375F, 0.125F}, // outer midslope (halved from 0.75, 0.25)
-        {0.5F, -0.15F}    // terrain blend - well below terrain to avoid z-fighting on hills
+        float distance_from_water;
+        float height_offset;
       };
-      
-      // 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.075F;
-      float const base_bank_width = 0.5F + ring_noise; // Halved from 1.0F
-
-      // Build vertices for left bank rings
-      unsigned int const ring_start_idx = static_cast<unsigned int>(vertices.size());
-      
+
+      constexpr RingProfile k_left_rings[k_rings_per_side] = {{0.0F, 0.02F},
+                                                              {0.125F, 0.175F},
+                                                              {0.25F, 0.3F},
+                                                              {0.375F, 0.125F},
+                                                              {0.5F, -0.15F}};
+
+      float const ring_noise =
+          noise(center_pos.x() * 3.0F, center_pos.z() * 3.0F) * 0.075F;
+      float const base_bank_width = 0.5F + ring_noise;
+
+      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_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);
+
+        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
+
+        if (ring == 0) {
           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()
-          );
+
+          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 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());
+                              (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 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();
+                                    (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();
@@ -233,67 +246,80 @@ void RiverbankRenderer::build_meshes() {
         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_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);
+
+        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
+
+        if (ring == 0) {
           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()
-          );
+          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 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());
+                              (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 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();
+                                    (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();
@@ -301,91 +327,85 @@ void RiverbankRenderer::build_meshes() {
         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.position[1] = -0.05F;
         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 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.position[1] = -0.05F;
         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 base_idx = ring_start_idx;
-        unsigned int const next_base_idx = base_idx + k_total_rings + 2; // +2 for skirts
-        
-        // Left bank strips
+        unsigned int const next_base_idx = base_idx + k_total_rings + 2;
+
         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);
@@ -417,6 +437,77 @@ void RiverbankRenderer::submit(Renderer &renderer, ResourceManager *resources) {
 
   renderer.set_current_shader(shader);
 
+  auto *backend = renderer.backend();
+  auto &visibility = Game::Map::VisibilityService::instance();
+  const bool use_visibility = visibility.is_initialized();
+
+  Texture *visibility_tex = nullptr;
+  QVector2D visibility_size(0.0F, 0.0F);
+
+  if (use_visibility) {
+    const int vis_w = visibility.getWidth();
+    const int vis_h = visibility.getHeight();
+    const std::uint64_t version = visibility.version();
+    bool const size_changed =
+        (vis_w != m_visibilityWidth) || (vis_h != m_visibilityHeight);
+
+    if (!m_visibilityTexture || size_changed) {
+      m_visibilityTexture = std::make_unique<Texture>();
+      m_visibilityTexture->create_empty(vis_w, vis_h, Texture::Format::RGBA);
+      m_visibilityTexture->set_filter(Texture::Filter::Nearest,
+                                      Texture::Filter::Nearest);
+      m_visibilityTexture->set_wrap(Texture::Wrap::ClampToEdge,
+                                    Texture::Wrap::ClampToEdge);
+      m_visibilityWidth = vis_w;
+      m_visibilityHeight = vis_h;
+      m_cachedVisibilityVersion = 0;
+    }
+
+    if (version != m_cachedVisibilityVersion || size_changed) {
+      auto cells = visibility.snapshotCells();
+      std::vector<unsigned char> texels(
+          static_cast<std::size_t>(vis_w * vis_h * 4), 0U);
+
+      for (int z = 0; z < vis_h; ++z) {
+        for (int x = 0; x < vis_w; ++x) {
+          int const idx = z * vis_w + x;
+          unsigned char val = 0U;
+          switch (static_cast<Game::Map::VisibilityState>(cells[idx])) {
+          case Game::Map::VisibilityState::Visible:
+            val = 255U;
+            break;
+          case Game::Map::VisibilityState::Explored:
+            val = 128U;
+            break;
+          case Game::Map::VisibilityState::Unseen:
+          default:
+            val = 0U;
+            break;
+          }
+          texels[static_cast<std::size_t>(idx * 4)] = val;
+          texels[static_cast<std::size_t>(idx * 4 + 3)] = 255;
+        }
+      }
+
+      m_visibilityTexture->bind();
+      glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, vis_w, vis_h, GL_RGBA,
+                      GL_UNSIGNED_BYTE, texels.data());
+      visibility_tex = m_visibilityTexture.get();
+      m_cachedVisibilityVersion = version;
+    } else {
+      visibility_tex = m_visibilityTexture.get();
+    }
+
+    visibility_size =
+        QVector2D(static_cast<float>(vis_w), static_cast<float>(vis_h));
+  }
+
+  if (backend != nullptr) {
+    backend->setRiverbankVisibility(use_visibility && visibility_tex != nullptr,
+                                    visibility_tex, visibility_size,
+                                    m_tile_size, m_exploredDimFactor);
+  }
+
   QMatrix4x4 model;
   model.setToIdentity();
 
@@ -433,8 +524,36 @@ void RiverbankRenderer::submit(Renderer &renderer, ResourceManager *resources) {
       continue;
     }
 
-    // Always render - fog overlay handles visibility naturally (like terrain)
-    renderer.mesh(mesh, model, QVector3D(1.0F, 1.0F, 1.0F), nullptr, 1.0F);
+    float segment_visibility = 1.0F;
+    if (use_visibility) {
+      enum class SegmentState { Hidden, Explored, Visible };
+      SegmentState state = SegmentState::Hidden;
+
+      const auto &samples = m_visibilitySamples[mesh_index - 1];
+      if (samples.empty()) {
+        state = SegmentState::Visible;
+      }
+      for (const auto &sample : samples) {
+        if (visibility.isVisibleWorld(sample.x(), sample.z())) {
+          state = SegmentState::Visible;
+          break;
+        }
+        if ((state == SegmentState::Hidden) &&
+            visibility.isExploredWorld(sample.x(), sample.z())) {
+          state = SegmentState::Explored;
+        }
+      }
+
+      if (state == SegmentState::Hidden) {
+        continue;
+      }
+
+      segment_visibility =
+          (state == SegmentState::Visible) ? 1.0F : m_exploredDimFactor;
+    }
+
+    renderer.mesh(mesh, model, QVector3D(1.0F, 1.0F, 1.0F), nullptr,
+                  segment_visibility);
   }
 
   renderer.set_current_shader(nullptr);

+ 8 - 0
render/ground/riverbank_renderer.h

@@ -1,8 +1,10 @@
 #pragma once
 
 #include "../../game/map/terrain.h"
+#include "../gl/texture.h"
 #include "../i_render_pass.h"
 #include <QMatrix4x4>
+#include <cstdint>
 #include <memory>
 #include <vector>
 
@@ -31,6 +33,12 @@ private:
   std::vector<float> m_heights;
   std::vector<std::unique_ptr<Mesh>> m_meshes;
   std::vector<std::vector<QVector3D>> m_visibilitySamples;
+
+  std::unique_ptr<Texture> m_visibilityTexture;
+  std::uint64_t m_cachedVisibilityVersion = 0;
+  int m_visibilityWidth = 0;
+  int m_visibilityHeight = 0;
+  float m_exploredDimFactor = 0.6F;
 };
 
 } // namespace Render::GL

+ 0 - 3
render/humanoid/humanoid_specs.h

@@ -39,18 +39,15 @@ struct HumanProportions {
   static constexpr float UPPER_ARM_LEN = 0.32F;
   static constexpr float FORE_ARM_LEN = 0.27F;
 
-  // Body structure offsets
   static constexpr float HIP_LATERAL_OFFSET = 0.10F;
   static constexpr float HIP_VERTICAL_OFFSET = -0.02F;
   static constexpr float FOOT_Y_OFFSET_DEFAULT = 0.022F;
 
-  // Torso depth and shape constants
   static constexpr float TORSO_DEPTH_FACTOR_BASE = 0.55F;
   static constexpr float TORSO_DEPTH_FACTOR_MIN = 0.40F;
   static constexpr float TORSO_DEPTH_FACTOR_MAX = 0.85F;
   static constexpr float TORSO_TOP_COVER_OFFSET = -0.03F;
 
-  // Rendering thresholds and tolerances
   static constexpr float EPSILON_SMALL = 1e-5F;
   static constexpr float EPSILON_TINY = 1e-6F;
   static constexpr float EPSILON_VECTOR = 1e-8F;

+ 8 - 7
render/humanoid/rig.cpp

@@ -81,7 +81,8 @@ void advance_pose_cache_frame() {
   if ((s_current_frame & 0x1FF) == 0) {
     auto it = s_pose_cache.begin();
     while (it != s_pose_cache.end()) {
-      if (s_current_frame - it->second.frame_number > k_pose_cache_max_age * 2) {
+      if (s_current_frame - it->second.frame_number >
+          k_pose_cache_max_age * 2) {
         it = s_pose_cache.erase(it);
       } else {
         ++it;
@@ -440,9 +441,9 @@ void HumanoidRendererBase::draw_common_body(const DrawContext &ctx,
   const float torso_r = torso_r_base * torso_scale;
   float const depth_scale = scaling.z();
 
-  const float torso_depth_factor = std::clamp(
-      HP::TORSO_DEPTH_FACTOR_BASE + (depth_scale - 1.0F) * 0.20F,
-      HP::TORSO_DEPTH_FACTOR_MIN, HP::TORSO_DEPTH_FACTOR_MAX);
+  const float torso_depth_factor =
+      std::clamp(HP::TORSO_DEPTH_FACTOR_BASE + (depth_scale - 1.0F) * 0.20F,
+                 HP::TORSO_DEPTH_FACTOR_MIN, HP::TORSO_DEPTH_FACTOR_MAX);
   float torso_depth = torso_r * torso_depth_factor;
 
   const float y_top_cover =
@@ -1146,9 +1147,9 @@ void HumanoidRendererBase::draw_simplified_body(const DrawContext &ctx,
       std::max(HP::TORSO_TOP_R, shoulder_half_span * 0.95F);
   const float torso_r = torso_r_base * torso_scale;
   float const depth_scale = scaling.z();
-  const float torso_depth_factor = std::clamp(
-      HP::TORSO_DEPTH_FACTOR_BASE + (depth_scale - 1.0F) * 0.20F,
-      HP::TORSO_DEPTH_FACTOR_MIN, HP::TORSO_DEPTH_FACTOR_MAX);
+  const float torso_depth_factor =
+      std::clamp(HP::TORSO_DEPTH_FACTOR_BASE + (depth_scale - 1.0F) * 0.20F,
+                 HP::TORSO_DEPTH_FACTOR_MIN, HP::TORSO_DEPTH_FACTOR_MAX);
   float torso_depth = torso_r * torso_depth_factor;
 
   const float y_top_cover =

+ 2 - 2
render/humanoid_math.h

@@ -1,5 +1,5 @@
-// This file is deprecated. Include humanoid/humanoid_math.h instead.
-// Kept for backwards compatibility with any legacy includes.
+
+
 #pragma once
 
 #include "humanoid/humanoid_math.h"

+ 2 - 2
render/humanoid_specs.h

@@ -1,5 +1,5 @@
-// This file is deprecated. Include humanoid/humanoid_specs.h instead.
-// Kept for backwards compatibility with any legacy includes.
+
+
 #pragma once
 
 #include "humanoid/humanoid_specs.h"