Light shafts
Open the live demo · Read the source · View on GitHub
The cheap way to fake a light shaft takes bright pixels on screen and streaks them away from the sun, and it needs the sun in the picture to do it. This page's effect is a different thing: it marches each view ray and asks the sun's own shadow map whether that point in the air is lit. A beam through a doorway comes out shaped like the doorway, and the sun does not have to be anywhere in frame.
Step 1: Something to cast a shape #
Two pillars and a lintel make a doorway. Nothing about them is special, they are ordinary stone.
final DeviceMesh pillar = DeviceMesh.upload(
device,
CuboidShape(size: Vector3(1.2, 4.0, 1.2)).build(),
);
final MeshNode left = MeshNode(pillar, stone, name: 'left pillar')
..setPosition(-1.6, 2.0, -2.0);
final MeshNode right = MeshNode(pillar, stone, name: 'right pillar')
..setPosition(1.6, 2.0, -2.0);
final MeshNode lintel = MeshNode(
DeviceMesh.upload(
device,
CuboidShape(size: Vector3(4.4, 0.8, 1.2)).build(),
),
stone,
name: 'lintel',
)..setPosition(0.0, 4.4, -2.0);
Step 2: A sun that casts a shadow #
Light shafts read the shadow map, so there has to be one. A directional light that does not ask for a shadow gives the march nothing to sample, and the pass draws no shafts at all rather than failing.
final LightNode sun = LightNode(
name: 'sun',
intensity: 6.0,
castsShadow: true,
)..setLocalForward(Vector3(-0.3, -0.6, 0.7));
Step 3: Turn the march on #
LightShaftSettings.enabled runs it, strength is how thick the air is (a
density per metre: 0.005 is a clear day's haze, a few hundredths a dusty
room), anisotropy is which way it scatters, and distance is how far along
each ray the march reaches before it gives up. The light itself is the sun's:
its colour and intensity are what the air scatters. Walk around the doorway
while the app is running and the beam keeps the shape of the gap between the
pillars from every angle, because it is reading actual occlusion rather than a
screen-space streak. Turn to face the sun and the air glows; turn your back on
it and, with forward scattering, it almost disappears.
lightShafts: LightShaftSettings(
enabled: true,
strength: strength,
anisotropy: anisotropy,
distance: distance,
),
Drag Reach up and the beam extends further into the room. Drag it down and the beam stops short, spending its samples on the part of the air closest to the doorway.
Warning. Turn Density to 0 and the pass is skipped. Remove
castsShadowfrom the sun instead and there is nothing left to march against: the shafts disappear because the map they read has nothing in it, which is the honest failure this effect is supposed to have.
Step 4: Confirm it ran #
if (!frame.passes.any((FramePass p) => p.name == 'light shafts')) {
throw StateError('the light shafts pass did not run: ${frame.skipped}');
}