Skip to content

scene: sun shadow maps #52

Description

@dvoyni

Parent

Scene follow-ups. Revisited 2026-09-22 against cog 4e178c7. Blocked by scene: move sceneFrame to a uniform block. Absorbs gfx: depth bias for shadow maps.

What it is

Sun shadow maps for both renderers: a shadow camera renders a depth-only pass, and model's shader multiplies a shadow term into the sun's contribution.

The work spans three bundles:

  • model owns the shader and the frame packer: SceneFrame (bundles/model/internal/builtin/scene/frame.wgsl:26) and PackFrameLighting (bundles/model/utils.go:137).
  • model also owns the bundled PBR material, which gains the shadow entry.
  • scene and ecsscene each own a camera vocabulary and a PassTag (bundles/scene/types.go:38-43, bundles/ecsscene/types.go:144-172). Each needs the link from a lit camera to its shadow source.

The storage budget is 8 of 8 (model.md). A depth texture plus a comparison sampler spend no storage slot, but any per-cascade or per-light shadow data in a buffer needs the slot #100 frees. This ticket is blocked on #100 so that the shadow design is never squeezed by the budget.

Settled, still valid

  • The shape is in scene.md §Shadows (Extension points: shadows, post-processing, IBL). A shadow camera is a camera:
    • m.LookAt along SunDirection, with an Orthographic projection;
    • one pass with NoTarget() and DepthTarget(tex);
    • a lower Order than the cameras that sample it.
  • What casts is decided by the shadow camera's CullMask and by whether the material has a shadow tag entry. There is no per-draw flag; if one is ever added, its zero value means "casts".
  • The sun's contribution only. Punctual-light shadows belong with clustered lighting.
  • The frame block grows the light-space matrix and bias. Group 0 grows a texture_depth_2d and a sampler_comparison, with a 1x1 default depth texture bound when there is no shadow. The default is still needed, but for a different reason: since #133, an unbound binding costs that draw with gfx.ErrStorageBufferUnsupplied rather than the whole frame.
  • Ordering is gfx's job: write-then-read barriers cover depth attachments (#112, 684e995).
  • Depth-only passes run on Vulkan since gogpu/wgpu#353 (pinned v0.34.5) and in the browser. depthOnlyPassesWork (extensions/gogpu/internal/gfxdepthonly.go:56-69) refuses GLES, the software HAL and untried HALs with ErrDepthOnlyPassUnsupported. A pass that depends on the output is only valid where it is encoded.

Depth bias (from #32), acceptance-to-be

  • DepthBias, DepthBiasSlopeScale and DepthBiasClamp are added to gfx.MaterialState (slots/gfx/internal/types/state.go:119), all three together. A constant bias misses slanted receivers, and the clamp bounds the peter-panning the slope term causes.
  • They are plumbed in depthStencilState (extensions/gogpu/internal/gfxbackend.go:708-717). wgpu.DepthStencilState carries them, with DepthBias as an int32.
  • The zero value is WebGPU's "no bias", so the change is additive and needs no migration.
  • pipelineKey embeds MaterialState whole (slots/gfx/internal/translator.go:35-45). The two float fields keep it comparable but make it NaN-sensitive as a map key, so NaN must be rejected or canonicalised.
  • Defaults ship tuned, not zeroed, on model's shadow entry, against a real shadow-casting demo.
  • Bias stays per material, through the shadow tag entry (Pipeline state growth for 3D).

What is still open

  • Behaviour on GLES and the software HAL, where the shadow pass is refused: draw unshadowed with a report, or refuse the camera?
  • The link field's name and shape, in both scene.CameraDescr and ecsscene.Camera.
  • Cascades: N textures and N cameras, an atlas through PassDescr.Viewport, or a DepthTarget that takes a layer. All three are additive, and none is chosen.

Trigger

A demo needs sun shadows, and #100 has landed.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions