Renderer · type

SceneCasterMaterial

What a shadow-caster enumeration may hand to a depth pass.

Explained in Hello world.

type SceneCasterMaterial = (SurfaceMaterial<SurfaceTextureHandle> &
import type { SceneCasterMaterial } from '@driftengine/core';

In depth

Occluding light and stopping a body are two independent properties of a piece of geometry, and this interface exists to keep them apart. The set that collides is a physics question; the set that casts is an optics one. They overlap heavily — most solid things do both — but neither contains the other, and a renderer that derives one from the other gets a whole class of geometry wrong in one stroke.

The failure this was written for: a tree's canopy is deliberately not a collider (nothing should be halted mid-run by leaves) and is therefore not merged into the static world mesh either, because it bends in the wind and merged geometry cannot. The only thing a depth pass could accept was a rigid Mesh, so the canopy had no way in — and a crown that occludes nothing reads as sunlight passing straight through several metres of solid foliage. Nothing was switched off; there was simply no route for that kind of geometry to travel.

So a sink takes both kinds. A caller enumerates what casts, once, and the same enumeration serves the directional cascade and every point-light cubemap.

Every draw may also carry its material, which is what lets one enumeration serve a colour pass as well as a depth one. A depth pass reads one thing from it, since 4.4.0: whether the caster is a cutout (cutoutOf), so a leaf card casts the leaf rather than the card. Everything else a material says cannot change a depth, which is why this interface carried only a handle, a matrix and a palette for as long as depth was the only thing replaying it — and why, until then, every alpha-cut surface cast its whole quad. What that cost was measured from outside. A consumer wanting the scene from a second viewpoint — a mirror, a probe face — had one option, which was to run its whole draw path again; the first attempt to avoid it replayed this sink into the mirror and got every car unpainted, because the list carries no material. So the game re-enters its own draw with a mirrored camera, and pays its heaviest phase twice.

Optional, and for the reason instanced gives at length: this interface is implemented outwards, so a required parameter added in a minor stops every consumer's sink and every test double compiling. A sink that ignores it behaves exactly as it did.