flutter3d
Showcase Changelog 38 packages API reference

Cascaded shadows

since 0.2.0 Shadows

One shadow map cannot be sharp under your feet and still cover a field a hundred metres long: there are only so many pixels in it. A cascaded shadow splits the view into a few slices, near to far, and gives each its own map. The near slice is small and sharp, the far one large and soft.

Step 1: A place to cast on #

Shadows need a surface to fall on. The floor is a flat PlaneShape, eighty metres each way, in one plain stone material, so the far pillars have somewhere to put their shadow too.

final Material stone = Material(
  name: 'stone',
  baseColor: Vector4(0.72, 0.7, 0.66, 1.0),
  roughness: 0.85,
);
final MeshNode floor = MeshNode(
  DeviceMesh.upload(
    context.device,
    const PlaneShape(width: 80, depth: 80).build(),
  ),
  stone.copy()..doubleSided = true,
  name: 'floor',
);

Step 2: Things that cast #

Ten pillars in a row, from your feet toward the horizon. They share one mesh: a mesh is uploaded once and any number of nodes can draw it.

final Scene scene = Scene()..add(floor);
final DeviceMesh pillar = DeviceMesh.upload(
  context.device,
  CuboidShape(size: Vector3(1.0, 3.0, 1.0)).build(),
);
for (var i = 0; i < 10; i++) {
  scene.add(
    MeshNode(pillar, stone, name: 'pillar $i')
      ..setPosition(i.isEven ? -3.0 : 3.0, 1.5, -i * 5.0),
  );
}

Step 3: A sun that asks for a shadow #

A light casts no shadow unless it says so. castsShadow: true asks the renderer to draw the scene from the sun's side; only a directional light is cascaded.

scene.add(
  LightNode(name: 'sun', intensity: 3.0, castsShadow: true)
    ..setLocalForward(Vector3(-0.5, -0.8, -0.3)),
);

Warning. A light asking for a shadow is a request, not a promise: the renderer shadows one directional light and a few point lights, and reports in FrameResult.shadowsDenied when it had to refuse.

Step 4: Choose the cascades #

ShadowSettings is where the split is decided. cascades is how many maps, cascadeSplit how strongly the near ones are made smaller than the far ones and viewDistance how far the last one reaches. Drag the sliders and watch the edge of the near shadows.

shadows: ShadowSettings(
  cascades: cascades,
  cascadeSplit: split,
  viewDistance: viewDistance,
  resolution: _resolutions[resolutionChoice],
),