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.
// 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.
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!
// 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.
// 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.
// 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.
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.