Overview
CSM splits the view frustum into multiple cascades, each with its own shadow map at different distances. This provides:- High detail near the camera (fine shadow map resolution)
- Extended range far from the camera (coarse shadow map resolution)
- Smooth transitions between cascades using PCF filtering
Architecture
Core Components
The shadow system is owned byWorld class (src/world.h:42-61):
Initialization
Shadow resources are initialized inWorld::init_shadow_resources() (src/world.cpp:431-493):
Cascade Calculation
Split Distribution
Cascades are distributed using a log/linear hybrid scheme (src/world.cpp:495-571):
0.0 to 1.0):
0.0= Linear distribution (even split)1.0= Logarithmic distribution (more near detail)0.85= Default (good balance)
Light-Space Matrices
For each cascade, compute a tight-fitting orthographic projection (src/world.cpp:526-570):
- Sphere-based bounds (simpler than AABB fitting)
- Radius snapping to 1/16 block increments (reduces shadow shimmering)
- Light position offset ensures all geometry is in front of near plane
Shadow Rendering Pass
Render Loop
Called before main rendering inWorld::render_shadows() (src/world.cpp:573-622):
Alpha Testing
The shadow shader performs alpha testing for foliage/transparent blocks:Shadow Sampling in Main Shader
Cascade Selection
In the fragment shader, select cascade based on view-space depth (colored_lighting/frag.glsl:23-32):
Shadow Map Lookup
Transform world position to light-space and sample depth (colored_lighting/frag.glsl:34-43):
PCF Filtering
Percentage-Closer Filtering (PCF) samples multiple depth values for soft shadows (colored_lighting/frag.glsl:45-63):
[0.9, 1.0] to avoid overly dark shadows during development. Can be adjusted to [0.0, 1.0] for full shadow intensity.
Configuration Options
Insrc/options.h:20-28:
Performance Tuning
SHADOW_MAP_RESOLUTION:- Higher = sharper shadows, more memory/fillrate
- Lower = softer shadows, better performance
- Common values: 1024, 2048, 4096
- More cascades = better quality at distance, more draw calls
- Fewer cascades = worse quality, better performance
- Typical: 3-4 cascades for good quality
- 0 = No PCF, hard shadows
- 1 = 3×3 PCF, soft shadows (9 samples)
- 2 = 5×5 PCF, very soft shadows (25 samples)
GL State Interactions (Critical)
TextureManager Binding
Problem: Both shadow map and texture atlas areGL_TEXTURE_2D_ARRAY. If the shadow map is bound when TextureManager::add_texture() is called, block textures upload into the wrong texture.
Solution: TextureManager always explicitly binds texture_array before GL calls (src/renderer/texture_manager.cpp:14, 26, 43):
AGENTS.md:41-49 for detailed explanation of this bug.
Shader Sampler Setup
InWorld::draw() (src/world.cpp:630-656):
Debugging Tips
Pure Black Blocks When Shadows Enabled
Cause: Texture atlas writes went into shadow map. Fix:- Check
TextureManageralways bindstexture_arraybefore uploads - Verify initialization order (texture manager before world/shadows)
Black Screen After Shadow Pass
Cause:glDrawBuffer(GL_NONE) or viewport not restored.
Fix: Ensure render_shadows() restores all GL state (src/world.cpp:618-621).
Shadow Shimmering
Cause: Light-space matrices change every frame due to camera movement. Mitigation: Radius snapping to 1/16 block increments helps reduce this (src/world.cpp:560).
No Shadows Visible
Check:Options::SHADOWS_ENABLED = true- Shadow shader compiled successfully
u_ShadowCascadeCount > 0in main shader- Shadow map bound to texture unit 1
- Light direction not perpendicular to ground
Related Pages
- Rendering Overview - Shadow pass in render loop
- Shader System - Shadow shader details
- Lighting System - How shadows combine with lighting
- Texture Management - Texture array binding issues