Skip to content

OrbitFlyCamera ​

A WASD + mouse-look fly camera. It extends CameraComponent and, unlike Camera3DControls, can pivot around a focus point instead of always flying free: focusMode: "lock" orbits a fixed point at a scroll-adjusted distance, and focusMode: "elastic" lets WASD/look pilot a free-floating pivot that the camera trails behind. Movement and damping follow wall-clock time (world.time.unscaledDelta), so the camera keeps its speed in slow motion and while the game is paused.

Usage ​

ts
import { Actor, OrbitFlyCamera } from "@jolly-pixel/engine";

const actor = new Actor(world, { name: "Camera" });
actor.addComponent(OrbitFlyCamera, {
  position: { x: 8, y: 12, z: 32 },
  focusMode: "lock"
});

focusMode: "elastic" trails a free-floating pivot instead:

ts
actor.addComponent(OrbitFlyCamera, {
  focusMode: "elastic",
  pivotPosition: { x: 0, y: 2, z: 0 },
  initialTrailDistance: 12
});

Options ​

OrbitFlyCameraOptions extends CameraOptions, so near, projectionMode, orthographicScale, viewport, depth, addAudioListener and postProcessing all work here. Only the fov and far defaults differ.

ts
import { pass } from "three/tsl";

actor.addComponent(OrbitFlyCamera, {
  far: 5000,
  postProcessing: ({ scene, camera }) => {
    return pass(scene, camera);
  }
});
ts
type OrbitFlyCameraFocusMode = "none" | "lock" | "elastic";

interface OrbitFlyCameraOptions extends CameraOptions {
  position?: THREE.Vector3Like;
  pivotPosition?: THREE.Vector3Like;
  initialTrailDistance?: number;
  yaw?: number;
  pitch?: number;
  moveSpeed?: number;
  minMoveSpeed?: number;
  maxMoveSpeed?: number;
  accelerationTime?: number;
  brakingTime?: number;
  responsiveness?: number;
  mouseSensitivity?: number;
  touchpadLookSensitivity?: number;
  touchpadSwipe?: TouchpadSwipeMode; // "look" | "pan"
  invertY?: boolean;
  keyLookSpeed?: number;
  keys?: OrbitFlyKeys;
  maxPitch?: number;
  scrollSpeed?: number;
  speedAdjustStep?: number;
  focusMode?: OrbitFlyCameraFocusMode;
  minPivotDistance?: number;
  maxPivotDistance?: number;
  pivotZoomStep?: number;
  pivotNudgeStep?: number;
  showPivotMarker?: boolean;
  fov?: number;
  far?: number;
}
OptionDefaultDescription
position{ x: 16, y: 20, z: 40 }Starting camera position
pivotPositionposition"elastic" mode's pivot the camera starts trailing behind
initialTrailDistance0"elastic" mode's starting trail distance, in world units
yaw / pitch0 / -0.2Starting orientation, in radians
moveSpeed18Cruise speed in units per second
minMoveSpeed / maxMoveSpeed2 / 240Bounds for the scroll-adjusted moveSpeed
accelerationTime0.15Seconds to ease into a new cruise velocity. Acceleration starts at zero and builds up; 0 starts at full speed
brakingTime0.1Seconds to glide to a stop once the movement keys are released; 0 stops at once
responsiveness18Frame-rate-independent damping rate of the "lock" pivot and distance and the "elastic" trail distance, in 1/s
mouseSensitivity0.003Mouse-look sensitivity
touchpadLookSensitivity0.36Radians turned per wheel notch of a two-finger touchpad swipe; a negative value inverts it
touchpadSwipe"look""look": a two-finger swipe turns the camera. "pan": it moves the camera sideways and up or down, a twentieth of moveSpeed per notch. While a "lock" pivot is engaged, a swipe always orbits
invertYfalseTurns the camera down when the mouse or the fingers move up. The look keys keep their direction
keyLookSpeedπ / 2Radians per second turned while a look key is held
keysOrbitFlyKeys.defaultThe keys held to fly and look around, see Keys
maxPitchπ / 2 - 0.01Maximum look-up/down pitch, in radians
scrollSpeed2.5Trail distance changed per wheel notch in "elastic" mode, in world units
speedAdjustStep0.15Fraction moveSpeed grows per wheel notch in "none" mode, and in "lock" mode while not orbiting
focusMode"none""none": no pivot. "lock": enterOrbitFocus engages a fixed pivot; scroll adjusts distance; exitOrbitFocus or teleport releases it. "elastic": WASD/look pilot a free-floating pivot; scroll trails the camera behind it, reaching 0 (free-fly) at full zoom-in
minPivotDistance / maxPivotDistance1 / 200Bounds for the scroll-adjusted pivot distance in "lock" mode; minPivotDistance must be greater than 0 or the constructor throws a RangeError. maxPivotDistance also doubles as "elastic" mode's max trail distance
pivotZoomStep0.1While orbiting in "lock" mode, each wheel notch multiplies the pivot distance by 1 + pivotZoomStep zooming out and divides it zooming in, so a notch covers the same share of the distance near the pivot as far from it
pivotNudgeStep1Distance nudged per key press while orbiting in "lock" mode
showPivotMarkertrueDraws a marker at the pivot in "lock" and "elastic" modes
fov60Vertical field of view, in degrees
far2000Far clipping plane

