Procedural textures
Open the live demo · Read the source · View on GitHub
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');
}