Skip to content

Camera

A streamlined, code-driven camera system: one-call rigs for the common camera styles, with camera blending, procedural noise (handheld sway), and impulse shake — and no draw() plumbing. You create a rig once; the engine ticks the camera each frame and pushes the view/projection itself.

import "engine" for Scene, Cam, Ease, Impulse
class Game {
construct new() {}
init() {
_scene = Scene.new()
_player = _scene.addNode("player")
// One line. No camera node, no setViewMatrix, no aspect math.
_cam = Cam.thirdPerson(_scene, _player).distance(6).shoulder(0.6, 1.6).damping(0.25)
}
update(dt) { /* move _player; the camera follows */ }
draw() { _scene.draw() } // that's it
}

Rigs are driven by a per-scene director that solves the active rig at the end of each frame (after your update, so it tracks final transforms) and writes the view/projection. It coexists with the manual Graphics.setViewMatrix path — a scene with no rig behaves exactly as before, and a manual push in draw() overrides the director that frame (last-write-wins). See the Camera Showcase example.

Static factory. Each Cam.<preset> creates a real camera + node in the scene and returns a CameraRig; the first rig created for a scene becomes the live camera automatically.

Returns: CameraRig — An over-the-shoulder follow camera that booms out behind target and faces it.

Parameters:

  • scene (Scene) — The scene to drive.
  • target (Node) — The node the camera follows.

Returns: CameraRig — Hard-locked to head (an eye node) with POV mouse-look.

Returns: CameraRig — Looks down at target from height metres above (strategy/RTS).

Returns: CameraRig — Orbits target on a ring of radius, heading/elevation driven by the mouse.

Returns: CameraRig — A WASD + right-drag mouse-look fly camera (no target). The direct replacement for a hand-rolled free camera.

Makes rig the live camera immediately (an instant cut).

Eases from the live camera to rig over seconds, using an Ease curve (position lerp + rotation slerp + lens crossfade).

Returns: CameraRig or null — The rig currently driving scene’s camera.

A live camera rig. Every setter returns the rig, so calls chain. Which setters are meaningful depends on the rig kind (e.g. radius for orbit, height for top-down, moveSpeed for free-fly).

Stability: the Cam factory, the five named rigs, Ease, Impulse, and blend behaviour are a frozen contract (ADR 0058). The CameraRig setter parameter names/order are experimental for one release while they settle — pin your engine version if you depend on their exact shape.

Shared setters: distance(d) (boom length) · damping(tau) (smoothing time-constant in seconds; 0 = locked) · offset(x, y, z) · fov(radians) · near(n) · far(f) · sensitivity(s) (radians of look per pixel) · noise(amplitude, frequency) (handheld sway; 0 clears) · collider(padding) (pull the camera in front of any wall that would occlude the target, keeping padding metres of clearance — a masked physics raycast; see the collider example) · setActive() · blendTo(seconds, ease).

Per-rig setters: shoulder(x, y) (third-person) · height(h) (top-down) · radius(r) (orbit) · moveSpeed(u) (free-fly).

Easing curves for blends (integer constants): Ease.linear · Ease.easeIn · Ease.easeOut · Ease.easeInOut (smoothstep) · Ease.cut (instant).

Shakes the scene’s live camera with a decaying, distance-attenuated impulse originating at the world point (x, y, z) — for hits and explosions. Concurrent impulses sum.

onPlayerHit(dmg, hx, hy, hz) { Impulse.emit(scene, 0.4 + dmg * 0.05, hx, hy, hz) }

Local co-op split-screen. SplitScreen.new(scene, players) gives each player its own third-person camera in its own screen region; the renderer draws the scene once per viewport with a divider between them. With dynamic merge/split the views fuse into one group-framed camera when the players cluster and split back apart when they spread — see the split-screen example.

import "engine" for Scene, SplitScreen, SplitLayout
var split = SplitScreen.new(scene, [p1, p2, p3, p4]) // players = List of Node
split.layout(SplitLayout.grid) // auto / horizontal / vertical / grid
split.distance(7)
split.height(2)
split.dynamic(true) // merge/split by proximity
split.mergeDistance(5) // spread below this → merge to one view
split.splitDistance(11) // spread above this → split apart (hysteresis)
split.transitionDuration(0.6) // animate the merge/split over 0.6s (0 = instant)

Returns: SplitScreen. players is a List of Node — one third-person camera is created per player. The first SplitScreen for a scene takes over rendering (the single-camera path is suspended while it is active).

  • layout(mode) — a SplitLayout: how the regions are arranged.
  • distance(d) · height(h) — boom length / eye height applied to every player camera.
  • dynamic(on)Bool; enable proximity-based merge/split.
  • mergeDistance(d) / splitDistance(d) — the hysteresis band: players within mergeDistance merge to one group-framed view; spreading past splitDistance splits them apart.
  • transitionDuration(s) — seconds the merge↔split blend takes. The cells hold in place while each camera eases from its player pose to the shared group pose and the dividers fade, then collapse to one view. 0 is the old instant hard-switch. Default 0.4.
  • compositeMode(on)Bool; force the RTT composite path (each viewport renders to an offscreen target, then composites) instead of the axis-aligned sub-rect path. Required for overlap / picture-in-picture and enabled automatically by SplitLayout.angled. The composited disjoint split is pixel-equivalent to the fast path, so turning it on is always safe.
  • peel(on)Bool; the animated rect-peel merge. While a merge is in flight, slide each viewport’s rect from its split cell toward fullscreen (z-stacked, over the composite path) — the Kronnect “views expand together” look — instead of the default fixed-rect camera-converge blend. See /examples/peel.
  • perPlayerInput(on)Bool (default on); player i reads gamepad i (right stick = look) instead of the shared mouse/keyboard — real split-screen co-op. Falls back to mouse/keyboard for any player whose gamepad is not connected; off routes every player to the shared input.
  • isMergedBool getter; has the target state settled on merged?
  • mergeFractionNum getter, 0..1; the animated blend progress (0 split → 1 merged). Handy for a HUD or to gate logic on the transition.
  • playerCountNum getter.

