Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Fix tile flips when using canvas.drawAtlas #1610

Merged
merged 6 commits into from
May 8, 2022
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
55 changes: 45 additions & 10 deletions packages/flame/lib/src/sprite_batch.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,16 @@ class BatchItem {
/// The transform values for this batch item.
final RSTransform transform;

/// The flip value for this batch item.
final bool flip;

/// The background color for this batch item.
final Color color;

/// Fallback matrix for the web.
///
/// Because `Canvas.drawAtlas` is not supported on the web we also
/// build a `Matrix4` based on the [transform] values.
/// build a `Matrix4` based on the [transform] and [flip] values.
final Matrix4 matrix;

/// Paint object used for the web.
Expand All @@ -62,9 +65,10 @@ class BatchItem {
BatchItem({
required this.source,
required this.transform,
this.flip = false,
required this.color,
}) : matrix = Matrix4(
transform.scos, transform.ssin, 0, 0, //
transform.scos * (flip ? -1 : 1), transform.ssin, 0, 0, //
-transform.ssin, transform.scos, 0, 0, //
0, 0, _defaultScale, 0, //
transform.tx, transform.ty, 0, 1, //
Expand Down Expand Up @@ -167,22 +171,36 @@ class SpriteBatch {
Images? images,
bool useAtlas = true,
}) async {
final _images = images ?? Flame.images;
return SpriteBatch(
await _images.load(path),
await _generateAtlas(images, path),
defaultColor: defaultColor,
defaultTransform: defaultTransform ?? RSTransform(1, 0, 0, 0),
defaultBlendMode: defaultBlendMode,
useAtlas: useAtlas,
);
}

static Future<Image> _generateAtlas(Images? images, String path) async {
final _images = images ?? Flame.images;
final image = await _images.load(path);
final recorder = PictureRecorder();
final canvas = Canvas(recorder);
final _emptyPaint = Paint();
canvas.drawImage(image, Offset.zero, _emptyPaint);
canvas.scale(-1, 1);
canvas.drawImage(image, Offset(-image.width * 2, 0), _emptyPaint);

final picture = recorder.endRecording();
final atlas = picture.toImage(image.width * 2, image.height);
return atlas;
}

/// Add a new batch item using a RSTransform.
///
/// The [source] parameter is the source location on the [atlas].
///
/// You can position, rotate and scale it on the canvas using the [transform]
/// parameter.
/// You can position, rotate, scale and flip it on the canvas using the
/// [transform] and [flip] parameters.
///
/// The [color] parameter allows you to render a color behind the batch item,
/// as a background color.
Expand All @@ -195,17 +213,28 @@ class SpriteBatch {
void addTransform({
required Rect source,
RSTransform? transform,
bool flip = false,
Color? color,
}) {
final batchItem = BatchItem(
source: source,
transform: transform ??= defaultTransform ?? RSTransform(1, 0, 0, 0),
flip: flip,
color: color ?? defaultColor,
);

_batchItems.add(batchItem);

_sources.add(batchItem.source);
_sources.add(
flip
? Rect.fromLTWH(
atlas.width - source.left - source.width,
source.top,
source.width,
source.height,
)
: batchItem.source,
);
_transforms.add(batchItem.transform);
_colors.add(batchItem.color);
}
Expand All @@ -215,8 +244,8 @@ class SpriteBatch {
/// The [source] parameter is the source location on the [atlas]. You can
/// position it on the canvas using the [offset] parameter.
///
/// You can transform the sprite from its [offset] using [scale], [rotation]
/// and [anchor].
/// You can transform the sprite from its [offset] using [scale], [rotation],
/// [anchor] and [flip].
///
/// The [color] paramater allows you to render a color behind the batch item,
/// as a background color.
Expand All @@ -234,6 +263,7 @@ class SpriteBatch {
Vector2? anchor,
double rotation = 0,
Vector2? offset,
bool flip = false,
Color? color,
}) {
anchor ??= Vector2.zero();
Expand All @@ -257,7 +287,12 @@ class SpriteBatch {
);
}

addTransform(source: source, transform: transform, color: color);
addTransform(
source: source,
transform: transform,
flip: flip,
color: color,
);
}

/// Clear the SpriteBatch so it can be reused.
Expand Down
6 changes: 2 additions & 4 deletions packages/flame_tiled/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# flame_tiled

> :warning: Under the current sprite batch implementation, Flips are not working properly.
> Any help is appreciated looking into this issue.
>
> You might also experience extra lines while rendering due to a bug in Flutter,
> :warning: Under the current sprite batch implementation, you might experience extra
> lines while rendering due to a bug in Flutter,
> see [this issue](https://github.com/flame-engine/flame/issues/1152).

Package to bridge the `tiled` library into easy-to-use Flame components.
Expand Down
23 changes: 16 additions & 7 deletions packages/flame_tiled/lib/src/renderable_tile_map.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'dart:math' as math;
import 'dart:ui' as ui;

import 'package:collection/collection.dart';
import 'package:flame/extensions.dart';
Expand Down Expand Up @@ -178,14 +178,23 @@ class RenderableTiledMap {
final src = ts.computeDrawRect(t).toRect();
final flips = SimpleFlips.fromFlips(tile.flips);
final size = destTileSize;
final scale = size.x / src.width;
final anchorX = src.width / 2;
final anchorY = src.height / 2;
final offsetX = ((tx + .5) * size.x) + (layerOffset.x * scale);
final offsetY = ((ty + .5) * size.y) + (layerOffset.y * scale);
final scos = flips.cos * scale;
final ssin = flips.sin * scale;
if (batch != null) {
batch.add(
batch.addTransform(
source: src,
offset: Vector2((tx + .5) * size.x, (ty + .5) * size.y)
..add(layerOffset * size.x / src.width),
rotation: flips.angle * math.pi / 2,
anchor: Vector2(src.width / 2, src.height / 2),
scale: size.x / src.width,
transform: ui.RSTransform(
scos,
ssin,
offsetX + -scos * anchorX + ssin * anchorY,
offsetY + -ssin * anchorX - scos * anchorY,
),
flip: flips.flip,
);
}
}
Expand Down
83 changes: 50 additions & 33 deletions packages/flame_tiled/lib/src/simple_flips.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,65 +4,82 @@ import 'package:tiled/tiled.dart';
/// Tiled represents all flips and rotation using three possible flips:
/// horizontal, vertical and diagonal.
/// This class converts that representation to a simpler one, that uses one
/// angle (with pi/2 steps) and two flips (H or V).
/// angle (with pi/2 steps) and one flip (horizontal). All vertical flips are
/// represented as horizontal flips + 180º.
/// Further reference:
/// https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#tile-flipping.
///
/// `cos` and `sin` are the cosine and sine of the rotation respectively, and
/// and are provided for simple calculation with RSTransform.
/// Further reference:
/// https://api.flutter.dev/flutter/dart-ui/RSTransform/RSTransform.html
/// {@endtemplate}
class SimpleFlips {
/// The angle (in steps of pi/2 rads), clockwise, around the center of the tile.
final int angle;

/// Whether to flip across a central vertical axis.
final bool flipH;
/// The cosine of the rotation.
final int cos;

/// The sine of the rotation.
final int sin;

/// Whether to flip across a central horizontal axis.
final bool flipV;
/// Whether to flip (across a central vertical axis).
final bool flip;

/// {@macro _simple_flips}
SimpleFlips(this.angle, this.flipH, this.flipV);
SimpleFlips(this.angle, this.cos, this.sin, this.flip);

/// This is the conversion from the truth table that I drew.
factory SimpleFlips.fromFlips(Flips flips) {
int angle;
bool flipV, flipH;
int angle, cos, sin;
bool flip;

if (!flips.diagonally && !flips.vertically && !flips.horizontally) {
angle = 0;
flipV = false;
flipH = false;
cos = 1;
sin = 0;
flip = false;
} else if (!flips.diagonally && !flips.vertically && flips.horizontally) {
angle = 0;
flipV = false;
flipH = true;
} else if (!flips.diagonally && flips.vertically && !flips.horizontally) {
angle = 0;
flipV = true;
flipH = false;
} else if (!flips.diagonally && flips.vertically && flips.horizontally) {
angle = 2;
flipV = false;
flipH = false;
} else if (flips.diagonally && !flips.vertically && !flips.horizontally) {
angle = 1;
flipV = false;
flipH = true;
cos = 1;
sin = 0;
flip = true;
} else if (flips.diagonally && !flips.vertically && flips.horizontally) {
angle = 1;
flipV = false;
flipH = false;
} else if (flips.diagonally && flips.vertically && !flips.horizontally) {
angle = 3;
flipV = false;
flipH = false;
cos = 0;
sin = 1;
flip = false;
} else if (flips.diagonally && flips.vertically && flips.horizontally) {
angle = 1;
flipV = true;
flipH = false;
cos = 0;
sin = 1;
flip = true;
} else if (!flips.diagonally && flips.vertically && flips.horizontally) {
angle = 2;
cos = -1;
sin = 0;
flip = false;
} else if (!flips.diagonally && flips.vertically && !flips.horizontally) {
angle = 2;
cos = -1;
sin = 0;
flip = true;
} else if (flips.diagonally && flips.vertically && !flips.horizontally) {
angle = 3;
cos = 0;
sin = -1;
flip = false;
} else if (flips.diagonally && !flips.vertically && !flips.horizontally) {
angle = 3;
cos = 0;
sin = -1;
flip = true;
} else {
// this should be exhaustive
throw 'Invalid combination of booleans: $flips';
}

return SimpleFlips(angle, flipH, flipV);
return SimpleFlips(angle, cos, sin, flip);
}
}
Binary file added packages/flame_tiled/test/assets/4_color_sprite.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions packages/flame_tiled/test/assets/8_tiles-flips.tmx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.8" tiledversion="1.8.4" orientation="orthogonal" renderorder="right-down" width="4" height="2" tilewidth="16" tileheight="16" infinite="0" nextlayerid="2" nextobjectid="1">
<tileset firstgid="1" name="4_color_sprite" tilewidth="16" tileheight="16" tilecount="1" columns="1">
<image source="4_color_sprite.png" width="16" height="16"/>
</tileset>
<layer id="1" name="Tile Layer 1" width="4" height="2">
<data encoding="csv">
2684354561,3221225473,1073741825,536870913,
1,1610612737,3758096385,2147483649
</data>
</layer>
</map>
58 changes: 58 additions & 0 deletions packages/flame_tiled/test/tiled_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,64 @@ void main() {
});
});

group('Flipped and rotated tiles render correctly with sprite batch', () {
late Uint8List canvasPixelData;
late RenderableTiledMap overlapMap;
setUp(() async {
Flame.bundle = TestAssetBundle(
imageNames: [
'4_color_sprite.png',
],
mapPath: 'test/assets/8_tiles-flips.tmx',
);
overlapMap = await RenderableTiledMap.fromFile(
'8_tiles-flips.tmx',
Vector2.all(16),
);
final canvasRecorder = PictureRecorder();
final canvas = Canvas(canvasRecorder);
overlapMap.render(canvas);
final picture = canvasRecorder.endRecording();

final image = await picture.toImage(64, 48);
final bytes = await image.toByteData();
canvasPixelData = bytes!.buffer.asUint8List();
});

test('Green tile pixels are in correct spots', () {
final leftTilePixels = <int>[];
for (var ind = 65 * 8 * 4;
ind < ((64 * 23) + (8 * 3)) * 4;
ind += 64 * 4) {
leftTilePixels.addAll(canvasPixelData.getRange(ind, ind + (16 * 4)));
}

var allGreen = true;
for (var indGreen = 0; indGreen < leftTilePixels.length; indGreen += 4) {
allGreen &= leftTilePixels[indGreen] == 0 &&
leftTilePixels[indGreen + 1] == 255 &&
leftTilePixels[indGreen + 2] == 0 &&
leftTilePixels[indGreen + 3] == 255;
}
expect(allGreen, true);

final rightTilePixels = <int>[];
for (var ind = 69 * 8 * 4;
ind < ((64 * 23) + (8 * 7)) * 4;
ind += 64 * 4) {
rightTilePixels.addAll(canvasPixelData.getRange(ind, ind + (16 * 4)));
}

for (var indGreen = 0; indGreen < rightTilePixels.length; indGreen += 4) {
allGreen &= rightTilePixels[indGreen] == 0 &&
rightTilePixels[indGreen + 1] == 255 &&
rightTilePixels[indGreen + 2] == 0 &&
rightTilePixels[indGreen + 3] == 255;
}
expect(allGreen, true);
});
});

group('Test getLayer:', () {
late RenderableTiledMap _renderableTiledMap;
setUp(() async {
Expand Down