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.
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:
SceneFrame(bundles/model/internal/builtin/scene/frame.wgsl:26) andPackFrameLighting(bundles/model/utils.go:137).shadowentry.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
m.LookAtalongSunDirection, with anOrthographicprojection;NoTarget()andDepthTarget(tex);Orderthan the cameras that sample it.CullMaskand by whether the material has ashadowtag entry. There is no per-draw flag; if one is ever added, its zero value means "casts".texture_depth_2dand asampler_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 withgfx.ErrStorageBufferUnsuppliedrather than the whole frame.684e995).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 withErrDepthOnlyPassUnsupported. A pass that depends on the output is only valid where it is encoded.Depth bias (from #32), acceptance-to-be
DepthBias,DepthBiasSlopeScaleandDepthBiasClampare added togfx.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.depthStencilState(extensions/gogpu/internal/gfxbackend.go:708-717).wgpu.DepthStencilStatecarries them, withDepthBiasas anint32.pipelineKeyembedsMaterialStatewhole (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.shadowentry, against a real shadow-casting demo.shadowtag entry (Pipeline state growth for 3D).What is still open
scene.CameraDescrandecsscene.Camera.PassDescr.Viewport, or aDepthTargetthat takes a layer. All three are additive, and none is chosen.Trigger
A demo needs sun shadows, and #100 has landed.