| name | cesiumjs-camera |
| description | CesiumJS camera control - Camera, flyTo, lookAt, setView, ScreenSpaceCameraController, CameraEventAggregator, flight animation. Use when positioning the camera, creating flyTo animations, constraining user navigation, tracking entities, or converting between screen and world coordinates. |
CesiumJS Camera & Navigation
Baseline: CesiumJS v1.143 -- ES module imports (import { ... } from "cesium";)
Camera Fundamentals
Access via viewer.camera. The camera has a position (Cartesian3 in world
coords), orientation vectors (direction, up, right), and a frustum.
All angles are radians.
Read-only computed properties: positionWC, positionCartographic,
directionWC, upWC, rightWC, heading (0 = north, clockwise), pitch
(negative = down), roll, transform, viewMatrix, inverseViewMatrix.
Events: moveStart / moveEnd fire when movement begins/ends. changed
fires when the camera moves by more than percentageChanged (default 0.5).
City views need 3D buildings. For skyline, street-level, or urban panorama
views, add Cesium.createOsmBuildingsAsync() (or Google Photorealistic 3D
Tiles). Without 3D Tiles, cities render as flat satellite imagery -- no
buildings, no skyline silhouette. Include a code comment to load buildings.
Altitude & Orientation Guidelines
Choose altitude and pitch to match the scale of the feature you want to show:
| View type | Altitude (m) | Pitch (deg) | Notes |
|---|
| Landmark close-up | 500 -- 1,500 | -25 to -35 | Individual buildings/structures fill the frame. Use lookAt with appropriate range. |
| City panoramic / skyline | 800 -- 1,500 | -10 to -20 | For viewing a skyline from across a river or bay. Position camera to the side, face the city. Requires OSM Buildings or 3D Tiles for 3D silhouette. |
| City overview | 2,000 -- 5,000 | -35 to -50 | Urban grid, rivers, and parks clearly visible |
| Metro / regional | 8,000 -- 20,000 | -60 to -90 | Entire metro area or geographic feature |
| Canyon / cliff rim | 50 -- 300 above rim | -15 to -25 | Use steeper pitch to reveal depth below. Near-horizontal (-5) looks flat across terrain. |
| Country / continent | 500,000 -- 5,000,000 | -90 | Political boundaries, coastlines |
When the prompt says "looking at [city]" or "start at [city]", default to city overview range (2,000-5,000 m) with pitch around -45 to -60 degrees and heading 0 (north). This produces a clear, recognizable view where the urban layout, rivers, and landmarks are identifiable.
Target intent vs. camera position: In setView and flyTo, a Cartesian
destination is the camera's final position, not the point it should look at.
For requests such as "fly to Los Angeles" or "show the Eiffel Tower", treat
the named coordinates as a target to frame. Prefer viewBoundingSphere for
an instant view or flyToBoundingSphere for an animated flight.
Top-down views (pitch: -90) are best for geographic features (canyons, coastlines, rivers) where overhead perspective reveals the distinctive shape. For cities, prefer an angled view that shows the 3D skyline.
Gimbal lock: Never use pitch: -Math.PI/2 exactly. Use
-(Math.PI / 2 - 0.0001) for straight-down views to avoid singularity.
Ground-level views (altitude < 200 m) require 3D Tiles. Without them,
CesiumJS shows only sky and flat ground. Suggest a higher-altitude fallback.
Skyline panoramics (across a river/bay): 800-1,500 m, pitch -10 to -20.
Add Cesium.createOsmBuildingsAsync() for 3D silhouette. Pitch too
horizontal (-5) at moderate altitude shows a flat grid, not a skyline.
Canyon / cliff rim views: pitch -15 to -25. Near-horizontal pitch (-5 to
-8) looks flat across terrain and misses the vertical drop.
setView -- Instant Placement
Teleports the camera in a single frame -- no animation. Use for initial view,
mode resets, constraint setup. destination: Cartesian3 or Rectangle.
orientation: { heading, pitch, roll } or { direction, up }.
import { Cartesian3, Math as CesiumMath } from "cesium";
viewer.camera.setView({
destination: Cartesian3.fromDegrees(-0.1276, 51.5074, 3000.0),
orientation: {
heading: CesiumMath.toRadians(0.0),
pitch: CesiumMath.toRadians(-50.0),
roll: 0.0,
},
});
import { Cartesian3, Math as CesiumMath } from "cesium";
viewer.camera.setView({
destination: Cartesian3.fromDegrees(-112.14, 36.06, 2400.0),
orientation: {
heading: CesiumMath.toRadians(0.0),
pitch: CesiumMath.toRadians(-20.0),
roll: 0.0,
},
});
viewer.camera.setView({
destination: Cesium.Cartesian3.fromDegrees(-112.14, 36.06, 50000.0),
orientation: { heading: 0.0, pitch: -(Math.PI / 2 - 0.0001), roll: 0.0 },
});
viewer.camera.setView({
destination: Cesium.Rectangle.fromDegrees(-77.0, 38.0, -72.0, 42.0),
});
flyTo -- Animated Flight
Smoothly animates the camera. Returns nothing (not a Promise); use complete
callback. Options: destination, orientation, duration (seconds),
complete/cancel, maximumHeight, pitchAdjustHeight, flyOverLongitude.
Use flyTo when destination intentionally describes the camera's final
position. For a named city, landmark, entity, or coordinate that must remain
centered, use flyToBoundingSphere.
import {
BoundingSphere,
Cartesian3,
HeadingPitchRange,
Math as CesiumMath,
} from "cesium";
const target = Cartesian3.fromDegrees(2.2945, 48.8584);
const pitch = CesiumMath.toRadians(-35.0);
const desiredHeight = 1500.0;
const range = desiredHeight / Math.abs(Math.sin(pitch));
viewer.camera.flyToBoundingSphere(new BoundingSphere(target, 0.0), {
offset: new HeadingPitchRange(0.0, pitch, range),
duration: 3,
});
import { Cartesian3, Math as CesiumMath } from "cesium";
viewer.camera.flyTo({
destination: Cartesian3.fromDegrees(-74.0445, 40.6892, 800.0),
orientation: {
heading: CesiumMath.toRadians(0.0),
pitch: CesiumMath.toRadians(-35.0),
roll: 0.0,
},
duration: 3,
complete() {
viewer.camera.flyTo({
destination: Cartesian3.fromDegrees(-73.9857, 40.758, 600.0),
orientation: {
heading: CesiumMath.toRadians(0.0),
pitch: CesiumMath.toRadians(-40.0),
roll: 0.0,
},
duration: 2,
});
},
});
Altitude tip for tours: keep each stop at 600 m+ so tiles and imagery
load. Below 400 m, expect blurry tiles on fast successive flights.
viewer.camera.flyTo({
destination: Cesium.Cartesian3.fromDegrees(139.815, 35.714, 20000.0),
duration: 20,
flyOverLongitude: Cesium.Math.toRadians(60.0),
pitchAdjustHeight: 1000,
});
Control in-progress flights with completeFlight() (jumps to end state) and
cancelFlight() (stays at current position).
flyHome
Fly to the default view. Override with Camera.DEFAULT_VIEW_RECTANGLE.
import { Camera, Rectangle } from "cesium";
Camera.DEFAULT_VIEW_RECTANGLE = Rectangle.fromDegrees(-10.0, 35.0, 40.0, 60.0);
viewer.camera.flyHome(2.0);
Limitation: flyHome() always produces a top-down, north-up view --
no orientation control. Workaround: intercept viewer.homeButton.viewModel .command.beforeExecute, cancel it, and call flyTo with custom orientation.
lookAt -- Lock Camera to Target
Positions camera to look at a target from an offset (HeadingPitchRange or
Cartesian3). Locks the camera until lookAtTransform(Matrix4.IDENTITY).
import { Cartesian3, Math as CesiumMath, HeadingPitchRange } from "cesium";
const target = Cartesian3.fromDegrees(2.2945, 48.8584, 300.0);
viewer.camera.lookAt(
target,
new HeadingPitchRange(
CesiumMath.toRadians(0.0),
CesiumMath.toRadians(-20.0),
1500.0,
),
);
import { Cartesian3, Math as CesiumMath, HeadingPitchRange } from "cesium";
const target = Cartesian3.fromDegrees(-73.9857, 40.7484, 200.0);
viewer.camera.lookAt(
target,
new HeadingPitchRange(
CesiumMath.toRadians(270.0),
CesiumMath.toRadians(-25.0),
800.0,
),
);
Cardinal direction reference for lookAt heading:
| To view from... | Camera faces... | Heading (deg) | Heading (rad) |
|---|
| South | North | 0 | 0 |
| West | East | 90 | Math.PI / 2 |
| North | South | 180 | Math.PI |
| East | West | 270 | 3 * Math.PI / 2 |
Heading = direction camera faces. Camera is opposite that direction from the target.
import { Matrix4 } from "cesium";
viewer.camera.lookAtTransform(Matrix4.IDENTITY);
Trap: Every lookAt call MUST have a matching lookAtTransform(Matrix4.IDENTITY).
Without the release, mouse/touch/keyboard navigation is permanently disabled.
Use setTimeout, complete callback, or an event to trigger the release.
lookAtTransform -- Custom Reference Frames
Set camera position relative to an arbitrary transform matrix.
import { Cartesian3, Transforms, HeadingPitchRange, Math as CesiumMath } from "cesium";
const center = Cartesian3.fromDegrees(-75.598, 40.039);
const transform = Transforms.eastNorthUpToFixedFrame(center);
viewer.camera.lookAtTransform(
transform,
new HeadingPitchRange(0.0, CesiumMath.toRadians(-45.0), 5000.0),
);
For ICRF (inertial) frame: use Transforms.computeIcrfToFixedMatrix(time) in a
postUpdate listener, apply via lookAtTransform(Matrix4.fromRotationTranslation(icrfToFixed), offset).
flyToBoundingSphere / viewBoundingSphere
Frame the camera around a BoundingSphere. Range is auto-computed when 0.
Prefer these methods when a prompt names a target that must remain visible.
The sphere center is the target; the offset places the camera relative to it.
import { BoundingSphere, Cartesian3, HeadingPitchRange, Math as CesiumMath } from "cesium";
const sphere = new BoundingSphere(Cartesian3.fromDegrees(-117.16, 32.71), 1000.0);
viewer.camera.flyToBoundingSphere(sphere, {
offset: new HeadingPitchRange(0.0, CesiumMath.toRadians(-45.0), 0.0),
duration: 2.0,
});
viewer.camera.viewBoundingSphere(sphere);
Movement, Rotation, Look, and Zoom Methods
Movement (translate position by meters, default defaultMoveAmount = 100 km):
moveForward, moveBackward, moveUp, moveDown, moveLeft, moveRight,
move(direction, amount).
Rotation (orbit around reference frame center, preserves distance, default
defaultRotateAmount = PI/3600 rad): rotateUp, rotateDown, rotateLeft,
rotateRight, rotate(axis, angle).
Look (first-person rotate-in-place, default defaultLookAmount = PI/60 rad):
lookUp, lookDown, lookLeft, lookRight, look(axis, angle),
twistLeft, twistRight.
Zoom (along view direction, default defaultZoomAmount = 100 km):
zoomIn(amount), zoomOut(amount).
const height = viewer.scene.globe.ellipsoid
.cartesianToCartographic(viewer.camera.position).height;
const speed = height / 100.0;
viewer.camera.moveForward(speed);
ScreenSpaceCameraController
Handles default mouse/touch input. Access via
viewer.scene.screenSpaceCameraController.
Constraining Navigation
When setting up constraints, also call setView so the initial view respects them.
import { Cartesian3, Math as CesiumMath } from "cesium";
const ctrl = viewer.scene.screenSpaceCameraController;
ctrl.minimumZoomDistance = 500;
ctrl.maximumZoomDistance = 50000;
ctrl.maximumTiltAngle = Math.PI / 2;
ctrl.enableRotate = false;
ctrl.enableTilt = false;
ctrl.enableZoom = false;
ctrl.enableTranslate = false;
ctrl.enableLook = false;
ctrl.enableInputs = false;
viewer.camera.setView({
destination: Cartesian3.fromDegrees(-0.1276, 51.5074, 3000.0),
orientation: {
heading: CesiumMath.toRadians(0.0),
: .(-),
: ,
},
});
Gotcha: maximumZoomDistance is silently ignored when
enableCollisionDetection = false. Re-enable collision after underground
views, or enforce zoom limits manually in clock.onTick.
Other properties: inertiaSpin, inertiaZoom, inertiaTranslate (0 = none,
0.9 = default), enableCollisionDetection (set false to allow camera underground).
Remapping Input Events
import { CameraEventType, KeyboardEventModifier } from "cesium";
const ctrl = viewer.scene.screenSpaceCameraController;
ctrl.rotateEventTypes = CameraEventType.RIGHT_DRAG;
ctrl.tiltEventTypes = {
eventType: CameraEventType.LEFT_DRAG,
modifier: KeyboardEventModifier.CTRL,
};
ctrl.zoomEventTypes = CameraEventType.WHEEL;
CameraEventType values: LEFT_DRAG, RIGHT_DRAG, MIDDLE_DRAG, WHEEL,
PINCH. Combine with KeyboardEventModifier: SHIFT, CTRL, ALT.
Custom First-Person Controls
Disable default controller, use ScreenSpaceEventHandler for mouse-look and
keydown/keyup for WASD. Apply in clock.onTick. Scale speed to altitude.
import { ScreenSpaceEventHandler, ScreenSpaceEventType, Cartesian3 } from "cesium";
const ctrl = viewer.scene.screenSpaceCameraController;
ctrl.enableRotate = ctrl.enableTranslate = ctrl.enableZoom = false;
ctrl.enableTilt = ctrl.enableLook = false;
const canvas = viewer.canvas;
canvas.setAttribute("tabindex", "0");
let looking = false, startPos, mousePos;
const handler = new ScreenSpaceEventHandler(canvas);
handler.setInputAction((m) => { looking = true; startPos = mousePos = Cartesian3.clone(m.position); }, ScreenSpaceEventType.LEFT_DOWN);
handler.setInputAction((m) => { mousePos = m.endPosition; }, ScreenSpaceEventType.MOUSE_MOVE);
handler.setInputAction(() => { looking = false; }, ScreenSpaceEventType.LEFT_UP);
const flags = {};
document.(, { flags[e.] = ; });
.(, { flags[e.] = ; });
viewer...( {
cam = viewer.;
(looking) {
cam.((mousePos. - startPos.) / canvas. * );
cam.(-(mousePos. - startPos.) / canvas. * );
}
spd = viewer....(cam.). / ;
(flags.) cam.(spd); (flags.) cam.(spd);
(flags.) cam.(spd); (flags.) cam.(spd);
(flags.) cam.(spd); (flags.) cam.(spd);
});
Camera Events
const off = viewer.camera.moveStart.addEventListener(() => console.log("moving"));
viewer.camera.moveEnd.addEventListener(() => console.log("stopped"));
viewer.camera.percentageChanged = 0.1;
viewer.camera.changed.addEventListener((pct) => console.log(`Changed ${(pct*100).toFixed(1)}%`));
pickEllipsoid -- Screen to Globe
import { Cartesian2 } from "cesium";
const center = new Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2);
const worldPos = viewer.camera.pickEllipsoid(center);
if (worldPos) {
const carto = viewer.scene.globe.ellipsoid.cartesianToCartographic(worldPos);
console.log(Cesium.Math.toDegrees(carto.longitude), Cesium.Math.toDegrees(carto.latitude));
}
Entity Tracking
viewer.trackedEntity = viewer.entities.getById("vehicle");
import { Camera, HeadingPitchRange, Math as CesiumMath } from "cesium";
Camera.DEFAULT_OFFSET = new HeadingPitchRange(
CesiumMath.toRadians(90.0), CesiumMath.toRadians(-25.0), 500.0,
);
viewer.trackedEntity = undefined;
Debug: viewer.scene.primitives.add(new Cesium.DebugCameraPrimitive({ camera: viewer.camera, color: Cesium.Color.YELLOW, updateOnChange: true }));
Performance Tips
- Prefer
setView over flyTo with duration: 0 -- avoids tween overhead.
- Avoid reading
heading/pitch/roll every frame -- each computes an
ENU transform. Cache or use direction/up vectors.
- Throttle
changed events -- raise percentageChanged (e.g., 0.5).
- Always release
lookAt locks -- lookAtTransform(Matrix4.IDENTITY).
- Set
maximumHeight for short flights -- prevents zooming to space.
- Scale movement to altitude -- divide camera height for natural speed.
- Re-enable collision after underground views --
enableCollisionDetection = true.
- Use 600 m+ altitude for tour stops -- avoids blurry tiles on successive flights.
Common Patterns Quick Reference
| Task | Method | Key detail |
|---|
| Jump to a city | viewBoundingSphere | Treat city coordinates as the target; use 2,000-5,000 m desired height and pitch -50 |
| Animate to a landmark | flyToBoundingSphere | Target stays centered; use 1,000-2,000 m desired height and pitch -30 to -40 |
| City skyline / panoramic | setView or flyTo | 800-1,500 m, pitch -10 to -20. Position camera across river/bay, face the city. Load OSM Buildings. |
| Overhead / map view | setView or flyTo | pitch -(Math.PI/2 - 0.0001), altitude matches feature size |
| Canyon / cliff rim | setView or flyTo | 50-300 m above rim, pitch -15 to -25 for depth |
| Lock on a target | lookAt | Must release with lookAtTransform(Matrix4.IDENTITY) |
| Camera tour (multi-stop) | flyTo chain | Use complete callback, keep altitude 600 m+ |
| Ground-level / street view | setView | Requires 3D Tiles (OSM Buildings or Google Photorealistic). Without them, only sky and flat ground visible. |
| Constrain user nav | screenSpaceCameraController | Set min/max zoom, tilt angle; also call setView for initial position |
See Also
- cesiumjs-spatial-math -- Cartesian3, Cartographic, Matrix4, Transforms, coordinate conversions
- cesiumjs-interaction -- ScreenSpaceEventHandler, Scene.pick, mouse/touch events
- cesiumjs-entities -- Entity, trackedEntity, EntityCollection, data sources