Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion doc/flame/rendering/post_processing.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ 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.
shader uses to generate the pixelated output. The texture is rendered at the post process's
`pixelRatio` and covers the whole subtree, so the shader can sample it with coordinates from 0
to 1 across `size`.

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ class WaterPostProcess({
..setFloatUniforms((value) {
value
..setVector(size)
..setFloat(pixelRatio)
..setFloat(uvGround)
..setFloat(time);
})
Expand Down
3 changes: 1 addition & 2 deletions examples/games/crystal_ball/shaders/ground.frag
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ precision highp float;
#include <flutter/runtime_effect.glsl>

uniform vec2 uSize;
uniform float uPixelSize;
uniform float uWaterLevel;
uniform float uTime;

Expand Down Expand Up @@ -45,7 +44,7 @@ vec4 fragment(vec2 uv) {
fogIntensity = waterProximity * 0.1;
}

vec4 baseColor = texture(tGameCanvas, reflectedUv / uPixelSize) * waterColor;
vec4 baseColor = texture(tGameCanvas, reflectedUv) * waterColor;

return mix(baseColor, fogColor, fogIntensity);
}
Expand Down
11 changes: 8 additions & 3 deletions packages/flame/lib/src/post_process/post_process.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ import 'package:flutter/foundation.dart';
/// - [PostProcessingContextFinder] for a way to access the post process
/// context from within a component during rendering
abstract class PostProcess({double? pixelRatio}) {
/// The pixel ratio of the screen. This is used to scale the image generated
/// by [rasterizeSubtree] to the correct size.
/// The pixel ratio of the screen. The image generated by [rasterizeSubtree]
/// is rendered at this many pixels per logical pixel, so that it stays sharp
/// on high density screens.
///
/// Defaults to [FlutterView.devicePixelRatio].
double pixelRatio =
Expand Down Expand Up @@ -111,13 +112,17 @@ abstract class PostProcess({double? pixelRatio}) {
/// subtree to an image, which can be used as a texture in a shader. This
/// is an expensive operation, so it should be used sparingly.
///
/// The image is `pixelRatio` times the size of the post process and the
/// subtree fills all of it, so a shader can sample it with texture
/// coordinates going from 0 to 1 across the size of the post process.
///
/// This method will set the context of the post process, so that
/// components can know they are being rendered within a post process.
@nonVirtual
@protected
ui.Image rasterizeSubtree() {
final recorder = PictureRecorder();
final innerCanvas = Canvas(recorder);
final innerCanvas = Canvas(recorder)..scale(pixelRatio);

renderSubtree(innerCanvas);

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
151 changes: 151 additions & 0 deletions packages/flame/test/post_process/rasterize_subtree_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import 'dart:typed_data';
import 'dart:ui';

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

void main() {
group('rasterizeSubtree', () {
const red = Color(0xFFFF0000);
const blue = Color(0xFF0000FF);

for (final pixelRatio in [1.0, 1.5, 2.0, 3.0]) {
test('fills the whole image at pixel ratio $pixelRatio', () async {
final size = Vector2(40, 20);
final postProcess = _CapturingPostProcess(pixelRatio: pixelRatio);

postProcess.render(
Canvas(PictureRecorder()),
size,
(canvas) {
// Red on the left half, blue on the right half.
canvas.drawRect(
Rect.fromLTWH(0, 0, size.x / 2, size.y),
Paint()..color = red,
);
canvas.drawRect(
Rect.fromLTWH(size.x / 2, 0, size.x / 2, size.y),
Paint()..color = blue,
);
},
(_) {},
);

final image = postProcess.image!;
expect(image.width, (size.x * pixelRatio).ceil());
expect(image.height, (size.y * pixelRatio).ceil());

final pixels = await _Pixels.of(image);
expect(pixels.at(0, 0), red);
expect(pixels.at(image.width ~/ 4, image.height ~/ 2), red);
expect(pixels.at(image.width * 3 ~/ 4, image.height ~/ 2), blue);
expect(pixels.at(image.width - 1, image.height - 1), blue);
});
}
});

group('PostProcessComponent with rasterizeSubtree', () {
const goldenFile = '../_goldens/post_process_rasterize_subtree.png';
final goldenSize = Vector2(200, 100);
final cardPosition = Vector2(-30, -20);

testGolden(
'renders the subtree without a post process',
(game, tester) async {
game.world.add(_TestCard()..position = cardPosition);
},
goldenFile: goldenFile,
size: goldenSize,
);

for (final pixelRatio in [1.0, 2.0, 3.0]) {
testGolden(
'renders the rasterized subtree at its full size at pixel ratio '
'$pixelRatio',
(game, tester) async {
game.world.add(
PostProcessComponent(
postProcess: _DrawImagePostProcess(pixelRatio: pixelRatio),
position: cardPosition,
size: _TestCard.cardSize,
children: [_TestCard()],
),
);
},
goldenFile: goldenFile,
size: goldenSize,
);
}
});
}

class _CapturingPostProcess({super.pixelRatio}) extends PostProcess {
Image? image;

@override
void postProcess(Vector2 size, Canvas canvas) {
image = rasterizeSubtree();
}
}

/// Draws the rasterized subtree stretched over the size of the post process,
/// which should look exactly like rendering the subtree directly.
class _DrawImagePostProcess({super.pixelRatio}) extends PostProcess {
final _paint = Paint()..filterQuality = FilterQuality.none;

@override
void postProcess(Vector2 size, Canvas canvas) {
final image = rasterizeSubtree();
canvas.drawImageRect(
image,
Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()),
Offset.zero & size.toSize(),
_paint,
);
}
}

/// A card made only of pixel aligned rectangles, so that downscaling a
/// rasterized copy of it with nearest sampling gives back the same pixels.
class _TestCard() extends PositionComponent {
this : super(size: cardSize);

static Vector2 get cardSize => Vector2(60, 40);

static final _fill = Paint()..color = const Color(0xFF1E4D8C);
static final _border = Paint()..color = const Color(0xFFE53935);
static final _center = Paint()..color = const Color(0xFFFDD835);
static final _corner = Paint()..color = const Color(0xFF43A047);

@override
void render(Canvas canvas) {
canvas
..drawRect(const Rect.fromLTWH(0, 0, 60, 40), _fill)
..drawRect(const Rect.fromLTWH(0, 0, 60, 4), _border)
..drawRect(const Rect.fromLTWH(0, 36, 60, 4), _border)
..drawRect(const Rect.fromLTWH(0, 0, 4, 40), _border)
..drawRect(const Rect.fromLTWH(56, 0, 4, 40), _border)
..drawRect(const Rect.fromLTWH(20, 14, 20, 12), _center)
..drawRect(const Rect.fromLTWH(0, 0, 10, 10), _corner)
..drawRect(const Rect.fromLTWH(50, 30, 10, 10), _corner);
}
}

class _Pixels(final ByteData _bytes, final int _width) {
static Future<_Pixels> of(Image image) async {
final data = await image.toByteData();
return _Pixels(data!, image.width);
}

Color at(int x, int y) {
final offset = (y * _width + x) * 4;
return Color.fromARGB(
_bytes.getUint8(offset + 3),
_bytes.getUint8(offset),
_bytes.getUint8(offset + 1),
_bytes.getUint8(offset + 2),
);
}
}
Loading