← Discover MCPs and Agents
P
AgentAI & MLGitHub

Procedura

Agentic 3D Modeling with Procedural Control — turns a text prompt into an editable parametric program, with optional per-part materials and articulation.

Links

README

From the repo.

Procedura

Agentic 3D Modeling with Procedural Control

Project page   Paper (PDF)

Procedura turns a text prompt into an editable procedural assembly

Procedura turns a text prompt into an editable procedural assembly — a parametric program whose named parts are joined by typed mates, written with a frozen LLM and no 3D training. Left: the compiled geometry. Right: further objects coloured by their named modules, a part decomposition that comes free with the representation.

The output is not a point cloud or a soup of triangles. It is source code you can open, edit, and re-compile — optionally with per-part PBR materials (--paint) and articulation exported to OpenUSD/URDF (--motion).


Install

One command installs everything: Bun, the packages, a Manifold-capable OpenSCAD, Blender, and a starter .env.

git clone git@github.com:SpatiaOS/Procedura.git && cd Procedura
bash scripts/install-deps.sh

It is safe to re-run — anything already working is left alone — so it doubles as a "what am I missing?" check:

bash scripts/install-deps.sh --check        # report only, install nothing
bash scripts/install-deps.sh --no-blender   # skip the ~350 MB download
PREFIX=/opt bash scripts/install-deps.sh    # install binaries somewhere else
whatwhere it goeswhy
Bun ≥ 1.3~/.bunthe whole pipeline is one TypeScript program
OpenSCAD (newest snapshot)~/opt/openscadcompiles the generated program to a mesh
Blender (newest release)~/opt/blender/the AO / parts-colour / PBR renders the critic looks at
Packagesnode_modules/, web/node_modules/pipeline + Studio dependencies
.envrepo rootcopied from .env.example; add your endpoint and key

Two things it deliberately does not do. It never installs NVIDIA Isaac Sim (multi-GB and account-gated) — that is only needed for --motion physics validation, and everything else runs without it. And it never touches an existing .env.

OpenSCAD must have the Manifold backend. A pre-Manifold build does not fail — it falls back to CGAL and gets orders of magnitude slower. One hull() measured 1774s under CGAL against 1.77s under Manifold, which turns a working run into one that looks hung for hours. The installer checks for --backend rather than merely "is openscad installed", because the version most distributions package is 2021.01 and predates it. Procedura makes the same check at startup and refuses to run without it unless you set PROCEDURA_ALLOW_CGAL_OPENSCAD=1.

Configuration

Procedura ships no keys and no hosted backend. It talks to whatever endpoint you configure, through two variables:

OPENAI_API_KEY=sk-...
OPENAI_BASE_URL=https://api.openai.com/v1

Anything that speaks POST {base}/chat/completions with SSE works here — the OpenAI API, OpenRouter, a corporate gateway, or a local vLLM / Ollama / LM Studio server. For Gemini on its native GenAI path (where thinkingConfig and first-class thought parts are available), set GEMINI_API_KEY / GEMINI_BASE_URL instead and use a gemini: model key.

Model selection is just a string. Any id your endpoint serves works, whether or not this repo has heard of it:

PROCEDURA_MODEL=gpt-5.2                      # via the openai provider
PROCEDURA_MODEL=gemini:gemini-3-pro-preview  # force the native Gemini transport

src/config/models.ts holds a small catalog of known-good presets and explains the resolution order. .env.example documents every other knob — reasoning effort, binary paths, timeouts, retry budgets.

Reference images are an input, not an assumption. Procedura will not call an image API on your behalf: with no --image and no image model configured, a run goes text-only rather than quietly spending on an endpoint you never named. Supplying a reference is the single largest fidelity gain available, so pass --image <path> when you have one — or name an image model and let the run render its own:

PROCEDURA_IMAGE_MODEL=gpt-image-1    # unset = generation off; --image-model for one run

It reuses OPENAI_BASE_URL / OPENAI_API_KEY, so nothing else needs setting.


Usage

The default run

Nothing to configure past the endpoint. Plans the parts, builds them one at a time, refines the whole shape — from the prompt alone:

bun run scripts/procedura.ts -o outputs/daybed \
  --prompt "a brutalist brass daybed with tapered legs"

That is text-only, part-by-part, with no image API and no Blender in the draft loop, and it is what you get with no flags at all. It is the cheapest useful configuration, not the strongest one.

The best-quality run

Everything on. This is what the paper's results use, and every switch below buys accuracy at the cost of time or money:

# big plans mean long single calls; the default 600s cap is not enough
export PROCEDURA_LLM_TIMEOUT_MS=1800000
export PROCEDURA_LLM_DEADLINE_MS=1800000

bun run scripts/procedura.ts -o outputs/gripper \
  --image gripper_ref.png \
  --3d-feedback \
  --assembly \
  --paint \
  --motion --motion-urdf \
  --max-steps 12 \
  --prompt "a two-finger parallel robot gripper"
switchwhat it buys
--image ref.pnga reference to reconstruct — the single largest fidelity gain. Set PROCEDURA_IMAGE_MODEL instead to have the run render its own
--3d-feedbackthe generator sees the build-so-far before each part instead of reading about it. One Blender pass per part
--assemblyparts join through real mating features — shared-nominal pegs/sockets, bolt patterns, snaps — rather than bare overlap
--paintper-part PBR materials assigned by a vision call
--motion --motion-urdfarticulation planned, exported to OpenUSD + URDF, and validated headlessly in Isaac
--max-steps 12a longer refine budget; each cycle is render → critic → patch → gate, at 2 LLM calls

