Command Palette

Search for a command to run...

Hectal
Course outline
Topic 7.6Intermediate ~30 min · step 6 of 6

Build: Camera Views

In one line

The showroom gets orbit controls with limits, three preset views (side, front, rear wheel) on buttons that work with a keyboard, a camera that refits when the screen changes shape, and the bike's rear wheel.

0/6 · 0%

Before you start

You will learn

  • Add orbit controls with limits and three preset views
  • Compute each view from the bike's size and the screen's shape
  • Add the rear wheel to the bike

You write code in

In your lumen-3d project: add or replace these files, then run npm run dev. Copy the texture rubber_tiles_diff_1k.jpg from this site into public/data/3d/textures/

Files you touch

  • index.html
  • src/main.js
  • src/world/camera.js
  • src/systems/controls.js
  • src/objects/bike.js
  • public/data/3d/textures/rubber_tiles_diff_1k.jpg

Think of it like this

A shop window with a rotating plinth is turned by an assistant. When a customer says "show me the back wheel", the assistant turns it gently to that spot and stops if the customer starts turning it themselves. This build gives the showroom that assistant.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

preset view
A camera position and target saved under a name, such as "side", that a button can fly to.
refit
Computing a preset again after the screen changed shape, if the visitor has not moved the camera since.
tick
A method the loop calls every frame with the time since the last frame, in seconds (Topic 3.2).
hub
The centre of a wheel, where the axle goes. The detail view looks at the rear wheel's hub.

Step by step

01Views are computed, not typed in

viewFor(name, camera, bike) measures the bike with Box3, then returns a { position, target }. Side: from +z at the distance that fits the bike's length. Front: from +x. Detail: close to the rear wheel's hub. It returns null for an empty bike, so the buttons do nothing instead of sending the camera to NaN.

02Controls with limits

createControls() sets minDistance 0.8, maxDistance 6, maxPolarAngle π / 2, no panning and damping. maxPolarAngle stops the camera at the horizon; the bike's middle is above the floor, so the camera stays above it too.

03Fly, cut or cancel

goTo(name) starts a 1.2 s flight, using the arc of Topic 7.5. With reduced motion, or { instant: true }, it sets the camera at once. A start event from the controls cancels the flight and forgets the preset, so refit() leaves a camera the visitor placed alone.

04Wire the buttons and the shape

index.html has three buttons with a data-view each. main.js shows them only after the support check, so they do not appear on the no-WebGL fallback. The ResizeObserver calls controls.refit() after setting the aspect.

05The rear wheel

createWheel(name, x) builds a tyre (a torus with the tread texture), a rim, a hub and 18 spokes as one InstancedMesh, so a wheel costs 4 draw calls. The bike scene now makes 18 draw calls: 9 for the frame tubes, stays and fork legs, 8 for the two wheels and 1 for the stand.

Try it yourself

  1. 1

    A view that is too close

    In the example change new THREE.Vector3(0.35, 0.35, 0.75) in viewFor to new THREE.Vector3(0.35, 0.35, 0.3) and press "Rear wheel". The offset is 0.58 m, closer than minDistance, so the controls push the camera back: it stops 0.80 m from the hub.

  2. 2

    A faster flight

    Set FLIGHT_SECONDS to 0.3. The buttons now almost cut. Turn on "reduce motion" in your operating system and press a button: the camera cuts at once.

Code & diagrams

The showroom after Phase 7 New tab

Local imports cannot run in the preview, so this example has the code of every project file inline, each under a comment with its path. The files of Phases 3, 5 and 6 are shortened stand-ins. Press the three buttons; drag, scroll or pinch. The tread texture loads from /data/3d/textures.

idle

Console

Run the scene to see its console.

Expected console output

bike parts: down-tube, top-tube, seat-tube, chain-stays, seat-stays, fork, front-wheel, rear-wheel
draw calls: 18 | triangles: 7968

Your project files

Your lumen-3d project after this topic. Click a file to see it in full.

index.htmlhtml

