2. Outline Post Process

Responsibility

The PostProcess class manages the fragment (pixel) shader. It is responsible for loading the shader program, creating GPU resources, and keeping uniform variables up to date each frame. You can also expose runtime settings through uniforms, for example to enable or disable effects.

Post process

Create a new file named outline_postprocess.dart. This class loads the shader program in onLoad() and passes uniform values to the GPU each frame in postProcess():

import 'dart:ui';

import 'package:flutter/material.dart';

import 'package:flame/components.dart';
import 'package:flame/post_process.dart';

extension on Color {
  Vector4 toVector4() {
    return Vector4(r, g, b, a);
  }
}

class OutlinePostProcess extends PostProcess {
  final double outlineSize;
  Color outlineColor;
  final Anchor anchor;

  OutlinePostProcess({
    this.outlineSize = 7.0,
    this.outlineColor = Colors.purpleAccent,
    this.anchor = Anchor.topLeft,
  });

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

  @override
  Future<void> onLoad() async {
    await super.onLoad();

    _fragmentProgram =
        await FragmentProgram.fromAsset('assets/shaders/outline.frag');
  }

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

    _fragmentShader.setFloatUniforms((value) {
      value
        ..setVector(size)
        ..setFloat(outlineSize)
        ..setVector(outlineColor.toVector4());
    });

    _fragmentShader.setImageSampler(0, preRenderedSubtree);

    canvas
      ..save()
      ..translate(-size.x * anchor.x, -size.y * anchor.y)
      ..drawRect(Offset.zero & size.toSize(), _myPaint)
      ..restore();
  }
}

With this file in place, the syntax error from the previous step will go away.

Since the PostProcessComponent is the parent of the SpriteComponent, the post process renders first and the sprite is drawn on top. The rasterizeSubtree() call captures all children into an image that the shader can sample from.

Usage

Now we need to wire everything together. Open main.dart and add both a plain sprite and an outlined sprite to the world so we can compare them side by side:

import 'package:flutter/material.dart';

import 'package:flame/components.dart';
import 'package:flame/game.dart';

import 'package:basic_shader_tutorial/sword_component.dart';

void main() {
  runApp(
    GameWidget(game: MyGame()),
  );
}

class MyGame extends FlameGame {
  MyGame() : super(world: MyWorld());

  @override
  Color backgroundColor() => Colors.green;
}

class MyWorld extends World {
  @override
  Future<void> onLoad() async {
    add(
      SwordSprite()
        ..position = Vector2(-200, 0)
        ..anchor = Anchor.center,
    );

    add(
      OutlinedSwordSprite(
        position: Vector2(200, 0),
        anchor: Anchor.center,
      ),
    );
  }
}

Here we use a custom FlameGame subclass to override the background color. Adjust the positions and color to suit your own images.

Run the application. You should see only one sprite, the outlined one is missing. The console will show why: [...] Unhandled Exception: Exception: Asset 'assets/shaders/outline.frag' not found [...]

We haven’t created the shader file yet. Let’s do that in the next step.