Navigate

Search topics across all sections

GitHub
Particles & Lines

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.

terminal
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

CurvePath.tsxTSX
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

CurvePath.tsxTSX
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

CurvePath.tsxTSX
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

CurvePath.tsxTSX
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.

1
Curve looks jagged even with many points
Too few segments on the TubeGeometry
Don't do this
SmoothTube.tsxTSX
// 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
TubeGeometry's first numeric argument is how many cross-sections are placed along the curve. Too few and the tube cuts corners. 64 is a good default. For very long or twisty curves, go higher.
2
Line is invisible because lineWidth has no effect
Most GPUs ignore lineWidth in WebGL
Don't do this
ThickLine.tsxTSX
// 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
WebGL's native line rendering ignores lineWidth on most hardware — lines are always 1 pixel. Drei's Line component uses THREE.Line2 internally, which draws lines as screen-space quads with a shader, giving you real control over thickness.
3
getPointAt returns undefined outside 0-1 range
Progress value wraps incorrectly
Don't do this
CurveTraveler.tsxTSX
// Progress goes past 1.0 and crashes!
progressRef.current += delta * speed;
const pt = curve.getPointAt(progressRef.current);
// When progress > 1: returns undefined → TypeError
getPointAt(t) expects t between 0 and 1, representing 0% to 100% along the curve. If t exceeds 1, the method returns undefined and your code crashes. Always use modulo (% 1) to keep the value in range.

Best 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.