Second Life Shadow System - ApertureViewer/Aperture-Viewer GitHub Wiki

Technical Analysis: Second Life Shadow System

This document provides a detailed technical analysis of the dynamic shadow system implemented in the Second Life viewer, based on a review of the Aperture Viewer v1.0.0 codebase.

(Analysis based on review of relevant C++ files like pipeline.cpp, llvovolume.cpp, various GLSL shader files, etc., from the Aperture v1.0.0 source.)

1. Overview

The shadow system provides dynamic shadows for the main directional light (Sun/Moon) and a limited number of local spotlights. It uses standard Shadow Mapping techniques, rendering the scene from the light's perspective into depth textures (shadow maps) which are then sampled during the main deferred lighting pass to determine if a surface point is occluded from that light.

  • Shadow Types Implemented:
    • Directional Light Shadows (Sun/Moon): Uses Cascaded Shadow Maps (CSM) to cover the large area illuminated by the sun/moon with varying levels of detail.
    • Spotlight Shadows: Provides shadows for up to TWO dynamic local spotlights selected based on scene priority.
  • Core Technique: Standard depth-based shadow mapping.
  • Integration: Shadow maps are generated in separate rendering passes before or during the main rendering pipeline and sampled within the deferred lighting shaders (sunLightF.glsl, spotLightF.glsl, multiSpotLightF.glsl via shadowUtil.glsl).

2. Sun/Moon Shadows (Cascaded Shadow Maps - CSM)

Purpose

To provide shadows for the primary global directional light source (Sun or Moon) across the entire viewable scene distance, optimizing detail distribution.

Technique

Cascaded Shadow Maps (CSM). The camera's view frustum is divided into multiple sections (cascades) along its depth. A separate, tighter shadow map is rendered for each cascade, providing higher resolution shadows closer to the camera and lower resolution further away.

Cascade Splitting (pipeline.cpp::generateSunShadow)

  • The view frustum is split into up to 4 cascades. The exact number used is determined by the RenderShadowSplits setting (values 0-3 likely map to 1-4 cascades).
  • The split distances are calculated dynamically based on the visible scene depth (near_clip, far_clip derived from getVisiblePointCloud).
  • The split distances (mSunClipPlanes uniform in shaders) are determined using a combination of linear and exponential distribution, influenced by the RenderShadowSplitExponent setting and the sun's angle (da factor), aiming for a visually balanced distribution of shadow resolution. The first cascade (mSunClipPlanes.x) is intentionally pushed back slightly ("transition padding").

Cascade Frustum Calculation (pipeline.cpp::generateSunShadow)

  • For each cascade, a tight bounding box (min, max) is calculated around the visible geometry within that cascade's depth slice using getVisiblePointCloud.
  • A light-view matrix (view[j]) is created using look() aiming from a calculated origin towards the light direction (lightDir, opposite of caster_dir), with an 'up' vector chosen based on the camera's orientation to maintain stability.
  • A projection matrix (proj[j]) is generated. A heuristic based on the point cloud's spread in light space (mShadowError, mShadowFOV) compared to settings (RenderShadowErrorCutoff, RenderShadowFOVCutoff) determines whether to use an orthographic (glm::ortho) or a perspective (glm::perspective) projection to best fit the cascade's geometry. Perspective is preferred for tighter fits when points converge towards the light source.
  • The final shadow matrix (mSunShadowMatrix[j]) used for sampling is computed by combining the light's view (view[j]), projection (proj[j]), and a transform to texture coordinates (trans), then transforming back from world space using the inverse camera view (inv_view). This shadow_matrix uniform transforms world-space positions directly to shadow map UV coordinates.

