Navigate
Search topics across all sections
Lines & Curves
Meshes fill surfaces. Points render dots. Lines connect points into paths. And curves smooth those paths into elegant arcs. Together they let you draw trails, orbits, wires, roller coasters, or any continuous path through 3D space.
You render a line with LineBasicMaterial and set lineWidth to 5 so it is clearly visible. On your dev machine it looks fine — but on your friend's computer, the line is paper thin. The lineWidth setting is completely ignored. No errors, no warnings.
lineWidth has no visible effect. Line is always 1 pixel wide on most hardware.
Real-world
Think of lines and curves as drawing in 3D with a magic pen that works in three dimensions.
A line is the simplest stroke — connect point A to point B in a straight segment. String together multiple points and you get a polyline, like connecting dots on a map.
But straight segments look robotic. A curve is like switching your pen to calligraphy mode. You place a few guide points and the math smooths the path between them into a flowing arc. That is what CatmullRomCurve3 does.
And if you want your line to have volume — like a garden hose instead of a wire — you wrap a TubeGeometry around it. Same path, but now with thickness and material.
From Points to Thick Curves
Each step adds more visual richness to the same path.
Control Points
A few 3D positions that define the shape
Curve
CatmullRomCurve3 smooths between points
Line
Render as a thin visible path
Tube
TubeGeometry gives it real thickness
Building a Curve Path Step by Step
Let us create a smooth curve and animate something along it.
Step 1 -- Define control points
import * as THREE from "three";
const points = [
new THREE.Vector3(-3, 0, 0),
new THREE.Vector3(-1, 2, 1),
new THREE.Vector3( 1, -1, -1),
new THREE.Vector3( 3, 0, 0),
];These are the guide posts. The curve will pass through each one in order. You only need a handful — the math fills in the thousands of in-between points.
Step 2 -- Create a smooth curve
const curve = useMemo(() => {
return new THREE.CatmullRomCurve3(points);
}, []);
// Get 100 evenly spaced points along the curve
const linePoints = curve.getPoints(100);CatmullRomCurve3 creates a smooth path that passes through every control point. getPoints(100) samples the curve at 100 evenly spaced intervals so we can render it.
Step 3 -- Render as a tube with thickness
const tubeGeo = useMemo(() => {
return new THREE.TubeGeometry(
curve,
64, // segments along the curve
0.05, // tube radius
12, // radial segments (roundness)
false // closed loop?
);
}, [curve]);
<mesh geometry={tubeGeo}>
<meshStandardMaterial color="#6366f1" />
</mesh>TubeGeometry wraps a cylinder around the curve, giving it real 3D thickness. It responds to lights and materials just like any other mesh. The radius controls how thick the tube is.
Step 4 -- Animate an object along the curve
const dotRef = useRef<THREE.Mesh>(null);
const progress = useRef(0);
useFrame((_, delta) => {
progress.current = (progress.current + delta * 0.2) % 1;
const point = curve.getPointAt(progress.current);
dotRef.current!.position.copy(point);
});
<mesh ref={dotRef}>
<sphereGeometry args={[0.1]} />
<meshBasicMaterial color="#22d3ee" />
</mesh>getPointAt(t) returns the exact position at percentage t along the curve (0 = start, 1 = end). We increment t each frame and use modulo to loop. The dot glides smoothly along the path.
What you just learned
CatmullRomCurve3 creates smooth curves that pass through a set of control points.
TubeGeometry wraps a real 3D mesh around a curve, giving it thickness and letting it respond to lighting.
curve.getPointAt(t) returns the position at any percentage along the curve — perfect for animating objects along paths.
Drei's Line component solves the WebGL lineWidth limitation using a custom shader.
Question
A roller coaster track is a tube that follows a curve through space. But the carts also need to tilt and bank through turns. How could you use the curve to calculate the correct rotation at each point? (Hint: look into Frenet-Serret frames.)
Think about it...
You draw a line with THREE.LineBasicMaterial and set lineWidth to 8. On Chrome it renders 1 pixel wide. What is the correct fix?
Try These Challenges
Put what you learned into practice. Try each challenge in the demo above using the Leva controls, then check the solution.
Try This!
Beginner
Set tube radius to 0.5 — thick rope!
Try This!
Beginner
Toggle control points — see the anchors
Try This!
Beginner
Max speed — racing dot
These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.
// Only 4 segments for the whole tube!
const tubeGeo = new THREE.TubeGeometry(
curve,
4, // tubularSegments — way too few
0.1, // radius
8, // radialSegments
false
);
// Result: a blocky tube that barely curves// lineWidth > 1 is not supported on most GPUs!
const mat = new THREE.LineBasicMaterial({
color: "red",
lineWidth: 5, // This does NOTHING on most systems
});
// Result: always 1 pixel wide// Progress goes past 1.0 and crashes!
progressRef.current += delta * speed;
const pt = curve.getPointAt(progressRef.current);
// When progress > 1: returns undefined → TypeErrorBest Practices
Use getPointAt for even spacing
Always prefer getPointAt(t) over getPoint(t) for animation. It reparameterizes by arc length so objects move at constant speed along the curve.
Use drei Line for thick lines
Native WebGL lines are always 1px on most GPUs. Drei's Line component uses Line2 and a custom shader to render properly thick, anti-aliased lines.
Memoize curve and geometry
Wrap CatmullRomCurve3 and TubeGeometry in useMemo. Creating them is expensive and they only need to be recalculated when control points change.
Match tube segments to curvature
Simple arcs need fewer segments than tight spirals. Start with 64 and increase only if you see faceting. More segments means more triangles means more GPU work.