flutter3d
Showcase Changelog 38 packages API reference

Image-based lighting

since 0.3.0 Light and environment

A lamp lights a surface from one direction. The real world lights it from everywhere at once: the sky above, the ground below, a bright window to one side. Image-based lighting does the same with a picture. The picture is a cube map of the surroundings, and each surface picks its light out of it.

There is no light in this scene at all. Everything you see comes from the cube.

Step 1: Make the environment #

EnvironmentMap.fromSky turns a SkySettings into a cube map, and EnvironmentMap.fromPanorama does the same from an ordinary 360 degree picture. The page builds both, so you can switch between them. The panorama here is drawn in code, 64 by 32 pixels: a blue sky, a brown floor and one bright window.

Both calls do a second thing that matters. A rough surface does not reflect one direction, it gathers a wide cone of light, so the cube is blurred several times, sharper for smooth surfaces and softer for rough ones, and the blurred copies are stored as the mip levels of the same cube.

_fromSky = EnvironmentMap.fromSky(
  context.device,
  const SkySettings(enabled: true, glowStrength: 0.4, sunIntensity: 2.0),
);
_fromPanorama = EnvironmentMap.fromPanorama(
  context.device,
  _studioPanorama(),
  width: 64,
  height: 32,
);

Step 2: Spheres of every roughness #

Five metal spheres, from nearly a mirror on the left to fully rough on the right. They share a mesh and differ only in roughness. Because each one reads the level of the cube that matches its roughness, you can see the blurring from step 1 laid out in a row: the left sphere holds a clear picture of the sky and the window, the right one only a smear of colour.

Drag Metallic down and the spheres turn from metal into plastic, which reflects less and shows more of its own colour.

final DeviceMesh sphere = DeviceMesh.upload(
  context.device,
  SphereShape(radius: 0.8, segments: 32, rings: 16).build(),
);
final Scene scene = _scene = Scene();
_balls = <Material>[];
for (var i = 0; i < 5; i++) {
  final Material material = Material(
    name: 'ball $i',
    lighting: LightingModel.pbr,
    baseColor: Vector4(0.95, 0.75, 0.45, 1.0),
    metallic: metallic,
    roughness: 0.05 + i * 0.24,
  );
  _balls.add(material);
  scene.add(
    MeshNode(sphere, material, name: 'ball $i')
      ..setPosition(i * 1.9 - 3.8, 0.0, 0.0),
  );
}

Step 3: Give the scene the cube #

Two fields on the scene do it. environment is the cube and environmentLevels says how many blurred levels follow the base. The ambientIntensity that used to scale a flat ambient colour now scales the cube, which is what the Strength slider changes. Switch Environment on off and the scene falls back to the flat ambient, which is nearly black here.

final ({TextureHandle texture, int levels})? chosen = source == 0
    ? _fromSky
    : _fromPanorama;
_scene
  ..environment = lit ? chosen?.texture : null
  ..environmentLevels = chosen?.levels ?? 0
  ..ambientIntensity = strength;

Note. The cube is eight bits per channel, so a sun far brighter than the sky around it is clamped to white before it is stored. It reflects less than a real sun would.