Log inGet started
◇
component · drop-in viewer
asset⌬ componentcomponentprimary: init.luau·originates fromworld 07158574-5…

Camera

Manages viewport priority, render-to-texture, and capture. State is stored in the native `Camera` ECS component; the Rust camera system handles render scheduling and render targets.

byzero-proxy @ DESKTOP-DB3UJOJ·posted 2mo ago
What it does

Camera

Manages viewport priority, render-to-texture, and capture. State is stored in the native Camera ECS component; the Rust camera system handles render scheduling and render targets.

Public fields: fov, near, far, priority, textureHandle (the guid of the texture the camera renders into; empty = main viewport), renderLayers (which render layers this camera draws — a space-separated spec of names, e.g. "all", "all !ui", or "default sky"; ui/sky/debug/EditorUI are built-in layers), postProcessing (whether this camera runs the post-process chain), debugChannel, plus the behavior slot below.

Methods: :lookAt(target) (entity id string OR {x, y, z} table), :render(), :capture(), :setTargetTexture(tex?) (a renderer.texture.create handle to render into, or nil for the viewport).

entity(id).component.add("Camera", { fov = 90, priority = 10 })
entity(id).component.get("Camera"):capture()

The scene's play-mode camera is reached as layers.active.camera, a handle that reads and writes the fields above on whichever entity currently carries them. layers.active.camera.entity is the entity ref for that camera and layers.active.camera.entityId its id string, so a script that needs to attach something to the camera — an AudioListener, a child entity — goes through the ref:

local cam = layers.active.camera
cam.fov = 70                                  -- the camera's settings
cam.entity.component.add("AudioListener")     -- the entity carrying them

How the camera moves: behavior and follow

A Camera does not move itself. behavior names a component that does, and setting it attaches that component to this entity. Clearing it detaches whatever was attached.

local cam = entity(id).component.get("Camera")
cam.behavior = asset.ref("@builtin::controller.orbital_follow", "component")
cam.follow = playerBody

follow is the standard slot every shipped behavior reads. Set the follow target on the Camera, not on the behavior, so swapping behaviors keeps it. A behavior that finds its own follow field empty falls back to this one, which is what lets a rig keep tracking the player across a behavior swap.

followResolves answers whether that slot names an entity that is live — true while it names a live one or names nothing at all, false once the target is despawned or the id names no entity. A rig whose target does not resolve holds its last pose, and this is the field that tells it from a rig posed correctly on a subject that has not moved. camera.get carries the same value beside follow, and a write naming an id with no entity behind it draws a warning where it lands.

cam.followResolves                            -- false once the followed entity is gone
tools.use("camera", "get", id).followResolves -- the same answer off the tool

The shipped behaviors live under @builtin::controller.*: orbital_follow, third_person_follow, first_person, free, orbit, chase, isometric, rts, birds_eye, side_scroller, cinematic, menu.

Writing your own

Any component can be a camera behavior. Write one that moves its own entity and attach it the same way:

cam.behavior = asset.ref("MyChaseCam", "component")

follow lives on the Camera, so a behavior gets no property notification of its own when the target changes. Declare onFollowChanged(newFollow, oldFollow) to be told the moment it does — the Camera calls it on the component it attached, which is what lets a rig re-pose on the new subject at once. A behavior that reads Camera.follow on its own schedule declares nothing and is attached the same way.

typed function public:onFollowChanged(newFollow: any, oldFollow: any)
    -- pose this entity against the new target
end

To make it appear in the discovery catalog alongside the shipped ones, declare the cameraBehavior tag in the component's .metadata:

{ "tags": ["cameraBehavior"] }

The tag governs discovery, not attachment. A tagged component is listed by layers.active.camera.behaviors and is what tooling offers when something asks "which camera behaviors exist"; an untagged component attaches just as well and simply stays out of that list. Tag the ones you want other people (and agents) to find.

for name, ref in pairs(layers.active.camera.behaviors) do
    print(name, ref.identity)
end

Texture colour space

textureColorSpace = "display" applies the display transform when rendering into a texture. "linear" writes scene-linear values instead. postProcessing independently controls the effects chain in either mode. Floating-point targets retain values above one; normalized targets clamp to their representable range. The main viewport uses display encoding.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

