❒
bundle · drop-in viewer
What it does
No description provided yet.
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
components
["SkinnedModel"]humanoidfalseimport{"at":98.8003531,"importer":"5210d113-558a-4637-ac09-94e9c57b41cd","iteration":1,"rowPath":"/zero/source/libs/@builtin/models/xr/generic_hand_left.glb","source":{"guid":"8f5200fb-684a-4358-a743-1ebe16f67401","path":"/zero/source/libs/@builtin/models/xr/generic_hand_left.bundle/generic_hand_left.glb"}}source_checksumbc67783144944ea1cda54d9247885825ea5fb9d4651469fe7d00be517a5c2b87|032b8da2d9e82989fe61e37b97f3856344c3cce1bef0f12c9f980bd3d50e41bcSub-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.
76+items
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
# Bundle (asset type)
A bundle is a composable, spawnable entity assembly— a saved entity
hierarchy plus its components, sub-assets (meshes, textures,
animations, materials, sub-bundles), and any nested data, packaged
into a single addressable identity. Bundles are the engine's "prefab"
primitive.
There are **two ways** to bring a bundle into a scene — and the
difference matters enormously for scene saving and live editing. See
"How to spawn" below before you write anything.
If you want one snapshot of a whole world / level, use a `.scene`.
## Where it lives
- Source: `/zero/source/.../<Name>.bundle/`
- Identity: `<Name>` (the `.bundle` suffix strips for use in
identities, but the on-disk folder keeps the suffix).
- Folder shape (container type — open child set):
- `entity_template` — JSON of the entity hierarchy + components.
**Required.**
- `.metadata`: optional.
- Anything else: meshes / textures / materials / animations /
sub-bundles. Validator allows arbitrary additional files
(`allow_unlisted: true`).
## How to create one
Two paths, depending on whether you have an in-scene hierarchy or are
authoring from scratch:
```luau
-- From an existing in-scene entity: composes the given entity tree into a bundle.
local bundle = asset.create("bundle", "<Name>", { entity = entityRef })
-- Or scaffold an empty bundle, then update it from an entity:
local bundleRef = asset.create("bundle", "<Name>")
bundleRef:update(entityId)
```
## How to spawn
### 1. `Asset` component — live-linked (the canonical way)
**This is the primary way to put a bundle into a scene that survives
saving as an asset reference.** Spawn an empty entity, attach the
`Asset` component, point it at the bundle:
```luau
local car = entity.spawn("car_1")
car.component.add("Asset", { source = "<bundleIdentity>" })
```
What you get:
- The bundle's hierarchy spawns as **temporary** children under the
owning entity. They're real, addressable runtime entities, but
they are NOT serialized into `scene.json` on save, only the Asset component and its entity is saved.
- **Live editing of the bundle is supported.** When you edit
anything inside the `.bundle/` (entity_template, meshes,
materials, sub-bundles, components), every entity in the world
carrying that `Asset` component automatically tears down its
spawned children and rebuilds them from the new template — same
frame, no restart, no manual reload. The `onAssetReload` hook in
`Asset.component/init.luau` drives this.
- **Per-instance overrides survive across reloads.** Move a spawned
child, rename it, add/remove components on it, edit a component's
public state — the changes land in the Asset's `public.diff` (a
sparse delta keyed by the child's `original_id`) at save time and
are replayed by `bundleRef:instantiate()` on next load. The bundle stays
a single source of truth; per-spot tweaks ride on top.
- **Stable child ids across reloads.** `public.idMap` records the
`original_id → runtimeId` assignment on first spawn; on every
subsequent spawn the same mapping is reused so cross-entity
references (e.g. `SkinnedModel.skeletonRoot = "ent_xxx"`) stay
valid.
### 2. `bundleRef:instantiate` — baked, no live link
Resolve the bundle and instantiate it onto an entity — this spawns the
children **without** the live-link plumbing the `Asset` component
provides:
```luau
local car = entity.spawn("car_1")
asset.resolve("<Name>", "bundle"):instantiate(car)
```
What you get — and the trade-offs:
- The children spawn as **regular** (non-temporary) entities. On the
next `layers.active:save()`, the entire spawned hierarchy is serialized into
`scene.json` verbatim.
- **No live link to the bundle.** Editing the `.bundle/` after the
fact has no effect on already-spawned hierarchies. The bundle was
effectively consumed at spawn time.
- Scene-save / restore round-trip works the same as for any other
hand-authored entity tree.
Use this path when you want a one-shot expansion of the bundle into
permanent scene state (e.g. you're populating a scene's static
layout from a parametric bundle and don't want a live link).
You can convert a live-linked instance into a baked one explicitly
via `Asset:bakeIntoScene()` — this drops the `Asset` component and
promotes the temporary children to permanent scene entities.
## How it operates
1. **Registration.** Writing into a `.bundle/` folder indexes the
asset with the container-category restore plumbing.
2. **Inner refs.** Component fields that reference assets inside the
bundle use the `__ref` shape (`{ "__ref": "<guid>", "name": "...",
"type": "..." }`). The asset registry resolves them at spawn
time.
3. **Identity.** The `.bundle` suffix is stripped, so a bundle
resolves as `<Name>` (`asset.resolve("<Name>", "bundle")`); the
type argument tells it apart from any `<Name>` of another type.
4. **Hot reload.** Saving `entity_template` (or any file in the
bundle directory) fires the engine's per-asset content-changed
hook, which calls `onAssetReload("source")` on every entity
whose `Asset` component points at the bundle. Each carrier
despawns its children and re-instantiates against the new
template — typically next frame.
## Discovery
- `asset.list("bundle")` — every registered bundle.
- `asset.inspect("<name>")` — sub-asset catalog, source, this type
README.
- `ls /zero/source/<Name>.bundle` lists the files the folder holds.
## Parts and slots
A bundle can be an object assembled from parts, and a part that fills one. A
child carrying a `Slot` component is a named place another part fills (its
`kind`, the `tags` a part must carry, a `default` part, and where the part
attaches: that child's transform). A bundle whose root carries a `Part`
component is a part, and its `fits` names the slot kinds it fits. An
`Assembly` on the root holds what fills each slot by path (`wheel_fl`,
`engine/turbo`, a part's own slots nesting under the slot it fills), and
resolves, swaps and explains them at runtime:
```luau
local car = entity.spawn("car")
car.component.add("Asset", { source = "Buggy" }) -- a bundle with slots and an Assembly at its root
local asm = car.component.get("Assembly")
asm:configure({ wheel_fl = "SportWheel", engine = { part = "V8", turbo = "Turbo" } })
asm:fill("wheel_fl", "OffroadWheel") -- rebuilds that wheel alone
print(asm:fits("wheel_fl", "V8")) -- false, and why
```
The configuration is saved with the scene and synced; the parts are runtime
children rebuilt from it. `guides { path: "topics/assemblies" }` covers the
whole family.
## Authoring conventions
- Prefer `bundle:update()` or `asset.create("bundle", "<name>", { entity = ref })` over hand-authoring `entity_template`. The
in-scene composer wires `__ref` shapes correctly and pulls in every
transitively-needed asset.
- Keep bundles focused — one prefab per bundle. If you have a "vehicle
+ driver + sound" assembly, split into three bundles and reference
them from a scene rather than packing all three into one bundle.
- Name child entities meaningfully — bundle interiors are
inspectable, and stable IDs let other tooling (`entity_template`
edits, animation hooks) find them.
- Sub-bundles compose: a bundle's `entity_template` can `__ref` a
child `.bundle/`. The asset registry resolves them transitively.
## Common pitfalls
- **Confusing the two spawn paths.** `Asset` component = live link,
reference-only scene save. `bundleRef:instantiate()` directly = baked,
full hierarchy in `scene.json`.
- **Copying JSON between bundles.** A copied `entity_template` does not
bring the assets it references along with it; re-compose from a
live scene rather than copy-pasting JSON.
- **Live editing only fires for live-linked carriers.** Already-baked
entities don't update when you edit the source bundle.
## Related types
- `Asset.component` — the canonical mount point for a live-linked
bundle. See its README for the `idMap` / `diff` machinery in
detail.
- `.scene` — owns a whole world's worth of entities; spawns bundles
into itself via `Asset` components on per-spot entities.
## In the Inspector
Selecting a bundle opens its **Contents** section, from the bundle's inspect detail: the template's node count, its root, the component types attached across it, a table of each renderable node's mesh and material, and a link to each of those meshes and materials.
▲ 0↑ born
# 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).
**Tint is synced.** Every `tint*` field replicates, so `:setTint` is the colour every participant sees. A colour that changes every frame on many characters (a damage flash, a team colour, a heat map) belongs in an instance-data lane instead: wear `@builtin::materials.instance_color` (or `instance_color_unlit`) and write `renderer.instanceData.set(id, 2, r, g, b, coverage)`, which replicates nothing. `guides { path = "topics/rendering-instance-data" }` has the lane layout.
## 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
# Material (asset type)
A material is a named, configured instance of a `.shader`. The shader
defines the surface look (lighting model, available property fields,
texture slots); the material picks values for those fields and a
shader to bind to. Renderable entities reference materials, not
shaders, so the same geometry can wear different materials and the
same material can be reused across many entities.
Entities sharing one material can still differ in a value: every drawn
object carries four `vec4` lanes of its own that a surface shader reads
as `input.shader_data[lane]` and `renderer.instanceData.set` writes. Use
that for a dissolve at its own progress per subject, a hit flash at its
own age, a fill level per instance — a material per entity is for when
the LOOK differs, not when a number does.
> New to how a material relates to its shader, its textures, and the live GPU
> material behind it? Read `core/resource-model` first. It also covers the
> difference between an authored `.material` asset (this type) and a runtime GPU
> material made with `renderer.material.create`.
## When to use one
- You want a named surface configuration an entity wears by referencing it
from its `Model` component (`material = "@.../<Name>"`).
- You want runtime property tweaks (`asset.resolve(ref):setProperty(...)`) to
affect every entity using the material at once.
- You want to publish a curated look (`Gold`, `RedGlow`,
`BrickWall`) other authors can reuse without re-typing shader
bindings.
If you need a brand-new lighting model or a custom visual effect, you
need a `.shader` first; the material wraps it.
## Where it lives
- Source: `/zero/source/.../<Name>.material/`
- Identity: `<Name>` (the `.material` suffix strips).
- Folder shape:
- `mat.yaml` — canonical material spec (shader + property values).
**Required.**
- `README.md` / `.metadata` — optional prose + tags, indexed when an
author writes real ones.
- `textures/` — optional folder for embedded textures. Validator
allows the folder; its interior is unrestricted.
- `shaders/` — optional folder for a co-located custom shader.
Same rule.
## How to create one
```luau
asset.create("material", "<Name>")
-- Creates: /zero/source/<Name>.material/
-- mat.yaml (shader binding + property scaffold)
-- `folder` places it in a subfolder of /source instead of the root:
asset.create("material", "<Name>", { folder = "materials" })
```
Pass the shader and initial values through `asset.create`, then tune with
`setProperty` and keep what you tuned with `saveDefinition`:
```luau
local mat = asset.create("material", "<Name>", { shader = "@builtin::shaders.pbr" })
mat:setProperty("base_color", { 0.8, 0.6, 0.1, 1 })
mat:setProperty("roughness", 0.3)
mat:saveDefinition() -- writes the tuned values into mat.yaml
```
This mints the `.material` asset and returns a handle you tune with
`setProperty`. A set is a runtime change: it reaches the GPU material and every
entity wearing it, and `saveDefinition` is the call that writes it into
`mat.yaml`, so a property driven every frame costs no file write and leaves the
authored file as its author wrote it. A purely runtime `asset.create` lives only
for the session.
For an ephemeral, runtime-only GPU material that is not an asset at all, use
`renderer.material.create` instead (see `core/resource-model`).
## How it operates
1. **Property reflection.** The bound shader's uniform struct fields
become the material's configurable properties — no manual property
declarations needed.
2. **Property values.** `mat.yaml` groups values by type, each block
keyed by the shader's field names: `properties:` holds scalars and
colors/vectors, `ints:` holds integer-typed fields, `bools:` holds
boolean-typed fields, and `textures:` holds texture slots. Omitted
fields take the shader's default. A scalar is a bare number
(`roughness: 0.5`); a color or vector is an array
(`base_color: [1, 1, 1, 1]`). `shader:` is the shader's identity
string. This is the shape every material the engine writes uses. A
`saveDefinition` writes the changed values into the file where they
already stand and leaves the rest of it — its comments, the order of
its keys and blocks, the spelling of its literals, untouched.
3. **Render state.** `render:` holds how the mesh is drawn rather than
what the shader reads: `blend` (the equation the fragment composites
with, also spelled `type`), `cull`, `depth_write`, `depth_compare`,
`topology` and `queue` (the draw order, a whole number: `2000` opaque,
`3000` transparent, `4000` overlay). The `.shader` type README's "The
render block" section lists every value each key takes, and
`asset.create`'s `render` argument takes the same keys and values.
4. **Application.** An entity wears a material by referencing it from its
`Model` component (`material = "@.../<Name>"`). Materials affect every entity
that references them; runtime `asset.resolve(ref):setProperty(...)` changes
propagate to all of them immediately, and last until the next mode change
unless `saveDefinition` writes them into `mat.yaml`.
5. **Texture binding.** Texture slots accept `color:r,g,b,a` for
solid colors, `default:white` / `default:black` / `default:normal` for the
engine's own fallback texels, `@builtin::textures.<name>` for builtins,
`path/to/image.png` for file textures, or a render-target handle
for camera renders. The GPU texture cache reads the `color:` and
`default:` forms from the string itself, so a slot written in one names no
asset and carries nothing with the material when it travels; every other
form names a texture asset the slot depends on.
A texture in `base_color_texture` is multiplied by `base_color`: every
built-in surface shader declaring both draws the texture's colour times
`base_color`, channel by channel. Under the default white `base_color` the
texture shows as itself, and a coloured `base_color` tints it. The two
read a colour differently: `base_color` is linear, and the slot reads its
texels as sRGB-encoded, a `color:` value's included, the way it reads an
image. A colour stated in both places is therefore multiplied by a darker
copy of itself: `color:0.62,0.14,0.05,1` in the slot decodes to
`0.34, 0.017, 0.0039`, so beside `base_color` `0.62, 0.14, 0.05` the
surface draws `0.21, 0.0024, 0.0002`, far darker than either. State a
solid colour once, as `base_color`, and leave the slot at its default
white.
6. **Hot reload.** Editing `mat.yaml` repacks the GPU uniform buffer
on the next frame; visible immediately.
**Properties that follow a named value.** `values:` binds a property to a
named value of the entity wearing the material (`values.set(entity, name,
v)`, the `topics/values` guide): `property: valueName`, or
`property: { value: "name", scale: s, offset: o }`, and the property draws
`value * scale + offset` on each entity holding the value, read on the draw.
A boolean reads as 1 or 0, and a number fills every channel of a colour or
vector property. The wearer's own value is read, else its nearest
ancestor's, so a lamp mesh under a car lights from the car's `brake`; a
wearer holding none draws the material's own value. A scalar, vector,
colour or bool property binds (at most 32 per material); a texture slot
does not. `asset.create("material", name, { values = { ... } })` and
`renderer.material.create({ ..., values = { ... } }, key)` take the same
map.
```yaml
values:
emissive_intensity: { value: "brake", scale: 3 }
```
7. **Inspector.** `type.yaml` names `inspector: { type: "inspector.luau" }`,
and `inspector.luau` declares the material's sections beside the generic
ones: `shader` (the bound shader, swappable), `properties` (each shader
field as its typed control) and `textures` (each texture slot as an asset
chip). They read the material through `mat:getShaderRef()` (the bound
shader's ref, `~` expanded) and `mat:getTextures()` (slot name →
`{ ref, name }`: the bound texture's guid and the id the material names
it by). An edit there is an authoring write: `mat:writeProperty(name,
value)` and `mat:writeTexture(slot, texture)` write the value into
`mat.yaml` (and drop the session's own value for it, so the material
reads what the file holds), and `setShader` rewrites the file; inside an
undo operation each file write is recorded, so undo puts `mat.yaml` back.
`setProperty` / `setTexture` stay runtime writes that last until the next
mode change.
## Discovery
- `asset.list("material")` — every registered material.
- `asset.inspect("<name>")` — shader binding, property values,
source path, this type README.
- `ls /zero/source/<Name>.material` lists the files the folder holds.
- `asset.resolve("<name>"):getProperties()` — the property fields exposed by
the bound shader, each at the value it currently holds: what `mat.yaml`
authored until something writes to the material, and the written value from
then on, whichever call wrote it. That is the value the surface is drawn
with. Read them back rather than guessing names; which fields exist comes
from the shader.
- `asset.resolve("<name>"):getDefinition()` — the authored `mat.yaml` bytes,
the value the material rests at.
- `asset.resolve("<name>"):getShader()` — the shader the material binds.
## Authoring conventions
- Use lowercase, descriptive names (`brushed_steel`, `red_glow`,
`tarmac_wet`). Avoid shader-name suffixes in the material name —
the binding lives in `mat.yaml::shader`.
- Drive every property the shader exposes; defaults are a fallback,
not a contract for which fields exist.
- Embed textures alongside the material (`textures/` subfolder) when
they're used only by this material; reference shared textures from
`@builtin::textures.*` or a library.
## Common pitfalls
- **Property names must match the shader's uniform struct exactly**
(case-sensitive). Misnamed properties silently get the shader's
default.
- **`base_color_texture` multiplies `base_color`.** A `color:` texture holding
the same value as `base_color` multiplies the colour by its own
sRGB-decoded value, so the surface renders far darker than the value
written. Write a solid colour as `base_color` alone.
- **Broken shaders break the material.** A material whose shader has a
parse error renders as the magenta checkerboard. Inspect the shader
first if the material looks wrong.
- **`setProperty` targets the material, not the entity.** Changes affect
every entity that references the material.
- **`setProperty` is a runtime change.** It reaches the GPU, not the file:
call `saveDefinition` to write the current values into `mat.yaml`. For a
per-entity override that no other entity sees, give the entity its own copy
with `Model:applySessionMaterial(renderer.material.create(...))`.
- **Renaming the folder changes the identity.** A `Model` component or
other reference to the material is recorded against the material
itself, so renaming or moving it does not break them.
## Related types
- `.shader` — defines what properties exist; required before a
material can bind to it.
- `.preset` — captures a component configuration; conceptually
similar to a material, but for components instead of shaders.
▲ 0↑ born
# mesh
A mesh is renderable geometry — vertices and triangle indices, and, for a skinned
mesh, the per-vertex skinning data that binds it to a skeleton. A component that
draws geometry references a mesh, and the renderer binds the mesh's GPU resource
for that entity.
> New to how a mesh exists as an asset, as CPU-side vertex data, and as a live
> GPU resource (and how `renderer.mesh.*` moves between them)? Read
> `core/resource-model` first.
Geometry is stored in `data.zmsh`; the GPU resource is keyed by
the asset's guid.
## Create
`asset.create("mesh", name, opts)` writes the geometry and registers the mesh.
`opts` is raw geometry — flat arrays; `positions` and `indices` are required,
`normals` and `uvs` optional:
```luau
asset.create("mesh", "tri", {
positions = { 0,0,0, 1,0,0, 0,1,0 }, -- [x,y,z, ...] (required)
indices = { 0, 1, 2 }, -- [i0,i1,i2, ...] (required, 0-based)
normals = { 0,0,1, 0,0,1, 0,0,1 }, -- [x,y,z, ...] (optional)
uvs = { 0,0, 1,0, 0,1 }, -- [u,v, ...] (optional)
})
```
The full authorable geometry, all flat arrays unless noted:
| Field | Shape | Notes |
|---|---|---|
| `positions` | `[x,y,z, ...]` | required |
| `indices` | `[i0,i1,i2, ...]` | required; 0-based, counter-clockwise for the front face (see Conventions) |
| `normals` | `[x,y,z, ...]` | optional; the surface's outward direction |
| `uvs` | `[u,v, ...]` | texture UVs, optional; `(0,0)` is the image's top-left |
| `colors` | `[r,g,b,a, ...]` | optional |
| `tangents` | `[x,y,z,w, ...]` | optional (`w` = handedness) |
| `uvs1` | `[u,v, ...]` | the lightmap UV set, optional |
| `unwrapUvs` | boolean | generate `uvs1` when it is absent |
| `skinning` | `{ joints, weights }` | per-vertex skin binding (four each per vertex) |
| `skins` | array of skin tables | the skeleton (see `renderer.mesh.decode` for the shape) |
| `morphTargets` | array of `{ name?, positions, normals? }` | the shapes the mesh blends towards, each a per-vertex displacement |
`encode` / `decode` round-trip every one of these, so a **skinned** mesh can be
built in engine, not only imported. Pass `unwrapUvs = true` to generate a lightmap
UV set at creation time.
## Conventions
Geometry is read against these whether it arrived through `asset.create`,
`renderer.mesh.create`, or an importer.
| Convention | How the engine reads the geometry |
|---|---|
| Index base | `indices` count vertices from **0** — index `0` is the first `positions` triple. The Luau array carrying them is 1-based like any other, so `indices[1]` is the first triangle's first index, and vertex `v` has its x at `positions[v * 3 + 1]`. |
| Triangle winding | A triangle's **front** face is the one whose three vertices turn **counter-clockwise** as the viewer sees them. Put another way, `cross(v1 - v0, v2 - v0)` points out of the front face. |
| Back faces | A material culls its back faces by default, so a triangle turned away from the camera draws nothing where it stands and whatever is behind it shows through. |
| Normals | `normals` give the surface its outward direction and shade the face. Which side of a triangle draws comes from the index order alone, so a normal pointing away from the side the winding draws leaves that face standing and shades it as if the light were behind it. |
| UVs | `(0, 0)` samples the first texel of an image — its top-left corner — and `v` grows downward through it. |
| Axes | Model space carries the world's basis: `+X` right, `+Y` up, and `-Z` the direction an entity's `transform.forward` points. |
Winding is what the author states: the inward faces of a room and a sky shell
seen from inside are geometry meant to be wound the other way, and `asset.create`
writes the index order it is handed. Two levers turn a face around:
```luau
indices = { 0, 2, 1 } -- the same three vertices, reversed
asset.create("material", "two_sided", { -- or keep the order and draw both sides
shader = "@builtin::shaders.pbr", render = { cull = "none" },
})
```
`cull = "front"` draws the far side by itself, which is what an inverted-hull
outline is built on, and `"back"` is the default. `.shader` documents the whole
`render` block a material's `mat.yaml` carries.
A mesh states which way its own triangles face, so the question is answerable
off the geometry — every triangle whose cross product agrees with the normal it
carries is wound to draw the side that normal points at:
```luau
local cpu = meshRef:load()
local g = cpu:geometry()
local function facesOutward(t: number): boolean -- t counts triangles from 0
local i = { g.indices[t * 3 + 1], g.indices[t * 3 + 2], g.indices[t * 3 + 3] }
local function p(k: number, c: number): number return g.positions[i[k] * 3 + c] end
local ax, ay, az = p(2,1) - p(1,1), p(2,2) - p(1,2), p(2,3) - p(1,3)
local bx, by, bz = p(3,1) - p(1,1), p(3,2) - p(1,2), p(3,3) - p(1,3)
local cx, cy, cz = ay*bz - az*by, az*bx - ax*bz, ax*by - ay*bx
local n = i[1] * 3
return cx * g.normals[n+1] + cy * g.normals[n+2] + cz * g.normals[n+3] > 0
end
local inward = 0
for t = 0, #g.indices // 3 - 1 do
if not facesOutward(t) then inward += 1 end
end
cpu:unload()
```
## Built-in primitive meshes
The engine ships blockout geometry every world references by bare name
(`{ model = "cube" }`). Each one is centred on its local origin, so an entity's
`scale` reads directly as the size the mesh covers in the world:
| Mesh | Extent (x, y, z) | Local AABB |
|---|---|---|
| `cube` | 1 x 1 x 1 | `-0.5, -0.5, -0.5` .. `0.5, 0.5, 0.5` |
| `sphere` | 1 x 1 x 1 | `-0.5, -0.5, -0.5` .. `0.5, 0.5, 0.5` |
| `cylinder` | 1 x 1 x 1 | `-0.5, -0.5, -0.5` .. `0.5, 0.5, 0.5` |
| `cone` | 1 x 1 x 1 | `-0.5, -0.5, -0.5` .. `0.5, 0.5, 0.5` |
| `octahedron` | 1 x 1 x 1 | `-0.5, -0.5, -0.5` .. `0.5, 0.5, 0.5` |
| `rounded_box` | 1 x 1 x 1 | `-0.5, -0.5, -0.5` .. `0.5, 0.5, 0.5` |
| `capsule` | 0.5 x 1 x 0.5 | `-0.25, -0.5, -0.25` .. `0.25, 0.5, 0.25` |
| `torus` | 1 x 0.3 x 1 | `-0.5, -0.15, -0.5` .. `0.5, 0.15, 0.5` |
| `plane` | 1 x 0 x 1 | `-0.5, 0, -0.5` .. `0.5, 0, 0.5` |
| `ground_quad` | 1 x 0.1 x 1 | `-0.5, -0.05, -0.5` .. `0.5, 0.05, 0.5` |
| `ground_hex` | 1.732 x 0.1 x 2 | `-0.866, -0.05, -1` .. `0.866, 0.05, 1` |
| `shader_ball` | 0.933 x 1 x 0.933 | `-0.467, -0.5, -0.467` .. `0.467, 0.5, 0.467` |
`plane` is a flat quad in the XZ plane, so it has no vertical extent at all and
scaling it on Y moves nothing. `meshRef:load():getBounds()` reads the same
numbers back off any mesh, built-in or authored.
## Put a mesh on an entity
A component draws a mesh through a `resource("mesh")` field, which accepts the mesh
**by name, by ref, or as a `MeshHandle`** — the component resolves whatever you give
it. The builtin `Model` and `SkinnedModel` both expose a `model` field:
```luau
entity.find("box").component.add("Model", { model = "tri" }) -- by name
entity.find("box").component.add("Model", { model = asset.ref("tri") }) -- by ref
entity.find("box").component.add("Model", { model = meshHandle }) -- a MeshHandle (below)
```
## Runtime mesh — no asset
Build geometry straight onto the GPU and render it without creating an asset. The
`resource("mesh")` field takes the returned `MeshHandle` directly:
```luau
local h = renderer.mesh.create({ positions = {0,0,0, 1,0,0, 0,1,0}, indices = {0,1,2} })
entity.find("box").component.add("Model", { model = h })
```
Its geometry reads back off the handle. `renderer.mesh.geometry(mesh)` returns the
same flat-array `MeshGeometry` `create` takes, carrying every stream the mesh has —
so a runtime mesh can be inspected, edited, and rebuilt without ever becoming an
asset:
```luau
local geom = renderer.mesh.geometry(h) -- { positions, indices, tangents?, ... }
geom.positions[1] = 0.5
local h2 = renderer.mesh.create(geom)
```
An optional stream is present only when the mesh carries one, so `geom.tangents`
answers whether the mesh has a tangent basis and `geom.uvs1` whether it has a
second UV set.
## Read geometry — `meshRef:load()`
`:load()` decodes `data.zmsh` into the CPU store and returns a CPU handle carrying
`vertexCount` / `indexCount`:
| Method | Returns |
|---|---|
| `cpu:getVertices()` | array of `{ pos = {x,y,z}, normal = {x,y,z}, uv = {u,v} }`, one per vertex |
| `cpu:getTriangles()` | array of `{ v1, v2, v3 }`, each a full vertex (the shape above) |
| `cpu:getBounds()` | `{ min = {x,y,z}, max = {x,y,z} }` — the local-space AABB |
| `cpu:geometry()` | the complete `MeshGeometry` — flat arrays, every stream the mesh carries |
| `cpu:encode()` | the `data.zmsh` bytes |
| `cpu:unload()` | drop this guid's CPU copy |
```luau
local cpu = meshRef:load()
local v = cpu:getVertices()[1] -- { pos = {x,y,z}, normal = {x,y,z}, uv = {u,v} }
local aabb = cpu:getBounds()
cpu:unload()
```
By default the CPU copy is transient (decode → read → `unload`). Set `keepCpu` to
keep it resident — see Settings.
## Write geometry back — `meshRef.primary`
The inverse of reading: encode a `MeshGeometry` and write it to the asset's
primary content file. The assetType picks the write up and re-uploads the live
GPU mesh, so the change shows without a reload:
```luau
local geom = renderer.mesh.geometry(meshRef)
geom.tangents = computeTangents(geom) -- add a stream it did not carry
vfs.write(meshRef.primary, renderer.mesh.encode(geom), { overwrite = true })
```
`meshRef.primary` is the asset's `data.zmsh` path. This is the route for a
stream with no setting of its own; `keepCpu` + `meshRef:setVertices` moves
positions in place, and `lightmapUvs` regenerates the second UV set (both
below).
## GPU handle — `meshRef:handle()`
`:handle()` uploads the geometry (Disk → CPU → GPU) and returns a `MeshHandle`,
cached on the asset so every consumer shares ONE GPU entry. A component does this
for you when you pass it the mesh; call it directly only for the runtime/low-level
path.
## Settings — `meshRef:settings()` / `meshRef:setSettings(patch)`
A mesh's settings are declared in `settings.luau` and live in `settings.yaml`.
`:settings()` returns them with every declared default filled in.
`:setSettings(patch)` writes any subset — sibling settings are preserved, and
a patch that fails the schema is refused, naming the field and why.
| Setting | Type | Default | Meaning |
|---|---|---|---|
| `keepCpu` | boolean | `false` | Keep the CPU geometry copy resident — for an in-use mesh — so geometry can be read repeatedly without re-loading. |
| `lightmapUvs` | string | `"keep"` | The mesh's second (lightmap) UV set. `"keep"` leaves the stored geometry's set as-is; `"generate"` unwraps a fresh non-overlapping set; `"none"` strips it. See below. |
| `clusterLod` | boolean | `true` | How deep the cluster set is built. Every static mesh draws through clusters, from the frame it materialises (its `lod0` set) until its bake lands. `true` (default) builds the LOD hierarchy; `false` builds one level, drawn at full detail. |
```luau
local s = meshRef:settings() -- { keepCpu = false }
meshRef:setSettings({ keepCpu = true })
```
`keepCpu` is an active lifecycle control, and only ever holds geometry for a mesh
that is **in use** (GPU-resident):
- Setting it **on** while the mesh is in use loads the CPU copy immediately.
- Setting it **on** for a mesh that is **not** in use loads nothing — the copy is
retained the next time the mesh is materialised, so an unused mesh is never
pinned in memory.
- Setting it **off** drops a resident CPU copy (the renderer keeps drawing from
the GPU).
`clusterLod` is the same kind of lifecycle control, for the depth of the
cluster set rather than the CPU copy:
- Setting it **off** on a mesh in use rebakes it as one level of clusters
(`single`), drawn at full detail.
- Setting it **on** on a mesh in use rebakes the LOD hierarchy (`dag`).
- A mesh that is not in use bakes at the depth the setting asks for when it
next materialises.
Acting on either setting for a mesh in use can read the mesh's stored geometry,
and where the geometry is fetched on first read that read spans frames.
`meshRef:settingsPending()` answers whether one is still in flight; once it
answers `false`, every settings change made so far has reached the mesh.
### Second UV set (`lightmapUvs`)
A mesh can carry a **second**, independent UV set (`uv1`) alongside its texture UVs
(`uv`) — a distinct per-vertex channel, non-overlapping where the texture UVs tile
and reuse charts. `lightmapUvs` controls whether the stored geometry keeps one,
generates one, or drops it:
| Value | Effect |
|---|---|
| `"keep"` | Leave the stored geometry's `uv1` as-is (the default). |
| `"generate"` | Unwrap a fresh non-overlapping `uv1` (near-planar charts packed into `[0,1]` with gutters). |
| `"none"` | Strip the `uv1` set. |
```luau
meshRef:setSettings({ lightmapUvs = "generate" })
```
Changing the setting reprocesses the asset's geometry in **edit** mode; a change in
play mode never rewrites the source. The reprocess round-trips the geometry through
the codec, so positions, normals, tangents, skinning, and the skeleton are preserved
— only the `uv1` set changes. A runtime mesh carries `uv1` the same way: pass `uvs1`
(or `unwrapUvs = true`) to `renderer.mesh.create`.
## Read vertices — `meshRef:getVertices()`
With `keepCpu` on for an in-use mesh, read its vertices straight from the resident
CPU copy:
```luau
meshRef:setSettings({ keepCpu = true }) -- on an in-use mesh
local verts = meshRef:getVertices() -- { { pos = {x,y,z}, normal = {x,y,z}, uv = {u,v} }, ... }
```
If the CPU copy isn't resident — `keepCpu` is off, or the mesh isn't in use — it
errors loudly, naming the reason and the remedy, rather than silently loading a
throwaway copy:
```
mesh:getVertices: cannot read vertices — keepCpu is off, so no CPU copy is kept.
Turn on CPU retention for an in-use mesh first: meshRef:setSettings({ keepCpu = true }).
```
## Morph targets — `meshRef:morphTargets()`
A mesh can carry morph targets: shapes it blends towards, each a per-vertex
displacement from the base geometry. Every target carries the name it was
authored under, and an imported model keeps the one its source file gave its
blend shapes, so content addresses a shape as the thing it means rather than as
the ordinal it happened to import at.
```luau
for i, name in meshRef:morphTargets() do print(i, name) end -- 1 Eyes_Blink
```
An entity blends them with `ecs.MorphWeights`, weight `i` driving target `i`.
`renderer.mesh.morphWeights(mesh, { Eyes_Blink = 1 })` builds that ordered array
from weights named by shape — shapes left out sit at 0, and an unknown name
errors listing the ones the mesh carries.
## Discovery
| Call | Shows |
|---|---|
| `asset.list("mesh")` | every registered mesh |
| `asset.inspect("<name>")` | a text report about the mesh (metadata, source, status) |
| `meshRef:settings()` | the resolved settings |
| `meshRef:settingsPending()` | whether a settings change is still reading the stored geometry |
| `meshRef:morphTargets()` | the shapes it blends towards, named, in target order |
For the on-disk shape of a mesh asset, run `asset.inspect("<name>")` or read the
type's `type.yaml` — those are the live source of truth.
## Related
- `renderer.mesh.*` — the GPU/CPU resource layer (`create` a runtime GPU mesh,
`encode`/`decode` the `ZMSH` payload, `readback` GPU → CPU). `:load`/`:handle`
wrap it; see `man renderer`.
- `.material` + `.shader` — what shades the geometry.
- `.bundle` — container assets that carry `.mesh` interiors.
## In the Inspector
Selecting a mesh opens its **Geometry** section, after the asset's identity and before its Settings. It reports what `data.zmsh` holds, decoded once per payload version: vertices, triangles, the bounds and their size, the vertex streams beyond positions (normals, uvs, uvs1, colors, tangents, skinning), and, when the mesh has them, its skin joints and morph targets. While the renderer holds the mesh's cluster hierarchy, the section also reports its cluster and level counts. A payload is one index stream, so the section names no submeshes. A mesh folder with no `data.zmsh` says so.
▲ 0↑ born
# `rig` asset type
A skeleton, as a composite asset. A rig is its own primitive — not a mesh and
not an animation. The folder `<name>.rig/` carries:
- **`rig.json`** — the readable skeleton document, the asset's *primary*.
Consumers reference the `.rig` asset's guid, never this inner file. It holds:
- **`bones`** — ordered bones, each with a `name`, a `parent` index (-1 for a
root), the `node` index it maps to, its `rest` local transform
(translation / rotation / scale relative to the parent — the bone's offset and
length), and its `inverseBind` matrix (16 floats). The `inverseBind` is what
skinning reads.
- **`bind`** — where the rig's bind pose, the pose retargeting reads anatomy
from, comes from: `"skin"` (its inverse binds are a skin's bind, as a model
import writes), `"rest"` (it has none: its inverse binds restate its rest, as an
animation-only import's rest is the clip's first frame), or `{ "__ref": <rig
guid> }` (a skinned rig of the same skeleton whose bind it shares; an
animation-only import names one when the world holds it). A rig with no `bind`
reads its own inverse binds.
- **`overrides`** — what an author set by hand, keyed by avatar type guid:
`{ [typeGuid] = { roles = { [role] = boneName | false } } }`. `false` leaves a
role empty. Carried across a re-import of the rig's source file.
The format is JSON, not binary, because a rig is small structural metadata an
agent inspects and corrects — the bulk binary `.mesh`/`.animation` carry their own
decode paths, but a rig is meant to be read.
## How a rig maps onto an avatar type
Which bone is the hips, the left hand, a hair strand, is derived from the bones
whenever it is asked for, by the avatar type (see the `avatarType` asset type) —
never stored. The skeleton's structure decides it, for any naming and in any pose
the rig rests in, and the author's overrides are laid over the result.
```luau
local rig = asset.resolve(rigGuid, "rig")
local q = rig:qualify() -- the builtin humanoid type; pass another type to ask it
print(q.qualifies, q.reason) -- whether it is a humanoid, and why not
print(q.mapping.roles.hips) -- the bone filling a role
print(q.report.verdict, #q.report.stale)
```
| Method | Returns |
|---|---|
| `:doc()` | The decoded `rig.json`. |
| `:structure()` | The skeleton's structure (`modules.rigStructure`). |
| `:qualify(avatarType?)` | Whether the rig qualifies as the type, the effective mapping, and the report (confidence per chain, missing chains, ambiguities, name contradictions, overrides naming bones the rig lacks). |
| `:mapping(avatarType?)` | `:qualify(avatarType).mapping`. |
| `:isHumanoid()` | Whether the rig qualifies as the builtin humanoid. |
A skinned `.mesh` references a `.rig` (the mesh keeps only per-vertex skin
weights; the skeleton lives here). An `.animation` references its source `.rig`.
Retargeting maps a clip from its source rig onto a body's rig through the roles
both rigs fill.
## In the Inspector
Selecting a rig opens its **Skeleton** section: the bone count, the number of roots, whether the rig qualifies as a humanoid with its verdict and confidence, and the bone hierarchy, indented by depth.
▲ 0↑ born
# assetType (asset type)
An asset type says what a kind of asset is: the files an asset of the kind is
made of, the code that gives it behaviour, the interface a person uses to work
with it, and how it is found, pictured and checked. A `<typename>.assetType/`
folder declares a type called `<typename>`, and every `<name>.<typename>/`
folder is an asset of it. `assetType` is itself a type, so a type is an asset
like any other: it is created, versioned, installed and contributed the same way.
## Making a type
`asset.create("assetType", "<typename>")` scaffolds a type. The type registers
the moment its folder is written, anywhere under `/zero/source/`, and from then
on `<name>.<typename>/` folders are its assets. An asset of a type that is not
registered yet is not an asset, so make the type first.
A type folder is a root: `~.x` in any file inside it names the type's own `x`,
wherever the type is installed. Keep the type's whole implementation inside its
folder and reach it that way.
What a type folder must hold is set by this type's own `type.yaml`, the same way
every type's `type.yaml` sets what its assets hold. `asset.validate` on a type
reports anything missing, and the scaffold produces a folder that passes.
## What sets your type's schema (`type.yaml`)
`type.yaml` is the structural contract every asset of the type is checked
against. The keys below are the ones the engine reads; a key not listed here is
ignored.
| Key | What it controls |
|---|---|
| `suffix` | The folder suffix of the type's assets. Defaults to `.<typename>`. |
| `primary` | The file or files that are the asset's content: what the asset resolves to when it is used. |
| `entry_files`, `primary_aliases` | Further files that load as the asset or share its identity. |
| `is_container` | Whether the suffix stays in the identities of what the asset holds. |
| `is_opaque_walker` | Whether a walk of the tree stops at the asset and registers only its primary. |
| `plural_dir`, `classify_exts` | Loose files in a folder of this name, or with these extensions, are assets of the type. |
| `is_text_default` | Whether the asset's files are text unless stated otherwise. |
| `broadcasts_subassets` | Whether assets nested inside an asset of the type are found by their own short name. |
| `root` | `true` makes every asset of the type a root, so `~.x` inside one names that asset's own `x`. |
| `contract_ref: { field, category }` | A key of the primary file that names another asset the asset is built on. |
| `files: { required, optional }` | The files an asset holds (below). |
| `allow_unlisted` | `false` reports every file the type does not declare. |
| `aliases_from: [{ file, key }]` | Further names an asset answers to, read from its own files. |
| `settings:` | Typed per-asset settings (below). |
| `inspector:` | The interface people use for the type and its assets (below). |
| `preview:` | How an asset is pictured (below). |
| `references:` | Names of other assets the type's files carry (below). |
| `indexing:` | What search reads for each asset (below). |
| `describe:` | How a missing description is generated (below). |
### The files an asset holds
Each entry under `files.required` or `files.optional` is
`{ path, kind: file | folder, one_of_group?, content? }`. `*` in a path matches
within one segment. Entries that share a `one_of_group` are satisfied by any one
of them. `content` lists checks on the file's bytes:
| Check | Passes when |
|---|---|
| `non_empty` | the file has content |
| `json_field { key, type?, non_empty?, accept? }` | the JSON holds the key (a dotted path) with that shape |
| `yaml_field { key, type?, non_empty?, accept? }` | the same for YAML |
| `described { keys?, headings?, message? }` | the file says what the asset is (see "Ask your assets to say what they are") |
Each check takes a `message`, which is what the author is told when it fails.
### Settings
`settings: { values, schema: { type | instance, export? } }` gives every asset
typed settings. `values` names the file an asset's values live in; `schema.type`
declares one schema for the whole type, and `schema.instance` names a file each
asset declares its own in. `asset.create` writes the resolved values, and a write
to the values file reaches `onChange` with what changed.
## The human side (`inspector.luau`)
A type is used by people through its Inspector, and without one the type is
usable only from code. The Inspector is the type's interface for a person: for a
procedural graph it is the graph editor, for a kit it is the tracking view of
what the kit holds and what is still missing. Writing it is part of writing the
type.
`inspector: { type, instance? }` in `type.yaml` names the files. `type` is the
interface for the type itself and for its assets; `instance` names a file an
asset may ship to add its own. Each returns:
```luau
return {
sections = { { id = "graph", title = "Graph", build = function(ctx) ... end } },
replace = { contents = { ... } }, -- replace a default section by id
order = { "graph", "contents" }, -- section order
}
```
`build(ctx)` receives the asset and builds the section. A section's `title`, and
the text its rows carry, is a string or an `i18n.text` value read from a catalog
(`i18n.text("inspectors.mesh.title")`), which follows the editor's language. `asset.create("assetType",
...)` scaffolds a working `inspector.luau` to start from. `asset.validate` on a
type that names no inspector, or names one that does not load, reports it.
## Say what your type is for (`about.yaml`)
Every type carries an `about.yaml` that says what the type is for, in three
plain-language texts:
```yaml
intent: "Sets the typeface of the text in your game: UI, labels, signs and 3D lettering."
function: "A font, from a TTF, OTF or WOFF file, made available to all of Zero's text, including 3D text."
usage: "You choose it by name for your UI or your text, and the text appears in that typeface."
```
- **`intent`** is what the type is for in someone's game, in the words a game maker uses. Up to 800 bytes.
- **`function`** is how it works today: what an asset of it is made of and what the engine does with it. Up to 1500 bytes.
- **`usage`** is what a person or an agent does with one. Up to 1200 bytes.
Write about the type as a whole, not one asset of it, and say what it does now.
Agents read these texts to learn when to reach for the type and how; people
searching find the type by them. The scaffold starts each text with
`[[describe]]` and a note on what to write; a text that still starts that way
counts as not written. `services describe` can write them.
They show up in `asset.inspect` as `detail.about`, in
`assetTypeRef:getAbout()`, at the end of the type's `types/<typename>` guide,
and at the top of the type's Inspector.
## Ask your assets to say what they are
A type can require each asset to carry a description: a file, written by the
author or generated, saying what the asset is and what it is for. Declare it with
a `described` check:
```yaml
files:
required:
- path: "README.md"
kind: "file"
content:
- { check: "described", message: "Say what this widget is for and where it goes." }
```
A file that is missing, empty or still has a line starting with `[[describe]]`
fails the check, and so does a YAML file whose named `keys` are missing or over
their `max_bytes`, or a Markdown file missing a named heading. `asset.validate`
names the asset, and a push that carries it is refused with `message`.
`services describe` finds such assets and writes their descriptions.
`describe:` steers what is generated:
```yaml
describe:
guidance: "Say what the widget shows, what input it reacts to, and where it fits in a UI."
examples:
- "<identity of an asset of this type whose description reads well>"
```
An example is an asset of your type whose description is a true, written one,
never a placeholder and never the asset being described; with none, leave
`examples` empty. A type whose assets the model cannot perceive (a sound, a
motion) says so in `guidance`, and the description is drawn only from what the
asset's own files state.
Start the file unwritten in your `template/`: a heading and one line starting
with `[[describe]]`, never example content, which every new asset would carry
verbatim. A type whose assets say what they are through their own content (a
mesh, a texture) declares no check.
## The type's code (`behavior.luau`)
`behavior.luau` returns one table. These are the keys the engine reads from it;
anything else on the table is the type's own.
### What the type's assets can do
| Key | Shape | What the engine does with it |
|---|---|---|
| `ref` | `{ [name] = function(self, ...) }` | Every function becomes a method on every asset of the type: `ref:name(...)`. |
| `modules` | `{ [name] = require(".x") }` | Code the type's assets reach with `asset.containing(__FILE__).modules.<name>`. |
| `events` | an events schema | Events an asset fires. Readers subscribe through `ref.events.<name>:connect(...)`; the type fires them. |
| `refShapes` | `{ [method] = function(self) -> (typeExpr, source?) }` | The result type of a `ref` method for one particular asset (below). |
| `namePattern` | a Lua pattern | The names `asset.create` accepts for the type's assets. Default `^[A-Za-z][A-Za-z0-9_]*$`. |
| `derive` | `{ output, module, compute, fingerprint, matches?, required?, timeout? }` | A file computed from the asset when it is committed; a `required` one holds the commit until it lands. |
Some `ref` names mean the same for every type, and the engine calls them:
- **`instantiate(self, target?, opts?) -> (root, idMap)`**: puts the asset in a scene. Defining it is what makes the type scene-instantiable. Its return is a contract (below).
- **`inspect(self) -> detail`**: what `asset.inspect` reports for the asset beyond the common record (below).
- **`preview(self, opts)`**: renders the asset's picture; it wins over the type's `preview:` recipe.
- **`previewMaterial(self) -> { material }`**: the material a `surface` preview paints.
- **`saveDefinition(self)`**: writes the asset's in-memory edits back to its files in edit mode.
Some names are answered for every asset before the type is asked, so a `ref`
entry of the same name is never reached: `getSource`, `getBytes`, `getText`,
`exists`, `deps`, `canInstantiate`, `canPreview`, `preview` (which calls yours
when you define one), `meta`, `runtime`, `events`, `typeRef`, `modules`, and the
residency flags `has_backing_asset`, `has_runtime_changes`, `cpu_resident`,
`gpu_resident`.
### When the engine calls the type
| Hook | Signature | When it runs |
|---|---|---|
| `onCreate` | `(name, opts) -> { [path] = contents }` | On `asset.create`. Returns the files of the new asset, laid over the type's `template/`. Annotate `opts`: its type is the schema `asset.create` checks the caller's arguments against. A `""` key writes a single loose file. |
| `onReplace` | `(dest, files) -> files` | On an `asset.create` over an asset already at `dest`, after `onCreate`, so what the author added can be carried into the replacement. |
| `onRegister` | `(self)` | Once per asset, when it first registers: when the world loads, on a live `asset.create`, and when an asset arrives later (installed, pulled, synced). This is what makes an asset take effect the moment it is written. |
| `onChange` | `(ref, change)` | On every write to a file inside an asset of the type. `change` is `{ path, asset, type, kind, origin }`, plus `settings` and `settingsChanged` for a write to the settings values file; `kind` is `edited` or `seeded`, `origin` is `local` or `remote`. Filter on `change.path`. Writes to one asset arrive in order, each after the hook for the one before has finished, and the hook may wait on what it reads. A write the hook makes inside its own asset arrives later like any other, so keep the hook convergent. |
| `onDependencyChange` | `(ref, change)` | When a module the asset's code requires is re-run, or a name the asset carries starts resolving. `change.kind` is `dependency`. |
| `onDelete` | `(ref)` | When an asset of the type leaves the world: deleted, or the world it is in is swapped away. |
| `validate` | `(ref) -> { { code, message, severity?, path? } }` | On `asset.validate`, after the `type.yaml` checks, for the type's own rules. `severity` defaults to `error`. Runs on every push, so a type's rules hold at publish. A hook that fails reports `validate.hook_failed`. |
### Settings hooks
| Hook | When it runs |
|---|---|
| `settingsDefaults(name, opts, asked) -> table` | Once per `asset.create`, for values the schema's defaults do not give. |
| `settingsCorrections(values, name, opts) -> (corrections?, reasons?)` | On every settings write. A correction to a field the writer named refuses the write, giving the reason. |
| `settingsDerivedDefaults(values, explicit, changed)` | On a settings write, for fields derived from others. |
| `normalizeSettings(values, stored?)` | On every read and write, to map an earlier spelling of a field to the current one. |
| `settingsHealed(values, stored)` | On a read, to repair stored values. |
| `settingsSchemaProblem(schema, ref) -> string?` | When an asset's own schema is read; a returned string refuses the schema. |
`creationInputs` (`{ [opt] = true | selectorOpt }`) names `asset.create` options
that are inputs to the type rather than settings.
### Reloading
An edit to `behavior.luau` reaches existing assets with no restart.
## How an asset is pictured (`preview:`)
| `kind` | The picture is | The type provides |
|---|---|---|
| `instantiate` | the asset placed in the preview studio through its `instantiate` | `ref.instantiate` |
| `image` | a file the asset holds, fitted to the frame | `source: { files }`, the candidate files in order |
| `surface` | the asset painted on a mesh | `mesh`, and `ref.previewMaterial` |
| `custom` | whatever `ref.preview` returns | `ref.preview` |
```yaml
preview:
kind: surface
mesh: sphere
```
`angle: [yaw, pitch]` turns the camera. The studio owns the light, the backdrop
and the framing, so an asset pictures the same in every world. A type with no
`preview:` that defines `instantiate` still answers `ref:preview()`.
A picture is rendered when something asks to view it (the editor showing the
asset, `ref:preview()`), when previews are generated on request
(`assets.generatePreviews`), and when the asset is staged. An edit alone
renders nothing. The picture records what it was made
from, so a stale one is told from a current one.
## The names your type's assets hold (`references:`)
Where your type's own format or API carries the name of another asset, declare
it here:
```yaml
references:
keys: # a top-level key of the type's files that holds a name, or a table of them
- { key: "shader", type: "shader" }
args: # an argument of a call, counting from 1
- { call: "g:node", position: 2, type: "procNode" }
fields: # a field of the options table a call takes
- { call: "fx.beam", field: "material", type: "material" }
```
`type` is the category the name resolves in. `asset.declareReferenceKey`,
`asset.declareReferenceArg` and `asset.declareReferenceField` declare the same
from a script.
## What search reads (`indexing:`)
`indexing:` is how assets are found. Search reaches an asset only through what
its type's `indexing:` names, and adds nothing of its own. That includes you: an
asset indexed badly is one you cannot find later, when you are making something
else and need it. A type whose assets are never looked for says `indexing: none`.
```yaml
indexing:
data: # text, read from the asset's files
- role: identity
extractor: verbatim
source: { file: "README.md" }
- role: interface
extractor: symbols
source: { files: ["init.luau"] }
- role: implementation
extractor: verbatim
source: { files: ["init.luau"] }
content: # the picture
- role: identity
modality: image
source: { file: "preview.png" }
facets: # what a query filters and re-ranks by
file: ".metadata"
rigged: { type: bool, from: { components: { contains: SkinnedModel } } }
roughness: { type: number, filter: range, from: { file: "mat.yaml", field: "floats.roughness" } }
```
**Roles.** Each `data` and `content` entry has a `role`, and a query matches in
two slots:
- **`identity`**: what an asset is. For anything you look at, the picture; for anything else, the text that says what it is.
- **`capability`**: what it does and how it is made. `interface` (the signatures or shape a caller uses) and `implementation` (the body) both count as capability, and a type with code usually declares both.
Ask what a person would recognise an asset of the type by, and what it does, and
point each role at the file that says it. For text, that is the asset's
description: the file your template starts with a `[[describe]]` block, which the
author writes or `services describe` fills in (see "Ask your assets to say what
they are").
**`data`.** `extractor: verbatim` embeds a file's text as it is; `symbols` embeds
the declared signatures of code. `source` is `{ file }` or `{ files: [...] }`.
Never index a file name (`source: { field: name }`): one
or two words carry nothing to match.
**`content`.** `modality: image` embeds the picture itself, so a text query
matches what the asset looks like. A type whose assets are looked at makes the
picture its identity.
**`facets`.** What a query narrows and re-ranks the matches by. `file` is embedded
whole, so keys nobody declared in it are still found. Each further key is
computed for every asset and states a value for every one, including `false`: a
file can imply "not rigged" but never say it, and a query separates rigged from
unrigged only because the unrigged ones say `rigged: false`. A key is
`{ type: bool | number | enum, filter?: range, from }`, where `from` is
`{ file, field }` (a field of one of the asset's files, dotted for nesting),
`{ metadata: key }`, or `{ components: { contains | any_outside } }` (what the
asset's entities carry).
## What blocks a push
A push checks every asset it carries against its type. These refuse it:
- a required file that is missing, a `content` check that fails, a `described` file not written yet;
- an error from the type's `validate`.
Unlisted files and warnings do not. A `described` file blocks only a push that
changes the asset. When a push is refused, the message names each asset and what
it lacks. `world.publishBlockers()` and `zm status` read the same list without
pushing.
## Typing a result from the asset (`refShapes`)
A `ref` method whose result depends on the asset (one entry per child folder, per
row of a config, per control a map declares) can only be annotated as
`{ [string]: Thing }`, which accepts any key. `refShapes` states the result for
each asset instead: a function returning a Luau type expression for that asset,
and the module whose types the expression uses. Read the asset; never run it.
```lua
-- inputMap.assetType/behavior.luau
M.refShapes = {
activate = function(self): (string, string)
local names = {}
for _, control in ipairs(M.ref.controls(self)) do
table.insert(names, control.name .. ": Handle")
end
if #names == 0 then return "", "" end
return "{ " .. table.concat(names, ", ") .. " }", "@builtin::modules.zinput.scheme"
end,
}
```
With it, `map.jump:onPressed(fn)` is checked and `map.noexisting` is reported.
A per-asset type applies where the call site names which asset: a component
field whose default names it, or an annotation `AssetRef<"inputMap", "<identity>">`.
`asset.resolve(name, category)` carries only the category, and a name computed
at runtime carries nothing, so calls on those keep the declared return. The type
belongs to the call's result: index it there, or bind what you need from it
there, and the names stay checked.
`require("@builtin::assetTypes.assetType.shared.refShapes").published()` shows
what was published: `categories` (each type's method surface) and `returns`
(per-asset results by identity, then method). An identity missing from
`returns` was never published. Results are recomputed when an asset is written;
at most 64 assets of one type are read.
## Putting an asset in a scene (`ref.instantiate`)
A caller writes one piece of code against every instantiable type, so what
`instantiate` returns is fixed:
```lua
local Instantiable = require("@builtin::assetTypes.assetType.shared.instantiable")
M.ref = {
instantiate = function(self, target, opts)
local root = Instantiable.root(self, target, opts)
-- compose the asset under `root`, now
return Instantiable.result(self, root, idMap)
end,
}
```
**In.** `target` is the entity the asset lands under, or onto for a hierarchy
type; with none, the type spawns a new root. `position`, `rotation` (three
numbers in degrees, or a quaternion), `scale`, `name` and `temporary` mean the
same for every type: `Instantiable.root` applies them to a root you make,
`Instantiable.place` to one you adopt. Document any options of your own in the
hook's `--!arg` docs.
**Out.** `(root, idMap)`. `root` is the composed root, live when you return:
build synchronously, never defer to a component's `awake`. `idMap` maps each
original id to the spawned one, so a component that composes the asset again on
every load can pass it back in and keep references into the composition. Every
call goes through `Instantiable.result`, which refuses a root that is not live
and turns a missing `idMap` into `{}`.
## Inspecting an asset (`ref.inspect`)
`asset.inspect(ref)` builds a record every asset shares (`identity`, `name`,
`guid`, `source`, `typeName`, `typeDefinitionPath`, `scope`, `origin`,
`description`, `tags`), then calls `ref.inspect(self)` for the type's own
`detail`:
```lua
M.ref = {
inspect = function(self)
-- read the asset's own files, never runtime state: inspect works on an asset
-- that has not been compiled, uploaded or registered
return { --[[ what makes this asset what it is ]] }
end,
}
```
Without it, the record is the shared part alone, and an agent inspecting the
asset learns nothing of what it is. A hook that raises leaves `detail` empty and
sets `record.warning`.
## Discovery
- `asset.list("assetType")`: every registered type.
- `asset.inspect("<typename>")`: the type, its README and its `about.yaml` texts.
- `asset.resolve("<typename>", "assetType")`: the type itself.
▲ 0↑ born
# Component (asset type)
A component is a reusable behavior + data unit attached to an entity.
The component file declares public fields, methods, and lifecycle
callbacks; the engine instantiates one per attachment and drives its
hooks every frame. Components are the primary unit of gameplay code —
movement, AI, animations, UI panels, network sync, and per-entity state
all live here.
## When to use one
- You need per-entity state that survives across frames.
- You need per-entity behavior driven by engine lifecycle hooks
(`start`, `update`, `onDestroy`).
- You want to expose configurable fields to designers / `entity.spawn`
callers / scenes without code changes.
If you need a stateless pure-function library, use a `.module`. If you
need to generate content declaratively, use a `.service`; compute work
belongs in a `.computeShader`.
## Where it lives
- Source: `/zero/source/.../<Name>.component/`
- Identity: `<Name>` (the `.component` suffix strips from the
identity, but the folder retains the suffix on disk).
- Folder shape:
- `init.luau` (or `init.lua`) — declares the component. **Required.**
- `README.md` — instance-level documentation. **Required.**
- `.metadata` — agent-editable tags + free-form fields. **Required.**
The structural contract is enforced by `asset.validate` against this
type's `type.yaml`.
## How to create one
Always start with the scaffold — it guarantees the folder shape
matches `type.yaml` from the moment it's on disk:
```luau
asset.create("component", "<Name>")
-- Creates: /zero/source/<Name>.component/
-- init.luau (canonical declare + public block + hooks stubbed)
-- README.md (instance README template)
```
After scaffolding, edit `init.luau` to fill in the actual fields and
behaviour. The component registers as soon as the file is written —
hot-reload is automatic; no engine restart required.
## How it operates
1. **Registration.** Writing `init.luau` into a `.component/` folder
indexes the asset, mints a guid (in the `.meta` sidecar), and
surfaces the type to the engine's component registry. Same-frame
visible to `entity.component.add`.
2. **Attachment.** `entity(id).component.add(name, data)` (or the
batch form `tools.use("entityOps", "addComponents", targets,
{ [name] = data })`) attaches an instance.
The `data` table's fields override the component's declared
defaults; omitted fields take the declared default. The `data`
values are copied into `public.*` BEFORE `awake()` runs.
3. **Lifecycle.** The hooks run in this order:
- `awake()` — same frame as attach, immediately after the data
copy. Initialise state, set up native bridges, register with
other systems. Other components on the same entity may not have
awoken yet.
- `start()` — the next frame. Every component on the entity has
completed `awake()` by then, so cross-component setup is safe
here.
- `update(dt)` — same frame as `start()`, then every frame after,
for as long as the gameplay clock is running. `fixedUpdate(dt)`
runs once per physics step, straight after it, with the step's
`dt`, so a frame can run it several times or not at all;
`editorUpdate(dt)` runs
while the clock is held. Which of them ticks is decided by
`engine.mode` AND `engine.paused` together — see **Per-frame
hooks and the two axes** below, and `man modes`.
- `onDestroy()` — when the component is removed via
`component.remove` or when the owning entity is despawned.
Release resources, drop native ECS components.
For the full list (`onEnable`, `onDisable`, `fixedUpdate`,
`editorUpdate`, `onPropertyChanged`, `onAssetReload`,
`onModuleReload`,
`onBeforeSave`, …) and the timing details, **read `man components`
— that is the canonical lifecycle reference. Generic component
patterns (declare blocks, public fields, sync, ECS bindings,
typed methods) all live there.**
4. **Public surface.** Fields declared in `public` are inspectable,
editable in the inspector, and persist with the entity. Methods are
declared on that same table with a colon —
`typed function public:method(args)` — and every one of them takes
the component ref as `self`, so a caller invokes it with a colon
too: `entity(id).component.get(name):method(args)`. That is the one
calling convention a component ref answers to: the reflection
methods `ref:fields()` / `ref:getField(name)` /
`ref:setField(name, value)` take the same spelling. A function the
chunk declares at file scope (`function helper()`) is the
component's own, reachable from its hooks under its bare name;
`public` is what the ref carries.
5. **Settings and the Inspector card.** `type.yaml` names the component's
settings schema as `settings: { schema: { instance: "init.luau",
export: "public" } }`: the `public` table IS the schema, and its values
live on each entity the component is on, so `asset.schema(componentRef)`
answers the Field descriptors and no values file exists. On an entity
the Inspector shows the component as a card whose generic `settings`
section edits those fields as typed controls, each edit checked against
the schema and written to the live component as one undoable operation.
`type.yaml` also names `inspector: { instance: "inspector.luau" }`: a
component that ships an `inspector.luau` beside its `init.luau` shapes
its card — the file returns `{ sections, replace, order }` (see
`assetTypes/README.md`, "`inspector:`"), each section's `build(ctx)`
reading `ctx.component` (the live component) and `ctx.selection.id` (the
entity). `SceneModule.component/inspector.luau` replaces `settings` with
the bound module's declared inputs.
6. **Events.** An `events` block declares typed moments the component
fires from inside — `events = { onHit = Event({ dmg =
Field.number(0, NoSync) }) }`. The owner fires with
`events.onHit:fire({ dmg = 10 })`; outside code subscribes through
the proxy's subscribe-only facade,
`entity(id).component.get(name).events.onHit:connect(fn)`
(`:once` / `:wait` too). Pass `Sync` as the event's second argument
to replicate a fire to other peers. Events let one component react
to another's moments without polling. Full reference: **`man
components`**, Events section.
7. **Hot reload.** Editing `init.luau` reloads the component
definition; live instances are reconciled with the new declaration
in-place, and each crosses one lifecycle boundary: `onDestroy()`
ends the running life, then `awake()` and `start()` construct the
one that replaces it, and `update()` resumes on the next frame.
`public` and `private` values carry across; module-level `local`s
belong to the chunk, so the new life starts with them at their
initial values and its `awake`/`start` are what fill them in.
Whatever the old life handed elsewhere — a subscription, a shared
registration, a spawned entity — is released in `onDestroy`, the
call that pairs with the construction about to run. An edit that
leaves the source byte-identical crosses no boundary at all.
A field carries its value across when something stated it: a key of
the `data` table the instance was attached with (a scene record's
data is that table), or a write made since. Every other field takes
the edited source's declared default, so changing a default reaches
every instance that left the field to it. An instance that states
its own value for a field whose default the edit changed keeps that
value, and the reload logs a `[HotReload]` warning naming the field,
the entity, the value kept and both defaults.
## Discovery
- `asset.list("component")` — every registered component (engine +
world + libraries).
- `asset.inspect("<name>")` — identity, scope, public fields,
lifecycle methods, source path, and this type README. `detail`
carries the component's declared `fields`, `methods`, and `events`,
parsed from its source — `tools.use("assets","describe", "<name>")`
renders it as markdown, so a component's events are visible without
opening its source.
- `ls /zero/source/<Name>.component` lists the files the folder holds.
- `entity(id).component.list()` — components attached to an entity.
## Per-frame hooks and the two axes
`engine.mode` (`"edit"` / `"play"`) and `engine.paused` (the gameplay
clock) are independent, and the per-frame hooks read them separately:
- `update(dt)` / `fixedUpdate(dt)` tick while **the gameplay clock is
running** — whichever mode the engine is in.
- `editorUpdate(dt)` ticks while **the clock is held** — because the
mode is `edit`, or because play is paused — and also alongside
`update` whenever edit is running.
Which gives four states, and this table is the contract:
| `engine.mode` | `engine.paused` | hooks that tick |
|---|---|---|
| `edit` | `true` | `editorUpdate` |
| `edit` | `false` | `update`, `fixedUpdate`, `editorUpdate` |
| `play` | `false` | `update`, `fixedUpdate` |
| `play` | `true` | `editorUpdate` |
Entering edit sets `paused = true` and entering play sets it back to
`false`, so a session that never writes `engine.paused` only ever sees
rows 1 and 3 — which is where the shorthand "`update` is the play tick,
`editorUpdate` is the edit tick" comes from. It is the two default rows
of a four-row table, not the rule.
**Row 2 is the one that surprises.** With `engine.paused = false` in
edit — set by hand, or by a tool that advances the gameplay clock to
sample it, such as a `capture.collage` given a `duration` — `update` and `editorUpdate`
both fire, once each, every frame. A component that declares both and
routes them to one body therefore runs that body **twice per frame**:
```luau
local function drive(dt) ... end
function update(dt) drive(dt) end
function editorUpdate(dt) drive(dt) end -- twice per frame in row 2
```
Nothing reports the doubling; it reads as an animation running at twice
its intended speed. To get exactly one tick per frame in every row, ask
which axis you meant and answer it in the body:
```luau
function update(dt) drive(dt) end
function editorUpdate(dt) if engine.paused then drive(dt) end end
```
A scene `entrypoint.luau` declaring the same two hook names is dispatched
by the scene loader, not by this table: there `update` needs play AND
running, `editorUpdate` needs edit, and play-paused ticks neither. Read
`man scenes` before carrying a component's rule to an entrypoint.
## Authoring conventions
- Annotate public fields with `--!desc` so the inspector + LSP show
documentation alongside the field name.
- Declare lifecycle hooks (`start`, `update`, `onDestroy`) explicitly
rather than relying on engine defaults — readers can see at a glance
what runs when.
- Keep `update(dt)` cheap — it runs every frame per instance. Move
heavy work to `start`, to a `.computeShader`, or behind a job (`man jobs`).
- For multi-entity batching, prefer
`tools.use("entityOps", "addComponents", targets, components)` over
a loop of `entity(id).component.add` — it takes many targets and
many components in one call.
## Common pitfalls
- **Naming.** The component identity is the folder stem with
`.component` stripped (e.g. `<Spinner>.component/` → identity
`Spinner`). Don't repeat the suffix in the identity passed to
`component.add`.
- **`init.lua` vs `init.luau`.** Either is accepted (it's a
`one_of_group: "entrypoint"`), but `.luau` is canonical. Mixing both
in one folder is invalid.
- **`update` in edit mode.** It runs whenever the gameplay clock is
running, and entering edit stops that clock — so `update` is idle in
a freshly-entered edit session and ticks again the moment anything
sets `engine.paused = false`. Use `tools.use("wld", "play")` to test
update logic under the mode it ships in; `tools.use("wld", "edit")`
to return. See **Per-frame hooks and the two axes** above.
- **Renaming.** Renaming the folder changes the identity. References
to the component are recorded against the component itself, so
renaming or moving it does not break them.
## Related types
- `.module` — pure-function library, no per-entity state.
- `.service`: a declarative content generator, no per-entity state.
- `.preset` — a captured component configuration, replayable into
`component.add` without re-typing fields.
- `.bundle` — entity hierarchies that may carry component attachments
as part of the bundle.
## In the Inspector
Fields: its execution order, hooks, and how many methods and events it declares, then the fields its `public` table declares as read-only typed controls holding their defaults, with **Check** and **Open** for its entry file. Instances: a link to every entity carrying it. What its card shows on an entity comes from the component's own `inspector.luau`.
▲ 0↑ born
# pbr
The engine's standard physically-based material — the full glTF metallic-roughness
workflow lit by the shared `@builtin::shaderModules.pbr_shading` model. This is the default surface shader a
model uses when its material doesn't specify another, and it is also registered
under the legacy alias **`standard`** (the name many materials reference).
It is a `surface()` shader: it fills a `PbrSurface` and the engine generates every
pipeline entry point and lights it. Every parameter defaults to a neutral value, so
a material that sets nothing renders as a clean dielectric; the advanced lobes only
cost anything when a material opts into them.
## Properties
| Property | Type | Default | Meaning |
|---|---|---|---|
| `base_color` | color | `[1,1,1,1]` | Albedo tint, multiplied by `base_color_texture` and vertex colour. |
| `emissive` | color | `[0,0,0,1]` | Emissive tint, multiplied by `emissive_texture` and `emissive_intensity`. |
| `emissive_intensity` | float | `1.0` | HDR multiplier on the emissive output. |
| `roughness` | range | `0.5` | Perceptual roughness, multiplied by the green channel of `roughness_metallic_texture`. |
| `metallic` | range | `0.0` | Metalness, multiplied by the blue channel of `roughness_metallic_texture`. |
| `reflectance` | range | `0.5` | Dielectric reflectance knob (neutral 0.5 → 4% F0). |
| `ior` | float | `1.5` | Index of refraction → dielectric F0 and the refraction direction. |
| `normal_scale` | float | `1.0` | Strength of the tangent-space `normal_texture`. |
| `occlusion_strength` | range | `1.0` | Blend of the `occlusion_texture` (R) into the ambient term. |
| `clearcoat` | range | `0.0` | Strength of a second dielectric clearcoat specular lobe. |
| `clearcoat_roughness` | range | `0.0` | Roughness of the clearcoat lobe. |
| `anisotropy` | float | `0.0` | Anisotropic highlight stretch along the tangent ([-1,1]). |
| `sheen_color` | color | `[0,0,0,1]` | Tint of the sheen lobe — the retroreflective rim of velvet, satin and brushed fabric. Black leaves the base untouched. |
| `sheen_roughness` | range | `0.3` | Width of the sheen lobe. |
| `iridescence` | range | `0.0` | Blend toward the thin-film response, the hue that shifts with view angle. |
| `iridescence_thickness` | float | `400.0` | Film thickness in nanometres — which wavelengths cancel and which reinforce. |
| `iridescence_ior` | float | `1.3` | Index of refraction of the film. |
| `subsurface_color` | color | `[0,0,0,1]` | Tint of the light that entered the surface, scattered beneath it and left again. Black leaves the base untouched. |
| `subsurface_radius` | float | `0.5` | How far the diffuse falloff wraps past the terminator, taken per channel against `subsurface_color`. |
| `fibre_color` | color | `[0,0,0,1]` | Tint of the second of the two highlights a hair or fur strand gives. Black leaves the base untouched. |
| `fibre_specular` | range | `0.35` | Strength of the first strand highlight, in the light's own colour. |
| `fibre_shift` | float | `0.06` | How far apart the two strand highlights sit along the fibre. |
| `fibre_roughness` | range | `0.1` | Width of the first strand highlight. |
| `transmission` | range | `0.0` | Specular transmission — refracts the environment through the surface. |
| `thickness` | float | `0.0` | Volume thickness for Beer–Lambert transmission absorption. |
| `attenuation_color` | color | `[1,1,1,1]` | Transmission absorption tint. |
| `attenuation_distance` | float | large | Beer–Lambert absorption distance. |
| `uv_scale` | float2 | `[1,1]` | UV tiling applied before every texture sample. |
| `uv_offset` | float2 | `[0,0]` | UV offset applied before every texture sample. |
| `alpha_cutoff` | range | `0.0` | Mask threshold — fragments with `alpha <` this are discarded (0 = no cutoff). |
| `double_sided` | bool | `false` | Flip the shading normal on back faces. |
| `receives_fog` | bool | `true` | Whether the screen-space fog passes (height fog, aerial perspective) reach this surface. `false` leaves an opaque deferred draw at the colour it shaded to, however deep in the medium it stands — see below. |
| `instance_color_lane` | float | `-1` | Which of the instance's four shader-data lanes colours it (rgb colour, a coverage). `-1` reads none. See below. |
| `instance_emission` | float | `0` | How brightly the lane colour glows where it covers. |
### Emissive authoring
The glow output is `emissive × emissive_intensity` (× the `emissive_texture`). The glow
**colour** is `emissive`, a vec; the **intensity** over it is `emissive_intensity`, a
scalar. So `{ emissive = {0.1, 0.9, 1, 1}, emissive_intensity = 5 }` is a cyan glow at 5×,
and it is these two names that `getProperties()` lists and `setProperty` writes.
A spelling another engine's convention uses for the same role — `emissive_color`,
`emission`, `emissive_tint` — is resolved onto `emissive` when the material is authored,
so content carried in from glTF, Unity or Godot reaches the property under the name this
shader declares. Where two such spellings meet on one of the two names, the shape of each
value says which of them it is: a vec is the colour and a number is the intensity, so
`{ emissive_color = {0.2, 0.8, 1, 1}, emissive = 5 }` is the same cyan glow at 5x. Two
values of one shape are one property's, and the one written under the name the shader
declares applies while the other is named in a warning.
A bare positive scalar `emissive` with no colour beside it is the "turn emission on"
shorthand and means a white glow at that intensity, so `emissive = 20` on a white material
lights up.
### The lobes over the base layer
Beyond metallic-roughness, the shading model carries six layers this material
declares. Each is gated on its own neutral value, so a material that sets none of
them shades as a plain dielectric and each one costs only where it is asked for.
| Layer | Turned on by | What it looks like |
|---|---|---|
| Clearcoat | `clearcoat > 0` | A second dielectric specular lobe on top — car paint, lacquer, a varnished surface. |
| Anisotropy | `anisotropy ~= 0` | The highlight stretches along the tangent — brushed metal, a vinyl record. |
| Sheen | `sheen_color` above black | A retroreflective rim that brightens toward grazing angles — velvet, satin, dusty cloth. |
| Iridescence | `iridescence > 0` | The specular hue shifts with view angle — an oil slick, a soap film, anodised metal, a beetle shell. Thickness picks the colours. |
| Subsurface | `subsurface_color` above black | Light wraps past the terminator and glows through thin geometry lit from behind — skin, wax, marble, a leaf. `thickness` gates how much reaches the far side. |
| Fibre | `fibre_color` above black | The two offset highlights a strand gives instead of the one a surface gives — hair, fur. Taken around the world tangent as the fibre axis. |
All six are evaluated inside the engine's own light loop, once for every light
that reaches the surface, so each one is shadowed, attenuated and cone-masked
with the base layer it sits over.
Each layer, and specular transmission beside them, is a shader feature its
gating property switches on (`features:` in `properties.yaml`: `PBR_CLEARCOAT`,
`PBR_ANISOTROPY`, `PBR_SHEEN`, `PBR_IRIDESCENCE`, `PBR_SUBSURFACE`, `PBR_FIBRE`,
`PBR_TRANSMISSION`; the per-instance colour below is `PBR_INSTANCE_COLOR`). A material that sets the property away from its neutral
value draws a program holding that layer; one that leaves every layer at rest
draws a program holding none of them, so the default material pays only for the
metallic-roughness model it shades as. Setting a property with
`renderer.material.setProperty` moves the material between the two programs, and
`renderer.shaderVariants()` lists the ones a scene has asked for.
An oil-slicked wet surface is `iridescence = 1`, an `iridescence_thickness`
around 300-500 nm and a low `roughness` over a dark `base_color`. Skin is a warm
`subsurface_color` with a `subsurface_radius` near 0.5. Velvet is a `sheen_color`
at the fabric's own hue over a dark base.
### Standing outside the fog
`receives_fog = 0` declares that the screen-space fog passes — `heightFog` and
`aerialPerspective` in `@builtin::systems.atmosphere` — leave this surface at the
colour it shaded to.
The declaration travels to those passes as a per-pixel surface flag written into
the alpha of the G-buffer normal (`@builtin::shaderModules.surface_flags`). Only
the deferred G-buffer fragment entry writes that channel, so the declaration
reaches the fog from an **opaque** draw on the **deferred** path. A draw the
renderer sends through the forward fragment entry — every draw on the forward
path, and the transparent phase on either path — writes no G-buffer channel to
carry it, and its pixels take the fog like any other.
### Per-instance colour
`instance_color_lane` names one of the four shader-data lanes every drawn
instance carries (`renderer.instanceData.set` for an entity, the
`instanceDataBuffer` of `renderer.mesh.drawInstanced` for a population). The
lane's `rgb` is this instance's colour and its `a` is how much of it covers the
material's own base colour and emission, 0 none through 1 all, so one material
draws every member of a structure, a crowd or a population in a colour of its
own: a stress map, a heat map, damage tints, team colours, a selection
highlight. A lane never written reads zero and leaves the material's own look.
```luau
local mat = renderer.material.create({ shader = "@builtin::shaders.pbr",
properties = { instance_color_lane = 2, roughness = 0.6 } }, "beams")
beam.component.add("Model", { model = "cube", material = mat }) -- the handle
renderer.instanceData.set(beam, 2, 1, 0.2, 0, 1) -- this beam orange-red
```
The colour replaces the base colour before lighting, so the surface still shades
and the `albedo` capture pass reads the lane colour. `instance_emission` adds
the lane colour as emission where it covers. Lanes 0 and 1 carry a baked
lightmap's rect and layer, so a lit material takes lane 2 or 3. The read is the
`PBR_INSTANCE_COLOR` feature, so a material that leaves `instance_color_lane` at
`-1` draws a program without it. `@builtin::materials.instance_color` is this
material ready made, reading lane 2.
A write is local to the engine that makes it and replicates nothing, which is
what makes it cheap enough to recolour hundreds of members every frame;
`Model.tint` is the synced tint every participant sees.
## Textures (glTF metallic-roughness set)
| Slot | Default | Channels |
|---|---|---|
| `base_color_texture` | white | RGBA albedo (sRGB). |
| `roughness_metallic_texture` | white | G = roughness, B = metallic (glTF packing). |
| `emissive_texture` | white | RGB emissive (sRGB). |
| `normal_texture` | flat-normal | Tangent-space normal map. |
| `occlusion_texture` | white | R = ambient occlusion. |
Because the engine holds the full `PbrSurface`, the capture tool's debug passes
(albedo / roughness / metallic / AO / emissive / world-normal / shadow / depth /
motion) all read real material data.
▲ 0↑ born
▲ 0↑ born
# handles
Loads the native components a saved record lists. A record (a scene entity, a bundle `entity_template` entry) states its native components beside its script components as `ecs = { { type = "<public name>", data = <row> } }`: `type` is the name the component answers to under `ecs`, and `data` is the row `ecs.snapshot` answers for it. Every field that holds a GPU handle is written as the guid of the ASSET the handle came from, because the guid outlives the session and the handle does not.
Loading resolves each of those guids to the handle the component holds (`ref:handle()`, the call a `Model` makes, which puts the asset on the GPU) and inserts one value into every entity that states the same entry.
## Exports
- `M.handleFields(typeName) -> { { rust, category } }?` - the fields of a native component that hold a GPU handle, each with the asset category its guid names (`mesh`, `material` or `texture`). nil for a name `ecs` does not hold.
- `M.value(entry, site?) -> value` - the component value one `{ type, data }` entry states, ready for `ecs.insert`. A guid that names no asset of the field's category raises, naming the component and field. `site` is the path or identity of the asset the entry was read from: a site inside a carried mount reads each guid within that mount's closure (`asset.tryResolve(guid, category, site)`).
- `M.insertAll(specs, site?) -> failures` - inserts `{ id, type, data }` specs, one `ecs.insertMany` per distinct `(type, data)`, reading every guid at `site` the way `M.value` does. Answers one `{ type, ids, message }` per entry that did not land.
## Usage
```luau
local handles = require("modules.api.engine.ecs.handles")
local failed = handles.insertAll({
{ id = a, type = "Mesh", data = { mesh = meshGuid } },
{ id = b, type = "Mesh", data = { mesh = meshGuid } },
{ id = a, type = "Material", data = { id = materialGuid } },
})
for _, failure in ipairs(failed) do
log.error(failure.type .. ": " .. failure.message)
end
```
The scene loader and `bundle:instantiate` call `insertAll` after a record's script components are added.
▲ 0↑ born
# entity_records
Walks a live entity hierarchy into the flat record array the engine's entity
templates use — `{ name, original_id, parent_id, position, rotation, scale,
hidden, components }` per entity, root first, parents before children.
`bundle` composes its `entity_template` through this module; scene builds
compose their baked output through it. One record shape, one implementation.
`compose(rootId)` walks one hierarchy. `composeMany(rootIds)` walks several,
which is what a build needs — a builder may leave more than one unparented root.
Temporary descendants are pruned; an explicitly-named temporary root is kept. `compose(rootId, nil, nil, true)` keeps the temporary descendants too, so the records hold every
entity under the root.
▲ 0↑ born
# scene_instantiable (module)
The instantiation contract's shared implementation. An asset type opts into
scene instantiation by defining `instantiate(self, target?, opts?)` on its
behaviour `ref` table; this module carries the parts that mean the same thing
for every type, so a caller writes ONE piece of code against all of them.
```lua
local root, idMap = ref:instantiate(target?, opts?)
```
## The contract
**IN — the base opts.** `target` is an owning entity ref: the instance lands
under (or, for a hierarchy type like a bundle, onto) that owner. With no
target the type spawns a fresh root. `position`, `rotation`, `scale`, `name`
and `temporary` place that root and mean the same for every type. `rotation`
takes three numbers as pitch/yaw/roll in DEGREES, or four as a quaternion. A
type may honour more opts of its own — `params` for a type built from declared
inputs, `idMap` / `diff` / `sourceTag` for the bundle override contract — and
says so in its own `instantiate` docs.
**OUT — the two returned values.** Every type returns the same pair:
- **`root`** — the composed root, as an `EntityRef`. Composition is
**synchronous**: the root and everything the type built under it are live
the moment the call returns, so a caller can parent to it, read its
components and hand it on in the same statement. There is no frame to wait
for and no callback.
- **`idMap`** — the `originalId -> runtimeId` map naming what the composition
spawned, `{}` for a type with no addressable children. **Never nil.** A
component that re-composes the asset on every load (`Asset`) keeps this map
and passes it back in, which is how a cross-entity reference into the
composition — `SkinnedModel.skeletonRoot` pointing at a bone — survives a
reload.
The return is enforced, not merely described: `AssetRef` dispatches every
`ref:instantiate(...)` through `result` on the way out, whether or not the
type called it. A type that returns something else fails at its own call
rather than handing its caller a nil root or a map that is sometimes absent.
## Exports
- `M.root(self, target?, opts?) -> root` — stand the root entity for a type
that spawns one: parented to `target`, born temporary when
`opts.temporary`, named `opts.name` else the asset's own name, placed.
- `M.place(root, opts?) -> root` — apply the placement opts to a root the type
already has (the adopt path: a bundle exploding onto its target, a
sceneModule reconciling under one).
- `M.result(self, root, idMap?) -> (root, idMap)` — return through the
contract. Checks `root` is a live entity ref, normalises a missing map to
`{}`, and errors naming the asset type when either is something else.
- `M.isOwned(opts?) -> boolean` — whether a component drives this call. The
`Asset` / `SceneModule` components tag their own calls with `sourceTag`;
they hold the reference and re-compose on every load. An untagged call came
straight from `ref:instantiate(...)` and has no such owner.
- `M.own(root, self, idMap?) -> root` — hand an already-composed root to an
`Asset` component pointing at `self`, so the reference and its map persist
and the composition is rebuilt on the next load. The component adopts the
live composition rather than building a second one.
- `M.check(value, constraint) -> ok, reason?` — the `sceneInstantiable`
field-constraint validator, also registered under that kind on load.
## The field constraint
`Field.instantiableRef` emits `constraint = { kind = "sceneInstantiable" }`,
and this module registers the validator for it on load. Given a written value:
1. `nil` passes — the field is optional (no asset assigned).
2. Reads the value's identity — a bare string, or a table's `__ref` / `guid` /
`identity` / `name`.
3. Resolves it (`asset.resolve(identity)` — any category) and asks the
resolved ref `ref:canInstantiate()`: true iff the asset's type defines an
`instantiate` method.
This is what lets `Field.instantiableRef` accept by CAPABILITY rather than a
hardcoded type list — a new scene-instantiable asset type is accepted the
moment it defines the hook, with no edit to the field or its consumers.
▲ 0↑ born
# 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
# references
What content of a carried mount means by the asset references it holds. A carried mount holds the closure of an asset of another world, and that content names assets the way its own world named them: by the guid, path or identity the closure states. Read at the site of the asset holding them, inside the mount, those references name the closure's own copies (each known in this session by its session guid) and the engine library at the version the closure was built against, through `asset.tryResolve(ref, type, site)`. The open world's asset under the same guid or name is another asset.
A bundle of a carried mount composes from its template through `records`, and an `Asset` instance of it measures its overrides against the same records, so a reference the closure answers is the same value on both sides.
## Exports
- `M.site(path, identity?) -> (site?, prefix?)` - where an asset of a carried mount stands: its path in the mount (its carried identity when no path is given) and the prefix its mount spells content with (`@carried.<key>::`). Both nil for an asset of the open world.
- `M.close(value, site, prefix) -> value` - a copy of decoded component data with every asset-reference envelope (`{ __ref, type?, name? }`), at any depth, naming what the closure at `site` means by it: the guid it resolves to there, or, while the mount answers nothing for it, the reference spelled in the mount (`@carried.<key>::<reference>`), which waits on the mount and applies once the mount holds the asset. A component reference (`componentType` beside `__ref`) stays as written.
- `M.records(template, path, identity?) -> template` - the records of an entity template as the asset at `path` composes them: closed through `close` for an asset of a carried mount, `template` itself for an asset of the open world.
## Usage
```luau
local references = require("modules.api.engine.carried.references")
local records = references.records(bundleRef:getTemplate(), bundleRef.path, bundleRef.identity)
local site, prefix = references.site(bundleRef.path, bundleRef.identity)
if site ~= nil then
local data = references.close({ material = { __ref = guid, type = "material" } }, site, prefix)
end
```
▲ 0↑ born
# defaults
The Preset Manager: the world's ordered `[[presets]]` rows in
`/zero/source/.world_settings`. A row names one declarer (`target`), one
preset for that declarer (`preset`) and a path glob (`filter`). When something
a declarer governs is created — an asset of an assetType, an import an
importer runs, a component added to an entity — the first row naming that
declarer whose filter matches the new thing's path gives a layer of its
settings. Rows are read only at creation.
A component row's filter reads the entity's hierarchy path (names from the root
down, joined with `/`). What a scene build's capture scope
adds takes the rows in any mode, whoever runs the build; outside a build, a
component takes a row when it is added in edit mode. A component put back from
existing content takes none: one added inside `materializing` (a saved scene, a
peer's record, a template clone, a bundle's hierarchy, a build's records), and
one a component's own code adds outside a build, on any coroutine that code
runs. A scene build records which fields the rows gave each component, and a
rebuild restates a component only when the code states it differently, so a
row edit alone changes nothing a build already placed.
Rows are written as guid envelopes and checked as a whole. A row is refused
when its target does not resolve, is consulted at no creation, or declares no
settings; when its preset does not resolve, is not a preset, or targets
another declarer; or when its filter is not a string. A write replaces the
`[[presets]]` block and keeps the rest of the file byte for byte.
## The row format
```toml
[[presets]]
target = { __ref = "8cc28fc8-ed63-48ee-83a6-15f4192e27e1", type = "importer", name = "texture" }
preset = { __ref = "0b6c1f0e-5d0c-4d5e-9d64-3c1b7f2a9e11", type = "preset", name = "pixel_art" }
filter = "sprites/**"
```
`target` and `preset` are guid envelopes: `__ref` is the guid, and `type`
and `name` tell a person reading the file what it names. A row written by
hand may name either by identity or guid; the next write rewrites it as
envelopes. `.world_settings`'s `.refs` pins each row's target and preset by
guid.
## Matching
For a new thing a declarer governs, the rows are read in order and the
first naming that declarer whose filter matches the thing's path applies;
the rest are not consulted. A filter is a glob (`preset.glob`) over the
path relative to `/source` with forward slashes, or an entity's hierarchy
path for a component row; the empty filter matches everything, and a
filter that is not a string matches nothing. A matching row whose preset
does not resolve, or targets another declarer, is the match and contributes
nothing, with one warning. A preset value the declarer's schema rejects is
left out and the rest apply.
## Sync
The rows are part of `.world_settings`, so they sync with the world like
any other file and every peer reads the same rows. A row applies where the
creation happens, when it happens: a peer's edit reaches only what is
created after it arrives, and what was created before keeps the settings it
was stamped with. A runtime asset lives for the engine process; a peer that
started before a row edit keeps its old settings on a re-created runtime
identity until it restarts.
## Exports
- `Defaults.PATH`, `Defaults.BLOCK` — the file and the array-of-tables name.
- `Defaults.SCHEMA` — the block's Field schema, `{ presets = Field.list(row) }`.
- `Defaults.rows() -> { Row }` — every row, resolved, with its problems.
- `Defaults.write(rows)` — normalise, check, and replace the rows.
- `Defaults.normalize(input) -> StoredRow`
- `Defaults.rowProblems(row) -> (problems, targetRef, presetRef)`
- `Defaults.consultedAt(declarer) -> "asset.create" | "import" | "component.add" | nil`
- `Defaults.relativePathOf(path) -> string?` — the path a filter reads.
- `Defaults.match(declarerGuid, relPath) -> (Row?, index?)`
- `Defaults.layerFor(declarerGuid, relPath) -> (values?, Match?)`
- `Defaults.check(value, constraint) -> (ok, reason)` — the `presetDefaultRow` constraint.
- `Defaults.removeAt(index)` — remove one row, writing every other row back as the file holds it.
- `Defaults.importerSettings(importerRef, sourcePath) -> (settings, Match?)`
- `Defaults.materializing(fn, ...) -> ...` — run `fn` with no row reaching a component add.
- `Defaults.entityPath(id) -> string` — the hierarchy path a component row's filter reads.
- `Defaults.forComponent(entityId, typeName, data?) -> data?` — the data a component add takes.
- `Defaults.batchLayers(targets, typeName, data?) -> { { targets, data } }` — a batch add split into runs.
- `Defaults.resolveValue(v) -> AssetRef?` — a row value as the asset it names.
- `Defaults.recordingStamps(fn, ...) -> (stamps, ...)` — run `fn` and return what the rows gave the components a capture scope added meanwhile, per `stampKey`.
- `Defaults.stampKey(entityId, componentGuid, instanceName?) -> string`
The `preset` global exposes the rows as `preset.defaults`,
`preset.setDefaults`, `preset.addDefault`, `preset.removeDefault` and
`preset.defaultFor`.
▲ 0↑ born
# json
JSON encode/decode library for Luau. Encodes Lua values to JSON
strings and decodes JSON strings back to Lua values. Used for
communication with the Rust side of the engine, the VFS read/write
bridge, and any wire-format that needs JSON.
Pure Luau, no engine dependencies. Compact and pretty-printed
encoders, plus a hand-rolled decoder that streams the input by position
so it works under WASM as well as native.
## Exports
- `Json.encode(value: any, indent?: string, currentIndent?: string) -> string` — compact encode. Functions / unknown types and NaN/Inf encode as `null`.
- `Json.encodePretty(value: any, indentStr?: string) -> string` — pretty-printed encode with sorted object keys (diff-friendly).
- `Json.encodeArgs(...: any) -> string` — encode varargs as a JSON array.
- `Json.decode(str: string) -> any` — decode a JSON string. Returns the decoded value, or `nil` + error message on failure.
## Usage
```luau
local Json = require("@builtin::modules.json")
local widget = { type = "button", text = "Click Me" }
local compact = Json.encode(widget) -- '{"text":"Click Me","type":"button"}'
local pretty = Json.encodePretty(widget, " ")
local decoded = Json.decode(compact)
local v, err = Json.decode("oops") -- v = nil, err = error message
```
## Notes
- Object keys are sorted alphabetically in both encoders for consistent
output across runs.
- Numeric keys on objects are stringified at encode time (JSON has no
numeric keys). Pure-integer key sets get detected as arrays via
`isArray` and encoded with brackets.
- NaN, +Inf, -Inf encode as `null` — JSON has no representation. Round
trips through `decode` recover `null` (Lua `nil`), so they don't
preserve.
- Unicode `\uXXXX` escapes decode to UTF-8 by hand to stay WASM-safe.
Only the BMP is covered; supplementary planes via surrogate pairs
are not.
- Functions encode as `null`.
- Decode is character-streamed — no regex, no `string.match` patterns
on the whole input — so the line-and-column information needs to be
reconstructed from the position offset.
▲ 0↑ born
# 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
# 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 · models/xr/generic_hand_left.bundle
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
+ 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.