Log inGet started
·
toolbox · drop-in viewer
asset⌬ toolboxtoolbox·originates fromworld 07158574-5…

entityOps

Operate on scene entities — spawn, instantiate content, change state, swap, and navigate the hierarchy. Every tool resolves targets by entity name or id** (single, a list, or the records `scene.find` returns) or by a **`scene.find` query** (`{ name = { "wheel" } }`, `{ component…

by◐lumi·posted 2mo ago
What it does

entityOps

Operate on scene entities — spawn, instantiate content, change state, swap, and navigate the hierarchy. Every tool resolves targets by entity name or id (single, a list, or the records scene.find returns) or by a scene.find query ({ name = { "wheel" } }, { component = { "Light" } }, { under = { "car" } }) that names a selection without looking it up first, and asset inputs by a name / path / identity string that the tool resolves and reports back canonically — so the caller never needs to know an asset's exact identity up front.

Finding and reading entities is the scene toolbox (scene.find, scene.tree, scene.summary); this toolbox is the operating surface.

Spawn + instantiate

  • spawn — spawn entities from a template: name the components they carry and their field values, plus a transform. Authored (serializing) components only. Pass a { defaults, items } list to spawn MANY in one call with shared defaults and per-item overrides — a six-walled room is one call, not six — validated whole before any entity is created.
  • fromAsset — instantiate an existing asset (bundle / avatar / mesh / material / texture, or anything with an :instantiate()): resolves a name/path/identity string, dispatches by category, and reports the canonical asset it hit. Controls the live link and synced flags.

State + hierarchy

  • modify — delete / enable / disable / hide / show / temporary / reparent / rename, over one or many entities.
  • swap — replace an entity with another entity that already exists in the scene (moves it into the target's slot).
  • addComponents — attach component(s) with setup data to one or many entities; create spawns missing ones.
  • removeComponents — detach component(s) from one or many entities, reporting per entity what was removed and what was absent.
  • component — inspect a component TYPE's schema: its public fields (name + type + current value), methods, and lifecycle hooks — so you set fields that exist instead of guessing. Reach for it before addComponents / spawn.
  • roots — find the root ancestor of one or many entities.

Transform + copy

  • transform — move, rotate and scale: absolute sets, relative offset / rotate / scaleBy, and grid snapping. With no ops it is a pure read of the current transform.
  • duplicate — copy entities, one at a time or as a numbered run stepped along an accumulating offset.

Addressing a set you built

Every tool here resolves targets by query as well as by name or id, so a set can be operated on without retaining a single id. Tag the set as you build it, then address it by that tag:

-- Build the set in one call, tagged.
entityOps.spawn({
  defaults = { components = { Model = { model = "cube" } },
               attributes = { group = "roomA" } },
  items    = { { name = "wall_n", position = { 0, 1, -2 } },
               { name = "wall_s", position = { 0, 1,  2 } } },
})

local set = { attribute = { name = "group", value = "roomA" } }

entityOps.transform(set, { offset = { 0, 5, 0 } })   -- move the whole set
renderLayer.set(set, "shell")                        -- re-layer the whole set
entityOps.modify(set, { delete = true })             -- remove the whole set

The tag is an ordinary entity attribute, so it persists with the scene and is as much a part of the content as a name is.

To see what is already present rather than what you made, scene.find with no search axis returns the whole live population, and include = { "transform", "componentValues", "attributes" } annotates each hit — one call, no follow-up per entity.

Interface

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

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

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 up to 3 levels deep. Open a part to see what it holds further down.

197+items
◇
component · born here
❒asset
# Chase Camera Sticky low-pass-filtered follow cam. Stays behind the target's facing direction at a fixed `distance` + `height`, no user-driven orbit, no mouse-look. Designed for vehicles + fast platformers where `orbital_follow`'s orbit feels too floaty. ## Vs. `orbital_follow` | | `orbital_follow` | `chase` | |---|---|---| | Orbit | RMB-drag (yaw + pitch) | none — derives from target facing | | Mouse-look | yes | no | | Position lag | n/a (camera is at fixed orbit) | per-second exponential lerp | | Rotation lag | n/a | separate per-second exponential lerp | | Best for | character action, RPG | vehicles, platformers, ski/snowboard | The split between `positionSmoothing` and `rotationSmoothing` lets you tune "position snaps, rotation drifts" (or vice versa) for the cinematic feel you want. ## Fields | Field | Default | Notes | |---|---|---| | `target` | `""` | Entity ID. Falls back to `Camera.follow` when empty. | | `distance` | `6.0` | Horizontal trail distance behind target. | | `height` | `2.5` | Vertical offset above target origin. | | `minDistance`, `maxDistance` | `1.5`, `25.0` | Scroll-zoom bounds. | | `zoomSpeed` | `0.12` | `exp(-scroll * zoomSpeed)` per tick. | | `positionSmoothing` | `0.05` | 0 = instantly stuck, 1 = static. Per-second blend. | | `rotationSmoothing` | `0.15` | Same shape — higher = more "looks where it's been". | | `lookAhead` | `0.6` | How far in front of target the camera aims. | ## Use as a camera behavior ```lua world.camera_default = asset.ref("@builtin::controller.chase") -- Or per-scene layers.active.camera.behavior = asset.ref("@builtin::controller.chase") layers.active.camera.follow = layers.active.players.localPlayer ``` The component carries the `cameraBehavior` tag in `.metadata`. ## Controls - **Scroll** — zoom in/out (distance only; height ratio stays constant) - No user-driven rotation — the camera always trails the target's facing. ## Pair with - `standard_controller` — the controller already rotates the player to face the input direction; chase locks behind that facing. - `topdown_controller` — works, but `birds_eye` is usually a better fit for top-down gameplay. - A custom vehicle controller that rotates with steering — chase will track the steering arc.
▲ 1↑ born
❒
package · born here
❒asset
▲ 1↑ born
◇
component · born here
❒asset
# controller.orbit Orbit camera controller. Orbits around a target entity or position. Mouse drag to rotate, scroll to zoom, optional auto-rotate. Public fields: `inputMap` (the camera-rig scheme, `@builtin::inputMaps.cameraRig`, by default; point it at a world's own map to rebind this camera), `target` (empty = use targetX/Y/Z), `targetX`, `targetY`, `targetZ`, `distance`, `minDistance`, `maxDistance`, `sensitivity`, `zoomSpeed`, `autoRotate`, `autoRotateSpeed`. Controls: RMB drag to rotate, scroll to zoom. ```luau entity(id).component.add("controller.orbit", { target = "player", distance = 10 }) ```
▲ 1↑ born
·
tool · born here
❒asset
# entityOps.addComponents Attach component(s) — with their setup data — to one or many entities in a single call, creating the entities first when asked. ```lua entityOps.addComponents("crate", { Physics = { kind = "dynamic" } }) entityOps.addComponents({ "crate_1", "crate_2" }, { "Physics", "Audio" }) entityOps.addComponents("spinner", { Rotator = { speed = 2 } }, { create = true }) ``` Targets are entity **names or ids** (arrays and `scene.find` results pipe straight in). Components are a name, an array of names, or a `{ Name = dataTable }` map carrying each component's setup data — names accept the leaf (`"Physics"`), the full identity (`"@builtin::components.Physics"`), or a VFS path, resolved before anything mutates. `create = true` spawns a fresh root entity for any string target that matches nothing, so one call both creates the entity and gives it its setup. Returns one `{ id, name, added, created? }` record per entity.
▲ 0↑ born
·
tool · born here
❒asset
# entityOps.component Inspect a component **type** so you set fields that exist instead of guessing. Reports the component's public fields (name + type + current value), its callable methods, its lifecycle hooks, and its asset-ref fields. ```lua entityOps.component("Camera") -- fields, methods, hooks + default values entityOps.component("Model", { on = "my_prop" }) -- actual values on that entity entityOps.component("@builtin::components.Light") ``` Resolves the component by name / identity / VFS path. Pass `on` (an entity name or id that carries the component) and every field's `value` is that entity's **actual** value, read off its live component proxy; omit `on` and each `value` is the **declared default**, as written in the component's source. A live value keeps its shape: a number reads back as a number, a vector or struct as its plain table. A field that resolves on read reports the identity of what it points at — an asset (`resource` / `assetRef`) as a `{ __ref, identity, name, type }` envelope whose `identity` you hand straight back to `addComponents`, an `entityRef` as the entity id. A field whose live value could not be read carries an `error` instead of a value, so a value that failed to read is never mistaken for the default. ```lua -- on a wall carrying a mesh and a custom material entityOps.component("Model", { on = "wall_north" }) -- fields: model = { __ref = "336f4feb-…", identity = "@builtin::meshes.cube", name = "cube", type = "mesh" } -- material = { __ref = "99d699e9-…", identity = "@user::materials.brick", name = "brick", type = "material" } ``` Returns `{ identity, description, builtin, executionOrder, hooks, fields, methods, assetFields, source }`. Reach for it before `entityOps.addComponents` / `entityOps.spawn` to learn the exact field names and value shapes — the fix for setting fields a component doesn't have. To inspect the components + values on an *entity* (rather than a component type's schema), use `debug.inspect`.
▲ 0↑ born
·
tool · born here
❒asset
# entityOps.duplicate Copy entities that already exist — one duplicate, or a numbered run of them spread along an axis. ```lua entityOps.duplicate("crate") entityOps.duplicate("crate", { count = 5, offset = { 2, 0, 0 } }) entityOps.duplicate("pillar", { count = 4, offset = { 0, 3, 0 }, name = "pillar_%d" }) entityOps.duplicate({ name = { "tree" } }, { offset = { 10, 0, 0 } }) ``` Targets take a name or id, a list of them, `scene.find` records, or a **query** resolved for you — so everything matching `{ name = { "tree" } }` is copied without looking the entities up first. `count` makes that many copies of **each** source. It must be a whole number of at least 1. `offset` is a world-space `{x,y,z}` step that **accumulates**: copy 1 sits one offset from the source, copy 2 sits two, copy 3 sits three. Every copy is placed relative to the **source**, so a run spreads evenly instead of compounding rounding from copy to copy. `name` names each copy, and a `%d` in it becomes the copy's 1-based index — `"pillar_%d"` yields `pillar_1`, `pillar_2`, … Without a `name`, copies are named after their source (`crate_copy`, or `crate_copy_1`, `crate_copy_2`, … for a run). Copies carry the source's render layer. An option key outside the documented set is an error that names it and lists the accepted keys, so a mistyped `conut` is reported instead of returning copies that quietly ignored it. Every call returns the resulting `{ id, name, position, localPosition, rotation, localRotation, eulerAngles, scale, parent }` per copy, in creation order — the copy and the read are the same call.
▲ 0↑ born
·
tool · born here
❒asset
# entityOps.fromAsset Instantiate an existing asset into the scene — **resolution-first**: pass a name / path / identity string, the tool resolves it and reports the canonical asset it hit, so you never need to know the exact identity up front. ```lua entityOps.fromAsset("@builtin::avatars.humanoid") entityOps.fromAsset("oak_chest", { type = "bundle", position = {2, 0, 0} }) entityOps.fromAsset("gold", { type = "material" }) entityOps.fromAsset("/zero/source/generated/meshes/a_statue.bundle", { link = false }) ``` Dispatch by resolved category: - **bundle / avatar** — instantiated via its own `:instantiate()`. `link` (default true) keeps the live Asset-component link so the source drives the hierarchy; `link = false` bakes the hierarchy into the scene. - **mesh** — a renderable (Model + mesh Collider). - **material** — a sphere carrying the material. - **texture** — a plane sampling the texture (through a fresh unlit material, so it never rewrites another preview). - **anything else** — instantiated through its own `:instantiate()` when it has one, else a legible error. `type` limits the asset search to one category (avoids name clashes). `position` / `rotation` / `scale` place it; `synced` replicates gameplay edits to peers. Returns `{ id, name, resolved = { category, identity, path } }` — `resolved` is the canonical asset your string hit. To spawn from hand-named components instead of an existing asset, use `entityOps.spawn`.
▲ 0↑ born
·
tool · born here
❒asset
# entityOps.modify Change entity state — one tool for delete / enable / disable / hide / show / temporary / reparent / rename, over one or many entities resolved by **name or id**. ```lua entityOps.modify("debug_probe", { hidden = true }) entityOps.modify({ "probe_a", "probe_b" }, { delete = true }) entityOps.modify("old_character", { enabled = "toggle" }) entityOps.modify("hat", { parent = "player_head" }) ``` Ops: `delete` (removes the whole subtree; wins over everything else), `enabled`, `hidden`, `temporary` (booleans, `"toggle"` flips the current state), `parent` / `unparent` (with `keepWorldTransform`, default true), and `rename` (single target only). Returns one `{ id, name, applied }` record per target. `scene.find` results pipe straight in as targets.
▲ 0↑ born
·
tool · born here
❒asset
# entityOps.removeComponents Detach component(s) from one or many entities — the counterpart to `entityOps.addComponents`. ```lua entityOps.removeComponents("lamp", "Light") entityOps.removeComponents({ "crate_1", "crate_2" }, { "Physics", "Audio" }) entityOps.removeComponents({ component = { "Light" } }, "Light") entityOps.removeComponents("lamp", "light") -- partial name, resolved ``` Targets take a name or id, a list of them, `scene.find` records, or a **query** resolved for you — so a component is stripped from everything carrying it without naming a single entity. Component names match loosely: the leaf (`"Light"`), the full identity (`"@builtin::components.Light"`), a VFS path, or a case-insensitive partial. The response reports the **canonical** name it resolved to, so a half-remembered name both works and teaches you the real one. A name that could mean several components is an error listing the candidates rather than a guess, and a name matching nothing is an error too — nothing is silently skipped. Every entity comes back as `{ id, name, removed, absent }`: - `removed` — components it carried and lost. - `absent` — components it never had, so nothing happened for them. That split makes a call which changed nothing visible, instead of letting it read like a success.
▲ 0↑ born
·
tool · born here
❒asset
# entityOps.roots Find the root ancestor of one or many entities — walks each up its parent chain to the top-level entity. ```lua entityOps.roots("player_hand") entityOps.roots({ "wheel_fl", "wheel_fr" }) ``` Targets are entity names or ids (a single one, an array, or `scene.find` records). Returns one `{ target, targetName, root, rootName, depth }` per target; an entity that is already a root reports itself with `depth = 0`.
▲ 0↑ born
▣
module · born here
❒asset
# entityOps shared Cross-tool helpers for the `entityOps` toolbox — the pieces more than one of its tools reuse. - **Entity resolution** — `resolveEntity` / `resolveTargets` / `toList` turn a name / id / proxy / `scene.find` record (single or a list) into live entity proxies, raising a legible error on anything that doesn't match. - **Subtree removal** — `collectDescendants` / `despawnSubtree` remove an entity and its whole subtree, raising rather than leaving a half-removed tree behind. - **Slot capture** — `captureSlot` snapshots the parent + local transform + name + sync flag an entity occupies, so a replacement can take its exact place. - **Asset resolution** — `resolveAsset` turns a caller's name / path / identity string (or an already-resolved AssetRef) into a resolved asset, reporting the canonical identity/path it hit, optionally limited to one asset category. Tools are thin wrappers over these; the helpers call the engine APIs.
▲ 0↑ born
·
tool · born here
❒asset
# entityOps.spawn Spawn entities from a template — you name the components they carry and their field values, plus a transform. ## Many entities in ONE call Pass a `{ defaults, items }` list: `defaults` holds what every entity shares, each `items` entry overrides only what differs. A six-walled room is one call, not six. ```lua entityOps.spawn({ defaults = { components = { Model = { model = "cube" }, BoxCollider = {} }, scale = { 4, 3, 0.2 } }, items = { { name = "wall_n", position = { 0, 1.5, -2 } }, { name = "wall_s", position = { 0, 1.5, 2 } }, { name = "wall_e", position = { 2, 1.5, 0 }, rotation = { 0, 90, 0 } }, { name = "wall_w", position = {-2, 1.5, 0 }, rotation = { 0, 90, 0 } }, }, }) ``` - A per-item key wins over the matching `defaults` key. - `components` merges **per component**: an item naming `Model` replaces only that entry and keeps the shared `Collider`. - Every item is validated **before any entity is created** — a typo in the last entry fails the whole call rather than leaving a half-built scene. - An unrecognised key is an error naming the accepted set, at the top level, in `defaults`, and in an item alike. - Returns one `{ id, name, components }` record per item, in item order. ## One entity ```lua entityOps.spawn( { Model = { model = "cube" }, BoxCollider = {} }, { position = {0, 1, 0} }) entityOps.spawn( { Light = { kind = "point", intensity = 5 } }, { name = "lamp", position = {0, 3, 0} }) entityOps.spawn({}, { name = "group" }) -- bare entity, no components ``` Components is a `{ [ComponentName] = { field = value } }` map. Each is an **authored** component (leaf name like `"Model"`, a full identity, or a VFS path) added with its data so it **serializes** — native ECS components are intentionally not exposed here, so you never author something that renders at runtime but vanishes on save. Transform: `position` `{x,y,z}`; `rotation` a `{x,y,z,w}` quaternion or `{pitch,yaw,roll}` euler degrees; `scale` a number or `{x,y,z}`. Also `temporary`, `hidden`, `synced` (replicate gameplay edits to peers), and `attributes`. Returns `{ id, name, components }` with the component leaf names added. To instantiate an existing **asset** (a bundle / avatar / mesh / material / texture) rather than hand-name components, use `entityOps.fromAsset`.
▲ 0↑ born
·
tool · born here
❒asset
# entityOps.swap Replace one entity with another entity that **already exists in the scene**. The replacement is moved into the target's slot — same parent, local position and rotation, the target's scale (`keepScale = false` opts out) and name (`keepName = false` opts out) — and the target is removed with its whole subtree. ```lua entityOps.swap("DefaultBody", "my_knight") entityOps.swap("placeholder_car", "sports_car", { keepName = false }) entityOps.swap("DefaultBody", "my_knight", { dryRun = true }) ``` Both arguments are entity names or ids. `dryRun = true` reports what would move and what would be removed without mutating. To replace an entity with a content **asset** (a bundle/mesh/…) rather than another scene entity, use `entityOps.fromAsset` (spawn) or `appearance.replaceWithContent` (in-place slot swap).
▲ 0↑ born
·
tool · born here
❒asset
# entityOps.transform Move, rotate and scale entities that already exist — over one or many entities. ```lua entityOps.transform("lamp", { position = { 0, 3, 0 } }) entityOps.transform({ "wall_n", "wall_s" }, { offset = { 0, 2, 0 } }) entityOps.transform({ name = { "wall" } }, { offset = { 0, 2, 0 } }) entityOps.transform("crate", { rotation = { 0, 45, 0 }, scale = 2 }) entityOps.transform("lamp") -- no ops: a pure read ``` Targets take a name or id, a list of them, `scene.find` records, or a **query** resolved for you — so you can move everything matching `{ name = { "wall" } }` without looking the entities up first. **Absolute** ops carry their space in the name: `position` / `rotation` are world, `localPosition` / `localRotation` are parent-relative, `scale` is a number or `{x,y,z}`. **Relative** ops are `offset` (position delta), `rotate` (applied on top of the current rotation) and `scaleBy` (multiplier). `space` — `"world"` (default) or `"local"` — governs these. `snap` quantizes the resulting position to grid steps. Rotations take a `{x,y,z,w}` quaternion or `{pitch,yaw,roll}` euler degrees, the same forms `entityOps.spawn` accepts. Absolute and relative forms of one channel are exclusive — passing `position` and `offset` together is an error rather than a silent precedence rule. An option key outside the documented set is an error that names it and lists the accepted keys, so a mistyped `positon` is reported instead of returning a successful-looking read of an entity that never moved. Per-entity values in one call: ```lua entityOps.transform({ { target = "wall_n", position = { 0, 1, -5 }, rotation = { 0, 0, 0 } }, { target = "wall_e", position = { 5, 1, 0 }, rotation = { 0, 90, 0 } }, }) ``` Every record is validated and resolved before any of them is applied, so a bad entry fails the whole call rather than moving its siblings and leaving itself behind. Every call returns the resulting `{ id, name, position, localPosition, rotation, localRotation, eulerAngles, scale, parent }` per entity — the mutation and the read are the same call.
▲ 0↑ born
·
assettype · born here
❒asset
# Toolbox (asset type) A toolbox is a namespace folder that groups related `.tool/` children plus shared helpers. Toolboxes are the addressing layer for tools — every tool's identity is `<toolbox>.<name>`, so the toolbox name appears in every callsite. Toolboxes also own a `shared.module/` helper module that the toolbox's tools `require(".shared")` for common utilities (result envelopes, validators, formatters). ## When to use one - You have two or more related tools that belong together (an `anim` toolbox with `play` / `stop` / `list`; an `assets` toolbox with `compose` / `validate`). - You need a shared helper module exclusively used by a tool set. - You want a clean dot-namespaced identity for your tools (`my_tools.foo`, `my_tools.bar`). If you only have one tool and no shared helpers, you still need a toolbox — it's the required parent. ## Where it lives - Source: `/zero/source/.../<Name>.toolbox/` - Identity: `<Name>` (the `.toolbox` suffix strips). - Folder shape (container-style — open child set): - `README.md` — instance documentation. **Required.** - `.metadata` — agent-editable tags + free-form fields. **Required.** - `shared.module/` — optional shared helper module (`init.luau` + `README.md`). - `<x>.tool/` — any number of `.tool/` children. The validator permits unlisted children (`allow_unlisted: true`) so tool subfolders don't need to be declared per-name. ## How to create one ```luau tools.createToolbox({ name = "<Name>", description = "..." }) -- or the generic scaffold: asset.create("toolbox", "<Name>") -- Creates: /zero/source/<Name>.toolbox/ -- README.md (instance README template) -- shared.module/ (ok / fail result-envelope constructors) -- `folder` places it in a subfolder of /source; `into` nests it in a -- resolved `.package`: asset.create("toolbox", "<Name>", { folder = "tools" }) ``` Then add tools under it: ```luau local box = asset.resolve("<Name>.toolbox") asset.create("tool", "<toolName>", { into = box }) ``` ## How it operates 1. **Registration.** Writing into a `.toolbox/` folder indexes the toolbox. It becomes the parent namespace for any `.tool/` children. 2. **Tool identity composition.** Each child `.tool/`'s identity is `<toolbox>.<tool>`. Listing the toolbox's tools enumerates the children. 3. **Shared helpers.** Tools inside the toolbox pull `shared.module/` in via `require(".shared")` (relative require from the tool's `init.luau`). 4. **Hot reload.** Editing `shared.module/init.luau` reloads every tool in the toolbox (they all `require` it). Editing the toolbox folder structure (adding / removing tools) reflects in `tools.list` immediately. ## Discovery - `asset.list("toolbox")` — every registered toolbox. - `asset.inspect("<name>")` — child tools, source, this type README. - `ls /zero/source/<Name>.toolbox` lists the files the folder holds. - `tools.list()` — every tool across every toolbox. ## Authoring conventions - **Per-tool implementation lives in each `<x>.tool/init.luau`.** The tool's full function body — argument validation, the call into engine APIs, the return value — goes in `init.luau` with `--!desc` / `--!arg` / `--!return` / `--!example` annotations above the function. **Every exported function is declared `typed function`** — the keyword compiles to the runtime argument check, so a plain `function` accepts any input and reports success on garbage. Do NOT make `init.luau` a one-line forwarder onto a function defined in `shared.module/`. - Keep `shared.module/` thin: result envelope constructors (`shared.ok(value, formatted)`, `shared.fail(message, exitCode)`), argument validators, common formatters — anything genuinely used by more than one tool. Per-tool logic does NOT belong here. - Group by domain, not by surface — `assets` toolbox holds asset operations, `entity` toolbox holds entity operations. Tools that feel unrelated probably want different toolboxes. - Document the toolbox's surface in `README.md` — what problem space it covers, who calls into it, any shared invariants. ## Common pitfalls - **Missing parent toolbox.** Tools cannot exist outside a toolbox. Always scaffold the toolbox first. - **Cross-toolbox shared.** Each toolbox has its own `shared.module/`. Don't try to share helpers across toolboxes; if you need cross-toolbox helpers, factor them into a standalone `.module`. - **Toolbox identity.** Strip the `.toolbox` suffix in identity references (`my_tools`, not `my_tools.toolbox`). ## Related types - `.tool` — the required child(ren) of a toolbox. - `.module` — for cross-toolbox utilities. ## In the Inspector Tools: how many tools the box holds, whether it ships a shared module, what each tool does, and a link to each tool.
▲ 0↑ born
·
assettype · born here
❒asset
# Tool (asset type) A tool is a single agent-callable function whose schema is its `typed function` signature. Tools are the workflow surface — composed Luau operations that bundle multiple engine APIs into one named call, suitable for agents (and for use from `execute`). Each tool has a declared input shape, return shape, and a strict result envelope (`ZmToolResult`). **Every exported tool function is declared `typed function`.** The `typed` keyword is what makes the signature real: it compiles to a runtime dispatch wrapper that checks each argument against its declared type and raises on a mismatch. A plain `function` — even one carrying Luau type annotations — is checked by nothing at call time, so the tool accepts any input and reports success on garbage. See "The typed signature is the schema" below. ## When to use one - You're composing several engine APIs into a workflow that's worth naming and discovering (`tools.list`, `search_tools`). - You want an agent to be able to call this function by name without reading source code — the typed signature is the API contract. - The operation has clear success / failure modes that fit `{ exitCode, data?, stdout?, message? }`. If the function is just a Luau utility called from other code, use a `.module`. If it's stateful background work, use a `.service`. If it's pure GPU compute, use a compute `.shader`. ## Where it lives Tools nest under a `.toolbox/` parent — toolboxes are the namespace. - Source: `/zero/source/.../<Toolbox>.toolbox/<name>.tool/` - Identity: `<Toolbox>.<name>` (dotted form — the toolbox name is part of the tool identity). - Folder shape: - `init.luau` (or `init.lua`) — exports the function, and carries its schema in `--!desc` / `--!arg` / `--!return` / `--!example` annotations above it. The typed signature IS the tool's signature. **Required.** - `README.md` — instance documentation. **Required.** - `.metadata` — tool-level tier and tags. **Required.** ## How to create one Use the workflow tool that scaffolds the right `.tool/` shape under an existing toolbox: ```luau -- The `tools` global authors a tool inside an existing toolbox: tools.create({ name = "<name>", toolbox = "<Toolbox>", description = "What this tool does, agent-facing.", code = "return shared.ok(true)", -- the function body }) -- Or scaffold the folder manually: local box = asset.resolve("<Toolbox>.toolbox") asset.create("tool", "<name>", { into = box }) ``` Always nest under an existing toolbox — if you don't have one yet, call `tools.createToolbox({ name = "<Name>", description = "..." })` first. ## How it operates 1. **Registration.** Writing `init.luau` into a `.tool/` folder registers the tool with the tools registry. It becomes callable as `tools.use("<toolbox>", "<name>", ...)` from Luau, `use_tool { toolbox, tool, args }` over MCP, and `zero <toolbox> <name> [args]` from the engine shell. 2. **Schema validation.** The `typed function` signature declares the parameter shape. Its runtime dispatch wrapper checks each argument before the body runs and raises a message naming the argument, the expected type, and what arrived. 3. **Return envelope.** Tools MUST return `ZmToolResult`: ``` { exitCode = 0|n, data? = any, stdout? = string, message? = string } ``` `exitCode = 0` means success; non-zero means failure, with `message` carrying the human-readable reason. 4. **Discovery.** The tool surfaces in `tools.list()`, `search_tools { query: "..." }`, and `asset.inspect`'s `exposes:` section (parsed from the typed signature and the `--!` annotations). 5. **Hot reload.** Editing `init.luau` re-registers the tool. Subsequent calls see the new behaviour / schema. ## Discovery - `tools.list()` — every registered tool (any toolbox). - `search_tools { query: "..." }` — MCP-side tool search. - `asset.list("tool")` — same set, asset-style listing. - `asset.inspect("<toolbox>.<name>")` — signature, parameters, return shape, this type README. - `ls /zero/source/<Toolbox>.toolbox/<name>.tool` lists the files the folder holds. ## Authoring conventions - **Implementation lives in `init.luau`.** The tool's full function body — argument validation, the call into engine APIs, the return value — goes in `init.luau`. The toolbox's `shared.module/` is for genuinely cross-tool helpers (`ok` / `fail` constructors, shared parsers / validators), not per-tool logic. Do NOT make `init.luau` a one-line forwarder onto a function defined in `shared.module/`. - Use `shared.module/` in the parent toolbox for `ok` / `fail` result constructors. Don't hand-roll the envelope per tool. Pull it in with `local shared = require(".shared")` from any tool that needs the helpers. - Annotate every public function with `--!desc` / `--!arg` / `--!return` / `--!example` directly above the function — the LSP and toolbox surfaces extract these. - **Declare every exported function `typed function M.name(arg: T, opt: U?): R`.** This is not conditional on the signature being "clear" — a loose argument is the case that needs the checked contract most. Give the argument the type it actually accepts (a union, a named table type, a literal enum) instead of dropping to an unchecked `function`. - Keep tools focused. One tool, one named workflow. Split rather than grow. - Tag tools in the sibling `.metadata` (`{ "tags": [...], "tier": "core|content|specialized" }`) so `search_tools` can find them by domain (e.g. `entity`, `scene`, `assets`, `physics`). ## The typed signature is the schema The `typed` keyword compiles the function into a dispatch wrapper that validates arguments at call time. Without it there is no check at all, so the tool runs its body on whatever arrived and returns a success envelope built from garbage. Type each argument as narrowly as the operation actually accepts: ```luau export type Mode = "edit" | "play" export type SpawnOpts = { position: { number }?, parent: EntityRef?, count: number?, } --!desc Spawn a primitive and return the entities it created. --!arg kind Primitive shape to spawn. --!arg opts Placement and repetition options. --!return The spawned entity refs. --!example "cube", { count = 4 } typed function M.spawn(kind: ("cube" | "sphere" | "plane"), opts: SpawnOpts?): { EntityRef } ``` - **A fixed set of accepted words is a literal union**, never `string`. `("cube" | "sphere")` rejects `"Cube"` at the call and lists the accepted values in the error; `kind: string` accepts `"banana"` and fails somewhere deeper, or not at all. - **A structured argument is a named `export type`**, never `{ [string]: any }`. Naming it puts every field and its type on the tool's page and checks them on entry. - **A named type is only as strong as its fields.** `opts: DrawOpts` where `DrawOpts = { value: any }` passes for a table and checks nothing inside it — the same hole, one level down. The argument checker walks nested fields, so type each one; a type name is not a place to put the work off. - **A dispatch table keyed by an `op` string is several tools.** `{ op: string, [string]: any }` is one unchecked entry point wearing N signatures — split it so each operation carries its own checked arguments. - **An entity or asset argument is `EntityRef` / `AssetRef<kind>`**, never `any` and never a bare name string. - `any` is correct only for a value the tool genuinely does not constrain — a passthrough payload, arbitrary user data. It is not a placeholder for a type not yet written down. ## Common pitfalls - **A plain `function` export.** The tool registers and dispatches, so nothing looks wrong — it just never rejects bad input, and every call reports success. Declare it `typed function`. - **Annotations without `typed`.** `function M.f(x: string)` reads as typed and checks nothing at runtime; the annotation only informs the LSP. The keyword is what enforces it. - **Wrong return envelope.** Returning a raw value instead of `{ exitCode = 0, ... }` will be flagged by callers expecting `ZmToolResult`. - **Tool identity.** The fully-qualified identity is `<toolbox>.<name>`, NOT just `<name>`. Toolbox-less tools don't exist by design. - **Missing dependency.** Tools requiring another package / service must declare it; otherwise the call fails at runtime with a missing-module error. ## Related types - `.toolbox` — the required parent namespace for tools. - `.module` — for Luau libraries with no agent-facing schema. - `.service` — for stateful background work, not callable request/response operations. ## In the Inspector Tool: how the tool is called (its signature), what it does, each argument, what it returns, and its examples, with **Open** for its source.
▲ 0↑ born
·
assettype · born here
❒asset
# Module (asset type) A module is a reusable, stateless (or globally-stateful) Luau library that other code pulls in with `require()`. Modules are the unit of factored-out logic in a world: helpers, math libraries, format parsers, registries, shared algorithms. ## When to use one - You have logic you want to share across multiple components, services, scenes, or tools. - You have a registry, lookup table, or shared cache that callers all read from / write to. - You want a stable API surface (`M.fn`) other code can depend on. If the logic is bound to one entity, use a `.component`. If you want a named UI of fields rather than functions, use a `.preset`. ## Where it lives - Source: `/zero/source/.../<name>.module/` - Identity: the source path with `/` written as `.` and each segment's type suffix dropped — `/zero/source/demo/beats.module` is `demo.beats`, and `/zero/source/kit.package/beats.module` is `kit.beats`, the container's name kept and its `.package` suffix gone. `asset.create` returns this string, `asset.resolve(...).identity` reads it back, and the `require` forms below write it as `<identity>`. - Folder shape: - `init.luau` (or `init.lua`) — module body. **Required.** Returns a table (conventionally named `M`). - `README.md` — instance-level documentation. **Required.** - `.metadata` — agent-editable tags + free-form fields. **Required.** ## How to create one ```luau asset.create("module", "<name>") -- Creates: /zero/source/<name>.module/ -- init.luau (canonical `local M = {} … return M`) -- README.md (instance README template) -- `folder` places it in a subfolder of /source, and that subfolder -- leads the identity: `{ folder = "lib" }` produces `lib.<name>`. -- `into` authors it inside a resolved container (a package, a toolbox), -- whose own identity leads the same way: asset.create("module", "<name>", { folder = "lib" }) asset.create("module", "<name>", { into = asset.resolve("myPkg.package") }) ``` ## How it operates 1. **Registration.** Writing `init.luau` into a `.module/` folder indexes the asset and registers the module path with the resolver. 2. **Loading.** The first `require("<identity>")` runs the file's body and caches the returned table. Subsequent requires from anywhere return the **same table** — this is the shared-state lever for "module-local" registries. 3. **Hot reload.** Editing the file re-runs the module body and folds what it returns into the table already loaded, so that table keeps its identity: a caller that captured it with `require` reads the new source's functions and values through the reference it already holds, and a later `require` returns that same table. A key the new source drops goes away; a key something set at runtime stays. The body's own `local`s start again from their initial values, so state that must outlive an edit lives in `modules.state()` (`guides { path: "core/scripting-and-tasks" }`). 4. **Identity resolution.** A module is reached by its `<identity>`, and where the caller lives decides which spelling of it resolves. For a module in a **world**, from a caller in that same world — another module, a component, a scene entrypoint, or an `execute` chunk: - `require("<identity>")` — the identity on its own, resolved against the caller's own root. It carries every folder segment, so a module created with `{ folder = "demo" }` is `require("demo.beats")`. - `require("@root::<identity>")` — the same root-relative resolution, written out. - `require(".sibling")` — a module in the same folder, by its bare name; `..name` steps up one folder, `...name` two. Logs, stack traces and hot-reload notices name that same module `@local.source.<identity>` — its key under the world source root. The `<identity>` tail of that key is the form above, so a trace reading `@local.source.demo.beats` is `require("demo.beats")`. For a module in a **library** — content under `/zero/source/libs/@<lib>/` — `@<lib>::` addresses that library's root from anywhere: `require("@builtin::modules.transform")`. Mounting a world as a library moves its files under such a root and flips the caller root to `@<lib>::`, which is what keeps the root-relative forms above resolving across the move. ## Discovery - `asset.list("module")` — every registered module. - `asset.inspect("<name>")` — public functions, source path, this type README. - `ls /zero/source/<name>.module` lists the files the folder holds. ## Authoring conventions - Return a single `M` table from `init.luau`. Top-level statements with side effects run on first require — useful for one-time initialization, dangerous if they touch the engine before it's ready. - Write exported functions as `typed function`, not `function`: the argument types are enforced at the call and the checker knows the signature, so a caller's mistake is reported where they made it. - Annotate exported functions with `--!desc` / `--!arg` / `--!return` / `--!example` so the LSP, `tools.list`, and `asset.inspect` surface them. A function with no `--!desc` is reachable only by someone already reading this file. - Declare a VALUE with `Field.<kind>(default, mode, description)`, and a table whose members are reached through a metatable with `--!members <TypeName>` — the latter gives the surface its accepted member set, its type, and its documentation from one table. See `guides { path: "core/authoring" }` for all four rules and `modules/api/engine/entity.module/members.module` for a worked member table. - Keep module state in module-local upvalues or in `modules.state()`. Globals leak across reloads. Module-locals start again from their initial values with each hot reload; the table `modules.state()` returns is the same table on both sides of every reload. - Prefer focused modules over kitchen-sink modules. If two halves of a module have no shared state, split them. ## Common pitfalls - **Cyclic requires.** A `require` chain that loops will return the partial table (the half built before the cycle was detected). Design for one-way dependencies. - **Engine-time side effects.** Don't `entity.spawn` at module top-level — the engine may not be ready. Expose an `M.init` and call it from a scene entrypoint. - **Hot-reload + module-local cache.** A module that caches expensive computation in upvalues loses that cache on reload — fine for dev, but be aware in performance work. - **`init.lua` vs `init.luau`.** Either works, `.luau` is canonical. - **Writing one while play runs.** A `/source` write made while play is running lands on the play shadow, and a guarded play-exit discards it unless it is kept. One `.module` write shadows `init.luau`, the `README.md` and the `.metadata` together. `vfs.write(path, bytes, { durable = true })` skips the shadow, and `vfs.promotePlayShadow` keeps what is already on it — the `core/vfs` guide has the model and what each route costs. ## Related types - `.component` — for entity-bound state + lifecycle hooks. - `.service` — generates content you don't have yet (a mesh, sound, texture, …) via a metered provider. - `.tool` — for a single agent-callable function with a YAML schema. - `.package` — to group several modules + components + scenes into one shippable folder. ## In the Inspector Exports: what the module exports (name, kind, signature), parsed from its source without running it, with **Check** (the LSP's diagnostics for its entry file) and **Open**.
▲ 0↑ born
▣
module · born here
❒asset
# shared Cross-tool helpers for the [name] toolbox. Every tool inside the toolbox can pull these in with `require(".shared")`. Holds the small set of helpers that more than one tool needs — typically the `ok(value, formatted?)` / `fail(message, exitCode?)` constructors that produce the canonical `[name]ToolResult` shape, plus any toolbox-wide parsers, validators, or shared state. Per-tool implementation stays inside each `<x>.tool/init.luau`; this module is not a place to forward logic from. ## Exports - `M.ok(value: any?, formatted: string?) -> [name]ToolResult` — success envelope (`exitCode = 0`, `ok = true`). - `M.fail(message: string, exitCode: number?) -> [name]ToolResult` — failure envelope (`exitCode` defaults to 1, `ok = false`). Types: - `[name]ToolResult = { stdout: string, stderr: string, exitCode: number, ok: boolean, value: any? }` - `stdout` — human-readable formatted output; empty when the op produces no text. - `stderr` — error message on failure; empty on success. - `exitCode` — POSIX-style exit code: `0` success, `1` generic failure, `2` bad arguments. - `ok` — `true` iff `exitCode == 0`. - `value` — structured return from the underlying op; `nil` for void ops. ## Usage ```luau local shared = require(".shared") function M.someTool(args) if not args.id then return shared.fail("missing id", 2) end return shared.ok({ id = args.id }, "created " .. args.id) end ``` ## Notes - Pure constructors — no engine calls, no state. Safe to call from any context. - Tools that opt into this shape get a stable envelope every other tool in the toolbox can rely on. Tools that do something custom can return whatever shape they want — `[name]ToolResult` is opt-in. - Per-tool implementations still own their own `--!desc` / `--!arg` / `--!return` / `--!example` annotations even when their result is constructed through these helpers.
▲ 0↑ born
·
assettype · born here
❒asset
# assetType (asset type) The `assetType` is the **type of types** — the meta-type that every `<typename>.assetType/` folder is an instance of, including itself. A `<typename>.assetType/` folder declares a new asset type called `<typename>`: its on-disk shape, its identity rule, and the structural contract instances of the type must satisfy. This type is self-hosting. `assetType.assetType/` conforms to its own schema — a `type.yaml` plus a `README.md` — so the type system is defined in terms of itself rather than a hidden engine special-case (the same way a C compiler is written in C). Every asset in the engine, including each `.assetType` definition, now resolves to a registered assetType: a `.material` resolves to `material.assetType`, and `material.assetType` resolves to `assetType.assetType`, which resolves to itself. ## Why it exists Assets used to be mapped to their type *implicitly*, by matching the folder suffix against a name-keyed registry. That match carried no stored link: nothing on a `Foo.material` recorded *which* `material.assetType` it was written against, so two worlds shipping a same-named type could silently disagree. Making `assetType` a real, resolvable asset closes that gap — the link from an asset to its type is now an explicit reference pinned in the asset's `.refs` sidecar (`via: "asset_type"`), exactly like every other dependency. ## Where it lives - Source: `/zero/source/.../<typename>.assetType/` - Identity: `<typename>` (the `.assetType` suffix strips from the identity; the folder retains the suffix on disk). - Folder shape: - `type.yaml` — the structural spec the validator reads. **Required.** - `README.md` — type-level documentation. **Required.** - `about.yaml` — three short plain-language texts saying what the type is for (see "Say what your type is for" below). Every built-in type ships one. - `behavior.luau` — optional behavior/scaffolding module. - `template/` — optional canonical placeholder body the templater copies into a new asset of the type. - `<name>.module/` — optional shared code the type ships to **every instance** of it, conventionally `shared.module/` (see below). ## Shared code its instances reach (`asset.containing` + `.modules`) A type ships code every instance of it uses by declaring the modules in its own `behavior.luau`, as a map of tracked `require`s: ```luau -- inside <typename>.assetType/behavior.luau M.modules = { shared = require(".shared") } -- the sibling shared.module/ ``` An instance reaches it with one location-independent line: ```luau -- inside any foo.<typename>/init.luau local api = asset.containing(__FILE__).modules.shared ``` `__FILE__` is the chunk's own VFS path; `asset.containing` resolves the calling asset, and `.modules.<name>` follows the instance's pinned `typeRef` (the type's **guid**, recorded in the instance's `.refs` as `via = "asset_type"`) to the module its type declares. Because resolution follows that link rather than a category name, the same line in an instance of a different type resolves to that type's module, and identically-named modules in two types never collide. `asset.typeRef(target)` reads that guid for any target; `asset.resolve(asset.typeRef(target))` gives the full `AssetRef<assetType>`. See `assetTypes/README.md` § "Shared code: type-owned modules" for the full mechanism. ## How to create one Authoring a new asset type is just creating a `<typename>.assetType/` folder under `/zero/source/`. The engine registers `<typename>` as a category **the moment the folder is written** — no engine restart, no Rust change, and it works for built-in (`@builtin/assetTypes/`) and user-defined types identically. Once `<typename>` is registered, `<name>.<typename>/` folders are recognised as assets of that type and resolve their `asset_type` link back to this definition. Creating a `<name>.<typename>/` folder *before* its `<typename>.assetType/` exists fails fast: the suffix has no registered type, so the folder can't be a valid asset. Author the type first. ## Say what your type is for (`about.yaml`) Every type carries a small `about.yaml` beside its `type.yaml`, and the README you write for the type documents how to author an asset of it. Both are required, and both are meant to be written as you make the type, while you know what it is for. `about.yaml` answers three questions in plain words, one key each: ```yaml intent: "Sets the typeface of the text in your game: UI, labels, signs and 3D lettering." function: "A font, from a TTF, OTF or WOFF file, made available to all of Zero's text, including 3D text." usage: "You choose it by name for your UI or your text, and the text appears in that typeface." ``` - **`intent`** is what the type is for in someone's game. Name the whole range of things it covers, in the words a game maker would use for them. - **`function`** is how it works today: what an asset of the type is made of and what the engine does with it. - **`usage`** is what a person or an agent does with one: how they pick it, place it, call it, or hand it to something else. A few habits keep it useful. Write about the type as a whole, so the text holds for every asset of it, and leave out any one asset's name. Say what the type does right now. Stay with words a game maker would say, and mention a file name or a call only where a game maker would know it. The limits are in bytes: `intent` 800, `function` 1500, `usage` 1200. Each is generous, and a few sentences usually say it. Why bother: agents working in a world read these three texts to learn what your type is for, so they reach for it when it fits and use it the way you meant. People searching find the type by what they want to make, and the three texts let them tell at a glance whether it is the one they need. `asset.create("assetType", name)` scaffolds the file with all three keys, and a README whose first paragraph is a `[[describe]]` note. Each value starts with `[[describe]]` followed by a note on what to write; replace the whole value with your own words. A value or a README line that still starts with `[[describe]]` counts as not written yet, which the push's check of each asset it carries against its type reports, naming the file. A type you leave as it was is not asked, so older types stand. Many types at once, or one you would rather not write by hand: `services describe` lists the types that lack these texts and what filling them costs, and `{ mode = "all" }` writes them into the same files, where you edit them like anything else you wrote. Where the texts show up: `asset.inspect("<typename>", "assetType")` returns them as `detail.about`, `assetTypeRef:getAbout()` returns them as a table with the keys `intent`, `function` and `usage`, the type's `types/<typename>` guide page ends with an "About this type" section holding them, and the Inspector's Type section lists them at the top. The guide page is this README plus that section, so a README that explains the type well and an `about.yaml` that says what it is for read well together. ## Ask your assets to say what they are A type can ask each of its assets to carry a description: a file, written by the author or generated, that says what that asset is and what it is for. The people and agents who find an asset read it to decide whether it fits, and the author knows what it is for at the moment of making it. **1. Declare the file with a `described` check.** This is the whole requirement: ```yaml files: required: - path: "README.md" # any file name works kind: "file" content: - { check: "described", message: "Say what this widget is for and where it goes." } ``` With that in place: - An asset whose file is missing, empty, or still has a line starting with `[[describe]]` does not meet the type's requirements. `asset.validate` names it, `zm status` and `world.publishBlockers()` list it, and a push that carries it is refused with `message`. - `services describe` finds it (`mode = "find"` lists it and the price) and can write the description for it (`mode = "all"`). It writes only this file. For a YAML file that holds several texts, name the keys and their limits in bytes, the way this type does for `about.yaml`: ```yaml content: - check: "described" keys: - { key: "intent", max_bytes: 800 } - { key: "usage", max_bytes: 1200 } message: "Write intent and usage." ``` `headings` names sections a Markdown file must have. `assetTypes/README.md` has the full check. **2. Optionally, steer what gets generated with a `describe:` block:** ```yaml describe: guidance: "Say what the widget shows, what input it reacts to, and where it fits in a UI." examples: - "<identity of an asset of this type whose description reads well>" ``` `guidance` says what a description of your type covers, and `examples` names assets whose descriptions show the shape you want. Without the block, generation still works from the general describing guide and the check's `message`. An example is an asset of your type in the library whose description is a true, written one, never a scaffold's placeholder, and never the asset being described. When the library has none, leave `examples` empty rather than name one that is not. A type whose assets the model cannot perceive (a sound it cannot hear, a motion it cannot see) says so in `guidance`: the description comes only from what the asset's own files state, such as its name and the `.metadata` where a generation service records the prompt that made it, and when they state nothing the model answers `cannot` with that reason. `services describe` then fails that one asset with the reason and leaves its file as it was. **3. Start the file unwritten in your template.** Put a heading and one line that starts with `[[describe]]` and a short note on what to write, in `template/<file>`, so a new asset starts unwritten rather than with text that only looks written. Do not put example content in it: every new asset carries the template verbatim, and the same text on many assets makes them impossible to tell apart in search. Types whose assets say what they are through their own content (a mesh, a texture, a sound) leave this out: declare nothing and nothing is asked. ## The `behavior.luau` contract `behavior.luau` is the type's code. It returns ONE table, and the keys below are the ones the framework reads off it for EVERY type — the surface you get by declaring them. Anything else on the table is your own: reachable by name through `require("@builtin::assetTypes.assetType.shared.ref").loadTypeModule("<typename>")` for a system that knows your type (`computeShader` publishes `dispatchKey` that way), and never called by the framework. ```luau local M = {} M.ref = { ... } -- methods on every AssetRef of this type M.modules = { ... } -- shared code the instances reach M.global = {} -- reserved key; ship the empty table function M.onCreate(name, opts) ... end return M ``` ### The tables | Key | Shape | What the engine does with it | |-----|-------|------------------------------| | `M.ref` | `{ [name] = function(self, ...) }` | Every function becomes a method on every `AssetRef` of this type: `ref:name(...)`, with `self` the ref. This is how a type gives its instances an API. Two names in here are contracted — see below. | | `M.modules` | `{ [name] = require(".sibling") }` | Shared code the type's instances reach through `asset.containing(__FILE__).modules.<name>`, resolved by the instance's pinned `typeRef` guid. See the section above. | | `M.refShapes` | `{ [method] = "TypeName" }` | Declares the result type of a `M.ref` method so the LSP can check what a call site does with it. See the `M.refShapes` section below. | | `M.events` | an events schema | The events an ASSET of this type fires, declared the way a component declares its own. Readers subscribe through `ref.events.<name>:connect(...)`; firing authority stays with the type. Keyed by the asset's guid, so every resolver of the same asset shares the signals and a re-resolved ref re-attaches to subscriptions already there. | | `M.namePattern` | a Lua pattern string | The name shape `asset.create` enforces for instances of this type, replacing the default `^[A-Za-z][A-Za-z0-9_]*$`. | | `M.global` | `{}` | Reserved. Ship the empty table. | ### The lifecycle hooks Each is optional; a type that omits one costs nothing. The engine calls them: | Hook | Signature | When it fires | |------|-----------|---------------| | `M.onCreate` | `(name, opts) -> { [filename] = contents }` | On `asset.create("<type>", name, opts)`. Returns the files that scaffold the new instance, as a map of relative path to contents; the framework writes them. The `opts` type annotation is the schema `asset.create` validates the caller's arguments against, so annotate it. | | `M.onReplace` | `(dest, files) -> { [filename] = contents }` | On an `asset.create` that writes over an asset already at `dest`, after `onCreate`. Receives the files `onCreate` produced and returns the files to write, so a type that re-derives its content (a rig re-imported from its source file) can carry what an author added to the asset it replaces. Optional. | | `M.onRegister` | `(self)` | Once per instance, the first time it registers: on `engine.onWorldLoaded` for instances already in the world, immediately on a live `asset.create`, and when an instance arrives after that sweep (installed, pulled, or synced from a peer). Guarded by the instance's path, so a double trigger never double-registers. An instance that leaves the runtime releases the guard, so one arriving at the same path again registers. This is what makes "write an asset into the world and it takes effect live" work for content that registers into a runtime registry. The `@builtin` library is excluded from the sweep. | | `M.onChange` | `(ref, change)` | On every VFS write to a file INSIDE one of this type's instances. `change` is `{ path, asset, type, kind, origin }`: `kind` is `"edited"` or `"seeded"`, `origin` is `"local"` or `"remote"` (an importer runs on the originator only). **Filter on `change.path`**: the hook fires for any file under the folder, so act on the one you care about. It runs as its own task and may wait on what it reads: a `vfs.read` of bytes only the blob store holds (a binary file on the web build) fetches them. The writes to one asset reach the hook in the order they were made, each once the hook before it has finished. A re-entrancy guard suppresses writes back into the same asset routed inline from the hook, and a write the hook makes is routed on a later frame like any other, so the hook must be convergent on its own: diff the meaningful state and short-circuit while your own regeneration is in flight. | | `M.onDelete` | `(ref)` | When an instance of this type leaves the runtime: it is deleted, or the engine swaps away from the world binding that held it. Its files are gone from `/zero/source` by then. A removal routed while the instance's `onChange` is waiting reaches `onDelete` once that hook, and every write queued behind it, has finished. On a swap the instance still exists in the world it belongs to, and a VFS write the hook makes reaches the world now bound. | | `M.validate` | `(assetRef) -> { { code, message, severity? } }` | On `asset.validate`, after the structural `type.yaml` check, for the type's own SEMANTIC validation. `severity` defaults to `"error"`; error-severity problems flip `ok` to false, warnings do not. A hook that raises or returns a non-table is itself reported as a `validate.hook_failed` error. `world.push` runs this per user asset, so declaring it enforces your type's rules at publish time with no further wiring. | ### The two contracted `M.ref` names Most `M.ref` methods are your type's own surface: name them what you like, return what you like. Two are read by the engine and mean the same thing for every type, so their shape is fixed: - **`instantiate(self, target?, opts?) -> (root, idMap)`** — makes an instance part of the scene. Defining it is the whole opt-in: `ref:canInstantiate()` is true exactly when it exists. Its **return is a contract** — see the next section. - **`inspect(self) -> detail`** — the type-specific half of `asset.inspect`. See its section below. ### Names a type cannot shadow Some keys resolve on every `AssetRef` before per-type dispatch, so an `M.ref` entry of the same name is never reached: `canInstantiate`, `getSource` / `getBytes` / `getText`, `exists`, `deps`, `meta`, `runtime`, `events`, `modules`, `typeRef`, and the residency flags `has_backing_asset`, `has_runtime_changes`, `cpu_resident`, `gpu_resident`. These are the behaviours every asset must expose identically, which is why they win. ### Reloading A `behavior.luau` edit ripples to existing refs with no restart: dispatch reads `mod.ref` fresh each time and `require`'s cache is re-run in place. A type whose folder appears at runtime is picked up on the next dispatch — negative results are not cached either. ## The names your type's assets hold (`references:`) An asset that names another asset by a string depends on it: the named asset travels with it into a pack or a pull, and a name nothing answers to is an unresolved dependency `worldValidation` reports and the push gate refuses. The engine reads `require(...)` and the asset lookups on its own; a name your type's format or API carries anywhere else is one you declare, in `type.yaml`: ```yaml references: # A top-level key of this type's own files that holds a name (or a table of them). keys: - { key: "shader", type: "shader" } # A call's argument, counting from 1, wherever the call is written. args: - { call: "g:node", position: 2, type: "procNode" } # A field of the options table a call takes. fields: - { call: "fx.beam", field: "material", type: "material" } ``` `type` is the category the name resolves in, one of `asset.categories()`. An argument declared once per category, at the same `call` and `position`, takes the name from whichever category answers. A key or a field holds one category: declared again with another `type`, it takes the later one. The block is declared as the type registers, when the library or the world holding it is laid into the VFS, so every reference table reads it whatever script has or has not run. `asset.declareReferenceKey`, `asset.declareReferenceArg` and `asset.declareReferenceField` declare the same things from a script, for a surface that is no type's. ## What search does with your type (`indexing:`) A type declares what search embeds for its instances. **This is not optional and there is no useful default**: ZeroMind runs your declaration and adds nothing of its own, so anything you do not point at is absent from the index — silently, and for every instance of the type forever. Twenty types once shipped with no `indexing:` block and every instance of them was unfindable. A type whose instances are never searchable (a run record, a log) says so with `indexing: none`; every other type declares a recipe. A type without an `about.yaml` beside its `type.yaml` refuses every push that carries it, whether or not the push edited the type. Texts in it that are not written yet follow the rule every `described` file follows: they refuse the push that edits, stages or commits the type. Two words carry the whole thing, and they are the same two the search tool exposes: - **`identity`** — what an instance IS. For anything you can look at, that is the picture (`content:` + `modality: image`, matched directly by a text query). For everything else it is the authored text that says what it is. - **`capability`** — what it DOES and how it is made. Code, settings, the model-written summary of them. A type with no behaviour has no `capability` entry. A type with nothing to look at has no image. Leaving a slot empty is a statement; filling it with whatever happens to be lying around is not. The one you have to decide when authoring a type is: *for an instance of this, what is the thing a person would recognise it by, and what is the thing it does?* A texture answers "the image" and "its compression settings". A module answers "its README" and "its code". Write those two answers into `indexing:` and the rest follows. Three specifics worth knowing before you write one: - **`derive` is instructed by this type's own `README.md`.** A model reads an instance's source and writes a description; what it embeds is the description, not the source. It follows your README to know what it is looking at, so a type whose README says what its instances are gets good derivations for free. There is no prompt to name. - **`derive` loses detail.** It keeps the main technique and drops secondary ones. If the source is code you want searchable by its own vocabulary, declare the same files a second time as `capability` with `extractor: verbatim`. Two entries, same role, different failure modes. - **Never `source: { field: name }`.** A filename is one or two words with no usable embedding, and a corpus indexed that way answers "language runtime" with `say_runtime_2.soundClip`. `facets:` is the second stage: a `file:` embedded whole (`.metadata`, so keys nobody declared still get indexed) plus computed keys that state a value for every instance including `false` — the raw file can imply "not rigged" and can never say it. The full schema, every extractor and the facet sources are in [`assetTypes/README.md`](../README.md); the scaffolded block with inline guidance is in [`template/type.yaml`](template/type.yaml). ## A container whose contents work together (`system: true`) A type that is a container can say its contents are one working whole by declaring `system: true` at the top level of its `type.yaml`. `package` declares it: the components, modules, scenes and tools a package ships are parts of one system. The declaration is plain data, read from the decoded `type.yaml` (`require("@builtin::assetTypes.assetType.shared.declaration").of("package").system`), and a type that leaves it out is an ordinary type. ## What your type looks like (`preview:`) A type declares how its instances are pictured, and the engine makes the picture when something needs it: staging an instance whose `preview.png` is missing or pictures content it no longer holds queues a render, a view that shows the instance asks for one, and `ref:preview()` renders one on demand. An edit renders nothing on its own. Each `preview.png` records the content it pictured, which is how a stale one is told from a current one. The preview studio owns the light, the backdrop, the framing and the camera, and nothing in the world reaches it, so an instance pictures the same in every world. Declare one of four recipes: | `kind` | The picture is | Your type provides | |---|---|---| | `instantiate` | the asset stood in the studio through its own `instantiate` | `ref.instantiate` | | `image` | a file the instance holds, fitted to the frame, nothing rendered | `source.files`, the candidate files in order | | `surface` | the asset painted on a builtin mesh | `mesh`, and `ref.previewMaterial(self) -> { material }` | | `custom` | whatever `ref.preview(self, opts)` returns | `ref.preview` | ```yaml preview: kind: surface mesh: sphere ``` A scene-instantiable type that declares nothing still answers `ref:preview()` through its `instantiate`, but persists no picture: the declaration is what makes the engine keep one. List `preview.png` among the type's files so an instance may hold it. The picture is findable only when `indexing.content` names it. The two are separate statements, and neither is derived from the other: ```yaml indexing: content: - role: identity modality: image source: { file: "preview.png" } ``` `@builtin::assetTypes.assetType.shared.previewRecipe` reads the declaration; the scaffolded block with inline guidance is in [`template/type.yaml`](template/type.yaml). ## Typing a result from the instance (`M.refShapes`) Every method on `M.ref` is checked against the type its `--!return` names, and every asset of the type gets the same one. That is right for most methods and wrong for the ones whose result is shaped by the ASSET: a method answering one entry per child folder, per row of a config, per binding an input map declares. The widest true annotation for those is `{ [string]: Thing }`, and an indexer accepts every key — so a caller's typo reads as valid and nothing reports. `M.refShapes` lets the type state the result per instance. Each entry is `function(self) -> (typeExpression, source?)`: a Luau type expression for THIS asset, and the module whose type vocabulary the expression uses. Read the asset; never run it — stating a type must not take effect. ```lua -- inputMap.assetType/behavior.luau — the shipped example. -- `activate()` hands back one handle per control the map declares. M.refShapes = { activate = function(self): (string, string) local names = {} for _, control in ipairs(M.ref.controls(self)) do table.insert(names, control.name .. ": Handle") end if #names == 0 then return "", "" end return "{ " .. table.concat(names, ", ") .. " }", "@builtin::modules.zinput.scheme" end, } ``` With it, `map.jump:onPressed(fn)` resolves and `map.noexisting` is reported with the map's real controls. Without it, both pass silently. ### What the call site has to say A per-instance type is matched by the asset's IDENTITY, so it applies only where the reference the method is called on says WHICH asset. Two spellings do: ```lua -- a component field, whose declared default names the asset public = { map = Field.assetRef("inputMap", "@builtin::inputMaps.default", Sync) } local controls = public.map:activate() -- typed for THAT map -- an annotation, naming category and identity local m: AssetRef<"inputMap", "@builtin::inputMaps.default"> ``` `asset.resolve("<identity>", "<category>")` carries the CATEGORY, so the methods the category defines are checked on the result — but not which asset it found, so a per-instance result keeps the category's declared return. A reference that names no asset (`asset.resolve(someVariable)`, a value passed in as a parameter) carries neither, and calls on it are unchecked. That is the same rule everywhere: the checker states what the code states, and a name computed at runtime states nothing. ### The result is a value, not a binding The type belongs to the expression, so it travels the way any inferred type travels. Indexing the call's result is checked; stashing it in a module local and indexing it from another function is not, because the local's declared type is what carries across — and `any` (or no annotation) carries nothing: ```lua -- checked: `d.greting` is reported function awake() local d = public.dialogue:lines() d.greeting:say() end -- NOT checked: `lines` is a module local typed `any` local lines: any = nil function awake() lines = public.dialogue:lines() end function start() lines.greting:say() end ``` Bind what you need at the call site and the names stay checked in both places — the asset-derived type at the boundary, ordinary scoping after it: ```lua local lineGreeting = nil function awake() local d = public.dialogue:lines() lineGreeting = d.greeting -- `d.greting` is reported here end function start() lineGreeting:say() end ``` ### When nothing is reported and you expected something From a call site, a type that is correct and a type that was never published look the same: no diagnostic either way. `require("@builtin::assetTypes.assetType.shared.refShapes").published()` answers which it is. It returns two maps — the category surfaces, and the per-asset results keyed by identity and then by method: ```lua local shapes = require("@builtin::assetTypes.assetType.shared.refShapes").published() shapes.categories.dialogue --> "{ lines: () -> any, lineNames: () -> { string } }" shapes.returns.Shopkeeper.lines --> "{ browsing: Line, farewell: Line, greeting: Line, wares: Line }" ``` An identity absent from `returns` was never published for, which is a different thing from published-and-correct — and the reason to look here rather than at the call site. The answers are recomputed when an instance is written, so a control added to a map reaches the checker with no restart. Only types that declare `refShapes` are read per instance, and at most 64 assets of one type are — past that the type keeps its declared return and the engine log names what was dropped. ## The instantiate hook (`M.ref.instantiate`) — a contracted return A type opts into becoming part of the scene by defining `instantiate` on its `ref` table. That is the whole opt-in: `ref:canInstantiate()` is true exactly when the hook exists, so consumers offer a scene path — an `Asset.source` field, a viewport drop, a tool argument — by CAPABILITY rather than by a list of type names. Unlike `inspect`, this hook's **return is a contract**. A caller writes one piece of code against every instantiable type, so what comes back cannot vary by type: ```lua -- behavior.luau local Instantiable = require("@builtin::assetTypes.assetType.shared.instantiable") local M = {} M.ref = { instantiate = function(self, target, opts) local root = Instantiable.root(self, target, opts) -- IN: the base opts -- ... compose whatever this type is, under `root`, NOW ... return Instantiable.result(self, root, idMap) -- OUT: the contract end, } return M ``` **IN.** `target` is an owning entity ref: the instance lands under (or, for a hierarchy type, onto) that owner. With no target the type spawns a fresh root. `position`, `rotation`, `scale`, `name` and `temporary` mean the same for every type — `Instantiable.root` applies them to a root you mint, `Instantiable.place` to one you adopt, so you never re-read the spec. Honour more opts of your own if your type needs them, and document them in the hook's `--!arg` docs. **OUT.** Return `(root, idMap)` through `Instantiable.result`: - **`root`** — the composed root, as an `EntityRef`. Composition is **synchronous**: everything your type builds is live when you return, so the caller can parent to it and read its components in the same statement. Do not defer the work to a component's `awake` and return an empty shell — a root that is not live is refused. - **`idMap`** — the `originalId -> runtimeId` map naming what you spawned, or nil for a type with no addressable children (`result` normalises it to `{}`). **A caller never receives nil.** A component that re-composes your asset on every load keeps this map and passes it back in, which is how a cross-entity reference into the composition survives a reload. `AssetRef` runs every `instantiate` through `result` on the way out whether or not your type called it, so returning something else fails at your own call rather than handing a caller a nil root. Calling it yourself is still how you say what you return, and running it twice changes nothing. `@builtin::assetTypes.assetType.shared.instantiable`'s README is the full reference for both halves. ## The inspect hook (`M.ref.inspect`) `asset.inspect(ref)` resolves `ref`, builds a common envelope shared by every asset (`identity`, `name`, `guid`, `source`, `typeName`, `typeDefinitionPath`, `scope`, `origin`, `description`, `tags`), then calls the resolved type's own inspect hook — `M.ref.inspect(self)` on `behavior.luau` — for the type-specific `detail`. The hook returns *only* `detail`; the framework fills in everything else: ```lua -- behavior.luau local M = {} M.ref = { inspect = function(self) -- self.path is the asset's own folder. Parse the asset's OWN -- files — source text, a native payload header, its `.metadata` -- — never runtime state (a live component instance, a GPU -- upload, a compiled schema): inspect must work on an asset that -- hasn't been compiled, uploaded, or registered yet. return { -- type-specific fields, whatever this type wants to surface } end, } return M ``` A type that omits `M.ref.inspect` yields the generic record: the envelope alone, `detail = nil`. That's a valid, working state — not an error — but it means agents calling `asset.inspect` / `tools.use("assets","describe", ...)` on an instance of the type see only identity and scope, none of what makes an instance of it meaningful. Declaring the hook is what makes a type's instances inspectable for what they actually are. A hook that raises leaves `detail = nil` and sets `record.warning` to the error text — `asset.inspect` itself never raises for a resolved ref. ## Discovery - `asset.list("assetType")` — every registered asset type. - `asset.inspect("<typename>")` — the type's identity, source path, and this README, plus its `about.yaml` texts as `detail.about`. - `asset.resolve("<typename>", "assetType")` — the `<typename>.assetType` asset itself (used by the validator to find the type's `type.yaml`). ## Related - `assetTypes/README.md` — the full `type.yaml` schema and validator rules. - `@builtin::assetTypes.assetType.shared.instantiable` — the instantiation contract's shared implementation, both halves. - Every other `*.assetType/` here — the concrete types built on this meta-type. ## The Inspector a type ships Every type names the file its Inspector sections come from with `inspector: { type: "inspector.luau" }` in its type.yaml. `asset.create("assetType", name)` scaffolds the block and an `inspector.luau` whose Contents section lists an asset's files, built from `ctx.parts`. `asset.validate` on a type that names none reports `assetType.inspector_required` (an error for a `@builtin` type the engine's library authors; a warning for a world's own, and for one the library carries by reference, which the world that published it answers for), and one whose file does not load `assetType.inspector_invalid`. ## In the Inspector Type: what the type.yaml declares (suffix, container, primary files, the settings and inspector blocks), whether the type ships a behavior, how many instances it has, and **Open** for its files. Schema: the settings every instance carries, as read-only typed controls at their defaults (a type whose schema is per instance says where it is declared). Instances: a link to each of the first 20 assets of the type, and how many there are when there are more.
▲ 0↑ born
▣
module · born here
❒asset
# component_snapshot Serialized snapshots of script-component public data. A component's `public` table is a live proxy — its fields live behind `__iter` / `__index` metamethods, so copying or JSON-encoding the proxy directly yields an empty table. This module materializes the plain, serializable form. It is the script-component parallel to `ecs.snapshot` (native components). ```lua local componentSnapshot = require("modules.component_snapshot") -- one component: live proxy -> plain table local data = componentSnapshot.snapshot(entity(id).component.get("Camera")) -- whole entity: { [type] = data }, named/multi instances nested as -- { [type] = { [instanceName or "__default"] = data } } local all = componentSnapshot.snapshotEntity(entity(id)) ``` `componentSnapshot.plainCopy(value)` materializes ONE live value the same way — a table behind a proxy is walked into a plain table, every other value passes through — for callers reading a single field rather than a whole component. Serialized form: entity refs become id strings, asset refs become `{ __ref, name, type }` envelopes, vectors and colors their plain-table forms; framework functions are omitted. `snapshot` returns nil for a proxy with no serializable fields. Persistence (scene serializer, bundle capture, prototype spawn) and inspection tooling (`debug.inspect`) read component data through this module. Live gameplay reads and writes stay on the proxy itself via `entity(id).component.get` / `getAll`.
▲ 0↑ born
▣
module · born here
❒asset
# transform Math helpers for positions, rotations, and directions on transforms. Exposed as the global `Transform` table via `--!global Transform` — no explicit require needed in user code. Functions that take an entity accept either an entity ID string or an entity proxy table from `entity("id")`. ## Exports Look-at and entity-aware helpers: - `Transform.lookAtQuat(fx, fy, fz, tx, ty, tz) -> (qx?, qy?, qz?, qw?)` — quaternion from origin toward target. Nil when degenerate. - `Transform.lookAt(entity, txOrTarget, ty?, tz?) -> (boolean, string?)` — make an entity face a world position or another entity. Both slots read world space: the subject and an entity target are read as `entity(id).position` and the aim is written as `entity(id).rotation`, so a parent under either one still leaves the aim on the point named. Returns whether the rotation was written, and the reason when it was not. - `Transform.distance(x1, y1, z1, x2, y2, z2) -> number` — Euclidean distance between two points. - `Transform.distanceBetween(entityA, entityB) -> number?` — distance between two entities' world positions. Nil when either is unresolvable. - `Transform.direction(fromX, fromY, fromZ, toX, toY, toZ) -> (dx, dy, dz)` — unit direction vector. - `Transform.directionBetween(entityA, entityB) -> (dx, dy, dz)` — unit world-space direction between two entities' world positions. Rotation shapes: A quaternion **constructor** here returns the four components as four separate values, so a caller either names them or braces the call to make one table: ```lua local qx, qy, qz, qw = Transform.quatFromAxisAngle(0, 1, 0, math.rad(90)) entity("cam").localRotation = { Transform.quatFromAxisAngle(0, 1, 0, math.rad(90)) } ``` A rotation-taking **surface** reads that table through `Transform.toQuaternion`, which also takes euler DEGREES — so `{ qx, qy, qz, qw }`, `{ x =, y =, z =, w = }`, `{ pitch, yaw, roll }` and `{ pitch =, yaw =, roll = }` all mean the same thing wherever a rotation is assigned: `entity(id).rotation` / `.localRotation`, `entityOps.spawn`, `entityOps.transform`, and the capture viewpoints. - `Transform.toQuaternion(rotation, label?) -> { qx, qy, qz, qw }` — the shared reading of a rotation a caller wrote. Raises when the value matches no form, naming what arrived; a value that is one of the shapes a quaternion helper returns is named as such along with the packing it goes in as. - `Transform.tryQuaternion(rotation, label?) -> ({ qx, qy, qz, qw } | nil, message?)` — the same reading without raising, for a surface that wants to raise the message at its own caller's line. - `Transform.readVec3(value, label?) -> { x, y, z }` — the same for a vector. - `Transform.snapVec3(v, step) -> { x, y, z }` — quantize a vector to a step grid. Quaternion construction / conversion: - `Transform.quatFromYaw(yaw)`, `Transform.quatFromYawPitch(yaw, pitch)`, `Transform.quatFromAxisAngle(ax, ay, az, angle)` — quaternion constructors. - `Transform.quatIdentity()` — identity quaternion. - `Transform.euler(qx, qy, qz, qw) -> (yaw, pitch, roll)` and the named alias `Transform.quatToEuler`. - `Transform.eulerToQuat(yaw, pitch?, roll?)` — euler-to-quaternion in YXZ order. Lerps and interpolation: - `Transform.lerp(ax, ay, az, bx, by, bz, t) -> (x, y, z)` — vec3 lerp. - `Transform.lerp1(a, b, t) -> number` — scalar lerp. - `Transform.normalizeAngle(a) -> number` — wrap angle into `[-pi, pi]`. - `Transform.lerpAngle(a, b, t) -> number` — shortest-arc angle lerp. - `Transform.slerp(ax, ay, az, aw, bx, by, bz, bw, t) -> (qx, qy, qz, qw)` — quaternion slerp with shortest-path and near-parallel fallback. Quaternion operations: - `Transform.quatMul(...) -> (qx, qy, qz, qw)` — `qa * qb` composition. - `Transform.quatInverse(qx, qy, qz, qw) -> (qx, qy, qz, qw)` — inverse (= conjugate for unit quats). - `Transform.quatRotateVec(qx, qy, qz, qw, vx, vy, vz) -> (x, y, z)` — rotate a vec3 by a quaternion. Pose helpers: - `Transform.orbit(centerX, centerY, centerZ, radius, height, angle) -> (x, y, z, qx, qy, qz, qw)` — orbital pose facing the center. - `Transform.worldToLocal(...)` / `Transform.localToWorld(...)` — pose-space conversions. Nested `Transform.vec.*` namespace (component-wise vec3): - `Transform.vec.add`, `sub`, `scale`, `dot`, `cross`, `length`, `normalize`. Types: - `Vec3 = { x: number, y: number, z: number }` - `EntityRef = string | { entityId: string }` ## Usage ```luau -- Look-at by coordinates or by target entity: Transform.lookAt("cam", 0, 1, 0) -- an entity target resolves to that entity's world position local aimed, why = Transform.lookAt("cam", "box") -- Orbit pose around a point: local x, y, z, qx, qy, qz, qw = Transform.orbit(0, 1, 0, 5, 2, t) entity.find("cam").localPosition = { x, y, z } entity.find("cam").localRotation = { qx, qy, qz, qw } -- Quaternion math: local qx, qy, qz, qw = Transform.quatFromYawPitch(math.pi / 4, 0) local sx, sy, sz, sw = Transform.slerp(0, 0, 0, 1, qx, qy, qz, qw, 0.5) -- Component-wise vec3 helpers: local nx, ny, nz = Transform.vec.normalize(1, 1, 0) ``` ## Notes - The `--!global Transform` directive promotes the module's typed functions onto the runtime universe's globals bucket, so `Transform.*` is available without any per-source `require`. - Entity-aware functions (`lookAt`, `distanceBetween`, `directionBetween`) report a missing entity or a missing transform in their return value rather than raising: `lookAt` answers `false, "unresolved"` / `"no-transform"` / `"incomplete-target"` / `"degenerate"`, `distanceBetween` answers `nil`, and `directionBetween` answers zeros. - Quaternion APIs operate on raw `(qx, qy, qz, qw)` tuples for parity with the entity proxy's `localRotation.get`/`set`. Use `Transform.quatIdentity()` rather than hand-rolling `(0, 0, 0, 1)`. - `Transform.slerp` flips the second quaternion if `dot < 0` to take the shortest path, and falls back to lerp+normalize when the inputs are within `dot > 0.9995` to avoid `1/0` near-parallel issues.
▲ 0↑ born
▣
module · born here
❒asset
# opts The contract an options table is held to. A call that reads a fixed set of keys acts on those keys and no others, so a key it does not know names a change the caller asked for and never got — a cube asked for `size = 3` that comes back one unit wide, reporting success. `Opts.check` makes that a refusal carrying what to write instead: the key, the accepted set, and the nearest accepted spelling. The same three parts answer for a name of any kind — `tools.use` names the near-miss toolbox, `MaterialAuthor` names the near-miss shader property — so the distance search lives here too, and every caller ranks candidates the same way. Pure Luau, no engine dependencies. ## Exports - `Opts.check(where: string, value: any, accepted: { string }, inTable?: string)` — raise unless every key of `value` is named by `accepted`. `nil` accepts. - `Opts.forward(where: string, own: { string }, fn, ...) -> ...` — call `fn(...)`, and when it refuses an option, append the vocabulary `where` itself takes. - `Opts.unknown(value, accepted) -> { string }` — the keys `value` carries that `accepted` does not name, sorted. - `Opts.nearest(key: string, candidates: { string }) -> string?` — the candidate `key` is most plausibly a misspelling of, or nil. - `Opts.editDistance(a: string, b: string, limit: number) -> number` — single-character edits between two names, capped at `limit + 1`. ## Usage ```luau local Opts = require("@builtin::modules.opts") -- The keys this call reads, named once. local CUBE_OPTS = { "animate", "scale" } function M.cube(name, x, y, z, opts) Opts.check("primitives.cube", opts, CUBE_OPTS) -- primitives.cube: unknown option 'scail'. Accepted: animate, scale. Did you mean 'scale'? -- The spawn target is handed to `entity.spawn`, so it is that call's -- contract a key is checked against — `own` says what this one takes. local e = Opts.forward("primitives.cube", { "opts.scale", "opts.animate" }, entity.spawn, name) -- entity.spawn: unknown option 'size'. Valid options: name, hidden, … -- (primitives.cube itself takes: opts.scale, opts.animate) end ``` A call taking more than one table names which one the key was written in: ```luau Opts.check("primitives.ground", color, { "r", "g", "b", "a" }, "color") -- primitives.ground: unknown option 'red' in color. Accepted: r, g, b, a. Did you mean 'r'? ``` ## Notes - Refusals raise at level 0: the message is the whole error, so a caller reads the contract rather than the line inside the library that checked it. - A key of any type is reported by how it is written, so the positional form of a named shape (`{ 200, 50, 50 }` for `{ r = , g = , b = }`) names its indices rather than passing as a table with nothing the call reads. - `nearest` is case-insensitive, and a candidate containing the key scores by how much longer it is, so `roughnes` prefers `roughness` over `clearcoat_roughness`. How far a plain misspelling may stray scales with the key's own length — a four-letter key admits two edits, a longer one three — so a short key is not answered with an unrelated short name. - Equal scores go to the shorter candidate, then the alphabetically earlier one, so the answer never depends on the order candidates were listed in. - `forward` matches the phrase both this module and the engine's own option checks use for the condition (`unknown option '<key>'`). Every other error passes through as raised.
▲ 0↑ born
△
mesh · born here
❒asset
▲ 0↑ born
△
mesh · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# Model Loads and displays a 3D mesh on an entity. Disabling the component hides the mesh. Supports procedural meshes (`cube`, `sphere`, ...), model files, and remote URLs. Tint and outline are shader effects applied to the mesh. **Material lives on this component.** There is no standalone `Material` component — a material only renders where there is a mesh to render it on, so the `material` field is part of Model (and SkinnedModel). Set it at add time or assign the field later; properties are registry-wide. Public fields: `model`, `material` (`AssetRef<material>`), `tintR/G/B`, `tintBlend`, `outlineR/G/B`, `outlineIntensity`. Methods: `:setTint(color, blend?)` (color: `{r, g, b}` array or `{r=, g=, b=}` map), `:clearTint()`, `:setOutline(color, intensity?)`, `:clearOutline()`, `:setMaterialProperty(prop, value)`, `:getMaterialProperty(prop)`, `:getMaterialPropertyNames()`. ```luau entity(id).component.add("Model", { model = "cube", material = "gold" }) entity(id).component.get("Model").material = "checkerboard" -- swap material entity(id).component.get("Model"):setTint({1, 0, 0}, 0.5) entity(id).component.get("Model"):setMaterialProperty("roughness", 0.2) ```
▲ 0↑ born
◇
component · born here
❒asset
# MeshCollider (component) Collision against mesh geometry. There are no size fields: the collider is the mesh, scaled by the entity's transform. ## The two fits | `fit` | Shape | Bodies | Concavity | |-------|-------|--------|-----------| | `"hull"` (default) | Convex hull of the mesh's points | Static and dynamic | Filled in, so a doorway becomes a slab | | `"exact"` | The mesh's own triangles | Static | Followed exactly | `"exact"` is for static bodies because a triangle mesh is a surface with no interior: rapier cannot derive a mass tensor from one, and a dynamic body built from it would fall through everything it touched. Setting `fit = "exact"` on a dynamic body raises an error naming `"hull"` rather than quietly substituting it. ## Choosing the geometry `mesh` names the asset to collide against. Left unset, the collider tracks whatever mesh the entity renders, including a later swap. Set it to collide against a cheaper proxy than the one on screen, which is the usual move for detailed scenery where the visual mesh carries far more triangles than collision needs. ```lua entity.find("rock").component.add("MeshCollider", {}) -- what you see entity.find("level").component.add("MeshCollider", { fit = "exact" }) -- concave, static entity.find("rock").component.add("MeshCollider", { mesh = "rock_collision" }) -- cheap proxy ``` Because `mesh` is an asset reference, a collision proxy is visible to the asset graph and survives a rename. ## Cost Built shapes are shared: every collider asking for the same mesh, fit and scale gets the one built shape, so a field of a hundred identical rocks pays for one hull rather than a hundred. ## Fields | Field | Meaning | |-------|---------| | `mesh` | Mesh asset to collide against. Unset tracks the entity's renderable. | | `fit` | `"hull"` or `"exact"`. | | `isTrigger` | Report overlaps without blocking. | | `friction` | Surface friction. | | `restitution` | Bounciness. |
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
▲ 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
❒
package · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# DecalProjector Turns its entity into an oriented **box decal projector** — the deferred-decal equivalent of Unreal's Decal Actor or Unity's Decal Projector. The entity's Transform is the projection volume: position and rotation orient the box, scale sizes it, and the projector stamps its `material` along the box's local −Z onto every surface inside the box, reconstructing each covered pixel's world position from scene depth and fading on faces turned away from the projection axis so the decal wraps the surfaces facing it. ## Surface | Field | Type | Meaning | |-------|------|---------| | `material` | `AssetRef<material>` | What the projector stamps. Its base-colour texture + tint drive the decal — a textured material projects its texture, a flat-colour material projects its colour. Defaults to `@builtin::materials.decal_default`. | | `opacity` | `number` | Master fade, `0..1`. | Registers into the `@builtin::modules.decals` registry on `awake`/`onEnable` (and re-registers on property change / in edit mode), which the `@builtin::renderFeatures.decals` render feature reads every frame. ## Example ```lua -- A downward projector that stamps a texture onto whatever is under it. local p = entity.spawn("floorDecal") p.localPosition = { 0, 3, 0 } p.localScale = { 6, 6, 4 } p.localRotation = { 0.70710678, 0, 0, 0.70710678 } -- project down (-Y) p.component.add("DecalProjector", { material = "@builtin::materials.decal_default", opacity = 1.0, }) ```
▲ 0↑ born
◇
component · born here
❒asset
# SphereCollider (component) A spherical collision shape. Works with or without a `Physics` component: on its own it is static collision geometry, and alongside one it moves with the body. ## Sizing `radius` is in the entity's local space, multiplied by the entity's world scale. Leave it unset and the sphere derives from the entity's model bounds. ```lua entity.find("ball").component.add("SphereCollider", { radius = 0.5 }) entity.find("zone").component.add("SphereCollider", { radius = 5, isTrigger = true }) ``` ## Fields | Field | Meaning | |-------|---------| | `radius` | Sphere radius. Unset derives from model bounds. | | `isTrigger` | Report overlaps without blocking. | | `friction` | Surface friction. | | `restitution` | Bounciness. | ## Composing A collider on a child entity attaches to the nearest ancestor carrying a `Physics` component. A character is a `CapsuleCollider` for the body and a `SphereCollider` trigger on a head entity; queries report whichever shape they hit, so the head stays distinguishable from the torso.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# DirectionalLight A directional light in addition to the scene's sun: parallel light arriving from one direction everywhere in the world, the way a body far enough away delivers it. A second star over an alien planet, a moon hanging opposite the sun, a cold fill from the far side of a set. The sun is `Light { kind = "directional" }` — one direction, one colour and the cascade set, held as a single field, so a second `Light` of that kind moves the sun rather than joining it. This component adds a light instead. Several can coexist; they share the scene's light buffer with point, spot and area lights, so the cap is the combined count (64). Public fields: `directionX/Y/Z` (the direction the light TRAVELS, normalised by the engine), `colorR/G/B`, `intensity`, `mobility` (`auto` / `static` / `mixed` / `dynamic` — how a GI bake treats it). Input aliases accepted by `component.add`: `direction`, `color`, `brightness`. Methods: `:resolveMobility()`, `:lightMobility()`. `lighting.lightRows()` reports one of these with `kind = "distant"`; the row's `direction`, `color` and `intensity` are what the shading reads, and its `position` follows the carrying entity. A GI bake gathers it the way it gathers the sun, so a `mobility = "static"` light is baked whole and its live contribution withheld. ```luau entity(id).component.add("DirectionalLight", { direction = { 0.3, -0.5, 0.4 }, color = { 0.7, 0.8, 1.0 }, intensity = 0.6, }) ```
▲ 0↑ born
◇
component · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
# lightCookies Projects an authored image through a spot light's cone — a gobo, window blinds, a headlight cutoff, a projector pattern. ## How it divides up The renderer already projects a texture through a spot's frustum. At each shaded point it reads `zero_feature_texture(uv, cookieLayer)`, with `uv` from that point's position in the light's own clip space, and multiplies the cone by what it finds. `SpotLight.cookieLayer = -1` means no cookie. What the renderer does not decide is what goes in the layer. That is this package: resample an authored image into one full layer of the shared feature-texture array, and point a spot at that layer. ## Using it ```luau local lightCookies = require("@builtin::systems/lightCookies.package/lightCookies") -- Size the array once for every layer the scene will use. lightCookies.configure({ resolution = 256, layers = 4 }) lightCookies.project(lampId, "/textures/cookies/window_blinds.png", { layer = 0 }) ``` Or attach a `LightCookie` component next to the `SpotLight` and author `texture` / `layer` / `gain` / `tint` on it. `lightCookies.clear(lampId)` stops the projection; the layer keeps its contents. ## When a fill lands `fill` and `project` queue the fill and return its layer at once. The module's own loop writes it over the frames that follow: the rasterize is issued on the next frame and the copy into the layer on the frame after, and the fill is written once `renderer.featureTexture.state().fills` reports the copy landed in its layer, one fill at a time in the order they were asked for. The loop belongs to the module, so a fill lands even when the code that asked for it (a one-shot script, a task scope, a component that is destroyed) has ended. `lightCookies.status(layer)` reports where the last fill of a layer stands: ```luau local s = lightCookies.status(0) -- s.state: "queued" | "rasterizing" | "copying" | "written" | "refused" -- s.reason: why a refused fill was refused, in the engine's words -- s.filled: whether renderer.featureTexture.state().filled lists the layer ``` `status` reads a fill `"written"` from the first reading of the array that reports its copy landed, so `s.state` and `s.filled` agree whichever of the module's loop and the reader runs first in a frame. A fill names a layer below the count `configure` sized the array to; one past it is refused at the call, which returns nil and the reason. A fill that cannot land once it is on its way (the GPU refuses its rasterize because its image was given back first, a later `configure` left the array without its layer, or its copy never reached the array) reads `"refused"` with the reason, and is also logged as a warning. A `LightCookie` component whose fill is refused at the call, such as one authored before the scene's `configure` has sized the array, asks again each time `configure` sizes it. `lightCookies.observe().configurations` counts those calls for code of its own that does the same. `lightCookies.observe()` reports how many fills are waiting, the one in progress, and the loop's own counters. ## An empty layer puts the light out The cone is multiplied by what it reads from its layer, so a spot pointed at a layer nothing has filled renders black rather than plain. `project` fills before it points the light, which is the order that keeps the cone lit; if you set `SpotLight.cookieLayer` by hand, fill that layer first. ## The array is shared, and sizing it is explicit The feature-texture array is one resource that every feature using `zero_feature_texture` draws layers from, and it has no allocator: sizing it zeroes every layer, including layers another feature owns. So this package never sizes it as a side effect of setting a cookie, and `layer` is authored rather than assigned. `lightCookies.configure` is the one call that resizes, made once by whoever knows what else in the scene wants layers. A scene that also bakes global illumination shares this array with the lightmap atlas. Size it for both, and give cookies layers above the ones the bake fills. ## A cookie fills a whole layer The cone's lookup runs over the layer's entire `uv` range, so an image is resampled to the layer's extent rather than placed in a corner of it. One layer carries one cookie. ## Alpha The source's alpha is how much of the pattern applies. At alpha 0 the cone passes through unmodulated, so a fully transparent image is a no-op rather than a black cone.
▲ 0↑ born
❒
package · born here
❒asset
# restirLighting Direct light from many small sources at a cost that does not grow with how many there are. A shading pass that loops every light pays for every light at every pixel, which is why the number of lights has to be capped. Resampled importance sampling breaks that link: each pixel draws a few candidates, keeps ONE of them with a probability proportional to what it would contribute, and carries a weight that makes the survivor stand for the whole set. What it keeps is then reused — by its neighbours this frame, and by itself on the next one — so the set a pixel effectively samples keeps growing while the number of candidates it draws stays where you put it. A cave of glowing crystals, a street of practicals, a control room of indicator lamps: hundreds of sources, and each pixel looks at about a dozen. ## Using it Put an `EmissiveSource` on the geometry that glows: ```lua local id = entity.spawn("crystal", { position = { 2, 1, 0 } }).id entity(id).component.add("EmissiveSource", { color = { 0.3, 0.8, 1.0 }, intensity = 8.0, radius = 6.0, size = 0.4, }) ``` Or register sources from a script, which is how a generated scene does it: ```lua local restir = require("@builtin::systems.restirLighting.restirLighting") for i = 1, 400 do restir.set(`lamp{i}`, { position = { math.random() * 60 - 30, 1.5, math.random() * 60 - 30 }, color = { math.random(), math.random(), math.random() }, intensity = 4.0, radius = 5.0, }) end ``` The pass starts with the first source and stops with the last. Settings are on `restir.configure`, or on a `ResampledLighting` component for a scene to author: ```lua restir.configure({ candidates = 16, spatialSamples = 6 }) ``` ## What a pixel pays `candidates + spatialSamples + 1` evaluations, reported by `restir.perPixelCost()`. None of those terms is the source count. The estimate is formed on a grid of one cell per 2x2 pixels (`resolution = "full"` gives every pixel its own cell), across four passes: | Pass | Runs over | Does | |---|---|---| | `restir_sample` | grid cells | draws `candidates` sources at random, resamples to one, merges the cell's own history | | `restir_spatial` | grid cells | borrows from `spatialSamples` neighbours, each evaluated against this cell's surface | | `restir_resolve` | pixels | shades the chosen source with the pixel's own normal, roughness and albedo | | `restir_denoise` | pixels | averages that light over the cells around the pixel and composites it | Which source to look at is a low-frequency decision a cell of pixels can share. What the surface does with it is not, and is computed per pixel. One source per cell is correct on average and different from its neighbour, which is the grain a single sample carries. `restir_denoise` averages across the cells around a pixel, weighing each by how nearly its surface is this one, so the estimate keeps its value and loses its variance. `mode = "reference"` replaces the reservoir lookup with a loop over every registered source at every pixel. That is the picture the resampled mode is estimating and the cost it is measured against — the same shading, the same filter and the same composite, so the two are comparable directly. ## Temporal reuse A cell keeps its reservoir across frames and merges it with the fresh candidates, so a good find keeps paying after the frame that made it. The stored world position is what says the cell is still looking at the same surface; a camera move or an occluder passing in front fails that test and the history is dropped rather than smeared. `historyCap` bounds how many candidates the history is allowed to stand for. Without it a converged cell would outvote every new candidate forever and a lighting change would never work its way out. At the default of 20 a change is absorbed over roughly twenty frames. Removing a source, or turning its `intensity` down to 0, releases it; the pass stops with the last one. ## Where it sits Sources registered here are additional to the lights the engine holds rows for. The passes run in `afterLighting` at orders 80-84 and add to the scene as it was drawn, reading position, normal, albedo and metallic-roughness back out of the G-buffer. A source lights every surface within `radius` that faces it, along the straight line between them. ## One frame, several targets A render feature's `render` runs once a frame, and the passes it enqueues run once per render target — the window, an offscreen capture, a camera rendering to a texture. Each pass therefore reads the frame size out of the textures it is handed and works out its own cell grid, so every target resamples at its own resolution. A reservoir carries the grid width it was written under, which is what stops one target's history being read as another's. The reservoir buffers are sized to the widest grid any target has asked for. A target wider than they have yet been sized for reports the grid it needed, and the frame after that covers it. ## Reading what it did `restir.stats()` returns the counters the passes keep on the GPU for the most recent frame a read-back has landed for, carrying that frame's number so a repeated read is distinguishable from a fresh one. `meanWeight` is counted in sixteenths with each cell capped at 16 so the total fits a 32-bit counter; `peakWeight` alongside it is exact to a thousandth, and is where an estimator that has come loose shows first.
▲ 0↑ born
◇
component · born here
❒asset
# PlayerPrototype Marks the root of a player-setup subtree — the entity and children that define what a joining user becomes (avatar slot, spawn behaviour, controller). On awake it flips the entity to PrototypeOnly participation, so the prototype lives in the editor and drives spawning without being saved as a world entity itself; `kind` and `label` describe it for authoring surfaces. In edit mode it draws a cyan silhouette outline so the prototype root is easy to pick out, and surfaces its own setup-validation messages (missing camera, avatar slot, or wrong participation) on the component's error channel. Location: `src/lua/lib/components/PlayerPrototype.component`
▲ 0↑ born
◇
component · born here
❒asset
# Skybox The scene's sky as a single **material**. Add it to an entity and the engine renders that material across the sky behind everything else: ```lua entity.spawn("sky").component.add("Skybox", { material = "sky_cubemap" }) entity.spawn("sky").component.add("Skybox", { material = "my_custom_sky" }) entity.spawn("sky").component.add("Skybox", { kind = "none" }) -- explicit no sky ``` `Skybox` is the generic, material-driven sky: it points at **any** sky material — a builtin (`sky_procedural`, `sky_solid`, `sky_cubemap`, `sky_equirect`) or your own authored sky-domain material — and makes it the active sky. For the editable procedural atmosphere (day/night, sun, stars, moon), use the [`ProceduralSky`](../ProceduralSky.component/README.md) component instead — it is a `Skybox` over the `sky_procedural` material plus typed parameter controls. A Skybox **materialises its material's GPU handle** when it binds it. The sky's point-of-use is the sky pass, which never binds the material the way a `Model` does, so the component performs the Disk→CPU→GPU upload (`:handle()`) itself. The renderer binds the sky by the material's **identity** (the key it is resident under once materialised), so a material-mode sky renders correctly after boot and mode flips — no manual `:handle()` needed. Like the directional light, the sky is conceptually **singular per scene**: the engine copies the most recently authored sky into the scene-wide sky config the renderer reads each frame. A sky belongs to the scene that spawned it; removing the component reverts the scene to the engine fallback sky. State is bridged through the native `Sky` ECS component (sky type `material` / `none`), so there is no global sky singleton — two scenes can never clobber each other. ## Fields | Field | Type | Default | Meaning | |---|---|---|---| | `material` | string | `sky_procedural` | The sky material to render. Any registered material whose shader is a sky-domain shader. Defaults to the procedural sky so an empty `Skybox{}` is never a black void. | | `kind` | string | `material` | `"material"` renders `material`; `"none"` turns the sky pass off — the explicit authored form of "this scene has no sky". | ## Methods | Method | Description | |---|---| | `skybox:setMaterial(name)` | Point the sky at a different material (materialises its handle). | | `skybox:setNone()` | Turn the sky off explicitly. |
▲ 0↑ born
◇
component · born here
❒asset
# AreaLight A rectangular light source on an entity. The rect is centred on the entity's world position, faces along the entity's world rotation applied to `face` (default "Front", the entity-local -Z that `transform.forward` reports and that `entity:lookAt` aims), and spans `width` by `height`. A rect under a carrier moves and turns with it. Its light arrives from a surface rather than a point, so shading falls off gradually across a wall, highlights stretch to the shape of the source, and shadows carry a penumbra that widens with the rect's size and the caster's distance.
▲ 0↑ born
◇
component · born here
❒asset
# Text3D Renders text in 3D world space. The quad auto-sizes to fit the text content and never clips. Positioning works like TextMeshPro: `offsetX/Y/Z` is a local offset from the entity, `pivotX/pivotY` is the anchor within the quad, and `billboard` makes it face the camera. With `billboard = false` the text reads from the entity's own forward, the local -Z that `transform.forward` reports and that `entity:lookAt` aims. Sizing — three fields, two jobs: - `worldHeight` — the quad's height in **world units** (default `1.0`). This is the one field that resizes a label, and it holds that height under every transform between the label and the world: a label hung off a shrunken detail box on a model comes out the size it asked for, and so does one whose own entity carries a `localScale`. Width follows the rasterised text's aspect. Measure the result with `getWorldSize()`, which reports the extent the quad is drawn at. - `fontSize` — the **raster resolution** in texels (default `32`). Higher values sharpen the texture and change wrapping against `maxWidth`; the world-space size stays `worldHeight`. - `scale` — rasterisation scale multiplier applied at raster time. Resolution only, like `fontSize`. `maxWidth` is the width the text wraps at, in pixels of the raster `fontSize` states; `0` keeps it on one line. A label already drawn is laid out again when the field is written, so a caller re-wraps one label to line after line rather than making a label per line. `alphaCutoff` decides whether the letters are a surface. At `0` (the default) the text is pure alpha blending: it draws over what is behind it and leaves the depth buffer alone, so a screen-space effect that reads scene depth — volumetric fog, screen-space shadows — integrates the whole distance behind the letters and the text sits inside it. Above `0`, coverage at or over the threshold is drawn opaque and written to depth, so those effects stop at the glyph shape instead. `0.5` reads well for most fonts; higher thins the letters, lower keeps more of the antialiased edge. Public fields: `content`, `fontSize`, `color`, `alignment`, `richText`, `maxWidth`, `outline`, `outlineColor`, `background`, `scale`, `worldHeight`, `alphaCutoff`, `shadowX/Y`, `fontFamily`, `weight`, `slant`, `offsetX/Y/Z`, `pivotX/Y`, `billboard`. `weight` and `slant` are strings (`"regular"` / `"bold"`, `"normal"` / `"italic"`). Methods: `setText(content)`, `setStyle(options)`, `getText()`, `getSize()`, `getWorldSize()`, `refresh()`. ```luau entity(id).component.add("Text3D", { content = "Hello World" }) entity(id).component.add("Text3D", { content = "HP: 100", worldHeight = 0.5, fontSize = 96, color = "red", offsetY = 2.0, pivotY = 0 }) -- A title that keeps its letters crisp through volumetric fog. entity(id).component.add("Text3D", { content = "RAISING", worldHeight = 3.0, alphaCutoff = 0.5 }) ``` A label that is not showing, or came out in a face you did not ask for, reads back out of the text system: `text.observe()` lists every live text object with the entity that owns it and the texture its raster is in, and `text.face(h)` names the font face the shaper actually used against the `fontFamily` that was requested. `topics/text` walks both.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# Transform Position, rotation, and scale for an entity, as the authoring face of the transform the entity holds. Add it to an entity to reach these fields and methods. Public fields: `positionX/Y/Z`, `rotationX/Y/Z/W` (quaternion), `scaleX/Y/Z`. They hold the entity's LOCAL transform — where it stands relative to its parent — and an entity with no parent stands at those coordinates in the world. The entity's transform is what the component reports, and the fields mirror it. Adding the component to an entity that already stands somewhere reads that placement into the fields and leaves the entity where it is. A field the init table names is written onto the entity instead, so `component.add("Transform", { positionX = 1, positionY = 2, positionZ = 3 })` moves the entity to `(1, 2, 3)`; every axis the table says nothing about keeps the value the entity already holds, so adding it with `{ scaleY = 2 }` alone stretches the entity on Y and leaves it standing where it stood. Writing one field moves the entity on the axis that field names. The other axes of the same vector are read off the entity first, so a position written after something else moved the entity keeps the move on the axes it says nothing about. Methods take one table each and are called with a colon: | Method | Argument | |---|---| | `t:setPosition({x, y, z})` | position table, `{x, y, z}` or `{x =, y =, z =}` | | `t:addPosition({dx, dy, dz})` | offset table, added to where the entity stands now | | `t:setRotation({x, y, z, w})` | quaternion table, `{x, y, z, w}` or `{x =, y =, z =, w =}` | | `t:rotateAxisAngle({ax, ay, az}, angle)` | axis table, and an angle in radians | | `t:setScale(2)` or `t:setScale({x, y, z})` | one number for a uniform scale, or a table | | `t:lookAt(target)` | a position table, or an entity id / name / proxy | | `t:getPosition()` / `t:getRotation()` / `t:getScale()` | none | `setPosition`, `addPosition`, `setRotation`, `rotateAxisAngle` and `setScale` write the entity's local transform. `rotateAxisAngle` composes onto the rotation the entity already holds, and reads its axis in the frame that rotation is relative to — the parent's, and the world's for an entity with no parent. The three getters report what the entity's transform holds at the moment they are called, so a rotation written by `lookAt` — or a position written by another script — reads back through them. `lookAt` is the one method on a WORLD position. It takes one target: a position table (`{x, y, z}` or `{x =, y =, z =}`), or an entity — an id string, an entity name, or an entity proxy — whose world position is aimed at. It answers whether the rotation was written, and the reason when it was not. The `Camera` component's `lookAt` reads the same slot. ```luau local t = entity(id).component.get("Transform") t:setPosition({ 0, 5, 0 }) t:setScale({ 2, 2, 2 }) t:rotateAxisAngle({ 0, 1, 0 }, math.pi / 2) local pos = t:getPosition() ```
▲ 0↑ born
◇
component · born here
❒asset
# BoxCollider (component) A box collision shape. Works with or without a `Physics` component: on its own it is static collision geometry, and alongside one it moves with the body. ## Sizing `half` is the box's half-extents in the entity's local space, multiplied by the entity's world scale. Leave it unset and the box derives from the entity's model bounds, so a bare `BoxCollider` matches what is rendered at any scale. ```lua entity.find("crate").component.add("BoxCollider", {}) -- fits the mesh entity.find("wall").component.add("BoxCollider", { half = { 2, 4, 0.2 } }) -- authored ``` ## Fields | Field | Meaning | |-------|---------| | `half` | Half-extents. Unset derives from model bounds. | | `isTrigger` | Report overlaps without blocking. | | `friction` | Surface friction. | | `restitution` | Bounciness. | ## Composing A collider on a child entity attaches to the nearest ancestor carrying a `Physics` component, so a body made of several shapes puts each on its own entity and each keeps its own material and trigger flag. Past a few dozen boxes sharing one material, `CompoundCollider` is cheaper: it is a single broadphase volume where separate components are one each.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# VehicleController Drives a car, truck, kart or buggy built from `WheelCollider`s. Put it on the chassis, the dynamic rigid body the wheels sit under, and the player's controls reach the wheels on keyboard, gamepad and touch alike: the controls come from `@builtin::inputMaps.vehicle` through `Zin.controllers.vehicle`. | Control | Keyboard | Gamepad | Touch | |---|---|---|---| | throttle | `W` / `S` | right / left trigger | stick, vertical | | steer | `D` / `A` | left stick | stick, horizontal | | brake | `Space` | south | button | | handbrake | `Ctrl` | east | button | | boost | `Shift` | north | button | | recover | `R` | select | button | | zoom | scroll | shoulders | pinch | What each one does to the vehicle: - **throttle** drives the driven wheels (`drive`: `all`, `front` or `rear`), the torque fading to nothing at `topSpeed`. Pulled back while the vehicle rolls forward it brakes; at a stop it reverses, up to `reverseTopSpeed`. Pushed forward while it rolls backward it brakes too. - **steer** turns the steered wheels, the pair nearest the chassis' forward (its local -Z), at up to `maxSteerAngle` at rest, narrowing to `steerAtTopSpeed` at `topSpeed`, turning at `steerRate`. - **brake** brakes every wheel with `brakeTorque`; **handbrake** locks the rear pair with `handbrakeTorque`. - **boost** multiplies the drive torque and the top speed by `boost`. - **in the air**, with no wheel on the ground, the chassis turns toward level, its nose raised to `airPitch` by throttle and lowered by reverse or brake, spinning at `airSpin` where it is steered; `airControl` is how fast it gets there (0 lets it fly as it left the ground). - **recover**, or lying on its side or roof slower than 1 m/s for `recoverAfter` seconds, sets the chassis back on its wheels where it lay, `recoverLift` above it, facing the way it faced. - **zoom** changes how far the `camera`'s chase controller trails. The component writes `motorTorque`, `brakeTorque` and `steerAngle` on the wheels, each only when its value changes. Only the peer that owns the chassis reads its controls. ## Fields | Field | Default | Unit | Meaning | |---|---|---|---| | `inputMap` | `@builtin::inputMaps.vehicle` | | The controls it reads. | | `takeControlOnAwake` | `true` | | Take the controls as it wakes; `false` waits for `takeControl()`. | | `drive` | `all` | | Which wheels the throttle drives. | | `motorTorque` | 1500 | N·m | Drive torque per driven wheel at full throttle. | | `reverseTorque` | 800 | N·m | Drive torque per driven wheel at full reverse. | | `brakeTorque` | 3000 | N·m | Brake torque per wheel at full brake. | | `handbrakeTorque` | 6000 | N·m | Brake torque per rear wheel under the handbrake. | | `topSpeed` | 25 | m/s | Forward speed where the drive has faded to nothing. | | `reverseTopSpeed` | 8 | m/s | Reverse speed where the drive has faded to nothing. | | `boost` | 1.6 | × | Drive and top speed multiplier while boosting. | | `maxSteerAngle` | 30 | degrees | Full lock at rest. | | `steerAtTopSpeed` | 8 | degrees | Full lock at `topSpeed`. | | `steerRate` | 150 | degrees/s | How fast the wheels turn toward the lock asked for. | | `airControl` | 4 | rad/s² | How fast the chassis turns in the air toward the attitude asked for. | | `airPitch` | 20 | degrees | Nose angle held in the air at full throttle or full reverse. | | `airSpin` | 90 | degrees/s | Spin about the up axis at full steer in the air. | | `recoverAfter` | 3 | s | Time overturned before it rights itself; 0 waits for the control. | | `recoverLift` | 1 | m | Height above where it lay that a righted vehicle is set down. | | `camera` | | | A camera with `@builtin::controller.chase` that zoom moves. | ## Methods - `takeControl()`: the controls go live; the walking controls stand down. - `releaseControl()`: the controls go back; the wheels coast straight. - `hasControl() -> boolean` - `driveState()`: `{ throttle, steer, brake, handbrake, boost, speed, groundedWheels, wheels, steerAngle, overturned, recoveries, lastRecover }`. ## Example A monster truck: a heavy chassis on four big wheels with long travel, the suspension force raised to carry the landings. ```luau local truck = entity.spawn("truck") truck.position = { 0, 2.2, 0 } truck.component.add("Physics", { kind = "dynamic", mass = 2000 }) truck.component.add("BoxCollider", { half = { 1.3, 0.4, 2.2 } }) for _, at in { { -1.5, -1.7 }, { 1.5, -1.7 }, { -1.5, 1.7 }, { 1.5, 1.7 } } do local wheel = entity.spawn("wheel", { parent = truck.id }) wheel.localPosition = { at[1], -0.2, at[2] } local tyre = entity.spawn("tyre", { parent = wheel.id }) tyre.component.add("Model", { model = "cylinder" }) tyre.localRotation = { 0, 0, 0.7071068, 0.7071068 } tyre.localScale = { 1.6, 0.6, 1.6 } wheel.component.add("WheelCollider", { radius = 0.8, width = 0.6, suspensionDistance = 0.8, springRate = 25000, damperRate = 5000, maxSuspensionForce = 40000, visual = tyre, }) end truck.component.add("VehicleController", { motorTorque = 4000, brakeTorque = 8000, topSpeed = 25 }) ``` A car the player gets into takes the controls when the driver enters: ```luau car.component.add("VehicleController", { takeControlOnAwake = false }) car.component.get("VehicleController"):takeControl() -- driver in car.component.get("VehicleController"):releaseControl() -- driver out ```
▲ 0↑ born
◇
component · born here
❒asset
# controller.character Character-oriented controller component for builtin movement-driven actors.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# Video Plays an MP4 video onto a plane mesh attached to this entity. Creates a hidden child entity with a `Model` + `Material` that displays the video render target — same pattern as `UiPanel`'s 3D mode. The video renders to a standard render target (`rt:video_N`), so it can also be used in any material via `material.setTexture(...)`. Public fields: `url`, `loop`, `autoplay`, `rate`, `width`, `height` (0 = auto aspect), `maxResolution`, `showOverlay`. Methods: `play()`, `pause()`, `stop()`, `seek(time)`, `getInfo()`, `getHandle()`. ```luau entity(id).component.add("Video", { url = "@builtin/videos/test_bars.mp4", loop = true, autoplay = true, }) ```
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# Birds Eye Camera Strict overhead camera — looks straight down (-Y world) and follows the target's X/Z position. No yaw, no pitch, no orbit; only `height` (zoom). ## Vs. the other top-down behaviours | Behavior | What it does | When to pick it | |---|---|---| | `birds_eye` | locked overhead, follows target XZ only | tactical / map views, navigation overlays | | `rts` | overhead with yaw + WASD pan + RMB rotate | strategy games where the user drives the view | | `isometric` | fixed angle (not straight down) + WASD pan | classic isometric game-feel | ## Fields | Field | Default | Notes | |---|---|---| | `inputMap` | `@builtin::inputMaps.cameraRig` | The map whose `zoom` control adjusts the height. Point it at a world's own map to rebind this camera. | | `target` | `""` | Entity ID. Falls back to `Camera.follow` when empty. | | `height` | `25.0` | World-Y distance above target. | | `minHeight` | `5.0` | Scroll-zoom lower bound. | | `maxHeight` | `120.0` | Scroll-zoom upper bound. | | `zoomSpeed` | `0.15` | Multiplicative per scroll tick (`exp(-scroll * zoomSpeed)`). | | `smoothing` | `0.18` | Per-second blend. `0` = instantly stuck to target. `1` = never moves. | | `offsetX`, `offsetZ` | `0.0` | Constant XZ offset added to the target's position. | ## Use as a camera behavior ```lua -- World-default world.camera_default = asset.ref("@builtin::controller.birds_eye") -- Per-scene swap layers.active.camera.behavior = asset.ref("@builtin::controller.birds_eye") layers.active.camera.follow = layers.active.players.localPlayer ``` The component carries the `cameraBehavior` tag in `.metadata`, so the slot validation in `Camera.behavior = ref` will accept it. ## Controls - **Scroll** — adjust height (zoom in/out) - No rotation, no panning — the target moves; the camera just follows.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# CompoundCollider (component) One collision shape built from many box children, sharing a single broadphase volume and a single material. ## When to reach for it | Situation | Use | |-----------|-----| | A handful of shapes, each wanting its own material or trigger flag | Separate collider components on child entities | | Many boxes sharing one material, such as a voxel chunk or a destructible wall | `CompoundCollider` | The difference is cost. Separate collider components are one broadphase entry each; a compound is one entry for the whole set, with contacts still resolved per child. That crossover is what makes a compound the right answer for a voxel chunk and the wrong one for a character's head. ## Children Each child is `{ offset = vec3, half = vec3 }` in the entity's local space. The whole set scales with the entity's transform. ```lua entity.find("chunk").component.add("CompoundCollider", { children = { { offset = { 0, 0, 0 }, half = { 0.5, 0.5, 0.5 } }, { offset = { 1, 0, 0 }, half = { 0.5, 0.5, 0.5 } }, }, }) ``` ## Fields | Field | Meaning | |-------|---------| | `children` | The box set. | | `isTrigger` | Report overlaps without blocking, for every child. | | `friction` | Surface friction, shared by every child. | | `restitution` | Bounciness, shared by every child. |
▲ 0↑ born
◇
component · born here
❒asset
# third_person Third-person camera controller. Orbits behind/above a target entity. Right-click drag to rotate around the target, scroll to zoom. Falls back to a static center if no target. Public fields: `target`, `centerX`, `centerY`, `centerZ`, `height` (vertical offset above target origin), `distance`, `minDistance`, `maxDistance`, `sensitivity`, `zoomSpeed`, `minPitch`, `maxPitch`, `yaw` (exposed for parent scripts). Controls: RMB drag to orbit while nothing else in the world binds the right button, scroll to zoom. ```luau entity(id).component.add("third_person", { target = "player", distance = 5 }) ```
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# Physics Adds rigid body dynamics to an entity. Body kind is `"dynamic"` (affected by forces), `"static"` (immovable), or `"kinematic"` (moved by script, affects other bodies). Public fields: `kind`, `gravityScale`, `mass`, `linearDamping`, `angularDamping`, `ccd`, `interpolation`, `lockRotationX/Y/Z`, `lockTranslationX/Y/Z`. `ccd` (continuous collision detection) sweeps the body along its motion so it cannot pass through thin geometry between two steps. It is off by default: the sweep runs on every body that travels further than its own thickness in one step — most of a collapsing structure or a debris burst — and it dominates the physics budget when a scene has many such bodies. Turn it on for the individual bodies that would visibly tunnel: bullets, thrown props, a wrecking ball. ```luau entity(id).component.add("Physics", { kind = "dynamic", ccd = true }) ``` `mass` is the body's total mass in kilograms. At its default of 0 the mass comes from the collider volume at 1 kg per cubic metre, so a bigger collider makes a heavier body; a value greater than zero sets the total mass exactly. Methods: `:applyForce(x, y, z)`, `:applyImpulse(x, y, z)`, `:applyTorque(x, y, z)`, `:setVelocity(x, y, z)`, `:addVelocity(dx, dy, dz)`. ```luau entity(id).component.add("Physics", { kind = "dynamic" }) entity(id).component.get("Physics"):applyImpulse(5, 0, 0) ```
▲ 0↑ born
◇
component · born here
❒asset
# Collider Gives an entity a collision shape. Can exist without a `Physics` component (static collision geometry like walls/floors). For dynamic bodies, `"mesh"` is automatically promoted to `"convex"` since concave trimesh colliders only work with static bodies in Rapier3D. Shapes: `"box"`, `"sphere"`, `"capsule"`, `"convex"`, `"mesh"`, `"compound"`. Sizing: dimensions are half-extents in the entity's local space, and the collider is multiplied by the entity's world scale — it scales with the entity, the same way its mesh does. When dimensions are omitted, the shape derives from the entity's model bounds (still scaled), so a bare `Collider` matches the rendered geometry. Providing dimensions (`size`/`sizeX`/`sizeY`/`sizeZ`, `radius`, `height`) makes them authoritative over the model bounds — but the world-scale multiply still applies. So a box with `sizeX = 0.5` on an entity scaled `[50, 0.2, 50]` is a `25 x 0.1 x 25` half-extent collider, not `0.5`; pass local half-extents and let scale do the rest (or leave the entity at scale 1 to size in world units). The rule is the same with or without a `Physics` component. Public fields: `shape`, `sizeX`, `sizeY`, `sizeZ` (box half-extents, local — scaled by the entity), `radius` (sphere/capsule), `height` (capsule), `isTrigger`, `friction`, `restitution`. Methods: `:setShape(shape)`, `:setSize(size)` (accepts `{x,y,z}` array or `{x=,y=,z=}` map), `:setRadius(r)`, `:setTrigger(bool)`. ```luau entity(id).component.add("Collider", { shape = "box" }) -- box fitted to the entity's model entity(id).component.add("Collider", { shape = "box", sizeX = 2, sizeY = 4, sizeZ = 0.2 }) entity(id).component.add("Collider", { shape = "sphere", radius = 0.5 }) entity(id).component.add("Collider", { shape = "capsule", radius = 0.3, height = 2 }) ```
▲ 0↑ born
◇
component · born here
❒asset
# Light Adds a light source to an entity. The entity's world transform determines the light's position (point) or direction (directional). Point light positions are auto-synced from that world transform, so a light on a child entity burns where the entity stands — the position `entity.position` reports. A point light's `intensity` is on one scale with the `SpotLight` component's `intensity`/`brightness`: the same number at the same `radius`/`range` puts the same light on a surface either kind faces from the same place, and a spot spends it on the cone it opens on rather than all around itself. Kinds: `"point"`, `"spot"`, `"directional"` (the scene's sun), `"ambient"`, `"distant"` (a parallel light beside the sun). Public fields: `kind`, `colorR/G/B`, `intensity`, `radius`, `directionX/Y/Z`, `castsShadows`, `lightChannels`, `mobility`. `range`, `color` and `direction` are aliases accepting the composite/renamed forms. Methods: `:setColor(color)` (color: `{r, g, b}` array or `{r=, g=, b=}` map), `:setIntensity(i)`, `:setRadius(r)`, `:setDirection(dir)`, `:setKind(lightKind)`. `kind = "spot"` opens a cone along the entity's forward axis, at the engine's default 30° outer and 20° inner half-angles; the `SpotLight` component is the one that carries the cone angles and the `face` axis as fields. `"directional"` and `"distant"` are parallel lights: they arrive from the same direction at every point in the world and no distance attenuates them, so their `intensity` reads against the sun's rather than against a point light's. The `SpotLight` README carries the rest of how to balance a mixed point-and-spot rig. ```luau entity(id).component.add("Light", { kind = "point", intensity = 2, radius = 10 }) entity(id).component.add("Light", { kind = "directional", direction = {-0.5, -1, -0.3} }) ```
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
# shadowSystem Cascaded shadow map (CSM) **policy** for crisp directional shadows across a large view distance. The rendering *mechanism* lives in the engine; this package is the per-frame policy that drives it. ## Layout - `shadowSystem.module` — the policy: PSSM frustum splitting, per-cascade bounding-sphere fit + texel-snap stabilization, and the matrix math. Pushes per-cascade light-space matrices + split distances to the renderer via `__rendering.setShadowCascades` each frame. - `ShadowCascadeDriver.component` — attach ONE to a scene entity to drive the module every frame from the active camera + sun. - `depthBounds.renderFeature` — reduces the frame's depth target to the nearest and farthest view depth any surface occupies, and hands the result to the module. ## Flow 1. Add a `ShadowCascadeDriver` component to any entity in the scene. 2. Each frame it slices the camera frustum into N cascades (N from `render.shadow_cascades`), fits a stable light-space ortho to each, and pushes the matrices + split distances to the renderer. 3. The renderer renders one shadow depth pass per cascade; the fragment shaders pick a cascade by the fragment's distance from the camera. ## Fitting splits to the depth that is there Splits are placed over a range. Taken from the camera's near and far planes that range is whatever the scene *could* occupy; taken from the depth buffer it is what the scene *does* occupy. A courtyard that stops at 40 m under a 200 m shadow distance spends most of its cascade texels on empty space. ```lua entity.find("scene").component.add("ShadowCascadeDriver", { fitToDepth = true, -- place splits over the measured depth depthMargin = 0.1, -- room past the far end for a frame of movement }) ``` The **far** end is what moves. The near end stays where the camera puts it: the renderer picks a cascade from a fragment's distance and cascade 0 is fitted to the range's start, so raising that start would leave anything nearer sampling a box it does not sit inside. The measurement is one frame old, because a GPU read-back cannot land in the frame that produced it. `depthMargin` is the room that gives something to move away in before it falls outside the last cascade. With `fitToDepth` off, no reduction is dispatched and splits are placed exactly as before. Purely additive: with no driver the renderer falls back to its legacy single camera-fit directional shadow. Quality is controlled by the `render.shadow_resolution` / `render.shadow_cascades` / `render.shadow_distance` / `render.shadow_split_lambda` world settings. `render.shadow_distance` is a cap on the range the splits run over, and its default of 0 leaves that range to the frame — the depth its own renderables reach. ## Documentation See `shadowSystem.module/README.md` and `ShadowCascadeDriver.component/README.md`.
▲ 0↑ born
◇
component · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
# areaLights system Lights that emit from a surface rather than from a point. Every screen, neon sign, glowing window, ceiling panel and lantern face is an area emitter. Standing a point light in the middle of one gets the brightness roughly right and the character completely wrong: a point puts a pinpoint reflection on the floor and a radial falloff on the wall, where the real thing puts the shape of the emitter and a falloff that follows its extent. ## Using it An emitter that should follow an entity's face is a component: ```lua local id = entity.spawn("tv").id entity(id).component.add("SurfaceLight", { width = 2.0, height = 1.2, face = "front", -- front | back | up | down | left | right intensity = 6.0, range = 12, }) ``` The rectangle turns with the entity, so a tilted screen lights the direction it actually points. Every field is live, and an `intensity` of 0 removes the light. For code placing emitters directly: ```lua local surfaceLight = require("@builtin::systems.areaLights.surfaceLight") surfaceLight.set("strip", { position = { 0, 5, -12 }, right = { 1, 0, 0 }, up = { 0, 1, 0 }, width = 0.6, height = 7, color = { 1, 0.72, 0.35 }, intensity = 5, }) surfaceLight.remove("strip") surfaceLight.configure({ diffuse = 1, specular = 0.5 }) surfaceLight.clear() ``` ## What the knobs do | field | effect | |---|---| | `width` / `height` | Size of the emitting rectangle, world units. The shape of the light is the shape of this. | | `face` | Which way the entity emits. | | `range` | How far the light carries, with a smooth cutoff at the end. | | `intensity` | Brightness. 0 removes the light. | | `colorR` / `colorG` / `colorB` | Emitter colour. | | `offset` | Distance the rectangle sits proud of the entity's origin, along the face's normal. | | `diffuse` / `specular` | Scales on the two terms — the wash across a surface, and the reflected shape of the emitter. | Shape is the point. A wide panel puts a broad pool on the floor; a narrow strip puts a narrow elongated one. That difference is not reachable with a point light at any brightness. At most 32 emitters are held at once, because the shader loops every light for every pixel. ## How it renders `areaLighting.renderFeature` runs one compute pass after lighting. Diffuse is the exact irradiance of the quad — Lambert's formula over its four edges — which matters where a point approximation fails hardest: a wide emitter close to a wall. Specular uses the representative point on the rectangle nearest the reflection ray, with the lobe widened to cover the emitter's solid angle and its peak scaled down to match, so a large emitter reads broader rather than brighter. ## Files ``` areaLights.package/ SurfaceLight.component/ -- an emitter on an entity's face surfaceLight.module/ -- the emitter registry and scales areaLighting.renderFeature/ -- the compute pass ```
▲ 0↑ born
◇
component · born here
❒asset
# controller.editor The viewport camera you author from. It is the behavior on the editor scene's `EditorCamera` — a world's players never meet it, which is why it reads the editor's controls rather than a game's. Rotates the pivot child exposed by a sibling `Player` component if present, otherwise rotates self. Public fields: `moveSpeed`, `sensitivity`, `speedMin`, `speedMax`. Controls come from the map on `inputMap`, `@builtin::inputMaps.editor` by default: `move` to fly, `flyVertical` to rise and drop, `look` to aim while flying, `zoom` to adjust move speed. Rebind them by pointing `inputMap` at another map. ```luau entity(id).component.add("controller.editor", { moveSpeed = 10 }) ```
▲ 0↑ born
◇
component · born here
❒asset
# WASDInput Action-to-input-bus driver. Reads the `move` axis and `jump` action each frame, remaps `move` to the active viewport camera's forward/right basis (via `camera.main()` + `entity(camId).transform.forward`), and writes the resulting world-space horizontal velocity to a target component's input fields: - `target.inputX`, `target.inputZ` — world-space velocity × speed - `target.inputJump` — boolean, true while the jump action is held Defaults to writing to `CharacterController` on the same entity, but the field names are the contract — any component that exposes `inputX/inputZ/inputJump` is a valid sink (vehicles, hovercraft, custom CCs). ## Why generic Independent of which camera is active and independent of the visual layer: - Works with any camera (third-person, top-down, FPS, fixed, vehicle). - Works with no camera (falls back to world-axis WASD). - Works without `Locomotion` or any animation system — CC moves, visualization is opt-in. - Works without a follow rig — `camera.main()` is the seam, not a specific camera component. ## Public fields | Field | Default | Purpose | |---|---|---| | `speed` | `5.0` | Multiplier on the unit-length input vector | | `targetComponent` | `"CharacterController"` | Component to write `inputX/inputZ/inputJump` onto | A custom control scheme is authored as an inputMap that rebinds the `move` axis and `jump` action — not per-instance key fields on this component. ## Example ```lua -- Player with CC + WASDInput + any camera entity(playerId).component.add("CharacterController") entity(playerId).component.add("WASDInput") -- Active viewport camera (any kind) local cam = entity.spawn("cam", { parent = playerId }) cam.component.add("Camera", { fov = 60, priority = 0 }) cam.component.add("orbital_follow", { follow = playerId }) -- Press W → CC.inputX/inputZ are written camera-relative each frame -- → CC translates the body → MovementState reports velocity → -- (optional) Locomotion rotates the visible mesh + plays anims. ```
▲ 0↑ born
◇
component · born here
❒asset
# Cinematic Camera Wide-FOV, slow-lerp follow with a configurable over-the-shoulder offset. Intended for cutscenes, dialogues, and walking moments where you want a more composed framing than `orbital_follow` and softer motion than `chase`. The camera holds a right-shoulder offset relative to the target's facing and slow-lerps both position and look-at point so quick player movements feel cinematic rather than jittery. ## Vs. the other follow cams | | `orbital_follow` | `chase` | `cinematic` | |---|---|---|---| | Orbit (mouse) | yes | no | no | | Position lag | n/a | low (sticky) | high (cinematic) | | Look-at lag | n/a | low | even higher than position | | Composition | centered behind | locked behind | shoulder-framed | | Best for | character action | vehicles, fast | cutscenes, walking, dialogue | ## Fields | Field | Default | Notes | |---|---|---| | `target` | `""` | Entity ID. Falls back to `Camera.follow`. | | `distance` | `4.5` | Trail distance behind target. | | `height` | `1.8` | Vertical offset above target origin. | | `shoulderX` | `0.6` | Right-shoulder offset (negative = left shoulder). | | `fov` | `60.0` | Pushed once to the sibling `Camera` component on `awake`. | | `positionSmoothing` | `0.35` | High = slow, cinematic. Per-second blend. | | `lookAtSmoothing` | `0.45` | Higher than position so framing reads as "anticipating". | ## Use as a camera behavior ```lua world.camera_default = asset.ref("@builtin::controller.cinematic") -- Or per-scene layers.active.camera.behavior = asset.ref("@builtin::controller.cinematic") layers.active.camera.follow = layers.active.players.localPlayer ``` The component carries the `cameraBehavior` tag in `.metadata`. ## Notes - No user-driven rotation, no scroll-zoom — `cinematic` is set-it-and-let-it-frame. - Swap `shoulderX` for a left-shoulder shot. Sign-flip in script for an interactive switch. - Pairs especially well with `humanoid_visual` + `standard_controller` for narrative gameplay.
▲ 0↑ born
◇
component · born here
❒asset
# controller.side_scroller Side-scroller camera controller. Fixed-offset camera that follows a target from the side (classic 2.5D perspective). Maintains a constant position relative to the target and looks slightly ahead. Public fields: `target`, `centerX`, `centerY`, `centerZ` (fallback), `offsetX`, `offsetY`, `offsetZ` (camera offset from target), `lookAheadX`, `lookAheadY`, `lookAheadZ` (look-at offset from target). Controls: none — follows the target automatically. ```luau entity(id).component.add("controller.side_scroller", { target = "player", offsetX = -8, offsetY = 3 }) ```
▲ 0↑ born
◇
component · born here
❒asset
# controller.first_person First-person camera controller. Positions the camera at an entity's location (+ eye-height offset) and rotates with mouse look. It resolves its follow target in this order: its own `target` field, then `Camera.follow` when `target` is empty, then the static `posX/posY/posZ` when neither is set. `Camera.follow` is the standard slot every behavior reads, so a first-person camera selected through `Camera.behavior` tracks whatever `Camera.follow` points at — the correct source for a player camera, whose body id is minted fresh per spawned clone and cannot be authored onto the behavior ahead of time. Public fields: `target` (entity id to follow, empty = use `Camera.follow`, then `posX/Y/Z`), `posX`, `posY`, `posZ`, `offsetX`, `offsetY` (eye height above target origin), `offsetZ`, `sensitivity`, `minPitch`, `maxPitch`. Controls: pointer lock to rotate (yaw + pitch), which the standard scheme takes while the world runs. With the cursor handed back, an RMB drag rotates too — for as long as nothing else in the world binds the right button. ```luau -- Track the Camera's follow target (set Camera.follow, no id authored here): entity(id).component.add("controller.first_person", { offsetY = 1.6 }) -- Or a static camera at a fixed position: entity(id).component.add("controller.first_person", { posX = 0, posY = 1.6, posZ = 3 }) ```
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# ParentConstraint Copies the full transform (position + rotation + scale) from a target entity — like parenting but without hierarchy. The entity stays independent in the scene tree. Public fields: `targetEntityId`, `axisX`, `axisY`, `axisZ`, `weight`. Methods: `setTarget(entityId)`, `setWeight(w)`. ```luau entity(id).component.add("ParentConstraint", { targetEntityId = playerId, weight = 1.0, }) ```
▲ 0↑ born
❒
package · born here
❒asset
# oceans A sea and the ground under it, as one package: the wave simulation, the surface that renders it, the seabed it is seen through, and the answer to "how high is the water here" that gameplay needs to float things on it. ```lua local Ocean = require("@builtin::systems.oceans.modules.ocean") Ocean.create({ preset = "gale" }) ``` That is a whole ocean — waves out to the horizon, foam on the breaking crests, a seabed below, and a surface that follows whoever is looking at it. To float something on it: ```lua local ocean = Ocean.forEntity(oceanEntityId) boat.position = { x, ocean:sampleHeight(x, z), z } ``` It is a building block. The sun the water glitters under, the ambient its foam sits in and the sky it reflects all come from the scene, through the same directional light and reflection probes every other material in the engine reads — so an ocean drops into a world that already has lighting instead of bringing a second set, and changing the scene's light changes the water with it. ## What it does The simulation is **spectral**: instead of animating individual waves, it builds the sea's energy across every wavelength and direction from an oceanographic spectrum, then inverse-transforms that into a height field. A real sea is what comes out — wind speed alone decides wave height, and it rises with wind the way a fully developed sea does, following the Pierson–Moskowitz law. It runs in **cascades** — independent wave fields over different patch sizes, each band-limited to its own range of wavelengths. A tile of roughly two kilometres carries the long waves and one of a few hundred metres the short ones; they sum into a surface with detail at every scale, and because the bands are disjoint no wavelength is synthesised twice. Above them a **Gerstner bank** adds the swell analytically, which no cascade can hold: a wave longer than its own patch has nowhere to live, and swell arrives from a distant storm on its own heading anyway. **Foam** is not painted on. It comes from the Jacobian of the wave displacement: where the surface folds over itself, the wave is breaking. Whitecaps appear where and when the geometry actually breaks, accumulate into a persistent world-space field, and streak behind the crest as they decay. **Colour and transparency are the same mechanism.** Per-channel Beer–Lambert extinction over the distance light travelled through water gives both — red dies within a metre, blue survives tens. That is why shallow water over sand reads turquoise and the open sea reads near-black blue without switching materials. **The seabed** is one noise field read twice: scaled it is the terrain's relief, unscaled it picks between sand and rock, so outcrops stand on the high ground and sand fills the hollows. The sun's caustic web moves over both, bent by the horizontal displacement of the water directly above it. Bodies land on it: the pass that raises the ground writes the same field out as a height patch that travels with the viewer, so what a body stands on is what the frame drew. Nothing here ships an image. Every map the surface and the seabed sample — the foam patterns, the sand, the rock, the caustics — is grown on the GPU at startup. ## The pieces | Asset | What it is | |---|---| | `components/Ocean` | Puts a sea on an entity. Its fields are the sea state; the simulation is rebuilt from them, so an ocean survives a scene reload. | | `modules/ocean` | The substrate: `attach`, `create`, `forEntity`, `sampleHeight`, `sampleDisplacement`. | | `components/Buoyancy` | Floats a rigid body on the sea. Its fields are the hull: how dense it is, how steady, and what box displaces water. | | `modules/buoyancy` | The forces: `attach`, `forEntity`, `columns`, `submergedFraction`, `restingY`. | | `modules/oceanPresets` | Nine named sea states, from `calmClear` through `gale`. | | `modules/oceanQuality` | The four tiers — how finely a sea is resolved, as distinct from what sea it is. | | `modules/oceanClipmap` | The camera-following ring geometry both the surface and the seabed are built on. | | `modules/oceanFloor` | The seabed: terrain, ground materials, caustics and the ground physics collides with. | | `modules/oceanScene` | The seam between the scene capture and the materials that sample it. | | `shaders/oceanSurface` | The water: cascade normals, absorption, exact dielectric Fresnel, subsurface scattering, sun glitter, foam, and the view from below. | | `shaders/oceanFloor` | The seabed: sand and rock blended by terrain noise, under a caustic web. | | `materials/oceanWater`, `materials/oceanSeabed` | The default looks. | | `renderFeatures/oceanScene` | Copies the frame's colour and depth so the surface can refract what is behind it and measure its own water column. | | `computeShaders/ocean_*` | The ten-stage GPU pipeline. | ## Wind is the control Significant wave height goes as the **square** of wind speed, so that one field does most of the work: | Wind | Sea | Wave height | |---|---|---| | 3 m/s | glassy, a mirror | ~0.2 m | | 11 m/s | working sea, occasional whitecap | ~2.5 m | | 24 m/s | gale, breaking and streaked | ~12 m | Everything else shapes character rather than size: `choppiness` pinches crests into peaks, `standingWaveRatio` runs waves both ways instead of only downwind, `jonswapGamma` sharpens a fetch-limited sea, and the foam layers decide how readily a breaking crest whitens and how long it streaks. Read `documentation/oceans.guide` for the full walkthrough, including the seabed, buoyancy, reflections and what each tier costs.
▲ 0↑ born
·
assettype · born here
❒asset
# effect A set of visuals packaged as one thing: every impact a weapon makes, every stage of an explosion, the muzzle flash, impact and tracer of a gun. Each visual is an entry the effect plays by name. The graphs, the parts they are built from, and the shaders, materials, compute shaders and textures they use all live inside the effect. ## The shape on disk <name>.effect/ effect.yaml what it plays: entry name -> one graph, or a list of graphs played together graphs/ the graphs its entries play parts/ graphs those graphs use as nodes, each one whole piece of the look shaders/ shaders and shader modules its materials draw with materials/ compute/ compute shaders its graphs run textures/ README.md what this effect shows, what each entry plays, what each input changes .metadata settings.yaml its values for the settings below preview.png its default entry at a representative moment `effect.yaml`, `graphs/`, `README.md` and `.metadata` are required. ```yaml plays: flare: "~.graphs.flare" impact: - "~.graphs.impactFlash" - "~.graphs.impactDebris" default: flare ``` Inside an effect `~.x` names the effect's own `x`: `~.graphs.flare`, `~.parts.crack`, `~.shaders.lava`, wherever the effect sits. ## Graphs and parts A graph under `graphs/` composes what one entry shows. A piece worth building and looking at on its own (the crack of a fissure, the spray, the smoke) is a graph under `parts/`, called as a node: ```lua local crack = g["~/parts/crack"]({ length = length }) ``` A part is a whole graph, so it previews and plays alone: the crack without the smoke in front of it. An operation several effects use (a streak look, a smoke puff) is a `.procNode` outside the effect, used the way a module is required. ## Playing one ```lua local beacons = asset.resolve("beacons", "effect") local shown = beacons:play("flare", { root = mast, offset = { 0, 0.9, 0 }, inputs = { size = 0.4 } }) shown:set("size", 0.6) shown:onFinished(function() print("done") end) ``` `root` is the entity it plays on and follows; `inputs` go to every graph of the entry that declares them; the handle answers `set`, `pause`, `resume`, `seek`, `getTime`, `stop`, `isFinished` and `onFinished`. A play finishes when every graph of its entry reaches the end of its timeline; a graph that states `realTime` alone has no end, so a play of it runs until `stop()`, and a simulation that runs once states its frames (`g:timeline({ realTime = true, frames = 120 })`). Plays are kept ready, built ahead of time: `beacons:warm("flare", 6)` keeps six ready for a burst. An `Effect` component, or a drop into the viewport, places an entry in a scene. `entries()` and `entry(name)` read what it plays; `setEntry`, `removeEntry`, `renameEntry`, `setDefault` and `newGraph` change it, keeping every line of `effect.yaml` they do not touch. ## Settings `pool`: how many plays of each entry are kept ready (default 2). ## In search Each asset inside an effect is found on its own and names the effect it belongs to. The effect itself is found by its preview, its README and its entries.
▲ 0↑ born
◇
component · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# PlayerNameLabel Drives this entity's `Text3D` with the owning player's name, read from the nearest ancestor `PlayerAvatar`'s synced `displayName` so every peer renders the same name without a local registry lookup. The label re-rasterises when the display name changes, and in play it hides itself on the local player's own avatar — name labels exist so other players are identifiable. An unowned avatar reads a placeholder. Location: `src/lua/lib/components/PlayerNameLabel.component`
▲ 0↑ born
◇
component · born here
❒asset
# Joint Connects two bodies with a joint constraint. Each end is a rigid body (an entity with `Physics`) or a soft body (an entity with `SoftBody`). Joint kinds: `"fixed"`, `"hinge"`, `"ball"`, `"prismatic"`, `"spring"`, `"rope"`. Public fields: `kind`, `connected`, `localAnchorX/Y/Z`, `remoteAnchorX/Y/Z`, `axisX/Y/Z`, `stiffness`, `damping`, `restLength`, `maxDistance`, `breakForce`, `breakTorque`, `region`, `remoteRegion`. Methods: `:setMotor(targetVelocity, maxForce)`. ```luau entity(id).component.add("Joint", { connected = frameEntityId, kind = "hinge", axis = {0, 1, 0}, }) ``` A `"rope"` joint is a hard maximum-distance limit solved inside the physics step: the bodies move freely while the anchors are closer than `maxDistance` (slack rope) and are stopped from separating beyond it (taut rope). It requires `maxDistance > 0`, and `maxDistance` is reactive — writing it on a live joint retunes the limit in place, so a winch can reel a rope in or out without recreating the joint. ```luau entity(ballId).component.add("Joint", { connected = hookEntityId, kind = "rope", maxDistance = 8, }) ``` A soft body end is anchored on the body as a whole (its frame, the rigid motion that best fits its particles), or on a region of its particles: `region` for this entity, `remoteRegion` for the connected one, each `{ box = { min, max } }` (every particle between the corners) or `{ nearest = point }` (the one particle nearest the point), in the entity's unit box (-0.5 to 0.5 on each axis) unless `space = "world"`. A region end's anchor is measured from the centre of its particles. Motors, `breakForce` / `breakTorque` and `Physics.jointReaction` work the same at either kind of end, and the joint is removed with either body. ```luau -- a soft punching bag hung by its top particle from a fixed hook bag.component.add("Joint", { connected = hook.id, kind = "ball", region = { nearest = { 0, 0.5, 0 } } }) -- two jellies held face to face, breaking under 200 N a.component.add("Joint", { connected = b.id, kind = "fixed", breakForce = 200, region = { box = { min = { 0.4, -0.5, -0.5 }, max = { 0.5, 0.5, 0.5 } } }, remoteRegion = { box = { min = { -0.5, -0.5, -0.5 }, max = { -0.4, 0.5, 0.5 } } }, }) ```
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# ProceduralSky The editable atmospheric sky: a day/night gradient with a sun disc, glow, stars and a moon, all derived procedurally from the directional light's elevation. Add it to an entity like a light: ```lua entity.spawn("sky").component.add("ProceduralSky", { timeOfDay = 18.5 }) entity.spawn("sky").component.add("ProceduralSky", { preset = "sunset" }) entity.spawn("sky").component.add("ProceduralSky", { zenithColor = { r = 0.05, g = 0.1, b = 0.3 }, horizonColor = { r = 0.9, g = 0.4, b = 0.2 }, }) ``` A `ProceduralSky` owns its material outright: it registers a **runtime GPU material** of its own over the builtin procedural-sky shader and pushes every visual parameter (colours, sun, stars, moon, turbidity, exposure) straight to that record. The component's fields **are** the material definition — no `.material` asset backs it, so retuning a scene's sky changes that scene's sky and nothing else. The renderer reads the values from the material's group(1) uniform like any other material — there is no sky-specific render path. Those values also travel the native `Sky` bridge into the scene sky config, alongside the time-of-day / auto-cycle / sun-sync controls. That config is what `sky.get()` reports and what a saved scene records, so both name the sky being drawn. With `syncSunToLight` on (the default), the sky's `timeOfDay` is what the scene is lit by: the scene's directional light takes its direction, colour and intensity from the sun's position, so the sun in the sky and the sun the scene is lit by are the same sun through a whole day/night cycle. Turn it off for a light the sky leaves alone. The intensity that arrives is the day/night curve between night and noon, and `sunPeakIntensity` is the noon end of it — `1.0`, the sky's own daylight, unless a scene asks for more. A harder sun on water or snow is stated here; setting the light itself does not survive, because the sky writes over it every time the hour moves. Like the directional light, the sky is conceptually **singular per scene**. A `ProceduralSky` fully defines its material on `awake` (every parameter from its own fields), so switching scenes never inherits a previous scene's overrides. Day/night comes from the sun's elevation, so evening presets render dark — drive the look by `timeOfDay` (which orients the sun). ## Fields | Field | Type | Default | Meaning | |---|---|---|---| | `preset` | string | `""` | Named look applied before explicit fields: `clear_day`, `sunset`, `sunrise`, `overcast`, `night`. | | `timeOfDay` | number | `14.0` | 0..24 hours; orients the sun and the day/night gradient. | | `autoCycle` | bool | `false` | Advance `timeOfDay` each frame. | | `cycleSpeed` | number | `60.0` | Game-seconds per real-second for the auto cycle. | | `syncSunToLight` | bool | `true` | Drive the directional light from the sun. | | `sunPeakIntensity` | number | `1.0` | What the sun's light measures at noon; the day/night curve runs from night up to this. | | `zenithColor` / `horizonColor` / `groundColor` | color | — | Sky gradient colours. | | `sunSize` / `sunIntensity` | number | `0.02` / `20.0` | Sun disc size + brightness. | | `starsIntensity` | number | `1.0` | Night star-field intensity. | | `moonSize` | number | `0.03` | Moon disc size. | | `turbidity` | number | `4.0` | Atmospheric haziness. | | `exposure` | number | `1.0` | Sky exposure multiplier. | ## Methods | Method | Description | |---|---| | `sky:setTimeOfDay(hours)` | Set the time of day (0..24). | | `sky:applyPreset(name)` | Apply a named look. | | `sky:setZenithColor(c)` / `setHorizonColor(c)` / `setGroundColor(c)` | Set a gradient colour (`{r,g,b}` array or map; >1 auto-scales /255). |
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# UiPanel A UI panel component that renders a widget tree in one of three modes: - `"2d"` — egui viewport overlay (HUDs, menus, debug panels). - `"projected"` — screen-space UI at the entity's world position via `worldToScreen`. Always on top, no occlusion or lighting. - `"3d"` — world-space billboard using egui render-to-texture. Respects depth/occlusion. Public fields: `mode`, `layer`, `visible`, `source` (inline or `"lib::<path>"` / `"@..."` library module), `offsetX/Y/Z`, `maxDistance`, `fontSize`, `textColor`, `background`, `billboard3dScale`, `theme`, `rasterizeWidth`, `rasterizeHeight`. Methods: `setTree(tree)`, `show()`, `hide()`, `toggle()`, `setMode(newMode)`, `getScreenName()`. A library module named by `source` is mounted with `mount(ctx)`. When `mount` registers its own screen, the panel registers that screen again from its own code, so the screen is the panel's: its callbacks reach the panel's `onCallback`, which hands them to the `onCallback` the mount returned, and the screen is unregistered when the panel ends. ```luau entity(id).component.add("UiPanel", { mode = "2d", layer = 10, tree = { type = "label", props = { text = "HP: 100" } }, }) ```
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# Population Draws a `.population` asset — one mesh at every transform the asset carries, as ONE draw call per variant at any instance count. ```luau entity.spawn("forest").component.add("Population", { source = "forest" }) ``` The component owns what the draw costs: one GPU transform buffer and one instanced-draw registration per variant. It builds them on awake, rebuilds them when `source` changes or the asset's contents change on disk, and releases them on destroy — so a scene reload replaces the draws instead of stacking a second set, and despawning the entity takes them with it. ## Fields | Field | Meaning | |---|---| | `source` | The `.population` asset to draw. | | `castsShadows` | `false` renders the instances while blocking no light. | ## Reading what it draws Both answers come back from the renderer, across every live registration: ```luau local pop = entity.find("forest").component.get("Population") pop:instanceCount() -- instances the engine reports drawing pop:drawCalls() -- registrations — one per variant, at any instance count ``` ## Where it comes from `populationRef:instantiate(target?, opts?)` spawns exactly this: one entity carrying this component. That is also what `Asset { source = <a population> }` drives, which is how a baked procedural population reaches a scene — a root record plus one child holding the reference, at any instance count. The matrices a population carries are taken in this entity's space, so its instances stand where the recipe placed them relative to the entity and move, turn and scale with it.
▲ 0↑ born
◇
component · born here
❒asset
# RenderFeatures The scene's render features, on one entity: a list of `.renderFeature` assets, each with the settings it runs under. ```lua entity(id).component.add("RenderFeatures", { features = { { feature = "@builtin::systems.reflections.ssr", settings = { intensity = 0.8, quality = "high" } }, { feature = "@builtin::renderFeatures.outline" }, }, }) ``` While the component is enabled every listed feature is live. A feature something already runs — a script's `renderer.feature.create`, another entity's list — is adopted and configured rather than stacked a second time. Each feature runs its passes at the phase and order its own `render(ctx)` declares. Disabling or destroying the component tears every one of them down. Editing the list re-syncs it: an entry removed goes, one added comes up, and a changed `settings` table reaches the live feature on its next frame. `settings` are the knobs the feature's asset declares — `renderer.feature.schema("@builtin::systems.reflections.ssr")` lists them with their defaults. A knob the feature does not declare, or a value of the wrong shape, is refused by name in the log. Methods: `:getHandles()` — the live handles this component holds, keyed by the feature asset's guid. For code that tunes a feature at runtime — a quality slider, a cutscene — `renderer.feature.find(ref)` and `renderer.feature.configure(handle, settings)` reach the same knobs without an entity.
▲ 0↑ born
◇
component · born here
❒asset
# Camera Manages viewport priority, render-to-texture, and capture. State is stored in the native `Camera` ECS component; the Rust camera system handles render scheduling and render targets. Public fields: `fov`, `near`, `far`, `priority`, `textureHandle` (the guid of the texture the camera renders into; empty = main viewport), `renderLayers` (which render layers this camera draws — a space-separated spec of names, e.g. `"all"`, `"all !ui"`, or `"default sky"`; `ui`/`sky`/`debug`/`EditorUI` are built-in layers), `postProcessing` (whether this camera runs the post-process chain), `debugChannel`, plus the behavior slot below. Methods: `:lookAt(target)` (entity id string OR `{x, y, z}` table), `:render()`, `:capture()`, `:setTargetTexture(tex?)` (a `renderer.texture.create` handle to render into, or nil for the viewport). ```luau entity(id).component.add("Camera", { fov = 90, priority = 10 }) entity(id).component.get("Camera"):capture() ``` The scene's play-mode camera is reached as `layers.active.camera`, a handle that reads and writes the fields above on whichever entity currently carries them. `layers.active.camera.entity` is the entity ref for that camera and `layers.active.camera.entityId` its id string, so a script that needs to attach something to the camera — an `AudioListener`, a child entity — goes through the ref: ```luau local cam = layers.active.camera cam.fov = 70 -- the camera's settings cam.entity.component.add("AudioListener") -- the entity carrying them ``` ## How the camera moves: `behavior` and `follow` A `Camera` does not move itself. `behavior` names a component that does, and setting it attaches that component to this entity. Clearing it detaches whatever was attached. ```luau local cam = entity(id).component.get("Camera") cam.behavior = asset.ref("@builtin::controller.orbital_follow", "component") cam.follow = playerBody ``` The attached behavior is an ordinary component on the camera's entity, so it is tuned the way any component is: read it with `component.get` and set its fields there. A `component.add` of the same type configures the instance the slot attached instead of adding a second one: the values it passes are written onto that instance and its proxy is returned. That add is the form a scene `build.luau` tunes the behavior with, because its values are saved with the scene and the loaded Camera's behavior takes them back. A key the component reads only as it is created (one of its `initKeys`, a private field) refuses the add and names the key. ```luau cam.behavior = asset.ref("@builtin::controller.third_person_follow", "component") local rig = entity(id).component.get("third_person_follow") rig.cameraDistance = 6 -- the same, as an add of the type the slot attached entity(id).component.add("third_person_follow", { cameraDistance = 6 }) ``` `follow` is the standard slot every shipped behavior reads. Set the follow target on the **Camera**, not on the behavior, so swapping behaviors keeps it. A behavior that finds its own `follow` field empty falls back to this one, which is what lets a rig keep tracking the player across a behavior swap. `followResolves` answers whether that slot names an entity that is live — `true` while it names a live one or names nothing at all, `false` once the target is despawned or the id names no entity. A rig whose target does not resolve holds its last pose, and this is the field that tells it from a rig posed correctly on a subject that has not moved. `camera.get` carries the same value beside `follow`, and a write naming an id with no entity behind it draws a warning where it lands. ```luau cam.followResolves -- false once the followed entity is gone tools.use("camera", "get", id).followResolves -- the same answer off the tool ``` The shipped behaviors live under `@builtin::controller.*`: `orbital_follow`, `third_person_follow`, `first_person`, `free`, `orbit`, `chase`, `isometric`, `rts`, `birds_eye`, `side_scroller`, `cinematic`, `menu`. ## Writing your own Any component can be a camera behavior. Write one that moves its own entity and attach it the same way: ```luau cam.behavior = asset.ref("MyChaseCam", "component") ``` `follow` lives on the Camera, so a behavior gets no property notification of its own when the target changes. Declare `onFollowChanged(newFollow, oldFollow)` to be told the moment it does — the Camera calls it on the component it attached, which is what lets a rig re-pose on the new subject at once. A behavior that reads `Camera.follow` on its own schedule declares nothing and is attached the same way. ```luau typed function public:onFollowChanged(newFollow: any, oldFollow: any) -- pose this entity against the new target end ``` To make it appear in the discovery catalog alongside the shipped ones, declare the `cameraBehavior` tag in the component's `.metadata`: ```json { "tags": ["cameraBehavior"] } ``` The tag governs **discovery**, not attachment. A tagged component is listed by `layers.active.camera.behaviors` and is what tooling offers when something asks "which camera behaviors exist"; an untagged component attaches just as well and simply stays out of that list. Tag the ones you want other people (and agents) to find. ```luau for name, ref in pairs(layers.active.camera.behaviors) do print(name, ref.identity) end ``` ## Texture colour space `textureColorSpace = "display"` applies the display transform when rendering into a texture. `"linear"` writes scene-linear values instead. `postProcessing` independently controls the effects chain in either mode. Floating-point targets retain values above one; normalized targets clamp to their representable range. The main viewport uses display encoding.
▲ 0↑ born
▣
module · born here
❒asset
# scene.toolbox shared module Cross-tool sharing for the `scene.*` toolbox: the hierarchy snapshot core (`snapshot`, `findAll`, `leaf`) that backs tree / findAll / findByComponent / summary, and the last-scene-name state that save / load share. Tools call these functions; the functions call engine APIs.
▲ 0↑ born
◇
component · born here
❒asset
# CapsuleCollider (component) A capsule collision shape: a cylinder along the entity's local Y axis capped by hemispheres. This is the shape characters use, because a rounded bottom slides over small steps instead of catching on them. ## Sizing `radius` and `height` are in the entity's local space, multiplied by the entity's world scale. `height` is the full height of the cylindrical section, excluding the caps. Leave both unset and the capsule derives from the entity's model bounds. ```lua entity.find("player").component.add("CapsuleCollider", { radius = 0.3, height = 1.6 }) entity.find("pillar").component.add("CapsuleCollider", {}) ``` ## Fields | Field | Meaning | |-------|---------| | `radius` | Capsule radius. Unset derives from model bounds. | | `height` | Cylindrical section height. Unset derives from model bounds. | | `isTrigger` | Report overlaps without blocking. | | `friction` | Surface friction. | | `restitution` | Bounciness. |
▲ 0↑ born
◇
component · born here
❒asset
# Tessellation Compute-shader mesh subdivision with material-driven displacement. `Tessellation` sits next to `Model` on a renderable. The engine's `tessellation` system reads the entity's current mesh geometry, subdivides each triangle into `4^level` micro-triangles, displaces the result along the interpolated normal from the entity material's displacement map, and swaps the entity's `MeshInstance.mesh` for the displaced derivative. The material is bound per-entity, so the tessellated surface renders with the same PBR parameters and normal maps as the source. ```luau entity.find("floor").component.add("Tessellation", { level = 4, displacementScale = 0.05, }) ``` ## Fields | Field | Type | Purpose | | --- | --- | --- | | `level` | number | Subdivision level (0..=6). Clamped when out of range, and `getStatus()` answers `"clamped"`. `4^level` micro-tris per source tri. Under `mode = "adaptive"` this is the ceiling. | | `displacementScale` | number | World-units of displacement per unit of sampled height. Sign matters — negative inverts inward. With no displacement map on the material, the surface is subdivided but left flat. | | `mode` | string | `"static"` or `"adaptive"`; any other value makes `getStatus()` answer `"rejected_mode"` and leaves the entity rendering the mesh it already had. | | `screenError` | number | Target micro-triangle edge in pixels, read under `mode = "adaptive"`. Smaller buys more subdivision at a given distance; zero or less asks for level 0. Default 8. | | `stitch` | boolean | Place this surface's boundary on the coarsest lattice any patch sharing that boundary uses. Default `true`. | ## Methods The `tessellation` system writes what each bake did onto the entity's native `ecs.Tessellation` component, and these methods read it back. `ecs.get(id, ecs.Tessellation)` reads the same values as its `status`, `activeLevel`, `outputTriangles`, `outputVertices` and `stitchedEdges` fields. | Method | Purpose | | --- | --- | | `tess:getStatus()` | Read the latest status string: `""` before the first dispatch, then `"pending"`, `"ok"`, `"clamped"`, `"too_large"` or `"rejected_mode"`. | | `tess:getActiveLevel()` | Read the level the installed mesh was baked at. | | `tess:getTriangleCount()` | Read the installed mesh's triangle count: source triangles × `4^activeLevel`. | | `tess:getVertexCount()` | Read the installed mesh's vertex count: source triangles × the lattice a patch carries at `activeLevel`. | | `tess:getStitchedEdges()` | Read how many boundary edges the last bake placed on a coarser neighbour's lattice. | ## Adaptive level ```luau entity.find("cliff").component.add("Tessellation", { mode = "adaptive", level = 5, -- ceiling, reached up close screenError = 8, -- micro-triangles ~8 px on an edge }) ``` Under `mode = "adaptive"` the level is re-derived each frame from how much screen the surface covers: the entity's bounding sphere is projected under the viewport camera's own projection (perspective or orthographic), and the level is the one whose `4^level` micro-triangles land near `screenError` pixels on an edge — clamped to `level`. A surface therefore gains detail as the camera approaches and sheds it on retreat. A sphere the view frustum does not reach covers no pixels, so a surface behind or outside the frame drops to level 0 until it comes back into view. Sample `getTriangleCount()` across a dolly to watch it move. Two rules bound the re-baking: a half-level dead band around the active level, so a camera parked on a boundary holds one level, and a minimum spacing between re-bakes, so a fast sweep through several levels re-bakes at a bounded rate rather than every frame. A whole entity holds one level, so the micro-triangles along every shared edge inside it match by construction. Where two entities meet, see *Shared boundaries* below. ## Shared boundaries A surface's outline — the edges only one of its triangles uses — is the only place another surface can meet it. Two surfaces whose outlines share an edge and subdivide it to different levels place their points along it on different curves: the finer one follows the displacement between the coarser one's samples, and the space between the two curves is a crack the background shows through. Every frame, each tessellated surface's outline is placed in world space and matched against the others'. Where a shared edge is found, the finer surface bakes its points along that edge onto the coarser one's lattice, so both trace one boundary. The rest of the finer surface keeps its own level. `getStitchedEdges()` reports how many of its boundary edges were placed this way, and a neighbour that changes level, moves away, or appears re-bakes the surfaces that share an edge with it. ```luau -- Two tiles meeting at x = 0, subdivided differently. entity.find("tile_west").component.add("Tessellation", { level = 5, displacementScale = 1.5 }) entity.find("tile_east").component.add("Tessellation", { level = 2, displacementScale = 1.5 }) -- tile_west:getStitchedEdges() == 1 — its east edge is on tile_east's lattice. ``` Two outlines count as shared when both endpoints land on the same point of a 0.1 mm world-space grid, which is what lets tiles authored to meet match however each one is placed — the rounding each entity's own transform leaves behind is orders of magnitude finer than one cell. `stitch = false` leaves a surface subdivided uniformly to its own level everywhere; its level is still what neighbours that do stitch clamp themselves to. ## Output size Each source triangle's micro-vertices are the grid points of one shared barycentric lattice: `(n + 1)(n + 2) / 2` of them for `n = 2^activeLevel`, carrying `n^2` micro-triangles. Micro-triangles meeting along an edge inside a patch therefore name the same vertex, and the index buffer names lattice addresses — three per micro-triangle. At level 6 a patch is 2145 vertices for 4096 triangles. Read the two counts with `getVertexCount()` and `getTriangleCount()`. ## Displacement Displacement height comes from the entity material's **displacement map** (a grayscale height texture). A material without one still tessellates — the geometry is subdivided for a smoother silhouette — but stays flat. `displacementScale` scales the sampled height into world units along each micro-vertex's interpolated normal. ## Limits - Maximum subdivision level: 6 (= 4096 micro-triangles per source triangle). Higher values are clamped, and `getStatus()` answers `"clamped"`. - Projected output cap: 64 MiB, counting the vertex buffer and the index buffer together. A dispatch that exceeds this is skipped, and `getStatus()` answers `"too_large"`: lower the level or use a lower-poly source mesh.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# SpotLight Adds a cone-shaped light source to an entity. Position comes from the entity's world transform; the cone axis is the entity's world rotation applied to the `face` direction, so a spot under a carrier turns with it. `face` defaults to `"Front"`, the entity-local -Z that `transform.forward` reports and that `entity:lookAt` aims, so aiming the carrier at something is what puts the cone on it. Shares the 64-light scene cap with `Light` (point lights), and shares its intensity scale: the same `intensity` at the same range puts the same light on a surface either kind faces from the same place, and the cone decides how much of the room that light reaches rather than how bright it is where it lands. `angle` and `innerAngle` are half-angles from the cone axis, so `angle = 30` opens a 60-degree cone. Public fields: `colorR/G/B`, `brightness` (→ intensity), `range` (→ radius), `angle` (outer half-angle, deg), `innerAngle` (inner half-angle, deg), `face` (`Front`/`Back`/`Left`/`Right`/`Top`/`Bottom`), `shadows`, `cookieLayer`, `mobility`. `intensity` and `radius` are aliases of `brightness` and `range`, spelled the way the `Light` component spells them. Methods: `:setColor(color)`, `:setBrightness(b)`, `:setRange(r)`, `:setAngle(deg)`, `:setInnerAngle(deg)`, `:setFace(face)`. Falloff: 0..innerAngle = full bright, innerAngle..angle = smoothstep to 0, > angle = 0. Range falloff is UE4-style smooth windowed inverse-square clamped at `range`. The cone is one further factor on that shared scale, and it is 1 inside `innerAngle`. So the reading that separates the two kinds is a room-wide or frame-wide average: a spot lifts it far less than a point light carrying the same number, because it reaches less of the room. Balance a mixed rig by the brightness of the surfaces each light is aimed at, not by how much of the frame each one lifts — and widen `angle` or `range` to cover more of the room where raising `brightness` would only deepen the spot's own pool. A spot casts shadows by default; `shadows = false` keeps it purely additive. `cookieLayer` names a layer of the shared feature texture to project through the cone as a gobo, and `-1` (the default) leaves the cone uniform — fill the layer with `renderer.featureTexture.setLayer` before pointing a spot at it. ```luau entity(id).component.add("SpotLight", { color = {1, 0.95, 0.8}, brightness = 3.0, range = 30, angle = 30, innerAngle = 20, face = "Front", shadows = true, cookieLayer = 0, }) ```
▲ 0↑ born
◇
component · born here
❒asset
# Player Identity tag for a player entity. Attaches the Rust `PlayerOwned` marker via `__native` in `awake` and removes it in `onDestroy`. No public state, no update loop, no visuals. ```luau entity(id).component.add("UserIdentity") entity(id).component.has("UserIdentity") -- true ``` Future rules (not yet enforced): - **Non-serialising.** Scene and world saves omit this component. Players are attached at runtime per peer, not inherited from the save file. - **Non-removable.** Once an entity is a player, removing this component is a no-op. A player stays a player for its lifetime outside an explicit identity swap. Visuals, cameras, input, and movement live in separate components (e.g. `PlayerController`, `PlayerTemplate` bundle content) so adding or removing them never affects identity.
▲ 0↑ born
◇
component · born here
❒asset
# Asset Promote the bundle's spawned children from temporary to permanent scene state and remove the Asset component, "baking" the bundle's contents directly into the owning scene. Subsequent saves persist the baked entities verbatim and stop replaying the bundle template. Location: `src/lua/lib/components/Asset.component`
▲ 0↑ born
◇
component · born here
❒asset
# AudioListener The "ears" of the scene. Spatial audio is heard relative to the entity carrying this component — its transform sets the listener position and orientation. Attach it to whatever should hear: a camera, a character, a dedicated listener rig. Public fields: `active` (`boolean`, default `true`). Methods: `setActive(on)`. ```luau entity.find("camera").component.add("AudioListener") entity.find("player").component.add("AudioListener", { active = true }) ``` A scene needs an active listener for its `Audio` sources to be heard positionally. Sources with no active listener are reported at load and play centered until a listener is added. `active` is live: writing it takes effect on the next frame, so clearing it on the listener holding the ears hands them to another active listener, and clearing it everywhere leaves the scene with no ears. When several are active one of them drives the ears — the same one for as long as those entities live, whatever else changes about them — and the engine warns once, naming it. Recreating the entities, as a scene reload or entering play does, chooses again. `audio.observe().listener` reports the winner as `entity`, alongside `present`, `count` (how many are active) and the pose. Which one it picks is not something to author against: deactivate or remove the extras when it matters.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
# ambientOcclusion system Screen-space ambient occlusion — the contact darkening in corners, creases and where objects meet. Without it, every concave region is lit exactly like a flat one: a box resting on a floor has no seam, a corner is as bright as the wall, and an untextured scene reads as plastic. Occlusion is the cheapest shading cue there is per millisecond, because it needs no extra geometry pass — only the depth and normal the frame already produced. Three techniques are shipped. All three answer the same question — what fraction of the hemisphere above this pixel is visible — and all three cost the same taps to answer it. ## Using it The scene's occlusion is a component — author it once and it persists with the scene: ```lua local id = entity.spawn("ambient_occlusion").id entity(id).component.add("AmbientOcclusion", { technique = "gtao", -- ssao | hbao | gtao intensity = 0.9, -- how far the darkening goes, 0..1 radius = 1.5, -- world-space reach quality = "high", -- low | medium | high | ultra }) ``` Every field is live: edit one in the inspector and the shading follows. Setting `intensity` to 0 turns it off without losing the rest. For code that adjusts occlusion at runtime — a quality slider, a cutscene — reach for the module directly (same options, no component): ```lua local ao = require("@builtin::systems.ambientOcclusion.ao") ao.set({ technique = "hbao", intensity = 0.9 }) -- any subset; the rest is kept ao.get() -- the settings in force ao.techniques() -- the three, each with what it does ao.qualityLevels() -- the sampling shape each level costs ao.active() -- is the pass running ao.clear() -- off, settings kept ``` ## The three techniques | technique | what it measures | |---|---| | `ssao` | Taps scattered over a disk, each weighed on its own: how far the neighbour rises above the tangent plane, over how far away it is. | | `hbao` | One horizon per direction — a ray marched outward until it finds the steepest thing along it — averaged as sines. | | `gtao` | The cosine-weighted arc between the two horizons of a slice, in closed form. This is the visibility the other two estimate. | They are scaled to agree. A point in a right-angle corner sees exactly half the hemisphere, and each technique is normalised so it reads that corner as half: | technique | flat plane | right-angle corner | occlusion | |---|---|---|---| | `ssao` | 1.000 | 0.566 | 0.434 | | `hbao` | 1.000 | 0.592 | 0.408 | | `gtao` | 1.000 | 0.594 | 0.406 | That is what lets `intensity` mean the same thing whichever technique is selected, so switching one for another changes how the estimate is made rather than how dark the scene gets. ## What the knobs do | field | effect | |---|---| | `technique` | Which estimator runs: `ssao` \| `hbao` \| `gtao`. | | `intensity` | How far the darkening goes, in [0, 1]. 0 turns it off and releases the pass. | | `radius` | World-space reach — roughly "how big is a crease worth darkening". Constant in world space, so it does not swell as the camera approaches. | | `power` | Contrast curve on the visibility term. Above 1 deepens the dark end. | | `thickness` | World-space thickness assumed for an occluder, so a thin foreground object stops occluding everything behind it out to the full radius. | | `bias` | How far a neighbour must rise above the surface before it counts as an occluder rather than as the surface itself, in [0, 1]. Raise it if a flat surface shades itself in patches; lower it if fine creases disappear. | | `quality` | Sampling shape. See the table below. | | quality | slices × steps | taps per pixel | resolution | |---|---|---|---| | `low` | 2 × 3 | 12 | half | | `medium` | 3 × 4 | 24 | half | | `high` | 4 × 6 | 48 | full | | `ultra` | 6 × 8 | 96 | full | Every technique spends the same budget: `slices × steps × 2` taps. `gtao` walks each slice both ways, `hbao` treats each of the two directions as its own ray, and `ssao` takes that many independent taps of the disk. The lower two levels compute occlusion on a half-resolution grid — a quarter of the pixels, each still costing its level's taps. Occlusion is low-frequency almost everywhere, and the filter that removes the sampling noise lifts the coarse grid back to the frame in the same pass, weighting each texel by the depth where it sits so silhouettes survive the upsample. What a half-resolution grid loses is the innermost pixel of a sharp crease; everything wider than a couple of pixels comes back the same. ## How it renders One compute pass after lighting writes the per-pixel visibility, a second filters it and multiplies it into the frame, and a blit puts the result on screen. The filter is depth-aware: a plain blur would bleed occlusion across silhouettes, so each tap is weighted by how close its depth is to the centre pixel's. Every technique jitters its sampling by a per-pixel offset, which trades banding for noise; the filter is the half of that trade that removes the noise. Depth, normal and camera come from the render target being drawn (`@scene.depth`, `@scene.normal`, `@frame.camera`), so the result is correct in offscreen captures and render-to-texture cameras at their own resolutions, not only the live viewport. Sky pixels are passed through — there is no surface to occlude. The normal buffer is the deferred G-buffer, so this needs the deferred path (the default). ## Seeing the occlusion on its own While occlusion is running, `ambientOcclusion` is a capture view: the filtered visibility with no scene colour, white where the hemisphere is open and dark where it is closed. ``` capture { pass = "ambientOcclusion" } ``` It is the fastest way to tell a shading problem from an occlusion one, and to compare two techniques or two `radius` values without the lighting in the way. ## Files ``` ambientOcclusion.package/ AmbientOcclusion.component/ -- the scene's occlusion, authored and persisted ao.module/ -- ao.set / .get / .techniques / .qualityLevels / .active / .clear aoShared.module/ -- the prelude, filter and passes the techniques share ssao.renderFeature/ -- taps weighed on their own hbao.renderFeature/ -- one horizon per direction gtao.renderFeature/ -- the arc between two horizons ```
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# CollisionGroup Sets collision group membership and filter masks on an entity's physics colliders using a bitmask. Two colliders collide only when `(a.membership & b.filter) ~= 0` AND `(b.membership & a.filter) ~= 0`. Public fields: `membership` (default `0xFFFFFFFF`), `filter` (default `0xFFFFFFFF`). Methods: `setGroups(membership, filter)`, `ignoreEntity(entityId, ignore?)`. ```luau entity(id).component.add("CollisionGroup", { membership = 1, filter = 3 }) ```
▲ 0↑ born
◇
component · born here
❒asset
# third_person_follow Over-the-shoulder camera. The rig hangs off a shoulder pivot beside the follow target and sits back along the heading it looks down, so framing is authored as offsets in space. The offset is measured in the camera's own frame, so the subject holds the same spot on screen whichever way it turns. The rig owns that heading. Binding a follow target aims it along the subject's facing, and look input turns it from there — the subject's facing is never read again. That is what makes the camera steerable for a character who turns to face where it is walking: hold "forward" and the character walks the line the camera looks down, instead of chasing a heading that moves as it turns. The facing a bind reads comes from the body's own declaration of which way it faces (`Locomotion.forward`), so the rig lands behind a `+Z`-forward Synty body and a `-Z`-forward glTF body alike. A target that declares nothing falls back to its transform forward. Public fields: `follow`, `shoulderOffset` (x across the frame, y up it, z along the heading), `verticalArmLength`, `cameraSide` (0 = left shoulder, 1 = right), `cameraDistance`, `minDistance`, `maxDistance`, `aimYaw` (the world heading the camera looks along, in radians about +Y from world +Z) and `aimPitch` (how far it tilts off level), `damping` (per-axis time constants in seconds), `cameraRadius` (radius of the pull-in sweep; `0` turns the pull-in off), `cameraIgnore` (entities whose colliders the pull-in passes through), `sensitivity`, `invertY` (off by default: pushing the mouse forward looks UP; on pitches the other way), `zoomSpeed`, `minPitch`, `maxPitch`. Writing any framing field re-poses the rig immediately with damping bypassed, so edit mode shows the framing play mode will produce. `follow` falls back to the `Camera.follow` slot when it is empty, so a rig selected through `Camera.behavior` tracks whatever that camera already follows. Controls: mouse to look (play mode requests pointer lock, so no button is held), scroll to adjust `cameraDistance`. ## The control scheme `inputMap` is the scheme the rig answers to. It reads two controls from it, each on its own: - `look` — turns `aimYaw` and `aimPitch`. - `zoom` — steps `cameraDistance`. A map declaring only one of the two gives the rig that one and keeps the other where it is; the rig logs which control the map left out, and names the map. A field naming no map leaves the rig answering nothing, and logs that too. Point `inputMap` at a world's own map to rebind looking and zooming without touching this component. **Repointing it on a live rig takes on the next frame.** The map the rig was holding is stood down — its controls leave the live set and its touch buttons leave the screen — and the map named here is taken up in its place: ```luau entity(rigId).component.get("third_person_follow").inputMap = asset.resolve("mygame.input.cinema", "inputMap") ``` Assigning the map the rig already holds changes nothing, so re-stating the scheme leaves the live set where it is. While gameplay is paused the rig holds no map at all — the editor's camera has the controls — and it takes whatever `inputMap` names when play resumes. ```luau local cam = entity(id).component.get("Camera") cam.behavior = asset.ref("@builtin::controller.third_person_follow", "component") entity(id).component.get("controller.third_person_follow").cameraDistance = 3 ``` ## The camera pulls in when a collider is in the way Each frame the rig sweeps a sphere of `cameraRadius` from the point it aims at out to where its framing puts the camera, and stands the camera where the sphere first meets a collider. Every collider in the world stops that sweep, whether or not anything draws it: an invisible walk bound (a `BoxCollider` with no `Model`), a trigger volume and a solid wall all pull the camera in alike. The follow subject is the one entity the sweep passes by default. So a camera that snaps in close to the player, or collapses onto it, while nothing visible stands behind the player is meeting a collider nobody can see. Two fields settle it: - `cameraIgnore` lists the entities whose colliders the camera sees through. Name the walk bounds and gameplay volumes there and the sweep passes them as it passes the subject, while walls the player can see still pull the camera in. Each entry is an entity (or its id), and it is the collider's own entity that is listed. - `cameraRadius = 0` turns the pull-in off: the camera stands where its framing puts it, through every collider. ```luau local rig = entity(camId).component.get("controller.third_person_follow") rig.cameraIgnore = { entity.find("NorthBound"), entity.find("SouthBound") } -- or drop the pull-in entirely rig.cameraRadius = 0 ``` ## Attach through `Camera.behavior` The Camera owns the behavior slot, so assigning the rig there is what lets the Camera tell it when `follow` changes. A rig added straight onto the entity with `component.add` still poses, but the Camera does not know about it and a `follow` assigned afterwards will not reach it. Assigning the slot attaches the rig to the camera's entity, so it is tuned on that instance: `component.get("third_person_follow")` and set its fields. A `component.add("third_person_follow", { ... })` after the assignment, in a scene `build.luau` or anywhere else, writes its values onto the instance the slot attached and returns it, so the Camera keeps the one rig it knows about. In a `build.luau` that add is the way to tune the rig: its values are saved with the scene and the loaded rig holds them. ## Player cameras: leave `follow` unset Entering play drops and respawns `PlayerPrototype` subtrees, so the body is recreated with a NEW entity id. A `follow` authored on the rig keeps pointing at the original, which is no longer live, and the camera freezes at its last pose instead of tracking the player. For a player camera, set the target on the **Camera** and leave the rig's own `follow` empty: ```luau entity(rigId).component.get("Camera").follow = body -- PlayerPrototype rebinds this on every spawn ``` `PlayerPrototype` refreshes `Camera.follow` to the live body each time it clones, and the rig reads that slot whenever its own `follow` is empty. Set the rig's `follow` only for a camera that watches a fixed, non-respawning subject.
▲ 0↑ born
◇
component · born here
❒asset
# ReflectionProbe A reflection probe: bakes the scene into its own cube slot from its position, so reflective surfaces within its `radius` mirror the captured surroundings. Multiple probes in a scene are blended by proximity and rank (the renderer's per-fragment probe blend); a surface outside every probe's radius reflects the sky fallback, or the scene's ambient when no sky is captured. The probe renders a small editor-only gizmo marker (cyan octahedron) in edit mode and is invisible in play mode. Every field is live: editing `radius`, `priority`, the proxy or `skyFallback` in the inspector re-pushes the probe, and `reflectionProbe.list()` reports what the component holds. A probe is an **authoring-time** bake: it loads its persisted baked cube on awake (`reflectionProbe.loadBaked`) rather than re-baking on every load. A probe reflects once it holds a cube: until it is baked (or has a persisted bake to load) it adds nothing to the frame, arms no sky fallback, reads `baked = false` in `reflectionProbe.list()` and in `lighting.get`'s `environment.probes`, and the engine log names it with the call that bakes it. Set `probeId` on authored probes so the baked `probe_<probeId>.texture` asset keeps the same identity across reloads (the runtime entity id changes; the probeId does not). Public fields: - `radius` (number, default `12`) — influence radius in world units. A surface within it reflects this probe at a weight falling from full at the probe to zero at the radius. - `bakeOnAwake` (bool, default `false`) — opt-in first-time bake on awake (deferred a few frames so geometry is GPU-resident; baking before the scene renders captures black). Only fires when no persisted baked cube exists yet. - `probeId` (string, default `""`) — stable asset identity for the persisted baked cube. Empty → falls back to the entity id. - `skyFallback` (bool, default `true`) - once the probe holds a cube, capture the scene's sky into the fallback slot so a surface outside every probe's radius reflects the sky. Methods: `:bake()` — (re)bake this probe from its current position into its cube slot and persist it (runs off the calling frame). ```luau -- One-liner (spawns an entity carrying this component): reflectionProbe.add(0, 3, 0, { radius = 15, probeId = "lobby" }) -- Or attach directly + bake everything once the scene is built: entity.find("zone").component.add("ReflectionProbe", { radius = 12 }) reflectionProbe.bakeAll() ``` Max 8 probes (cube slots). Registry policy lives in `@builtin::api.engine.reflectionProbe`; the generic per-slot capture primitive is the `environment` module.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# controller.rts RTS camera controller. Top-down strategy camera. WASD to pan (aligned to camera yaw), scroll to zoom, right-click drag to rotate. Optionally lock to a target entity. Public fields: `inputMap` (the camera-rig scheme, `@builtin::inputMaps.cameraRig`, by default; point it at a world's own map to rebind this camera), `target` (empty = free pan), `centerX`, `centerY`, `centerZ`, `distance`, `minDistance`, `maxDistance`, `moveSpeed`, `sensitivity`, `zoomSpeed`, `minPitch`, `maxPitch`. Controls: WASD to pan, scroll to zoom, RMB drag to rotate. ```luau entity(id).component.add("controller.rts", { distance = 15, moveSpeed = 12 }) ```
▲ 0↑ born
◇
component · born here
❒asset
# SceneModule A placement of a `.sceneModule`: the module this entity is built from, and the params this placement sets. In edit mode it runs the module's builder and reconciles the result under this entity; what lands is permanent scene content — saved in `scene.json`, replicated, selectable and inspectable like anything placed by hand. Public fields: - `source` — the module to build. Gated by capability, so it accepts any asset whose type defines `instantiate`. `module` is an input alias for it. - `params` — this placement's input overrides, keyed by the names the module declares in its `inputs`. A name the module declares overrides that input's default; a name it does not declare is left alone. Those two are the whole record. A placement in `scene.json` reads: ```json { "id": "ent_ee2d09b7c4ee5a28", "name": "orchard_0", "components": [ { "type": "@builtin::components.SceneModule", "data": { "enabled": true, "params": { "rows": 2 }, "source": { "name": "Orchard", "type": "sceneModule", "guid": "49ba97ac-…" } } } ] } ``` The entities the build placed sit beside it as records of their own, each naming this entity as the build that placed it and its own chain of names from the build root down to it: ``` buildOwner=ent_ee2d09b7c4ee5a28 buildKey=orchard buildOwner=ent_ee2d09b7c4ee5a28 buildKey=orchard/tree_r1 buildOwner=ent_ee2d09b7c4ee5a28 buildKey=orchard/tree_r1/canopy ``` ```luau local pusher = entity.spawn("pusher_0") pusher.component.add("SceneModule", { source = asset.resolve("CoinPusher", "sceneModule"), params = { seed = 0, coinCount = 40 }, }) pusher.localPosition = { 0, 0, 0 } ``` Placing the same module twice with different params yields two different results from one asset: ```luau for i = 0, 2 do local p = entity.spawn("pusher_" .. i) p.component.add("SceneModule", { source = coinPusher, params = { seed = i } }) p.localPosition = { i * 4, 0, 0 } end ``` ## Rebuilds A write to the module's `init.luau`, or a change to `params`, re-runs the builder and **reconciles** the result: an entity the previous build placed is updated in place, one the build no longer emits is removed, and a new one is spawned. Ids hold, so a reference to a built entity survives the rebuild. A rebuild recognises its own entities by computing those ids again: the same placement and the same place in the hierarchy give the same string. The scene save writes them and the load brings them back, so a rebuild in a later session lands on exactly the entities the first build placed with nothing carried between the two. A CHANGE is what rebuilds, and the changes are `source`, `params`, and the module's code on disk. Loading and mode flips are not changes: the entities are in the scene as authored content, the last bake is what they hold, and the placement runs nothing. A rebuild whose module, params and module source are all unchanged is skipped too. `:rebuild()` forces one anyway — for a change nothing else reports, such as an asset the builder resolves changing underneath it, or a module edited while the world was closed. A build that raises leaves the previous one in the scene and says so, rather than emptying the placement over a typo. ## Who states the params A placement an author made — dragged in, spawned by a tool, written by a script — holds its own params. Nothing else sets them. A placement a **build** made is stated by that build. A scene's `build.luau` that places one names its `source` and its `params`, and every run of that build states them again — on load, on a write to the file, on `:build()`. So a param set on such a placement is a preview: it rebuilds at once and holds until the next run of the build, which states the params its own source gives. Both moments say so. Setting a param on a build-placed placement names the file that states it: ``` [warn] a build placed this entity and states its SceneModule params — a param set on the placement holds until that build runs again and states them from its own source { placement="tent_ring_placement", set="count = 9", statedIn="/zero/source/scenes/main.scene/build.luau (content)" } ``` and the run that states them again names what it replaced: ``` [warn] a build ran and stated this placement's SceneModule params over the ones set on the placement — the file the build is written in is where they are stated { placement="tent_ring_placement", replaced="count: 9 → 7", statedIn="/zero/source/scenes/main.scene/build.luau (content)" } ``` A run that states what the placement already holds says nothing. To change the value for good, change it where the build states it. A build that stops stating a placement takes what the placement built with it. When a run no longer adds the `SceneModule` to an entity it places, because it dropped the module or now places it there through `instantiate`, the entities the placement built under that entity go with the component, so the entity holds only what the build states now. A placement an author added to an entity a build places is the author's: a run of that build leaves it and what it built in place. ## Play Nothing runs and nothing instantiates. The entities are already in `scene.json` and load as authored content, so play is deterministic, costs no Luau construction, works on a runtime-profile boot with no edit session, and every multiplayer peer loads the same records. This component is inert there. ## Methods - `:rebuild()` — re-run the builder and reconcile, ignoring the signature. Returns false in play. - `:declaredInputs()` — the bound module's declared inputs, sorted: `{ name, kind, default, values? }`. The set `params` can address.
▲ 0↑ born
◇
component · born here
❒asset
# PositionConstraint Constrains an entity's transform (position, optionally rotation and scale) to follow a target entity, with optional offset. Use `weight` to control smoothing. Public fields: `targetEntityId`, `targetPositionX/Y/Z`, `offsetX/Y/Z`, `axisX`, `axisY`, `axisZ`, `weight`, `constrainRotation`, `constrainScale`. Methods: `setTarget(entityId)`, `setOffset(x, y, z)`, `setWeight(w)`. ```luau entity(id).component.add("PositionConstraint", { targetEntityId = carId, offset = {0, 5, 10}, weight = 0.15, }) ```
▲ 0↑ born
◇
component · born here
❒asset
# PlayerSpawn A world entity that spawns players from a prototype. It carries the fields describing how spawning happens: which `prototype` to instantiate, the `team` and `role` assigned to spawned players, a `maxPlayers` cap (0 = unbounded), the `spawnPolicy` (how many players spawn from this point), and the `placement` (where a spawned player is positioned). The entity's own transform is the spawn point when `placement` is `at_spawn_transform`. In edit mode it draws an orange silhouette outline at the spawn point and surfaces its own setup-validation messages (missing or mis-targeted prototype) on the component's error channel. Location: `src/lua/lib/components/PlayerSpawn.component`
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# components.FreeMover WASD + mouse free-fly mover. Attach to an entity to make it move under WASD input with mouse look, independent of any character controller or physics rig. Used by the editor's free camera and any debug "fly-around" viewpoint. Public fields: `speed`, `sensitivity`, `sprintMultiplier`.
▲ 0↑ born
◇
component · born here
❒asset
# Mover `Mover.component` — see `init.luau` for the component surface. Location: `src/lua/lib/components/Mover.component`
▲ 0↑ born
◇
component · born here
❒asset
# HeightfieldCollider (component) A regular grid of heights, which is the shape terrain collides with. Testing against it costs a cell lookup rather than a walk down a triangle tree, so a large landscape collides at a cost that stays flat as it grows. ## The grid `rows` and `cols` are the grid's dimensions, `extent` its world span as `{ x = width, y = heightScale, z = depth }`, and `heights` the row-major grid itself, indexed `heights[r * cols + c + 1]`. The grid is centred on the entity. `rows` maps to the entity's local Z and `cols` to its local X. ```lua entity.find("terrain").component.add("HeightfieldCollider", { rows = 64, cols = 64, extent = { x = 300, y = 1, z = 300 }, heights = grid, }) ``` Use `setGrid(rows, cols, heights)` to replace the grid and the dimensions describing it together, so the two are never out of step. ## Replication `heights` is `NoSync`. It is large, and every peer can rebuild it from its own copy of the terrain, so replicating it would send a whole landscape over the wire to reproduce something each peer already has. ## Fields | Field | Meaning | |-------|---------| | `rows` / `cols` | Grid dimensions. | | `extent` | World span, with `y` scaling the heights. | | `heights` | Row-major height grid. Not replicated. | | `isTrigger` | Report overlaps without blocking. | | `friction` | Surface friction. | | `restitution` | Bounciness. |
▲ 0↑ born
▣
module · born here
❒asset
# assetType.shared.ref Builds the metatable every `AssetRef` (the `{ __ref, type, name, guid, identity, path }` envelope returned by `asset.resolve` / `asset.ref`) carries. Replaces the per-call C metatable that used to live in `crates/zero_scripting/src/ffi/bindings/asset.rs::attach_asset_ref_metatable`. ## Public API ```lua local assetRef = require("@builtin::assetTypes.assetType.shared.ref") -- Called by the Rust factory (push_asset_ref_handle) via -- _G.__build_asset_ref_proxy after the envelope fields are set. assetRef.build(envelope) -- attaches AssetRefMT, returns the table ``` The module exposes no invalidation surface — discovery is registry-driven and hot-reload is transparent (see "Discovery and hot-reload" below). ## Method dispatch order `ref.foo` is resolved in this order: 1. **Default methods** — `getSource` / `getBytes` / `getText` / `exists` / `inspect` and the lazy `meta` property. Per-type definitions cannot override these (engine contract from gh#1889). 2. **Per-type `ref` table** — discovered through `asset.resolve(<typename>, "assetType")` + `require(<identity>.behavior)`. The lookup resolves to whichever `<typename>.assetType/` folder is currently registered for the typename (built-in, user, or library-vendored) and pulls its `ref` methods. 3. **Nil** — unknown key, same shape as the original C metatable. ## Adding methods for a new asset type Drop a `behavior.luau` into the type folder: ``` src/lua/lib/assetTypes/<typename>.assetType/ ├── type.yaml -- structural spec (existing) ├── behavior.luau -- ref + global behaviour (new) └── template/ -- placeholder body (existing) ``` `behavior.luau` returns: ```lua return { ref = { --!desc Per-instance method on every `<typename>` AssetRef. myMethod = function(self, ...) ... end, }, global = { -- Reserved for the asset.<typename>.* surface — not consumed -- yet; included so the contract from gh#1889 lands cleanly. }, } ``` Within `ref` methods, `self` is the AssetRef envelope: `self.path`, `self.identity`, `self.guid`, `self.type`, `self.name` are available. Engine-required fields (`guid`, `path`, `identity`, `type`) must not be redefined. ## Discovery and hot-reload Type discovery uses the asset registry, so it doesn't matter where a `<typename>.assetType/` folder lives — engine built-ins under `@builtin::assetTypes.<X>`, user types dropped under `/zero/source/<X>.assetType/`, or types vendored inside an imported library all resolve identically. The dispatcher walks `asset.resolve(<typename>, "assetType")` → canonical identity → `require <identity>.behavior` on every miss; the asset registry is the cache for the resolve half and Luau's `_LOADED` is the cache for the require half. Hot-reload is transparent. When a `<typename>.assetType/behavior.luau` is rewritten through VFS, the engine's `invalidate_require_cache_for_state` re-runs the module and mutates the cached `_LOADED[<identity>.behavior]` table in place. Dispatch reads `mod.ref` on every access (rather than caching a sub-pointer), so existing AssetRefs see the new methods on the very next access — no Rust-side hook into this module is needed, and no invalidation API is exposed for callers to call. The same registry-driven path also handles type **registration** correctness: dropping a new `<typename>.assetType/behavior.luau` updates the asset registry, the next `asset.resolve(<typename>, "assetType")` returns the new ref, and AssetRefs of that type immediately gain the new methods. ## Why Luau, not Rust The asset surface needed per-category behaviour (`material:getProperties()`, `bundle:instantiate(entityId)`, `tool:run(args)`) and the old shape couldn't grow without an FFI change per method. Owning the proxy in Luau means: - Type-specific methods ship as content (the `<typename>.assetType/` folder), not engine code. - Adding a new asset type drops in a new folder; no Rust rebuild. - Methods can call any other Luau global (`vfs`, `asset`, `Material`, `entity`, etc.) directly, instead of going through narrow FFI primitives. ## Consumers - `crates/zero_scripting/src/ffi/bindings/asset.rs::attach_asset_ref_metatable` — the Rust factory that pushes envelopes invokes `_G.__build_asset_ref_proxy` once per push. - Every Luau call site of `asset.resolve` / `asset.ref` / `entity(id).component.X.material` / any binding declared `AssetRef<...>` — they all receive envelopes built through this module.
▲ 0↑ born
◇
component · born here
❒asset
# Humanoid Marker component for entities carrying a skeleton conforming to the standard humanoid bone layout (Synty / Unreal-Mannequin convention). Locomotion, LocomotionFull, future IK, equipment slots, and ragdoll all read bones through this component instead of re-walking the descendant tree. ## Bone naming convention ``` Hips Spine_01 → Spine_02 → Spine_03 → Neck → Head └ Eyes, Eyebrows, Jaw UpperLeg_R / UpperLeg_L LowerLeg_R / LowerLeg_L Ankle_R / Ankle_L Ball_R / Ball_L → Toes_R / Toes_L Clavicle_R / Clavicle_L Shoulder_R / Shoulder_L Elbow_R / Elbow_L Hand_R / Hand_L IndexFinger_01..04 / Thumb_01..04 / Finger_01..04 ``` Mixamo / Unity-Humanoid imports go through a bone-rename pass at import time (tracked separately). ## Fields | Field | Type | Notes | |---|---|---| | `height` | number | Foot-to-head height in T-pose, metres. Default 1.8. | | `eye_height` | number | Foot-to-eyes height, metres. Default 1.65. | | `bones` | `{ [string]: entity }` | Computed; bone-name → entity-proxy map. Re-walks on read. | ## Methods | Method | Returns | Notes | |---|---|---| | `:bone(name)` | `entity \| nil` | Nil-safe single-bone lookup; same as `self.bones[name]`. | ## Usage ```luau -- Read a specific bone local h = entity(playerId).component.get("Humanoid") local head = h.bones.Head -- entity proxy local handR = h:bone("Hand_R") -- nil-safe lookup -- Mount something on the head local hat = entity.spawn("hat", { parent = head.id }) -- Read height for camera mounting local cam = entity.spawn("eye_cam", { parent = playerId }) cam.localPosition = { 0, h.eye_height, 0 } ``` ## Tag convention Bundles carrying a humanoid skeleton tag themselves with both `playerVisual` AND `humanoid` in their `.metadata`. Tooling can filter to humanoid-conforming visuals via `asset.list("bundle", nil, { fields = { tags = { "playerVisual", "humanoid" } } })`.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# controller.isometric Isometric camera controller. Fixed-angle overhead camera (classic isometric view). WASD to pan on the XZ plane, scroll to zoom. The viewing angle is fixed (no rotation). Public fields: `inputMap` (the camera-rig scheme, `@builtin::inputMaps.cameraRig`, by default; point it at a world's own map to rebind this camera), `target` (empty = free pan), `centerX`, `centerY`, `centerZ`, `distance`, `minDistance`, `maxDistance`, `moveSpeed`, `zoomSpeed`, `yaw` (default pi/4), `pitch` (default ~35.26 degrees). Controls: WASD to pan (world-axis-aligned), scroll to zoom. ```luau entity(id).component.add("controller.isometric", { distance = 12, moveSpeed = 8 }) ```
▲ 0↑ born
◇
component · born here
❒asset
# LookAtConstraint Rotates an entity to face a target entity or a fixed world position. Use `weight` for partial/smoothed orientation. Public fields: `targetEntityId`, `targetPositionX/Y/Z`, `axisX`, `axisY`, `axisZ`, `weight`. Methods: `setTarget(entityId)`, `setWeight(w)`. ```luau entity(id).component.add("LookAtConstraint", { targetEntityId = playerId, weight = 0.5, }) ```
▲ 0↑ born
◇
component · born here
❒asset
# Audio Adds audio playback to an entity, driven by a `.soundClip` asset. Supports sound effects, music, and spatial/3D audio tied to entity position. Public fields: `clip` (`AssetRef<soundClip>`), `sourceType` (`"SoundEffect"` / `"Music"` / `"Ambient"`), `volume`, `pitch`, `looping`, `spatial`, `maxDistance`, `rolloff`, `autoplay`, `channel`. A spatial source plays at full volume within one unit of the listener and reaches silence at `maxDistance` (default 50). Across that window its amplitude is `(1 - t)^rolloff` for a source `t` of the way through, so the default `rolloff` of `1` is a linear fade — half the distance, half the amplitude — a larger exponent drops away sooner, and `0` holds full volume to the edge. Methods: `play()`, `pause()`, `stop()`, `setVolume(v)`, `setPitch(p)`. ```luau entity(id).component.add("Audio", { clip = explosionClip }) entity(id).component.add("Audio", { clip = fireClip, spatial = true, looping = true }) ``` A loose audio file (ogg/mp3/wav/flac) needs to be imported into a `.soundClip` asset before it can be referenced here — the importer creates the container and encodes its playable payload. Whether the clip streams its compressed payload or decodes fully on load follows the clip's own `loadType` setting: `"decompressOnLoad"` and `"streaming"` apply directly, and `"auto"` (the clip's default) picks by the clip's duration — short one-shots decode up front, longer tracks stream. The clip decides: each source this component builds asks `clipRef:streams(bytes)`.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# CutsceneTrigger Plays a cutscene when something happens, without writing the wiring: the scene entering play, the player reaching this entity, or a call from your own code. ```luau -- the game opens on a cutscene entity.spawn("Intro").component.add("CutsceneTrigger", { cutscene = "mygame.scenes.intro", trigger = "onPlay", }) -- a cutscene when the player reaches the gate entity.find("GateArea").component.add("CutsceneTrigger", { cutscene = "mygame.scenes.gate_opens", trigger = "onEnter", radius = 4, }) ``` The cutscene itself is a module returning a definition — see the `cutscene` module for its shape, and the `topics/cutscenes` guide for the whole picture. ## Fields | Field | Default | Notes | |---|---|---| | `cutscene` | `""` | The module path a cutscene definition is returned from. | | `trigger` | `"onPlay"` | `onPlay` as the scene enters play · `onEnter` when the local player comes within `radius` · `manual` only on a `play()` call. | | `radius` | `3` | How close the local player has to get, for `onEnter`. | | `delay` | `0` | Seconds to wait after the trigger before it starts. | | `once` | `true` | Whether it plays only the first time it is triggered. | | `skippable` | `true` | Whether the player can skip it, overriding the definition. | | `played` | `false` | Whether it has already played. Local to this peer. | ## Methods - `play()` — start the cutscene now, whatever `trigger` says. This is what `trigger = "manual"` is for. - `isPlaying()` — whether the cutscene this trigger started is still running. ## Notes - `played` is `NoSync` on purpose: a cutscene is something each player watches for themselves, so one player having seen it is not a reason another player does not. - Playing goes through `cutscene.play`, so only one cutscene is ever on screen — a trigger that fires while another cutscene runs ends that one first. The `onEnter` trigger holds off while any cutscene is playing rather than cutting across it. - A trigger destroyed while its own cutscene is on screen ends the cutscene, so the player is never left looking through a camera nothing moves.
▲ 0↑ born
◇
component · born here
❒asset
# SoftBody Makes an entity a deformable body: a jelly, a cushion, a balloon, a cloth, a soft version of the entity's own model, or a shell that crumples and keeps the dent, like a car body after a crash. The body is a set of particles held together by elastic elements, simulated in the same step as rigid bodies, so it rests on and pushes rigid bodies, rigid bodies rest on, bounce off and dent it, and soft bodies collide with each other. A soft body is its own body and its own collision surface: it goes on an entity with no `Physics` and no collider component, and whichever is added second is refused with an error naming the conflict (logged, and readable as the refused component's `disabledReason`). The body fills the entity's unit box (-0.5 to 0.5 on each axis) times its world scale, so it sits where a default cube or sphere primitive of that scale would. `shape = "mesh"` instead fills every closed part of the entity's model with tetrahedra of `cellSize`, and the model is drawn carried by them; a model with no closed part, or a cell size past the 60000-cell cap, is refused with the reason and the fields that change it. While the simulation runs, the entity's position and rotation follow the body as a whole, and a `Model` on the entity moves with it; the deformed surface is drawn by the physics view (`capture` with `pass = "physics"`) and read by `Physics.softBodyState`. Public fields: `shape` (`"box"`, `"sphere"`, `"balloon"`, `"cloth"`, `"mesh"`), `cellSize` (for `mesh`: the cells' edge in metres, 0 takes the model's longest extent divided by `resolution`), `collideWith` (for `mesh`: `"cage"`, the cells' boundary, or `"surface"`, the model's own surface), `resolution` (particles along the longest edge, 2 to 24), `mass` (kg; the default 0 weighs it at 1 kg per cubic metre of its shape, like a rigid body), `stiffness` (Young's modulus in pascals for a box or sphere, default 300; spring stiffness for a balloon or cloth), `damping` (0 rings on, 1 settles without overshoot), `model` (a solid's cells: `"corotational"`, the default, `"neoHookean"` or the cheap `"volume"`), `poisson` (Poisson's ratio, 0 up to 0.5), `plasticYield` (the strain past which a dent stays; 0, the default, is fully elastic), `plasticCreep` (how fast per second the strain past the yield sets, default 10), `plasticMax` (the most permanent strain a part takes, default 0.5), `inflation` (the share of its built volume a closed shape is held at), `shapeMatching` (hertz of the pull back toward the body's rest shape as a whole, 0 for none), `selfContact` (whether the surface collides with itself), `repairs`, `preserveVolume`, `friction`, `restitution`, `collisionMemberships`, `collisionFilter`, `canSleep`, `attachments`. The material (`stiffness`, `damping`, `poisson`, the plastic fields, `inflation`, `shapeMatching`, `preserveVolume`, `friction`, `restitution`, the collision groups) and `repairs` take effect on the running body, waking it. Writing `shape`, `resolution`, `cellSize`, `collideWith`, `mass`, `model`, `selfContact` or `canSleep` rebuilds the body; in play mode a rebuild of the same particles (a new `model`, `mass` or `selfContact`, or the entity moved while the clock is paused) keeps the deformation that stayed, and one while the simulation runs keeps every particle where it is and how it moves. A reload of this component's code keeps the body as it stands: the reloaded component takes it over, dents, motion and pins included. **Permanent deformation.** With `plasticYield` above 0, a part of the body strained further than the yield flows toward the shape it is held in, so a dent, a crumple or a crease stays once the load is gone: a car shell that keeps its crash, clay that keeps a print, a sheet that keeps a fold. `Physics.softBodyState(id).plastic` reads how deep it is, in metres; `:repair()` (or `Physics.repairSoftBody(id)`) restores the shape the body was built in, the body put back in it where it stands and moving on as it moved. Leaving play for edit mode puts the body back in that shape too. `attachments` holds regions of the body to rigid bodies, two-way: a list of `{ target, region }`, `target` the entity id of a body with `Physics`, `region` `{ box = { min, max } }` (every particle between the corners) or `{ nearest = point }` (the one particle nearest the point) in the entity's unit box, `space = "world"` for world coordinates, empty for the whole body. Each region follows its rigid body and the rigid body carries its weight and pull. Changing the list re-ties only the entries that changed; a rebuilt body is tied again, and removing either side releases the hold. `Physics.softBodyState(id).attachments` reads each entry back with the force it pulls with, or why it holds nothing. To join soft bodies to each other, or to anchor a joint on a soft body, use a `Joint`, whose ends each take a region. Methods: `:state(opts?)` returns the same report as `Physics.softBodyState(entityId, opts)`. `:repair()` restores the shape the body was built in and returns the new `repairs` count. `:attach(target, region?)` adds an attachment and returns its index (the same as `Physics.attachSoftRegion`); `:detach(target?)` releases the regions held to `target`, or all of them, and returns how many it removed. The body is built at the start of the frame after the component is added. Until then `Physics.softBodyState(id).status` is `"pending"` (with `exists = false`, and `waitingFor` naming what the build waits on); once built it is `"built"`, and a component that was not built reads `"refused"` with the reason in `refusal`. `Physics.softBodyState(id).cage` reports a `mesh` body's fill: `cellSize`, `cached` (taken from the cage cache, as a second instance of a model is), `buildMs`, `generateMs`, `filledParts`, `strayParts`, `openParts`, `seams` (springs sewing touching parts together), `pieces`, `enclosedVolume` and `digest`. ```luau local jelly = entity.spawn("jelly") jelly.position = { 0, 2, 0 } jelly.component.add("SoftBody", { shape = "box", resolution = 6 }) print(Physics.softBodyState(jelly).status) -- "pending" in the call that added it -- on a later frame: local s = Physics.softBodyState(jelly) print(s.status, s.particleCount, s.volumeRatio, s.sleeping) -- "built", 216, ... ``` ```luau -- a cloth whose left edge hangs from a pole flag.component.add("SoftBody", { shape = "cloth" }) flag.component.get("SoftBody"):attach(pole, { box = { min = { -0.5, -1, -0.5 }, max = { -0.45, 1, 0.5 } } }) print(Physics.softBodyState(flag).attachments[1].forceMagnitude) -- on a later frame ``` ```luau -- a slab of clay that keeps the print of whatever lands on it clay.component.add("SoftBody", { shape = "box", stiffness = 3000, plasticYield = 0.02 }) -- on a later frame, after something landed on it and left: print(Physics.softBodyState(clay).plastic) -- metres of dent that stayed clay.component.get("SoftBody"):repair() -- and gone again ```
▲ 0↑ born
◇
component · born here
❒asset
# ClipPlayer Plays one `.animation` clip on the skinned body it is attached to, through the canonical AnimGraph. The graph retargets the clip from its source rig onto the body's rig and drives the body's bone entities each frame, so any humanoid clip plays on any humanoid body. It is the single-clip case of the animation contract: build a one-clip graph on start, then advance and apply it each frame. Attach it to the same entity as the skinned mesh. The body's rig comes from its `Skeleton`; the clip's source rig from the clip itself (or `sourceRig`). All the retarget and pose machinery lives in the graph — this component only configures the clip and ticks it. ```luau entity.find("hero").component.add("ClipPlayer", { clip = "A_Walk_F_Masc" }) ``` Public fields: `clip` (the `.animation` to play), `sourceRig` (the clip's source rig; empty resolves from the clip), `looping`, `speed` and `playing`.
▲ 0↑ born
◇
component · born here
❒asset
# PostProcess Component Applies a **stack of post-process effects** to the scene, declaratively, from a single entity. Each effect references a `.shader` asset (`@domain: post-process`) and carries its own **configurable property values** — tuned like material properties, with no shader edit required. This is the load-time **reproducer** for post-processing: where an agent's live `postprocess.add(...)` + `postprocess.setProperty(...)` calls produce effects that vanish on reload, a `PostProcess` component re-registers the exact same stack — with the exact same tuning — every time the scene loads. The persist freeze converts live execute-origin effects into `.shader` assets + one entity carrying this component, so **what you see is what you get** across save/reload. ## Properties | Field | Type | Description | |---|---|---| | `effects` | array | The effect stack. Each entry: `{ shader, priority?, enabled?, layer?, values?, textures? }`. | | `shape` | `"global"` \| `"box"` \| `"sphere"` | Where the stack applies. Default `"global"` (unbounded). | | `extents` | vec3 | Box region half-extents, world units. Default `{5, 5, 5}`. | | `radius` | number | Sphere region radius, world units. Default `5`. | | `falloff` | number | Soft-edge distance outside the volume the stack fades over. Default `0`. | | `priority` | number | Order of this region against other `PostProcess` regions, added to each effect's own priority. Default `0`. | | `display` | struct `{ curve, exposure }` | The display transform this region sets while it is live — `curve` (0 linear, 1 Reinhard, 2 ACES, 3 Khronos PBR Neutral, default `3`) and `exposure` (multiplier on the scene's light before the curve, default `1`). Takes effect only when `setsDisplay` is `true` and `shape` is `"global"`. | | `setsDisplay` | boolean | Whether this region writes `display` to the chain's `__display__` transform while it is live. Default `false`. | Each effect entry: | Key | Type | Description | |---|---|---| | `shader` | asset reference | The `.shader` asset (a `@domain: post-process` shader) this effect runs. Builtin shaders (`@builtin::shaders.post.vignette`, …) or scene-authored custom shaders both work. | | `priority` | number | Chain order within this region — lower runs earlier. Default `100`. | | `enabled` | boolean | Per-effect toggle. Default `true`. | | `layer` | `"all"` \| `"scene"` | Which composited image this effect grades: `"scene"` is the rendered picture before the UI reaches it, in the scene's own light; `"all"` is the picture fitted to the screen with every widget on it. Left empty, the entry follows its shader's `space` — a `light` shader grades `"scene"`, a `display` shader grades `"all"`. | | `values` | table | Named property overrides `{ [prop] = value }`, applied via `postprocess.setProperty` after the effect is added — the configurable tuning. | | `textures` | table | Texture bindings `{ [prop] = path }`, applied via `postprocess.setTexture` after the effect is added. | The component-level `enabled` flag (engine-managed, not declared here) toggles the **whole stack**: disabling the component removes every effect (and restores the chain's own display transform if this region set one); enabling it re-registers them (`onEnable` / `onDisable`). ## Usage ```lua entity.find("fx").component.add("PostProcess", { effects = { { shader = "@builtin::shaders.post.vignette", priority = 0, values = { intensity = 0.6 } }, { shader = "@builtin::shaders.post.grayscale", priority = 10, values = { intensity = 1.0 } }, }, }) ``` Setting the display transform from a `global` region: ```lua entity.find("look").component.add("PostProcess", { setsDisplay = true, display = { curve = 2, exposure = 1.5 }, -- ACES }) ``` Two `global` regions with `setsDisplay = true` both write `__display__` — whichever registers last wins the frame. The regions setting it share one ordered record of who is live, so they come off in any order: removing the newer of two leaves the older one's display standing, removing the older leaves the newer one's, and removing the last leaves whatever stood before any of them wrote. The exposure package drives `__display__.exposure` on its own, from a GPU readback each tick it meters. A region arriving or leaving sets the exposure and the package's next readback sets it again, so with auto-exposure running the exposure a region asks for holds for a frame while its curve holds for as long as the region does. ## Methods - `pp:getEffectNames()` — the live effect names currently registered, in stack order. - `pp:setProperty(index, prop, value)` — tune a property of one effect at runtime (forwards to `postprocess.setProperty`), like editing a material property.
▲ 0↑ born
◇
component · born here
❒asset
# orbital_follow Three-ring orbit camera. Framing is authored as three rings around the follow target — a bottom, a centre and a top, each with a `height` above the target's origin and a `radius` out from its axis. Vertical look slides the camera along the spline through those rings; horizontal look orbits around them. The shape of the orbit is geometry you can see and edit rather than a pair of pitch limits you cannot. Public fields: `follow`, `ringBottom` / `ringCenter` / `ringTop` (each `{ height, radius }`), `verticalAxis` (0 at the bottom ring, 1 at the top), `horizontalAxis` (the world heading the camera looks along, in radians about +Y from world +Z), `radiusScale`, `aimHeight` (the aim point above the target's origin), `damping` (per-axis time constants in seconds), `cameraRadius` (radius of the pull-in sweep; `0` turns the pull-in off), `cameraIgnore` (entities whose colliders the pull-in passes through), `sensitivity`, `invertY` (off by default: pushing the mouse forward looks UP, drawing the camera toward the bottom ring; on pitches the other way), `zoomSpeed`, `minRadiusScale`, `maxRadiusScale`. The rig owns that heading. Binding a follow target aims it along the subject's facing, so the shot opens from behind whoever the camera is told to follow, and look input turns it from there — the subject's facing is never read again. That is what makes the camera steerable for a character who turns to face where it is walking: hold "forward" and the character walks the line the camera looks down, instead of chasing a heading that moves as it turns. The default rings frame a roughly 1.8 m humanoid standing at its origin: the centre ring sits above head height at the widest radius for a resting shot from behind and slightly above, the bottom ring drops to knee height and pulls in to look up, and the top ring climbs and tightens for a near-overhead look down. Writing any framing field re-poses the rig immediately with damping bypassed, so editing a ring moves the camera to the shot that ring describes. `follow` falls back to the `Camera.follow` slot when it is empty, so a rig selected through `Camera.behavior` tracks whatever that camera already follows. Controls: mouse to orbit and to slide along the rings (play mode requests pointer lock, so no button is held), scroll to scale the ring radii. ```luau local cam = entity(id).component.get("Camera") cam.behavior = asset.ref("@builtin::controller.orbital_follow", "component") entity(id).component.get("controller.orbital_follow").ringCenter = { height = 2.6, radius = 6 } ``` ## The camera pulls in when a collider is in the way Each frame the rig sweeps a sphere of `cameraRadius` from the point it aims at out to where its framing puts the camera, and stands the camera where the sphere first meets a collider. Every collider in the world stops that sweep, whether or not anything draws it: an invisible walk bound (a `BoxCollider` with no `Model`), a trigger volume and a solid wall all pull the camera in alike. The follow subject is the one entity the sweep passes by default. So a camera that snaps in close to the player, or collapses onto it, while nothing visible stands behind the player is meeting a collider nobody can see. Two fields settle it: - `cameraIgnore` lists the entities whose colliders the camera sees through. Name the walk bounds and gameplay volumes there and the sweep passes them as it passes the subject, while walls the player can see still pull the camera in. Each entry is an entity (or its id), and it is the collider's own entity that is listed. - `cameraRadius = 0` turns the pull-in off: the camera stands where its framing puts it, through every collider. ```luau local rig = entity(camId).component.get("controller.orbital_follow") rig.cameraIgnore = { entity.find("NorthBound"), entity.find("SouthBound") } -- or drop the pull-in entirely rig.cameraRadius = 0 ``` ## Attach through `Camera.behavior` The Camera owns the behavior slot, so assigning the rig there is what lets the Camera tell it when `follow` changes. A rig added straight onto the entity with `component.add` still poses, but the Camera does not know about it and a `follow` assigned afterwards will not reach it. ## Player cameras: leave `follow` unset Entering play drops and respawns `PlayerPrototype` subtrees, so the body is recreated with a NEW entity id. A `follow` authored on the rig keeps pointing at the original, which is no longer live, and the camera freezes at its last pose instead of tracking the player. For a player camera, set the target on the **Camera** and leave the rig's own `follow` empty: ```luau entity(rigId).component.get("Camera").follow = body -- PlayerPrototype rebinds this on every spawn ``` `PlayerPrototype` refreshes `Camera.follow` to the live body each time it clones, and the rig reads that slot whenever its own `follow` is empty. Set the rig's `follow` only for a camera that watches a fixed, non-respawning subject.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# SkinnedModel Component that drives skinned model playback and animation graph integration. Like `Model`, it carries the `material` field (`AssetRef<material>`) — material lives on the model that renders it, not as a standalone component. Set it at add time or assign `entity(id).component.get("SkinnedModel").material = "..."` later; `:setMaterialProperty` / `:getMaterialProperty` / `:getMaterialPropertyNames` operate on the assigned material (registry-wide). ## Blend model `skinning` chooses how the pass combines a vertex's joint influences. - `"linear"` (default) averages the joint matrices themselves. The average of two rotations is not a rotation, so a strongly twisted joint loses volume toward its axis — at a 180° twist a limb collapses onto the axis entirely. - `"dualQuaternion"` averages the joints' rigid motions as unit dual quaternions and renormalises, so the blend is a rotation at every weight and the cross-section is carried through the twist. Non-uniform joint scale rides along as a separately blended stretch. It costs a quaternion extraction per influence, so it is worth setting on characters whose wrists, shoulders or spine twist far, and leaving off elsewhere. ```lua entity.find("hero").component.get("SkinnedModel").skinning = "dualQuaternion" ``` A rig at rest renders identically under either model. ## Render self-check A few frames after its mesh attaches, a SkinnedModel checks that the mesh names something the engine can draw (on the GPU, or held off the device until this display puts it back) and that the pose it is drawn in has not collapsed it: the posed vertices the skinning pass wrote are measured against the mesh's own bind-pose extent, so a 1 cm part of a prop is judged by its own size. A mesh that fails is swapped for the builtin ERROR marker, drawn un-skinned with the error material, and the reason is logged. Setting `model` again takes the marker down and checks the new mesh. `renderCheck()` answers the verdict: `{ state = "pending" | "passed" | "failed" | "skipped", reason }`. ```lua local v = entity.find("prop_part").component.get("SkinnedModel"):renderCheck() if v.state == "failed" then print(v.reason) end ```
▲ 0↑ born
❒
package · born here
❒asset
# deprecatedParticles (deprecated) > **Deprecated.** This belongs to the earlier particle substrate. It stays loadable by identity and guid for content that already uses it; nothing new should use it, and search does not offer it. Particles are procgen sim nodes (`sim.spawn`, `sim.particles`, `sim.shader`) played through an effect: use `@builtin::assetTypes.effect`. The package holds the GPU particle engine module (`engine`), the `deprecatedParticles` module over it, the `DeprecatedParticleEmitter` and `DeprecatedSimpleParticleEmitter` components, the shaders, compute shaders and materials they draw with, and a demo scene. The package's `.metadata` carries the deprecation marker, so every asset inside is deprecated.
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# WheelCollider A wheel of a ray-cast vehicle (Unity-style). Add it to a child entity of a rigid body: every wheel under one body is one wheel of that body's vehicle, simulated by rapier's ray-cast vehicle controller (`DynamicRayCastVehicleController`). The system finds the nearest ancestor with a `Physics` component and drives it. Each physics tick a wheel casts its suspension ray straight down the chassis' local **-Y** from its own position, which is the suspension **mount**. The ray passes through triggers and through the chassis' own colliders, so a chassis `BoxCollider` around the mounts is fine. The controller then applies the suspension, drive, brake and tyre grip to the chassis as impulses. Drive conventions: positive `motorTorque` drives toward the parent body's local **-Z** (the engine's forward); positive `steerAngle` steers right. ## Fields | Field | Meaning | |---|---| | `suspensionDistance` | Suspension travel (m): the wheel hangs at most this far below the mount, and the spring compresses by at most this much from its rest, where the bump stop holds the wheel. | | `targetPosition` | Where the spring rests, as a share of `suspensionDistance` below the mount. The spring's rest length is `suspensionDistance * targetPosition`, and the wheel reaches the ground within that length plus its radius. 1 rests at full extension. The spring compresses from its rest by up to `suspensionDistance`, where the bump stop is. | | `springRate`, `damperRate` | Spring stiffness (N/m) and damping (N·s/m) per wheel. | | `maxSuspensionForce` | The most force (N) one wheel's suspension pushes with (default 6000). Raise it for a vehicle heavier than about 2 t on four wheels. | | `radius`, `width` | Wheel size (m). An unset radius comes from the wheel entity's model bounds, or else from its visual's. The ray is cast from the point across the tread where the ground is nearest, so a kerb under one shoulder holds the wheel up. | | `motorTorque` | Drive torque (N·m). The wheel drives with `motorTorque / radius` newtons, up to `forwardFriction` times the load it carries. | | `brakeTorque` | Brake torque (N·m), braking with up to `forwardFriction` times the load. It acts while `motorTorque` is 0, and so does `rollingResistance`. A braked car stands still on any slope its brakes' grip holds, gravity included. | | `steerAngle` | Steering angle (degrees), positive steers right. At light throttle the car turns on the radius its geometry gives, the wheelbase over `tan(steerAngle)`; driving the steered wheels hard tightens the turn and driving only the others widens it. | | `frictionSlip` | The tyre's combined grip limit (default 10.5): the drive, brake and side grip together reach at most the suspension load times this before the tyre slides sideways. Lower it (1 or less) for ice, a drift car or skids. | | `sideFrictionStiffness` | How strongly the tyre resists sliding sideways below its grip limit (default 1). | | `forwardFriction` | Longitudinal grip coefficient (default 1): drive and brake force are held to this times the load the wheel carries. | | `sidewaysFriction` | Multiplier on `sideFrictionStiffness`. | | `rollingResistance` | Drag on a freely rolling wheel, times the load it carries. | | `is2D` | The wheel applies no sideways grip. | | `visual` | The wheel's visual: an entity, or its id (see below). | The suspension ends at a **bump stop**: once the spring has compressed by `suspensionDistance` from its rest, the wheel holds the chassis off the ground there like a solid contact. It stops the chassis closing on the ground within the tick, so a landing faster than the springs can take, or a chassis heavier than its springs carry, ends on the stops with the tyre on the ground rather than in it, and the chassis is not thrown back up. A chassis that stands past its stops (put there by a script, say) is eased back out at no more than 1 m/s. The spring is applied as an impulse worked out from the state at the start of each tick. So that it cannot feed on itself, its rate is held, per tick, to what the chassis' mass **and its rotational inertia** integrate stably: a light chassis, or one whose pitch or roll inertia is small next to its wheelbase, gets less of a very stiff spring than authored. The damper is applied to the velocity the chassis ends the tick with, which is stable at any rate, so the authored `damperRate` always acts whole. A damper pulls a wheel down no harder than its spring pushes it. ## Runtime state Read-only, updated every physics tick, and readable through `Physics.getWheelState(wheelId)`: - `isGrounded`: the tyre touches the ground anywhere within the suspension's full travel, `suspensionDistance` below the mount. Past the spring's rest it carries no load. - `compression` (0 with the spring at or past its rest length, up to `suspensionDistance`, where the bump stop is). - `angularVelocity` (rad/s) and `rotation` (rad): the wheel's spin and its angle about the axle, which grow as it rolls forward by the distance rolled over the radius. - `skid` (m/s): how fast the tyre slides sideways while its grip is used up; 0 while the tyre holds. - `sideImpulse`, `forwardImpulse` (N·s): what the tyre applied to the chassis this tick. `suspensionForce` (N). `stopImpulse` (N·s): what the bump stop applied this tick, the part of a landing or a load the suspension could not take. - `center`, `hitPoint`, `hitNormal`: the wheel centre and the ground contact, in world space. A wheel with no ground within its travel hangs at full travel. A driver for the wheels is built in: `VehicleController` on the chassis reads the vehicle input map and writes `motorTorque`, `brakeTorque` and `steerAngle`. `Physics.vehicleState(chassisId)` reads the whole vehicle: the chassis' forward speed (m/s, negative in reverse), how many wheels are grounded, and each wheel's state. ## Seeing the suspension `capture { pass = "physics" }` and the edit-mode collider overlay draw every wheel over the colliders: its suspension ray over its whole reach, `suspensionDistance` and the radius below the mount (green on ground, red in the air, as `isGrounded` reads), the travel from the mount to the tyre's centre coloured by how much of it is taken up (blue extended, yellow half way, red bottomed out), the tyre's radius as a white circle across its steered axle, and where the tyre meets the ground (yellow) with the ground's normal (cyan). `Physics.debugLines({ layers = { "wheels" }, entities = { chassis } })` returns those lines with each wheel's `grounded` and `compression` (the same values as `isGrounded` and `compression` here), `travel`, `suspensionLength`, `restLength`, `radius`, `mount` and `contactPoint` / `contactNormal`. In edit mode the vehicle stands at rest, so its wheels draw where they are mounted and follow their authored fields as they change. ## Visuals The wheel entity's transform is the suspension mount, so it stays where it is authored. Put the tyre mesh on another entity under the chassis (a child of the wheel is the usual place) and name it in `visual`, as the entity or its id (a prefab or player clone of the car points it at its own copy of the tyre): every tick the wheel places that entity at the wheel centre, turns it by the steering angle and spins it about the axle, starting from the rotation it had when the wheel first posed it. Methods: `setMotor(torque)`, `setBrake(torque)`, `setSteer(angle)`. ```luau local car = entity.spawn("car") car.position = { 0, 1, 0 } car.component.add("Physics", { kind = "dynamic", mass = 600 }) car.component.add("BoxCollider", { half = { 0.9, 0.3, 2.0 } }) for _, at in { { -0.85, -1.3 }, { 0.85, -1.3 }, { -0.85, 1.3 }, { 0.85, 1.3 } } do local wheel = entity.spawn("wheel", { parent = car.id }) wheel.localPosition = { at[1], -0.3, at[2] } local tyre = entity.spawn("tyre", { parent = wheel.id }) tyre.component.add("Model", { model = "cylinder" }) tyre.localRotation = { 0, 0, 0.7071068, 0.7071068 } -- the cylinder's axis along X tyre.localScale = { 0.7, 0.25, 0.7 } wheel.component.add("WheelCollider", { suspensionDistance = 0.3, springRate = 20000, damperRate = 2000, visual = tyre, }) end ```
▲ 0↑ born
◇
component · born here
❒asset
# RenderFeature Runs a `.renderFeature` render pass for the lifetime of the entity carrying it. A scene's features are listed together, each with its settings, on `RenderFeatures`; this is the one-feature form. A feature already live on the same asset is adopted rather than stacked a second time. A render feature is authored content — a `.renderFeature` asset whose `init.luau` returns `{ setup?, render, teardown? }` and enqueues GPU passes every frame. This component is the declarative way to keep one running: on awake / enable it calls `renderer.feature.create(feature)`, and on disable / destroy it tears the feature down. A render feature is a **global** pass, so a single component on a single entity drives it for the whole scene. ## Fields | Field | Type | Default | Description | |-------|------|---------|-------------| | `feature` | `assetRef("renderFeature")` | none | The render feature asset to run. | ## Methods - `:getHandle()` — the live `RenderFeatureHandle` this component created (its `guid` is the feature's stable id), or `nil` when disabled / before awake. ## Persist This component is the persist-side mirror of a live `renderer.feature.create(…)` call. When you enable a render feature ad-hoc during play, the scene-authoring freeze (`tools.sceneAuthoring.acceptChanges`) promotes the feature's source into a `.renderFeature` asset and records it on one entity carrying this component — so loading the scene re-runs the exact same render pass. ## Example ```lua local e = entity.spawn("acrylic_pass", {}) e.component.add("@builtin::components.RenderFeature", { feature = asset.resolve("acrylic", "renderFeature"), }) ```
▲ 0↑ born
▣
module · born here
❒asset
# luau_introspect (module) Parses Luau/asset source TEXT into structured declarations. Pure string/pattern parsing (`string.find` / `string.match` / `string.gmatch`) over the literal source — no compiling, no VM spawn, no runtime state, no engine FFI. Given source text, it extracts what is written in the file. This is the shared parser every per-assetType `M.ref.inspect` hook uses to turn a component/module/tool's own source into the structured record `asset.inspect` returns. ## Exports - `docstrings(src) -> DocMap` — scans `--!desc` / `--!arg` / `--!return` / `--!example` comment runs and binds each run to the name of the declaration on the next non-comment line (the identifier after `function`, `public:`, `local function`, or `M.`). - `publicFields(src) -> { FieldEntry }` — parses `public = { name = Field.<kind>(default, mode), ... }` table-literal entries and module-scope `public.<name> = Field.<kind>(...)` assignments. - `events(src) -> { EventEntry }` — parses `events = { name = Event(payloadSchema?, syncMode?), ... }` table-literal entries. - `methods(src) -> { MethodEntry }` — parses `function public:<name>(...)` / `typed function public:<name>(...)` declarations (params + optional return type). - `lifecycleHooks(src, catalog) -> { string }` — top-level `function <name>(` declarations whose name is in the caller-supplied `catalog` array. - `moduleExports(src) -> { ExportEntry }` — parses `M.<name> = function` assignments, `function M.<name>(...)` declarations, and a trailing `return { foo = foo, ... }` export table. - `forSource(src, computeFn) -> any` — memoises `computeFn(src)` keyed on `src` itself in a module-local table. The same source text returns the cached result without re-running `computeFn`; different source text recomputes. Keyed on the source content (not an asset checksum) because composite/container assets (component folders, scene folders, etc.) all hash to the empty-string checksum — a checksum-keyed cache collapses every such asset's detail onto whichever one was inspected first. ## Usage ```luau local Introspect = require("modules.luau_introspect") local src = self:getInitScript() local detail = Introspect.forSource(src, function(s) return { fields = Introspect.publicFields(s), events = Introspect.events(s), methods = Introspect.methods(s), hooks = Introspect.lifecycleHooks(s, lifecycleCatalog), } end) ``` ## Notes - Every function is a pure string→table transform: same source text in, same structured table out, every time. No reads of live component state, no entity/world queries. - Structural scanning runs over a comment- and string-aware **mask** of the source (a same-length copy where comment bodies and string-literal contents are blanked to spaces, delimiters and newlines preserved). Pattern matches and brace/paren balancing run on the mask — so a brace, `Field.`, `Event(`, or a whole entry that appears inside a comment or a string literal never registers — while captured values are sliced from the original source at the same (aligned) indices. This makes the parser robust to apostrophes in comments, commented-out entries, string defaults containing `}` / `Field.` / `Event(`, and `[[ ]]` long strings. - `default` / `category` are raw trimmed source-text slices (with any trailing comment stripped), not evaluated values. - For a **ref constructor** (`Field.assetRef` / `dataRef` / `resource` / `componentRef`) the FIRST positional argument is the category / contract / componentType selector, captured under `category`; the SECOND argument is captured under `default`. - For every **other** Field kind the first argument is the `default` and `category` is nil. - A trailing `Sync` / `NoSync` identifier is captured as `sync`, tolerating a trailing comma or a trailing comment after the last argument. - Field/Event entries whose key is a computed or indirect expression (`[expr] = Field.number(...)`) are omitted — only literal identifier keys are recognised. - `methods` captures a `...` variadic parameter (as `name = "..."`), with its `: <type>` annotation when present. - A `--!desc` written above a field/event declaration (`<name> = Field.<kind>(...)` / `<name> = Event(...)`) or a `public.<name> =` assignment binds to that name in `docstrings`, and is folded into the corresponding `publicFields` / `events` entry's `desc`. - `lifecycleHooks` never hardcodes the lifecycle-callback catalog; the caller passes the list of names to match against. - `forSource`'s memo is bounded (oldest-key eviction) so a long session touching many distinct asset versions can't grow it without limit.
▲ 0↑ born
·
toolbox · born here
❒asset
# ik Look at a rig's bones and at what an IK solve is actually doing. Debugging IK means answering three questions, and none of them can be answered from a screenshot: what are this rig's bones called, where are they right now, and is the chain reaching its target or merely near it. `bones` lists a body's bones with their world positions and parents — the names you need before naming a chain, and the positions you measure against when checking a solve from outside the component. Rigs rarely use the word you would guess: the standard humanoid here calls its feet `Ankle_L` and `Ankle_R`, with no bone containing "foot" anywhere. `reach` reports how far a limb can extend and how far away a point is, both in the same units. A target beyond a limb's reach has no solution — the chain extends straight toward it and stops, which is correct and looks exactly like a broken solver. This is the first thing to check when a solve looks inert. `state` gathers every IK component on an entity and reports what each one is doing, so "is anything solving at all" is one call rather than five.
▲ 0↑ born
▣
module · born here
❒asset
# assetType runtime The machinery that reads a `<typename>.assetType/behavior.luau` and turns its declarations into live behaviour. A `behavior.luau` is a table of declarations — `ref` methods, `modules` shared code, an `events` schema, a `namePattern`, `refShapes`, and the `onCreate` / `onRegister` / `onChange` / `onDelete` / `validate` / `settingsDefaults` / `settingsCorrections` hooks. None of it runs itself. This is the code that runs it, and it lives here because reading a type's declarations is the **assetType type's own behaviour** — the same way `material.assetType` owns what a `.material` does. ## Sub-modules | Module | Reads | Responsibility | |---|---|---| | `ref.module` | `ref`, `modules` | Builds every `AssetRef` envelope, attaches the shared metatable, and dispatches per-type methods and shared modules onto it. Also owns edit-mode write-behind persistence. | | `create.module` | `onCreate`, `namePattern`, `validate` | Backs `asset.create`: resolves the type, checks the name against its pattern, runs `onCreate` (or clones `template/`), and writes the instance. `validate.module` beside it checks a creation `opts` table against the type's `createSchema`. Before running `onCreate`, resolves the type's settings chain (below) and hands the result to the hook as `opts.settings`. | | `settings.module` | `settings`, `settingsDefaults`, `settingsCorrections` | A type's declared settings schema and where its values live (`type.yaml`'s `settings:` block). `Settings.resolveForCreate` layers the schema's own defaults, the type's own `settingsDefaults(name, opts, asked)` hook (a DEFAULT the schema can't express — a name-derived convention, a fact read off a payload the caller handed it, a value following a field `asked` shows a higher layer naming; any later layer overrides it), the Preset Manager, an importer's settings, the caller's `settings` table, then the type's own `settingsCorrections(values, name, opts)` hook (a CROSS-FIELD CONSTRAINT the schema can't express, applied last over the fully merged values). A correction over a default/preset/importer value lands silently; a correction that would override what the caller's own `settings` table explicitly asked for refuses the creation instead, naming the field and why. Neither hook is ever handed a table to mutate; each returns what it wants changed (`settingsCorrections` may also return a `{ [field]: reason }` table for that refusal message). `Settings.cloneValues` deep-copies a values table — `create.module` hands `onCreate` a copy, never the table it stamps. `Settings.valuesOf` / `writeValues` read and write an asset's own values file. | | `typeYaml.module` | `type.yaml` | The folder a type lives in and its parsed `type.yaml`, decoded once per content version of the file. `settings.module` and `inspector.module` read their blocks through it. `about` / `aboutAt` read a type's `about.yaml` (`intent`, `function`, `usage`), leaving out any key that is empty or still starts with `[[describe]]`; `aboutRows` and `aboutMarkdown` lay the texts out for the Inspector and the `types/<name>` guide. | | `inspector.module` | `type.yaml`'s `inspector:` block | Which inspector a type declares for its assets (`inspector: { type: ... }` or `{ instance: ... }`), and the declaration that file returns — the sections it adds, the generic ones it replaces, and their order — or the problem that kept it from loading. | | `onRegister.module` | `onRegister` | Fires an instance's once-only registration hook — on arrival, on world load, and for `@builtin` library instances. | | `changeDispatch.module` | `onChange`, `onDelete`, `refShapes` | Routes a VFS source write or a folder delete to the enclosing typed asset's type hooks. | | `refShapes.module` | `refShapes`, `ref` | Publishes what every `AssetRef<category>` answers to, so a member read on an asset-typed value is checked against that category's real surface. | | `events.module` | `events` | The per-asset event runtime behind `ref.events`. | | `instantiable.module` | `ref.instantiate` | The instantiation contract an asset type opts into: `root` stands the root entity, `place` applies the base placement opts to one the type adopted, and `result` shapes the `(root, idMap)` every `instantiate` returns. Also registers the `sceneInstantiable` field-constraint validator. | | `declaration.module` | `type.yaml` | The type's decoded declaration, read once and held until a write inside an `.assetType` folder; what every question about a type as a whole reads. | | `previewRecipe.module` | `type.yaml` `preview:` | How an instance's `preview.png` is produced: which of the four recipes (`instantiate`, `image`, `surface`, `custom`) the type declares, and what that recipe needs. | ## Reaching it ```luau local rt = require("@builtin::assetTypes.assetType.shared") rt.ref.loadTypeBehavior("material") ``` Fields resolve lazily. The prelude requires the sub-modules directly, in dependency order (`ref`, then `changeDispatch`, `onRegister.install()`, `refShapes.install()`) — a table that required all six on load would take that ordering away from it.
▲ 0↑ born
backing path · tools/entityOps.toolbox

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.