Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commitdd348ac6c3

README.md

procNode: one reusable step of procedural generation

A .procNode is a node authored as content: a folder whose init.luau is the node. It returns proc.define(function(g) ... end), the same function-of-g form a .procGraph is written in, so a node is a small graph that other graphs call. Its g:describe text says what it makes and when to use it, its g:input declarations are what a call passes, and what it returns (or declares with g:output) is what the call gives back.

MyNode.procNode/
  init.luau     -- the node
  README.md     -- what it produces, its inputs, its outputs
  .metadata     -- description and tags

What a node states

  • g:describe(text): what the node produces and when to use it. It is the text search, the node catalogue and the Inspector show.
  • g:input(name, default, opts): one input of the node. The default types it (Float, String, Bool, Vec3, Color, List<T>, Record); opts adds type (for a default that does not state one, or none), description, options, min, max and optional. A call names inputs in a table, or passes them in declaration order.
  • Outputs: the returned table names them (return { geometry = geometry }), and g:output(name, value) declares one as the node builds. A node with one output answers that value to its caller; a node with several answers a record by output name; a node with none answers nothing.
  • Written as literals. What a node takes and gives is read from its source without running it, the way search and description read it here and the way ZeroMind reads it, so each declaration states its values as written: g:describe and each g:input once, at the top level of the function, with a literal name, default and options (a file constant holding a literal, or asset.resolve("<identity>", "<type>"), reads as one); outputs named by the table the function returns, or by g:output, the same names on every branch of a condition and never inside a loop. asset.interface(node) answers what the source declares, with each part it cannot read as a problem, and a push refuses a node with such a problem (interface.unreadable).
  • The body is the node's work: calls to other nodes, arithmetic over sockets, for, folds, steps and state, as the builder README (proc.module/builder.module/README.md) describes. A node that uses another node is recorded in the graph that calls it, so a node can be built from nodes.

How a graph calls a node

A node inside a <family>.nodebox is called by its box and folder name: points.nodebox/disc.procNode is g.points.disc({ ... }), and nothing inside the node states that name. A node is also reached by:

  • its path (g["/zero/source/fx/ring.procNode"]),
  • ~, the nearest enclosing package, effect or world root (g["~/ring"]),
  • its guid,
  • a short name only one node answers to (g.disc).

A name that matches two nodes and a name that matches none are errors that name the candidates. Each call is recorded in the calling graph by the node's guid, so the node travels with every graph that uses it, and a rename or a move keeps the reference. A .procGraph is called the same way and answers what it returns.

Nodeboxes

A nodebox is the family a node lives in: a namespace folder grouping related nodes (points, mesh, field, terrain). nodebox.assetType describes it. A node placed in a box takes the box's name as its prefix; a node outside any box is reached by path, ~ or guid. asset.create("nodebox", family) makes a box and asset.create("procNode", name, { into = box }) makes a node in it.

Finding nodes

asset.list("procNode") lists every node. The procgen toolbox's searchNodes finds them by meaning across every published node, and with no query lists the ones this engine holds grouped by family; its describeNode answers one node's inputs, their types and defaults, and its outputs. Both read what each node's source declares and run no node. The type's indexing recipe embeds a node's README and its source, so a search by meaning finds a node by what its description and README say it is for: write them in the words someone looking for the node would use.

Registration

Every node the asset index lists is known to the procgen registry by what its source declares, read when it is asked for and read again when the source changes; nothing runs a node to list it. Its code loads the first time a build runs it. After that, the lifecycle hooks keep it current: onChange loads it again and hot-reloads on an edit, onDelete removes it. Editing init.luau cooks again every graph that reaches the node, and only the nodes downstream of the change do work. init.luau is compiled from its current source on each load, so a module it pulls in uses an absolute require (require("@builtin::systems.procgen.proc")).

What a node is built from is more than its own file. A module its init.luau requires while it builds, at any depth of requires, is part of it: when that module changes, onDependencyChange loads the node again from the module as it now stands. A file its init.luau reads with vfs.read while it builds is part of it too, and so is a file a module it calls reads with proc.read (a module's own vfs.read is its own and makes no dependency). Writing such a file loads the node again (proc.sourceChanged). Either way the node's definition moves, so every graph and Generator reaching it cooks again, from one version of everything it was built from. proc.builder.readsOf("<node>.procNode/init.luau") lists the files its last build read. A graph that references a node which is not registered errors at load, naming the missing guid. A graph that reaches itself, directly or through other nodes, errors at cook.

Content requirements

README.md carries real text: what the node produces, its inputs and its outputs. .metadata carries a description (what the node computes and the value type it answers) and tags (the family, so search finds it). It may state where the node works: context, pulls and pushes (the values proc.registry.contexts(), "scene" and "asset" name). A node in a nodebox that states none takes its box's; procgen describeNode and searchNodes answer them.

A node written with proc.graph and g:node (the procNodes/ of the procgen package) is called only by a proc.graph graph, through g:node. A graph built with proc.define does not call it: g.op.<name> of it is refused where it is written, naming the node written in the call form that answers to the same name.

In the Inspector

Operation: the node's id and what it does, its cost and caching when it declares them, and its inputs and outputs, from the procgen registry's schema for it. A node that did not register says so.