Builtin Library
README.md
capture
Render a screenshot of the scene from anywhere — an explicit position, a named camera station around an entity, a specific camera, or a mirror of the active viewport — and a grid of them in one image; or read back the frame the engine actually presented, exactly as the player saw it.
The picture of the machine's own camera (a webcam, a phone camera) is cameraInput, covered by the topics/camera-input guide; this toolbox photographs the scene.
Every source but presented and window (see The presented frame) spawns an ephemeral offscreen camera, triggers one render, and returns the picture as an image value — returned from a tool, a probe or execute, it reaches you as a picture, with what the capture has to say about it (its size, the frame it is of, the cells of a grid) beside it. A PNG is written as well when you pass a path. The convenience wrappers render from a world-space position (with optional look-at), frame a target entity, mirror the active viewport camera including its layers, or render a specific camera by name from its exact authored pose and settings (fromCamera, with optional per-capture overrides that never touch the camera); each waits for the render and the GPU readback, cleans up its camera, and returns only once the picture is read back. The low-level oneshot spawns the camera, triggers one render, and returns the handles at once without waiting, leaving the readback and cleanup to the caller; the capture MCP tool is built on it. A cleanup tool clears the capture output directory.
A frame drawn while pipelines build
A shader that was just written, or a module many shaders include, has its
pipelines built off the frame thread, and a frame drawn before they land draws
the magenta placeholder or nothing where they would draw. A capture renders
on the frame it asks for; when that frame was drawn around builds, it waits
for the renderer's builds in flight to land and takes the picture again, for
up to 20 seconds of wall clock from the moment it began. Once that runs out,
it answers with the frame it drew: the cell states the builds it was drawn
without (pipelinesPending) and the cell and the result carry a note
naming them, so a picture taken mid-rebuild is told apart from one of the
settled scene. renderer.pipelineCache().buildsPending says when the builds
have landed. Code that keeps a capture as the picture of its subject reads
buildsDrawnWithout(result) from the toolbox's shared module, which names the
builds such a picture was drawn without (nil for a settled one); an asset's
preview keeps the picture it has when the render for a new one is named there.
Where a shot lands
A capture given a path writes the PNG there and says where those bytes
stand, beside the path it wrote: durable is true when the PNG is on the path
it names and the next session reads it back, and false when the play shadow
took it (live in this session and discarded on a guarded play-exit unless kept,
with survivesRestart saying whether a restart keeps it and playShadow
naming the routes that keep it) or when the path is in /source/tmp/ scratch
(gone when the engine stops or restarts, with a warning opening SCRATCH).
Every capture carries it on the picture it returns as durability. Shooting
gameplay means shooting in play, which is exactly when a /source destination
is the shadow's, so this is the field that says whether a shot survives leaving
play — vfs.promotePlayShadow(path) keeps it with play still running.
The presented frame
source = "presented" reads back the frame the engine presented to the
player instead of rendering one. Nothing is drawn for it: it is the frame the
viewport drew, copied out as it was finished, after the post chain, the
upscale, the display transform, the content UI and every post-scene pass, at
the size it was composed at. So it is the one capture whose temporal
antialiasing, motion blur, camera motion, auto exposure and render scale are
the real ones, and the source to verify any of them with. source = "window"
is the whole presented surface (the window, the browser canvas or the
headless framebuffer) with everything painted on the window: the editor's
chrome and every window-level UI pass. Where no Scene panel shows the
viewport, the two are the same image: the scene fills the window and the
editor's chrome is painted over it, and each such frame says so in its
note (a cell of a presented capture, a frame renderer.presentedFrames
returns) for as long as that chrome is on the window. Show the
Scene panel (the editor toolbox's layout tool) to read the game view alone.
frames = n (1-32) takes n presented frames, one every interval frames
(default 1: consecutive), at real gameplay speed, and lays them out as a grid
in the order they were presented. Each cell states what its frame was drawn
with: frame (the renderer's count, so consecutive frames carry consecutive
numbers), rasterWidth / rasterHeight (the size the scene rasterized at),
compositeWidth / compositeHeight, renderScale and scaleMode, jitter
(the projection's sub-pixel offset in raster pixels), exposure (what the
display transform multiplied the scene by) and historyReset (the viewport
camera's first frame, a cut, or a hand-over to another camera). A single frame
comes back at the size it was presented at; width / height resample it, or
size the sheet a grid is laid into.
A presented frame is drawn through the camera, layers, lens and chain that
drew the screen, so the options that aim, light or shape a rendered frame
(camera, entity, position, viewpoint, fov, pass, renderLayers,
postProcessing, environment, mode = "collage" and the collage axes) are
refused for it; aim the camera holding the viewport instead, or take an
offscreen capture. A source that renders refuses frames and interval the
same way, since they count presented frames. deterministic and clearAir
hold the clock and the air for the whole engine, so they reach the presented
frame too.
A diagnostic buffer of the viewport itself, under its real camera motion and
history, is read the same way: put the camera holding the viewport on that
pass (the camera toolbox's set with debugPass = "motion_vectors") and take
source = "presented". The viewport's velocity buffer is the frame's travel
against the frame before it, so a turning camera reads in it, where an
offscreen pass = "motion_vectors" holds the objects' own motion alone.
The motion_vectors picture is a visualization: each channel is the square
root of 100 times the stored displacement's size on its axis (R horizontal, G
vertical, B the length), the sign is dropped, and R reads 255 from
width / 200 px of horizontal travel, G from height / 200 px of vertical
travel (raster pixels). The rendering guide's part on motion
(topics/rendering-motion), "What the motion_vectors picture encodes", states it in full.
The same frames, with their pixels as TextureCpuHandles, come from
renderer.presentedFrames({ scope = "viewport" | "window", frames, interval }),
and velocity = true adds the viewport's velocity buffer from each frame as
numbers (frame.velocity, a VelocityField): each pixel's travel in pixels
per frame, x right and y down, signed and unclamped, read with
field:at(x, y), field:row(y) and field:region(rect).
The feed
Every capture publishes the frame it made to @builtin::tools.capture.feed,
so a viewer can show the newest one without taking a shot of its own.
Feed.latest() is that frame: a src an image widget draws (the render
target's guid while the engine still holds it, the written path when the
capture produced a file), with the size, the source and the pass beside it.
Feed.watch(fn) calls fn as each new frame arrives. The editor's Capture
panel is built on it, and the watch layout puts that panel under the scene.
A viewer calls Feed.shown() to say it is drawing frames from the feed. From
then on the feed holds the newest capture's render target so the picture stays
up after the capture that made it is finished with it, and lets it go when a
later frame replaces it; Feed.shown(false) stops. With nobody drawing, a
capture's target is released the moment its capture is done, and nothing is
held.
Viewpoints
A viewpoint names where the camera stands and which way is up in the image: front, back, left, right, top, bottom, iso. It replaces reverse-engineering a {yaw, pitch} pair, and names the seven stations that answer "is this built correctly".
basis says which axes the name is measured against, and it is required alongside a viewpoint when a subject is being framed:
"local"— the subject's own axes.frontis the side it faces however it is turned, and the frame is fitted to its own extents rather than to the world-axis box around them."world"— the world axes.frontis whatever faces world +Z.
For a crate turned 40 degrees those are different pictures, so the call says which one it wants. Framing a world position rather than an entity, both values mean the world axes and either is accepted.
Passes
pass names what the frame holds, by name. "final" is the lit picture; a built-in diagnostic pass (albedo, normal, depth, motion_vectors, …) replaces the shaded image with the buffer behind it; and any capture view a render feature has published (renderer.captureView.list() enumerates them) is selectable by its own name. A name that is neither is refused against the list of what is registered.
The name carries two decisions: which channel the camera renders, and which render layers that channel is legible under. A diagnostic buffer is read flat, so the sky behind it and the post-process chain over it come off; a view that draws its own geometry declares the layers it needs and is rendered under them. Stating renderLayers answers that second question yourself, for the one call.
passes spreads the same vocabulary across the cells of a collage, and each cell reports the layers it was read under.
Projection
projection = "orthographic" covers a fixed world-space height at every distance. Parallel edges stay parallel and two equal-size objects at different depths cover equal pixels, which is what makes it the projection to check a shape or compare sizes under — a perspective frame's convergence hides both. orthoHeight sets that height; omit it and it is fitted to what is being framed.
Any camera can be orthographic, not only a capture: it is a field on the Camera component. A capture through a camera the scene holds (fromCamera, viewport, and source = "camera", "screen" or the scene camera) takes that camera's projection and orthoHeight with the rest of its lens, and its lighting environment with an isolated camera's background and ambient; a projection or orthoHeight stated for the call stands over the camera's.
Which entity a capture framed
A capture names an entity, or a camera to render from, by its id or by its name.
A name exactly one entity carries means that entity. A name several entities
carry means none of them on its own, so the capture is refused, and the refusal
says how many entities carry it and names each by name and id; pass the id of
the one you mean. A player is drawn as its body, so a capture of a player
identity (an entity carrying UserIdentity, named user) frames the body its
avatar names. A player prototype is absent from the live scene while it
plays, so its name reaches no entity there: a capture of that name is refused,
and the refusal names each player spawned from the prototype, by its identity
and its body, with their ids.
Every cell of a capture aimed at an entity reports what it framed under
subjects: one { id, name } per subject, carrying
playerIdentity = { id, name } when the picture is the body of the identity
the capture named.
Isolate
Framing a subject fits the frame to it; it does not decide what is drawn in that frame. Everything else standing there is still rendered — and a scene's default player spawn sits at the world origin, which is exactly where a prop usually gets built, so a tight shot of a new prop at the origin can come back showing the player instead.
isolate = true draws the subject without the other geometry. It changes exactly that — lighting, sky and post-process are untouched, so a final frame stays a final frame, and a flat read of the geometry is a pass. Excluded geometry still casts shadows onto the subject and still bounces light into it, the same as any render-layer exclusion. In a setups collage each cell shows the entity its own set-up frames alone, and a set-up can state isolate for its own cell; a set-up aimed from a position or a camera frames no entity and is refused when it is to be isolated.
The tone the frame carries
Every capture reports luma on its cell record: the tone of the pixels it just
encoded, in code values on the 0-255 scale the image is delivered at.
luma = { pixels, min, max, mean, p1, p5, p50, p95, p99, span, spread, crushed, clipped }
span is max - min — the whole range, a single stray pixel included. spread
is p95 - p5 — the range the body of the picture occupies, which is the reading
that answers whether a shot is legible. crushed and clipped are the shares of
the picture sitting at code 0 and at code 255.
A frame's mean cannot answer that question: half a picture at 40 and half at 200
carries the same mean as a flat field at 120, and only one of them has anything
in it. A subject can be modelled, shaded, lit and drawn and still arrive inside a
handful of code values, in which case the image looks like a wash and every other
field here reports success. spread is the number that says so.
The same reading is available anywhere pixels are: cpu:tone() on a
TextureCpuHandle — from renderer.texture.readback(rtHandle) on a capture
handle, from a render-to-texture camera's target, from a CPU canvas — and
cpu:histogram() under it, the per-code-value counts for luminance and for each
of the three channels, which every reading above is a sum over.
cpu:histogram(x, y, w, h) counts one rectangle of the picture alone, so a
reading per region of the frame — how much of each cell is dark, lit or a
given colour — is one call per region.
cpu:flip(reference) holds the picture against another of its size by NVIDIA
FLIP, the comparison a golden is judged by; @builtin::modules.postMetrics
carries it with the golden runner, the pixel metrics (banding, temporal
variance, halo overshoot, ring energy, hue drift) and the scopes, each
reading a presented frame or a capture result as it comes.
The grade an offscreen frame carries
A capture spawns its own camera and renders offscreen, and the post-process
chain runs over that frame the way it runs over the viewport's — for a station,
for an orbit around an entity, for a named camera, for a render-to-texture
camera alike. Inside an effect, engine.view_proj / engine.prev_view_proj /
engine.inv_view_proj are the camera THAT render was drawn from and
engine.resolution is that target's own size, so a grade that reconstructs
world space from scene depth reconstructs against the station and lens the
capture asked for. That makes an offscreen capture an oracle for an authored
look: it photographs a chosen station and lens without taking the on-screen
camera away from whoever else is driving the scene, and moving that camera
somewhere else does not move the frame this one returns.
An offscreen render keeps no view history of its own, so engine.prev_view_proj
there holds that same matrix rather than the frame before it, and a pass taking
camera motion from the two reads none. On the viewport the pair is a frame
apart, so a camera's motion (and the blur and temporal reconstruction built on
it) is read from the presented frame: source = "presented".
A render feature's passes reach an offscreen render the same way, against the
same camera — the renderFeature assetType README carries that half, including
@frame.camera, its own per-target camera input. render(ctx) itself runs once
a frame for the main viewport, so ctx.viewport is the window's size rather
than the captured target's; a feature sizes its scratch with screen = true and
lets the engine track the drawn target.
postProcessing = false takes the chain off the frame a capture returns, and
with it the film a render feature draws — the flare and grain, a short focus's
bokeh, the shutter's smear. It is a Camera property rather than a render
layer, so renderLayers does not reach it. A capture naming a diagnostic pass
drops the chain by default, so the buffer comes back as the value it encodes.
Collage
collage renders a grid of frames into one image. What varies across its cells is what you pass — setups for a cell per whole camera set-up, viewpoints for a cell per view of one subject (or "sides" for all six), passes for a cell per render pass, duration for a cell per sample over time. A collage with no axis is refused rather than quietly sampling time.
setups is the shot list: each entry is written the way a single capture is aimed (source, camera, entity/entities, position, lookAt, rotation, distance, angle, margin, viewpoint, basis, projection, orthoHeight, fov, near, far), plus label to name the shot on the result, and a field an entry leaves out comes from the collage's own options. setups and viewpoints are one axis — both say where the camera stands — so a request names one of them; either crosses with passes into a 2D grid, a row per station and a column per pass.
The shot most of this exists for is one call: six sides of one object, each an orthographic elevation, nothing else in frame.
Authoring the cameras themselves — creating, listing, aiming, framing, and setting their parameters — is the camera toolbox.