Metadata-Version: 2.4
Name: scenic-bpy
Version: 1.6.0
Summary: A FastAPI-style framework for building Blender scenes from typed, composable components.
Project-URL: Repository, https://github.com/satyam-fp/scenic-bpy
Author: Satyam Kumar
License-Expression: MIT
License-File: LICENSE
Keywords: 3d,blender,bpy,procedural,scene-generation
Requires-Python: >=3.11
Requires-Dist: pydantic>=2.5
Provides-Extra: blender
Requires-Dist: bpy>=4.2; extra == 'blender'
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == 'dev'
Provides-Extra: serve
Requires-Dist: fastapi>=0.110; extra == 'serve'
Requires-Dist: uvicorn>=0.29; extra == 'serve'
Description-Content-Type: text/markdown

# scenic

A FastAPI-style framework for building Blender scenes with `bpy`: declare typed, composable
components; scenic compiles them into a `.blend`. No UI, no clicks — just Python.

![lighthouse at dusk](docs/images/lighthouse.jpg)

*The scene above is [`examples/lighthouse.py`](examples/lighthouse.py) — a raw-mesh island,
component-composed tower, driver-rotated beams, geonode-scattered rocks, gradient-sky
backdrop, and an NLA-looped buoy. One spec file, rendered headless.*

![village at golden hour](docs/images/village.jpg)

*A full game environment: [`examples/village/`](examples/village) is a reusable asset
`Library` (seeded parametric houses, windmill, well, stalls, trees, fences, torches) composed
onto a terrain heightfield with geonode scattering — 278 objects sharing 53 meshes and
22 materials, and `scenic build -o village.glb` exports the whole set engine-ready at ~300 KB.*

![city aerial](docs/images/city_aerial.jpg)

*The stress test: [`examples/city/`](examples/city) — 2,985 objects across 8 structured
files. 46 cars and 36 pedestrians run on modulo drivers, traffic signals cycle, an
elevated train loops, a physics truck scatters crates at the construction site, and
every window is a shader. Compiles in **0.49 s**; a no-op rebuild reconciles in 0.13 s;
the whole city shares 24 meshes.*

| Procedural rust (shader DSL) | Geometry-node scatter |
|---|---|
| ![rust crates](docs/images/rust_crates.jpg) | ![meadow](docs/images/meadow.jpg) |

| Driver-powered orbit | IK arm + auto-skinning |
|---|---|
| ![orbit](docs/images/orbit.jpg) | ![arm wave](docs/images/arm.gif) |

Every image above is the output of a spec file in [`examples/`](examples), rendered headless
with `scenic render` — no Blender UI was opened.

```python
from scenic import Scenic, Depends, Field
from scenic.geometry import Cube, Bevel
from scenic.shading import PrincipledBSDF
from scenic.staging import Scene, Camera, Sun

app = Scenic()

@app.material()
def steel(roughness: float = Field(0.3, ge=0, le=1)):
    return PrincipledBSDF(metallic=1.0, roughness=roughness)

@app.asset()
def crate(size: float = 1.0, mat=Depends(steel)):
    return Cube(size=size).modify(Bevel(width=0.03, segments=3)).shade(mat)

@app.scene()
def main(box=Depends(crate)):
    return Scene(
        objects=[box],
        camera=Camera(location=(4, -4, 3), look_at=(0, 0, 0)),
        lights=[Sun(energy=3, rotation=(50, 0, 30))],
    )
```

```bash
scenic build  scenes.py -o out/main.blend          # compile to a .blend
scenic render scenes.py -o out/main.png -p size=2  # render a still, override params
scenic docs   scenes.py --json                     # component catalog as JSON Schemas
```

## How it works

Builders never call bpy. They return **specs** — immutable Pydantic models. The compiler
(`scenic.compile`, the only module that imports bpy) realizes specs into datablocks. That split
means params are validated before anything mutates a scene, tests run without Blender, scenes
serialize to JSON, and equal specs dedup to shared datablocks (two crates with the same `steel`
share one material).

See [DESIGN.md](DESIGN.md) for the full architecture and roadmap.

## Install