By default, movement is WASD or the arrows + Space/Shift (up/down) and look-around is middle-mouse-drag, Alt+left-drag, or I/J/K/L (look up/left/down/right). keys rebinds the keys. The wheel adjusts moveSpeed, or the pivot/trail distance when a focus mode claims it. Ctrl is reserved for other scroll-driven interactions (e.g. brush size), so Ctrl+wheel is ignored.

On a touchpad, a two-finger swipe looks around (orbits while a pivot is locked) and a pinch does what the wheel does. With natural scrolling, moving the fingers right looks right. The camera reads Mouse.scrollSource to tell swipes from wheel notches; set input.mouse.scrollDevice when that guess is wrong for a device.

Keys ​

OrbitFlyKeys lists the physical keys (KeyboardEvent.code) held for each action. The camera reads held keys, so a binding is one key with no modifier; ShiftLeft or ControlLeft alone is a valid binding. While a "lock" pivot is engaged, pressing a movement key nudges the pivot instead.

ts
type OrbitFlyAction =
  | "forward" | "back" | "left" | "right" | "up" | "down"
  | "lookUp" | "lookDown" | "lookLeft" | "lookRight";

class OrbitFlyKeys {
  // The default keys listed above.
  static readonly default: OrbitFlyKeys;

  // Reads every action, dropping the strings that are not key codes.
  static from(
    codes: Readonly<Record<OrbitFlyAction, readonly string[]>>
  ): OrbitFlyKeys;

  // An action left out keeps its default keys; an empty list unbinds it.
  constructor(codes?: Partial<Record<OrbitFlyAction, readonly KeyCode[]>>);

  readonly codes: Readonly<Record<OrbitFlyAction, readonly KeyCode[]>>;

  // Down while any key of the action is held; `.pressed` on the frame the
  // first one goes down.
  held(action: OrbitFlyAction): AliasedKeyInput;
}
ts
import { OrbitFlyCamera, OrbitFlyKeys } from "@jolly-pixel/engine";

const camera = actor.addComponentAndGet(OrbitFlyCamera, {
  keys: new OrbitFlyKeys({
    forward: ["KeyE"],
    back: ["KeyD"],
    left: ["KeyS"],
    right: ["KeyF"]
  })
});

// Takes effect on the next frame.
camera.keys = OrbitFlyKeys.default;

ORBIT_FLY_ACTIONS lists the actions, and ORBIT_FLY_DEFAULT_KEYS holds the default codes.

Runtime properties ​

ts
interface OrbitFlyCamera {
  // The underlying THREE.PerspectiveCamera.
  camera: THREE.PerspectiveCamera;

  // Gates all input handling; false freezes the camera in place.
  enabled: boolean;

  // Change moveSpeed at runtime (clamped to minMoveSpeed/maxMoveSpeed).
  set moveSpeed(value: number);

  // The look options, read and written at runtime.
  mouseSensitivity: number;
  touchpadLookSensitivity: number;
  touchpadSwipe: TouchpadSwipeMode;
  invertY: boolean;

  // The keys held to fly and look around.
  keys: OrbitFlyKeys;

  // Whether a "lock" pivot is engaged, or an "elastic" pivot has a
  // non-zero trail distance.
  get isOrbiting(): boolean;

  // The current pivot position, or null when not orbiting.
  get orbitPivot(): THREE.Vector3Like | null;

  // True while the camera glides or its pivot/trail distance damps.
  get moving(): boolean;

  // Snaps position/orientation, dropping any carried momentum. In "elastic"
  // mode the pivot moves to keep its trail distance ahead of the new pose;
  // in "lock" mode the orbit is released.
  teleport(pose: CameraPose): void;

  // "lock" mode only: engages a fixed pivot, optionally facing `point`.
  // No-op outside "lock" mode, or while already orbiting.
  enterOrbitFocus(point?: THREE.Vector3Like): void;

  // "lock" mode only: releases the pivot.
  exitOrbitFocus(): void;
}

The camera keeps an on-demand host rendering through world.keepAlive() while it is enabled and moving. teleport(), enterOrbitFocus() and exitOrbitFocus() call world.invalidate().

See also ​