flutter3d
Showcase Changelog 38 packages API reference

Rectangle area lights

since 0.7.0 Shading and materials

LightType.area is the one light shape in this engine that is not punctual: it has extent, a width and a height, so what reaches a surface is an integral over the rectangle rather than a value read at a single point. That is the difference between a room lit by a window and a room lit by a bright dot with a window painted behind it.

Step 1: A window #

The panel faces the node's local -Z, the same axis a spot light and a camera both look along, so lookAt aims it exactly the way it aims everything else in the scene. Here it sits in the back wall and looks into the room.

// The panel faces the node's local -Z. Set in the back wall and aimed
// at a point in front of it, it shines into the room.
_window = LightNode(name: 'window', type: LightType.area, intensity: 8.0)
  ..width = width
  ..height = height
  ..setPosition(0.0, 0.3, -1.45);
_window.lookAt(Vector3(0.0, 0.3, 3.0));

Step 2: Something to see #

The light itself is not drawn, so a room lit by it has a window you cannot see. A glowing rectangle of the same size, just behind the light, stands in for the glass.

// The light itself is not drawn. A glowing rectangle of the same size,
// just behind it, is what a person sees as the window.
_glass = Material(
  name: 'window glass',
  baseColor: Vector4(0.0, 0.0, 0.0, 1.0),
  emissive: Vector3(3.0, 3.0, 2.8),
);
_pane =
    MeshNode(
        DeviceMesh.upload(
          context.device,
          const PlaneShape(width: 1, depth: 1).build(),
        ),
        _glass,
        name: 'window pane',
      )
      ..setRotation(_standUp)
      ..setPosition(0.0, 0.3, -1.48);

Step 3: A room for it to light #

A plain, rough back wall. The window shines away from it, into the room, so the wall stays dim and the window reads against it.

final MeshNode backWall =
    MeshNode(
        DeviceMesh.upload(
          context.device,
          const PlaneShape(width: 6, depth: 4).build(),
        ),
        Material(
          name: 'wall',
          baseColor: Vector4(0.6, 0.58, 0.55, 1.0),
          roughness: 0.9,
          doubleSided: true,
        ),
        name: 'back wall',
      )
      ..setRotation(_standUp)
      ..setPosition(0.0, 0.0, -1.5);

Step 4: A floor that can shine #

The floor has a material of its own, so its roughness can change on its own.

// A floor of its own, so its roughness can go down to a polish.
_floorMaterial = Material(
  name: 'floor',
  baseColor: Vector4(0.45, 0.45, 0.47, 1.0),
  roughness: floorRoughness,
);
final MeshNode floor = MeshNode(
  DeviceMesh.upload(
    context.device,
    const PlaneShape(width: 6, depth: 5).build(),
  ),
  _floorMaterial,
  name: 'floor',
)..setPosition(0.0, -1.8, 1.0);

Step 5: Resize the window, polish the floor #

The sliders' width and height are written back into the light every frame, so the sliders change the panel a person can see rather than a copy nothing reads. The glowing pane is scaled to the same size, and the floor's roughness is written back the same way.

The light's intensity stays the same as the panel grows, and the shader spreads it over the area: each square metre of a bigger window is dimmer. The pane's glow is a material the light knows nothing about, so the page scales it by the same ratio, the starting 2.5 by 1.5 over the current width times height, to keep the pane as bright as the reflection it stands for.

_window
  ..width = width
  ..height = height;
_pane.setScale(width, 1.0, height);
// The light's radiance is its intensity over its area, so a wider
// window is dimmer per square metre. The pane dims by the same ratio,
// measured from the 2.5 by 1.5 it starts at.
final double dim = (2.5 * 1.5) / (width * height);
_glass.emissive.setValues(3.0 * dim, 3.0 * dim, 2.8 * dim);
_floorMaterial.roughness = floorRoughness;

Bring Floor roughness down toward 0.05 and the floor reflects the window as a sharp rectangle. Widen the window and the reflection widens with it and dims. Raise the roughness and the reflection softens: at middling roughness it still shows the panel's proportions, and near 1 it spreads into a blob. The highlight is integrated over the whole rectangle, not read at one point of it, which is why a long window gives a long reflection.

if (_window.type != LightType.area) {
  throw StateError('the window is not an area light');
}
if (_window.width != width || _window.height != height) {
  throw StateError('the panel size did not track the sliders');
}
final Vector3 halfWidth = _window.readHalfWidth();
final Vector3 halfHeight = _window.readHalfHeight();
if (halfWidth.length2 == 0.0 || halfHeight.length2 == 0.0) {
  throw StateError('the panel has no extent to shade against');
}
if (_floorMaterial.roughness != floorRoughness) {
  throw StateError('the floor roughness did not track its slider');
}
if (frame.drawCalls < 3) {
  throw StateError('the room was not drawn');
}