# agentSkill
A skill packages what an agent needs to do one job well: the instructions,
the assets and toolboxes the job runs through, and — for jobs with distinct
branches — subskills that go deeper.
Skills advertise themselves. Every registered skill's name and one-line
description rides the agent's tool responses, so an agent discovers what this
world knows how to do without being told to look. Invoking one returns the
instructions plus the exact identities and tool names the job needs.
## Shape
```
<name>.agentSkill/
skill.yaml # the manifest: description, when, checks, dependencies, order
instructions.md # the prose an agent receives on invoke
README.md # what this skill is, for a human reading the tree
.metadata # tags
<sub>.agentSkill/ # optional subskills, reachable as <name>/<sub>
```
## The manifest
```yaml
$schema: zero/agent-skill/v1
description: Making a character a person can actually play.
when: >
Reach for this when the player should look like something else, or when
you are about to build a camera and a body and wire them together by hand.
checks:
- check: Holding a movement key in play moves the body across the ground.
observe: Read its position before and after holding the key; the numbers differ.
- check: The body animates while it moves.
observe: A time-sampled collage from a camera outside the player, while the key is held.
dependencies:
- "@builtin::tools.scene"
- "@builtin::tools.scene.player"
- "@builtin::components.PlayerPrototype"
- "@builtin::docs.core.multiplayer"
order: [spawn-points, camera-rig]
```
`description` is the line the roster carries, and the whole basis on which a
skill is chosen. Name the job the skill makes a reader competent at, in the
outcome they are after — "getting a scene's player and view right", "making
content the engine will load". A reader scanning the roster is matching it
against the job in front of them, so the closer it reads to their own
sentence about that job, the sooner they open it.
`when` is the longer trigger — the situations that should send a reader here,
in the words they would use for their own task.
`kind` says what the skill is to its reader, and `job` is assumed when it is
left out. A `job` is work taken on and finished: opening it holds it, so it
rides the reader's tool responses until it is released, and its `checks` say
when it is done. A `reference` is knowledge for the whole session, such as how
the engine fits together: opening it does not hold it, it is never released,
and it states no `checks`, `stages` or `verdicts`, because it describes no job
whose end they would mark. A check on a reference turns it into a step that one
pass completes, after which its reader stops applying it.
`checks` is what the reader of a job is answerable for at the end, and it leads the
rendered skill, ahead of the procedure. Each entry pairs a **condition that
must hold** with **where it is seen**, and both halves are required — a
condition alone is satisfied by any glance, and the wrong vantage is how a
job passes while every one of its faults sits outside the frame. Write them
as observations someone else could repeat and disagree with: a number that
changed, a thing visible in a named shot, a count that came back as one.
A job publishes only once it carries at least one.
`stages` models a skill whose passes run in order, when the order is
load-bearing. Each stage is `{ name, detail?, checks? }`, and a check that
sits in a stage is answered after that pass has run — which is the difference
between a criterion that means something and one that misleads. A skill with
no meaningful order leaves `stages` out and lists its checks flat; a skill
that has one may do both, and the flat checks then read as the final gate.
`verdicts` names the fixed set of outcomes a reader reports — at the end of
each stage when the skill has stages, and once at the end when it does not.
Each is `{ verdict, means }`: the word, and what reporting it asserts. Both
halves are required for the reason both halves of a check are. A verdict is a
check one level up, deciding a whole pass rather than one condition, and a
bare `refine` is a label each reader fills in privately — so two runs report
the same word about different states and nothing downstream can tell them
apart. Saying what it means settles the boundary in the skill instead of in
whoever happens to be reading it.
A check marked `perRun: true` is one whose content is written for the job at
hand: the manifest can say *keep a list of every feature this object must
have and answer against it*, but not what is on that list, because that is
per run. It renders with that instruction attached.
```yaml
verdicts:
- verdict: continue
means: Every check in the stage held, and the next stage can build on it.
- verdict: refine
means: A check failed on something this stage can still fix. Run it again.
- verdict: stop
means: A check failed on something an earlier stage decided. Go back to it.
stages:
- name: blockout
detail: Mass and proportion only.
checks:
- check: Every part on the identity list has a stand-in at roughly the right size.
observe: A capture beside the reference, at the same angle.
perRun: true
- name: material
checks:
- check: Metal reads as metal and cloth as cloth.
observe: The metallic and roughness passes, after the lighting stage has run.
```
`dependencies` is one flat list of asset **identities**: a toolbox, a single
tool, a guide, a component, a module, another skill. Each entry is resolved
when the skill is opened and reported with what it turned out to be, so a
skill never declares "this one is a toolbox" — the asset says so, and the
reader groups by that.
Identities are what make a skill portable. They pin like any other reference,
so installing a skill into another world brings the toolboxes and guides it
runs through along with it. A bare name (`scene`) or a guide path
(`core/multiplayer`) resolves to nothing and is refused before publish.
A toolbox entry lists the tools it carries, and a tool entry carries its own
signature, so the instructions never restate a call contract that can drift.
An identity this world lacks is reported as missing rather than dropped.
Write instructions that name the toolboxes and tools a job runs through, and
leave the call form to the reader — an agent may be holding the MCP tool
surface, the shell, or Luau, and the signatures are listed for it either way.
## Subskills
A `<sub>.agentSkill/` folder nested inside a skill is a subskill. It has the
same shape as its parent and is invoked as `parent/sub`.
Subskills are reachable only through their parent: they are absent from the
roster and from `skills.list`, and appear in the parent's invoke result. A
skill can therefore carry a dozen narrow procedures while costing the roster
one line.
`order` in the manifest names the subskills that lead. Any subskill it omits
still appears, sorted by name, after the ordered ones — so adding a subskill
folder is the only step needed to publish it.
## Publishing one
Make a skill and it is live: it joins the roster agents read and it opens by
name from the moment it exists. Edit its description and that is the line
agents see next.
A skill lives anywhere under `/zero/source/`. `asset.create` hands you a
valid one to fill in:
```luau
asset.create("agentSkill", "waveSpawning", { folder = "skills" })
```
A skill's scope — `builtin`, `library`, or `world` — comes from where it
lives rather than from anything it declares, so a world's own skill always
reads as the world's.
## Opening one
```
agent_skill {} -- the roster
agent_skill { name = "scenes" } -- one skill's instructions + deps
agent_skill { name = "scenes/player-setup" } -- a subskill
```
The same three through the toolbox — `skills.list`, `skills.invoke` — and
from Luau via `tools.use("skills", "invoke", "scenes")`. `asset.inspect` on a
skill ref returns the structured record all of them render.
Skills authored in a world are the world's own, and are labelled as world
content wherever they surface beside built-in ones.
## In the Inspector
Skill: what the skill is for and when it applies, how many checks and verdicts it declares, its stages, a link to each asset it depends on (and a problem for each that does not resolve), its subskills, and **Open** for instructions.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](#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.
## 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 `TextureCpuHandle`s, 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. `front` is 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. `front` is 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.
# assetType (asset type)
An asset type says what a kind of asset is: the files an asset of the kind is
made of, the code that gives it behaviour, the interface a person uses to work
with it, and how it is found, pictured and checked. A `<typename>.assetType/`
folder declares a type called `<typename>`, and every `<name>.<typename>/`
folder is an asset of it. `assetType` is itself a type, so a type is an asset
like any other: it is created, versioned, installed and contributed the same way.
## Making a type
`asset.create("assetType", "<typename>")` scaffolds a type. The type registers
the moment its folder is written, anywhere under `/zero/source/`, and from then
on `<name>.<typename>/` folders are its assets. An asset of a type that is not
registered yet is not an asset, so make the type first.
A type folder is a root: `~.x` in any file inside it names the type's own `x`,
wherever the type is installed. Keep the type's whole implementation inside its
folder and reach it that way.
What a type folder must hold is set by this type's own `type.yaml`, the same way
every type's `type.yaml` sets what its assets hold. `asset.validate` on a type
reports anything missing, and the scaffold produces a folder that passes.
## What sets your type's schema (`type.yaml`)
`type.yaml` is the structural contract every asset of the type is checked
against. The keys below are the ones the engine reads; a key not listed here is
ignored.
| Key | What it controls |
|---|---|
| `suffix` | The folder suffix of the type's assets. Defaults to `.<typename>`. |
| `primary` | The file or files that are the asset's content: what the asset resolves to when it is used. |
| `entry_files`, `primary_aliases` | Further files that load as the asset or share its identity. |
| `is_container` | Whether the suffix stays in the identities of what the asset holds. |
| `is_opaque_walker` | Whether a walk of the tree stops at the asset and registers only its primary. |
| `plural_dir`, `classify_exts` | Loose files in a folder of this name, or with these extensions, are assets of the type. |
| `is_text_default` | Whether the asset's files are text unless stated otherwise. |
| `broadcasts_subassets` | Whether assets nested inside an asset of the type are found by their own short name. |
| `root` | `true` makes every asset of the type a root, so `~.x` inside one names that asset's own `x`. |
| `contract_ref: { field, category }` | A key of the primary file that names another asset the asset is built on. |
| `files: { required, optional }` | The files an asset holds (below). |
| `allow_unlisted` | `false` reports every file the type does not declare. |
| `aliases_from: [{ file, key }]` | Further names an asset answers to, read from its own files. |
| `settings:` | Typed per-asset settings (below). |
| `inspector:` | The interface people use for the type and its assets (below). |
| `preview:` | How an asset is pictured (below). |
| `references:` | Names of other assets the type's files carry (below). |
| `indexing:` | What search reads for each asset (below). |
| `describe:` | How a missing description is generated (below). |
### The files an asset holds
Each entry under `files.required` or `files.optional` is
`{ path, kind: file | folder, one_of_group?, content? }`. `*` in a path matches
within one segment. Entries that share a `one_of_group` are satisfied by any one
of them. `content` lists checks on the file's bytes:
| Check | Passes when |
|---|---|
| `non_empty` | the file has content |
| `json_field { key, type?, non_empty?, accept? }` | the JSON holds the key (a dotted path) with that shape |
| `yaml_field { key, type?, non_empty?, accept? }` | the same for YAML |
| `described { keys?, headings?, message? }` | the file says what the asset is (see "Ask your assets to say what they are") |
Each check takes a `message`, which is what the author is told when it fails.
### Settings
`settings: { values, schema: { type | instance, export? } }` gives every asset
typed settings. `values` names the file an asset's values live in; `schema.type`
declares one schema for the whole type, and `schema.instance` names a file each
asset declares its own in. `asset.create` writes the resolved values, and a write
to the values file reaches `onChange` with what changed.
## The human side (`inspector.luau`)
A type is used by people through its Inspector, and without one the type is
usable only from code. The Inspector is the type's interface for a person: for a
procedural graph it is the graph editor, for a kit it is the tracking view of
what the kit holds and what is still missing. Writing it is part of writing the
type.
`inspector: { type, instance? }` in `type.yaml` names the files. `type` is the
interface for the type itself and for its assets; `instance` names a file an
asset may ship to add its own. Each returns:
```luau
return {
sections = { { id = "graph", title = "Graph", build = function(ctx) ... end } },
replace = { contents = { ... } }, -- replace a default section by id
order = { "graph", "contents" }, -- section order
}
```
`build(ctx)` receives the asset and builds the section. A section's `title`, and
the text its rows carry, is a string or an `i18n.text` value read from a catalog
(`i18n.text("inspectors.mesh.title")`), which follows the editor's language. `asset.create("assetType",
...)` scaffolds a working `inspector.luau` to start from. `asset.validate` on a
type that names no inspector, or names one that does not load, reports it.
## Say what your type is for (`about.yaml`)
Every type carries an `about.yaml` that says what the type is for, in three
plain-language texts:
```yaml
intent: "Sets the typeface of the text in your game: UI, labels, signs and 3D lettering."
function: "A font, from a TTF, OTF or WOFF file, made available to all of Zero's text, including 3D text."
usage: "You choose it by name for your UI or your text, and the text appears in that typeface."
```
- **`intent`** is what the type is for in someone's game, in the words a game maker uses. Up to 800 bytes.
- **`function`** is how it works today: what an asset of it is made of and what the engine does with it. Up to 1500 bytes.
- **`usage`** is what a person or an agent does with one. Up to 1200 bytes.
Write about the type as a whole, not one asset of it, and say what it does now.
Agents read these texts to learn when to reach for the type and how; people
searching find the type by them. The scaffold starts each text with
`[[describe]]` and a note on what to write; a text that still starts that way
counts as not written. `services describe` can write them.
They show up in `asset.inspect` as `detail.about`, in
`assetTypeRef:getAbout()`, at the end of the type's `types/<typename>` guide,
and at the top of the type's Inspector.
## Ask your assets to say what they are
A type can require each asset to carry a description: a file, written by the
author or generated, saying what the asset is and what it is for. Declare it with
a `described` check:
```yaml
files:
required:
- path: "README.md"
kind: "file"
content:
- { check: "described", message: "Say what this widget is for and where it goes." }
```
A file that is missing, empty or still has a line starting with `[[describe]]`
fails the check, and so does a YAML file whose named `keys` are missing or over
their `max_bytes`, or a Markdown file missing a named heading. `asset.validate`
names the asset, and a push that carries it is refused with `message`.
`services describe` finds such assets and writes their descriptions.
`describe:` steers what is generated:
```yaml
describe:
guidance: "Say what the widget shows, what input it reacts to, and where it fits in a UI."
examples:
- "<identity of an asset of this type whose description reads well>"
```
An example is an asset of your type whose description is a true, written one,
never a placeholder and never the asset being described; with none, leave
`examples` empty. A type whose assets the model cannot perceive (a sound, a
motion) says so in `guidance`, and the description is drawn only from what the
asset's own files state.
Start the file unwritten in your `template/`: a heading and one line starting
with `[[describe]]`, never example content, which every new asset would carry
verbatim. A type whose assets say what they are through their own content (a
mesh, a texture) declares no check.
## The type's code (`behavior.luau`)
`behavior.luau` returns one table. These are the keys the engine reads from it;
anything else on the table is the type's own.
### What the type's assets can do
| Key | Shape | What the engine does with it |
|---|---|---|
| `ref` | `{ [name] = function(self, ...) }` | Every function becomes a method on every asset of the type: `ref:name(...)`. |
| `modules` | `{ [name] = require(".x") }` | Code the type's assets reach with `asset.containing(__FILE__).modules.<name>`. |
| `events` | an events schema | Events an asset fires. Readers subscribe through `ref.events.<name>:connect(...)`; the type fires them. |
| `refShapes` | `{ [method] = function(self) -> (typeExpr, source?) }` | The result type of a `ref` method for one particular asset (below). |
| `namePattern` | a Lua pattern | The names `asset.create` accepts for the type's assets. Default `^[A-Za-z][A-Za-z0-9_]*$`. |
| `derive` | `{ output, module, compute, fingerprint, matches?, required?, timeout? }` | A file computed from the asset when it is committed; a `required` one holds the commit until it lands. |
Some `ref` names mean the same for every type, and the engine calls them:
- **`instantiate(self, target?, opts?) -> (root, idMap)`**: puts the asset in a scene. Defining it is what makes the type scene-instantiable. Its return is a contract (below).
- **`inspect(self) -> detail`**: what `asset.inspect` reports for the asset beyond the common record (below).
- **`preview(self, opts)`**: renders the asset's picture; it wins over the type's `preview:` recipe.
- **`previewMaterial(self) -> { material }`**: the material a `surface` preview paints.
- **`saveDefinition(self)`**: writes the asset's in-memory edits back to its files in edit mode.
Some names are answered for every asset before the type is asked, so a `ref`
entry of the same name is never reached: `getSource`, `getBytes`, `getText`,
`exists`, `deps`, `canInstantiate`, `canPreview`, `preview` (which calls yours
when you define one), `meta`, `runtime`, `events`, `typeRef`, `modules`, and the
residency flags `has_backing_asset`, `has_runtime_changes`, `cpu_resident`,
`gpu_resident`.
### When the engine calls the type
| Hook | Signature | When it runs |
|---|---|---|
| `onCreate` | `(name, opts) -> { [path] = contents }` | On `asset.create`. Returns the files of the new asset, laid over the type's `template/`. Annotate `opts`: its type is the schema `asset.create` checks the caller's arguments against. A `""` key writes a single loose file. |
| `onReplace` | `(dest, files) -> files` | On an `asset.create` over an asset already at `dest`, after `onCreate`, so what the author added can be carried into the replacement. |
| `onRegister` | `(self)` | Once per asset, when it first registers: when the world loads, on a live `asset.create`, and when an asset arrives later (installed, pulled, synced). This is what makes an asset take effect the moment it is written. |
| `onChange` | `(ref, change)` | On every write to a file inside an asset of the type. `change` is `{ path, asset, type, kind, origin }`, plus `settings` and `settingsChanged` for a write to the settings values file; `kind` is `edited` or `seeded`, `origin` is `local` or `remote`. Filter on `change.path`. Writes to one asset arrive in order, each after the hook for the one before has finished, and the hook may wait on what it reads. A write the hook makes inside its own asset arrives later like any other, so keep the hook convergent. |
| `onDependencyChange` | `(ref, change)` | When a module the asset's code requires is re-run, or a name the asset carries starts resolving. `change.kind` is `dependency`. |
| `onDelete` | `(ref)` | When an asset of the type leaves the world: deleted, or the world it is in is swapped away. |
| `validate` | `(ref) -> { { code, message, severity?, path? } }` | On `asset.validate`, after the `type.yaml` checks, for the type's own rules. `severity` defaults to `error`. Runs on every push, so a type's rules hold at publish. A hook that fails reports `validate.hook_failed`. |
### Settings hooks
| Hook | When it runs |
|---|---|
| `settingsDefaults(name, opts, asked) -> table` | Once per `asset.create`, for values the schema's defaults do not give. |
| `settingsCorrections(values, name, opts) -> (corrections?, reasons?)` | On every settings write. A correction to a field the writer named refuses the write, giving the reason. |
| `settingsDerivedDefaults(values, explicit, changed)` | On a settings write, for fields derived from others. |
| `normalizeSettings(values, stored?)` | On every read and write, to map an earlier spelling of a field to the current one. |
| `settingsHealed(values, stored)` | On a read, to repair stored values. |
| `settingsSchemaProblem(schema, ref) -> string?` | When an asset's own schema is read; a returned string refuses the schema. |
`creationInputs` (`{ [opt] = true | selectorOpt }`) names `asset.create` options
that are inputs to the type rather than settings.
### Reloading
An edit to `behavior.luau` reaches existing assets with no restart.
## How an asset is pictured (`preview:`)
| `kind` | The picture is | The type provides |
|---|---|---|
| `instantiate` | the asset placed in the preview studio through its `instantiate` | `ref.instantiate` |
| `image` | a file the asset holds, fitted to the frame | `source: { files }`, the candidate files in order |
| `surface` | the asset painted on a mesh | `mesh`, and `ref.previewMaterial` |
| `custom` | whatever `ref.preview` returns | `ref.preview` |
```yaml
preview:
kind: surface
mesh: sphere
```
`angle: [yaw, pitch]` turns the camera. The studio owns the light, the backdrop
and the framing, so an asset pictures the same in every world. A type with no
`preview:` that defines `instantiate` still answers `ref:preview()`.
A picture is rendered when something asks to view it (the editor showing the
asset, `ref:preview()`), when previews are generated on request
(`assets.generatePreviews`), and when the asset is staged. An edit alone
renders nothing. The picture records what it was made
from, so a stale one is told from a current one.
## The names your type's assets hold (`references:`)
Where your type's own format or API carries the name of another asset, declare
it here:
```yaml
references:
keys: # a top-level key of the type's files that holds a name, or a table of them
- { key: "shader", type: "shader" }
args: # an argument of a call, counting from 1
- { call: "g:node", position: 2, type: "procNode" }
fields: # a field of the options table a call takes
- { call: "fx.beam", field: "material", type: "material" }
```
`type` is the category the name resolves in. `asset.declareReferenceKey`,
`asset.declareReferenceArg` and `asset.declareReferenceField` declare the same
from a script.
## What search reads (`indexing:`)
`indexing:` is how assets are found. Search reaches an asset only through what
its type's `indexing:` names, and adds nothing of its own. That includes you: an
asset indexed badly is one you cannot find later, when you are making something
else and need it. A type whose assets are never looked for says `indexing: none`.
```yaml
indexing:
data: # text, read from the asset's files
- role: identity
extractor: verbatim
source: { file: "README.md" }
- role: interface
extractor: symbols
source: { files: ["init.luau"] }
- role: implementation
extractor: verbatim
source: { files: ["init.luau"] }
content: # the picture
- role: identity
modality: image
source: { file: "preview.png" }
facets: # what a query filters and re-ranks by
file: ".metadata"
rigged: { type: bool, from: { components: { contains: SkinnedModel } } }
roughness: { type: number, filter: range, from: { file: "mat.yaml", field: "floats.roughness" } }
```
**Roles.** Each `data` and `content` entry has a `role`, and a query matches in
two slots:
- **`identity`**: what an asset is. For anything you look at, the picture; for anything else, the text that says what it is.
- **`capability`**: what it does and how it is made. `interface` (the signatures or shape a caller uses) and `implementation` (the body) both count as capability, and a type with code usually declares both.
Ask what a person would recognise an asset of the type by, and what it does, and
point each role at the file that says it. For text, that is the asset's
description: the file your template starts with a `[[describe]]` block, which the
author writes or `services describe` fills in (see "Ask your assets to say what
they are").
**`data`.** `extractor: verbatim` embeds a file's text as it is; `symbols` embeds
the declared signatures of code. `source` is `{ file }` or `{ files: [...] }`.
Never index a file name (`source: { field: name }`): one
or two words carry nothing to match.
**`content`.** `modality: image` embeds the picture itself, so a text query
matches what the asset looks like. A type whose assets are looked at makes the
picture its identity.
**`facets`.** What a query narrows and re-ranks the matches by. `file` is embedded
whole, so keys nobody declared in it are still found. Each further key is
computed for every asset and states a value for every one, including `false`: a
file can imply "not rigged" but never say it, and a query separates rigged from
unrigged only because the unrigged ones say `rigged: false`. A key is
`{ type: bool | number | enum, filter?: range, from }`, where `from` is
`{ file, field }` (a field of one of the asset's files, dotted for nesting),
`{ metadata: key }`, or `{ components: { contains | any_outside } }` (what the
asset's entities carry).
## What blocks a push
A push checks every asset it carries against its type. These refuse it:
- a required file that is missing, a `content` check that fails, a `described` file not written yet;
- an error from the type's `validate`.
Unlisted files and warnings do not. A `described` file blocks only a push that
changes the asset. When a push is refused, the message names each asset and what
it lacks. `world.publishBlockers()` and `zm status` read the same list without
pushing.
## Typing a result from the asset (`refShapes`)
A `ref` method whose result depends on the asset (one entry per child folder, per
row of a config, per control a map declares) can only be annotated as
`{ [string]: Thing }`, which accepts any key. `refShapes` states the result for
each asset instead: a function returning a Luau type expression for that asset,
and the module whose types the expression uses. Read the asset; never run it.
```lua
-- inputMap.assetType/behavior.luau
M.refShapes = {
activate = function(self): (string, string)
local names = {}
for _, control in ipairs(M.ref.controls(self)) do
table.insert(names, control.name .. ": Handle")
end
if #names == 0 then return "", "" end
return "{ " .. table.concat(names, ", ") .. " }", "@builtin::modules.zinput.scheme"
end,
}
```
With it, `map.jump:onPressed(fn)` is checked and `map.noexisting` is reported.
A per-asset type applies where the call site names which asset: a component
field whose default names it, or an annotation `AssetRef<"inputMap", "<identity>">`.
`asset.resolve(name, category)` carries only the category, and a name computed
at runtime carries nothing, so calls on those keep the declared return. The type
belongs to the call's result: index it there, or bind what you need from it
there, and the names stay checked.
`require("@builtin::assetTypes.assetType.shared.refShapes").published()` shows
what was published: `categories` (each type's method surface) and `returns`
(per-asset results by identity, then method). An identity missing from
`returns` was never published. Results are recomputed when an asset is written;
at most 64 assets of one type are read.
## Putting an asset in a scene (`ref.instantiate`)
A caller writes one piece of code against every instantiable type, so what
`instantiate` returns is fixed:
```lua
local Instantiable = require("@builtin::assetTypes.assetType.shared.instantiable")
M.ref = {
instantiate = function(self, target, opts)
local root = Instantiable.root(self, target, opts)
-- compose the asset under `root`, now
return Instantiable.result(self, root, idMap)
end,
}
```
**In.** `target` is the entity the asset lands under, or onto for a hierarchy
type; with none, the type spawns a new root. `position`, `rotation` (three
numbers in degrees, or a quaternion), `scale`, `name` and `temporary` mean the
same for every type: `Instantiable.root` applies them to a root you make,
`Instantiable.place` to one you adopt. Document any options of your own in the
hook's `--!arg` docs.
**Out.** `(root, idMap)`. `root` is the composed root, live when you return:
build synchronously, never defer to a component's `awake`. `idMap` maps each
original id to the spawned one, so a component that composes the asset again on
every load can pass it back in and keep references into the composition. Every
call goes through `Instantiable.result`, which refuses a root that is not live
and turns a missing `idMap` into `{}`.
## Inspecting an asset (`ref.inspect`)
`asset.inspect(ref)` builds a record every asset shares (`identity`, `name`,
`guid`, `source`, `typeName`, `typeDefinitionPath`, `scope`, `origin`,
`description`, `tags`), then calls `ref.inspect(self)` for the type's own
`detail`:
```lua
M.ref = {
inspect = function(self)
-- read the asset's own files, never runtime state: inspect works on an asset
-- that has not been compiled, uploaded or registered
return { --[[ what makes this asset what it is ]] }
end,
}
```
Without it, the record is the shared part alone, and an agent inspecting the
asset learns nothing of what it is. A hook that raises leaves `detail` empty and
sets `record.warning`.
## Discovery
- `asset.list("assetType")`: every registered type.
- `asset.inspect("<typename>")`: the type, its README and its `about.yaml` texts.
- `asset.resolve("<typename>", "assetType")`: the type itself.
# capture.cleanup
Clears every file from the capture output directory, removing the PNGs left behind by earlier captures. Safe to call when the directory is missing or already empty.
# collage
A grid of frames in one image.
One picture that answers a question no single frame can: what does this look like
from every side, what does this frame look like under each diagnostic pass, what
moved over the last three seconds, which of these twenty shots is the shot.
## The axis is always stated
A collage varies one thing across its cells, and the argument you pass is what
that thing is:
| Argument | One cell per |
|----------|--------------|
| `setups` | whole camera set-up — a shot list, each entry aimed the way a single capture is |
| `viewpoints` | named view of one subject — a list, `"front top left"`, or `"sides"` for all six |
| `passes` | render pass — `final`, `albedo`, `normal`, `depth`, `motion_vectors`, or any content-registered capture view |
| `duration` | sample, spread across that many seconds of gameplay |
`setups` and `viewpoints` are one axis — both say where the camera stands — so a
request names one of them.
Naming a station axis **and** `passes` lays out a 2D grid: a row per station, a
column per pass. Reading across a row is one set-up under every pass; reading
down a column is one pass from every set-up.
A collage with no axis is refused, naming all of them. It does not fall back to
time — a silent time default is exactly what taught callers that a collage could
only ever be time, so nobody looked for a grid of views.
`duration` cannot combine with `setups`, `viewpoints` or `passes`. A cell that
differs both in what it looks at and in when it was taken answers neither
question; take one collage per axis.
## The shot list
Each entry of `setups` 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. A field an entry
leaves out comes from the collage's own options, so a shot list writes down only
what makes each shot differ:
```lua
zero capture collage --setups '[
{ "position": [-14.6, 1.35, 4.5], "lookAt": [-16.3, 1.1, 1.6], "fov": 28, "label": "doorway" },
{ "position": [8, 3, 12], "lookAt": [0, 1, 0], "fov": 50, "label": "wide" },
{ "entity": "hero", "viewpoint": "front", "basis": "local", "label": "closeup" }
]'
```
Three stations, three lenses, three subjects, one instant, one picture — which is
the comparison a framing pass is made of. A key outside that vocabulary is
refused where it was written, so a misspelled `fov` names itself instead of
quietly framing a shot nobody asked for.
## Reading the result
The grid shape is worked out from the number of cells — pass `grid` (`"3x2"`) to
state it, and it is an error rather than a silent crop if it cannot hold them.
Every cell comes back labelled in `cells`, with its `label`, the `viewpoint`
and/or `pass` that made it differ, the `eye` it was rendered from and the lens it
was rendered through. A grid is meant to be readable from the response, not
guessable from the pictures. Only a time collage reports the window it covered —
a station or pass grid samples one instant, and reporting a span for it would
invite reading the difference between cells as motion.
## Aiming it
Exactly as a single capture is aimed: `entity` (with `viewpoint` and `basis`, or
`angle` and `distance`), `position` with `lookAt`, `camera`, or nothing at all
for the main scene camera. `projection`, `orthoHeight`, `isolate`,
`renderLayers`, `fov`, `width` and `height` all mean what they mean for one
frame, and apply to every cell a set-up of its own does not override.
The shot most of this exists for is one call:
```lua
zero capture collage --entity crate --viewpoints sides --basis local \
--projection orthographic --isolate
```
Six sides of one object, each an orthographic elevation, nothing else in frame.
# feed
The newest frame a capture produced on this engine, and who is watching for it.
Every capture core publishes here. `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. `watch(fn)` calls `fn` as each new frame arrives, and
`unwatch(token)` drops the subscription.
`shown()` says a viewer 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; `shown(false)` stops. With nobody drawing, a capture's target is
released the moment its capture is done.
The release path asks `retains(rtHandle)` before destroying a target, which is
how a held frame survives its own capture. `capture.shared.releaseFrame` is the
one place that asks.
# capture.fromCamera
Render the scene from a specific named camera's authored pose and settings, returning the picture as an image value (and writing a PNG as well when a `path` is given). By default it uses the camera's exact world-space pose plus its whole lens (fov/near/far, and the projection it builds with the orthoHeight an orthographic camera covers), its diagnostic debugChannel, its post-process setting, its lighting environment (an isolated camera's background and ambient with it) and the scene layers it draws: the camera's own `renderLayers` with the editor's chrome (`EditorUI`) and the authoring overlays (`debug`) taken off, since those are drawn for whoever is editing rather than by that camera into the world. So the frame is precisely what that camera draws of the scene, without changing what is on screen. Pass overrides (`renderLayers`, `debugChannel`, `fov`, `near`, `far`, `projection`, `orthoHeight`, `width`, `height`) to change any of those for the one capture without touching the camera; `renderLayers = "all"` is how the overlays are captured on purpose.
# capture.fromEntity
Renders a screenshot orbiting a target entity. The camera frames the union world-space bounds of the entity (or a list of entities): it is aimed with a `viewpoint` plus `basis`, or a raw `angle` ({yaw, pitch}), stands at the distance that fits the subject (or a pinned `distance`), and leaves `margin` around it, with a configurable field of view and output size. Returns the picture as an image value, and writes a PNG as well when a `path` is given; fails when the entity is missing or its position can't be read. The call waits for the render and the GPU readback and returns only once the picture is read back (and the PNG written, when a `path` is given).
# capture.fromPosition
Renders a screenshot from an explicit world-space camera position and returns the picture as an image value. Aim it with a look-at target (defaulting to the origin) or a quaternion rotation that takes precedence, and set field of view, output size, and near/far clip planes. A PNG is written as well when a `path` is given. It spawns an ephemeral camera, triggers one render, reads the frame back, and despawns the camera automatically — the call waits for the render and the readback, and returns only once the picture is read back.
# isolate
Show the subject without the other geometry drawn, then put everything back.
A prop inside a built scene is behind whatever stands in front of it. The
render-layer system already expresses "draw only these" — an entity is a member
of layers, a camera renders a filter over them — but it does not remember what
it displaced, so every caller wanting one clean shot of one thing hand-rolls the
same record, swap and restore. This is that, once, with the restore guaranteed.
## What it changes
Exactly one thing: the other geometry is not drawn.
Sky, post-process and UI are left exactly as the capture's own render-layer spec
stated them, so a final frame stays a final frame. A caller who wants the
geometry read flat — no lighting, no atmosphere — asks for a **pass**
(`pass = "albedo"`, `pass = "normal"`), which is what passes are for.
## What it does not change
Excluded geometry still **casts shadows** onto the subject and still bounces
light into it. Lights are not filtered by render layer at all, and the layer mask
drives object culling and the geometry passes rather than the shadow passes. This
is the render-layer system's deliberate behaviour — a wall stays out of the shot
while it still exists for lighting, shadows and physics — so a shadow with no
visible caster in an isolated capture is the system working, not a bug.
## The layer
One reserved layer name, `captureIsolate`, reused by every isolated capture.
Membership is a 32-bit mask with six reserved bits. A layer minted per call would
exhaust the namespace in twenty-six captures and leave a trail of empty layers
that `renderLayer.list` would show forever.
## Restoring
`begin` records each affected entity's membership before moving anything, so the
token describes the whole subtree even if the move fails part-way. `restore`
puts every entity back to the layers it carried — and an entity that carried
**no** explicit membership ends with none, rather than an explicit `default` that
looks identical in a picture and different in the data.
`restore` is safe to call twice and safe on a token whose subjects have since
despawned, which is what lets a caller restore unconditionally on every exit
path: success, failure, and once around a whole collage grid rather than between
its cells.
# capture.oneshot
The low-level one-shot capture primitive. Spawns an ephemeral offscreen camera configured from a source — the viewport, an entity (or several), a world-space position, or a UI window — triggers a single render, and returns the camera and render-target handles synchronously so the caller drives the readback and cleanup itself. It does not wait for the render: it returns at once, and the caller reads the target back and cleans up, which makes it the path for a caller that must not yield (the `capture` MCP tool is built on it).
When framing an entity, omitting the distance auto-fits the camera to the union world-space bounds of the target(s), so the subject is guaranteed fully in frame at any scale — a small prop and a large building both fill the shot. Because the data drives the camera, an empty result is a real signal that the geometry or material is wrong rather than a framing mistake; a distance can be passed to force a fixed orbit radius, and an angle sets only the viewing direction. Further options select the output by `pass` name (or `view`, or a raw `debugChannel`), set which render layers draw with a `renderLayers` spec such as `"all !ui"`, turn the post-process chain on or off with `postProcessing`, choose an orthographic `projection`, and set field of view, clip planes, and output size. The returned camera is left disabled so it can be re-triggered to produce additional frames on the same target, as when stitching a collage.
# shared Module
Cross-tool helpers for the `capture` toolbox. Every tool inside this
toolbox can pull these in with `require(".shared")`.
## Purpose
This module holds the small set of helpers that more than one tool in
the toolbox needs — typically the `ok(value, formatted?)` /
`fail(message, exitCode?)` constructors that produce the canonical
`captureToolResult` shape, plus any toolbox-wide parsers,
validators, or shared state. Keep it focused: per-tool implementation
stays inside each `<x>.tool/init.luau` (with its own `--!desc` /
`--!arg` / `--!return` / `--!example` annotations).
## Usage
```luau
local shared = require(".shared")
function M.someTool(...)
if not ok then
return shared.fail("reason", 1)
end
return shared.ok(value, "")
end
```
## Exports
See `init.luau` for the full export surface. Typical exports include
`shared.ok(value, formatted?)` and `shared.fail(message, exitCode?)`
for building the toolbox's standard result envelope.
# subject
What a capture of an entity is a picture of. Every path that frames or isolates
an entity (`fromEntity`, `oneshot`, a `setups` or `viewpoints` collage,
`isolate`) resolves the name, id or proxy it was given here, so they agree on
which entity a name means.
## A player is drawn as its body
A player identity (an entity carrying `UserIdentity`) is the registry anchor a
connected user is known by. The player is drawn as the body its `avatar` names,
a standalone entity at the scene root, so a capture of an identity frames and
isolates that body.
In play, a player prototype's spawn turns the prototype's copy into the joining
user's identity under the prototype's own name and stands the body it names at
the spawn point. A capture of the name an author gave their player prototype
therefore frames the player's body in play, and the prototype's subtree in
edit.
## What a refusal names
`describe(target, via)` is the phrase a framing refusal uses for each subject it
looked at: the entity's name and id with the number of descendants it holds,
and the player identity it is the body of when it was reached through one, or,
for a player identity with no live body bound, that the body is missing.
| Function | Returns |
|----------|---------|
| `resolve(t)` | the entity a capture pictures for a name, id or proxy (nil when none matches), and the player identity it was reached through |
| `bodyOf(target)` | the live body a player identity is drawn as, or nil |
| `isPlayerIdentity(target)` | whether the entity carries `UserIdentity` |
| `describe(target, via)` | the refusal phrase for one resolved subject |
# viewpoints
The named camera stations a capture can be taken from, and the rule that decides
what a name like `front` means.
A **viewpoint** is a direction with an image-up: where the camera stands relative
to what it is looking at, and which way is up in the resulting picture. Naming
one replaces reverse-engineering a `{yaw, pitch}` pair, and names the seven
stations that answer "is this built correctly" — the six sides and the corner.
| Viewpoint | Sees |
|-----------|------|
| `front` | the side the subject faces |
| `back` | the opposite side |
| `right` | the subject's right |
| `left` | the subject's left |
| `top` | plan view, the subject's front toward the top of the image |
| `bottom` | the underside |
| `iso` | the corner view where all three axes project equally |
An entity faces its local -Z, so the camera that sees its front sits on its +Z.
`top` and `bottom` take their image-up from the subject's forward axis, because
world up is undefined when you are looking straight down it — that is what puts
the subject's front at the top of a plan view instead of leaving the roll to
whatever the yaw happened to be.
## Basis
`basis` says which axes the name is measured against, and it has **no default**:
- **`"local"`** — the subject's own axes. `front` is the side the subject faces,
whichever way it is turned, and the frame is fitted to the subject's own
extents (`entity:orientedBounds()`).
- **`"world"`** — the world axes. `front` is the side facing world +Z, whatever
the subject is doing.
For a crate turned 40 degrees these are different pictures, so the caller states
which one it wants. A default would mean one call site's `front` is the crate's
front and another's is the world's, and the difference only shows up in the
image. With no subject to take axes from — a capture framed on a world position
rather than an entity — both values mean the world axes, and both are accepted.
## Orthographic size
`orthoHeightFor` answers the question an orthographic frame asks that a
perspective one does not: how big is the frame? A perspective camera backs off
until the subject fits; an orthographic camera does not converge, so its size is
decided by the subject alone — project the bounds' eight corners onto the
camera's up and right axes and take whichever needs more room.
Its `half` extents and `axes` must be stated in the same frame. A resolved
viewpoint carries both: `frame` axes to use with local-frame extents, and its
world axes to use with world extents.
# capture.viewport
Renders a screenshot mirroring the active viewport. It mirrors the on-screen camera through the shared capture core, so the frame carries that camera's pose, its lens (`fov`, clip planes and the projection it builds), the lighting environment it states (an `isolated` camera's background and ambient) and the layers it draws the screen with, so the image matches the viewport's picture: the scene, its debug overlays and the content UI. The editor's own chrome (the `EditorUI` layer) is never in the frame. The raster is the viewport's own pixel size, the size `getViewportSize()` reports. Returns the picture as an image value, and writes a PNG as well when a `path` is given; fails when no active viewport camera exists.
Its one argument is a path to also write the PNG to, or a table overriding any of that for one capture: `width` / `height` for a raster of some other size, `renderLayers` for the frame under a different layer spec (`!EditorUI` is always appended to it, whatever it names; `"all !debug"` is the frame without the debug overlays) and `postProcessing` to take it with the grade off.
```lua
tools.use("capture", "viewport", "/source/tmp/capture/shot.png")
tools.use("capture", "viewport", { path = "/source/tmp/capture/clean.png",
renderLayers = "all !debug" })
tools.use("capture", "viewport", { width = 960, height = 540 })
```
# toolbox (asset type)
A toolbox is an asset grouping set of `<tool>.tool` assets around one area of work. Each tool in
it becomes callable under the toolbox `<toolbox>.<tool>`, and is a shell command,
`zero <toolbox> <tool> <args>`; `guides core/tools`, "A tool's command line", covers its
flags, paths, `render.luau` and exit status.
## When to group tools together
When creating tools for the engine, a common theme should be identified and
and a toolbox created to house them, this allows each individual operation
to be easily accessible instead of being scattered in a million places.
## Sharing functionality between tools
A toolbox features ability to define functions that code inside each individual tool
inside it can access, this is usefull when establishing helpers etc so you dont have
to duplicate code for each new tool you write.
A `shared.module` inside the toolbox root is where this code lives,
each tool inside a toolbox can access its functions by requiring it with `require(".shared")`.
```luau
-- <toolbox>.toolbox/shared.module/init.luau
local M = {}
-- One step a tool reports in its result.
export type Step = { name: string, ok: boolean, detail: string? }
--!desc Check that a numeric argument lies within bounds.
--!arg arg The argument's name, for the error.
--!arg value The value passed.
--!arg min The lowest value allowed.
--!arg max The highest value allowed.
--!return The value; raises naming the argument and the range when it lies outside.
--!example "count", 12, 1, 100
typed function M.range(arg: string, value: number, min: number, max: number): number
if value < min or value > max then
error(string.format("%s must be between %s and %s, got %s", arg, tostring(min), tostring(max), tostring(value)))
end
return value
end
--!desc A byte count as a readable size.
--!arg bytes The count.
--!return The size in B, KB, MB or GB.
--!example 2681107
typed function M.size(bytes: number): string
local units = { "B", "KB", "MB", "GB" }
local i = 1
while bytes >= 1024 and i < #units do
bytes /= 1024
i += 1
end
return if i == 1 then string.format("%d %s", bytes, units[i]) else string.format("%.1f %s", bytes, units[i])
end
--!desc A step for a tool's result.
--!arg name What the step did.
--!arg ok Whether it succeeded.
--!arg detail What happened, when there is more to say.
--!return The step.
--!example "copy textures", true
typed function M.step(name: string, ok: boolean, detail: string?): Step
return { name = name, ok = ok, detail = detail }
end
--!desc The asset of `kind` that a caller named, looked up by that name.
--!arg kind The asset type to look in.
--!arg name The name the caller passed.
--!return The asset; raises naming the kind and name when nothing matches.
--!example "material", "gold"
typed function M.named(kind: string, name: string): AssetRef
local ref = asset.tryResolve(name, kind)
if ref == nil then
error(string.format("no %s named '%s'", kind, name))
end
return ref
end
return M
```
This shared code should be limited to being used by the tools inside it,
dont put general functionality you might need otuside the toolbox into its shared module.
Instead reference those in the shared module should you wish to utilize something.
A benefit the shared module shares with the tools own code is that it allows
the author to write code that has computed references, meaning eg resolving
an asset from a computed variable `asset.resolve(<type>,name)` that is normally
not allowed in other parts of the engine surface, due to said references not being
recorded.
This is usefully for utilizing tools to handle operations that are called by users/agents
and are dynamic in nature, such as finding/inspecting/testing assets, or content based on an input etc.
When writing a shared module, you should follow standard module practices, please see `module.assetType` readme.md
for more indepth information.
## Describing the toolbox
When writing the readme for your toolbox, ensure you focus on the overall
area what the toolbox focuses on, rather than listing individual tools inside it,
discovery surfcaces already group tools and toolboxes together so a toolbox can be discoverd
by the tools it hosts and doesnt require separately maintaining a list of the tools in it's readme.
## What are tools and when do i create ones?
Tools are typed operations with a known serialized output format, made to enable efficient operation in the engine and provide an easily callable interface for operations around a topic.
They are also useful in that they can be called from a multitude of surfaces, from `execute`, to `bash` to `tool_exec`
to users Tools portion in the edui.
A toolbox and its tools differ from eg a module that has multiple callable functionm in that they might return anything,
and come with no guarantee that the output is meant to be inspected directly, where as tools are meant to perform an self contained operation and return results/info/relevant data in a readable format, including eg. image data.
Whenever you identify an operation that requires specific orchestration and or is something that could be a repeated operation and makes it easier to manage the project/task or when you want to expose logic written in `luau`
to the other call surfaces like `bash` under its `zero` command,you should reach for creating a toolbox.
Toolboxes also come in handy for limiting control/inputs, exposing a limited set of functionality in a clean manner or ensuring the caller doesnt have to understand the entire code.
For comparison, you could think of toolboxes as programs, and the tools inside them as commands.
A package might contain tens of tousands of lines of code, dozens of modules, components etc,
a toolbox turns that surface into a system that any agent/user can then utilize without having
to read or understand the code inside.
Its the UI equilevant to operations.
For the indepth explanation of what `tool.assetType` is and does, read it's readme.
# terrainData
A landscape's baked data, as one asset.
A terrain is never a single image. It is a heightfield, the control textures
saying which surface covers where, a mask of where there is no ground at all,
the water surface standing over it, and the table of what those surfaces are.
Every one of those is an ordinary `.texture` the renderer already knows how to
sample; this container holds them together and names their roles, so a `Terrain`
component references a **landscape** rather than a handful of loose images, and a
heightfield never turns up in the picker beside an albedo map.
```
dunes.terrainData/
terrain.yaml the manifest: where the ground stands, and role -> child (required)
README.md what landscape this holds (required)
.metadata description and tags (required)
height.texture/ rgba32f: R metres, G/B gradient, A coverage
holes.texture/ rgba8: R non-zero where there IS ground
water.texture/ rgba32f: R the surface's world Y, G its coverage
water_shade.texture/ rgba8: R the water column over the deepest column, G coverage
control0.texture/ rgba8: four layer weights, one per channel
control1.texture/ rgba8: layers 5-8
slice_albedo_0.texture/ rgba8: one layer texture per distinct albedo
slice_normal_0.texture/ rgba8: one per distinct normal map
slice_mask_0.texture/ rgba8: one per distinct mask map
```
The children are written by the same `asset.create` every other texture goes
through and placed inside the container by `dest`, the way a bundle carries the
meshes it assembles. A container holds `holes`, `water` and the `slice_*` children
only when the landscape has them.
## The manifest
```yaml
version: 1
size: [4000, 4000] # metres of ground, along world X and Z
origin: [0, 0] # world centre of that rect
heightRange: [-8.1, 63.4] # the vertical reach the geometry declares
height: height.texture
holes: holes.texture
water: water.texture
waterShade: { file: water_shade.texture, depthMax: 12.5 }
controls: [control0.texture, control1.texture]
layers:
- { name: sand, tint: [0.62, 0.56, 0.42], tiling: 12, albedoSlice: 0, normalSlice: 0, maskSlice: -1 }
- { name: rock, tiling: 8, albedoSlice: 1, normalSlice: -1, maskSlice: -1 }
slices:
albedo: [slice_albedo_0.texture, slice_albedo_1.texture]
normal: [slice_normal_0.texture]
size: [1024, 1024]
```
Children are named **relative to the container**, so a landscape that is moved
or renamed carries its own textures with it.
## The payloads
**The heightfield** is `rgba32f`: R the height in **metres**, G and B its
gradient along world X and Z, A coverage. Metres and not a normalized 16-bit
ramp, because relief is often a few metres inside a range of hundreds and eight
or sixteen bits band that into terraces. The gradient is what a surface normal
comes out of without differencing the height four more times.
**The control textures** are `rgba8`, one channel per surface layer, four layers
a texture and another texture for every four after that. A texel holds how much
of each layer covers it, and the weights are **normalized to sum to one**: a
texel covered half by sand and half by rock reads 128/128, whatever the two
masks happened to evaluate to, so a shader blends by them without renormalizing
per pixel. Layer `n` is channel `((n - 1) % 4) + 1` of control texture
`ceil(n / 4)`; `:layerSlot(name)` answers that for a named layer.
**The holes mask** is `rgba8`, R non-zero where there **is** ground. Terrain a
cave mouth or a quarry has been cut out of has holes, and a hole is absence
rather than a height of zero.
**The water surface** is `rgba32f`: R the world Y the surface stands at, in
metres, G whether a surface covers that texel. A sea, a lake and a river are the
same thing here: a level and where it reaches. `water_shade` is its filterable
`rgba8` companion that the water's material reads per pixel, R the column over
`depthMax` and G coverage.
**The layer table** says what each control channel stands for: its tint, its
albedo, its normal map, its mask map (R metallic, G ambient occlusion, B height,
A smoothness), and the metres one repeat of them covers (`tiling`).
**The layer slices** are the layers' own textures, one child per distinct texture
per role (albedo, normal, mask), all resampled to one square edge (`slices.size`).
A surface binds each role's slices as one texture array, and each layer's
`albedoSlice`, `normalSlice` and `maskSlice` says which slice it reads (0-based,
-1 for none). A slot per layer per map would be twenty-four sampled textures in
one bind group, past what a fragment stage is guaranteed; three arrays cost three
slots whatever the layer count, and every slice wraps on its own.
## Filling one
`terrain.write` in a procgen graph writes the manifest and every child in one
step, and answers the reference to the container. It takes:
- `raster`: the ground; its `height` layer becomes the heightfield.
- `name`: the container's identity, a bare identifier (`dunes` writes
`dunes.terrainData`). `folder` or `dest` places it.
- `layers`: the names of the raster layers holding each surface's coverage, in
channel order, separated by commas (`"sand, rock"`). Coverage per surface is an
ordinary mask layer written by `terrain.mask`.
- `surfaces`: optional records of `{ name, tint?, albedo?, normal?, mask?, tiling? }`
matched to a layer by `name`, saying what each surface is shaded with.
- `holes` / `solid`: a mask layer marking where ground is cut out, or where it
exists.
- `height`, `water`, `waterMask`: the raster layers read for the heightfield and
the water surface.
- `resolution` and `layerResolution`: the texel size of the maps and the edge of
one layer slice.
A surface that names no texture is shaded by its `tint`. Writing the same name
again re-bakes in place and keeps the guid, so every reference already pointing
at that landscape stays resolved.
## Reading one
| Method | |
|---|---|
| `:placement()` | `{ size, origin, heightRange }`: where the ground stands and how far it reaches. |
| `:height()` | The heightfield texture. |
| `:holes()` | The holes mask, or nil when the ground is solid everywhere. |
| `:water()` | The water surface texture, or nil when the landscape is dry. |
| `:waterShade()` | `{ texture, depthMax }`, or nil for a landscape carrying no water. |
| `:control(i)` / `:controls()` | The weight textures, in channel order. |
| `:layers()` | The surface table. |
| `:slices()` | `{ albedo?, normal?, mask?, size }`: the layer textures per role, or nil when no layer carries a texture. |
| `:layerSlot(name)` | Which control texture and channel a named layer is in. |
| `:manifest()` | The whole thing, decoded. |
| `:instantiate(target?, opts?)` | An entity carrying a `Terrain` component bound to this asset. |
| `:setChild(role, file)` / `:setPlacement(p)` / `:setLayers(t)` / `:setSlices(s)` / `:setWaterShade(file, depthMax)` | What `terrain.write` fills it through. |
The placement travels with the landscape, so a `Terrain` component standing on
one does not re-state how big it is.
## Validation
`asset.validate` (and the publish gate) refuse a container whose manifest does
not decode, that does not say where its ground stands, that names a child which
is not there, or that declares more layers than its control textures can carry.
A landscape whose heightfield is missing is ground nothing can stand on, and
that is worth catching before it is published rather than when an entity tries
to load it.
## In the Inspector
Selecting a terrain opens its **Terrain** section: its size, origin and height range, the heightfield's resolution once one is written, a table of its material layers (name, tiling, whether it has an albedo), and a link to each child texture (heightfield, holes, water, control maps). A terrain not yet sculpted says it has no child textures.
# Terrain (component)
Ground, as scene content. Part of the
[terrainSystem package](../../readme.md) — every terrain entity in a scene
carries exactly one of these.
An entity carrying this component IS a landscape: a `.terrainData` standing over
a rect of world, drawn through a quadtree of LOD nodes that
refines under whichever camera is looking at it, with a collider the default
avatar can walk.
```luau
-- The whole of it: one reference. The landscape says how big it is.
entity(id).component.add("Terrain", { terrain = dunes })
```
## Properties
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `terrain` | `AssetRef<terrainData>` | `nil` | The landscape this entity IS — the heightfield, the control textures, the holes mask and the layer table, held together as one asset. It states the rect it covers and the reach it declares. |
| `material` | `AssetRef<material>` | `nil` | What the ground is shaded with. Falls back to the package's `terrainStandard`. |
| `size` | `vec2` | `{ 0, 0 }` | Where the ground stands, when it should stand somewhere other than where the landscape says. Left at zero, the container's own placement is used. |
| `origin` | `vec2` | `{ 0, 0 }` | World centre of that rect, same rule. |
| `heightRange` | `vec2` | `{ 0, 0 }` | The vertical reach the geometry declares — `x` the lowest metre, `y` the highest. Left at zero, the landscape's own reach is used. |
| `collision` | `boolean` | `true` | A static heightfield collider over the whole rect. |
| `collisionResolution` | `number` | `257` | Collider samples along each axis. |
`heightRange` is the mesh's bounding box, so a range smaller than the ground
culls the terrain away at grazing angles and the symptom is nothing at all on
screen.
The collider is ONE grid over the whole rect, so `collisionResolution` is its
spacing: 257 over four kilometres is a sample every sixteen metres, and raising
it buys resolution at O(n²) cells to build.
The heightfield's own texel count sets how finely the ground is described inside
the rect; the landscape's `size` sets how much world that description covers.
## Where the landscape comes from
A graph. It opens a rect of ground with `terrain.source`, shapes it with the terrain
nodes (`fbm`, `cone`, `ridge`, `erode`, ...), writes the coverage of each surface as a
mask layer with `terrain.mask`, and ends in `terrain.write`, which bakes the manifest and
every child texture in one step and answers the reference to the `.terrainData`. That is
the same move every other procgen family makes to persist what it made. The reference is
what the component's `terrain` property holds:
`entity(id).component.add("Terrain", { terrain = ref })`.
A heightmap made anywhere else (an 8-bit image, a downloaded elevation tile) comes in
through `terrain.from_texture` and back out through `terrain.write`.
## Lifecycle
- `awake` — opens the landscape, builds the LOD pool, binds it to the entity,
adds the `Model` and the collider.
- `update` / `editorUpdate` — follows the on-screen camera with the cut, in edit
and in play alike.
- `onPropertyChanged` — a `material` change swaps the Model's material in place;
anything else describes the ground itself, so the ground is rebuilt.
- `onDestroy` — releases the mesh, the pool and the heightfield's pixels.
## Methods
| Method | |
|---|---|
| `:heightAt(x, z)` | Bilinear metres. |
| `:normalAt(x, z)` | Unit surface normal, three numbers. |
| `:slopeAt(x, z)` | Radians; 0 is flat. |
| `:field()` | The heightfield itself — the value every `terrainHeightfield` verb takes. |
| `:reload()` | Read `terrain` again and rebuild what the entity draws. |
The queries answer from the same texels the geometry is built out of, so what a
script is told and what a player walks on cannot disagree.
Re-baking the landscape and calling `:reload()` moves every terrain standing on
that asset, without a restart and without touching the scene graph —
the mesh keeps its guid, so the Model is never re-bound.
## One draw
Every node of the LOD cut is one range of one index buffer, so the whole
terrain — at every level, however large — is a single indexed draw on a single
entity. Nodes are never entities. `modules/terrainMesh` is the whole surface.
# TerrainStream (component)
Ground many kilometres across, streamed. Part of the
[terrainSystem package](../../readme.md).
A `Terrain` is one heightfield standing whole. An entity carrying
`TerrainStream` is a landscape divided into square tiles that load, draw and
collide around the places it is needed and are let go of behind them, within a
per-frame budget, so the memory it holds follows the radius it streams rather
than the size of the map.
```luau
-- The ground, as a module: any function of x and z.
-- /source/modules/mountains.module returns { height = function(x, z) ... end }
entity(id).component.add("TerrainStream", {
size = { 8192, 8192 }, tileSize = 512, resolution = 2,
source = "modules.mountains",
})
-- Or at runtime, with no module at all.
local t = entity(id).component.get("TerrainStream")
t:setSource({
height = function(x, z) return 60 * math.noise(x / 900, z / 900) end,
layer = function(x, z) return if math.abs(z) < 4 then "asphalt" else "grass" end,
})
```
The tiles stream around every entity carrying `StreamingSource` (the player, a
vehicle), around the camera, and around what `addFocus` names. The ground near
an entity focus collides, in blocks of `colliderCells` cells; the camera only
draws. The tiles stand under the entity, in its local space: the landscape
moves, turns and scales with it, and the queries below take world X and Z.
## Properties
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `size` | `vec2` | `{ 4096, 4096 }` | The rect the landscape covers, in metres along X and Z. A whole number of tiles each way. |
| `origin` | `vec2` | `{ 0, 0 }` | The rect's centre. |
| `tileSize` | `number` | `512` | Metres along a tile's side. |
| `resolution` | `number` | `2` | Metres between two samples of the ground. `tileSize` is a whole number of them. |
| `source` | `string` | `""` | The identity of a module returning `{ height = function(x, z) -> metres, layer? = function(x, z) -> surface name, layers? = { names } }`: a module of the world's source by its identity (`modules.mountains` for `/source/modules/mountains.module`, `myGame.landscape` for `/source/myGame.package/landscape.module`), or a library module by its `@builtin::` identity. |
| `tiles` | `table` | `{}` | One baked `.terrainData` a tile, `{ ["tx:tz"] = path or guid }`, each baked over `terrainStream.tileSource`. |
| `material` | `AssetRef<material>` | `nil` | What every tile is shaded with. Empty: a baked tile by its own layer table, procedural ground by `terrainStandard`. |
| `collision` | `boolean` | `true` | Collider blocks around entity focus points. |
| `loadRadius` | `number` | `1500` | Tiles within this distance of a focus are drawn. |
| `unloadRadius` | `number` | `0` | Tiles past this distance from every focus are let go of. Zero is 1.25 x `loadRadius`. |
| `colliderRadius` | `number` | `160` | Collider blocks within this distance of an entity focus collide. |
| `budgetMs` | `number` | `4` | Milliseconds a frame may spend on tiles. |
| `colliderCells` | `number` | `32` | Cells along a side of one collider block. |
Distances are measured from a focus to the nearest edge of a tile, the way
`spatialStreaming` measures a cell, and the band between `loadRadius` and
`unloadRadius` keeps a focus resting on a tile boundary from loading and
releasing the same tile every frame.
## What a frame does
- Every tile within reach of a focus is wanted; a tile past `unloadRadius` of
all of them is let go of: its entity, its colliders, its mesh pool and its
heights.
- The collider block under each entity focus is stood up the frame it is
missing, whatever the budget: a body over ground that is not there falls.
`isReadyAt(x, z)` says whether it stands.
- The LOD cut is selected once across every tile standing, so it is balanced
across tile edges and every edge between two tiles is stitched the way an
edge between two nodes is.
- The rest of the work is spent inside `budgetMs`, the ground bodies collide
with first: the collider blocks around entity focus points, one block of
`colliderCells` cells at a time, nearest block first. A procedural block
reads its samples from the source, so collision never waits on drawing; a
baked tile is opened before its blocks, which read its texels. Then the
drawn ground, nearest tile first: sampling a tile's heights (a run of
samples at a time, stopping at the budget), putting them on the GPU and
meshing it. A piece of work that cannot be cut short starts only when what
that kind of work has cost so far fits in what is left of the frame; a
frame after one that ran nothing for that reason starts its first piece
anyway, so work larger than the budget still lands. A far tile's heights
are held only as finely as its finest node draws them.
- A collider block is let go of once it is a block's span further than
`colliderRadius` from every entity focus.
## Seams
Every tile samples one grid of points, `resolution` metres apart from the
rect's corner. Two tiles sharing an edge carry the same samples along it, and
two collider blocks share their edge row, so the drawn surfaces and the
colliders of neighbouring tiles meet sample for sample.
## Several peers
The tiles are local to each engine. Every peer streams the same source, so
peers agree on the ground under every body; a peer that simulates a vehicle
puts a `StreamingSource` on it (or names it with `addFocus`) so the ground
under it collides on that peer.
## Methods
- `stream()`: the `terrainStream` object (`tiles`, `stats`, ...).
- `setSource(source)`: the ground as a table of functions or a module identity.
A ground given in edit stands through play; one given while play runs
stands until play ends.
- `heightAt(x, z)`, `normalAt(x, z)`, `layerAt(x, z)`: the ground at a point,
whether or not its tile is standing.
- `isReadyAt(x, z)`: whether the collider under a point stands.
- `addFocus(target)`: stream around an entity or point too.
- `stats()`: tiles standing, drawn and colliding, bytes held, the frame time
spent and its worst, and the cost of each kind of work.
- `standingTiles()`: every tile standing, with the detail its heights are held at (the `tiles` field names the baked ground).
·computeshader · born here
❒asset # terrain_apply_delta
Folds an erosion pass's accumulated fixed-point deltas into a channel and
clears the accumulator.
An erosion pass never writes height. Every gram of material it moves
accumulates into an integer accumulator in units of 1/65536 m, because integer
addition commutes and float addition does not — the summed result of thousands
of droplets landing on one cell is the same whatever order the threads resolved
in. This pass is where that sum becomes terrain.
It runs once per erosion iteration, after the gather that brings in the
material neighbouring tiles moved across the seam. `atomicExchange` reads and
zeroes in one step, so the accumulator is empty for the next iteration without
a second clearing pass.
One thread per stored cell, apron included. An apron cell's own delta is
applied here as well: the following apron exchange overwrites it wherever a
neighbour owns the cell, and where the field ends and no neighbour does, the
applied value is what keeps the border apron a plausible continuation of the
terrain rather than a step.
·computeshader · born here
❒asset # terrain_apron
Copies a rectangular block of a neighbour tile's stored cells into this tile.
A tile's apron holds a second copy of cells its neighbour owns. An op that is a
pure function of world position writes both copies to the same value on its
own; an op whose result depends on what the tile already held — erosion above
all — leaves the two copies disagreeing, and every neighbourhood read at the
seam then sees a different surface depending on which tile it came through.
This pass makes the apron read what the neighbour computed.
The block is given in stored-cell coordinates (`dstX0, dstZ0`, `srcX0, srcZ0`,
`blockWidth`, `blockHeight`) rather than as an edge index, so one shader moves
an edge band and a corner square. The caller issues one dispatch per neighbour
that exists: eight of them tile a tile's apron ring exactly, with no cell
written twice.
A block facing off the edge of the field has no neighbour and no dispatch. Its
apron keeps whatever the generating op wrote there, which for a world-space op
is the correct extrapolation of the landscape past the field boundary.
Run it after any op whose result is not a pure function of world position, and
between erosion batches — a droplet may walk at most `apron` cells before the
neighbourhood it is reading is no longer the neighbourhood its neighbour has.
·computeshader · born here
❒asset # terrain_coast
Pulls the ground outside a landmass down to a seabed altitude, by a radial
falloff about a point.
The land weight is 1 inside `radius`, falls to 0 across `falloff` metres beyond
it, and the height is interpolated between the seabed and whatever the stack
already built:
```
value = mix(seabed, prev, landWeight)
```
The interior keeps its landscape untouched, the rim slopes off it, and
everything past the rim sits at the seabed. Put the seabed below the sea level
and the result is an island — the terrain ends before the field's rect does,
and the rect goes on carrying the sea.
`shapeMode` picks the metric: 0 is a disc, 1 is the Chebyshev square.
This op covers the **whole field**, not the disc it is centred on: the ground
it changes is the ground outside that disc, and an op that only touched its own
radius would leave the far corners standing above the water.
It is an ordinary op in the stack, so an erosion below it weathers the
coastline it drew, a flatten cuts a harbour into it, and a river carves a mouth
through it.
`terrain_coast_mask` is the same operation driven by a painted raster instead of
a radius, for a coastline whose outline is drawn rather than described.
Every op carries a mask. The op's weight at a cell is scaled by what the mask
reads there — multiplied in, or subtracted out — so the op applies only where
the mask lets it and a cell the mask holds out keeps exactly what it already
held, at every blend mode. An op that names no mask writes at full strength
everywhere.
·computeshader · born here
❒asset # terrain_coast_mask
Pulls the ground outside a landmass down to a seabed altitude, with the
landmass given by a painted raster rather than a radius.
The raster is a layer co-registered with the field — one texel is one cell — so
the outline an author paints is the outline the coast follows. The value is the
land weight directly:
```
value = mix(seabed, prev, clamp(mask * gain, 0, 1))
```
1 is land and keeps the landscape untouched, 0 is open water and sits at the
seabed, and the values between are the shoreline slope: the softness of the
brush is the softness of the coast. `gain` scales the raster before the clamp,
so a stroke painted at a fraction of full strength can still read as solid land.
Like the radial form, this covers the **whole field** — the ground it changes
is the ground the mask does not cover.
`terrain_coast` is the same operation driven by a radius, for a coastline
described rather than drawn.
Every op carries a mask. The op's weight at a cell is scaled by what the mask
reads there — multiplied in, or subtracted out — so the op applies only where
the mask lets it and a cell the mask holds out keeps exactly what it already
held, at every blend mode. An op that names no mask writes at full strength
everywhere.
·computeshader · born here
❒asset # terrain_delta_gather
Adds a neighbour tile's apron-ring erosion deltas into this tile's interior.
A droplet spawned in one tile's interior may walk into that tile's apron and
deposit there. Those apron cells belong to the neighbour — the neighbour's
interior is what carries the field's value for them — so the deposit has to
reach the neighbour's accumulator before either tile folds its accumulator into
height. Without this pass, every cell within a droplet's reach of a seam
collects less material than the same cell away from one, and the seam shows as
a band running through the eroded terrain.
The block is described in stored-cell coordinates, the same description
`terrain_apron` takes, so the two passes address the same geometry from
opposite ends: this one reads the neighbour's apron ring and adds into this
tile's interior band; `terrain_apron` reads the neighbour's interior and
overwrites this tile's apron ring.
The accumulation is a fixed-point integer add. Integer addition commutes, so a
corner cell reached from three neighbours at once sums to the same value
whichever dispatch lands first — which is what keeps a tiled erosion run
reproducible.
·computeshader · born here
❒asset # terrain_erode_droplet
Droplet hydraulic erosion over one tile, one batch at a time.
Each thread carries one droplet. It is placed by a hash of its index, the op's
seed, the batch number and the tile's field-wide base cell, then runs downhill:
it picks up material where it moves fast over steep ground and drops it where
the ground flattens or rises. That is what cuts channels into a ridge and lays
the fans out below them, and it is why the result looks like water shaped it
rather than like a filter ran over it.
## Reproducibility
The pass never writes height. Every change accumulates into `delta` as
fixed-point integers, 1/65536 m per unit, through `atomicAdd`. Integer addition
commutes; float addition does not. Thousands of droplets landing on one cell
therefore sum to the same total whatever order the threads resolve in, and the
same seed over the same stack gives the same terrain every run — on any GPU,
with any scheduling.
A float accumulator would be a few last bits different each run, and everything
downstream that assumes a seed names a landscape (storage, streaming, a shared
world) would drift.
## What one batch may move
A batch works out every droplet's change from the ground as it stood when the
batch began and adds them all at the end, so nothing in a batch can see what
the rest of it is doing to the cell it is standing on. Two bounds are what keep
that from running away.
`share` is the part of one cell's rainfall a droplet carries. A batch rains
`dropletCount` droplets over the interior and each walks up to `apron` cells, so
a cell is stepped on `dropletCount * apron / interiorCells` times a batch and
every one of those steps adds into the same accumulator. Scaling each droplet's
change by the reciprocal is what makes `rain` a sampling density: doubling it
resolves the flow better and moves the same material. Rain too sparse to step on
every cell gives a droplet one cell's rainfall and no more, because a droplet
carrying several cells' worth would put a whole batch's cut into a single step
and reopen the swing below. A run below that density — fewer than
`interiorCells / apron` droplets a batch — therefore samples the flow sparsely
and moves proportionally less; above it the result stops changing with `rain`.
`MAX_SHARE` is the most of the drop below a cell one batch may cut, and the most
of the rise above it one batch may fill. Without it a droplet cuts the whole
drop, which leaves the cell level with the one it drains into — and every other
droplet in the batch cuts that same drop from the same starting ground, so the
cell ends the batch far below its neighbour. The next batch finds the pair
reversed and cuts back further, and the swing grows every batch until the
fixed-point accumulator wraps. `terrain_erode_thermal` names the same bound; a
quarter rather than a half, because water gathers where the ground funnels it
and a cell along a channel is stepped on several times over what an average
cell gets.
## Tiling
A droplet takes at most `maxSteps` steps, and the caller keeps that at or below
the apron width. Past that it would read cells whose real owner is a
neighbouring tile and whose contents have not been exchanged since the batch
began, and the two tiles would disagree about the ground at their seam.
Droplets spawn in the interior only. A spawn in the apron would place a second
droplet on ground the neighbouring tile has already rained on, doubling the
rainfall in a band along every seam. Material a droplet carries out of the
interior and into the apron is not lost: `terrain_delta_gather` adds it into
the neighbour's interior before either tile folds its accumulator into height.
What a droplet is still carrying when its walk ends goes with it. A run
therefore lowers the ground it cuts: material leaves the uplands, and a field
has no basin outside itself for it to arrive in. The fall in the field's mean
is a small fraction of the depth the channels reach.
## Parameters
Lane 1 carries `seed, batch, dropletCount, maxSteps`; lane 2
`solubility, deposition, evaporation, inertia`; lane 4
`minSlope, capacity, gravity, apron`. Heights are metres and horizontal
distances are cells, so `capacity` and `minSlope` read as metres of fall per
cell of run.
Every op carries a mask, and for a simulation it bounds what a step MOVES. The
mask scales both what a step cuts and what it lays down, and the material a step
declines to move stays where it was — in the ground, or in the droplet — so a
droplet carries its load ACROSS ground the mask holds out rather than losing it
there.
What a droplet still holds when its walk ends lands where the walk ended,
whatever the mask reads there. The ground it walked is already lighter by
exactly that much, so scaling that last drop would leave the difference nowhere
and the pass would remove material instead of moving it. The mask therefore
bounds where a run cuts exactly, and where it deposits everywhere except the
cell a walk ends on. An op that names no mask erodes at full strength
everywhere.
·computeshader · born here
❒asset # terrain_erode_thermal
Talus-angle relaxation over one tile, one iteration per dispatch.
Loose material resting on a slope steeper than its angle of repose slides down.
Repeat that and cliffs grow scree fans at their feet while the crests a droplet
pass leaves knife-edged round off — which is why this is usually the finishing
pass over hydraulic erosion rather than a substitute for it.
The rest angle is a world angle. A cell may stand `tan(talus) * cellSize`
metres above a four-neighbour and still rest, so refining the grid resolves the
same slope rather than changing which slopes are stable.
Each iteration is one dispatch: a cell above the rest angle sheds a fraction of
its excess, split across the neighbours below the angle in proportion to how
far below each one is. Half the excess is the ceiling per iteration, above
which the two cells either side of a step trade places every step instead of
settling.
Changes accumulate through the same fixed-point integer path droplet erosion
uses, so the result is equally independent of the order threads land in.
One thread per INTERIOR cell. The apron is read and never written: an apron
cell belongs to the neighbouring tile, which relaxes it itself, and relaxing it
here as well would move its material twice. Material that slides out of the
interior lands in the apron, and `terrain_delta_gather` carries it into the
neighbour before either tile folds its accumulator into height.
Every op carries a mask, and for a relaxation the mask gates the cell that
SHEDS. A cell the mask holds out keeps its own material, and what it still
receives from an unmasked neighbour is the scree that slope throws past the
mask's edge — material moving downhill, which is what this pass does. An op that
names no mask relaxes at full rate everywhere.
·computeshader · born here
❒asset # terrain_fbm
Domain-warped fractal noise written into one scalar channel of one tile.
Gradient noise with quintic interpolation. Sample positions come from the
field-wide cell index, the field's minimum corner, and the seed, so a tile
evaluated on its own is identical to the same tile evaluated as part of the
whole field — and a cell reached as one tile's apron lands on exactly the
world position it has as its neighbour's interior.
`featureSize` is the world size in metres of the largest noise feature.
Halving `resolution` therefore refines the same landscape instead of
producing a different one.
`warp` is a world distance: how far the domain is displaced by two
lower-octave fields before the main fractal is sampled. Non-zero warp is what
turns isotropic lumps into ridgelines and basins.
`ridge` folds the fractal about zero and squares it, giving sharp crests and
rounded valleys.
`blend` decides how the contribution meets what the channel already holds:
`replace`, `add`, `max`, or `min`.
Every op carries a mask. The op's weight at a cell is scaled by what the mask
reads there — multiplied in, or subtracted out — so the op applies only where
the mask lets it and a cell the mask holds out keeps exactly what it already
held, at every blend mode. An op that names no mask writes at full strength
everywhere.
·computeshader · born here
❒asset # terrain_index
Writes one node's triangles into the terrain's shared index buffer. One thread
per quad, six indices each.
The node's vertices live at `vertexSlot` in the vertex pool and its triangles
land at `writeSlot` in the index buffer. Keeping the two apart is what makes a
cut change cheap: a node that survives a cut keeps its vertex slot, so a cut
that only adds, drops or reorders nodes rewrites indices without re-meshing a
vertex, and re-meshing a node for a moved camera rewrites vertices without
touching an index.
The active cut's indices are written as one contiguous run from index 0, so the
whole terrain — every node, at every level — draws as a single indexed draw.
Winding is CCW seen from +Y, the outward side of a heightfield.
·computeshader · born here
❒asset # terrain_layer
Blends a co-registered raster layer into a scalar channel.
A layer shares the field's grid exactly — same tile, same cell, same apron — so
there is no sampling and no transform involved, only a blend. Compare
`terrain_stamp`, which samples a texture through a world-space transform
because the source has a grid of its own.
That exactness is what lets freehand editing live inside a parametric stack.
Brush strokes accumulate into a layer, the layer is one `layer` op among the
others, and changing an op above it re-runs the stack with the strokes still in
place. Painting is an edit to the layer's raster rather than a bake over the
terrain the ops produced.
`opacity` scales the layer before the blend, so the same raster can be dialled
in and out without repainting it. `blendMode` follows the numbering every
terrain op shares: 0 replace, 1 add, 2 max, 3 min.
Every op carries a mask. The op's weight at a cell is scaled by what the mask
reads there — multiplied in, or subtracted out — so the op applies only where
the mask lets it and a cell the mask holds out keeps exactly what it already
held, at every blend mode. An op that names no mask writes at full strength
everywhere.
·computeshader · born here
❒asset # terrain_layer_weights
Evaluates a material layer stack's rules into two RGBA weight textures — layers
0-3 in one, 4-7 in the other, with the eight shares at a texel summing to one.
One thread per destination weight texel. The dispatch is per tile: a tile writes
the texels whose centres fall inside its interior, reading the field through its
own apron-inclusive buffer. Because every read is placed from the field minimum
and the texel size in world metres — never from the tile's own origin — the
texels one tile writes agree exactly with its neighbour's along the seam.
## The six inputs
| input | unit | where it comes from |
|---|---|---|
| `height` | metres, world Y | the `height` channel, bilinear |
| `slope` | degrees, 0 flat / 90 vertical | central difference of `height` over `gradientStep` metres |
| `curvature` | 1/metres, + gully / − ridge | Laplacian of `height`, magnitude ≈ 1/radius |
| `mask` | 0..1 | a bound `mask.<name>` channel, bilinear |
| `noise` | 0..1 | `terrain_fbm`'s gradient noise over world position |
| `channel` | the channel's own stored values | any bound field channel, bilinear |
`slope` and `curvature` are derived here rather than stored, so they cost no
channel and can never fall behind the ground they describe.
`mask` and `channel` both read a named channel through the same four slots and
the same bilinear tap; they differ in what the band is written against. A mask
channel holds 0..1, so its band is in that range. A `channel` band is in the
values the channel stores — cubic metres of deposited sediment, metres of water
depth, whatever the op that wrote it produces — with no rescaling on the way in.
That is what lets a layer put sand where the water actually dropped it and wet
shore where water actually stands.
## A rule is a band, whatever it reads
Every rule carries `min`, `max`, `softness` and `jitter` **in the unit of its own
input**. A layer covers the ground by its `strength` times the product of its
rules; a layer with no rules covers unconditionally. `invert` takes the
complement of the band, which is how "anywhere but here" is written.
`softness` is a feather half-width and `jitter` is how far noise is allowed to
move the value before it is banded — metres on a height rule, degrees on a slope
rule. Each rule draws its jitter from its own noise offset, so two rules on one
layer do not wander in step.
## The layers are a stack
Working from the top down, each layer takes its coverage share of whatever is
still showing. A cliff rule that fully passes therefore leaves nothing of the
grass under it, a layer at half coverage lets half the layer below through, and
whatever no layer claimed goes to layer 0 — the ground the stack stands on. The
eight shares add to one by construction, with nothing divided by a sum, and an
unclaimed texel is a surface rather than a hole.
Rules are numbered bottom-up across the whole stack and each layer reads its own
range of them, so covering downward moves no rule's noise offset.
## Bindings
`params` carries the whole stack: four head lanes, eight layer lanes, then three
lanes per rule. `height` is the field's height tile. `chan0`..`chan3` are the
field channels the stack's rules name, in sorted order — a slot no rule reads is
bound to the height tile and never sampled. Four is what the kernel declares; a
compute stage carries eight storage buffers and `params` and `height` take two,
so six is the ceiling a stack could be given.
`weightsLo` and `weightsHi` are `rgba8` storage targets, which is the widest
format a material slot can sample: a material reads every slot through a
filtering sampler, and the 32-bit float formats are not filterable.
·computeshader · born here
❒asset # terrain_mask
A soft-band rule over one feature of a raster, written into a named layer.
| input kind | value | unit |
|---|---|---|
| 0 height | the surface's own value | world metres |
| 1 slope | the angle of its gradient | degrees from flat |
| 2 curvature | its Laplacian — positive in a gully, negative on a ridge | 1/metre |
| 3 noise | gradient noise over world position | 0..1 |
| 4 curve | distance to a polyline's sampled control points | world metres |
A band is `min .. max`, feathered by `feather` — a soft half-width in the
input's own unit — on either edge; a side left open (`±1e29`) is not a bound
at all. `jitter` perturbs the sampled value before the band, by up to
`jitter` over a gradient-noise field sized `jitterSize`, so the boundary
wanders instead of tracing a contour. `invert` swaps what the band selects.
The `height`/`slope`/`curvature` inputs are derived from the raster's own
"height" layer, read through `op_source` at the same resolution as the layer
this op writes — one indexed read per cell, no resampling. `slope` and
`curvature` are central differences over `step` world metres.
The `curve` input reads a polyline's sampled control points off `op_payload`
— the same array `terrain.project` rides — and measures distance to it in
the ground plane; a control point's height plays no part in the distance.
`blend` decides how the band's weight meets what the layer already holds:
`replace`, `add`, `max`, or `min`. A layer written fresh is usually
`replace`; combining two masks into one accumulated layer is `add` or `max`.
The written value is a weight in 0..1 — a layer this op writes is what
`terrain.mask`'s `name` param names, and any other raster kernel op's own
`mask` param can reference it by that name, multiplied in or subtracted out,
through the generic `op_mask`/`op_common` contract every raster kernel op
already carries.
·computeshader · born here
❒asset # terrain_mesh
Builds one quadtree node's vertices from the heightfield's texels, in the
engine's 23-word vertex layout, straight into the shared vertex pool the
terrain draws from. One thread per node vertex.
Every vertex stands on a stop of the collider's lattice: the field's two edges
and its texel centres along each axis, the stops the terrain's collider holds
its samples at. The node's grid places each vertex, from whole indices counted
from the corner of the rect the cut tiles, and the vertex takes the lattice
stop nearest that place. A node whose vertices are the texel spacing apart or
closer therefore draws exactly the collider's cells, split along the diagonal
from the corner one texel along +Z to the corner one texel along +X, which is
the split the collider and `terrainHeightfield.cellHeight` make; grid vertices
that land on one stop draw triangles with no area. Along an edge stitched to a
coarser neighbour, the strip of cells next to the edge follows the neighbour's
edge. A coarser node draws a subset of the same lattice.
The height is read from the four texels of the cell around a stop through that
same split, and the normal from the gradient the texels carry, blended across
them.
Two edge behaviours make an LOD cut renderable:
- **Stitching.** `coarseMask` names the edges whose neighbour is one level
coarser. On such an edge the odd-indexed vertices snap onto the previous even
one, so the fine edge becomes the polyline through the coarse neighbour's own
vertices. The crack closes because the two edges are then the same line, not
because something covers it.
- **Geomorphing.** Over the far end of the distance band a node is a leaf
across, every vertex slides onto the position it would hold one level coarser.
By the time the cut merges the node into its parent, the node is already
drawing the parent's geometry, so the merge moves nothing. The morph runs off
each vertex's own distance to the camera rather than the node's, so a vertex
two nodes share is placed identically by both.
A vertex is placed from its whole index alone, so every node that holds it, and
every child whose vertex morphs onto it, computes the same bits for it and the
same lattice stop.
·computeshader · born here
❒asset # terrain_project
A curve drives the ground: the points of the curve a geometry holds ride
`op_payload`, and every cell's height is banded by its ground-plane distance
to the resulting polyline — one dispatch, so the channel it cuts has no seam
where a chain of stamps would have met.
Inside `width`, falling to zero across `falloff` metres beyond it, a cell
blends toward a target height:
- `useCurveHeight` set: the curve's own y at the nearest point on the
polyline — a road or a riverbed that follows the curve's authored
elevation.
- otherwise: `fromHeight` .. `toHeight`, interpolated by how far along the
polyline (ground-plane arc length, 0 at the first control point, 1 at the
last) the nearest point falls — a ramp that runs the whole curve's length
in one dispatch instead of one `ramp` stamp per segment.
`blend` decides how the target height meets what the layer already holds:
`replace`, `add`, `max`, or `min`.
The polyline is open — segment `i` to `i+1` for every consecutive pair of the
curve's sampled points — and distance and arc length are both measured in
the ground plane; a point's height does not distort either.
·computeshader · born here
❒asset # terrain_scatter_density
Evaluates one scatter rule into a density channel: **instances per square
metre** at every cell of one tile.
The unit is what makes a rule portable. An author asks for one tree per fifty
square metres and gets that count whether the field is sampled at half a metre
or at four, and whether the terrain is 512 m across or 4 km.
## The rule inputs are the material stack's inputs
| Input | Where it comes from |
|---|---|
| height | the surface channel's own value, world metres |
| slope | the angle of its gradient, radians |
| curvature | its laplacian, 1/m — positive in gullies, negative on ridges and crests |
| mask | a co-registered raster, multiplied in or subtracted out |
| noise | gradient FBM over world position, so a forest edge is not a contour line |
Each band contributes a weight in `0..1` and the weights multiply. A rule that
sets one band and leaves the rest open is exactly that one band, so an author
who understands one understands all of them.
The density written is the peak density times the product of the weights.
## The region
`atX/atZ`, `radius` and `falloff` bound where the rule applies at all, in world
metres. A radius of **zero** covers the whole field — what a ground cover
wants. Outside the region the shader writes nothing, so a second rule's density
on that cell stands.
## The mask slot
`maskMode` picks what the mask raster does:
| mode | effect |
|---|---|
| 0 | multiply — the rule applies only where the mask is painted |
| 1 | subtract — the rule is kept OUT of where the mask is painted |
| 2 | none — the binding is not read |
Subtract is how an authored `place` op's `clearing` keeps a scattered forest
out of the ground a landmark stands on: the placement writes its footprint into
the `placement` channel and the scatter rule subtracts it.
A binding has to point somewhere, so at mode 2 the caller binds the height
channel into the mask slot and the shader never reads it.
One thread per cell, apron included. The cell's world position comes from the
field-wide cell index and the field's minimum corner, so a cell reached as one
tile's apron resolves to the same density as the same cell reached as its
neighbour's interior.
## The op contract's mask
This kernel carries no `op_mask` / `op_common`. Those bind the mask every op
that writes GROUND is scaled by, and this one writes a density channel — it has
the `mask` binding above, with modes and a gain of its own, which is the mask a
scatter rule reads. The two are separate surfaces on purpose.
·computeshader · born here
❒asset # terrain_scatter_place
Turns one tile's density channel into instance transforms in the shared pool
`renderer.mesh.drawInstanced` reads.
One thread per candidate **site**, not per cell.
## Sites are world-anchored
The sites are a grid of `spacing` world metres running from the field's minimum
corner. A site belongs to exactly one tile — the one its centre falls in — so
the same field produces the same sites whatever the tiling, whatever the
resolution, and however much of the field is being re-evaluated.
A site carries an instance with probability `density * spacing^2`. That is what
makes the density channel mean **instances per square metre**: summed over the
sites of an area, the expected count is the integral of the density over it.
One site carries at most one instance, so `1 / spacing^2` is the densest cover
a spacing can express. `terrainScatter` derives the spacing from the density
the rule actually reaches rather than letting a caller find that ceiling by
hitting it.
## Determinism
Acceptance, the jitter inside the site, the yaw and the scale are all hashes of
the site's integer grid indices and the seed. The instance a site carries is
therefore the same instance every run: a reload does not reshuffle a forest,
and re-evaluating one corner of the field leaves the rest of the population
exactly where it stood.
Slots in the pool are handed out by an atomic, so **which** slot an instance
lands in varies between runs. The set of instances and each one's matrix do
not.
## Overflow is a resize, not a short forest
When the accepted count runs past `capacity` the counter keeps counting and the
extra instances are not written. The total it reports is what the caller grows
the pool to before running the pass again, which is how a population that grew
past its reservation ends up complete rather than silently clipped.
## The matrix layout
Column-major 4x4, four `vec4` columns per instance, tightly packed — the layout
a vertex shader reads as `array<mat4x4<f32>>`. Columns 0..2 are the basis
scaled by the instance's scale; column 3 is the translation.
## The op contract's mask
This kernel carries no `op_mask` / `op_common`. Those bind the mask every op
that writes GROUND is scaled by, and this one writes instance transforms. Where
a population may grow is already decided by the density channel it reads, and
`terrain_scatter_density` is where a mask shapes that. The separation is on
purpose.
·computeshader · born here
❒asset # terrain_shape
Blends an analytic shape into one scalar channel of one tile.
One shader covers every placed primitive, selected by a mode lane:
| mode | shape | what it draws |
|---|---|---|
| 0 | disc | a circular pad held at one altitude |
| 1 | square | the same, measured in the Chebyshev metric |
| 2 | cone | a rounded hill, cosine profile, tangent to flat at the rim |
| 3 | ridge | that profile swept along a segment |
| 4 | ramp | an altitude interpolated along a segment |
| 5 | terrace | the existing height quantized into steps |
A negative height on a `ridge` cuts the same profile downward, which is how a
valley or a sunken road corridor is drawn.
The shape's coverage at a cell is a weight in 0..1 — full inside `radius`,
falling to zero across `falloff` metres beyond it — and that weight is what the
blend is applied through. A flatten therefore settles into the terrain rather
than stamping a cliff-edged plateau into it.
Every coordinate is world metres. A shape about a point puts that point in both
segment endpoints; a shape along a line puts the two ends in them. `radius` is
the half-width for the segment shapes. Because nothing here is in cells, the
same op covers the same ground at any resolution.
`blend` decides how the shape's value meets what the channel already holds:
`replace`, `add`, `max`, or `min`. The rounded shapes read best with `add`,
which lays a hill on top of the terrain instead of replacing it; the level
shapes read best with `replace`.
Every op carries a mask. The op's weight at a cell is scaled by what the mask
reads there — multiplied in, or subtracted out — so the op applies only where
the mask lets it and a cell the mask holds out keeps exactly what it already
held, at every blend mode. An op that names no mask writes at full strength
everywhere.
·computeshader · born here
❒asset # terrain_stamp
Blends a source texture into a scalar channel under a world-space transform.
The source is an ordinary `.texture`. That is the point: importing a real-world
elevation model and placing an authored shape are the same operation, so there
is one path for both and no separate importer.
Sampling runs in world space. Each destination cell's world position is carried
into the stamp's local square — translated to `at`, rotated by `rotation`,
scaled so the square spans `radius * 2` metres — and the texture is read there.
A cell outside that square is left untouched, so the op's declared extent is
exact and the evaluator dispatches only the tiles the stamp reaches.
`channelIndex` picks which of the texture's four channels carries the value;
`amplitude` scales it into metres and `bias` offsets it, so an 8-bit greyscale
heightmap in 0..1 becomes real elevation. `falloff` feathers the disc inscribed
in the square, which is what lets a stamp meet the surrounding terrain without
a seam.
Compare `terrain_layer`, which blends a raster that already shares the field's
grid and therefore needs no sampling at all.
Every op carries a mask. The op's weight at a cell is scaled by what the mask
reads there — multiplied in, or subtracted out — so the op applies only where
the mask lets it and a cell the mask holds out keeps exactly what it already
held, at every blend mode. An op that names no mask writes at full strength
everywhere.
·computeshader · born here
❒asset # terrain_water
Fills a water surface over a region of one tile, into the `water` and
`waterMask` channels.
One shader covers every water body, selected by a region-mode lane:
| mode | region | what fills |
|---|---|---|
| 0 | disc | a round body about a point — a lake |
| 1 | square | the same, measured in the Chebyshev metric |
| 2 | path | a body swept along a segment, its surface running from one end's level to the other's — a river |
| 3 | everywhere | the whole field — a sea |
A cell takes the surface only where the ground stands **below** it, so a body
fills the basins its region reaches and leaves ground that rises out of it dry.
That is the whole difference between a lake and a sea: the region an op is
applied to.
## The two channels are one thing
| channel | what it holds |
|---|---|
| `water` | the world-space Y of the surface covering that cell |
| `waterMask` | 1 where a surface covers the cell, 0 where none does |
A mask of 0 is what "no water here" IS — there is no height in the `water`
channel standing in for it, so no authored water level can ever be mistaken for
an absence. Everything that reads water reads the pair.
## Bodies meet through `blend`
Where two bodies reach one cell, the second meets the first through `blend` —
`replace`, `add`, `max` or `min`. The default `max` makes the meeting
independent of authoring order: the higher surface wins whichever op ran first,
so a lake in a coastal basin reads the same whether it was placed before or
after the sea.
Every coordinate and every level is world metres, so a body keeps its size, its
place and its altitude when the field's resolution changes.
Every op carries a mask. The op's weight at a cell is scaled by what the mask
reads there, and here the water mask channel is what carries that coverage: a
cell the mask holds out keeps both channels as they stand, and a partly covered
cell holds part of the surface it already had. An op that names no mask fills at
full strength everywhere.
·computeshader · born here
❒asset # terrain_water_kernel
Fills a water surface over a region of one tile, into the `water` and
`waterMask` layers of a `proc.raster` `Raster` — `terrainOps.water`'s
`lake`, `river` and `sea` share this one dispatch.
One shader covers every water body, selected by a region-mode lane:
| mode | region | what fills |
|---|---|---|
| 0 | disc | a round body about a point — a lake |
| 1 | square | the same, measured in the Chebyshev metric |
| 2 | path | a body swept along a segment, its surface running from one end's level to the other's — a river |
| 3 | everywhere | the whole raster — a sea |
A cell takes the surface only where the ground stands **below** it, so a body
fills the basins its region reaches and leaves ground that rises out of it dry.
That is the whole difference between a lake and a sea: the region an op is
applied to. The ground it reads is `op_source`, bound to the raster's
`height` layer.
## The two layers are one thing
| layer | what it holds |
|---|---|
| `water` | the world-space Y of the surface covering that cell |
| `waterMask` | 1 where a surface covers the cell, 0 where none does |
A mask of 0 is what "no water here" IS — there is no height in the `water`
layer standing in for it, so no authored water level can ever be mistaken for
an absence. Everything that reads water reads the pair.
## Bodies meet through `blend`
Where two bodies reach one cell, the second meets the first through `blend` —
`replace`, `add`, `max` or `min`. The default `max` makes the meeting
independent of authoring order: the higher surface wins whichever op ran
first, so a lake in a coastal basin reads the same whether it was placed
before or after the sea.
Every coordinate and every level is world metres, so a body keeps its size,
its place and its altitude when the raster's resolution changes.
Every op carries a mask. The op's weight at a cell is scaled by what the mask
reads there, and here the water mask layer is what carries that coverage: a
cell the mask holds out keeps both layers as they stand, and a partly covered
cell holds part of the surface it already had. An op that names no mask fills
at full strength everywhere.
·computeshader · born here
❒asset # terrain_water_mesh
Builds one node of the water surface from the `water` / `waterMask` channels,
into the same 23-word vertex layout and the same slot-addressed pool
`terrain_mesh` writes.
## The baked column
Alongside altitude, each vertex bakes the WATER COLUMN — how far the surface
stands above the bed — into the vertex colour's red channel (word 19):
`max(water - height, 0)` where covered, `0` where not, and a sentinel standing
for open ocean on a flat skirt vertex, which has no channel to measure
against. `terrainWater.shader` reads it back as an interpolated attribute
instead of reconstructing it from the scene depth buffer, which a plain
material draws with no depth copy behind it.
## Where a vertex's altitude comes from
| the cell under the vertex | y |
|---|---|
| covered by water | `water` — the surface itself |
| not covered | `min(water, height - sink)` |
The uncovered branch is what ends the surface at the shore. `height - sink` is
under the terrain by construction, so a vertex with no water above it is buried
whatever the water channel holds there; taking the lesser of the two keeps the
plane flat right up to the last covered vertex, so the waterline falls where
the plane and the ground cross rather than on a node boundary.
Coverage is read from the nearest cell rather than interpolated: a cell either
carries a surface or it does not, and half-covered is not a state the surface
has.
## The skirt
A node with `flat` set reads no channel and sits at `flatLevel`. That is the
outer skirt — the ring of nodes carrying the sea from the field's rect out to
the horizon, so an island has water all the way around it instead of stopping
at the grid. Skirt vertices meet the rect's edge vertices at exactly the sea
level, which is what makes the join seamless rather than merely close.
Because the skirt's edge pieces are as long as the field and as deep as the
horizon is far, a node here is a **rectangle**: `nodeSizeX` and `nodeSizeZ` are
separate.
## Stitching
Boundary stitching is the same rule as `terrain_mesh`: on an edge whose
neighbour is one level coarser, the odd boundary vertices snap onto the
previous even one. There is no geomorph — a level surface has nothing to pop.
# terrain guide
The terrain system's reference guide: a landscape composed in code as a procgen graph
— the `Raster` the `terrain.*` ops pass along, the source → shape → mask → realize
shape of a terrain graph, every op in the family, and a worked valley with a river.
Surfaced through `guides { query: "terrain" }` and
`guides { path: "systems/terrainSystem/documentation/terrain" }` (likewise `man`). The
content lives in `guide.md`; this README is the asset summary.
The hand-authoring face of the same system is the `terrain` toolbox
(`search_tools terrain`), which carries its own README.
# terrainSplat (material)
The eight-layer terrain surface. Blends three layer atlases — albedo, normal,
mask — by the two RGBA weight textures the landscape was baked with.
## You normally do not apply this material yourself
`terrainSurface.forLandscape(data)` instances it per landscape: the
`.terrainData`'s control textures bound as the weight maps, its rect as
`field_min` / `field_size`, its packed sheets as the three atlases, and its
layer table as the per-layer tints and tilings. What you apply is the handle
that call returns — and a `Terrain` component does even that for you.
This asset is the same shader with defaults, so it is inspectable and it is what
a hand-authored variant starts from. Applied by hand it has no weight textures
and no field rect, so it renders **layer 0 everywhere** — an unbound stack looks
unbound rather than looking broken.
## Properties
| Property | Unit | What it does |
|---|---|---|
| `field_min`, `field_size` | metres | the rect the weight textures cover; zero size means no weight map |
| `weights_lo_data`, `weights_hi_data` | — | layers 0-3 and 4-7, normalised to sum to one |
| `layer_atlas_albedo` / `_normal` / `_mask` | — | 4x2 atlases, layer N at (N mod 4, N div 4) |
| `atlas_size` | texels | atlas dimensions, for the half-texel inset between cells |
| `layerN_tint` | — | multiplies layer N's albedo sample |
| `layerN_size` | metres | world metres one repeat of layer N's texture covers |
| `weight_sharpen` | — | 1 is the stack as evaluated; higher narrows every transition without moving it |
| `height_blend` | — | 0 blends by weight alone; higher gives a contested pixel to whichever layer's mask height stands proudest |
| `normal_strength` | — | scales the blended tangent-space XY; 0 is flat |
| `smoothness`, `metallic` | — | scale the mask atlas's alpha and red |
## The mask atlas
Each cell is a mask MAP in the usual channel order: **R metallic, G ambient
occlusion, B height, A smoothness**. White is the neutral one — no occlusion, a
level height that leaves `height_blend` with nothing to reorder, and metallic
and smoothness left to the two scalars.
The height channel is what makes a transition look like two materials meeting
rather than a crossfade: where the weights call a tie, `height_blend` gives the
pixel to the layer standing proudest there, so sand settles into the gaps
between cobbles instead of fading evenly across their faces. Layers at equal
height are attenuated equally, so a white atlas comes through unchanged.
## Three atlases, not twenty-four slots
A slot per layer per map would put twenty-four sampled textures in one bind
group, which is past what WebGPU guarantees a shader stage. The atlas keeps the
cost at three whatever the layer count is. `terrain.write` packs the sheets when
it bakes a landscape — a layer names its own `albedo` / `normal` / `mask`
textures and they are resampled into that layer's cell — so the packing is not
something an author does by hand.
A stack with no atlas at all renders its eight `layerN_tint` colours, which is
enough to see every rule working.
# terrainStandard (material)
Built-in default material for the terrain system. Uses the
`~terrainSystem.shaders.terrainBlend` shader, which renders a four-layer
surface blended by altitude and slope, with a procedurally-detailed
cliff override for steep faces.
## Layers
| Slot | Source field | Default | Role |
|------|--------------|---------|------|
| 0 (valley) | `colors.base_color` | green | Low ground, grass, valley floor. |
| 1 (mid) | `colors.layer1_color` | tan | Mid-band: dry grass / scrub between grass and rock. |
| 2 (high) | `colors.attenuation_color` | grey | High band: rock, exposed cliff face. |
| 3 (peak) | `reflectance` (engine default ≈ white) | white | Peaks: snow, ice. |
## Tunables (re-purposed StandardMaterial slots)
| Material field | Shader role |
|----------------|-------------|
| `parallax_depth_scale` | `layer1_height` — Y where layer 1 mid-band peaks. |
| `max_parallax_layer_count` | `layer2_height` — Y where layer 2 rock takes over. |
| `lightmap_exposure` | `layer3_height` — Y where layer 3 snow takes over. |
| `normal_scale` | `blend_width` — width of each altitude transition. |
| `clearcoat` | `slope_start` — cos-θ where cliff blend begins. |
| `clearcoat_roughness` | `slope_end` — cos-θ where cliff fully overrides. |
| `anisotropy_strength` | `detail_scale` — boundary-breakup noise frequency. |
| `anisotropy_rotation` | `(detail_amp, macro_scale)` — boundary amp + macro tiling. |
Authors who want different colours / heights just override the
respective `floats.` / `colors.` blocks in their own material.
## Customizing per terrain
```lua
Material.Create("alpine_meadow", "~terrainSystem.shaders.terrainBlend", {
base_color = { 0.18, 0.45, 0.22, 1 }, -- meadow green
emissive = { 0.35, 0.42, 0.30, 1 }, -- scrub
attenuation_color = { 0.55, 0.55, 0.55, 1 }, -- granite
parallax_depth_scale = 12, -- layer1 (scrub) peak
max_parallax_layer_count = 24,-- layer2 (rock) onset
lightmap_exposure = 38, -- snow onset
clearcoat = 0.7, clearcoat_roughness = 0.3,
})
```
## Caveats
- This iteration paints **solid colours per layer**. Texture sampling
per layer is a planned extension — swap the `mix(albedo, layerN_color, wN)`
in the shader for a tiled `textureSample(layerN_texture, ...)` and
add the texture slots to the material schema. Tracking issue:
filed alongside the terrain system PR.
- Author-controlled splatmap (an RGBA texture that paints which layer
is at each pixel) is similarly planned. The current altitude/slope
blend is procedurally driven only.
# terrainWater
The material a terrain field's water surface draws with — lakes, rivers, the
sea, and the skirt that carries the sea past the field's rect.
It is a plain transparent PBR body of water with no simulation behind it, which
is what makes it correct at every scale the surface is used at: a pond, a river
reach and a horizon all read the same. `terrainSystem.spawn` applies it to a
terrain's water entity unless `waterMaterial` names another.
It draws in the transparent queue after the terrain, with both faces, so the
ground under the surface is already in the frame and the same material is the
underside once the camera goes below the waterline.
Reach for `@builtin::systems.oceans` instead when the water is the subject —
that package simulates a spectrum, whitecaps and shore foam, and wants an ocean
rather than a terrain's water table.
# terrainHeightfield
A `.texture` standing over a rect of world, read as ground.
The durable form of a landscape is an ordinary texture asset — one with a guid,
that a component references, that travels with a world, that the renderer
already knows how to sample. `terrain.to_texture` is what puts a graph's raster
into one and `texture.write` persists it. This module is the other end: what a
runtime holds while that ground is on screen.
```lua
local terrainHeightfield = require("~terrainSystem.modules.terrainHeightfield")
-- A `.terrainData` states the rect it covers, so opening one needs nothing else.
local hf = terrainHeightfield.open("/zero/source/terrain/dunes.terrainData")
-- The heightfield alone can be read too; then the caller says where it stands.
local bare = terrainHeightfield.open(tex, { size = { 4000, 4000 }, origin = { 0, 0 } })
local y = hf:heightAt(120, -60) -- metres
local nx, ny, nz = hf:normalAt(120, -60)
local slope = hf:slopeAt(120, -60) -- radians
hf:close()
```
## The texels
`rgba32f`, in the layout `terrain.to_texture` writes:
| Channel | Holds |
|---|---|
| R | the height, in METRES — it goes negative |
| G | the gradient along world X |
| B | the gradient along world Z |
| A | coverage |
A height's interesting relief is often a few metres inside a range of hundreds,
so eight bits band it into terraces; that is what the precision is for. The
gradient is what a surface normal comes out of without differencing the height
four more times, and it is why `terrain_mesh` costs four texel reads a vertex
rather than twenty.
A heightmap authored anywhere else — an 8-bit image, a downloaded elevation
tile, a raster an earlier graph baked — becomes one of these by going through a
graph: `terrain.from_texture` reads it as ground, and `terrain.to_texture`
writes the result back out at this precision.
## Two carriers, one set of texels
An open heightfield holds the texture in both forms the runtime reads it in:
- **`hf.texels`** — a GPU buffer of the texel bytes, which the
`terrain_mesh` compute kernel builds vertices out of.
- **`hf.cpu`** — the pixels, which `heightAt` / `normalAt` / `slopeAt` /
`sampleGrid` answer from.
Both are the same texels, uploaded from one read, so what the ground LOOKS like
and what a query SAYS about it cannot drift apart.
The pixels are the heightfield's own copy, decoded from the asset when it opens
and held in a buffer for as long as it stands, and so are its holes mask, its
water surface and its layer weights. A heightfield answers in edit, in play and
after play, whatever else loads or unloads the same texture. The cost is one
copy a heightfield: 16 bytes a texel for the heights and the water, 4 for the
holes mask and each layer-weight texture. Two heightfields open on one landscape
hold two copies, which is also what keeps an edit to one (`setHeights`) to
that one.
## The surface
| Verb | |
|---|---|
| `open(ground, opts?)` | Read a `.terrainData` — or the heightfield `.texture` alone — as ground. A container carries its own `{ size, origin, heightRange }`; a bare texture takes them from `opts`, and an explicit opt outranks the container either way. |
| `hf:heightAt(x, z)` | Bilinear metres. |
| `hf:normalAt(x, z)` | Unit normal, three numbers, from the carried gradient. |
| `hf:slopeAt(x, z)` | Radians; 0 is flat. |
| `hf:sampleGrid(cols, rows)` | Row-major heights over the whole rect — the shape a heightfield collider takes. |
| `hf:hasGroundAt(x, z)` | Whether there is ground at all — false over a hole. |
| `hf:refresh()` | Read the texture's pixels again and put them back on the GPU. Bumps `gen`. |
| `hf:close()` | Let go of the pixels and the GPU buffer. The asset is untouched. |
`heightRange` is the vertical reach the geometry declares, which becomes the
mesh's bounding box — a range smaller than the ground culls the terrain away at
grazing angles. Left out, it is read off the pixels, which costs one pass over
them.
## Holes
A landscape can be CUT OUT — a cave mouth, a quarry, a shaft — and its holes
mask is carried the same two ways the heightfield is: on the GPU for the index
kernel, which writes the quads it cuts out as degenerate triangles, and on the
CPU for `hasGroundAt`. Same pixels, so a place with no surface to draw is a
place nothing can be stood on either, and scatter that asks before it places
never drops a tree into a shaft.
A landscape with no mask is solid everywhere and answers `hasGroundAt` true
without reading anything. The edge of a hole follows the mask at the density the
node is meshed at, so a hole is quad-shaped at the LOD it is seen from.
## Generations
`hf.gen` counts the times the texels have been read. `terrainMesh` records the
generation each node was meshed at, so re-authoring the ground under a standing
terrain and calling `hf:refresh()` brings the geometry with it — nobody has to
say which nodes changed.
# terrainMesh
Puts a heightfield on screen as **one draw**.
## What it is
A vertex **pool** of `maxNodes` slots, each a fixed `nodeVerts` square grid
(33x33 by default). A quadtree **cut** decides which parts of the ground are
drawn at which density: near nodes are small and dense, far nodes large and
sparse, and every node meshes to the same grid, so a node costs the same
wherever it is and the cut stays a few hundred nodes at any camera distance.
`terrain_mesh` fills the slots the cut needs; `terrain_index` writes the index
run over them; `renderer.mesh.create { vertexBuffer, indexBuffer }` turns the
pair into one mesh. One entity carries one `Model` bound to it, so the whole
terrain — every node, at every level — is one indexed draw.
**Nodes are never entities.** Entity count is the binding constraint on this
renderer; a per-chunk entity would put a hundred draws where this puts one. A
step that seems to need one is a step that has the design backwards.
A cut change calls `renderer.mesh.update` under the **same guid**, so the Model
is never re-bound and the scene graph is never touched.
## Using it
```luau
local terrainHeightfield = require("~terrainSystem.modules.terrainHeightfield")
local terrainMesh = require("~terrainSystem.modules.terrainMesh")
local hf = terrainHeightfield.open(groundTexture, { size = { 2048, 2048 } })
local mesh = terrainMesh.create(hf, { camera = terrainMesh.cameraPosition() })
terrainMesh.updateCut(mesh, hf, { x = 900, y = 30, z = 900 })
```
The `Terrain` component does all of this from a `.texture` on an entity and
follows the on-screen camera with the cut every frame, in edit and in play
alike. Reach for the module directly only when the ground is not an entity's.
## The vertices
`terrain_mesh` writes the engine's 23-word vertex layout, one thread per node
vertex, positions in WORLD space — so the entity carrying the mesh stands at the
world origin with no transform of its own.
It reads the heightfield's texels as one array over the whole rect. The height
is bilinear across the four texels around a position, and the normal comes from
the gradient those same four texels already carry — four reads a vertex rather
than the twenty a central difference of the height would cost.
## Holes
`terrain_index` writes one node's triangles, and a quad whose ground the
landscape cuts out is written DEGENERATE — six copies of one index — rather than
left out. The run is a fixed stride per node, so a quad that vanished would
shift every quad after it; a triangle whose three corners are one point covers
no pixels and is discarded before it rasterises. A hole therefore costs the
index slots it already owned and nothing else.
The quad's own centre decides, so the edge of a hole follows the mask at the
density the node is meshed at. The run carries the heightfield's generation as
well as the slot order, so a mask re-read under a standing terrain moves the
hole with it.
## Cracks and popping
Two things happen at node edges, and both are in `terrain_mesh`:
- **Stitching.** The cut is *balanced* — no leaf borders a neighbour more than
one level coarser — and each node carries the mask of which of its four edges
do. On such an edge the fine node's odd-indexed vertices snap onto the
previous even one, so the fine edge becomes the polyline through the coarse
neighbour's own vertices. The two edges are then the same line.
- **Geomorphing.** Over the far end of the distance band a node is a leaf
across, its vertices slide onto the positions they would hold one level
coarser. By the time the cut merges the node into its parent, the node is
already drawing the parent's geometry. The morph runs off each vertex's own
distance to the camera, so a vertex two nodes share is placed identically by
both.
## What re-meshes, and when
A node is re-meshed when it is new to the cut, when its coarse-neighbour mask
changes, when the camera moves (the geomorph is camera-relative), or when the
heightfield's `gen` has moved past the generation that node was last built at.
Nothing else touches it — `updateCut` from a viewpoint the cut was already taken
from remeshes zero nodes and reports `changed = false`.
`terrainMesh.invalidate(state, rect)` marks the nodes overlapping a world rect,
for when something changed the ground in one place.
## Nodes that are not drawn
`create` takes a `keep` predicate — `(node) -> boolean` — and the cut then
covers whatever it keeps rather than the whole rect. That is what lets the
terrain END before the grid does: an island's seabed is ground nobody can see,
and ground nobody can see is ground nothing has to mesh.
The predicate is a parameter of selection rather than a filter applied
afterwards, because the node budget is spent on what is **drawn**: a cut that
coarsened to fit nodes it then threw away would be spending the budget on
seabed. It runs after the tree is balanced, so what it drops cannot unbalance
what remains, and a node whose coarser neighbour was dropped has nothing on
that side to crack against.
## Ground that is not square
The quadtree tiles the rect with square **root** nodes. A square rect is one
root; an oblong one is a row or grid of them, sized by `rootSize`, which
defaults to the shorter side. A rect whose sides are not a whole number of
`rootSize` squares is refused by name — pass a `rootSize` that divides both.
## The bounding box
The mesh's box is the heightfield's rect by its declared `heightRange`. It is
taken from the declaration rather than reduced from whatever is drawn, because a
box that is too small culls the terrain away at grazing camera angles and the
symptom is nothing at all on screen.
## Entity binding
`terrainMesh.bind(entityId, hf, state)` records that an entity's geometry IS this
heightfield drawn through this pool. `forEntity` reads it back, `isBound` asks,
and `unbind` releases the mesh, the pool and the heightfield's pixels together.
`followCamera(entityId)` is the per-frame entry the `Terrain` component calls.
# terrainOps
The `terrain.*` procgen op family: a whole landscape (a valley, a mountain
range, an island ending in a coastline, a plateau cut into terraces) built as
a procgen graph.
`M.ops` is the family, keyed by registry id. `procPacks/terrain.procPack`
returns it, and that asset's registration is what puts every op in the proc
registry, the same `.procPack` path any author outside this package uses to
contribute a family. The procgen toolbox's `searchNodes` lists them and `describeNode`
gives one's inputs, params and outputs. The nodes a graph calls as `g.terrain.<name>`
are this package's `terrain.nodebox`.
| Movement | Ops |
|---|---|
| source | `terrain.source`: the rect of ground, in metres, at a resolution |
| shape | `terrain.fbm` · `terrain.cone` · `terrain.ridge` · `terrain.ramp` · `terrain.flatten` · `terrain.terrace` · `terrain.stamp` · `terrain.layer` · `terrain.project` · `terrain.erode` · `terrain.coast` · `terrain.sea` · `terrain.lake` · `terrain.river` |
| mask | `terrain.mask` writes a named layer; every raster kernel op reads one through `mask` / `maskMode` / `maskGain` |
| bake | `terrain.write`: the whole landscape as a `.terrainData`: heightfield, control textures, holes mask, layer table |
| carry | `terrain.to_texture` · `terrain.from_texture`: one layer in and out of a `.texture` |
| stand | `terrain.scatter` · `terrain.drape`: points onto the surface, and anyone else's points back down onto it |
The value every op takes and returns is a `proc.raster` **`Raster`**: a tiled
GPU grid carrying named scalar layers (`height`, `water`, `waterMask`, and
whatever `terrain.mask` names).
## What is in here
| File | Role |
|---|---|
| `init.luau` | Assembles the family into `M.ops`. Owns `terrain.source`. |
| `shape.luau` | The placed shapes: `flatten`, `cone`, `ridge`, `ramp`, `terrace`. |
| `noise.luau` | `terrain.fbm`: multi-octave, optionally domain-warped fractal relief. |
| `mask.luau` | `terrain.mask`: a soft band over one feature, written to a named layer. |
| `project.luau` | `terrain.project`: a curve driving the ground by distance to its polyline. |
| `surface.luau` | `layer`, `stamp`, `coast`: an external source blended into height. |
| `water.luau` | `lake`, `sea`, `river`: one water body over three regions. |
| `erosion.luau` | `terrain.erode`: droplet and thermal erosion batched over the tiles. |
| `convert.luau` | `to_texture`, `from_texture`: a raster layer and a texture are the same field in two carriers. |
| `write.luau` | `terrain.write`: the whole landscape as one `.terrainData` container. |
| `scatter.luau` | `terrain.scatter`, `terrain.drape`: a point cloud off the ground, and points put back on it. |
| `scene.luau` | `terrain.ground`: the ground the scene's Terrain entities stand on, which `drape` sets points on. |
| `curve.luau` | A `Curve`'s control points flattened for the payload binding, shared by `project` and `mask`. |
| `lanes.luau` | The eight vec4 dispatch lanes a placed-shape kernel's numbers are packed into. |
| `modes.luau` | The shader numberings the ops declare. |
`M.packers` and `M.payloads` expose each kind's dispatch numbers with no GPU
dispatch and no readback, which is what a test verifies the maths through.
## Out of the graph
A landscape leaves as a `.terrainData`: the heightfield, the control textures
saying which surface covers where, the holes mask, the water surface and the
table of what those surfaces are, held together as one asset. `terrain.write`
takes the raster, a `name`, and the `layers` (the mask layer names holding each
surface's coverage, in channel order), and answers the reference to the container.
Its children are ordinary `.texture` assets written inside the container, so
nothing here is a private format and a material samples a control texture the
way it samples any texture. A `Terrain` component standing an entity on that
container is what puts the ground in a scene.
`terrain.to_texture` carries ONE layer out on its own, for a raster that is
feeding something other than a landscape; `terrain.from_texture` is the way back
in, and it is how a heightmap made anywhere else becomes ground these ops can
carve.
# terrain
Ground as nodes. `terrain.source` opens a rect of ground as a tiled raster with a `height` layer; every other node
takes that raster, changes it and hands it on.
| Part | Nodes |
|---|---|
| the ground | `source`, `from_texture`; a landscape asset comes in through `io.import` |
| landforms | `flatten`, `cone`, `ridge`, `ramp`, `terrace`, `coast`, `layer`, `stamp`, `project`, `fbm` |
| rules | `mask` writes a soft band over height, slope, curvature, noise or a curve into a named layer; any node's `mask` input names it |
| water | `lake` (floods the seed's basin until it is dry), `sea`, `river` |
| weather | `erode`: droplets walked on the device with integer sums, or thermal relaxation |
| standing on it | `scatter`, `drape` |
| out of the graph | `landscape` (one element standing as the ground, the `Terrain` component on it), `write` (the same landscape saved as a named `.terrainData`), `to_texture` |
The landforms are one pattern: a coverage weight (`footprint`), a profile of the distance (`dome`) or a value, and
`core.raster.blend`. Parts the nodes share: `footprint`, `dome`, `mask_weight`, `nearest` (the nearest point of a
curve), `surface` (height, normal and slope under a position), `bilinear` (a grid of samples read between the four
around a point), `texel` and `image` (a layer read per texel of an image, and the image's texels), `dimensions`, `wet`
(water into a region), `control` (normalized surface weights packed as 8-bit words).
The raster holds more ground than its rect: the tiles store the cells of a border tile past the rect and a ring of
cells around the tiles, and every landform and every blend writes them at their own world positions, as the tiles of
the ops did. A node that reads at an offset (`cell` with `dx`, `dz`, the slope and curvature of `mask`, thermal
`erode`, the walk of a droplet) reads those cells, so ground beside the edge slumps, drains and washes into them.
The water floods (`lake` fill) stay inside the rect.
A call answers a node's output directly when it has one (`g.terrain.cone({ ... })` is the raster) and a record when it
has several (`g.terrain.footprint({ ... }).weight`).
`nearest` holds the module that reads a curve's polyline. `landscape` and `write` name the landscape record kind
(`records/terrain.procRecord` of this package), whose module declares the surfaces and writes the container.
The noise terrain's relief and masks are made of is the noise prelude of `kernels/` (`zero_angle_fbm2` and
`zero_angle_warped2` for `fbm`, `zero_proc_gradient` for the noise mask), called through `field.call`. Erosion droplets are a step Field handed to the `walk` kernel,
their changes summed with the `scatter` kernel.
# Landscape (procNode)
The last node of a terrain graph: the ground as one element. It is
`terrain.landscape` under the name a graph ends a landscape with: the raster goes
in, and one point comes out carrying the `Terrain` component, its `terrain` a
landscape record holding the heightfield, the control textures, the holes mask,
the water surface and the table of what the surfaces are.
`io.output` stands the element as an entity with the ground, its water and its
colliders, and `io.export` saves it with its `.terrainData` inside the asset it
writes. A terrain among other elements is merged and parented like any element.
## Inputs
- `raster` (Raster): the ground.
- `name` (String, default `"landscape"`): the entity's name, the element's `name`.
- `height` (String, default `"height"`): the raster layer written as the
heightfield.
- `water` (String, default `"water"`) and `waterMask` (String, default
`"waterMask"`): the raster layers holding the water surface and where it covers.
- `layers` (String, default empty): the surfaces, in channel order, as the names
of the raster layers holding their coverage, separated by commas:
`"sand, grass"`.
- `surfaces` (List of records, optional): what each surface in `layers` is
shaded with, `{ name, tint?, albedo?, normal?, mask?, tiling? }`, matched to a
layer by `name`.
- `holes` and `solid` (String, default empty): a mask layer marking where ground is
cut out, or where it exists.
- `resolution` (Int, default 0): texels across the longer axis; 0 takes one texel
per raster cell.
- `layerResolution` (Int, default 0): the edge of one layer slice; 0 takes the
largest edge any layer map carries.
- `fields` (Record, optional): fields of the `Terrain` component beside the
ground: `material`, `waterMaterial`, `collision`, `collisionResolution`, `size`,
`origin`, `heightRange`, `water`.
## Output
- `terrain`: one element carrying `Terrain`, standing as a landscape.
# terrain
Ground, as procedural operations. `terrain.source` opens a rect of ground as a
tiled `Raster` with a `height` layer; every other op in the family takes that
`Raster`, changes it, and hands it on — so a valley, a mountain range, an
island and a coastline are all the same graph with different nodes in it.
`terrain.write` carries the result back out — into a `.terrainData` that a
`Terrain` component stands an entity on.
Every op is an ordinary procgen op, so any graph can reach them and they
compose with the rest of the registry: the shaping ops take a `Raster` and hand
one on, and the ops that stand things on the ground hand back points.
## Ops
**The ground**
- `terrain.source` — the rect a terrain graph starts from: a tiled raster with
a height layer, at a chosen resolution.
**Shapes and noise**
- `terrain.fbm` — fractal noise over the height layer; ridged or plain.
- `terrain.flatten` — level the ground inside a placed shape.
- `terrain.cone` — a cone or crater at a placed centre.
- `terrain.ridge` — a raised or cut ridge along a placed line.
- `terrain.ramp` — a graded slope between two heights.
- `terrain.terrace` — quantize height into steps.
**Rules and blending**
- `terrain.mask` — write a soft band over one feature (height, slope, or
distance to a curve) into a named layer.
- `terrain.project` — band height by distance to a curve.
- `terrain.layer` — blend a painted layer into height.
- `terrain.stamp` — blend a texture into height at a placement.
- `terrain.coast` — pull the ground outside a landmass down to the shore.
**Water**
- `terrain.lake` — fill a basin to a water level.
- `terrain.river` — carve and fill a channel along a curve.
- `terrain.sea` — flood everything below a level.
**Erosion**
- `terrain.erode` — hydraulic (droplet) or thermal erosion, batched over the
raster's tiles.
**Standing things on it**
- `terrain.scatter` — points across the surface, weighted by a mask layer and
banded by slope and height, each carrying the normal and slope under it.
- `terrain.drape` — anyone else's points put down onto the ground: a graph's
own `raster`, or the scene's `ground`.
- `terrain.ground` — the ground the scene's Terrain entities stand on, as a
graph value read afresh every frame. Wired into `terrain.drape`, points land
on it wherever the graph stands, each marked `grounded`, and a Generator sets
them again when the ground is rebuilt or the Generator moves.
**Out of the graph**
- `terrain.write` — the whole landscape as a `.terrainData`: the heightfield
(`rgba32f`, R metres, G/B gradient), the control textures saying which surface
covers where (`rgba8`, four layers a texture, weights normalized to sum to
one), the holes mask, and the table of what those surfaces are.
- `terrain.to_texture` — ONE layer as a Texture on its own, for a raster feeding
something other than a landscape.
- `terrain.from_texture` — the way back in, and how ground made anywhere else
becomes something these ops can carve.
```luau
g:node("bake", "terrain.write"):input("raster", ground)
:param("name", "dunes"):param("layers", { "sand", "rock" })
```
Then a `Terrain` component standing on that landscape is what puts it in a scene.
# landscape (procRecord)
The landscape record kind: the ground a raster becomes, with every texture it needs read back from the device (the
heightfield, the holes mask, the water surface and its shade, one control texture per four surface layers), the
surfaces it declares, and the rect it covers. `terrain.landscape` answers one, held on one element as
`Terrain = { terrain = <record>, ... }`.
Live, a record becomes a `.terrainData` written in the session's scratch folder, the asset the `Terrain` component's
`terrain` field stands on; it is removed once nothing stands on it. Saved, it is a `.terrainData` container with its
textures as `.texture` children inside it, written inside the folder of the asset being exported. `terrain.write`
saves one under the name and folder it is given.
`landscape.module` is the writer both use: `declare` states the surfaces a layer list names, and `writeLandscape`
writes the container.
# Package (asset type)
A package is an asset — a `<Name>.package/` folder that bundles
components, modules, scenes, presets, tools, and any other typed
sub-assets under one identity, installed into a world as one unit.
Packages are how authoring sets get distributed across worlds.
A **library** is the addressable root scope content resolves under
(`@builtin`, `@<name>`) — a whole world mounted into this one under a
namespace, via `zeromind.installLib` / `world.installLibrary`. A package is
an asset that lives inside such a root, and one library holds as many
packages as its author put there. A package is installed by guid the
way every asset is (`zeromind.installAsset`).
## When to use one
- You're authoring a coherent feature suite (character controller
stack, voxel engine, UI kit, animation library) and want to ship
it as one install target.
- You want a stable identity worlds can install and resolve sub-assets
against (`@<lib>::pkg.sub_module`).
- You want versioning + a per-package metadata layer that travels with
the contents.
If you're authoring inside a single world and don't need a separate
install lifecycle, you don't need a package — drop your `.component`
/ `.module` / etc. directly in `/zero/source/`. Packages are for
cross-world distribution.
## Where it lives
- Source: `/zero/source/.../<Name>.package/` (or, for legacy
packages, a folder containing `package.yaml` at its root).
- Identity: `<Name>` (the `.package` suffix collapses in identity —
mirrors `.module`). A file at `<Name>.package/<sub>/init.luau`
resolves under the same identity it would at `<Name>/<sub>/init.luau`.
- Folder shape (container-style — open child set):
- `package.yaml`: metadata (name, version, description) and the
packages it depends on (`dependencies`). **Required.**
- `README.md` — instance documentation. **Required.**
- `.metadata` — agent-editable tags + free-form fields. **Required.**
- Sub-asset folders: any combination of `.component/`,
`.module/`, `.scene/`, `.preset/`, `.tool/`, `.toolbox/`,
`.material/`, `.shader/`, etc.
The validator permits unlisted children (`allow_unlisted: true`) by
design — packages are open containers.
## How to create one
```luau
asset.create("package", "<Name>")
-- Creates: /zero/source/<Name>.package/
-- package.yaml (metadata scaffold)
-- README.md (instance README template)
-- `folder` places it in a subfolder of /source instead of the root:
asset.create("package", "<Name>", { folder = "systems" })
```
Then scaffold sub-assets inside it:
```luau
local pkg = asset.resolve("<Name>.package")
asset.create("component", "Spinner", { into = pkg })
asset.create("module", "util", { into = pkg })
```
## `package.yaml`
```yaml
name: inkBlocks # optional; the folder stem when absent
version: "1.0.3"
description: "Ink and paper style blocks"
dependencies:
- { name: starterBlocks, version: "1.0.8" }
- styleKit # a bare name states no version
```
`dependencies` lists the packages this package needs. Each entry's `name` is
a reference the package records, the same kind of reference a `require` or an
asset name in a data file makes:
- **It resolves in the author's world.** Install the dependency here first
(`zeromind.installAsset`); the entry then names the copy this world holds.
A push refuses a package whose dependency resolves to nothing, naming it.
- **An install brings it along.** Installing the package installs every
dependency it lists, at the version this world holds when the package is
pushed, under `/source/deps/<owner world>/`, deduplicated against what the
installing world already holds. `zeromind.preview` lists each one before
anything is written, and the install's `deps` lists what it brought.
Uninstalling the package leaves a dependency that another installed
package still uses.
- **`version` is checked, not chosen.** A stated `version` has to equal the
`version` the dependency's own `package.yaml` states in this world;
`asset.validate` (and so the push) refuses the package naming both when
they differ. To depend on another version, install that version here.
## How it operates
1. **Registration.** Writing `package.yaml` indexes the package with
the package registry.
2. **Install.** A package is read before it is installed: `zm ls-tree
@<guid>` in the shell lists what it contains, and `zm show
@<guid>:<path>` and `zm grep <pattern> @<guid>` read it, so candidates
are compared by what they do. The one that fits is installed by guid,
the way every asset is: `zeromind_install` over the engine's MCP, or
`tools.use("zeromind", "installAsset", <guid>)` inside the engine. The
install writes the package's files (and the deps its `.refs`
record, the packages its `package.yaml` lists under `dependencies`
included) into this world's source, so its sub-assets resolve here.
`tools.use("zeromind", "preview", <guid>)` reports what an install would
write before it writes anything.
3. **Sub-asset resolution.** Because `.package` collapses in
identity, sub-asset paths work whether the package folder is
suffixed (`<Name>.package/<sub>`) or not (`<Name>/<sub>`).
4. **Updates.** `tools.use("zeromind", "updates")` lists installed content
with upstream changes available; re-running the install pulls them.
5. **Metadata.** `package.yaml::name` overrides the folder stem if
present (rare); `version` + `description` are surfaced in the
package index.
6. **Hot reload.** Edits to sub-assets hot-reload as normal; edits to
`package.yaml` reload package metadata.
## Discovery
- `asset.list("package")` — every registered package.
- `asset.inspect("<name>")` — sub-asset catalog, source, this type
README.
- `ls /zero/source/<Name>.package` lists the files the folder holds.
- `library.list()` — the library assets registered in this world,
including the ones packages under an installed library contribute.
## Authoring conventions
- Name packages by their feature (`voxels`, `cameraRigs`,
`dialogSystem`), not by their owner — the host library is the
`@<lib>::` scope prefix.
- Cluster cohesive sub-assets together. A "character controller"
package should ship the controller component, its preset configs,
its supporting modules, and a demo scene — not just the component.
- Document the package's surface in `README.md` — what install
installs, what each sub-asset does, how they relate. Agents reading
`cat /zero/source/<Name>.package/README.md` get the full picture in one shot.
- Version conservatively — breaking a package means breaking every
world that installed it. Use semantic version bumps.
## Common pitfalls
- **Missing `package.yaml`.** Without it the folder isn't recognized
as a package even if it has a `.package/` suffix.
- **A dependency named only by a string the code builds.** A package
whose files reach another package only through a name assembled at
runtime records no reference to it, so an install arrives without it.
List that package under `dependencies` in `package.yaml`.
- **Identity collapse.** Sub-asset paths skip the `.package` suffix.
Don't include `.package` in `require` strings.
## Referencing inside a package: the `~` rule
`~` means **this package**, resolved structurally from the referring file's own
location. It is the only package-specific syntax. It is rename-safe (no package
name is ever written) and library-agnostic (it never names the host library, so
it survives `@builtin` being renamed or the package being mounted elsewhere).
`~.tail` maps to `<pkg>.package/tail` from anywhere inside the package.
| Referencing from | Use | Example |
|---|---|---|
| An asset/module **inside this package** | `~.tail` | `require("~.modules.myThing")`, `shader: "~.shaders.myThingBlend"` |
| A **different package** under the same root | root-relative identity | `require("@root::systems.otherPkg.modules.x")` |
| A **different library's** public API (a deliberate external dependency) | `@otherlib::…` | `require("@physics::modules.rigidbody")` |
**Never write `@builtin::` (or any host-library name) to reference your own
package** — that couples the package to the library it currently lives in and
breaks if the library is renamed or the files move. Use `~.`. It works from
Luau `require()` and from asset-reference strings in data files (a material's
`mat.yaml` `shader:` field, a component's default asset ref); the engine
resolves it against the referring file's own package. `asset.resolve` takes a
`~.` name the same way, against the package of the file that calls it:
`asset.resolve("~.materials.myThingStandard", "material")`.
`~.` is the package-relative self-reference; `@root::` is the root-relative one
(the library root, or the world source tree for non-library content). The
`core/requiring-modules` guide covers the root model in full.
A package that ships substantial functionality should include a `.guide` asset
(a `<name>.guide/guide.md` folder anywhere under the package); the `guides` tool
discovers it live, so collaborators find the system the way they find built-in
guides. A package's test suite reaches the shared `Test` framework through its
enclosing asset: `local Test = asset.containing(__FILE__).modules.shared`
(`asset.containing` returns the deepest enclosing typed asset, the `.testSuite`,
not the outer `.package`).
## Related types
- Any of `.component`, `.module`, `.scene`, `.preset`, `.tool`,
`.toolbox`, `.material`, `.shader`, `.style`, `.service`,
`.importer` — packages bundle these.
## In the Inspector
Package: the name, version and description package.yaml declares, how many things the package holds and what they are, and **Open** for package.yaml. A package.yaml that does not decode is listed under Problems with the decoder's error.
# Generator
A live procedural generator: it binds a `.procGraph`, plays the graph's
timeline and stands each of the graph's outputs in the scene as children of
the entity it is on. Change a param and the output is made again in place; in
edit and in play alike, nothing it makes is written to the world's source.
```lua
local graph = asset.create("procGraph", "crate") -- a box with a `size` input; edit its init.luau
local e = entity.spawn("crate")
e.component.add("Generator", { graph = graph })
e.component.get("Generator").params = { size = 2 }
```
## Fields
| Field | What it does |
|---|---|
| `graph` | The `.procGraph` it plays. |
| `params` | Overrides for the graph's declared inputs, by name. |
| `output` | One declared output to stand; empty, the default, stands every output, one root each. |
| `playOn` | What starts its clock in play mode: `"auto"` (default) plays a timeline with no end as soon as it binds and holds any other on its first frame; `"load"` plays on bind; `"manual"` waits for `play()`. In edit mode a Generator is not set to play, whatever `playOn` says: it shows the frame it is on until it is played. |
| `ending` | `"graph"` (default) does what the timeline declares; `"loop"` plays the range round and round; `"hold"` plays it once and stays on its last frame. |
| `speed` | How fast its clock runs: 1 its own rate, 0 still, negative plays a timeline with an end backwards. |
| `diskCache` | Whether the outputs the graph caches load from, and are written to, the world's disk cache. On by default. |
| `cookBudgetMs` | Milliseconds of cook work a frame before a cook yields to the next frame. 5 by default. |
## Methods
`play()`, `pause()`, `seek(seconds)`, `seekFrame(frame)`, `loop()`, `hold()`,
`setFps(fps)`, `setRange(first, last)` and `setWallTime(on)` drive its clock,
on every peer. `getFrame()`, `getTime()`, `describeGraph()` and `playback()`
read it; `playback()` also says what running the graph costs and how each
output is drawn. `cancelCook()` stops a cook in flight and leaves the last
output standing. `apply()` swaps the generator for the asset it makes: the
frame being presented becomes a bundle, and an `Asset` component standing it
takes the Generator's place. The `onFinished` event fires when a range played
once reaches its end.
A timeline that runs in real time (`realTime = true`) has an end when its graph
states `frames` beside it (`g:timeline({ realTime = true, fps = 60, frames = 180 })`)
or when `setRange` gives it one; with neither it runs on and never finishes.
When the playhead of one with an end reaches its last frame, `ending` applies as
it does to any timeline: `"graph"` loops where the graph declares `loop` and
otherwise holds on the last frame and fires `onFinished` once; `"loop"` plays it
again from the start, a fresh simulation each time, without firing it; `"hold"`
stays on the last frame. `play()` plays a held one again from its start.
`playback().ends` is whether the timeline has an end, and `last` is its last
frame. A real-time timeline begins again, by `seek(0)`, a loop or `play()`, in
the runs its solvers hold on the device: a restart costs a rewrite like any
other present, and opens no cook.
## Under an effect
An effect's play runs each graph of its entry on a Generator, a child of the
play's entity. `effectRef:play(entry, opts)` and the `Effect` component make
them and drive their clocks together, so a shot, an impact or a cast is a play
of an effect (`guides { path = "systems/effects/documentation/visual-effects" }`). The play's own
`playback()` adds its Generators' costs together.
Every play of one graph with the same params plays the frames the first one
cooked. A Generator of a graph with `playOn = "manual"` cooks every frame of the
timeline while it waits, and `playback().cookedThrough` reaching
`playback().last` says every frame is cooked.
## Related
- `guides { path = "systems/procgen/documentation/procgen" }`: graphs, ops,
timelines, effects and the live generator in full.
- The `procgen` toolbox: `spawn`, `adjust`, `apply`, `generators`, `cooks`,
`frames` and `onPresent` work on generators by entity, and `play` plays a graph on the preview stage.
# abi
The ops every procgen node is built from. Each states where it runs in its `runs` field, and none calls another op.
## Ops
| Op | Runs | Does |
|---|---|---|
| `device.kernel` | `gpu` | Runs a `.computeShader` over a domain of elements and returns what it wrote as columns |
| `transfer.upload` | `cpu_to_gpu` | Turns CPU numbers, lists and tables into device columns |
| `transfer.readback` | `gpu_to_cpu` | Turns device columns and live counts into CPU values, a frame or more later |
| `cpu.call` | `cpu` | Calls a Luau function with declared return types and a time ceiling |
| `cpu.read` | `cpu` | A `cpu.call` over state outside the graph, with a digest of what it reads |
| `device.bound` | `gpu` | Answers a count as a live count that states the most it can reach |
| `device.live` | `gpu` | Answers a column carrying a count, so a reader of it takes the live elements only |
| `device.guard` | `gpu` | Holds a cook to a condition the device answers, checked a cook later when a recorded list replays |
## device.kernel
`kernel` (param) is the shader, a reference resolved from a literal where the node is written
(`asset.resolve("<identity>", "computeShader")`; a name is refused); `specialize` (param) gives each of its `specialize`
parameters one of its options. `bind` (input) gives every binding its value and `domain` (input) says how many
elements run. `uniforms` (input) gives the shader's params by name, each a Number or a Bool, and a name the shader does
not declare is refused; `step` (input) gives numbers the shader takes where it declares a param of the same name (`dt`,
`time`, ...) and leaves the others out; `columns` (input) is a table of Columns, or a class of a Geometry, whose
columns every read binding left unbound takes by its own name.
- A buffer binding takes a Column. A binding the shader writes and that is bound to a Column writes a copy, and the
copy comes back under the binding's name.
- A `columns` binding takes a table of Columns, packed on the device into one buffer in the order the binding's
`order` field states (`order: "shapeKind center extent"`; the table must hold exactly those columns), and in
sorted-name order where the binding states none. A kernel that reads its columns by slot number states `order`;
one that reads them by `NAME_col_<column>` needs none.
- `specialize` takes the table `fieldc.walkSpec(step, opts).specialize` states as it stands (the walk kernel's
`WALK_STEP`, `WALK_STEP_HI`, `WALK_DONE` and `WALK_PRELUDE`), and the resources the spec runs over come from
`fieldc.bindSpec(spec)`: its `cols` (a table of columns already packed on the device) binds to the kernel's `columns`
binding and its `u` (the numbers the chain carries, a device buffer) to its read `buffer` binding. The caller that made
them lets them go (`bound.release()`) once the kernel's outputs are no longer read.
- A Field bound to a binding is evaluated by the field compiler (`fieldc.evaluate`) in one fused dispatch over the
domain, and the column it lands in stands as long as the outputs of the kernel do.
- `bind.columns` is a domain (a record of Columns, or a class of a Geometry): every read binding the bind left
unnamed takes the column of the same name, and an unbound `columns` binding takes every column of it.
- An appended output bound to a column that an append returned writes a copy of it, and the copy keeps the live
count that column carries, so a later pass appends after what an earlier one appended.
- A param takes a Number or a Bool (1 or 0). A Vec spreads over `<param>_x` to `<param>_w`. `count`, `time` and
`frame` are names any node may give; a kernel that declares none of them leaves them out.
- A Texture binds its `texels` column, and sets `<name>_width` and `<name>_height` where the shader declares them.
- `domain` is a Number, a Column (its elements), or a live count: a count Column, an appended output, or the record
`{ count = <one-word Column>, capacity = <Number> }`. A live count dispatches over its capacity, and the shader reads
the count through a `buffer` binding named `domain_count`. A shader that declares `domain_count` over a plain number
gets the number in it. The live-count form is stated in "Over a live count" below.
- An output with `count` or `capacity` is allocated before the dispatch. An appended output comes back as a Column
(its `_capacity` and `_liveCount` set) and as `<name>_count`.
Output: `columns`, a Record holding every binding the shader wrote as a Column by binding name, and for an appended
output `<name>_count`, its live count.
Column elements are 32-bit words: an `Int` column holds `i32`, a `UInt` column `u32`, every other type `f32`
(`column:element()`). A kernel's `i32` and `u32` outputs are `Int` and `UInt` columns. The first run of a kernel
waits for its program to be built, so a readback asked for after the run sees what the dispatch wrote.
A column whose words are not the ones a binding reads is cast by value before the dispatch: a Float column meeting an
`i32` or `u32` binding (and an Int or UInt column meeting an `f32` one) is read as the numbers it holds, not as the bits
of its words. The cast covers read bindings and in-place bindings; what a kernel writes back through an in-place binding
is the column of the binding's type (`Int` for `i32`).
## Over a live count
A kernel runs over a plain number of elements (every slot live) or over a capacity whose live count sits in a
one-word device buffer. A node that deletes, welds, expands or pairs elements states the count it made on the device,
and the next kernel takes it as its domain, so the count never crosses to the CPU between them.
**What a node passes.** `domain` is the count and the capacity it is bounded by: `found.indices.count` (the count of an
appended output, whose capacity the output carries), an appended output Column itself, or the record
`{ count = <one-word Column>, capacity = <Number> }` for any other count held on the device (a scan's `total`, a
count made by `proc.device.count`; its words are read as u32). A Number is the plain form, as before.
**What a kernel declares.** In `bindings.yaml`, a count binding that exists only in the variant that reads a count, and
the specialization that picks the variant:
```yaml
bindings:
- { name: domain_count, kind: buffer, access: read, element: u32, when: "COUNT=device" }
specialize:
- { name: COUNT, options: [plain, device] }
```
`device.kernel` runs the `COUNT=device` variant when the domain is a live count, and the `plain` variant (the bindings
and the code the kernel always had) when it is a Number. A kernel that declares `domain_count` without `when` reads it
in every variant, and gets the number in it over a plain domain.
A count that is arithmetic over live counts (`geometry.point.count * 8`, `min(kept.count, room)`) is a domain too when
the arithmetic only grows with the counts it reads: the dispatch covers the most it can reach and the kernel reads the
count the arithmetic comes to, evaluated on the device. Arithmetic that takes a count away from something is read
back where the dispatch needs the number.
**An output sized by a live count.** An output whose `count` states a name that is no param (`count: "nv"`) takes its
size from the `bind` of that name. Bound to a number, the output holds that many elements. Bound to `{ live = <count> }`,
where the count is a count Column that carries its capacity or arithmetic over counts that only grows with them, the
output is allocated for the capacity and carries the count as its live count, with nothing read back.
**What the shader does.** The dispatch covers the capacity (`zero_n()`), outputs sized by `n` hold the capacity, and
the live count is `min(domain_count_get(0u), zero_n())`. The code that reads it sits in `// @when COUNT device` ...
`// @end` blocks (`compute_abi` removes a block from the variants it does not name). A slot at or past the live count
reads nothing and writes the inert value of each output it owns:
| Kernel | Inert value of a slot past the live count |
|---|---|
| `gather` | `dst` is zeros |
| `compact` | `inverse` is -1; `indices` is -1 past the kept ones |
| `scan` | `dst` is the total of the last group (0 for an empty run) |
| `jump` (rank, min) | `parent_out` is the slot itself, `value_out` is 0 |
| `loops` | stage clear writes every slot of the dispatch; stage link: `next` is the slot itself, `head` is 0 |
| `loops_cut` | `head` is 0, `cut` is the slot itself |
| `loops_place` | `ring` is -1, `pos` is 0 |
| `edges_runs` | `flag` is 0; `a` and `b` are -1, `fcount` is 0 |
| `expand` | `owner` and `local_index` are -1 past the sum of the counts |
| `hash_gather` | `items` past the count are left as allocated (0) |
| `hash_hook` | a slot past the live count reads and writes nothing |
| `scatter` (fold pass) | an index past the live count reaches no place |
| `reduce` | the last segment ends at the live count; a segment past `segment_count` is not written |
| `scan` (`SEGMENTS=device`) | a segment past `segment_count` is not scanned or written |
| `expand` (`ELEMENTS=device`) | `counts` and `offsets` hold `element_count` live elements, whatever the dispatch covers |
| `intersect_pairs` | a triangle of A past the live ones has no entries |
| `intersect_cuts`, `intersect_unify` | a slot past the live ones is not read or written |
| `intersect_retri` | a triangle past the live ones becomes no triangle |
**A new count.** A kernel that makes a count writes it to an appended output's counter, which the op returns as
`<output>_count` and as the live count of the output (`output.count`); it carries its capacity and is a domain as it is.
`compact` writes the kept count, `expand` the sum of the counts, `hash_gather` the number of points listed, and
`scan` states its total in `total` (an `i32` scan's total is a count: `{ count = scanned.total, capacity = ... }`).
**Capacity.** When the node does not know a count it allocates for the bound it can state, and the kernel never writes
past it. `compact`, `gather`, `scan`, `jump`, `loops*` and `edges_runs` write at most as many slots as the capacity
they run over. `expand` holds at most `n` slots (`n * k` when no count exceeds `k`). `hash_gather` holds at most its
`total` param, which is the bound the node states (a pair list has no smaller bound than points times queries). In
those two the count buffer states the true count whatever the capacity: a count above `.capacity` is an overflow, the
first `capacity` slots are written and the count minus the capacity is how many did not fit. A node that states a
bound checks `count <= capacity`.
**Storage buffers.** A stage binds at most 8 on the web. A `COUNT=device` variant binds one more than the plain one,
and the table counts the variants that exist:
| Kernel | plain | device |
|---|---|---|
| `gather` | 3 | 4 |
| `compact` | 4 | 5 |
| `scan` | 4 | 5 |
| `jump` | 7 | 8 |
| `loops`, `loops_cut`, `loops_place` | 6 | 7 |
| `edges_runs` | 8 | 5 (heads), 8 (table) |
| `expand` | 5 | 6 |
| `hash_gather` | 6 | 8 |
| `hash_hook` | 6 (hook, `MATCH=normal`), 1 (find) | 7, 2 |
`edges_runs` binds the buffers of its own stage in the device variant (`when: "COUNT=plain; COUNT=device STAGE=table"`),
which is how it stays at 8. `abi.directLimit` counts every `buffer` binding the schema declares, `domain_count`
included, so a kernel with a `columns` binding that adopts the form has one direct column fewer; none of these eleven has
one.
## device.bound, device.live, device.guard
`device.bound { value, capacity?, live? }` answers `value` as a live count: a count Column that carries the most it can
reach, so a kernel runs over it (`domain = count`), a geometry carries it (`class.count = count`) and nothing is read
back. `value` is a one-word Column, arithmetic over counts, or a number. A number is already known and stays the number
unless `live` is true, which writes it to the device as the count, allocated the next power of two at or above it (a
list recorded over it runs again for every number inside that room and is recorded again for a number outside it).
A count held on the device with no `capacity` stated is allocated the room a cook of it measures: twice its count, in
powers of two, and never less than an earlier recording of the same cook allocated. The cook reads the count once. A
recorded list runs the count on the device clamped to that room, and holds a guard: a word that is 1 when the count
outgrew the room. The count a node can bound by what it reads (a triangle count by the vertices, pairs by the pairs
there could be) states `capacity` and holds no guard.
`device.live { values, count }` answers `values` carrying `count` as its live count. A kernel output declared
`live: true` in `bindings.yaml` carries the count its dispatch ran over the same way.
`device.guard { value, flag, why, detail? }` answers `value` once `flag` (a number, a one-word Column or a Field over
one) is 0. A cook that finds it otherwise refuses with `why`, or with what the lazy input `detail` raises. A recording
of the cook holds the word of `flag`, and a replay of it reads that word without waiting for a frame
(`traffic().deferredReads`): the cook that follows finds out whether the replay before it ran past the guard, lets the
recording go and records again. The replay that ran past a guard has handed out its answer by then: a count clamped
to its room, or the result for an input that is no longer a solid. A count that outgrew its room is recorded again with
four times the room (`cookrun.counts().guarded` counts the replays found to have run past a guard).
## transfer.upload
`values` is a Record. A Number becomes a one-element Float column; a list of numbers a Float column; a list of
`{x,y,z}` a Vec column, a list of text (or `{ type = "String", values = {...} }`) a column of the values; `{ type = "Int", values = {...} }` a typed column (`Float`, `Int`, `UInt`, `Vec2`, `Vec3`,
`Vec4`, `Color`); a table of those a table of columns; a Column is held as the device run it has. `columns` (output)
holds the columns under the keys `values` gave them.
## transfer.readback
`columns` is a Record of Columns, live counts, plain values (numbers, booleans and text answer themselves) or tables
of them, a Field (evaluated over the elements of the column it reads, then read; a Field that reads no column has no
length and is refused naming the form to use), or a Geometry or a Part, which answers the copy holding what its columns
held. A live count answers a Number, a Float or Int
column a list of numbers, a vector column a list of `{x,y,z,w}`, and `values` (output) holds them under the keys
`columns` gave them. The op waits across frames, and the play report names it as the point the plan waited. Anything
that waits on a readback value is a planning step.
## cpu.call
`fn` (input) is called with `args` (input), and `values` (output) is what it returned, as a list in order. `returns`
(param) lists the type name of each result (`Number`, `String`, `Bool`, `Function`, `List`, `Column`, `BufferRef`,
`Handle`, or a procgen type) and a call that returns anything else is refused; a Handle in the results is released when
the result is. `ceilingMs` (param) refuses a call that ran longer (5000 when it is 0 or left out), counting the CPU
time the call runs for on the thread that makes it, in every coroutine it resumes, and in every task it starts
(`task.spawn`, `task.defer`, `task.delay`) or component hook it sets off while the call lasts, so a task it starts and
waits for is charged to it and one it leaves running is charged only what it ran before the call returned: the time it
waits on a frame, a program build, a read-back or a task, and the time its thread waits for a core on a busy machine,
are not counted. A device Column reaches the function as a
`BufferRef { buffer, type, count, lanes, element }`, and its data does not move. A Geometry reaches it with its columns
read to the CPU, or, with `device` (param) on, as it stands on the device. `curves` (param)
on hands an animated value among `args` to the function as the curve it is, and the function answers with what follows
it; off, the function gets the value it holds at the cook's time. `pure` (param) on states that the results are a
function of the arguments alone and the call changes nothing: a call whose arguments the plan already knows is made
while the graph is planned, and its results stand in the plan as constants.
## cpu.read
`cpu.read` is `cpu.call` (`fn`, `args`, `returns`, `ceilingMs`, `device`, output `values`) for a function that reads
state the graph does not hold, such as entities in the scene. `digest` (input) is a function called with the same
values that answers a value standing for what `fn` reads; the graph's live digest folds it in, so a Generator cooks the
graph again when that state moves. It takes no `curves` or `pure`.
# abiBox
A node for the procgenAbi suite: it is called through the builder and written out in a graph.
·computeshader · born here
❒asset # abi_add
Adds the delta param to every element of src into dst.
·computeshader · born here
❒asset # abi_columns
Adds the first lane of the columns a and b of a table into dst.
·computeshader · born here
❒asset # abi_filter
Appends every element of src above the threshold param to hits.
·computeshader · born here
❒asset # abi_inout
Writes src doubled into dst, or adds src to the dst it is bound when mode is 1.
·computeshader · born here
❒asset # abi_inplace
Adds one to every element of data in place.
·computeshader · born here
❒asset # abi_len
Copies src into a dst the length of src.
·computeshader · born here
❒asset # abi_live
Doubles src into dst for the elements a live count holds and writes -1 past it.
·computeshader · born here
❒asset # abi_nothing
Reads src and writes nothing.
·computeshader · born here
❒asset # abi_ordered
Writes ten times the first lane of slot 0 plus the first lane of slot 1 of a table into dst. The binding states the slot
order, `tail` then `head`.
·computeshader · born here
❒asset # abi_pair
Adds two read bindings, a and b, element by element into dst.
·computeshader · born here
❒asset # abi_scale
Multiplies every element of src by the scale param into dst, one element per invocation.
·computeshader · born here
❒asset # abi_spec
Doubles src into dst as the element type T the specialization names.
# procgen guide
Making things from rules, as graphs of nodes: geometry and meshes, scattering and
terrain, and visual effects — particles, sparks, embers, magic, debris, swarms,
flocks, trails, cracks and lightning — simulated and drawn live every frame,
with your own compute and surface shaders as nodes.
The procgen package's reference guide. Surfaced through
`guides { query: "procgen" }` and `guides { path: "systems/procgen/documentation/procgen" }`
(likewise `man`), and reachable from the `procgen` toolbox via `search_tools procgen`.
The content lives in `guide.md`; this README is the asset summary.
# Scatter
Strews copies of one shape over the surface of another, each with its own turn, lean and size.
Both the surface and the thing scattered are inputs, so this covers rocks on a hillside, crates on a deck, bushes
along a bank or rubble across a floor: wire it a different `source` and it is a different field.
It makes points on the surface (`points.on_surface`, area-weighted), gives every point its own random yaw, lean and
scale (`instances.randomize_transform`), and places the source on those points (`instances.on_points`). The transform
is read from the same point cloud the instances sit on, so each copy takes the pose of its own point.
## Inputs
| Input | Type | Default | What it does |
|-------|------|---------|--------------|
| `surface` | Geometry | none | What the copies land on. Points are area-weighted, so a bigger face gets proportionally more. |
| `source` | Geometry | none | The shape being copied. |
| `count` | Int | 200 | How many copies. |
| `seed` | Int | 1 | Changes the arrangement. The same seed always gives the same field. |
| `scaleMin` | Float | 0.8 | Smallest per-copy size multiplier. |
| `scaleMax` | Float | 1.4 | Largest per-copy size multiplier. Equal to `scaleMin` means uniform. |
| `tilt` | Float | 0.15 | Radians of random lean off vertical. `0` stands every copy upright. |
## Output
`instances`: a population, the points each carrying the source as its `Mesh`. A Generator draws it as one
hardware-instanced draw call per source; `instances.realize` merges it into a single Geometry.
## Calling it
Another graph calls a `.procGraph` as it calls a node, by its path or identity, naming the inputs in a table; the
call answers what the graph outputs (`instances`). Its declared inputs are the call's inputs.
# Scatter (node)
A scattering node: its `init.luau` holds the graph that does the work, so the declared inputs are this node's inputs
and the declared output is this node's output. The node is its pipeline, in one self-contained asset.
It makes points on the surface (`points.on_surface`, area-weighted), gives every point its own random yaw, lean and
scale (`instances.randomize_transform`), and places the source on those points (`instances.on_points`). The
transform is read from the same point cloud the instances sit on, so each copy takes the pose of its own point.
A node is findable by what it does: the `procgen` toolbox's `ops` with the query `scatter` lists it beside
`points.on_surface` and `instances.on_points`. Editing this node's `init.luau` cooks again every graph that calls it.
## Inputs
- `surface` (Geometry): what the copies land on.
- `source` (Geometry): the shape being copied.
- `count` (Int, default 200): how many.
- `seed` (Int, default 1): changes the arrangement.
- `scaleMin` / `scaleMax` (Float, defaults 0.8 and 1.4): per-copy size range.
- `tilt` (Float, default 0.15): radians of random lean.
## Output
- `instances`: a population, the points each carrying the source as its `Mesh`.
## Authoring a node
A node is a graph returned from a `.procNode`'s `init.luau` (`proc.define`). `asset.create("procNode", name)`
scaffolds that shape, and `procNode.assetType/README.md` describes what a node states and how graphs call it.
# flipbook (shader)
Plays a texture sheet on a card. The card's `shader_data` lanes say which cells of the sheet to show: lane 0 holds (frame, next frame, how far between them) and lane 1 holds (columns, rows) - the lanes `sim.flipbook` writes onto particle points as `data0` and `data1`. Frames count row by row from the top-left cell.
The `base_color_texture` is the sheet and `base_color` tints it; `emission` scales the colour for an additive, bloom-fed look. With `face_camera` on the card turns to face whichever camera draws it; off, it keeps the pose the instance stands at. The fragment is unlit and returns the sheet's alpha, so the material's blend state (alpha, additive, premultiplied) says how it combines with what is behind it.
# generator
The parts of the `Generator` component (`@builtin::systems.procgen.Generator`).
The component makes one context table per instance, `gen`, holding its
`public` and `private` surface, and calls this module with it. Each part
installs its state and operations on `gen`:
- `realized`: What a Generator has realized and from which graph: the entities, populations, meshes, materials and volumes it stands, tracked per output, and the retirement that keeps an output's earlier realization standing until its replacement lands.
- `translated`: A Generator's translated outputs: the states they stand as, the bounds it publishes for them, the frames play of a held output, and the clock a translated output is presented at.
- `cook`: Cooking a Generator's graph: the outputs it resolves, the node and frame caches a cook reads and fills, the cook's own state and costs, the world values a graph follows, the pacing of a cook on a worker, the inputs a cook takes, the evaluation of one output, and the per-node placement the `playback` report reads.
- `realize`: Standing a cooked value as a Generator's children: a geometry, grid, bundle, population or asset reference realized under the generator, an output split into the pieces it realizes as, and the folding of outputs into the one bundle `apply` writes.
- `run`: Running a Generator's bound graph: the bound graph and its signatures, its timeline and the clock each frame is cooked at, the cook that runs ahead through the timeline, presenting a cooked frame on the device, and standing a frames play of a held output.
- `diskcache`: A Generator's disk cache and baked snapshot: the outputs a graph declares `diskCache` on, loaded from the world's cache, the writes a commit makes for them, and the snapshot an earlier engine stored with the scene.
- `play`: A Generator's play of its graph (`call`): loading, clocking, presenting, standing and advancing it; binding a graph to the generator (`regenerate`); and standing a baked snapshot in place of cooking.
- `clock`: A Generator's clock, frame by frame: the live sources it polls, the material clocks and live material values it writes, the playhead it presents, the transport its peers share, advancing time, cooking again once the device returns, and the tick each frame runs.
# generator_inspector
The `Generator` component's card on an entity. It replaces the card's generic
`settings` section with one built from the bound graph: the graph it runs, as
an asset picker, and which of its outputs it realizes; then groups — Inputs,
one row per input the graph declares in the control its type takes, each able
to take an expression over the graph's own time; Timeline, the transport,
scrubber, range, frame rate, looping and wall time; Cook, what is cooking, what
cooking has cost and the frame budget — and the Flatten action. The
component's `inspector.luau` returns this module, so the editor's inspector
framework (`edui.inspector.componentSections`) shows it wherever the component
is on an entity. While the clock runs, or the range is still cooking, the card
asks the panel to follow it.
Editing an input row writes the value into the component's `params`, which
regenerates the live output in place. A write the input's type cannot hold is
refused, and the row says why.
`buildFieldSpecs(graph, params)` is the pure row mapping: one spec per declared
graph input, sorted by name, carrying its kind, its current value (the param
override when set, else the declared default), its declared range, the asset
kind a reference input picks from, and whether the value changes over time.
# grid
How a compute shader finds the points near a point: the walk over a spatial
hash grid `proc.device.gridInto` builds on the device. `zero_grid_cell`,
`zero_grid_bucket` (from `grid_cell`) and `zero_grid_range`.
A shader run by `shader.compute` or `sim.shader` gets a grid over its domain's
`P` by declaring three read bindings — `gcfg` (f32), `grange` (u32) and
`gorder` (u32) — and giving the node a `neighborRadius`:
```wgsl
#include "@builtin::systems.procgen.grid"
let c = zero_grid_cell(p);
for (var dz = -1; dz <= 1; dz = dz + 1) {
for (var dy = -1; dy <= 1; dy = dy + 1) {
for (var dx = -1; dx <= 1; dx = dx + 1) {
let cc = c + vec3<i32>(dx, dy, dz);
let span = zero_grid_range(cc);
for (var s = span.x; s < span.y; s = s + 1u) {
let j = gorder[s];
// skip j == i, and any j whose own cell is not cc
}
}
}
}
```
# grid_cell
The cell a point stands in (`zero_grid_cell`) and the bucket of a spatial hash
grid that cell hashes to (`zero_grid_bucket`), read against the grid settings
bound as `gcfg`. What a pass that only keys points by cell includes; a pass that
walks the grid includes `grid`, which includes this.
```wgsl
#include "@builtin::systems.procgen.grid_cell"
```
·computeshader · born here
❒asset # bvh
The tree of a bounding volume hierarchy over the primitives of a geometry (an
LBVH): a leaf per primitive in Morton order with its bounds, and the internal
nodes linked by the longest common prefix of the sorted codes, equal codes broken
by place. It reads the points (`pts`), the point each vertex reads (`vpt`), each
primitive's `vstart` and `vcount`, the `codes` of `bvh_codes` and the `order` the
`sort` kernel gives them, over the params `np` (the primitives) and `pyr_size`
(`np` rounded up to a power of two). It writes every leaf and every link of
`nodes` (`(2 * np - 1) * 8` words) and allocates `pyr` (`6 * pyr_size` words),
the pyramid `bvh_pyramid` fills. `bvh_pyramid` and `bvh_internal` then fill in
the internal bounds, and the `ray` and `intersect_pairs` kernels traverse the
finished table. The `geometry.bvh` node runs the whole build and answers the
table as its `tree`. `bindings.yaml` states the node layout and the order the
kernels run in. Dispatch over `np`. Exact: the same geometry gives the same tree.
# bvh_box
The boxes the bvh kernels fold: `ZeroBox`, the empty box and `zero_grow`. The
module binds nothing, so any shader includes it.
·computeshader · born here
❒asset # bvh_codes
A 30-bit Morton code for each primitive of a geometry: its bounds centre,
normalized inside the scene bounds and interleaved bit by bit. Sorting
primitives by this code puts primitives that are near in space near in the
order, which is what the `bvh` kernel builds its tree from. It reads the points
(`pts`), the point each vertex reads (`vpt`), each primitive's `vstart` and
`vcount`, and the scene bounds `lo` and `hi` (xyz each: a min and a max reduce
of the points), and writes `codes`, one word per primitive, 0 for a primitive
with no vertices. The `geometry.bvh` node runs it, sorts the codes and hands
both to `bvh`. Dispatch over the primitives. Exact.
·computeshader · born here
❒asset # bvh_internal
The last step of a bounding volume hierarchy build: each internal node's bounds,
the fold of the leaves its run of sorted places covers, read from the pyramid
`bvh_pyramid` wrote. It reads `codes` and `order` (to find each node's run),
`pyr` and the leaves of `nodes`, over the params `np` and `pyr_size`, and writes
the bounds words of nodes `0 .. np - 2` of `nodes` in place, after which the
`ray` kernel can traverse it. The `geometry.bvh` node runs it last. Dispatch
over `np - 1`, one invocation per internal node. Exact.
# bvh_levels
How the bvh kernels read bounds: a node of the node table (`zero_node_box`), a
node of the pyramid of leaf bounds (`zero_pyr_box`) and a node of the segment
tree both form (`zero_tree_box`). The module header lists the buffers and params
the includer declares and the node layout.
# bvh_prim
`zero_prim_box`, the bounds of one primitive over its vertices, with the box
types of `bvh_box`. The module header lists the buffers the includer binds.
·computeshader · born here
❒asset # bvh_pyramid
One level of the pyramid of leaf bounds: each pyramid node folds the bounds of
two nodes of the level below (leaves at the first level). Dispatched once per
level, the levels let `bvh_internal` fold the bounds of any run of leaves in a
few reads. It reads the leaves of `nodes` and writes `pyr` (6 words a node,
minimum then maximum) in place, each level reading what the level before it
wrote. The params are `np`, `pyr_size` and `level` (1 .. log2(`pyr_size`)); a
dispatch covers `pyr_size >> level` nodes. The `geometry.bvh` node runs it once
per level, between `bvh` and `bvh_internal`. Exact.
# bvh_span
The binary radix tree over sorted Morton codes: for internal node `i`,
`zero_span_of` gives the run of sorted places it covers and the place it splits
at, from the length of the common prefix of the sorted codes. The module header
lists the buffers and the param the includer declares.
# color
Colour helpers a per-element template calls: `zero_color_srgb_to_linear`,
`zero_color_linear_to_srgb`, `zero_color_luminance`, `zero_color_hsv_to_rgb`,
`zero_color_rgb_to_hsv`, `zero_color_blackbody(kelvin)` (linear RGB of a
blackbody) and `zero_ramp_ease(f)`, the smooth ease a `ramp` blends keys with.
```wgsl
#include "@builtin::systems.procgen.kernels.color"
fn ember(heat: f32) -> vec3<f32> { return zero_color_blackbody(1000.0 + 2500.0 * heat); }
```
·computeshader · born here
❒asset # compact
Turns a keep mask into the indices of the kept elements, and the map back from
every element to the place it holds among them.
`indices` lists the elements whose mask word is not 0 (a float mask of 0.0 and
1.0 reads the same way), in run order; its live count is the number kept.
`inverse` holds, for every element, its place among the kept ones, or -1 for an
element that is not kept, so a column carried to the kept elements (a gather by
`indices`) and a column of per-kept values carried back (a read through
`inverse`) agree on which element is which. The nodes that keep a subset of
elements run it: `core.select`, `core.weld`, `core.edges`, `core.boundary`,
`core.islands`, `curve.grow`, `geometry.extrude`, `geometry.ribbon`,
`instances.to_points`, `sdf.mesh` and `sim.particles`.
The dispatch size `n` is the number of elements. One workgroup of 256 works the
whole run, a tile of 256 elements at a time; the other workgroups of a larger
dispatch return.
Exact: the order is the run's own, so the same mask gives the same answer on
every dispatch.
Over a live count (`COUNT=device`, see the abi README) `n` is the capacity and the
first `domain_count` mask words are the elements: words past it are not read, a slot
of `inverse` at or past it holds -1, and the slots of `indices` past the kept ones
hold -1. `indices` has room for `n` and its counter is the live count the next kernel
takes as its domain. It binds 5 storage buffers (4 in the plain variant).
·computeshader · born here
❒asset # concat
Joins one attribute of several geometries into one column. `sources` is a column
table with one column per geometry, in order; `desc` says where each geometry's
elements start in the result, how many it has, how far to shift each word of a
linkage column, and whether the geometry holds the attribute at all. Where it
does not, its elements take the default stated in the first four words of
`desc`. The params are `lanes` (the words an element holds) and `parts` (how
many geometries are joined); the joined column is `dst`, `n * lanes` words.
Linkage is a shift: `vertex.pt` takes the number of points before the geometry,
`primitive.vstart` the number of vertices before it, and `point.id` the amount
that keeps the ids unique across the geometries. Any other attribute shifts by
0. The `core.concat` node joins a whole geometry by running the kernel once per
attribute of the union of the geometries' attributes.
Dispatch over `n`, the elements of the joined domain. Exact: each result
element has one writer.
·computeshader · born here
❒asset # contour_cells
Whether each cell of a grid of samples straddles the surface: its eight corner
samples hold both a value below zero and one at or above it. It reads `vals`,
the distance at every grid sample (x fastest), over the grid the params state
(`sx`, `sy` samples along x and y; `nx`, `ny`, `nz` cells; `ox`, `oy`, `oz` the
position of sample (0, 0, 0); `h` the cell size). `flag[c]` is 1 for a
straddling cell and 0 otherwise, the keep mask `compact` turns into the list of
cells that own a vertex. The `sdf.mesh` node runs it first.
Dispatch over the cells. Exact: each flag has one writer.
# contour_core
The shared WGSL of the dual contouring kernels (`contour_cells`, `contour_crossings`,
`contour_solve`, `contour_quads`, `contour_triangles`): `ZsGrid`, the addressing of a grid of
samples and its cells, the corners and edges of a cell, and the least-squares
solve (`ZsQef`, `zs_qef_add`, `zs_qef_solve`) that places a cell's vertex where
the surface normals around it agree.
```wgsl
#include "@builtin::systems.procgen.kernels.contour_core"
```
·computeshader · born here
❒asset # contour_crossings
Where the surface crosses each of the twelve edges of every straddling cell:
the point (`cross_p`) and whether it crosses (`cross_w`, 1 or 0). The crossing
is the linear interpolation of the two corner samples to zero. An edge that does
not cross carries its cell's centre, a point any shape can be read at. Slot `s`
of the outputs is the `s`-th straddling cell, `cells[s]`, and edge `e` of it is
slot `s * 12 + e`. It reads `vals` and the grid params as `contour_cells` does,
`cells` (the list `compact` makes of the straddling cells) and `domain_count`
(the live count `compact` made, bound in every variant): a slot at or past it
holds the point (0, 0, 0) and weight 0. `contour_solve` reads the outputs.
Dispatch over the straddling cells, or the capacity of their list. Exact: each
slot has one writer.
·computeshader · born here
❒asset # contour_quads
One quad per grid edge the surface crosses, spanning the four cells around it.
Edge `e` is the edge of sample `e / 3` along axis `e % 3`. `quad_flag[e]` is 1
for an edge that owns a quad and `quad_v[e]` holds its four vertex numbers, wound
so every face points away from the solid. An edge on the grid's boundary, or one
with a cell around it that owns no vertex, owns no quad.
`vertex_of[c]` is the number of the vertex cell `c` owns, or below 0 for a cell
that owns none: the inverse map `compact` writes beside the list of straddling
cells. It reads `vals` and the grid params as `contour_cells` does. The
`sdf.mesh` node compacts `quad_flag` into the kept quads `contour_triangles`
splits.
Dispatch over `3 * samples` edges. Exact: each edge has one writer.
·computeshader · born here
❒asset # contour_solve
The vertex of each straddling cell, written to `pos`. With `PLACEMENT` `qef` the
vertex is the point nearest the planes the surface normals at the cell's
crossings describe (least squares, pulled back toward the mean of the crossings
by `lambda` per crossing, default 0.02), which puts the vertices on a box's
edges and close to its corners; with `centroid` it is the mean of the crossings.
A `qef` vertex is held just inside its own cell (a thousandth of the cell size
in from each wall), so two cells sharing a wall never land two vertices on one
point.
It reads the grid params as `contour_cells` does, `cells` and `domain_count`
(the list of straddling cells and the live count `compact` made; a slot at or
past the count takes the origin), `cross_p` and `cross_w` from
`contour_crossings`, and `cross_n`, the unit surface normal at each crossing,
read when `PLACEMENT` is `qef` (a crossing with a zero normal adds no plane and
still counts toward the mean). The `sdf.mesh` node runs it with the normals of the shape's
gradient and takes `pos` as the mesh's points.
Dispatch over the straddling cells, or the capacity of their list. Exact: each
vertex has one writer.
·computeshader · born here
❒asset # contour_triangles
Each kept quad as two triangles: `pt` holds, per quad, the vertex numbers of
its corners (`a b c`, `a c d`), and `vstart` the position of each triangle's
first corner in `pt`. Slot `n` of the dispatch is the `n`-th quad `edges[n]`
names (the list `compact` makes of `quad_flag`), read from `quad_v` of
`contour_quads`, so the quads are numbered in edge order.
Dispatch over the kept quads. The `sdf.mesh` node dispatches over every grid
edge and states the kept count times 6 as the vertex count and times 2 as the
primitive count, so the slots past the kept quads lie past the live elements.
Exact: each slot has one writer.
·computeshader · born here
❒asset # edges
The sides of a mesh as point pairs: the first stage of the edge table. It reads
`pt` (the point each vertex reads), and each primitive's `vstart` and `vcount`.
One invocation per primitive writes, for every vertex slot of its run, the side
leaving that vertex as `lo`, `hi` (its lower and higher point), `face` (the owning
primitive) and `rev` (1 when the side runs from the higher point to the lower).
The outputs hold `nv` slots, a bind of the vertex count. A slot with no side (the
last vertex of an open run, a side joining a point to itself, a primitive with
fewer than two vertices) holds `ZERO_NONE` (the largest i32) in `lo` and `hi`
and -1 in `face`, so a sort puts it last. Integers are 32-bit words, exact.
The edge table is this kernel, a stable sort of the slots by `hi` and then by
`lo`, and the `edges_runs` kernel (run heads, then the table). The sort and
the compaction between are the `sort` and `compact` kernels; `edges_runs` states the
whole sequence, and the `core.edges` node runs it. `closed_all` is 1 for a mesh of
faces, which wrap; with 0 the `closed` column decides per primitive, for curves.
`np` is the number of primitives in the plain variant. Over a live count
(`COUNT=device`, see the abi README) the live primitives are read from
`domain_count` and the dispatch covers their capacity; that variant wraps every
primitive and binds no `closed`, which keeps it at 8 storage buffers (8 in the
plain variant).
·computeshader · born here
❒asset # edges_runs
Turns the sorted sides of a mesh into the edge table. Two stages share one
dispatch shape, chosen by the `STAGE` specialization:
- `STAGE heads` flags, per sorted place, where a new edge starts;
- `STAGE table`, after the flags are compacted into `starts`, writes each edge's two
points (`a` < `b`) and the number of sides along it (`fcount`).
The whole sequence for a mesh with `nv` vertices: `edges` writes `lo`, `hi`,
`face`, `rev`; a stable sort by `hi` and then by `lo` gives `order`; `STAGE heads` flags
the heads; `compact` of the flags gives `starts` and the live edge count; `STAGE table`
writes the edges. An edge's faces are the primitives `face[order[starts[e] + j]]`
for j below `fcount[e]`, so `gather(face, order)` is the `faces` column the
`core.edges` node answers, with `starts` as its `faceStart` and `fcount` its
`faceCount`. An edge is open when `fcount` is 1 and manifold when it is at most
2. Integers are 32-bit words, exact.
In the plain variant the `n` param is the number of sorted places (the vertex
slots, also the most edges there can be); stage table runs over the edges, and an
invocation past the live edges writes values nothing reads.
Over a live count (`COUNT=device`, see the abi README) `domain_count` is the number
of live sorted places in both stages, and the `n` param is not read. Stage heads runs
over the capacity of places and holds 0 in `flag` past the live ones. Stage table
runs over the capacity of edges: an edge is live where `starts` holds a place below
the live places (the slots past the live edges hold -1 where `compact` over a live
count makes them), a run ends at the live places, and every other slot holds -1 and
-1 in `a` and `b` and 0 in `fcount`. The device variant binds the buffers of its own
stage only: 5 storage buffers in stage heads and 8 in stage table (8 in the plain
variant).
·computeshader · born here
❒asset # expand
Turns a count per element into runs. Element `j` takes `counts[j]` slots, in
element order; slot `s` of the result holds `owner[s]`, the element it belongs
to, and `local_index[s]`, its place within that element's run. `offsets` is the
exclusive prefix sum of `counts` (what the `scan` kernel makes of it), so every
slot finds its element by a binary search and has one writer.
`owner` is an appended output: its live count is the sum of the counts, up to
the capacity the dispatch was given. Slots at and past the live count hold -1.
An element with a count of zero has no slot; a column of all zeros gives a live
count of zero.
Dispatch over `n`, the capacity of the result. Exact.
`core.owner` runs it to give every vertex its primitive and its place in the primitive's run. `core.realize`,
`core.pairs`, `core.adjacency`, `core.resample.curve`, `core.sample.surface`, `core.normals.corner` and the geometry
nodes that make new primitives (`geometry.extrude`, `geometry.sweep`, `geometry.triangulate`, `geometry.bevel` and
others) expand their counts with it.
Over a live count (`COUNT=device`, see the abi README) the elements are the first
`domain_count` of `counts` and `offsets`, so they can be held in runs larger than
their live count, and the counter of `owner` states the sum of their counts as it is,
above the capacity too: the first `n` slots are written and the sum minus `n` is how
many did not fit. A node that states `n` as a bound checks the count against it. The
plain variant clamps the count to `n`. It binds 6 storage buffers (5 in the plain
variant).
Over live elements (`ELEMENTS=device`) the elements are the first `element_count` of `counts` and `offsets`, whatever
`n` covers: a result sized by one class (the vertices) expanded from the elements of another (the primitives). It binds
one storage buffer more.
# filter_weights
The reconstruction filters an image resample weighs source pixels with:
bilinear (triangle), lanczos3 and Catmull-Rom, each as `zf_weight(filter, x)`
over a distance in output-pixel units with its reach from `zf_support`.
```wgsl
#include "@builtin::systems.procgen.kernels.filter_weights"
```
·computeshader · born here
❒asset # gather
Builds a column from another column and a run of indices: element `i` of the
result is element `index[i]` of `src`. An index below 0 or past the end of
`src` gives zeros. The element type is the `T` specialization (`f32`, `i32`,
and two to four lanes of either); a three-lane column is read and written
tightly packed, twelve bytes an element.
Dispatch over `n`, the number of indices. Exact: each result element has one
writer. It declares `fusion:` (the value of element `i` is `element(i)`, a thread
runs where `skip(i)` is false), so a chain of fused kernels evaluates it inside
one dispatch.
Most nodes that reorder, select or copy elements read their columns through it:
`core.select`, `core.concat`, `core.make`, `core.weld`, `attribute.gather`,
`attribute.promote`, `elements.sort` and the geometry nodes among them.
Over a live count (`COUNT=device`, see the abi README) `n` is the capacity and the
first `domain_count` slots are live: a slot at or past it holds zeros. It binds 4
storage buffers (3 in the plain variant), and `dst` holds `n` elements.
·computeshader · born here
❒asset # grid
Reads and writes a field of cells in one kernel. The field is a Grid (one tile,
no apron, x fastest, `lanes` values per cell) or a Raster (tiles with aprons,
one batched dispatch) through the layout in `tile_layout`.
- sample (`MODE sample`): the field at positions, nearest or trilinear, held to the
edge cell past the field. Reads each cell from the tile that owns it.
- write (`MODE write`): a patch of values blended into the field (replace, add,
max, min, multiply, optionally weighted per cell). Every tile that holds a cell
as core or apron receives it, so apron copies agree. The patch window is stated
in field cells and may reach past the field (negative origin, or a size beyond
the cell counts): a stored cell a border tile holds past the field takes the
patch where the window covers it.
- stencil (`MODE stencil`): per stored cell, a neighbour at an offset, slope in
degrees, curvature (the laplacian over x and z) or the gradient, with neighbour
reads held to the tile's own stored edge. Slope and curvature are the terrain
mask's. The gradient writes two values a cell, d/dx and d/dz per metre.
`MODE`, `SAMPLING` (nearest, trilinear), `STENCIL` (offset, slope, curvature, gradient) and `BLEND` (replace, add, max, min, multiply) are specializations.
The bindings: `field` (the field, read by sample and stencil), `aux` (the
positions, xyz a position, for sample; the patch values for write), `weight` (one
weight a patch cell, read by write when the param `hasWeight` is 1) and `out` (the
sampled column, the stencil result, or the field being written). Sample runs one
thread per position (`count` of them) and writes `outLanes` values each, from lane
`lane`; write and stencil run one thread per stored cell.
`determinism: exact`: each output element has one writer. The bindings a mode
does not use take any buffer of at least one element that is not bound elsewhere
in the dispatch.
`grid.sample` samples with it, `core.raster.blend` writes a patch into a raster
with it, and `core.raster.stencil` measures slope and curvature with it.
·computeshader · born here
❒asset # hash
The first step of a spatial hash over a run of points: the bucket of the cell
each point stands in. The sort kernel orders the points by bucket, and
`hash_starts` then states where each bucket's points start and end in that
order. The keys, the order and the bucket spans, with the cell size, are the
index `hash_count`, `hash_gather`, `hash_nearest`, `hash_repel` and `hash_hook`
search. `core.spatial` builds the index with it. Exact: the same points give the
same keys.
It reads `pts` (xyz a point) and writes `keys`, one a point; the params are
`buckets` (a power of two) and `inv_cell` (one over the cell side).
Over a live count (`COUNT=device`, see the abi README) `n` is the capacity and the
first `domain_count` slots are live. A slot at or past the live count holds the key
`0x7fffffff`, which is past every bucket: the sort (over the same live count, or
over the capacity) places it after every live key, and `hash_starts` over the
capacity states every bucket's span over the live points only. It binds 3 storage
buffers (2 in the plain variant).
# hash_cell
The cell a point stands in (`zero_hash_cell`) and the bucket a cell hashes to
(`zero_hash_bucket`). The `hash` kernel keys points with them and the
`hash_query` kernel looks cells up with them, so one definition decides every
point's bucket.
·computeshader · born here
❒asset # hash_count
For each query position, how many indexed points lie within a radius of it,
searched through the spatial hash the `hash` and `hash_starts` kernels build. An
exclusive scan of the counts gives the offsets `hash_gather` writes each query's
neighbour list at; `core.pairs` runs the three in that order. Exact.
It reads `pts`, `order` and `range` (the index), `qpts` (xyz a query) and the
params `buckets`, `inv_cell`, `radius` and `skip_self` (1 leaves out the point
whose index is the query's, for queries that are the indexed points), and writes
`counts`, one a query. Dispatch over the queries.
·computeshader · born here
❒asset # hash_gather
Lists, for each query position, the indexed points within a radius of it, query
after query: query `q`'s points start at `qoff[q]`, the exclusive prefix sum of
the `hash_count` counts. The list is in the order the spatial hash walk visits
points, the same on every run. Exact. It takes the params of `hash_count`
(`buckets`, `inv_cell`, `radius`, `skip_self`) and `total`, the sum of the counts,
which sizes `items`. `core.pairs` lists its pairs with it.
Over a live count (`COUNT=device`, see the abi README) the queries are the first
`domain_count` of a capacity `n`, `total` is the capacity of `items` (the bound the
node states), and `items` is an appended output: its counter is the number of points
the live queries listed, above the capacity too when the lists do not fit (the first
`total` words are written and the counter minus `total` is how many did not). Words of
`items` past the lists are left as allocated, 0. A node that states `total` as a bound
checks the count against it. It binds 8 storage buffers (6 in the plain variant).
·computeshader · born here
❒asset # hash_hook
One hooking round of the connected sets of the points that stand within a radius of each other, searched through
the spatial hash the `hash` and `hash_starts` kernels build. `label` names each point's root, and every point of a set
names the same root (a star); `hooked` starts as a copy of `label`. Every point walks the points within the radius and
lowers the entry of its own root in `hooked` to the smallest root among them, so after the round each root links to
the smallest root next to its set, or to itself. Links only ever point at a smaller root, so they form a forest, and
the `find` stage, which follows each point's links in `hooked` to their end and lowers its entry to that root, makes
every set a star again.
| Specialize | Options | Meaning |
|------------|---------|---------|
| `STAGE` | `hook`, `find` | the hooking round over the hash (binds `pts`, `order`, `range`, `label`, `hooked`), or the walk of each point's links to its root (binds `hooked` alone) |
| `MATCH` | `position`, `normal` | every point within the radius is next to the point, or only one whose normal (`nrm`) is within `cos_angle` of its own |
| `COUNT` | `plain`, `device` | every slot is live, or the live count is read from `domain_count` |
A round of hooking then finding halves the number of sets that still have a neighbouring set every two rounds, so
twice as many rounds as the bits of the number of points leave every set a star of its smallest point. `core.weld`
folds that many rounds, with no read-back. Exact: the smallest root wins whatever order the invocations run in.
·computeshader · born here
❒asset # hash_nearest
The nearest indexed point of each query position, found through the spatial hash
the `hash` and `hash_starts` kernels build, with its squared distance. A query
walks the cells within a few shells of its own and, when that cannot prove its
answer, reads every point. The lower index wins of two points equally near.
Exact.
It writes `nearest` (the index, `0xffffffff` when no point is indexed) and `dist2`,
one each a query. The params are `points` (the indexed points), `buckets`,
`inv_cell`, `rings` (the shells walked before the search over every point,
default 2) and `skip_self` (1 leaves out the point whose index is the query's).
`core.neighbours` finds each point's nearest source and its distance with it.
Over a live number of indexed points (`POINTS=device`) `params.points` is the most points there can be, and the first
`point_count` of them are indexed: the search over every point stops at the live ones, and a query with none indexed
reports the none marker.
·computeshader · born here
❒asset # hash_repel
For each query position, the push away from the indexed points within a radius of it, searched through the spatial
hash the `hash` and `hash_starts` kernels build. Each neighbour strictly inside the radius pushes along the unit
direction from itself to the query, scaled by `1 - distance / radius`; the query takes the mean over its neighbours.
With `skip_self` 1 the point whose index is the query's and every point standing on the query are left out. The walk
visits the points in the same order on every run. Exact.
It reads `pts`, `order` and `range` (the index), `qpts` (xyz a query) and `domain_count`, which it binds in every
variant: a slot at or past the live count takes a zero push. It writes `push`, one `vec3<f32>` a query.
`core.neighbours` computes its `repel` push with it when no cone is given.
·computeshader · born here
❒asset # hash_starts
Finishes a spatial hash: given each point's bucket and the permutation that
sorts the points by bucket, states for every bucket where its points start and
end in the sorted order. Each bucket finds its two places by binary search over
the sorted keys, so no bucket needs clearing and the answer is a function of the
keys alone. Exact.
Dispatch over the buckets. It reads `keys` (from `hash`), `order` (the sort's
permutation) and the param `points`, and writes `range`, two words a bucket: the
places `range[2k]` .. `range[2k + 1] - 1` hold bucket `k`'s points. `core.spatial`
finishes its index with it, and `hash_count`, `hash_gather`, `hash_nearest`,
`hash_repel` and `hash_hook` read `range`.
# hash_walk
How a shader reads the spatial hash `hash` and `hash_starts` build: a walk over
the points within a radius of a query position (`zero_nb_begin`,
`zero_nb_next`), and the nearest point of a query (`zero_hash_nearest`). The
module header lists the bindings and params the includer declares. The walk
visits cells in a fixed order and, within a cell, points in sorted order, so a
shader that lists neighbours lists them in the same order on every run.
# intersect_core
The triangle pair tests of the `intersect` kernels. A pair of triangles, `a` of mesh A and `b` of mesh B,
has a cut in parts; every part is a segment, and an endpoint is named by the edges or the corner it is made
of, and computed from nothing else, so the triangles that share an edge compute the same point to the bit.
The includer binds `apts`, `atri`, `bpts` and `btri`. `zi_part(a, b, part)` answers part `part`.
## Parts
| Part | What | Cuts |
|---|---|---|
| 0 | The ordinary cut of two triangles that are not coplanar: the segment with two endpoints, each an edge of one triangle crossing the other (`zi_pair`). A vertex exactly on a plane counts as on its positive side, so triangles that only touch cross consistently or not at all. | A and B |
| 1 to 3 | Coplanar triangles: edge `part - 1` of `b`, clipped to the closed triangle `a`. | A |
| 4 to 6 | Coplanar triangles: edge `part - 4` of `a`, clipped to the closed triangle `b`. | B |
| 7 to 15 | Edge `i` of `a` and edge `j` of `b` lie in one plane and cross strictly inside both: the crossing, part `7 + 3 i + j`. A segment of no length. | A and B |
| 16 to 24 | Corner `v` of `a` lies exactly strictly inside edge `j` of `b`: part `16 + 3 v + j`. A segment of no length. | A and B |
| 25 to 33 | Corner `v` of `b` lies exactly strictly inside edge `i` of `a`: part `25 + 3 v + i`. A segment of no length. | A and B |
Triangles are coplanar when neither is degenerate and every corner of each is exactly on the plane of the
other (the signed volume is 0). Coplanar triangles have parts 1 to 6 and no part 0; a part is present when
the clipped edge keeps a length. The clip is made in the projection along the axis the plane's normal points
along most, with every edge taken from its lower point id to its higher; an edge is cut where it leaves the
triangle by the line of an edge of the triangle.
Parts 7 to 33 split an edge: a cut point that lies on an edge is a point of every triangle that uses the
edge, whatever plane that triangle lies in, so every pair of a triangle of A and a triangle of B that share
the edges reaches it and reports the same point. A crossing is computed along the edge of A from the two
edges alone.
## Endpoint records
An endpoint is `ZiEnd`: its position, the kind word (the kind in the low byte, which meshes the segment
cuts in bits 8 and 9, 1 mesh A, 2 mesh B, 3 both), `lo`, `hi`, `tri` and `extra`.
| Kind | Endpoint | `lo`, `hi` | `tri`, `extra` |
|---|---|---|---|
| 0 | an edge of A through triangle `tri` of B | the edge | `tri`, 0 |
| 1 | an edge of B through triangle `tri` of A | the edge | `tri`, 0 |
| 3 | an edge of B crossing an edge of A in their plane | the edge of A | the edge of B |
| 4 | a point of B | its id, twice | 0, 0 |
| 5 | a point of A | its id, twice | 0, 0 |
Two endpoints with all of kind word, `lo`, `hi`, `tri` and `extra` equal are one point.
·computeshader · born here
❒asset # intersect_cuts
The segment each entry of `intersect_pairs` is cut along, one thread per entry. The thread reads the entry's
`a`, `b` and part and writes the part's two endpoints (`intersect_core` states the parts). An endpoint record
is 8 words: the position as f32 bits, the kind word (the kind in the low byte, which meshes the segment cuts
in bits 8 and 9), then `lo`, `hi`, `tri` and `extra`, which name the crossing; two endpoints, 16 words, an
entry. The endpoint slot `s = 2 * entry + e` names an endpoint to `intersect_unify` and `intersect_retri`.
A point on a shared edge writes the same record as both endpoints. Exact.
It binds `apts`, `atri`, `bpts`, `btri` and `pairs` and writes `cut`. Over a live count (`COUNT=device`, see the
abi README) an entry past the live ones is not read or written. `geometry.boolean` runs it.
·computeshader · born here
❒asset # intersect_pairs
The entries of two meshes that cut each other. One thread per triangle of mesh A walks the bounding volume
hierarchy of mesh B (the `bvh` family builds it, one primitive a triangle) and, for every triangle of B whose
bounds overlap, lists each part of the cut of the two triangles that `intersect_core` states (the ordinary
cut, the coplanar clips, the points on shared edges).
An entry is three words: `a`, `b` and the part. Two stages with an exclusive prefix sum (the `scan` kernel)
between them: `STAGE count` writes how many entries each triangle of A has, `STAGE write` writes the entries
at the offsets the sum gives, ordered by `a` and, within a triangle of A, in the order the tree is walked and
the parts of a pair in increasing order. Exact: the entries and their order are the same on every run.
Bind `apts`, `atri`, `bpts`, `btri`, `nodes` and, in the write stage, `start`; params `nb` (triangles of B) and
`total` (the sum of the counts, which sizes `pairs`, three words an entry). The count stage writes `cnt`. Over a live
count (`COUNT=device`, see the abi README) a triangle of A past the live ones has no entries.
## The intersect family
`intersect` is a kernel family: `intersect_pairs`, `intersect_cuts`, `intersect_unify` and
`intersect_retri` (shared by `intersect_core`). Two triangulated meshes A and B go in; the pieces of
both, cut along their common curve and along the regions they share, come out. `geometry.boolean` composes the
family with `bvh`, `scan`, `sort`, `compact` and `gather`:
1. Build the hierarchy of B (`bvh_codes`, `sort`, `bvh`, `bvh_pyramid` per level, `bvh_internal`), one
primitive a triangle (`vstart` 3 t, `vcount` 3).
2. `intersect_pairs` `STAGE count`, an exclusive `scan` of the counts, `STAGE write`: `pairs` (a, b, part),
ordered by a. `total` is the scan's total, the number of entries.
3. `intersect_cuts` over `total` entries: the endpoint records, slot `2 * entry + e`. Word 0 to 2 of a record
are the position as f32 bits.
4. `intersect_unify` (three stages) over `2 * total` slots: `rep`. `compact` of the slots with
`rep[s] == s` numbers the cut points; a cut endpoint's point is the number of `rep[s]`, and its
position is the record's position.
5. `intersect_retri` for A with `runs` from the counts and `start` and `order` the entry indices, `side` 0,
then for B with the entry indices stably sorted by b (`sort` on the b words) as `order`, `runs` the first
and count of each b and `side` 1. `STAGE count`, `scan`, `STAGE write`. Vertex references at or above
`npts` are cut endpoint slots, mapped through `rep`.
The pieces of each mesh share the cut points, so the two results agree along the curve and along the
boundary of every shared region. Each piece lies on one side of the other mesh, or on its surface.
## What a boolean reads
A boolean keeps pieces by what lies just outside and just inside each piece's centre, probed along the
piece's normal against the other mesh (`ray` winding at both probe points). Pieces on the other mesh's
surface read the same way:
- Same normal (the two surfaces coincide and face one way): the probe outside the piece is outside the other
mesh and the probe inside it is inside the other mesh.
- Opposite normals (the surfaces coincide and face each other): the probe outside the piece is inside the
other mesh and the probe inside it is outside.
| Operation | Pieces of A kept | Pieces of B kept |
|---|---|---|
| union | outside probe outside B | outside probe outside A and inside probe outside A |
| intersect | inside probe inside B | outside probe inside A and inside probe inside A |
| subtract (A minus B) | inside probe outside B | outside probe inside A and inside probe inside A, flipped |
A region shared with the same normal is kept once from A in a union and an intersection and dropped from
both in a subtract; a region shared with opposite normals is dropped from both in a union and an
intersection, and kept from A in a subtract.
·computeshader · born here
❒asset # intersect_retri
The triangles of one mesh retriangulated along the segments that cut them. One thread per triangle: a
triangle no segment reaches is written as itself; a cut triangle is worked in its own plane, in world
lengths, where its corners and the cut endpoints are the points and its sides and the cut segments are the
constraints. The triangulation holds every constraint first (each cut out by the points that lie on it),
then holds the remaining candidate edges shortest first, a length class at a time (the exponent and top
three mantissa bits of the squared length), each when it crosses no held edge and has no point on it. The held edges are a planar graph: the
neighbours of each point are sorted by direction, the faces are walked with their inside on the left, a face
of three points is a triangle and a face of more is cut ear by ear (each ear a corner that turns left with no
other point of the face inside it or on its sides), and the face outside the triangle is left out. Every
triangle is wound the way the source triangle was, so the pieces of a triangle agree on the direction of every
side they share however thin the sliver between two cut points. A triangle whose three points stand on one
line in the world has no area and is left out. Every cut segment is a union of edges of the result, and the
pieces of a triangle tile it. Points and distances are compared within a few steps of the coordinates'
precision (`5e-7` of the largest coordinate of the triangle, and no less than `1e-7` of its longest side),
the same measure for a triangle of either mesh, so cut points a few millionths apart are told apart by the
triangles of both meshes alike.
Param `side` names the mesh the pass cuts, 0 for A and 1 for B; a segment cuts the mesh when its endpoint
records say so (`intersect_core`). A triangle takes every point of its entries, a segment of no length
giving a point on an edge and nothing more.
`STAGE count` writes how many triangles each triangle becomes; `STAGE write` writes them at the offsets of
the prefix sum, four words each (three vertex references, the source triangle) from word 4 of `out`. A
reference below `npts` is a point of the mesh; `npts + s` is the cut endpoint in slot `s`, which
`intersect_unify`'s `rep` names. `out[0]` is 1 when a triangle ran out of working space, which the sizing
below prevents. `geometry.boolean` builds the pieces of each mesh from `out`.
It binds `pts`, `tri`, `cut`, `runs`, `order` and `out`, with `cnt` in the count stage and `off` (the exclusive
prefix sum of the counts) in the write stage; the params are `npts`, `total`, `entries` and `side`. Over a live count
(`COUNT=device`, see the abi README) a triangle past the live ones becomes no triangle.
## Working space
A triangle takes a slice of working memory sized by its entries, so the number of points a triangle can
carry follows the data: a run of `m` entries gives at most `3 + 2 m` points, `3 + m` segments and three
edges a point, and the slice is `64 + 40 m` words. The slices of consecutive triangles follow one another
and make the tail of `out`, after its 4 header words and the `4 * total` words of the triangles
(`total` is 0 in the count stage): the slice of triangle `t` starts `64 * t + 40 * first(t)` words into the
tail, `first(t)` the first entry of its run, so the runs' firsts are the exclusive prefix sum of their
counts (the `start` of `intersect_pairs` for mesh A, the offsets of the per-b counts for mesh B). `out`
is sized `4 + 4 * total + 64 * n + 40 * entries` words from the params `total` and `entries` (the number of
entries of `intersect_pairs`).
The triangles of mesh A are grouped by `a` already (`runs` is `start` and the counts of
`intersect_pairs`, `order` the identity); for mesh B the entries are stably sorted by `b` first and
`order` holds the sorted entry indices. Exact: each triangle is worked alone and in a fixed order.
·computeshader · born here
❒asset # intersect_unify
The first slot of every cut point. Endpoint slots whose records name the same crossing (the kind word, `lo`,
`hi`, `tri` and `extra`) are one point; `rep[s]` is the lowest slot among them, which is the point's
identity. A slot with `rep[s] == s` is a new point's first slot, so a compaction of those slots
numbers the cut points, and every endpoint reference maps through `rep`.
Three dispatches (`STAGE claim`, `fold`, `resolve`) over a hash table `table` of `cells` words (a power of two,
at least twice the slot count) and a second table `lowest` of the same size, both zero before the first stage.
`table`, `lowest` and `rep` (one word a slot) carry from stage to stage: each stage is bound the three the stage
before it wrote, so `fold` reads the claims and `resolve` the cells `fold` recorded in `rep`. Exact: the cell a key
takes depends on thread timing, the answer does not.
Over a live count (`COUNT=device`, see the abi README) a slot past the live ones is not read or written.
`geometry.boolean` numbers its cut points with it.
·computeshader · born here
❒asset # jump
One round of pointer jumping over a run of links, three ways:
- `rank` (MODE rank): list ranking. Repeated rounds give each element its root and
its distance to it, for chains and trees. Both live in the double-buffered
`parent`/`parent_out` and `value`/`value_out` pairs; the caller swaps them
between rounds.
- `min` (MODE min): doubling of the smallest label within reach. After
ceil(log2 n) rounds, every element on a cycle holds the cycle's smallest label.
- `label` (MODE label): label propagation over pairs, in place. Iterated to
`changed` = 0 it labels each connected set of elements with its smallest label.
`changed` counts what the round altered; the caller zeroes it before a round and
reads it after, so iteration with a stop is one dispatch and one count per round.
Integers are 32-bit words and every mode is exact: the final labels do not depend
on the order invocations ran in.
The label mode reads the pairs from `la` and `lb`; rank and min read `parent` and
`value` and write `parent_out` and `value_out`. `core.boundary` orders its loops
with min and rank rounds, `curve.chain_ranks` ranks chains with it, and
`core.islands` labels connected sets with the label mode.
Over a live count (`COUNT=device`, see the abi README) the elements (rank, min) or
the pairs (label) are the first `domain_count` slots of a capacity `n`, and the `n`
param is not read. In rank and min mode a slot at or past the live count links to
itself with the value 0. It binds 8 storage buffers (7 in the plain variant).
·computeshader · born here
❒asset # loops
Orders directed edges into rings and chains. This kernel links each edge to the
edge that follows it (`next`) and marks the edges a chain starts with (`head`);
the `jump` and `loops_cut` and `loops_place` kernels do the rest.
For the boundary of a mesh (`core.boundary` runs the sequence), the directed edges
are its open edges run the way their one face runs them (`tail` is `a` when the
`core.edges` table's `reversed` is 0, else `b`). The whole sequence over `m` edges
and `npts` points:
1. `loops` (`STAGE` clear, claim, link): `next`, `head`.
2. `jump` (`MODE` min), ceil(log2 m) rounds, starting from `parent` = `next` and
`value` = the edge index: every edge holds the smallest index within reach,
and `parent` holds the edge 2^k steps ahead.
3. `loops_cut` (`STAGE` head, cut): a ring (a cycle) starts at its smallest edge; a chain
starts at its head. The edge before each start becomes the end of its list.
4. `jump` (`MODE` rank) over the cut lists, starting with `value` 1 on every
non-end edge: each edge's end and its distance to it.
5. `loops_place` (`STAGE` claim, place): each edge's ring (the first edge of its list) and its
position along the ring.
A compaction of the first edges numbers the rings, and a prefix sum of ring
lengths (`rank` of the first edge plus 1) gives the ring starts; an edge's place in
the ordered listing is its ring's start plus its position.
Over a live count (`COUNT=device`, see the abi README) the stages claim and link run
over a capacity of edges whose live count is `domain_count`, and the `n` and `npts`
params are not read. Stage clear runs over the capacity of points and writes the
cleared value into every slot, which is the value a slot past the live points holds.
In stage link a slot past the live edges links to itself and is no head. It binds 7
storage buffers (6 in the plain variant).
·computeshader · born here
❒asset # loops_cut
Finds where each ring and chain of linked edges starts and ends. `STAGE head`
marks the first edge of every list (a chain's head, a ring's smallest edge) in
`head`, from `next` and `head0` (the `loops` outputs) and `label` and `par` (the
min rounds of `jump`); `STAGE cut` makes the edge before each first edge the end
of its list in `cut`. The lists that result are what `jump` ranks. See `loops`
for the whole sequence.
Over a live count (`COUNT=device`, see the abi README) the edges are the first
`domain_count` slots of a capacity `n`, and the `n` param is not read. A slot past
the live edges is no head (`head` 0) and ends its own list (`cut` is the slot itself).
It binds 7 storage buffers (6 in the plain variant).
·computeshader · born here
❒asset # loops_place
Gives every edge its ring and its position along it, from the ranked lists
`loops_cut` and `jump` produced. `ring` is the first edge of the edge's list, so
two edges are on one ring exactly when their `ring` values are equal; `pos`
counts along the ring from that first edge. `STAGE claim` records each list's
first edge in `headof` under the list's end (`root`); `STAGE place` reads it back
for every edge and writes `ring` and `pos` from `rank`. `core.boundary` lists its
loops by `ring` and `pos`. See `loops` for the whole sequence.
Over a live count (`COUNT=device`, see the abi README) the edges are the first
`domain_count` slots of a capacity `n`, and the `n` param is not read. A slot past
the live edges has the ring -1 and the position 0. It binds 7 storage buffers (6 in
the plain variant).
# luau_noise
The Perlin noise of Luau's `math.noise`, for a field chain's kernel: `zero_luau_noise(x, y, z)` answers what
`math.noise(x, y, z)` answers on the CPU, from the same permutation table (`ZERO_LUAU_NOISE_HASH`) and gradient
table (`ZERO_LUAU_NOISE_GRADIENT`). The value is about -1 to 1, 0 at every whole-number point, and repeats every 256
units in each axis; the coordinates are wrapped into that period first.
```wgsl
#include "@builtin::systems.procgen.kernels.luau_noise"
fn ripple(p: vec3<f32>) -> f32 { return zero_luau_noise(p.x, p.y, p.z); }
```
The `math.noise` node of `nodes/math.nodebox` (the `noise` operation of `proc.fieldc.operators`) includes this module
in the chain it writes.
·computeshader · born here
❒asset # make
Builds the elements and the primitives of a geometry from a count per owner and
a shape (`points`, `strip`, `fan`, `grid`, `ring`, the `SHAPE` specialization).
The result is the columns a Geometry holds: `P` over the points, `vertex_pt` over
the vertices (the point each reads), and `prim_vstart` / `prim_vcount` over the
primitives (each one's run of vertices). All indices are into the result, so
owners' pieces sit end to end with their links already offset.
It runs as two passes of one shader (`PASS`): `sizes` writes the points,
vertices and primitives each owner makes; after a prefix sum of each component
into `bases`, `fill` writes every element once, found by a binary search over the
bases. Exact: each output element has one writer. The shape's layout is stated
at the top of `bindings.yaml`; `grid` takes its second axis from the `across`
param (default 2, at least 2).
`core.make` runs it: the `sizes` pass, a `scan` of the sizes, then the `fill`
pass, and the columns become the Geometry the node answers.
·computeshader · born here
❒asset # map
The per-element kernel: `dst[id] = TEMPLATE` over `n` elements. The template is
a WGSL expression over `p` (the element's position), `id`, the numbers `u_get(k)`
and the columns of the `cols` table; the preludes `rand`, `noise` and `color`
supply the functions a template calls. `fieldc.mapSpec` writes a chain of field
templates as one function, so a chain of any length is one dispatch.
Specialize `T` (f32, i32, vec2, vec3 or vec4 of f32), `TEMPLATE`, and optionally
`PRELUDE` and `POSITION`. Bind `cols` (a table from `compute_abi.table`) and `u`.
The output `dst` holds `n` elements of `T`, tightly packed.
```luau
local program = ref:program()
local variant = program:variant({ specialize = { T = "f32", TEMPLATE = "zero_rand(7u, id)",
PRELUDE = '#include "@builtin::systems.procgen.kernels.rand"' } })
local run = variant:run({ n = 64, inputs = { cols = emptyTable, u = numbers } })
```
`fieldc.evaluate(field, domain)` runs a Field through this kernel and answers the Column it wrote. Leaves of Int and UInt runs are read through `cols_i32` and `cols_u32`, and a read past a run's end reads its last element.
# noise
Lattice noise over the shared hash of `rand`: `zero_proc_value3(seed, p)` in
[0, 1), `zero_proc_gradient3(seed, p)` in [-1, 1], `zero_flow_worley(seed, p,
jitter)`, `zero_flow_simplex(seed, p)`, `zero_flow_curl(seed, p, freq, amp)`,
and the octave sums `zero_noise_fbm_value` / `zero_noise_fbm_gradient`. The plane forms `zero_proc_value2(seed, p)`, `zero_proc_gradient2(seed, p)`, `zero_noise_fbm2(seed, p, octaves, lacunarity, gain, gradient)`, `zero_noise_fbm_value2` and `zero_noise_fbm_gradient2` return the values of `proc.noise` `value2`, `gradient2` and `fbm2`, to f32 rounding of the interpolation. Each
returns the values the field ops of the same name compute on the CPU, to f32
rounding of the interpolation.
```wgsl
#include "@builtin::systems.procgen.kernels.noise"
fn height(p: vec3<f32>) -> f32 { return zero_noise_fbm_gradient(7u, p * 0.1, 4u); }
```
# noise_angle
Gradient noise over a plane whose lattice gradients are unit vectors at a hashed angle, and the fractal sum of it.
- `zero_angle_gradient2(p, seed)`: gradient noise with quintic interpolation, roughly -0.7 to 0.7.
- `zero_angle_fbm2(p, octaves, lacunarity, gain, seed)`: `octaves` layers, each `lacunarity` times finer and `gain` times as strong as the one before, octave `i` (from 0) drawn from `seed + i * 7919`, summed and divided by the root of the sum of the squared strengths.
- `zero_angle_warped2(p, warp, feature, seed)`: `p` pushed by a second noise, two 4-octave sums from seeds of their own, scaled by `warp / feature` noise units; a `warp` of 0 or less leaves `p` where it is.
```wgsl
#include "@builtin::systems.procgen.kernels.noise_angle"
fn relief(p: vec2<f32>) -> f32 { return zero_angle_fbm2(p, 6u, 2.0, 0.5, 1u); }
```
# order_key
Maps the 32-bit word of a float or a signed integer to an unsigned key that
sorts the way the value does, and back. `zero_order_key(word, is_float)` and
`zero_order_word(key, is_float)` are inverses. A kernel that keeps the least or
the greatest of many contributions runs `atomicMin` or `atomicMax` on keys and
reads the same answer whatever order the contributions arrive in.
# rand
The lattice hash and the unit random draw the procgen kernels share:
`zero_proc_fnv_u32`, `zero_proc_avalanche`, `zero_proc_unit(prefix, element)`,
and the seeded forms `zero_rand(seed, element)`, `zero_rand_range`,
`zero_rand3`. The draw equals the CPU hash to the last bit, because it folds
the same bytes and mixes with the same decimal constants.
```wgsl
#include "@builtin::systems.procgen.kernels.rand"
fn jitter(id: u32) -> f32 { return zero_rand_range(7u, id, -0.5, 0.5); }
```
`rand.unit(seed, ...scope, id)` of the field nodes is `zero_proc_unit(prefix, id)`: the prefix is `hash.scopePrefix` of the seed and scope folded once on the CPU, so the draw equals `hash.unitFloat` to the bit.
·computeshader · born here
❒asset # ray
Casts rays and segments against the triangles of a geometry through the
hierarchy the `bvh` kernel builds (closest hit, or any hit for occlusion), and
computes the generalized winding number of the geometry about each origin, which
is the inside test a boolean or a distance field needs. A hit is the parametric
t, the primitive index and the barycentric weights. The header of
`bindings.yaml` states the ray and hit layouts: a ray is 8 words of `rays`
(origin xyz, tmin, direction xyz, tmax); a miss writes t = -1 and `hit_prim`
0xffffffff. Exact: the same rays give the same hits, ties going to the lower
index.
`ray` binds `rays`, the geometry's `pts` (xyz a point), `vpt`, `vstart` and
`vcount`, and the `accel` node table the `bvh` kernel builds, with the `np` param
the number of primitives; the `STAGE` specialization picks `closest`, `any` or
`winding`. The winding stage sums over every primitive and reads only the origin
of each ray. `geometry.boolean` and `geometry.to_sdf` run the `winding`
stage for their inside test, and `sim.particles` runs `ray_shapes` for its
collisions.
`ray` is a family of three kernels, since one dispatch binds at most 8 storage
buffers a stage:
| Kernel | Answers |
|---|---|
| `ray` | `hit` (t, u, v, fan) and `hit_prim` (the primitive, the shape index of a mesh hit) for the `closest`, `any` and `winding` stages |
| `ray_surface` | for the `closest` hits `ray` found: `hit_normal` (unit triangle normal facing the ray's origin side) and `hit_end` (the hit point pushed out along the normal by the `skin` param; a miss answers the ray's end) |
| `ray_shapes` | the analytic shape table, bound as a `columns` binding (`shapeKind, center, extent, rotation, bounce, friction, index`; a plane's extent is its unit normal, a sphere's extent.x its radius): `hit`, `hit_prim` (the row of the shape table), `hit_normal`, `hit_end` |
Chain for a mesh: dispatch `ray` with `STAGE = closest`, then `ray_surface` with
the same `rays`, geometry bindings and the `hit` and `hit_prim` that `ray`
wrote.
·computeshader · born here
❒asset # ray_shapes
Casts rays and segments against a table of analytic shapes (plane, sphere, box,
capsule) and answers the closest hit of each. A ray is 8 words of `rays`: origin
xyz, tmin, direction xyz, tmax; the direction need not be unit length, so a
segment from a to b is origin a, direction b - a, tmin 0, tmax 1. The table is one `columns`
binding named `shapes`, a row a shape, columns in this order:
`shapeKind` (Int: 0 plane, 1 sphere, 2 box, 3 capsule), `center` (Vec3),
`extent` (Vec3: a plane's unit normal, a sphere's radius in x, a box's half
size, a capsule's radius in x and half length in y), `rotation` (Vec4
quaternion x y z w; a capsule lies along its own y), `bounce`, `friction`
(Float, carried for the caller) and `index` (Int, the caller's id for the
shape).
Outputs, a value per ray: `hit` (t, then three zeros), `hit_prim` (the row of the
hit shape, `0xffffffff` for a miss), `hit_normal` (the unit surface normal,
turned to face the ray's origin side, zero for a miss) and `hit_end` (the hit
point pushed out along the normal by the `skin` param; for a miss the ray's end,
origin + direction * tmax). A shape is hit from outside at its entry and from
inside at its exit; of two hits at equal t the lower row wins. Exact.
`sim.particles` runs it in its collision stage, against the shape table of its
colliders.
·computeshader · born here
❒asset # ray_surface
The normal and the corrected end of the closest triangle hits the `ray` kernel
found. Bind the same `rays`, `pts`, `vpt` and `vstart` the ray dispatch read,
and its `hit` and `hit_prim` outputs. Outputs, a value per ray: `hit_normal`
(the unit normal of the triangle hit, turned to face the ray's origin side,
zero for a miss) and `hit_end` (the hit point pushed out along the normal by the
`skin` param; for a miss the ray's end, origin + direction * tmax). The shape
index of a hit is `hit_prim`, the primitive. Exact.
·computeshader · born here
❒asset # reduce
Folds a run of elements to one element per group: the sum, least, greatest,
mean, count, or the index of the least or greatest, of the whole run or of each
segment of it.
| Specialize | Options | Meaning |
|------------|---------|---------|
| `T` | `f32`, `i32`, `vec2` to `vec4` of either | the element; a vector folds lane by lane |
| `FOLD` | `add`, `min`, `max`, `mean`, `count`, `argmin`, `argmax` | the fold |
The dispatch size `n` is the number of elements. One workgroup folds one group at
a time, so a segmented fold runs its segments side by side and an unsegmented
one is a single group; any workgroup count from 1 up folds every group.
`params.segments` above 0 makes `starts` partition the run into that many
segments: it holds where each segment begins (a segment ends where the next
begins, the last at `n`), and `dst` holds one element per segment. With
`segments` 0 the whole run is one group, `starts` is any one-word run, and `dst`
holds one element. An empty group answers 0 for every
fold. `argmin` and `argmax` write the lowest index among equal elements to
`picked` (for a vector element, the element that beats the best before it in
every lane); every other fold writes -1 there. The mean of an integer run is
the integer quotient. The bounds of a run are its `min` and its `max`.
Exact: every group folds its elements in a fixed order and combines the 256
partial results in a fixed tree, so the same run gives the same bits on every
dispatch, and an integer fold is the exact integer answer.
Over a live count (`COUNT=device`) `n` is the capacity and the first `domain_count` elements are the run: the last
segment ends at the live count. Over live segments (`SEGMENTS=device`) `params.segments` is the most segments there can
be, which sizes `dst` and `picked`, and the first `segment_count` of them are live; a group past them is not written, and
a run of no live segments folds nothing.
Nodes that fold columns run it, among them `core.reduce`, `core.select`, `core.arclength`, `core.normals.face`,
`attribute.promote` and `sim.particles`.
·computeshader · born here
❒asset # resample
Image resample of one or more slices (a Texture's texels, a Grid layer, each
tile of a Raster) to a target size, with the `FILTER` specialization (`bilinear`, `lanczos3`, `catmull_rom`, `nearest`). Downscaling widens the filter by the shrink ratio so it
averages what it drops. `axis` resamples width (0), height (1) or both (2); width
then height gives the both-axes result up to float rounding.
`determinism: exact`.
`src` holds `slices` images of `sw` x `sh` pixels, `lanes` f32 values a pixel,
slice by slice with rows x fastest; `dst` holds the same layout at the output
size (`dw` x `sh`, `sw` x `dh` or `dw` x `dh`) and is bound by the caller. The
lanes `laneBase` to `laneBase + laneCount - 1` (at most 16) are resampled and
written; `clampRange` 1 holds each result to `lo` .. `hi`. One thread per
output pixel of every slice, numbered `gid.x + gid.y * num_workgroups.x * 64`,
so a dispatch past 65535 workgroups spreads over rows. The bindings are bound by
list position.
# run_sides
How many sides a primitive's run of vertices has (`zero_run_sides`), which vertex
the end of a side lands on (`zero_run_next`), and `ZERO_NONE`, the value a slot
holds when it names no edge or point. The `edges` and `topology` kernels walk
runs with them, so both agree on what a face's perimeter is.
·computeshader · born here
❒asset # scan
The prefix fold of a run of elements, whole or per segment: running sums,
running least and greatest, running products. (The running product of 4x4
matrices is `scan_mat4`.)
| Specialize | Options | Meaning |
|------------|---------|---------|
| `T` | `f32`, `i32`, `vec2` to `vec4` of either | the element; a vector scans lane by lane |
| `OP` | `add`, `min`, `max`, `mul` | the operator |
`params.exclusive` picks the fold of the elements before each one or the fold up
to and including it. The first element of a group has nothing before it: an
exclusive scan holds 0 there for `add`, 1 for `mul`, and the element itself for
`min` and `max`. `total` holds the fold of each group, 0 for an empty group. A
segmented scan (`params.segments` above 0, the number of segments) restarts at
each start in `starts`; an unsegmented one binds any one-word run there.
One workgroup scans one group at a time, a tile of 128 elements at a time, with
the fold of everything before a tile carried into the next. The dispatch size
`n` is the number of elements; any workgroup count from 1 up scans every group.
Exact: the order of combination is fixed, so the same run gives the same bits on
every dispatch, and an integer scan is the exact integer answer.
Over a live count (`COUNT=device`, see the abi README) `n` is the capacity and the
first `domain_count` elements are the run. Every slot of `dst` at or past it holds the
total of the last group (the whole run, or the last segment; 0 for an empty one), which
is the scan of the run padded with the operator's identity. An `i32` scan's `total` is a count another kernel takes as its
domain (`{ count = total, capacity = ... }`). It binds 5 storage buffers (4 in the plain
variant).
Over live segments (`SEGMENTS=device`) `params.segments` is the most segments there can be, which sizes `total`, and the
first `segment_count` of them are live: a segment past them is not scanned, and a run of no live segments scans nothing.
It binds one storage buffer more than the variant it extends.
Nodes that need offsets, running totals or arc lengths run it, among them `core.make`, `core.select`, `core.edges`,
`core.arclength`, `core.realize`, `core.sample_curve`, `core.sample.surface`, `attribute.accumulate` and
`sim.particles`.
·computeshader · born here
❒asset # scan_mat4
The running product of a run of 4x4 matrices, whole or per segment: the frames
of a sweep, each placed relative to the one before it.
A matrix is 16 consecutive floats, column by column. The product runs in run
order, so `out[i] = m[0] * m[1] * ... * m[i]`. `params.exclusive` picks the
product of the matrices before each one (the identity for the first) or the
product up to and including it. `total` holds each group's product, the
identity for an empty group. A segmented scan (`params.segments` above 0, the
number of segments) restarts at each start in `starts`.
One workgroup scans one group at a time, a tile of 128 matrices at a time, with
the product of everything before a tile carried into the next. The dispatch size
`n` is the number of matrices; any workgroup count from 1 up scans every group.
Exact: the order of multiplication is fixed, so the same run gives the same bits
on every dispatch.
·computeshader · born here
❒asset # scatter
Folds the elements of a column into the places a run of indices names: element
`i` of `src` goes to place `index[i]` of the result, and elements that meet in
one place are combined by the `FOLD` specialization (`add`, `min`, `max`,
`mean`, `set`). `T` is the scalar a lane holds (`f32` or `i32`); an element is
`lanes` words. Alongside the result the kernel counts the elements folded into
each place (`hits`) and records one more than the greatest source index that
reached it (`last`, 0 when none), so a node can tell a place nothing reached from
one that holds zero. `set` keeps the element with the greatest source index, and
a place nothing reached holds zeros. An index outside `0` to `size - 1` is
skipped.
It runs as three passes of one shader, `init`, `fold` and `finish`, chosen by the
`PASS` specialization. Min, max, set and every integer fold give the same answer
whatever order the contributions arrive in. Float add and float mean sum in
arrival order, and the kernel declares `float_order` for them; with `EXACT` on,
each float is rounded to a multiple of `1 / scale` and summed as an integer,
which is the same on every run.
Over a live count (`COUNT=device`, see the abi README) the fold pass's `n` is the capacity of the indices and the first
`domain_count` of them are folded; an index past it reaches no place. The `init` and `finish` passes run over the places
(`size`) as they always do, and a node that sizes the places by a capacity states the live indices through the fold
pass alone. It binds 6 storage buffers (5 in the plain variant).
Over live places (`SIZE=device`) the places are the first `size_count` of the `size` the outputs are allocated for: the
node bound `size` as `{ live = count }`, and the `init` and `finish` passes leave the places past it alone while the `fold`
pass sends an index past it nowhere. The result carries the count as its live count. It binds one storage buffer more.
`core.scatter` runs the three passes.
·computeshader · born here
❒asset # search
Binary search of a run of queries in a sorted column: the pick of a value from
a cumulative distribution, the segment of an arc length.
| Specialize | Options | Meaning |
|------------|---------|---------|
| `T` | `f32`, `i32` | the scalar the column and the queries hold |
| `SIDE` | `lower`, `upper` | which boundary a query answers |
`lower` answers the first place whose value is not below the query, `upper` the
first place whose value is above it, and both answer the length of the column
when no place qualifies. The greatest place whose value is not above a query is
`upper - 1`; with the cumulative sums of a run of weights, `upper` of a draw in
`[0, total)` is the index of the weight it lands in.
The column bound as `sorted` is the sorted run, every element of it. One thread
per query; the dispatch size `n` is the number of queries.
Exact: a search reads the column and nothing else, so the same inputs give the
same answers on every dispatch.
`core.sample.surface` picks a primitive by area with `upper`; `attribute.normalize`
and `geometry.shared` search with `lower`.
·computeshader · born here
❒asset # search_curve
Places samples on curves by arc length. Each sample names a curve (`owner`, Int) and how far along it (`at`, Float, 0
at the start to 1 at the end); the kernel answers the segment it stands on (`segment`, a `vec2<i32>` of the vertex the
segment starts at and the one it ends at), where it stands (`placed`: the position in xyz and how far along the
segment in w, 0 at its start to 1 at its end) and the segment's unit direction (`heading`, xyz, zero for a segment of
no length).
| Specialize | Options | Meaning |
|------------|---------|---------|
| `COUNT` | `plain`, `device` | every slot is live, or the live count is read from `domain_count` and a slot at or past it is left alone |
The curves are described per vertex by `vertices` (`vec4<f32>`: the position in xyz and the arc length from the start
of the first curve in w, ascending) and per curve by `curves` (`vec4<f32>`: the vertex its run starts at in x, how many
vertices it holds in y, negative for a closed curve, the arc length that comes before it in z and its own length in
w). A sample stands at arc length `curves[owner].z + at * curves[owner].w`.
A sample on the last vertex of an open curve stands on the segment before it. On a closed curve the last vertex starts
the segment that closes the loop.
One thread per sample; the dispatch size `n` is the number of samples. Exact: every output element has one writer and
reads only its inputs, so the same inputs give the same answers on every dispatch.
`core.sample_curve` runs it and takes the sample's position, its blend between the segment's ends and its tangent from
the outputs.
·computeshader · born here
❒asset # sort
A stable radix sort of a column's keys into a permutation: place `s` of `perm`
holds the index of the element that sorts `s`-th, so a gather by `perm` puts
any column in key order.
| Specialize | Options | Meaning |
|------------|---------|---------|
| `KEY` | `f32`, `i32` | the scalar a key holds |
Elements with equal keys keep their run order, in both directions
(`params.descending` 1 sorts from the greatest key down), and `-0` and `+0` are
one key. The sort works 4 bits of the key at a time from the least significant
up, and a pass whose digit is the same in every key is skipped, so a run of keys
inside a narrow range takes fewer passes. `work` is the sort's second
permutation; its contents after a dispatch are not an answer.
The dispatch size `n` is the number of keys. One workgroup of 128 works the whole
run; the other workgroups of a larger dispatch return.
Exact: the sort is a function of the keys and their order, so the same keys give
the same permutation on every dispatch.
Over a live count (`COUNT=device`, see the abi README) `n` is the capacity and the
first `domain_count` keys are live: they are sorted, and a slot at or past the live
count holds its own index in `perm`. A live count of 0 sorts nothing and `perm` is
the identity. It binds 4 storage buffers (3 in the plain variant).
Nodes that order or group elements run it, among them `attribute.order`, `elements.sort`, `core.edges`,
`core.adjacency`, `core.spatial`, `geometry.bvh`, `geometry.boolean`, `geometry.decimate` and `geometry.shared`.
# tile_layout
How a field of cells is stored as tiles: a tile holds its core cells plus an
apron of copies of its neighbours' cells, and a tile covering the whole field
with no apron is a plain grid with x fastest. The includer states the layout as
a `ZtLayout` and uses `zt_decode` (stored cell number to tile, local and field
cell), `zt_owner_index` (the core cell that owns a field cell) and
`zt_neighbour_index` (a stored neighbour held to the tile's edge).
```wgsl
#include "@builtin::systems.procgen.kernels.tile_layout"
```
·computeshader · born here
❒asset # topology
Mesh edit operations over a geometry's linkage (`vertex.pt`, `primitive.vstart`,
`primitive.vcount`), selected by the `OP` specialization (`split_edge`, `collapse_edge`, `insert_face`, `delete_face`, `split_vertex`), each stage by `STAGE` (`count`, `write`): split an edge (insert a point),
collapse an edge (merge two points and drop the faces that degenerate), insert a
face, delete a face, split a vertex (give a group of corners a point of their own).
Each operation runs in two stages with an exclusive prefix sum between them. The `count`
stage writes `cnt`, how many output vertices each primitive contributes (`insert_face`
appends one entry per new face after the `np` primitives). The caller's exclusive prefix sum
of `cnt` is `off`. The `write` stage writes the output vertices' points (`o_pt`) and where
each came from (`o_src`). The output primitives are `off` as run starts and `cnt` as run
lengths; a primitive with an empty run is gone, and `core.select` removes it. `split_vertex`
runs per vertex instead: `cnt[v]` is 1 for the first vertex of each group, the groups'
new points are numbered from `npts` in order of their `off`, and `o_pt[v]` / `o_src[v]`
are written at the vertex's own slot. The shader's header states the records each
operation reads in `rec`.
The params are `np`, `nv` and `npts` (the input's primitives, vertices and points),
`nnew` (the faces `insert_face` appends), `wrap` (1 when runs wrap from their last vertex
to their first) and `minv` (the fewest vertices a primitive keeps after a collapse). The
caller binds `nv_out`, the output vertex count `o_pt` and `o_src` are sized by (the total
of the prefix sum). The dispatch is one thread per primitive, per primitive and new face
for `insert_face`, and per vertex for `split_vertex`.
The operation's records come from the edge table: for split edge, the point
for each split edge gathered to the vertex slots through `edges`' side slots
(both faces of an edge name the same point); for collapse, the point each
point becomes, from an independent set of edges. The point an inserted
vertex or a split vertex stands for is carried by the output maps: `o_src` names the vertex
whose attributes an output vertex takes (negative when it is new, with ~v the
vertex a split inserted after), and the compaction of the records names each
new point's source. Integers are 32-bit words, exact.
`geometry.decimate` runs `collapse_edge`.
·computeshader · born here
❒asset # walk
One thread per walker running `steps` dependent steps: each walker's start state goes in, its end
state and the state after every step come out, with the number of steps it ran. A state is 1 to 8
floats (`width`).
The step is a Field, handed to the kernel the way `map` takes its template: the fields package
fuses the Field into WGSL expressions and the kernel is specialized with them. A node holds no
WGSL.
| Specialization | Type | Meaning |
|---|---|---|
| `WALK_STEP` | `vec4<f32>` expression | lanes 0 to 3 of the next state; default `zero_walk_advance(s)`, the constant step |
| `WALK_STEP_HI` | `vec4<f32>` expression | lanes 4 to 7 of the next state; default `s_hi` |
| `WALK_DONE` | `bool` expression | whether the walker stops before step `k`; default `false` |
| `WALK_PRELUDE` | module-scope WGSL | the functions the expressions call, and `#include` lines for the `rand`, `noise` and `color` preludes |
The expressions read the state leaves `s` (lanes 0 to 3) and `s_hi` (lanes 4 to 7), the step
number `k` (u32) and the walker number `id` (u32), the numbers of `u` (`u_get(n)`) and the columns
of the `cols` table (`cols_f32(slot, i, lane)`, `cols_vec3(slot, i)`), which is how a step reads a
raster, a field column or a per-walker attribute. The constant step needs no Field: its vector is
the params `dx`, `dy`, `dz` and `dw`.
Bind `cols` (a table from `compute_abi.table`), `u`, `start` (`width` floats a walker). Outputs:
`finish` (`n * width`), `path` (`n * steps * width`, row `k` of walker `i` at
`(i * steps + k) * width`) and `steps_run`.
```luau
local variant = ref:program():variant({ specialize = {
WALK_STEP = "vec4<f32>(s.x + 1.0, s.y * 0.5, 0.0, 0.0)",
WALK_DONE = "s.x > 8.0",
} })
local run = variant:run({ n = walkers, inputs = { cols = emptyTable, u = numbers, start = startBuffer },
params = { steps = 8, width = 2 } })
```
`fieldc.walkSpec(step, opts)` writes the specializations from a step Field over the leaves `field.walk_state` and
`field.walk_index`, and `fieldc.bindSpec(spec)` the `cols` and `u` to bind; `device.kernel` runs the kernel with them
(the abi README states the form).
Exact: each walker writes its own path rows, so the result does not depend on thread order.
# walk_advance
`zero_walk_advance(s)`: a walk state advanced by the constant vector (dx, dy, dz, dw). It is
the default `WALK_STEP` of the `walk` kernel, the constant step in its Field form: a walker's
path is a straight line.
# walk_core
The loop a walk kernel runs for one walker: load its state (up to 8 floats, held as two vec4), call
the step hooks for each of `steps` steps (stopping when the stop hook says so), write the state
after every step to the walker's path rows, and write the end state and the number of steps that
ran. The includer defines `zero_walk_lo`, `zero_walk_hi` and `zero_walk_stop`, and calls
`zero_walk_run` from its entry point; the module header lists the bindings and params the includer
declares.
# Nodebox (asset type)
A nodebox is a namespace folder that groups related `.procNode/` children, the way a toolbox groups
`.tool/` children. A node's name is its folder name inside its box: `points.nodebox/disc.procNode` is
called `points.disc`, and nothing inside the node states that name. Moving or renaming a node is a VFS
move that keeps its guid.
## Where it lives
- Source: `<anywhere>/<family>.nodebox/`, usually inside a `.package`, an effect or a world's content.
- Identity: the folders above it and `<family>` (`fx.points` for `fx/points.nodebox`; the `.nodebox`
suffix strips). A graph calls its nodes by `<family>.<node>` alone.
- Folder shape (open child set):
- `README.md`: what the family is for. Required.
- `.metadata`: description and tags. Required. It also states where the box's nodes work: `context` (one of
`proc.registry.contexts()`: `geometry`, `field`, `value`, `sim`, `raster`, `material`, `prefab`) and, when every
node of the box reads the scene or assets beside its inputs or writes them beside its outputs, `pulls` /
`pushes` (`scene` or `asset`). A node that works elsewhere states its own in its `.metadata`.
`procgen` `describeNode` and the `context`, `pulls` and `pushes` filters of `searchNodes` read them.
- `<node>.procNode/`: any number of nodes.
## How a graph reaches a node
`g.<family>.<node>(args)` resolves the node by its asset identity: the box and the folder name together
(`g.points.disc`), a path (`g["/zero/source/fx/ring.procNode"]`), `~` (the nearest enclosing package,
effect or world root: `g["~/ring"]`) or a short name the asset system resolves (`g.disc`). A name that
matches two nodes is an error naming both; a name that matches none is an error naming the closest nodes.
Each call is recorded in the graph's reference table by the node's guid.
## How to create one
```luau
asset.create("nodebox", "points", { folder = "fx" })
local box = asset.resolve("points.nodebox")
asset.create("procNode", "disc", { into = box })
```
## Discovery
- `asset.list("nodebox")`: every box.
- `asset.inspect("points")`: the nodes in it and what each does.
- `asset.list("procNode")`: every node, in or out of a box.
## In the Inspector
Nodes: how many nodes the box holds, each node's full name and what it does, and a link to each.
# component (nodebox)
component.set gives every element of a class a component with the fields given.
# core
The parts the other procgen nodes are built from: connectivity (edges, boundary, adjacency, islands, owner, corners), normals, weld, select, stitch, curve measure and resample, sampling, neighbours, realize, the raster set (`raster.new`, `raster.read`, `raster.stencil`, `raster.sample`, `raster.add_layer`, `raster.remove_layer`, `raster.reduce`, `raster.column`, `raster.resolve`, `raster.blend`, and `raster.cells` under them), `column`, and `entity.read`, which brings scene entities in (assets come in through `io.import`).
Each is a graph of the four ops, the kernels of `kernels/` and other core nodes. `join` holds the module that places the parts of a join, called by `concat` and `realize`.
A graph calls them as `g.core.<name>`, for instance `g.core.edges`. A node with several outputs answers a record: `local faces = g.core.normals.face(...)` then `faces.geometry`, `faces.scaled`. Every node's README states its inputs and outputs.
# debug (nodebox)
debug.dump summarizes any graph value into a record; debug.validate checks a Geometry's structure and reports every issue it finds.
# entity (nodebox)
entity.transform and entity.bounds read one entity's pose and bounds as values, entity.points reads a set of entities as points, and entity.geometry reads the mesh an entity's Model draws as a Geometry.
# geometry
Nodes that move, join, cut, round, thicken and sample meshes. A graph transforms a geometry (`geometry.transform`, `geometry.set_position`, `geometry.merge`), welds and shades it (`geometry.weld`, `geometry.smooth_normals`), models it (`geometry.extrude`, `geometry.solidify`, `geometry.bevel`, `geometry.subdivide`, `geometry.decimate`), builds surfaces from outlines (`geometry.sweep`, `geometry.revolve`, `geometry.loft`, `geometry.ribbon`) and reads it into volumes (`geometry.voxelize`, `geometry.to_sdf`). Each is built from the core nodes and the kernels of the library.
## Nodes
- `geometry.transform`, `geometry.set_position`, `geometry.set_material`, `geometry.merge`, `geometry.triangulate`, `geometry.measure`: the basic edits and the measurement of a geometry. `geometry.merge` also joins pieces into a piece set for `instances.to_points`; a part that lacks an attribute another part carries takes the value an element without it has: an instance attribute's identity, else zero (its README says exactly how).
- `geometry.weld` (with `match = "position+normal"` it keeps hard edges), `geometry.smooth_normals`: merging points and shading normals.
- `geometry.extrude`, `geometry.solidify`, `geometry.bevel`, `geometry.subdivide`, `geometry.decimate`: modelling.
- `geometry.sweep`, `geometry.revolve`, `geometry.loft`, `geometry.ribbon`: surfaces from outlines and paths.
- `geometry.voxelize`, `geometry.to_sdf`: a mesh as cells and as a distance field.
- `geometry.boolean`: union, subtract or intersect of two closed meshes, over the `intersect` kernels.
- `geometry.delete`, `geometry.separate`: the triangles a mask field or primitive attribute selects, taken away or split into two geometries (`elements.keep` on the primitives); `geometry.recalculate_normals`: `geometry.smooth_normals` at 180 degrees, area-weighted.
- `geometry.selected`, `geometry.flip`, `geometry.profile`, `geometry.tangents`, `geometry.sides`, `geometry.shared`, `geometry.bvh`, `geometry.rings`, `geometry.first`: the parts the nodes above are built from, each usable on its own.
- `geometry.unit`, `geometry.perpendicular`, `geometry.rotate`, `geometry.transport`, `geometry.split`: vector arithmetic over sockets that the nodes which orient points and normals share.
- `geometry.offsets`, `geometry.kept`, `geometry.following`, `geometry.trim`, `geometry.blank`: column arithmetic the nodes above share (a prefix sum of counts, the places a mask keeps, a ring read one place on, the primitives of no area dropped, a one-word column to bind).
## Conventions
- Each node's folder name is its call name inside the box: `geometry.<folder>`.
- Inputs and outputs carry the names of the op each node replaces.
- A node that answers a Data table from numbers read back off the device holds the Luau for it in a `readings.module` inside its own folder.
- A node with one output answers that output's value; a node with several answers a record by output name.
- A fold's index runs from 1: the first pass is 1, so a pass that works on level, chunk or step `n` reads `index - 1` when it counts from 0 (`geometry.bvh`, `geometry.to_sdf`, `geometry.sweep`, `geometry.loft`).
- A field bound to a kernel takes the length of that dispatch, so a node writes a value out as a column (a gather with an explicit `count`) before it binds one whose length differs from the dispatch's.
# instances (nodebox)
Instances: pack a geometry into a piece (instances.pack), place a piece set on target points (instances.to_points) and take a population or piece set apart (instances.unpack). A piece is one point whose `Mesh` holds a geometry whole, as the node that made it left it: a geometry whose elements are counted on the device (what `geometry.triangulate` makes) keeps its counts there, and draining the piece set (`Geometry.drain`) drains the geometries its records hold with it, so a CPU reader reads each piece's elements; `geometry.merge` with `pack` puts several together into a piece set. The copies `instances.to_points` makes are a population, which the output node draws as instanced draws, one per piece and material, and export saves as a population asset; any other points stand as one entity each.
instances.pack, instances.to_points and instances.unpack are the nodes of that chain, with `points.pose` composing the instance attributes (`P`, `orient`, `scale`, `N`, `v`, `up`, `pscale`, `rot`, `trans`, `pivot`) the copies stand at.
Earlier nodes of the nodebox stand a source on points (instances.on_points), deal one of several sources to each point (instances.pick_random, instances.pick_by_attribute), vary and move the copies (instances.randomize_transform, instances.transform, instances.set_material) and merge them into one geometry (instances.realize). The points they make carry a `Mesh` and stand as entities through the output node.
instances.place, instances.variants, instances.turn and instances.sources are the parts the others are built from: a transform written onto points, a variant index written onto points, the product of two rotations, and the geometries a population stands (the ones given, else the ones its points carry).
# io
The ways in and out of a graph: `import` brings an asset in as the value its type states, `luau` runs a function of a Luau module on the CPU, `output` shows a value in the world and answers a Handle, `export` saves a value as an asset, and `engine` calls the engine bridge that `import`, `export` and the core reading nodes use. `output` holds `show.module` and `engine` holds `engine.module`, the functions they call.
# material (nodebox)
Materials as records: material.pbrMat is the standard metallic-roughness material made from its PBR maps (base colour, metallic-roughness, normal, occlusion, emissive) and their factors, and material.shader a material of any surface shader, holding every property and slot to what the shader declares. Each answers one element holding the record of the material kind (`records/material.procRecord`), which geometry.set_material, instances.set_material and the other inputs that take a material put on what draws; the output node shows it and the export node saves it as a `.material`.
# math
One node per operator and per function of Luau's `math` table that is a function of its numbers alone: add, sub, mul, div, mod, pow, neg, abs, min, max, floor, ceil, round, sqrt, sin, cos, tan, asin, acos, atan, atan2, sinh, cosh, tanh, exp, log, log10, fmod, ldexp, frexp, modf, noise, map, sign, deg, rad, clamp, lerp, smoothstep, isnan, isinf, isfinite, length, normalize, dot, cross, vec2, vec3, vec4, the comparisons (lt, le, gt, ge, eq, ne, and `compare` over them), `where`, the integer functions (to_int, to_float, bor, bxor, shl, floordiv), and `concat` for text. `math.random` and `math.randomseed` have no node: they draw from state outside the graph, a graph that called one would cook differently each time, and a call of either in a graph is refused, naming `field.rand`, `field.random` and `rand.unit`, which draw from a seed.
Operators and `math.*` over sockets resolve to these nodes, and `vector.create` over sockets resolves to `math.vec2`, `math.vec3` or `math.vec4` by its component count; with plain numbers only, `math.*` and `vector.create` are Luau's own, and `vector.create` holds three components. Each node but `compare` (built from the comparisons) and `concat` (text) calls an operation of `proc.fieldc.operators`: over plain numbers and vectors it answers the plain result, over a Column or a Field it answers a Field (a fieldc template), so a chain of math nodes is one expression that the node consuming it evaluates in a single dispatch (`fieldc.evaluate`).
`math.min` and `math.max` over sockets take any number of operands, folded two at a time; `math.log(x, base)` is the `log` node with its `base`. Operands of different widths widen to the widest: a number applies to every component, and a shorter vector reads 0 for the components it lacks. Division, `floordiv`, `mod` and `fmod` by 0 answer 0, `sqrt`, `log` and `log10` of a value at or below 0 answer 0, `asin` and `acos` of a value past -1 or 1 read the nearest of the two, and `map` over an input range of no width answers its output minimum, so a chain keeps producing numbers. The orderings (`lt`, `le`, `gt`, `ge`) compare first components; `eq` and `ne` compare every component.
# mesh
The primitive solids and sheets a graph starts from: `mesh.box`, `mesh.sphere`, `mesh.cylinder`, `mesh.cone`, `mesh.capsule`, `mesh.octahedron`, `mesh.torus`, `mesh.rounded_box`, `mesh.plane` and `mesh.grid`. Each is made on the device from lattices and fans of the `make` kernel, with positions, normals and texture coordinates written as formulas over the lattice, and answers triangles centred on the origin that fill the box its parameters name.
## Conventions
- Each node's folder name is its call name inside the box: `mesh.<folder>`.
- Parameters keep the names and defaults of the op each node replaces.
- `mesh.lattice` makes the lattices and fans the presets share, `mesh.spherical` the point of a sphere and `mesh.cube_faces` the faces of a cube; a preset writes its positions, normals and texture coordinates as formulas over the lattice.
# part (nodebox)
part.merge packs several values into one part, each piece keeping what it is; part.unpack gives one piece back by its name.
# prefab (nodebox)
Prefabs: the entity records a spawned hierarchy is made of, as record and list values. prefab.empty makes a root, prefab.entity, prefab.parent, prefab.add_component and prefab.link shape the hierarchy (each calls prefab.edit, which holds the record-list edits and the name lookup that prefab.find answers), and prefab.from_geometry, prefab.from_geometries, prefab.from_instances, prefab.from_population and prefab.from_asset make prefabs from what a graph built. The output node shows a prefab and the export node saves it as a bundle.
prefab.record and prefab.find are the parts the others are built from: one entity record, and a lookup of an entity by name.