Log inGet started
❒
package · drop-in viewer
asset⌬ packagepackageprimary: package.yaml·originates fromworld 07158574-5…●1problem

procgen

No description provided.

byzero-proxy @ DESKTOP-DB3UJOJ·posted 3mo 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
◇
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` | `"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()`. | | `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 = "topics/visual-effects" }`). The play's own `playback()` adds its Generators' costs together, and `procgen.frames` and `procgen.onPresent` take the play's entity as they take a Generator's. 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.
▲ 0↑ born
·
procpack · born here
❒asset
# abi The four 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 | ## 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. - 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`. Outputs: `columns` (Record of Columns) and `info` (the kernel, the variant key, the elements, the determinism the shader declares, the specialization). 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. **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) | **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 | `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 ten has one. ## 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. ## 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}`. The op waits across frames; `report` states the names, elements, bytes and frames. Anything that waits on a readback value is a planning step. ## cpu.call `fn` (input) is called with `args` (input). `returns` (param) lists the type name of each result (`Number`, `String`, `Bool`, `Function`, `List`, `Column`, `BufferRef`, or a procgen type) and a call that returns anything else is refused. `ceilingMs` (param) refuses a call that took longer. A device Column reaches the function as a `BufferRef { buffer, type, count, lanes, element }`.
▲ 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
·
computeshader · born here
❒asset
# abi_add Adds the delta param to every element of src into dst.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_columns Adds the first lane of the columns a and b of a table into dst.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_filter Appends every element of src above the threshold param to hits.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_inout Writes src doubled into dst, or adds src to the dst it is bound when mode is 1.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_inplace Adds one to every element of data in place.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_len Copies src into a dst the length of src.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_live Doubles src into dst for the elements a live count holds and writes -1 past it.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_nothing Reads src and writes nothing.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_ordered Writes ten times the first lane of slot 0 plus the first lane of slot 1 of a table into dst. The binding states the slot order, `tail` then `head`.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_pair Adds two read bindings, a and b, element by element into dst.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_scale Multiplies every element of src by the scale param into dst, one element per invocation.
▲ 0↑ born
·
computeshader · born here
❒asset
# abi_spec Doubles src into dst as the element type T the specialization names.
▲ 0↑ born
·
guide · born here
❒asset
# procgen guide Making things from rules, as graphs of nodes: geometry and meshes, scattering and terrain, and visual effects — particles, sparks, embers, magic, debris, swarms, flocks, trails, cracks and lightning — simulated and drawn live every frame, with your own compute and surface shaders as nodes. The procgen package's reference guide. Surfaced through `guides { query: "procgen" }` and `guides { path: "systems/procgen/documentation/procgen" }` (likewise `man`), and reachable from the `procgen` toolbox via `search_tools procgen`. The content lives in `guide.md`; this README is the asset summary.
▲ 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
# 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
✦
shader · born here
❒asset
# flipbook (shader) Plays a texture sheet on a card. The card's `shader_data` lanes say which cells of the sheet to show: lane 0 holds (frame, next frame, how far between them) and lane 1 holds (columns, rows) - the lanes `sim.flipbook` writes onto particle points as `data0` and `data1`. Frames count row by row from the top-left cell. The `base_color_texture` is the sheet and `base_color` tints it; `emission` scales the colour for an additive, bloom-fed look. With `face_camera` on the card turns to face whichever camera draws it; off, it keeps the pose the instance stands at. The fragment is unlit and returns the sheet's alpha, so the material's blend state (alpha, additive, premultiplied) says how it combines with what is behind it.
▲ 0↑ born
▣
module · born here
❒asset
# generator The parts of the `Generator` component (`@builtin::systems.procgen.Generator`). The component makes one context table per instance, `gen`, holding its `public` and `private` surface, and calls this module with it. Each part installs its state and operations on `gen`: - `realized`: What a Generator has realized and from which graph: the entities, populations, meshes, materials and volumes it stands, tracked per output, and the retirement that keeps an output's earlier realization standing until its replacement lands. - `translated`: A Generator's translated outputs: the states they stand as, the bounds it publishes for them, the frames play of a held output, and the clock a translated output is presented at. - `cook`: Cooking a Generator's graph: the outputs it resolves, the node and frame caches a cook reads and fills, the cook's own state and costs, the world values a graph follows, the pacing of a cook on a worker, the inputs a cook takes, the evaluation of one output, and the per-node placement the `playback` report reads. - `realize`: Standing a cooked value as a Generator's children: a geometry, grid, bundle, population or asset reference realized under the generator, an output split into the pieces it realizes as, and the folding of outputs into the one bundle `apply` writes. - `run`: Running a Generator's bound graph: the bound graph and its signatures, its timeline and the clock each frame is cooked at, the cook that runs ahead through the timeline, presenting a cooked frame on the device, and standing a frames play of a held output. - `diskcache`: A Generator's disk cache and baked snapshot: the outputs a graph declares `diskCache` on, loaded from the world's cache, the writes a commit makes for them, and the snapshot an earlier engine stored with the scene. - `play`: A Generator's play of its graph (`call`): loading, clocking, presenting, standing and advancing it; binding a graph to the generator (`regenerate`); and standing a baked snapshot in place of cooking. - `clock`: A Generator's clock, frame by frame: the live sources it polls, the material clocks and live material values it writes, the playhead it presents, the transport its peers share, advancing time, cooking again once the device returns, and the tick each frame runs.
▲ 0↑ born
▣
module · born here
❒asset
# generator_inspector The `Generator` component's card on an entity. It replaces the card's generic `settings` section with one built from the bound graph: the graph it runs, as an asset picker, and which of its outputs it realizes; then groups — Inputs, one row per input the graph declares in the control its type takes, each able to take an expression over the graph's own time; Timeline, the transport, scrubber, range, frame rate, looping and wall time; Cook, what is cooking, what cooking has cost and the frame budget — and the Flatten action. The component's `inspector.luau` returns this module, so the editor's inspector framework (`edui.inspector.componentSections`) shows it wherever the component is on an entity. While the clock runs, or the range is still cooking, the card asks the panel to follow it. Editing an input row writes the value into the component's `params`, which regenerates the live output in place. A write the input's type cannot hold is refused, and the row says why. `buildFieldSpecs(graph, params)` is the pure row mapping: one spec per declared graph input, sorted by name, carrying its kind, its current value (the param override when set, else the declared default), its declared range, the asset kind a reference input picks from, and whether the value changes over time.
▲ 0↑ born
·
shadermodule · born here
❒asset
# grid How a compute shader finds the points near a point: the walk over a spatial hash grid `proc.device.gridInto` builds on the device. `zero_grid_cell`, `zero_grid_bucket` (from `grid_cell`) and `zero_grid_range`. A shader run by `shader.compute` or `sim.shader` gets a grid over its domain's `P` by declaring three read bindings — `gcfg` (f32), `grange` (u32) and `gorder` (u32) — and giving the node a `neighborRadius`: ```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]; // skip j == i, and any j whose own cell is not cc } } } } ```
▲ 0↑ born
·
shadermodule · born here
❒asset
# grid_cell The cell a point stands in (`zero_grid_cell`) and the bucket of a spatial hash grid that cell hashes to (`zero_grid_bucket`), read against the grid settings bound as `gcfg`. What a pass that only keys points by cell includes; a pass that walks the grid includes `grid`, which includes this. ```wgsl #include "@builtin::systems.procgen.grid_cell" ```
▲ 0↑ born
·
computeshader · born here
❒asset
# bvh The tree of a bounding volume hierarchy over the primitives of a geometry (an LBVH): a leaf per primitive in Morton order with its bounds, and the internal nodes linked by the longest common prefix of the sorted codes, equal codes broken by place. `bvh_pyramid` and `bvh_internal` then fill in the internal bounds, and the `ray` kernel traverses the finished table. `bindings.yaml` states the node layout and the order the kernels run in. Exact: the same geometry gives the same tree.
▲ 0↑ born
·
shadermodule · born here
❒asset
# bvh_box The boxes the bvh kernels fold: `ZeroBox`, the empty box and `zero_grow`. The module binds nothing, so any shader includes it.
▲ 0↑ born
·
computeshader · born here
❒asset
# bvh_codes A 30-bit Morton code for each primitive of a geometry: its bounds centre, normalized inside the scene bounds and interleaved bit by bit. Sorting primitives by this code puts primitives that are near in space near in the order, which is what the `bvh` kernel builds its tree from. The scene bounds are a min and a max reduce of the points. Exact.
▲ 0↑ born
·
computeshader · born here
❒asset
# bvh_internal The last step of a bounding volume hierarchy build: each internal node's bounds, the fold of the leaves its run of sorted places covers, read from the pyramid `bvh_pyramid` wrote. The node table is written in place, after which the `ray` kernel can traverse it. Exact.
▲ 0↑ born
·
shadermodule · born here
❒asset
# bvh_levels How the bvh kernels read bounds: a node of the node table (`zero_node_box`), a node of the pyramid of leaf bounds (`zero_pyr_box`) and a node of the segment tree both form (`zero_tree_box`). The module header lists the buffers and params the includer declares and the node layout.
▲ 0↑ born
·
shadermodule · born here
❒asset
# bvh_prim `zero_prim_box`, the bounds of one primitive over its vertices, with the box types of `bvh_box`. The module header lists the buffers the includer binds.
▲ 0↑ born
·
computeshader · born here
❒asset
# bvh_pyramid One level of the pyramid of leaf bounds: each pyramid node folds the bounds of two nodes of the level below (leaves at the first level). Dispatched once per level, the levels let `bvh_internal` fold the bounds of any run of leaves in a few reads. The pyramid is written in place, each level reading what the level before it wrote. Exact.
▲ 0↑ born
·
shadermodule · born here
❒asset
# bvh_span The binary radix tree over sorted Morton codes: for internal node `i`, `zero_span_of` gives the run of sorted places it covers and the place it splits at, from the length of the common prefix of the sorted codes. The module header lists the buffers and the param the includer declares.
▲ 0↑ born
·
shadermodule · born here
❒asset
# color Colour helpers a per-element template calls: `zero_color_srgb_to_linear`, `zero_color_linear_to_srgb`, `zero_color_luminance`, `zero_color_hsv_to_rgb`, `zero_color_rgb_to_hsv`, `zero_color_blackbody(kelvin)` (linear RGB of a blackbody) and `zero_ramp_ease(f)`, the smooth ease a `ramp` blends keys with. ```wgsl #include "@builtin::systems.procgen.kernels.color" fn ember(heat: f32) -> vec3<f32> { return zero_color_blackbody(1000.0 + 2500.0 * heat); } ```
▲ 0↑ born
·
computeshader · born here
❒asset
# compact Turns a keep mask into the indices of the kept elements, and the map back from every element to the place it holds among them. `indices` lists the elements whose mask word is not 0 (a float mask of 0.0 and 1.0 reads the same way), in run order; its live count is the number kept. `inverse` holds, for every element, its place among the kept ones, or -1 for an element that is not kept, so a column carried to the kept elements (a gather by `indices`) and a column of per-kept values carried back (a read through `inverse`) agree on which element is which. The dispatch size `n` is the number of elements. One workgroup of 256 works the whole run, a tile of 256 elements at a time; the other workgroups of a larger dispatch return. Exact: the order is the run's own, so the same mask gives the same answer on every dispatch. Over a live count (`COUNT=device`, see the abi README) `n` is the capacity and the first `domain_count` mask words are the elements: words past it are not read, a slot of `inverse` at or past it holds -1, and the slots of `indices` past the kept ones hold -1. `indices` has room for `n` and its counter is the live count the next kernel takes as its domain. It binds 5 storage buffers (4 in the plain variant).
▲ 0↑ born
·
computeshader · born here
❒asset
# concat Joins one attribute of several geometries into one column. `sources` is a column table with one column per geometry, in order; `desc` says where each geometry's elements start in the result, how many it has, how far to shift each word of a linkage column, and whether the geometry holds the attribute at all. Where it does not, its elements take the default stated in the first four words of `desc`. Linkage is a shift: `vertex.pt` takes the number of points before the geometry, `primitive.vstart` the number of vertices before it. Any other attribute shifts by 0. A node joins a whole geometry by running the kernel once per attribute of the union of the geometries' attributes. Dispatch over `n`, the elements of the joined domain. Exact: each result element has one writer.
▲ 0↑ born
·
computeshader · born here
❒asset
# contour_cells Whether each cell of a grid of samples straddles the surface: its eight corner samples are not all on one side of zero. `flag[c]` is 1 for a straddling cell and 0 otherwise, the keep mask `compact` turns into the list of cells that own a vertex. Dispatch over the cells. Exact: each flag has one writer.
▲ 0↑ born
·
shadermodule · born here
❒asset
# contour_core The shared WGSL of the dual contouring kernels (`contour_cells`, `contour_crossings`, `contour_solve`, `contour_quads`, `contour_triangles`): `ZsGrid`, the addressing of a grid of samples and its cells, the corners and edges of a cell, and the least-squares solve (`ZsQef`, `zs_qef_add`, `zs_qef_solve`) that places a cell's vertex where the surface normals around it agree. ```wgsl #include "@builtin::systems.procgen.kernels.contour_core" ```
▲ 0↑ born
·
computeshader · born here
❒asset
# contour_crossings Where the surface crosses each of the twelve edges of every straddling cell: the point and whether it crosses. An edge that does not cross carries its cell's centre, a point any shape can be read at. Slot `s` of the outputs is the `s`-th straddling cell, `cells[s]`, and edge `e` of it is slot `s * 12 + e`. Dispatch over the straddling cells. Exact: each slot has one writer.
▲ 0↑ born
·
computeshader · born here
❒asset
# contour_quads One quad per grid edge the surface crosses, spanning the four cells around it. Edge `e` is the edge of sample `e / 3` along axis `e % 3`. `quad_flag[e]` is 1 for an edge that owns a quad and `quad_v[e]` holds its four vertex numbers, wound so every face points away from the solid. An edge on the grid's boundary, or one with a cell around it that owns no vertex, owns no quad. `vertex_of[c]` is the number of the vertex cell `c` owns, or below 0 for a cell that owns none: the inverse map `compact` writes beside the list of straddling cells. Dispatch over `3 * samples` edges. Exact: each edge has one writer.
▲ 0↑ born
·
computeshader · born here
❒asset
# contour_solve The vertex of each straddling cell. With `PLACEMENT` `qef` the vertex is the point nearest the planes the surface normals at the cell's crossings describe (least squares, pulled back toward the mean of the crossings by `lambda` per crossing), which reproduces a box's edges and corners; with `centroid` it is the mean of the crossings. The vertex is held just inside its own cell, so two cells sharing a wall never land two vertices on one point. Dispatch over the straddling cells. Exact: each vertex has one writer.
▲ 0↑ born
·
computeshader · born here
❒asset
# contour_triangles Each kept quad as two triangles: `pt` holds, per quad, the vertex numbers of its corners (`a b c`, `a c d`), and `vstart` the position of each triangle's first corner in `pt`. Slot `n` of the dispatch is the `n`-th quad `edges[n]` names, so the quads are numbered in edge order. Dispatch over the kept quads. Exact: each slot has one writer.
▲ 0↑ born
·
computeshader · born here
❒asset
# edges The sides of a mesh as point pairs: the first stage of the edge table. One invocation per primitive writes, for every vertex slot of its run, the side leaving that vertex as `lo`, `hi` (its lower and higher point), `face` (the owning primitive) and `rev` (1 when the side runs from the higher point to the lower). A slot with no side holds `ZERO_NONE` in `lo` and `hi`, so a sort puts it last. Integers are 32-bit words, exact. The edge table is this kernel, a stable sort of the slots by `hi` and then by `lo`, and the `edges_runs` kernel (run heads, then the table). The sort and the compaction between are the `sort` and `compact` kernels; `edges_runs` states the whole sequence. `closed_all` is 1 for a mesh of faces, which wrap; with 0 the `closed` column decides per primitive, for curves.
▲ 0↑ born
·
computeshader · born here
❒asset
# edges_runs Turns the sorted sides of a mesh into the edge table. Two stages share one dispatch shape, chosen by the `STAGE` specialization: - `STAGE heads` flags, per sorted place, where a new edge starts; - `STAGE table`, after the flags are compacted into `starts`, writes each edge's two points (`a` < `b`) and the number of sides along it (`fcount`). The whole sequence for a mesh with `nv` vertices: `edges` writes `lo`, `hi`, `face`, `rev`; a stable sort by `hi` and then by `lo` gives `order`; `STAGE heads` flags the heads; `compact` of the flags gives `starts` and the live edge count; `STAGE table` writes the edges. An edge's faces are the primitives `face[order[starts[e] + j]]` for j below `fcount[e]`, so `gather(face, order)` is the CSR `edgeFace` column with `starts` as its starts and `fcount` its counts. An edge is open when `fcount` is 1 and manifold when it is at most 2. Integers are 32-bit words, exact. Over a live count (`COUNT=device`, see the abi README) `domain_count` is the number of live sorted places in both stages, and the `n` param is not read. Stage heads runs over the capacity of places and holds 0 in `flag` past the live ones. Stage table runs over the capacity of edges: an edge is live where `starts` holds a place below the live places (the slots past the live edges hold -1 where `compact` over a live count makes them), a run ends at the live places, and every other slot holds -1 and -1 in `a` and `b` and 0 in `fcount`. The device variant binds the buffers of its own stage only: 5 storage buffers in stage heads and 8 in stage table (8 in the plain variant).
▲ 0↑ born
·
computeshader · born here
❒asset
# expand Turns a count per element into runs. Element `j` takes `counts[j]` slots, in element order; slot `s` of the result holds `owner[s]`, the element it belongs to, and `local_index[s]`, its place within that element's run. `offsets` is the exclusive prefix sum of `counts` (what the `scan` kernel makes of it), so every slot finds its element by a binary search and has one writer. `owner` is an appended output: its live count is the sum of the counts, up to the capacity the dispatch was given. Slots at and past the live count hold -1. An element with a count of zero has no slot; a column of all zeros gives a live count of zero. Dispatch over `n`, the capacity of the result. Exact. Over a live count (`COUNT=device`, see the abi README) the elements are the first `domain_count` of `counts` and `offsets`, so they can be held in runs larger than their live count, and the counter of `owner` states the sum of their counts as it is, above the capacity too: the first `n` slots are written and the sum minus `n` is how many did not fit. A node that states `n` as a bound checks the count against it. The plain variant clamps the count to `n`. It binds 6 storage buffers (5 in the plain variant).
▲ 0↑ born
·
shadermodule · born here
❒asset
# filter_weights The reconstruction filters an image resample weighs source pixels with: bilinear (triangle), lanczos3 and Catmull-Rom, each as `zf_weight(filter, x)` over a distance in output-pixel units with its reach from `zf_support`. ```wgsl #include "@builtin::systems.procgen.kernels.filter_weights" ```
▲ 0↑ born
·
computeshader · born here
❒asset
# gather Builds a column from another column and a run of indices: element `i` of the result is element `index[i]` of `src`. An index below 0 or past the end of `src` gives zeros. The element type is the `T` specialization (`f32`, `i32`, and two to four lanes of either); a three-lane column is read and written tightly packed, twelve bytes an element. Dispatch over `n`, the number of indices. Exact: each result element has one writer. Over a live count (`COUNT=device`, see the abi README) `n` is the capacity and the first `domain_count` slots are live: a slot at or past it holds zeros. It binds 4 storage buffers (3 in the plain variant), and `dst` holds `n` elements.
▲ 0↑ born
·
computeshader · born here
❒asset
# grid Reads and writes a field of cells in one kernel. The field is a Grid (one tile, no apron, x fastest, `lanes` values per cell) or a Raster (tiles with aprons, one batched dispatch) through the layout in `tile_layout`. - sample (`MODE sample`): the field at positions, nearest or trilinear, held to the edge cell past the field. Reads each cell from the tile that owns it. - write (`MODE write`): a patch of values blended into the field (replace, add, max, min, multiply, optionally weighted per cell). Every tile that holds a cell as core or apron receives it, so apron copies agree. The patch window is stated in field cells and may reach past the field (negative origin, or a size beyond the cell counts): a stored cell a border tile holds past the field takes the patch where the window covers it. - stencil (`MODE stencil`): per stored cell, a neighbour at an offset, slope in degrees, curvature (the laplacian over x and z) or the gradient, with neighbour reads held to the tile's own stored edge. Slope and curvature are the terrain mask's. `MODE`, `SAMPLING` (nearest, trilinear), `STENCIL` (offset, slope, curvature, gradient) and `BLEND` (replace, add, max, min, multiply) are specializations. `determinism: exact`: each output element has one writer. The bindings a mode does not use take any buffer that is not bound elsewhere in the dispatch.
▲ 0↑ born
·
computeshader · born here
❒asset
# hash The first step of a spatial hash over a run of points: the bucket of the cell each point stands in. The sort kernel orders the points by bucket, and `hash_starts` then states where each bucket's points start and end in that order. The keys, the order and the bucket spans, with the cell size, are the index `hash_count`, `hash_nearest` and `hash_gather` search. Exact: the same points give the same keys. Over a live count (`COUNT=device`, see the abi README) `n` is the capacity and the first `domain_count` slots are live. A slot at or past the live count holds the key `0x7fffffff`, which is past every bucket: the sort (over the same live count, or over the capacity) places it after every live key, and `hash_starts` over the capacity states every bucket's span over the live points only. It binds 3 storage buffers (2 in the plain variant).
▲ 0↑ born
·
shadermodule · born here
❒asset
# hash_cell The cell a point stands in (`zero_hash_cell`) and the bucket a cell hashes to (`zero_hash_bucket`). The `hash` kernel keys points with them and the `hash_query` kernel looks cells up with them, so one definition decides every point's bucket.
▲ 0↑ born
·
computeshader · born here
❒asset
# hash_count For each query position, how many indexed points lie within a radius of it, searched through the spatial hash the `hash` and `hash_starts` kernels build. An exclusive scan of the counts gives the offsets `hash_gather` writes each query's neighbour list at. Exact.
▲ 0↑ born
·
computeshader · born here
❒asset
# hash_gather Lists, for each query position, the indexed points within a radius of it, query after query: query `q`'s points start at `qoff[q]`, the exclusive prefix sum of the `hash_count` counts. The list is in the order the spatial hash walk visits points, the same on every run. Exact. Over a live count (`COUNT=device`, see the abi README) the queries are the first `domain_count` of a capacity `n`, `total` is the capacity of `items` (the bound the node states), and `items` is an appended output: its counter is the number of points the live queries listed, above the capacity too when the lists do not fit (the first `total` words are written and the counter minus `total` is how many did not). Words of `items` past the lists are left as allocated, 0. A node that states `total` as a bound checks the count against it. It binds 8 storage buffers (6 in the plain variant).
▲ 0↑ born
·
computeshader · born here
❒asset
# hash_hook One hooking round of the connected sets of the points that stand within a radius of each other, searched through the spatial hash the `hash` and `hash_starts` kernels build. `label` names each point's root, and every point of a set names the same root (a star); `hooked` starts as a copy of `label`. Every point walks the points within the radius and lowers the entry of its own root in `hooked` to the smallest root among them, so after the round each root links to the smallest root next to its set, or to itself. Links only ever point at a smaller root, so they form a forest, and pointer jumping (`gather` of the links through themselves) makes every set a star again. | Specialize | Options | Meaning | |------------|---------|---------| | `MATCH` | `position`, `normal` | every point within the radius is next to the point, or only one whose normal (`nrm`) is within `cos_angle` of its own | | `COUNT` | `plain`, `device` | every slot is live, or the live count is read from `domain_count` | A round of hooking then pointer jumping halves the number of sets that still have a neighbouring set every two rounds, so `2 * k` rounds with `k` jumps each, where `2^k` exceeds the number of points, leave every set a star of its smallest point. `core.weld` folds it that many times, with no read-back. Exact: the smallest root wins whatever order the invocations run in.
▲ 0↑ born
·
computeshader · born here
❒asset
# hash_nearest The nearest indexed point of each query position, found through the spatial hash the `hash` and `hash_starts` kernels build, with its squared distance. A query walks the cells within a few shells of its own and, when that cannot prove its answer, reads every point. The lower index wins of two points equally near. Exact.
▲ 0↑ born
·
computeshader · born here
❒asset
# hash_repel For each query position, the push away from the indexed points within a radius of it, searched through the spatial hash the `hash` and `hash_starts` kernels build. Each neighbour pushes along the unit direction from itself to the query, scaled by `1 - distance / radius`; the query takes the mean over the neighbours that count. The walk visits the points in the same order on every run. Exact.
▲ 0↑ born
·
computeshader · born here
❒asset
# hash_starts Finishes a spatial hash: given each point's bucket and the permutation that sorts the points by bucket, states for every bucket where its points start and end in the sorted order. Each bucket finds its two places by binary search over the sorted keys, so no bucket needs clearing and the answer is a function of the keys alone. Exact.
▲ 0↑ born
·
shadermodule · born here
❒asset
# hash_walk How a shader reads the spatial hash `hash` and `hash_starts` build: a walk over the points within a radius of a query position (`zero_nb_begin`, `zero_nb_next`), and the nearest point of a query (`zero_hash_nearest`). The module header lists the bindings and params the includer declares. The walk visits cells in a fixed order and, within a cell, points in sorted order, so a shader that lists neighbours lists them in the same order on every run.
▲ 0↑ born
·
shadermodule · born here
❒asset
# intersect_core The triangle pair tests of the `intersect` kernels. A pair of triangles, `a` of mesh A and `b` of mesh B, has a cut in parts; every part is a segment, and an endpoint is named by the edges or the corner it is made of, and computed from nothing else, so the triangles that share an edge compute the same point to the bit. The includer binds `apts`, `atri`, `bpts` and `btri`. `zi_part(a, b, part)` answers part `part`. ## Parts | Part | What | Cuts | |---|---|---| | 0 | The ordinary cut of two triangles that are not coplanar: the segment with two endpoints, each an edge of one triangle crossing the other (`zi_pair`). A vertex exactly on a plane counts as on its positive side, so triangles that only touch cross consistently or not at all. | A and B | | 1 to 3 | Coplanar triangles: edge `part - 1` of `b`, clipped to the closed triangle `a`. | A | | 4 to 6 | Coplanar triangles: edge `part - 4` of `a`, clipped to the closed triangle `b`. | B | | 7 to 15 | Edge `i` of `a` and edge `j` of `b` lie in one plane and cross strictly inside both: the crossing, part `7 + 3 i + j`. A segment of no length. | A and B | | 16 to 24 | Corner `v` of `a` lies exactly strictly inside edge `j` of `b`: part `16 + 3 v + j`. A segment of no length. | A and B | | 25 to 33 | Corner `v` of `b` lies exactly strictly inside edge `i` of `a`: part `25 + 3 v + i`. A segment of no length. | A and B | Triangles are coplanar when neither is degenerate and every corner of each is exactly on the plane of the other (the signed volume is 0). Coplanar triangles have parts 1 to 6 and no part 0; a part is present when the clipped edge keeps a length. The clip is made in the projection along the axis the plane's normal points along most, with every edge taken from its lower point id to its higher; an edge is cut where it leaves the triangle by the line of an edge of the triangle. Parts 7 to 33 split an edge: a cut point that lies on an edge is a point of every triangle that uses the edge, whatever plane that triangle lies in, so every pair of a triangle of A and a triangle of B that share the edges reaches it and reports the same point. A crossing is computed along the edge of A from the two edges alone. ## Endpoint records An endpoint is `ZiEnd`: its position, the kind word (the kind in the low byte, which meshes the segment cuts in bits 8 and 9, 1 mesh A, 2 mesh B, 3 both), `lo`, `hi`, `tri` and `extra`. | Kind | Endpoint | `lo`, `hi` | `tri`, `extra` | |---|---|---|---| | 0 | an edge of A through triangle `tri` of B | the edge | `tri`, 0 | | 1 | an edge of B through triangle `tri` of A | the edge | `tri`, 0 | | 3 | an edge of B crossing an edge of A in their plane | the edge of A | the edge of B | | 4 | a point of B | its id, twice | 0, 0 | | 5 | a point of A | its id, twice | 0, 0 | Two endpoints with all of kind word, `lo`, `hi`, `tri` and `extra` equal are one point.
▲ 0↑ born
·
computeshader · born here
❒asset
# intersect_cuts The segment each entry of `intersect_pairs` is cut along, one thread per entry. The thread reads the entry's `a`, `b` and part and writes the part's two endpoints (`intersect_core` states the parts). An endpoint record is 8 words: the position as f32 bits, the kind word (the kind in the low byte, which meshes the segment cuts in bits 8 and 9), then `lo`, `hi`, `tri` and `extra`, which name the crossing; two endpoints, 16 words, an entry. The endpoint slot `s = 2 * entry + e` names an endpoint to `intersect_unify` and `intersect_retri`. The points on shared edges have both endpoints in one record. Exact.
▲ 0↑ born
·
computeshader · born here
❒asset
# intersect_pairs The entries of two meshes that cut each other. One thread per triangle of mesh A walks the bounding volume hierarchy of mesh B (the `bvh` family builds it, one primitive a triangle) and, for every triangle of B whose bounds overlap, lists each part of the cut of the two triangles that `intersect_core` states (the ordinary cut, the coplanar clips, the points on shared edges). An entry is three words: `a`, `b` and the part. Two stages with an exclusive prefix sum (the `scan` kernel) between them: `STAGE count` writes how many entries each triangle of A has, `STAGE write` writes the entries at the offsets the sum gives, ordered by `a` and, within a triangle of A, in the order the tree is walked and the parts of a pair in increasing order. Exact: the entries and their order are the same on every run. Bind `apts`, `atri`, `bpts`, `btri`, `nodes` and `start` (any buffer in the count stage); params `nb` (triangles of B) and `total` (the sum of the counts, which sizes `pairs`, three words an entry). ## The intersect family `intersect` is a kernel family: `intersect_pairs`, `intersect_cuts`, `intersect_unify` and `intersect_retri` (shared by `intersect_core`). Two triangulated meshes A and B go in; the pieces of both, cut along their common curve and along the regions they share, come out. A node composes the family with `bvh`, `scan`, `sort`, `compact` and `gather`: 1. Build the hierarchy of B (`bvh_codes`, `sort`, `bvh`, `bvh_pyramid` per level, `bvh_internal`), one primitive a triangle (`vstart` 3 t, `vcount` 3). 2. `intersect_pairs` `STAGE count`, an exclusive `scan` of the counts, `STAGE write`: `pairs` (a, b, part), ordered by a. `total` is the scan's total, the number of entries. 3. `intersect_cuts` over `total` entries: the endpoint records, slot `2 * entry + e`. Word 0 to 2 of a record are the position as f32 bits. 4. `intersect_unify` (three stages) over `2 * total` slots: `rep`. `compact` of the slots with `rep[s] == s` numbers the cut points; a cut endpoint's point is the number of `rep[s]`, and its position is the record's position. 5. `intersect_retri` for A with `runs` from the counts and `start` and `order` the entry indices, `side` 0, then for B with the entry indices stably sorted by b (`sort` on the b words) as `order`, `runs` the first and count of each b and `side` 1. `STAGE count`, `scan`, `STAGE write`. Vertex references at or above `npts` are cut endpoint slots, mapped through `rep`. The pieces of each mesh share the cut points, so the two results agree along the curve and along the boundary of every shared region. Each piece lies on one side of the other mesh, or on its surface. ## What a boolean reads A boolean keeps pieces by what lies just outside and just inside each piece's centre, probed along the piece's normal against the other mesh (`ray` winding at both probe points). Pieces on the other mesh's surface read the same way: - Same normal (the two surfaces coincide and face one way): the probe outside the piece is outside the other mesh and the probe inside it is inside the other mesh. - Opposite normals (the surfaces coincide and face each other): the probe outside the piece is inside the other mesh and the probe inside it is outside. | Operation | Pieces of A kept | Pieces of B kept | |---|---|---| | union | outside probe outside B | outside probe outside A and inside probe outside A | | intersect | inside probe inside B | outside probe inside A and inside probe inside A | | subtract (A minus B) | inside probe outside B | outside probe inside A and inside probe inside A, flipped | A region shared with the same normal is kept once from A in a union and an intersection and dropped from both in a subtract; a region shared with opposite normals is dropped from both in a union and an intersection, and kept from A in a subtract.
▲ 0↑ born
·
computeshader · born here
❒asset
# intersect_retri The triangles of one mesh retriangulated along the segments that cut them. One thread per triangle: a triangle no segment reaches is written as itself; a cut triangle is worked in its own plane, in world lengths, where its corners and the cut endpoints are the points and its sides and the cut segments are the constraints. The triangulation holds every constraint first (each cut out by the points that lie on it), then holds the remaining candidate edges shortest first, a length class at a time (the exponent and top three mantissa bits of the squared length), each when it crosses no held edge and has no point on it. The held edges are a planar graph: the neighbours of each point are sorted by direction, the faces are walked with their inside on the left, a face of three points is a triangle and a face of more is cut ear by ear (each ear a corner that turns left with no other point of the face inside it or on its sides), and the face outside the triangle is left out. Every triangle is wound the way the source triangle was, so the pieces of a triangle agree on the direction of every side they share however thin the sliver between two cut points. A triangle whose three points stand on one line in the world has no area and is left out. Every cut segment is a union of edges of the result, and the pieces of a triangle tile it. Points and distances are compared within a few steps of the coordinates' precision (`5e-7` of the largest coordinate of the triangle, and no less than `1e-7` of its longest side), the same measure for a triangle of either mesh, so cut points a few millionths apart are told apart by the triangles of both meshes alike. Param `side` names the mesh the pass cuts, 0 for A and 1 for B; a segment cuts the mesh when its endpoint records say so (`intersect_core`). A triangle takes every point of its entries, a segment of no length giving a point on an edge and nothing more. `STAGE count` writes how many triangles each triangle becomes; `STAGE write` writes them at the offsets of the prefix sum, four words each (three vertex references, the source triangle) from word 4 of `out`. A reference below `npts` is a point of the mesh; `npts + s` is the cut endpoint in slot `s`, which `intersect_unify`'s `rep` names. `out[0]` is 1 when a triangle ran out of working space, which the sizing below prevents. ## Working space A triangle takes a slice of working memory sized by its entries, so the number of points a triangle can carry follows the data: a run of `m` entries gives at most `3 + 2 m` points, `3 + m` segments and three edges a point, and the slice is `64 + 40 m` words. The slices of consecutive triangles follow one another and make the tail of `out`, after its 4 header words and the `4 * total` words of the triangles (`total` is 0 in the count stage): the slice of triangle `t` starts `64 * t + 40 * first(t)` words into the tail, `first(t)` the first entry of its run, so the runs' firsts are the exclusive prefix sum of their counts (the `start` of `intersect_pairs` for mesh A, the offsets of the per-b counts for mesh B). `out` is sized `4 + 4 * total + 64 * n + 40 * entries` words from the params `total` and `entries` (the number of entries of `intersect_pairs`). The triangles of mesh A are grouped by `a` already (`runs` is `start` and the counts of `intersect_pairs`, `order` the identity); for mesh B the entries are stably sorted by `b` first and `order` holds the sorted entry indices. Exact: each triangle is worked alone and in a fixed order.
▲ 0↑ born
·
computeshader · born here
❒asset
# intersect_unify The first slot of every cut point. Endpoint slots whose records name the same crossing (the kind word, `lo`, `hi`, `tri` and `extra`) are one point; `rep[s]` is the lowest slot among them, which is the point's identity. A slot with `rep[s] == s` is a new point's first slot, so a compaction of those slots numbers the cut points, and every endpoint reference maps through `rep`. Three dispatches (`STAGE claim`, `fold`, `resolve`) over a hash table of `cells` words (a power of two, at least twice the slot count) and a second table of the same size, both zero before the first stage. Bind `rep` (one word a slot) to the same buffer in all three. Exact: the cell a key takes depends on thread timing, the answer does not.
▲ 0↑ born
·
computeshader · born here
❒asset
# jump One round of pointer jumping over a run of links, three ways: - `rank` (MODE rank): list ranking. Repeated rounds give each element its root and its distance to it, for chains and trees. Both live in the double-buffered `parent`/`parent_out` and `value`/`value_out` pairs; the caller swaps them between rounds. - `min` (MODE min): doubling of the smallest label within reach. After ceil(log2 n) rounds, every element on a cycle holds the cycle's smallest label. - `label` (MODE label): label propagation over pairs, in place. Iterated to `changed` = 0 it labels each connected set of elements with its smallest label. `changed` counts what the round altered; the caller zeroes it before a round and reads it after, so iteration with a stop is one dispatch and one count per round. Integers are 32-bit words and every mode is exact: the final labels do not depend on the order invocations ran in. Over a live count (`COUNT=device`, see the abi README) the elements (rank, min) or the pairs (label) are the first `domain_count` slots of a capacity `n`, and the `n` param is not read. In rank and min mode a slot at or past the live count links to itself with the value 0. It binds 8 storage buffers (7 in the plain variant).
▲ 0↑ born
·
computeshader · born here
❒asset
# loops Orders directed edges into rings and chains. This kernel links each edge to the edge that follows it (`next`) and marks the edges a chain starts with (`head`); the `jump` and `loops_cut` and `loops_place` kernels do the rest. For the boundary of a mesh, the directed edges are its open edges run the way their one face runs them (`tail` is `a` when `rev` is 0, else `b`). The whole sequence over `m` edges and `npts` points: 1. `loops` (`STAGE` clear, claim, link): `next`, `head`. 2. `jump` (`MODE` min), ceil(log2 m) rounds, starting from `parent` = `next` and `value` = the edge index: every edge holds the smallest index within reach, and `parent` holds the edge 2^k steps ahead. 3. `loops_cut` (`STAGE` head, cut): a ring (a cycle) starts at its smallest edge; a chain starts at its head. The edge before each start becomes the end of its list. 4. `jump` (`MODE` rank) over the cut lists, starting with `value` 1 on every non-end edge: each edge's end and its distance to it. 5. `loops_place` (`STAGE` claim, place): each edge's ring (the first edge of its list) and its position along the ring. A compaction of the first edges numbers the rings, and a prefix sum of ring lengths (`rank` of the first edge plus 1) gives the ring starts; an edge's place in the ordered listing is its ring's start plus its position. Over a live count (`COUNT=device`, see the abi README) the stages claim and link run over a capacity of edges whose live count is `domain_count`, and the `n` and `npts` params are not read. Stage clear runs over the capacity of points and writes the cleared value into every slot, which is the value a slot past the live points holds. In stage link a slot past the live edges links to itself and is no head. It binds 7 storage buffers (6 in the plain variant).
▲ 0↑ born
·
computeshader · born here
❒asset
# loops_cut Finds where each ring and chain of linked edges starts and ends. Stage 0 marks the first edge of every list (a chain's head, a ring's smallest edge); stage 1 makes the edge before each first edge the end of its list. The lists that result are what `jump` ranks. See `loops` for the whole sequence. Over a live count (`COUNT=device`, see the abi README) the edges are the first `domain_count` slots of a capacity `n`, and the `n` param is not read. A slot past the live edges is no head (`head` 0) and ends its own list (`cut` is the slot itself). It binds 7 storage buffers (6 in the plain variant).
▲ 0↑ born
·
computeshader · born here
❒asset
# loops_place Gives every edge its ring and its position along it, from the ranked lists `loops_cut` and `jump` produced. `ring` is the first edge of the edge's list, so two edges are on one ring exactly when their `ring` values are equal; `pos` counts along the ring from that first edge. See `loops` for the whole sequence. Over a live count (`COUNT=device`, see the abi README) the edges are the first `domain_count` slots of a capacity `n`, and the `n` param is not read. A slot past the live edges has the ring -1 and the position 0. It binds 7 storage buffers (6 in the plain variant).
▲ 0↑ born
·
computeshader · born here
❒asset
# make Builds the elements and the primitives of a geometry from a count per owner and a shape (`points`, `strip`, `fan`, `grid`, `ring`, the `SHAPE` specialization). The result is the columns a Geometry holds: `P` over the points, `vertex_pt` over the vertices (the point each reads), and `prim_vstart` / `prim_vcount` over the primitives (each one's run of vertices). All indices are into the result, so owners' pieces sit end to end with their links already offset. It runs as two passes of one shader (`PASS`): `sizes` writes the points, vertices and primitives each owner makes; after a prefix sum of each component into `bases`, `fill` writes every element once, found by a binary search over the bases. Exact: each output element has one writer. The shape's layout is stated at the top of `bindings.yaml`.
▲ 0↑ born
·
computeshader · born here
❒asset
# map The per-element kernel: `dst[id] = TEMPLATE` over `n` elements. The template is a WGSL expression over `p` (the element's position), `id`, the numbers `u_get(k)` and the columns of the `cols` table; the preludes `rand`, `noise` and `color` supply the functions a template calls. `fieldc.mapSpec` writes a chain of field templates as one function, so a chain of any length is one dispatch. Specialize `T` (f32, i32, vec2, vec3 or vec4 of f32), `TEMPLATE`, and optionally `PRELUDE` and `POSITION`. Bind `cols` (a table from `compute_abi.table`) and `u`. The output `dst` holds `n` elements of `T`, tightly packed. ```luau local program = ref:program() local variant = program:variant({ specialize = { T = "f32", TEMPLATE = "zero_rand(7u, id)", PRELUDE = '#include "@builtin::systems.procgen.kernels.rand"' } }) local run = variant:run({ n = 64, inputs = { cols = emptyTable, u = numbers } }) ``` `fieldc.evaluate(field, domain)` runs a Field through this kernel and answers the Column it wrote. Leaves of i32 runs are read through `cols_i32` and a read past a run's end reads its last element.
▲ 0↑ born
·
shadermodule · born here
❒asset
# noise Lattice noise over the shared hash of `rand`: `zero_proc_value3(seed, p)` in [0, 1), `zero_proc_gradient3(seed, p)` in [-1, 1], `zero_flow_worley(seed, p, jitter)`, `zero_flow_simplex(seed, p)`, `zero_flow_curl(seed, p, freq, amp)`, and the octave sums `zero_noise_fbm_value` / `zero_noise_fbm_gradient`. The plane forms `zero_proc_value2(seed, p)`, `zero_proc_gradient2(seed, p)`, `zero_noise_fbm2(seed, p, octaves, lacunarity, gain, gradient)`, `zero_noise_fbm_value2` and `zero_noise_fbm_gradient2` return the values of `proc.noise` `value2`, `gradient2` and `fbm2`, to f32 rounding of the interpolation. Each returns the values the field ops of the same name compute on the CPU, to f32 rounding of the interpolation. ```wgsl #include "@builtin::systems.procgen.kernels.noise" fn height(p: vec3<f32>) -> f32 { return zero_noise_fbm_gradient(7u, p * 0.1, 4u); } ```
▲ 0↑ born
·
shadermodule · born here
❒asset
# noise_angle Gradient noise over a plane whose lattice gradients are unit vectors at a hashed angle, and the fractal sum of it. - `zero_angle_gradient2(p, seed)`: gradient noise with quintic interpolation, roughly -0.7 to 0.7. - `zero_angle_fbm2(p, octaves, lacunarity, gain, seed)`: `octaves` layers, each `lacunarity` times finer and `gain` times as strong as the one before, octave `i` (from 0) drawn from `seed + i * 7919`, summed and divided by the root of the sum of the squared strengths. - `zero_angle_warped2(p, warp, feature, seed)`: `p` pushed by a second noise, two 4-octave sums from seeds of their own, scaled by `warp / feature` noise units; a `warp` of 0 or less leaves `p` where it is. ```wgsl #include "@builtin::systems.procgen.kernels.noise_angle" fn relief(p: vec2<f32>) -> f32 { return zero_angle_fbm2(p, 6u, 2.0, 0.5, 1u); } ```
▲ 0↑ born
·
shadermodule · born here
❒asset
# order_key Maps the 32-bit word of a float or a signed integer to an unsigned key that sorts the way the value does, and back. `zero_order_key(word, is_float)` and `zero_order_word(key, is_float)` are inverses. A kernel that keeps the least or the greatest of many contributions runs `atomicMin` or `atomicMax` on keys and reads the same answer whatever order the contributions arrive in.
▲ 0↑ born
·
shadermodule · born here
❒asset
# rand The lattice hash and the unit random draw the procgen kernels share: `zero_proc_fnv_u32`, `zero_proc_avalanche`, `zero_proc_unit(prefix, element)`, and the seeded forms `zero_rand(seed, element)`, `zero_rand_range`, `zero_rand3`. The draw equals the CPU hash to the last bit, because it folds the same bytes and mixes with the same decimal constants. ```wgsl #include "@builtin::systems.procgen.kernels.rand" fn jitter(id: u32) -> f32 { return zero_rand_range(7u, id, -0.5, 0.5); } ``` `rand.unit(seed, ...scope, id)` of the field nodes is `zero_proc_unit(prefix, id)`: the prefix is `hash.scopePrefix` of the seed and scope folded once on the CPU, so the draw equals `hash.unitFloat` to the bit.
▲ 0↑ born
·
computeshader · born here
❒asset
# ray Casts rays and segments against the triangles of a geometry through the hierarchy the `bvh` kernel builds (closest hit, or any hit for occlusion), and computes the generalized winding number of the geometry about each origin, which is the inside test a boolean or a distance field needs. A hit is the parametric t, the primitive index and the barycentric weights. The shader's header states the ray and hit layouts. Exact: the same rays give the same hits, ties going to the lower index. `ray` is a family of three kernels, since one dispatch binds at most 8 storage buffers a stage: | Kernel | Answers | |---|---| | `ray` | `hit` (t, u, v, fan) and `hit_prim` (the primitive, the shape index of a mesh hit) for the `closest`, `any` and `winding` stages | | `ray_surface` | for the `closest` hits `ray` found: `hit_normal` (unit triangle normal facing the ray's origin side) and `hit_end` (the hit point pushed out along the normal by the `skin` param; a miss answers the ray's end) | | `ray_shapes` | the analytic shape table, bound as a `columns` binding (`shapeKind, center, extent, rotation, bounce, friction, index`; a plane's extent is its unit normal, a sphere's extent.x its radius): `hit`, `hit_prim` (the row of the shape table), `hit_normal`, `hit_end` | Chain for a mesh: dispatch `ray` with `STAGE = closest`, then `ray_surface` with the same `rays`, geometry bindings and the `hit` and `hit_prim` that `ray` wrote.
▲ 0↑ born
·
computeshader · born here
❒asset
# ray_shapes Casts rays and segments against a table of analytic shapes (plane, sphere, box, capsule) and answers the closest hit of each. The table is one `columns` binding named `shapes`, a row a shape, columns in this order: `shapeKind` (Int: 0 plane, 1 sphere, 2 box, 3 capsule), `center` (Vec3), `extent` (Vec3: a plane's unit normal, a sphere's radius in x, a box's half size, a capsule's radius in x and half length in y), `rotation` (Vec4 quaternion x y z w; a capsule lies along its own y), `bounce`, `friction` (Float, carried for the caller) and `index` (Int, the caller's id for the shape). Outputs, a value per ray: `hit` (t, then three zeros), `hit_prim` (the row of the hit shape, `0xffffffff` for a miss), `hit_normal` (the unit surface normal, turned to face the ray's origin side, zero for a miss) and `hit_end` (the hit point pushed out along the normal by the `skin` param; for a miss the ray's end, origin + direction * tmax). A shape is hit from outside at its entry and from inside at its exit; of two hits at equal t the lower row wins. Exact.
▲ 0↑ born
·
computeshader · born here
❒asset
# ray_surface The normal and the corrected end of the closest triangle hits the `ray` kernel found. Bind the same `rays`, `pts`, `vpt` and `vstart` the ray dispatch read, and its `hit` and `hit_prim` outputs. Outputs, a value per ray: `hit_normal` (the unit normal of the triangle hit, turned to face the ray's origin side, zero for a miss) and `hit_end` (the hit point pushed out along the normal by the `skin` param; for a miss the ray's end, origin + direction * tmax). The shape index of a hit is `hit_prim`, the primitive. Exact.
▲ 0↑ born
·
computeshader · born here
❒asset
# reduce Folds a run of elements to one element per group: the sum, least, greatest, mean, count, or the index of the least or greatest, of the whole run or of each segment of it. | Specialize | Options | Meaning | |------------|---------|---------| | `T` | `f32`, `i32`, `vec2` to `vec4` of either | the element; a vector folds lane by lane | | `FOLD` | `add`, `min`, `max`, `mean`, `count`, `argmin`, `argmax` | the fold | The dispatch size `n` is the number of elements. One workgroup folds one group at a time, so a segmented fold runs its segments side by side and an unsegmented one is a single group; any workgroup count from 1 up folds every group. `params.segments` above 0 makes `starts` partition the run into that many segments: it holds where each segment begins (a segment ends where the next begins, the last at `n`), and `dst` holds one element per segment. With `segments` 0 the whole run is one group, `starts` is any one-word run, and `dst` holds one element. An empty group answers 0 for every fold. `argmin` and `argmax` write the lowest index among equal elements to `picked` (for a vector element, the element that beats the best before it in every lane); every other fold writes -1 there. The mean of an integer run is the integer quotient. The bounds of a run are its `min` and its `max`. Exact: every group folds its elements in a fixed order and combines the 256 partial results in a fixed tree, so the same run gives the same bits on every dispatch, and an integer fold is the exact integer answer. Over a live count (`COUNT=device`) `n` is the capacity and the first `domain_count` elements are the run: the last segment ends at the live count. Over live segments (`SEGMENTS=device`) `params.segments` is the most segments there can be, which sizes `dst` and `picked`, and the first `segment_count` of them are live; a group past them is not written, and a run of no live segments folds nothing.
▲ 0↑ born
·
computeshader · born here
❒asset
# resample Image resample of one or more slices (a Texture's texels, a Grid layer, each tile of a Raster) to a target size, with the `FILTER` specialization (`bilinear`, `lanczos3`, `catmull_rom`, `nearest`). Downscaling widens the filter by the shrink ratio so it averages what it drops. `axis` resamples width, height or both; width then height gives the both-axes result. `determinism: exact`.
▲ 0↑ born
·
shadermodule · born here
❒asset
# run_sides How many sides a primitive's run of vertices has (`zero_run_sides`), which vertex the end of a side lands on (`zero_run_next`), and `ZERO_NONE`, the value a slot holds when it names no edge or point. The `edges` and `topology` kernels walk runs with them, so both agree on what a face's perimeter is.
▲ 0↑ born
·
computeshader · born here
❒asset
# scan The prefix fold of a run of elements, whole or per segment: running sums, running least and greatest, running products. (The running product of 4x4 matrices is `scan_mat4`.) | Specialize | Options | Meaning | |------------|---------|---------| | `T` | `f32`, `i32`, `vec2` to `vec4` of either | the element; a vector scans lane by lane | | `OP` | `add`, `min`, `max`, `mul` | the operator | `params.exclusive` picks the fold of the elements before each one or the fold up to and including it. The first element of a group has nothing before it: an exclusive scan holds 0 there for `add`, 1 for `mul`, and the element itself for `min` and `max`. `total` holds the fold of each group, 0 for an empty group. A segmented scan (`params.segments` above 0, the number of segments) restarts at each start in `starts`; an unsegmented one binds any one-word run there. One workgroup scans one group at a time, a tile of 128 elements at a time, with the fold of everything before a tile carried into the next. The dispatch size `n` is the number of elements; any workgroup count from 1 up scans every group. Exact: the order of combination is fixed, so the same run gives the same bits on every dispatch, and an integer scan is the exact integer answer. Over a live count (`COUNT=device`, see the abi README) `n` is the capacity and the first `domain_count` elements are the run. Every slot of `dst` at or past it holds the total of the last group (the whole run, or the last segment; 0 for an empty one), which is the scan of the run padded with the operator's identity. An `i32` scan's `total` is a count another kernel takes as its domain (`{ count = total, capacity = ... }`). It binds 5 storage buffers (4 in the plain variant). Over live segments (`SEGMENTS=device`) `params.segments` is the most segments there can be, which sizes `total`, and the first `segment_count` of them are live: a segment past them is not scanned, and a run of no live segments scans nothing. It binds one storage buffer more than the variant it extends.
▲ 0↑ born
·
computeshader · born here
❒asset
# scan_mat4 The running product of a run of 4x4 matrices, whole or per segment: the frames of a sweep, each placed relative to the one before it. A matrix is 16 consecutive floats, column by column. The product runs in run order, so `out[i] = m[0] * m[1] * ... * m[i]`. `params.exclusive` picks the product of the matrices before each one (the identity for the first) or the product up to and including it. `total` holds each group's product, the identity for an empty group. A segmented scan (`params.segments` above 0, the number of segments) restarts at each start in `starts`. One workgroup scans one group at a time, a tile of 128 matrices at a time, with the product of everything before a tile carried into the next. The dispatch size `n` is the number of matrices; any workgroup count from 1 up scans every group. Exact: the order of multiplication is fixed, so the same run gives the same bits on every dispatch.
▲ 0↑ born
·
computeshader · born here
❒asset
# scatter Folds the elements of a column into the places a run of indices names: element `i` of `src` goes to place `index[i]` of the result, and elements that meet in one place are combined by the `FOLD` specialization (`add`, `min`, `max`, `mean`, `set`). `T` is the scalar a lane holds (`f32` or `i32`); an element is `lanes` words. Alongside the result the kernel counts the elements folded into each place (`hits`) and records the greatest source index that reached it (`last`), so a node can tell a place nothing reached from one that holds zero. It runs as three passes of one shader, `init`, `fold` and `finish`, chosen by the `PASS` specialization. Min, max, set and every integer fold give the same answer whatever order the contributions arrive in. Float add and float mean sum in arrival order and declare `float_order`; with `EXACT` on, each float is rounded to a multiple of `1 / scale` and summed as an integer, which is the same on every run.
▲ 0↑ born
·
computeshader · born here
❒asset
# search Binary search of a run of queries in a sorted column: the pick of a value from a cumulative distribution, the segment of an arc length. | Specialize | Options | Meaning | |------------|---------|---------| | `T` | `f32`, `i32` | the scalar the column and the queries hold | | `SIDE` | `lower`, `upper` | which boundary a query answers | `lower` answers the first place whose value is not below the query, `upper` the first place whose value is above it, and both answer the length of the column when no place qualifies. The greatest place whose value is not above a query is `upper - 1`; with the cumulative sums of a run of weights, `upper` of a draw in `[0, total)` is the index of the weight it lands in. The column bound as `sorted` is the sorted run, every element of it. One thread per query; the dispatch size `n` is the number of queries. Exact: a search reads the column and nothing else, so the same inputs give the same answers on every dispatch.
▲ 0↑ born
·
computeshader · born here
❒asset
# search_curve Places samples on curves by arc length. Each sample names a curve (`owner`) and how far along it (`at`, 0 at the start to 1 at the end); the kernel answers the segment it stands on (`from_vertex`, `to_vertex`), how far along that segment (`weight`), the position there (`position`) and the segment's unit direction (`tangent`, zero for a segment of no length). | Specialize | Options | Meaning | |------------|---------|---------| | `COUNT` | `plain`, `device` | every slot is live, or the live count is read from `domain_count` | The curves are described per curve by `firsts` (the vertex the run starts at), `sizes` (its vertex count), `closed` (1 or 0), `before` (the arc length ahead of it) and `extents` (its own length), and per vertex by `along` (the cumulative arc length, ascending) and `positions`. A sample on the last vertex of an open curve stands on the segment before it. On a closed curve the last vertex starts the segment that closes the loop. One thread per sample; the dispatch size `n` is the number of samples. Exact: every output element has one writer and reads only its inputs, so the same inputs give the same answers on every dispatch.
▲ 0↑ born
·
computeshader · born here
❒asset
# sort A stable radix sort of a column's keys into a permutation: place `s` of `perm` holds the index of the element that sorts `s`-th, so a gather by `perm` puts any column in key order. | Specialize | Options | Meaning | |------------|---------|---------| | `KEY` | `f32`, `i32` | the scalar a key holds | Elements with equal keys keep their run order, in both directions (`params.descending` 1 sorts from the greatest key down), and `-0` and `+0` are one key. The sort works 4 bits of the key at a time from the least significant up, and a pass whose digit is the same in every key is skipped, so a run of keys inside a narrow range takes fewer passes. `work` is the sort's second permutation; its contents after a dispatch are not an answer. The dispatch size `n` is the number of keys. One workgroup of 128 works the whole run; the other workgroups of a larger dispatch return. Exact: the sort is a function of the keys and their order, so the same keys give the same permutation on every dispatch. Over a live count (`COUNT=device`, see the abi README) `n` is the capacity and the first `domain_count` keys are live: they are sorted, and a slot at or past the live count holds its own index in `perm`. A live count of 0 sorts nothing and `perm` is the identity. It binds 4 storage buffers (3 in the plain variant).
▲ 0↑ born
·
shadermodule · born here
❒asset
# tile_layout How a field of cells is stored as tiles: a tile holds its core cells plus an apron of copies of its neighbours' cells, and a tile covering the whole field with no apron is a plain grid with x fastest. The includer states the layout as a `ZtLayout` and uses `zt_decode` (stored cell number to tile, local and field cell), `zt_owner_index` (the core cell that owns a field cell) and `zt_neighbour_index` (a stored neighbour held to the tile's edge). ```wgsl #include "@builtin::systems.procgen.kernels.tile_layout" ```
▲ 0↑ born
·
computeshader · born here
❒asset
# topology Mesh edit operations over a geometry's linkage (`vertex.pt`, `primitive.vstart`, `primitive.vcount`), selected by the `OP` specialization (`split_edge`, `collapse_edge`, `insert_face`, `delete_face`, `split_vertex`), each stage by `STAGE` (`count`, `write`): split an edge (insert a point), collapse an edge (merge two points and drop the faces that degenerate), insert a face, delete a face, split a vertex (give a group of corners a point of their own). Each operation runs in two stages with an exclusive prefix sum between them. Stage 0 writes `cnt`, how many output vertices each primitive contributes (each vertex, for split vertex). The prefix sum of `cnt` is `off`. Stage 1 writes the output vertices' points (`o_pt`) and where each came from (`o_src`). The output primitives are `off` as run starts and `cnt` as run lengths; a primitive with an empty run is gone, and the select node removes it. The shader's header states the records each operation reads. The operation's records come from the edge table: for split edge, the point for each split edge gathered to the vertex slots through `edges`' side slots (both faces of an edge name the same point); for collapse, the point each point becomes, from an independent set of edges. The point an inserted vertex or a split vertex stands for is carried by the output maps: `o_src` names the vertex whose attributes an output vertex takes (negative when it is new, with ~v the vertex a split inserted after), and the compaction of the records names each new point's source. Integers are 32-bit words, exact.
▲ 0↑ born
·
computeshader · born here
❒asset
# walk One thread per walker running `steps` dependent steps: each walker's start state goes in, its end state and the state after every step come out, with the number of steps it ran. A state is 1 to 8 floats (`width`). The step is a Field, handed to the kernel the way `map` takes its template: the fields package fuses the Field into WGSL expressions and the kernel is specialized with them. A node holds no WGSL. | Specialization | Type | Meaning | |---|---|---| | `WALK_STEP` | `vec4<f32>` expression | lanes 0 to 3 of the next state; default `zero_walk_advance(s)`, the constant step | | `WALK_STEP_HI` | `vec4<f32>` expression | lanes 4 to 7 of the next state; default `s_hi` | | `WALK_DONE` | `bool` expression | whether the walker stops before step `k`; default `false` | | `WALK_PRELUDE` | module-scope WGSL | the functions the expressions call, and `#include` lines for the `rand`, `noise` and `color` preludes | The expressions read the state leaves `s` (lanes 0 to 3) and `s_hi` (lanes 4 to 7), the step number `k` (u32) and the walker number `id` (u32), the numbers of `u` (`u_get(n)`) and the columns of the `cols` table (`cols_f32(slot, i, lane)`, `cols_vec3(slot, i)`), which is how a step reads a raster, a field column or a per-walker attribute. The constant step needs no Field: its vector is the params `dx`, `dy`, `dz` and `dw`. Bind `cols` (a table from `compute_abi.table`), `u`, `start` (`width` floats a walker). Outputs: `finish` (`n * width`), `path` (`n * steps * width`, row `k` of walker `i` at `(i * steps + k) * width`) and `steps_run`. ```luau local variant = ref:program():variant({ specialize = { WALK_STEP = "vec4<f32>(s.x + 1.0, s.y * 0.5, 0.0, 0.0)", WALK_DONE = "s.x > 8.0", } }) local run = variant:run({ n = walkers, inputs = { cols = emptyTable, u = numbers, start = startBuffer }, params = { steps = 8, width = 2 } }) ``` Exact: each walker writes its own path rows, so the result does not depend on thread order.
▲ 0↑ born
·
shadermodule · born here
❒asset
# walk_advance `zero_walk_advance(s)`: a walk state advanced by the constant vector (dx, dy, dz, dw). It is the default `WALK_STEP` of the `walk` kernel, the constant step in its Field form: a walker's path is a straight line.
▲ 0↑ born
·
shadermodule · born here
❒asset
# walk_core The loop a walk kernel runs for one walker: load its state (up to 8 floats, held as two vec4), call the step hooks for each of `steps` steps (stopping when the stop hook says so), write the state after every step to the walker's path rows, and write the end state and the number of steps that ran. The includer defines `zero_walk_lo`, `zero_walk_hi` and `zero_walk_stop`, and calls `zero_walk_run` from its entry point; the module header lists the bindings and params the includer declares.
▲ 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: `<family>` (the `.nodebox` suffix strips). - Folder shape (open child set): - `README.md`: what the family is for. Required. - `.metadata`: description and tags. Required. - `<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
·
nodebox · born here
❒asset
▲ 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
# 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
·
nodebox · born here
❒asset
▲ 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
·
nodebox · born here
❒asset
▲ 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
·
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.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.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`: vector arithmetic over sockets that the nodes which orient points and normals share. ## 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
·
nodebox · born here
❒asset
▲ 0↑ born
·
nodebox · born here
❒asset
▲ 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; `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
·
nodebox · born here
❒asset
# io The ways in and out of a graph: `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 the core reading nodes and `export` use. `output` holds `show.module` and `engine` holds `engine.module`, the functions they call.
▲ 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
·
nodebox · born here
❒asset
# math One node per operator and function of the authoring surface: add, sub, mul, div, mod, pow, neg, abs, min, max, floor, ceil, sqrt, sin, cos, tan, acos, atan, log, sign, deg, rad, clamp, lerp, smoothstep, 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. Operators and `math.*` over sockets resolve to these nodes. Each 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`).
▲ 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
# 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
·
nodebox · born here
❒asset
▲ 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
·
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
·
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
·
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
·
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
# 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
▲ 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
·
assettype · born here
❒asset
# procGraph (asset type) A `.procGraph` asset is a **procedural graph**: a description of how to make something, written as code, that can be evaluated over and over with different numbers. Its `init.luau` returns `proc.define(...)` — that source IS the asset, the way a `.procNode`'s `init.luau` is. ## Shape A `<name>.procGraph/` folder containing: - `init.luau`: the graph's definition. Returns `proc.define(function(g) … end)`, the graph named by its asset folder. - `graph.json` — the compiled form (`proc.graph.v1`), written by this type from `init.luau`. Generated, never hand-edited. - `README.md`, `.metadata` — the universal asset files. ## Authoring Edit `init.luau`. The type recompiles on the write, rewrites `graph.json`, and every live `Generator` bound to the asset re-cooks from it — there is no build step between saving and seeing the result. A recompile is cheap however large the graph: node results are keyed by content, so recompiling an unchanged node reuses its cooked output, and an edit invalidates only that node and the ones downstream of it. Changing the last step of a landscape does not re-run the erosion above it. Two words worth keeping apart, because both happen here: - **compile** — `init.luau` → `graph.json`. Produces the graph. - **cook** — each node performing its operation to produce a value. Runs the graph. Driven by a `Generator`, reported by `procgen.cooks`. ## Using one Through the ref rather than a path string: `ref:eval("mesh")` evaluates an output, `ref:inputs()` lists the knobs a Generator can override by name, `ref:outputs()` the values it produces, `ref:validate()` type-checks it, and `ref:asOp()` wraps it so another graph can composite it as a node. Generated outputs (meshes, materials, textures, bundles) are persisted through their own asset types. ## Related - `@builtin::systems.procgen.proc` — the authoring + evaluation model. - `procgen.toolbox` — `preview`, `spawn`, `bake`, `validate`, `ops`, `schema` and the wider tool family. ## In the Inspector The Inspector shows the graph as nodes and wires, and edits it there. Every graph input, op, graph used as a node and output is a node; each input port that nothing is wired into carries the value that drives it, and a click on a value edits it where it stands (typing, dragging a number, flipping a switch, choosing from a list). Drag a node to move it, drag from a port to another port to connect, drag a connected input away to take its wire off, Delete removes the selection. A node is added from the op search under the graph, or from the procGraph assets, which stand as nodes that open their own asset on a double click. The graph's inputs and outputs are nodes too: their name, type, default and range, and an output's domain, are values on them, and the Inputs and outputs section declares new ones. Describes is the sentence search finds the graph by. Every edit is written into `init.luau` with `proc.saveSource` and compiled by the type, so a live Generator re-cooks on it; the node positions are kept beside the graph in `layout.json`. The Result section scrubs the timeline of a graph that has one and shows what the selected node makes (`procgen.preview` with the target `node:<id>`), and the Preview section above it shows what the graph makes, live. **Compile** and **Open** for its source stay as they were.
▲ 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 keyword and by meaning and lists them grouped by family, and 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 it by its description and by its inputs. ## 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")`). 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). ## 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
# 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
·
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
·
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
·❒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
·
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
·
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
·
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
·
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
·
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 writes the id directly. 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, 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. ## procPack or procNode Both contribute ops that a graph evaluates, and both take the same def table. They differ in how an op is addressed: | | `.procNode` | `.procPack` | |---|---|---| | contributes | one op | a family | | addressed by | the asset's name, or its ref from `asset.resolve(id, "procNode")`, which the graph records by guid | the string id the pack names it with | | a graph writes | `g:node("n", "ScaleBy")` or `g:node("n", ref)` | `g:node("n", "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. ## Registration lifecycle `init.luau` is compiled from its own source on every registration rather than `require`d, so an edit lands immediately and anything it pulls in uses an absolute require (`@builtin::systems.procgen.proc`). Every op's `version` is derived from the pack's source, 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 still naming it errors loudly at eval rather than cooking against a definition that no longer exists, and 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
·
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
·
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
# 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
# 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
·
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
# 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
# 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
·
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
# 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
·
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
# 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
# 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
·
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
·
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
·
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
# 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
·
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
·
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
·
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
# 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. ```lua local ember = g:node("ember", "material.shader", { properties = { gain = 6 } }, { shader = "spark", blend = "additive", depthWrite = "false", cull = "none" }) ``` `material.pbr`, the metallic-roughness material, comes with the `texture` pack.
▲ 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
·
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
·
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
·
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
# 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
·
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
·
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
# 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
# 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
·
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
·
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
# 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
# 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
·
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
·
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
·
assettype · born here
❒asset
# procRecord: a kind of record a graph makes and saves A `.procRecord` defines a kind of value a procedural graph moves that becomes an asset when it is saved: a material, a mesh, a texture, or a kind a world defines for its own asset type. Its `init.luau` returns `proc.record({ ... })`: ``` Glow.procRecord/ init.luau -- returns proc.record({ ... }) README.md -- what a record holds and the asset it saves as .metadata -- description and tags ``` ## What a kind states - **`name`**: the kind's display name. A kind is identified by its guid, so two kinds may share a name. - **`asset`**: the asset type a record saves as (`"material"`). - **`applies`**: `{ component, field }`, the component a record stands in live and the field it feeds (`{ component = "Material", field = "material" }`). - **`fields`**: the record's own fields by name, each with a short description. - **`live(record, key)`**: the runtime value the applied field takes, filed under `key`. - **`save(record, name, place)`**: writes the record as its asset and answers the reference. Left out, the asset type's own `graph.write` writes it. - **`load(ref, options)`**: the record an asset of the kind holds. Left out, the asset type's own `graph.read` reads it. ## How records travel A record carries its kind's guid as `kind`. The node that writes a record holds it on an element, in the attribute named after the component the kind applies to (`Material = { material = <record> }`), and states the kind on the geometry's detail attribute `records`: `{ [<kind guid>] = <reference to the .procRecord> }`. `io.output` stands the element with the runtime value the kind's `live` answers, `io.export` saves the record through the kind, and `io.import` reads an asset back as the same element: an asset type names the kind its assets load as in its `graph` conversion (`graph.record`). A node references a kind by a literal reference, `asset.resolve("<identity>", "procRecord")`, and `proc.records.stamp(ref)` gives the `records` map that names it. `io.import` of a `.procRecord` answers the kind itself, `{ kind, name, asset, applies }`: the guid a record of it carries and what it saves as and applies to.
▲ 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. ## 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 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` — type-check a graph: missing inputs, type mismatches, cycles, invalid composite bindings. - `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` takes 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 a target 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 graph: creates an entity with a Generator component bound to it, realizing the output immediately and regenerating live as params change. - `adjust` — adjust a live generator in place: merge param overrides or switch the realized 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 a target and realize it straight into the scene as durable content, without a live generator. ## 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.
▲ 0↑ born
·
procrecord · born here
❒asset
# material (procRecord) The material record kind: a surface shader, its property values by name, its texture slots by name and, optionally, render state (`blend`, `cull`, `depthWrite`) and a buffer property filled from elements. `material.pbrMat` and `material.shader` answer one, held on one element as `Material = { material = <record> }`. Live, a record becomes the runtime material the `Material` component's `material` field holds. Saved, it is a `.material` asset, and a Texture record in one of its slots is written as a texture beside it. `io.import` of a `.material` asset answers the same element, its record read from the asset.
▲ 0↑ born
·
assettype · born here
❒asset
# Package (asset type) A package is an asset — a `<Name>.package/` folder that bundles components, modules, scenes, presets, tools, and any other typed sub-assets under one identity, installed into a world as one unit. Packages are how authoring sets get distributed across worlds. A **library** is the addressable root scope content resolves under (`@builtin`, `@<name>`) — a whole world mounted into this one under a namespace, via `zeromind.installLib` / `world.installLibrary`. A package is an asset that lives inside such a root, and one library holds as many packages as its author put there. A package is installed by guid the way every asset is (`zeromind.install`, `zeromind.installAsset`). ## When to use one - You're authoring a coherent feature suite (character controller stack, voxel engine, UI kit, animation library) and want to ship it as one install target. - You want a stable identity worlds can install and resolve sub-assets against (`@<lib>::pkg.sub_module`). - You want versioning + a per-package metadata layer that travels with the contents. If you're authoring inside a single world and don't need a separate install lifecycle, you don't need a package — drop your `.component` / `.module` / etc. directly in `/zero/source/`. Packages are for cross-world distribution. ## Where it lives - Source: `/zero/source/.../<Name>.package/` (or, for legacy packages, a folder containing `package.yaml` at its root). - Identity: `<Name>` (the `.package` suffix collapses in identity — mirrors `.module`). A file at `<Name>.package/<sub>/init.luau` resolves under the same identity it would at `<Name>/<sub>/init.luau`. - Folder shape (container-style — open child set): - `package.yaml` — metadata (name, version, description). **Required.** - `README.md` — instance documentation. **Required.** - `.metadata` — agent-editable tags + free-form fields. **Required.** - Sub-asset folders: any combination of `.component/`, `.module/`, `.scene/`, `.preset/`, `.tool/`, `.toolbox/`, `.material/`, `.shader/`, etc. The validator permits unlisted children (`allow_unlisted: true`) by design — packages are open containers. ## How to create one ```luau asset.create("package", "<Name>") -- Creates: /zero/source/<Name>.package/ -- package.yaml (metadata scaffold) -- README.md (instance README template) -- `folder` places it in a subfolder of /source instead of the root: asset.create("package", "<Name>", { folder = "systems" }) ``` Then scaffold sub-assets inside it: ```luau local pkg = asset.resolve("<Name>.package") asset.create("component", "Spinner", { into = pkg }) asset.create("module", "util", { into = pkg }) ``` ## How it operates 1. **Registration.** Writing `package.yaml` indexes the package with the package registry. 2. **Install.** A package is read before it is installed: `zm ls-tree @<guid>` in the shell lists what it contains, and `zm show @<guid>:<path>` and `zm grep <pattern> @<guid>` read it, so candidates are compared by what they do. The one that fits is installed by guid, the way every asset is: `zeromind.install` from the shared library, or `tools.use("zeromind", "installAsset", <guid>)` inside the engine. The install writes the package's files (and the deps its `.refs` record) into this world's source, so its sub-assets resolve here. `tools.use("zeromind", "preview", <guid>)` reports what an install would write before it writes anything. 3. **Sub-asset resolution.** Because `.package` collapses in identity, sub-asset paths work whether the package folder is suffixed (`<Name>.package/<sub>`) or not (`<Name>/<sub>`). 4. **Updates.** `tools.use("zeromind", "updates")` lists installed content with upstream changes available; re-running the install pulls them. 5. **Metadata.** `package.yaml::name` overrides the folder stem if present (rare); `version` + `description` are surfaced in the package index. 6. **Hot reload.** Edits to sub-assets hot-reload as normal; edits to `package.yaml` reload package metadata. ## Discovery - `asset.list("package")` — every registered package. - `asset.inspect("<name>")` — sub-asset catalog, source, this type README. - `ls /zero/source/<Name>.package` lists the files the folder holds. - `library.list()` — the library assets registered in this world, including the ones packages under an installed library contribute. ## Authoring conventions - Name packages by their feature (`voxels`, `cameraRigs`, `dialogSystem`), not by their owner — the host library is the `@<lib>::` scope prefix. - Cluster cohesive sub-assets together. A "character controller" package should ship the controller component, its preset configs, its supporting modules, and a demo scene — not just the component. - Document the package's surface in `README.md` — what install installs, what each sub-asset does, how they relate. Agents reading `cat /zero/source/<Name>.package/README.md` get the full picture in one shot. - Version conservatively — breaking a package means breaking every world that installed it. Use semantic version bumps. ## Common pitfalls - **Missing `package.yaml`.** Without it the folder isn't recognized as a package even if it has a `.package/` suffix. - **Dependency installs.** A package that depends on another package must declare it (in `package.yaml::dependencies` once supported) and the parent world must install dependencies in order. - **Identity collapse.** Sub-asset paths skip the `.package` suffix. Don't include `.package` in `require` strings. ## Referencing inside a package: the `~` rule `~` means **this package**, resolved structurally from the referring file's own location. It is the only package-specific syntax. It is rename-safe (no package name is ever written) and library-agnostic (it never names the host library, so it survives `@builtin` being renamed or the package being mounted elsewhere). `~.tail` maps to `<pkg>.package/tail` from anywhere inside the package. | Referencing from | Use | Example | |---|---|---| | An asset/module **inside this package** | `~.tail` | `require("~.modules.myThing")`, `shader: "~.shaders.myThingBlend"` | | A **different package** under the same root | root-relative identity | `require("@root::systems.otherPkg.modules.x")` | | A **different library's** public API (a deliberate external dependency) | `@otherlib::…` | `require("@physics::modules.rigidbody")` | **Never write `@builtin::` (or any host-library name) to reference your own package** — that couples the package to the library it currently lives in and breaks if the library is renamed or the files move. Use `~.`. It works from Luau `require()` and from asset-reference strings in data files (a material's `mat.yaml` `shader:` field, a component's default asset ref); the engine resolves it against the referring file's own package. `asset.resolve` takes a `~.` name the same way, against the package of the file that calls it: `asset.resolve("~.materials.myThingStandard", "material")`. `~.` is the package-relative self-reference; `@root::` is the root-relative one (the library root, or the world source tree for non-library content). The `core/requiring-modules` guide covers the root model in full. A package that ships substantial functionality should include a `.guide` asset (a `<name>.guide/guide.md` folder anywhere under the package); the `guides` tool discovers it live, so collaborators find the system the way they find built-in guides. A package's test suite reaches the shared `Test` framework through its enclosing asset: `local Test = asset.containing(__FILE__).modules.shared` (`asset.containing` returns the deepest enclosing typed asset, the `.testSuite`, not the outer `.package`). ## Related types - Any of `.component`, `.module`, `.scene`, `.preset`, `.tool`, `.toolbox`, `.material`, `.shader`, `.style`, `.service`, `.importer` — packages bundle these. ## In the Inspector Package: the name, version and description package.yaml declares, how many things the package holds and what they are, and **Open** for package.yaml. A package.yaml that does not decode is listed under Problems with the decoder's error.
▲ 0↑ born
·
assettype · born here
❒asset
# Component (asset type) A component is a reusable behavior + data unit attached to an entity. The component file declares public fields, methods, and lifecycle callbacks; the engine instantiates one per attachment and drives its hooks every frame. Components are the primary unit of gameplay code — movement, AI, animations, UI panels, network sync, and per-entity state all live here. ## When to use one - You need per-entity state that survives across frames. - You need per-entity behavior driven by engine lifecycle hooks (`start`, `update`, `destroy`). - You want to expose configurable fields to designers / `entity.spawn` callers / scenes without code changes. If you need a global service that outlives any entity, use a `.service` instead. If you need a stateless pure-function library, use a `.module`. ## Where it lives - Source: `/zero/source/.../<Name>.component/` - Identity: `<Name>` (the `.component` suffix strips from the identity, but the folder retains the suffix on disk). - Folder shape: - `init.luau` (or `init.lua`) — declares the component. **Required.** - `README.md` — instance-level documentation. **Required.** - `.metadata` — agent-editable tags + free-form fields. **Required.** The structural contract is enforced by `asset.validate` against this type's `type.yaml`. ## How to create one Always start with the scaffold — it guarantees the folder shape matches `type.yaml` from the moment it's on disk: ```luau asset.create("component", "<Name>") -- Creates: /zero/source/<Name>.component/ -- init.luau (canonical declare + public block + hooks stubbed) -- README.md (instance README template) ``` After scaffolding, edit `init.luau` to fill in the actual fields and behaviour. The component registers as soon as the file is written — hot-reload is automatic; no engine restart required. ## How it operates 1. **Registration.** Writing `init.luau` into a `.component/` folder indexes the asset, mints a guid (in the `.meta` sidecar), and surfaces the type to the engine's component registry. Same-frame visible to `entity.component.add`. 2. **Attachment.** `entity(id).component.add(name, data)` (or the batch form `tools.use("entityOps", "addComponents", targets, { [name] = data })`) attaches an instance. The `data` table's fields override the component's declared defaults; omitted fields take the declared default. The `data` values are copied into `public.*` BEFORE `awake()` runs. 3. **Lifecycle.** The hooks run in this order: - `awake()` — same frame as attach, immediately after the data copy. Initialise state, set up native bridges, register with other systems. Other components on the same entity may not have awoken yet. - `start()` — the next frame. Every component on the entity has completed `awake()` by then, so cross-component setup is safe here. - `update(dt)` — same frame as `start()`, then every frame after, for as long as the gameplay clock is running. `fixedUpdate(dt)` runs beside it on the fixed timestep; `editorUpdate(dt)` runs while the clock is held. Which of them ticks is decided by `engine.mode` AND `engine.paused` together — see **Per-frame hooks and the two axes** below, and `man modes`. - `onDestroy()` — when the component is removed via `component.remove` or when the owning entity is despawned. Release resources, drop native ECS components. For the full list (`onEnable`, `onDisable`, `fixedUpdate`, `editorUpdate`, `onPropertyChanged`, `onAssetReload`, `onModuleReload`, `onBeforeSave`, …) and the timing details, **read `man components` — that is the canonical lifecycle reference. Generic component patterns (declare blocks, public fields, sync, ECS bindings, typed methods) all live there.** 4. **Public surface.** Fields declared in `public` are inspectable, editable in the inspector, and persist with the entity. Methods are declared on that same table with a colon — `typed function public:method(args)` — and every one of them takes the component ref as `self`, so a caller invokes it with a colon too: `entity(id).component.get(name):method(args)`. That is the one calling convention a component ref answers to: the reflection methods `ref:fields()` / `ref:getField(name)` / `ref:setField(name, value)` take the same spelling. A function the chunk declares at file scope (`function helper()`) is the component's own, reachable from its hooks under its bare name; `public` is what the ref carries. 5. **Settings and the Inspector card.** `type.yaml` names the component's settings schema as `settings: { schema: { instance: "init.luau", export: "public" } }`: the `public` table IS the schema, and its values live on each entity the component is on, so `asset.schema(componentRef)` answers the Field descriptors and no values file exists. On an entity the Inspector shows the component as a card whose generic `settings` section edits those fields as typed controls, each edit checked against the schema and written to the live component as one undoable operation. `type.yaml` also names `inspector: { instance: "inspector.luau" }`: a component that ships an `inspector.luau` beside its `init.luau` shapes its card — the file returns `{ sections, replace, order }` (see `assetTypes/README.md`, "`inspector:`"), each section's `build(ctx)` reading `ctx.component` (the live component) and `ctx.selection.id` (the entity). `SceneModule.component/inspector.luau` replaces `settings` with the bound module's declared inputs. 6. **Events.** An `events` block declares typed moments the component fires from inside — `events = { onHit = Event({ dmg = Field.number(0, NoSync) }) }`. The owner fires with `events.onHit:fire({ dmg = 10 })`; outside code subscribes through the proxy's subscribe-only facade, `entity(id).component.get(name).events.onHit:connect(fn)` (`:once` / `:wait` too). Pass `Sync` as the event's second argument to replicate a fire to other peers. Events let one component react to another's moments without polling. Full reference: **`man components`**, Events section. 7. **Hot reload.** Editing `init.luau` reloads the component definition; live instances are reconciled with the new declaration in-place, and each crosses one lifecycle boundary: `onDestroy()` ends the running life, then `awake()` and `start()` construct the one that replaces it, and `update()` resumes on the next frame. `public` and `private` values carry across; module-level `local`s belong to the chunk, so the new life starts with them at their initial values and its `awake`/`start` are what fill them in. Whatever the old life handed elsewhere — a subscription, a shared registration, a spawned entity — is released in `onDestroy`, the call that pairs with the construction about to run. An edit that leaves the source byte-identical crosses no boundary at all. A field carries its value across when something stated it: a key of the `data` table the instance was attached with (a scene record's data is that table), or a write made since. Every other field takes the edited source's declared default, so changing a default reaches every instance that left the field to it. An instance that states its own value for a field whose default the edit changed keeps that value, and the reload logs a `[HotReload]` warning naming the field, the entity, the value kept and both defaults. ## Discovery - `asset.list("component")` — every registered component (engine + world + libraries). - `asset.inspect("<name>")` — identity, scope, public fields, lifecycle methods, source path, and this type README. `detail` carries the component's declared `fields`, `methods`, and `events`, parsed from its source — `tools.use("assets","describe", "<name>")` renders it as markdown, so a component's events are visible without opening its source. - `ls /zero/source/<Name>.component` lists the files the folder holds. - `entity(id).component.list()` — components attached to an entity. ## Per-frame hooks and the two axes `engine.mode` (`"edit"` / `"play"`) and `engine.paused` (the gameplay clock) are independent, and the per-frame hooks read them separately: - `update(dt)` / `fixedUpdate(dt)` tick while **the gameplay clock is running** — whichever mode the engine is in. - `editorUpdate(dt)` ticks while **the clock is held** — because the mode is `edit`, or because play is paused — and also alongside `update` whenever edit is running. Which gives four states, and this table is the contract: | `engine.mode` | `engine.paused` | hooks that tick | |---|---|---| | `edit` | `true` | `editorUpdate` | | `edit` | `false` | `update`, `fixedUpdate`, `editorUpdate` | | `play` | `false` | `update`, `fixedUpdate` | | `play` | `true` | `editorUpdate` | Entering edit sets `paused = true` and entering play sets it back to `false`, so a session that never writes `engine.paused` only ever sees rows 1 and 3 — which is where the shorthand "`update` is the play tick, `editorUpdate` is the edit tick" comes from. It is the two default rows of a four-row table, not the rule. **Row 2 is the one that surprises.** With `engine.paused = false` in edit — set by hand, or by a tool that advances the gameplay clock to sample it, such as a `capture.collage` given a `duration` — `update` and `editorUpdate` both fire, once each, every frame. A component that declares both and routes them to one body therefore runs that body **twice per frame**: ```luau local function drive(dt) ... end function update(dt) drive(dt) end function editorUpdate(dt) drive(dt) end -- twice per frame in row 2 ``` Nothing reports the doubling; it reads as an animation running at twice its intended speed. To get exactly one tick per frame in every row, ask which axis you meant and answer it in the body: ```luau function update(dt) drive(dt) end function editorUpdate(dt) if engine.paused then drive(dt) end end ``` A scene `entrypoint.luau` declaring the same two hook names is dispatched by the scene loader, not by this table: there `update` needs play AND running, `editorUpdate` needs edit, and play-paused ticks neither. Read `man scenes` before carrying a component's rule to an entrypoint. ## Authoring conventions - Annotate public fields with `--!desc` so the inspector + LSP show documentation alongside the field name. - Declare lifecycle hooks (`start`, `update`, `destroy`) explicitly rather than relying on engine defaults — readers can see at a glance what runs when. - Keep `update(dt)` cheap — it runs every frame per instance. Move heavy work to `start`, to a service, or behind a job (`man jobs`). - For multi-entity batching, prefer `tools.use("entityOps", "addComponents", targets, components)` over a loop of `entity(id).component.add` — it takes many targets and many components in one call. ## Common pitfalls - **Naming.** The component identity is the folder stem with `.component` stripped (e.g. `<Spinner>.component/` → identity `Spinner`). Don't repeat the suffix in the identity passed to `component.add`. - **`init.lua` vs `init.luau`.** Either is accepted (it's a `one_of_group: "entrypoint"`), but `.luau` is canonical. Mixing both in one folder is invalid. - **`update` in edit mode.** It runs whenever the gameplay clock is running, and entering edit stops that clock — so `update` is idle in a freshly-entered edit session and ticks again the moment anything sets `engine.paused = false`. Use `tools.use("wld", "play")` to test update logic under the mode it ships in; `tools.use("wld", "edit")` to return. See **Per-frame hooks and the two axes** above. - **Renaming.** Renaming the folder changes the identity. Update every `component.add(<oldname>, ...)` call and the scene JSON entries that reference it. ## Related types - `.module` — pure-function library, no per-entity state. - `.service` — globally-scoped behavior, no per-entity state. - `.preset` — a captured component configuration, replayable into `component.add` without re-typing fields. - `.bundle` — entity hierarchies that may carry component attachments as part of the bundle. ## In the Inspector Fields: its execution order, hooks, and how many methods and events it declares, then the fields its `public` table declares as read-only typed controls holding their defaults, with **Check** and **Open** for its entry file. Instances: a link to every entity carrying it. What its card shows on an entity comes from the component's own `inspector.luau`.
▲ 0↑ born
·
procnode · born here
❒asset
# double Doubles the first element of a column on the device.
▲ 0↑ born
·
assettype · born here
❒asset
# Compute shader (asset type) A `.computeShader` is a GPU compute (GPGPU) program with a **zero-scaffolding** authoring contract: you write only `@compute fn main(...)` plus a declarative `bindings.yaml`, and the engine GENERATES every `@group/@binding` declaration — storage buffers, textures, samplers, storage textures, and a `params:` uniform — expands `#include`s, naga-validates the result, and registers it. The single source of truth for the binding layout is `bindings.yaml`; there are no hand-written `@binding` lines to keep in sync. > New to how content lives across the CPU and GPU? Read `core/resource-model` > first: it covers the asset / CPU / GPU forms every resource takes, the upload > path, and how compute fits the performance ladder (`core/performance`). ## Folder shape A `.computeShader` is a folder asset: ``` <name>.computeShader/ shader.wgsl # the WGSL body — only `@compute fn main` + helpers (required) bindings.yaml # the binding + params schema (required) README.md # a one-paragraph description of THIS shader (required) .metadata # asset metadata ``` Each file carries a committed `.meta` sidecar pinning its stable guid. The identity is `<name>` (the `.computeShader` suffix strips). Resolve the shader once, then dispatch through the reference: ```lua local carve = asset.resolve("carve", "computeShader") carve:dispatch({ buffers = { heights }, workgroups = { 64 } }) ``` `asset.resolve` is what records your script's dependency on the shader, so the shader travels with the world your script lives in — packed, pulled, or published. It compiles on first dispatch; there is no setup step. ## bindings.yaml One ordered `bindings:` list — the binding index is the list position (0, 1, 2, …) — plus an optional `params:` scalar-uniform block. `kind` defaults to `buffer` when omitted. ```yaml bindings: - { name: clouds, kind: storage3d, format: rgba16f } # texture_storage_3d<rgba16float, write> - { name: occupancy, kind: texture3d } # texture_3d<f32> - { name: smp, kind: sampler } # sampler - { name: out_tex, kind: storage2d, format: rgba16f } # texture_storage_2d<rgba16float, write> - { name: U, kind: buffer, access: read, element: f32 } # var<storage, read> U: array<f32> - { name: Uniforms, kind: buffer, access: read, element: VolumeUniforms, array: false } # var<storage, read> Uniforms: VolumeUniforms params: - { name: scale, type: float, default: 2.0 } - { name: count, type: int, default: 0 } ``` | `kind` | extra keys | generated WGSL declaration | |---|---|---| | `buffer` (default) | `access: read \| read_write` (default `read_write`), `element: <wgsl type>` (default `f32`), `array: true \| false` (default `true`) | `var<storage, ACCESS> NAME: array<ELEMENT>;` (or `… NAME: ELEMENT;` when `array: false`) | | `texture3d` | — | `var NAME: texture_3d<f32>;` | | `texture2d` | `sample: float \| uint \| sint` (default `float`) | `var NAME: texture_2d<f32>;`, or `<u32>` / `<i32>` for the other classes | | `texture_depth` | — | `var NAME: texture_depth_2d;` — the one sampled shape with no `sample:` of its own; a depth-aspect channel (`@scene.depth`) binds here, not through `texture2d` | | `storage3d` | `format`, `access: write \| read \| read_write` (default `write`) | `var NAME: texture_storage_3d<FMT, ACCESS>;` | | `storage2d` | `format`, `access: write \| read \| read_write` (default `write`) | `var NAME: texture_storage_2d<FMT, ACCESS>;` | | `sampler` | — | `var NAME: sampler;` | | `textureCubeArray` | — | `var NAME: texture_cube_array<f32>;` — the environment cube array, bound to `@scene.environment` by a render feature's pass | | `vertices` | `access: read \| read_write` (default `read_write`) | `var<storage, ACCESS> NAME: array<u32>;` plus the mesh's layout uniform and the `zeroVertex*` accessors | | `acceleration_structure` | — | `var NAME: acceleration_structure;` (hardware ray tracing) | - **Eight storage buffers per shader** — the WebGPU guarantee, and what the engine asks every device for. `buffer` and `vertices` bindings spend that budget; textures, samplers, storage textures and `params:` do not. An `acceleration_structure` brings engine-owned buffers with it — **three** where the device has hardware ray query (the tables `zeroHitSurface` / `zeroMaterial` read) and **five** on the software backend (those, plus the scene triangles and the hierarchy over them) — so a traced pass has three slots of its own on the backend web users get. Over budget, the engine refuses the shader and logs which buffers it declared. - A `buffer` defaults to `array<ELEMENT>`. Set `array: false` for a single NON-array storage buffer — a struct binding (e.g. `element: VolumeUniforms`, `array: false` → `var<storage, read> NAME: VolumeUniforms;`). The struct definition itself lives in `shader.wgsl`, not `bindings.yaml`. - **A sampled texture binds under the class its FORMAT puts it in**, so `sample` states that class and the generated `texture_2d<..>` matches it. An integer channel — `@scene.material`, which is `Rgba8Uint` — is `uint`; everything else is the `float` default. Bind an integer channel to a `float` slot and the engine refuses the pass and names both classes, because the device would otherwise reject the bind group and invalidate the pipeline. - A storage texture's `access` decides both the generated declaration and the layout the engine registers, so the two always agree. It defaults to `write`, the access every target grants every storage-compatible format. `read_write` is granted on `r32f` alone; declaring it on any other format is refused at the compile, with the reason on `compute.programState`. A pass that needs to read a wider target reads it through a separate `texture3d` / `texture2d` binding, or through a second storage binding declared `read`. - `format` tags (storage textures): `r8`, `r16f`, `r32f`, `rgba8`, `rgba16f`, `rgba32f`. Storage textures only allow storage-compatible formats (`r32f` / `rgba8` / `rgba16f` / `rgba32f`). - An `acceleration_structure` binding resolves to the scene acceleration structure when the shader runs as a render-pass with ray tracing enabled (`renderer.setRaytrace(true)`). Declaring one makes the engine emit the helper functions the body can call: `zeroTraceClosest(origin, dir, t_min, t_max) -> ZeroHit` (struct: `hit, t, instance, primitive, bary, attr`, where `instance` is the hit object's render slot), `zeroTraceAny(origin, dir, t_min, t_max) -> bool` (terminate-on-first-hit, for shadow/occlusion rays), and `zeroHitSurface(hit) -> ZeroSurface` (`normal, uv, material`) + `zeroMaterial(index) -> ZeroMaterial` (`base_color, emissive, metallic, roughness`) to shade what a hit landed on. The same surface works on both backends — hardware ray query and the software/compute path — so the shader doesn't branch on `renderer.raytraceCapability()`. Bind it from a render-pass, which is where the scene acceleration structure exists. `params:` declares scalar uniforms (`type: float | int`). When non-empty the engine appends a `ZeroComputeParams` uniform as the trailing binding (read it in WGSL as `params.<name>`) and tunes its values at runtime with `shaderRef:setParam(prop, value)`. ## The kernel ABI: `columns`, counted and appended outputs, `specialize`, `determinism` One shader asset can serve many shapes of data. `bindings.yaml` states the shape with these extensions, and the engine's compile path lowers them to the plain bindings and WGSL above (`modules/compute_abi`, whose README has the whole surface): ```yaml workgroup: 64 # the size the body declares (default 64) bindings: - { name: cols, kind: columns, access: read } # a table of columns in one binding - { name: src, kind: buffer, access: read, element: T } - { name: dst, kind: buffer, access: read_write, element: T, count: "len(src)" } - { name: hits, kind: buffer, access: read_write, element: vec4<f32>, capacity: "n * 4", append: true } specialize: - { name: T, options: [f32, i32, vec2<f32>, vec3<f32>, vec4<f32>] } - { name: FOLD, options: [add, min, max] } - { name: BODY, kind: source, default: "0.0" } # text that replaces the identifier determinism: exact # or float_order ``` - A `buffer` of a scalar or vector element gets `NAME_get(i)`, `NAME_len()` and (unless `access: read`) `NAME_set(i, v)`. A `vec3` element is stored as a flat scalar array, so a column of vec3 keeps its 12-byte stride. `zero_n()` is the dispatch size. - `columns` binds a table of columns (`compute_abi.table`) as one storage buffer; `cols_f32(c, i, lane)`, `cols_vec3(c, i)` and the rest read it. - `count` / `capacity` give the size, in elements, of an output the run allocates. `append: true` adds a live counter binding, `NAME_count`, with `NAME_store(slot, v)`, `NAME_set_count(n)` and, under `determinism: float_order`, `NAME_append(v)`. - `specialize` parameters are compile-time: type options become `alias NAME`, named options become `const NAME` and `NAME_<option>`, `kind: source` text replaces the identifier. Each specialization compiles once. - `determinism: exact` refuses `NAME_append`: an exact output is written at ranks the kernel computed. Dispatch such a shader through its program: ```lua local variant = shaderRef:program():variant({ specialize = { T = "vec3<f32>" } }) local run = variant:run({ n = 64, inputs = { src = srcBuffer } }) local values = require("@builtin::modules.compute_abi").read(run.outputs.dst.buffer) ``` `shaderRef:program()` answers the same program, with the variants it has lowered and compiled, for as long as `shader.wgsl` and `bindings.yaml` hold the bytes they held when it was made. A write to either file, from any origin (`vfs.contentVersion` moves), makes the next `program()` a new one, and a dispatch through it runs the new text. The schema `getBindings()` answers is the one the program was made over. ## Writing a mesh's vertices — `kind: vertices` A mesh's vertices are raw bytes on the GPU, and the engine stores a mesh in one of two layouts: 92 bytes a vertex with every attribute a `f32`, or 32 bytes a vertex with normals and tangents quantized to bytes and UVs to shorts. Which one a mesh got depends on the mesh. Declare `kind: vertices` and the shader is handed both the buffer and the layout that mesh is in, plus accessors that address it: ```yaml bindings: - { name: verts, kind: vertices } # the mesh, writable - { name: heights, kind: buffer, access: read, element: f32 } params: - { name: lift, type: float, default: 1.0 } ``` ```wgsl @compute @workgroup_size(64) fn main(@builtin(global_invocation_id) id: vec3<u32>) { if (id.x >= zeroVertexCount()) { return; } var p = zeroVertexPosition(id.x); p.y = p.y + heights[id.x] * params.lift; zeroSetVertexPosition(id.x, p); zeroSetVertexNormal(id.x, vec3<f32>(0.0, 1.0, 0.0)); } ``` ```lua carve:dispatchOnVertices({ model = meshRef:handle().guid, buffers = { heights }, workgroups = { 64 } }) ``` The accessors, over a binding declared `read_write`: | read | write | |---|---| | `zeroVertexCount() -> u32` | — | | `zeroVertexPosition(i) -> vec3<f32>` | `zeroSetVertexPosition(i, vec3<f32>)` | | `zeroVertexNormal(i) -> vec3<f32>` | `zeroSetVertexNormal(i, vec3<f32>)` | | `zeroVertexTangent(i) -> vec4<f32>` | `zeroSetVertexTangent(i, vec4<f32>)` | | `zeroVertexUv(i) -> vec2<f32>` | `zeroSetVertexUv(i, vec2<f32>)` | | `zeroVertexColor(i) -> vec4<f32>` | `zeroSetVertexColor(i, vec4<f32>)` | | `zeroVertexNodeIndex(i) -> u32` | — | | `zeroVertexWord(i, offset) -> u32` | — | `access: read` gives you the readers only — a `var<storage, read>` binding cannot be assigned to, so the writers are left out rather than handed to you broken. Once a pass has written a mesh's vertices, the engine bounds that mesh by what the pass wrote: it reduces the vertices to an AABB every frame and culls against that, so geometry a pass moves across the scene needs no oversized placeholder to survive culling. `renderer.mesh.boundsSource(guid)` reports `"compute"` for such a mesh and `"geometry"` for one still bounded by what it was created with. A shader declares at most one `vertices` binding; the accessors are generated for that mesh. It can be declared anywhere in the list — the mesh is bound by the dispatch, and `buffers` fills the other storage slots in declaration order. The tangent's `w` carries handedness, and quantized normals/tangents come back in -1..1 while quantized UVs and colours come back in 0..1, so the values are the same ones a surface shader sees whichever layout the mesh is in. ## shader.wgsl Write only `@compute fn main` (and any helpers / `#include`s). The names declared in `bindings.yaml` are in scope — `data`, `out_tex`, `params.scale`, … — with no `@group`/`@binding` lines of your own. ```wgsl // @domain: compute // Bindings are declared in bindings.yaml — do NOT write @group/@binding here. @compute @workgroup_size(64) fn main(@builtin(global_invocation_id) id: vec3<u32>) { if (id.x < arrayLength(&data)) { data[id.x] = data[id.x] * 2.0; } } ``` ## Dispatch & runtime Everything a compute shader does, it does through its reference: | method | what it does | |---|---| | `ref:dispatch(opts)` | `{ buffers, workgroups, params? }` — bind one buffer per declared storage binding, in order | | `ref:dispatchOnVertices(opts)` | `{ model, buffers?, workgroups, params? }` — bind the mesh `model` names to the `vertices` binding, `buffers` filling the rest | | `ref:dispatchEx(opts)` | `{ resources, workgroups }` — for shaders mixing textures, samplers and storage textures: one `{ kind, name }` per declared binding, in order | | `ref:setParam(prop, value)` | write one `params:` scalar uniform | | `ref:createBuffer(name, opts)` | `{ type, len, usage? }` — a GPU buffer owned by this shader | | `ref:buffer(name)` | the buffer this shader last made under `name` | | `ref:getBindings()` | the parsed `bindings.yaml` schema, read and parsed again only after the file is written | | `ref:getSource()` / `ref:setSource(wgsl)` | read / overwrite `shader.wgsl` | | `ref:compile()` | compile now instead of on the first dispatch | | `ref:status()` | what the engine did with this shader's dispatches, one record per target | The `params:` uniform is engine-owned and takes no entry in `buffers` or `resources`, and neither does a `vertices` binding's layout. `setParam` writes the uniform every later dispatch reads. A dispatch that passes `params = { name = value }` reads those values instead, in a copy of the uniform only it binds, with every name it leaves out at the value the shader holds — so one shader dispatched twice in a frame over different data reads different values each time: ```lua lift:dispatch({ buffers = { a }, workgroups = { 4 }, params = { height = 1.0 } }) lift:dispatch({ buffers = { b }, workgroups = { 4 }, params = { height = 3.0 } }) ``` A name the shader does not declare fails that dispatch, and the failure names what it does declare. ### `dispatchEx` resources Each `resources` entry fills the binding at its position. Its `kind` says what the resource is, in the words `bindings.yaml` uses, and the binding decides how it is read. A slot takes the kind it is declared with; a `texture2d` slot also reads a storage texture, a side of a history pair or the scene depth, and a `storage2d` slot also writes this frame's side of a history pair: | binding's `bindings.yaml` kind | `resources` entry | `name` | |---|---|---| | `buffer` | `{ kind = "buffer", name = buf }` | a buffer handle from `ref:createBuffer` | | `texture3d` | `{ kind = "texture3d", name = vol }` | a 3D texture handle from `ref:createTexture3D` | | `storage3d` | `{ kind = "storage3d", name = vol }` | a 3D texture handle from `ref:createTexture3D` made with `storage = true` | | `storage2d` | `{ kind = "storage2d", name = target }` | a storage texture handle from `ref:createStorageTexture2D` | | `storage2d` | `{ kind = "history_curr", name = history }` | a history pair from `ref:createTextureHistory`: this frame's side | | `texture2d` | `{ kind = "texture2d", name = tex }` | a handle `ref:copyBufferToTexture` returns, or a texture asset's guid once `texRef:handle()` has made it GPU-resident | | `texture2d` | `{ kind = "storage2d", name = target }` | a storage texture handle, read as a texture | | `texture2d` | `{ kind = "history_prev", name = history }` / `{ kind = "history_curr", name = history }` | a history pair: the previous frame's side or this frame's | | `texture2d` | `{ kind = "scene_depth" }` | none: the frame's scene depth | | `sampler` | `{ kind = "sampler", name = smp }` | a sampler handle from `ref:createSampler` | `texture3d` and `storage3d` name the same resource, a 3D texture, so either one fills either slot. The engine's own spellings name the same resources and keep working: `texture_3d` for `texture3d` and `storage3d`, `texture_2d` for `texture2d`, and `storage_2d` for `storage2d`. Any other `kind` is refused at the call, naming every kind it accepts. A `vertices` binding is filled by `ref:dispatchOnVertices`, and `texture_depth`, `textureCubeArray` and `acceleration_structure` bindings by a render feature's pass. ```lua local march = asset.resolve("volume_march", "computeShader") -- bindings.yaml: density (storage3d), out_tex (storage2d), U (buffer) march:dispatchEx({ resources = { { kind = "storage3d", name = density }, { kind = "storage2d", name = target }, { kind = "buffer", name = uniforms }, }, workgroups = { 8, 8, 8 }, }) ``` `workgroups` is `{ x, y, z }`, `{ x }`, or a bare `x`, and every dimension it leaves out is 1. A zero in any dimension is refused: the dispatch is dropped and recorded as a failure, so it surfaces in `ref:status()` and `compute.failing()` with the reason. A count derived from how much data there is passes through `math.max` first, so an empty set dispatches one workgroup rather than none: ```lua carve:dispatch({ buffers = { heights }, workgroups = { math.max(1, math.ceil(#points / 64)) } }) ``` ### Buffers and readback (the CPU side) A buffer is created through the shader that owns it and comes back as a handle. Pass the handle to a dispatch — including another shader's — to bind it there: ```lua local double = asset.resolve("@builtin::shaders.compute_double", "computeShader") -- `type` is the element the shader declares, `len` how many of them. local data = double:createBuffer("data", { type = "f32", len = 4, usage = { "readback" } }) data:write({ 1.0, 2.0, 3.0, 4.0 }) -- :writeU32 for integer words, :writeBytes for packed bytes double:dispatch({ buffers = { data }, workgroups = { 1, 1, 1 } }) local pending = data:read() -- starts an async GPU->CPU readback -- a frame or two later, drain it (nil until it has arrived): local out = pending:result() -- :resultU32 / :resultBytes read the same bytes another way data:destroy() -- free it when done ``` `type` is one of `"f32"`, `"vec3"`, `"vec4"`, `"quat"`, `"mat4"`, and `usage` adds `"readback"`, `"vertex"`, `"index"` or `"indirect"` on top of the storage a buffer always has. An `"indirect"` buffer holds a draw's arguments where the GPU reads them, so a shader that compacts geometry writes the drawn count into the same dispatch that produced it and a render feature's `kind = "draw"` pass submits exactly that — the `renderFeature` asset type's README documents that pass and the `storage.indirect` name it reads. The handle's key is the shader's guid plus the name you gave it and a serial, so two shaders that both want a buffer called `data` get one each — and so do two callers asking this same shader for one. `ref:buffer(name)` returns the last one made under that name, which is how two callers share one on purpose. A buffer that no shader computes — a vertex or index run that is only ever drawn — is allocated straight from the primitive instead: `substrate.createBuffer({ name = ..., type = ..., len = ..., kind = "gpu", usage = { "vertex" } })`. The first dispatch of a shader compiles it, so the very first readback can still reflect the un-run shader; dispatch again once it is compiled, or `ref:compile()` to pre-warm. This create/write/dispatch/readback loop is the CPU side of the resource model in `core/resource-model`. ### Is it compiled? `ref:compile()` queues the compile; the pipeline is built on the render side frames later, so that call cannot say whether it produced a program. `compute.programState(ref)` is where that lands: ```lua if compute.programState(carve) == "ready" then carve:dispatch(opts) end local state, reason = compute.programState(carve) if state == "failed" then print(reason) end ``` - `"absent"` — the engine holds nothing under this key and nothing is in flight: never asked for, or released. - `"pending"` — a compile was asked for and has not reached the device. A recompile of a resident program reads pending too, because what it produces is a different program from the one bound now — which is what makes this the precondition to wait on after an edit. - `"ready"` — compiled and resident, so a dispatch naming it binds it. - `"failed"` — the most recent compile produced no program, and the second return says why. A dispatch issued while the program is `"pending"` is held behind the compile and dropped if it never lands, so a readback taken before `"ready"` reports the buffer as it was. Wait for `"ready"`, not for a count of frames. ### Did it run? A dispatch is recorded into a command encoder frames after the call that asked for it returned, so that call's `true` means "queued", not "ran". `ref:status()` is where the outcome lands — one record per target, cumulative: ```lua for _, d in ipairs(carve:status()) do print(d.target, d.ok, d.dispatches, d.failures, d.lastError) end ``` `target` is the mesh guid for a `dispatchOnVertices` and the buffers it bound for a dispatch that writes only those, so two systems driving one shader over their own buffers read as the two passes they are. `ok` is the most recent outcome; `lastError` says why the last failure failed and is kept after a recovery. An empty result means nothing has dispatched this shader at all. `compute.failing()` answers the same question across every shader at once. A failing dispatch also posts a notice, so a pass that stops running reports itself rather than waiting to be found. ### Running one every frame A `kind = "compute"` substrate job queues one dispatch per frame: ```lua jobs.register({ phase = "main", executor = { kind = "compute", shader = asset.resolve("carve", "computeShader"), buffers = { "heights" }, workgroups = { 64 }, }, }) ``` ## Registration & hot reload Writing `shader.wgsl` or `bindings.yaml` fires the `.computeShader` assetType's `onChange`, which calls `__compute.compile` — that generates the binding declarations, expands `#include`s, naga-validates, and registers. This happens on create and on every edit, so saving recompiles with no world reload. A shader with invalid WGSL (a type mismatch, an undefined name) **fails to compile** with the reason logged (`[compute] Compile of '<name>' failed: …`) and is **not** registered. You don't need to register a shader before dispatching it — the first dispatch auto-compiles it on use. `ref:compile()` remains as an optional explicit pre-warm, only to avoid the one-frame first-dispatch warm-up in a latency-critical spot. ## What compute is holding, and why a key names nothing Every buffer, volume, storage target, history pair and sampler a dispatch can bind is filed under a key, and `compute.observe()` lists all of them with what each costs: ```lua local r = compute.observe() print(r.totals.count, r.totals.bytes) for _, res in ipairs(r.resources) do print(res.key, res.kind, res.bytes, res.owner.name) end ``` A row carries the key verbatim — the same string a dispatch names it by and a dispatch-failure message prints — plus its `kind`, its `bytes`, its extent and `format` where it has them, the `usage` bits it was created with, and the frame it was created on. `owner` is read back out of the key (`<shader-guid>::<name>#<serial>`), so a resource nothing has ever dispatched over still names the shader that made it. `compute.resources(ref)` narrows to one shader's — including the `<shader-guid>::__params` uniform the compile created for a `params:` block, which the shader owns like any other resource. `r.totals.bytes` is the `compute` figure of `renderer.gpuMemory()` — the two are read off the same registries, so a listing accounts for the category exactly. The same reading is served at `/zero/runtime/compute`. When a key names nothing, `compute.diagnose(key)` says which of the states the inventory distinguishes it is in, from the closed set `compute.absentReasons()` enumerates: | reason | what the inventory read | |---|---| | `resident` | a resource is filed under exactly this key; the row is on `resource` | | `replaced` | the owner holds the same name at a different serial — `current` names the live key | | `nameUnused` | the owner holds resources, none under this name | | `ownerAbsent` | no live resource carries this key's owner | | `unowned` | the key carries no owner segment, and nothing is filed under it | | `notPublished` | no renderer has published an inventory, so no key can be answered for | So a key out of `[compute] dispatch '…' failed` resolves against what exists, and a typo reports which part of it the engine could not match rather than coming back empty. A reason is the inventory at the moment the call read it. The reading is republished on a frame where a registry gained or lost an entry, so a resource created and diagnosed inside one script sees the reading from before it was made; wait a frame and ask again. ## Discovery - `asset.list("computeShader")` — every registered compute shader. - `asset.inspect("<name>")` — status, source, parse errors, this README. - `computeRef:getBindings()` — the parsed `bindings.yaml` schema (`{ bindings, params }`). - `compute.programState(ref)` — whether the program is compiled, still compiling, or failed, and why. - `compute.observe()` — every GPU resource compute holds, and what it costs. - `compute.diagnose(key)` — whether a key names a live resource, and why not. - `profiler.gpuFrame()` — what each dispatch of this shader spends on the GPU. A dispatch is timed under `compute.<identity>` — the same identity `asset.resolve` takes, so a timing row names the shader that produced it. ## Common pitfalls - **Compute shaders don't render** — verify output via a buffer readback or a debug pass, not a screenshot. - **`ref:compile()` returns before the program exists** — the pipeline is built on the render side; `compute.programState(ref)` says when it is `"ready"`, and a dispatch issued before that is dropped. - **A dispatch call returning true means queued** — ask `ref:status()` whether it ran. - **A destroyed buffer is refused where it is bound** — a handle outlives its storage, and `buf:alive()` reports which. - **Binding order is the contract** — `buffers` and `resources` bind in `bindings:` list order; reordering the list reorders the bindings. - **Don't write `@group`/`@binding` in `shader.wgsl`** — the engine owns them; re-declaring is the fastest way to a layout mismatch. - **Don't infer a mesh's vertex stride** — declare `kind: vertices` and use the accessors. A wrong stride writes positions correctly and normals into the wrong word, which renders as flat lighting rather than as broken geometry. - **Storage-texture format must be storage-compatible** — `r32f` / `rgba8` / `rgba16f` / `rgba32f`; others are rejected at compile. - **A key that comes back empty is not the same as one that failed** — `compute.diagnose(key)` separates a name never used from a generation that was replaced, and names the live key in the second case. ## Related types - `.shader` — render-domain shaders (surface, sky, post-process, screen) and the legacy `@domain: compute` fallback. ## In the Inspector Selecting a compute shader opens its **Compute** section: whether its program is ready (and why not), how many dispatches it has run and failed in this session (and the last error), tables of the bindings and params its `bindings.yaml` declares, and openers for `shader.wgsl` and `bindings.yaml`.
▲ 0↑ born
backing path · systems/procgen.package

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

These counts cover only the parts this page shows. Problems on the parts it leaves out are not counted here.

1+problem

Inside

1 problem across 1 item
●
dep.unresolved · L90
could not resolve `@builtin::systems.procgen.abi_kernel`
⌬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.