The whole file after this topic. If yours differs, you can copy this one and carry on.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Lumen Bikes · 3D showroom</title>
    <style>
      html, body { margin: 0; height: 100%; background: #0b1118; color: #e6edf6; font-family: system-ui, sans-serif; }
      #app { position: fixed; inset: 0; }
      canvas { display: block; }
      #views { position: fixed; left: 50%; bottom: 16px; transform: translateX(-50%); display: flex; gap: 8px; }
      #views[hidden] { display: none; }
      #views button { min-height: 44px; padding: 0 18px; border: 1px solid #3b4757; border-radius: 22px; background: #121b26; color: inherit; font: inherit; cursor: pointer; }
      #views button:hover { border-color: #ff6b2c; }
      #views button:focus-visible { outline: 3px solid #2a9df4; outline-offset: 2px; }
      #fallback { position: fixed; inset: 0; display: grid; place-content: center; justify-items: center; gap: 12px; padding: 16px; text-align: center; background: #0b1118; }
      #fallback[hidden] { display: none; }
      #fallback img { max-width: min(100%, 420px); max-height: 60%; border-radius: 8px; }
    </style>
  </head>
  <body>
    <div id="app"></div>
    <nav id="views" aria-label="Camera views" hidden>
      <button type="button" data-view="side">Side</button>
      <button type="button" data-view="front">Front</button>
      <button type="button" data-view="detail">Rear wheel</button>
    </nav>
    <div id="fallback" hidden>
      <p>Your browser can't draw 3D here, so here is a picture of the Lumen bike instead.</p>
      <img src="/data/3d/showroom-poster.png" alt="The Lumen Bikes showroom: an orange e-bike on a round display stand" />
    </div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Forget the aspect on resize

In main.js remove the line resizeCamera(camera, width, height); from resize().

terminal
$ Run
── what you'll see ──
no error: the bike is stretched sideways and the wheels are ovals

Myth vs fact

Myth

Preset views can be fixed numbers.

Fact

Fixed numbers fit one screen shape. Computed views refit for a phone held upright, which needs more than three times the distance.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The detail view reads the wheel's world position with getWorldPosition(). When Phase 9 spins the wheels, the hub stays where it is, so the view stays valid. If the whole bike moved, the view would have to be computed again each time.

  • ▸

    maxDistance caps the refit. On a window narrower than about 0.43 (width divided by height), such as a folded phone's cover screen, the side view would need more than 6 m. The controls clamp it to 6 m (measured: 5.48 m at 0.47, 5.99 m at 0.43, 6.00 m at 0.40) and the bike's ends are cropped. Raise maxDistance if you must support such screens.

  • ▸

    Every wheel shares one tyre material, so the texture is on the GPU once. The two wheels are separate groups, not clones of one mesh, because later phases need them as separate parts to name, pick and paint.

Remember this

  1. 1

    The problem this solves: Lena's two bugs are fixed. Her swipe is no longer a surprise: the showroom fills the whole screen, so a swipe on it is meant to turn the bike and there is no page to scroll (a page that scrolls around a canvas needs the pan-y fix of Topic 7.3). The camera can no longer go under the floor or through the bike, because of limits. Ama's request is met: one tap on "Rear wheel" flies to a close view of it.

  2. 2

    What it adds. src/world/camera.js gains fitDistance() and viewFor() (each preset computed from the bike's size and the screen's shape). The new src/systems/controls.js wraps OrbitControls (damping, limits, no pan), flies between views on an eased arc, stops when the visitor grabs the controls, and cuts when reduced motion is on. bike.js gets createWheel() and the rear wheel ("rear-wheel"); index.html three buttons.

  3. 3

    How it plugs in. controls has a tick(delta), so loop.add(controls) is all the loop needs. main.js creates the bike first, because the views are measured from it. The old turntable is gone: the camera now does the moving. A gentle turntable returns in Phase 9, with a pause button.

  4. 4

    What the showroom can do now: orbit, zoom and tilt with mouse or touch, within limits; jump to the side, front or rear wheel with buttons (Tab, then Enter or Space); stay fitted when a phone turns; show both wheels. Still missing: clicking a part (Phase 8), keys that orbit the camera (Topic 8.4), colours (Phase 12) and the real model (Phase 10).

  5. 5

    Starting point. This build starts from your files after Phase 6. The live example below shortens the files this phase does not change, so it runs on its own; if your files differ in detail, keep yours and add what is new. These main.js and bike.js are compact: they keep the same function and part names, but leave out Phase 6's loadEnvironment(scene, renderer) call, the tyre's normal map and the shadow flags. Keep those lines from your own files if you want the reflections and shadows.

Explain it without notes

01

Why does the build compute viewFor when you click, instead of when the page loads?

02

What happens when the visitor drags the camera and then turns the phone?

Practice

01

Add a fourth preset, "top", that looks straight down at the bike from 2 m above. What limit gets in the way, and what do you change?

02

Make a key press, 1, 2 or 3, jump to a preset. Where does the listener go?

Done when you can

  • Wire OrbitControls with limits and a target into the loop

  • Compute preset views from the bike's size and the aspect

  • Cancel flights on user input and respect reduced motion

  • Say which files this phase added or changed and why