flutter3d
Showcase Changelog 38 packages API reference

Sprite-sheet animation

since 0.2.0 or earlier Physics and particles

A single sprite makes a particle a shape. A Flipbook makes it a small animation instead: a grid of frames on one texture, one of them showing at a time depending on how far through its life a particle is.

Step 1: An atlas of frames #

Four cells in a row stand in for four drawn frames of a spark burning out, from bright to dark. A real atlas would be four pictures; the shape a Flipbook reads is the same either way.

/// Four cells in a row, each a solid colour: a stand-in for four drawn
/// frames of a burning-out spark, from bright to dark.
ByteData _atlas() {
  const List<List<int>> colours = <List<int>>[
    <int>[255, 255, 220],
    <int>[255, 190, 90],
    <int>[220, 90, 40],
    <int>[60, 20, 10],
  ];
  final Uint8List bytes = Uint8List(_cellSize * _frames * _cellSize * 4);
  final int width = _cellSize * _frames;
  for (var y = 0; y < _cellSize; y++) {
    for (var frame = 0; frame < _frames; frame++) {
      final List<int> colour = colours[frame];
      for (var x = 0; x < _cellSize; x++) {
        final int at = (y * width + frame * _cellSize + x) * 4;
        bytes[at] = colour[0];
        bytes[at + 1] = colour[1];
        bytes[at + 2] = colour[2];
        bytes[at + 3] = 255;
      }
    }
  }
  return bytes.buffer.asByteData();
}

Step 2: Describe the grid #

Flipbook only needs to know the grid's shape. frames would trim it to fewer cells than columns * rows if the last row were not full; here every cell is used.

_flipbook = Flipbook(columns: _frames, rows: 1);

Step 3: What a cell is chosen by #

A flipbook is driven by how far through its life a particle is, not by a clock: a long-lived particle and a short one both play the whole sequence exactly once, which a frames-per-second animation could not promise either of them.

// A particle just born and one about to die should be showing different
// cells of the sheet: the whole point of a flipbook over a static sprite.
final Particle newborn = Particle()
  ..age = 0.0
  ..lifetime = 1.0;
final Particle dying = Particle()
  ..age = 0.99
  ..lifetime = 1.0;
if (_flipbook.cellFor(newborn).left == _flipbook.cellFor(dying).left) {
  throw StateError('the flipbook should play across a particle\'s life');
}

Note. This is entirely vertex data. Billboarding already happens on the CPU in this engine, so a cell is a rectangle the corner coordinates are scaled into before they are written; no shader and no backend had to change for a flipbook to exist.