Skip to content
Draft
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
69 changes: 67 additions & 2 deletions packages/flame/lib/src/sprite_batch.dart
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class BatchItem {
required this.transform,
Color? color,
this.flip = false,
this.bleed = 0,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should do an assertion to prevent negative values

}) : color = color ?? const Color(0x00000000),
paint = Paint()..color = color ?? const Color(0x00000000),
destination = Offset.zero & source.size;
Expand All @@ -59,6 +60,12 @@ class BatchItem {
/// The flip value for this batch item.
bool flip;

/// The bleed value for this batch item in pixels.
/// When greater than 0, the destination is expanded outward by this amount
/// while keeping the source sampling region the same, which helps prevent
/// edge artifacts (seams between tiles in a tilemap).
double bleed;

/// The color of the batch item (used for building the drawAtlas color list).
Color color;

Expand Down Expand Up @@ -305,6 +312,37 @@ class SpriteBatch {
);
}

/// Computes a transform with bleed applied.
///
/// The bleed expands the destination rectangle outward by the bleed amount
/// in all directions while keeping the source sampling region unchanged.
/// This helps prevent edge artifacts (seams between tiles in a tilemap).
static RSTransform _computeBleedTransform(
RSTransform transform,
Rect source,
double bleed,
) {
if (bleed == 0) {
return transform;
}
Comment on lines +325 to +327
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (bleed == 0) {
return transform;
}
if (bleed <= 0) {
return transform;
}


// Scale factors for width and height with bleed
final scaleX = (source.width + bleed * 2) / source.width;
final scaleY = (source.height + bleed * 2) / source.height;

// Apply scale to the rotation/scale components
final scos = transform.scos * scaleX;
final ssin = transform.ssin * scaleY;
Comment on lines +334 to +335
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not scale by x or y but by math.max of either so we can preserve rotation (and ensure scale happens uniformly even if width and height are not equal).

And then the tx and ty should be multiplied by the scos and ssin (subtract for tx, addition for ty). That should keep the centerr fixed to bleed across the borders when we are dealing with rotation


// Adjust translation to keep centered after scaling
// When we scale up, we need to shift back by the bleed amount
// adjusted for the current scale and rotation
final tx = transform.tx - bleed * scaleX;
final ty = transform.ty - bleed * scaleY;

return RSTransform(scos, ssin, tx, ty);
}

