Course outline
OrbitControls
In one line
OrbitControls lets a visitor orbit, zoom and (optionally) pan the camera around a target with mouse, touch or wheel. Left alone it lets the camera go under the floor and through the bike, and it takes over touch scrolling, so you set limits and know what it claims.
Think of it like this
A photographer's tripod with a ball joint. The camera always looks at the subject. You can swing it around, tilt it up and down, or slide the whole tripod closer or farther. OrbitControls is that tripod, and the subject is its target.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- target
- The point the camera looks at and orbits around, a
Vector3. - polar angle
- The camera's angle from straight up: 0 looks down from above, π / 2 is the horizon, π is from below.
- damping
- Letting a motion fade out instead of stopping at once, like a spinning wheel slowing down.
- dolly
- Moving the camera closer to or farther from the target (zooming by moving).
- pan
- Sliding the camera and its target sideways together.
- touch-action
- A CSS property that tells the browser which touch gestures it may handle itself, such as scrolling.
nonehands all of them to your code.
Step by step
01Run the controls
Drag the first example with the mouse or a finger, scroll to zoom. The camera orbits the middle of the "frame" at 0.45 m, because the target is set there.
02Compare limits with no limits
The same example pushes two controls to the extreme: the camera dragged 115° down and a zoom of 100×. Default controls end at a camera height of −2.60 m (under the floor) and 0.03 m from the target. Limited controls stop at 0.64 m and 0.80 m.
03See what the controls claim
The second example sends a wheel event and reads the canvas's touch-action at five moments: before the controls, after creating them, with enableZoom = false, after setting pan-y, and after dispose().
04Keep update() in the loop
With enableDamping = true, a drag only starts the motion; each controls.update() call applies the next share of it. So the line controls.update() goes inside the animation loop, before renderer.render(scene, camera). Remove it and the camera still turns while you drag, but stops dead when you let go, because nothing applies the remaining motion.
Try it yourself
- 1
Change a limit
In the first example set
minDistanceto2: the limited line ends withclosest distance 2.00 m. SetmaxPolarAngletoMath.PI / 2instead: the camera height becomes0.45 m, exactly the target's height, because the camera sits on the horizon. - 2
Lock the tilt
Replace the
maxPolarAngleline withcontrols.minPolarAngle = controls.maxPolarAngle = Math.PI / 2.5;. The same 115° downward drag now ends at a camera height of1.39 m: the tilt is frozen at 72° from straight up and only the sideways turn and the zoom still work.
Code & diagrams
Drag, scroll or pinch to try it. The two lines come from a test that pushes both setups as hard as a user could. `dollyOut(100)` shrinks the distance, and `rotateUp(-2)` drags the camera downwards.
Console
Run the scene to see its console.Expected console output
default limits: camera height -2.60 m; closest distance 0.03 m
with limits : camera height 0.64 m; closest distance 0.80 mThe WheelEvent is sent by the code itself. In a real page the same applies to a person's wheel, and the touch-action line decides what a finger does.
Console
Run the scene to see its console.Expected console output
before the controls: touch-action = "" | wheel scrolls the page: yes
controls made: touch-action = "none" | wheel scrolls the page: no (the controls took it)
enableZoom = false: touch-action = "none" | wheel scrolls the page: yes
pan-y set by us: touch-action = "pan-y" | wheel scrolls the page: no (the controls took it)
after dispose(): touch-action = "" | wheel scrolls the page: yesBreak 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 update() with damping
In the first example, delete the line controls.update(); inside setAnimationLoop.
Myth vs fact
Myth
OrbitControls rotates the object.
Fact
It moves the camera around a target. The bike never turns, so lights fixed in the world stay where they are and the reflections change, which is what a visitor walking around a real bike would see.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
update()returnstruewhen the camera moved since the last call. Together with thechangeevent this is the hook for rendering only when something changed, which saves battery (Phase 17). - ▸
minTargetRadiusandmaxTargetRadiuslimit how far the target may be panned fromcontrols.cursor. That allows a small pan without letting a visitor lose the bike.
Remember this
- 1
The problem this solves: turning a bike with the mouse needs pointer events, angles, damping and touch gestures, and every part is hard to get right.
new OrbitControls(camera, renderer.domElement)does all of it. It lives in the add-ons:import { OrbitControls } from "three/addons/controls/OrbitControls.js". - 2
How it works. The controls keep the camera on a sphere around
target: a distance, a polar angle from straight up (0 to π) and an azimuth around the vertical. A drag changes the angles, the wheel the distance, a pan moves the target;update()writes the position and aims the camera. Mouse: left orbits, wheel zooms, right pans. Touch: one finger orbits, two pinch and pan. Arrow keys needcontrols.listenToKeyEvents(window). - 3
Target. The default
targetis (0, 0, 0), the stand's floor. The bike's middle is about 0.45 m up, so setcontrols.target.set(0, 0.45, 0)and callupdate(). Without it the bike swings around a point under its frame. - 4
**Damping needs
update().**enableDamping = truemakes the motion ease out, but only ifcontrols.update()runs every frame. The damping factor (default 0.05) is a share applied perupdate()call, not per second. After a 57° flick, 1 update applies 2.9°, 30 updates 45° and 90 updates 99%: 1.5 s on a 60 Hz screen but 0.63 s on a 144 Hz screen. - 5
Limits are not optional. Defaults allow any distance and any polar angle from 0 to π, so a visitor can drag the camera below the target, under the floor, or zoom through the bike. Set
minDistanceandmaxDistance, andmaxPolarAngle(π / 2 is the horizon: with a target above the floor the camera then never goes below it).minPolarAngle,minAzimuthAngleandmaxAzimuthAnglelimit the other directions.enablePan = falsekeeps the bike centred. - 6
It claims the touch screen. The constructor sets
touch-action: noneon the canvas, so every finger movement goes to the controls and the page never scrolls; the wheel is taken withpreventDefault()too. On a full-screen canvas that is right. On a scrolling page a full-width canvas traps the user's finger: leave room outside it, or setcanvas.style.touchAction = "pan-y"so vertical swipes scroll the page (one finger then orbits only sideways). - 7
Clean up.
controls.dispose()removes the listeners and restorestouch-action. The controls also firestart,changeandendevents, which the next topics use.
Explain it without notes
A visitor on a phone says the page will not scroll when the finger is on the 3D view. What causes it and what are two fixes?
Why can a camera end up under the floor with default OrbitControls, and what one setting prevents it?
Practice
Make the controls so the bike can only turn sideways (no tilt), between 1 m and 4 m away, with no panning.
Print a line each time the user starts and stops moving the camera.
Done when you can
Create OrbitControls, set its target and call
update()in the loopSet limits so the camera cannot go under the floor or through the bike
Explain damping in terms of
update()callsSay what the controls take from the page (touch, wheel) and how to give it back