---
title: "Mission: NASA's Perseverance Rover, Flown to Mars by Your Scroll"
description: "How I turned NASA's public Perseverance 3D model into a scroll-driven CAD drawing: designed, assembled, launched on an Atlas V, landed in Jezero and driven. Three.js, hidden lines, prompts inside."
date: "2026-11-12"
type: "build"
tags: ["three.js", "webgl", "scroll animation", "nasa", "creative coding", "nextjs"]
output: "/lab/mission"
---

I saw a space company's site that scrolls a wireframe satellite through its life, from design to orbit, and wanted one that felt less like a demo and more like a real engineering drawing. So I used a real spacecraft. [It is live at avivashishta.com/lab/mission](/lab/mission): NASA's Perseverance rover, drawn like CAD, carried through its actual mission by your scroll.

You start on the drafting floor with the rover in pieces, slowly turning (drag it to turn it yourself). Scroll and it assembles, one subsystem at a time, each named as it flies in, then dimensioned. The aeroshell closes round it, an Atlas V 541 plots in on the pad at Cape Canaveral, and it lifts off: boosters away, fairing open, stage separation. Seven months of cruise, Mars on approach, the sky crane lowering it into Jezero Crater on three cables, and then it drives.

<div class="highlight"><p><strong>The short version:</strong> the rover is NASA/JPL-Caltech's public 3D model, baked down to bare geometry (1.8 MB). Everything is drawn in two passes: a fill in the page colour that hides what is behind it, and the edges as thin lines, plus a faint ghost of the hidden edges. The rocket and the pad are built in code from published dimensions. One number, the scroll position from 0 to 1, drives every camera move, part and engine.</p></div>

## Step 1: a real model, baked to geometry