Camera Component Manages viewport priority, render-to-texture, and capture. State is stored in the native "Camera" ECS component; the Rust camera system handles render scheduling and render targets. This Luau side is the schema, the lifecycle glue, and the public method surface that callers reach via `entity(id).component.get("Camera")`. Local-only by design — the camera entity is NEVER synced. Each peer spawns + owns its own primary camera. `Camera.follow` is a local-id reference (typically `lp.avatar`), never broadcast. Multi-peer cameras would mean N players = N cameras rendering on every client, which is structurally wrong. See `multiplayer_sync::broadcast_entity_spawns`'s "Relay surface scope" comment for the broader transport boundary. Pilot for the canonical component shape every other component must match (see /zero/source/libs/@builtin/docs/core/proxies.guide/guide.md): - Schema in `public = { ... }` with default values for every readable field. - Computed properties via `public.X = computed(fn)` for read-only derived values; `computed` is a global installed by the prelude. - Lifecycle hooks (`awake`, `onPropertyChanged`, `onDestroy`, etc.) are plain `function` declarations. Inside callback bodies, access state through `public.X` — there is no `self` global. - Public methods are colon-form `typed function public:method(args)` with full doc blocks. Inside method bodies `self === public === proxy` via colon-call binding; `self.entityId`, `self.entity`, `self.instanceId`, `self.type`, `self.enabled`, `self.data`, `self.reportError`, `self.clearError`, `self.errors` all resolve through the framework. Never declare any of those names in `public = {}`. Usage: entity.find("cam1").component.add("Camera", { fov = 90, priority = 10 }) entity.find("cam1").component.get("Camera"):capture()

assertProjectionPair(label: string) → void

An orthographic frame is defined by the world-space height it covers, so a camera set orthographic without one has stated no frame to draw. Checked where both fields are final rather than as either field is written.

argtypedescription
labelstring

behaviorTypeFromRef(ref: ?) → void

Extract the component-type name an AssetRef points at. Behavior refs come from `asset.ref(name, "component")` — `.name` is the canonical short type name (`"orbital_follow"`, `"first_person"`, etc.) that `entity(id).component.add(typeName)` accepts.

argtypedescription
ref?

refPath(ref: ?) → void

Resolve the VFS path for an AssetRef so `.metadata` lookups work. Returns the path field directly if present, else the identity, else the name. Used for tag validation.

argtypedescription
ref?

reconcileBehavior(newRef: ?) → void

Detach the previously-attached behavior component (if any) and attach the one pointed at by `newRef`. Any component attaches here. `cameraBehavior` is a DISCOVERY tag: it lists a controller in `layers.active.camera.behaviors` and in `asset.list("component", nil, { fields = { tags = "cameraBehavior" } })`, so tooling can offer the ones meant to be found. Constraining the slot to tagged refs is expressible — `Field.taggedRef("cameraBehavior", …)` does exactly that — and is deliberately not used: a camera behavior is any component that moves its own entity, and content should be able to write one without first asking permission from a tag.

argtypedescription
newRef?

declaresMethod(comp: ?, name: string) → boolean

Whether a component ref carries a method under `name`. A component ref answers a name it declares and raises on one it does not, which is the right answer for a field the caller meant to read and the wrong question to ask about an OPTIONAL hook: the raise is logged at error level as it is thrown, so a guarded read still costs a line that names the read rather than the choice behind it. Iteration answers it quietly instead — a ref's `__iter` walks the surface it declares, fields and methods alike, and yields what it carries.

argtypedescription
comp?
namestring

entityRefId(value: ?) → string

The entity id an `entityRef` value names, or nil when it names none. The value arrives as a ref table on a proxy read and as the bare id a caller assigned, so both spellings resolve here.

argtypedescription
value?

reportUnresolvedFollow(value: ?) → void

Report a follow target that names no live entity, at the write that named it. A behavior poses the camera from the entity this slot names, so a slot naming none leaves the rig standing still while reading back exactly like one naming a live entity. Naming it here is the moment the camera holds both the id and the answer to whether it resolves.

argtypedescription
value?

includeMask( ) → number

