Shadows of cut-out leaves
Open the live demo · Read the source · View on GitHub
A leaf, a fence or a net is usually one flat quad with a texture whose transparent pixels are the holes. The picture drawn for the eye already respects those holes. The picture drawn for the shadow has to as well, or every leaf casts the shadow of a solid rectangle.
The engine does this for materials in the cut-out alpha mode: while the shadow map is drawn, each caster's texture is read and any pixel below the cutoff is skipped.
Step 1: A texture with holes #
The page makes its own leaf texture in code: a grid of soft discs, opaque in the middle and fading to nothing at the rim. The fade is what makes the cutoff meaningful, since a higher cutoff keeps only the more opaque middle of each disc.
/// A grid of soft discs on a clear background: opaque in the middle of each
/// disc, fading to nothing at its rim.
final class DiscsTexture extends ProceduralTexture {
const DiscsTexture({this.size = 64, this.cells = 4});
@override
final int size;
final int cells;
@override
ByteData encode() {
final Uint8List bytes = Uint8List(size * size * 4);
final double cell = size / cells;
for (var y = 0; y < size; y++) {
for (var x = 0; x < size; x++) {
final double dx = (x + 0.5) % cell - cell / 2;
final double dy = (y + 0.5) % cell - cell / 2;
final double radius = Vector2(dx, dy).length / cell;
final double alpha = ((0.5 - radius) / 0.35).clamp(0.0, 1.0);
final int at = (y * size + x) * 4;
bytes[at] = 70;
bytes[at + 1] = 150;
bytes[at + 2] = 70;
bytes[at + 3] = (alpha * 255).round();
}
}
return bytes.buffer.asByteData();
}
}
Step 2: A material that cuts out #
MaterialAlphaMode.mask is the switch. It needs a base-colour texture with an alpha
channel, here albedo, and a threshold, alphaCutoff. The material is double sided
so the leaves show from underneath as well. Nothing on the light or the settings says
that this caster is special: the renderer sees the mode and picks the cut-out shadow
stage by itself.
_leaves = Material(
name: 'leaves',
albedo: const DiscsTexture().upload(context.device),
alphaMode: MaterialAlphaMode.mask,
alphaCutoff: cutoff,
doubleSided: true,
roughness: 0.8,
);
final MeshNode canopy = MeshNode(
DeviceMesh.upload(
context.device,
const PlaneShape(width: 3, depth: 3).build(),
),
_leaves,
name: 'canopy',
)..setPosition(-0.5, 1.6, 0.0);
Step 3: A sun that casts #
The same request as on every shadow page. Look at the ground: the canopy's shadow is a pattern of discs, not a square.
final LightNode sun = LightNode(
name: 'sun',
intensity: 3.0,
castsShadow: true,
)..setLocalForward(Vector3(-0.5, -0.8, -0.3));
Step 4: Change the cut #
The page writes the mode and the cutoff into the material each frame. Switch Cut out off and the canopy becomes opaque, so its shadow fills in as a solid square. Drag Cutoff up and the discs in the shadow shrink, because fewer pixels are opaque enough.
_leaves
..alphaMode = cutOut ? MaterialAlphaMode.mask : MaterialAlphaMode.opaque
..alphaCutoff = cutoff;