renderViews method
Renders a list of views of this scene onto canvas.
Each RenderView binds a camera to a normalized sub-rectangle of
region (its RenderView.viewport), a RenderView.layerMask, and a
compositing RenderView.order; views are drawn lowest-order first.
This is how split-screen and picture-in-picture are rendered. render
is the single-view convenience over this.
Each view renders into its own offscreen target (its own swapchain texture and transient texture pool), so simultaneous views never share a render target within a frame.
region is the canvas rectangle the views subdivide; it defaults to
the canvas clip bounds. pixelRatio is the logical-to-physical
multiplier for the offscreen render targets (defaults to the view's
device pixel ratio).
The scene is advanced once per call (a single per-frame tick), then every view is rendered from that shared scene state.
Implementation
void renderViews(
List<RenderView> views,
ui.Canvas canvas, {
ui.Rect? region,
double? pixelRatio,
}) {
if (!_readyToRender) {
debugPrint('Flutter Scene is not ready to render. Skipping frame.');
debugPrint(
'You may wait on the Future returned by Scene.initializeStaticResources() before rendering.',
);
return;
}
final drawArea = region ?? canvas.getLocalClipBounds();
if (drawArea.isEmpty || views.isEmpty) {
return;
}
// Blend the environment volumes over the base by the primary view's camera
// position, before the environment, sky bake, and sun light are read.
_applyEnvironmentVolumes(views.first.camera);
final dpr =
pixelRatio ??
ui.PlatformDispatcher.instance.implicitView?.devicePixelRatio ??
1.0;
// Re-bake the sky-driven environment when its refresh policy says one is
// due. The bake submits its own passes, so like the lazy default-prefilter
// below it must run before this frame's render graph is built.
final skyEnv = skyEnvironment;
if (skyEnv != null) {
final baked = skyEnv.bakeIfDue(DateTime.now());
if (baked != null) {
environment = baked;
// When volume blending holds a sky-lit base, the base snapshot's
// environment is captured once and would otherwise go stale as the sky
// re-bakes. Refresh it, but only while the active binding is the base's
// own (not a volume's), so a volume's bake never overwrites the base.
final base = baseEnvironment;
if (base != null && identical(skyEnv, base.skyEnvironment)) {
base.environment = baked;
}
}
}
// Aim the sky-driven sun light before the tick collects lights, so its
// direction/color follow the sky this frame. The binding mutates one light
// in place, so it is registered with the graph once and updated thereafter.
final sun = _sunLight;
if (sun != null) {
final resolved = sun.resolve();
if (!identical(directionalLight, resolved)) {
directionalLight = resolved;
}
}
// The web radiance prefilter is degenerate when built on a cold WebGL
// context (before the first frame composites); environments built then
// (the lazily built default below, or any the app built up front) are
// re-baked once a frame has been presented and the context is warm. No-op
// on other backends and after the one-time rebuild. See
// EnvironmentMap.markContextWarmAndRebakeRadiance.
if (_hasPresentedFrame) {
EnvironmentMap.markContextWarmAndRebakeRadiance();
}
// Resolve the IBL environment up front (before building any render
// graph): the default is built lazily here on first use, which submits
// a one-time prefilter pass that must not be nested inside the frame's
// render passes. Doing this in the constructor instead would break the
// OpenGL ES backend, which sets up its context lazily on the raster
// thread only after the first frame.
final environmentMap = environment ?? Material.getDefaultEnvironmentMap();
// Advance the per-frame transient arenas (uniform blocks and
// instance-rate vertex data): recycle blocks whose GPU work completed
// and reset the frame stats. Shared by every view this frame.
uniformTransients.beginFrame();
instanceTransients.beginFrame();
final TransientWriter transientsBuffer = uniformTransients;
// Advance the scene once per frame (not once per view): tick components
// and animations and refresh the flat render list before the passes
// iterate it. Skipped when update() already ran the tick this frame.
if (!_tickedThisFrame) {
final nowMillis = DateTime.now().millisecondsSinceEpoch;
final lastMillis = _lastTickMillis ?? nowMillis;
_tick((nowMillis - lastMillis) / 1000.0);
}
_tickedThisFrame = false;
// Rebuild the spatial culling structure once if the pre-pass changed the
// scene, before the views' render passes query it.
renderScene.rebuildIfDirty();
// The renderer shades a single directional light: the first one
// registered in the graph (the [directionalLight] convenience, or a
// [DirectionalLightComponent] attached to any node).
final lightComponent = renderScene.directionalLights.isEmpty
? null
: renderScene.directionalLights.first;
// Select this frame's shadow-casting spots (view-independent).
final spotShadowFrame = collectSpotShadows(renderScene.spotLights);
// The additional analytic lights (point, spot, and directional lights past
// the first) are view-independent, so build their shared data texture once
// per frame here rather than per view.
final punctualLighting = _punctualLightBuffer.build(
directionals: renderScene.directionalLights,
points: renderScene.pointLights,
spots: renderScene.spotLights,
items: renderScene.items,
bvh: renderScene.bvh,
spotShadows: spotShadowFrame,
);
// Texture-target views render first so screen views (and the HUD)
// composite this frame's captures, the simple form of the
// produce-before-consume rule.
// TODO(rendertarget): order texture views among themselves by
// resource read/write edges once materials can sample render textures.
final textureViews = <RenderView>[
for (final view in this.views)
if (view.target != null) view,
for (final view in views)
if (view.target != null) view,
]..sort((a, b) => a.order.compareTo(b.order));
final now = DateTime.now();
for (final view in textureViews) {
final target = view.target!;
if (!target.shouldUpdate(now)) {
continue;
}
_renderViewToTexture(
view: view,
outputColor: target.acquireNextTexture(),
pixelSize: ui.Size(target.width.toDouble(), target.height.toDouble()),
pool: target.transientTexturePool,
environmentMap: environmentMap,
transientsBuffer: transientsBuffer,
lightComponent: lightComponent,
punctualLighting: punctualLighting,
spotShadowFrame: spotShadowFrame,
);
target.markUpdated(now);
}
// Composite lower-order screen views first.
final screenViews = [
for (final view in views)
if (view.target == null) view,
];
final ordered = screenViews.length == 1
? screenViews
: (screenViews..sort((a, b) => a.order.compareTo(b.order)));
for (var i = 0; i < ordered.length; i++) {
final view = ordered[i];
final viewArea = _viewDrawArea(drawArea, view.viewport);
if (viewArea.isEmpty) {
continue;
}
_renderViewToCanvas(
view: view,
canvas: canvas,
drawArea: viewArea,
dpr: dpr,
viewIndex: i,
environmentMap: environmentMap,
transientsBuffer: transientsBuffer,
lightComponent: lightComponent,
punctualLighting: punctualLighting,
spotShadowFrame: spotShadowFrame,
);
}
// A frame has now been submitted; the next one runs on a warm context (see
// the rebuild near the environment resolution above).
_hasPresentedFrame = true;
}