Plain `function` form — these are engine-dispatched callbacks invoked at fixed lifecycle points (awake on attach, onPropertyChanged on every schema-field write, onDestroy on remove). The typed-function wrapper is incompatible with how the engine looks them up and calls them, so lifecycle hooks stay untyped. Public methods on `public` ARE typed — see below. Resolve the space-separated render-layer include spec to a u32 mask via the per-world interner. Tokens: `all` seeds every layer; `name` adds a layer's bit; `!name` clears it. So "all" (default) = everything, "all !debug" = everything but debug, "default enemy" = only those. Empty → every layer.

cameraValue( ) → void

Build the typed ecs.Camera value. `textureHandle` is the guid of the texture the camera renders into (empty = main viewport).

stateEnvironment(identity: string) → void

Hand the renderer the environment this camera states, so a surface in its frame has it to reflect. A camera naming none states nothing, and one naming a cube the renderer already holds costs nothing.

argtypedescription
identitystring

awake( ) → void

onEnable( ) → void

onDisable( ) → void

onPropertyChanged(key: ?, value: ?, oldValue: ?) → void

argtypedescription
key?
value?
oldValue?

onDestroy( ) → void

lookAt(target: string | table) →

Aim this camera at a world position or another entity. Returns whether the camera was rotated, so a target naming an entity the scene does not carry is reported rather than leaving the camera on its previous aim. position table ({x, y, z} array form or {x=, y=, z=} map form). False plus the reason otherwise — `"unresolved"` when the target names no entity, `"no-transform"` when one of the two carries no transform, `"degenerate"` when the camera already sits on the point it was asked to face.

argtypedescription
targetstring | tableAn entity id string, an entity name, an entity proxy, or a

examples

cam:lookAt(playerId)
cam:lookAt("player")
cam:lookAt({0, 1, 0})
cam:lookAt({x = 0, y = 1, z = 0})

render(rtGuid: string?) →

Schedule a single-frame render for this camera. Works whether the component is enabled or disabled. Uses the camera's current transform, fov, near/far, and render layers. Pass a render-target guid to render this frame into THAT texture instead of the camera's configured output — the one-shot-target form the capture path uses to read a camera's exact view back without disturbing where it normally renders. render into this frame; omit to use the camera's configured target. scheduled on. It is filled at once when the camera's ECS row stands, and later — on the frame the row lands — when the camera was spawned and asked to render in one step, so a reader that binds the render to its frame reads the field once the render is in.

argtypedescription
rtGuidstring?Optional render-target guid (from `renderer.texture.create`) to

examples

cam:render()
local request = cam:render(tex.guid)

capture( ) → void

Capture a frame from this camera to its current render target. Alias of render() — kept so the verb matches the agent-facing capture toolbox.

examples

cam:capture()

setTargetTexture(tex: renderer.TextureHandle?) → void

Point the camera at a GPU texture to render into, or back to the main viewport. Create the texture first with `renderer.texture.create({ width, height })` (it owns its own size); the camera only references it — free it with `renderer.destroy(handle)` when done.

argtypedescription
texrenderer.TextureHandle?A `TextureHandle` to render into, or nil for the main viewport.

examples

local tex = renderer.texture.create({ width = 512, height = 512 })
cam:setTargetTexture(tex)   -- render into the texture
cam:setTargetTexture(nil)   -- back to the main viewport

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

