▣
module · 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.
Sub-parts
Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.
Showing the 199 items nearest this part. It holds more than this page lists.
199+items
# Module (asset type)
A module is a reusable, stateless (or globally-stateful) Luau library
that other code pulls in with `require()`. Modules are the unit of
factored-out logic in a world: helpers, math libraries, format
parsers, registries, shared algorithms.
## When to use one
- You have logic you want to share across multiple components,
services, scenes, or tools.
- You have a registry, lookup table, or shared cache that callers all
read from / write to.
- You want a stable API surface (`M.fn`) other code can depend on.
If the logic is bound to one entity, use a `.component`. If you want a
named UI of fields rather than functions, use a `.preset`.
## Where it lives
- Source: `/zero/source/.../<name>.module/`
- Identity: the source path with `/` written as `.` and each segment's
type suffix dropped — `/zero/source/demo/beats.module` is
`demo.beats`, and `/zero/source/kit.package/beats.module` is
`kit.beats`, the container's name kept and its `.package` suffix
gone. `asset.create` returns this string, `asset.resolve(...).identity`
reads it back, and the `require` forms below write it as
`<identity>`.
- Folder shape:
- `init.luau` (or `init.lua`) — module body. **Required.** Returns
a table (conventionally named `M`).
- `README.md` — instance-level documentation. **Required.**
- `.metadata` — agent-editable tags + free-form fields. **Required.**
## How to create one
```luau
asset.create("module", "<name>")
-- Creates: /zero/source/<name>.module/
-- init.luau (canonical `local M = {} … return M`)
-- README.md (instance README template)
-- `folder` places it in a subfolder of /source, and that subfolder
-- leads the identity: `{ folder = "lib" }` produces `lib.<name>`.
-- `into` authors it inside a resolved container (a package, a toolbox),
-- whose own identity leads the same way:
asset.create("module", "<name>", { folder = "lib" })
asset.create("module", "<name>", { into = asset.resolve("myPkg.package") })
```
## How it operates
1. **Registration.** Writing `init.luau` into a `.module/` folder
indexes the asset and registers the module path with the resolver.
2. **Loading.** The first `require("<identity>")` runs
the file's body and caches the returned table. Subsequent requires
from anywhere return the **same table** — this is the
shared-state lever for "module-local" registries. The body runs in
an environment of the module's own: a name it declares without
`local` is readable by the module's own functions and by no other
script, so an export reaches its callers through the returned table
and nothing else. Requiring a component's folder runs its chunk the
same way, once, and its `update`, `start` and `public` stay in that
environment; a component runs on an entity through
`entity(id).component.add(name)`.
3. **Hot reload.** Editing the file re-runs the module body and folds
what it returns into the table already loaded, so that table keeps
its identity: a caller that captured it with `require` reads the new
source's functions and values through the reference it already
holds, and a later `require` returns that same table. A key the new
source drops goes away; a key something set at runtime stays. The
body's own `local`s start again from their initial values, so state
that must outlive an edit lives in `modules.state()`
(`guides { path: "core/scripting-and-tasks" }`).
4. **Identity resolution.** A module is reached by its `<identity>`,
and where the caller lives decides which spelling of it resolves.
For a module in a **world**, from a caller in that same world —
another module, a component, a scene entrypoint, or an `execute`
chunk:
- `require("<identity>")` — the identity on its own, resolved
against the caller's own root. It carries every folder segment,
so a module created with `{ folder = "demo" }` is
`require("demo.beats")`.
- `require("@root::<identity>")` — the same root-relative
resolution, written out.
- `require(".sibling")` — a module in the same folder, by its bare
name; `..name` steps up one folder, `...name` two.
Logs, stack traces and hot-reload notices name that same module
`@local.source.<identity>` — its key under the world source root.
The `<identity>` tail of that key is the form above, so a trace
reading `@local.source.demo.beats` is `require("demo.beats")`.
For a module in a **library** — content under
`/zero/source/libs/@<lib>/` — `@<lib>::` addresses that library's
root from anywhere: `require("@builtin::modules.transform")`.
Mounting a world as a library moves its files under such a root and
flips the caller root to `@<lib>::`, which is what keeps the
root-relative forms above resolving across the move.
## Discovery
- `asset.list("module")` — every registered module.
- `asset.inspect("<name>")` — public functions, source path, this
type README.
- `ls /zero/source/<name>.module` lists the files the folder holds.
## Authoring conventions
- Return a single `M` table from `init.luau`. Top-level statements
with side effects run on first require — useful for one-time
initialization, dangerous if they touch the engine before it's
ready.
- Write exported functions as `typed function`, not `function`: the
argument types are enforced at the call and the checker knows the
signature, so a caller's mistake is reported where they made it.
- Annotate exported functions with `--!desc` / `--!arg` / `--!return`
/ `--!example` so the LSP, `tools.list`, and `asset.inspect`
surface them. A function with no `--!desc` is reachable only by
someone already reading this file.
- Declare a VALUE with `Field.<kind>(default, mode, description)`, and
a table whose members are reached through a metatable with
`--!members <TypeName>` — the latter gives the surface its accepted
member set, its type, and its documentation from one table. See
`guides { path: "core/authoring" }` for all four rules and
`modules/api/engine/entity.module/members.module` for a worked
member table.
- Keep module state in module-local upvalues or in `modules.state()`.
A name the body declares without `local` lives in the module's
environment, which every run of the module shares: a function the
previous run left in place reads what the reload assigned. Module-locals start again from their
initial values with each hot reload; the table `modules.state()`
returns is the same table on both sides of every reload.
- Prefer focused modules over kitchen-sink modules. If two halves of a
module have no shared state, split them.
## Common pitfalls
- **Cyclic requires.** A `require` chain that loops will return the
partial table (the half built before the cycle was detected).
Design for one-way dependencies.
- **Engine-time side effects.** Don't `entity.spawn` at module
top-level — the engine may not be ready. Expose an `M.init` and
call it from a scene entrypoint.
- **Hot-reload + module-local cache.** A module that caches
expensive computation in upvalues loses that cache on reload —
fine for dev, but be aware in performance work.
- **`init.lua` vs `init.luau`.** Either works, `.luau` is canonical.
- **Writing one while play runs.** A `/source` write made while play is
running lands on the play shadow, and a guarded play-exit discards it
unless it is kept. One `.module` write shadows `init.luau`, the
`README.md` and the `.metadata` together.
`vfs.write(path, bytes, { durable = true })` skips the shadow, and
`vfs.promotePlayShadow` keeps what is already on it — the `core/vfs`
guide has the model and what each route costs.
## Related types
- `.component` — for entity-bound state + lifecycle hooks.
- `.service` — generates content you don't have yet (a mesh, sound, texture, …) via a metered provider.
- `.tool` — for a single agent-callable function with a YAML schema.
- `.package` — to group several modules + components + scenes into
one shippable folder.
## In the Inspector
Exports: what the module exports (name, kind, signature), parsed from its source without running it, with **Check** (the LSP's diagnostics for its entry file) and **Open**.
▲ 0↑ born
# prefab
Entity prefabs and bundles: the records a spawned hierarchy is made of, and the asset they persist as.
The definitions live in `proc.ops.prefabs`; this pack is what puts them
in the op registry, so `procgen.searchNodes("", { pack = "prefab" })` lists the family
and `procgen.describeNode("op.<id>")` describes any one of them. Each `prefab.*` op
also answers to the same id with `template` in place of `prefab`.
## Ops
- `prefab.empty`
- `prefab.add_component`
- `prefab.from_geometry`
- `prefab.from_geometries`
- `prefab.from_instances`
- `prefab.from_population`
- `prefab.from_asset`
- `bundle.from_population`
- `bundle.from_asset`
- `bundle.from_prefab`
- `bundle.from_instances`
- `bundle.join`
- `bundle.write`
- `bundle.spawn`
- `prefab.entity`
- `prefab.parent`
- `prefab.link`
- `scene_fragment.write`
- `part.merge`
- `part.unpack`
▲ 0↑ born
# prefab (nodebox)
Prefabs: the entity records a spawned hierarchy is made of, as record and list values. prefab.empty makes a root, prefab.entity, prefab.parent, prefab.add_component and prefab.link shape the hierarchy (each calls prefab.edit, which holds the record-list edits and the name lookup that prefab.find answers), and prefab.from_geometry, prefab.from_geometries, prefab.from_instances, prefab.from_population and prefab.from_asset make prefabs from what a graph built. The output node shows a prefab and the export node saves it as a bundle.
prefab.record and prefab.find are the parts the others are built from: one entity record, and a lookup of an entity by name.
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
# component
Components an element carries into the scene, their fields set from constants,
animated values or other nodes' outputs.
`procgen.searchNodes("", { pack = "component" })` lists the family and `procgen.describeNode("op.<id>")`
describes any one of them.
## Ops
- `component.set` — give every element of a class a component, native or
script, with its fields given by name: a constant, an animated value, or a
node's output wired in, such as the Material a `material.shader` node made.
▲ 0↑ born
# sweep
Building a surface from a curve: sweeping a profile along it, revolving it, lofting between them.
The definitions live in `proc.ops.sweep`; this pack is what puts them
in the op registry, so `procgen.searchNodes("", { pack = "sweep" })` lists the family
and `procgen.describeNode("op.<id>")` describes any one of them.
## Ops
- `geometry.sweep`
- `geometry.revolve`
- `geometry.loft`
▲ 0↑ born
# instances
Placing copies of a source on points, varying them, and realizing, drawing or spawning the result.
The definitions live in `proc.ops.instances`; this pack is what puts them
in the op registry, so `procgen.searchNodes("", { pack = "instances" })` lists the family
and `procgen.describeNode("op.<id>")` describes any one of them.
## Ops
- `instances.on_points`
- `instances.randomize_transform`
- `instances.pick_random`
- `instances.pick_by_attribute`
- `instances.realize`
- `instances.spawn`
- `instances.draw`
- `instances.cull`
- `instances.set_material`
- `instances.transform`
- `instances.from_bundle`
▲ 0↑ born
# emit
Shapes particles are born from. Each op makes a point cloud, and each point
carries `N`, the direction pointing out of its shape. Wire the cloud into
`sim.spawn`: a particle is born on one of its points, and the source's `speed`
launches it along that point's `N`.
`procgen.searchNodes("", { pack = "emit" })` lists the family and `procgen.describeNode("op.<id>")`
describes any one of them.
## Ops
- `points.sphere` — points in a ball, or on its surface with `shell = 0`. `N`
points straight out from the centre.
- `points.cone` — points spread evenly across a cone's angle around an axis.
With no `length`, every point sits at the apex: a nozzle whose `N`s fan out
across the cone. A `length` fills the cone's volume out to that distance.
- `points.cylinder` — points in a cylinder, or on its side with `shell = 0`.
`N` points straight out from the axis.
- `points.on_mesh` — points scattered evenly by area over a mesh's faces. Each
point carries every attribute the mesh holds where it landed:
- point and vertex values are blended across the face;
- a face's own values come from that face;
- values that cannot be blended come from the nearest corner.
`N` is the mesh's normal there, or the face's own normal when the mesh
carries none. Particles born on a painted mesh take its colour with them.
```lua
local g = proc.graph("jet")
g:node("nozzle", "points.cone", nil, { count = 256, direction = { x = 0, y = 1, z = 0 }, angle = 12 })
g:node("src", "sim.spawn", { points = "nozzle" }, { rate = 400, speed = { x = 4, y = 7 } })
g:node("sim", "sim.particles", { sources = { "src" } })
```
▲ 0↑ born
▲ 0↑ born
# field.distance (procNode)
How far each element's position is from a target, as a Float field. It makes the target the wired `b` field or,
when `b` is left unwired, the fixed `point` as a constant field (`value.fallback`, `field.constant`), then
measures with `field.vector_math` in `distance` mode from `a` to that target. The result compiles to the device with
the rest of the field chain.
## Inputs
- `a` (`Field<Vec3> | Vec3`): the positions being measured, one per element.
- `b` (`Field<Vec3> | Vec3`): what each element is measured to, per element, so the target can move with the
element. Wired, it stands in for `point`.
- `point` (`Vec3`, default `{ 0, 0, 0 }`): one fixed position every element is measured to when `b` is unwired. It is
how a radial falloff around a place is built.
## Outputs
- `field`: the distance, a Float field.
▲ 0↑ born
# topology
What a mesh is connected to: edges, boundaries, islands, adjacency, and conversion between element classes.
The definitions live in `proc.ops.topology`; this pack is what puts them
in the op registry, so `procgen.searchNodes("", { pack = "topology" })` lists the family
and `procgen.describeNode("op.<id>")` describes any one of them.
## Ops
- `topology.edges`
- `topology.boundary`
- `topology.islands`
- `topology.adjacency`
- `topology.convert`
▲ 0↑ born
▲ 0↑ born
# strip
Surfaces laid along curves. `geometry.ribbon` lays a flat ribbon of a width
along every curve of a geometry, lying square to `up`. It is what a crack
glowing in the ground, a trail, a beam, a tyre mark or a road is drawn with.
Each edge of a ribbon carries every attribute of the curve point it was laid
from, so a field or a material reads `generation`, `heat` or anything the curve
carries across the ribbon. The mesh's uv is laid for a shader:
- `uv.x` runs 0 to 1 along each curve, by distance;
- `uv.y` runs 0 to 1 across it, so a hot core down the middle and a soft edge
come from `uv.y` alone.
Width can change along a ribbon:
- `taper` narrows each ribbon toward its end, the way a crack thins to its tip;
- `widthBy` names a Float point attribute that scales the width where the points
carry it — for example a branch's width from its `generation` through a ramp,
or a trail thinning with age.
`lift` raises the ribbon off its curve along `up`, so one laid on a surface
draws above it.
`procgen.searchNodes("", { pack = "strip" })` lists the family and `procgen.describeNode("op.<id>")`
describes any one of them.
```lua
local g = proc.graph("fissure")
g:node("line", "curve.line", nil, { start = { x = -1, y = 0, z = 0 }, finish = { x = 1, y = 0, z = 0 }, segments = 8 })
g:node("cracks", "curve.branch", { curve = "line" }, { generations = 3, branches = 2, normal = { x = 0, y = 1, z = 0 } })
g:node("glow", "geometry.ribbon", { curve = "cracks" }, { width = 0.05, taper = 0.9, lift = 0.004 })
g:output("glow", "glow")
```
▲ 0↑ born
# instances (nodebox)
Instances: pack a geometry into a piece (instances.pack), place a piece set on target points (instances.to_points) and take a population or piece set apart (instances.unpack). A piece is one point whose `Mesh` holds a geometry whole, as the node that made it left it: a geometry whose elements are counted on the device (what `geometry.triangulate` makes) keeps its counts there, and draining the piece set (`Geometry.drain`) drains the geometries its records hold with it, so a CPU reader reads each piece's elements; `geometry.merge` with `pack` puts several together into a piece set. The copies `instances.to_points` makes are a population, which the output node draws as instanced draws, one per piece and material, and export saves as a population asset; any other points stand as one entity each.
instances.pack, instances.to_points and instances.unpack are the nodes of that chain, with `points.pose` composing the instance attributes (`P`, `orient`, `scale`, `N`, `v`, `up`, `pscale`, `rot`, `trans`, `pivot`) the copies stand at.
Earlier nodes of the nodebox stand a source on points (instances.on_points), deal one of several sources to each point (instances.pick_random, instances.pick_by_attribute), vary and move the copies (instances.randomize_transform, instances.transform, instances.set_material) and merge them into one geometry (instances.realize). The points they make carry a `Mesh` and stand as entities through the output node.
instances.place, instances.variants, instances.turn and instances.sources are the parts the others are built from: a transform written onto points, a variant index written onto points, the product of two rotations, and the geometries a population stands (the ones given, else the ones its points carry).
▲ 0↑ born
# scene
Reading the scene into a graph. These nodes pull from the scene; every other
node takes what they make as an input.
A graph is evaluated in its own space, whose origin is the entity it stands on
— a Generator's. An effect built at the origin therefore works wherever it is
played, with nothing wired, and a pose brought in from the scene lands in that
same space unless a node asks for the world's.
## Nodes
- `scene.query` — entity ids out of the scene: by Name or a glob over Names, by
a component they carry (native or script), as the children, descendants or
parent of other entities, or the camera the scene is seen through. Its `of`
and `within` take another query's ids, so queries compose. It outputs `ids`
and their `count`.
- `scene.fetch` — the entities `ids` names as points, one each, in that order.
Every point carries its entity's id as `entity`; `transform` brings the pose
as `P`, `orient` and `scale`, in the graph's space, the world's or each
entity's own under its parent (`space`); `components` brings any component
as an attribute named after it holding its record. Those are the attributes
`proc.translate` writes back to an entity, so what a graph fetches, edits and
outputs lands back on the same components. The pose follows the entities,
and the graph's origin, as they move, without a cook.
- `scene.input` — scene.fetch on the entity the graph stands on, always. Its
pose in the graph's space is the origin; `space = "world"` says where the
graph stands. A graph standing on no entity has no point here.
- `scene.water`: the water the scene holds, read as it stands each frame:
every surface a water system provides. Wired into `terrain.drape`'s `ground`,
it sets points on the water, each taking the surface's height and normal and
marked `wet`, and they follow the waves.
`procgen.searchNodes("", { pulls = "scene" })` lists every node that pulls from the scene,
these and the ones reading a particular part of it (`terrain.ground`,
`sim.scene_colliders`).
▲ 0↑ born
# procgen
The procedural-generation tool family: find the nodes a graph is built from,
edit a graph's code, preview it, try values for its inputs, inspect it, spawn a
live generator to iterate in the scene, and watch cooks as they run. A graph is
the code of its `init.luau` (`proc.define(function(g) ... end)`); the tools here
read that code, edit it, picture it and run it.
Each tool is called as `tools.use("procgen", "<tool>", ...)` from Luau, as
`zero procgen <tool>` in the engine shell (`zero procgen <tool> --help` gives its
arguments), and through `use_tool` over MCP. A tool's README and
`describe_tool` give its arguments and what it answers.
## Discovery
- `searchNodes`: find nodes, the counterpart of `search_tools`: every node a
graph calls (`g.<family>.<name>`, and the ops of packs as `g.op.<id>`), by
keyword over names, families, inputs, outputs and descriptions and, in a
session bound to a world, by meaning through ZeroMind. With no query it
names every family and the nodes in it.
- `describeNode`: one node in full, the counterpart of `describe_tool`: what
it does, its inputs in call order with their types, defaults, ranges and
options, its outputs, its family, the call a graph writes and, for an op of a
pack, what it costs.
- `search` — free-text search over the `.procGraph` graph library (name /
id / description / tag / category).
- `find` — discover graphs by what they *produce* (an output type or
producing-op substring, e.g. `{ produces = "Bundle" }`).
- `inspect` — one node's op, sockets, producers, and consumers.
- `coverage` — every current op with the new node that replaces it and its
parity state, and every library graph with the worst state among the ops it
names.
- `stats` — structural counts (nodes / composites / inputs / outputs),
plus evaluated-output stats when a `target` is given.
- `trace` — the dependency chain for a target, in evaluation order.
- `explain` — an agent-oriented natural-language summary of a graph.
- `diff` — a structural diff between two graphs (changed/added/removed
nodes, per-node deltas).
- `validate` — check a graph the way its cook will: that its source builds,
its connections (type mismatches, missing inputs, unknown nodes and params),
then what a cook of its first frame refuses (bounded by `timeoutMs`), and the
nodes that run on the CPU every frame.
- `generators` — survey every live procedural generator in the scene:
bound graph, params, realized child count, and cook state.
## Edit the code
These edit a graph's (or node's) `init.luau` as code and write it only once it
builds, so the graph stays valid. A section is a run of statements named by the
lines it spans; a call is named by the local it declares or a line it spans.
- `collapse`: lift a section (a few node calls doing one job) into a node of
its own: its inputs are the values the section reads, its outputs the values
the code after it reads, and the section becomes one call of the new node.
The graph cooks to the same result, and `searchNodes` finds the node.
- `flatten`: the inverse: write a node's body out in place of a call to it
(one call, every call of a node, or every call of a node outside the
built-in library).
- `reference`: put a call of an existing node in place of a section.
- `promote`: make a part (a `.procGraph` called by its path) a node found by
name, which stays the part's.
- `patch`: set and unset what one call is given.
- `modify`: structural edits as one change: add, remove and repoint calls,
rename locals, declare inputs and outputs.
## Try it
A tool that takes a graph to preview or run takes it by its VFS path or its
name, or as a graph object (what `proc.define`, `proc.graph` and `proc.load`
return); `spawn` and `play` take a saved graph, since a generator plays an asset.
- `tweak` — preview a graph with other values for its declared inputs, in a
single step, leaving its source as it is: the knob-tuning loop.
## See it / make it real
- `preview` — evaluate one target (an output or a node) of a graph and render
it for any output type (Geometry, points, Field heatmap, Material ball,
Texture, Bundle); the picture comes back in the result beside its stats.
- `spawn` — spawn a live procedural generator from a saved graph: an entity
with a Generator component bound to it, which cooks the graph on its own
runner and cooks again as its params change.
- `adjust` — adjust a live generator in place: merge param overrides or
select another output.
- `apply` — swap a live generator in place for the asset it makes: its
current output becomes a durable bundle, and an Asset component standing it
replaces the Generator, like applying a modifier.
- `bake` — evaluate one target of a graph and realize it straight into the
scene as durable content, without a live generator.
## Play it
- `play` — play a graph on the preview stage with nothing added to the scene: it
plays from its first frame with the inputs given (each a value or a path over
the play's time), for `seconds`, and answers the profile of that play and a
picture of what it draws at each time of `at`.
## Cook observability
- `cooks` — the scene-wide cook monitor: every in-flight cook, a bounded
history of recently finished ones, with progress (the nodes cooked so far and
the node running now), status, duration, and — on failure — the error and the node
that broke, and the frames generators presented most recently.
- `cancel` — cancel an entity's in-flight cook; it stops at its next
checkpoint and the previous output stays realized.
- `onPresent` — be told the moment a generator presents the frame you
describe (an output, a graph time or a time within a repeating cycle, an
element count), with the probe's own options beside the terms: `capture` a
picture of that frame (what the generator draws alone, framed to fit, over
the grey studio backdrop), `freeze` the generator and the world on it (the
generator plays on with `generator:play()`), a `when` and `run` of your
own. It arms a probe, so `probe.list`, `probe.hits` and `probe.cancel`
manage it.
- `frames` — see how an effect goes: a picture of the frame a generator
presents at each graph time you name (or `count` of them from `from` to
`to`), each of what it draws alone, framed to fit, over the grey studio
backdrop an asset preview is photographed in, with the frame, how it was
presented and each output's element count beside it. A time past the end
of a timeline shows its last frame, with a `note` saying so; a picture of
a frame that draws nothing, or one that holds the backdrop alone, says so in
its own `note`. A generator whose cook failed raises, naming the node and
the reason. The clock goes back where it stood after.
- `profile` — what evaluating graphs has cost, read from what was recorded as
the work ran: every graph's run and wait time, its nodes costliest first,
the verdicts on what was paid again, saved runs to compare against, and one
graph invoked once with what that invoke cost.
▲ 0↑ born
# texture
Textures generated from noise and gradients, and the PBR material that consumes them.
The definitions live in `proc.ops.textures`; this pack is what puts them
in the op registry, so `procgen.searchNodes("", { pack = "texture" })` lists the family
and `procgen.describeNode("op.<id>")` describes any one of them.
## Ops
- `texture.noise`
- `texture.gradient`
- `texture.from_raster`
- `texture.from_shader`
- `material.pbr`
▲ 0↑ born
# value (nodebox)
Single values in a graph: value.math and value.compare do arithmetic and comparison with the operation picked by a parameter, value.expr follows a Luau expression over the graph's clock, value.get reads one field of a record by a dotted path, and value.switch picks one of two values by a Bool. value.present and value.fallback are how a graph used as a node handles an input its caller may leave unwired: present answers whether anything is wired, fallback takes the wired value or a fallback.
▲ 0↑ born
# part (nodebox)
part.merge packs several values into one part, each piece keeping what it is; part.unpack gives one piece back by its name.
▲ 0↑ born
# curve
Curves as geometry. A curve is a primitive of a Geometry: a run of its points,
in order, open or closed, that is a line rather than a surface. A mesh built
from the geometry draws its faces and not its curves. The curve's points are
points like any other, so every point and field op moves them. One geometry
holds any number of curves (the branches of a crack, the strands of a bolt),
and the ops that sample, sweep or grow along lines read every one.
The definitions live in `proc.ops.curves`; this pack is what puts them in the
op registry, so `procgen.searchNodes("", { pack = "curve" })` lists the family and
`procgen.describeNode("op.<id>")` describes any one of them.
## Ops
- `curve.line` — a straight curve from `start` to `finish`, in `segments`
spans.
- `curve.circle` — a closed circular curve in the XZ plane.
- `curve.bezier` — a cubic Bézier curve from four control points.
- `curve.from_points` — one curve through a geometry's points, in their order;
the points keep every attribute they carry.
- `curve.smooth` — every curve rounded into a Catmull-Rom spline through its
own points.
- `curve.resample` — every curve laid out again with points evenly spaced by
arc length, a set number per curve or as many as a spacing fits.
Move curves with `geometry.transform`, displace them with
`geometry.set_position` and a field, and join them with `geometry.append`.
`points.on_curve` samples points along them, and `geometry.sweep` carries a
cross-section along each one.
▲ 0↑ born
# field
Fields: a value per element, built from position, attributes, noise and maths, sampled where an op needs it.
The definitions live in `proc.ops.field`; this pack is what puts them
in the op registry, so `procgen.searchNodes("", { pack = "field" })` lists the family
and `procgen.describeNode("op.<id>")` describes any one of them.
## Ops
- `field.constant`
- `field.index`
- `field.position`
- `field.normal`
- `field.attribute`
- `field.random`
- `field.noise`
- `field.math`
- `field.vector_math`
- `field.compare`
- `field.mix`
- `field.map_range`
- `field.distance`
- `field.swizzle`
- `field.combine`
▲ 0↑ born
# math
One node per operator and per function of Luau's `math` table that is a function of its numbers alone: add, sub, mul, div, mod, pow, neg, abs, min, max, floor, ceil, round, sqrt, sin, cos, tan, asin, acos, atan, atan2, sinh, cosh, tanh, exp, log, log10, fmod, ldexp, frexp, modf, noise, map, sign, deg, rad, clamp, lerp, smoothstep, isnan, isinf, isfinite, length, normalize, dot, cross, vec2, vec3, vec4, the comparisons (lt, le, gt, ge, eq, ne, and `compare` over them), `where`, the integer functions (to_int, to_float, bor, bxor, shl, floordiv), and `concat` for text. `math.random` and `math.randomseed` have no node: they draw from state outside the graph, a graph that called one would cook differently each time, and a call of either in a graph is refused, naming `field.rand`, `field.random` and `rand.unit`, which draw from a seed.
Operators and `math.*` over sockets resolve to these nodes, and `vector.create` over sockets resolves to `math.vec2`, `math.vec3` or `math.vec4` by its component count; with plain numbers only, `math.*` and `vector.create` are Luau's own, and `vector.create` holds three components. Each node but `compare` (built from the comparisons) and `concat` (text) calls an operation of `proc.fieldc.operators`: over plain numbers and vectors it answers the plain result, over a Column or a Field it answers a Field (a fieldc template), so a chain of math nodes is one expression that the node consuming it evaluates in a single dispatch (`fieldc.evaluate`).
`math.min` and `math.max` over sockets take any number of operands, folded two at a time; `math.log(x, base)` is the `log` node with its `base`. Operands of different widths widen to the widest: a number applies to every component, and a shorter vector reads 0 for the components it lacks. Division, `floordiv`, `mod` and `fmod` by 0 answer 0, `sqrt`, `log` and `log10` of a value at or below 0 answer 0, `asin` and `acos` of a value past -1 or 1 read the nearest of the two, and `map` over an input range of no width answers its output minimum, so a chain keeps producing numbers. The orderings (`lt`, `le`, `gt`, `ge`) compare first components; `eq` and `ne` compare every component.
▲ 0↑ born
# entity
Reading the live scene into a graph: an entity's transform, points, geometry and bounds.
The definitions live in `proc.ops.entity`; this pack is what puts them
in the op registry, so `procgen.searchNodes("", { pack = "entity" })` lists the family
and `procgen.describeNode("op.<id>")` describes any one of them.
## Ops
- `entity.transform` — one entity's pose, as a value.
- `entity.points` — a list of entities, or a parent's children, as ordered
points: `scene.fetch` in the world's space or under each parent, with each
entity's own `up` beside its pose.
- `entity.geometry` — the mesh one entity's Model draws.
- `entity.bounds` — one entity's bounds, or its hierarchy's.
`scene.query` and `scene.fetch` (the `scene` pack) read any number of entities,
with any of their components, in the graph's own space.
▲ 0↑ born
# ramp
A ramp is a value that changes over 0..1. It is how an effect makes one thing
follow another: colour over life, size over speed, opacity over height.
`procgen.searchNodes("", { pack = "ramp" })` lists the family and `procgen.describeNode("op.<id>")`
describes any one of them.
## Ops
- `ramp.keys` — a ramp built from keys. Each key is `{ t, value }`, and the
value can be a `Float`, `Color`, `Vec2`, `Vec3` or `Vec4`. Between keys the
value moves in one of three ways:
- `linear`: a straight line from key to key;
- `smooth`: eased into and out of each key;
- `constant`: each key's value held until the next key.
- `ramp.blackbody` — the colours a blackbody glows at across a temperature
range, as a Color ramp: red embers through orange flame to white. `glow`
makes the hotter keys brighter; 4 follows the fourth power of temperature
that real blackbody radiance does.
- `ramp.sample` — a ramp read at every element, at the position along it a
0..1 field names. The ramp compiles to the element's dispatch, so a
solver's particles read it on the device.
- `field.ramp` — a ramp read at every element by a value it carries: a graph
of `field.attribute`, `field.map_range` and `ramp.sample`. By default each
element reads it by its own `life` attribute, so particles change over their
lifetime. `by` names any other attribute, `range` maps that attribute's
values onto 0..1, and wiring `t` reads the ramp by any field.
A ramp is a graph value like any other. It can be wired, animated (its keys
are params) and cached.
```lua
g:node("fire", "ramp.blackbody", nil, { from = 900, to = 4000, glow = 1 })
g:node("tint", "field.ramp", { ramp = "fire" })
g:node("lit", "attribute.set", { domain = "sim", value = "tint" }, { name = "Cd" })
```
▲ 0↑ born
# sink
The sinks: a generated mesh, texture, material or population written out as a real asset.
The definitions live in `proc.ops.sinks`; this pack is what puts them
in the op registry, so `procgen.searchNodes("", { pack = "sink" })` lists the family
and `procgen.describeNode("op.<id>")` describes any one of them.
## Ops
- `mesh.write`
- `texture.write`
- `material.write`
- `population.write`
- `debug.dump`
- `debug.validate`
▲ 0↑ born
# core
The parts the other procgen nodes are built from: connectivity (edges, boundary, adjacency, islands, owner, corners), normals, weld, select, stitch, curve measure and resample, sampling, neighbours, realize, the raster set (`raster.new`, `raster.read`, `raster.stencil`, `raster.sample`, `raster.add_layer`, `raster.remove_layer`, `raster.reduce`, `raster.column`, `raster.resolve`, `raster.blend`, and `raster.cells` under them), `column`, and `entity.read`, which brings scene entities in (assets come in through `io.import`).
Each is a graph of the four ops, the kernels of `kernels/` and other core nodes. `join` holds the module that places the parts of a join, called by `concat` and `realize`.
A graph calls them as `g.core.<name>`, for instance `g.core.edges`. A node with several outputs answers a record: `local faces = g.core.normals.face(...)` then `faces.geometry`, `faces.scaled`. Every node's README states its inputs and outputs.
▲ 0↑ born
# group
Named selections over a mesh's points, vertices, edges or faces, for other ops to act on.
The definitions live in `proc.ops.groups`; this pack is what puts them
in the op registry, so `procgen.searchNodes("", { pack = "group" })` lists the family
and `procgen.describeNode("op.<id>")` describes any one of them.
## Ops
- `group.create`
- `group.by_normal`
- `group.by_bounds`
- `group.by_random`
- `group.by_island`
- `group.combine`
- `group.invert`
- `group.expand`
- `group.shrink`
- `group.list`
- `group.info`
- `group.remove`
▲ 0↑ born
▲ 0↑ born
# subdivide
Catmull-Clark style subdivision: more faces where a surface needs to be smooth.
The definitions live in `proc.ops.subdivide`; this pack is what puts them
in the op registry, so `procgen.searchNodes("", { pack = "subdivide" })` lists the family
and `procgen.describeNode("op.<id>")` describes any one of them.
## Ops
- `geometry.subdivide`
▲ 0↑ born
# asset
The one node an asset enters a graph through.
## Ops
- `asset` — an asset's content as a graph value, in the form its type states:
a mesh is a Surface, a texture is a Texture. The asset is read when the node
cooks.
An asset type states the conversion in the `graph` its `behavior.luau`
declares: `read(ref)` answers the value, and `write(value, name, place)`
persists a value as that asset again. The sinks that write a value back
(`mesh.write`, `texture.write`) go through the same `write`, so what enters a
graph and what leaves it are one conversion.
```lua
local rock = g:node("rock", "asset"):param("asset", "@builtin::meshes.sphere")
local heights = g:node("heights", "asset"):param("asset", "terrain_height")
```
`procgen.searchNodes("", { pack = "asset" })` lists the family and `procgen.describeNode("op.asset")`
describes it.
▲ 0↑ born
# Scatter (node)
A scattering node: its `init.luau` holds the graph that does the work, so the declared inputs are this node's inputs
and the declared output is this node's output. The node is its pipeline, in one self-contained asset.
It makes points on the surface (`points.on_surface`, area-weighted), gives every point its own random yaw, lean and
scale (`instances.randomize_transform`), and places the source on those points (`instances.on_points`). The
transform is read from the same point cloud the instances sit on, so each copy takes the pose of its own point.
A node is findable by what it does: the `procgen` toolbox's `ops` with the query `scatter` lists it beside
`points.on_surface` and `instances.on_points`. Editing this node's `init.luau` cooks again every graph that calls it.
## Inputs
- `surface` (Geometry): what the copies land on.
- `source` (Geometry): the shape being copied.
- `count` (Int, default 200): how many.
- `seed` (Int, default 1): changes the arrangement.
- `scaleMin` / `scaleMax` (Float, defaults 0.8 and 1.4): per-copy size range.
- `tilt` (Float, default 0.15): radians of random lean.
## Output
- `instances`: a population, the points each carrying the source as its `Mesh`.
## Authoring a node
A node is a graph returned from a `.procNode`'s `init.luau` (`proc.define`). `asset.create("procNode", name)`
scaffolds that shape, and `procNode.assetType/README.md` describes what a node states and how graphs call it.
▲ 0↑ born
# oceans
A sea and the ground under it, as one package: the wave simulation, the surface
that renders it, the seabed it is seen through, and the answer to "how high is
the water here" that gameplay needs to float things on it.
```lua
local Ocean = require("@builtin::systems.oceans.modules.ocean")
Ocean.create({ preset = "gale" })
```
That is a whole ocean — waves out to the horizon, foam on the breaking crests, a
seabed below, and a surface that follows whoever is looking at it. To float
something on it:
```lua
local ocean = Ocean.forEntity(oceanEntityId)
boat.position = { x, ocean:sampleHeight(x, z), z }
```
It is a building block. The sun the water glitters under, the ambient its foam
sits in and the sky it reflects all come from the scene, through the same
directional light and reflection probes every other material in the engine
reads — so an ocean drops into a world that already has lighting instead of
bringing a second set, and changing the scene's light changes the water with it.
## What it does
The simulation is **spectral**: instead of animating individual waves, it builds
the sea's energy across every wavelength and direction from an oceanographic
spectrum, then inverse-transforms that into a height field. A real sea is what
comes out — wind speed alone decides wave height, and it rises with wind the way
a fully developed sea does, following the Pierson–Moskowitz law.
It runs in **cascades** — independent wave fields over different patch sizes,
each band-limited to its own range of wavelengths. A tile of roughly two
kilometres carries the long waves and one of a few hundred metres the short
ones; they sum into a surface with detail at every scale, and because the bands
are disjoint no wavelength is synthesised twice. Above them a **Gerstner bank**
adds the swell analytically, which no cascade can hold: a wave longer than its
own patch has nowhere to live, and swell arrives from a distant storm on its own
heading anyway.
**Foam** is not painted on. It comes from the Jacobian of the wave displacement:
where the surface folds over itself, the wave is breaking. Whitecaps appear
where and when the geometry actually breaks, accumulate into a persistent
world-space field, and streak behind the crest as they decay.
**Colour and transparency are the same mechanism.** Per-channel Beer–Lambert
extinction over the distance light travelled through water gives both — red dies
within a metre, blue survives tens. That is why shallow water over sand reads
turquoise and the open sea reads near-black blue without switching materials.
**The seabed** is one noise field read twice: scaled it is the terrain's relief,
unscaled it picks between sand and rock, so outcrops stand on the high ground
and sand fills the hollows. The sun's caustic web moves over both, bent by the
horizontal displacement of the water directly above it. Bodies land on it: the
pass that raises the ground writes the same field out as a height patch that
travels with the viewer, so what a body stands on is what the frame drew.
Nothing here ships an image. Every map the surface and the seabed sample — the
foam patterns, the sand, the rock, the caustics — is grown on the GPU at
startup.
## The pieces
| Asset | What it is |
|---|---|
| `components/Ocean` | Puts a sea on an entity. Its fields are the sea state; the simulation is rebuilt from them, so an ocean survives a scene reload. |
| `modules/ocean` | The substrate: `attach`, `create`, `forEntity`, `sampleHeight`, `sampleDisplacement`. |
| `components/Buoyancy` | Floats a rigid body on any water, inside the physics step. Its fields are the hull: how dense it is, how quickly it settles, and what displaces water (its colliders, a stated box or what it draws). |
| `modules/buoyancy` | The float: `attach`, `forEntity`, `list`, `clear`, `submergedFraction`, `restingY`. |
| `modules/oceanPresets` | Nine named sea states, from `calmClear` through `gale`. |
| `modules/oceanQuality` | The four tiers — how finely a sea is resolved, as distinct from what sea it is. |
| `modules/oceanClipmap` | The camera-following ring geometry both the surface and the seabed are built on. |
| `modules/oceanFloor` | The seabed: terrain, ground materials, caustics and the ground physics collides with. |
| `modules/oceanScene` | The seam between the scene capture and the materials that sample it. |
| `shaders/oceanSurface` | The water: cascade normals, absorption, exact dielectric Fresnel, subsurface scattering, sun glitter, foam, and the view from below. |
| `shaders/oceanFloor` | The seabed: sand and rock blended by terrain noise, under a caustic web. |
| `materials/oceanWater`, `materials/oceanSeabed` | The default looks. |
| `renderFeatures/oceanScene` | Copies the frame's colour and depth so the surface can refract what is behind it and measure its own water column. |
| `computeShaders/ocean_*` | The ten-stage GPU pipeline. |
## Wind is the control
Significant wave height goes as the **square** of wind speed, so that one field
does most of the work:
| Wind | Sea | Wave height |
|---|---|---|
| 3 m/s | glassy, a mirror | ~0.2 m |
| 11 m/s | working sea, occasional whitecap | ~2.5 m |
| 24 m/s | gale, breaking and streaked | ~12 m |
Everything else shapes character rather than size: `choppiness` pinches crests
into peaks, `standingWaveRatio` runs waves both ways instead of only downwind,
`jonswapGamma` sharpens a fetch-limited sea, and the foam layers decide how
readily a breaking crest whitens and how long it streaks.
Read `documentation/oceans.guide` for the full walkthrough, including the
seabed, buoyancy, reflections and what each tier costs.
▲ 0↑ born
# field.ramp (procNode)
A ramp read at every element by a value it carries: a particle's colour, size or opacity following its age. It takes
the value each element reads by (the wired `t`, else the element's own `by` attribute, through `value.fallback` and
`field.attribute`), maps it from `range` onto 0..1 (`field.map_range`, the ends read off `range` by `value.get`), and
reads the ramp there with `ramp.sample`, compiled into the element's dispatch. A value past either end of `range` reads
that end of the ramp.
## Inputs
- `ramp` (`Ramp`): the ramp to read.
- `t` (`Field<Float> | Float`): where along the ramp each element reads. Unwired, it is the element's own `by`
attribute.
- `by` (`String`, default `"life"`): the Float attribute each element reads the ramp by when `t` is unwired. A particle
solver's particles carry `life`, 0 at birth to 1 at death.
- `range` (`Vec2`, default `{ 0, 1 }`): the values of `t` that land at 0 and at 1 on the ramp, so a speed from 0 to 20
or a temperature from 300 to 3000 reads the whole ramp.
## Outputs
- `field`: the ramp's value at each element.
▲ 0↑ born
# decimate
Reducing a mesh's triangle count while holding its silhouette.
The definitions live in `proc.ops.decimate`; this pack is what puts them
in the op registry, so `procgen.searchNodes("", { pack = "decimate" })` lists the family
and `procgen.describeNode("op.<id>")` describes any one of them.
## Ops
- `geometry.decimate`
▲ 0↑ born
# bevel
Rounding an edge or a corner into a chamfer of a chosen width and segment count.
The definitions live in `proc.ops.bevel`; this pack is what puts them
in the op registry, so `procgen.searchNodes("", { pack = "bevel" })` lists the family
and `procgen.describeNode("op.<id>")` describes any one of them.
## Ops
- `geometry.bevel`
▲ 0↑ born
# geometry
Composing and measuring meshes: transform, join, merge, triangulate, voxelize, delete, separate.
The definitions live in `proc.ops.geometry`; this pack is what puts them
in the op registry, so `procgen.searchNodes("", { pack = "geometry" })` lists the family
and `procgen.describeNode("op.<id>")` describes any one of them.
## Ops
- `geometry.transform`
- `geometry.append`
- `geometry.recalculate_normals`
- `geometry.triangulate`
- `geometry.merge`
- `geometry.measure`
- `geometry.voxelize`
- `geometry.set_material`
- `geometry.set_position`
- `geometry.delete`
- `geometry.separate`
▲ 0↑ born
# editorSelection
Named selection scopes for the editor. A scope is an independently
tracked, ordered set of typed refs plus a primary (the last ref added
or clicked). Distinct scopes never clobber each other, so viewport,
outliner, inspector and asset browser share one answer to "what is
selected" per kind.
A ref is `{ kind: string, id: string }` — a stable id, never a display
string, so rename/move never invalidates a selection.
## Exports
- `M.scope(name) -> Scope` — get/create a named scope handle.
- `M.set(scope, refs)` — replace refs in order; primary becomes the last.
- `M.get(scope) -> { Ref }` — refs in click order (fresh array).
- `M.primary(scope) -> Ref?` — last-clicked ref, or nil.
- `M.clear(scope)` — empty a scope.
- `M.toggle(scope, ref)` — add if absent, remove if present.
- `M.add(scope, refs)` — add each ref not already present; primary becomes the last added.
- `M.remove(scope, refs)` — remove each of `refs`; primary becomes the last remaining or nil.
- `M.contains(scope, ref) -> boolean` — membership by `(kind, id)`.
- `M.count(scope) -> number` — the scope's selection size.
- `M.subscribe(scope, fn) -> handle` / `M.unsubscribe(scope, handle) -> boolean`.
- `M.context() -> { scope, refs, primary }` — last-focused scope, for command ctx.
## Usage
```luau
local Selection = require("@builtin::modules.api.editor.selection")
local scope = Selection.scope("entity")
Selection.set(scope, { { kind = "entity", id = "player_1" } })
Selection.subscribe(scope, function() refreshInspector() end)
```
## Notes
- State lives in a fixed `_G` slot, seeded pre-seal by the boot chain
(`prelude.luau`); it survives hot-reload and edit↔play flips. Mutations
after boot write into nested tables only.
- `set` / `toggle` / `clear` mark their scope as last-focused, which drives
`context()` and therefore which scope commands act on.
- The service holds the editor's selection state in Luau. Refs handed back by
`get` / `primary` / `context` are copies, so a caller can hold or mutate them
without touching internal state.
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
# Generator
A live procedural generator: it binds a `.procGraph`, plays the graph's
timeline and stands each of the graph's outputs in the scene as children of
the entity it is on. Change a param and the output is made again in place; in
edit and in play alike, nothing it makes is written to the world's source.
```lua
local graph = asset.create("procGraph", "crate") -- a box with a `size` input; edit its init.luau
local e = entity.spawn("crate")
e.component.add("Generator", { graph = graph })
e.component.get("Generator").params = { size = 2 }
```
## Fields
| Field | What it does |
|---|---|
| `graph` | The `.procGraph` it plays. |
| `params` | Overrides for the graph's declared inputs, by name. |
| `output` | One declared output to stand; empty, the default, stands every output, one root each. |
| `playOn` | What starts its clock in play mode: `"auto"` (default) plays a timeline with no end as soon as it binds and holds any other on its first frame; `"load"` plays on bind; `"manual"` waits for `play()`. In edit mode a Generator is not set to play, whatever `playOn` says: it shows the frame it is on until it is played. |
| `ending` | `"graph"` (default) does what the timeline declares; `"loop"` plays the range round and round; `"hold"` plays it once and stays on its last frame. |
| `speed` | How fast its clock runs: 1 its own rate, 0 still, negative plays a timeline with an end backwards. |
| `diskCache` | Whether the outputs the graph caches load from, and are written to, the world's disk cache. On by default. |
| `cookBudgetMs` | Milliseconds of cook work a frame before a cook yields to the next frame. 5 by default. |
## Methods
`play()`, `pause()`, `seek(seconds)`, `seekFrame(frame)`, `loop()`, `hold()`,
`setFps(fps)`, `setRange(first, last)` and `setWallTime(on)` drive its clock,
on every peer. `getFrame()`, `getTime()`, `describeGraph()` and `playback()`
read it; `playback()` also says what running the graph costs and how each
output is drawn. `cancelCook()` stops a cook in flight and leaves the last
output standing. `apply()` swaps the generator for the asset it makes: the
frame being presented becomes a bundle, and an `Asset` component standing it
takes the Generator's place. The `onFinished` event fires when a range played
once reaches its end.
A timeline that runs in real time (`realTime = true`) has an end when its graph
states `frames` beside it (`g:timeline({ realTime = true, fps = 60, frames = 180 })`)
or when `setRange` gives it one; with neither it runs on and never finishes.
When the playhead of one with an end reaches its last frame, `ending` applies as
it does to any timeline: `"graph"` loops where the graph declares `loop` and
otherwise holds on the last frame and fires `onFinished` once; `"loop"` plays it
again from the start, a fresh simulation each time, without firing it; `"hold"`
stays on the last frame. `play()` plays a held one again from its start.
`playback().ends` is whether the timeline has an end, and `last` is its last
frame. A real-time timeline begins again, by `seek(0)`, a loop or `play()`, in
the runs its solvers hold on the device: a restart costs a rewrite like any
other present, and opens no cook.
## Under an effect
An effect's play runs each graph of its entry on a Generator, a child of the
play's entity. `effectRef:play(entry, opts)` and the `Effect` component make
them and drive their clocks together, so a shot, an impact or a cast is a play
of an effect (`guides { path = "systems/effects/documentation/visual-effects" }`). The play's own
`playback()` adds its Generators' costs together.
Every play of one graph with the same params plays the frames the first one
cooked. A Generator of a graph with `playOn = "manual"` cooks every frame of the
timeline while it waits, and `playback().cookedThrough` reaching
`playback().last` says every frame is cooked.
## Related
- `guides { path = "systems/procgen/documentation/procgen" }`: graphs, ops,
timelines, effects and the live generator in full.
- The `procgen` toolbox: `spawn`, `adjust`, `apply`, `generators`, `cooks`,
`frames` and `onPresent` work on generators by entity, and `play` plays a graph on the preview stage.
▲ 0↑ born
# debug (nodebox)
debug.dump summarizes any graph value into a record; debug.validate checks a Geometry's structure and reports every issue it finds.
▲ 0↑ born
# sim
Simulation: what a solver's particles are born from, what pushes them and what
they bounce off, and `sim.particles`, the particle solver.
These are the sim context's nodes. What one of them makes is read by the
solver at every step, so it is wired into another sim node or into an input
that opens sim — `sim.particles`' `sources`, `forces` and `colliders`.
Wired anywhere else, `procgen.validate` reports it and the cook refuses it.
The definitions live in `proc.ops.sim`; this pack is what puts them in the node
registry, so `procgen.searchNodes("", { pack = "sim" })` lists the family and
`procgen.describeNode("op.<id>")` describes any one of them.
## Nodes
- `sim.spawn` — a spawn description: particles born on a set of points at a
rate and in bursts, with a starting velocity, a lifetime, a size and a stream
name.
- `sim.force` — a force the solver applies at every step, from any Vec3 field
evaluated where each particle is: as an acceleration (`mode =
"acceleration"`, scaled by `strength`), or as a velocity each particle is
pulled toward at `strength` a second (`mode = "velocity"`). Wind is a wind
field and drag is still air, both as velocities.
- `sim.collider` — a plane, a sphere or a box the particles bounce off.
- `sim.scene_colliders` — the scene's collision environment: every box, sphere
and capsule physics collider that is not a sensor, turned and moving with its
entity, solid from every side, and read where it stands in the graph's space
at every step. Its `within` takes only the colliders near its `center`; a
solver takes at most 128 shapes.
Every shape is met along the whole of each step's move, so a fast particle
does not pass through a thin one.
- `sim.particles` — the particle solver, a geometry node: every particle from
every spawn in one geometry on the GPU, stepped over the graph's timeline
under the forces and against the colliders, output as the particles alive
now.
```lua
local g = proc.graph("sparks")
g:node("emit", "points.sphere", nil, { radius = 0.1, count = 16 })
g:node("src", "sim.spawn", { points = "emit" }, { rate = 300, speed = { x = 3, y = 6 } })
g:node("curl", "field.curl", nil, { frequency = 0.8, amplitude = 4 })
g:node("swirl", "sim.force", { field = "curl" })
g:node("world", "sim.scene_colliders", nil, { bounce = 0.3 })
g:node("sim", "sim.particles", { sources = { "src" }, forces = { "swirl" }, colliders = { "world" } },
{ gravity = { x = 0, y = -9.8, z = 0 } })
g:output("points", "sim")
```
▲ 0↑ born
# extrude
Pushing faces out along their normals, and giving a surface thickness.
The definitions live in `proc.ops.extrude`; this pack is what puts them
in the op registry, so `procgen.searchNodes("", { pack = "extrude" })` lists the family
and `procgen.describeNode("op.<id>")` describes any one of them.
## Ops
- `geometry.extrude`
- `geometry.solidify`
▲ 0↑ born
# gpu
Field ops evaluated on the GPU: one dispatch over a whole field, matching the CPU result.
The definitions live in `proc.gpu`; this pack is what puts them in the op
registry, so `procgen.searchNodes("", { pack = "gpu" })` lists the family and
`procgen.describeNode("op.<id>")` describes either of them. `proc.gpu.eval` is a
convenience wrapper over the same registered ops.
## Ops
- `gpu.scale`
- `gpu.noise`
▲ 0↑ born
# shader
An author's own compute shaders as graph nodes. A `.computeShader` written for
anything else runs inside a graph unchanged: its bindings are the geometry's
columns, its `params:` are the node's knobs, and what it writes comes out as
columns every op after it reads.
`procgen.searchNodes("", { pack = "shader" })` lists the family and `procgen.describeNode("op.<id>")`
describes any one of them.
## Ops
- `shader.compute` — run a `.computeShader` over a geometry's elements, one
invocation per element. A buffer binding is the column of its own name, or the
one `bind` names: `access: read` reads it, `access: read_write` writes it and
makes it when the domain has none. The shader's `params:` take their values
from `uniforms`, where each can be animated or wired, and `count`, `time` and
`frame` are filled in when the shader declares them. The same shader placed as
two nodes reads each node's own columns and values.
- `sim.shader` — a solver whose step is a `.computeShader`: the domain's
columns are its state, the shader runs once per element at every fixed step
writing its `read_write` bindings in place, and `dt`, `time` and `frame` are
the solver's own.
## Writing a shader for a graph
The element index is `global_invocation_id.x`. A column's lanes are packed
`f32`s: a Float column binds as `array<f32>`, a Vec3 column as `array<f32>` read
three to an element, and a Vec2 or Vec4 column as `array<vec2<f32>>` /
`array<vec4<f32>>`.
```yaml
bindings:
- { name: P, access: read_write, element: f32 }
- { name: weight, access: read, element: f32 }
params:
- { name: lift, type: float, default: 1.0 }
- { name: count, type: int, default: 0 }
```
```wgsl
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
let i = id.x;
if (i >= u32(params.count)) { return; }
P[i * 3u + 1u] = P[i * 3u + 1u] + params.lift * weight[i];
}
```
```lua
g:node("raised", "shader.compute", { domain = "pts" }, { shader = "lift", uniforms = { lift = 2 } })
```
## Finding the points near a point
A shader that looks at its neighbours — separation in a flock, the car ahead in
traffic, a density — declares the three read bindings of a spatial grid and
includes the `grid` shader module; the node gets a `neighborRadius`, builds a
grid of the domain's points with cells that wide before it runs (and again at
every step of a `sim.shader`), and binds it. The shader then walks the 27 cells
around a point instead of every point.
```yaml
bindings:
- { name: P, access: read, element: f32 }
- { name: nb, access: read_write, element: f32 }
- { name: gcfg, access: read, element: f32 }
- { name: grange, access: read, element: u32 }
- { name: gorder, access: read, element: u32 }
```
```wgsl
#include "@builtin::systems.procgen.grid"
let c = zero_grid_cell(p);
for (var dz = -1; dz <= 1; dz = dz + 1) {
for (var dy = -1; dy <= 1; dy = dy + 1) {
for (var dx = -1; dx <= 1; dx = dx + 1) {
let cc = c + vec3<i32>(dx, dy, dz);
let span = zero_grid_range(cc);
for (var s = span.x; s < span.y; s = s + 1u) {
let j = gorder[s];
// j == i is the point itself; a j whose own cell is not cc
// shares a bucket with it and belongs to another cell
}
}
}
}
```
```lua
g:node("flock", "sim.shader", { domain = "birds" }, { shader = "flock_step", neighborRadius = 1.5 })
```
A step that reads its neighbours' state while every element writes its own
would read some of them half-updated. A read binding named `prev_<column>`
(`prev_P`, `prev_v`) is that column as it stood before the dispatch — a copy
`sim.shader` takes before every step — so each element reads the same
before-state and writes only its own:
```yaml
bindings:
- { name: P, access: read_write, element: f32 }
- { name: prev_P, access: read, element: f32 }
```
`points.neighbors` answers the common questions — how many neighbours, the
nearest, their centroid and their mean of an attribute — without a shader.
▲ 0↑ born
# grid
A grid is space cut into cells: a geometry whose points are the centres of its
cells, laid out x fastest, then y, then z, and whose point columns are its
channels (a density, a distance, a height, a colour). A grid one cell thick
along an axis is flat: the height over a ground, an image.
Because a grid is a geometry, everything that works on points works on its
cells. `attribute.set` writes a channel from any field, `attribute.accumulate`
totals one, `attribute.promote` folds one into a single value. These ops add
what knowing the layout gives:
- `grid.box`: a grid of cells filling a box. Give it a cell count per axis, or
a cell size.
- `grid.cell_index`: which cell each position falls in (-1 outside), as a
field.
- `grid.neighbor_index`: the cell a fixed step away from each cell, held to
the grid's edge, as a field.
- `grid.sample`: a channel read at every point of a geometry, blended between
the eight cell centres around it.
`procgen.searchNodes("", { pack = "grid" })` lists the family and `procgen.describeNode("op.<id>")`
describes any one of them.
Particles deposited into cells, then read back by the particles:
```lua
local air = g:node("air", "grid.box", nil, { size = { x = 4, y = 3, z = 4 }, cells = { x = 32, y = 24, z = 32 } })
local cell = g:node("cell", "grid.cell_index", { grid = air })
local dense = g:node("dense", "attribute.scatter", { geometry = air, source = sparks, index = cell },
{ name = "mass", as = "density" })
g:node("felt", "grid.sample", { geometry = sparks, grid = dense }, { name = "density" })
```
A blur along x is two neighbours gathered and averaged:
```lua
local right = g:node("right", "grid.neighbor_index", { grid = dense }, { offset = { x = 1, y = 0, z = 0 } })
local left = g:node("left", "grid.neighbor_index", { grid = dense }, { offset = { x = -1, y = 0, z = 0 } })
local a = g:node("a", "attribute.gather", { geometry = dense, index = right }, { name = "density", as = "r" })
local b = g:node("b", "attribute.gather", { geometry = a, index = left }, { name = "density", as = "l" })
```
▲ 0↑ born
# geometry.recalculate_normals (procNode)
Every vertex's normal averaged, by area, over the faces meeting it. The node is `geometry.smooth_normals` with a
crease angle of 180 degrees and area weighting, so no vertex splits: a vertex shared by two faces is smooth across
them, and one already split between two copies stays a hard edge.
## Inputs
- `geometry` (`Geometry`): whose surface the normals are read off. Attributes and material carry to the result.
## Outputs
- `geometry`: the geometry with recomputed per-vertex normals.
▲ 0↑ born
# data
The operations a new node is built from. Each one is a single pass over a
whole class of elements on the device. Processing data is a graph of these:
a field computes a value per element, and these ops move it, fold it, and keep
or reorder the elements.
`procgen.searchNodes("", { pack = "data" })` lists the family and `procgen.describeNode("op.<id>")`
describes any one of them.
## Ops
- `attribute.gather` — each element reads a column from the element its index
names, in this geometry or in a `source` geometry. Use it to read a
neighbour or a parent, or to copy values across by id.
- `attribute.scatter` — each element sends a column to the element its index
names. Every receiving element folds what reached it (sum, mean, min or
max), and an element nothing reached holds `fill`. Use it for deposits,
per-cell totals and group sizes.
- `attribute.promote` — moves a column between points, corners, faces and the
whole geometry (`detail`). Moving to fewer elements folds; moving to more
copies.
- `attribute.accumulate` — a running or total fold of a column, over the whole
class or within the groups that `by` names.
- `elements.keep` — keeps the points or faces whose value is not 0, with every
column they carry.
- `elements.sort` — puts the points or faces in the order of a value.
A threshold filter is `field.compare` into `elements.keep`:
```lua
local tall = g:node("tall", "field.compare", { a = height, b = 2 }, { op = "gt" })
g:node("kept", "elements.keep", { geometry = pts, keep = tall })
```
A total per group, standing on every member:
```lua
g:node("islandArea", "attribute.accumulate", { geometry = mesh, by = island },
{ name = "area", class = "primitive", mode = "total", as = "islandArea" })
```
▲ 0↑ born
# material (nodebox)
Materials as records: material.pbrMat is the standard metallic-roughness material made from its PBR maps (base colour, metallic-roughness, normal, occlusion, emissive) and their factors, and material.shader a material of any surface shader, holding every property and slot to what the shader declares. Each answers one element holding the record of the material kind (`records/material.procRecord`), which geometry.set_material, instances.set_material and the other inputs that take a material put on what draws; the output node shows it and the export node saves it as a `.material`.
▲ 0↑ born
# instances.cull (procNode)
Keeps the instances a test says to. An instance survives when the value it is judged by is strictly above
`threshold`; `invert` keeps the rest instead. The value is the `mask` field when it is wired, else the named point
`attribute` (`field.attribute`). `elements.keep` on the points carries every attribute onto the instances that stay, so
a culled set realizes as the whole one would have. With nothing to judge by, every instance stays, and inverted, none
does.
## Inputs
- `instances` (`Points`): the set to thin out, points carrying a Mesh.
- `mask` (`Field`): the value each instance is judged by. Wired, it outranks `attribute`.
- `threshold` (`Float`, default 0.5): an instance survives when its value reads strictly above this. Raising it thins
the set, lowering it fills it back in.
- `attribute` (`String`): a point attribute on the set to judge by, read while `mask` is unwired. A name the points do
not carry is refused.
- `invert` (`Bool`, default false): true keeps the complement, every instance the test dropped.
## Outputs
- `instances`: the surviving instances.
▲ 0↑ born
# points.box (procNode)
A 3D lattice of points filling a box centred on the origin. Each count is at least 1, and the extent the lattice spans
is the counts times `spacing` (`value.math`). `grid.box` lays out the cells, x fastest, then y, then z, on the device,
and each cell's centre is one point. `attribute.remove` takes the cell layout off the detail so the lattice reads as
Points, and each point carries its own number as `id`.
## Inputs
- `countX` (`Int`, default 3): how many points along X. The lattice holds countX x countY x countZ points and stays
centred on the origin on every axis. Below 1 reads as 1.
- `countY` (`Int`, default 3): how many points along Y, the layers the lattice is stacked in. 1 collapses it to a single
flat sheet at y = 0. Below 1 reads as 1.
- `countZ` (`Int`, default 3): how many points along Z. Below 1 reads as 1.
- `spacing` (`Float`, default 1.0): distance between neighbouring points, the same on all three axes. The lattice spans
(count - 1) x spacing per axis.
## Outputs
- `points`: the lattice, with `id` numbering each point.
▲ 0↑ born
# material
Materials made from any surface shader, with every property the shader declares
set, animated or wired from the graph.
`procgen.searchNodes("", { pack = "material" })` lists the family and `procgen.describeNode("op.<id>")`
describes any one of them.
## Ops
- `material.shader`: a material from a surface `.shader`. Each property its
`properties.yaml` declares is set by name in `properties`, as a constant, an
animated value or a wired output, and each texture slot by name in
`textures`. `blend`, `cull` and `depthWrite` set how it draws. A shader that
declares a `buffer` property reads, through `elements`, one vec4 per live
element another part of the graph made.
`shader` is a reference, written `asset.resolve("<identity>", "shader")`; a
name alone is refused.
```lua
local ember = g.op.material.shader({ shader = asset.resolve("@builtin::shaders.unlit", "shader"),
properties = { base_color = { r = 1, g = 0.5, b = 0.1, a = 1 } }, blend = "additive", depthWrite = "false",
cull = "none" })
```
`material.pbr`, the metallic-roughness material, comes with the `texture` pack.
`g.material.shader` and `g.material.pbrMat`, nodes of the `material` nodebox, answer one element
holding the material record (`records/material.procRecord`), which any input taking a material takes,
`io.output` shows and `io.export` saves as a `.material`.
▲ 0↑ born
# abi
The ops every procgen node is built from. Each states where it runs in its `runs` field, and none calls another op.
## Ops
| Op | Runs | Does |
|---|---|---|
| `device.kernel` | `gpu` | Runs a `.computeShader` over a domain of elements and returns what it wrote as columns |
| `transfer.upload` | `cpu_to_gpu` | Turns CPU numbers, lists and tables into device columns |
| `transfer.readback` | `gpu_to_cpu` | Turns device columns and live counts into CPU values, a frame or more later |
| `cpu.call` | `cpu` | Calls a Luau function with declared return types and a time ceiling |
| `cpu.read` | `cpu` | A `cpu.call` over state outside the graph, with a digest of what it reads |
| `device.bound` | `gpu` | Answers a count as a live count that states the most it can reach |
| `device.live` | `gpu` | Answers a column carrying a count, so a reader of it takes the live elements only |
| `device.guard` | `gpu` | Holds a cook to a condition the device answers, checked a cook later when a recorded list replays |
## device.kernel
`kernel` (param) is the shader, a reference resolved from a literal where the node is written
(`asset.resolve("<identity>", "computeShader")`; a name is refused); `specialize` (param) gives each of its `specialize`
parameters one of its options. `bind` (input) gives every binding its value and `domain` (input) says how many
elements run. `uniforms` (input) gives the shader's params by name, each a Number or a Bool, and a name the shader does
not declare is refused; `step` (input) gives numbers the shader takes where it declares a param of the same name (`dt`,
`time`, ...) and leaves the others out; `columns` (input) is a table of Columns, or a class of a Geometry, whose
columns every read binding left unbound takes by its own name.
- A buffer binding takes a Column. A binding the shader writes and that is bound to a Column writes a copy, and the
copy comes back under the binding's name.
- A `columns` binding takes a table of Columns, packed on the device into one buffer in the order the binding's
`order` field states (`order: "shapeKind center extent"`; the table must hold exactly those columns), and in
sorted-name order where the binding states none. A kernel that reads its columns by slot number states `order`;
one that reads them by `NAME_col_<column>` needs none.
- `specialize` takes the table `fieldc.walkSpec(step, opts).specialize` states as it stands (the walk kernel's
`WALK_STEP`, `WALK_STEP_HI`, `WALK_DONE` and `WALK_PRELUDE`), and the resources the spec runs over come from
`fieldc.bindSpec(spec)`: its `cols` (a table of columns already packed on the device) binds to the kernel's `columns`
binding and its `u` (the numbers the chain carries, a device buffer) to its read `buffer` binding. The caller that made
them lets them go (`bound.release()`) once the kernel's outputs are no longer read.
- A Field bound to a binding is evaluated by the field compiler (`fieldc.evaluate`) in one fused dispatch over the
domain, and the column it lands in stands as long as the outputs of the kernel do.
- `bind.columns` is a domain (a record of Columns, or a class of a Geometry): every read binding the bind left
unnamed takes the column of the same name, and an unbound `columns` binding takes every column of it.
- An appended output bound to a column that an append returned writes a copy of it, and the copy keeps the live
count that column carries, so a later pass appends after what an earlier one appended.
- A param takes a Number or a Bool (1 or 0). A Vec spreads over `<param>_x` to `<param>_w`. `count`, `time` and
`frame` are names any node may give; a kernel that declares none of them leaves them out.
- A Texture binds its `texels` column, and sets `<name>_width` and `<name>_height` where the shader declares them.
- `domain` is a Number, a Column (its elements), or a live count: a count Column, an appended output, or the record
`{ count = <one-word Column>, capacity = <Number> }`. A live count dispatches over its capacity, and the shader reads
the count through a `buffer` binding named `domain_count`. A shader that declares `domain_count` over a plain number
gets the number in it. The live-count form is stated in "Over a live count" below.
- An output with `count` or `capacity` is allocated before the dispatch. An appended output comes back as a Column
(its `_capacity` and `_liveCount` set) and as `<name>_count`.
Output: `columns`, a Record holding every binding the shader wrote as a Column by binding name, and for an appended
output `<name>_count`, its live count.
Column elements are 32-bit words: an `Int` column holds `i32`, a `UInt` column `u32`, every other type `f32`
(`column:element()`). A kernel's `i32` and `u32` outputs are `Int` and `UInt` columns. The first run of a kernel
waits for its program to be built, so a readback asked for after the run sees what the dispatch wrote.
A column whose words are not the ones a binding reads is cast by value before the dispatch: a Float column meeting an
`i32` or `u32` binding (and an Int or UInt column meeting an `f32` one) is read as the numbers it holds, not as the bits
of its words. The cast covers read bindings and in-place bindings; what a kernel writes back through an in-place binding
is the column of the binding's type (`Int` for `i32`).
## Over a live count
A kernel runs over a plain number of elements (every slot live) or over a capacity whose live count sits in a
one-word device buffer. A node that deletes, welds, expands or pairs elements states the count it made on the device,
and the next kernel takes it as its domain, so the count never crosses to the CPU between them.
**What a node passes.** `domain` is the count and the capacity it is bounded by: `found.indices.count` (the count of an
appended output, whose capacity the output carries), an appended output Column itself, or the record
`{ count = <one-word Column>, capacity = <Number> }` for any other count held on the device (a scan's `total`, a
count made by `proc.device.count`; its words are read as u32). A Number is the plain form, as before.
**What a kernel declares.** In `bindings.yaml`, a count binding that exists only in the variant that reads a count, and
the specialization that picks the variant:
```yaml
bindings:
- { name: domain_count, kind: buffer, access: read, element: u32, when: "COUNT=device" }
specialize:
- { name: COUNT, options: [plain, device] }
```
`device.kernel` runs the `COUNT=device` variant when the domain is a live count, and the `plain` variant (the bindings
and the code the kernel always had) when it is a Number. A kernel that declares `domain_count` without `when` reads it
in every variant, and gets the number in it over a plain domain.
A count that is arithmetic over live counts (`geometry.point.count * 8`, `min(kept.count, room)`) is a domain too when
the arithmetic only grows with the counts it reads: the dispatch covers the most it can reach and the kernel reads the
count the arithmetic comes to, evaluated on the device. Arithmetic that takes a count away from something is read
back where the dispatch needs the number.
**An output sized by a live count.** An output whose `count` states a name that is no param (`count: "nv"`) takes its
size from the `bind` of that name. Bound to a number, the output holds that many elements. Bound to `{ live = <count> }`,
where the count is a count Column that carries its capacity or arithmetic over counts that only grows with them, the
output is allocated for the capacity and carries the count as its live count, with nothing read back.
**What the shader does.** The dispatch covers the capacity (`zero_n()`), outputs sized by `n` hold the capacity, and
the live count is `min(domain_count_get(0u), zero_n())`. The code that reads it sits in `// @when COUNT device` ...
`// @end` blocks (`compute_abi` removes a block from the variants it does not name). A slot at or past the live count
reads nothing and writes the inert value of each output it owns:
| Kernel | Inert value of a slot past the live count |
|---|---|
| `gather` | `dst` is zeros |
| `compact` | `inverse` is -1; `indices` is -1 past the kept ones |
| `scan` | `dst` is the total of the last group (0 for an empty run) |
| `jump` (rank, min) | `parent_out` is the slot itself, `value_out` is 0 |
| `loops` | stage clear writes every slot of the dispatch; stage link: `next` is the slot itself, `head` is 0 |
| `loops_cut` | `head` is 0, `cut` is the slot itself |
| `loops_place` | `ring` is -1, `pos` is 0 |
| `edges_runs` | `flag` is 0; `a` and `b` are -1, `fcount` is 0 |
| `expand` | `owner` and `local_index` are -1 past the sum of the counts |
| `hash_gather` | `items` past the count are left as allocated (0) |
| `hash_hook` | a slot past the live count reads and writes nothing |
| `scatter` (fold pass) | an index past the live count reaches no place |
| `reduce` | the last segment ends at the live count; a segment past `segment_count` is not written |
| `scan` (`SEGMENTS=device`) | a segment past `segment_count` is not scanned or written |
| `expand` (`ELEMENTS=device`) | `counts` and `offsets` hold `element_count` live elements, whatever the dispatch covers |
| `intersect_pairs` | a triangle of A past the live ones has no entries |
| `intersect_cuts`, `intersect_unify` | a slot past the live ones is not read or written |
| `intersect_retri` | a triangle past the live ones becomes no triangle |
**A new count.** A kernel that makes a count writes it to an appended output's counter, which the op returns as
`<output>_count` and as the live count of the output (`output.count`); it carries its capacity and is a domain as it is.
`compact` writes the kept count, `expand` the sum of the counts, `hash_gather` the number of points listed, and
`scan` states its total in `total` (an `i32` scan's total is a count: `{ count = scanned.total, capacity = ... }`).
**Capacity.** When the node does not know a count it allocates for the bound it can state, and the kernel never writes
past it. `compact`, `gather`, `scan`, `jump`, `loops*` and `edges_runs` write at most as many slots as the capacity
they run over. `expand` holds at most `n` slots (`n * k` when no count exceeds `k`). `hash_gather` holds at most its
`total` param, which is the bound the node states (a pair list has no smaller bound than points times queries). In
those two the count buffer states the true count whatever the capacity: a count above `.capacity` is an overflow, the
first `capacity` slots are written and the count minus the capacity is how many did not fit. A node that states a
bound checks `count <= capacity`.
**Storage buffers.** A stage binds at most 8 on the web. A `COUNT=device` variant binds one more than the plain one,
and the table counts the variants that exist:
| Kernel | plain | device |
|---|---|---|
| `gather` | 3 | 4 |
| `compact` | 4 | 5 |
| `scan` | 4 | 5 |
| `jump` | 7 | 8 |
| `loops`, `loops_cut`, `loops_place` | 6 | 7 |
| `edges_runs` | 8 | 5 (heads), 8 (table) |
| `expand` | 5 | 6 |
| `hash_gather` | 6 | 8 |
| `hash_hook` | 6 (hook, `MATCH=normal`), 1 (find) | 7, 2 |
`edges_runs` binds the buffers of its own stage in the device variant (`when: "COUNT=plain; COUNT=device STAGE=table"`),
which is how it stays at 8. `abi.directLimit` counts every `buffer` binding the schema declares, `domain_count`
included, so a kernel with a `columns` binding that adopts the form has one direct column fewer; none of these eleven has
one.
## device.bound, device.live, device.guard
`device.bound { value, capacity?, live? }` answers `value` as a live count: a count Column that carries the most it can
reach, so a kernel runs over it (`domain = count`), a geometry carries it (`class.count = count`) and nothing is read
back. `value` is a one-word Column, arithmetic over counts, or a number. A number is already known and stays the number
unless `live` is true, which writes it to the device as the count, allocated the next power of two at or above it (a
list recorded over it runs again for every number inside that room and is recorded again for a number outside it).
A count held on the device with no `capacity` stated is allocated the room a cook of it measures: twice its count, in
powers of two, and never less than an earlier recording of the same cook allocated. The cook reads the count once. A
recorded list runs the count on the device clamped to that room, and holds a guard: a word that is 1 when the count
outgrew the room. The count a node can bound by what it reads (a triangle count by the vertices, pairs by the pairs
there could be) states `capacity` and holds no guard.
`device.live { values, count }` answers `values` carrying `count` as its live count. A kernel output declared
`live: true` in `bindings.yaml` carries the count its dispatch ran over the same way.
`device.guard { value, flag, why, detail? }` answers `value` once `flag` (a number, a one-word Column or a Field over
one) is 0. A cook that finds it otherwise refuses with `why`, or with what the lazy input `detail` raises. A recording
of the cook holds the word of `flag`, and a replay of it reads that word without waiting for a frame
(`traffic().deferredReads`): the cook that follows finds out whether the replay before it ran past the guard, lets the
recording go and records again. The replay that ran past a guard has handed out its answer by then: a count clamped
to its room, or the result for an input that is no longer a solid. A count that outgrew its room is recorded again with
four times the room (`cookrun.counts().guarded` counts the replays found to have run past a guard).
## transfer.upload
`values` is a Record. A Number becomes a one-element Float column; a list of numbers a Float column; a list of
`{x,y,z}` a Vec column, a list of text (or `{ type = "String", values = {...} }`) a column of the values; `{ type = "Int", values = {...} }` a typed column (`Float`, `Int`, `UInt`, `Vec2`, `Vec3`,
`Vec4`, `Color`); a table of those a table of columns; a Column is held as the device run it has. `columns` (output)
holds the columns under the keys `values` gave them.
## transfer.readback
`columns` is a Record of Columns, live counts, plain values (numbers, booleans and text answer themselves) or tables
of them, a Field (evaluated over the elements of the column it reads, then read; a Field that reads no column has no
length and is refused naming the form to use), or a Geometry or a Part, which answers the copy holding what its columns
held. A live count answers a Number, a Float or Int
column a list of numbers, a vector column a list of `{x,y,z,w}`, and `values` (output) holds them under the keys
`columns` gave them. The op waits across frames, and the play report names it as the point the plan waited. Anything
that waits on a readback value is a planning step.
## cpu.call
`fn` (input) is called with `args` (input), and `values` (output) is what it returned, as a list in order. `returns`
(param) lists the type name of each result (`Number`, `String`, `Bool`, `Function`, `List`, `Column`, `BufferRef`,
`Handle`, or a procgen type) and a call that returns anything else is refused; a Handle in the results is released when
the result is. `ceilingMs` (param) refuses a call that ran longer (5000 when it is 0 or left out), counting the CPU
time the call runs for on the thread that makes it, in every coroutine it resumes, and in every task it starts
(`task.spawn`, `task.defer`, `task.delay`) or component hook it sets off while the call lasts, so a task it starts and
waits for is charged to it and one it leaves running is charged only what it ran before the call returned: the time it
waits on a frame, a program build, a read-back or a task, and the time its thread waits for a core on a busy machine,
are not counted. A device Column reaches the function as a
`BufferRef { buffer, type, count, lanes, element }`, and its data does not move. A Geometry reaches it with its columns
read to the CPU, or, with `device` (param) on, as it stands on the device. `curves` (param)
on hands an animated value among `args` to the function as the curve it is, and the function answers with what follows
it; off, the function gets the value it holds at the cook's time. `pure` (param) on states that the results are a
function of the arguments alone and the call changes nothing: a call whose arguments the plan already knows is made
while the graph is planned, and its results stand in the plan as constants.
## cpu.read
`cpu.read` is `cpu.call` (`fn`, `args`, `returns`, `ceilingMs`, `device`, output `values`) for a function that reads
state the graph does not hold, such as entities in the scene. `digest` (input) is a function called with the same
values that answers a value standing for what `fn` reads; the graph's live digest folds it in, so a Generator cooks the
graph again when that state moves. It takes no `curves` or `pure`.
▲ 0↑ born
# component (nodebox)
component.set gives every element of a class a component with the fields given.
▲ 0↑ born
# texture (nodebox)
Textures: images drawn on the device as Texture values, a width, a height and one colour per texel. texture.noise and texture.gradient draw from a formula, texture.from_shader from an author's compute shader and texture.from_raster from the layers of a raster. A material takes a Texture in a slot, and the output node shows one.
texture.cells is the part the first two are built from: one point per texel carrying its column and row.
▲ 0↑ born
▲ 0↑ born
# geometry
Nodes that move, join, cut, round, thicken and sample meshes. A graph transforms a geometry (`geometry.transform`, `geometry.set_position`, `geometry.merge`), welds and shades it (`geometry.weld`, `geometry.smooth_normals`), models it (`geometry.extrude`, `geometry.solidify`, `geometry.bevel`, `geometry.subdivide`, `geometry.decimate`), builds surfaces from outlines (`geometry.sweep`, `geometry.revolve`, `geometry.loft`, `geometry.ribbon`) and reads it into volumes (`geometry.voxelize`, `geometry.to_sdf`). Each is built from the core nodes and the kernels of the library.
## Nodes
- `geometry.transform`, `geometry.set_position`, `geometry.set_material`, `geometry.merge`, `geometry.triangulate`, `geometry.measure`: the basic edits and the measurement of a geometry. `geometry.merge` also joins pieces into a piece set for `instances.to_points`; a part that lacks an attribute another part carries takes the value an element without it has: an instance attribute's identity, else zero (its README says exactly how).
- `geometry.weld` (with `match = "position+normal"` it keeps hard edges), `geometry.smooth_normals`: merging points and shading normals.
- `geometry.extrude`, `geometry.solidify`, `geometry.bevel`, `geometry.subdivide`, `geometry.decimate`: modelling.
- `geometry.sweep`, `geometry.revolve`, `geometry.loft`, `geometry.ribbon`: surfaces from outlines and paths.
- `geometry.voxelize`, `geometry.to_sdf`: a mesh as cells and as a distance field.
- `geometry.boolean`: union, subtract or intersect of two closed meshes, over the `intersect` kernels.
- `geometry.delete`, `geometry.separate`: the triangles a mask field or primitive attribute selects, taken away or split into two geometries (`elements.keep` on the primitives); `geometry.recalculate_normals`: `geometry.smooth_normals` at 180 degrees, area-weighted.
- `geometry.selected`, `geometry.flip`, `geometry.profile`, `geometry.tangents`, `geometry.sides`, `geometry.shared`, `geometry.bvh`, `geometry.rings`, `geometry.first`: the parts the nodes above are built from, each usable on its own.
- `geometry.unit`, `geometry.perpendicular`, `geometry.rotate`, `geometry.transport`, `geometry.split`: vector arithmetic over sockets that the nodes which orient points and normals share.
- `geometry.offsets`, `geometry.kept`, `geometry.following`, `geometry.trim`, `geometry.blank`: column arithmetic the nodes above share (a prefix sum of counts, the places a mask keeps, a ring read one place on, the primitives of no area dropped, a one-word column to bind).
## Conventions
- Each node's folder name is its call name inside the box: `geometry.<folder>`.
- Inputs and outputs carry the names of the op each node replaces.
- A node that answers a Data table from numbers read back off the device holds the Luau for it in a `readings.module` inside its own folder.
- A node with one output answers that output's value; a node with several answers a record by output name.
- A fold's index runs from 1: the first pass is 1, so a pass that works on level, chunk or step `n` reads `index - 1` when it counts from 0 (`geometry.bvh`, `geometry.to_sdf`, `geometry.sweep`, `geometry.loft`).
- A field bound to a kernel takes the length of that dispatch, so a node writes a value out as a column (a gather with an explicit `count`) before it binds one whose length differs from the dispatch's.
▲ 0↑ born
# edui.inspector.parts
The building blocks a type's `inspector.luau` composes its sections from. Every
Context carries this module as `ctx.parts`, so an inspector file requires
nothing from `@builtin::editor`:
```luau
return { sections = { {
id = "summary", title = "Summary",
build = function(ctx)
local rows = ctx.parts.facts(ctx, {
{ label = "Vertices", value = 24 },
{ label = "Size", value = ctx.parts.bytes(1536) },
})
local n = #rows
for _, node in ipairs(ctx.parts.action(ctx, "rebuild", "Rebuild", function() ctx.ref:rebuild() end)) do
n += 1
rows[n] = node
end
return rows
end,
} } }
```
Every function takes the section's Context first and returns a list of edui
nodes, unless stated.
| Part | What it shows | Callback key |
|---|---|---|
| `format(v)` | a value as text: integers plain, other numbers to 4 significant digits, booleans yes/no, a list as its length, a map as its entry count (returns a string) | — |
| `bytes(n)` | a byte count as text: `84 B`, `1.5 KiB`, `2.00 MiB` (returns a string) | — |
| `facts(ctx, rows, empty?)` | label/value rows (`{ label, value, mono? }`); the value takes the width its label leaves and wraps within it; a row whose value is nil shows "—", so a value that could not be read is seen to be missing. A row that does not apply to the subject is left out of `rows` by the section; with no rows `empty` shows | — |
| `list(ctx, items, { max?, empty?, mono? })` | one text row per item, at most `max` (40), then "… and N more" | — |
| `table(ctx, key, columns, rows, empty?)` | a table of records, each cell through `format` | `<keyPrefix>:tbl:<key>` (widget id) |
| `links(ctx, refs, { max?, empty? })` | one button per asset (an AssetRef, a `{ __ref = guid }` envelope, or a guid or name), named and typed; clicking selects it. An item that does not resolve is listed as text | `<keyPrefix>:link:<guid>` |
| `entityLinks(ctx, ids, { max?, empty? })` | one button per entity, by name; clicking selects it. An id whose entity no longer exists is listed as text, marked despawned | `<keyPrefix>:ent:<id>` |
| `action(ctx, id, label, run, { primary? })` | a button running `run`; a run that raises shows its message beneath the button until the next run | `<keyPrefix>:act:<id>` |
| `openFile(ctx, path, label?)` | an action opening `path` in the Code panel; nothing when the file is absent | `<keyPrefix>:act:open:<path>`, the path relative to the subject's folder |
| `problems(ctx, problems, clean?)` | one notice per `{ message, code?, severity?, line? }` (error tone, or info for a warning, info or hint), or `clean` ("No problems.") | — |
| `swatch(ctx, color, label)` | a colour square (`{ r, g, b }` in 0..1) beside its label and hex (returns one node) | — |
| `cached(key, paths, compute)` | `compute()`'s value, computed again only when the content version of one of `paths` moves (returns the value). Read through `ctx.parts`, it is held while a Context that read it stays open; read through the module itself, until the next `Inspector.release` | — |
| `state(ctx)` | the table a Context's sections keep state in between builds (a picked zoom, a chosen tab), forgotten when the Context is released (returns the table) | — |
| `check(ctx, path)` | a "Check" action running `lsp.check(path)`, then the counts and diagnostics of the last check of the file's current content | `<keyPrefix>:act:check:<path>`, the path relative to the subject's folder |
| `values(ctx, { subject, schema?, values?, write?, readOnly? })` | typed controls over any values: one control per schema field, each edit checked with `Field.check` and written through `write` inside one undoable operation; `readOnly` shows the values with no control that writes. The same subject shown by two Contexts keeps two states | `<keyPrefix>:val:<subject>:set:<field>` |
## Rules a section keeps
- **No write in `build`.** A build only reads; writes happen in an action's
`run`, in `ctx.setSettings`, or in a `values` part's `write`.
- **Expensive reads are once per content version.** A build that decodes a
payload, walks a file or asks the LSP goes through `cached` (keyed on the
files the value comes from) or runs behind an action. `check` holds each
file's last result for as long as the file's content is unchanged.
- **Every callback key starts with `ctx.keyPrefix`**, so two subjects — the
panel's asset and an inline editor of the same asset — never share one.
- **What the parts hold goes with the Context.** `ctx.parts` is the parts as
that Context reads them. `Inspector.release(keyPrefix)` forgets the
Context's action failures, file checks and state, and every cached value no
other open Context read.
▲ 0↑ born
# normals
Welding coincident points and smoothing the normals that shading reads.
The definitions live in `proc.ops.normals`; this pack is what puts them
in the op registry, so `procgen.searchNodes("", { pack = "normals" })` lists the family
and `procgen.describeNode("op.<id>")` describes any one of them.
## Ops
- `geometry.weld`
- `geometry.smooth_normals`
▲ 0↑ born
# points
Point clouds: grids, discs, poisson sets, points on a surface or a curve, and the ops that filter, relax and look around them.
The definitions live in `proc.ops.points`; this pack is what puts them
in the op registry, so `procgen.searchNodes("", { pack = "points" })` lists the family
and `procgen.describeNode("op.<id>")` describes any one of them.
## Ops
- `points.grid`
- `points.disc`
- `points.poisson`
- `points.box`
- `points.on_surface`
- `points.from_vertices`
- `points.filter`
- `points.on_curve`
- `points.relax`
- `points.neighbors`
- `points.count_neighbors`
▲ 0↑ born
# proc
Code-authored, graph-backed procedural generation for the Zero engine.
A graph is one function of `g`: `proc.define(function(g) ... end)`. Nodes are called by
family and name (`g.<family>.<name>({ ... })`), inputs are declared with `g:input(name, default, opts)`,
the returned table is the graph's outputs, and `io.output` is how the graph stands in the world.
`builder.module/README.md` holds every rule of that form.
This module owns the whole procedural model: the graph builder + data model,
the operation registry, the type system, the lazy cached evaluator, deterministic scoped hashing, and the node-family op
implementations. Authored `.procGraph` graphs are persisted via
`procGraph.assetType`; the `procgen.preview` / `procgen.spawn` operations live in
`procgen.toolbox` and are invoked as tools: `tools.use("procgen", "preview", args)`
from a script or an agent's `tool_exec` script, or `zero procgen preview` in the
shell. Generated
meshes/materials/textures/bundles are persisted through their own `.assetType`.
An op family can also come from outside this package: a `.procPack` asset
registers its ops in the same registry, called as `g.op.<id>(...)` exactly like the ones
shipped here (`proc.registerPack(name, ops)` is what it registers through, and
what a test or a tool calls to hold a family for the life of one VM).
`terrain.*` is one such pack, shipped in `terrainSystem.package/procPacks/`
and covering a whole landscape; `guides { path =
"systems/terrainSystem/documentation/terrain" }` is its guide.
To find out what the registry holds, use the `procgen` toolbox: `searchNodes`
searches it and `describeNode` describes one node or op. `proc.search_ops(query)` and
`proc.schema(id)` are the same answers in-process, for a graph builder looking
something up while it runs.
▲ 0↑ born
# sdf (nodebox)
Signed distance field nodes: shapes, smooth combines, round and shell, space warps (transform, twist, bend, taper, elongate, mirror, repeat), displacement, grids and fields in and out, and sdf.mesh, which contours a shape into a closed mesh.
▲ 0↑ born
# Scatter
Strews copies of one shape over the surface of another, each with its own turn, lean and size.
Both the surface and the thing scattered are inputs, so this covers rocks on a hillside, crates on a deck, bushes
along a bank or rubble across a floor: wire it a different `source` and it is a different field.
It makes points on the surface (`points.on_surface`, area-weighted), gives every point its own random yaw, lean and
scale (`instances.randomize_transform`), and places the source on those points (`instances.on_points`). The transform
is read from the same point cloud the instances sit on, so each copy takes the pose of its own point.
## Inputs
| Input | Type | Default | What it does |
|-------|------|---------|--------------|
| `surface` | Geometry | none | What the copies land on. Points are area-weighted, so a bigger face gets proportionally more. |
| `source` | Geometry | none | The shape being copied. |
| `count` | Int | 200 | How many copies. |
| `seed` | Int | 1 | Changes the arrangement. The same seed always gives the same field. |
| `scaleMin` | Float | 0.8 | Smallest per-copy size multiplier. |
| `scaleMax` | Float | 1.4 | Largest per-copy size multiplier. Equal to `scaleMin` means uniform. |
| `tilt` | Float | 0.15 | Radians of random lean off vertical. `0` stands every copy upright. |
## Output
`instances`: a population, the points each carrying the source as its `Mesh`. A Generator draws it as one
hardware-instanced draw call per source; `instances.realize` merges it into a single Geometry.
## Calling it
Another graph calls a `.procGraph` as it calls a node, by its path or identity, naming the inputs in a table; the
call answers what the graph outputs (`instances`). Its declared inputs are the call's inputs.
▲ 0↑ born
# points.grid (procNode)
A flat lattice of points in the XZ plane, centred on the origin. Each count is at least 1, and the extent the lattice
spans is the counts times `spacing` (`value.math`). `grid.box`, one cell thick at height `y`, lays out the cells, x
fastest, then z, on the device, and each cell's centre is one point. `attribute.remove` takes the cell layout off the
detail so the lattice reads as Points, and each point carries its own number as `id`.
## Inputs
- `countX` (`Int`, default 4): how many points along X. The grid holds countX x countZ points and stays centred on the
origin, so each one added widens it by `spacing`, half of that to either side. Below 1 reads as 1.
- `countZ` (`Int`, default 4): how many points along Z. A count of 1 leaves a single row running along X. Below 1 reads
as 1.
- `spacing` (`Float`, default 1.0): distance between neighbouring points, the same on both axes. The grid spans
(countX - 1) x spacing by (countZ - 1) x spacing.
- `y` (`Float`, default 0.0): the height the whole lattice sits at. Every point takes this as its y.
## Outputs
- `points`: the lattice, with `id` numbering each point.
▲ 0↑ born
# growth
Curves that grow from other curves. The result is a single geometry holding
every curve, starting curves included. Each point carries:
- `generation`: 0 on the starting curves, one more per fork;
- `u`: how far along its own curve, 0 to 1.
Because the result is geometry, every curve, point and field op works on it:
- roughen it with `geometry.set_position` and a noise field;
- sweep it into tubes with `geometry.sweep`;
- sample it with `points.on_curve`, for sparks along a crack;
- read `generation` through a ramp, so branches thin as they fork.
`procgen.searchNodes("", { pack = "growth" })` lists the family and `procgen.describeNode("op.<id>")`
describes any one of them.
## Ops
- `curve.branch` — branches sprout from each curve, generation after
generation. Each curve sends out `branches` children from along its length.
A child turns off its parent by up to `angle`, is `ratio` as long, and
wanders as it goes. A `normal` keeps the growth in one plane, the way a
crack keeps to the surface it crosses. Use it for cracks, lightning forks,
frost and coral.
- `curve.grow` — branches reach toward a cloud of attractor points (space
colonisation). A node grows a step toward the attractors that pull on it,
and an attractor a branch reaches is spent, so the growth fills the cloud's
shape. Use it for canopies, root balls and leaf veins.
```lua
g:node("impact", "curve.line", nil, { start = { x = -1, y = 0, z = 0 }, finish = { x = 1, y = 0, z = 0 }, segments = 8 })
g:node("cracks", "curve.branch", { curve = "impact" }, { generations = 4, branches = 2, angle = 45, wander = 25, normal = { x = 0, y = 1, z = 0 } })
g:node("here", "field.position")
g:node("jag", "field.curl", { position = "here" }, { frequency = 3, amplitude = 0.05 })
g:node("rough", "geometry.set_position", { geometry = "cracks", offset = "jag" })
```
▲ 0↑ born
# points.filter (procNode)
Keeps the points a test says to. The value each point is tested by is the `mask` field when it is wired, else the named
`attribute`'s values (`attribute.labels`). The test is a comparison (`field.compare`): above `threshold`, or, when
`equals` is given, equal to the index that value or label names. `elements.keep` on the points carries every attribute
and the detail onto the points that stay, counted on the device. Given neither a mask nor an attribute, the points come
back as they are.
## Inputs
- `points` (`Geometry`): the cloud to thin. Every point attribute and every detail value is carried onto the survivors,
so a colour or an index still belongs to the point it started on.
- `mask` (`Field`): the per-point value tested; a point stays when it is above `threshold`. Wired, it decides, and
`attribute` is read only when this is empty.
- `threshold` (`Float`, default 0.5): a point stays when its value is strictly above this. Raising it keeps fewer
points.
- `attribute` (`String`): a point attribute to test instead of wiring `mask`. A name the points do not carry is refused,
naming the ones they do.
- `equals` (`String | Float`): keeps the points whose `attribute` equals this instead of testing it against
`threshold`. It is a number, or a label: one of the texts a text attribute holds, or one of the names the points'
detail `labels` gives a numeric one (how one solver's particles are split back by the `stream` their source named).
## Outputs
- `points`: the points that stay.
▲ 0↑ born
# boolean
Union, intersection and difference between two solids.
The definitions live in `proc.ops.boolean`; this pack is what puts them
in the op registry, so `procgen.searchNodes("", { pack = "boolean" })` lists the family
and `procgen.describeNode("op.<id>")` describes any one of them.
## Ops
- `geometry.boolean`
▲ 0↑ born
▲ 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
# geometry.delete (procNode)
Deletes the triangles a test selects. A triangle is selected when its value is strictly above `threshold`. The value
each triangle is tested by is the `mask` field when it is wired, else the named primitive `attribute`
(`field.attribute`), else 0. `invert` deletes the triangles the test did not select. `elements.keep` on the primitives
does the removal: every point stays, and the triangles that remain keep their corners and attributes, counted on the
device.
## Inputs
- `geometry` (`Curves | Surface`): whose triangles are thinned. Point and detail attributes carry over as they are;
primitive and vertex attributes stay with the triangles that survive.
- `mask` (`Field`): read once per triangle for whether it goes. Wired, it is what the selection reads, and `attribute`
is read only when this is empty.
- `threshold` (`Float`, default 0.5): a triangle is selected when its value is strictly above this. Raising it deletes
less.
- `attribute` (`String`): a primitive attribute on the geometry to read the selection from, instead of a wired mask. A
name the triangles do not carry is refused.
- `invert` (`Bool`, default false): true deletes every triangle the test did not select, so a mask naming what to keep
cuts away the rest.
## Outputs
- `geometry`: the geometry with the selected triangles removed.
▲ 0↑ born
# shader (nodebox)
An author's own compute shaders as nodes. shader.compute runs a .computeShader over the elements of a geometry through the device.kernel op: each buffer binding is a column of the geometry, each param a knob of the node, and what the shader writes comes out as columns.
▲ 0↑ born
# flow
Fields that push things around, and the noise they are made of. Each is a field
like any other: `sim.force` applies it to a solver's particles, `attribute.set`
writes it onto a geometry, and a field op combines it with others. Each also compiles to
the dispatch its CPU form computes, on the same lattice hash as `field.noise`,
so a solver evaluates it on the device at every step.
A `position` left unwired is where each element of the domain stands: the
particles a solver steps, or the points an `attribute.set` writes. Wire a
position to sample the field somewhere else.
`procgen.searchNodes("", { pack = "flow" })` lists the family and `procgen.describeNode("op.<id>")`
describes any one of them.
## Fields that push
- `field.curl` — curl noise. It swirls with no sources or sinks, so what
follows it eddies without bunching up or thinning out. This is how smoke,
dust and sparks move in turbulent air.
- `field.vortex` — turning around an axis through a centre. It is strongest at
the axis and falls to half at `radius`.
- `field.attractor` — pulls toward a centre, fading to zero at `radius` when one
is given. A negative strength repels.
## Orientation
- `field.align` — the rotation turning an element's `axis` (+Y unless given)
onto a direction, as the quaternion `orient` takes. An unwired direction is
each element's own `v`, so written onto particles as `orient` it turns each
one onto its motion. A card drawn with `zero_billboard_streak` then streaks
along it.
## Noise
- `field.worley` — the distance to the nearest scattered feature point. Use it
for cells, cracks, scales and stone.
- `field.simplex` — smooth signed noise in [-1, 1].
```lua
local g = proc.graph("embers")
g:node("emit", "points.grid", nil, { countX = 1, countZ = 1 })
g:node("src", "sim.spawn", { points = "emit" }, { rate = 200, lifetime = { x = 2, y = 4 } })
g:node("curl", "field.curl", nil, { frequency = 0.6, amplitude = 5 })
g:node("swirl", "sim.force", { field = "curl" })
-- Air rising at 2 m/s: each ember is pulled toward that velocity, which is
-- wind and drag in one.
g:node("updraft", "field.constant", nil, { value = { x = 0, y = 2, z = 0 } })
g:node("air", "sim.force", { field = "updraft" }, { mode = "velocity", strength = 0.4 })
g:node("sim", "sim.particles", { sources = { "src" }, forces = { "swirl", "air" } })
```
▲ 0↑ born
# scene (nodebox)
scene.query finds entity ids by name, component and hierarchy, scene.fetch brings entities in as points carrying their pose and components, scene.input is the entity the graph stands on, and scene.water is the water the scene holds.
▲ 0↑ born
# geometry.separate (procNode)
Splits a geometry's triangles in two by a test. The value each triangle is tested by is the `mask` field when it is
wired, else the named primitive `attribute` (`field.attribute`), else 0. Two comparisons against `threshold` follow
(`field.compare`): strictly above, and at or below. `elements.keep` on the primitives runs once for each, so `a` holds
the triangles above and `b` the rest. Both keep every point, and between them they hold every triangle once.
## Inputs
- `geometry` (`Curves | Surface`): whose triangles are divided. Point and detail attributes carry to both halves as they
are; primitive and vertex attributes stay with each half's own triangles.
- `mask` (`Field`): read once per triangle for which half it joins. Wired, it is what the split reads, and `attribute`
is read only when this is empty.
- `threshold` (`Float`, default 0.5): a triangle goes to `a` when its value is strictly above this, and to `b`
otherwise.
- `attribute` (`String`): a primitive attribute on the geometry to read the split from, instead of a wired mask. A name
the triangles do not carry is refused.
## Outputs
- `a`: the triangles whose value is strictly above `threshold`.
- `b`: the rest. A call answers both as a record by output name.
▲ 0↑ born
# attribute
Reading, writing and moving the per-element columns a mesh carries.
The definitions live in `proc.ops.attribute`; this pack is what puts them
in the op registry, so `procgen.searchNodes("", { pack = "attribute" })` lists the family
and `procgen.describeNode("op.<id>")` describes any one of them.
## Ops
- `attribute.set`
- `attribute.labels`
- `attribute.remove`
- `attribute.transfer`
- `attribute.statistics`
- `attribute.normalize`
▲ 0↑ born
# sdf
Signed distance fields: shapes as distance, combined smoothly, and carried across to and from geometry.
The definitions live in `proc.ops.sdf`; this pack is what puts them
in the op registry, so `procgen.searchNodes("", { pack = "sdf" })` lists the family
and `procgen.describeNode("op.<id>")` describes any one of them.
## Ops
- `sdf.sphere`
- `sdf.box`
- `sdf.rounded_box`
- `sdf.torus`
- `sdf.capsule`
- `sdf.cylinder`
- `sdf.cone`
- `sdf.plane`
- `sdf.union`
- `sdf.subtract`
- `sdf.intersect`
- `sdf.transform`
- `sdf.twist`
- `sdf.bend`
- `sdf.taper`
- `sdf.elongate`
- `sdf.round`
- `sdf.shell`
- `sdf.mirror`
- `sdf.repeat`
- `sdf.position`
- `sdf.displace`
- `sdf.mesh`
- `geometry.to_sdf`
- `sdf.from_grid`
- `sdf.sample`
▲ 0↑ born
# sim (nodebox)
Simulation nodes: sim.spawn, sim.force, sim.collider, sim.scene_colliders, sim.particles and sim.shader, built on the runtime's g:state and g:step.
▲ 0↑ born
▲ 0↑ born
# abiBox
A node for the procgenAbi suite: it is called through the builder and written out in a graph.
▲ 0↑ born
▲ 0↑ born
# mesh
The primitive solids and sheets a graph starts from: `mesh.box`, `mesh.sphere`, `mesh.cylinder`, `mesh.cone`, `mesh.capsule`, `mesh.octahedron`, `mesh.torus`, `mesh.rounded_box`, `mesh.plane` and `mesh.grid`. Each is made on the device from lattices and fans of the `make` kernel, with positions, normals and texture coordinates written as formulas over the lattice, and answers triangles centred on the origin that fill the box its parameters name.
## Conventions
- Each node's folder name is its call name inside the box: `mesh.<folder>`.
- Parameters keep the names and defaults of the op each node replaces.
- `mesh.lattice` makes the lattices and fans the presets share, `mesh.spherical` the point of a sphere and `mesh.cube_faces` the faces of a cube; a preset writes its positions, normals and texture coordinates as formulas over the lattice.
▲ 0↑ born
# io
The ways in and out of a graph: `import` brings an asset in as the value its type states, `luau` runs a function of a Luau module on the CPU, `output` shows a value in the world and answers a Handle, `export` saves a value as an asset, and `engine` calls the engine bridge that `import`, `export` and the core reading nodes use. `output` holds `show.module` and `engine` holds `engine.module`, the functions they call.
▲ 0↑ born
# value
Single values a graph computes with. A measurement turns into something a
loop stops on, a count another node takes, or anything a switch decides.
`procgen.searchNodes("", { pack = "value" })` lists the family and `procgen.describeNode("op.<id>")`
describes any one of them.
## Ops
- `value.get` — one field of a Data or record value, by a dotted path such
as `bounds.max.y`. When the path misses, the error names the fields that
are there.
- `value.math` — `a <op> b` for add, sub, mul, div, min, max or pow. `b` is
a wire or the `constant` param.
- `value.expr` — a number that follows an expression over the graph's time
(`t`, `frame`, the math functions) and the names `variables` gives it, each
a constant, a graph input or another node's number. A curve that depends on
graph inputs is this one node: `{ variables = { peak = peak, decay = decay } }`
with `expression = "peak * max(0, 1 - t / decay) ^ 2"`.
- `value.compare` — `a <op> b` for less, less_equal, greater, greater_equal,
equal or not_equal, answering a Bool.
- `value.switch`: `ifTrue` when `when` is true, `ifFalse` otherwise. Only the
one taken is cooked.
- `value.present`: whether anything is wired into `value`, as a Bool: what a
graph used as a node asks of an input its caller may leave unwired.
- `value.fallback`: the value wired into `value`, or `fallback` when nothing
is. `fallback` is cooked only when it is the one taken.
```lua
local stats = g:node("stats", "geometry.measure", { geometry = grown })
local top = g:node("top", "value.get", { data = stats }, { path = "bounds.max.y" })
local done = g:node("done", "value.compare", { a = top }, { op = "greater_equal", constant = 3 })
```
▲ 0↑ born
# mesh
The primitive solids a graph starts from. A `.mesh` asset enters a graph through
the `asset` node (`procPacks/asset.procPack`), as the Surface the mesh type
states.
The definitions live in `proc.ops.sources`; this pack is what puts them
in the op registry, so `procgen.searchNodes("", { pack = "mesh" })` lists the family
and `procgen.describeNode("op.<id>")` describes any one of them.
## Ops
- `mesh.box`
- `mesh.sphere`
- `mesh.cylinder`
- `mesh.cone`
- `mesh.plane`
- `mesh.capsule`
- `mesh.octahedron`
- `mesh.torus`
- `mesh.rounded_box`
- `mesh.grid`
▲ 0↑ born
# entity (nodebox)
entity.transform and entity.bounds read one entity's pose and bounds as values, entity.points reads a set of entities as points, and entity.geometry reads the mesh an entity's Model draws as a Geometry.
▲ 0↑ born
# 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
# procPack — a family of procedural ops as content
A `.procPack` is a FAMILY of procedural operations authored as content: a
folder whose `init.luau` returns a **pack table**. When the asset registers,
every def in its `ops` map joins the proc op registry under the stable string
id the pack names it with, so a graph calls it by that id (`g.op.<id>`). Discover the
available packs with `asset.list("procPack")`, and the ops any of them
contributed with `procgen.searchNodes("", { pack = "<name>" })`.
This is the one path an op family reaches the registry through. Procgen's own
families are packs — `procPacks/` in this package holds one per family,
`abi.procPack` beside it holds the ops every node is built from, and
terrain's `terrain.procPack` sits in its own package the same way — so the
packs shipped here are worked examples of exactly what you are writing, not a
separate mechanism the built-ins are exempt from.
```
MyPack.procPack/
init.luau -- returns the pack table
README.md
.metadata
```
## The pack table
```lua
local proc = require("@builtin::systems.procgen.proc")
local function transformed(ctx: any, geometry: any, params: { [string]: any }): any
return proc.registry.get("geometry.transform").eval(ctx, { geometry = geometry }, params)
end
return {
name = "myPack", -- the asset's own name when omitted
description = "Moves and turns whole geometries.",
ops = {
["myPack.lift"] = {
description = "Lift a Geometry by a height.",
inputs = { geometry = { type = "Geometry" } },
params = { height = { type = "Float", default = 1.0 } },
outputs = { geometry = { type = "Geometry", default = true } },
eval = function(ctx, inputs, params)
return transformed(ctx, inputs.geometry, { translate = { x = 0, y = params.height, z = 0 } })
end,
},
["myPack.spin"] = {
description = "Turn a Geometry about the up axis.",
inputs = { geometry = { type = "Geometry" } },
params = { degrees = { type = "Float", default = 45 } },
outputs = { geometry = { type = "Geometry", default = true } },
eval = function(ctx, inputs, params)
return transformed(ctx, inputs.geometry, { rotate = { x = 0, y = params.degrees, z = 0 } })
end,
},
},
}
```
Each def is the same table every op in the registry is described by — typed
input sockets, params, typed output sockets, and an `eval(ctx, inputs, params)`
returning the outputs. `ctx` carries seed / nodeId / compositePath / hash /
checkpoint. Validation is the same for every pack, so a malformed socket or an
undeclared param type is refused here exactly as it is for the ones shipped in
this package.
Beside `name`, `description` and `ops`, the pack table takes:
- `context`: the context every op of the family works in (`"geometry"`,
`"field"`, `"sim"`, ...), for each def that states none of its own. A name
that is no registered context is refused.
- `aliases`: former op ids mapped onto current ones
(`{ ["myPack.raise"] = "myPack.lift" }`). A graph naming a former id cooks
the current op, while listings and schemas carry the current id. An alias
onto an id that is no registered op is refused, and so is an alias whose
former id is itself a registered op.
A graph calls a pack's op as `g.op.<id>`, with one table naming its inputs and
params:
```lua
return proc.define(function(g)
local box = g.mesh.box({})
local lifted = g.op.myPack.lift({ geometry = box, height = 2 })
return { mesh = g.op.myPack.spin({ geometry = lifted }) }
end)
```
## procPack or procNode
Both contribute nodes that a graph calls. They differ in how a node is written
and addressed:
| | `.procNode` | `.procPack` |
|---|---|---|
| contributes | one node, written as a graph (`proc.define(function(g) ... end)`) | a family of ops, each a def table |
| addressed by | the asset's identity (`g.points.disc`, a path, a guid or a short name), recorded in the graph by guid | the string id the pack names it with |
| a graph writes | `g.<family>.<name>({ ... })` | `g.op.myPack.lift({ ... })` |
Reach for a `.procPack` when the ops belong together and you want to read the
family's name at every call site; reach for a `.procNode` when one operation
stands on its own.
## Finding a pack's ops
Search embeds a pack's `README.md` and `init.luau` (the `indexing` block of `type.yaml`), so the README is
where a search by what an op does reaches it: list every op of the pack as a line of its own, the op's id and
what it does, and keep the line true when the op changes.
```
- `myPack.lift` — Lift a Geometry by a height.
- `myPack.spin` — Turn a Geometry about the up axis.
```
`procgen.searchNodes` asks for `.procNode` and `.procPack` assets together. A hit that is a pack is listed as the
ops of that pack that answer the query's words (`op.myPack.lift`, with `matched.pack` naming the pack), or as the
pack itself when none of them does.
## Registration lifecycle
`init.luau` is compiled from its own source on every registration rather than
`require`d, so an edit lands immediately. Another module is reached by its
absolute name (`@builtin::systems.procgen.proc`), and a file of the pack's own
by a relative require (`require(".helpers")`). Every op's `version` is derived
from the pack's source, unless its def declares a `version` of its own, so
editing the file re-keys the node caches of every graph that reaches any op in
the family — those graphs recook.
Editing the pack to REMOVE an op unregisters it. A graph built before the edit
that still names the op errors loudly at eval, naming the op that is not
registered, and a graph source that calls it fails to build, naming the ops
the pack still holds. Deleting the `.procPack` folder unregisters the whole
family the same way.
## In the Inspector
Operations: how many ops the pack registers, and each op's id, kind and what it does.
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
# prefab.place (procNode)
Place one prefab inside another: put a whole prefab, a geometry's prefab among them, under a named entity of a host prefab at a local position, rotation and scale. Builds a hierarchy out of parts: a wheel under an axle, a lamp on a post, a door in a wall, each part its own entity. The placed records are renumbered and re-parented, so the same prefab can be placed any number of times.
Every placed record takes the id `place<n>_<its id>`, n being the number of records the host held before it (raised past any id the host already holds). A placed record's `parent_id`, and every string in its components' data that names a placed record's id (what `prefab.link` writes), take the new ids, so links inside the placed prefab keep pointing at their own entities. Each placed root keeps its own pose inside the frame `position`, `rotation` and `scale` pose: a root at (0, 1, 0) placed at (2, 0, 0) stands at (2, 1, 0) under `parent`. Built from `prefab.edit` (`what = "place"`).
## Inputs
- `prefab` (Prefab). The host prefab the placed one goes into. It comes out as a new value; the prefab wired in keeps the shape it had.
- `placed` (Prefab). The prefab placed: every record of it is appended, its root (each record with no parent) hung under `parent`. `prefab.from_geometry` makes one of a single mesh.
- `parent` (String, optional). The name of the host entity the placed prefab hangs under: the first record carrying it. Left out, it hangs on the host's root; a name the host does not carry is an error.
- `position` (List<Float>, default { 0, 0, 0 }). Where the placed prefab's root stands, local to `parent`.
- `rotation` (List<Float>, default { 0, 0, 0, 1 }). The placed prefab's root rotation local to `parent`, a quaternion (x, y, z, w).
- `scale` (List<Float>, default { 1, 1, 1 }). The placed prefab's root scale local to `parent`.
## Outputs
- `prefab` (Prefab). A new prefab value, `{ records = { ... } }`: the host's records, then the placed ones.
```luau
local cart = g.prefab.empty({ name = "cart" })
cart = g.prefab.entity({ prefab = cart, name = "axle" })
local wheel = g.prefab.from_geometry({ geometry = g.mesh.cylinder({}), name = "wheel" })
cart = g.prefab.place({ prefab = cart, placed = wheel, parent = "axle", position = { 1, 0, 0 } })
cart = g.prefab.place({ prefab = cart, placed = wheel, parent = "axle", position = { -1, 0, 0 } })
```
▲ 0↑ born
▲ 0↑ born
# Nodebox (asset type)
A nodebox is a namespace folder that groups related `.procNode/` children, the way a toolbox groups
`.tool/` children. A node's name is its folder name inside its box: `points.nodebox/disc.procNode` is
called `points.disc`, and nothing inside the node states that name. Moving or renaming a node is a VFS
move that keeps its guid.
## Where it lives
- Source: `<anywhere>/<family>.nodebox/`, usually inside a `.package`, an effect or a world's content.
- Identity: the folders above it and `<family>` (`fx.points` for `fx/points.nodebox`; the `.nodebox`
suffix strips). A graph calls its nodes by `<family>.<node>` alone.
- Folder shape (open child set):
- `README.md`: what the family is for. Required.
- `.metadata`: description and tags. Required. It also states where the box's nodes work: `context` (one of
`proc.registry.contexts()`: `geometry`, `field`, `value`, `sim`, `raster`, `material`, `prefab`) and, when every
node of the box reads the scene or assets beside its inputs or writes them beside its outputs, `pulls` /
`pushes` (`scene` or `asset`). A node that works elsewhere states its own in its `.metadata`.
`procgen` `describeNode` and the `context`, `pulls` and `pushes` filters of `searchNodes` read them.
- `<node>.procNode/`: any number of nodes.
## How a graph reaches a node
`g.<family>.<node>(args)` resolves the node by its asset identity: the box and the folder name together
(`g.points.disc`), a path (`g["/zero/source/fx/ring.procNode"]`), `~` (the nearest enclosing package,
effect or world root: `g["~/ring"]`) or a short name the asset system resolves (`g.disc`). A name that
matches two nodes is an error naming both; a name that matches none is an error naming the closest nodes.
Each call is recorded in the graph's reference table by the node's guid.
## How to create one
```luau
asset.create("nodebox", "points", { folder = "fx" })
local box = asset.resolve("points.nodebox")
asset.create("procNode", "disc", { into = box })
```
## Discovery
- `asset.list("nodebox")`: every box.
- `asset.inspect("points")`: the nodes in it and what each does.
- `asset.list("procNode")`: every node, in or out of a box.
## In the Inspector
Nodes: how many nodes the box holds, each node's full name and what it does, and a link to each.
▲ 0↑ born
# grid.box (procNode)
The cell centres are `min + (coords + 0.5) * cellSize`, computed on the device from each element's index. The
layout (`cells`, `min`, `max`) is stored on the detail as `grid`, which is what makes the value a Grid.
Built from: `core.points`, `field.index`, `math.*`, `vector.*`.
```luau
local air = g.grid.box({ size = vector.create(4, 3, 4), cells = vector.create(32, 24, 32) })
```
▲ 0↑ born
# grid.cell_index (procNode)
`floor((p - min) / cellSize)` per axis, flattened `x + nx * (y + ny * z)`; any axis outside `[0, n)` gives -1.
Built from: `field.position`, `math.*`, `vector.floor`, `g.where`, `g.compare`.
▲ 0↑ born
# grid.neighbor_index (procNode)
Each cell's coordinates plus the rounded `offset`, clamped to `[0, n - 1]` per axis and flattened. Gathering a
channel through the result is a shift of the channel by one cell.
Built from: `field.index`, `math.*`, `vector.*`.
▲ 0↑ born
# grid.sample (procNode)
The `grid` kernel in sample mode (trilinear) reads the channel at each position: `u = clamp((p - min) / cell - 0.5,
0, n - 1)` and a blend of the eight cells around it, over the grid as one tile with no apron.
Built from: `op.device.kernel` (grid), `core.points`.
▲ 0↑ born
# edui.widgets (module)
Editor-chrome primitive builders for `edui`. Each returns a styled `ui.*`
widget subtree; the common ones are re-exported flat onto `edui`
(`edui.panel{...}`, `edui.text{...}`, `edui.iconButton{...}`, …).
Two things make these "framework" primitives rather than raw `ui.*`:
1. **They read the active theme's editor tokens** (`editor-row-height`,
`editor-selection-bg`, `editor-hover-bg`, `border`, `radius-md`, …) via
`ui.getToken`, so chrome looks like the editor by default and restyles with
the theme. `edui.palette()` returns the resolved set.
2. **Interactive builders register their closures with the currently-building
`edui` app** (`edui.current()`), so you pass `onClick = function() end` and
never touch a callback id.
## Builders
- Containers: `vbox`, `hbox`, `spacer`, `divider`, `panel`, `toolbar`.
- Content: `text`, `svgIcon`, `iconButton`, `button`.
- `panel{ title? | toolbar?, body }` is the standard dockable-panel shell (a
header over a height-filling body).
- `nodeGraph{ key, nodes, edges, selected?, height?, onSelect?, onMove?, onConnect?,
onDisconnect?, onDelete?, onOpen?, onField?, redraw? }` — a node graph on one
canvas: titled nodes with output and input ports, wires between them, and a value on
every input port that has no wire into it. Drag the background to pan, scroll to
zoom, drag a node to move it, drag from a port to another port to connect, drag a
connected input away to pick its wire up, click a value to edit it where it stands,
press Delete to remove the selection. The widget draws and reports; the owner holds
the graph and applies what `onField`, `onConnect`, `onDisconnect`, `onMove` and
`onDelete` report. Nodes with no position are laid out left to right over the wires.
`edui.nodeGraphView(key)` answers where the view stands and, through its `locate`,
where a port or a value is on the canvas.
- `iconButton{ d, tooltip?, active?, onClick, key }` — a hoverable/active icon
square; `d` is an SVG path (or full `<svg>`).
Icons are inline SVG (feather/lucide-style 24-view stroke paths), matching the
editor's icon language — pass a path `d` and it is wrapped for you.
▲ 0↑ born
# points.axis_basis (procNode)
Outputs `axis`, `u` and `w`. `points.cone` and `points.cylinder` build their directions in this basis, and
`curve.branch` turns a growing direction about an axis perpendicular to it.
Built from: `vector.*` and `g.where` over sockets.
▲ 0↑ born
# points.box (procNode)
The centres of a `grid.box` whose cells are `spacing` apart, as Points numbered by `id`.
Built from: `grid.box`, `points.from_grid`, `math.*`.
▲ 0↑ born
# points.cone (procNode)
The direction is drawn evenly over the spherical cap the angle bounds: `ct = 1 - draw0 * (1 - cos(angle))`,
then an angle around the axis from draw 1, and the distance from the apex is `length * draw2 ^ (1/3)`.
Built from: `rand.unit`, `points.axis_basis`, `core.points`, `math.*`.
▲ 0↑ born
# points.count_neighbors (procNode)
The coordinates are rounded to cells and laid in as positions of a fresh point set; every cell's neighbours at
lattice distance 1 (the 6 faces) or up to the cube's corner at 1.732 (the 26 cells) fall inside a search radius
of 1.01 or 1.74, so `core.neighbours` counts them. The tally is written to the original points.
Built from: `core.points`, `core.neighbours`, `vector.floor`, `math.*`.
▲ 0↑ born
# points.cylinder (procNode)
Draws on channels 0 to 2 of the `emit` scope: the angle around the axis, the radial share, and the height.
Built from: `rand.unit`, `points.axis_basis`, `points.shell_radius`, `core.points`, `math.*`.
▲ 0↑ born
# points.disc (procNode)
Draws on channels 0 to 3 of the `points` scope: radius, angle, dome height and jitter.
Built from: `rand.unit`, `core.points`, `math.*`.
```luau
local pile = g.points.disc({ count = 200, radius = 3, height = 1.2 })
```
▲ 0↑ born
# points.filter (procNode)
The value each point is tested by is the `mask` field or the named attribute's values; the test is `> threshold`
or, with `equals`, `== equals` (a label read through its index). `core.select` keeps the elements and renumbers
the linkage. Asked for no test the points are returned as they are.
Built from: `attribute.labels`, `g.compare`, `core.select`.
▲ 0↑ born
# points.from_grid (procNode)
`points.box` and `points.grid` end here: the cell layout leaves the detail so the value reads as Points, and
the element index is written as `id`.
Built from: member writes on the detail and point domains, `g.element.id`.
▲ 0↑ born
# points.from_vertices (procNode)
Reads the point column `P` (and `N` when the geometry's point names list it) and makes Points of that count in
the same order.
Built from: `core.points`.
▲ 0↑ born
# points.grid (procNode)
The centres of a `grid.box` one cell thick at height `y`, as Points numbered by `id`.
Built from: `grid.box`, `points.from_grid`, `math.*`.
▲ 0↑ born
# points.neighbors (procNode)
`core.neighbours` in measure mode, which finds each point's neighbours through a spatial hash and writes
`neighbors`, `nearest`, `centroid` and, when `mean` names an attribute, `neighbor_<name>`.
Built from: `core.neighbours`.
▲ 0↑ born
# points.on_curve (procNode)
Per curve the count is `count`, or at a spacing `floor(length / spacing + 0.5)` plus one for an open curve, at
least 1, held to `maxPoints` in all. `curve.lay_points` gives each laid point its curve and its index along it; the
arc-length fraction is `index / count` around a closed curve (wrapping with `offset`) and `index / (count - 1)`
along an open one (clamped). `core.sample_curve` finds the position and tangent there and `core.interpolate`
carries the curve's attributes.
A point that falls exactly on a vertex takes the direction of either segment that meets there. The point number
`id` is an Int.
Built from: `core.arclength`, `curve.lay_points`, `field.gather`, `core.sample_curve`, `core.interpolate`,
`core.points`, `math.*`.
▲ 0↑ born
# points.on_mesh (procNode)
`points.surface_samples` on the `emit` scope: a face is picked by its share of the total area and a place inside
it from draws 0, 1 and 2, and every attribute of the mesh is carried onto the sample.
Built from: `points.surface_samples`.
▲ 0↑ born
# points.on_surface (procNode)
`points.surface_samples` on the `points` scope, reduced to `P` and the unit normal under each point as `normal`.
Built from: `points.surface_samples`, `core.points`.
▲ 0↑ born
# points.poisson (procNode)
All the candidates of all `maxTries` rounds are thrown at once, about `(radius / minDist)^2` for each round, round
after round, uniformly over the disc. Each round of a fold lets the candidates of that round stand where no point
already standing and no other candidate of the round lies within `minDist`. Every point carries a tag, 1 when it
stands and 1000 when it is a candidate of the round, and `core.neighbours` averages the tags of the points around
each one, so a single query answers both counts. The points that stand are kept at the end by `core.select`, in
round order. Later rounds fill the gaps earlier ones left, because a candidate near a standing point is dropped.
Invariants (the parity test checks these, not the old op's exact points): no two points closer than `minDist`,
every point inside the disc at height `y`, and the disc covered: a probe anywhere in it finds a point within
twice `minDist`.
Built from: `rand.unit`, `core.points`, `core.neighbours`, `core.select`, `g:fold`.
▲ 0↑ born
▲ 0↑ born
# points.relax (procNode)
Each pass asks `core.neighbours` (mode `repel`) for every point's push: the mean over the points within `radius`
of the unit vector away from each, weighted `(radius - d) / radius`. The point then steps `strength * radius`
along it. `g:fold` repeats the pass `iterations` times.
Built from: `core.reduce`, `core.neighbours`, `g:fold`, `math.*`.
▲ 0↑ born
# points.shell_radius (procNode)
`radius = (lo + (1 - lo) * draw) ^ (1 / dims)` with `lo = (1 - clamp(shell, 0, 1)) ^ dims`. Used by
`points.sphere` (dims 3) and `points.cylinder` (dims 2).
Built from: `math.*` over fields.
▲ 0↑ born
# points.sphere (procNode)
Draws on channels 0 to 2 of the `emit` scope: `u` is the height on the unit sphere, then the angle around it,
then the radial share `points.shell_radius` turns into a radius.
Built from: `rand.unit`, `points.shell_radius`, `core.points`, `math.*`.
```luau
local burst = g.points.sphere({ count = 500, radius = 0.2, shell = 0 })
```
▲ 0↑ born
# points.surface_samples (procNode)
`core.sample_surface` picks a face by its share of the total area and a place inside it from the draws of the
named scope; `core.interpolate` carries each attribute of the mesh onto the samples; the normal is made a unit
vector and the sampler's face and corner columns are dropped. `points.on_mesh` and `points.on_surface` end here.
Built from: `core.sample_surface`, `core.interpolate`, `vector.normalize`.
▲ 0↑ born
# procNode: one reusable step of procedural generation
A `.procNode` is a node authored as content: a folder whose `init.luau` is the node. It
returns `proc.define(function(g) ... end)`, the same function-of-`g` form a `.procGraph` is
written in, so a node is a small graph that other graphs call. Its `g:describe` text says
what it makes and when to use it, its `g:input` declarations are what a call passes, and
what it returns (or declares with `g:output`) is what the call gives back.
```
MyNode.procNode/
init.luau -- the node
README.md -- what it produces, its inputs, its outputs
.metadata -- description and tags
```
## What a node states
- **`g:describe(text)`**: what the node produces and when to use it. It is the text search,
the node catalogue and the Inspector show.
- **`g:input(name, default, opts)`**: one input of the node. The default types it (`Float`,
`String`, `Bool`, `Vec3`, `Color`, `List<T>`, `Record`); `opts` adds `type` (for a default
that does not state one, or none), `description`, `options`, `min`, `max` and `optional`. A
call names inputs in a table, or passes them in declaration order.
- **Outputs**: the returned table names them (`return { geometry = geometry }`), and
`g:output(name, value)` declares one as the node builds. A node with one output answers
that value to its caller; a node with several answers a record by output name; a node with
none answers nothing.
- **Written as literals.** What a node takes and gives is read from its source without running
it, the way search and description read it here and the way ZeroMind reads it, so each
declaration states its values as written: `g:describe` and each `g:input` once, at the top
level of the function, with a literal name, default and options (a file constant holding a
literal, or `asset.resolve("<identity>", "<type>")`, reads as one); outputs named by the table
the function returns, or by `g:output`, the same names on every branch of a condition and never
inside a loop. `asset.interface(node)` answers what the source declares, with each part it
cannot read as a problem, and a push refuses a node with such a problem
(`interface.unreadable`).
- **The body** is the node's work: calls to other nodes, arithmetic over sockets, `for`, folds,
steps and state, as the builder README (`proc.module/builder.module/README.md`) describes.
A node that uses another node is recorded in the graph that calls it, so a node can be built
from nodes.
## How a graph calls a node
A node inside a `<family>.nodebox` is called by its box and folder name:
`points.nodebox/disc.procNode` is `g.points.disc({ ... })`, and nothing inside the node states
that name. A node is also reached by:
- its path (`g["/zero/source/fx/ring.procNode"]`),
- `~`, the nearest enclosing package, effect or world root (`g["~/ring"]`),
- its guid,
- a short name only one node answers to (`g.disc`).
A name that matches two nodes and a name that matches none are errors that name the
candidates. Each call is recorded in the calling graph by the node's guid, so the node travels
with every graph that uses it, and a rename or a move keeps the reference. A `.procGraph` is
called the same way and answers what it returns.
## Nodeboxes
A nodebox is the family a node lives in: a namespace folder grouping related nodes (`points`,
`mesh`, `field`, `terrain`). `nodebox.assetType` describes it. A node placed in a box takes the
box's name as its prefix; a node outside any box is reached by path, `~` or guid.
`asset.create("nodebox", family)` makes a box and `asset.create("procNode", name, { into = box })`
makes a node in it.
## Finding nodes
`asset.list("procNode")` lists every node. The `procgen` toolbox's `searchNodes` finds them by
meaning across every published node, and with no query lists the ones this engine holds grouped
by family; its `describeNode` answers one node's inputs, their types and defaults, and its
outputs. Both read what each node's source declares and run no node. The type's `indexing`
recipe embeds a node's README and its source, so a search by meaning finds a node by what its
description and README say it is for: write them in the words someone looking for the node
would use.
## Registration
Every node the asset index lists is known to the procgen registry by what its source declares,
read when it is asked for and read again when the source changes; nothing runs a node to list
it. Its code loads the first time a build runs it. After that, the lifecycle hooks keep it
current: `onChange` loads it again and hot-reloads on an edit, `onDelete` removes it. Editing
`init.luau` cooks again every graph that reaches the node, and only the nodes downstream of the
change do work. `init.luau` is compiled from its current source on each load, so a module it
pulls in uses an absolute require
(`require("@builtin::systems.procgen.proc")`).
What a node is built from is more than its own file. A module its `init.luau` requires while it
builds, at any depth of requires, is part of it: when that module changes, `onDependencyChange`
loads the node again from the module as it now stands. A file its `init.luau` reads with
`vfs.read` while it builds is part of it too, and so is a file a module it calls reads with
`proc.read` (a module's own `vfs.read` is its own and makes no dependency). Writing such a file
loads the node again (`proc.sourceChanged`). Either way the node's definition moves, so every
graph and Generator reaching it cooks again, from one version of everything it was built from.
`proc.builder.readsOf("<node>.procNode/init.luau")` lists the files its last build read. A graph that references a node which is not
registered errors at load, naming the missing guid. A graph that reaches itself, directly or
through other nodes, errors at cook.
## Content requirements
`README.md` carries real text: what the node produces, its inputs and its outputs. `.metadata`
carries a `description` (what the node computes and the value type it answers) and `tags` (the
family, so search finds it). It may state where the node works: `context`, `pulls` and `pushes`
(the values `proc.registry.contexts()`, `"scene"` and `"asset"` name). A node in a nodebox that
states none takes its box's; `procgen` `describeNode` and `searchNodes` answer them.
A node written with `proc.graph` and `g:node` (the `procNodes/` of the procgen package) is called only
by a `proc.graph` graph, through `g:node`. A graph built with `proc.define` does not call it: `g.op.<name>`
of it is refused where it is written, naming the node written in the call form that answers to the same name.
## In the Inspector
Operation: the node's id and what it does, its cost and caching when it declares them, and its
inputs and outputs, from the procgen registry's schema for it. A node that did not register says
so.
▲ 0↑ born
# topology.adjacency
Answer a neighbour relation over a geometry: the faces, points or edges around a point, the faces across an edge, the faces across a face. A neighbour table (offsets into values, 0-based starts) plus a per-element neighbour count written as an attribute. A `group` limits the answer to the faces it selects: the table and the geometry then describe the geometry of those faces, whose faces and edges are numbered from 0 in their order and whose points keep their numbers.
## Inputs
- `geometry`: The geometry whose neighbours are listed.
- `query` (one of: edge_faces, edge_points, face_edges, face_faces, face_points, point_edges, point_faces, point_points): The neighbour relation to answer, named `<source>_<target>`: "point_points" is the points an edge joins a point to, "point_faces" the faces using a point, "face_faces" the faces sharing an edge with a face. The source half also decides what the table and the count attribute are indexed by.
- `element`: One source element id to report the neighbour list of directly, in `adjacency.neighbours`; negative, only the whole table comes back. An id outside the source class's element count is an error.
- `attribute`: Name of the per-source-element neighbour count written onto the geometry.
- `group`: Names a 0/1 attribute on `class` restricting which faces take part in the answer; empty, the whole mesh takes part.
- `class` (one of: edge, point, primitive, vertex): The component class `group` lives on.
- `promote` (one of: all, any): How a group on a class other than "primitive" becomes a face set.
▲ 0↑ born
# topology.borders
For every edge of the geometry's edge table, how many of the faces that take part border it: 1 is an open edge of the participating faces, 2 a shared one. The faces taking part are all of them when no group is named. What `topology.edges` and `topology.boundary` read their open edges from.
## Inputs
- `geometry`: The geometry whose edges are measured.
- `group`: Names a 0/1 attribute on `class` restricting which faces take part; empty, the whole mesh takes part.
- `class` (one of: edge, point, primitive, vertex): The component class `group` lives on.
- `promote` (one of: all, any): How a group on a class other than "primitive" becomes a face set.
## Outputs
- `borders`: Per edge of the `core.edges` table, the number of participating faces that border it.
▲ 0↑ born
# topology.boundary
Find the open boundary of a surface: the edges exactly one face borders, as a 0/1 group on the edge and point classes, plus the ordered boundary loops.
## Inputs
- `geometry`: The geometry whose open edges are traced.
- `group`: Names a 0/1 attribute on `class` restricting which faces take part; empty, the whole mesh takes part.
- `class` (one of: edge, point, primitive, vertex): The component class `group` lives on.
- `promote` (one of: all, any): How a group on a class other than "primitive" becomes a face set.
- `output`: Name the boundary is written under: a 0/1 group on the edge class for the open edges, the same name on the point class for their endpoints, and a detail attribute `<output>EdgeCount`.
▲ 0↑ born
# topology.convert
Convert a component group between classes: points to the faces that use them, faces to their points, edges to their points, and back. `mode` = "any" selects a target element when any of its incident source elements is in the group, "all" when every one is; an element with no incident source element is selected under neither.
## Inputs
- `geometry`: The geometry carrying the group converted.
- `group`: The 0/1 attribute on `from` to convert.
- `from` (one of: edge, point, primitive, vertex): The class the group is read from.
- `to` (one of: edge, point, primitive, vertex): The class the converted group is written to.
- `mode` (one of: all, any): How a target element reads the source elements incident to it: "any" selects it when at least one of them is in the group; "all" only when every one is.
- `output`: Name the converted group is written under on `to`. Left empty it is `group`.
▲ 0↑ born
# topology.edges
Derive a geometry's unique edge set with face adjacency: the `edge` attribute class (points, faces, faceCount, length) plus the edge table. An edge is the unordered point pair it joins; its index is its position in the list sorted by (low point, high point), stable for a mesh.
## Inputs
- `geometry`: The geometry whose edges are listed.
- `group`: Names a 0/1 attribute on `class` restricting which faces take part in the answer. Restricting it changes what is measured: an edge between a participating face and a left-out one is bordered by one face here.
- `class` (one of: edge, point, primitive, vertex): The component class `group` lives on.
- `promote` (one of: all, any): How a group on a class other than "primitive" becomes a face set.
- `output`: Name of the 0/1 edge group written for the edges the participating faces border. Written only when `group` names a selection.
▲ 0↑ born
# topology.islands
Label a geometry's connected components: a per-primitive island id, and for each point the lowest island id among the faces using it. `connectivity` joins faces across a shared edge or a shared point. Ids run from 0 in order of the first face that carries them. Faces a `group` leaves out carry -1.
## Inputs
- `geometry`: The geometry whose connected pieces are labelled.
- `group`: Names a 0/1 attribute on `class` restricting which faces take part; empty, the whole mesh takes part.
- `class` (one of: edge, point, primitive, vertex): The component class `group` lives on.
- `promote` (one of: all, any): How a group on a class other than "primitive" becomes a face set.
- `connectivity` (one of: edge, point): What joins two faces into one island: a shared edge, or a shared point.
- `attribute`: Name the island id is written under, per primitive and per point, plus a detail attribute `<attribute>Count` holding how many islands there were.
▲ 0↑ born
# topology.participation
The faces a group selects, as a 0/1 column over the primitives: all of them when no group is named. A group on another class selects the faces every one of whose elements is in it (`promote` = "all") or at least one is ("any"). The selection every topology node restricts its answer by.
## Inputs
- `geometry`: The geometry carrying the group.
- `group`: Names a 0/1 attribute on `class` restricting which faces take part; empty, the whole mesh takes part.
- `class` (one of: edge, point, primitive, vertex): The component class `group` lives on.
- `promote` (one of: all, any): How a group on a class other than "primitive" becomes a face set: the faces every one of whose elements of that class is in the group, or the faces at least one is.
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
▲ 0↑ born
backing path · systems/procgen.package/graphEditor.module
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.