/// Ensures that the given [handle] exists and returns its slot.
int _requireSlot(int handle) {
final slot = _handleToSlot[handle];
Expand Down Expand Up @@ -338,7 +376,13 @@ class SpriteBatch {
}

_sources[slot] = _resolveSourceForAtlas(currentBatchItem);
_transforms[slot] = currentBatchItem.transform;

// Apply bleed to the updated transform
_transforms[slot] = _computeBleedTransform(
currentBatchItem.transform,
currentBatchItem.source,
currentBatchItem.bleed,
);

// If color is not explicitly provided, store transparent.
_colors[slot] = color ?? _defaultColor;
Expand All @@ -360,6 +404,11 @@ class SpriteBatch {
/// The [color] parameter allows you to render a color behind the batch item,
/// as a background color.
///
/// The [bleed] parameter expands the destination rectangle outward by this
/// amount in all directions while keeping the source sampling region the
/// same. This helps prevent edge artifacts (seams between tiles in a
/// tilemap). For best results, the atlas should have padding between sprites.
///
/// The [add] method may be a simpler way to add a batch item to the batch.
/// However, if there is a way to factor out the computations of the sine and
/// cosine of the rotation so that they can be reused over multiple calls to
Expand All @@ -370,6 +419,7 @@ class SpriteBatch {
RSTransform? transform,
bool flip = false,
Color? color,
double bleed = 0,
}) {
final handle = _allocateHandle();

Expand All @@ -378,6 +428,7 @@ class SpriteBatch {
transform: transform ??= defaultTransform ?? RSTransform(1, 0, 0, 0),
flip: flip,
color: color ?? defaultColor,
bleed: bleed,
);

if (flip && useAtlas && _flippedAtlasStatus.isNone) {
Expand All @@ -391,7 +442,14 @@ class SpriteBatch {

_batchItems.add(batchItem);
_sources.add(_resolveSourceForAtlas(batchItem));
_transforms.add(batchItem.transform);

// Apply bleed to the transform
final bleedTransform = _computeBleedTransform(
batchItem.transform,
batchItem.source,
batchItem.bleed,
);
_transforms.add(bleedTransform);

// If color is not explicitly provided, store transparent.
_colors.add(color ?? _defaultColor);
Expand All @@ -410,6 +468,11 @@ class SpriteBatch {
/// The [color] parameter allows you to render a color behind the batch item,
/// as a background color.
///
/// The [bleed] parameter expands the destination rectangle outward by this
/// amount in all directions while keeping the source sampling region the
/// same. This helps prevent edge artifacts (seams between tiles in a
/// tilemap). For best results, the atlas should have padding between sprites.
///
/// This method creates a new [RSTransform] based on the given transform
/// arguments. If many [RSTransform] objects are being created and there is a
/// way to factor out the computations of the sine and cosine of the rotation
Expand All @@ -425,6 +488,7 @@ class SpriteBatch {
Vector2? offset,
bool flip = false,
Color? color,
double bleed = 0,
}) {
anchor ??= Vector2.zero();
offset ??= Vector2.zero();
Expand Down Expand Up @@ -452,6 +516,7 @@ class SpriteBatch {
transform: transform,
flip: flip,
color: color,
bleed: bleed,
);
}

Expand Down
Binary file modified packages/flame/test/_goldens/advanced_button_component.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/flame/test/_goldens/align_component_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/flame/test/_goldens/camera_component_order_test.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/flame/test/_goldens/camera_component_test1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/flame/test/_goldens/circular_viewport_test1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/flame/test/_goldens/circular_viewport_test2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/flame/test/_goldens/circular_viewport_test3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/flame/test/_goldens/circular_viewport_test4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/flame/test/_goldens/circular_viewport_test5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/flame/test/_goldens/clip_component_circle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/flame/test/_goldens/clip_component_polygon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/flame/test/_goldens/clip_component_rect.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/flame/test/_goldens/fixed_size_viewport_test_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/flame/test/_goldens/has_decorator_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/flame/test/_goldens/nine_tile_box_test_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/flame/test/_goldens/nine_tile_box_test_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/flame/test/_goldens/paint_decorator_blur.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/flame/test/_goldens/paint_decorator_grayscale.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/flame/test/_goldens/paint_decorator_tinted.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/flame/test/_goldens/paint_decorator_with_blur.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/flame/test/_goldens/rotate3d_decorator_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified packages/flame/test/_goldens/rotate3d_decorator_2.png
Binary file modified packages/flame/test/_goldens/rotate3d_decorator_3.png
Binary file modified packages/flame/test/_goldens/route_decorator_removed.png
Binary file modified packages/flame/test/_goldens/route_opaque.png
Binary file modified packages/flame/test/_goldens/route_transparent.png
Binary file modified packages/flame/test/_goldens/route_with_decorators.png
Binary file modified packages/flame/test/_goldens/shadow3d_decorator_1.png
Binary file modified packages/flame/test/_goldens/shadow3d_decorator_2.png
Binary file modified packages/flame/test/_goldens/shadow3d_decorator_3.png
Binary file modified packages/flame/test/_goldens/snapshot_test_3.png
Binary file modified packages/flame/test/_goldens/sprite_batch_test_1.png
Binary file modified packages/flame/test/_goldens/sprite_batch_test_2.png
Binary file added packages/flame/test/_goldens/sprite_batch_test_3.png
Binary file modified packages/flame/test/_goldens/sprite_font_renderer_1.png
Binary file modified packages/flame/test/_goldens/sprite_font_renderer_2.png
Binary file modified packages/flame/test/_goldens/sprite_font_renderer_3.png
Binary file modified packages/flame/test/_goldens/sprite_font_renderer_4.png
Binary file modified packages/flame/test/_goldens/sprite_test_1.png
Binary file modified packages/flame/test/_goldens/text_box_component_test_1.png
Binary file modified packages/flame/test/_goldens/text_box_component_test_2.png
Binary file modified packages/flame/test/_goldens/text_layouting_1.png
Binary file modified packages/flame/test/_goldens/visibility_test_1.png
119 changes: 119 additions & 0 deletions packages/flame/test/sprite_batch_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,89 @@
);
});

test('can add batch item with bleed', () {
final image = _MockImage();
when(() => image.width).thenReturn(100);
when(() => image.height).thenReturn(100);
final spriteBatch = SpriteBatch(image);
const source = Rect.fromLTWH(0, 0, 10, 10);
const bleed = 2.0;

final index = spriteBatch.add(
source: source,
bleed: bleed,
);
final batchItem = spriteBatch.getBatchItem(index);

expect(batchItem.bleed, bleed);
});

test('bleed scales the transform correctly', () {
final image = _MockImage();
when(() => image.width).thenReturn(100);
when(() => image.height).thenReturn(100);
final spriteBatch = SpriteBatch(image);
const source = Rect.fromLTWH(0, 0, 10, 10);
const bleed = 1.0;
// Expected scale: (10 + 2*1) / 10 = 1.2
const expectedScale = 1.2;

spriteBatch.add(
source: source,
bleed: bleed,
);

// The stored transform should be scaled by the bleed factor
final storedTransform = spriteBatch.transforms.first;
expect(storedTransform.scos, closeTo(expectedScale, 0.001));
expect(storedTransform.ssin, closeTo(0.0, 0.001));
});

test('bleed is preserved when replacing transform', () {
final image = _MockImage();
when(() => image.width).thenReturn(100);
when(() => image.height).thenReturn(100);
final spriteBatch = SpriteBatch(image);
const source = Rect.fromLTWH(0, 0, 10, 10);
const bleed = 2.0;

final index = spriteBatch.add(
source: source,
bleed: bleed,
);

// Replace the transform - bleed should be re-applied
spriteBatch.replace(index, transform: RSTransform(2, 0, 5, 5));

final batchItem = spriteBatch.getBatchItem(index);
expect(batchItem.bleed, bleed);

// The new transform should have bleed applied
// Original: scos=2, with bleed scale 1.4 -> scos=2.8
const expectedScale = (10 + 2 * bleed) / 10; // 1.4
final storedTransform = spriteBatch.transforms.first;
expect(storedTransform.scos, closeTo(2 * expectedScale, 0.001));
});

test('zero bleed does not affect transform', () {
final image = _MockImage();
when(() => image.width).thenReturn(100);
when(() => image.height).thenReturn(100);
final spriteBatch = SpriteBatch(image);
const source = Rect.fromLTWH(0, 0, 10, 10);

spriteBatch.add(
source: source,
bleed: 0,

Check notice on line 142 in packages/flame/test/sprite_batch_test.dart

View workflow job for this annotation

GitHub Actions / analyze

The value of the argument is redundant because it matches the default value.

Try removing the argument. See https://dart.dev/lints/avoid_redundant_argument_values to learn more about this problem.
);

final storedTransform = spriteBatch.transforms.first;
expect(storedTransform.scos, closeTo(1.0, 0.001));
expect(storedTransform.ssin, closeTo(0.0, 0.001));
expect(storedTransform.tx, closeTo(0.0, 0.001));
expect(storedTransform.ty, closeTo(0.0, 0.001));
});

const margin = 2.0;
const tileSize = 6.0;

Expand Down Expand Up @@ -128,5 +211,41 @@
backgroundColor: const Color(0xFFFFFFFF),
goldenFile: '_goldens/sprite_batch_test_2.png',
);

