Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit3bb1da2647

README.md

camera_rig

Shared internals for the camera rigs — the parts every follow camera needs before it gets to have a personality.

The built-in rigs (chase, mount, cinematic, birds_eye, third_person_follow, orbital_follow and the rest) resolve their follow target the same way, read where it is drawn the same way, ease toward their framing measured from the subject the same way, turn a look-around the same way, avoid geometry the same way, and write their pose the same way. Only the placement of the camera relative to the subject differs, so everything else lives here and the rigs stay small enough to read in one screen.

local CameraRig = require("controller.camera_rig")
local follow = CameraRig.newTrail()

function update(dt)
    local target = CameraRig.resolveFollow(public.entity.id, public.follow)
    if target == nil then return end

    local tx, ty, tz = CameraRig.drawnPosition(target)
    if tx == nil then return end
    local px, py, pz = tx, ty + public.height, tz

    local h, radius = CameraRig.ringAt(public.rings, public.orbitT)
    local cx = px + math.sin(public.yaw) * radius
    local cy = py + h
    local cz = pz + math.cos(public.yaw) * radius

    cx, cy, cz = CameraRig.pullIn(px, py, pz, cx, cy, cz, 0.3,
        CameraRig.pullInExclude(target, public.cameraIgnore))

    cx, cy, cz = CameraRig.trail(follow, tx, ty, tz, cx, cy, cz,
        public.followTau, public.followTau, public.followTau, dt)
    CameraRig.applyPose(public.entity.id, cx, cy, cz, px, py, pz)
end

Exports

  • CameraRig.damp(current, goal, tau, dt) -> number — exponential convergence toward goal. tau is the time constant in seconds: the smaller it is the tighter the camera tracks, and 0 snaps. Frame-rate independent, so a hitched 100 ms frame and six clean 16 ms frames land in the same place, and no frame length overshoots the goal. Damp each component separately.
  • CameraRig.drawnPosition(id), CameraRig.drawnForward(id), CameraRig.drawnPose(id) -> (px, py, pz, qx, qy, qz, qw): where a subject is drawn this frame, which way it faces there, and its whole drawn pose. A body the simulation moves is drawn blended between physics ticks; a rig placed from these holds it steady on screen at any frame rate. drawnPose is what a rig riding the body (a seat, a turret) places itself in the body's frame from.
  • CameraRig.newTrail() and CameraRig.trail(state, subjectX, subjectY, subjectZ, goalX, goalY, goalZ, tauX, tauY, tauZ, dt) -> (x, y, z): ease the camera toward its goal measured as an OFFSET from the subject. The subject's own motion carries the camera at any speed, so a camera framed 6 m behind stays 6 m behind at 5 m/s and at 200 m/s, and the time constants soften only a change of the framing. Damping the camera's world position instead leaves it speed x tau behind a moving subject.
  • CameraRig.tauOf(remaining) -> number: the time constant of a per-second blend field (the share of a gap left after one second), for damp and trail.
  • CameraRig.newLook() and CameraRig.lookAround(state, dx, dy, limits, dt) -> (yaw, pitch): a look-around turned by the look control within limits ({ sensitivity, maxYaw, minPitch, maxPitch, returnDelay, returnTime, invertY? }, radians and seconds) that comes back to rest returnDelay seconds after the input stops. Yaw is positive right, pitch positive up.
  • CameraRig.newPullIn() and CameraRig.pullInEased(state, pivotX, pivotY, pivotZ, camX, camY, camZ, radius, exclude, outTime, dt) -> (x, y, z): pullIn that comes in on the frame a collider gets in the way, so no frame is drawn from inside it, and eases back out over outTime once the way is clear.
  • CameraRig.ringAt(rings, t) -> (number, number) — height and radius of the orbit at t along the three-ring spline, where rings is { bottom = { height, radius }, center = ..., top = ... }. t runs 0 at the bottom ring to 1 at the top, passing exactly through the centre ring at 0.5; adjacent rings are blended with a smoothstep so the seam has no visible kink. Out-of-range t clamps rather than extrapolating.
  • CameraRig.resolveFollow(selfId, followField) -> string? — the entity this rig follows. Takes the rig's own follow field (an entity id or an entity reference) when it is set, and otherwise falls back to the Camera.follow slot, which is the standard place every camera behavior reads its target from — that is what makes "swap the behavior, keep the follow target" work. Returns nil when nothing is set AND when the resolved id has no live entity, which is the normal state for the first frames of play mode while the avatar is still spawning.
  • CameraRig.pullIn(pivotX, pivotY, pivotZ, camX, camY, camZ, radius, exclude?) -> (number, number, number): the camera position moved in along the pivot-to-camera line until no collider is between the two. A sphere of radius is swept from the pivot, and every collider in the world stops it, whether or not anything draws it: an invisible walk bound and a trigger volume pull the camera in exactly as a wall does. radius is also the distance kept off the surface the camera lands on; 0 turns the pull-in off and returns the input untouched. exclude is an entity id, or a list of them, whose colliders the sweep passes through: the subject the camera is looking at, plus anything the camera should see through.
  • CameraRig.pullInExclude(subjectId, ignore) -> { string }: the exclude list a rig hands pullIn: the subject first, then every entity in the rig's cameraIgnore list (ids or entity proxies). orbital_follow and third_person_follow both build theirs with it from their cameraIgnore field. A rig written on this module gives its authors the same control by declaring a cameraIgnore list of its own (Field.list(Field.entityRef(nil, Sync), Sync)) and passing it here.
  • CameraRig.applyPose(selfId, camX, camY, camZ, lookX, lookY, lookZ) — place the camera and aim it at a point. Both the pose and the aim point are in the camera entity's own transform frame: parent-relative when the camera is parented to its subject, world space when it is standalone. The write goes to the local transform, so transform propagation composes the world matrix from parent_world * local instead of being fought by a world-space write.