flutter3d
Showcase Changelog 38 packages API reference

Plan a frame before drawing

since 0.7.0 Scene and geometry

The renderer decides which passes a frame needs by compiling a dependency graph. planFrame runs that decision without encoding draws or touching the renderer state used by the next real frame.

Step 1: Build a scene with visible bloom #

The emissive torus gives bloom something to spread beyond its edge. The scene itself stays small so changes in the pass list are easy to attribute to the two controls.

final Material bright = Material(
  name: 'bright metal',
  baseColor: Vector4(0.7, 0.2, 0.08, 1.0),
  emissive: Vector3(1.0, 0.16, 0.03),
  emissiveStrength: 2.5,
  metallic: 0.62,
  roughness: 0.22,
);
_scene = Scene()
  ..ambientColor = Vector3(0.38, 0.46, 0.64)
  ..ambientIntensity = 0.12
  ..add(
    MeshNode(
      DeviceMesh.upload(
        context.device,
        const TorusShape(radius: 1.15, tubeRadius: 0.38).build(),
      ),
      bright,
      name: 'emissive torus',
    ),
  )
  ..add(
    LightNode(name: 'sun', intensity: 3.0)
      ..setLocalForward(Vector3(-0.45, -0.82, -0.35)),
  );

Step 2: Describe the requested passes #

Bloom and antialiasing are independent settings. Disabling either one removes its node and any graph resources used only by that node.

final RenderSettings next = RenderSettings(
  bloom: BloomSettings(intensity: bloom ? 0.65 : 0.0),
  antiAlias: AntiAliasSettings(enabled: antiAlias),
);

Step 3: Compile without drawing #

planFrame receives the same scene, views, and settings as render. Its order lists the passes that would run. skipped lists rejected passes and the reason for each rejection.

final CompiledFrameGraph graph = context.renderer.planFrame(
  scene: _scene,
  views: <RenderView>[context.view],
  settings: next,
);
_planned = <String>[
  for (final FrameGraphNode node in graph.order) node.name,
];
_skipped = <String>[
  for (final SkippedPass pass in graph.skipped) pass.name,
];

Step 4: Change the graph live #

The controls let you remove bloom and antialiasing separately. Each change is visible in the next plan before the frame is submitted.

List<DemoControl> controls(DemoContext context) => <DemoControl>[
  ToggleControl(
    'Bloom pass',
    value: () => bloom,
    onChanged: (bool value) => bloom = value,
  ),
  ToggleControl(
    'Antialias pass',
    value: () => antiAlias,
    onChanged: (bool value) => antiAlias = value,
  ),
];

Step 5: Compare plan and frame #

The page compares the ordered planned names with FrameResult.passes. It also checks that no skipped pass appears among the drawn ones. This proves the dry run and real frame used the same graph rules.

final List<String> drawn = <String>[
  for (final FramePass pass in frame.passes) pass.name,
];
if (_planned.isEmpty ||
    !_planned.contains('scene') ||
    !_planned.contains('composite') ||
    bloom && !_planned.contains('bloom') ||
    antiAlias && !_planned.contains('antialias') ||
    !_sameNames(_planned, drawn) ||
    _skipped.any(drawn.contains)) {
  throw StateError('the planned graph did not match the drawn frame');
}