10items
·
other · born here
▤file
▲ 0↑ born
▣
module · born here
❒asset
# environment Module Environment / reflection capture — bake the scene into reflection-probe cube slots from world positions, persist them as `faces6` `.texture` assets, set per-probe blend data so surfaces reflect the probes covering them, and capture the sky into its own slot as the fallback under them. Public Luau surface over the `__environment` Internal FFI namespace, auto-injected as `_G.environment` via the prelude. ## Purpose The generic "render the scene into a cubemap from a point" capability the reflection-probe system is built on. Captures are queued for the render system (which owns the live scene); `captureSlotToAsset` additionally yields a few frames while the GPU readback completes. Persisted cubes are `faces6` `.texture` assets (px/nx/py/ny/pz/nz PNGs + a `cube.yaml` sidecar — see `docs/specs/cubemap-textures.md` §4 for the face convention). For probe authoring use the higher-level `reflectionProbe` module; reach for `environment` when you need the raw per-slot primitives. ## Usage ```luau -- Register probe blend data: index i maps to cube slot i. environment.setProbes({ { x = 0, y = 2, z = 0, radius = 12 } }) -- Bake slot 0 from a point (queued, next frame). environment.captureSlot(0, 0, 2, 0) -- Bake + persist to /source/probe_lobby.texture/ (yields; call from a -- task/coroutine/execute context). local path, err = environment.captureSlotToAsset("probe_lobby", 0, 0, 2, 0) -- Restore a persisted cube into a slot WITHOUT re-rendering. environment.loadSlotFromAsset("probe_lobby", 0) -- Capture the sky alone into the fallback slot: a surface no probe covers -- reflects the sky rather than black. environment.captureSky() ``` ## Exports - `environment.setProbes(probes) -> boolean` — set active probes' blend data; array of `{ x, y, z, radius, priority? }`, index i → cube slot i, gathered highest `priority` first - `environment.captureSky(x?, y?, z?) -> boolean` — render the sky alone into the fallback slot and arm it (queued) - `environment.setSkyFallback(active) -> boolean` — arm/disarm the fallback against the sky already captured (arming is refused while the slot holds none) - `environment.captureSlot(slot, x, y, z) -> boolean` — bake the scene into a slot from a point (queued) - `environment.captureSlotToAsset(name, slot, x, y, z, timeoutFrames?) -> (string?, string?)` — bake + persist as a `faces6` `.texture`; yields - `environment.loadSlotFromAsset(name, slot) -> (boolean, string?)` — upload a persisted cube into a slot without re-rendering Back-compat single-global-reflection helpers (slot 0 + one full-coverage probe): - `environment.capture(x, y, z) -> boolean` - `environment.captureToAsset(name, x, y, z) -> (string?, string?)` - `environment.loadFromAsset(name) -> (boolean, string?)`
▲ 0↑ born
▣
module · born here
❒asset
# transform Math helpers for positions, rotations, and directions on transforms. Exposed as the global `Transform` table via `--!global Transform` — no explicit require needed in user code. Functions that take an entity accept either an entity ID string or an entity proxy table from `entity("id")`. ## Exports Look-at and entity-aware helpers: - `Transform.lookAtQuat(fx, fy, fz, tx, ty, tz) -> (qx?, qy?, qz?, qw?)` — quaternion from origin toward target. Nil when degenerate. - `Transform.lookAt(entity, txOrTarget, ty?, tz?) -> (boolean, string?)` — make an entity face a world position or another entity. Both slots read world space: the subject and an entity target are read as `entity(id).position` and the aim is written as `entity(id).rotation`, so a parent under either one still leaves the aim on the point named. Returns whether the rotation was written, and the reason when it was not. - `Transform.distance(x1, y1, z1, x2, y2, z2) -> number` — Euclidean distance between two points. - `Transform.distanceBetween(entityA, entityB) -> number?` — distance between two entities' world positions. Nil when either is unresolvable. - `Transform.direction(fromX, fromY, fromZ, toX, toY, toZ) -> (dx, dy, dz)` — unit direction vector. - `Transform.directionBetween(entityA, entityB) -> (dx, dy, dz)` — unit world-space direction between two entities' world positions. Rotation shapes: A quaternion **constructor** here returns the four components as four separate values, so a caller either names them or braces the call to make one table: ```lua local qx, qy, qz, qw = Transform.quatFromAxisAngle(0, 1, 0, math.rad(90)) entity("cam").localRotation = { Transform.quatFromAxisAngle(0, 1, 0, math.rad(90)) } ``` A rotation-taking **surface** reads that table through `Transform.toQuaternion`, which also takes euler DEGREES — so `{ qx, qy, qz, qw }`, `{ x =, y =, z =, w = }`, `{ pitch, yaw, roll }` and `{ pitch =, yaw =, roll = }` all mean the same thing wherever a rotation is assigned: `entity(id).rotation` / `.localRotation`, `entityOps.spawn`, `entityOps.transform`, and the capture viewpoints. - `Transform.toQuaternion(rotation, label?) -> { qx, qy, qz, qw }` — the shared reading of a rotation a caller wrote. Raises when the value matches no form, naming what arrived; a value that is one of the shapes a quaternion helper returns is named as such along with the packing it goes in as. - `Transform.tryQuaternion(rotation, label?) -> ({ qx, qy, qz, qw } | nil, message?)` — the same reading without raising, for a surface that wants to raise the message at its own caller's line. - `Transform.readVec3(value, label?) -> { x, y, z }` — the same for a vector. - `Transform.snapVec3(v, step) -> { x, y, z }` — quantize a vector to a step grid. Quaternion construction / conversion: - `Transform.quatFromYaw(yaw)`, `Transform.quatFromYawPitch(yaw, pitch)`, `Transform.quatFromAxisAngle(ax, ay, az, angle)` — quaternion constructors. - `Transform.quatIdentity()` — identity quaternion. - `Transform.euler(qx, qy, qz, qw) -> (yaw, pitch, roll)` and the named alias `Transform.quatToEuler`. - `Transform.eulerToQuat(yaw, pitch?, roll?)` — euler-to-quaternion in YXZ order. Lerps and interpolation: - `Transform.lerp(ax, ay, az, bx, by, bz, t) -> (x, y, z)` — vec3 lerp. - `Transform.lerp1(a, b, t) -> number` — scalar lerp. - `Transform.normalizeAngle(a) -> number` — wrap angle into `[-pi, pi]`. - `Transform.lerpAngle(a, b, t) -> number` — shortest-arc angle lerp. - `Transform.slerp(ax, ay, az, aw, bx, by, bz, bw, t) -> (qx, qy, qz, qw)` — quaternion slerp with shortest-path and near-parallel fallback. Quaternion operations: - `Transform.quatMul(...) -> (qx, qy, qz, qw)` — `qa * qb` composition. - `Transform.quatInverse(qx, qy, qz, qw) -> (qx, qy, qz, qw)` — inverse (= conjugate for unit quats). - `Transform.quatRotateVec(qx, qy, qz, qw, vx, vy, vz) -> (x, y, z)` — rotate a vec3 by a quaternion. Pose helpers: - `Transform.orbit(centerX, centerY, centerZ, radius, height, angle) -> (x, y, z, qx, qy, qz, qw)` — orbital pose facing the center. - `Transform.worldToLocal(...)` / `Transform.localToWorld(...)` — pose-space conversions. Nested `Transform.vec.*` namespace (component-wise vec3): - `Transform.vec.add`, `sub`, `scale`, `dot`, `cross`, `length`, `normalize`. Types: - `Vec3 = { x: number, y: number, z: number }` - `EntityRef = string | { entityId: string }` ## Usage ```luau -- Look-at by coordinates or by target entity: Transform.lookAt("cam", 0, 1, 0) -- an entity target resolves to that entity's world position local aimed, why = Transform.lookAt("cam", "box") -- Orbit pose around a point: local x, y, z, qx, qy, qz, qw = Transform.orbit(0, 1, 0, 5, 2, t) entity.find("cam").localPosition = { x, y, z } entity.find("cam").localRotation = { qx, qy, qz, qw } -- Quaternion math: local qx, qy, qz, qw = Transform.quatFromYawPitch(math.pi / 4, 0) local sx, sy, sz, sw = Transform.slerp(0, 0, 0, 1, qx, qy, qz, qw, 0.5) -- Component-wise vec3 helpers: local nx, ny, nz = Transform.vec.normalize(1, 1, 0) ``` ## Notes - The `--!global Transform` directive promotes the module's typed functions onto the runtime universe's globals bucket, so `Transform.*` is available without any per-source `require`. - Entity-aware functions (`lookAt`, `distanceBetween`, `directionBetween`) report a missing entity or a missing transform in their return value rather than raising: `lookAt` answers `false, "unresolved"` / `"no-transform"` / `"incomplete-target"` / `"degenerate"`, `distanceBetween` answers `nil`, and `directionBetween` answers zeros. - Quaternion APIs operate on raw `(qx, qy, qz, qw)` tuples for parity with the entity proxy's `localRotation.get`/`set`. Use `Transform.quatIdentity()` rather than hand-rolling `(0, 0, 0, 1)`. - `Transform.slerp` flips the second quaternion if `dot < 0` to take the shortest path, and falls back to lerp+normalize when the inputs are within `dot > 0.9995` to avoid `1/0` near-parallel issues.
▲ 0↑ born
·
other · born here
▤file
▲ 0↑ born

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported. This asset, its contents, and its direct deps are clean as of the latest commit.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.