Skip to content

saofund/mirage

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

112 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mirage

An AI-native 3D renderer + lightweight physics simulator — built to be driven by coding agents (e.g. Claude Code via MCP), aimed at robotics and synthetic-data use cases.

Status: 🌿 the native core has landed. One legible op-log is the source of truth; a first-party C++20 core builds it (byte-identical Python + C++ mesh kernels), a from-scratch path tracer (mirage_render) shoots the ground-truth stills, and a native GL viewport (mirage_viewer) is the realtime preview — no external DCC. Multi-object scenes + physics ride OpenUSD + MuJoCo behind small interfaces. Full design & roadmap: docs/design.md.

Gallery

Every image below is one op-log replayed through the native mesh kernel and shot with the in-repo path tracer — no external DCC, no fakes.

Beyond primitives — a passenger jet modeled entirely from the engine's own operators: a surface-of-revolution fuselage (the lathe), lofted swept wings with winglets, capped-cylinder engines on pylons, all mirrored for symmetry and given a per-face livery, then path-traced. Reproduce with uv run python examples/airplane.py.

Airliner modeled in Mirage

That op-log isn't a static export — it's a sequence of operations you can replay. Here is that jet being modelled in Mirage's own viewport, assembling operator by operator — fuselage (a lathe), wings and tailplane (lofted, then mirrored), the fin, and the podded engines — then a gentle turn to show it off. Every frame is a headless screenshot of the real native GUI (mirage_viewer) fed a growing op-log, so this is the tool building the model, not a mock-up. The recorder is a reusable module — mirage.capture.record_build(stages, …) films any op-log the same way, so every polished case can be captured — regenerate this one (.mp4 for video, .gif for inline) with uv run python docs/gallery/render_viewer_build.py.

Modelling the airliner in Mirage's viewport, operator by operator

A whole interior — every object native, and the engine composes it. A furnished room where each thing is modelled from Mirage's own operators (the lathe turns the vase and lampshade, bevel rounds the sofa, array stacks the shelves, boolean cuts the window), assembled by the first-class place operator: the scene is a legible op-log of place ops, each carrying its object's operators and a transform, so the op-log stays multi-object and human/AI-editable — not baked geometry, not Python glue. That op-log builds byte-identically in the Python kernel and the C++ core, and the path tracer shoots it from a camera inside the room. Reproduce with uv run python examples/cases/18_interior_scene.py --hero.

A whole interior, every object native-modelled, engine-composed

