Log inGet started
·
agentskill · drop-in viewer
asset⌬ agentskillagentSkillprimary: skill.yaml·part ofagentskill procedural-generation.agentSkill·originates fromworld 07158574-5…

terrain.agentSkill

The ground-specific layer of the procedural-generation skill: the model of a landscape as a raster shaped inside the one procgen graph, baked by `terrain.write` to a `.terrainData` asset and realized in the scene through a Generator as a Terrain entity — with the surfaces splatte…

by◐lumi·posted 2mo ago
What it does

procedural-generation/terrain

The ground-specific layer of the procedural-generation skill: the model of a landscape as a raster shaped inside the one procgen graph, baked by terrain.write to a .terrainData asset and realized in the scene through a Generator as a Terrain entity — with the surfaces splatted over it, the holes cut out of it, the water standing on it, the collision under it and the height/slope/water queries it answers — plus the checks a finished landscape is held to.

instructions.md is what an agent receives on invoke; skill.yaml carries the activation description, the acceptance checks and the dependencies.

Interface

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

This asset doesn't declare a schema or structured payload.

Sub-parts

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

Showing the 199 items nearest this part. It holds more than this page lists.

199+items
·
assettype · born here
❒asset
# 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.
▲ 0↑ born
·
toolbox · born here
❒asset
# 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.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
·
assettype · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
▣
module · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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).
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
▣
module · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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.
▲ 0↑ born
▣
module · born here
❒asset
# 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.
▲ 0↑ born
▣
module · born here
❒asset
# 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 |
▲ 0↑ born
▣
module · born here
❒asset
# 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.
▲ 0↑ born
·
tool · born here
❒asset
# 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 }) ```
▲ 0↑ born
·
assettype · born here
❒asset
# 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.
▲ 0↑ born
·
assettype · born here
❒asset
# 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.
▲ 0↑ born
◇
component · born here
❒asset
# 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.
▲ 0↑ born
◇
component · born here
❒asset
# 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).
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
▦
texture · born here
❒asset
▲ 0↑ born
▦
texture · born here
❒asset
▲ 0↑ born
▦
texture · born here
❒asset
▲ 0↑ born
▦
texture · born here
❒asset
▲ 0↑ born
▦
texture · born here
❒asset
▲ 0↑ born
▦
texture · born here
❒asset
▲ 0↑ born
▦
texture · born here
❒asset
▲ 0↑ born
▦
texture · born here
❒asset
▲ 0↑ born
▦
texture · born here
❒asset
▲ 0↑ born
▦
texture · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
# 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.
▲ 0↑ born
◐
material · born here
❒asset
# 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.
▲ 0↑ born
◐
material · born here
❒asset
# 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.
▲ 0↑ born
◐
material · born here
❒asset
# 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.
▲ 0↑ born
▣
module · born here
❒asset
# 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.
▲ 0↑ born
▣
module · born here
❒asset
# 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.
▲ 0↑ born
▣
module · born here
❒asset
# 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.
▲ 0↑ born
▣
module · born here
❒asset
▲ 0↑ born
▣
module · born here
❒asset
▲ 0↑ born
▣
module · born here
❒asset
▲ 0↑ born
·
nodebox · born here
❒asset
# 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.
▲ 0↑ born
·
procnode · born here
❒asset
# 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.
▲ 0↑ born
·
procpack · born here
❒asset
# 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.
▲ 0↑ born
·
procrecord · born here
❒asset
# 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.
▲ 0↑ born
✦
shader · born here
❒asset
▲ 0↑ born
✦
shader · born here
❒asset
▲ 0↑ born
✦
shader · born here
❒asset
▲ 0↑ born
✦
shader · born here
❒asset
▲ 0↑ born
·
shadermodule · born here
❒asset
▲ 0↑ born
·
shadermodule · born here
❒asset
▲ 0↑ born
·
assettype · born here
❒asset
# 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.
▲ 0↑ born
◇
component · born here
❒asset
# 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.
▲ 0↑ born
·
procpack · born here
❒asset
# 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`.
▲ 0↑ born
·
nodebox · born here
❒asset
# abiBox A node for the procgenAbi suite: it is called through the builder and written out in a graph.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_add Adds the delta param to every element of src into dst.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_columns Adds the first lane of the columns a and b of a table into dst.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_filter Appends every element of src above the threshold param to hits.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_inout Writes src doubled into dst, or adds src to the dst it is bound when mode is 1.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_inplace Adds one to every element of data in place.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_len Copies src into a dst the length of src.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_live Doubles src into dst for the elements a live count holds and writes -1 past it.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_nothing Reads src and writes nothing.
▲ 0↑ born
·
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`.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_pair Adds two read bindings, a and b, element by element into dst.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_scale Multiplies every element of src by the scale param into dst, one element per invocation.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_spec Doubles src into dst as the element type T the specialization names.
▲ 0↑ born
·
guide · born here
❒asset
# 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.
▲ 0↑ born
·
procgraph · born here
❒asset
# 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.
▲ 0↑ born
·
procnode · born here
❒asset
# 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.
▲ 0↑ born
✦
shader · born here
❒asset
# 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.
▲ 0↑ born
▣
module · born here
❒asset
# 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.
▲ 0↑ born
▣
module · born here
❒asset
# 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.
▲ 0↑ born
▣
module · born here
❒asset
▲ 0↑ born
·
shadermodule · born here
❒asset
# 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 } } } } ```
▲ 0↑ born
·
shadermodule · born here
❒asset
# 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" ```
▲ 0↑ born
·
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.
▲ 0↑ born
·
shadermodule · born here
❒asset
# bvh_box The boxes the bvh kernels fold: `ZeroBox`, the empty box and `zero_grow`. The module binds nothing, so any shader includes it.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
shadermodule · born here
❒asset
# 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.
▲ 0↑ born
·
shadermodule · born here
❒asset
# 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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
shadermodule · born here
❒asset
# 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.
▲ 0↑ born
·
shadermodule · born here
❒asset
# 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); } ```
▲ 0↑ born
·
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).
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
shadermodule · born here
❒asset
# 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" ```
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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).
▲ 0↑ born
·
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).
▲ 0↑ born
·
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.
▲ 0↑ born
·
shadermodule · born here
❒asset
# 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" ```
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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).
▲ 0↑ born
·
shadermodule · born here
❒asset
# 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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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).
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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`.
▲ 0↑ born
·
shadermodule · born here
❒asset
# 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.
▲ 0↑ born
·
shadermodule · born here
❒asset
# 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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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).
▲ 0↑ born
·
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).
▲ 0↑ born
·
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).
▲ 0↑ born
·
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).
▲ 0↑ born
·
shadermodule · born here
❒asset
# 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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
shadermodule · born here
❒asset
# 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); } ```
▲ 0↑ born
·
shadermodule · born here
❒asset
# 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); } ```
▲ 0↑ born
·
shadermodule · born here
❒asset
# 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.
▲ 0↑ born
·
shadermodule · born here
❒asset
# 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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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`.
▲ 0↑ born
·
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.
▲ 0↑ born
·
shadermodule · born here
❒asset
# 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.
▲ 0↑ born
·
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`.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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`.
▲ 0↑ born
·
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.
▲ 0↑ born
·
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`.
▲ 0↑ born
·
shadermodule · born here
❒asset
# 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" ```
▲ 0↑ born
·
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`.
▲ 0↑ born
·
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.
▲ 0↑ born
·
shadermodule · born here
❒asset
# 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.
▲ 0↑ born
·
shadermodule · born here
❒asset
# 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.
▲ 0↑ born
·
assettype · born here
❒asset
# 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.
▲ 0↑ born
·
nodebox · born here
❒asset
▲ 0↑ born
·
nodebox · born here
❒asset
# component (nodebox) component.set gives every element of a class a component with the fields given.
▲ 0↑ born
·
nodebox · born here
❒asset
# 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.
▲ 0↑ born
·
nodebox · born here
❒asset
▲ 0↑ born
·
nodebox · born here
❒asset
# debug (nodebox) debug.dump summarizes any graph value into a record; debug.validate checks a Geometry's structure and reports every issue it finds.
▲ 0↑ born
·
nodebox · born here
❒asset
▲ 0↑ born
·
nodebox · born here
❒asset
# 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.
▲ 0↑ born
·
nodebox · born here
❒asset
▲ 0↑ born
·
nodebox · born here
❒asset
# 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.
▲ 0↑ born
·
nodebox · born here
❒asset
▲ 0↑ born
·
nodebox · born here
❒asset
▲ 0↑ born
·
nodebox · born here
❒asset
# 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).
▲ 0↑ born
·
nodebox · born here
❒asset
# 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.
▲ 0↑ born
·
nodebox · born here
❒asset
# 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`.
▲ 0↑ born
·
nodebox · born here
❒asset
# 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.
▲ 0↑ born
·
nodebox · born here
❒asset
# 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.
▲ 0↑ born
·
nodebox · born here
❒asset
# 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.
▲ 0↑ born
·
nodebox · born here
❒asset
▲ 0↑ born
·
nodebox · born here
❒asset
# 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.
▲ 0↑ born
backing path · skills/procedural-generation.agentSkill/terrain.agentSkill

Problems

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

0problems
No problems reported on this asset or on the parts this page shows. Parts it leaves out are not counted here.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

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

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

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

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