testGolden(
'can render a batch with bleed',
(game, tester) async {
final spriteSheet = await loadImage('alphabet.png');
final spriteBatch = SpriteBatch(spriteSheet);

// Source is a single tile - we want to see if bleed expands the render
const source = Rect.fromLTWH(3 * tileSize, 0, tileSize, tileSize);
const bleed = 1.0;

// Add sprite without bleed (left)
spriteBatch.add(
source: source,
offset: Vector2.all(margin),
scale: 2.0,
);

// Add sprite with bleed (right) - should appear slightly larger
spriteBatch.add(
source: source,
offset: Vector2(2 * margin + tileSize * 2 + 4, margin),
scale: 2.0,
bleed: bleed,
);

game.add(
SpriteBatchComponent(
spriteBatch: spriteBatch,
),
);
},
size: Vector2(4 * margin + 4 * tileSize + 4, 3 * margin + 2 * tileSize),
backgroundColor: const Color(0xFFFFFFFF),
goldenFile: '_goldens/sprite_batch_test_3.png',
);
});
}
Binary file modified packages/flame_svg/test/_goldens/render_sharply.png
Binary file modified packages/flame_test/example/test/goldens/game.png
Binary file modified packages/flame_test/test/golden_debug_text.png
Binary file modified packages/flame_test/test/golden_test.png
Binary file modified packages/flame_test/test/golden_test_small.png
Binary file modified packages/flame_tiled/test/goldens/flat_hex_even.png
Binary file modified packages/flame_tiled/test/goldens/flat_hex_odd.png
Binary file modified packages/flame_tiled/test/goldens/image_layer_covers_map.png
Binary file modified packages/flame_tiled/test/goldens/isometric.png
Binary file modified packages/flame_tiled/test/goldens/larger_atlas.png
Binary file modified packages/flame_tiled/test/goldens/larger_atlas_component.png
Binary file modified packages/flame_tiled/test/goldens/larger_atlas_with_spacing.png
Binary file modified packages/flame_tiled/test/goldens/orthogonal.png
Binary file modified packages/flame_tiled/test/goldens/oversized_tiles_hexagonal.png
Binary file modified packages/flame_tiled/test/goldens/oversized_tiles_isometric.png
Binary file modified packages/flame_tiled/test/goldens/oversized_tiles_orthogonal.png
Binary file modified packages/flame_tiled/test/goldens/oversized_tiles_staggered.png
Binary file modified packages/flame_tiled/test/goldens/pointy_hex_even.png
Binary file modified packages/flame_tiled/test/goldens/pointy_hex_odd.png
Binary file modified packages/flame_tiled/test/goldens/rendered_with_flip.png
Binary file modified packages/flame_tiled/test/goldens/rendered_with_flip_ignored.png
Binary file modified packages/flame_tiled/test/goldens/shifted_scaled_larger.png
Binary file modified packages/flame_tiled/test/goldens/shifted_scaled_regular.png
Binary file modified packages/flame_tiled/test/goldens/shifted_scaled_smaller.png
Binary file modified packages/flame_tiled/test/goldens/single_atlas.png
Binary file modified packages/flame_tiled/test/goldens/single_tile_atlas.png
Binary file modified packages/flame_tiled/test/goldens/single_tile_map_1.png
Binary file modified packages/flame_tiled/test/goldens/single_tile_map_2.png
Binary file modified packages/flame_tiled/test/goldens/test_tile_offset_hexagonal.png
Binary file modified packages/flame_tiled/test/goldens/test_tile_offset_isometric.png
Binary file modified packages/flame_tiled/test/goldens/test_tile_offset_staggered.png
Binary file modified packages/flame_tiled/test/goldens/texture_with_flip.png
Binary file modified packages/flame_tiled/test/goldens/texture_with_flip_ignored.png
Binary file modified packages/flame_tiled/test/goldens/tile_stack_all_move.png
Binary file modified packages/flame_tiled/test/goldens/tile_stack_single_move.png
Loading