Three.js Geometry: Complete Guide to 3D Shapes
Geometry is the skeleton of every 3D object in Three.js. It defines the shape through vertices, edges, and faces — all packed into typed arrays that go straight to the GPU. Three.js ships 21 built-in geometry constructors, from simple boxes to parametric tubes and extruded shapes.
What is BufferGeometry?
Every geometry in Three.js is a BufferGeometry. The old Geometry class was removed from core in r125 — there is no alternative, and you don't need one.
A BufferGeometry stores vertex data in typed arrays (Float32Array) grouped into attributes:
- position — xyz coordinates for each vertex
- normal — the direction each vertex faces (used for lighting)
- uv — texture mapping coordinates
- index — which vertices form each triangle (optional but saves memory)
These arrays map directly to GPU buffers, which is why BufferGeometry is fast — there's no conversion step between your JavaScript data and what the GPU actually reads.
// Access raw vertex data
const geo = new THREE.BoxGeometry(1, 1, 1);
const positions = geo.attributes.position.array;
console.log(positions.length); // 72 floats = 24 vertices * 3 (xyz)
// Modify a vertex position
positions[0] += 0.5;
geo.attributes.position.needsUpdate = true;
Primitive Geometries
Primitives are the building blocks. Three.js generates them procedurally from parameters — no external model files needed.
BoxGeometry(width, height, depth, wSegments, hSegments, dSegments) — the most common starting point. A single-segment box has 12 triangles (two per face, six faces). Increase segments when you need smooth deformation (e.g., vertex displacement shaders).
SphereGeometry(radius, widthSegments, heightSegments) — default is 32x16 segments (960 triangles). That's fine for a hero object. Background spheres can drop to 16x8. Doubling segments roughly quadruples the triangle count.
CylinderGeometry(radiusTop, radiusBottom, height, radialSegments) — set radiusTop to 0 for a cone. Set both radii equal for a cylinder. openEnded: true removes the caps.
PlaneGeometry(width, height, wSegments, hSegments) — a flat rectangle. Essential for floors, walls, water surfaces, and terrain when combined with displacement maps or vertex shaders.
TorusGeometry(radius, tube, radialSegments, tubularSegments) — a donut. TorusKnotGeometry wraps the tube into a knot defined by p/q winding numbers — great for abstract visuals.
// Box — 2x2x2, single segment
const box = new THREE.BoxGeometry(2, 2, 2);
// Sphere — radius 1, smooth
const sphere = new THREE.SphereGeometry(1, 32, 16);
// Cone — cylinder with zero top radius
const cone = new THREE.CylinderGeometry(0, 1, 2, 24);
// Ground plane — 50x50 units
const ground = new THREE.PlaneGeometry(50, 50);
ground.rotateX(-Math.PI / 2); // lay flat
Path-Based Geometries
These geometries generate shapes from 2D paths or 3D curves, which makes them far more flexible than primitives.
ExtrudeGeometry(shape, options) — takes a THREE.Shape (a 2D path) and pushes it into 3D. You control depth, bevelSize, bevelThickness, and bevelSegments. This is how you make text, logos, floor plans, and any custom cross-section.
LatheGeometry(points, segments) — revolves a set of 2D points around the Y axis. Feed it the profile of a vase, bottle, or chess piece and it generates the full 3D shape. The points array is just Vector2[].
TubeGeometry(curve, segments, radius) — wraps a tube along any 3D curve (CatmullRomCurve3, QuadraticBezierCurve3, etc.). Used for pipes, roads, roller coasters, and flow visualizations.
ShapeGeometry(shape) — renders a 2D shape as a flat mesh (no depth). Useful for UI elements, outlines, and 2D overlays in a 3D scene.
// Extrude a star shape into 3D
const shape = new THREE.Shape();
const outerR = 1, innerR = 0.4, points = 5;
for (let i = 0; i < points * 2; i++) {
const r = i % 2 === 0 ? outerR : innerR;
const angle = (i / (points * 2)) * Math.PI * 2 - Math.PI / 2;
const method = i === 0 ? 'moveTo' : 'lineTo';
shape[method](Math.cos(angle) * r, Math.sin(angle) * r);
}
const geo = new THREE.ExtrudeGeometry(shape, {
depth: 0.3,
bevelEnabled: true,
bevelThickness: 0.05,
bevelSize: 0.05,
bevelSegments: 3
});
Segments: The Performance Lever
Every parametric geometry takes segment parameters. Segments control how many triangles make up the surface — more segments = smoother shape = more GPU work.
The relationship is not linear. For a sphere:
- 16x8 segments = 224 triangles
- 32x16 segments = 960 triangles
- 64x32 segments = 3,968 triangles
- 128x64 segments = 16,128 triangles
Each doubling roughly 4x the triangle count. On mobile GPUs, this matters. A scene with 50 high-segment spheres can drop below 30fps on mid-range phones.
Rule of thumb: use the minimum segments that look acceptable at the camera distance your object will be viewed from. A background sphere at 16x8 is indistinguishable from 64x32 when it's 50 units away.
Building Custom Geometry from Scratch
When built-in constructors aren't enough, you can create geometry manually by setting vertex attributes directly.
The process is: create a BufferGeometry, define your vertex positions in a Float32Array, wrap it in a BufferAttribute, and attach it to the geometry. You can do the same for normals, UVs, and colors.
For indexed geometry (where multiple triangles share vertices), use geometry.setIndex() with a Uint16Array or Uint32Array. This reduces memory usage and can improve performance because the GPU caches transformed vertices.
After modifying vertex data at runtime, set attribute.needsUpdate = true to push changes to the GPU. If the bounding box/sphere is used for culling or raycasting, also call geometry.computeBoundingBox() and geometry.computeBoundingSphere().
// Build a triangle from scratch
const geo = new THREE.BufferGeometry();
const vertices = new Float32Array([
-1, -1, 0, // vertex 0
1, -1, 0, // vertex 1
0, 1, 0, // vertex 2
]);
geo.setAttribute('position',
new THREE.BufferAttribute(vertices, 3)
);
geo.computeVertexNormals(); // auto-calculate normals
const mesh = new THREE.Mesh(
geo,
new THREE.MeshStandardMaterial({ color: 0x00d4ff })
);
Merging and Reusing Geometry
Reuse geometry across meshes. If you have 100 crates that are all the same size, create one BoxGeometry and pass it to 100 Mesh instances. Geometry is immutable GPU data — duplicating it wastes VRAM for no visual difference.
Merge static geometry with BufferGeometryUtils.mergeGeometries() when you have many objects that share a material and never move independently. Merging turns N draw calls into 1 — a major win for scenes with thousands of static objects like terrain chunks, rocks, or foliage.
Merging has a trade-off: you lose per-object control (no individual transforms, raycasting, or material swaps). Use InstancedMesh instead when objects share geometry and material but need individual transforms.
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
// Merge 100 boxes into one geometry
const geos = [];
const template = new THREE.BoxGeometry(1, 1, 1);
for (let i = 0; i < 100; i++) {
const clone = template.clone();
clone.translate(i * 2, 0, 0);
geos.push(clone);
}
const merged = mergeGeometries(geos);
const mesh = new THREE.Mesh(merged, material); // 1 draw call
Disposing Geometry
Three.js does not garbage-collect GPU resources. When you remove a mesh from the scene, the geometry's GPU buffers stay in VRAM until you explicitly call geometry.dispose().
This matters in apps that create and destroy geometry frequently (level loading, procedural generation, user-uploaded models). Without disposal, VRAM usage grows until the browser tab crashes or the GPU driver forces a context loss.
The pattern is simple: when removing a mesh, dispose both geometry and material, then remove from the scene.
// Clean removal of a mesh
function removeMesh(mesh, scene) {
scene.remove(mesh);
mesh.geometry.dispose();
mesh.material.dispose();
}
Quick Reference: All Three.js Built-in Geometry Types
| Geometry | Key Parameters | Typical Use |
|---|---|---|
| BoxGeometry | width, height, depth, wSeg, hSeg, dSeg |
Crates, buildings, UI panels |
| SphereGeometry | radius, widthSeg, heightSeg |
Planets, balls, skydomes |
| CylinderGeometry | radiusTop, radiusBottom, height, radialSeg |
Pillars, cans, tree trunks |
| ConeGeometry | radius, height, radialSeg |
Arrows, rooftops, funnels |
| PlaneGeometry | width, height, wSeg, hSeg |
Floors, walls, terrain, water |
| CircleGeometry | radius, segments |
Discs, spotlights, UI elements |
| RingGeometry | innerRadius, outerRadius, thetaSegments |
Health bars, halos, indicators |
| TorusGeometry | radius, tube, radialSeg, tubularSeg |
Donuts, rings, orbital paths |
| TorusKnotGeometry | radius, tube, tubularSeg, radialSeg, p, q |
Abstract art, knot visualizations |
| DodecahedronGeometry | radius, detail |
Low-poly planets, gems |
| IcosahedronGeometry | radius, detail |
Geodesic spheres, low-poly balls |
| OctahedronGeometry | radius, detail |
Diamonds, crystals |
| TetrahedronGeometry | radius, detail |
Simple pyramids, particles |
| CapsuleGeometry | radius, length, capSeg, radialSeg |
Character colliders, pills |
| LatheGeometry | points[], segments |
Vases, bottles, chess pieces |
| ExtrudeGeometry | shape, options{} |
Text, logos, floor plans |
| ShapeGeometry | shape |
Flat 2D shapes, UI overlays |
| TubeGeometry | curve, segments, radius |
Pipes, roads, cables |
| EdgesGeometry | source geometry, thresholdAngle |
Wireframe outlines |
| WireframeGeometry | source geometry |
Full wireframe overlays |
| PolyhedronGeometry | vertices[], indices[], radius, detail |
Custom polyhedra |
Tips & Best Practices
Use geometry.center() after extrusion
ExtrudeGeometry and LatheGeometry often produce geometry whose origin is at a corner, not the center. Call geometry.center() to shift vertices so the bounding box is centered at (0,0,0) — makes positioning and rotation predictable.
Profile triangle count with renderer.info
renderer.info.render.triangles gives you the exact triangle count per frame. Log it after rendering to spot geometry that's heavier than expected. Aim for under 500k triangles total if you need 60fps on mobile.
Use toNonIndexed() for flat shading
Indexed geometry shares vertices between faces, which forces smooth normals. If you want hard edges (flat shading), call geometry.toNonIndexed() first — this duplicates shared vertices so each face gets its own normals.
computeVertexNormals() is your friend
When building custom geometry, call geometry.computeVertexNormals() after setting positions. Without normals, lit materials will render black. This auto-calculates smooth normals from face orientation.
Use EdgesGeometry for clean outlines
new THREE.EdgesGeometry(geo, 15) extracts only edges where the face angle exceeds the threshold (15 degrees). Pair it with LineSegments and a LineBasicMaterial for sharp outlines without rendering every internal wireframe edge.
Try Geometry Interactively
Explore geometry with live 3D demos — adjust parameters in real-time and see the results instantly.
Open Interactive DemoFrequently Asked Questions
What is the difference between Geometry and BufferGeometry in Three.js?
There is no difference anymore. The old Geometry class was removed in Three.js r125 (2021). BufferGeometry is the only geometry type. It stores vertex data in typed arrays (Float32Array) that map directly to GPU buffers, making it significantly faster than the old class which used JavaScript objects for each vertex.
How many segments should I use for a SphereGeometry?
32 width segments and 16 height segments is a solid default for most use cases (960 triangles). For a hero object that fills the screen, go to 64x32. For background objects or particles, 16x8 or even 8x4 is sufficient. Each doubling roughly quadruples the triangle count, so it matters on mobile.
How do I create a custom shape in Three.js?
For 2D-to-3D shapes, use THREE.Shape to define a 2D path (with moveTo, lineTo, bezierCurveTo), then pass it to ExtrudeGeometry for depth or ShapeGeometry for a flat mesh. For fully custom 3D shapes, create a BufferGeometry and set the vertex positions, normals, and UVs manually using Float32Array and BufferAttribute.
Does Three.js garbage-collect geometry automatically?
No. Three.js does not free GPU resources automatically. When you remove a mesh from the scene, its geometry and material stay in VRAM. You must call geometry.dispose() and material.dispose() explicitly. Forgetting this in apps that create and destroy objects frequently causes VRAM leaks that eventually crash the browser tab.
What is the fastest way to render thousands of identical shapes?
Use InstancedMesh. It draws thousands of copies of the same geometry + material in a single GPU draw call. Each instance can have its own position, rotation, scale, and color. For static objects that never move independently, mergeGeometries() from BufferGeometryUtils is even faster because there's no per-instance overhead.