Post Processing

Post processing is a technique used in game development to apply visual effects to a component tree after it has been rendered. Once a frame is rendered — either directly or rasterized into an image—post processing can modify or enhance the visuals.

Post processing leverages fragment shaders to create dynamic visual effects such as blur, bloom, color grading, distortion, and lighting adjustments.

In Flame, the post processing system is modular and flexible, allowing developers to:

  • Define custom post processes by sub-classing the abstract class PostProcess.

  • Apply a single post process effect or chain multiple effects using groups.

  • Manage effects globally with the CameraComponent or locally with PostProcessComponent.

Key Components of the Post Processing System

  • PostProcess: Abstract base class for defining custom post-processing effects. Implement your effect logic in its postProcess method.

  • PostProcessComponent: Applies a post process specifically to its children, enabling localized effects.

  • CameraComponent: Applies post processes globally to the entire scene or world.

  • PostProcessGroup: Applies multiple post processes in parallel, useful when effects can be applied independently.

  • PostProcessSequentialGroup: Applies post processes sequentially, where each process uses the output of the previous one.

Creating a Custom Post Process

To implement a custom post process:

  1. Subclass PostProcess.

  2. Override the postProcess method, implementing your rendering logic with renderSubtree or rasterizeSubtree.

  3. Optionally, implement onLoad and update methods for managing resources and updating effects each frame.

This system makes it easy to add creative and useful visual effects to your Flame game.

Example: pixelation

Here’s an example of creating a pixelation effect using a fragment shader:

class PostProcessGame extends FlameGame {
  @override
  Future<void> onLoad() async {
    await super.onLoad();

    world.add(
      PostProcessComponent(
        postProcess: PixelationPostProcess(),
        anchor: Anchor.center,
        children: [
          EmberPlayer(size: Vector2(100, 100)),
        ],
      ),
    );
  }
}

class PixelationPostProcess extends PostProcess {
  @override
  Future<void> onLoad() async {
    await super.onLoad();

    _fragmentProgram = await FragmentProgram.fromAsset(
      'packages/flutter_shaders/shaders/pixelation.frag',
    );
  }

  late final FragmentProgram _fragmentProgram;
  late final FragmentShader _fragmentShader = _fragmentProgram.fragmentShader();

  double _time = 0;

  @override
  void update(double dt) {
    super.update(dt);
    _time += dt;
  }

late final myPaint = Paint()..shader = _fragmentShader;


  @override
  void postProcess(Vector2 size, Canvas canvas) {
    final preRenderedSubtree = rasterizeSubtree();

    _fragmentShader.setFloatUniforms((value) {
      value
        ..setVector(size / (20 * sin(_time)))
        ..setVector(size);
    });

    _fragmentShader.setImageSampler(0, preRenderedSubtree);

    canvas
      ..save()
      ..drawRect(Offset.zero & size.toSize(), myPaint)
      ..restore();
  }
}

In this example:

  • A fragment shader (pixelation.frag) is loaded and used to apply a pixelation effect.

  • The rasterizeSubtree method captures the component tree rendering as a texture, which the shader uses to generate the pixelated output.

  • The effect dynamically changes over time, creating an animated pixelation effect.

This example demonstrates how straightforward it is to add visual effects to your Flame game using the post-processing system.

post_process.dart
 1import 'dart:math';
 2import 'dart:ui';
 3
 4import 'package:doc_flame_examples/ember.dart';
 5import 'package:flame/components.dart';
 6import 'package:flame/game.dart';
 7import 'package:flame/post_process.dart';
 8import 'package:flutter_shaders/flutter_shaders.dart';
 9
10class PostProcessGame extends FlameGame {
11  @override
12  Future<void> onLoad() async {
13    await super.onLoad();
14
15    world.add(
16      PostProcessComponent(
17        postProcess: PixelationPostProcess(),
18        position: Vector2(0, 0),
19        anchor: Anchor.center,
20        children: [
21          EmberPlayer(size: Vector2(100, 100)),
22        ],
23      ),
24    );
25  }
26}
27
28class PixelationPostProcess extends PostProcess {
29  @override
30  Future<void> onLoad() async {
31    await super.onLoad();
32
33    _fragmentProgram = await FragmentProgram.fromAsset(
34      'packages/flutter_shaders/shaders/pixelation.frag',
35    );
36  }
37
38  late final FragmentProgram _fragmentProgram;
39  late final FragmentShader _fragmentShader = _fragmentProgram.fragmentShader();
40
41  double _time = 0;
42
43  @override
44  void update(double dt) {
45    super.update(dt);
46    _time += dt;
47  }
48
49  late final myPaint = Paint()..shader = _fragmentShader;
50
51  @override
52  void postProcess(Vector2 size, Canvas canvas) {
53    final preRenderedSubtree = rasterizeSubtree();
54
55    _fragmentShader.setFloatUniforms((value) {
56      value
57        ..setVector(size / (20 * sin(_time)))
58        ..setVector(size);
59    });
60
61    _fragmentShader.setImageSampler(0, preRenderedSubtree);
62
63    canvas
64      ..save()
65      ..drawRect(Offset.zero & size.toSize(), myPaint)
66      ..restore();
67  }
68}

The pixelation shader file:

#version 460 core

precision highp float;

#include <flutter/runtime_effect.glsl>

uniform vec2 uPixels;
uniform vec2 uSize;
uniform sampler2D uTexture;

out vec4 fragColor;

void main() {
  vec2 uv = FlutterFragCoord().xy / uSize;
  vec2 puv = round(uv * uPixels) / uPixels;
  fragColor = texture(uTexture, puv);
}

Advanced Example: Crystal Ball

For a more advanced use case of post processing, check out the Crystal Ball example, which demonstrates camera-level post processing and chaining multiple effects using PostProcessSequentialGroup.

Crystal Ball Example

Here’s how multiple post-processing effects are combined on a camera:

class CrystalBallGame extends FlameGame<CrystalBallGameWorld> {

  CrystalBallGame() : super(
          camera: CameraComponent.withFixedResolution(
            width: kCameraSize.x,
            height: kCameraSize.y,
          ),
          world: CrystalBallGameWorld(),
        ) {
    camera.postProcess = PostProcessGroup(
      postProcesses: [
        PostProcessSequentialGroup(
          postProcesses: [
            FireflyPostProcess(),
            WaterPostProcess(),
          ],
        ),
        ForegroundFogPostProcess(),
      ],
    );
  }
}

In this code:

  • The camera applies a PostProcessGroup containing multiple effects.

  • PostProcessSequentialGroup chains two effects (FireflyPostProcess and WaterPostProcess) sequentially.

  • An additional parallel effect (ForegroundFogPostProcess) is applied alongside the sequential group.

You can explore the source code on GitHub.