```bash
pip install -e ".[dev]"        # framework + tests (no Blender needed)
pip install -e ".[blender]"    # + the bpy wheel for headless build/render
```

Requires Python 3.11+. Runs headless via the `bpy` wheel or inside Blender 4.2+.

## Shader node DSL

Materials are node graphs written as expressions — sockets pipe with `>>`, scalar arithmetic
becomes Math nodes, and the whole graph is still an immutable, JSON-serializable spec:

```python
from scenic.shading import Bump, ColorRamp, Mix, NoiseTexture, PrincipledBSDF

@app.material()
def rust_metal(rust: float = Field(0.5, ge=0, le=1)):
    steel = PrincipledBSDF(base_color=(0.55, 0.56, 0.58), metallic=1.0, roughness=0.35)
    corrosion = PrincipledBSDF(
        base_color=NoiseTexture(scale=24).fac >> ColorRamp(stops=((0.3, (0.22, 0.06, 0.02)), (0.7, (0.45, 0.18, 0.06)))),
        roughness=0.85,
        normal=NoiseTexture(scale=30, detail=8).fac >> Bump(strength=0.4),
    )
    mask = NoiseTexture(scale=6, detail=6).fac >> ColorRamp(stops=((0.35, "black"), (0.65, "white")))
    return Mix(fac=mask * rust, a=steel, b=corrosion)
```

## Animation

Keyframes are authored on a `Timeline` and attached to objects with `.animate()`.
`.ease()` shapes the motion *into* the keys you just set — what animators mean:

```python
from scenic.animation import Timeline

@app.animation()
def drop_and_squash(height: float = 4.0, land: int = 18):
    t = Timeline()
    t.at(1).loc(z=height)
    t.at(land).loc(z=0.5).ease("BOUNCE", "EASE_OUT")   # arrive with a bounce
    t.at(land + 3).scale(1.2, 1.2, 0.6)                # squash
    t.at(land + 9).scale(1, 1, 1).ease("BACK", "EASE_OUT")
    return t

ball = Sphere(radius=0.5).animate(drop_and_squash(height=6))
```

```bash
scenic render examples/bounce.py -o out/bounce.mp4    # full frame range
```

Rotations are authored in degrees. The compiler targets Blender 5.0's slotted
actions with a 4.2 legacy fallback. The pip `bpy` wheel can't encode video, so
`.mp4` output renders PNG frames and assembles them with system ffmpeg.

## Live reload

```bash
scenic watch scenes.py -o out/main.blend    # rebuilds on every save
```

Builds are **incremental**: every managed datablock carries a content-hash key, and
rebuilds only create/remove what changed (a no-op rebuild of a 30-object scene is ~4 ms;
identical geometry and materials are shared datablocks).

