Log inGet started
▣
module · drop-in viewer
asset⌬ module·part ofpackage procgen.package·originates fromworld 07158574-5…

graphEditor

No description provided.

by◐lumi·posted 7d ago
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
·
assettype · born here
❒asset
# Module (asset type) A module is a reusable, stateless (or globally-stateful) Luau library that other code pulls in with `require()`. Modules are the unit of factored-out logic in a world: helpers, math libraries, format parsers, registries, shared algorithms. ## When to use one - You have logic you want to share across multiple components, services, scenes, or tools. - You have a registry, lookup table, or shared cache that callers all read from / write to. - You want a stable API surface (`M.fn`) other code can depend on. If the logic is bound to one entity, use a `.component`. If you want a named UI of fields rather than functions, use a `.preset`. ## Where it lives - Source: `/zero/source/.../<name>.module/` - Identity: the source path with `/` written as `.` and each segment's type suffix dropped — `/zero/source/demo/beats.module` is `demo.beats`, and `/zero/source/kit.package/beats.module` is `kit.beats`, the container's name kept and its `.package` suffix gone. `asset.create` returns this string, `asset.resolve(...).identity` reads it back, and the `require` forms below write it as `<identity>`. - Folder shape: - `init.luau` (or `init.lua`) — module body. **Required.** Returns a table (conventionally named `M`). - `README.md` — instance-level documentation. **Required.** - `.metadata` — agent-editable tags + free-form fields. **Required.** ## How to create one ```luau asset.create("module", "<name>") -- Creates: /zero/source/<name>.module/ -- init.luau (canonical `local M = {} … return M`) -- README.md (instance README template) -- `folder` places it in a subfolder of /source, and that subfolder -- leads the identity: `{ folder = "lib" }` produces `lib.<name>`. -- `into` authors it inside a resolved container (a package, a toolbox), -- whose own identity leads the same way: asset.create("module", "<name>", { folder = "lib" }) asset.create("module", "<name>", { into = asset.resolve("myPkg.package") }) ``` ## How it operates 1. **Registration.** Writing `init.luau` into a `.module/` folder indexes the asset and registers the module path with the resolver. 2. **Loading.** The first `require("<identity>")` runs the file's body and caches the returned table. Subsequent requires from anywhere return the **same table** — this is the shared-state lever for "module-local" registries. The body runs in an environment of the module's own: a name it declares without `local` is readable by the module's own functions and by no other script, so an export reaches its callers through the returned table and nothing else. Requiring a component's folder runs its chunk the same way, once, and its `update`, `start` and `public` stay in that environment; a component runs on an entity through `entity(id).component.add(name)`. 3. **Hot reload.** Editing the file re-runs the module body and folds what it returns into the table already loaded, so that table keeps its identity: a caller that captured it with `require` reads the new source's functions and values through the reference it already holds, and a later `require` returns that same table. A key the new source drops goes away; a key something set at runtime stays. The body's own `local`s start again from their initial values, so state that must outlive an edit lives in `modules.state()` (`guides { path: "core/scripting-and-tasks" }`). 4. **Identity resolution.** A module is reached by its `<identity>`, and where the caller lives decides which spelling of it resolves. For a module in a **world**, from a caller in that same world — another module, a component, a scene entrypoint, or an `execute` chunk: - `require("<identity>")` — the identity on its own, resolved against the caller's own root. It carries every folder segment, so a module created with `{ folder = "demo" }` is `require("demo.beats")`. - `require("@root::<identity>")` — the same root-relative resolution, written out. - `require(".sibling")` — a module in the same folder, by its bare name; `..name` steps up one folder, `...name` two. Logs, stack traces and hot-reload notices name that same module `@local.source.<identity>` — its key under the world source root. The `<identity>` tail of that key is the form above, so a trace reading `@local.source.demo.beats` is `require("demo.beats")`. For a module in a **library** — content under `/zero/source/libs/@<lib>/` — `@<lib>::` addresses that library's root from anywhere: `require("@builtin::modules.transform")`. Mounting a world as a library moves its files under such a root and flips the caller root to `@<lib>::`, which is what keeps the root-relative forms above resolving across the move. ## Discovery - `asset.list("module")` — every registered module. - `asset.inspect("<name>")` — public functions, source path, this type README. - `ls /zero/source/<name>.module` lists the files the folder holds. ## Authoring conventions - Return a single `M` table from `init.luau`. Top-level statements with side effects run on first require — useful for one-time initialization, dangerous if they touch the engine before it's ready. - Write exported functions as `typed function`, not `function`: the argument types are enforced at the call and the checker knows the signature, so a caller's mistake is reported where they made it. - Annotate exported functions with `--!desc` / `--!arg` / `--!return` / `--!example` so the LSP, `tools.list`, and `asset.inspect` surface them. A function with no `--!desc` is reachable only by someone already reading this file. - Declare a VALUE with `Field.<kind>(default, mode, description)`, and a table whose members are reached through a metatable with `--!members <TypeName>` — the latter gives the surface its accepted member set, its type, and its documentation from one table. See `guides { path: "core/authoring" }` for all four rules and `modules/api/engine/entity.module/members.module` for a worked member table. - Keep module state in module-local upvalues or in `modules.state()`. A name the body declares without `local` lives in the module's environment, which every run of the module shares: a function the previous run left in place reads what the reload assigned. Module-locals start again from their initial values with each hot reload; the table `modules.state()` returns is the same table on both sides of every reload. - Prefer focused modules over kitchen-sink modules. If two halves of a module have no shared state, split them. ## Common pitfalls - **Cyclic requires.** A `require` chain that loops will return the partial table (the half built before the cycle was detected). Design for one-way dependencies. - **Engine-time side effects.** Don't `entity.spawn` at module top-level — the engine may not be ready. Expose an `M.init` and call it from a scene entrypoint. - **Hot-reload + module-local cache.** A module that caches expensive computation in upvalues loses that cache on reload — fine for dev, but be aware in performance work. - **`init.lua` vs `init.luau`.** Either works, `.luau` is canonical. - **Writing one while play runs.** A `/source` write made while play is running lands on the play shadow, and a guarded play-exit discards it unless it is kept. One `.module` write shadows `init.luau`, the `README.md` and the `.metadata` together. `vfs.write(path, bytes, { durable = true })` skips the shadow, and `vfs.promotePlayShadow` keeps what is already on it — the `core/vfs` guide has the model and what each route costs. ## Related types - `.component` — for entity-bound state + lifecycle hooks. - `.service` — generates content you don't have yet (a mesh, sound, texture, …) via a metered provider. - `.tool` — for a single agent-callable function with a YAML schema. - `.package` — to group several modules + components + scenes into one shippable folder. ## In the Inspector Exports: what the module exports (name, kind, signature), parsed from its source without running it, with **Check** (the LSP's diagnostics for its entry file) and **Open**.
▲ 0↑ born
·
procpack · born here
❒asset
# 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
·
nodebox · born here
❒asset
# 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
·
nodebox · born here
❒asset
▲ 0↑ born
▣
module · born here
❒asset
▲ 0↑ born
·
procpack · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
nodebox · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
nodebox · born here
❒asset
▲ 0↑ born
·
procpack · born here
❒asset
# 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
·
nodebox · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
toolbox · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
nodebox · born here
❒asset
# 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
·
nodebox · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
nodebox · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
nodebox · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
nodebox · born here
❒asset
▲ 0↑ born
·
procpack · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
procnode · born here
❒asset
# Scatter (node) A scattering node: its `init.luau` holds the graph that does the work, so the declared inputs are this node's inputs and the declared output is this node's output. The node is its pipeline, in one self-contained asset. It makes points on the surface (`points.on_surface`, area-weighted), gives every point its own random yaw, lean and scale (`instances.randomize_transform`), and places the source on those points (`instances.on_points`). The transform is read from the same point cloud the instances sit on, so each copy takes the pose of its own point. A node is findable by what it does: the `procgen` toolbox's `ops` with the query `scatter` lists it beside `points.on_surface` and `instances.on_points`. Editing this node's `init.luau` cooks again every graph that calls it. ## Inputs - `surface` (Geometry): what the copies land on. - `source` (Geometry): the shape being copied. - `count` (Int, default 200): how many. - `seed` (Int, default 1): changes the arrangement. - `scaleMin` / `scaleMax` (Float, defaults 0.8 and 1.4): per-copy size range. - `tilt` (Float, default 0.15): radians of random lean. ## Output - `instances`: a population, the points each carrying the source as its `Mesh`. ## Authoring a node A node is a graph returned from a `.procNode`'s `init.luau` (`proc.define`). `asset.create("procNode", name)` scaffolds that shape, and `procNode.assetType/README.md` describes what a node states and how graphs call it.
▲ 0↑ born
❒
package · born here
❒asset
# oceans A sea and the ground under it, as one package: the wave simulation, the surface that renders it, the seabed it is seen through, and the answer to "how high is the water here" that gameplay needs to float things on it. ```lua local Ocean = require("@builtin::systems.oceans.modules.ocean") Ocean.create({ preset = "gale" }) ``` That is a whole ocean — waves out to the horizon, foam on the breaking crests, a seabed below, and a surface that follows whoever is looking at it. To float something on it: ```lua local ocean = Ocean.forEntity(oceanEntityId) boat.position = { x, ocean:sampleHeight(x, z), z } ``` It is a building block. The sun the water glitters under, the ambient its foam sits in and the sky it reflects all come from the scene, through the same directional light and reflection probes every other material in the engine reads — so an ocean drops into a world that already has lighting instead of bringing a second set, and changing the scene's light changes the water with it. ## What it does The simulation is **spectral**: instead of animating individual waves, it builds the sea's energy across every wavelength and direction from an oceanographic spectrum, then inverse-transforms that into a height field. A real sea is what comes out — wind speed alone decides wave height, and it rises with wind the way a fully developed sea does, following the Pierson–Moskowitz law. It runs in **cascades** — independent wave fields over different patch sizes, each band-limited to its own range of wavelengths. A tile of roughly two kilometres carries the long waves and one of a few hundred metres the short ones; they sum into a surface with detail at every scale, and because the bands are disjoint no wavelength is synthesised twice. Above them a **Gerstner bank** adds the swell analytically, which no cascade can hold: a wave longer than its own patch has nowhere to live, and swell arrives from a distant storm on its own heading anyway. **Foam** is not painted on. It comes from the Jacobian of the wave displacement: where the surface folds over itself, the wave is breaking. Whitecaps appear where and when the geometry actually breaks, accumulate into a persistent world-space field, and streak behind the crest as they decay. **Colour and transparency are the same mechanism.** Per-channel Beer–Lambert extinction over the distance light travelled through water gives both — red dies within a metre, blue survives tens. That is why shallow water over sand reads turquoise and the open sea reads near-black blue without switching materials. **The seabed** is one noise field read twice: scaled it is the terrain's relief, unscaled it picks between sand and rock, so outcrops stand on the high ground and sand fills the hollows. The sun's caustic web moves over both, bent by the horizontal displacement of the water directly above it. Bodies land on it: the pass that raises the ground writes the same field out as a height patch that travels with the viewer, so what a body stands on is what the frame drew. Nothing here ships an image. Every map the surface and the seabed sample — the foam patterns, the sand, the rock, the caustics — is grown on the GPU at startup. ## The pieces | Asset | What it is | |---|---| | `components/Ocean` | Puts a sea on an entity. Its fields are the sea state; the simulation is rebuilt from them, so an ocean survives a scene reload. | | `modules/ocean` | The substrate: `attach`, `create`, `forEntity`, `sampleHeight`, `sampleDisplacement`. | | `components/Buoyancy` | Floats a rigid body on any water, inside the physics step. Its fields are the hull: how dense it is, how quickly it settles, and what displaces water (its colliders, a stated box or what it draws). | | `modules/buoyancy` | The float: `attach`, `forEntity`, `list`, `clear`, `submergedFraction`, `restingY`. | | `modules/oceanPresets` | Nine named sea states, from `calmClear` through `gale`. | | `modules/oceanQuality` | The four tiers — how finely a sea is resolved, as distinct from what sea it is. | | `modules/oceanClipmap` | The camera-following ring geometry both the surface and the seabed are built on. | | `modules/oceanFloor` | The seabed: terrain, ground materials, caustics and the ground physics collides with. | | `modules/oceanScene` | The seam between the scene capture and the materials that sample it. | | `shaders/oceanSurface` | The water: cascade normals, absorption, exact dielectric Fresnel, subsurface scattering, sun glitter, foam, and the view from below. | | `shaders/oceanFloor` | The seabed: sand and rock blended by terrain noise, under a caustic web. | | `materials/oceanWater`, `materials/oceanSeabed` | The default looks. | | `renderFeatures/oceanScene` | Copies the frame's colour and depth so the surface can refract what is behind it and measure its own water column. | | `computeShaders/ocean_*` | The ten-stage GPU pipeline. | ## Wind is the control Significant wave height goes as the **square** of wind speed, so that one field does most of the work: | Wind | Sea | Wave height | |---|---|---| | 3 m/s | glassy, a mirror | ~0.2 m | | 11 m/s | working sea, occasional whitecap | ~2.5 m | | 24 m/s | gale, breaking and streaked | ~12 m | Everything else shapes character rather than size: `choppiness` pinches crests into peaks, `standingWaveRatio` runs waves both ways instead of only downwind, `jonswapGamma` sharpens a fetch-limited sea, and the foam layers decide how readily a breaking crest whitens and how long it streaks. Read `documentation/oceans.guide` for the full walkthrough, including the seabed, buoyancy, reflections and what each tier costs.
▲ 0↑ born
·
procnode · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
▣
module · born here
❒asset
# 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
·
nodebox · born here
❒asset
▲ 0↑ born
❒
package · born here
❒asset
▲ 0↑ born
◇
component · born here
❒asset
# Generator A live procedural generator: it binds a `.procGraph`, plays the graph's timeline and stands each of the graph's outputs in the scene as children of the entity it is on. Change a param and the output is made again in place; in edit and in play alike, nothing it makes is written to the world's source. ```lua local graph = asset.create("procGraph", "crate") -- a box with a `size` input; edit its init.luau local e = entity.spawn("crate") e.component.add("Generator", { graph = graph }) e.component.get("Generator").params = { size = 2 } ``` ## Fields | Field | What it does | |---|---| | `graph` | The `.procGraph` it plays. | | `params` | Overrides for the graph's declared inputs, by name. | | `output` | One declared output to stand; empty, the default, stands every output, one root each. | | `playOn` | 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
·
nodebox · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·❒asset
# 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
·
procpack · born here
❒asset
# 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
·
nodebox · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
nodebox · born here
❒asset
# component (nodebox) component.set gives every element of a class a component with the fields given.
▲ 0↑ born
·
nodebox · born here
❒asset
# 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
·
nodebox · born here
❒asset
▲ 0↑ born
·
nodebox · born here
❒asset
# 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
▣
module · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
▣
module · born here
❒asset
# 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
·
nodebox · born here
❒asset
# 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
·
procgraph · born here
❒asset
# Scatter Strews copies of one shape over the surface of another, each with its own turn, lean and size. Both the surface and the thing scattered are inputs, so this covers rocks on a hillside, crates on a deck, bushes along a bank or rubble across a floor: wire it a different `source` and it is a different field. It makes points on the surface (`points.on_surface`, area-weighted), gives every point its own random yaw, lean and scale (`instances.randomize_transform`), and places the source on those points (`instances.on_points`). The transform is read from the same point cloud the instances sit on, so each copy takes the pose of its own point. ## Inputs | Input | Type | Default | What it does | |-------|------|---------|--------------| | `surface` | Geometry | none | What the copies land on. Points are area-weighted, so a bigger face gets proportionally more. | | `source` | Geometry | none | The shape being copied. | | `count` | Int | 200 | How many copies. | | `seed` | Int | 1 | Changes the arrangement. The same seed always gives the same field. | | `scaleMin` | Float | 0.8 | Smallest per-copy size multiplier. | | `scaleMax` | Float | 1.4 | Largest per-copy size multiplier. Equal to `scaleMin` means uniform. | | `tilt` | Float | 0.15 | Radians of random lean off vertical. `0` stands every copy upright. | ## Output `instances`: a population, the points each carrying the source as its `Mesh`. A Generator draws it as one hardware-instanced draw call per source; `instances.realize` merges it into a single Geometry. ## Calling it Another graph calls a `.procGraph` as it calls a node, by its path or identity, naming the inputs in a table; the call answers what the graph outputs (`instances`). Its declared inputs are the call's inputs.
▲ 0↑ born
·
procnode · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
nodebox · born here
❒asset
▲ 0↑ born
▣
module · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
nodebox · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
nodebox · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
nodebox · born here
❒asset
# 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
·
nodebox · born here
❒asset
▲ 0↑ born
·
nodebox · born here
❒asset
# abiBox A node for the procgenAbi suite: it is called through the builder and written out in a graph.
▲ 0↑ born
·
nodebox · born here
❒asset
▲ 0↑ born
·
nodebox · born here
❒asset
# 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
·
nodebox · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
procpack · born here
❒asset
# 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
·
nodebox · born here
❒asset
# 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 · born here
❒asset
# assetType (asset type) An asset type says what a kind of asset is: the files an asset of the kind is made of, the code that gives it behaviour, the interface a person uses to work with it, and how it is found, pictured and checked. A `<typename>.assetType/` folder declares a type called `<typename>`, and every `<name>.<typename>/` folder is an asset of it. `assetType` is itself a type, so a type is an asset like any other: it is created, versioned, installed and contributed the same way. ## Making a type `asset.create("assetType", "<typename>")` scaffolds a type. The type registers the moment its folder is written, anywhere under `/zero/source/`, and from then on `<name>.<typename>/` folders are its assets. An asset of a type that is not registered yet is not an asset, so make the type first. A type folder is a root: `~.x` in any file inside it names the type's own `x`, wherever the type is installed. Keep the type's whole implementation inside its folder and reach it that way. What a type folder must hold is set by this type's own `type.yaml`, the same way every type's `type.yaml` sets what its assets hold. `asset.validate` on a type reports anything missing, and the scaffold produces a folder that passes. ## What sets your type's schema (`type.yaml`) `type.yaml` is the structural contract every asset of the type is checked against. The keys below are the ones the engine reads; a key not listed here is ignored. | Key | What it controls | |---|---| | `suffix` | The folder suffix of the type's assets. Defaults to `.<typename>`. | | `primary` | The file or files that are the asset's content: what the asset resolves to when it is used. | | `entry_files`, `primary_aliases` | Further files that load as the asset or share its identity. | | `is_container` | Whether the suffix stays in the identities of what the asset holds. | | `is_opaque_walker` | Whether a walk of the tree stops at the asset and registers only its primary. | | `plural_dir`, `classify_exts` | Loose files in a folder of this name, or with these extensions, are assets of the type. | | `is_text_default` | Whether the asset's files are text unless stated otherwise. | | `broadcasts_subassets` | Whether assets nested inside an asset of the type are found by their own short name. | | `root` | `true` makes every asset of the type a root, so `~.x` inside one names that asset's own `x`. | | `contract_ref: { field, category }` | A key of the primary file that names another asset the asset is built on. | | `files: { required, optional }` | The files an asset holds (below). | | `allow_unlisted` | `false` reports every file the type does not declare. | | `aliases_from: [{ file, key }]` | Further names an asset answers to, read from its own files. | | `settings:` | Typed per-asset settings (below). | | `inspector:` | The interface people use for the type and its assets (below). | | `preview:` | How an asset is pictured (below). | | `references:` | Names of other assets the type's files carry (below). | | `indexing:` | What search reads for each asset (below). | | `describe:` | How a missing description is generated (below). | ### The files an asset holds Each entry under `files.required` or `files.optional` is `{ path, kind: file | folder, one_of_group?, content? }`. `*` in a path matches within one segment. Entries that share a `one_of_group` are satisfied by any one of them. `content` lists checks on the file's bytes: | Check | Passes when | |---|---| | `non_empty` | the file has content | | `json_field { key, type?, non_empty?, accept? }` | the JSON holds the key (a dotted path) with that shape | | `yaml_field { key, type?, non_empty?, accept? }` | the same for YAML | | `described { keys?, headings?, message? }` | the file says what the asset is (see "Ask your assets to say what they are") | Each check takes a `message`, which is what the author is told when it fails. ### Settings `settings: { values, schema: { type | instance, export? } }` gives every asset typed settings. `values` names the file an asset's values live in; `schema.type` declares one schema for the whole type, and `schema.instance` names a file each asset declares its own in. `asset.create` writes the resolved values, and a write to the values file reaches `onChange` with what changed. ## The human side (`inspector.luau`) A type is used by people through its Inspector, and without one the type is usable only from code. The Inspector is the type's interface for a person: for a procedural graph it is the graph editor, for a kit it is the tracking view of what the kit holds and what is still missing. Writing it is part of writing the type. `inspector: { type, instance? }` in `type.yaml` names the files. `type` is the interface for the type itself and for its assets; `instance` names a file an asset may ship to add its own. Each returns: ```luau return { sections = { { id = "graph", title = "Graph", build = function(ctx) ... end } }, replace = { contents = { ... } }, -- replace a default section by id order = { "graph", "contents" }, -- section order } ``` `build(ctx)` receives the asset and builds the section. A section's `title`, and the text its rows carry, is a string or an `i18n.text` value read from a catalog (`i18n.text("inspectors.mesh.title")`), which follows the editor's language. `asset.create("assetType", ...)` scaffolds a working `inspector.luau` to start from. `asset.validate` on a type that names no inspector, or names one that does not load, reports it. ## Say what your type is for (`about.yaml`) Every type carries an `about.yaml` that says what the type is for, in three plain-language texts: ```yaml intent: "Sets the typeface of the text in your game: UI, labels, signs and 3D lettering." function: "A font, from a TTF, OTF or WOFF file, made available to all of Zero's text, including 3D text." usage: "You choose it by name for your UI or your text, and the text appears in that typeface." ``` - **`intent`** is what the type is for in someone's game, in the words a game maker uses. Up to 800 bytes. - **`function`** is how it works today: what an asset of it is made of and what the engine does with it. Up to 1500 bytes. - **`usage`** is what a person or an agent does with one. Up to 1200 bytes. Write about the type as a whole, not one asset of it, and say what it does now. Agents read these texts to learn when to reach for the type and how; people searching find the type by them. The scaffold starts each text with `[[describe]]` and a note on what to write; a text that still starts that way counts as not written. `services describe` can write them. They show up in `asset.inspect` as `detail.about`, in `assetTypeRef:getAbout()`, at the end of the type's `types/<typename>` guide, and at the top of the type's Inspector. ## Ask your assets to say what they are A type can require each asset to carry a description: a file, written by the author or generated, saying what the asset is and what it is for. Declare it with a `described` check: ```yaml files: required: - path: "README.md" kind: "file" content: - { check: "described", message: "Say what this widget is for and where it goes." } ``` A file that is missing, empty or still has a line starting with `[[describe]]` fails the check, and so does a YAML file whose named `keys` are missing or over their `max_bytes`, or a Markdown file missing a named heading. `asset.validate` names the asset, and a push that carries it is refused with `message`. `services describe` finds such assets and writes their descriptions. `describe:` steers what is generated: ```yaml describe: guidance: "Say what the widget shows, what input it reacts to, and where it fits in a UI." examples: - "<identity of an asset of this type whose description reads well>" ``` An example is an asset of your type whose description is a true, written one, never a placeholder and never the asset being described; with none, leave `examples` empty. A type whose assets the model cannot perceive (a sound, a motion) says so in `guidance`, and the description is drawn only from what the asset's own files state. Start the file unwritten in your `template/`: a heading and one line starting with `[[describe]]`, never example content, which every new asset would carry verbatim. A type whose assets say what they are through their own content (a mesh, a texture) declares no check. ## The type's code (`behavior.luau`) `behavior.luau` returns one table. These are the keys the engine reads from it; anything else on the table is the type's own. ### What the type's assets can do | Key | Shape | What the engine does with it | |---|---|---| | `ref` | `{ [name] = function(self, ...) }` | Every function becomes a method on every asset of the type: `ref:name(...)`. | | `modules` | `{ [name] = require(".x") }` | Code the type's assets reach with `asset.containing(__FILE__).modules.<name>`. | | `events` | an events schema | Events an asset fires. Readers subscribe through `ref.events.<name>:connect(...)`; the type fires them. | | `refShapes` | `{ [method] = function(self) -> (typeExpr, source?) }` | The result type of a `ref` method for one particular asset (below). | | `namePattern` | a Lua pattern | The names `asset.create` accepts for the type's assets. Default `^[A-Za-z][A-Za-z0-9_]*$`. | | `derive` | `{ output, module, compute, fingerprint, matches?, required?, timeout? }` | A file computed from the asset when it is committed; a `required` one holds the commit until it lands. | Some `ref` names mean the same for every type, and the engine calls them: - **`instantiate(self, target?, opts?) -> (root, idMap)`**: puts the asset in a scene. Defining it is what makes the type scene-instantiable. Its return is a contract (below). - **`inspect(self) -> detail`**: what `asset.inspect` reports for the asset beyond the common record (below). - **`preview(self, opts)`**: renders the asset's picture; it wins over the type's `preview:` recipe. - **`previewMaterial(self) -> { material }`**: the material a `surface` preview paints. - **`saveDefinition(self)`**: writes the asset's in-memory edits back to its files in edit mode. Some names are answered for every asset before the type is asked, so a `ref` entry of the same name is never reached: `getSource`, `getBytes`, `getText`, `exists`, `deps`, `canInstantiate`, `canPreview`, `preview` (which calls yours when you define one), `meta`, `runtime`, `events`, `typeRef`, `modules`, and the residency flags `has_backing_asset`, `has_runtime_changes`, `cpu_resident`, `gpu_resident`. ### When the engine calls the type | Hook | Signature | When it runs | |---|---|---| | `onCreate` | `(name, opts) -> { [path] = contents }` | On `asset.create`. Returns the files of the new asset, laid over the type's `template/`. Annotate `opts`: its type is the schema `asset.create` checks the caller's arguments against. A `""` key writes a single loose file. | | `onReplace` | `(dest, files) -> files` | On an `asset.create` over an asset already at `dest`, after `onCreate`, so what the author added can be carried into the replacement. | | `onRegister` | `(self)` | Once per asset, when it first registers: when the world loads, on a live `asset.create`, and when an asset arrives later (installed, pulled, synced). This is what makes an asset take effect the moment it is written. | | `onChange` | `(ref, change)` | On every write to a file inside an asset of the type. `change` is `{ path, asset, type, kind, origin }`, plus `settings` and `settingsChanged` for a write to the settings values file; `kind` is `edited` or `seeded`, `origin` is `local` or `remote`. Filter on `change.path`. Writes to one asset arrive in order, each after the hook for the one before has finished, and the hook may wait on what it reads. A write the hook makes inside its own asset arrives later like any other, so keep the hook convergent. | | `onDependencyChange` | `(ref, change)` | When a module the asset's code requires is re-run, or a name the asset carries starts resolving. `change.kind` is `dependency`. | | `onDelete` | `(ref)` | When an asset of the type leaves the world: deleted, or the world it is in is swapped away. | | `validate` | `(ref) -> { { code, message, severity?, path? } }` | On `asset.validate`, after the `type.yaml` checks, for the type's own rules. `severity` defaults to `error`. Runs on every push, so a type's rules hold at publish. A hook that fails reports `validate.hook_failed`. | ### Settings hooks | Hook | When it runs | |---|---| | `settingsDefaults(name, opts, asked) -> table` | Once per `asset.create`, for values the schema's defaults do not give. | | `settingsCorrections(values, name, opts) -> (corrections?, reasons?)` | On every settings write. A correction to a field the writer named refuses the write, giving the reason. | | `settingsDerivedDefaults(values, explicit, changed)` | On a settings write, for fields derived from others. | | `normalizeSettings(values, stored?)` | On every read and write, to map an earlier spelling of a field to the current one. | | `settingsHealed(values, stored)` | On a read, to repair stored values. | | `settingsSchemaProblem(schema, ref) -> string?` | When an asset's own schema is read; a returned string refuses the schema. | `creationInputs` (`{ [opt] = true | selectorOpt }`) names `asset.create` options that are inputs to the type rather than settings. ### Reloading An edit to `behavior.luau` reaches existing assets with no restart. ## How an asset is pictured (`preview:`) | `kind` | The picture is | The type provides | |---|---|---| | `instantiate` | the asset placed in the preview studio through its `instantiate` | `ref.instantiate` | | `image` | a file the asset holds, fitted to the frame | `source: { files }`, the candidate files in order | | `surface` | the asset painted on a mesh | `mesh`, and `ref.previewMaterial` | | `custom` | whatever `ref.preview` returns | `ref.preview` | ```yaml preview: kind: surface mesh: sphere ``` `angle: [yaw, pitch]` turns the camera. The studio owns the light, the backdrop and the framing, so an asset pictures the same in every world. A type with no `preview:` that defines `instantiate` still answers `ref:preview()`. A picture is rendered when something asks to view it (the editor showing the asset, `ref:preview()`), when previews are generated on request (`assets.generatePreviews`), and when the asset is staged. An edit alone renders nothing. The picture records what it was made from, so a stale one is told from a current one. ## The names your type's assets hold (`references:`) Where your type's own format or API carries the name of another asset, declare it here: ```yaml references: keys: # a top-level key of the type's files that holds a name, or a table of them - { key: "shader", type: "shader" } args: # an argument of a call, counting from 1 - { call: "g:node", position: 2, type: "procNode" } fields: # a field of the options table a call takes - { call: "fx.beam", field: "material", type: "material" } ``` `type` is the category the name resolves in. `asset.declareReferenceKey`, `asset.declareReferenceArg` and `asset.declareReferenceField` declare the same from a script. ## What search reads (`indexing:`) `indexing:` is how assets are found. Search reaches an asset only through what its type's `indexing:` names, and adds nothing of its own. That includes you: an asset indexed badly is one you cannot find later, when you are making something else and need it. A type whose assets are never looked for says `indexing: none`. ```yaml indexing: data: # text, read from the asset's files - role: identity extractor: verbatim source: { file: "README.md" } - role: interface extractor: symbols source: { files: ["init.luau"] } - role: implementation extractor: verbatim source: { files: ["init.luau"] } content: # the picture - role: identity modality: image source: { file: "preview.png" } facets: # what a query filters and re-ranks by file: ".metadata" rigged: { type: bool, from: { components: { contains: SkinnedModel } } } roughness: { type: number, filter: range, from: { file: "mat.yaml", field: "floats.roughness" } } ``` **Roles.** Each `data` and `content` entry has a `role`, and a query matches in two slots: - **`identity`**: what an asset is. For anything you look at, the picture; for anything else, the text that says what it is. - **`capability`**: what it does and how it is made. `interface` (the signatures or shape a caller uses) and `implementation` (the body) both count as capability, and a type with code usually declares both. Ask what a person would recognise an asset of the type by, and what it does, and point each role at the file that says it. For text, that is the asset's description: the file your template starts with a `[[describe]]` block, which the author writes or `services describe` fills in (see "Ask your assets to say what they are"). **`data`.** `extractor: verbatim` embeds a file's text as it is; `symbols` embeds the declared signatures of code. `source` is `{ file }` or `{ files: [...] }`. Never index a file name (`source: { field: name }`): one or two words carry nothing to match. **`content`.** `modality: image` embeds the picture itself, so a text query matches what the asset looks like. A type whose assets are looked at makes the picture its identity. **`facets`.** What a query narrows and re-ranks the matches by. `file` is embedded whole, so keys nobody declared in it are still found. Each further key is computed for every asset and states a value for every one, including `false`: a file can imply "not rigged" but never say it, and a query separates rigged from unrigged only because the unrigged ones say `rigged: false`. A key is `{ type: bool | number | enum, filter?: range, from }`, where `from` is `{ file, field }` (a field of one of the asset's files, dotted for nesting), `{ metadata: key }`, or `{ components: { contains | any_outside } }` (what the asset's entities carry). ## What blocks a push A push checks every asset it carries against its type. These refuse it: - a required file that is missing, a `content` check that fails, a `described` file not written yet; - an error from the type's `validate`. Unlisted files and warnings do not. A `described` file blocks only a push that changes the asset. When a push is refused, the message names each asset and what it lacks. `world.publishBlockers()` and `zm status` read the same list without pushing. ## Typing a result from the asset (`refShapes`) A `ref` method whose result depends on the asset (one entry per child folder, per row of a config, per control a map declares) can only be annotated as `{ [string]: Thing }`, which accepts any key. `refShapes` states the result for each asset instead: a function returning a Luau type expression for that asset, and the module whose types the expression uses. Read the asset; never run it. ```lua -- inputMap.assetType/behavior.luau M.refShapes = { activate = function(self): (string, string) local names = {} for _, control in ipairs(M.ref.controls(self)) do table.insert(names, control.name .. ": Handle") end if #names == 0 then return "", "" end return "{ " .. table.concat(names, ", ") .. " }", "@builtin::modules.zinput.scheme" end, } ``` With it, `map.jump:onPressed(fn)` is checked and `map.noexisting` is reported. A per-asset type applies where the call site names which asset: a component field whose default names it, or an annotation `AssetRef<"inputMap", "<identity>">`. `asset.resolve(name, category)` carries only the category, and a name computed at runtime carries nothing, so calls on those keep the declared return. The type belongs to the call's result: index it there, or bind what you need from it there, and the names stay checked. `require("@builtin::assetTypes.assetType.shared.refShapes").published()` shows what was published: `categories` (each type's method surface) and `returns` (per-asset results by identity, then method). An identity missing from `returns` was never published. Results are recomputed when an asset is written; at most 64 assets of one type are read. ## Putting an asset in a scene (`ref.instantiate`) A caller writes one piece of code against every instantiable type, so what `instantiate` returns is fixed: ```lua local Instantiable = require("@builtin::assetTypes.assetType.shared.instantiable") M.ref = { instantiate = function(self, target, opts) local root = Instantiable.root(self, target, opts) -- compose the asset under `root`, now return Instantiable.result(self, root, idMap) end, } ``` **In.** `target` is the entity the asset lands under, or onto for a hierarchy type; with none, the type spawns a new root. `position`, `rotation` (three numbers in degrees, or a quaternion), `scale`, `name` and `temporary` mean the same for every type: `Instantiable.root` applies them to a root you make, `Instantiable.place` to one you adopt. Document any options of your own in the hook's `--!arg` docs. **Out.** `(root, idMap)`. `root` is the composed root, live when you return: build synchronously, never defer to a component's `awake`. `idMap` maps each original id to the spawned one, so a component that composes the asset again on every load can pass it back in and keep references into the composition. Every call goes through `Instantiable.result`, which refuses a root that is not live and turns a missing `idMap` into `{}`. ## Inspecting an asset (`ref.inspect`) `asset.inspect(ref)` builds a record every asset shares (`identity`, `name`, `guid`, `source`, `typeName`, `typeDefinitionPath`, `scope`, `origin`, `description`, `tags`), then calls `ref.inspect(self)` for the type's own `detail`: ```lua M.ref = { inspect = function(self) -- read the asset's own files, never runtime state: inspect works on an asset -- that has not been compiled, uploaded or registered return { --[[ what makes this asset what it is ]] } end, } ``` Without it, the record is the shared part alone, and an agent inspecting the asset learns nothing of what it is. A hook that raises leaves `detail` empty and sets `record.warning`. ## Discovery - `asset.list("assetType")`: every registered type. - `asset.inspect("<typename>")`: the type, its README and its `about.yaml` texts. - `asset.resolve("<typename>", "assetType")`: the type itself.
▲ 0↑ born
·
assettype · born here
❒asset
# 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
❒
package · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
▲ 0↑ born
·
assettype · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
▣
module · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
assettype · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
# 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
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 0↑ born
·
procnode · born here
❒asset
▲ 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

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.