flutter3d
Showcase Changelog 38 packages API reference

Textured billboards

since 0.2.0 Physics and particles

An untextured particle draws as a procedural disc: cheap, and the same shape every time. Handing ParticleContributor a texture swaps in a sprite, sampled per particle, and the sprite can be anything a texture can hold.

Step 1: Paint one sprite #

This one is drawn on the CPU, a spark shaped like a four-pointed star rather than a circle: bright along its two axes, dark at the corners.

/// A four-pointed spark: bright along the axes, dark at the corners. Not a
/// shape the procedural disc every untextured particle uses can make.
ByteData _sparkSprite() {
  final Uint8List bytes = Uint8List(_size * _size * 4);
  for (var y = 0; y < _size; y++) {
    for (var x = 0; x < _size; x++) {
      final double dx = (x + 0.5) / _size * 2.0 - 1.0;
      final double dy = (y + 0.5) / _size * 2.0 - 1.0;
      final double along = math.max(dx.abs(), dy.abs());
      final double across = math.min(dx.abs(), dy.abs());
      final double alpha = (1.0 - along) * (1.0 - across * 3.0);
      final int at = (y * _size + x) * 4;
      bytes[at] = 255;
      bytes[at + 1] = 220;
      bytes[at + 2] = 160;
      bytes[at + 3] = (alpha.clamp(0.0, 1.0) * 255).round();
    }
  }
  return bytes.buffer.asByteData();
}

Step 2: Put it on the device #

final TextureHandle sprite = context.device.createTextureFromPixels(
  width: _size,
  height: _size,
  format: TextureFormat.r8g8b8a8UNormInt,
  pixels: _sparkSprite(),
)!;

Step 3: Hand it to the contributor #

null picks the procedural disc; anything else picks the textured stage. The two are different fragment shaders, not the same one with a white default, because binding a texture to a slot a compiled shader has no room for is expensive to get wrong.

_contributor = context.renderer.addContributor(
  ParticleContributor(_particles, texture: sprite),
);

Step 4: What changed #

The page's own check is only that a texture is actually there: the shape a sprite draws is a matter for your eyes, not for a test running headless.

if (_contributor.texture == null) {
  throw StateError('the contributor should be drawing a sprite');
}

Warning. Build a texture with a mip chain if you can, and check GraphicsDevice.supportsMipmaps first. A particle is a quad that shrinks as it recedes, and one without a chain sparkles on the way out.