For a live viewport inside Blender, install `scenic/addon.py` (requires
`pip install scenic-bpy` into Blender's Python): a **Scenic** tab appears in the
3D-viewport sidebar with *Build Once* and *Watch* — only the managed "Scenic"
collection is ever touched.

## Libraries

`Library` is the `APIRouter` analog — a mountable pack of components, distributable as a
pip package:

```python
from scenic import Library

parts = Library("scifi")

@parts.asset()
def corridor(...): ...

app.include(parts)          # available as "scifi.corridor"
```

## Drivers

Scripted expressions on any channel; variables reference other objects by name:

```python
moon = IcoSphere(radius=0.2).named("moon").drive("location", "cos(frame / 24) * 3", index=0)
sat = Sphere(radius=0.1).drive("location", "moon_z + 2", index=2,
                               variables=(Var("moon_z", target="moon", transform="LOC_Z"),))
```

## Kit ecosystem

One layout engine, interchangeable asset packs. `settlement()` in
[`examples/biomes/`](examples/biomes) dresses any `Library` that satisfies a
six-component contract (dwelling / landmark / centerpiece / flora /
light_source / clutter). Both images below are the **same function call** —
only the kit, ground function, and lighting differ:

| `settlement(medieval, rolling_hills, ...)` | `settlement(scifi, cratered_flats, ...)` |
|---|---|
| ![medieval settlement](docs/images/biome_village.jpg) | ![scifi colony](docs/images/colony.jpg) |

The medieval kit adapts the existing village pack without modifying it — the
way you'd wrap a pip-installed asset library.

And packs really are pip packages. `scenic-pack-bazaar` lives in
[`packs/`](packs) as its own distribution; installing it makes a third world
from the same call — discovered by name, no import required:

![desert souk](docs/images/souk.jpg)

```bash
pip install scenic-pack-bazaar
scenic packs                      # bazaar v0.1.0 — dwelling, landmark, fountain, ...
```

```python
app.include("bazaar")             # resolved via the scenic.libraries entry point
```

Packs register one line in their pyproject —
`[project.entry-points."scenic.libraries"] bazaar = "scenic_pack_bazaar:bazaar"` —
and become visible to `scenic packs`, to `app.include("<name>")`, and to LLM
agents through the MCP `list_packs` tool.

Making your own is one command:

```bash
scenic new-pack props     # publishable skeleton: entry point, example asset,
pip install -e scenic-pack-props    # QC contact-sheet scene, spec-side tests
scenic packs              # ...and it's discoverable
```

## Physics, tracking, and interop

Rigid bodies are one chained call; the camera can track any named object; the
classic kinematic-throw handover is two Timeline keys:

```python
from scenic import Physics

crates = [Cube(size=0.6).at(0, 0, 0.3 + i * 0.61).rigid(mass=1) for i in range(6)]
throw = Timeline()
throw.at(1).loc(0, -7, 2.4).prop("rigid_body.kinematic", 1.0).ease("CONSTANT")
throw.at(12).loc(0, -1.6, 1.9).ease("LINEAR")
throw.at(13).prop("rigid_body.kinematic", 0.0)   # release: solver inherits velocity
ball = Sphere(radius=0.45).animate(throw).rigid(mass=25, shape="SPHERE")

Scene(
    objects=[*crates, ball, Plane(size=40).rigid(passive=True)],
    camera=Camera(location=(7, -7, 3.5), track="ball"),   # follows the sim
    physics=Physics(gravity=(0, 0, -9.81), substeps=15),
)
```

The compiler fills the physics cache by stepping frames before stills render, so
`scenic render --frame 40` of a simulation is deterministic. Object constraints
(`TrackTo`, `CopyLocation`, `CopyRotation`) resolve targets by object name.
`scenic build -o scene.usdc` exports USD alongside glTF and .blend.

## Diffing scenes

Preview exactly what a rebuild would touch — before building:

```bash
scenic diff scenes.py --base HEAD~1          # vs the scene file at a git rev (no bpy)
scenic diff scenes.py --against out/main.blend   # vs the tags in a built .blend
scenic diff scenes.py -p rows=5 --base HEAD --exit-code   # CI gate
```

Reconciler plan keys are content hashes, so the diff is exact: per-object
create/keep/remove, plus material/mesh/world/render deltas.

## Linting scenes

Because scenes are pure data, sanity checks run without Blender or a render:

```bash
scenic lint scenes.py        # exit 1 on findings — CI-friendly
```

`lint` computes world-space bounding boxes from the specs and flags floating
objects, group parts nothing connects to, siblings stacked on a shared origin
(rotated parts that should radiate from a pivot), below-ground and zero-scale
objects. Objects whose location is driven or keyframed are exempted.

## For LLM agents (MCP)

scenic speaks the Model Context Protocol — plug it into Claude Code, or any
MCP-capable harness:

```json
{ "mcpServers": { "scenic": { "command": "scenic", "args": ["mcp"] } } }
```

Seven tools cover the whole agent loop: `list_components` (JSON-Schema catalog),
`get_scene_spec` (resolve a scene to its editable JSON template), `validate` and
`lint` (no Blender needed), `diff` (what would rebuild), `build`
(.blend/.glb/.usd), and `render_preview` — a fast EEVEE thumbnail returned as an
MCP image for visual feedback in seconds. Failures return structured envelopes
(`{"type", "message", "errors", "hint"}`), never tracebacks. Scenes round-trip
as JSON: agents can edit a spec and rebuild it without executing any code
(`scenic build scene.json` works from the CLI too, and `scenic render --preview`
gives the same fast feedback).

## Serving scenes over HTTP

```bash
pip install "scenic-bpy[serve]"
scenic serve scenes.py --port 8000
```

Every `@app.scene()` becomes `POST /scenes/{name}/render` (PNG) and
`POST /scenes/{name}/build?format=blend|glb`, with request validation and OpenAPI
docs generated from the same schemas the CLI uses. `scenic build -o scene.glb`
exports glTF directly too.

## Rigging

Declarative bones, IK, and constraints; meshes bind to a parent armature with
automatic weights:

```python
from scenic.rigging import Armature, Bone, IK, Rig, chain

@app.rig()
def arm_rig():
    upper = Bone("upper", head=(0, 0, 0), tail=(0, 0.06, 0.5))
    fore = Bone("fore", head=(0, 0.06, 0.5), tail=(0, 0, 0.95), parent="upper", connected=True)
    ctrl = Bone("ctrl", head=(0, 0, 0.95), tail=(0, 0.22, 0.95), deform=False)
    return Rig(bones=(upper, fore, ctrl),
               constraints=(IK(bone="fore", target="ctrl", chain_count=2),))

armature = Armature(arm_rig()).animate(wave()).with_children(
    tube().skinned()          # ARMATURE_AUTO weights
)
```

Pose bones key through the same Timeline: `t.at(16).bone("ctrl").loc(x=0.45)`.
`chain("upper", "fore", lengths=(0.5, 0.45))` lays connected runs head-to-tail.

## NLA

Layer or sequence clips instead of baking one action:

```python
obj.play(walk_cycle(), at=1, repeat=4).play(blink(), at=10, track="face", blend="ADD")
```

## Geometry nodes

Procedural scattering and instancing with the same piping DSL, attached as a modifier:

```python
from scenic.geometry import GeoNodes
from scenic.geonodes import (DistributePoints, GeoInput, IcoSphereMesh, InputGeometry,
                             InstanceOnPoints, RandomVector, SetMaterial)

rocks = (
    InputGeometry()
    >> DistributePoints(density=GeoInput("density", 0.8), seed=3)
    >> InstanceOnPoints(
        instance=IcoSphereMesh(radius=0.14, subdivisions=1) >> SetMaterial(material=rock_mat()),
        scale=RandomVector(min=(0.4, 0.4, 0.3), max=(1.6, 1.6, 1.0)).value,
    )
)
field = Plane(size=14).modify(GeoNodes(rocks, inputs={"density": 2.0}))
```

`GeoInput` sockets become real tree inputs: modifiers sharing a graph share one
node tree and vary only their values (tweakable in Blender's modifier panel too).

`humanoid(height=1.8)` from `scenic.rigging` gives a ready 19-bone biped with IK
arms/legs and hand/foot controls.

## Status (v0.6)

| Area | State |
|---|---|
| Typed components, `Depends`, validation | ✅ |
| Primitives, modifiers, camera/lights/world/render | ✅ |
| Shader node DSL (`>>` piping, textures, ramps, mix, bump) | ✅ |
| Timeline animation DSL, animation components, mp4 render | ✅ |
| Incremental reconciler, shared mesh/material datablocks | ✅ |
| Drivers, `Library`/`include`, `scenic serve`, glTF export | ✅ |
| Rigging (bones, chains, IK/constraints, auto-skinning), NLA | ✅ |
| Geometry nodes (scatter/instance DSL), humanoid preset | ✅ |
| `scenic lint` / `scenic diff` — bpy-free static checks & rebuild preview | ✅ |
| Geo-tree `GeoInput` params (shared trees, per-modifier values) | ✅ |
| Rigid-body physics, object constraints, camera tracking, USD export | ✅ |
| Kit ecosystem: `settlement()` contract over interchangeable Libraries | ✅ |
| `scenic build / render / docs / watch / serve`, in-Blender addon | ✅ |

The original DESIGN.md roadmap is complete. Works on Blender/bpy 4.2 through 5.0
(socket renames and the slotted-action API are handled with fallbacks).