Shadow Map Rendering (pipeline.cpp::renderShadow called from generateSunShadow)

  • For each active cascade j:
    • The corresponding shadow map render target (mRT->shadow[j]) is bound.
    • The viewport is set to the shadow map's resolution.
    • The target is cleared (typically depth only, unless VSM is used - shadow_detail > 2).
    • The scene geometry is rendered using the calculated light view (view[j]) and projection (proj[j]) matrices.
    • Simplified shaders are used (shadowF.glsl, shadowAlphaMaskF.glsl, etc.) focusing on outputting correct depth and handling alpha-tested cutouts.
    • Depth clamping (GL_DEPTH_CLAMP) is enabled if available (depth_clamp parameter) to handle objects intersecting the near/far planes of the light frustum correctly.

Resolution (pipeline.cpp::allocateShadowBuffer)

  • The resolution of the sun shadow maps is determined based on the main screen render target resolution, scaled by the RenderShadowResolutionScale setting.
  • The BlurHappySize function ensures the dimensions are suitable for potential blur passes (divisible by 16). All cascades share the same base resolution.

Sampling (deferred/sunLightF.glsl, deferred/shadowUtil.glsl)

  • The sunLightF.glsl (or related deferred lighting shaders) determines which cascade the current pixel belongs to by comparing its view-space depth (spos.z) against the cascade split planes (shadow_clip uniform, derived from mSunClipPlanes).
  • It uses the corresponding shadow matrix (shadow_matrix[0..3]) to transform the pixel's world position into the shadow map's UV space.
  • It samples the appropriate shadow map texture (shadowMap0 to shadowMap3) using texture() on a sampler2DShadow. This performs the depth comparison (pixel depth vs shadow map depth).
  • Percentage-Closer Filtering (PCF) is implemented in pcfShadow by taking multiple samples around the projected coordinate (8 taps in a rotated grid pattern) and averaging the results for softer edges.
  • Depth bias (shadow_bias) and potentially slope-scale bias (shadow_offset modifying position before transform) are applied to the sampled depth or position to mitigate self-shadowing artifacts ("shadow acne"). The bias can be scaled by the dot product of the normal and light direction (bias_mul).

3. Spotlight Shadows

Purpose

To provide localized, higher-detail shadows for dynamic point/spot lights within the scene.

Limit

The system is hardcoded to support a maximum of TWO simultaneous shadow-casting spotlights. This is evident in array sizes (mSpotShadow[2], mTargetShadowSpotLight[2], mShadowSpotLight[2]) and loop bounds in pipeline.cpp.

Selection (pipeline.cpp::renderDeferredLighting, llvovolume.cpp::updateSpotLightPriority, pipeline.cpp::setupSpotLight)

  • Spotlights potentially eligible for shadows seem identified during the main lighting pass (renderDeferredLighting).
  • Each eligible spotlight calculates its priority using LLVOVolume::updateSpotLightPriority, which relies on LLPipeline::calcPixelArea - essentially prioritizing lights covering a larger screen area.
  • The two highest-priority lights are designated as target shadow casters (mTargetShadowSpotLight[0..1]).
  • A fading mechanism (mSpotLightFade[0..1]) is used to transition smoothly between which two lights are actually rendering shadows (mShadowSpotLight[0..1]). If a currently active shadow caster is no longer a target, its fade value decreases. If a slot becomes fully faded out (fade = 0), it picks up one of the target lights that isn't already assigned to the other active slot.

Frustum Calculation (pipeline.cpp::setupSpotLight)

  • For each of the two active shadow-casting spotlights:
    • A view matrix (view[i+4]) is created looking from the light's position (origin) along its direction (at_axis), derived from the LLVOVolume's rotation and scale.
    • A perspective projection matrix (proj[i+4]) is created using glm::perspective, based on the spotlight's Field of View (fov) and aspect ratio derived from its LLVOVolume parameters (params.mV[0], scale.mV[VX/VY]) and radius (s). Near/Far planes are calculated based on the light's radius and internal parameters.
  • The final shadow matrix (mSunShadowMatrix[i+4]) combines these with the inverse camera view for direct transformation from world space to shadow map UV space.

