Procedural shapes
Open the live demo · Read the source · View on GitHub
Small primitives do not need an asset pipeline. A shape value produces
MeshData on the CPU, which can be uploaded and used by an ordinary
MeshNode. This page builds five shapes from code.
Step 1: Describe the geometry #
Each shape keeps the numbers that define it. Segments and rings control how closely a curved mesh follows the ideal surface. A cuboid needs only its size.
final List<(String, Shape)> descriptions = <(String, Shape)>[
('cuboid', CuboidShape(size: Vector3(1.4, 1.4, 1.4))),
('sphere', const SphereShape(radius: 0.8, segments: 32, rings: 16)),
(
'torus',
const TorusShape(
radius: 0.58,
tubeRadius: 0.24,
segments: 40,
tubeSegments: 18,
),
),
(
'capsule',
const CapsuleShape(radius: 0.45, height: 0.8, segments: 32, rings: 8),
),
('disc', const DiscShape(radius: 0.9, innerRadius: 0.28, segments: 40)),
];
Step 2: Build and upload each mesh #
Calling build returns CPU-side vertices and indices. DeviceMesh.upload
moves that data to the active graphics device. From there the mesh uses the
same material and scene path as a model loaded from a file.
final Scene scene = Scene()
..ambientColor = Vector3(0.55, 0.62, 0.78)
..ambientIntensity = 0.16;
for (var i = 0; i < descriptions.length; i++) {
final (String name, Shape shape) = descriptions[i];
final MeshData data = shape.build();
final Material material = Material(
name: '$name material',
baseColor: Vector4(
0.28 + i * 0.12,
0.68 - i * 0.07,
0.82 - i * 0.1,
1.0,
),
roughness: 0.42,
metallic: i == 2 ? 0.55 : 0.05,
doubleSided: name == 'disc',
);
final MeshNode node = MeshNode(
DeviceMesh.upload(context.device, data),
material,
name: name,
)..setPosition((i - 2) * 2.0, name == 'disc' ? 0.25 : 0.85, 0.0);
_shapes.add(node);
scene.add(node);
}
Step 3: Light the surfaces #
A directional key light makes normals and curved silhouettes readable. The point light fills the darker side without flattening every shape into one colour.
scene
..add(
LightNode(name: 'key', intensity: 3.4)
..setLocalForward(Vector3(-0.45, -0.8, -0.38)),
)
..add(
LightNode(
type: LightType.point,
intensity: 10.0,
range: 12.0,
name: 'fill',
)..setPosition(-3.0, 3.5, 3.0),
);
Step 4: Check the result #
The page keeps the five nodes it created, checks their names, and confirms that at least one draw reached the frame for each shape.
final Set<String?> names = <String?>{
for (final MeshNode node in _shapes) node.name,
};
const Set<String> expected = <String>{
'cuboid',
'sphere',
'torus',
'capsule',
'disc',
};
if (_shapes.length != expected.length ||
!names.containsAll(expected) ||
frame.drawCalls < expected.length) {
throw StateError('not every procedural shape was drawn');
}