renderViews method

void renderViews(
  1. List<RenderView> views,
  2. Canvas canvas, {
  3. Rect? region,
  4. double? pixelRatio,
})

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;
}