My first version was a satellite I modelled myself, and it looked made up, because it was. Realism comes cheap if you start from the real thing: NASA publishes its spacecraft models in the public domain at [github.com/nasa/NASA-3D-Resources](https://github.com/nasa/NASA-3D-Resources), Perseverance included.

The file is built to be looked at in colour: textures, materials, a skinned cable, Draco compression. A line drawing needs none of that. A small [gltf-transform](https://gltf-transform.dev) script keeps only positions, normals and the node names (the page animates parts by name), then compresses with meshopt:

```js
for (const mesh of root.listMeshes()) for (const prim of mesh.listPrimitives()) {
  prim.setMaterial(mat);
  for (const sem of prim.listSemantics()) if (sem !== "POSITION" && sem !== "NORMAL") prim.setAttribute(sem, null);
}
for (const n of root.listNodes()) n.setSkin(null); // one cable is skinned; a drawing doesn't need it
for (const t of root.listTextures()) t.dispose();
await doc.transform(prune({ keepLeaves: true }), dedup(), weld(), meshopt({ encoder: MeshoptEncoder }));
```

Drop the skin but not the joints it pointed at, and three.js crashes while loading (`normalizeSkinWeights`). Remove both.

One more surgery happens in the browser. NASA's file has all six wheels in one mesh, so they can't turn. The page sorts its triangles by which corner of the rover their centre sits in, and makes six meshes, one per axle.

## Step 2: drawing like CAD

What makes it read as an engineering drawing and not a wireframe is **hidden line removal**. A wireframe shows every edge through the whole object, which is noise. A drawing shows only the edges you could actually see.

Every part is drawn three times:

1. **A fill in the page's own colour**, pushed slightly back with `polygonOffset`. You can't see it, but it hides whatever is behind it.
2. **The edges**, from `EdgesGeometry` at a 28 degree threshold, so flat panels don't show their triangles.
3. **A ghost of the same edges** at 4% opacity with the depth test off, so hidden structure is just barely there, like construction lines.

To pick out one subsystem, its fill turns solid blue with a little shading. That is the only colour on the page.

## Step 3: a pen that plots it in

After the download, nothing appears at once. A plane sweeps across the model and lines only exist behind it, with a bright blue pen line at the front, like a plotter drawing it. It is three lines in the shader:

```glsl
float s = dot(vW, uRevealDir);          // how far along the sweep this pixel is
if (s > uReveal) discard;               // not drawn yet
float pen = exp(-pow((uReveal - s) * 3., 2.)); // bright just behind the front
```

The rocket uses the same plane, pointed down, so it plots in from the nose to the pad while the camera pulls back.

## Step 4: the rocket and the pad, from the numbers

NASA's repo doesn't have the Atlas V, so I built it from its published dimensions: a 3.81 m core booster 32.5 m long with the RD-180's two nozzles, four 1.58 m solid boosters, the Centaur upper stage and the 5.4 m fairing in two halves, hinged so they peel open. Almost all of it is bodies of revolution, so one helper does most of the work: give it a profile (radius, height pairs) and it draws a hidden fill plus a ring at every profile point and a few meridians, the way an engineer would draw a cylinder, instead of every triangle.

The pad is Space Launch Complex 41: the mobile platform, an umbilical tower, four lightning towers with catenary wires between them, and the rails. The exhaust is 5,200 points with additive blending.

One honest cheat: the rover's deployed size doesn't fit inside a true-scale aeroshell, so the shell and rocket are drawn 1.3 times larger. Nobody notices, and everything else is to scale.

## Step 5: one number drives everything

The page is 1,300 viewport heights tall and the canvas is fixed. The scroll position, 0 to 1, is the whole timeline. Each phase starts at a fixed point (assemble at 0.14, launch at 0.32, cruise at 0.52, land at 0.63, explore at 0.82), and every event is a `smooth(start, end, p)` ramp:

```ts
const ignite = smooth(.392, .4, p);                               // the core lights
const lift = ramp(.4, .52, p) ** 2.3 * 520;                       // slow off the pad, then fast
const srbOn = smooth(.398, .402, p) * (1 - smooth(.43, .436, p)); // boosters burn, then go out
const fairSep = er(.452, .472, p);                                // the fairing halves peel away
```

The camera works the same way: a list of keyframes (position, target, field of view), eased between. Long lenses from far away keep the perspective flat, which is what makes it feel like a drawing and not a game. Scrubbing backwards just runs everything in reverse for free, because nothing is stored, it is all computed from `p`.

## Step 6: the turntable

On the first screen the rover turns slowly on its own, and you can drag it any way. When you let go it coasts, then settles back into the slow spin. As you scroll into assembly the drag fades out and the camera eases back onto its path, the short way round:

```ts
const wrap = (a: number) => Math.atan2(Math.sin(a), Math.cos(a)); // any angle to -π..π
off.applyAxisAngle(Y, wrap(orbit.yaw) * orbit.weight);            // weight goes 1 to 0 as assembly begins
```

Without `wrap`, a rover you spun five times would unspin five times on the way out.

## The small things

- **The loader is a title block**, the box in the corner of an engineering drawing, with the real download in MB filling a bar.
- **Labels follow the parts.** Each subsystem's name is projected from its 3D centre every frame, so it rides beside the part as it flies home, and only shows during assembly.
- **The mast unfolds and pans** by rotating NASA's own nodes about an axis in world space, so the hierarchy in the file just works.
- **The terrain is contour lines** (marching squares over a noise heightmap), so Mars is a topo map, not a texture.
- **No post-processing.** A composer with an output pass turned my near-black background grey; clearing to pure black fixed it, and CSS adds the grain and vignette.

## Build your own

<div class="promptbox">
  <button class="copybtn" type="button" data-copy>Copy prompt</button>
  <div class="prompt">
<p>Build a scroll-driven page (React + three.js) that tells the story of a real object as a CAD drawing. Load a public 3D model (for example from github.com/nasa/NASA-3D-Resources), baked to positions and normals only. Draw every mesh three ways: a fill in the background colour with polygonOffset so it hides what is behind it, EdgesGeometry at about 28 degrees as thin lines, and the same edges at 4% opacity with depthTest off as a ghost of the hidden lines. Highlighting a part turns its fill solid blue with simple shading.</p>
<p>Make the page about 1300vh tall with a fixed canvas, and compute one progress value p from 0 to 1 from the scroll. Every event is smoothstep(start, end, p): parts exploding out and flying home in order, camera keyframes (position, target, fov) eased between, with long lenses from far away. Plot the model in with a reveal plane in the shader (discard where dot(worldPos, dir) is beyond a uniform, add a bright pen line just behind it). Project each part's centre to the screen every frame to place an HTML label beside it. On the first screen let the user drag to orbit with inertia and a slow auto spin, and fade the orbit out as the next phase starts, wrapping the angle so it returns the short way. Add a segmented progress bar of the phases at the bottom that jumps to each. Respect prefers-reduced-motion.</p>
  </div>
</div>

The rover model is by NASA/JPL-Caltech, from NASA's 3D Resources.
