Point light shadows (ray traced + cube shadow-map fallback) #439

Open
opened 2026-10-05 14:43:55 +00:00 by sky_line · 1 comment
Owner

Point lights (SL's ordinary local lights) cast no shadows today. Only the sun/moon (ray traced) and projectors (shadow-map atlas RenderVKProjShadow*, or ray traced RenderVKRTProjShadowMax) do. Point lights are by far the most common lights in SL, and our site already says "lights cast proper shadows".

Same shape as the projector work: one setting, Point light shadows: Off / Shadow maps / Ray traced. Ray traced is greyed out without ray-query support. Both modes share a per-frame cap on how many lights cast shadows.

Ray traced (primary path)

No projection is involved. A shadow ray goes from the receiver to the light's position, so a sphere is the easiest case for RT.

  • Plug into the existing RT shadow mask pass (clean_rt_shadow_mask.comp). It already traces up to 4 shadowed projectors per pixel into the per-slot uRawProj array, and the temporal and spatial denoisers already handle those slots. Point lights become more candidates for the same per-pixel slots, picked in cluster order.
  • Soft shadows: jitter the ray target over a small sphere (the light's "size"), and the existing penumbra estimate and denoiser blur it.
  • Light selection each frame: score = screen coverage × intensity × proximity. Take the top N (RenderVKRTPointShadowMax, default 8–16). Unselected lights stay unshadowed, as today.

Shadow maps (fallback for cards without ray tracing)

  • Cube maps in a cube-map array: 6 faces per light, sampled by direction, with hardware-seamless filtering and no face seams. Rejected alternatives:
    • Dual-paraboloid (2 renders): it warps straight edges unless geometry is finely tessellated, and SL prims are large, coarse triangles.
    • Tetrahedral (4 faces): saves a third of the renders but needs custom seam handling.
  • GPU-driven, per our rules:
    • One compute cull for every face of every selected light. The light radius (≤ 20 m in SL) bounds the cull.
    • One layered multi-draw-indirect for all faces (gl_Layer / multiview).
    • No per-face CPU work.
  • Depth = linear distance to the light. Normal-offset and slope-scaled bias; PCF by jittering the lookup direction.
  • Caching, as the projector atlas does:
    • A static layer is re-rendered only when something static in range changes.
    • Dynamic casters (avatars, movers) are re-rendered on a budget (RenderVKPointShadowDynamicInterval) and composited over the static layer.
    • A per-frame cap on face renders.
  • Resolution tiers by screen size: 512² near, 256² mid, 128² far. Memory per light (D32): 512² ≈ 6 MB, 256² ≈ 1.5 MB, 128² ≈ 0.4 MB. It falls under the VRAM pressure ladder like the other shadow rungs.

SL-specific hazards (must handle, both paths)

  • Lights inside their own object. A bulb inside a lampshade, or a light inside a sconce mesh: the light's own linkset fully occludes it, and the lamp goes dark. Exclude the light's own object/linkset from its shadow casters (instance mask or per-instance ID compare). Also start rays / near-clip at a small radius around the light.
  • Attached lights / facelights. These would shadow the wearer's own head and hair. Default: attached lights don't shadow their owner, or don't cast at all, behind a setting.
  • Content built around no shadows. Fill lights placed behind walls or under floors are "designed" leaks. With shadows on, those spots go dark. Expected, but worth a release note and an easy toggle.
  • Lights on spinning, moving or animated objects need the dynamic path or the moving-caster flag the RT mask already tracks.

Performance estimate (to verify with RenderVKGpuTimers)

  • Ray traced: cost scales with the screen pixels each light covers, not the light count.
    • Half-res mask at 1440p is ~0.9 M pixels. One ray per covered pixel per light comes to roughly 0.3–0.6 ms for a light covering the whole screen on an RTX 4070/4080-class card. Alpha-tested foliage (any-hit) pushes toward the top of that range.
    • Typical point lights cover 5–20% of the screen, so about 0.05–0.15 ms each.
    • 8–16 shadowed lights: ~0.5–1.5 ms in a busy club; denoise adds little (the slots already exist).
  • Shadow maps:
    • Per light, a full refresh is 6 faces of geometry within ≤ 20 m. That's ~0.1–0.3 ms on a mid-high card, spread over frames by caching.
    • Steady state with a refresh budget: ~0.3–1 ms per frame, plus ~0.05–0.2 ms of filtering per light for the pixels it covers.
    • Worst case is a crowded club with every light seeing moving avatars. The dynamic-refresh budget caps that (stale-by-a-few-frames shadows instead of FPS loss).
Point lights (SL's ordinary local lights) cast no shadows today. Only the sun/moon (ray traced) and projectors (shadow-map atlas `RenderVKProjShadow*`, or ray traced `RenderVKRTProjShadowMax`) do. Point lights are by far the most common lights in SL, and our site already says "lights cast proper shadows". Same shape as the projector work: one setting, **Point light shadows: Off / Shadow maps / Ray traced**. Ray traced is greyed out without ray-query support. Both modes share a per-frame cap on how many lights cast shadows. ## Ray traced (primary path) No projection is involved. A shadow ray goes from the receiver to the light's position, so a sphere is the *easiest* case for RT. - Plug into the existing RT shadow mask pass (`clean_rt_shadow_mask.comp`). It already traces up to 4 shadowed projectors per pixel into the per-slot `uRawProj` array, and the temporal and spatial denoisers already handle those slots. Point lights become more candidates for the same per-pixel slots, picked in cluster order. - Soft shadows: jitter the ray target over a small sphere (the light's "size"), and the existing penumbra estimate and denoiser blur it. - Light selection each frame: score = screen coverage × intensity × proximity. Take the top N (`RenderVKRTPointShadowMax`, default 8–16). Unselected lights stay unshadowed, as today. ## Shadow maps (fallback for cards without ray tracing) - **Cube maps** in a cube-map array: 6 faces per light, sampled by direction, with hardware-seamless filtering and no face seams. Rejected alternatives: - Dual-paraboloid (2 renders): it warps straight edges unless geometry is finely tessellated, and SL prims are large, coarse triangles. - Tetrahedral (4 faces): saves a third of the renders but needs custom seam handling. - GPU-driven, per our rules: - One compute cull for every face of every selected light. The light radius (≤ 20 m in SL) bounds the cull. - One layered multi-draw-indirect for all faces (`gl_Layer` / multiview). - No per-face CPU work. - Depth = linear distance to the light. Normal-offset and slope-scaled bias; PCF by jittering the lookup direction. - Caching, as the projector atlas does: - A static layer is re-rendered only when something static in range changes. - Dynamic casters (avatars, movers) are re-rendered on a budget (`RenderVKPointShadowDynamicInterval`) and composited over the static layer. - A per-frame cap on face renders. - Resolution tiers by screen size: 512² near, 256² mid, 128² far. Memory per light (D32): 512² ≈ 6 MB, 256² ≈ 1.5 MB, 128² ≈ 0.4 MB. It falls under the VRAM pressure ladder like the other shadow rungs. ## SL-specific hazards (must handle, both paths) - **Lights inside their own object.** A bulb inside a lampshade, or a light inside a sconce mesh: the light's own linkset fully occludes it, and the lamp goes dark. Exclude the light's own object/linkset from its shadow casters (instance mask or per-instance ID compare). Also start rays / near-clip at a small radius around the light. - **Attached lights / facelights.** These would shadow the wearer's own head and hair. Default: attached lights don't shadow their owner, or don't cast at all, behind a setting. - **Content built around no shadows.** Fill lights placed behind walls or under floors are "designed" leaks. With shadows on, those spots go dark. Expected, but worth a release note and an easy toggle. - Lights on spinning, moving or animated objects need the dynamic path or the moving-caster flag the RT mask already tracks. ## Performance estimate (to verify with RenderVKGpuTimers) - **Ray traced:** cost scales with the *screen pixels each light covers*, not the light count. - Half-res mask at 1440p is ~0.9 M pixels. One ray per covered pixel per light comes to roughly 0.3–0.6 ms for a light covering the whole screen on an RTX 4070/4080-class card. Alpha-tested foliage (any-hit) pushes toward the top of that range. - Typical point lights cover 5–20% of the screen, so about 0.05–0.15 ms each. - 8–16 shadowed lights: ~0.5–1.5 ms in a busy club; denoise adds little (the slots already exist). - **Shadow maps:** - Per light, a full refresh is 6 faces of geometry within ≤ 20 m. That's ~0.1–0.3 ms on a mid-high card, spread over frames by caching. - Steady state with a refresh budget: ~0.3–1 ms per frame, plus ~0.05–0.2 ms of filtering per light for the pixels it covers. - Worst case is a crowded club with every light seeing moving avatars. The dynamic-refresh budget caps that (stale-by-a-few-frames shadows instead of FPS loss).
sky_line added this to the Slipstream project 2026-10-06 15:04:14 +00:00
Author
Owner

Direction decided 2026-10-10: point-light shadows come from ReSTIR DI (#91)

Instead of a standalone "top N shadowed point lights" selector in the RT mask's 4 slots, point lights become candidates in the shared ReSTIR DI many-light sampler. The design is in #91. Every point light can cast a shadow, with cost roughly independent of light count (~1 visibility ray per pixel plus reuse passes), and the same system handles projectors and emissive surfaces.

Requirements specific to this ticket (carry into the ReSTIR build):

  • Own-fixture exclusion: SL creators put point lights inside bulbs, lamp shades and lanterns. Shadow rays must ignore the light's own object/linkset, and/or start a small distance out from the light. Otherwise most lit builds go dark or get hard lamp-shade cut-outs.
  • Soft shadows: jitter the sample point over the light's radius.
  • Setting: Point light shadows Off / Ray traced (ReSTIR). The shadow-map (cube-map) fallback for non-RT GPUs comes later, after #567 makes maps cheaper, and with a small light cap.

The light scoring, light buffer and denoiser hookup planned here are the first pieces of the ReSTIR infrastructure, not separate work.

## Direction decided 2026-10-10: point-light shadows come from ReSTIR DI (#91) Instead of a standalone "top N shadowed point lights" selector in the RT mask's 4 slots, point lights become candidates in the shared **ReSTIR DI** many-light sampler. The design is in #91. Every point light can cast a shadow, with cost roughly independent of light count (~1 visibility ray per pixel plus reuse passes), and the same system handles projectors and emissive surfaces. **Requirements specific to this ticket (carry into the ReSTIR build):** - **Own-fixture exclusion:** SL creators put point lights inside bulbs, lamp shades and lanterns. Shadow rays must ignore the light's own object/linkset, and/or start a small distance out from the light. Otherwise most lit builds go dark or get hard lamp-shade cut-outs. - **Soft shadows:** jitter the sample point over the light's radius. - **Setting:** Point light shadows Off / Ray traced (ReSTIR). The shadow-map (cube-map) fallback for non-RT GPUs comes later, after #567 makes maps cheaper, and with a small light cap. The light scoring, light buffer and denoiser hookup planned here are the first pieces of the ReSTIR infrastructure, not separate work.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
sky_line/Slipstream#439
No description provided.