Log inGet started
·
tool · drop-in viewer
asset⌬ tooltoolprimary: init.luau·part oftoolbox tests.toolbox·originates fromworld 07158574-5…

list

List every registered test suite. Discovery reads the asset registry, so the result is exactly the set the runner and CI execute. Pass a suite name to narrow to one, or ask for details to load each matching suite and include its test count.

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

tests.list

List every registered test suite. Discovery reads the asset registry, so the result is exactly the set the runner and CI execute. Pass a suite name to narrow to one, or ask for details to load each matching suite and include its test count.

Interface

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

conforms to

zero/source-extract/v2

tests.toolbox/list.tool/init.luau `tests.list(opts?)` — enumerate every registered `.testSuite` asset. Discovery is driven by the asset registry (`asset.list("testSuite")` + `asset.resolve`), so it reflects exactly what `tests.run` will execute. Test counts come from each asset's own `AssetRef:tests()` (which loads THAT suite in isolation) — no global Test registry juggling.

suiteStem(ref: any) → string

Stem of the `.testSuite` folder a ref points at (e.g. "vfs").

argtypedescription
refany

initPathOf(p: string) → string

Normalize a resolved `.testSuite` path to its runnable entrypoint.

argtypedescription
pstring

list(opts: (ListOpts | string) →

List discovered test suites (registrable `.testSuite` assets). Scoped to `scope = "user"` (world-authored) by default — the same default as `tests.run`; pass `scope = "all"` to include the baked `@builtin` library. Optionally load each suite to also report its test count. a `ListOpts` table `{ filter, scope, details, quiet }` gives full control. `scope` is "user" (default) | "all" | "builtin". `details = true` loads each matching suite (in isolation) to include its `testCount`; `quiet = true` suppresses the printed summary.

argtypedescription
opts(ListOpts | stringOptional. A suite-name string filters to one suite (origin-agnostic);

examples

 
{ filter = "vfs", details = true }
⌬ Types
ListOpts = {SuiteEntry = {ListResult = {

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
❒
package · born here
❒asset
▲ 1↑ born
·
assettype · born here
❒asset
# Tool (asset type) A tool is a single agent-callable function whose schema is its `typed function` signature. Tools are the workflow surface — composed Luau operations that bundle multiple engine APIs into one named call, suitable for agents (and for use from `execute`). Each tool has a declared input shape, return shape, and a strict result envelope (`ZmToolResult`). **Every exported tool function is declared `typed function`.** The `typed` keyword is what makes the signature real: it compiles to a runtime dispatch wrapper that checks each argument against its declared type and raises on a mismatch. A plain `function` — even one carrying Luau type annotations — is checked by nothing at call time, so the tool accepts any input and reports success on garbage. See "The typed signature is the schema" below. ## When to use one - You're composing several engine APIs into a workflow that's worth naming and discovering (`tools.list`, `search_tools`). - You want an agent to be able to call this function by name without reading source code — the typed signature is the API contract. - The operation has clear success / failure modes that fit `{ exitCode, data?, stdout?, message? }`. If the function is just a Luau utility called from other code, use a `.module`. If it's stateful background work, use a `.service`. If it's pure GPU compute, use a compute `.shader`. ## Where it lives Tools nest under a `.toolbox/` parent — toolboxes are the namespace. - Source: `/zero/source/.../<Toolbox>.toolbox/<name>.tool/` - Identity: `<Toolbox>.<name>` (dotted form — the toolbox name is part of the tool identity). - Folder shape: - `init.luau` (or `init.lua`) — exports the function, and carries its schema in `--!desc` / `--!arg` / `--!return` / `--!example` annotations above it. The typed signature IS the tool's signature. **Required.** - `README.md` — instance documentation. **Required.** - `.metadata` — tool-level tier and tags. **Required.** ## How to create one Use the workflow tool that scaffolds the right `.tool/` shape under an existing toolbox: ```luau -- The `tools` global authors a tool inside an existing toolbox: tools.create({ name = "<name>", toolbox = "<Toolbox>", description = "What this tool does, agent-facing.", code = "return shared.ok(true)", -- the function body }) -- Or scaffold the folder manually: local box = asset.resolve("<Toolbox>.toolbox") asset.create("tool", "<name>", { into = box }) ``` Always nest under an existing toolbox — if you don't have one yet, call `tools.createToolbox({ name = "<Name>", description = "..." })` first. ## How it operates 1. **Registration.** Writing `init.luau` into a `.tool/` folder registers the tool with the tools registry. It becomes callable as `tools.use("<toolbox>", "<name>", ...)` from Luau, `use_tool { toolbox, tool, args }` over MCP, and `zero <toolbox> <name> [args]` from the engine shell. 2. **Schema validation.** The `typed function` signature declares the parameter shape. Its runtime dispatch wrapper checks each argument before the body runs and raises a message naming the argument, the expected type, and what arrived. 3. **Return envelope.** Tools MUST return `ZmToolResult`: ``` { exitCode = 0|n, data? = any, stdout? = string, message? = string } ``` `exitCode = 0` means success; non-zero means failure, with `message` carrying the human-readable reason. 4. **Discovery.** The tool surfaces in `tools.list()`, `search_tools { query: "..." }`, and `asset.inspect`'s `exposes:` section (parsed from the typed signature and the `--!` annotations). 5. **Hot reload.** Editing `init.luau` re-registers the tool. Subsequent calls see the new behaviour / schema. ## Discovery - `tools.list()` — every registered tool (any toolbox). - `search_tools { query: "..." }` — MCP-side tool search. - `asset.list("tool")` — same set, asset-style listing. - `asset.inspect("<toolbox>.<name>")` — signature, parameters, return shape, this type README. - `ls /zero/source/<Toolbox>.toolbox/<name>.tool` lists the files the folder holds. ## Authoring conventions - **Implementation lives in `init.luau`.** The tool's full function body — argument validation, the call into engine APIs, the return value — goes in `init.luau`. The toolbox's `shared.module/` is for genuinely cross-tool helpers (`ok` / `fail` constructors, shared parsers / validators), not per-tool logic. Do NOT make `init.luau` a one-line forwarder onto a function defined in `shared.module/`. - Use `shared.module/` in the parent toolbox for `ok` / `fail` result constructors. Don't hand-roll the envelope per tool. Pull it in with `local shared = require(".shared")` from any tool that needs the helpers. - Annotate every public function with `--!desc` / `--!arg` / `--!return` / `--!example` directly above the function — the LSP and toolbox surfaces extract these. - **Declare every exported function `typed function M.name(arg: T, opt: U?): R`.** This is not conditional on the signature being "clear" — a loose argument is the case that needs the checked contract most. Give the argument the type it actually accepts (a union, a named table type, a literal enum) instead of dropping to an unchecked `function`. - Keep tools focused. One tool, one named workflow. Split rather than grow. - Tag tools in the sibling `.metadata` (`{ "tags": [...], "tier": "core|content|specialized" }`) so `search_tools` can find them by domain (e.g. `entity`, `scene`, `assets`, `physics`). ## The typed signature is the schema The `typed` keyword compiles the function into a dispatch wrapper that validates arguments at call time. Without it there is no check at all, so the tool runs its body on whatever arrived and returns a success envelope built from garbage. Type each argument as narrowly as the operation actually accepts: ```luau export type Mode = "edit" | "play" export type SpawnOpts = { position: { number }?, parent: EntityRef?, count: number?, } --!desc Spawn a primitive and return the entities it created. --!arg kind Primitive shape to spawn. --!arg opts Placement and repetition options. --!return The spawned entity refs. --!example "cube", { count = 4 } typed function M.spawn(kind: ("cube" | "sphere" | "plane"), opts: SpawnOpts?): { EntityRef } ``` - **A fixed set of accepted words is a literal union**, never `string`. `("cube" | "sphere")` rejects `"Cube"` at the call and lists the accepted values in the error; `kind: string` accepts `"banana"` and fails somewhere deeper, or not at all. - **A structured argument is a named `export type`**, never `{ [string]: any }`. Naming it puts every field and its type on the tool's page and checks them on entry. - **A named type is only as strong as its fields.** `opts: DrawOpts` where `DrawOpts = { value: any }` passes for a table and checks nothing inside it — the same hole, one level down. The argument checker walks nested fields, so type each one; a type name is not a place to put the work off. - **A dispatch table keyed by an `op` string is several tools.** `{ op: string, [string]: any }` is one unchecked entry point wearing N signatures — split it so each operation carries its own checked arguments. - **An entity or asset argument is `EntityRef` / `AssetRef<kind>`**, never `any` and never a bare name string. - `any` is correct only for a value the tool genuinely does not constrain — a passthrough payload, arbitrary user data. It is not a placeholder for a type not yet written down. ## Common pitfalls - **A plain `function` export.** The tool registers and dispatches, so nothing looks wrong — it just never rejects bad input, and every call reports success. Declare it `typed function`. - **Annotations without `typed`.** `function M.f(x: string)` reads as typed and checks nothing at runtime; the annotation only informs the LSP. The keyword is what enforces it. - **Wrong return envelope.** Returning a raw value instead of `{ exitCode = 0, ... }` will be flagged by callers expecting `ZmToolResult`. - **Tool identity.** The fully-qualified identity is `<toolbox>.<name>`, NOT just `<name>`. Toolbox-less tools don't exist by design. - **Missing dependency.** Tools requiring another package / service must declare it; otherwise the call fails at runtime with a missing-module error. ## Related types - `.toolbox` — the required parent namespace for tools. - `.module` — for Luau libraries with no agent-facing schema. - `.service` — for stateful background work, not callable request/response operations. ## In the Inspector Tool: how the tool is called (its signature), what it does, each argument, what it returns, and its examples, with **Open** for its source.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
▣
module · born here
❒asset
# tests.shared (module) Internal helpers shared by the `tests` toolbox tools. Not bound to a global — required by sibling tools via `require(".shared")`. ## Surface - `M.DEFAULT_JSON` / `M.DEFAULT_MD` — canonical report paths the engine test runner writes (`/source/tmp/test_results.json`, `/source/tmp/test_results.md`). - `M.readReport(src)` — read a JSON report from a VFS path, or pass a decoded table through unchanged. - `M.diff(before, after)` — classify every test in `after` relative to the `before` baseline: `{ newFailures, fixed, stillFailing, newTests, removedTests }`. ## Example ```lua local shared = require(".shared") local before = shared.readReport("/source/before.json") local after = shared.readReport(shared.DEFAULT_JSON) local d = shared.diff(before, after) print(#d.newFailures .. " regressions") ```
▲ 0↑ born
❒
package · born here
❒asset
# oceans A sea and the ground under it, as one package: the wave simulation, the surface that renders it, the seabed it is seen through, and the answer to "how high is the water here" that gameplay needs to float things on it. ```lua local Ocean = require("@builtin::systems.oceans.modules.ocean") Ocean.create({ preset = "gale" }) ``` That is a whole ocean — waves out to the horizon, foam on the breaking crests, a seabed below, and a surface that follows whoever is looking at it. To float something on it: ```lua local ocean = Ocean.forEntity(oceanEntityId) boat.position = { x, ocean:sampleHeight(x, z), z } ``` It is a building block. The sun the water glitters under, the ambient its foam sits in and the sky it reflects all come from the scene, through the same directional light and reflection probes every other material in the engine reads — so an ocean drops into a world that already has lighting instead of bringing a second set, and changing the scene's light changes the water with it. ## What it does The simulation is **spectral**: instead of animating individual waves, it builds the sea's energy across every wavelength and direction from an oceanographic spectrum, then inverse-transforms that into a height field. A real sea is what comes out — wind speed alone decides wave height, and it rises with wind the way a fully developed sea does, following the Pierson–Moskowitz law. It runs in **cascades** — independent wave fields over different patch sizes, each band-limited to its own range of wavelengths. A tile of roughly two kilometres carries the long waves and one of a few hundred metres the short ones; they sum into a surface with detail at every scale, and because the bands are disjoint no wavelength is synthesised twice. Above them a **Gerstner bank** adds the swell analytically, which no cascade can hold: a wave longer than its own patch has nowhere to live, and swell arrives from a distant storm on its own heading anyway. **Foam** is not painted on. It comes from the Jacobian of the wave displacement: where the surface folds over itself, the wave is breaking. Whitecaps appear where and when the geometry actually breaks, accumulate into a persistent world-space field, and streak behind the crest as they decay. **Colour and transparency are the same mechanism.** Per-channel Beer–Lambert extinction over the distance light travelled through water gives both — red dies within a metre, blue survives tens. That is why shallow water over sand reads turquoise and the open sea reads near-black blue without switching materials. **The seabed** is one noise field read twice: scaled it is the terrain's relief, unscaled it picks between sand and rock, so outcrops stand on the high ground and sand fills the hollows. The sun's caustic web moves over both, bent by the horizontal displacement of the water directly above it. Bodies land on it: the pass that raises the ground writes the same field out as a height patch that travels with the viewer, so what a body stands on is what the frame drew. Nothing here ships an image. Every map the surface and the seabed sample — the foam patterns, the sand, the rock, the caustics — is grown on the GPU at startup. ## The pieces | Asset | What it is | |---|---| | `components/Ocean` | Puts a sea on an entity. Its fields are the sea state; the simulation is rebuilt from them, so an ocean survives a scene reload. | | `modules/ocean` | The substrate: `attach`, `create`, `forEntity`, `sampleHeight`, `sampleDisplacement`. | | `components/Buoyancy` | Floats a rigid body on the sea. Its fields are the hull: how dense it is, how steady, and what box displaces water. | | `modules/buoyancy` | The forces: `attach`, `forEntity`, `columns`, `submergedFraction`, `restingY`. | | `modules/oceanPresets` | Nine named sea states, from `calmClear` through `gale`. | | `modules/oceanQuality` | The four tiers — how finely a sea is resolved, as distinct from what sea it is. | | `modules/oceanClipmap` | The camera-following ring geometry both the surface and the seabed are built on. | | `modules/oceanFloor` | The seabed: terrain, ground materials, caustics and the ground physics collides with. | | `modules/oceanScene` | The seam between the scene capture and the materials that sample it. | | `shaders/oceanSurface` | The water: cascade normals, absorption, exact dielectric Fresnel, subsurface scattering, sun glitter, foam, and the view from below. | | `shaders/oceanFloor` | The seabed: sand and rock blended by terrain noise, under a caustic web. | | `materials/oceanWater`, `materials/oceanSeabed` | The default looks. | | `renderFeatures/oceanScene` | Copies the frame's colour and depth so the surface can refract what is behind it and measure its own water column. | | `computeShaders/ocean_*` | The ten-stage GPU pipeline. | ## Wind is the control Significant wave height goes as the **square** of wind speed, so that one field does most of the work: | Wind | Sea | Wave height | |---|---|---| | 3 m/s | glassy, a mirror | ~0.2 m | | 11 m/s | working sea, occasional whitecap | ~2.5 m | | 24 m/s | gale, breaking and streaked | ~12 m | Everything else shapes character rather than size: `choppiness` pinches crests into peaks, `standingWaveRatio` runs waves both ways instead of only downwind, `jonswapGamma` sharpens a fetch-limited sea, and the foam layers decide how readily a breaking crest whitens and how long it streaks. Read `documentation/oceans.guide` for the full walkthrough, including the seabed, buoyancy, reflections and what each tier costs.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
·
assettype · born here
❒asset
# assetType (asset type) The `assetType` is the **type of types** — the meta-type that every `<typename>.assetType/` folder is an instance of, including itself. A `<typename>.assetType/` folder declares a new asset type called `<typename>`: its on-disk shape, its identity rule, and the structural contract instances of the type must satisfy. This type is self-hosting. `assetType.assetType/` conforms to its own schema — a `type.yaml` plus a `README.md` — so the type system is defined in terms of itself rather than a hidden engine special-case (the same way a C compiler is written in C). Every asset in the engine, including each `.assetType` definition, now resolves to a registered assetType: a `.material` resolves to `material.assetType`, and `material.assetType` resolves to `assetType.assetType`, which resolves to itself. ## Why it exists Assets used to be mapped to their type *implicitly*, by matching the folder suffix against a name-keyed registry. That match carried no stored link: nothing on a `Foo.material` recorded *which* `material.assetType` it was written against, so two worlds shipping a same-named type could silently disagree. Making `assetType` a real, resolvable asset closes that gap — the link from an asset to its type is now an explicit reference pinned in the asset's `.refs` sidecar (`via: "asset_type"`), exactly like every other dependency. ## Where it lives - Source: `/zero/source/.../<typename>.assetType/` - Identity: `<typename>` (the `.assetType` suffix strips from the identity; the folder retains the suffix on disk). - Folder shape: - `type.yaml` — the structural spec the validator reads. **Required.** - `README.md` — type-level documentation. **Required.** - `about.yaml` — three short plain-language texts saying what the type is for (see "Say what your type is for" below). Every built-in type ships one. - `behavior.luau` — optional behavior/scaffolding module. - `template/` — optional canonical placeholder body the templater copies into a new asset of the type. - `<name>.module/` — optional shared code the type ships to **every instance** of it, conventionally `shared.module/` (see below). ## Shared code its instances reach (`asset.containing` + `.modules`) A type ships code every instance of it uses by declaring the modules in its own `behavior.luau`, as a map of tracked `require`s: ```luau -- inside <typename>.assetType/behavior.luau M.modules = { shared = require(".shared") } -- the sibling shared.module/ ``` An instance reaches it with one location-independent line: ```luau -- inside any foo.<typename>/init.luau local api = asset.containing(__FILE__).modules.shared ``` `__FILE__` is the chunk's own VFS path; `asset.containing` resolves the calling asset, and `.modules.<name>` follows the instance's pinned `typeRef` (the type's **guid**, recorded in the instance's `.refs` as `via = "asset_type"`) to the module its type declares. Because resolution follows that link rather than a category name, the same line in an instance of a different type resolves to that type's module, and identically-named modules in two types never collide. `asset.typeRef(target)` reads that guid for any target; `asset.resolve(asset.typeRef(target))` gives the full `AssetRef<assetType>`. See `assetTypes/README.md` § "Shared code: type-owned modules" for the full mechanism. ## How to create one Authoring a new asset type is just creating a `<typename>.assetType/` folder under `/zero/source/`. The engine registers `<typename>` as a category **the moment the folder is written** — no engine restart, no Rust change, and it works for built-in (`@builtin/assetTypes/`) and user-defined types identically. Once `<typename>` is registered, `<name>.<typename>/` folders are recognised as assets of that type and resolve their `asset_type` link back to this definition. Creating a `<name>.<typename>/` folder *before* its `<typename>.assetType/` exists fails fast: the suffix has no registered type, so the folder can't be a valid asset. Author the type first. ## Say what your type is for (`about.yaml`) Every type carries a small `about.yaml` beside its `type.yaml`, and the README you write for the type documents how to author an asset of it. Both are required, and both are meant to be written as you make the type, while you know what it is for. `about.yaml` answers three questions in plain words, one key each: ```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. Name the whole range of things it covers, in the words a game maker would use for them. - **`function`** is how it works today: what an asset of the type is made of and what the engine does with it. - **`usage`** is what a person or an agent does with one: how they pick it, place it, call it, or hand it to something else. A few habits keep it useful. Write about the type as a whole, so the text holds for every asset of it, and leave out any one asset's name. Say what the type does right now. Stay with words a game maker would say, and mention a file name or a call only where a game maker would know it. The limits are in bytes: `intent` 800, `function` 1500, `usage` 1200. Each is generous, and a few sentences usually say it. Why bother: agents working in a world read these three texts to learn what your type is for, so they reach for it when it fits and use it the way you meant. People searching find the type by what they want to make, and the three texts let them tell at a glance whether it is the one they need. `asset.create("assetType", name)` scaffolds the file with all three keys, and a README whose first paragraph is a `[[describe]]` note. Each value starts with `[[describe]]` followed by a note on what to write; replace the whole value with your own words. A value or a README line that still starts with `[[describe]]` counts as not written yet, which the push's check of each asset it carries against its type reports, naming the file. A type you leave as it was is not asked, so older types stand. Many types at once, or one you would rather not write by hand: `services describe` lists the types that lack these texts and what filling them costs, and `{ mode = "all" }` writes them into the same files, where you edit them like anything else you wrote. Where the texts show up: `asset.inspect("<typename>", "assetType")` returns them as `detail.about`, `assetTypeRef:getAbout()` returns them as a table with the keys `intent`, `function` and `usage`, the type's `types/<typename>` guide page ends with an "About this type" section holding them, and the Inspector's Type section lists them at the top. The guide page is this README plus that section, so a README that explains the type well and an `about.yaml` that says what it is for read well together. ## Ask your assets to say what they are A type can ask each of its assets to carry a description: a file, written by the author or generated, that says what that asset is and what it is for. The people and agents who find an asset read it to decide whether it fits, and the author knows what it is for at the moment of making it. **1. Declare the file with a `described` check.** This is the whole requirement: ```yaml files: required: - path: "README.md" # any file name works kind: "file" content: - { check: "described", message: "Say what this widget is for and where it goes." } ``` With that in place: - An asset whose file is missing, empty, or still has a line starting with `[[describe]]` does not meet the type's requirements. `asset.validate` names it, `zm status` and `world.publishBlockers()` list it, and a push that carries it is refused with `message`. - `services describe` finds it (`mode = "find"` lists it and the price) and can write the description for it (`mode = "all"`). It writes only this file. For a YAML file that holds several texts, name the keys and their limits in bytes, the way this type does for `about.yaml`: ```yaml content: - check: "described" keys: - { key: "intent", max_bytes: 800 } - { key: "usage", max_bytes: 1200 } message: "Write intent and usage." ``` `headings` names sections a Markdown file must have. `assetTypes/README.md` has the full check. **2. Optionally, steer what gets generated with a `describe:` block:** ```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>" ``` `guidance` says what a description of your type covers, and `examples` names assets whose descriptions show the shape you want. Without the block, generation still works from the general describing guide and the check's `message`. An example is an asset of your type in the library whose description is a true, written one, never a scaffold's placeholder, and never the asset being described. When the library has none, leave `examples` empty rather than name one that is not. A type whose assets the model cannot perceive (a sound it cannot hear, a motion it cannot see) says so in `guidance`: the description comes only from what the asset's own files state, such as its name and the `.metadata` where a generation service records the prompt that made it, and when they state nothing the model answers `cannot` with that reason. `services describe` then fails that one asset with the reason and leaves its file as it was. **3. Start the file unwritten in your template.** Put a heading and one line that starts with `[[describe]]` and a short note on what to write, in `template/<file>`, so a new asset starts unwritten rather than with text that only looks written. Do not put example content in it: every new asset carries the template verbatim, and the same text on many assets makes them impossible to tell apart in search. Types whose assets say what they are through their own content (a mesh, a texture, a sound) leave this out: declare nothing and nothing is asked. ## The `behavior.luau` contract `behavior.luau` is the type's code. It returns ONE table, and the keys below are the ones the framework reads off it for EVERY type — the surface you get by declaring them. Anything else on the table is your own: reachable by name through `require("@builtin::assetTypes.assetType.shared.ref").loadTypeModule("<typename>")` for a system that knows your type (`computeShader` publishes `dispatchKey` that way), and never called by the framework. ```luau local M = {} M.ref = { ... } -- methods on every AssetRef of this type M.modules = { ... } -- shared code the instances reach M.global = {} -- reserved key; ship the empty table function M.onCreate(name, opts) ... end return M ``` ### The tables | Key | Shape | What the engine does with it | |-----|-------|------------------------------| | `M.ref` | `{ [name] = function(self, ...) }` | Every function becomes a method on every `AssetRef` of this type: `ref:name(...)`, with `self` the ref. This is how a type gives its instances an API. Two names in here are contracted — see below. | | `M.modules` | `{ [name] = require(".sibling") }` | Shared code the type's instances reach through `asset.containing(__FILE__).modules.<name>`, resolved by the instance's pinned `typeRef` guid. See the section above. | | `M.refShapes` | `{ [method] = "TypeName" }` | Declares the result type of a `M.ref` method so the LSP can check what a call site does with it. See the `M.refShapes` section below. | | `M.events` | an events schema | The events an ASSET of this type fires, declared the way a component declares its own. Readers subscribe through `ref.events.<name>:connect(...)`; firing authority stays with the type. Keyed by the asset's guid, so every resolver of the same asset shares the signals and a re-resolved ref re-attaches to subscriptions already there. | | `M.namePattern` | a Lua pattern string | The name shape `asset.create` enforces for instances of this type, replacing the default `^[A-Za-z][A-Za-z0-9_]*$`. | | `M.global` | `{}` | Reserved. Ship the empty table. | ### The lifecycle hooks Each is optional; a type that omits one costs nothing. The engine calls them: | Hook | Signature | When it fires | |------|-----------|---------------| | `M.onCreate` | `(name, opts) -> { [filename] = contents }` | On `asset.create("<type>", name, opts)`. Returns the files that scaffold the new instance, as a map of relative path to contents; the framework writes them. The `opts` type annotation is the schema `asset.create` validates the caller's arguments against, so annotate it. | | `M.onReplace` | `(dest, files) -> { [filename] = contents }` | On an `asset.create` that writes over an asset already at `dest`, after `onCreate`. Receives the files `onCreate` produced and returns the files to write, so a type that re-derives its content (a rig re-imported from its source file) can carry what an author added to the asset it replaces. Optional. | | `M.onRegister` | `(self)` | Once per instance, the first time it registers: on `engine.onWorldLoaded` for instances already in the world, immediately on a live `asset.create`, and when an instance arrives after that sweep (installed, pulled, or synced from a peer). Guarded by the instance's path, so a double trigger never double-registers. An instance that leaves the runtime releases the guard, so one arriving at the same path again registers. This is what makes "write an asset into the world and it takes effect live" work for content that registers into a runtime registry. The `@builtin` library is excluded from the sweep. | | `M.onChange` | `(ref, change)` | On every VFS write to a file INSIDE one of this type's instances. `change` is `{ path, asset, type, kind, origin }`: `kind` is `"edited"` or `"seeded"`, `origin` is `"local"` or `"remote"` (an importer runs on the originator only). **Filter on `change.path`**: the hook fires for any file under the folder, so act on the one you care about. It runs as its own task and may wait on what it reads: a `vfs.read` of bytes only the blob store holds (a binary file on the web build) fetches them. The writes to one asset reach the hook in the order they were made, each once the hook before it has finished. A re-entrancy guard suppresses writes back into the same asset routed inline from the hook, and a write the hook makes is routed on a later frame like any other, so the hook must be convergent on its own: diff the meaningful state and short-circuit while your own regeneration is in flight. | | `M.onDelete` | `(ref)` | When an instance of this type leaves the runtime: it is deleted, or the engine swaps away from the world binding that held it. Its files are gone from `/zero/source` by then. A removal routed while the instance's `onChange` is waiting reaches `onDelete` once that hook, and every write queued behind it, has finished. On a swap the instance still exists in the world it belongs to, and a VFS write the hook makes reaches the world now bound. | | `M.validate` | `(assetRef) -> { { code, message, severity? } }` | On `asset.validate`, after the structural `type.yaml` check, for the type's own SEMANTIC validation. `severity` defaults to `"error"`; error-severity problems flip `ok` to false, warnings do not. A hook that raises or returns a non-table is itself reported as a `validate.hook_failed` error. `world.push` runs this per user asset, so declaring it enforces your type's rules at publish time with no further wiring. | ### The two contracted `M.ref` names Most `M.ref` methods are your type's own surface: name them what you like, return what you like. Two are read by the engine and mean the same thing for every type, so their shape is fixed: - **`instantiate(self, target?, opts?) -> (root, idMap)`** — makes an instance part of the scene. Defining it is the whole opt-in: `ref:canInstantiate()` is true exactly when it exists. Its **return is a contract** — see the next section. - **`inspect(self) -> detail`** — the type-specific half of `asset.inspect`. See its section below. ### Names a type cannot shadow Some keys resolve on every `AssetRef` before per-type dispatch, so an `M.ref` entry of the same name is never reached: `canInstantiate`, `getSource` / `getBytes` / `getText`, `exists`, `deps`, `meta`, `runtime`, `events`, `modules`, `typeRef`, and the residency flags `has_backing_asset`, `has_runtime_changes`, `cpu_resident`, `gpu_resident`. These are the behaviours every asset must expose identically, which is why they win. ### Reloading A `behavior.luau` edit ripples to existing refs with no restart: dispatch reads `mod.ref` fresh each time and `require`'s cache is re-run in place. A type whose folder appears at runtime is picked up on the next dispatch — negative results are not cached either. ## The names your type's assets hold (`references:`) An asset that names another asset by a string depends on it: the named asset travels with it into a pack or a pull, and a name nothing answers to is an unresolved dependency `worldValidation` reports and the push gate refuses. The engine reads `require(...)` and the asset lookups on its own; a name your type's format or API carries anywhere else is one you declare, in `type.yaml`: ```yaml references: # A top-level key of this type's own files that holds a name (or a table of them). keys: - { key: "shader", type: "shader" } # A call's argument, counting from 1, wherever the call is written. args: - { call: "g:node", position: 2, type: "procNode" } # A field of the options table a call takes. fields: - { call: "fx.beam", field: "material", type: "material" } ``` `type` is the category the name resolves in, one of `asset.categories()`. An argument declared once per category, at the same `call` and `position`, takes the name from whichever category answers. A key or a field holds one category: declared again with another `type`, it takes the later one. The block is declared as the type registers, when the library or the world holding it is laid into the VFS, so every reference table reads it whatever script has or has not run. `asset.declareReferenceKey`, `asset.declareReferenceArg` and `asset.declareReferenceField` declare the same things from a script, for a surface that is no type's. ## What search does with your type (`indexing:`) A type declares what search embeds for its instances. **This is not optional and there is no useful default**: ZeroMind runs your declaration and adds nothing of its own, so anything you do not point at is absent from the index — silently, and for every instance of the type forever. Twenty types once shipped with no `indexing:` block and every instance of them was unfindable. A type whose instances are never searchable (a run record, a log) says so with `indexing: none`; every other type declares a recipe. A type without an `about.yaml` beside its `type.yaml` refuses every push that carries it, whether or not the push edited the type. Texts in it that are not written yet follow the rule every `described` file follows: they refuse the push that edits, stages or commits the type. Two words carry the whole thing, and they are the same two the search tool exposes: - **`identity`** — what an instance IS. For anything you can look at, that is the picture (`content:` + `modality: image`, matched directly by a text query). For everything else it is the authored text that says what it is. - **`capability`** — what it DOES and how it is made. Code, settings, the model-written summary of them. A type with no behaviour has no `capability` entry. A type with nothing to look at has no image. Leaving a slot empty is a statement; filling it with whatever happens to be lying around is not. The one you have to decide when authoring a type is: *for an instance of this, what is the thing a person would recognise it by, and what is the thing it does?* A texture answers "the image" and "its compression settings". A module answers "its README" and "its code". Write those two answers into `indexing:` and the rest follows. Three specifics worth knowing before you write one: - **`derive` is instructed by this type's own `README.md`.** A model reads an instance's source and writes a description; what it embeds is the description, not the source. It follows your README to know what it is looking at, so a type whose README says what its instances are gets good derivations for free. There is no prompt to name. - **`derive` loses detail.** It keeps the main technique and drops secondary ones. If the source is code you want searchable by its own vocabulary, declare the same files a second time as `capability` with `extractor: verbatim`. Two entries, same role, different failure modes. - **Never `source: { field: name }`.** A filename is one or two words with no usable embedding, and a corpus indexed that way answers "language runtime" with `say_runtime_2.soundClip`. `facets:` is the second stage: a `file:` embedded whole (`.metadata`, so keys nobody declared still get indexed) plus computed keys that state a value for every instance including `false` — the raw file can imply "not rigged" and can never say it. The full schema, every extractor and the facet sources are in [`assetTypes/README.md`](../README.md); the scaffolded block with inline guidance is in [`template/type.yaml`](template/type.yaml). ## A container whose contents work together (`system: true`) A type that is a container can say its contents are one working whole by declaring `system: true` at the top level of its `type.yaml`. `package` declares it: the components, modules, scenes and tools a package ships are parts of one system. The declaration is plain data, read from the decoded `type.yaml` (`require("@builtin::assetTypes.assetType.shared.declaration").of("package").system`), and a type that leaves it out is an ordinary type. ## What your type looks like (`preview:`) A type declares how its instances are pictured, and the engine makes the picture when something needs it: staging an instance whose `preview.png` is missing or pictures content it no longer holds queues a render, a view that shows the instance asks for one, and `ref:preview()` renders one on demand. An edit renders nothing on its own. Each `preview.png` records the content it pictured, which is how a stale one is told from a current one. The preview studio owns the light, the backdrop, the framing and the camera, and nothing in the world reaches it, so an instance pictures the same in every world. Declare one of four recipes: | `kind` | The picture is | Your type provides | |---|---|---| | `instantiate` | the asset stood in the studio through its own `instantiate` | `ref.instantiate` | | `image` | a file the instance holds, fitted to the frame, nothing rendered | `source.files`, the candidate files in order | | `surface` | the asset painted on a builtin mesh | `mesh`, and `ref.previewMaterial(self) -> { material }` | | `custom` | whatever `ref.preview(self, opts)` returns | `ref.preview` | ```yaml preview: kind: surface mesh: sphere ``` A scene-instantiable type that declares nothing still answers `ref:preview()` through its `instantiate`, but persists no picture: the declaration is what makes the engine keep one. List `preview.png` among the type's files so an instance may hold it. The picture is findable only when `indexing.content` names it. The two are separate statements, and neither is derived from the other: ```yaml indexing: content: - role: identity modality: image source: { file: "preview.png" } ``` `@builtin::assetTypes.assetType.shared.previewRecipe` reads the declaration; the scaffolded block with inline guidance is in [`template/type.yaml`](template/type.yaml). ## Typing a result from the instance (`M.refShapes`) Every method on `M.ref` is checked against the type its `--!return` names, and every asset of the type gets the same one. That is right for most methods and wrong for the ones whose result is shaped by the ASSET: a method answering one entry per child folder, per row of a config, per binding an input map declares. The widest true annotation for those is `{ [string]: Thing }`, and an indexer accepts every key — so a caller's typo reads as valid and nothing reports. `M.refShapes` lets the type state the result per instance. Each entry is `function(self) -> (typeExpression, source?)`: a Luau type expression for THIS asset, and the module whose type vocabulary the expression uses. Read the asset; never run it — stating a type must not take effect. ```lua -- inputMap.assetType/behavior.luau — the shipped example. -- `activate()` hands back one handle per control the map declares. 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)` resolves and `map.noexisting` is reported with the map's real controls. Without it, both pass silently. ### What the call site has to say A per-instance type is matched by the asset's IDENTITY, so it applies only where the reference the method is called on says WHICH asset. Two spellings do: ```lua -- a component field, whose declared default names the asset public = { map = Field.assetRef("inputMap", "@builtin::inputMaps.default", Sync) } local controls = public.map:activate() -- typed for THAT map -- an annotation, naming category and identity local m: AssetRef<"inputMap", "@builtin::inputMaps.default"> ``` `asset.resolve("<identity>", "<category>")` carries the CATEGORY, so the methods the category defines are checked on the result — but not which asset it found, so a per-instance result keeps the category's declared return. A reference that names no asset (`asset.resolve(someVariable)`, a value passed in as a parameter) carries neither, and calls on it are unchecked. That is the same rule everywhere: the checker states what the code states, and a name computed at runtime states nothing. ### The result is a value, not a binding The type belongs to the expression, so it travels the way any inferred type travels. Indexing the call's result is checked; stashing it in a module local and indexing it from another function is not, because the local's declared type is what carries across — and `any` (or no annotation) carries nothing: ```lua -- checked: `d.greting` is reported function awake() local d = public.dialogue:lines() d.greeting:say() end -- NOT checked: `lines` is a module local typed `any` local lines: any = nil function awake() lines = public.dialogue:lines() end function start() lines.greting:say() end ``` Bind what you need at the call site and the names stay checked in both places — the asset-derived type at the boundary, ordinary scoping after it: ```lua local lineGreeting = nil function awake() local d = public.dialogue:lines() lineGreeting = d.greeting -- `d.greting` is reported here end function start() lineGreeting:say() end ``` ### When nothing is reported and you expected something From a call site, a type that is correct and a type that was never published look the same: no diagnostic either way. `require("@builtin::assetTypes.assetType.shared.refShapes").published()` answers which it is. It returns two maps — the category surfaces, and the per-asset results keyed by identity and then by method: ```lua local shapes = require("@builtin::assetTypes.assetType.shared.refShapes").published() shapes.categories.dialogue --> "{ lines: () -> any, lineNames: () -> { string } }" shapes.returns.Shopkeeper.lines --> "{ browsing: Line, farewell: Line, greeting: Line, wares: Line }" ``` An identity absent from `returns` was never published for, which is a different thing from published-and-correct — and the reason to look here rather than at the call site. The answers are recomputed when an instance is written, so a control added to a map reaches the checker with no restart. Only types that declare `refShapes` are read per instance, and at most 64 assets of one type are — past that the type keeps its declared return and the engine log names what was dropped. ## The instantiate hook (`M.ref.instantiate`) — a contracted return A type opts into becoming part of the scene by defining `instantiate` on its `ref` table. That is the whole opt-in: `ref:canInstantiate()` is true exactly when the hook exists, so consumers offer a scene path — an `Asset.source` field, a viewport drop, a tool argument — by CAPABILITY rather than by a list of type names. Unlike `inspect`, this hook's **return is a contract**. A caller writes one piece of code against every instantiable type, so what comes back cannot vary by type: ```lua -- behavior.luau local Instantiable = require("@builtin::assetTypes.assetType.shared.instantiable") local M = {} M.ref = { instantiate = function(self, target, opts) local root = Instantiable.root(self, target, opts) -- IN: the base opts -- ... compose whatever this type is, under `root`, NOW ... return Instantiable.result(self, root, idMap) -- OUT: the contract end, } return M ``` **IN.** `target` is an owning entity ref: the instance lands under (or, for a hierarchy type, onto) that owner. With no target the type spawns a fresh root. `position`, `rotation`, `scale`, `name` and `temporary` mean the same for every type — `Instantiable.root` applies them to a root you mint, `Instantiable.place` to one you adopt, so you never re-read the spec. Honour more opts of your own if your type needs them, and document them in the hook's `--!arg` docs. **OUT.** Return `(root, idMap)` through `Instantiable.result`: - **`root`** — the composed root, as an `EntityRef`. Composition is **synchronous**: everything your type builds is live when you return, so the caller can parent to it and read its components in the same statement. Do not defer the work to a component's `awake` and return an empty shell — a root that is not live is refused. - **`idMap`** — the `originalId -> runtimeId` map naming what you spawned, or nil for a type with no addressable children (`result` normalises it to `{}`). **A caller never receives nil.** A component that re-composes your asset on every load keeps this map and passes it back in, which is how a cross-entity reference into the composition survives a reload. `AssetRef` runs every `instantiate` through `result` on the way out whether or not your type called it, so returning something else fails at your own call rather than handing a caller a nil root. Calling it yourself is still how you say what you return, and running it twice changes nothing. `@builtin::assetTypes.assetType.shared.instantiable`'s README is the full reference for both halves. ## The inspect hook (`M.ref.inspect`) `asset.inspect(ref)` resolves `ref`, builds a common envelope shared by every asset (`identity`, `name`, `guid`, `source`, `typeName`, `typeDefinitionPath`, `scope`, `origin`, `description`, `tags`), then calls the resolved type's own inspect hook — `M.ref.inspect(self)` on `behavior.luau` — for the type-specific `detail`. The hook returns *only* `detail`; the framework fills in everything else: ```lua -- behavior.luau local M = {} M.ref = { inspect = function(self) -- self.path is the asset's own folder. Parse the asset's OWN -- files — source text, a native payload header, its `.metadata` -- — never runtime state (a live component instance, a GPU -- upload, a compiled schema): inspect must work on an asset that -- hasn't been compiled, uploaded, or registered yet. return { -- type-specific fields, whatever this type wants to surface } end, } return M ``` A type that omits `M.ref.inspect` yields the generic record: the envelope alone, `detail = nil`. That's a valid, working state — not an error — but it means agents calling `asset.inspect` / `tools.use("assets","describe", ...)` on an instance of the type see only identity and scope, none of what makes an instance of it meaningful. Declaring the hook is what makes a type's instances inspectable for what they actually are. A hook that raises leaves `detail = nil` and sets `record.warning` to the error text — `asset.inspect` itself never raises for a resolved ref. ## Discovery - `asset.list("assetType")` — every registered asset type. - `asset.inspect("<typename>")` — the type's identity, source path, and this README, plus its `about.yaml` texts as `detail.about`. - `asset.resolve("<typename>", "assetType")` — the `<typename>.assetType` asset itself (used by the validator to find the type's `type.yaml`). ## Related - `assetTypes/README.md` — the full `type.yaml` schema and validator rules. - `@builtin::assetTypes.assetType.shared.instantiable` — the instantiation contract's shared implementation, both halves. - Every other `*.assetType/` here — the concrete types built on this meta-type. ## The Inspector a type ships Every type names the file its Inspector sections come from with `inspector: { type: "inspector.luau" }` in its type.yaml. `asset.create("assetType", name)` scaffolds the block and an `inspector.luau` whose Contents section lists an asset's files, built from `ctx.parts`. `asset.validate` on a type that names none reports `assetType.inspector_required` (an error for a `@builtin` type the engine's library authors; a warning for a world's own, and for one the library carries by reference, which the world that published it answers for), and one whose file does not load `assetType.inspector_invalid`. ## In the Inspector Type: what the type.yaml declares (suffix, container, primary files, the settings and inspector blocks), whether the type ships a behavior, how many instances it has, and **Open** for its files. Schema: the settings every instance carries, as read-only typed controls at their defaults (a type whose schema is per instance says where it is declared). Instances: a link to each of the first 20 assets of the type, and how many there are when there are more.
▲ 0↑ born
◇
component · born here
❒asset
# ScreenSpaceReflections Reflections off the scene's own drawn frame, as a one-feature component. A scene lists its features together, each with its settings, on `RenderFeatures` (`{ feature = "@builtin::systems.reflections.ssr", settings = { ... } }`); this component carries the same knobs as fields for one feature. ```lua local id = entity.spawn("reflections").id entity(id).component.add("ScreenSpaceReflections", { intensity = 1.0, maxDistance = 40, quality = "high", }) ``` Polished floors start reflecting what stands on them, wet ground reflects what is above it, and metal picks up its surroundings — all from geometry the camera can already see, with nothing authored per surface. Roughness decides who gets them. A screen-space ray traces one direction, which is a mirror; `roughnessCutoff` is where that stops being a fair stand-in for a surface's real lobe, and past it the pass leaves the surface alone. Every field is live: edit one in the inspector and the reflections follow. Setting `intensity` to 0 turns them off without losing the rest of the settings. How much a reflection shows through is the surface's own Fresnel term, so metal takes almost all of it and a dielectric floor takes a few percent head-on and more at a grazing angle — the same way a wet road reads dry underfoot and mirror-like down the street. For code that adjusts reflections at runtime, reach for `@builtin::systems.reflections.ssr` — same options, no component. The full description of what each knob does and how the passes work is in the package readme: `guides { path = "systems/reflections" }`.
▲ 0↑ born
·
scene · born here
❒asset
# Polished floor A polished stone floor with three coloured pillars standing on it, under an afternoon sky, with screen-space reflections listed on the scene's `RenderFeatures` entity — so the floor reflects the pillars, and a player walking across it, rather than the sky alone. Load it with `scene.load("@builtin::systems.reflections.demo.polished_floor")`. Players arrive facing the pillars from floor height, which is the grazing angle a polished dielectric reflects at most strongly. ## What it shows - **The feature on one entity.** `render_features` carries a `RenderFeatures` list with `@builtin::systems.reflections.ssr` at `intensity = 1`, `maxDistance = 60`, `quality = "high"`. Delete the entity and the floor mirrors the sky alone; put it back and the pillars stand in the floor. - **Only what the camera sees.** A pillar's reflection comes from the frame the camera drew, so turning away from a pillar takes its reflection with it, and the reflection fades toward the screen edge rather than cutting off. - **The environment underneath.** Where a ray finds nothing the floor keeps the sky reflection the environment gives it, at the same mip and Fresnel weight, so the seam between the two is a change of content and not of brightness. - **Roughness decides who reflects.** The floor sits at roughness 0.06, the pillars at 0.55 — above the feature's `roughnessCutoff` — so the floor is the one surface tracing reflections in the scene. ## Contents - `floor` — a 60 m slab of `PolishedStone`, a near-black dielectric at roughness 0.06, with a collider. - `pillar_1..3` — 1.6 × 5 × 1.6 m boxes in `PillarRed`, `PillarGreen`, `PillarBlue`, sunk into the slab. - `sun`, `ambient`, `sky` — a side-lit afternoon under a `ProceduralSky`. - `render_features` — the `RenderFeatures` entity this scene exists for. - `PlayerSetups` / `Spawns` — the humanoid player on an orbital-follow camera, spawning in front of the pillars. The scene's player intent is `"spawns"`. ## Entry point - `entrypoint.luau` — nothing to set up: the reflections run from the `RenderFeatures` entity the moment the scene loads.
▲ 0↑ born
▣
module · born here
❒asset
# ssr Screen-space reflections — polished floors reflect what stands on them, from the frame the engine has already drawn. ```lua local ssr = require("@builtin::systems.reflections.ssr") ssr.set({ intensity = 1, maxDistance = 40, quality = "high" }) ssr.get() --> the settings in force ssr.active() --> are the reflection passes running ssr.clear() --> off, settings kept for the next set() ``` `set` takes any subset — omitted fields keep their current value, so adjusting one knob does not mean restating the rest. An `intensity` of 0 turns reflections off and releases the passes rather than running them at zero strength. `roughnessCutoff` is the field that decides who gets reflections at all. A screen-space ray traces one direction, which describes a mirror; past the cutoff a real surface scatters over a lobe too wide for one ray to stand in for, and the pass leaves those surfaces alone. `maxDistance` and `quality` trade against each other. Quality sets how many steps the march spends, so a long distance at a low quality samples coarsely — reflections thin out at range rather than disappearing, because refinement adapts to the step size on its own. For reflections a scene should simply have, author the `ScreenSpaceReflections` component instead — it carries the same settings, persists with the scene, and edits live in the inspector. This module is for code that changes reflections at runtime: a quality slider, a cutscene, weather turning pavement wet. The full description of what each knob does and how the passes work is in the package readme: `guides { path = "systems/reflections" }`.
▲ 0↑ born
·
renderfeature · born here
❒asset
# ssr The render feature behind `ScreenSpaceReflections` and `@builtin::systems.reflections.ssr`. One compute pass runs at the `afterDepthPrepass` seam, once the visibility raster has laid the frame's depth and before anything shades: | order | pass | output | |---|---|---| | 60 | `ssr_trace` | reflected colour plus a confidence weight, per pixel, into `@scene.reflection` | That channel is what a material's environment term reads the same frame, at its own pixel: the traced reflection stands in for the probes and the sky to the degree the trace trusts it, and the material's own Fresnel, roughness and tint weigh it into the pixel the way they weigh any environment. Nothing here touches the lit picture. The trace marches against this frame's depth, which the visibility raster has just written, so the reflection stands where the camera is now. What it reads about the surfaces — the reflecting surface's normal and roughness, the hit's normal and its colour — comes from the previous frame: the G-buffer as that frame left it (`readsPrevious`), and `@scene.colorHistory`, the light it ended with, each reprojected through the previous view-projection `@frame.camera` carries in its rows `[5..9)`. A point the previous frame did not show reflects its environment alone. ## The march `ssr_trace` reflects the view ray about the surface normal and walks it through the depth buffer. Steps are uniform in **screen** space, not world space. A ray heading toward the horizon covers enormous world distance across a few pixels, so stepping in world units there means either missing every thin occluder near the camera or spending the whole budget on distance nothing can be seen at. The world parameter is recovered from the screen parameter through `1/w`, which is what interpolates linearly under perspective. At each step the ray's world position is compared against the world position reconstructed from the depth sample under it. Comparing in world space is what lets `thickness` mean a distance in world units rather than a depth-buffer epsilon whose meaning changes with range. The march looks only for the **crossing** — where the ray first passes behind the depth buffer. Thickness is tested after bisecting that crossing, not during the march: one march step covers roughly as much depth as a surface is thick, so a ray tested during the march can jump from in front of a surface to well behind it without ever landing inside it. Bisection runs for whichever is larger, the quality tier's budget or however many halvings bring one march step under `thickness`. A coarse march that stopped bisecting early would leave the crossing bracketed more loosely than a surface is thick, and every hit would then fail the thickness test — long-range reflections would vanish outright instead of thinning. A hit is rejected when the surface it landed on faces along the ray, because what is on screen there is that surface's far side. ## What the channel holds `ssr_out` is `@scene.reflection`: linear radiance in `.rgb`, and in `.a` the confidence — the edge fade, the range fade, how squarely the ray carried on into the scene, and `intensity`, multiplied — so `intensity` is how much of the trace reaches the picture and a miss is a zero the material reads as "the environment alone". The channel is cleared at the top of every scene render this feature does not run for, so such a frame reflects the environment alone too. The trace reads the light the previous frame ended with, so it runs on the lit channel alone (`debugChannel = 0`); a camera drawing a diagnostic pass is drawn lit first, unpresented, so the trace runs for where that camera stands, and the diagnostic reads the channel traced for it. The cutoff exists because a single ray describes a mirror: past it the lobe a rough surface gathers over is wider than one ray can stand in for, and the material keeps the prefiltered environment it already has.
▲ 0↑ born
·
shadermodule · born here
❒asset
# ssr_common The screen-space reflection passes' shared ground: the view-space reconstruction from depth, and the ray-to-screen projection the march steps along. Included with `#include "@builtin::systems.reflections.ssr_common"`.
▲ 0↑ born
·
computeshader · born here
❒asset
# ssr_trace Marches a reflection ray through the depth buffer and reports where it hit. ## Includes - `ssr_common` ## Bindings | name | kind | |------|------| | `scene_color` | `texture2d` | | `scene_depth` | `texture_depth` | | `scene_normal` | `texture2d` | | `scene_material` | `texture2d` (`sample: uint`) | | `frame_cam` | `buffer` | | `ssr_params` | `buffer` | | `ssr_out` | `storage2d (rgba16f)` |
▲ 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.install`, `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). **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 }) ``` ## How it operates 1. **Registration.** Writing `package.yaml` indexes the package with the package registry. 2. **Install.** A package is installed by guid, the way every asset is: `zeromind.install` from the shared library, or `tools.use("zeromind", "installAsset", <guid>)` inside the engine. The install writes the package's files (and the deps its `.refs` record) 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. - **Dependency installs.** A package that depends on another package must declare it (in `package.yaml::dependencies` once supported) and the parent world must install dependencies in order. - **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
# GaussianSplat Renders a Gaussian splat capture on an entity — a photographic reconstruction stored as a cloud of oriented 3D Gaussians. `.spz` (Niantic) and `.ply` (3DGS) captures both load. ```luau local id = entity.spawn("scan").id entity(id).component.add("GaussianSplat", { source = "captures/room.gaussianSplat" }) ``` The entity's Transform places, rotates and scales the cloud like any other renderable: each Gaussian's covariance is conjugated by the transform, so a non-uniform scale shears the cloud the way it shears a mesh. ## Fields | Field | Default | Meaning | |-------|---------|---------| | `source` | `""` | VFS path of the capture. | | `convention` | `"rightDownFront"` | Axis convention the capture was authored in. `"engineNative"` for a capture already in engine space. | | `opacity` | `1.0` | Multiplies every splat's stored opacity. | | `size` | `1.0` | Multiplies every splat's stored size. | | `visible` | `true` | Whether the cloud draws. | | `buckets` | `4096` | Depth buckets the per-frame far-to-near sort files splats into. | | `near` / `far` | `0.05` / `500.0` | Depth range the sort resolves, in world units. Splats outside it are skipped. | ## Methods - `setSource(source)` — point the cloud at a different capture. - `setOpacity(scale)` — scale every splat's stored opacity. - `setSize(scale)` — scale every splat's stored size. ## Notes - The component only declares what to render. The `gaussianSplats` render feature owns the GPU buffers and the per-frame depth sort, because only it runs inside a frame with a live camera to sort against. - `guides { path = "topics/gaussian-splats" }` covers capturing, axis conventions, and tuning.
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
▣
module · born here
❒asset
# gaussianSplats The entity-id-keyed registry of live Gaussian splat clouds — the seam between `GaussianSplat.component` (which declares what to render) and the `gaussianSplats` render feature (which owns every GPU buffer and runs the per-frame depth sort). A plain table: no GPU state, no frame coupling. ## Exports - `M.set(id, entry)` — register or update an entity's cloud. Changing `source` or `convention` bumps the entry's generation, which makes the render feature drop its buffers and decode again. - `M.clear(id)` — drop an entity's cloud, along with the placement recorded for it. - `M.entries()` — the live registry, keyed by entity id. - `M.get(id)` — one entity's entry, or nil. - `M.setVelocity(enabled)` / `M.velocity()` — whether a moving cloud writes its screen-space displacement into the frame's velocity buffer. - `M.recordPlacement(id, matrix)` — the render feature records the placement it culled and drew a cloud at, as 16 column-major numbers. The numbers are copied in. - `M.placement(id)` — that placement, translation in elements 13, 14, 15, as a fresh table each call. Nil on a frame the feature did not draw the cloud, and once the cloud has left the registry. Types: - `SplatEntry` — `{ source, convention, opacity, size, buckets, near, far, visible, generation }`. ## Usage ```luau local registry = require("@builtin::systems.gaussianSplats.gaussianSplats") for id, entry in pairs(registry.entries()) do print(id, entry.source, entry.visible) end ```
▲ 0↑ born
·
computeshader · born here
❒asset
# splat_cull Stage 1 of the Gaussian splat per-frame depth sort. One thread per splat. Transforms the splat's packed position to world space, frustum-culls it at three sigma (the Gaussian's visible support), computes its view depth from the clip-space `w`, and files the survivor into one of `bucket_count` depth buckets ordered far to near. The bucket id lands in `keys[i]`; a culled splat gets `bucket_count`, one past the last real bucket, so it sorts behind every survivor and the scatter can recognise it. Buckets are spaced logarithmically between `near` and `far`, so each spans a constant ratio of depth: near geometry gets the same relative ordering precision as far geometry. `indices` is seeded to the identity permutation the radix passes start from. ## Bindings | Slot | Access | Contents | |------|--------|----------| | `records` | read | Packed splat records, 6 u32 words each. | | `view` | read | `CullUniforms` — view-projection, model, frustum planes, params. | | `keys` | read_write | Per-splat depth bucket, or `bucket_count` when culled. | | `indices` | read_write | The sort's input permutation, seeded to the identity. | | `stats` | read_write | `[0]` = surviving splat count (atomic). | A splat whose whole footprint projects to less than `screen.z` pixels of radius is dropped as well. Distance shrinks that projection, so a cloud far from the camera resolves to progressively fewer splats while a near one keeps every splat it has. The radius is estimated by projecting the three world axes from the splat's centre and keeping the widest, which holds under a sheared or non-uniformly scaled cloud. `params` is `(splat count, bucket count, near depth, far depth)` and `screen` is `(viewport width, viewport height, min screen radius in pixels, 0)`. Dispatch one workgroup of 64 per 64 splats.
▲ 0↑ born
·
assettype · born here
❒asset
# Module (asset type) A module is a reusable, stateless (or globally-stateful) Luau library that other code pulls in with `require()`. Modules are the unit of factored-out logic in a world: helpers, math libraries, format parsers, registries, shared algorithms. ## When to use one - You have logic you want to share across multiple components, services, scenes, or tools. - You have a registry, lookup table, or shared cache that callers all read from / write to. - You want a stable API surface (`M.fn`) other code can depend on. If the logic is bound to one entity, use a `.component`. If you want a named UI of fields rather than functions, use a `.preset`. ## Where it lives - Source: `/zero/source/.../<name>.module/` - Identity: the source path with `/` written as `.` and each segment's type suffix dropped — `/zero/source/demo/beats.module` is `demo.beats`, and `/zero/source/kit.package/beats.module` is `kit.beats`, the container's name kept and its `.package` suffix gone. `asset.create` returns this string, `asset.resolve(...).identity` reads it back, and the `require` forms below write it as `<identity>`. - Folder shape: - `init.luau` (or `init.lua`) — module body. **Required.** Returns a table (conventionally named `M`). - `README.md` — instance-level documentation. **Required.** - `.metadata` — agent-editable tags + free-form fields. **Required.** ## How to create one ```luau asset.create("module", "<name>") -- Creates: /zero/source/<name>.module/ -- init.luau (canonical `local M = {} … return M`) -- README.md (instance README template) -- `folder` places it in a subfolder of /source, and that subfolder -- leads the identity: `{ folder = "lib" }` produces `lib.<name>`. -- `into` authors it inside a resolved container (a package, a toolbox), -- whose own identity leads the same way: asset.create("module", "<name>", { folder = "lib" }) asset.create("module", "<name>", { into = asset.resolve("myPkg.package") }) ``` ## How it operates 1. **Registration.** Writing `init.luau` into a `.module/` folder indexes the asset and registers the module path with the resolver. 2. **Loading.** The first `require("<identity>")` runs the file's body and caches the returned table. Subsequent requires from anywhere return the **same table** — this is the shared-state lever for "module-local" registries. 3. **Hot reload.** Editing the file re-runs the module body and folds what it returns into the table already loaded, so that table keeps its identity: a caller that captured it with `require` reads the new source's functions and values through the reference it already holds, and a later `require` returns that same table. A key the new source drops goes away; a key something set at runtime stays. The body's own `local`s start again from their initial values, so state that must outlive an edit lives in `modules.state()` (`guides { path: "core/scripting-and-tasks" }`). 4. **Identity resolution.** A module is reached by its `<identity>`, and where the caller lives decides which spelling of it resolves. For a module in a **world**, from a caller in that same world — another module, a component, a scene entrypoint, or an `execute` chunk: - `require("<identity>")` — the identity on its own, resolved against the caller's own root. It carries every folder segment, so a module created with `{ folder = "demo" }` is `require("demo.beats")`. - `require("@root::<identity>")` — the same root-relative resolution, written out. - `require(".sibling")` — a module in the same folder, by its bare name; `..name` steps up one folder, `...name` two. Logs, stack traces and hot-reload notices name that same module `@local.source.<identity>` — its key under the world source root. The `<identity>` tail of that key is the form above, so a trace reading `@local.source.demo.beats` is `require("demo.beats")`. For a module in a **library** — content under `/zero/source/libs/@<lib>/` — `@<lib>::` addresses that library's root from anywhere: `require("@builtin::modules.transform")`. Mounting a world as a library moves its files under such a root and flips the caller root to `@<lib>::`, which is what keeps the root-relative forms above resolving across the move. ## Discovery - `asset.list("module")` — every registered module. - `asset.inspect("<name>")` — public functions, source path, this type README. - `ls /zero/source/<name>.module` lists the files the folder holds. ## Authoring conventions - Return a single `M` table from `init.luau`. Top-level statements with side effects run on first require — useful for one-time initialization, dangerous if they touch the engine before it's ready. - Write exported functions as `typed function`, not `function`: the argument types are enforced at the call and the checker knows the signature, so a caller's mistake is reported where they made it. - Annotate exported functions with `--!desc` / `--!arg` / `--!return` / `--!example` so the LSP, `tools.list`, and `asset.inspect` surface them. A function with no `--!desc` is reachable only by someone already reading this file. - Declare a VALUE with `Field.<kind>(default, mode, description)`, and a table whose members are reached through a metatable with `--!members <TypeName>` — the latter gives the surface its accepted member set, its type, and its documentation from one table. See `guides { path: "core/authoring" }` for all four rules and `modules/api/engine/entity.module/members.module` for a worked member table. - Keep module state in module-local upvalues or in `modules.state()`. Globals leak across reloads. Module-locals start again from their initial values with each hot reload; the table `modules.state()` returns is the same table on both sides of every reload. - Prefer focused modules over kitchen-sink modules. If two halves of a module have no shared state, split them. ## Common pitfalls - **Cyclic requires.** A `require` chain that loops will return the partial table (the half built before the cycle was detected). Design for one-way dependencies. - **Engine-time side effects.** Don't `entity.spawn` at module top-level — the engine may not be ready. Expose an `M.init` and call it from a scene entrypoint. - **Hot-reload + module-local cache.** A module that caches expensive computation in upvalues loses that cache on reload — fine for dev, but be aware in performance work. - **`init.lua` vs `init.luau`.** Either works, `.luau` is canonical. - **Writing one while play runs.** A `/source` write made while play is running lands on the play shadow, and a guarded play-exit discards it unless it is kept. One `.module` write shadows `init.luau`, the `README.md` and the `.metadata` together. `vfs.write(path, bytes, { durable = true })` skips the shadow, and `vfs.promotePlayShadow` keeps what is already on it — the `core/vfs` guide has the model and what each route costs. ## Related types - `.component` — for entity-bound state + lifecycle hooks. - `.service` — generates content you don't have yet (a mesh, sound, texture, …) via a metered provider. - `.tool` — for a single agent-callable function with a YAML schema. - `.package` — to group several modules + components + scenes into one shippable folder. ## In the Inspector Exports: what the module exports (name, kind, signature), parsed from its source without running it, with **Check** (the LSP's diagnostics for its entry file) and **Open**.
▲ 0↑ born
·
toolbox · born here
❒asset
# tests Run the engine test suite from inside a live engine and triage the results — the baseline-then-regression loop, without leaving the session. List every registered test suite (optionally with test counts) to see exactly what the runner and CI will execute. Run everything, one suite, or a subset — each suite runs through its own cooperative coroutine so multi-frame tests yield correctly — and get back structured results plus Markdown and JSON reports written to the VFS. Compare two JSON reports to surface the failures that are new since the baseline, which is how you catch regressions a change introduced. The lower-level framework for authoring suites lives in the test module and the `testSuite` asset type.
▲ 0↑ born
◇
component · born here
❒asset
▲ 0↑ born
·
scene · born here
❒asset
▲ 0↑ born
·
scene · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
▣
module · born here
❒asset
▲ 0↑ born
·
assettype · born here
❒asset
# pixelTemplate (asset type) A saved, reusable pixel-art sheet. A `<name>.pixelTemplate/` folder asset carries a JSON payload `template.pixbin` (width, height, pixelSize, plane, palette, the flat pixel array, and — for animated sheets — every frame) plus the `.metadata` the structural spec requires. Authoring paths: `PixelCanvas:saveAsTemplate(name)` / `Pixel.saveAsTemplate(...)`. Instantiation: `Pixel.fromTemplate(name, opts)` copies (bakes) the payload into a fresh canvas — there is no live link, so editing an instance never touches the template. ## In the Inspector Selecting a pixel template opens its **Sheet** section, decoded from `template.pixbin` once per version of the file: the sheet's size, pixel size and plane, how many frames it holds and, when it animates, its frame rate and whether it loops, followed by a swatch per palette entry. A template with no saved sheet says so.
▲ 0↑ born
·
toolbox · born here
❒asset
# pixelArt Create and animate pixel-art canvases — make sprites, draw on them, animate frames, manage palettes, and save them as templates. Spawn a canvas blank, seeded, painted from text-art rows, cloned from an existing one, or instantiated from a saved template. Draw onto the active frame with raster ops — pixels, rectangles, lines, circles, ellipses, whole rows, or clear. Build multi-frame animation: add, duplicate, or remove frames, pick the active frame, set the frame rate, and play or stop. Replace the palette or update a single colour. Read back pixels, bounds, counts, frame count, and the template list. Save a canvas as a reusable template or delete one, and despawn a canvas when you're done. A target — a handle, an entity id, or an entity name — is accepted uniformly across the toolbox.
▲ 0↑ born
◇
component · born here
❒asset
# Buoyancy Floats the entity's rigid body on the sea. ```lua entity.find("barrel").component.add("Buoyancy", { density = 0.4 }) ``` The entity needs a dynamic `Physics` body. Position and orientation stay the physics world's — this component only adds the forces water exerts, so a floating body still collides, still takes impulses, and still falls when it is lifted clear of the water. | Field | What it does | |---|---| | `density` | Relative to water: the lower it is the higher the hull rides, and a body denser than water sinks. A hull settles a little deeper than the number alone asks for, so trim it until the waterline sits where it should. 0 releases the body. | | `damping` | How hard water resists the hull moving through it, as a fraction of the damping that settles it without overshoot. 1 is that critical value, 0 bobs indefinitely. Applied per column, so it steadies rolling as well as heaving. | | `angularDamping` | The same, for a hull turning. The rigid body carries it as its own angular damping while the hull is in the water, and it is the only thing that damps a spin about the hull's own vertical axis, which the per-column damping cannot see. | | `columns` | Grid resolution per horizontal axis over the footprint. 1 floats but cannot right itself; 2 is the smallest grid that produces a righting moment. | | `sizeX`/`sizeY`/`sizeZ` | The box that displaces water, in the hull's own space. All three at 0 measures it from what the entity draws. | `barrel:submersion()` reads how much of the hull was under water on the last step, and `barrel:waterline()` reads the world Y the surface stood at under it, wave motion included. Forces reach a rigid body only while the scene is playing, so a hull sits where it was placed in edit mode and settles onto the water when play starts.
▲ 0↑ born
◇
component · born here
❒asset
# Ocean Puts a live sea on an entity. The component's fields ARE the sea state — wind speed and direction, choppiness, swell, foam, extent and detail — and the wave simulation, the GPU surface mesh and the material bound to the simulation are all rebuilt from them whenever the component wakes. That rebuild is what makes the ocean persistable. The simulation lives entirely in GPU buffers and a runtime mesh, none of which survive an engine restart; the component's settings do, and the whole sea is reconstructed from them. A scene with an ocean in it reloads into the same sea. The surface follows the viewer and covers `extent` metres around them, so one ocean is the whole sea however far anyone sails. `sampleHeight(x, z)` and `sampleDisplacement(x, z)` report where the water is — the same waves that are drawn — which is what a boat, a buoy or a swimmer floats on. Start from a `preset` (`calmClear`, `darkChop`, `darkRolling`, `heavySwell`, `whitecapped`, `murkyGrey`, `paleSwell`, `glassyDark` and `gale`) and override whatever the scene needs. Wind speed does most of the work: significant wave height goes as its square.
▲ 0↑ born
·
computeshader · born here
❒asset
# ocean_assemble Unpacks one cascade's two transformed fields into the `(dx, height, dz)` displacement field the surface mesh reads, applying the `(-1)^(x+z)` sign alternation that undoes the centred-on-zero wave-vector grid the spectrum was built on. Height and horizontal displacement carry independent gains, so crest pinching (`choppiness`) can be dialled without changing wave height.
▲ 0↑ born
·
computeshader · born here
❒asset
# ocean_displace Displaces the ocean surface mesh by one cascade, once per cascade per frame. The first cascade writes each vertex from the rest position stored in its UV and the rest accumulate onto it, so the mesh is rebuilt from a clean grid every frame instead of drifting. Cascades are sampled at the vertex's true world position — the mesh follows the camera, so its rest grid sits at a moving origin. The vertex normal accumulates each cascade's surface gradient (the ratio `n.xz / n.y`), which is how independent wave fields actually combine; per-fragment normal detail comes from the cascade normal maps in the surface shader, since the mesh is far too coarse near the horizon to carry it.
▲ 0↑ born
·
computeshader · born here
❒asset
# ocean_evolve Advances one cascade's static spectrum to the current time — `h(k,t) = h0(k) e^{iwt} + conj(h0(-k)) e^{-iwt}` at the deep-water dispersion `w = sqrt(g|k|)` — and emits the two complex fields the inverse FFT consumes. The frequency is quantised to a multiple of `2*pi / loop_period` so the whole sea state repeats seamlessly instead of drifting as elapsed time grows. Field A packs the height and X-displacement spectra into one complex field (`H + i Dx`), which the Hermitian symmetry of both lets a single inverse transform separate again — two FFTs per cascade instead of three.
▲ 0↑ born
·
computeshader · born here
❒asset
# ocean_fft Transforms one axis of an ocean cascade's complex field in place, as one dispatch per axis: a workgroup per line runs every Stockham auto-sort radix-2 butterfly stage against workgroup-shared memory, so a 256-wide transform costs one dispatch rather than eight butterfly passes and needs no bit-reversal permutation. Supports grids up to 512 per side. The axis, direction and scale arrive through the bound `plan` buffer rather than a `params:` uniform, because same-frame dispatches are chained into one submission and a uniform rewritten between them would not be seen per-dispatch.
▲ 0↑ born
·
computeshader · born here
❒asset
# ocean_floor_displace Raises the seabed's flat grid into terrain: one gradient-noise field over world XZ, written into each vertex's height, with the analytic slope written into its normal. Runs when the mesh recentres rather than every frame. The terrain is still in the world; only the grid under it moves. It is a compute pass rather than the surface shader's `vertex()` hook so that every pass that draws the mesh, and everything that reads its vertex buffer, meets the same displaced geometry. The normal is the derivative of the noise rather than a difference of neighbouring vertices, so it stays correct where the grid is coarse — which, over a clipmap, is nearly everywhere. ## The collision patch The threads past the mesh write a square patch of the same field into `heights` as plain metres, row-major with the column along world X and the row along world Z — the layout a heightfield collider indexes. `plan[2]` gives the patch its world centre, its samples per side and its world span. It is the same call to the same function that raised the vertices. A second evaluation of the noise, in Luau or in a pass of its own, is a copy free to drift from the one that is drawn, and ground stood on a metre from where it is drawn is worse than ground with no collision at all — nothing reports it. ## Bindings | Buffer | Access | Contents | |--------|--------|----------| | `plan` | read | lane 0: `xy` mesh world origin, `z` vertex count, `w` noise period (m). lane 1: amplitude, lacunarity, persistence. lane 2: `xy` patch centre, `z` samples per side, `w` patch span (m). | | `vertices` | read_write | The mesh's vertex buffer, engine 23-word layout. | | `heights` | read_write | The collision patch, `side * side` f32 metres. | Dispatch `vertexCount + side * side` threads.
▲ 0↑ born
·
computeshader · born here
❒asset
# ocean_floor_texture Grows the three tiling maps the seabed is built from: sand, rock, and the caustic web the sun draws on both of them. Albedo goes in `rgb` and height in `a`; the caustics map carries two independent phases in `r` and `g`. Generated rather than shipped as images. A stored texture is a binary, content-addressed asset, and a material blocked on a texture reference that never resolves never prepares — which leaves its entire uniform block unwritten and the surface black. Generating keeps the seabed on the same compute-texture path the cascade maps and the foam patterns already use. Run once per map at startup, each with its own seed. Every map tiles: each octave hashes its lattice modulo a period that divides the texture size. The caustics are constructed rather than painted. Sunlight refracting through a wavy surface lands as the light a patch gathers divided by the area it spreads over — the reciprocal Jacobian determinant of the refraction map. Writing the surface as a height field makes that map `p + k grad h`, so the determinant is that of `I + k H`; inverting it produces the sharp filaments and their crossing cusps, which are the places the determinant approaches zero and a finite bundle of light lands on almost nothing.
▲ 0↑ born
·
computeshader · born here
❒asset
# ocean_foam The persistent whitecap field: a world-space region, following the viewer, in which foam accumulates where the sea folds and drains everywhere at a fixed half-life. Shading foam directly from the instantaneous fold makes it blink with the wave that caused it. This field is what gives a broken crest a wake of white behind it, and what the surface's `waves` foam layer is actually gated on. Run once per frame, after every cascade's `ocean_normals`. The two history buffers swap roles each frame; the caller binds whichever holds last frame's result as `previous`.
▲ 0↑ born
·
computeshader · born here
❒asset
# ocean_foam_texture Grows the tiling greyscale pattern the three foam layers stamp on the water. It is generated rather than shipped as an image. A stored texture is a binary, content-addressed asset, and a material blocked on a texture reference that never resolves never prepares — which leaves its entire uniform block unwritten and the surface black, not merely un-foamed. Generating the pattern keeps foam on the same compute-texture path the cascade maps already use. Run once per layer at startup, each with its own seed. The noise tiles: every octave hashes its lattice modulo a period that divides the texture size.
▲ 0↑ born
·
computeshader · born here
❒asset
# ocean_normals Differentiates one cascade's displacement field into a shading normal and a foam mask. Normals come from central differences of the displaced positions (so crest pinching is accounted for, unlike a height-only gradient), with wrap-around neighbours because the cascade tiles. Foam comes from the smaller eigenvalue of the horizontal displacement Jacobian: where it goes negative the surface has folded over itself, which is what a breaking wave is — so whitecaps appear from the same displacement that raised the crest rather than from a painted-on noise threshold. Fresh foam replaces the stored value immediately while old foam fades at `foam_decay` per second, leaving streaks behind broken crests.
▲ 0↑ born
·
computeshader · born here
❒asset
# ocean_probe Resamples every cascade onto one small grid that the CPU reads back, so gameplay can ask where the water is without stalling a frame on the full simulation. Dispatched once per cascade: the first overwrites the grid, the rest add to it. `xyz` is the summed displacement in metres and `w` the strongest folding, so a caller can also ask whether the water at a point is breaking. The swell bank is deliberately absent — it is analytic and longer than the tile this grid covers, so `ocean.module` evaluates it directly on the CPU instead.
▲ 0↑ born
·
computeshader · born here
❒asset
# ocean_spectrum Builds one ocean cascade's time-independent complex wave amplitude `h0(k)` from a JONSWAP directional spectrum: a Phillips energy core, Donelan–Banner directional spreading around the wind, a JONSWAP peak enhancement, and a `k_min`/`k_max` band limit that keeps each cascade to its own disjoint range of wave numbers. The result is randomised into a complex Gaussian so the same sea state produces a real random surface rather than a deterministic ripple. Runs when the sea state changes, not per frame — `ocean_evolve` animates the field it writes.
▲ 0↑ born
·
procgraph · born here
❒asset
# islandLagoon The island the `island_lagoon` demo scene stands in, as a terrain graph. A flat lagoon bed at -14 m with a broad dome rising through sea level at roughly 1:21, carrying two surfaces: sand from the lagoon floor up past the waterline, grass over the dry crown. The gentle slope is the point — it puts a continuous range of water depth in front of the camera, which is what makes the ocean's shoaling, absorption and shoreline wash readable in one shot. ## Inputs None. Every dimension is authored in the graph; edit `init.luau` to move the waterline, widen the beach or change the relief. ## Outputs - `land`: the landscape as one element carrying `Terrain`, which the graph shows through `io.output`: the ground, its colliders and its water, standing in the scene. ## Consumers - `@builtin::systems.oceans.demo.island_lagoon` instantiates it as the demo's terrain and binds the ocean's `ground` to the entity it produces.
▲ 0↑ born
·
scene · born here
❒asset
# Island lagoon The spectral ocean against a shore: an island rising out of a lagoon at a beach's slope, so one view holds every water depth from the waterline to open blue. Load it with `scene.load("@builtin::systems.oceans.demo.island_lagoon")`, then walk down to the water. ## What it shows - **Depth as colour.** The water column's absorption is per-channel, so the sea reads turquoise where the sand is a metre or two down and turns blue as the bed falls away. The gradient is the ground's own slope seen through the water, not a painted ramp. - **The seabed through the surface.** The bed is refracted, attenuated over the real path the light travelled, and lit by the caustic web the surface focuses onto it. - **Shoaling.** Chop dies as the bottom comes up — geometry and shading together — so the shallows are glassy while the open water stays crisp. - **The shoreline.** Wash along the waterline, foam that clings to breaking crests, and a sea that meets the sand instead of ending at it. - **A sun path over water.** The glitter track is the surface's own slope statistics under this scene's directional light. ## Things to try ```luau local Presets = require("@builtin::systems.oceans.modules.oceanPresets") Presets.names() -- nine sea states, calm to gale entity("lagoonSea").Ocean.preset = "gale" -- the same shore under a gale entity("lagoonSea").Ocean.seaLevel = 2.0 -- flood the beach ``` A preset is a whole sea — spectrum, swell, colour, all three foam layers and the seabed — so swapping one changes the water and leaves the island, the light and the sky exactly where they are. ## What it is made of - `@builtin::systems.oceans.demo.islandLagoon` — the terrain graph the island is baked from. - `Ocean` — the sea, bound to that landscape through its `ground` field. - A directional sun, an ambient fill, a `ProceduralSky` and a `PhysicalAtmosphere`. The ocean carries no light of its own: every reflection, the glitter and the foam's shading come from these.
▲ 0↑ born
·
guide · born here
❒asset
# oceans guide The walkthrough for building a sea with the oceans package: putting an ocean in a scene, choosing a sea state and understanding which control does what, floating things on the waves, and what the simulation costs.
▲ 0↑ born
◐
material · born here
❒asset
# oceanSeabed The ground under the sea: one gradient-noise field mixed between sand and rock, lit by the scene's own sun and ambient, with the caustics the waves above focus onto it added on top. The simulation drives the cascade displacement map, the sand and rock maps the compute pass generates, and the terrain's world origin on this material as the clipmap recentres; the blend threshold, the tiling and the caustic strength are authored here. Sea-state presets in `~.modules.oceanPresets` override these values.
▲ 0↑ born
◐
material · born here
❒asset
# oceanWater The default open-ocean material the `Ocean` component applies: deep blue with per-channel absorption tuned for clear seawater, a near-mirror surface, and breaking-wave foam. The simulation drives the cascade normal maps, the cascade patch sizes and the scene-depth map on this material every frame; everything else is authored here. Sea-state presets in `~.modules.oceanPresets` override these values for paleSwell, gale and the rest.
▲ 0↑ born
▣
module · born here
❒asset
# buoyancy Floats a rigid body on the sea the simulation is already drawing. ```lua local buoyancy = require("@builtin::systems.oceans.modules.buoyancy") buoyancy.attach(barrel, { density = 0.4 }) ``` Or attach a `Buoyancy` component next to the entity's `Physics` body and author `density` / `damping` / `angularDamping` / `columns` on it — the component calls this module every frame. ## A hull is a bundle of columns A grid is laid over the hull's footprint, and each cell stands for a vertical prism spanning the hull's full height. Every column measures the water above its own base, and carries the share of the displaced volume that implies, applied at its own position. That is what the grid is for. One force at the centre of mass can only push a body up and down; forces spread over the footprint push harder on the side that is deeper, which is what rights a hull that has been tipped over and what makes it pitch as a wave rolls under it. `columns = 1` is a single central column: it floats, but it cannot right itself. ## Density sets the waterline `density` is relative to water: the lower it is, the higher the hull rides, and a body denser than water sinks. `buoyancy.restingY(waterY, height, density)` gives the height at which a hull of that density displaces its own weight. A hull settles somewhat deeper than that height, and how much deeper moves with how long the frames are — a force applied from a script acts over a frame, while gravity is integrated by the physics step, so the two balance a little below where the arithmetic puts them. Densities up to about a half float on every frame time measured; above that a hull rides progressively lower and sinks outright on a long frame. Trim the number until the waterline sits where it should. The body's own mass comes from its rigid body, whether `Physics.mass` sets it or the solver derives it from the collider volume, and the buoyant force is scaled against it, so a hull floats at the same waterline whatever mass it was authored with — mass changes how briskly it gets there, not where it ends up. `mass` and `gravityScale` may be passed to `attach` / `configure` for a body whose rigid body says something else. ## Damping is measured against the hull's own spring Buoyancy is a spring: push a hull under and the water pushes back in proportion to how far, so it has a natural frequency of its own, set by how deep it has to sink to displace its own weight. `damping` is a fraction of the damping that would return it to its waterline without a single overshoot — 1 is that critical value, lower values let it bob on the way, 0 bobs indefinitely. Reading it as a ratio rather than an absolute rate is what keeps a dinghy and a barge equally steady at the same setting. It is applied per column against that column's own velocity, so it steadies rolling as well as heaving; a body spinning about its own vertical axis moves every column but their forces cancel in pairs, so `angularDamping` damps the hull's turning on top of it. That one is handed to the rigid body as its own angular damping rate for as long as the hull is in the water, so the physics step takes the spin down itself and can never overshoot the rest it is bringing the body to. Both scale with how much of the hull is actually in the water, so a body lifted clear of the sea falls freely. ## The hull's box The hull is measured from what the entity draws, in the entity's own space, so it stays the same box however the body is turned. Where that box sits comes with it, so a model drawn off its own pivot — a keel-pivoted boat, an offset child mesh — displaces water where it draws and carries its columns over that footprint. What an entity draws arrives over the frames after it is spawned, so the measurement is retried until it lands and a body keeps its place while it waits. `sizeX`/`sizeY`/`sizeZ` name a box explicitly — for a body whose visible mesh is not what displaces water, or one that draws nothing at all. That box is centred on the entity's origin, and it belongs at the size of the body that is actually there: the water pushes on the rigid body's own inertia, so a footprint much wider than the collider rocks a hull the body has no weight to steady. ## What it reads The water height comes from `ocean:sampleHeight`, the same wave field the surface renders, so a hull rides the waves that are on screen. With no ocean in the scene there is nothing to float on and the body is left to the physics world; pass `ocean` to pick one when a scene has several.
▲ 0↑ born
▣
module · born here
❒asset
# ocean The oceans package's substrate. One `Ocean.create` call brings up a live sea: the GPU wave simulation, the surface mesh that renders it, the material bound to the simulation's output, and the coarse readback gameplay queries. The simulation is spectral and runs in cascades — independent wave fields over different patch sizes, each band-limited to its own range of wavelengths, summed into one surface. That is what gives detail at every scale (swell, wind waves, chop) while keeping each transform small. The surface is one mesh whose vertices a compute pass rewrites every frame, so no wave data crosses to the CPU for rendering. It is graded radially and follows the viewer, snapped to a wave-patch multiple so the sea stays put in the world while the mesh slides under it. `sampleHeight` / `sampleDisplacement` answer where the water is, from a small grid read back off the GPU a few times a second — enough to float a boat on the same waves that are drawn without stalling a frame on a round-trip. Both invert the horizontal wave displacement, so they return the surface point that actually ended up above the position you asked about. Use the `Ocean` component to place a sea in a scene; use this module directly to drive one from code.
▲ 0↑ born
▣
module · born here
❒asset
# oceanClipmap Builds the ocean's geometry: concentric rings that double in extent and halve in density outward, the stitch strips that remove the T-junctions between them, and the skirt that carries the water out to the horizon. Returns the vertex and index data as Luau buffers, ready to upload straight into the compute buffers `ocean_displace` rewrites each frame.
▲ 0↑ born
▣
module · born here
❒asset
# oceanFloor The ground the water is seen through. It spans the same extent as the clipmap, follows the viewer the way the surface does, and is shaded by `~.shaders.oceanFloor`. One noise field is the whole terrain: scaled it is the relief the seabed's vertex stage raises the grid into, and read again unscaled it picks between sand and rock. Over both moves the sun's caustic web, displaced by the horizontal displacement of the water directly above it. The seabed applies no absorption of its own. The water column belongs to the surface shader, which knows the real distance light travelled through water; attenuating here as well would darken the ground twice. Its three tiling maps are grown once by `ocean_floor_texture` — the same compute-texture path the cascade maps take — so the seabed carries no binary asset and no content reference that could fail to resolve. ```luau local Floor = require("@builtin::systems.oceans.modules.oceanFloor") local floor = Floor.create({ config = cfg, -- a resolved ocean config extent = 12800, -- the clipmap's own extent, metres prefix = "ocean1_floor_", name = "Seabed", }) floor:bindWave(cascadeDisplacementTexture, cascadeTile) floor:step(elapsed, viewerX, viewerZ) floor:destroy() ``` `Ocean` creates and drives one of these itself whenever the sea state's `floor` block is enabled, so a scene that uses the `Ocean` component gets a seabed without naming this module at all.
▲ 0↑ born
▣
module · born here
❒asset
# oceanPresets Named sea states for the oceans package — `calmClear`, `darkChop`, `darkRolling`, `heavySwell`, `whitecapped`, `murkyGrey`, `paleSwell`, `glassyDark` and `gale`. Each name says what the water does, so a sea can be picked off the list without reading its values. A state is an `OceanConfig` fragment applied underneath whatever the caller passes to `Ocean.create`, so any single field can be overridden without giving up the rest. The states are laid out along the Beaufort scale, because wind speed is what determines wave height in a spectral ocean — the remaining fields shape character: how long the swell runs, how readily crests break, and what colour the water body is. `materialProperties` returns the appearance half of a preset, ready to apply to an ocean material.
▲ 0↑ born
▣
module · born here
❒asset
# oceanQuality The four quality tiers: how finely the sea is resolved, as distinct from what sea it is. A tier owns the FFT grid per cascade, the clipmap's ring density, the swell band count, the whitecap field's resolution and whether screen-space reflections run. It never touches wind, spectrum shape or colour — those come from the preset, so the same named sea looks like itself on any machine.
▲ 0↑ born
▣
module · born here
❒asset
# oceanScene The seam between the `oceanScene` render feature, which captures the colour and depth behind the water, and the ocean materials that sample them. `renderer.feature.create` hands back a handle rather than the feature module, so a shared module is how the half that fills the textures reaches the half that reads them.
▲ 0↑ born
·
computeshader · born here
❒asset
# ocean_copy Copies the ocean's rendered surface into the scene target. ## Bindings | name | kind | |------|------| | `scene_depth` | `texture_depth` | | `scene_color` | `texture2d` | | `depth_out` | `storage2d (r32f)` | | `color_out` | `storage2d (rgba16f)` |
▲ 0↑ born
·
data · born here
❒asset
# oceanCalmClear The "Calm Clear" sea state, offered as `calmClear` in the ocean's preset list (slot 1 in the calm-to-gale progression). The lightest wind in the catalogue, at 4 m/s, over the brightest and clearest water it holds, above a shallow shoal. A complete `oceanPreset` configuration: spectrum and swell, colours, fresnel, all three foam layers, the seabed and its caustics, subsurface scattering, screen-space reflections and the sun sparkle.
▲ 0↑ born
·
data · born here
❒asset
# oceanDarkChop The "Dark Chop" sea state, offered as `darkChop` in the ocean's preset list (slot 2 in the calm-to-gale progression). A brisk wind chopping the darkest water in the catalogue, with almost no swell running underneath it. A complete `oceanPreset` configuration: spectrum and swell, colours, fresnel, all three foam layers, the seabed and its caustics, subsurface scattering, screen-space reflections and the sun sparkle.
▲ 0↑ born
·
data · born here
❒asset
# oceanDarkRolling The "Dark Rolling" sea state, offered as `darkRolling` in the ocean's preset list (slot 3 in the calm-to-gale progression). Dark water rolling under a long swell, on the least choppy surface in the catalogue. A complete `oceanPreset` configuration: spectrum and swell, colours, fresnel, all three foam layers, the seabed and its caustics, subsurface scattering, screen-space reflections and the sun sparkle.
▲ 0↑ born
·
data · born here
❒asset
# oceanGale The "Gale" sea state, offered as `gale` in the ocean's preset list (slot 9 in the calm-to-gale progression). The strongest wind in the catalogue, at 38 m/s, raising a fully wind-driven sea under the heaviest surface foam. A complete `oceanPreset` configuration: spectrum and swell, colours, fresnel, all three foam layers, the seabed and its caustics, subsurface scattering, screen-space reflections and the sun sparkle.
▲ 0↑ born
·
data · born here
❒asset
# oceanGlassyDark The "Glassy Dark" sea state, offered as `glassyDark` in the ocean's preset list (slot 8 in the calm-to-gale progression). No swell at all and the highest standing-wave ratio in the catalogue: a still, strongly refracting surface over near-black water. A complete `oceanPreset` configuration: spectrum and swell, colours, fresnel, all three foam layers, the seabed and its caustics, subsurface scattering, screen-space reflections and the sun sparkle.
▲ 0↑ born
·
data · born here
❒asset
# oceanHeavySwell The "Heavy Swell" sea state, offered as `heavySwell` in the ocean's preset list (slot 4 in the calm-to-gale progression). The largest and longest swell in the catalogue, running through mid teal-green water. A complete `oceanPreset` configuration: spectrum and swell, colours, fresnel, all three foam layers, the seabed and its caustics, subsurface scattering, screen-space reflections and the sun sparkle.
▲ 0↑ born
·
data · born here
❒asset
# oceanMurkyGrey The "Murky Grey" sea state, offered as `murkyGrey` in the ocean's preset list (slot 6 in the calm-to-gale progression). Grey, murky water whose reflections fall away close to the eye and which carries no glitter at all. A complete `oceanPreset` configuration: spectrum and swell, colours, fresnel, all three foam layers, the seabed and its caustics, subsurface scattering, screen-space reflections and the sun sparkle.
▲ 0↑ born
·
data · born here
❒asset
# oceanPaleSwell The "Pale Swell" sea state, offered as `paleSwell` in the ocean's preset list (slot 7 in the calm-to-gale progression). A big swell under one of the lightest winds in the catalogue, through the palest blue subsurface transmission it holds. A complete `oceanPreset` configuration: spectrum and swell, colours, fresnel, all three foam layers, the seabed and its caustics, subsurface scattering, screen-space reflections and the sun sparkle.
▲ 0↑ born
·
datatype · born here
❒asset
# oceanPreset A complete sea state for the ocean system: everything `ocean.create` needs to stand up a specific sea, from the wave spectrum to the seabed under it. Each `.data` instance is one named preset offered in the ocean's preset list. ## Fields - `key` — the name the preset answers to in `ocean.create({ preset = ... })` and the presets API. Stable; world content refers to it. - `label` — the human-readable name shown in pickers. - `order` — the preset's slot in the calm-to-gale progression the list is offered in (1 = flattest). - `seed` — the spectrum's random seed. - `waves` — the FFT spectrum: wind speed and direction, choppiness, gravity, JONSWAP gamma, spectral sharpness, standing-wave ratio, animation speed, and a global amplitude on top. - `cascades` — the three simulation bands: per-band enable, amplitude and world-space tile scale in metres. - `gerstner` — the analytic long swell layered under the FFT bands: wavelength and its spread, amplitude, wave count, directional spread. - `clipmap` — the surface mesh: base tile size in metres and ring count. - `color` — the water column: absorption rates authored as a hex triple (decoded to per-channel 1/m extinction), transmission and body colours, and the depth assumed where nothing stands behind the water. - `fresnel` — the mirror: distance fade start/power, index-of-refraction ratio, normal strength, screen-space refraction strength. - `foam` — the three layers: `surface` lace, `waves` whitecaps (with persistence, optional `breaking` window and `placement` mask), and the `shoreline` wash. - `floor` — the procedural seabed and its `caustics` web. - `shore` — how deep the bottom starts taking the waves (`shoalDepth`, metres); omitted, the ocean's default shoaling band applies. - `sss` — subsurface scattering through backlit crests. - `ssr` — screen-space reflections. - `sparkle` — the sun's specular glint. Colour-valued fields are authored as `#rrggbb` strings and decoded to linear RGB by `Presets.color` on the way to the GPU — including `absorptionColor`, whose channels are extinction rates rather than a colour.
▲ 0↑ born
·
data · born here
❒asset
# oceanWhitecapped The "Whitecapped" sea state, offered as `whitecapped` in the ocean's preset list (slot 5 in the calm-to-gale progression). A hard wind raising the steepest crests in the catalogue, whose foam is the heaviest and the slowest to fade. A complete `oceanPreset` configuration: spectrum and swell, colours, fresnel, all three foam layers, the seabed and its caustics, subsurface scattering, screen-space reflections and the sun sparkle.
▲ 0↑ born
·
renderfeature · born here
❒asset
# oceanScene Copies the colour and depth of everything drawn behind the water into textures the ocean surface material can sample. A surface shader cannot bind the frame's own depth buffer or colour target while drawing into them, and the water needs both: depth gives the length of the water column, and colour is what refraction bends and what screen-space reflections march through. Both copies run at `afterOpaque` and are published through `~.modules.oceanScene`, which is where the ocean materials pick them up.
▲ 0↑ born
✦
shader · born here
❒asset
# oceanFloor The seabed under the spectral ocean: sand where the ground has settled, rock where it has not, lit through the water by a moving web of focused sunlight. One noise field decides the whole terrain. Scaled, it is the relief the vertex stage raises the mesh into and the normal that shades it; unscaled, the same field picks between sand and rock — so rock outcrops stand on the high ground and sand fills the hollows, because the two are readings of one surface rather than independent patterns laid over each other. Both ground maps carry albedo in `rgb` and height in `a`. The height is differenced into a detail normal that rides on the terrain's own slope as a gradient, so relief accumulates rather than one normal replacing the other. The caustics are two layers of the caustics map scrolling against each other at slightly different scales and in opposite directions, displaced by the horizontal displacement of the water directly overhead — which is what makes the web ripple with the surface instead of sliding across the ground as one rigid sheet. Each colour channel is read at its own small offset, giving the chromatic fringe focused light through a dispersing medium has. The seabed applies NO absorption of its own. The water column above it belongs to the water surface, which knows the real distance the light travelled through water; attenuating here as well would darken the ground twice. The mesh follows the viewer, so `floor_center` carries where it currently sits and every terrain and texture lookup is made in world space. That is what keeps the ground still while the mesh slides beneath it.
▲ 0↑ born
✦
shader · born here
❒asset
# oceanSurface The water surface of the spectral ocean. Geometry arrives already displaced by `ocean_displace`, so this shader only decides what the surface looks like: the normal summed across cascades as gradients (which is what adds when wave fields overlap), a water column shaded by per-channel Beer–Lambert extinction over the distance to whatever lies behind it, Fresnel reflection from the scene's own reflection probes, subsurface scattering through backlit crests lit by the scene's directional light, a sharp sun-glitter lobe, and foam from the simulation's own breaking-wave mask plus a shoreline band. Opacity is the extinction rather than an authored constant, so colour and transparency stay consistent from a turquoise shallow to a near-black deep. Works from below as well as above.
▲ 0↑ born
◇
component · born here
❒asset
# StreamingSource The entity world content streams around. Put it on whatever should keep the world resident near it — the player, a chase camera, a vehicle. Content the `spatialStreaming` store holds becomes resident inside `loadRadius` of the nearest source and is released past `unloadRadius`. Past that, out to `proxyRadius`, a released cell is drawn as whatever stand-in it was given — so the far field is a silhouette rather than a hole. ```lua entity.find("Player").component.add("StreamingSource", { cellSize = 64, loadRadius = 120, unloadRadius = 180, budget = 32, }) ``` | Field | Meaning | |---|---| | `cellSize` | Edge length of a streaming cell in world units. Cells are square columns on the ground plane, so a tower and its foundation stream together. | | `loadRadius` | Distance within which a cell is made resident, measured to the cell's nearest edge. | | `unloadRadius` | Distance past which a resident cell is released. Must exceed `loadRadius`; the band between them is what keeps a source standing on a boundary from thrashing. | | `proxyRadius` | Distance within which this source sees a released cell's proxy. 0 follows `unloadRadius`, so the far field starts where the content stops however wide this source streams; a number of its own must exceed `unloadRadius`, since inside that the content itself is standing. | | `proxyUnloadRadius` | Distance past which the proxy is dropped too. 0 follows `proxyRadius`; a number of its own must exceed it, for the same reason `unloadRadius` must exceed `loadRadius`. | | `budget` | Entity records one frame may spend loading and releasing. A group is atomic, so a group larger than this still moves in one piece. | Several sources are fine — each contributes its position and cells stream around the closest one. They share a single step per frame, so the budget is spent once however many sources there are. The component contributes a position and the per-frame step. The content itself reaches the store through `@builtin::systems.spatialStreaming.spatialStreaming`: `capture` hands it live entities, `add` registers records for content that was never spawned. Streaming runs in edit mode as well as in play, so an author sees the same residency a player will.
▲ 0↑ born
▣
module · born here
❒asset
# spatialStreaming The store and the residency rule behind the `spatialStreaming` package. ## The rule Three pure functions carry the whole policy, so it can be read and tested without a scene: - `cellKeyAt(x, z, cellSize)` — the cell a point stands in. - `cellDistance(key, cellSize, x, z)` — distance to the cell's nearest **edge**, zero inside it. Measuring to the centre would put a cell's near corner well inside the load radius while the cell read as far, so a source walking a boundary would watch the ground it stands on get released. - `decide(distance, loadRadius, unloadRadius)` — `"resident"`, `"released"`, or `"hold"`. The hold band between the radii is what stops a boundary from thrashing. - `proxyDecide(distance, proxyRadius, proxyUnloadRadius)` — the same rule over the far-field radii, for the stand-in a released cell is drawn as. Each source runs that verdict with its own radii — its own, else its layer's, else the configuration's. A cell is resident while any source says resident and released once every source says released. A cell the budget stopped mid-move is the one case the band does not hold: it finishes the transition it was making, because half a cell is not a state a source can idle in. ## The far field Past the unload radius a cell can still be seen, so a cell carries an optional **proxy**: its own records, which makes a merged mesh, a billboard impostor and a bounding silhouette all expressible. `setProxy` / `clearProxy` give and take one; `deriveProxies` builds the silhouette case from what the store already holds, one box over a cell or one over each group. A record is measured as a box of its own `scale` placed through the whole of its parent's transform — scale, then rotation, then position — so a group standing rotated and scaled is enclosed where it actually stands. A mesh that is not one unit at scale 1 is measured as the unit box it scales from, which is what `padding` covers. The proxy stands on its own pair of radii, `proxyRadius` / `proxyUnloadRadius`, measured from the same cell-edge distance. Left alone the pair **follows the content**: the far field starts a fixed multiple past `unloadRadius` and carries its own band, so a world that streams over a wide radius gets a far field past it without naming one. A number pins it instead, and a pin is measured — a proxy radius inside the unload radius names ground the content is standing on, so it is refused where it is authored. Passing 0 hands the pin back to the derivation. Configuration, layer and source each follow that rule: a scope that names an unload radius of its own gets a far field measured from it. The exchange between the two representations is ordered. A cell's proxy is raised **before** the first of its groups is released, and dropped once the cell's own content has landed — so a cell in the proxy band is never represented by nothing, and an opaque silhouette is never left standing over the content it was covering for. A proxy whose entities left the scene by some other path is taken down whole on the next step and raised again from its records. ## The store `capture(targets, opts)` serializes a root and its descendants into a group and files it in the cell the root stands in, leaving the entities standing. `add` registers records for content that was never spawned. Either can write to a file with `store = "vfs"`, in which case a released cell keeps only its manifest — key, layer, byte count, entity count, path — in memory. A group is released by re-serializing it from the live subtree first, so what comes back is the state it was released in rather than the state it was captured in — including the cell it belongs to, which is re-derived from where its root now stands. A group whose root gameplay destroyed is dropped: the cell forgets it rather than resurrecting deleted content on the next visit, and the slot is closed so nothing later visits an index that names no content. ## The step `tick(dt)` spends at most `budget` entity records per frame, releases before it loads (so the peak stays at what the radius bought rather than at the sum of the cells on both sides of a boundary), and takes the nearest pending cell first. One step belongs to the frame rather than to the caller. A second `tick` inside the same engine frame reports `stepped = false` and changes nothing, so a world carrying several `StreamingSource` components spends one budget per frame rather than one per source. Parenting waits for the entities. `entity.instantiate` mints ids immediately but the entities land over the following drains, a bounded number per frame — so a group's links are applied at the top of each step and the ones whose child or parent has yet to land are retried on the next. The cell counts as pending until they are all applied, and a group with links outstanding is not released: re-serializing it while its children were still queued would store the root without them. ## Reporting `stats()` gives the residency picture: `cellsResident` / `cellsPending`, `entitiesResident` / `entitiesTotal`, and `bytesResident` / `bytesStored` — the encoded size of the groups standing now against the size of everything the store knows about. The far field is priced apart from the content it stands in for: `proxiesTotal` / `proxiesResident`, `entitiesProxy` and `bytesProxy` are what it costs right now, against the `entitiesTotal` / `bytesStored` it covers. `cells()` is the per-cell view (`proxy`, `proxyResident`), `proxies()` the per-cell view of the stand-ins — `records` is what one is made of, `entities` how much of it is standing — and `residentIds()` / `proxyIds()` the entity ids each half currently has standing.
▲ 0↑ born
·
testsuite · born here
❒asset
# spatialStreaming test suite Covers the `spatialStreaming` package on both halves of what it is. **The rule, as pure functions** — `cellKeyAt` / `cellCoords` / `cellBounds` agreeing on one square, `cellDistance` running to the nearest **edge** (the case that separates it from a centre-based measure, which would release the ground a source is standing on the corner of), `decide` returning resident / released / hold across the hysteresis band, `proxyDecide` doing the same over the far-field radii, and the refusal of a radius pair with no band between it. **The store, against a live scene** — a capture taking roots, counting the branches it was handed and refusing a root it already holds; content past the unload radius leaving `entity.findAll` entirely and returning with the tint, position and attribute written while it was resident; a subtree coming back parented; a 201-entity group — more than one frame's drain admits — coming back whole and releasing with every entity it started with; a group whose root gameplay destroyed leaving the store instead of lingering as a slot that names nothing, with the steps that follow completing; a root that moved streaming from where it ended up; a step spending the budget it was given; a transition the budget cut short finishing even inside the hold band; a cell standing while any one source holds it; a disabled layer releasing its own content and nothing else; a file-backed cell reporting zero resident bytes while it is away and reading them back on a visit; and a second `tick` inside one frame changing nothing. **The far field** — a proxy pair left alone following the content radii, so a configure and a setLayer that name only a wide unload radius are accepted and report a far field past it, while a pinned radius inside the unload radius is refused and changes nothing (the layer it was aimed at can still be switched off) and a zero hands a pin back to the derivation; a derived silhouette containing every record it stands for, including a group whose root stands rotated and scaled, where the raw offsets name a place the content is not; a walk over a store whose budget cannot move a cell in one step never taking a cell in the proxy band from covered to uncovered, with both representations exercised; a stand-in coming down once the content it covered has landed, so the two overlap for the handover and no longer; a proxy costing a fraction of the content it stands in for; clearing one taking down what it had standing and its now-empty cell with it; a disabled layer holding no stand-in either; a proxy despawned out from under the store being raised again from its records; and a store that was never given a proxy streaming exactly as it did. Each live case resets the store, restores the world-wide configuration and despawns its own `__stream_*` entities on the way out; the file-backed case writes under `Test.tmpPath` and removes the tree it wrote. The suite leaves the engine as it found it.
▲ 0↑ born
◇
component · born here
❒asset
# IKAim Turns a bone chain until a named axis on its tip points at a target. Where `IKLimb` drives a tip to a position, this drives it to an orientation — it is what aims a weapon barrel, a turret, a sensor, or a spotlight. ```luau turret.component.add("IKAim", { rootBone = "base", tipBone = "barrel", target = enemyRef, axisZ = 1 }) ``` ## Fields - `role` or `rootBone` + `tipBone` — which chain turns. - `target` — an `EntityRef`. - `axisX` / `axisY` / `axisZ` — the tip's local aiming direction, default `+Z`. A barrel modelled down `+Y` sets `axisY = 1` and `axisZ = 0`. - `weight` — blends against the incoming animation; 0 is an exact pass-through. - `iterations` — aiming is iterative because turning a bone also moves the tip, which changes the direction to the target. ## Checking it ```luau turret.component.get("IKAim"):aimState() -- { ready, status, boneCount, aimErrorDegrees, weight, error } ``` `aimErrorDegrees` is the angle still between the tip's axis and the target. If it stays large, check the axis before anything else.
▲ 0↑ born
◇
component · born here
❒asset
# IKChain Solves an arbitrary bone chain so its tip reaches a target. It names bones directly instead of resolving canonical humanoid roles, so it works on any rig at all — a crane boom, a robot arm, a tentacle, a lamp, a humanoid limb. ```luau crane.component.add("IKChain", { rootBone = "boom_base", tipBone = "boom_tip", target = hookRef, solver = "ccd", }) ``` ## Fields - `rootBone`, `tipBone` — the chain's ends, named as the rig names them. The tip must be a descendant of the root; the bones between come from the rig's own parent links. Use `ik.bones` to see what the rig actually calls things. - `target` — an `EntityRef`. Bones are strings, targets are refs. - `solver` — `"ccd"`, `"fabrik"`, or `"twoBone"`. CCD holds joint limits well on short runs; FABRIK converges faster on long floppy ones. - `weight` — blends against the incoming animation. 0 is an exact pass-through and skips the solve entirely. - `iterations`, `tolerance` — the iterative solvers' budget and stopping error. ## Checking it ```luau crane.component.get("IKChain"):chainState() -- { ready, status, boneCount, reach, distanceToTarget, solver, weight, error } ``` `status` says in words what it is doing when `ready` is false — usually a body whose rig has not arrived yet, which clears on its own unless the engine is paused. `distanceToTarget` is the residual after solving, not the distance to compare against `reach`; for that, use `ik.reach`.
▲ 0↑ born
◇
component · born here
❒asset
# IKLimb Drives a two-bone limb — an arm or a leg — so its tip lands on a target. Exact and single-pass: the law of cosines answers outright, so there is nothing to converge. ```luau hero.component.add("IKLimb", { role = "rightarm", target = doorknobRef, pole = elbowHintRef }) ``` ## Fields - `role` — `leftarm`, `rightarm`, `leftleg`, or `rightleg`, resolved through the body's canonical role map so one setup works on any humanoid mesh. The vocabulary is not the anatomical word: an upper arm is `leftarm` and the forearm is `leftforearm`, while an upper leg is `leftupleg` and the shin is `leftleg`. - `rootBone`, `tipBone` — explicit bones instead, for a rig with no role map. Only the ends are named; the joint between comes from the rig's parent links. - `target` — an `EntityRef`. - `pole` — an `EntityRef` steering which way the joint bends. Put one behind the elbow or in front of the knee. Without a pole the bend plane the incoming animation produced is preserved, which is stable but not always the plane you want. - `weight` — blends against the incoming animation; 0 is an exact pass-through. ## Checking it ```luau hero.component.get("IKLimb"):limbState() -- { ready, status, boneCount, reach, distanceToTarget, hasPole, weight, error } ``` A limb reaches as far as its bones are long. A target beyond that has no solution — the arm extends straight toward it and stops, which looks exactly like a broken solver. `ik.reach` answers whether a target is inside the band.
▲ 0↑ born
◇
component · born here
❒asset
# IKLookAt Turns a character's spine, neck, and head toward a target so it watches something as it moves. ```luau guard.component.add("IKLookAt", { target = playerRef, clampAngle = 80 }) ``` ## Fields - `target` — an `EntityRef`. Nil leaves the pose alone. - `rootBone`, `tipBone` — an explicit chain, for a rig with no canonical spine roles. Absent, the spine-to-head run is resolved from roles. - `axisX` / `axisY` / `axisZ` — the head's local forward, default `+Z`. Naming the wrong axis is the classic way to get a mangled pose: the solver faithfully points the axis you named, however strange the result. - `bodyWeight` — the share of the turn taken by the bones below the head. 0 turns the head alone; higher brings the torso round with it. - `clampAngle` — the largest total turn, in degrees. This is what stops a character wringing its neck round to follow something behind it. - `weight` — blends against the incoming animation; 0 is an exact pass-through. ## Checking it ```luau guard.component.get("IKLookAt"):lookState() -- { ready, status, boneCount, clamped, clampAngle, bodyWeight, weight, error } ``` `clamped` is the field worth watching: it distinguishes "watching the target" from "turned as far as allowed and still facing away", which look identical in a screenshot.
▲ 0↑ born
◇
component · born here
❒asset
# IKRig Whole-character IK on a humanoid: both legs, both arms, a look-at, and ground adaptation, resolved through the body's canonical roles so one configuration works on any humanoid mesh. ```luau hero.component.add("IKRig", { grounding = true, rightHandTarget = ledgeRef, lookTarget = playerRef }) ``` Everything registers on a single solve pass in a fixed order: grounding, then legs, then spine and head, then arms. Grounding leads because it moves the pelvis, and the pelvis is the root of every limb. ## Fields - `leftFootTarget`, `rightFootTarget`, `leftHandTarget`, `rightHandTarget`, each with a matching `…Weight`. - `lookTarget`, `lookWeight`, `lookClampAngle`, `lookBodyWeight`. - `grounding` — casts a ray under each foot, moves that foot's target to the surface, tilts it to the normal, and drops the pelvis far enough that neither leg over-reaches. - `groundingMaxSlope` — how far a foot may tilt onto a steep face, in degrees. - `groundingFootHeight` — how far the foot's origin sits above its sole. - `groundingHalfLife` — smoothing on every value that comes off a raycast. Without it a ray crossing a step edge pops the foot between two heights. - `groundingRayUp`, `groundingRayDown` — how far above and below the foot to cast. An explicit foot target wins over the grounder. The grounder still counts that foot toward the pelvis drop, so planting one foot on a ledge while the other follows the terrain stays coherent. ## Checking it ```luau hero.component.get("IKRig"):rigState() -- { ready, status, pelvisOffset, limbs, look, error } ``` `limbs` carries each resolved limb's weight, residual distance, ground contact and lift. `pelvisOffset` is the current drop — roughly zero on flat ground whether or not grounding is doing anything, so read the per-foot `grounded` flags rather than the offset alone. Climbing is a character controller's job. Grounding lowers the pelvis but never raises it, so a tall step asks a leg to stretch past its limit and the solve stops short.
▲ 0↑ born
▣
module · born here
❒asset
# ik The inverse kinematics library behind the `IKChain`, `IKLimb`, `IKLookAt`, `IKAim`, and `IKRig` components. Require this module when you are driving a chain from a script, or writing a solver of your own; the components are the normal way in. ## Exports - `chain` — resolve a root-to-tip bone run from a rig, run forward kinematics over it, and move it between pose buffers. Everything else operates on a chain. - `solvers` — `twoBone`, `ccd`, `fabrik`, `aim`, `lookAt`, plus `byName` and `names` for looking one up from a component field. The five are also lifted onto this module directly. - `limits` — cone and hinge rotation limits, measured from a bone's rest pose. - `grounding` — foot probes, slope-clamped surface alignment, pelvis drop, and the framerate-independent smoothing that keeps a foot from popping at a step edge. - `bind` — the component-side plumbing: find a body, get an IK node onto the graph driving it, and convert between world and model space. - `ikNode`, `basePose` — the animation nodes IK runs through. ## Usage ```luau local ik = require("systems.ik.ik") local rig = ik.bind.rigOf(bodyId) local c = ik.chain.fromRole(rig.bones, rig.roles, "rightarm") ik.chain.forward(c) ik.twoBone.solve(c, { target = modelSpaceTarget, pole = elbowHint, weight = 1 }) ``` A chain solves in the rig's own model space. `bind.worldToModel` and `bind.modelToWorld` convert a world-space point into and out of it, scale included — a target handed to a solver in world space will be wrong by whatever the body is scaled by. ## Notes - `chain.forward` runs forward kinematics over the chain's own bones; `chain.forwardBase` runs it over the bones above the chain's root, which is what puts an arm at its shoulder rather than at the character's origin. A solver needs both to have run. - Solvers rewrite `chain.lRot` in place and leave translations alone. Call `chain.forward` after a solve to see the result in `gPos` / `gRot`. - `weight` 0 is an exact pass-through on every solver and short-circuits before any work happens. - Every solver tolerates a target it cannot reach: the chain extends toward it rather than producing a degenerate pose or NaN.
▲ 0↑ born
·
testsuite · born here
❒asset
# ik suite Covers the pure-math core of the `ik` package — chain resolution, the five solvers, and rotation limits. Everything here runs without a rendered frame, a live skeleton, or an entity, so it executes in the offline shard. What this locks in: - **Chain resolution** — root-to-tip ordering, segment lengths and total reach, forward kinematics, and the three ways resolution legitimately fails (tip not a descendant of the root, an unknown bone name, a role the rig does not fill). Also `rotateGlobal` pivoting a bone about its own joint rather than the chain root, which is the property every solver depends on. - **`twoBone`** — exact reach for targets in every direction around the root, graceful extension toward an unreachable one, weight 0 as an exact pass-through, partial weight landing between the incoming pose and the solve, idempotence across repeated solves, and pole targets producing opposite, deterministic bend planes. - **`ccd` and `fabrik`** — convergence, early exit at tolerance, segment lengths preserved through the solve, the root staying anchored, and rotation limits holding while iterating. - **`aim` and `lookAt`** — the tip axis landing on the target direction, the total turn respecting `clampAngle`, and `bodyWeight` actually moving the turn down the chain instead of leaving it all on the head. - **Rotation limits** — clamping outside the range, leaving values inside it untouched, stability under repeated application (a limit that drifted each frame would slowly collapse a pose), and a hinge stripping off-axis swing. Idempotence and repeat-application stability appear throughout because IK runs every frame on the result of the previous frame: a solver that drifts by a fraction of a degree per solve looks correct in a single-frame test and visibly wrong after a second of playback.
▲ 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 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
·
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 a height channel tile, in the engine's 23-word vertex layout, straight into the shared vertex pool the terrain draws from. One thread per node vertex. Positions are world space. Normals and tangents come from a central difference over the field in the same pass, which is why the tile's apron has to hold real data — a normal at a node edge reads a cell that belongs to the neighbouring tile's interior. 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 node coarser than one field tile covers several of them, so it is dispatched once per tile it overlaps and each dispatch writes only the vertices inside that tile's half-open world rect. Every position the stitch and the morph can produce stays inside the node, so the node's tiles cover its vertices exactly once.
▲ 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 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
·
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
▣
module · born here
❒asset
# layeredMaterial Compiles a stack of materials into one WGSL surface shader. ```luau local layeredMaterial = require("@builtin::systems.layeredMaterial.layeredMaterial") ``` | Call | What it does | |---|---| | `read(materialRef)` | Reads a layer's inputs off a material that already exists: `{ material, shader, values, textures, unsupported }`. | | `compile(stack, name?)` | Returns `{ name, wgsl, properties, layers, textureSlots, values, textures, unsupported }` — the two files a `.shader` asset holds, plus what a material wearing it has to be given. | | `check(stack)` | `compile` with the error handed back: `(true, result)` or `(false, message)`. | | `install(stack, opts?)` | Writes the compiled stack out as a real `.shader` asset. `opts.name` names it; `opts.into` places it as it is created. | | `material(stack, opts?)` | Installs, then makes a material wearing the shader and writes every property the stack declares onto it, from the stack where it says one and from the declared default where it does not. `opts.values` is written last, over them. | | `property(index, input)` | The property name a layer's input is declared under — `l<index>_<input>`, counting the bottom layer as 1. | | `inputs()` | The vocabulary: `{ values, textures, coverage, mask, surface }`. | | `limits()` | `{ maxLayers, textureSlots }`. | ## A stack ```luau { name = "worn_barrel", layers = { { material = "@builtin::materials.default" }, { material = "@builtin::materials.gold", blend = 0.6, mask = "@builtin::systems.terrainSystem.demo.textures.rock_diff", maskChannel = "r", -- or a { x, y, z, w } selector maskContrast = 2, instanceLane = 0, instanceComponent = 0, uvScale = { 4, 4 }, uvOffset = { 0, 0 }, }, }, } ``` The bottom layer is first, and the coverage controls belong to a layer laid over it: writing one on the bottom layer raises. A layer names a `material`, or writes its inputs directly as `values` and `textures`, or does both — written values win over the material's, which is how a stack lays a material over another and still says "but rougher here". A layer read off a material takes the maps that material binds; a slot it leaves on a built-in fallback (`default:white`) is a slot it binds nothing to. ## Coverage A layer past the first is revealed by the product of three terms: | Term | Property | |---|---| | Mask texture, one channel of it, shaped by a contrast | `l<i>_mask_texture`, `l<i>_mask_channel`, `l<i>_mask_contrast` | | Per-material amount | `l<i>_coverage` | | Per-instance amount, one lane of the entity's shader data | `l<i>_instance_lane`, `l<i>_instance_component` | `renderer.instanceData.set(entity, lane, amount)` writes that lane, one entity at a time, so one stack over many entities still reveals its top layer by a different amount on each. A layer written without a mask declares neither mask property nor mask slot, so the mask term is absent from the shader rather than neutral in it. ## Bounds `limits().maxLayers` layers, `limits().textureSlots` texture slots between them. Slots are spent on the maps the layers actually bind: a layer of uniform values alone spends none. A stack past the budget is refused, naming what each layer spent. ## Shading The layers are blended as shading inputs and shaded once. A fully covered layer therefore renders as that layer alone, and a half-covered surface as half of each.
▲ 0↑ born
▣
module · born here
❒asset
# materialGraph Compiles a material node graph into the engine's WGSL surface DSL. ```luau local materialGraph = require("@builtin::systems.materialGraph.materialGraph") ``` | Call | What it does | |---|---| | `compile(graph, name?)` | Pure. Returns `{ name, wgsl, properties, nodes, unused }` — the two files a `.shader` asset holds, the nodes emitted, and the nodes the terminal could not reach. | | `check(graph)` | `compile` with the error handed back: `(true, result)` or `(false, message)`. | | `install(graph, opts?)` | Writes the compiled graph out as a real `.shader` asset. `opts.name` names it; `opts.into` places it as it is created, and installing again writes wherever the asset already is. | | `material(graph, opts?)` | Installs, then makes a material wearing the shader. `opts.values` sets the material's own property values — on the first call and on every later one, so calling it again is how a graph is iterated on. | | `nodeTypes()` | Every operation, with the inputs a node of it is written with, which of those it cannot be written without, its result type and a line of prose. | | `surfaceChannels()` | Every channel of the terminal, and the shading-model field it drives. | ## Inputs A port takes any of: | Written as | Means | |---|---| | `0.5` | A float constant. | | `{ 1, 0.5, 0.25 }` | A vector constant, 2 to 4 components. | | `"other"` | The output of the node named `other`. | | `{ node = "other", swizzle = "rgb" }` | Part of that node's output. | | `{ op = "fresnel", power = 3 }` | A node written where it is used. | `swizzle` reads part of whatever the table it is written in resolves to — a node reference, an inline node, or a constant vector. A table carrying both an `op` and a `node` is refused: a port says where its value comes from once. ## Types `float`, `vec2`, `vec3`, `vec4`. A node's type is worked out from its inputs. In an arithmetic node the widest input decides the result and a float input is broadened to it; two inputs of different vector widths are refused, naming the node. At the terminal a float broadens into a colour channel, and a wider value is refused with a note to swizzle it. Every number a graph carries is written into the shader as an `f32`, so one past what an `f32` holds is refused where it is written. ## Operations | Group | Ops | |---|---| | Sources | `uv` `lightmapUv` `affineUv` `worldPosition` `worldNormal` `worldTangent` `viewDirection` `viewPosition` `vertexColor` `time` | | Values | `constant` `property` | | Textures | `sampleTexture` `normalMap` | | Arithmetic | `add` `subtract` `multiply` `divide` `min` `max` `pow` `step` `mix` `clamp` `smoothstep` | | Per-component | `abs` `floor` `ceil` `fract` `sign` `sin` `cos` `sqrt` `exp` `log` `saturate` `oneMinus` `negate` `normalize` | | Vector | `length` `distance` `dot` `cross` `combine` | | Shading | `fresnel` | ## Properties `graph.properties` holds the same declarations a hand-written `properties.yaml` carries: `float`, `range` (with `min` / `max`), `float2`, `float3`, `float4`, `color`, `int`, `bool` and `texture`. A `property` node reads one, and `sampleTexture` / `normalMap` sample a `texture` one through the texture and sampler pair the engine generates for it. `int` and `bool` are converted where they are read, so they compose with everything else. A `texture` declaration becomes a global of its own name in the interface the engine generates, so `s`, `input` and `material` — the names the generated body already stands on — and anything starting with `mg_` are refused as texture names. ## The terminal `graph.surface` names channels of the shading model: `albedo` `normal` `metallic` `roughness` `emissive` `occlusion` `alpha` `alphaCutoff` `ior` `reflectance` `clearcoat` `clearcoatRoughness` `anisotropy` `transmission` `thickness` `attenuationColor` `attenuationDistance` `sheenColor` `sheenRoughness` `iridescence` `iridescenceThickness` `iridescenceIor` `subsurfaceColor` `subsurfaceRadius` `fibreColor` `fibreSpecular` `fibreShift` `fibreRoughness` A channel left unset keeps the engine's own neutral default, so a graph names only the channels it has something to say about.
▲ 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` | `"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()`. | | `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 = "topics/visual-effects" }`). The play's own `playback()` adds its Generators' costs together, and `procgen.frames` and `procgen.onPresent` take the play's entity as they take a Generator's. 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.
▲ 0↑ born
·
procpack · born here
❒asset
# abi The four 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 | ## 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. - 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`. Outputs: `columns` (Record of Columns) and `info` (the kernel, the variant key, the elements, the determinism the shader declares, the specialization). 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. **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) | **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 | `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 ten has one. ## 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. ## 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}`. The op waits across frames; `report` states the names, elements, bytes and frames. Anything that waits on a readback value is a planning step. ## cpu.call `fn` (input) is called with `args` (input). `returns` (param) lists the type name of each result (`Number`, `String`, `Bool`, `Function`, `List`, `Column`, `BufferRef`, or a procgen type) and a call that returns anything else is refused. `ceilingMs` (param) refuses a call that took longer. A device Column reaches the function as a `BufferRef { buffer, type, count, lanes, element }`.
▲ 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
▣
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
·
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. `bvh_pyramid` and `bvh_internal` then fill in the internal bounds, and the `ray` kernel traverses the finished table. `bindings.yaml` states the node layout and the order the kernels run in. 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
backing path · tools/tests.toolbox/list.tool

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.