Raise the timeouts rather than capping the part count — a long plan is the pipeline working, and PROCEDURA_MAX_PARTS degrades the model to buy back time.

In between

# reconstruct a reference image you already have — the biggest single win
bun run scripts/procedura.ts -o outputs/daybed --image ref.png --prompt "..."

# generate the reference too (needs PROCEDURA_IMAGE_MODEL, or pass --image-model)
bun run scripts/procedura.ts -o outputs/daybed --image-model gpt-image-1 --prompt "..."

# long prompts, without fighting your shell
bun run scripts/procedura.ts -o outputs/daybed --prompt-file my_prompt.txt

# resume the refine loop on an existing run, or force a re-draft
bun run scripts/procedura.ts -o outputs/daybed --max-steps 12
bun run scripts/procedura.ts -o outputs/daybed --prompt "..." --redo

# the old monolithic draft: one call, no plan stage. Needs a reference
bun run scripts/procedura.ts -o outputs/daybed --one-shot --image ref.png --prompt "..."

Over a dataset

The paper's batches run the shape and articulation together, then paint each finished case as a separate pass — so a paint failure never costs the geometry:

bun run scripts/batch.ts \
  --prompts cases.jsonl --images \
  --incremental \
  --motion --validate --urdf \
  --max-steps 3 \
  --parallel 2 \
  --out-root outputs/batch_v1

bun run scripts/paint.ts outputs/batch_v1/<case_id>

Keep --parallel at ~2 with --motion --validate: concurrent headless Isaac instances exhaust GPU VRAM. Refine-only batches tolerate 8-12; each refine cycle spawns Blender, so running every case at once will exhaust memory long before it exhausts your rate limit.

Flags

flagmeaning
-o, --output DIRoutput directory (required)
--prompt TEXT / --prompt-file PATHthe text prompt
--image PATHreconstruct this reference image
--no-imagetext-only — the default when no reference is available
--image-model Mgenerate the reference with this model, for this run only
--3d-feedbackshow the generator a render of the build-so-far before each part (off by default)
--one-shotopt out of the part-by-part draft: one call, no plan stage
--assemblyinline the mating-feature library so parts join through real interfaces
--paintmaterial pass: a vision call assigns each part a PBR material
--motionOpenUSD/Isaac articulation export
--motion-urdfalso emit URDF + per-link meshes
--max-steps Nrefine-loop budget in edit cycles (default 6; 2 LLM calls each)
--no-refinedraft only — promote the draft to final.*
--redoforce a re-draft even if artifacts exist
--agent-model / --scad-model / --paint-model / --motion-modelper-stage model override

bun run scripts/procedura.ts --help lists all of them, including the ablation switches (--no-plan, --motion-aware, --motion-only) used in the paper.

What a run writes

image.png                       the reference (generated, or the one you passed)
plan.json  _parts/NN_<name>/    --incremental: the part list + per-part artifacts
draft.scad  draft.obj           the drafted program and its mesh
final.scad  final.obj           after refine — the deliverable
final_summary.txt               finish verdict + shipped connectivity
preview_final/                  Blender AO render of the final mesh
final_materials.json            --paint: part → colour / metalness / roughness
final_painted.scad/.obj/.mtl    --paint, plus preview_painted/ (PBR Cycles render)
motion/final_motion.usda        --motion, plus motion/urdf/ and motion/links/
_trajectory/procedura-<id>.jsonl   every prompt, response, tool call and gate decision
_refine_steps/step_NNN/         one dir per refine cycle (render, diagnosis, edit, compile)

Every OBJ Procedura writes is normalized to a unit bounding box; STL is opt-in (--export-stl).

Re-running a stage

paint.ts and motion.ts both take a finished run directory, so either stage can be re-run — with a different model, or after a failure — without touching the geometry:

bun run scripts/paint.ts  <runDir> [--model KEY] [--refine-steps N]
bun run scripts/motion.ts <runDir> [--model KEY] [--urdf] [--no-validate]

To compare runs across configurations — renders, source, metrics and trajectory, side by side per case:

bun run scripts/results-server.ts --root a=outputs/run_a --root b=outputs/run_b

Studio

A web front-end for the same pipeline: compose a run (prompt, optional reference image, preset or per-stage toggles), watch it build part by part, and inspect every intermediate artifact of any run — plan, parts, refine loop, final mesh in 3D, materials, articulation.

cd web && bun install && bun run start      # http://localhost:8080

It spawns the CLI from the repo root and reads the same .env, so there is nothing extra to configure. See web/README.md.


Acknowledgements

The vendored LLM harness in vendor/harness/ is an independent implementation of architectural patterns from two MIT-licensed projects — opencode (the four-axis Protocol / Endpoint / Auth / Framing route model, the permission evaluator, the cache-breakpoint policy) and openclaw (the sandbox bridge, the context engine, trajectory events). Credit for those designs belongs to them.

License

MIT. See LICENSE.

Procedura invokes OpenSCAD and Blender as external programs; both are GPL and neither is redistributed here. The four Blender scripts under scripts/ use Blender's bpy API and are MIT-licensed, which is GPL-compatible. No third-party assets ship in this repo — the PBR studio environment is read from your own Blender installation at render time.

Collected info

  • 238 stars
  • 17 forks
  • Language: TypeScript
  • Source updated: 9/19/2026