Split arrangements (integer constants): SplitLayout.auto (by count: 2 → side-by-side, 3/4 → grid) · SplitLayout.horizontal (columns) · SplitLayout.vertical (rows) · SplitLayout.grid (2×2 for 4) · SplitLayout.angled (2-player diagonal split — the divider is perpendicular to the on-screen direction between the players, so it rotates as they move around each other, Kronnect-style; each player renders full-screen and the renderer composites the two half-planes — it auto-enables compositeMode). See /examples/angled-split.

Scope (v1): up to 4 composited viewports; overlays (sprites/text/UI) draw once full-screen. The merge/split is an animated camera-converge blend by default (transitionDuration/mergeFraction, ADR 0060), with the RTT rect-peel (peel) + angled split available (ADR 0061). See ADR 0059–0061.

A first-class composite viewport (ADR 0062) for picture-in-picture / minimap / security-cam — independent of SplitScreen. Each Viewport owns its own camera; the renderer composites it at its rect/z/opacity/border over the RTT composite path. Keep using Cam for the main view and add Viewports on top (the main view becomes the fullscreen base), or build a fullscreen base Viewport yourself.

import "engine" for Scene, Cam, Viewport
var cam = Cam.thirdPerson(scene, player) // the main view (fullscreen base)
var mini = Viewport.new(scene) // a top-down minimap in the top-right corner
mini.rect(0.72, 0.04, 0.24, 0.24) // screen-fraction sub-rect (0..1)
mini.topDown(player, 34) // straight-down camera 34 units up
mini.border(3)
mini.z(1) // above the main view

Returns: Viewport. Renders scene from its own camera into its sub-rect. Goes out of scope → its camera is removed and it stops rendering.

  • rect(x, y, w, h) — screen-fraction sub-rect (0..1); default fullscreen.
  • z(order) — composite order (higher = on top; the main Cam view sits behind).
  • opacity(a)0..1 composite alpha (cross-fade / ghost overlays).
  • border(width) / borderColor(r, g, b) — a frame around the viewport (px; 0 = none).
  • Camera (pick one): follow(target) (third-person) · topDown(target, height) (minimap) · orbit(target, radius) · firstPerson(head) · freeFly().

See the picture-in-picture example.


View and projection. Attach to a Node via setNode(node) so the camera’s position and orientation follow the node. Most games use the rigs above instead — the Cam factory creates and drives a Camera for you — but you can drive a Camera yourself for full manual control.

Returns: Camera — A new camera. Attach it to a node with setNode and set a tag so Scene.findCameraByTag can find it.

Method / propertyReturns / ParametersDescription
getTag()StringTag string (e.g. for findCameraByTag)
setTag(tag)tag (String) — Set the tag
nodeNode or nullNode this camera is attached to
setNode(node)node (Node) — Attach camera to this node

The camera’s view matrix is derived from the node’s world transform. Set the node’s position/rotation to move the camera.

MethodReturnsParametersDescription
getViewMatrix()(matrix)View matrix (world → view space)
getProjectionMatrix(aspect)(matrix)aspect (Num) — width/heightProjection matrix for the given aspect ratio
getFovYRadians()NumVertical FOV in radians
setFovYRadians(r)r (Num)Set vertical FOV (radians)
getNearPlane()NumNear clip plane distance
setNearPlane(n)n (Num)Set near plane
getFarPlane()NumFar clip plane distance
setFarPlane(f)f (Num)Set far plane

Use with Graphics.setViewMatrix and Graphics.setProjectionMatrix when drawing the scene.

var aspect = Window.getWidth() / Window.getHeight()
var view = _camera.getViewMatrix()
var proj = _camera.getProjectionMatrix(aspect)
Graphics.setViewMatrix(view)
Graphics.setProjectionMatrix(proj)
_scene.draw()

worldToScreen(worldX, worldY, worldZ, viewportWidth, viewportHeight)

Section titled “worldToScreen(worldX, worldY, worldZ, viewportWidth, viewportHeight)”

Returns: List of three numbers [screenX, screenY, depth] or null if the point is behind the camera.

Parameters:

  • worldX, worldY, worldZ (Num) — World-space position.
  • viewportWidth, viewportHeight (Num) — Viewport size in pixels.

Convert a world position to screen coordinates. screenX, screenY are in pixel space; depth is the depth buffer value or distance.

screenToWorld(screenX, screenY, depth, viewportWidth, viewportHeight)

Section titled “screenToWorld(screenX, screenY, depth, viewportWidth, viewportHeight)”

Returns: List of three numbers [worldX, worldY, worldZ] — World position at the given screen point and depth.

Parameters:

  • screenX, screenY (Num) — Screen coordinates (e.g. mouse position).
  • depth (Num) — Depth value or distance (e.g. 0 = near plane, 1 = far plane, or linear depth depending on implementation).
  • viewportWidth, viewportHeight (Num) — Viewport size.

Convert a screen point and depth back to world space (e.g. for placing objects at the cursor).