Skip to content

Camera

player.camera gives the server full control of a player's camera: where it is, where it points (at a spot or thing, or by pan and tilt), roll, field of view and focus (depth of field). Use it for cutscenes, intros, spectating and showing places.

  • Shots: one camera position, cut to or glided to.
  • Sequences: shots, holds and paths played in order.
  • Paths: flying through a list of points on a smooth curve.
  • Freecam: the player flies the camera themselves.

Moves run on the player's game, frame by frame, so they're smooth whatever their ping, and the server hears when each step starts and ends.

Client scripts have the same API, as sagl.camera.

await player.camera.set({ position: { x: 1500, y: -1700, z: 60 }, lookAt: { x: 1480, y: -1750, z: 13 } });
await player.camera.set({ position: { x: 1450, y: -1700, z: 40 }, lookAt: { vehicle: car }, fov: 40, roll: 5, ms: 5000 });
player.camera.reset();

Shots

set(shot: CameraShot): Promise<boolean>

Cuts or glides to one shot. Resolves true when the camera gets there, as their game shows it, or false if something replaced it first. The camera then stays there, still following anything it looks at, until the next shot or reset().

Option Type Description
position Vector3 | { player \| ped \| vehicle, offset?, relative? } Where it is. A spot, or riding along with a player, ped or vehicle at offset from its middle. With relative: true the offset turns with it (a chase cam or bonnet cam); without, it keeps its world direction (a helicopter shot following a car).
lookAt Vector3 | { player \| ped \| vehicle, offset? } What it looks at: a spot, or a thing it keeps looking at as it moves.
rotation { pan, tilt? } Instead of lookAt: pan in degrees from north, anticlockwise (as headings), and tilt up from level. Riding along relative, pan is from the thing's heading.
roll number Degrees. Positive turns the camera clockwise, so the picture tilts anticlockwise.
fov number Field of view, 5 to 120 degrees. The game's is 70; lower zooms in.
focus number | Vector3 | { player \| ped \| vehicle, offset? } | "auto" | null Depth of field: sharp at this many metres from the camera, at a spot, or on a player, ped or vehicle (at offset from its middle; kept in focus as it and the camera move), or "auto" (the nearest thing in the middle of the screen, like a camera's autofocus). Nearer and further things blur. null turns it off. See Focus.
aperture number How shallow the focus is, as a lens's f-number: lower blurs more (1.4 very shallow, 4 the default, 16 hardly at all).
ms number Glides here from where the camera is, over this long. Default 0: a cut.
ease "linear" | "in" | "out" | "inOut" How it speeds up and slows down. Default "inOut". true and false mean "inOut" and "linear".
widescreen boolean Cinematic: black bars top and bottom, subtitles in the bottom one, and every HUD put away until reset().

Leave out lookAt and rotation to keep looking the way it was, and roll, fov, focus and aperture to keep them as they were. Everything that changes glides together: position, direction (turning the shortest way), roll, field of view and focus (a focus pull, when it moves from one distance to another).

If something the camera follows isn't around on the player's game (too far away, or gone), the camera stays where it last saw it.

// A chase cam behind and above a car, then a slow zoom on its driver.
await player.camera.set({ position: { vehicle: car, offset: { x: 0, y: -7, z: 2.5 }, relative: true }, lookAt: { vehicle: car } });
await player.camera.set({ position: { x: 2490, y: -1680, z: 16 }, lookAt: { player: driver, offset: { x: 0, y: 0, z: 0.6 } }, fov: 20, ms: 4000 });

Sequences

play(steps: CameraStep[], options?: { loop?: boolean; widescreen?: boolean }): CameraSequence

Plays steps in order: shots (as set), holds and paths. The whole sequence goes to the player's game at once, so it plays exactly as written. At most 32 steps, and 256 path points in all.

const intro = player.camera.play([
  { position: a, lookAt: { vehicle: car }, fov: 60 },                 // cut
  { position: b, lookAt: { vehicle: car }, fov: 35, ms: 4000 },       // glide
  { hold: 2000 },                                                     // stay 2 s
  { path: [p1, p2, p3, p4], ms: 10000, bank: true },                  // fly through
  { position: c, rotation: { pan: 90, tilt: -10 }, roll: 15, ms: 1500, ease: "out" },
], { widescreen: true });

intro.onStep((i) => { if (i === 3) player.subtitle("Welcome to Grove Street", 3000); });
if (await intro.done) player.camera.reset();

CameraSequence:

Member Description
done Resolves true when the last step finishes, false if it was stopped, replaced, reset, or the player left first.
onStep(fn) Calls fn(index) as each step starts on their game. Returns a function that removes it.
stop() Stops it. The camera stays where it is.

With loop: true it goes back to the first step after the last, until stopped. done then only resolves (false) when it's stopped.

A hold, { hold: ms }, stays where the step before left the camera, still following anything it looked at.

Paths

{ path: [p1, p2, p3], ms: 8000 }
{ path: [p1, p2, p3], speed: 15, lookAt: { player }, smoothing: 0.5 }

The camera flies through every point. It starts at the first point (a cut there, if the camera isn't already there) and moves evenly along the curve, so it doesn't rush between far-apart points.

Option Type Description
path { x, y, z, pan?, tilt?, roll?, fov? }[] The points, at least 2. Points can carry their own angles, roll and field of view, which are blended from point to point.
ms number How long the whole path takes.
speed number Metres a second, when ms isn't given. Default 10.
smoothing number 0 flies straight lines between points; 1 (default) a smooth curve through every point. The curve doesn't overshoot or loop at sharp corners.
lookAt "ahead" | a spot or thing "ahead" looks where it's going. A spot or thing keeps looking at it. Left out: the points' own pan and tilt if every point has them, otherwise "ahead".
rotation { pan, tilt? } Fixed angles all along, instead of lookAt.
lookAhead number With "ahead": how many metres along the path it looks. Default 5.
roll number Degrees, all along. Ignored if the points have their own.
bank boolean Rolls into turns like an aircraft, instead of roll.
fov number All along. Ignored if the points have their own.
focus, aperture as shots All along. A thing in focus stays in focus as the camera moves.
ease as shots Speeding up from the start and slowing at the end. Default "inOut"; "linear" keeps one speed throughout.

Freecam

freecam(options?: { speed?: number; widescreen?: boolean }): void

Lets the player fly the camera. The mouse turns it, the movement keys move it, sprint goes up, crouch goes down, jump goes faster, and the mouse wheel zooms. speed is in m/s (default 10). Their ped stays where it is, without controls, until reset() (or another shot) ends it.

player.camera.position and front follow the freecam, as the player's sync reports them, so a spectator mode can see where they're looking.

Building shots in game

Freeroam has a camera mode for taking pictures and writing shots and paths (examples/freeroam/client-files/scripts/camera.js). Type /camera: the HUD and chat go, and the keys show in the bottom right. Fly with the mouse and movement keys, and:

Key Does
Page Up / Down (held) Rise and sink.
Alt (held) Everything slower: moving, looking, zoom, roll, focus.
Z / X (held) Zoom in and out.
Q / E (held) Roll.
V Depth of field on (autofocus) and off.
R / F (held) Focus nearer and further by hand.
[ / ] (held) More and less blur.
Home Level, and zoom back out.
F8 Saves a picture to the player's Pictures\SAGL folder.
F9 Saves the shot: position, angles and lens, as code.
P / K / L Drop a path point, take the last back, save the path.
F1 How it works.
Backspace Leaves camera mode.

Shots and paths are saved as code, ready to paste into set or play, to camera-shots.txt in freeroam's folder. The keys can be changed in Settings, Controls.


Other members

Member Description
stop() Stops whatever's playing (or the freecam). The camera stays where it is.
reset() Back behind them, as normal. player.respawn() does it too.
front Where their camera looks (a unit vector), as their last sync. null before it, and for passengers. As SA-MP's GetPlayerCameraFrontVector.
position Where their camera is, as their last sync. null as front. As SA-MP's GetPlayerCameraPos.

While the server holds the camera

The player's ped stays where it is, and their controls work as before. Freeze them with player.frozen if needed. Aiming isn't affected: the game aims from the ped, not from this camera.

When the camera is more than 150 m from the player, their game loads the world around the camera instead of around them: buildings in full detail, collision, traffic and peds. Meanwhile the player (or their vehicle) is held still where they are, so they can't fall through ground that's no longer loaded. This ends once the camera comes back within 100 m. A cut to somewhere new takes a few seconds to load in full: start with a hold, or cover it with a fade.


Focus

await player.camera.set({ position: a, lookAt: { vehicle: car }, fov: 35, focus: { vehicle: car }, aperture: 1.8 });
await player.camera.set({ position: a, lookAt: { player: driver }, focus: { player: driver, offset: { x: 0, y: 0, z: 0.6 } }, ms: 1500 });  // pull focus to the driver's head
await player.camera.set({ position: a, lookAt: sign, focus: sign });                                        // a spot
player.camera.set({ position: a, lookAt: { vehicle: car }, focus: null });                                  // all sharp again

Focus gives the shot depth of field: what's at the focus distance is sharp, and things nearer and further blur more the further they are from it, as with a real lens. aperture sets how much: at 1.8 only a narrow band is sharp, at 8 most of the picture is. Focus glides with the rest of a move, so changing it over ms pulls focus from one thing to another, and turning it on or off fades the blur in or out.

Shooting through glass: a car's windscreen counts as the nearest thing for focus, so people seen through it blur if you focus on them. Focus on the glass instead (a spot on the windscreen, such as car.offset({ ahead: 0.9, up: 0.5 })), and they come out sharp. A ped in a vehicle is also where the vehicle is, so { ped } focuses on the vehicle's middle.

It works by reading the depth of the 3D picture, which SAGL makes readable on nearly all graphics cards (and under Wine and DXVK). On the rare card that can't, shots simply stay sharp. The HUD, chat and menus are never blurred.

Screen effects can use the same depth: see depthAt and the built-in dof effect, which gives depth of field without moving the camera.