flutter3d
Showcase Changelog 38 packages API reference

Procedural textures

since 0.5.0 Scene and geometry

A small texture can be generated with no asset file or image decoder. SolidColorTexture and CheckerboardTexture produce ordinary RGBA8 pixels, then use the same upload path as any other CPU-side image.

Step 1: Encode pixels on the CPU #

encode returns four bytes per pixel in row-major order. A solid 1 by 1 image needs four bytes; the 64 by 64 checkerboard needs 16,384. Checking those sizes at the boundary catches a malformed custom texture before upload.

final ByteData solidPixels = _solid.encode();
final ByteData checkerPixels = _checker.encode();
if (solidPixels.lengthInBytes != 4 ||
    checkerPixels.lengthInBytes != 64 * 64 * 4) {
  throw StateError('a procedural texture returned the wrong byte count');
}

Step 2: Upload through the active device #

upload creates a device texture from the encoded bytes. The resulting handle goes into Material.albedo. The checkerboard uses a repeating sampler, while a single solid texel looks the same at every UV coordinate.

_solidMaterial = Material(
  name: 'solid blue',
  baseColor: Vector4(1.0, 1.0, 1.0, 1.0),
  albedo: _solid.upload(context.device),
  roughness: 0.42,
);
_checkerMaterial = Material(
  name: 'checkerboard',
  baseColor: Vector4(1.0, 1.0, 1.0, 1.0),
  albedo: _checker.upload(context.device),
  albedoSampler: SamplerOptions.linearRepeat,
  roughness: 0.58,
);

Step 3: Use them on ordinary meshes #

The blue texel covers a sphere. The checkerboard follows the UVs generated for a torus, making its seam and surface direction visible without loading an external image.

final Scene scene = Scene()
  ..ambientColor = Vector3(0.45, 0.54, 0.72)
  ..ambientIntensity = 0.14
  ..add(
    MeshNode(
      DeviceMesh.upload(
        context.device,
        const SphereShape(radius: 1.05, segments: 36, rings: 18).build(),
      ),
      _solidMaterial,
      name: 'solid sphere',
    )..setPosition(-1.65, 1.05, 0.0),
  )
  ..add(
    MeshNode(
      DeviceMesh.upload(
        context.device,
        const TorusShape(
          radius: 0.82,
          tubeRadius: 0.34,
          segments: 48,
          tubeSegments: 24,
        ).build(),
      ),
      _checkerMaterial,
      name: 'checker torus',
    )..setPosition(1.65, 1.05, 0.0),
  )
  ..add(
    LightNode(name: 'key', intensity: 3.3)
      ..setLocalForward(Vector3(-0.45, -0.82, -0.35)),
  );

Step 4: Check pixels and uploads #

The page confirms that both material texture handles exist and that adjacent checker cells have different RGB values. It then checks that both meshes were drawn.

final Uint8List checker = _checker.encode().buffer.asUint8List();
final bool twoColours =
    checker[0] != checker[_checker.cell * 4] ||
    checker[1] != checker[_checker.cell * 4 + 1] ||
    checker[2] != checker[_checker.cell * 4 + 2];
if (_solidMaterial.albedo == null ||
    _checkerMaterial.albedo == null ||
    !twoColours ||
    frame.drawCalls < 2) {
  throw StateError('the procedural textures were not drawn');
}