flutter3d
Showcase Changelog 38 packages API reference

Surfaces of revolution

since 0.1.0 Scene and geometry

A lathe mesh starts with a line in the radius-height plane. Sweeping that line around the Y axis makes a vessel, column, wheel, or any other rotationally symmetric surface.

Step 1: Draw the side profile #

Each Vector2 stores radius in x and height in y. The profile runs from bottom to top, which gives the finished surface outward-facing normals. Points on the axis close the base without a separate mesh.

final List<Vector2> profile = <Vector2>[
  Vector2(0.0, -1.25),
  Vector2(0.72, -1.25),
  Vector2(0.78, -1.1),
  Vector2(0.82, -0.55),
  Vector2(0.62, 0.05),
  Vector2(0.46, 0.7),
  Vector2(0.46, 1.05),
  Vector2(0.62, 1.18),
  Vector2(0.62, 1.25),
  Vector2(0.5, 1.25),
];

Step 2: Sweep it around the axis #

segments controls how many angular slices are built. The first vessel keeps the slices visible, the second uses enough for a smooth outline, and the third stops after three quarters of a turn so the profile can be seen from inside.

final List<(String, double, int)> variants = <(String, double, int)>[
  ('faceted', math.pi * 2.0, 12),
  ('smooth', math.pi * 2.0, 48),
  ('open sweep', math.pi * 1.5, 36),
];
final Scene scene = Scene()
  ..ambientColor = Vector3(0.48, 0.56, 0.72)
  ..ambientIntensity = 0.14;
for (var i = 0; i < variants.length; i++) {
  final (String name, double sweep, int segments) = variants[i];
  final MeshData data = LatheShape(
    profile: profile,
    segments: segments,
    sweepAngle: sweep,
    name: name,
  ).build();
  final Material clay = Material(
    name: '$name clay',
    baseColor: Vector4(0.78, 0.35 + i * 0.12, 0.2 + i * 0.16, 1.0),
    roughness: 0.48,
    doubleSided: sweep < math.pi * 2.0,
  );
  final MeshNode node = MeshNode(
    DeviceMesh.upload(context.device, data),
    clay,
    name: name,
  )..setPosition((i - 1) * 2.6, 1.25, 0.0);
  _vessels.add(node);
  scene.add(node);
}

Step 3: Light the profile #

A directional light shows the broad change in normals around each vessel. A point light behind them catches the rim and makes the open sweep easier to read.

scene
  ..add(
    LightNode(name: 'key', intensity: 3.2)
      ..setLocalForward(Vector3(-0.45, -0.82, -0.34)),
  )
  ..add(
    LightNode(
      type: LightType.point,
      intensity: 8.0,
      range: 10.0,
      name: 'rim',
    )..setPosition(2.5, 3.8, -2.5),
  );

Step 4: Check every variant #

The page records the three nodes, checks their names, and confirms that each variant contributed a draw to the frame.

const Set<String> expected = <String>{'faceted', 'smooth', 'open sweep'};
final Set<String?> actual = <String?>{
  for (final MeshNode node in _vessels) node.name,
};
if (_vessels.length != 3 ||
    !actual.containsAll(expected) ||
    frame.drawCalls < 3) {
  throw StateError('the three lathe variants were not drawn');
}