Here is that room being built in Mirage's own viewport, in the editor's AUTO mode — when the AI is driving the op-log the tool panel steps aside for a top-left status HUD (what's being modelled right now), so the frame is all model: the lathe sweeping the vase, boolean punching the window, bevel rounding the armchair, then each object placed — and it settles onto a path-traced close-up of the finished scene (the real-time viewport for the build, the first-party path tracer for the money shot, both off one op-log). Regenerate with uv run python examples/cases/18_interior_scene.py --film (add ANIM_RAYTRACE=1 for a fully path-traced promo pass).

Building the interior in Mirage's viewport, object by object

And the same build rendered entirely by the first-party path tracer — every frame global illumination, soft shadows, sky+sun — a promo pass (ANIM_RAYTRACE=1 … --film), kept clean at low sample counts by the tracer's own edge-avoiding à-trous denoiser (--denoise):

The interior making-of, every frame path-traced and denoised

(How large scenes scale, and where the layers used to bottleneck, is measured in docs/scene-scaling.md — the composition seam that once forced a manual merge is now closed by the place op.)

Parametric — the op-log is a re-runnable generator

The model isn't a bag of geometry to poke at; it's a legible program. Give the op-log a params block, arithmetic expressions in any numeric field, and a repeat loop, and the whole form regenerates when you change one number — floors stacks storeys, twist spirals them, taper pinches the silhouette. Five legible ops resolve to ~100. This is the thing a puppet-an-app MCP can't do — and it's byte-identical in the C++ core and the Python kernel (differential-tested), so a parametric op-log path-traces and loads in the GUI natively.

Sweep two parameters over that one program and you get a design space — 16 towers, each path-traced and denoised (examples/cases/19_parametric_tower.py --grid):

A parametric design space — 16 towers from one op-log

…or animate a parameter and the structure morphs, every frame path-traced (--morph):

A parametric tower morphing as its parameters sweep

And it scales all the way up: the same machinery — params, expressions, nested repeat — builds a whole classical temple (a stepped stylobate, a peristyle of columns on all four sides, an entablature, a gabled roof with pediments) from 16 legible ops (examples/cases/22_parametric_temple.py). Path-traced under a low, art-directed sun (--sun-dir) so the colonnade rakes long shadows across the stone:

A parametric temple, path-traced under a raking sun

the same temple — a cinematic path-traced 360° orbit

How big can it get? Two ceilings, far apart. Composing separate objects through the legible place op is O(N²) — a wall around 1–2k objects. But a single mesh op sidesteps that, and the tracer's BVH eats millions of triangles: this displaced-noise mountain range is 1.28M triangles in one op, path-traced at 220 spp (measured on a 152-core box, 7.2M tris render in ~42 s / ~8 GB). Numbers in docs/scene-scaling.md.

A 1.28-million-triangle terrain, one mesh op, path-traced

And not everything is generated. A large interior can be composed by hand — ~160 objects, each modelled from operators (the lathe turns the vases and lamp bases, bevel softens every cushion, boolean cuts the window) and placed into a considered layout — then lit and surfaced for mood. Materials carry an emission, so the table and floor lamps are real area lights the tracer samples; they also carry real PBR texture maps — an albedo, a roughness, and a normal map each — sampled triplanar (projected off the world position, no UVs, no seams), so the floor reads as planked hardwood with grain that catches the light in relief, the console as figured walnut, and the upholstery as woven fabric. The starter map library is generated procedurally (mirage.textures), but any CC0 PBR set drops in as three image files. Shot at golden hour under a low, art-directed sun that rakes the window-mullion shadows across the boards, with a thin-lens depth of field and a bloom glow so the window and lamps read as real light sources rather than flat white (examples/cases/24_grand_interior.py):

A hand-composed living room at golden hour — textured surfaces, emissive lamps, bloom and depth of field

And here is that room being built in Mirage's own viewport, in the editor's AUTO mode — a moving camera through the open corner while the op-log assembles group by group: the boolean punches the window, the lathe turns the vase, the armchair is blocked out sharp and then bevel rounds it, and each piece is placed into the layout, the HUD naming the operator at work. It settles onto the path-traced golden-hour close-up (the real-time viewport for the build, the first-party tracer + denoiser for the money shot, both off one op-log). Regenerate with uv run python examples/cases/24_grand_interior.py --film (add ANIM_RAYTRACE=1 for a fully path-traced pass).

Building the hand-composed interior in Mirage's viewport, group by group

And the same build rendered entirely by the path tracer — every frame global illumination, the warm sun streaming through the window and raking the mullion shadows across the grain, the lamps glowing as real area lights — kept clean at low sample counts by the à-trous denoiser (a fully path-traced promo pass, ANIM_RAYTRACE=1 … --film, rendered headless on a 152-core box):

The hand-composed interior making-of, every frame path-traced and denoised

Sculpting — a cage, creases, and a limit surface

An icon, read off photographs and published dimensions and pushed into shape by hand: the Eames Lounge Chair (670 & 671), 130k faces, nothing imported or scanned (examples/cases/25_eames_lounge.py). It measures within an inch of the real thing on every published dimension — 31.9″ tall against a 31.5″ spec, a 17.1″ ottoman against 17.25″, 26″ wide against 26″ — because the model is checked against the numbers rather than eyeballed against the photos.

The Eames Lounge Chair and Ottoman, sculpted from a control cage and subdivided

The op-log has no vertex-addressing grammar — there is no "move vertex 47", by design. So the control cage is sculpted entirely through re-evaluable queries. Nested box selections are strict subsets, so a chain of band translates accumulates into a discrete integral, and choosing each step as a finite difference telescopes the cage exactly onto any curve. In two dimensions, quadrant boxes give a cumulative sum that the second difference inverts — so any z(x,y) you can write down lands on the cage, and the shape stays a function you can read. Three curved plywood shells, three buttoned cushions (the tufting is real dimpled geometry), a five-star base of tapered blades, the ottoman.

Two engine features carry it. Semi-sharp creases (crease{on, weight}, the DeRose/Kass/Truong scheme Pixar shipped) hold the plywood rims while subdivide takes each shell to its limit surface — without them Catmull-Clark rounds every rim into a pillow. A crease is measured in levels and decays one per subdivision, so a cube creased at weight 3 and subdivided 3× comes back exactly a cube, while a fractional 0.35 on the cushions is the difference between a stitched leather welt and a foam block.

And smooth shading, which is what stops 130k faces from reading as facets. Each face corner takes the area-weighted average of the faces meeting there whose normals lie within --smooth-angle (default 30°) of its own; anything sharper is excluded, so the same vertex shades smooth on one face and hard on the next. Nothing is authored — curvature is inferred from the geometry. A cylinder's side goes round while its cap rim stays a crisp line, and a cube is unchanged to the pixel:

Flat versus smooth shading on a sphere, cylinder and cube

And here is the chair being sculpted in Mirage's own viewport, in the editor's AUTO mode — a slow dolly around the front quarter while the HUD names each operator as it lands. A flat 10×10 grid bends under a chain of translate{on: box} queries, mirrors to exact symmetry, gains its 14 mm of ply — and only then does subdivide take it to the limit surface, the crease holding the rim crisp while everything else goes round. The rest is assembly. It settles onto the path-traced beauty frame (real-time viewport for the build, the first-party tracer + denoiser for the money shot, both off one op-log). Regenerate with uv run python examples/cases/25_eames_lounge.py --film.

Sculpting the Eames chair in Mirage's viewport: a flat grid bends, mirrors, thickens and subdivides

Diff & merge — the model is version-controllable

Because the op-log is legible, two versions can be diffed and 3-way merged like source code — a human at the GUI and an AI over MCP editing the same model on separate branches, then reconciling. Disjoint edits to different objects merge automatically; a spot both changed differently surfaces as a conflict (never silently lost). No opaque scene file can do this.

Below: one base scene, a human branch (recolour the vase, move the bowl) and an AI branch (repaint the floor, add a book) — merge_by_key combines all four edits with zero conflicts, and the render proves every one landed (examples/cases/20_diff_merge.py):

base, a human branch, an AI branch, and their automatic 3-way merge

the same, as a clip: base to human to AI to merged

Self-refinement — the agent sees its own model and fixes it

A puppet-an-app MCP is blind: it fires commands and can't tell what came out. Mirage's agent can read the op-log, render it (first-party tracer + denoiser), look at the frame, and edit the op-log to fix what it sees — a closed perception→action loop on its own creation. Starting from a scene with deliberate, render-only flaws — a floating vase, a book clipping it, a muddy bowl, a blown-out frame — it converges round by round, each edit derived from the previous render (examples/cases/21_self_refine.py):

round 0 flawed, round 1 grounded, round 2 composed — a self-refinement loop

the same refinement, as a clip

The core operators, one panel each (regenerate with uv run python docs/gallery/render_gallery.py):

Mirage modeling gallery

operator what it is
1 screw the helical sweep — a section revolved while climbing the axis → springs, threads, augers
2 curvature selector selection-as-query by mean dihedral: the flat-ish cap resolves apart from the round body
3 profile a first-class 2D generatrix — an open wire revolved makes a single-walled, hollow vase
4 boolean real BSP mesh-mesh CSG (union / difference / intersection) — here a cube minus a cylinder bore

Each modeling operator is implemented byte-identically in the C++ core and the Python kernel and pinned by differential tests, so one op-log builds the same mesh in either engine.

Why

Powerful DCC tools (Blender, …) have large, stateful automation surfaces that are awkward for programmatic/agent control. Full robotics simulators are excellent but heavy. Mirage takes the opposite bet:

  • Scene = plain data. The whole world is one serializable object (JSON today, USD later). An agent can read it, diff it, edit it, and reproduce it deterministically.
  • Tiny, swappable backends. A backend just consumes a Scene: render(scene, camera) or step(scene, dt) — MuJoCo behind both, permissively licensed. (Photoreal stills of a model take a different path: the op-log goes straight to Mirage's own mirage_render path tracer.)
  • AI-native control surface. A first-class MCP server exposes the build/step/render loop as a handful of orthogonal tools, so Claude Code can drive Mirage out of the box.
  • Light, fast, permissive. Python conducts; the heavy lifting is native — Mirage's own C++ mesh kernel and mirage_render path tracer, plus OpenUSD and MuJoCo behind small interfaces. Apache-2.0, no GPL entanglement.

Quickstart

git clone https://github.com/saofund/mirage
cd mirage
pip install -e .
python examples/falling_box.py

Use with Claude Code (and Codex, and any MCP client)

This repo ships a project-scoped MCP config (.mcp.json), so Claude Code picks Mirage up automatically when you open this folder as the workspace:

pip install -e ".[usd,mujoco,mcp,demos]"   # full surface: USD scene + MuJoCo physics/render + MCP
cd mirage                 # the project root, where .mcp.json lives
claude                    # approve the 'mirage' MCP server when prompted

Then /mcp shows mirage connected. The agent can author (add_box, add_sphere, add_cylinder, add_plane, add_camera, add_light), edit (move, set_transform, set_material, set_velocity, remove, rename), inspect & reproduce (get, list_objects, get_scene, set_scene, diff_scene, save_scene, load_scene, get_log, replay_log), and simulate & see (step, render). Every tool returns structured JSON; render returns a PNG the agent can look at.

A portable skill ships with Mirage. skills/mirage/SKILL.md (a Claude Code skill) and AGENTS.md (which OpenAI Codex reads natively) teach any agent to set up, connect, and drive the engine — model authoring, scene composition, rendering, and the performance rules — so a coding agent is productive in one read.

Run the server standalone (for any other MCP client):

python -m mirage.mcp_server

Architecture

See docs/design.md for the v0.1 design & roadmap (and docs/architecture.md for the current scaffold). In one diagram:

          agent (Claude Code)
                │  MCP tools
                ▼
          ┌───────────┐    reads / writes    ┌──────────┐
          │  Engine   │◀───────────────────▶ │  Scene   │   (JSON / USD)
          └───────────┘                       └──────────┘
            │       │
     step() │       │ render()
            ▼       ▼
     PhysicsBackend   RenderBackend
       (MuJoCo)       (MuJoCo raster · mirage_render path tracer)

License

Apache-2.0.

About

An AI-native 3D renderer + lightweight physics simulator, driven by coding agents (MCP), for robotics and synthetic-data generation.

Topics

Resources

License

Stars

2 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors