Log inGet started
·
editorpanel · drop-in viewer
asset⌬ editorpaneleditorPanelprimary: init.luau·part ofpackage edui.package·originates fromworld 07158574-5…

docs.editorPanel

The editor's Docs panel: an index of every `guide` asset the engine can reach, grouped by its identity segment and narrowed by a filter box, beside the open guide rendered as markdown.

by◐lumi·posted 1mo ago
What it does

docs

The editor's Docs panel: an index of every guide asset the engine can reach, grouped by its identity segment and narrowed by a filter box, beside the open guide rendered as markdown.

The index follows the guides the VFS holds. A guide written, copied, removed, renamed or moved anywhere under /zero/source/ (the world's own guides, its packages' and its libraries'), on its own or inside a folder that holds it, is listed as it stands on the panel's next build, with no reopen or reload. The open guide shows its current text, stays open through a move, and gives way to the index when it is removed.

Links

A [text](target) in a guide is a link. Clicking it, or pressing Enter or Space once Tab has brought the keyboard focus to it, goes where it points:

  • another guide, by any spelling open takes ([Bindings](controlsKit.bindings));
  • a heading, by its anchor: #anchor in the same guide, guide#anchor in another ([Slots](save-kit#save-slots)). A heading's anchor is its text in lower case, with punctuation removed and spaces turned into -;
  • a file or an asset, by its VFS path, a path relative to the guide's own folder ([the module](../saveKit.module)), or its name, identity or guid, which opens in a tab of its own;
  • a web address (https://...), which is copied to the clipboard for a browser.

A link that names nothing, or a heading the guide does not hold, puts a note above the guide saying so.

Exports

The panel record carries open(nameOrIdentity), invalidate(), renderMarkdown(md, opts?) and followLink(target, from?). invalidate drops the index so the next build reads the registry again; the panel does that itself on every guide change. open takes any spelling guides.read opens a guide by: the name the index shows, the guide's asset identity (controlsKit.my-guide), the path guides.list lists it under (controlsKit.package/my-guide), its guid, the VFS path of its folder, or a shorter spelling that names one guide. renderMarkdown is the same markdown-to-widgets rendering the panel shows a guide with, for another panel that shows prose (the World panel's body, a markdown file's preview); its links are live, a relative path and a #anchor read against opts.from alone, and a link that ends in a note brings the Docs panel forward to show it. followLink follows a link the way a click does and answers what it reached ("guide", "heading", "file", "web", or nil with the note saying why).

Interface

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

conforms to

zero/source-extract/v2

The Docs panel on edui — the engine's guide library rendered for reading. The index lists every registered `guide` asset grouped by its identity segment (core, topics, types, ...), a search box narrows it, and picking one renders its markdown: headings, paragraphs with inline code / bold / italic / links, bullet and numbered lists, quotes, and fenced code blocks highlighted through the same highlighters the Code panel uses. A link in a guide goes where it points when clicked, or activated with Enter once Tab has brought the keyboard focus to it: another guide by any spelling `open` takes, a heading of this guide or another (`#anchor`, `guide#anchor`), a file or an asset, a path relative to the guide's own folder included. A web address is copied for a browser, and a link that names nothing says so above the guide. The index follows the guides the VFS holds: a guide written, copied, removed, renamed or moved, on its own or inside a folder that holds it, is in the list as it stands on the panel's next build, and the open guide shows its current text, follows a move, and gives way to the index when it is removed. Requiring this module registers it into the edui shell.

canonical(path: string) → string

A VFS path in one spelling, so a path the watcher hands over and one the asset registry states compare equal.

argtypedescription
pathstring

headingIdPrefix( ) → string

The id prefix the open body's headings carry, followed by their anchor.

renderBody( ) → void

Render the open guide's markdown with the link callback this build issued.

openGuide(row: any, opened: boolean?) → boolean

Read a guide's markdown and show it. Answers whether its text was read. `opened` is a person (or a caller on their behalf) opening it: the note a followed link left goes, and another guide than the one shown opens at its top. The open guide read again after a change keeps both.

argtypedescription
rowany
openedboolean?

reconcileOpen(rows: { any }) → void

The open guide against a freshly read index: shown again from its current text under the identity it now carries (the same one, or the one its guid moved to), or closed, leaving the index, when no guide carries it.

argtypedescription
rows{ any }

buildIndex( ) → void

invalidate( ) → void

Drop the guide index so the next build reads the registry again. The panel drops it itself whenever a guide is written, removed or moved.

touchesGuides(path: string, kind: string) → boolean

Whether a write or a removal at `path` changes the index the panel holds: it lands in or on a `.guide` folder, it is the open guide's body, it writes a whole folder (a copy or a move lands one as a single write, the guides inside it included), or it removes a folder holding an indexed guide (a package deleted or renamed). With no index held, the next build reads the registry as it stands.

argtypedescription
pathstring
kindstring

open(nameOrIdentity: string) → boolean

Open a guide by any spelling that names it: the name and group the index shows it under, its asset identity (`controlsKit.my-guide`), the path `guides.list` lists it under (`controlsKit.package/my-guide`), its guid, the VFS path of its folder, or a shorter spelling that names one guide, resolved the way `guides.read` resolves one. The palette and other panels open guides through it.

argtypedescription
nameOrIdentitystringThe guide to open.

rowOf(candidates: { any }) → any

argtypedescription
candidates{ any }

resolveDots(path: string) → string

`path` with its `.` and `..` segments resolved, absolute.

argtypedescription
pathstring

existingPath(path: string, from: string?) → string

The VFS path a link's path names, when something is there: an absolute path as written (`/source/...` read as `/zero/source/...`), a relative one against the folder of the document the link is in.

argtypedescription
pathstring
fromstring?

note(text: string, tone: string) → void

argtypedescription
textstring
tonestring

revealHeading(anchor: string) → boolean

Bring the open guide's heading `anchor` into view once the body holding it has been laid out. Answers whether the open guide holds that heading.

argtypedescription
anchorstring

showElsewhere(target: string, path: string?) → boolean

Open a file or an asset in a dock tab of its own. Answers whether it opened.

argtypedescription
targetstring
pathstring?

follow(target: string, source: string?) → string

Follow `target`, read in the document at `source` (nil for prose read from no file): a relative path and a `#anchor` are read against `source` alone.

argtypedescription
targetstring
sourcestring?

followLink(target: string, from: string?) → string

Follow a link read in a guide or another markdown document: open the guide, heading, file or asset it names, the way a click on the link in the Docs panel does. `#anchor` names a heading of the open guide (or of the guide `from` lies in), `guide#anchor` a heading of another guide; a guide is named by any spelling `open` takes, a path by a VFS path or a path relative to the folder of `from`. A web address is copied for a browser. A link that names nothing, or a heading the guide does not hold, is answered with a note above the guide saying so. body when omitted. or nil when it reached nothing (the note says why).

argtypedescription
targetstringThe link's target, as the markdown wrote it.
fromstring?The VFS path of the document the link is in; the open guide's

examples

docs.followLink("../controls.guide#bindings")

build( ) → any

onClick( ) → void

renderMarkdown(md: string, opts: { from: string? }?) →

Render a markdown document to the widgets the Docs panel shows it with — headings, paragraphs with inline code, bold, italic and links, fenced code, lists and quotes — for another panel that shows prose, the World panel's body among them. Called while a panel builds, its links are live: one clicked is followed the way `followLink` follows it, a relative path and a `#anchor` read against `opts.from` alone, and a link that ends in a note brings the Docs panel forward to show it.

argtypedescription
mdstringThe markdown text.
opts{ from: string? }?`{ from? }`: the VFS path of the document the text was read from.

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
❒
package · pulled in
❒asset
ZeroJS is a JavaScript interpreter for the Zero engine, written in Luau. It parses and executes ES2017-practical JavaScript entirely inside the Luau VM — no native code, no WebAssembly, no external dependency — and marshals values back to Luau transparently. ## Importing ```luau local zerojs = require("~zerojs.api") ``` `~zerojs` is this package's alias, registered from the `name` in its `package.yaml` wherever the package sits. The same spelling therefore works from the world that authors the package and from a world that installed it, at any depth. Every module is reachable the same way — `~zerojs.parser`, `~zerojs.stdlib.promise` — and the package's own modules reference each other with those ids. ## API surface ### Module-level | Symbol | Type | Description | |--------|------|-------------| | `zerojs.eval(src)` | `(string) -> any` | Parse and execute `src` in a fresh one-shot VM. Returns the marshaled JS completion value. Raises a Luau error on a JS exception. | | `zerojs.new(opts?)` | `(table?) -> VM` | Create a persistent VM. `opts.env` seeds additional JS globals from a Luau table. `opts.engine` picks the tier (`"auto"`, `"compiled"`, `"interp"`). `opts.console` is a table of writers by level (`log`, `info`, `debug`, `warn`, `error`): each receives the formatted text of one console call, untagged, and a level without a writer uses `log`'s. With no `opts.console`, the console writes `[zerojs]`-tagged lines to the engine log. | | `zerojs.null` | sentinel | The JS `null` value (distinct from Luau `nil`). | | `zerojs.undefined` | sentinel | The JS `undefined` value, which marshals to Luau `nil` when returned. | | `zerojs.version` | `string` | Version string of this package. | | `zerojs.lastError(vm)` | `(VM) -> table?` | Returns the structured payload `{ name, message, stack }` from the most recent JS exception on `vm`. `nil` if no exception has occurred on that VM. | ### VM instance | Method | Description | |--------|-------------| | `vm:run(src, opts?)` | Execute `src` in this VM's persistent realm. `opts.sourceName` names the source for error messages and stack frames. Returns the marshaled completion value. Raises directing to `runAsync` when the script leaves a timer armed or a coroutine suspended across a frame. | | `vm:runAsync(src, opts?)` | Execute `src`, driving its event loop across engine frames until it settles. Returns a `RunHandle` immediately. `opts.timeoutMs` rejects the run if it outlasts that many milliseconds. Only one `runAsync` may be in flight per VM at a time; a second call while the first is unsettled raises. | | `vm:get(name)` | Read a global from the VM's realm. | | `vm:set(name, value)` | Write a marshaled Luau value as a JS global in the VM's realm. | ### `RunHandle` (returned by `vm:runAsync`) | Field | Description | |-------|-------------| | `done` | `false` until the run reaches a terminal state, then `true`. | | `ok` | Once `done`, `true` for a resolved run and `false` for a rejection or timeout. | | `value` | The marshaled completion value on a resolved run. A run that timed out after its script had already completed with a value (an armed `setInterval`, say, kept the run open) keeps that value here; every other failure leaves it `nil`. | | `error` | The rendered message text on a rejected or timed-out run. | | `lastError` | The structured `{ name, message, stack }` payload for a failed run. | | `thread` | The engine `task.spawn` driver pumping the loop each frame — present only while the run is still pumping, and `nil` once the run settles on its arming frame (a synchronous or microtask-only completion needs no per-frame driver). | | `handle:await(maxFrames?)` | From a yieldable Luau context, poll `done` frame-by-frame until the handle settles, then return it. | A `runAsync` stays pending while the loop has work: an armed `setInterval` keeps it pending exactly as an un-`unref`'d handle keeps Node alive — clear the interval or bound the run with `timeoutMs`. ## Marshaling Luau values passed to `opts.env` or `vm:set` are converted to JS values. JS values returned from `run`/`eval` are converted back to Luau: numbers, strings, and booleans are transparent; arrays become 1-based Luau tables; objects become string-keyed Luau tables; JS functions become callable Luau closures; `undefined` becomes `nil`; `null` becomes `zerojs.null`. ## Step-budget sandbox Each VM runs with a default execution budget of 1,000,000 interpreter steps. A script that exceeds the budget raises a JS `RangeError: step budget exceeded`, which surfaces as a Luau error. The budget resets on each `run`/`eval` call. ## Language scope - Variable declarations: `var` (function-scoped hoisting), `let` and `const` with full TDZ enforcement - Functions: declarations, expressions, arrow functions, closures, rest parameters, default parameters - Classes: declarations and expressions, inheritance via `extends`, `super()` in constructors and methods, getter and setter accessors, instance fields, static fields and methods, computed method keys, named class expressions, TDZ before declaration - Destructuring: object and array patterns in declarations, assignment, and function parameters; nested patterns; rename, default, rest; computed keys in object patterns; elision in array patterns - Spread: array literals, call arguments, `new` arguments, object literals - Template literals (untagged) - Optional chaining: `a?.b`, `a?.[k]`, `f?.(x)`. A `null` or `undefined` base ends the whole chain with `undefined` and skips every later link and argument; parentheses end the chain; the chain is not an assignment, update or `new` target - Nullish coalescing `??` and the logical assignments `??=`, `||=` and `&&=`, which evaluate the right side and write only when the current value calls for it. `??` does not mix with `||` or `&&` without parentheses - Full coercion and equality semantics: `==`/`!=` abstract equality, `===`/`!==` strict equality, `typeof`, `instanceof` - `try`/`catch`/`finally`, `throw` - All loop forms: `for`, `while`, `do-while`, `for-in`, `for-of`; `switch` with fallthrough - `async`/`await` — real event-loop semantics: an async function returns a promise, runs synchronously up to the first `await`, and resumes from a microtask when its awaited operand settles - Property enumerability: `for-in` yields only own enumerable keys; object rest excludes prototype methods ### Asynchrony and the event loop - `Promise` with the full ES surface: `then`/`catch`/`finally`, `resolve`/`reject`, and the combinators `all`, `allSettled`, `race`, and `any` (with `AggregateError` when every input to `any` rejects) - `queueMicrotask` for scheduling a microtask directly - Timers: `setTimeout`/`clearTimeout` and `setInterval`/`clearInterval`, with extra arguments forwarded to the callback - `vm:runAsync(src, opts?)` drives a script's timers, microtasks, and promise settlements across engine frames and returns a `RunHandle` (see the VM table above) - A long synchronous loop under `runAsync` yields at each step-budget boundary and resumes on a later frame instead of raising `RangeError`; the same script under `vm:run` still raises ### Standard library - `Object`: callable coercion plus `keys`, `values`, `entries`, `assign`, `freeze`, `isFrozen`, `create`, `getPrototypeOf`, `defineProperty`, `fromEntries`, `hasOwn`, and the `Object.prototype` methods (`hasOwnProperty`, `toString`, `isPrototypeOf`) - `Function.prototype`: `call`, `apply`, `bind` - Global conversions and predicates: `parseInt`, `parseFloat`, `isNaN`, `isFinite`, `String`, `Number`, `Boolean` - `Array` and `Array.prototype`: construction, `isArray`, `from`, `of`, and the iteration/mutation/query methods (`map`, `filter`, `reduce`, `forEach`, `find`, `some`, `every`, `slice`, `splice`, `concat`, `join`, `indexOf`, `includes`, `sort`, `reverse`, `push`, `pop`, `shift`, `unshift`, `flat`, `fill`, `keys`, `values`, `entries`) - `String.prototype`: `slice`, `substring`, `substr`, `split`, `indexOf`, `includes`, `startsWith`, `endsWith`, `repeat`, `padStart`, `padEnd`, `trim`, `trimStart`, `trimEnd`, `toUpperCase`, `toLowerCase`, `charAt`, `charCodeAt`, `codePointAt`, `at`, `concat`, `replace`, `replaceAll`, `match`, `matchAll`, `search` - `Number`: `isInteger`, `isSafeInteger`, `isFinite`, `isNaN`, `parseFloat`, `parseInt`, `MAX_SAFE_INTEGER`, `MIN_SAFE_INTEGER`, `MAX_VALUE`, `MIN_VALUE`, `EPSILON`, `POSITIVE_INFINITY`, `NEGATIVE_INFINITY`, `NaN`, and `Number.prototype` (`toFixed`, `toString`, `toPrecision`) - `Math`: the constants and functions (`abs`, `floor`, `ceil`, `round`, `trunc`, `sign`, `min`, `max`, `pow`, `sqrt`, `cbrt`, `hypot`, `exp`, `log`, `log2`, `log10`, the trig family, `random`) - `Date`: construction as `new Date()` (now) and `new Date(ms)` (epoch milliseconds), `now` (the wall clock at the moment of the call, never ahead of it and never moving backwards; whole-second readings of the wall clock bound it until a reading crosses a second boundary), `getTime`, `getFullYear`, `getMonth`, `getDate`, `getHours`, `getMinutes`, `getSeconds`, `getMilliseconds`, `getDay`, `toISOString`, `toJSON` - `JSON`: `parse` and `stringify` (indent argument and array replacer whitelist) - `Map` and `Set`: construction from iterables, `get`/`set`/`has`/`delete`/`clear`, `add`, `size`, `forEach`, `keys`, `values`, `entries`, insertion-order iteration - `RegExp`: literals and the constructor, `test`, `exec`, `lastIndex`, `source`, `flags`, `global`, `ignoreCase`, `multiline`, `dotAll`, and the `String` regex methods above - Errors: `Error`, `TypeError`, `RangeError`, `ReferenceError`, `SyntaxError` constructors and inheritance; `Error.prototype.name`, `message` and `toString`, so `String(e)`, `${e}` and `"" + e` read `<name>: <message>` - `console`: `log`, `info`, `debug`, `warn`, `error`, `dir` and `assert`. Values print the way a JavaScript console prints them: strings bare at the top and quoted inside, `{ a: 1, b: [ 1, 2 ] }`, `Map(1) { 'a' => 1 }`, `[Function: f]`, errors as `Error: message`, nested values below depth 2 as `[Object]` and `[Array]`, arrays and collections cut after 100 entries, a self-containing value as `<ref *1> { self: [Circular *1] }`, and a rendering wider than 80 characters across lines. A leading `%s`, `%d`, `%i`, `%f`, `%j`, `%o`, `%O`, `%c` or `%%` formats the arguments after it - `performance.now()` (milliseconds on a monotonic clock since the VM was created) and `performance.timeOrigin` - `globalThis`, `NaN`, `Infinity` ### Out of scope by design - Generators and iterators (`function*`, `yield`) - `Proxy` and `Reflect` - Labeled statements and `with` - Tagged template literals The full divergence list — where ZeroJS behavior differs from a browser engine — lives in the guide's divergence section. ## Test suites Eleven suites ship with the package, in its `tests/` folder, run via `tools.tests.run`: ```luau tools.tests.run({ suite = "zerojs_lexer" }) -- tokenization tools.tests.run({ suite = "zerojs_parser" }) -- AST construction and error recovery tools.tests.run({ suite = "zerojs_semantics" }) -- runtime semantics end-to-end tools.tests.run({ suite = "zerojs_language" }) -- ?., ??, logical assignment, error text, console, Date.now, performance.now tools.tests.run({ suite = "zerojs_stdlib" }) -- the standard-library surface tools.tests.run({ suite = "zerojs_typed_arrays" }) -- typed arrays, ArrayBuffer, DataView tools.tests.run({ suite = "zerojs_regex" }) -- the regex engine and RegExp methods tools.tests.run({ suite = "zerojs_async" }) -- the event loop, promises, and timers tools.tests.run({ suite = "zerojs_modules" }) -- the module graph tools.tests.run({ suite = "zerojs_assettype" }) -- the zJs asset type tools.tests.run({ suite = "zerojs_smoke" }) -- unmodified real-world JS programs ```
▲ 1↓ import
·
assettype · born here
❒asset
# editorPanel A dock tab in the editor, as an asset. A `<name>.editorPanel/` folder carries an `init.luau` that returns the tab's spec: ```luau local edui = require("@builtin::~edui.framework") return { title = "My Tab", -- the tab's label (`label` is accepted too) order = 60, -- sort position in the menu dock = "bottom", -- seeds the FIRST arrangement: left | right | bottom | center menu = "scene", -- which top-bar menu lists it; an unknown name creates that menu startClosed = true, -- registered closed; opened from the menu build = function() -- the tab's body, called again whenever it should change return edui.panel{ body = edui.text{ text = "hello" } } end, } ``` `build` is the only required field. The tab id defaults to the folder stem (`myTab.editorPanel` → `myTab`); an explicit `id` overrides it, and a duplicate id replaces the prior registration. An init may return a LIST of specs when one asset carries several tabs — a spec this registration no longer declares has its tab taken down. The spec is forwarded whole to `edui.shell.addPanel`, so anything the shell accepts (`badge`, `badgeColor`, …) works here; `man edui.shell` is its contract. `build` may return `nil` to mean "nothing changed, keep what is drawn" — the shell rebuilds open tabs on a timer, and a panel that re-publishes an identical tree pays for it. Only the ACTIVE tab of a dock leaf is rebuilt at all. An `init.luau` that fails to load does not break the editor: its tab comes up carrying the failure — the path and the error, selectable — so a panel that will not compile says so where you would look for it. Every instance of this type becomes a tab: the ones in the open world, and the ones the built-in library ships, on the same terms. Adding a tab to a project is creating a new `editorPanel` asset: it comes from the template above and registers its tab at once, wherever under `/source` it was made (`editorPanels/` is the type's conventional folder). Editing the `init.luau` re-registers the tab, deleting the folder removes it. Nothing registers panels by name anywhere. `guides { path = "editor/edui/edui" }` — the guide beside this type, inside the edui package — covers the build model, where panel state lives, and the `edui.*` surface a body draws with. ## In the Inspector Panels: each panel id this file registers (`panelRef:panelIds()`), whether it is open, an **Open** action for it, and **Open** for the source. A file whose init.luau does not load says why.
▲ 0↑ born
·
guide · born here
❒asset
# version-control guide Reference docs surfaced through `guides { path: "core/version-control" }` and `man core/version-control`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# ecs guide Reference docs surfaced through `guides { path: "..." }` and `man ecs`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
▣
module · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
# topics/byte-streams guide Reference docs surfaced through `guides { path: "..." }` and `man topics/byte-streams`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
❒
package · born here
❒asset
# volumetrics Volumetric (3D-texture) rendering, end to end, in one package. Built entirely on the engine's generic `compute.*` GPU primitives — no Rust changes are needed to author new volumetric content. ## Layout | Asset | Identity | Role | |---|---|---| | `Volume.component` | `@builtin::systems.volumetrics.Volume` | Attach a 3D voxel texture to an entity; raymarches it through the entity's transform AABB. | | `VolumetricSky.component` | `@builtin::systems.volumetrics.VolumetricSky` | Singleton procedural cloud layer with TAA. | | `ParticipatingMedium.component` | `@builtin::systems.volumetrics.ParticipatingMedium` | Mark an entity as fog/smoke/dust that attenuates the light passing through it. | | `mediaTransmittance.module` | `@builtin::systems.volumetrics.mediaTransmittance` | Light-space optical-depth map: the registry, the settings, and the basis the map is built in. | | `mediaShadow.renderFeature` | `@builtin::systems.volumetrics.mediaShadow` | Builds the optical-depth map and attenuates each surface by the density in front of it. | | `volume.module` | `@builtin::systems.volumetrics.volume` | Builders (`noise`/`sphere`/`box`/`wrap`) + the `.zvol` header reader + occupancy + brick paging + the per-entity raymarch dispatch. | | `VolumeSequence.component` | `@builtin::systems.volumetrics.VolumeSequence` | Play an open volume sequence through an entity's transform AABB. | | `volumeSequence.module` | `@builtin::systems.volumetrics.volumeSequence` | A run of `.zvol` frames streamed through a fixed ring of resident volumes. | | `volumeDemos.module` | `@builtin::systems.volumetrics.volumeDemos` | Shared pipeline for the showcase scenes (fill + emissive raymarch + composite). | | `volumetricSky.module` | `@builtin::systems.volumetrics.volumetricSky` | Cloud volume + raymarch + TAA + composite orchestration. | | `shaders/*.shader` | `@builtin::systems.volumetrics.shaders.*` | `volume_raymarch`, `volume_pages`, `volume_pack`, `cloud_noise[_v3/_v5]`, `cloud_raymarch[_v3/_v4/_v5]`, `cloud_taa`. | | `importers/vdb_to_zvol.importer` | (auto-discovered) | `.vdb` → dense `.zvol` via the `openvdb` WASM plugin (procedural fallback when the plugin isn't loaded). | | `presets/cumulus_default.preset` | `@builtin::systems.volumetrics.cumulus_default` | Canonical full `VolumetricSky` weather preset. | | `demo/*.scene` | `@builtin::systems.volumetrics.demo.*` | `sdf_solids`, `smoke_plume`, `nebula`, `godrays`, `zvol_viewer`, `sky`. | ## Path convention — fully-qualified builtin identities Every intra-package reference uses the asset's fully-qualified builtin identity, the same convention every other builtin package follows: ```luau local Volume = require("@builtin::systems.volumetrics.volume") local Sky = require("@builtin::systems.volumetrics.volumetricSky") local V = require("@builtin::systems.volumetrics.volumeDemos") -- shader identities, e.g. asset.resolve("@builtin::systems.volumetrics.shaders.volume_raymarch") ``` Qualified identities are caller-independent: they resolve identically from a module, a scene entrypoint, or a component. Caller-relative (`.sibling`) requires are deliberately avoided here because the component runtime caller identity is `@builtin::components.<Name>`, so a `.sibling` require from a component resolves against the wrong root. (An earlier revision of this package used a `~volumetrics` package-root alias for relocatability; that alias is not resolvable on current main, so the package uses qualified identities throughout.) ## Usage ```luau -- Per-entity volume. The component resolves the name through `Volume.get`, -- so a volume paged into a brick pool renders from the pool under the same -- name: local Volume = require("@builtin::systems.volumetrics.volume") local cloud = Volume.noise({ name = "cloud", size = 64, scale = 4 }) cloud:buildSparse({ brickSize = 8 }) cloud:releaseDense() entity(id).component.add("Volume", { volumeName = "cloud", density = 6.0 }) -- Procedural sky: local sky = entity.spawn("sky") sky.component.add("VolumetricSky", {}) preset.apply("cumulus_default", { entity = sky.id, component = "VolumetricSky" }) -- VDB import: drop a `.vdb` into the VFS; the importer writes a `.zvol` -- alongside it. Load it with the zvol_viewer demo scene. -- Animated run: one `.zvol` per frame, played through a bounded ring. local Seq = require("@builtin::systems.volumetrics.volumeSequence") Seq.open({ name = "plume", frames = framePaths, resident = 3, fps = 24 }) entity(id).component.add("VolumeSequence", { sequenceName = "plume", density = 6.0 }) ``` ## Plugin dependency The VDB importer delegates real OpenVDB parsing to the `openvdb` plugin in the builtin library, `@builtin::plugins.openvdb`. Without the plugin the importer falls back to a deterministic procedural torus so the pipeline stays functional.
▲ 0↑ born
▣
module · born here
❒asset
# edui (module) The editor UI framework, built on the CSS-parity `ui.*` surface. `edui` is **editor-only** — gameplay and normal UI author raw `ui.*` widget trees (`{ type, style, props, children }`) and need no framework. `edui` provides the one thing a framework earns its keep for: editor chrome — a reactive app, a dock shell, and chrome primitives (panels, trees, toolbars, inspector rows, context menus, drag-and-drop). It replaces the deprecated `zui` for the editor. ## The reactive core (`edui.app`) `ui.*` dispatches interaction through **string callback ids** — a widget carries `props.onClick = "<id>"`, and the screen's registered env receives `onCallback(id, data)`. Closures can't cross the FFI boundary, so raw `ui.*` makes you manage those ids by hand. An `edui` **app** hides that. During a build it allocates an id per closure and records id→closure; its single `onCallback` looks the id up, calls the closure, and rebuilds: ```lua local edui = require("@builtin::modules.edui") local count = 0 local app = edui.createApp("counter", { layer = editorMask }) app:mount(function(a) return { type = "vertical", children = { { type = "label", props = { text = "count: " .. count } }, { type = "button", props = { text = "+1", onClick = a:cb(function() count += 1 end, "inc") } }, }, } end) ``` - `app:cb(closure, key?)` — allocate a callback id for `closure`. Pass a stable `key` so the id survives rebuilds (required for focus / drag / `ui.widgetState`). - `app:dispatch(id, data)` — what the broadcast calls; routes id→closure and rebuilds. Returns whether this app owned the id. - `app:markDirty()` — request a rebuild (coalesced during an in-progress build). - `app:rebuild()` / `app:mount(fn)` / `app:unmount()` — lifecycle. - `edui.current()` — the app whose builder is running (the primitives use it so you don't thread the app through every call). Builder and handler errors are logged, never thrown — one bad build never wedges the editor.
▲ 0↑ born
·❒asset
# generating-assets-and-content guide Reference docs surfaced through `guides { path: "..." }` and `man generating-assets-and-content`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
# troubleshooting guide Reference docs surfaced through `guides { path: "..." }` and `man core/troubleshooting`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# topics/microphone guide Reference docs surfaced through `guides { path: "..." }` and `man topics/microphone`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# topics/building-a-game guide Reference docs surfaced through `guides { path: "..." }` and `man topics/building-a-game`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# topics/frame-streaming guide Reference docs surfaced through `guides { path: "..." }` and `man topics/frame-streaming`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# components guide Reference docs surfaced through `guides { path: "..." }` and `man components`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# authoring guide Reference docs surfaced through `guides { path: "core/authoring" }` and `man authoring`. The content lives in `guide.md`; this README is just the asset summary. The four rules that make authored content usable by somebody else: a callable is a `typed function`, it says what it is in `--!` directives, a value is a `Field.<kind>` carrying its description, and a metatable-dispatched surface declares its members with `--!members`.
▲ 0↑ born
·
guide · born here
❒asset
# home guide Reference docs surfaced through `guides { path: "core/home" }` and `man core/home`. The content lives in `guide.md`; this README is the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# topics/http-server guide Reference docs surfaced through `guides { path: "..." }` and `man topics/http-server`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# topics/physics guide The engine's own physics, simulated inside its step: structures that bend, show their stress and break (bridges, trusses, cranes and towers built from joints that carry tension and compression and break past a force), side-on 2D games held to a plane (a bridge builder, a platformer, an Angry Birds-like), vehicles, ragdolls, and how things move, collide and deform: rigid bodies (static, dynamic, kinematic) and the colliders that shape them; gravity that changes from place to place (planet gravity a player walks all the way around, orbits, a pull toward an axis or a surface, a zone pulling its own way) and which way is down at any point; soft bodies that squash, wobble, drape and dent (jelly, cushions, balloons that inflate, cloth that folds without passing through itself, a model made soft), including dents and crumples that stay after an impact, a car body that keeps its crash damage and is repaired, and stiff deformable parts that hold their shape (a beam, a chassis rail, a panel) on the FEM solver; soft regions held to rigid bodies; air and water acting on a body inside the step (buoyancy that floats a crate, a raft or a boat at the depth its density gives and tips a hull by where its load sits, water and air drag and a falling body's terminal speed, wind fields that blow and gust over a level or inside a box, the lift, drag and stall of a wing, spoiler, sail, keel, glider or kite with control surfaces a script deflects, a spinning ball's Magnus curve, and a flag or cape that streams and flutters in the wind); bodies and characters going into and out of the water, from a pool, a lake, a river or the sea (a splash at the point and speed it hit the surface, a splash sound, a swim state, a count of what is in the pool) and how much of each stands under and how deep; vehicles on wheels; drivetrains and geared machines (engines, clutches, gearboxes, gear ratios, differentials, shafts, winches) as networks of rotating parts; ragdolls of any rigged character or creature (a death, a knockout, a hit reaction, a crash-test dummy, one limp limb), each bone body taking every `Physics` call; joints, breakable joints and the load each carries (tension, compression, shear, bending: the stress a structure is coloured by); rails that hold a body to a track, a curve or a line (a rollercoaster car round a loop, a train on its line, a zip-line trolley, a door on a curved track, a bead on a wire) with end stops, a motor and friction; raycasts and overlaps; acceleration and g-force probes that read what a point on a body feels at every solver substep; the reads that report what the solver holds for a body and why it is not moving; replays: a stretch of simulation recorded and played back (an instant replay, a crash in slow motion, a kill cam, scrubbing back and forth, reversed, from any camera) while the live simulation keeps its state; snapshots that put the whole simulation back to a moment (a bridge tested and reset, a crash retried, an undo), saved and loaded as bytes; and state digests and determinism: a fingerprint of the whole simulation per step, the first step, body and property where two runs part, and when a replay, a verified run or a native and a browser engine reach the same bits. Surfaced through `guides { path: "topics/physics" }` and `man topics/physics`. The content lives in `guide.md`.
▲ 0↑ born
·
guide · born here
❒asset
# plugin-development guide Reference docs surfaced through `guides { path: "..." }` and `man core/plugin-development`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# scenes guide Reference docs surfaced through `guides { path: "..." }` and `man scenes`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# tool-system guide Reference docs surfaced through `guides { path: "..." }` and `man tool-system`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# topics/screen-coordinates guide Screen coordinates: projecting a world position to the screen (`worldToScreen`), the mouse cursor position and a touch, raycasting from a screen point through the camera (`screenToRay`, `Physics.raycastScreen`), and placing a HUD marker, a name tag, a health bar or an off-screen edge arrow over a 3D object. The scene viewport's physical pixels and the UI's logical points, and `ui.fromViewport` / `ui.toViewport`, the step between them. Surfaced through `guides { path: "topics/screen-coordinates" }` and `man topics/screen-coordinates`. The content lives in `guide.md`.
▲ 0↑ born
·
guide · born here
❒asset
# topics/ui guide Reference docs surfaced through `guides { path: "..." }` and `man topics/ui`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
# topics/text guide Reference docs surfaced through `guides { path: "topics/text" }` and `man topics/text`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
# scenes-as-code guide Reference docs surfaced through `guides { path: "core/scenes-as-code" }`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# development guide Reference docs surfaced through `guides { path: "..." }` and `man core/development`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
Terrain, voxels, spatial streaming and mesh LOD: what a world has resident of its detail right now, and why a chunk, tile, cell or level is not on screen.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
# topics/render-layers guide Reference docs surfaced through `guides { path: "..." }` and `man topics/render-layers`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# worlds guide Reference docs surfaced through `guides { path: "..." }` and `man worlds`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# editor-ui guide Reference docs surfaced through `guides { path: "..." }` and `man editor-ui`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# ik guide Reference docs surfaced through `guides { path: "topics/ik" }` and `man ik`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# readme guide Reference docs surfaced through `guides { path: "..." }` and `man readme`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
# discovering guide Reference docs surfaced through `guides { path: "..." }` and `man discovering`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
# topics/raytracing guide Reference docs surfaced through `guides { path: "topics/raytracing" }` and `man topics/raytracing`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# topics/camera-input guide Reference docs surfaced through `guides { path: "..." }` and `man topics/camera-input`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
# runtime-data guide Reference docs surfaced through `guides { path: "..." }` and `man runtime-data`. The content lives in `guide.md`; this README is just the asset summary.
▲ 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 any water, inside the physics step. Its fields are the hull: how dense it is, how quickly it settles, and what displaces water (its colliders, a stated box or what it draws). | | `modules/buoyancy` | The float: `attach`, `forEntity`, `list`, `clear`, `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
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
# entities guide Reference docs surfaced through `guides { path: "..." }` and `man entities`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
# topics/recording guide Reference docs surfaced through `guides { path: "..." }` and `man topics/recording`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# carried guide Reference docs surfaced through `guides { path: "core/carried" }` and `man core/carried`. The content lives in `guide.md`; this README is the asset summary.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
# topics/values guide Reference docs surfaced through `guides { path: "..." }` and `man topics/values`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# topics/animation guide Reference docs surfaced through `guides { path: "..." }` and `man topics/animation`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
# styling guide Reference docs surfaced through `guides { path: "..." }` and `man styling`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# vfs guide Reference docs surfaced through `guides { path: "..." }` and `man core/vfs`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# collaborating guide How to work with other people's content and with what they send you: rate what you installed (`zeromind.rate`), read what others said of it (the `reviews` of `zeromind.inspect` and `zeromind.search`), comment on and report published content, file issues on it and triage the ones filed on yours (`zeromind.fileIssue`, `zeromind.listIssues`, `zeromind.viewIssue`), send a fix upstream as a pull request (`zm pr create`), review and merge the pull requests sent to your world, answer what others do on your content as the inbox reports it (`zeromind.inbox`, `zeromind.markRead`, `zeromind.inboxSettings`), stay in sync with upstream (`zeromind.updates`, `zeromind.reviewUpdate`, `zm diff <root> upstream`), read text other accounts wrote as untrusted, and what the person's community setting (off, rate, report, contribute, full) lets an agent do without asking. Surfaced through `guides { path: "core/collaborating" }` and `man core/collaborating`; the content lives in `guide.md`.
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
# topics/assemblies guide Objects built from parts: named slots (sockets, mount points, hardpoints) that other parts fill, parts that declare which slots they fit, and an assembly that picks a part for every slot from Luau, swaps one at runtime while the rest keeps its state, explains why a part does not fit, and saves, syncs and undoes what fills each slot. A car with wheel, engine and seat slots, a mech's weapon hardpoints, a modular building, a character's equipment points. Surfaced through `guides { path: "topics/assemblies" }` and `man topics/assemblies`. The content lives in `guide.md`.
▲ 0↑ born
·
guide · born here
❒asset
# topics/external-editor guide Writing the engine's Luau in an external editor (VS Code, Neovim, or any editor running the luau-lsp language server): `lsp.exportDefinitions` writes the engine's API as a Luau definitions file (`zero.d.luau`), a documentation file and a `.luaurc`, the files are copied to the editor's machine, and luau-lsp pointed at them autocompletes the engine's globals, checks each call against its signature, reports a misspelled member, and shows the engine's documentation on hover. Surfaced through `guides { path: "topics/external-editor" }` and `man topics/external-editor`. The content lives in `guide.md`.
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
# topics/bash guide Reference docs surfaced through `guides { path: "..." }`, `man bash` and `man topics/bash`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# topics/paths guide Reference docs surfaced through `guides { path: "..." }` and `man topics/paths`. The content lives in `guide.md`; this README is just the asset summary: curves, roads, tracks, rails, lanes and routes, and moving things along them.
▲ 0↑ born
·
guide · born here
❒asset
# getting-started guide Reference docs surfaced through `guides { path: "..." }` and `man core/getting-started`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# resource model guide The read-first guide for working with meshes, textures, materials, and shaders. Explains the three forms every GPU-backed resource takes (authored asset, CPU-decoded data, live GPU resource), the Luau to CPU to GPU upload path, residency and memory management (load / unload / upload / drop / isResident), what keeps a runtime resource alive and how it is released (`renderer.references`, `renderer.collect`, `renderer.hold` / `renderer.release`, and the collection a root scene load runs), the asset material vs runtime material distinction (`gold.material` vs `renderer.material.create`), the asset graph (material to shader to texture), render textures, and surfaces. Surfaced through `guides { path: "core/resource-model" }`. Content lives in `guide.md`.
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
# multiplayer guide Reference docs surfaced through `guides { path: "..." }` and `man multiplayer`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# topics/localization guide The player's language, and showing a world in it: a world's text in catalogs (`/zero/source/locales/<tag>/<namespace>.json`) read with `i18n.t` and `i18n.text`, plurals, numbers, dates and sizes in the player's language, the pseudo-locale and the catalog checks; which languages the player's platform says they read, `i18n.pick` to choose among the languages a world has text for, following a language change while they play, trying the world under another visitor's languages, and how the engine picks its own locale. Read it with `guides { path: "topics/localization" }` or `man topics/localization`.
▲ 0↑ born
·
guide · born here
❒asset
# topics/rendering guide Reference docs surfaced through `guides { path: "..." }` and `man topics/rendering`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# asset-system guide Reference docs surfaced through `guides { path: "..." }` and `man asset-system`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# subclient guide Reference docs surfaced through `guides { path: "core/subclient" }` and `man subclient`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# topics/render-textures guide Reference docs surfaced through `guides { path: "..." }` and `man topics/render-textures`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
# edui guide Building editor panels: creating the `editorPanel` asset, the spec it returns, how a panel is drawn and redrawn, the whole widget vocabulary, and how a panel talks to the selection and the dock. The content lives in `guide.md`; this README is the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
# topics/input guide How a world reads its players' hands on keyboard and mouse, a gamepad and a touch screen: controls as assets that carry every device, axis shaping, several live maps, and testing on every device; controllers beyond the standard pad (a steering wheel, separate pedals, a flight stick or joystick, a HOTAS, a button board), read and bound by each axis and button they report; and haptic output: rumble, trigger rumble, and force feedback (constant force, spring, damper, friction) on a wheel or stick, with the capability query that states what each device and platform offers. Surfaced through `guides { path: "topics/input" }` and `man topics/input`. The content lives in `guide.md`.
▲ 0↑ born
▣
module · born here
❒asset
# guides.shared The guide catalogue, and the way a path resolves against it. Every tool in this toolbox reads it, so "which guides are there" has one answer whether it was asked from the shell, from Luau, or through MCP. ## What counts as a guide A `.guide` asset wherever it lives — built into the engine, shipped inside a package, or authored in this world — and an asset TYPE, whose README documents it and which answers under `types/<name>`. Both are documentation, and the documentation search ranks over both, so both belong in the same catalogue. ## One guide, one entry A guide reachable under two spellings is one asset, and the catalogue lists it once. The spelling that names a LOCATION wins the listing (`controlsKit.package/my-guide`, `topics/physics`) and every other spelling becomes an alias: the asset identity the engine states for it (`controlsKit.my-guide`, `@builtin::docs.topics.physics`), the form every other asset in a package is addressed by, and the spelling that names a PACKAGE. All of them keep resolving, so a path written against any of them still opens the guide, and `pathOf` is the one derivation of the listed path, which the documentation search hands back as a hit's `path` too. ## How a path widens `resolve` takes the spelling a caller typed and widens only while the answer stays unique: the exact spelling (a listed path, an alias, the guide's guid, or the VFS path of its folder or its `guide.md`), then the same ignoring case, then any identity ending in `/<wanted>`, then a bare last segment, then a run of name segments, read the way `segments` reads them (split at `/`, `.` and `:`, with `@lib::` and `~` dropped), so `controlsKit/my-guide` reaches `controlsKit.package/my-guide`. The moment a widening matches more than one guide it stops and reports those spellings, because which one was meant is not knowable from what was typed — and answering with one of them would be a guess the caller could not see.
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
# core/observing How to be told when something happens in a running world, instead of running something, waiting, and hoping the timing was right. Covers the `probe` toolbox: attaching an instrument to code that is already running, the three things you can watch (a function call, a line, a value), what a probe reports and why the text is required, and the case that costs the most time — the world reaching a state you did not put it in, where every value you read means something other than you assume. Read it when the question is "when did this happen" or "what was true at that instant". For "why does this look wrong" the guide is `core/troubleshooting`.
▲ 0↑ born
·
guide · born here
❒asset
# engine guide Reference docs surfaced through `guides { path: "..." }` and `man engine`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# topics/audio guide Reference docs surfaced through `guides { path: "..." }` and `man topics/audio`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# topics/cutscenes guide Reference docs surfaced through `guides { path: "topics/cutscenes" }` and `man topics/cutscenes`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# topics/graphics-quality guide Reference docs surfaced through `guides { path: "..." }` and `man topics/graphics-quality`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# performance guide Reference docs surfaced through `guides { path: "..." }` and `man performance`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# scripting-and-tasks guide Reference docs surfaced through `guides { path: "..." }` and `man scripting-and-tasks`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
# content-graph guide Reference docs surfaced through `guides { path: "..." }` and `man content-graph`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# topics/animated-images guide Reference docs surfaced through `guides { path: "..." }` and `man topics/animated-images`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
guide · born here
❒asset
# topics/cameras guide Reference docs surfaced through `guides { path: "..." }` and `man topics/cameras`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# requiring-modules guide Reference docs surfaced through `guides { path: "..." }` and `man requiring-modules`. The content lives in `guide.md`; this README is just the asset summary.
▲ 0↑ born
·
guide · born here
❒asset
# calling-models guide Call an AI language model, a judge model or any other metered offering from your own Luau and get its answer back directly: a reply's text, a verdict, a classification, with no run, job or file behind it. Reference docs surfaced through `guides { path: "core/calling-models" }` and `man calling-models`.
▲ 0↑ born
·
assettype · born here
❒asset
# assetType (asset type) An asset type says what a kind of asset is: the files an asset of the kind is made of, the code that gives it behaviour, the interface a person uses to work with it, and how it is found, pictured and checked. A `<typename>.assetType/` folder declares a type called `<typename>`, and every `<name>.<typename>/` folder is an asset of it. `assetType` is itself a type, so a type is an asset like any other: it is created, versioned, installed and contributed the same way. ## Making a type `asset.create("assetType", "<typename>")` scaffolds a type. The type registers the moment its folder is written, anywhere under `/zero/source/`, and from then on `<name>.<typename>/` folders are its assets. An asset of a type that is not registered yet is not an asset, so make the type first. A type folder is a root: `~.x` in any file inside it names the type's own `x`, wherever the type is installed. Keep the type's whole implementation inside its folder and reach it that way. What a type folder must hold is set by this type's own `type.yaml`, the same way every type's `type.yaml` sets what its assets hold. `asset.validate` on a type reports anything missing, and the scaffold produces a folder that passes. ## What sets your type's schema (`type.yaml`) `type.yaml` is the structural contract every asset of the type is checked against. The keys below are the ones the engine reads; a key not listed here is ignored. | Key | What it controls | |---|---| | `suffix` | The folder suffix of the type's assets. Defaults to `.<typename>`. | | `primary` | The file or files that are the asset's content: what the asset resolves to when it is used. | | `entry_files`, `primary_aliases` | Further files that load as the asset or share its identity. | | `is_container` | Whether the suffix stays in the identities of what the asset holds. | | `is_opaque_walker` | Whether a walk of the tree stops at the asset and registers only its primary. | | `plural_dir`, `classify_exts` | Loose files in a folder of this name, or with these extensions, are assets of the type. | | `is_text_default` | Whether the asset's files are text unless stated otherwise. | | `broadcasts_subassets` | Whether assets nested inside an asset of the type are found by their own short name. | | `root` | `true` makes every asset of the type a root, so `~.x` inside one names that asset's own `x`. | | `contract_ref: { field, category }` | A key of the primary file that names another asset the asset is built on. | | `files: { required, optional }` | The files an asset holds (below). | | `allow_unlisted` | `false` reports every file the type does not declare. | | `aliases_from: [{ file, key }]` | Further names an asset answers to, read from its own files. | | `settings:` | Typed per-asset settings (below). | | `inspector:` | The interface people use for the type and its assets (below). | | `preview:` | How an asset is pictured (below). | | `references:` | Names of other assets the type's files carry (below). | | `indexing:` | What search reads for each asset (below). | | `describe:` | How a missing description is generated (below). | ### The files an asset holds Each entry under `files.required` or `files.optional` is `{ path, kind: file | folder, one_of_group?, content? }`. `*` in a path matches within one segment. Entries that share a `one_of_group` are satisfied by any one of them. `content` lists checks on the file's bytes: | Check | Passes when | |---|---| | `non_empty` | the file has content | | `json_field { key, type?, non_empty?, accept? }` | the JSON holds the key (a dotted path) with that shape | | `yaml_field { key, type?, non_empty?, accept? }` | the same for YAML | | `described { keys?, headings?, message? }` | the file says what the asset is (see "Ask your assets to say what they are") | Each check takes a `message`, which is what the author is told when it fails. ### Settings `settings: { values, schema: { type | instance, export? } }` gives every asset typed settings. `values` names the file an asset's values live in; `schema.type` declares one schema for the whole type, and `schema.instance` names a file each asset declares its own in. `asset.create` writes the resolved values, and a write to the values file reaches `onChange` with what changed. ## The human side (`inspector.luau`) A type is used by people through its Inspector, and without one the type is usable only from code. The Inspector is the type's interface for a person: for a procedural graph it is the graph editor, for a kit it is the tracking view of what the kit holds and what is still missing. Writing it is part of writing the type. `inspector: { type, instance? }` in `type.yaml` names the files. `type` is the interface for the type itself and for its assets; `instance` names a file an asset may ship to add its own. Each returns: ```luau return { sections = { { id = "graph", title = "Graph", build = function(ctx) ... end } }, replace = { contents = { ... } }, -- replace a default section by id order = { "graph", "contents" }, -- section order } ``` `build(ctx)` receives the asset and builds the section. A section's `title`, and the text its rows carry, is a string or an `i18n.text` value read from a catalog (`i18n.text("inspectors.mesh.title")`), which follows the editor's language. `asset.create("assetType", ...)` scaffolds a working `inspector.luau` to start from. `asset.validate` on a type that names no inspector, or names one that does not load, reports it. ## Say what your type is for (`about.yaml`) Every type carries an `about.yaml` that says what the type is for, in three plain-language texts: ```yaml intent: "Sets the typeface of the text in your game: UI, labels, signs and 3D lettering." function: "A font, from a TTF, OTF or WOFF file, made available to all of Zero's text, including 3D text." usage: "You choose it by name for your UI or your text, and the text appears in that typeface." ``` - **`intent`** is what the type is for in someone's game, in the words a game maker uses. Up to 800 bytes. - **`function`** is how it works today: what an asset of it is made of and what the engine does with it. Up to 1500 bytes. - **`usage`** is what a person or an agent does with one. Up to 1200 bytes. Write about the type as a whole, not one asset of it, and say what it does now. Agents read these texts to learn when to reach for the type and how; people searching find the type by them. The scaffold starts each text with `[[describe]]` and a note on what to write; a text that still starts that way counts as not written. `services describe` can write them. They show up in `asset.inspect` as `detail.about`, in `assetTypeRef:getAbout()`, at the end of the type's `types/<typename>` guide, and at the top of the type's Inspector. ## Ask your assets to say what they are A type can require each asset to carry a description: a file, written by the author or generated, saying what the asset is and what it is for. Declare it with a `described` check: ```yaml files: required: - path: "README.md" kind: "file" content: - { check: "described", message: "Say what this widget is for and where it goes." } ``` A file that is missing, empty or still has a line starting with `[[describe]]` fails the check, and so does a YAML file whose named `keys` are missing or over their `max_bytes`, or a Markdown file missing a named heading. `asset.validate` names the asset, and a push that carries it is refused with `message`. `services describe` finds such assets and writes their descriptions. `describe:` steers what is generated: ```yaml describe: guidance: "Say what the widget shows, what input it reacts to, and where it fits in a UI." examples: - "<identity of an asset of this type whose description reads well>" ``` An example is an asset of your type whose description is a true, written one, never a placeholder and never the asset being described; with none, leave `examples` empty. A type whose assets the model cannot perceive (a sound, a motion) says so in `guidance`, and the description is drawn only from what the asset's own files state. Start the file unwritten in your `template/`: a heading and one line starting with `[[describe]]`, never example content, which every new asset would carry verbatim. A type whose assets say what they are through their own content (a mesh, a texture) declares no check. ## The type's code (`behavior.luau`) `behavior.luau` returns one table. These are the keys the engine reads from it; anything else on the table is the type's own. ### What the type's assets can do | Key | Shape | What the engine does with it | |---|---|---| | `ref` | `{ [name] = function(self, ...) }` | Every function becomes a method on every asset of the type: `ref:name(...)`. | | `modules` | `{ [name] = require(".x") }` | Code the type's assets reach with `asset.containing(__FILE__).modules.<name>`. | | `events` | an events schema | Events an asset fires. Readers subscribe through `ref.events.<name>:connect(...)`; the type fires them. | | `refShapes` | `{ [method] = function(self) -> (typeExpr, source?) }` | The result type of a `ref` method for one particular asset (below). | | `namePattern` | a Lua pattern | The names `asset.create` accepts for the type's assets. Default `^[A-Za-z][A-Za-z0-9_]*$`. | | `derive` | `{ output, module, compute, fingerprint, matches?, required?, timeout? }` | A file computed from the asset when it is committed; a `required` one holds the commit until it lands. | Some `ref` names mean the same for every type, and the engine calls them: - **`instantiate(self, target?, opts?) -> (root, idMap)`**: puts the asset in a scene. Defining it is what makes the type scene-instantiable. Its return is a contract (below). - **`inspect(self) -> detail`**: what `asset.inspect` reports for the asset beyond the common record (below). - **`preview(self, opts)`**: renders the asset's picture; it wins over the type's `preview:` recipe. - **`previewMaterial(self) -> { material }`**: the material a `surface` preview paints. - **`saveDefinition(self)`**: writes the asset's in-memory edits back to its files in edit mode. Some names are answered for every asset before the type is asked, so a `ref` entry of the same name is never reached: `getSource`, `getBytes`, `getText`, `exists`, `deps`, `canInstantiate`, `canPreview`, `preview` (which calls yours when you define one), `meta`, `runtime`, `events`, `typeRef`, `modules`, and the residency flags `has_backing_asset`, `has_runtime_changes`, `cpu_resident`, `gpu_resident`. ### When the engine calls the type | Hook | Signature | When it runs | |---|---|---| | `onCreate` | `(name, opts) -> { [path] = contents }` | On `asset.create`. Returns the files of the new asset, laid over the type's `template/`. Annotate `opts`: its type is the schema `asset.create` checks the caller's arguments against. A `""` key writes a single loose file. | | `onReplace` | `(dest, files) -> files` | On an `asset.create` over an asset already at `dest`, after `onCreate`, so what the author added can be carried into the replacement. | | `onRegister` | `(self)` | Once per asset, when it first registers: when the world loads, on a live `asset.create`, and when an asset arrives later (installed, pulled, synced). This is what makes an asset take effect the moment it is written. | | `onChange` | `(ref, change)` | On every write to a file inside an asset of the type. `change` is `{ path, asset, type, kind, origin }`, plus `settings` and `settingsChanged` for a write to the settings values file; `kind` is `edited` or `seeded`, `origin` is `local` or `remote`. Filter on `change.path`. Writes to one asset arrive in order, each after the hook for the one before has finished, and the hook may wait on what it reads. A write the hook makes inside its own asset arrives later like any other, so keep the hook convergent. | | `onDependencyChange` | `(ref, change)` | When a module the asset's code requires is re-run, or a name the asset carries starts resolving. `change.kind` is `dependency`. | | `onDelete` | `(ref)` | When an asset of the type leaves the world: deleted, or the world it is in is swapped away. | | `validate` | `(ref) -> { { code, message, severity?, path? } }` | On `asset.validate`, after the `type.yaml` checks, for the type's own rules. `severity` defaults to `error`. Runs on every push, so a type's rules hold at publish. A hook that fails reports `validate.hook_failed`. | ### Settings hooks | Hook | When it runs | |---|---| | `settingsDefaults(name, opts, asked) -> table` | Once per `asset.create`, for values the schema's defaults do not give. | | `settingsCorrections(values, name, opts) -> (corrections?, reasons?)` | On every settings write. A correction to a field the writer named refuses the write, giving the reason. | | `settingsDerivedDefaults(values, explicit, changed)` | On a settings write, for fields derived from others. | | `normalizeSettings(values, stored?)` | On every read and write, to map an earlier spelling of a field to the current one. | | `settingsHealed(values, stored)` | On a read, to repair stored values. | | `settingsSchemaProblem(schema, ref) -> string?` | When an asset's own schema is read; a returned string refuses the schema. | `creationInputs` (`{ [opt] = true | selectorOpt }`) names `asset.create` options that are inputs to the type rather than settings. ### Reloading An edit to `behavior.luau` reaches existing assets with no restart. ## How an asset is pictured (`preview:`) | `kind` | The picture is | The type provides | |---|---|---| | `instantiate` | the asset placed in the preview studio through its `instantiate` | `ref.instantiate` | | `image` | a file the asset holds, fitted to the frame | `source: { files }`, the candidate files in order | | `surface` | the asset painted on a mesh | `mesh`, and `ref.previewMaterial` | | `custom` | whatever `ref.preview` returns | `ref.preview` | ```yaml preview: kind: surface mesh: sphere ``` `angle: [yaw, pitch]` turns the camera. The studio owns the light, the backdrop and the framing, so an asset pictures the same in every world. A type with no `preview:` that defines `instantiate` still answers `ref:preview()`. A picture is rendered when something asks to view it (the editor showing the asset, `ref:preview()`), when previews are generated on request (`assets.generatePreviews`), and when the asset is staged. An edit alone renders nothing. The picture records what it was made from, so a stale one is told from a current one. ## The names your type's assets hold (`references:`) Where your type's own format or API carries the name of another asset, declare it here: ```yaml references: keys: # a top-level key of the type's files that holds a name, or a table of them - { key: "shader", type: "shader" } args: # an argument of a call, counting from 1 - { call: "g:node", position: 2, type: "procNode" } fields: # a field of the options table a call takes - { call: "fx.beam", field: "material", type: "material" } ``` `type` is the category the name resolves in. `asset.declareReferenceKey`, `asset.declareReferenceArg` and `asset.declareReferenceField` declare the same from a script. ## What search reads (`indexing:`) `indexing:` is how assets are found. Search reaches an asset only through what its type's `indexing:` names, and adds nothing of its own. That includes you: an asset indexed badly is one you cannot find later, when you are making something else and need it. A type whose assets are never looked for says `indexing: none`. ```yaml indexing: data: # text, read from the asset's files - role: identity extractor: verbatim source: { file: "README.md" } - role: interface extractor: symbols source: { files: ["init.luau"] } - role: implementation extractor: verbatim source: { files: ["init.luau"] } content: # the picture - role: identity modality: image source: { file: "preview.png" } facets: # what a query filters and re-ranks by file: ".metadata" rigged: { type: bool, from: { components: { contains: SkinnedModel } } } roughness: { type: number, filter: range, from: { file: "mat.yaml", field: "floats.roughness" } } ``` **Roles.** Each `data` and `content` entry has a `role`, and a query matches in two slots: - **`identity`**: what an asset is. For anything you look at, the picture; for anything else, the text that says what it is. - **`capability`**: what it does and how it is made. `interface` (the signatures or shape a caller uses) and `implementation` (the body) both count as capability, and a type with code usually declares both. Ask what a person would recognise an asset of the type by, and what it does, and point each role at the file that says it. For text, that is the asset's description: the file your template starts with a `[[describe]]` block, which the author writes or `services describe` fills in (see "Ask your assets to say what they are"). **`data`.** `extractor: verbatim` embeds a file's text as it is; `symbols` embeds the declared signatures of code. `source` is `{ file }` or `{ files: [...] }`. Never index a file name (`source: { field: name }`): one or two words carry nothing to match. **`content`.** `modality: image` embeds the picture itself, so a text query matches what the asset looks like. A type whose assets are looked at makes the picture its identity. **`facets`.** What a query narrows and re-ranks the matches by. `file` is embedded whole, so keys nobody declared in it are still found. Each further key is computed for every asset and states a value for every one, including `false`: a file can imply "not rigged" but never say it, and a query separates rigged from unrigged only because the unrigged ones say `rigged: false`. A key is `{ type: bool | number | enum, filter?: range, from }`, where `from` is `{ file, field }` (a field of one of the asset's files, dotted for nesting), `{ metadata: key }`, or `{ components: { contains | any_outside } }` (what the asset's entities carry). ## What blocks a push A push checks every asset it carries against its type. These refuse it: - a required file that is missing, a `content` check that fails, a `described` file not written yet; - an error from the type's `validate`. Unlisted files and warnings do not. A `described` file blocks only a push that changes the asset. When a push is refused, the message names each asset and what it lacks. `world.publishBlockers()` and `zm status` read the same list without pushing. ## Typing a result from the asset (`refShapes`) A `ref` method whose result depends on the asset (one entry per child folder, per row of a config, per control a map declares) can only be annotated as `{ [string]: Thing }`, which accepts any key. `refShapes` states the result for each asset instead: a function returning a Luau type expression for that asset, and the module whose types the expression uses. Read the asset; never run it. ```lua -- inputMap.assetType/behavior.luau M.refShapes = { activate = function(self): (string, string) local names = {} for _, control in ipairs(M.ref.controls(self)) do table.insert(names, control.name .. ": Handle") end if #names == 0 then return "", "" end return "{ " .. table.concat(names, ", ") .. " }", "@builtin::modules.zinput.scheme" end, } ``` With it, `map.jump:onPressed(fn)` is checked and `map.noexisting` is reported. A per-asset type applies where the call site names which asset: a component field whose default names it, or an annotation `AssetRef<"inputMap", "<identity>">`. `asset.resolve(name, category)` carries only the category, and a name computed at runtime carries nothing, so calls on those keep the declared return. The type belongs to the call's result: index it there, or bind what you need from it there, and the names stay checked. `require("@builtin::assetTypes.assetType.shared.refShapes").published()` shows what was published: `categories` (each type's method surface) and `returns` (per-asset results by identity, then method). An identity missing from `returns` was never published. Results are recomputed when an asset is written; at most 64 assets of one type are read. ## Putting an asset in a scene (`ref.instantiate`) A caller writes one piece of code against every instantiable type, so what `instantiate` returns is fixed: ```lua local Instantiable = require("@builtin::assetTypes.assetType.shared.instantiable") M.ref = { instantiate = function(self, target, opts) local root = Instantiable.root(self, target, opts) -- compose the asset under `root`, now return Instantiable.result(self, root, idMap) end, } ``` **In.** `target` is the entity the asset lands under, or onto for a hierarchy type; with none, the type spawns a new root. `position`, `rotation` (three numbers in degrees, or a quaternion), `scale`, `name` and `temporary` mean the same for every type: `Instantiable.root` applies them to a root you make, `Instantiable.place` to one you adopt. Document any options of your own in the hook's `--!arg` docs. **Out.** `(root, idMap)`. `root` is the composed root, live when you return: build synchronously, never defer to a component's `awake`. `idMap` maps each original id to the spawned one, so a component that composes the asset again on every load can pass it back in and keep references into the composition. Every call goes through `Instantiable.result`, which refuses a root that is not live and turns a missing `idMap` into `{}`. ## Inspecting an asset (`ref.inspect`) `asset.inspect(ref)` builds a record every asset shares (`identity`, `name`, `guid`, `source`, `typeName`, `typeDefinitionPath`, `scope`, `origin`, `description`, `tags`), then calls `ref.inspect(self)` for the type's own `detail`: ```lua M.ref = { inspect = function(self) -- read the asset's own files, never runtime state: inspect works on an asset -- that has not been compiled, uploaded or registered return { --[[ what makes this asset what it is ]] } end, } ``` Without it, the record is the shared part alone, and an agent inspecting the asset learns nothing of what it is. A hook that raises leaves `detail` empty and sets `record.warning`. ## Discovery - `asset.list("assetType")`: every registered type. - `asset.inspect("<typename>")`: the type, its README and its `about.yaml` texts. - `asset.resolve("<typename>", "assetType")`: the type itself.
▲ 0↑ born
❒
package · born here
❒asset
# edui The editor UI framework, on the CSS-parity `ui.*` surface. An editor-only toolkit: panels author closures instead of callback ids, a dock shell over `ui.*` native docking, and typed editor-chrome primitives. Gameplay and normal world UI author raw `ui.*` trees and need none of this. **`edui.guide` beside this file is how to use it** — creating a panel, the spec it returns, how a panel is drawn and redrawn, the whole widget vocabulary, and how a panel reaches the selection and the dock. ## What installs - **`framework`** — everything under one table. From outside the package, `require("@builtin::~edui.framework")`; inside it, `require("~.framework")`. It carries `app`, `widgets`, `shell`, `topbar`, `query`, `fileDialog`, `worldDialog`, `stats`, and the chrome primitives flat (`panel`, `tree`, `field`, `section`, `text`, `button`, `menu`, …). - **`app`** — maps a panel's closures to `ui.*` callback ids, dispatches to them, and coalesces redraws. - **`widgets`** — the primitives, plus the typed inspector layer (`widgets.tree`, `widgets.fields`, `widgets.controls`). - **`inspector`** — the Inspector as an ordered list of sections: the generic asset sections, the Settings section and its actions, and the merge with the sections a type's `inspector.luau` declares (`inspector.assetSections`, `inspector.settingsSection`), and the building blocks those sections compose (`inspector.parts`, reached as `ctx.parts`). - **`shell`** — the dock: one `ui.*` `dockArea` holding a `dockPanel` per registered panel, and the panel registry behind it. ## Referencing - Inside this package, modules reference siblings with `~.` — e.g. `require("~.app")`, `require("~.widgets.fields")`. - Editor content built on the framework references it by package identity — `require("~edui.framework")`, `require("~edui.shell")`.
▲ 0↑ born
·
assettype · born here
❒asset
# guide A guide is a single-file documentation asset: a `<name>.guide/` folder whose body is `guide.md`, surfaced through the `guides` MCP tool, the `man` shell builtin and the Docs panel. Drop one anywhere under `/zero/source/`, a `.package` included, and it shows beside the built-in guides. ## Linking guides together A markdown link in a guide is followed when it is clicked in the Docs panel: `[Bindings](controls-guide)` opens another guide by any name `guides read` takes, `[Slots](#save-slots)` a heading of the same guide (its text in lower case, punctuation removed, spaces turned into `-`), `[Slots](save-kit#save-slots)` a heading of another, and `[the module](../saveKit.module)` a file or asset, relative to the guide's own folder. A link the panel cannot follow says why above the guide. ## In the Inspector Guide: the guide's word count, its headings, and **Open in Docs**, which opens it in the Docs panel.
▲ 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. The body runs in an environment of the module's own: a name it declares without `local` is readable by the module's own functions and by no other script, so an export reaches its callers through the returned table and nothing else. Requiring a component's folder runs its chunk the same way, once, and its `update`, `start` and `public` stay in that environment; a component runs on an entity through `entity(id).component.add(name)`. 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()`. A name the body declares without `local` lives in the module's environment, which every run of the module shares: a function the previous run left in place reads what the reload assigned. 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
◇
component · born here
❒asset
# ParticipatingMedium Marks its entity as a volume of fog, smoke, dust or steam — something light has to travel through. What is behind the volume comes out dimmer than what is beside it, and overlapping volumes stack. ```luau local id = entity.spawn("fog_bank").id entity(id).component.add("ParticipatingMedium", { sizeX = 30, sizeY = 8, sizeZ = 30, density = 0.15, }) ``` ## Fields | Field | Meaning | |---|---| | `shape` | `"box"` (default) or `"sphere"` | | `density` | Extinction per world unit. `0` releases the medium. | | `sizeX` / `sizeY` / `sizeZ` | Half-extents, for a box | | `radius` | Radius, for a sphere | `density` is the physical knob: a ray crossing `d` world units of a medium of density `k` leaves with `exp(-k * d)` of the light it entered with. A 20-unit bank at `0.15` therefore passes about 5% of the light straight through it. The entity's position places the volume; move the entity and the shadow moves with it. ## Notes - The light-space map is sized to the bounding sphere of everything registered. A small bank sharing a scene with a very large one lands on few texels — set `extent` and `centre` through `mediaTransmittance.set` to bias the map toward the region you care about. - Attenuation applies to surfaces. The sky keeps its own colour. - The attenuated light is the sun's: `sunDirection` sets which way the map's rays travel. See `mediaTransmittance` for the scene-wide settings and the direct API.
▲ 0↑ born
◇
component · born here
❒asset
# Volume Per-entity attachment that renders a 3D voxel texture (cloud, smoke, nebula, imported VDB grid) into the scene as a volumetric body. The component does NOT own the volume's lifetime — producers (the `volume` module, a custom compute fill, or `compute.writeTexture3D`) build the texture and pass its name in `volumeName`. The entity's world transform defines the AABB the raymarcher samples. `volumeName` is resolved through `Volume.get`, so a volume the `volume` module built is rendered as it stands — at its own dimensions, from its own density channel, through its occupancy grid, and out of its brick pool if it is paged. A name no volume answers to is taken as a bare 3D texture some other system owns, and `occupancyName` / `isRgba` describe it. ## Usage ```luau local Volume = require("@builtin::systems.volumetrics.volume") local cloud = Volume.noise({ name = "cloud", size = 64, scale = 4 }) cloud:buildOccupancy({ brickSize = 8 }) local e = entity.spawn("cloud_entity") e.localScale.set(8, 4, 8) e.component.add("Volume", { volumeName = "cloud", occupancyName = cloud.occupancyName, density = 6.0, }) ``` A paged volume costs what it holds and is named the same way: ```luau local cloud = Volume.noise({ name = "cloud", size = 128, threshold = 0.6 }) cloud:buildSparse({ brickSize = 8, threshold = 0.01 }) cloud:releaseDense() -- the brick pool is now the only copy local e = entity.spawn("cloud_entity") entity(e.id).localScale = { 8, 4, 8 } e.component.add("Volume", { volumeName = "cloud", density = 6.0 }) ``` ## Public fields | Field | Type | Sync | Default | Meaning | |---|---|---|---|---| | `volumeName` | string | Sync | `""` | Required. Name of the volume, or of a bare 3D texture, to render. | | `occupancyName` | string | Sync | `""` | R8 occupancy texture for a bare texture's empty-space skipping. Empty = no occupancy. A volume the module built carries its own, and a name set against one is reported as left aside. | | `density` | number | Sync | `1.0` | Density gain multiplier. | | `absorption` | number | Sync | `1.0` | Beer-Lambert absorption coefficient. | | `emission` | number | Sync | `1.0` | Emission gain. | | `color` | color | Sync | `{1,1,1,0}` | RGB tint + density floor in alpha. | | `stepSize` | number | NoSync | `0` | World units per ray step. `0` = auto-derive from volume extent. | | `maxSteps` | number | NoSync | `256` | Ray-march step cap. | | `target` | string | Sync | `"scene_volume_rt"` | Output render target name (composite key). | | `targetWidth` | number | NoSync | `1280` | Output width (px). | | `targetHeight` | number | NoSync | `720` | Output height (px). | | `useOccupancy` | boolean | NoSync | `true` | Toggle empty-space skipping. | | `isRgba` | boolean | Sync | `true` | `false` reads density from the red channel. Left `true`, a volume the module built is read from the channel it was built with, and a bare texture as RGBA. | All public fields use the `Field.<kind>(default, Sync|NoSync)` schema (PR #3019). Visual params (look, tint, density) propagate Sync; quality knobs (stepSize, maxSteps, target resolution, useOccupancy) are NoSync — each client picks its own perf/quality point. ## Lifecycle - **`awake`** — no-op. The volume texture is owned by the producer and looked up lazily on the first `update`. - **`update(dt)`** — resolves `volumeName` through `Volume.get`, falling back to a wrapper over a bare texture of that name, and dispatches the compute raymarcher into `target`. The resolution runs every frame, so a volume built, paged or destroyed after this component attached is picked up on the next one. - **`onPropertyChanged(key, value, old)`** — drops the resolved volume when `volumeName`, `occupancyName`, or `isRgba` changes, forcing a re-resolve on the next frame. ## Related - `volume` module — texture builders and name lookup (`Volume.noise`, `Volume.get`, `Volume.wrap`, `:buildSparse`, `:render`). - `vdb_to_zvol.importer` — VDB → dense voxel pipeline that produces textures consumable by this component. - `volumeDemos` / `volumetricSky` packages — worked examples.
▲ 0↑ born
◇
component · born here
❒asset
# VolumeSequence Plays an open volume sequence through an entity's transform AABB. The sequence is opened by a producer and named here — the same split the `Volume` component makes between "what data to render" and "where it appears in the world". Each tick the component advances the playhead and raymarches the frame that is resident into the shared `scene_volume_rt` target, in both play and edit mode, so the run animates while it is being authored. ```luau local Seq = require("@builtin::systems.volumetrics.volumeSequence") Seq.open({ name = "plume", frames = paths, resident = 3, fps = 24 }) local id = entity.spawn("plume_entity").id entity(id).localScale = { 8, 8, 8 } entity(id).component.add("VolumeSequence", { sequenceName = "plume", density = 6.0 }) ``` `fps` and `playing` are pushed into the sequence when they change, so a script calling `seq:setFps()` / `seq:pause()` between two ticks is left standing until the authored value itself moves. The component does not own the sequence: despawning the entity leaves the ring allocated, and `volumeSequence.close(name)` is what frees it. ## Sharing The playhead, the rate and the ring belong to the sequence, so several entities naming one run play the same frame from the same three textures: the playhead advances once per engine frame however many components tick it, and the rate is whichever component's `fps` moved last. Give each entity its own run when they should play independently. `target` is a render target, and a raymarch writes the whole of it — two volumes visible at once (a sequence beside a `Volume`, or two sequences) need a `target` name each, or the later write replaces the earlier one and only one of them appears. Composite each target the way any volume render is composited — see `volume.compositeShaderSource()`.
▲ 0↑ born
◇
component · born here
❒asset
# VolumetricLighting The air between the camera and the scene, as a component. Add it to an entity and light travelling through that air scatters toward the viewer, so a lamp becomes a glow rather than an invisible point and the sun cuts shafts where something crosses it. ```lua local id = entity.spawn("air").id entity(id).component.add("VolumetricLighting", { density = 0.06, anisotropy = 0.7, }) ``` Every field is live — editing one in the inspector changes the air on the next frame, and the values persist with the scene. Setting `density` to 0 empties the air and releases the passes while keeping the rest authored; removing the last one in the scene clears it. | field | effect | |---|---| | `density` | How much medium there is per world unit at the reference altitude. 0 is off. | | `heightFalloff` / `heightRef` | How fast density thins with altitude, and the altitude `density` is quoted at. 0 falloff fills the scene evenly. | | `steps` | Samples along each view ray. More resolves a beam's edge more cleanly. | | `colorR/G/B` | What the medium scatters. Warm dust and cold mist differ here. | | `anisotropy` | How forward-biased the scattering is, in (-1, 1). Positive values throw light onward, so haze blooms around a source you look toward. | | `sunIntensity` / `sunColorR/G/B` | The sun's contribution to the medium. | | `lightScale` | The scene lights' contribution to the medium. | | `shadowSteps` / `shadowDistance` | Samples taken back toward the sun to decide whether a point in the air is lit, and how far they reach. **0 leaves the sun's contribution unshadowed**, so the haze glows evenly and no shafts form. | | `maxDistance` | How far along the view ray the march runs. A ray that escapes to sky stops here. | The lights are the scene's own, each scattering on the terms its kind emits on: a point fills the air around it, a spot fills its cone, a rect reaches the air on the side its face emits along, and a parallel light lays an even glow across the whole medium. One added, moved, re-aimed or recoloured at runtime changes the glow it throws with nothing to keep in step here. One body of air fills the scene, so one of these per scene is enough — the last one to push wins. The work runs in `lightScattering.renderFeature`; the settings live in `@builtin::systems.volumetrics.volumetricLighting`, which is also how to drive them from a script.
▲ 0↑ born
◇
component · born here
❒asset
# VolumetricSky Singleton component that drives the procedural cloud layer. Attach to one entity (usually a sky placeholder), and the component owns the cloud volume + render target lifecycle, bakes the noise field when what it is baked from changes, and dispatches the compute raymarcher every frame into `scene_volumetric_sky`. A composite (post-process or material) blends `scene_volumetric_sky` over the scene — see `demo/sky.scene/` for the full wiring. ## Usage ```luau -- Defaults (the v4-tuned cumulus look — `Sky.defaultPreset()` hands out a -- copy of the same table this field defaults to) local sky = entity.spawn("sky") sky.component.add("VolumetricSky", {}) -- Or apply a captured preset: preset.apply("cumulus_default", { entity = sky.id, component = "VolumetricSky" }) -- Inline tweak. `component.add` writes the `preset` field as it is given, -- so a table handed here carries the whole configuration: local p = require("@builtin::systems.volumetrics.volumetricSky").defaultPreset() p.noiseScale, p.octaves, p.coverage = 9.0, 5, 0.20 p.densityGain = 0.04 -- v5 σ_t sky.component.add("VolumetricSky", { shaderVariant = "v5", preset = p }) ``` `volumetricSky.module`'s `M.spawn` / `M.configure` take the same parameters by name, folding each into the current configuration and leaving the rest where they stand: ```luau local Sky = require("@builtin::systems.volumetrics.volumetricSky") Sky.spawn({ shaderVariant = "v5", noiseScale = 9.0, coverage = 0.20 }) ``` ## Schema (Field<T> / Sync / NoSync) The 28 shaping / weather / lighting params travel as ONE Sync field — `preset = Field.table(DEFAULT_PRESET, Sync)` — so replicating a sky change is **one mutation, not 28**, and copying a sky between scenes is **copying one field, not 28**. Per-client quality knobs sit outside the preset as NoSync fields so each client picks its own tier without disturbing peers. `SkyPreset` and `DEFAULT_PRESET` are declared by `volumetricSky.module`, which this component reads its default from and which routes an option by that same key set — so the shape below and the module's `M.spawn` options are one declaration. | Field | Type | Sync | Default | Meaning | |---|---|---|---|---| | `preset` | `SkyPreset` table | Sync | `DEFAULT_PRESET` (v4 cumulus) | All shaping / weather / lighting (see below). | | `shaderVariant` | string | Sync | `"v4"` | `"v3" \| "v4" (default) \| "v5"`. | | `wind` | vec3 | Sync | `{0,0,0}` | Animated drift offset (advanced per frame from `preset.windSpeed`). | | `cloudSize` | number | NoSync | `256` | Volume W,D resolution. | | `cloudHeight` | number | NoSync | `128` | Volume H resolution. | | `stepSize` | number | NoSync | `4.0` | World units per ray step. | | `maxSteps` | number | NoSync | `224` | Ray-march step cap. | | `shadowSteps` | number | NoSync | `8` | Shadow ray step count. | | `alphaCutoff` | number | NoSync | `0.99` | Early-exit α threshold. | | `jitter` | number | NoSync | `1.0` | Ray-origin jitter strength. | | `taaBlendAlpha` | number | NoSync | `0.1` | TAA current-sample weight. | | `targetWidth` | number | NoSync | `0` | Render target width (`0` = viewport). | | `targetHeight` | number | NoSync | `0` | Render target height. | | `noiseRegenRate` | number | NoSync | `10` | Times per second wind drift re-bakes the cloud volume (`0` = every frame the wind moved). | ### Preset (`SkyPreset`) shape Noise / shaping: `noiseScale, octaves, coverage, densityGain, altitudeLow, altitudeHigh, anvilBias, detailScale, detailOctaves, detailStrength, detailThreshold, baseLowRemap, baseHighRemap, color, windSpeed, seed`. Volume placement: `horizExtent, cloudBaseAltitude, cloudTopAltitude`. Look (visual, not perf): `phaseG`. Sun / sky: `sunDir, sunColor, sunIntensity, sunDiscSize, horizonColor, zenithColor, groundColor, ambient`. v5-only: `cloudType, worleyBlend`. See `presets/cumulus_default.preset/preset.yaml` for the canonical full preset shape, and the `init.luau` `DEFAULT_PRESET` table for the runtime defaults. ## Where the frame time goes Two passes carry the cost, and different fields reach each: - **The bake** (`cloud_noise_*`) fills every voxel of the `cloudSize × cloudHeight × cloudSize` volume with layered noise. It is the most expensive pass the layer has (around 10 ms at 256×128×256 on a desktop GPU), and it runs only when the volume's inputs move: at once for a change to `preset`, `cloudSize`, `cloudHeight`, `shaderVariant` or the render device, once the program it compiles is resident for an edit to the noise shader's own source, and at most `noiseRegenRate` times per second for wind drift. A sky whose `preset.windSpeed` is zero bakes once. `cloudSize` / `cloudHeight` set what one bake costs; `noiseRegenRate` and `windSpeed` set how often it is paid. - **The raymarch** (`cloud_raymarch_*`) runs for every view of every frame. `stepSize`, `maxSteps`, `shadowSteps`, `alphaCutoff` and `targetWidth` / `targetHeight` set its cost. `profiler.gpuFrame()` reports both as `compute.<shader>` spans; a span's `frames` count is how many frames of its window the pass ran in, and `volumetricSky.volumeBakes()` counts the bakes themselves. ## Field-mutation gotcha (and why `update()` reassigns the whole vec3) Under the new Field<T> / Sync / NoSync schema, `public` is not a plain Lua table — it's wrapped in a proxy whose `__newindex` metamethod performs type-checks, marks the field dirty, and queues Sync replication. Writing `public.wind = {x,y,z}` triggers all four steps. Writing `public.wind[1] = x` reads `public.wind` (getter), mutates the returned table directly, and **bypasses the setter** — no Sync replication, no dirty flag, no `onPropertyChanged`. Always assign the whole vec3 / preset / color table; never index-mutate. The same caveat applies to `public.preset`: `public.preset.coverage = 0.2` won't replicate. To change one preset field, copy the table, mutate, reassign: ```luau local p = public.preset local next = {} for k, v in pairs(p) do next[k] = v end next.coverage = 0.2 public.preset = next ``` ## Lifecycle - **`awake`**: starts the sky's clock and creates the render feature that runs the layer's passes. - **`update(dt)`** — advances `wind` (reading `preset.windSpeed`), hands the noise inputs to `volumetricSky.fillClouds`, which bakes the volume when they moved (see "Where the frame time goes"), then queues the raymarch dispatch for the frame's deferred phase. - **`onDespawn`** — releases the volume, the render target and the composite effect via `M.destroy`. ### Why the raymarch is dispatched from `task.defer` The raymarch produces ONE full-screen layer for ONE view, and the frame composites it over everything that frame drew — so it has to be rendered from the view the frame ends up drawn with. Which view that is settles only once the frame's script work is finished: a cutscene walking the camera from a task loop, a camera controller, a component ordered after this one, an editor tool — each writes the camera after `update` has run, while the renderer reads the camera once all of them are done. Taking the camera snapshot in the deferred phase reads what every one of them has already written, and still queues the dispatch ahead of the frame's render. Snapshotting in `update` instead renders the layer for whatever the camera was midway through the frame; on a hard cut that is a different place entirely, and the frame the cut lands on composites a cloud layer belonging to the shot it just left. A caller driving `volumetricSky.dispatchRaymarch` directly owes the same discipline. ## Related - `volumetricSky.module` — orchestration backend. - `presets/cumulus_default.preset` — canonical full preset; copy to author named weather (storm, overcast, dawn, …). - `Volume.component` — per-entity flow for non-cloud volumetric content. - `demo/sky.scene` — worked example.
▲ 0↑ born
▣
module · born here
❒asset
▲ 0↑ born
·
scene · born here
❒asset
▲ 0↑ born
·
computeshader · born here
❒asset
# godrays_fill Fills a volume with shafted light. One invocation per voxel. ## Bindings | name | kind | what it is | |------|------|------------| | `vol` | `storage3d` (rgba16f) | the volume written — RGB emission, A density | | `U` | `buffer` (read, f32) | the demo's fill parameters |
▲ 0↑ born
·
scene · born here
❒asset
▲ 0↑ born
·
computeshader · born here
❒asset
# nebula_fill Fills a volume with a spiral nebula: RGB carries emission, A carries density. One invocation per voxel at workgroup size 4x4x4. ## Bindings | name | kind | what it is | |------|------|------------| | `vol` | `storage3d` (rgba16f) | the volume written, read-write so a pass can accumulate | | `U` | `buffer` (read, f32) | the demo's fill parameters, in the order the shader reads them | `U` holds: elapsed time, spiral frequency, arm thickness, core brightness, edge brightness, dust density, noise strength.
▲ 0↑ born
·
scene · born here
❒asset
▲ 0↑ born
·
computeshader · born here
❒asset
# sdf_solids_fill Fills a volume with signed-distance solids. One invocation per voxel. ## Bindings | name | kind | what it is | |------|------|------------| | `vol` | `storage3d` (rgba16f) | the volume written — RGB emission, A density | | `U` | `buffer` (read, f32) | the demo's fill parameters |
▲ 0↑ born
·
scene · born here
❒asset
▲ 0↑ born
·
scene · born here
❒asset
▲ 0↑ born
·
scene · born here
❒asset
▲ 0↑ born
·
computeshader · born here
❒asset
# smoke_plume_fill Fills a volume with a rising smoke plume. One invocation per voxel. ## Bindings | name | kind | what it is | |------|------|------------| | `vol` | `storage3d` (rgba16f) | the volume written — RGB emission, A density | | `U` | `buffer` (read, f32) | the demo's fill parameters |
▲ 0↑ born
·
scene · born here
❒asset
▲ 0↑ born
·
computeshader · born here
❒asset
# zvol_viewer_fill Leaves the volume as it is. The ZVOL viewer uploads voxels straight into the volume's 3D texture from the CPU, so the fill stage this shader occupies has nothing to compute — it reads one value from each binding so both stay live. ## Bindings | name | kind | what it is | |------|------|------------| | `vol` | `storage3d` (rgba16f) | the volume the viewer writes into from the CPU | | `U` | `buffer` (read, f32) | the fill parameters the spawn API supplies |
▲ 0↑ born
·
guide · born here
❒asset
▲ 0↑ born
·
computeshader · born here
❒asset
# emission_raymarch Marches a camera ray through an emissive volume and writes the result to a 2D target. Emission accumulates along the ray and density attenuates what is behind it, so a volume reads as light rather than as a surface. ## Bindings | name | kind | what it is | |------|------|------------| | `vol` | `texture3d` | the volume marched — RGB emission, A density | | `smp` | `sampler` | how `vol` is filtered along the ray | | `out_tex` | `storage2d` (rgba16f) | the image the march writes | | `U` | `buffer` (read, f32) | camera, volume placement and march settings | `U` carries the inverse view-projection, the volume's world transform, the step count, density and emission gains, and the target's inverse size.
▲ 0↑ born
·
importer · born here
❒asset
# VDB → ZVOL importer Claims `.vdb` (OpenVDB) writes and produces a sibling `.zvol` — the engine-native dense RGBA8 volumetric format consumed by the volumetrics system. This is a standard `importer` asset: `init.luau` returns a `{ canImport, import }` module that the importer dispatcher (`@builtin::assetTypes.importer`) loads lazily and runs only when a matching file is written. It does **not** execute anything at load time. ## Pipeline 1. **Real parse** — if the `openvdb` plugin in the builtin library (`@builtin::plugins.openvdb`) is loaded, the VDB B-tree is parsed into a dense RGBA8 voxel buffer and wrapped in a 64-byte ZVOL header. 2. **Procedural fallback** — without the plugin, a deterministic noise-modulated torus is synthesised (FNV-1a hash of the first 256 source bytes seeds it) so the importer + demo wiring stays functional. The `produced_by` field in the result distinguishes the two paths. ## ZVOL header (64 bytes) | offset | size | field | |--------|------|-------| | 0 | 4 | magic `ZVOL` | | 4 | 4 | u32 version (1) | | 8 | 4 | u32 width | | 12 | 4 | u32 height | | 16 | 4 | u32 depth | | 20 | 4 | u32 format (1 = rgba8) | | 24 | 12 | f32[3] aabb_min | | 36 | 12 | f32[3] aabb_max | | 48 | 16 | reserved | | 64 | … | voxel data, `w*h*d*4` bytes, XY-major Z-slowest |
▲ 0↑ born
·
renderfeature · born here
❒asset
# lightScattering The render passes behind the `VolumetricLighting` component and the `volumetricLighting` module. Two compute passes after lighting. **`vol_scatter`** marches each view ray at half resolution, from the camera to the surface the ray hits — or to the medium's reach when it escapes to sky, since an unbounded march would integrate the whole horizon into every sky pixel. At each step it evaluates the medium's density, gathers the light arriving there, and accumulates the fraction sent toward the camera, attenuating by everything already crossed. A surface-only renderer draws the pool of light a lamp throws on the floor but not the beam that carried it there, because nothing between the lamp and the floor is ever shaded. Marching the ray shades that space. The lights are the scene's own. `@scene.lights` is the same buffer the lit passes read, so a light added, moved or recoloured at runtime changes the beam it throws with no bookkeeping here. It holds **twelve vec4s per light** — the light, then its punctual shadow record — read as raw words so no struct declaration has to mirror the engine's layout. A spot's cone gate is evaluated at the sample itself rather than at a surface, which is what makes the cone visible as a volume instead of only where it lands. Attenuation matches the lit passes' windowed falloff, so a beam and the pool of light it lands in agree about how far the source reaches. Area lights are skipped — their contribution to a medium is an integral over the rect rather than a single direction. The sun's contribution is gated by a short march back toward it, tested against the depth buffer. That is what turns uniform haze into shafts: the medium is lit only where the path back to the sun is clear, so an occluder carves a dark volume out of the beam rather than only shading the surface behind it. Because the test reads the depth buffer, an occluder outside the view frustum does not cast into the medium. Scattering follows Henyey-Greenstein, so the medium brightens toward a source you are looking at and stays dim when it is behind you — the difference between haze that reads as air and haze that reads as a grey wash. Each ray's first sample is offset by a per-pixel fraction of a step, trading the banding a fixed step pattern produces for noise. **`vol_composite`** upsamples that result and composites it. The taps are weighted by how closely their depth matches the full-resolution pixel's, which keeps a beam from bleeding across a silhouette — the halo a plain bilinear upsample leaves behind. The camera comes from `@frame.camera`, the camera of the render target being drawn, and the forward matrix is derived by inverting it in the shader rather than passed in from script — so an offscreen capture or an RTT camera marches through its own matrix at its own resolution. Parameters arrive through the `vol_params` buffer, packed by the `volumetricLighting` module.
▲ 0↑ born
·
renderfeature · born here
❒asset
# mediaShadow The shadow a fog bank casts on what is behind it. Two compute passes run after lighting: | Order | Pass | Work | |---|---|---| | 36 | `media_tau_map` | One ray per texel of a light-space map; accumulate optical depth | | 37 | `media_shadow_apply` | Attenuate each surface by the density in front of it | | 38 | composite | Blit the result | ## Why not a shadow map A depth-based shadow map answers one question per texel: is this point nearer the light than the first thing the light hit? Density has no first thing. What a medium does to light is continuous — a ray crossing `d` units of extinction `k` keeps `exp(-k*d)` of its energy — so what a receiver needs is the optical depth between it and the light, not a depth to compare against. The first pass therefore stores optical depth rather than distance, at four points down each ray. Two consequences fall out of that choice: - **Overlapping media stack.** Optical depth is additive, so two volumes that intersect contribute to the same running total with nothing to resolve. - **Geometry inside a volume is lit correctly.** A receiver looks up its own position along the ray and reads the depth accumulated *up to that point*, so a post standing in the middle of a fog bank is dimmed by the half in front of it rather than by the whole column. ## Inputs `@scene.color`, `@scene.depth`, `@frame.camera`, and the `media_params` / `media_volumes` buffers packed by `@builtin::systems.volumetrics.mediaTransmittance`. Sky pixels are passed through untouched.
▲ 0↑ born
▣
module · born here
❒asset
# mediaTransmittance Light attenuation through participating media, in a form both media and surfaces can sample. A shadow map stores one depth per texel and answers a yes/no question, so nothing made of density can cast with it. This module owns the other representation: a light-space map holding the **optical depth** accumulated along each light ray, recorded at four points down its length. Optical depth is additive. Every medium a ray crosses contributes to the same texel, so overlapping volumes stack with nothing to resolve between them. And because four points are stored rather than one total, a receiver standing halfway through a fog bank is attenuated by the half in front of it, not by the whole column. ## Direct use ```luau local Media = require("@builtin::systems.volumetrics.mediaTransmittance") Media.set({ sunDirection = { 0.55, 0.55, 0.55 }, steps = 32 }) Media.setMedium("bank", { position = { 0, 8, 0 }, size = { 20, 6, 20 }, density = 0.15, }) ``` Most scenes attach `ParticipatingMedium` to an entity instead and let it drive this module. ## API | Call | Effect | |---|---| | `set(opts)` | Scene-wide settings; omitted fields keep their value | | `setMedium(key, opts)` | Register or move a medium under a stable key | | `removeMedium(key)` | Drop one medium | | `get()` | A copy of the settings in force | | `clear()` | Drop everything and release the passes | | `active()` | Whether the passes are running | ### Settings | Field | Meaning | |---|---| | `sunDirection` | Points **toward** the sun; light travels the other way | | `strength` | How much of the computed attenuation is applied, `[0, 1]` | | `steps` | Samples along each light ray, `[4, 128]` | | `extent` | Half-width of the map in world units; `0` sizes it to the media | | `centre` | Point the map is centred on; omit to centre it on the media | ### Media | Field | Meaning | |---|---| | `position` | World-space centre | | `shape` | `"box"` (default) or `"sphere"` | | `size` | Half-extents, for a box | | `radius` | Radius, for a sphere | | `density` | Extinction per world unit | Up to 32 media are packed into the records buffer. ## Cost and limits The map is 512×512 and is rebuilt each frame; `steps` sets the march length. With nothing registered the passes are released entirely, so a scene with no media pays nothing. The map is sized to the bounding sphere of everything registered, so a small bank sharing a scene with a much larger one lands on few texels — set `extent` and `centre` explicitly to bias the map toward the region that matters. Attenuation applies to surfaces. The sky keeps its own colour.
▲ 0↑ born
·
shadermodule · born here
❒asset
# media_common The media-shadow passes' shared ground: the volume record layout, the transmittance accumulation along a ray, and the cap on how many volumes one frame carries (`MAX_VOLUMES` = 32). That cap sizes a fixed array, so it is a constant here rather than a param.
▲ 0↑ born
·
computeshader · born here
❒asset
# media_shadow_apply Applies the media's transmittance to the lit scene. ## Bindings | name | kind | |------|------| | `source_map` | `texture2d` | | `scene_depth` | `texture_depth` | | `tau_map` | `texture2d` | | `frame_cam` | `buffer` | | `media_params` | `buffer` | | `media_out` | `storage2d (rgba16f)` |
▲ 0↑ born
·
computeshader · born here
❒asset
# media_tau_map Builds the transmittance map through the media, so a later pass reads how much light survives to a point instead of marching for it. ## Includes - `media_common` ## Bindings | name | kind | |------|------| | `media_params` | `buffer` | | `media_volumes` | `buffer` | | `media_tau_out` | `storage2d (rgba16f)` |
▲ 0↑ born
·
preset · born here
❒asset
# cumulus_default Default weather preset for `VolumetricSky.component`. Captures the v4-tuned cumulus look: visible discrete clouds, warm-afternoon sun, moderate coverage. ## Use ```luau local sky = entity.spawn("sky") sky.component.add("VolumetricSky", {}) preset.apply("cumulus_default", { entity = sky.id, component = "VolumetricSky" }) ``` The values set the component's `preset` Field.table as a unit — one Sync replication carries every shaping/lighting parameter to peers. Tweak by reading the values and changing them before the component is added: ```luau local values = preset.values("cumulus_default") values.shaderVariant = "v5" -- swap raymarcher values.preset.coverage = 0.6 -- sparser sky sky.component.add("VolumetricSky", values) ``` ## Authoring more presets Capture a sky you have tuned with `preset.capture({ entity = sky.id, component = "VolumetricSky" }, "<name>")`, or drop another `<name>.preset/` folder next to this one (or anywhere under `/zero/source/`); the asset indexer registers it and `preset.values("<name>")` resolves it. Common alternatives: - `stratocumulus.preset` — broad overcast layer (`coverage ~= 0.15`, `cloudType = 0.5`, `altitudeHigh ~= 0.65`). - `clear.preset` — barely any cloud (`coverage ~= 0.7`, `densityGain ~= 0.2`). - `storm.preset` — dense black anvils (`coverage ~= 0.1`, `densityGain ~= 0.85`, `sunColor ~= [0.6, 0.55, 0.5]`). `preset` is one `Field.table`: applying a preset replaces it whole, so provide every field.
▲ 0↑ born
·
shadermodule · born here
❒asset
# scattering_common The light-scattering passes' shared ground: the phase function, the shadow lookup along a ray, and the froxel-to-world mapping. Included with `#include "@builtin::systems.volumetrics.scattering_common"`.
▲ 0↑ born
✦
shader · born here
❒asset
▲ 0↑ born
✦
shader · born here
❒asset
▲ 0↑ born
·
computeshader · born here
❒asset
# cloud_noise Procedural volumetric-cloud noise fill (Schneider/Hillaire style). A low-frequency FBM gives the base blob silhouette, a high-frequency FBM erodes it near the surface for the fluffy cumulus look, plus altitude shaping (round bottom, full middle, anvil top). Writes the 3D cloud density texture. Bindings (see bindings.yaml): `clouds` (storage3d rgba16f — the cloud volume), `U` (read `array<f32>` uniforms). Usage: dispatch by identity with `compute.dispatchEx("@builtin::systems.volumetrics.shaders.cloud_noise", ...)` (storage texture requires resource binding, not positional buffers) — it resolves to the shader's stable guid and auto-compiles on first use (no setup).
▲ 0↑ born
·
computeshader · born here
❒asset
# cloud_noise_v3 Volumetric-cloud noise fill (Schneider/Hillaire style), v3 variant. Low-frequency FBM base silhouette + high-frequency FBM surface erosion for the billowed cumulus look, with altitude shaping (round bottom, full middle, anvil top). Writes the 3D cloud density texture. Bindings (see bindings.yaml): `clouds` (storage3d rgba16f — the cloud volume), `U` (read `array<f32>` uniforms). Usage: dispatch by identity with `compute.dispatchEx("@builtin::systems.volumetrics.shaders.cloud_noise_v3", ...)` (storage texture requires resource binding, not positional buffers) — it resolves to the shader's stable guid and auto-compiles on first use (no setup).
▲ 0↑ born
·
computeshader · born here
❒asset
# cloud_noise_v5 Volumetric-cloud noise generator, v5 (Nubis/Schneider style). Builds two foundational noises — Perlin-Worley FBM for the lumpy "cauliflower" base shape and Worley FBM for high-frequency detail erosion — under weather-map control. Writes the 3D cloud density texture. Bindings (see bindings.yaml): `clouds` (storage3d rgba16f — the cloud volume), `U` (read `array<f32>` uniforms). Usage: dispatch by identity with `compute.dispatchEx("@builtin::systems.volumetrics.shaders.cloud_noise_v5", ...)` (storage texture requires resource binding, not positional buffers) — it resolves to the shader's stable guid and auto-compiles on first use (no setup).
▲ 0↑ born
·
computeshader · born here
❒asset
# cloud_raymarch Volumetric-sky cloud raymarcher with Frostbite-style lighting: Wrenninge/Hillaire multi-scatter approximation (3 octaves), a 5-sample cone-traced soft self-shadow, and combined Beer-Lambert + Powder so cloud edges glow toward the sun. Reads the cloud volume + occupancy textures and writes the lit raymarch result. Bindings (see bindings.yaml): `clouds` (texture3d), `occupancy` (texture3d), `cloud_smp` (sampler), `out_tex` (storage2d rgba16f), `U` (read `array<f32>` uniforms). Usage: dispatch by identity with `compute.dispatchEx("@builtin::systems.volumetrics.shaders.cloud_raymarch", ...)` (textures/sampler/storage texture require resource binding, not positional buffers) — it resolves to the shader's stable guid and auto-compiles on first use (no setup).
▲ 0↑ born
·
computeshader · born here
❒asset
# cloud_raymarch_v3 Volumetric-sky cloud raymarcher (Frostbite-style lighting), v3 variant: Wrenninge/Hillaire multi-scatter (3 octaves), 5-sample cone-traced soft shadow, and combined Beer-Lambert + Powder edge glow. Reads the cloud volume + occupancy textures and writes the lit raymarch result. Bindings (see bindings.yaml): `clouds` (texture3d), `occupancy` (texture3d), `cloud_smp` (sampler), `out_tex` (storage2d rgba16f), `U` (read `array<f32>` uniforms). Usage: dispatch by identity with `compute.dispatchEx("@builtin::systems.volumetrics.shaders.cloud_raymarch_v3", ...)` (textures/sampler/storage texture require resource binding, not positional buffers) — it resolves to the shader's stable guid and auto-compiles on first use (no setup).
▲ 0↑ born
·
computeshader · born here
❒asset
# cloud_raymarch_v4 Volumetric-sky cloud raymarcher, v4. Adds procedural detail noise computed in-shader on top of the large-scale texture silhouette (removes v3's voxel-grid blockiness) and uses multi-distance shadow sampling (4 geometrically spaced strides) instead of a cone trace. Reads the cloud volume + occupancy textures and writes the lit raymarch result. Bindings (see bindings.yaml): `clouds` (texture3d), `occupancy` (texture3d), `cloud_smp` (sampler), `out_tex` (storage2d rgba16f), `U` (read `array<f32>` uniforms). Usage: dispatch by identity with `compute.dispatchEx("@builtin::systems.volumetrics.shaders.cloud_raymarch_v4", ...)` (textures/sampler/storage texture require resource binding, not positional buffers) — it resolves to the shader's stable guid and auto-compiles on first use (no setup).
▲ 0↑ born
·
computeshader · born here
❒asset
# cloud_raymarch_v5 Production volumetric-cloud raymarcher, v5 (Nubis/Hillaire style): Hillaire multi-scatter octaves (fixes black cloud cores), energy-conserving Beer-Powder-MS lit-edge glow, a 6-offset Wrenninge cone-trace shadow, and a dual-lobe Henyey-Greenstein phase. Reads the cloud volume + occupancy textures and writes the lit raymarch result. Bindings (see bindings.yaml): `clouds` (texture3d), `occupancy` (texture3d), `cloud_smp` (sampler), `out_tex` (storage2d rgba16f), `U` (read `array<f32>` uniforms). Usage: dispatch by identity with `compute.dispatchEx("@builtin::systems.volumetrics.shaders.cloud_raymarch_v5", ...)` (textures/sampler/storage texture require resource binding, not positional buffers) — it resolves to the shader's stable guid and auto-compiles on first use (no setup).
▲ 0↑ born
✦
shader · born here
❒asset
▲ 0↑ born
·
computeshader · born here
❒asset
# cloud_taa Cloud TAA composite — temporal anti-aliasing for the raymarch output. Reprojects last frame's blended output through the previous view-projection, color-clamps the history to the current sample's neighborhood to suppress ghosting, and exponentially blends the two. Writes both the next-frame history slot and the externally-sampled `scene_volumetric_sky` texture the composite path reads. Reprojection runs through the world point named by uniform `[37]`, the NDC z of the range the cloud stands at along the view ray — `volumetricSky.reprojectionDepthNdc` computes it. The weight the history carries falls to zero across the last `REPROJECT_BORDER_TEXELS` of the reprojected frame, so a pixel whose history lies off the previous frame reaches the raymarch's own output down a gradient rather than across a line. Bindings (see bindings.yaml — slot order must match the Luau dispatch): `cur_tex` (texture2d), `hist_prev` (texture2d), `smp` (sampler), `out_hist` (storage2d rgba16f), `out_scene` (storage2d rgba16f), `U` (read `array<f32>` uniforms). Usage: dispatch by identity with `compute.dispatchEx("@builtin::systems.volumetrics.shaders.cloud_taa", ...)` (textures/sampler/storage textures require resource binding, not positional buffers) — it resolves to the shader's stable guid and auto-compiles on first use (no setup).
▲ 0↑ born
✦
shader · born here
❒asset
▲ 0↑ born
·
computeshader · born here
❒asset
# volume_pack Fills the brick pool from the volume `volume_pages` has just paged. One invocation per pool voxel. The pool is a grid of blocks, one block per slot; a block holds a brick plus a one-voxel apron taken from the neighbouring bricks, so a trilinear sample inside a brick reads what the dense volume would have given it rather than whatever brick the pool stores next door. ## Bindings | # | name | what | |---|---|---| | 0 | `source` | the dense volume being paged (sampled 3D texture) | | 1 | `atlas` | the brick pool (`rgba16f` storage) | | 2 | `page_slots` | four words per slot, written by `volume_pages` | | 3 | `K` | `PackParams` — volume dimensions and brick edge | ## Why the pool is always `rgba16f` A pool is written as a storage texture and sampled with filtering. `rgba16f` is the format that is both: the one-channel formats cannot be written as storage textures, and `r32f` cannot be filtered without a device feature the engine does not require. So a scalar volume's density rides in the pool's red channel — which is where the raymarcher reads density from in density mode — and pays `rgba16f` per resident voxel. `Volume:sparseStats()` reports the dense and resident sizes side by side rather than assuming paging is the smaller of the two. The pass writes every voxel of the pool, including blocks whose slot was never handed out: those hold whatever brick their stale slot entry names, and the page table never points at them.
▲ 0↑ born
·
computeshader · born here
❒asset
# volume_pages Decides which bricks of a volume are worth storing, and where each one lives. One invocation per brick. It scans the brick's voxels in the source volume, and a brick holding anything over `threshold` claims a slot from an atomic counter. The slot number lands in the page table (`slot + 1`, so 0 keeps meaning "empty"), and the brick's grid coordinates land in the slot list so `volume_pack` knows what to copy into that slot. Nothing is read back to the CPU and no brick waits on another, so the pass can run every frame — which is what makes the page table usable as the acceleration structure for a volume whose contents change while it renders. ## Bindings | # | name | what | |---|---|---| | 0 | `source` | the dense volume being paged (sampled 3D texture) | | 1 | `pages` | page table, one texel per brick (`rgba16f` storage) | | 2 | `page_alloc` | `[0]` slots handed out, `[1]` bricks that found the pool full | | 3 | `page_slots` | four words per slot: the brick's x, y, z, and 1 | | 4 | `P` | `PageParams` — volume dimensions, brick edge, threshold, channel, capacity | `page_alloc` must be zeroed before each run; the counter is what allocates, so a stale count hands out slots past the pool. ## The page value A resident brick's page value is `slot + 1`, split into two digits base 1024 across the texel's red and green channels. The page table is `rgba16f` because that is the format that is both storage-writable and filterable — the raymarcher binds it where a sampled 3D texture is expected — and a half float only carries integers exactly up to 2048, which one digit stays under. A brick holding nothing writes zero in both channels, so "is anything here" is the sum against zero, the same test the older single-channel occupancy grid answers. ## Overflow A brick that arrives after the pool is full is counted in `page_alloc[1]` and written as empty. The volume renders without it rather than sharing a slot. `page_alloc[0]` keeps counting past the capacity, so a caller that reads it back learns the capacity the volume actually needs.
▲ 0↑ born
·
computeshader · born here
❒asset
# volume_raymarch Generic volume raymarcher. One invocation per output pixel: builds a world-space ray from the camera inverse view-projection, clips against the volume AABB, and marches front-to-back, using the occupancy grid to skip empty bricks. Output is RGBA16F (premultiplied RGB, accumulated alpha) for the caller to composite. A paged volume marches through the same shader: `volume` is then the brick pool and `occupancy` the page table, whose texel per brick carries the pool slot that brick lives in (0 for a brick holding nothing, which is what the empty-space skip already reads it as). `VolumeUniforms.paged` carries the flag and the volume's own dimensions, which the pool no longer describes. Both the skip and the pool lookup find a point's brick by the voxel it lands on divided by the brick edge — the way the page table and the occupancy grid were written. A volume that is not a whole number of bricks across has a grid wider than `dims / brick`, so dividing the normalized coordinate by the grid instead would name a different brick for the same point. When the dimensions are unknown (a wrapper was never told them) the grid times the brick edge stands in for them. Bindings (see bindings.yaml): `volume` (texture3d), `occupancy` (texture3d), `volume_smp` (sampler), `out_tex` (storage2d rgba16f), `U` (read buffer, a single `VolumeUniforms` struct). Usage: dispatch by identity with `compute.dispatchEx("@builtin::systems.volumetrics.shaders.volume_raymarch", ...)` (textures/samplers/storage texture require resource binding, not positional buffers) — it resolves to the shader's stable guid and auto-compiles on first use (no setup).
▲ 0↑ born
·
computeshader · born here
❒asset
# vol_composite Composites the scattered light over the scene, depth-aware so geometry occludes it. ## Includes - `scattering_common` ## Bindings | name | kind | |------|------| | `scene_color` | `texture2d` | | `scene_depth` | `texture_depth` | | `scatter_map` | `texture2d` | | `vol_params` | `buffer` | | `vol_out` | `storage2d (rgba16f)` |
▲ 0↑ born
·
computeshader · born here
❒asset
# vol_scatter Accumulates in-scattered light through the volume, one froxel at a time. ## Includes - `scattering_common` ## Bindings | name | kind | |------|------| | `scene_depth` | `texture_depth` | | `frame_cam` | `buffer` | | `lights` | `buffer` | | `vol_params` | `buffer` | | `scatter_out` | `storage2d (rgba16f)` |
▲ 0↑ born
▣
module · born here
❒asset
# volume High-level Luau API for 3D textures and volumetric rendering. Composes the engine's generic `compute.*` primitives into volume-specific helpers — procedural builders (`sphere`, `box`, `noise`), a sparse NanoVDB-style occupancy grid for empty-space skipping, and a one-call raymarcher that composites a volume through any entity's transform. Pairs with the `Volume` component which attaches a built (or imported) texture to an entity. ## Exports - `M.create({name, width, height, depth, format?, storage?}) -> Volume` — allocate a fresh, zero-filled 3D texture and return a Volume wrapper. - `M.wrap({name, format, occupancyName?, occupancyBrick?}) -> Volume` — wrap an existing named texture without recreating it (used by the .zvol viewer). - `M.get(name) -> Volume?` — the live volume built under that texture name, with its dimensions, channel count, occupancy grid and brick pool. - `M.names() -> {string}` — every name `M.get` answers to, sorted. - `M.register(volume) -> Volume` — publish a wrapped volume under its own texture name. - `M.unregister(name) -> Volume?` — stop answering for a name and hand back the volume that held it, leaving its GPU resources alone. - `M.releaseTarget(name) -> boolean` — destroy a render target this module built for `render` and forget its size. - `M.sphere(opts) -> Volume` — procedural Volume filled with a soft sphere SDF. - `M.box(opts) -> Volume` — procedural Volume filled with a soft box SDF. - `M.noise(opts) -> Volume` — procedural Volume filled with FBM noise. - `M.raymarchShaderIdentity() -> string` — absolute identity of the shared raymarch shader. - `M.compositeShaderSource() -> string` — WGSL source for the post-process composite blending `scene_volume_rt` over the scene. - `M.FORMATS` — table of canonical format strings (`R8`, `R16F`, `R32F`, `RGBA8`, `RGBA16F`, `RGBA32F`). - `volume:destroy()` — release the GPU texture + occupancy. - `volume:writeBytes(bytes) / :writeFloats(floats)` — upload payload to the 3D texture (matches the texture's format). - `volume:fill(fn)` — CPU procedural fill via `fn(x,y,z) -> color`. - `volume:readback() -> bytes` — pull the texture's current contents back to Luau. - `volume:buildOccupancy({brickSize, sampler?})` — build an R8 occupancy companion for empty-space skipping. - `volume:buildSparse({brickSize?, threshold?, headroom?, capacity?, mode?})` — page the volume into bricks, keeping only the ones holding something. Yields while the brick count reads back — twice unless `capacity` says how many slots to take. - `volume:refreshSparse()` — re-page from the volume's current contents; two dispatches and no wait. - `volume:releaseDense()` — free the dense texture and render from the brick pool alone. - `volume:releaseSparse()` — free the brick pool and page table and go back to rendering the dense texture. - `volume:readSparseCounts()` — `{ bricks, wanted, overflow, capacity }` off the GPU; yields. - `volume:sparseStats()` — byte accounting: `denseBytes`, `poolBytes`, `pageBytes`, `residentBytes`, `ratio`. - `volume:fillCompute(shaderName, opts)` — drive a custom fill shader against this volume. - `volume:render(opts)` — dispatch the shared compute raymarcher into a 2D storage target, using `opts.entity` for AABB. A paged volume renders through its pool with no change at the call site. ## Usage ```luau local Volume = require("@builtin::systems.volumetrics.volume") -- Build a procedural noise cloud + occupancy. local cloud = Volume.noise({ name = "cloud", size = 64, scale = 4, octaves = 4, threshold = 0.55, }) cloud:buildOccupancy({ brickSize = 8 }) -- Render through the shared raymarcher; output to scene_volume_rt. cloud:render({ entity = entity(id), target = "scene_volume_rt", targetWidth = 1280, targetHeight = 720, density = 8.0, absorption = 1.2, emission = 0.9, color = { 1.0, 0.9, 0.7 }, maxSteps = 256, mode = "rgba", }) ``` The `Volume.component` is the per-entity wrapper around this module — it resolves the name it carries through `Volume.get` and dispatches `volume:render` from its `update` hook. ## Addressing a volume by name Every volume this module builds answers to its texture name, so anything holding only the name — an entity's `Volume` component, a second script, a tool — reaches the volume itself: ```luau local cloud = Volume.noise({ name = "cloud", size = 128 }) cloud:buildSparse({ brickSize = 8 }) cloud:releaseDense() -- somewhere else entirely Volume.get("cloud"):render({ entity = entity.find("cloud_host") }) ``` What the name alone cannot express is what makes this matter: the voxel dimensions, which channel holds density, and whether the volume is paged into a brick pool. A holder that reconstructs a wrapper from the name gets a dense volume that the paging may already have released. One volume owns a name at a time: the 3D texture, the page table and the brick pool all address it, so `create` over a name a live volume holds releases that volume first and says so in the log. `destroy` clears the entry. A wrapper around a texture another system owns is published with `Volume.register` and dropped with `Volume.unregister`, which hands the volume back so it can still be destroyed. The names live where this module is loaded — one set per Luau VM, alongside the raymarch target registry. `render` builds its `target` on the first call that names one and keeps its size, so repeated renders reuse it; `Volume.releaseTarget(name)` destroys that target and forgets the size, and the next render builds it again. ## Paging a volume into bricks A dense volume costs its bounding box. `buildSparse` splits it into bricks, keeps only the ones holding anything over `threshold`, and stores those in a brick pool; a page table one texel per brick says which slot each resident brick took, and 0 says the brick holds nothing. That page table is also the grid the raymarcher already skips empty space with, so paging and empty-space skipping are the same structure. ```luau local cloud = Volume.noise({ name = "cloud", size = 128, threshold = 0.6 }) local stats = cloud:buildSparse({ brickSize = 8, threshold = 0.01 }) print(stats.bricks, "of", stats.grid[1] * stats.grid[2] * stats.grid[3], "bricks") print(stats.poolBytes, "vs dense", stats.denseBytes) cloud:releaseDense() -- the pool is now the only copy cloud:render({ entity = entity.find("cloud_host") }) ``` Both passes run on the GPU with no readback between them, so a volume a compute shader rewrites keeps a matching page table for the cost of two dispatches: ```luau cloud:fillCompute(myFillShader, { extraResources = { { kind = "buffer", name = fillU } } }) cloud:refreshSparse() ``` The pool is fixed at the size the build measured, so a volume that grows past it loses the surplus bricks. `refreshSparse` asks the GPU for the counts without waiting on them and folds them in on a later call, so `sparseStats()` reports the overflow — and the log carries a warning naming the capacity to page at — a frame or two after the refresh that caused it. `buildSparse`'s `headroom` buys the slots up front for a volume that is expected to grow. What it costs: - A block in the pool is the brick plus a one-voxel apron on every side, so a resident brick costs `(brick + 2)³` voxels. At `brickSize = 8` that is 1.95x per resident brick, at 16 it is 1.42x. The apron is what makes a trilinear sample inside a brick read exactly what the dense volume would have given it. - A pool is always `rgba16f`: it has to be writable as a storage texture and sampled with filtering, and that is the format that is both. A scalar volume therefore spends more per resident voxel than it did dense (its density rides in the pool's red channel), so `sparseStats()` reports `denseBytes` and `residentBytes` side by side rather than assuming paging is the smaller of the two. - `refreshSparse` re-pages from the dense volume, so a volume that will keep changing keeps its dense copy; `releaseDense` is for one that is finished. - Classification reads one channel: alpha for a four-channel volume, red for a scalar one. A four-channel volume that carries density in red — the one `render`'s `mode = "density"` marches — has to say so, with the same word: `buildSparse({ mode = "density" })`. A `render` that names no `mode` marches a paged volume from the channel its pool was filled from, so the two agree without the caller repeating the choice. ## Notes - Volumes are GPU-owned; the Luau wrapper just holds the named handle. `volume:destroy()` releases both the texture and any occupancy companion. - The composite target `scene_volume_rt` is shared across every Volume entity in the scene, so multiple volumes composite into one image. Override `opts.target` if you need separate render targets per volume. - Occupancy grids use a brick size (default 8) — smaller = more precise skipping at the cost of grid memory. - The `vdb_to_zvol.importer` in this package produces dense voxel buffers consumable by `compute.writeFloatsTexture3D`, which can be wrapped via `Volume.wrap` (see the `zvol_viewer.scene/` demo for the full path).
▲ 0↑ born
▣
module · born here
❒asset
# volumeDemos Shared orchestration backend for the `volumeDemos.package` showcase scenes. Owns the per-demo resource pack (3D texture, 2D render target, two uniform buffers) and the auto-registered composite, exposing a small public surface the demo scenes drive once per frame. ## Exports - `M.spawn({name, size, fillShader, fillUniformBytes}) -> ()` — register a new demo pipeline. Creates the 3D volume, the 2D storage target, registers the per-demo fill shader, ensures the shared raymarcher is registered, and auto-installs the depth-aware composite. - `M.destroy(name) -> ()` — release everything `spawn` created (texture + render target + buffers + composite registration). - `M.dispatchFill(name, fillUniforms) -> ()` — pack `fillUniforms` (Lua array of f32) into the demo's fill UBO and dispatch the fill shader against the volume. - `M.dispatchFrame(name, {camera, aabbMin, aabbMax, width, height, stepSize, maxSteps, alphaCutoff, densityGain, emissionGain}) -> ()` — pack camera + raymarch params into the shared 40-float UBO and dispatch the raymarcher into the demo's render target. - `M.NAMES(name) -> {volume, rt, composite}` — engine-side resource names for the demo (used by the `.zvol` viewer to bypass the helpers and upload voxels via `compute.writeFloatsTexture3D` directly). ## Usage ```luau -- From a demo scene at <pkg>/demo/<name>.scene/entrypoint.luau, -- `...` climbs to the package root, then resolves volumeDemos.module: local V = require("@builtin::systems.volumetrics.volumeDemos") -- onLoad: spin up the pipeline. V.spawn({ name = "smoke_plume", size = { 96, 192, 96 }, fillShader = SMOKE_FBM_WGSL, fillUniformBytes = 64, }) -- update(dt): repaint the volume + raymarch into the RT. V.dispatchFill("smoke_plume", { elapsed, density_amp, spread, ... }) V.dispatchFrame("smoke_plume", { camera = cam.get(), aabbMin = { -40, 0, -40 }, aabbMax = { 40, 160, 40 }, width = 1280, height = 720, stepSize = 0.9, maxSteps = 240, densityGain = 2.5, emissionGain = 1.0, alphaCutoff = 0.995, }) -- onUnload: free GPU resources. V.destroy("smoke_plume") ``` ## Notes - `dispatchFrame` packs 40 f32s into the raymarch UBO: `[0..16)` inv_view_proj (mat4 row-major), `[16..19)` aabb_min, `[19..22)` aabb_max, `[22..23)` step_size, `[23..24)` max_steps, `[24..25)` alpha_cutoff, `[25..26)` density_gain, `[26..30)` target_width/height + inv reciprocals, `[30..31)` emission_gain, `[31..40)` padding. - Pipeline state lives in module-locals keyed by `name` — the same module instance can host many demos in parallel, but two scenes must not pick the same `name`. - Each demo scene supplies its own fill shader; the raymarcher is shared (one shader name, one compiled pipeline). Replacing `fillShader` between frames is supported; replacing the raymarcher isn't (use the volumetrics package for that use case).
▲ 0↑ born
▣
module · born here
❒asset
# volumeSequence Playback for a run of `.zvol` frames, through a ring of resident volumes. A sequence names its frames as VFS paths in playback order. It allocates `resident` 3D textures — one frame each — and nothing else: the frames the playhead is about to reach are read off the VFS on their own tasks and uploaded into whichever ring slot the playhead has travelled furthest past. So a 240-frame run and a 4-frame run cost the same on the GPU, and what a sequence costs is decided when it is opened rather than by how long it is. ```luau local Seq = require("@builtin::systems.volumetrics.volumeSequence") local frames = {} for i = 0, 239 do frames[#frames + 1] = string.format("/source/sim/plume_%04d.zvol", i) end local seq = Seq.open({ name = "plume", frames = frames, resident = 3, fps = 24 }) local s = seq:stats() print(s.residentBytes, s.allBytes) --> 3145728 251658240 ``` Drive it from a component, which is also what makes it visible: ```luau entity(id).component.add("VolumeSequence", { sequenceName = "plume", density = 6.0 }) ``` or by hand, if the caller already has a tick of its own: ```luau seq:tick(dt) local frame = seq:current() if frame then frame:render({ entity = entity(id), density = 6.0 }) end ``` The playhead advances once per engine frame however many callers tick the sequence — a component and a script, or two entities naming one run, share one playhead rather than each moving it. Every call still promotes the frame that has landed and starts the reads ahead of it. ## Residency Every frame of a run has the dimensions frame 1 declares, so one ring serves the whole run. `stats()` reports both what the ring holds (`residentBytes`) and what holding every frame at once would cost (`allBytes`); `residency()` says which frame each slot holds right now. The ring needs at least two slots for a multi-frame run — one holds the picture on screen while the other takes the read — and `open` raises the count to two if it was asked for less. `prefetch` says how many frames ahead of the playhead reads are started, and defaults to every slot the ring has spare — a slot left empty is a frame that gets waited on later. It is bounded by those same slots, so asking for more than fits reads as far ahead as it can. ## Keeping up A tick that wants a frame no slot holds yet keeps showing the frame it had and counts a `stall`. Playback is therefore never blank and never blocks, and `stats().stalls` is the number that says whether the ring and the prefetch distance are big enough for the rate: a run whose reads keep up shows a stall count that stops rising once the ring has filled. `errors` counts frames that failed to read or whose header disagreed with the ring. Each one is counted and logged once, with the path and what was wrong with it, and the run steps over it from then on rather than re-reading it every tick — so `errors` answers how many frames of the run are broken, and a mistyped path costs one line in the log rather than one per frame. The rate and the playhead are the sequence's, not a caller's: `setFps` keeps the frame the playhead stands on and the fraction of the way through it, and `seek` lands on exactly the frame asked for.
▲ 0↑ born
▣
module · born here
❒asset
# volumetricLighting The settings behind the `VolumetricLighting` component, and the way to drive the scene's participating medium from a script — fog that rolls in over a cutscene, a quality slider that drops the step count, a room that fills with smoke. ```lua local volumetricLighting = require("@builtin::systems.volumetrics.volumetricLighting") volumetricLighting.set({ density = 0.06, anisotropy = 0.7 }) volumetricLighting.get() -- a copy of the settings in force volumetricLighting.active() -- whether the march is running volumetricLighting.clear() -- empty the air, keep the settings ``` `set` takes any subset of the fields; whatever is omitted keeps its current value. A `density` of 0 releases the render feature rather than marching a medium that scatters nothing, and `clear` is the named way to say the same thing. `sunDirection` tracks the scene's sun on its own. Pin it through `set` when a scene wants the medium lit from somewhere else. The module packs the settings into the `vol_params` buffer the passes read, so a change lands on the next frame without recompiling anything. ## Cost The march is the expensive part, and its cost is `steps` × (`shadowSteps` + the number of live scene lights) per marched pixel. Those are the two knobs to reach for on a budget: `steps` controls how cleanly a beam's edge resolves, and `shadowSteps` controls whether the sun's contribution is shadowed at all. Setting `shadowSteps` to 0 removes the per-sample visibility work entirely, which is the single largest saving available — at the cost of the shafts, since the medium is then lit evenly wherever it reaches.
▲ 0↑ born
▣
module · born here
❒asset
# volumetricSky Orchestration backend for the `VolumetricSky.component`. Owns the GPU resources the cloud layer needs (cloud volume, raymarch render target, ping-pong TAA history pair, post-process composite) and exposes a small public surface the component drives every frame. ## Exports - `M.spawn(opts?) -> id` — spawn the singleton sky entity, or configure the one already standing, and return its id. Every weather parameter (`coverage`, `sunDir`, `sunColor`, `sunIntensity`, `windSpeed`, …) is settable at the top level or inside a `preset` sub-table; every `VolumetricSky` component field (`shaderVariant`, `cloudSize`, `taaBlendAlpha`, …) by its own name; `name` names the entity. Parameters left out keep their current values. An option that names neither raises, listing both sets, and leaves no entity behind. A weather parameter takes the shape its default carries — a number, or an array of that many numbers — and a value of another shape raises naming the parameter. - `M.configure(opts) -> boolean` — apply those same weather parameters and component fields to the sky already standing. Returns `false` when none has been spawned. - `M.defaultPreset() -> table` — a fresh copy of the weather configuration a sky spawned with no options carries. The copy is the caller's to edit. - `M.destroy() -> ()` — release every resource `spawn` created. - `M.fillClouds(noiseParams) -> boolean`: bake the cloud volume with the active `cloud_noise_*` shader when what it holds is not what `noiseParams` describe, and return whether this call baked. A change to a shaping parameter, the colour, the seed, the size, the variant or the render device bakes at once, and a write to the noise shader's `shader.wgsl` or `bindings.yaml` bakes once the program it compiles is resident; a change to the wind alone bakes at most `noiseParams.noiseRegenRate` times per second (default `M.DEFAULT_NOISE_REGEN_RATE`, `0` = every call the wind moved) on `noiseParams.clock` (default `getTime()`). `noiseParams` is the flat table produced by `VolumetricSky.component`'s `snapshotNoiseParams()`. - `M.volumeBakes() -> number`: how many times the cloud volume has been baked since the module loaded. - `M.dispatchRaymarch(renderParams, viewport) -> ()` — dispatch the active `cloud_raymarch_*` shader, blend with history via the TAA pass, and write into the composite target. `renderParams` is the flat table from `snapshotRenderParams()`. - `M.find() -> id?` — return the id of the active VolumetricSky entity, or `nil` if none has been spawned by `M.spawn`. - `M.despawn() -> ()` — despawn the entity `M.spawn` minted (does NOT free GPU resources — call `M.destroy()` for that). - `M.setCoverage(c) / M.setDensity(g) / M.setSun(dir, color, intensity?) / M.setWind(v)` — convenience setters that route through the active component's `public` fields. - `M.reprojectionDepthNdc(camPos, fwd, aabbMin, aabbMax, near, far) -> number` — the NDC z the TAA pass carries history through, taken from where the cloud stands along the centre view ray. A point at range `d` travels `translation / d` across the screen, so this is what keeps a translating camera's history landing on the part of the picture the cloud moved to. ## Usage ```luau -- The component drives this module automatically. Direct M.* use is -- rare; the canonical entry point is attaching VolumetricSky to an -- entity (or loading a preset and passing it to component.add): local sky = entity.spawn("sky") sky.component.add("VolumetricSky", {}) -- Driving the layer by hand instead, a frame at a time. Each dispatch -- creates the resources it needs at the size its own arguments name — -- the cloud volume from `noiseParams.cloudSize / cloudHeight`, the -- render target and composite from the viewport: local Sky = require("@builtin::systems.volumetrics.volumetricSky") Sky.fillClouds(noiseParams) Sky.dispatchRaymarch(renderParams, { width = 1280, height = 720 }) ``` ## Notes - `shaderVariant` selects which compute shader pair (`cloud_noise_v3 + cloud_raymarch_v3` / `cloud_noise + cloud_raymarch_v4` / `cloud_noise_v5 + cloud_raymarch_v5`). The module resolves the variant strings to absolute identities — relative paths don't work from a module body (the chunkname retains an `.init` segment that throws off the rsplit-to-parent step in `resolve_relative_dot`). - The cloud volume + history pair are RGBA16F — 256×128×256 ≈ 32 MiB VRAM. Drop to 192/96 via `cloudSize` / `cloudHeight` if tight. - The TAA pass is a single compute dispatch blending current sample with the previous frame's RT through the history pair. Set `taaBlendAlpha = 1.0` to disable while still going through the same code path (useful for benchmarking). - All public setters write through to `VolumetricSky.component`'s `public` fields, which means they replicate as Sync if the component is sync-enabled. Tweaks survive scene reloads as long as the same entity is alive. - `SkyPreset` and its defaults are declared here, and the component declares its `preset` field from them. `M.spawn` / `M.configure` read that same key set to tell a weather parameter from a component field, so a parameter added to the preset reaches the schema and the option routing in one edit.
▲ 0↑ born
·
renderfeature · born here
❒asset
# volumetricSky The cloud layer's render passes. The layer is raymarched for the camera being drawn, copied into the target the composite reads, temporally accumulated where there is a previous frame of that view to accumulate against, and laid over the frame behind whatever the depth buffer holds. Every pass but the accumulation runs for each view the frame draws, an environment cube's six faces included — which is what puts the clouds into reflections. The accumulation blends against the previous frame of the same view, and a cube face is rendered once with no previous frame, so it declares `environmentCapture = false` and the copy before it is what those faces read. `VolumetricSky` creates this feature; it is not meant to be created by hand.
▲ 0↑ born
·
assettype · born here
❒asset
# Package (asset type) A package is an asset — a `<Name>.package/` folder that bundles components, modules, scenes, presets, tools, and any other typed sub-assets under one identity, installed into a world as one unit. Packages are how authoring sets get distributed across worlds. A **library** is the addressable root scope content resolves under (`@builtin`, `@<name>`) — a whole world mounted into this one under a namespace, via `zeromind.installLib` / `world.installLibrary`. A package is an asset that lives inside such a root, and one library holds as many packages as its author put there. A package is installed by guid the way every asset is (`zeromind.installAsset`). ## When to use one - You're authoring a coherent feature suite (character controller stack, voxel engine, UI kit, animation library) and want to ship it as one install target. - You want a stable identity worlds can install and resolve sub-assets against (`@<lib>::pkg.sub_module`). - You want versioning + a per-package metadata layer that travels with the contents. If you're authoring inside a single world and don't need a separate install lifecycle, you don't need a package — drop your `.component` / `.module` / etc. directly in `/zero/source/`. Packages are for cross-world distribution. ## Where it lives - Source: `/zero/source/.../<Name>.package/` (or, for legacy packages, a folder containing `package.yaml` at its root). - Identity: `<Name>` (the `.package` suffix collapses in identity — mirrors `.module`). A file at `<Name>.package/<sub>/init.luau` resolves under the same identity it would at `<Name>/<sub>/init.luau`. - Folder shape (container-style — open child set): - `package.yaml`: metadata (name, version, description) and the packages it depends on (`dependencies`). **Required.** - `README.md` — instance documentation. **Required.** - `.metadata` — agent-editable tags + free-form fields. **Required.** - Sub-asset folders: any combination of `.component/`, `.module/`, `.scene/`, `.preset/`, `.tool/`, `.toolbox/`, `.material/`, `.shader/`, etc. The validator permits unlisted children (`allow_unlisted: true`) by design — packages are open containers. ## How to create one ```luau asset.create("package", "<Name>") -- Creates: /zero/source/<Name>.package/ -- package.yaml (metadata scaffold) -- README.md (instance README template) -- `folder` places it in a subfolder of /source instead of the root: asset.create("package", "<Name>", { folder = "systems" }) ``` Then scaffold sub-assets inside it: ```luau local pkg = asset.resolve("<Name>.package") asset.create("component", "Spinner", { into = pkg }) asset.create("module", "util", { into = pkg }) ``` ## `package.yaml` ```yaml name: inkBlocks # optional; the folder stem when absent version: "1.0.3" description: "Ink and paper style blocks" dependencies: - { name: starterBlocks, version: "1.0.8" } - styleKit # a bare name states no version ``` `dependencies` lists the packages this package needs. Each entry's `name` is a reference the package records, the same kind of reference a `require` or an asset name in a data file makes: - **It resolves in the author's world.** Install the dependency here first (`zeromind.installAsset`); the entry then names the copy this world holds. A push refuses a package whose dependency resolves to nothing, naming it. - **An install brings it along.** Installing the package installs every dependency it lists, at the version this world holds when the package is pushed, under `/source/deps/<owner world>/`, deduplicated against what the installing world already holds. `zeromind.preview` lists each one before anything is written, and the install's `deps` lists what it brought. Uninstalling the package leaves a dependency that another installed package still uses. - **`version` is checked, not chosen.** A stated `version` has to equal the `version` the dependency's own `package.yaml` states in this world; `asset.validate` (and so the push) refuses the package naming both when they differ. To depend on another version, install that version here. ## How it operates 1. **Registration.** Writing `package.yaml` indexes the package with the package registry. 2. **Install.** A package is read before it is installed: `zm ls-tree @<guid>` in the shell lists what it contains, and `zm show @<guid>:<path>` and `zm grep <pattern> @<guid>` read it, so candidates are compared by what they do. The one that fits is installed by guid, the way every asset is: `zeromind_install` over the engine's MCP, or `tools.use("zeromind", "installAsset", <guid>)` inside the engine. The install writes the package's files (and the deps its `.refs` record, the packages its `package.yaml` lists under `dependencies` included) into this world's source, so its sub-assets resolve here. `tools.use("zeromind", "preview", <guid>)` reports what an install would write before it writes anything. 3. **Sub-asset resolution.** Because `.package` collapses in identity, sub-asset paths work whether the package folder is suffixed (`<Name>.package/<sub>`) or not (`<Name>/<sub>`). 4. **Updates.** `tools.use("zeromind", "updates")` lists installed content with upstream changes available; re-running the install pulls them. 5. **Metadata.** `package.yaml::name` overrides the folder stem if present (rare); `version` + `description` are surfaced in the package index. 6. **Hot reload.** Edits to sub-assets hot-reload as normal; edits to `package.yaml` reload package metadata. ## Discovery - `asset.list("package")` — every registered package. - `asset.inspect("<name>")` — sub-asset catalog, source, this type README. - `ls /zero/source/<Name>.package` lists the files the folder holds. - `library.list()` — the library assets registered in this world, including the ones packages under an installed library contribute. ## Authoring conventions - Name packages by their feature (`voxels`, `cameraRigs`, `dialogSystem`), not by their owner — the host library is the `@<lib>::` scope prefix. - Cluster cohesive sub-assets together. A "character controller" package should ship the controller component, its preset configs, its supporting modules, and a demo scene — not just the component. - Document the package's surface in `README.md` — what install installs, what each sub-asset does, how they relate. Agents reading `cat /zero/source/<Name>.package/README.md` get the full picture in one shot. - Version conservatively — breaking a package means breaking every world that installed it. Use semantic version bumps. ## Common pitfalls - **Missing `package.yaml`.** Without it the folder isn't recognized as a package even if it has a `.package/` suffix. - **A dependency named only by a string the code builds.** A package whose files reach another package only through a name assembled at runtime records no reference to it, so an install arrives without it. List that package under `dependencies` in `package.yaml`. - **Identity collapse.** Sub-asset paths skip the `.package` suffix. Don't include `.package` in `require` strings. ## Referencing inside a package: the `~` rule `~` means **this package**, resolved structurally from the referring file's own location. It is the only package-specific syntax. It is rename-safe (no package name is ever written) and library-agnostic (it never names the host library, so it survives `@builtin` being renamed or the package being mounted elsewhere). `~.tail` maps to `<pkg>.package/tail` from anywhere inside the package. | Referencing from | Use | Example | |---|---|---| | An asset/module **inside this package** | `~.tail` | `require("~.modules.myThing")`, `shader: "~.shaders.myThingBlend"` | | A **different package** under the same root | root-relative identity | `require("@root::systems.otherPkg.modules.x")` | | A **different library's** public API (a deliberate external dependency) | `@otherlib::…` | `require("@physics::modules.rigidbody")` | **Never write `@builtin::` (or any host-library name) to reference your own package** — that couples the package to the library it currently lives in and breaks if the library is renamed or the files move. Use `~.`. It works from Luau `require()` and from asset-reference strings in data files (a material's `mat.yaml` `shader:` field, a component's default asset ref); the engine resolves it against the referring file's own package. `asset.resolve` takes a `~.` name the same way, against the package of the file that calls it: `asset.resolve("~.materials.myThingStandard", "material")`. `~.` is the package-relative self-reference; `@root::` is the root-relative one (the library root, or the world source tree for non-library content). The `core/requiring-modules` guide covers the root model in full. A package that ships substantial functionality should include a `.guide` asset (a `<name>.guide/guide.md` folder anywhere under the package); the `guides` tool discovers it live, so collaborators find the system the way they find built-in guides. A package's test suite reaches the shared `Test` framework through its enclosing asset: `local Test = asset.containing(__FILE__).modules.shared` (`asset.containing` returns the deepest enclosing typed asset, the `.testSuite`, not the outer `.package`). ## Related types - Any of `.component`, `.module`, `.scene`, `.preset`, `.tool`, `.toolbox`, `.material`, `.shader`, `.style`, `.service`, `.importer` — packages bundle these. ## In the Inspector Package: the name, version and description package.yaml declares, how many things the package holds and what they are, and **Open** for package.yaml. A package.yaml that does not decode is listed under Problems with the decoder's error.
▲ 0↑ born
◇
component · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
▲ 0↑ born
·
scene · born here
❒asset
▲ 0↑ born
·
scene · born here
❒asset
▲ 0↑ born
·
scene · born here
❒asset
▲ 0↑ born
·
scene · born here
❒asset
▲ 0↑ born
·
voxeltemplate · born here
❒asset
# bookshelf Voxel template `bookshelf` (demo asset). Live instances spawned via `Voxel.fromTemplate("bookshelf")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
·
voxeltemplate · born here
❒asset
# bot_seated Voxel template `bot_seated` (demo asset). Live instances spawned via `Voxel.fromTemplate("bot_seated")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
·
voxeltemplate · born here
❒asset
# bot_standing Voxel template `bot_standing` (demo asset). Live instances spawned via `Voxel.fromTemplate("bot_standing")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
·
voxeltemplate · born here
❒asset
# cabinet Voxel template `cabinet` (demo asset). Live instances spawned via `Voxel.fromTemplate("cabinet")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
·
voxeltemplate · born here
❒asset
# chair Voxel template `chair` (demo asset). Live instances spawned via `Voxel.fromTemplate("chair")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
·
voxeltemplate · born here
❒asset
# clock Voxel template `clock` (demo asset). Live instances spawned via `Voxel.fromTemplate("clock")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
·
voxeltemplate · born here
❒asset
# desk Voxel template `desk` (demo asset). Live instances spawned via `Voxel.fromTemplate("desk")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
·
voxeltemplate · born here
❒asset
# keyboard Voxel template `keyboard` (demo asset). Live instances spawned via `Voxel.fromTemplate("keyboard")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
·
voxeltemplate · born here
❒asset
# lamp Voxel template `lamp` (demo asset). Live instances spawned via `Voxel.fromTemplate("lamp")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
·
voxeltemplate · born here
❒asset
# linked_workstation_baked Voxel template `linked_workstation_baked` (demo asset). Live instances spawned via `Voxel.fromTemplate("linked_workstation_baked")` reload automatically when this template's `template.voxbin` payload is rewritten.
▲ 0↑ born
backing path · editor/edui.package/panels/docs.editorPanel

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.