Skip to main content

A FastAPI-style framework for building Blender scenes from typed, composable components.

Project description

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.

Procedural rust (shader DSL) Geometry-node scatter
rust crates meadow
Driver-powered orbit IK arm + auto-skinning
orbit arm wave

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

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))],
    )
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 for the full architecture and roadmap.

Install

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:

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:

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))
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

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:

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:

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"),))

Serving scenes over HTTP

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:

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:

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:

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

rocks = (
    InputGeometry()
    >> DistributePoints(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))

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 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).

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

scenic_bpy-0.6.0.tar.gz (584.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

scenic_bpy-0.6.0-py3-none-any.whl (48.5 kB view details)

Uploaded Python 3

File details

Details for the file scenic_bpy-0.6.0.tar.gz.

File metadata

  • Download URL: scenic_bpy-0.6.0.tar.gz
  • Upload date:
  • Size: 584.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for scenic_bpy-0.6.0.tar.gz
Algorithm Hash digest
SHA256 17a6227f4fd098444cc462d486dc6e9377b2ba48809969a307b9ca1f481f6c27
MD5 fff84e13f0bfcb8f643b79935e5b9dd7
BLAKE2b-256 5a4169a73d334ef35caa16675f2305b31db6056b25ccf9232beda501e3a89404

See more details on using hashes here.

Provenance

The following attestation bundles were made for scenic_bpy-0.6.0.tar.gz:

Publisher: publish.yml on satyam-fp/scenic-bpy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file scenic_bpy-0.6.0-py3-none-any.whl.

File metadata

  • Download URL: scenic_bpy-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 48.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for scenic_bpy-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 15e22f9d35afcf3db244d6c1150d9d1197c8731a7e4c73c777e5c4d053e27ce3
MD5 576978593f9871157e97fbd34665fc89
BLAKE2b-256 d146476fd97e4e8fa1109b67a17f35728a1753768635c4c9d0a48959cd43614a

See more details on using hashes here.

Provenance

The following attestation bundles were made for scenic_bpy-0.6.0-py3-none-any.whl:

Publisher: publish.yml on satyam-fp/scenic-bpy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page