Post effects on your own image
Open the live demo · Read the source · View on GitHub
Bloom, exposure and the tone curve normally run inside a frame, on a scene the
renderer just drew. Renderer.renderPost runs them on a picture you hand it.
That is what you want when the picture came from somewhere else: a video frame,
a texture you painted, or a second scene you drew earlier in the same update.
This page draws a glowing bar into a texture of its own, sends that texture
through renderPost, and shows the answer on a screen in a second scene.
Step 1: Make a picture #
The source is a small scene with one very bright bar on black. It has its own
Scene, its own camera and its own RenderView, so nothing in it is shared with
what you see on the screen.
final CameraNode eye = CameraNode(name: 'source eye')
..setPosition(0.0, 0.0, 4.0);
_source = Scene()
..add(
MeshNode(
DeviceMesh.upload(
context.device,
CuboidShape(size: Vector3(2.4, 0.5, 0.2)).build(),
),
Material(
name: 'bar',
baseColor: Vector4(0.0, 0.0, 0.0, 1.0),
emissive: Vector3(1.0, 0.55, 0.2),
emissiveStrength: 6.0,
),
name: 'bar',
),
)
..add(eye);
_sourceView = RenderView(
camera: eye,
clearColor: Vector4(0.0, 0.0, 0.0, 1.0),
);
Step 2: Draw it without post effects #
The first render turns bloom and tone mapping off, so what comes back is the
picture as it was drawn. frame is the renderer's own target and is only good
until the next render, so the next step has to use it straight away.
final TextureHandle picture = context.renderer
.render(
width: _width,
height: _height,
scene: _source,
views: <RenderView>[_sourceView],
settings: const RenderSettings(
bloom: BloomSettings(enabled: false),
shadows: ShadowSettings(enabled: false),
tonemap: false,
),
)
.frame;
Step 3: Send it through renderPost #
renderPost takes the picture as hdr, runs bloom over it, applies exposure and
the tone curve from the settings you pass, and writes the result into target.
Nothing about a scene is involved: there are no lights to count and no meshes to
cull, which is why the answer is a PostFrameResult and not a FrameResult.
Drag Threshold down and the glow around the bar gets wider, because more of the picture is bright enough to feed it. Drag Exposure to make the whole picture brighter or darker before the curve.
result = context.renderer.renderPost(
hdr: picture,
target: _output,
settings: RenderSettings(
exposure: exposure,
bloom: BloomSettings(threshold: threshold, intensity: intensity),
),
);
Only bloom and the composite run here. Passes that read a surface buffer, such as ambient occlusion, need a scene to describe and are not part of this call.
Step 4: Put the answer somewhere #
The target is an ordinary texture, so it can be used like one. Here it is the albedo of an unlit screen, and the scene that the viewport draws is only that screen.
_output = context.device.createTexture(
RenderTargetSpec(
width: _width,
height: _height,
format: context.device.defaultColorFormat,
),
);
final MeshNode screen = MeshNode(
DeviceMesh.upload(
context.device,
CuboidShape(size: Vector3(3.2, 1.8, 0.02)).build(),
),
Material(name: 'screen', lighting: LightingModel.unlit, albedo: _output),
name: 'screen',
);
Step 5: Say what happened #
The page checks that renderPost was called and that it wrote into the texture
it was given.
final PostFrameResult? post = result;
if (post == null) {
throw StateError('renderPost was never called');
}
if (!identical(post.frame, _output)) {
throw StateError('renderPost drew somewhere other than the target');
}