Log inGet started

procPack — a family of procedural ops as content

Updated October 10, 2026

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

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
contributesone opa family
addressed bythe asset's name, or its ref from asset.resolve(id, "procNode"), which the graph records by guidthe string id the pack names it with
a graph writesg: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 required, 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.

  • asset-type
  • reference