Shadow Map Rendering (pipeline.cpp::renderShadow called from generateSunShadow)

  • The process is similar to CSM cascades but runs only for the two active spotlights i = 0..1.
  • The corresponding shadow map render target (mSpotShadow[i]) is bound and cleared.
  • Scene geometry is rendered using the spotlight's specific view (view[i+4]) and projection (proj[i+4]) matrices.
  • The light source object itself is excluded from its own shadow map using the RenderSpotLight global flag checked within LLPipeline::stateSort.
  • Depth clamping is generally not used for spotlights (depth_clamp = false).

Resolution (pipeline.cpp::allocateShadowBuffer)

  • Spot shadow map resolution is also derived from the screen resolution and RenderShadowResolutionScale. It's typically square.

Sampling (deferred/spotLightF.glsl, deferred/multiSpotLightF.glsl, deferred/shadowUtil.glsl)

  • Spotlight shaders check the proj_shadow_idx uniform passed via setupSpotLight. If it's 0 or 1:
    • They use the appropriate shadow matrix (shadow_matrix[4] or shadow_matrix[5]) to transform the pixel's world position.
    • They sample the corresponding spot shadow map (shadowMap4 or shadowMap5) using pcfSpotShadow.
    • They apply spotlight-specific depth bias (spot_shadow_bias).
    • They modulate the shadow result by the current fade value (shadow_fade) to smooth transitions.

4. Shadow Rendering Shaders & Alpha Handling

Purpose

Shaders used in the renderShadow pass (shadowF.glsl, shadowAlphaMaskF.glsl, treeShadowF.glsl, pbrShadow*.glsl, avatarShadow*.glsl) are simplified. Their primary goal is accurate depth output.

Alpha Masking

For materials using alpha testing (like foliage, fences), shaders like shadowAlphaMaskF.glsl sample the diffuse/basecolor texture's alpha channel and use discard if the alpha value is below minimum_alpha. This creates properly shaped shadows for cutout textures. PBR materials use pbrShadowAlphaMaskF.glsl.

Alpha Blending (Dithering)

For materials that might normally use alpha blending but need to cast shadows (often alpha values between the mask threshold and fully opaque), a dithering pattern based on screen position (target_pos_x / post_pos.w) is used to stochastically discard pixels (avatarAlphaShadowF.glsl, pbrShadowAlphaBlendF.glsl). This simulates semi-transparency in the shadow map.

Color Output

Generally ignored, but if RenderShadowDetail > 2 (Variance Shadow Maps, VSM, though likely not fully implemented/used based on shader simplicity), color writes might be enabled to store depth squared for variance calculations.

5. Configuration & Key Files

  • Settings: RenderShadowDetail (0=Off, 1=Sun/Moon, 2=Sun/Moon+2 Spots, >2 implies potential VSM?), RenderShadowSplits (Number of CSM cascades-1), RenderShadowResolutionScale, RenderShadowBias, RenderShadowOffset, RenderSpotShadowBias, RenderSpotShadowOffset, RenderShadowSplitExponent, RenderShadowErrorCutoff, RenderShadowFOVCutoff.
  • Core Logic: pipeline.cpp (esp. generateSunShadow, renderShadow, calcNearbyLights, setupSpotLight, renderDeferredLighting), llvovolume.cpp (updateSpotLightPriority).
  • Sampling Logic: deferred/shadowUtil.glsl.
  • Lighting Integration: deferred/sunLightF.glsl, deferred/spotLightF.glsl, deferred/multiSpotLightF.glsl.
  • Shadow Pass Shaders: deferred/shadowF.glsl, deferred/*Shadow*.glsl.

6. Summary

The system provides robust Cascaded Shadow Maps for the main directional light and dynamic shadows for up to two prioritized spotlights. It uses standard depth mapping with PCF filtering and depth biasing. Alpha-masked objects are handled correctly via discard, and semi-transparent shadows are approximated using dithering. PBR materials have dedicated shadow shaders. The primary limitation confirmed by the code is the hard limit of only two simultaneous shadow-casting spotlights. The system integrates tightly with the deferred lighting pipeline, providing shadow information to the main lighting shaders via texture sampling.


This analysis describes the shadow system implementation as found within the reviewed Aperture Viewer v1.0.0 codebase.