flutter3d
Showcase Changelog 38 packages API reference

Procedural shapes

since 0.1.0 Scene and geometry

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');
}