render method
Renders the current state of this Scene onto the given ui.Canvas using the specified Camera.
The Camera provides the perspective from which the scene is viewed, and the ui.Canvas is the drawing surface onto which this Scene will be rendered.
Optionally, a ui.Rect can be provided to define a viewport, limiting the rendering area on the canvas. If no ui.Rect is specified, the entire canvas will be rendered.
pixelRatio is the multiplier from logical to physical pixels used
when allocating the offscreen render target. Defaults to the
implicit view's devicePixelRatio (or 1.0 if no view is attached),
so the scene is rasterized at the same density Flutter is
compositing the surrounding UI at. Pass a smaller value to trade
fidelity for performance, or a larger one for supersampling.
Implementation
void render(
Camera camera,
ui.Canvas canvas, {
ui.Rect? viewport,
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 = viewport ?? canvas.getLocalClipBounds();
if (drawArea.isEmpty) {
return;
}
// Allocate the offscreen render target at physical-pixel resolution so
// the rasterized 3D content matches Flutter's framebuffer density.
// Without this, the texture is sized in logical pixels and the
// framebuffer compositor upscales it (visible as pixelation on
// high-DPI devices). See: https://github.com/bdero/flutter_scene/issues/60
final dpr =
pixelRatio ??
ui.PlatformDispatcher.instance.implicitView?.devicePixelRatio ??
1.0;
final pixelSize = ui.Size(
(drawArea.width * dpr).ceilToDouble(),
(drawArea.height * dpr).ceilToDouble(),
);
final enableMsaa = _antiAliasingMode == AntiAliasingMode.msaa;
final gpu.Texture swapchainColor = surface.getNextSwapchainColorTexture(
pixelSize,
);
final swapchainTarget = gpu.RenderTarget.singleColor(
gpu.ColorAttachment(texture: swapchainColor),
);
// Resolve the IBL environment up front (before building the 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();
// Reuse one host buffer across frames; reset() cycles it to the next
// frame's backing storage.
final transientsBuffer =
_transientsBuffer ??= gpu.gpuContext.createHostBuffer();
transientsBuffer.reset();
final light = directionalLight;
// Cascaded shadows fit the camera frustum, so they require a
// perspective camera; other camera types render without shadows.
final cascades =
light != null && light.castsShadow && camera is PerspectiveCamera
? light.computeCascades(camera, pixelSize.width / pixelSize.height)
: const <ShadowCascade>[];
// Walk the graph once to tick components and animations and refresh
// the flat render list before the passes iterate it. Skipped when
// update() already ran the tick for 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 if the pre-pass changed the
// scene, before the render passes query it.
renderScene.rebuildIfDirty();
final graph = RenderGraph();
if (cascades.isNotEmpty) {
graph.addPass(
ShadowPass(
renderScene: renderScene,
cascades: cascades,
tileResolution: light!.shadowMapResolution,
),
);
}
graph.addPass(
ScenePass(
camera: camera,
renderScene: renderScene,
dimensions: pixelSize,
environmentMap: environmentMap,
environmentIntensity: environmentIntensity,
environmentTransform: environmentTransform,
enableMsaa: enableMsaa,
directionalLight: light,
cascades: cascades,
),
);
graph.addPass(
TonemapPass(
target: swapchainTarget,
exposure: exposure,
toneMappingMode: toneMapping,
),
);
graph.execute(
transientsBuffer: transientsBuffer,
texturePool: surface.transientTexturePool,
);
final image = swapchainColor.asImage();
canvas.drawImageRect(
image,
ui.Rect.fromLTWH(0, 0, pixelSize.width, pixelSize.height),
drawArea,
ui.Paint()..filterQuality = ui.FilterQuality.medium,
);
}