Custom Geometries

Shapes, extrusions, lathes, and tubes

Beyond Primitives

The standard primitives cover basic shapes, but real projects often need custom profiles, extruded text, curved tubes, or shapes defined by 2D outlines. Three.js provides several higher-level geometry generators that build on BufferGeometry to handle these cases.

ShapeGeometry

A Shape defines a 2D outline using path commands similar to the HTML Canvas or SVG. ShapeGeometry then triangulates that outline into a flat mesh. This is the starting point for many custom shapes, and is the input for ExtrudeGeometry.

typescript
// Create a 2D shape using path commands
const heartShape = new THREE.Shape();
heartShape.moveTo(0, 0.5);
heartShape.bezierCurveTo(0, 0.8, 0.4, 1.2, 0, 1.6);
heartShape.bezierCurveTo(-0.4, 1.2, -0.8, 0.8, 0, 0);

// Turn it into flat geometry
const geometry = new THREE.ShapeGeometry(heartShape);

// Shapes support holes
const outerShape = new THREE.Shape();
outerShape.moveTo(-1, -1);
outerShape.lineTo(1, -1);
outerShape.lineTo(1, 1);
outerShape.lineTo(-1, 1);

const hole = new THREE.Path();
hole.absarc(0, 0, 0.5, 0, Math.PI * 2, false);
outerShape.holes.push(hole);

ExtrudeGeometry

Extrusion takes a 2D Shape and pushes it into 3D along the Z axis. You control the depth, bevel size, and bevel segments. This is how Three.js generates 3D text and logos.

typescript
const shape = new THREE.Shape();
shape.moveTo(-1, -0.5);
shape.lineTo(1, -0.5);
shape.lineTo(1, 0.5);
shape.lineTo(-1, 0.5);

const extrudeSettings = {
  depth: 0.5,           // how far to extrude
  bevelEnabled: true,
  bevelThickness: 0.1,  // how deep the bevel goes
  bevelSize: 0.1,       // how far the bevel extends
  bevelSegments: 3      // smoothness of the bevel curve
};

const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);

// You can also extrude along a custom path
const path = new THREE.CatmullRomCurve3([
  new THREE.Vector3(0, 0, 0),
  new THREE.Vector3(0, 1, 1),
  new THREE.Vector3(0, 2, 0)
]);
const alongPath = new THREE.ExtrudeGeometry(shape, {
  extrudePath: path,
  steps: 50,
  bevelEnabled: false
});

LatheGeometry

A lathe spins a 2D profile around the Y axis to create a revolved surface that is perfect for vases, bottles, chess pieces, or anything with radial symmetry. You define the profile as an array of Vector2 points and spin, spin, spin!

typescript
// Define the profile curve as Vector2 points (x = radius, y = height)
const points = [
  new THREE.Vector2(0, 0),
  new THREE.Vector2(0.6, 0),
  new THREE.Vector2(1.0, 0.3),
  new THREE.Vector2(0.7, 1.0),
  new THREE.Vector2(0.5, 1.5),
  new THREE.Vector2(0.6, 2.0),
  new THREE.Vector2(0.4, 2.3),
  new THREE.Vector2(0, 2.3)
];

// LatheGeometry(points, segments, phiStart, phiLength)
const geometry = new THREE.LatheGeometry(points, 32);

// Partial lathe (e.g. a half-turn to see the inside)
const half = new THREE.LatheGeometry(points, 32, 0, Math.PI);

TubeGeometry

Tubes generate a cylindrical mesh that follows a 3D curve. You can provide any Curve object and Three.js will build the geometry along it. Great for pipes, wires, roller coasters, or any path-based shape.

typescript
// Define a 3D curve
const curve = new THREE.CatmullRomCurve3([
  new THREE.Vector3(-2, 0, 0),
  new THREE.Vector3(-1, 1, 1),
  new THREE.Vector3(0, 0, 0),
  new THREE.Vector3(1, -1, 1),
  new THREE.Vector3(2, 0, 0)
]);

// TubeGeometry(path, tubularSegments, radius, radialSegments, closed)
const geometry = new THREE.TubeGeometry(curve, 64, 0.2, 16, false);

// Closed loop
const loop = new THREE.CatmullRomCurve3([ /* ... */ ], true);
const closedTube = new THREE.TubeGeometry(loop, 64, 0.15, 16, true);

Modifying Existing Geometry

Sometimes the fastest path to a custom shape is starting from a primitive and displacing its vertices. Since every geometry is a BufferGeometry, you can access and modify the position attribute directly.

typescript
// Start with a sphere
const geometry = new THREE.SphereGeometry(1, 64, 32);
const positions = geometry.attributes.position;

// Displace each vertex by noise to create an asteroid
for (let i = 0; i < positions.count; i++) {
  const x = positions.getX(i);
  const y = positions.getY(i);
  const z = positions.getZ(i);

  // Simple displacement based on vertex position
  const noise = Math.sin(x * 3) * Math.cos(y * 3) * Math.sin(z * 3) * 0.2;
  const length = Math.sqrt(x * x + y * y + z * z);
  const scale = 1 + noise;

  positions.setXYZ(i, x * scale, y * scale, z * scale);
}

// Always recompute normals after modifying positions
geometry.computeVertexNormals();

Merging Geometries

When you have many static meshes using the same material, you can merge them into a single geometry to reduce draw calls. Three.js provides mergeGeometries from the BufferGeometryUtils addon for this.

typescript
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';

const geometries = [];

// Create many small box geometries at different positions
for (let i = 0; i < 100; i++) {
  const box = new THREE.BoxGeometry(0.2, 0.2, 0.2);

  // Apply a transform before merging
  box.translate(
    (Math.random() - 0.5) * 4,
    (Math.random() - 0.5) * 4,
    (Math.random() - 0.5) * 4
  );

  geometries.push(box);
}

// Merge into a single geometry (one draw call instead of 100)
const merged = mergeGeometries(geometries);
const mesh = new THREE.Mesh(merged, material);

Merging is a one-time operation so keep in mind that you can't move individual pieces afterward. For objects that need independent transforms, use InstancedMesh instead.

←   Standard PrimitivesInstanced Mesh   →