Plan a frame before drawing
Open the live demo · Read the source · View on GitHub
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');
}