scenic-bpy 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. scenic_bpy-0.6.0/.github/workflows/ci.yml +36 -0
  2. scenic_bpy-0.6.0/.github/workflows/publish.yml +23 -0
  3. scenic_bpy-0.6.0/.gitignore +9 -0
  4. scenic_bpy-0.6.0/DESIGN.md +258 -0
  5. scenic_bpy-0.6.0/LICENSE +21 -0
  6. scenic_bpy-0.6.0/PKG-INFO +258 -0
  7. scenic_bpy-0.6.0/README.md +238 -0
  8. scenic_bpy-0.6.0/docs/images/arm.gif +0 -0
  9. scenic_bpy-0.6.0/docs/images/meadow.jpg +0 -0
  10. scenic_bpy-0.6.0/docs/images/orbit.jpg +0 -0
  11. scenic_bpy-0.6.0/docs/images/rust_crates.jpg +0 -0
  12. scenic_bpy-0.6.0/examples/arm.py +88 -0
  13. scenic_bpy-0.6.0/examples/bounce.py +58 -0
  14. scenic_bpy-0.6.0/examples/hero_shot.py +88 -0
  15. scenic_bpy-0.6.0/examples/meadow.py +87 -0
  16. scenic_bpy-0.6.0/examples/orbit.py +82 -0
  17. scenic_bpy-0.6.0/pyproject.toml +32 -0
  18. scenic_bpy-0.6.0/scenic/__init__.py +16 -0
  19. scenic_bpy-0.6.0/scenic/addon.py +133 -0
  20. scenic_bpy-0.6.0/scenic/animation/__init__.py +8 -0
  21. scenic_bpy-0.6.0/scenic/animation/drivers.py +50 -0
  22. scenic_bpy-0.6.0/scenic/animation/specs.py +52 -0
  23. scenic_bpy-0.6.0/scenic/animation/timeline.py +113 -0
  24. scenic_bpy-0.6.0/scenic/cli/__init__.py +0 -0
  25. scenic_bpy-0.6.0/scenic/cli/main.py +260 -0
  26. scenic_bpy-0.6.0/scenic/compile/__init__.py +37 -0
  27. scenic_bpy-0.6.0/scenic/compile/compiler.py +820 -0
  28. scenic_bpy-0.6.0/scenic/compile/plan.py +46 -0
  29. scenic_bpy-0.6.0/scenic/core/__init__.py +5 -0
  30. scenic_bpy-0.6.0/scenic/core/app.py +25 -0
  31. scenic_bpy-0.6.0/scenic/core/component.py +133 -0
  32. scenic_bpy-0.6.0/scenic/core/hashing.py +6 -0
  33. scenic_bpy-0.6.0/scenic/core/library.py +21 -0
  34. scenic_bpy-0.6.0/scenic/core/registry.py +58 -0
  35. scenic_bpy-0.6.0/scenic/core/rng.py +31 -0
  36. scenic_bpy-0.6.0/scenic/core/spec.py +74 -0
  37. scenic_bpy-0.6.0/scenic/geometry/__init__.py +19 -0
  38. scenic_bpy-0.6.0/scenic/geometry/modifiers.py +59 -0
  39. scenic_bpy-0.6.0/scenic/geometry/objects.py +117 -0
  40. scenic_bpy-0.6.0/scenic/geometry/primitives.py +106 -0
  41. scenic_bpy-0.6.0/scenic/geonodes/__init__.py +29 -0
  42. scenic_bpy-0.6.0/scenic/geonodes/nodes.py +282 -0
  43. scenic_bpy-0.6.0/scenic/rigging/__init__.py +26 -0
  44. scenic_bpy-0.6.0/scenic/rigging/presets.py +62 -0
  45. scenic_bpy-0.6.0/scenic/rigging/specs.py +141 -0
  46. scenic_bpy-0.6.0/scenic/serve.py +77 -0
  47. scenic_bpy-0.6.0/scenic/shading/__init__.py +32 -0
  48. scenic_bpy-0.6.0/scenic/shading/materials.py +14 -0
  49. scenic_bpy-0.6.0/scenic/shading/nodes.py +405 -0
  50. scenic_bpy-0.6.0/scenic/staging/__init__.py +23 -0
  51. scenic_bpy-0.6.0/scenic/staging/specs.py +107 -0
  52. scenic_bpy-0.6.0/tests/test_animation.py +91 -0
  53. scenic_bpy-0.6.0/tests/test_component.py +87 -0
  54. scenic_bpy-0.6.0/tests/test_core.py +25 -0
  55. scenic_bpy-0.6.0/tests/test_drivers.py +37 -0
  56. scenic_bpy-0.6.0/tests/test_example.py +36 -0
  57. scenic_bpy-0.6.0/tests/test_geonodes.py +96 -0
  58. scenic_bpy-0.6.0/tests/test_library.py +67 -0
  59. scenic_bpy-0.6.0/tests/test_nodes.py +100 -0
  60. scenic_bpy-0.6.0/tests/test_plan.py +59 -0
  61. scenic_bpy-0.6.0/tests/test_rigging.py +89 -0
  62. scenic_bpy-0.6.0/tests/test_specs.py +75 -0
@@ -0,0 +1,36 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.11", "3.12"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v5
17
+ with:
18
+ enable-cache: true
19
+ - name: Run spec tests (no Blender required)
20
+ run: uv run --python ${{ matrix.python-version }} --extra dev pytest -q
21
+
22
+ bpy-smoke:
23
+ # End-to-end against the real bpy wheel: compile the examples headless.
24
+ runs-on: ubuntu-latest
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - uses: astral-sh/setup-uv@v5
28
+ with:
29
+ enable-cache: true
30
+ - name: Build examples to .blend and .glb
31
+ run: |
32
+ uv run --python 3.11 --extra blender scenic build examples/hero_shot.py -o /tmp/hero.blend
33
+ uv run --python 3.11 --extra blender scenic build examples/meadow.py -o /tmp/meadow.glb
34
+ uv run --python 3.11 --extra blender scenic build examples/arm.py -o /tmp/arm.blend
35
+ uv run --python 3.11 --extra blender scenic docs examples/orbit.py --json > /tmp/catalog.json
36
+ test -s /tmp/hero.blend && test -s /tmp/meadow.glb && test -s /tmp/catalog.json
@@ -0,0 +1,23 @@
1
+ name: Publish to PyPI
2
+
3
+ # Publishes on version tags (v0.6.0, ...). Uses PyPI Trusted Publishing — one-time
4
+ # setup at pypi.org: Account > Publishing > add GitHub publisher for
5
+ # satyam-fp/scenic-bpy, workflow publish.yml, environment pypi.
6
+
7
+ on:
8
+ push:
9
+ tags: ["v*"]
10
+
11
+ jobs:
12
+ publish:
13
+ runs-on: ubuntu-latest
14
+ environment: pypi
15
+ permissions:
16
+ id-token: write
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: astral-sh/setup-uv@v5
20
+ - name: Build sdist and wheel
21
+ run: uv build
22
+ - name: Publish
23
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ .pytest_cache/
5
+ dist/
6
+ out/
7
+ *.blend
8
+ *.blend1
9
+ uv.lock
@@ -0,0 +1,258 @@
1
+ # Scenic (working title) — a FastAPI-style framework for bpy
2
+
3
+ One-liner: **declare typed, composable scene components; the framework compiles them into a
4
+ .blend.** You write pure Python functions with type hints; Scenic handles everything that makes
5
+ raw bpy miserable.
6
+
7
+ ## Why FastAPI is the right model
8
+
9
+ FastAPI's magic is not HTTP — it's the shape:
10
+
11
+ | FastAPI | Scenic |
12
+ |---|---|
13
+ | `@app.get("/x")` registers a route | `@app.asset()` / `@app.material()` / `@app.scene()` registers a component |
14
+ | Pydantic validates request params | Pydantic validates component params before any bpy call runs |
15
+ | `Depends()` injects shared resources | `Depends()` injects other components (materials into objects, rigs into characters) |
16
+ | Starlette does the ugly ASGI work | The bpy backend does the ugly context/selection/mode work |
17
+ | Auto-generated OpenAPI docs | Auto-generated component catalog + JSON Schemas (also = LLM tool definitions) |
18
+ | `uvicorn --reload` | `scenic watch` — edit script, scene rebuilds incrementally |
19
+
20
+ ## What raw bpy gets wrong (the framework's job is to absorb all of this)
21
+
22
+ - **Global mutable state**: active object, selection, mode. Operators silently fail or hit the
23
+ wrong object. Scenic saves/restores state around every builder; builder code never touches it.
24
+ - **`bpy.ops` context fragility**: Scenic prefers `bpy.data` construction internally; when an
25
+ operator is unavoidable it runs under a correct `temp_override`.
26
+ - **Naming collisions & orphan datablocks**: Scenic owns naming (stable IDs) and lifecycle
27
+ (garbage-collects datablocks it created that are no longer referenced).
28
+ - **Node trees are graph plumbing**: 40 lines of `links.new(...)` for a simple material. Scenic
29
+ gives a socket-piping DSL.
30
+ - **No validation**: errors surface deep inside a 500-line script. Scenic validates every
31
+ parameter and every cross-reference before compiling anything.
32
+ - **Non-reproducibility**: same script, different results depending on the open file. Scenic
33
+ builds are deterministic functions of (code, params, seed).
34
+
35
+ ## Core architecture: spec layer vs. compiler
36
+
37
+ The single most important decision. Builders do **not** call bpy. They return **specs** — plain
38
+ Pydantic models describing what should exist:
39
+
40
+ ```
41
+ user code (builders) → SpecGraph (pure data, no bpy) → Compiler (bpy backend) → .blend
42
+ ```
43
+
44
+ Why split:
45
+
46
+ 1. **Testable without Blender** — unit tests over specs run in milliseconds, no bpy import.
47
+ 2. **Validatable before execution** — a bad param never half-mutates a scene.
48
+ 3. **Serializable** — a scene spec is JSON. Diff it, cache it, send it over the wire, have an
49
+ LLM emit it directly against the schema.
50
+ 4. **Reconcilable** — the compiler can diff desired spec vs. what's tagged in the open file and
51
+ rebuild only what changed (see Incremental builds).
52
+
53
+ The compiler is the *only* module that imports bpy. Everything above it is a normal Python
54
+ library.
55
+
56
+ **Escape hatch** (non-negotiable — no spec layer covers all of Blender): a builder can request
57
+ raw access. It runs inside a managed context that snapshots/restores selection/mode/active and
58
+ tags whatever datablocks it creates:
59
+
60
+ ```python
61
+ @app.asset(raw=True)
62
+ def weird_thing(ctx: BpyContext, twist: float = 1.0):
63
+ obj = ctx.new_mesh_object("weird") # tagged + tracked automatically
64
+ # ... arbitrary bpy against ctx ...
65
+ return obj
66
+ ```
67
+
68
+ ## The API, sketched
69
+
70
+ ```python
71
+ from scenic import Scenic, Depends, Field
72
+ from scenic.geometry import Cube, Bevel, Array
73
+ from scenic.shading import PrincipledBSDF, NoiseTexture, ColorRamp, Mix
74
+
75
+ app = Scenic()
76
+
77
+ @app.material()
78
+ def rust_metal(rust: float = Field(0.4, ge=0, le=1), scale: float = 8.0):
79
+ base = PrincipledBSDF(metallic=1.0, roughness=0.35)
80
+ mask = NoiseTexture(scale=scale).fac >> ColorRamp(stops=[(0.4, "black"), (0.6, "white")])
81
+ rusty = PrincipledBSDF(base_color=(0.35, 0.13, 0.05, 1), roughness=0.9)
82
+ return Mix(fac=mask * rust, a=base, b=rusty) # sockets pipe with >> and arithmetic ops
83
+
84
+ @app.asset()
85
+ def crate(size: float = 1.0, mat=Depends(rust_metal)):
86
+ return (
87
+ Cube(size=size)
88
+ .modify(Bevel(width=0.02, segments=3))
89
+ .shade(mat)
90
+ )
91
+
92
+ @app.asset()
93
+ def crate_stack(rows: int = Field(3, ge=1, le=20), jitter: float = 0.05, seed: int = 0):
94
+ rng = app.rng(seed) # seeded randomness, injected → reproducible
95
+ return Group(
96
+ crate(size=1.0).at(pos + rng.uniform(-jitter, jitter, 3))
97
+ for pos in grid(rows)
98
+ )
99
+
100
+ @app.scene()
101
+ def hero_shot(stack=Depends(crate_stack)):
102
+ return Scene(
103
+ objects=[stack],
104
+ camera=Camera(pos=(6, -6, 4), look_at=stack.center, lens=50),
105
+ lights=[Sun(energy=3, rotation=(45, 0, 30))],
106
+ world=HDRI("studio.exr", strength=1.2),
107
+ render=Render(engine="CYCLES", resolution=(1920, 1080), samples=128),
108
+ )
109
+ ```
110
+
111
+ Everything is a function of typed params → components compose like ordinary code. `Depends`
112
+ gives you the DAG for free: build order, datablock reuse (two objects depending on
113
+ `rust_metal(rust=0.4)` share one material), and cache keys.
114
+
115
+ ### Animation
116
+
117
+ Two entry points, both compiling to fcurves:
118
+
119
+ ```python
120
+ @app.animation()
121
+ def crate_drop(target: ObjectRef, height: float = 5.0):
122
+ with timeline(fps=24) as t:
123
+ t.at(0).of(target).loc(z=height)
124
+ t.at(30).of(target).loc(z=0).ease("BOUNCE", "EASE_OUT")
125
+ t.at(30).of(target).scale(1, 1, 0.8) # squash
126
+ t.at(36).of(target).scale(1, 1, 1)
127
+ ```
128
+
129
+ Plus declarative `Track`/`Key` models for programmatic generation (LLMs, mocap import), and
130
+ `Driver(expr="...", vars=...)` as a first-class spec.
131
+
132
+ ### Rigging
133
+
134
+ Declarative skeleton spec — bones, constraints, IK chains, and weight strategy:
135
+
136
+ ```python
137
+ @app.rig()
138
+ def simple_arm():
139
+ return Rig(
140
+ bones=chain("upper", "fore", "hand", lengths=[0.35, 0.30, 0.1]),
141
+ ik=[IK(target_bone="hand", chain_len=2, pole="elbow_pole")],
142
+ controls=[Control("hand_ctrl", drives="hand", shape="cube")],
143
+ )
144
+ ```
145
+
146
+ Humanoid preset (`HumanoidRig(proportions=...)`) later; the primitive layer
147
+ (Bone/Constraint/IK/Driver specs) comes first.
148
+
149
+ ## Incremental builds (the `--reload` moment)
150
+
151
+ Every datablock the compiler creates is tagged with a component ID and a hash of the resolved
152
+ params (custom properties: `scenic_id`, `scenic_hash`). On rebuild:
153
+
154
+ 1. Resolve the spec graph (cheap, pure Python).
155
+ 2. Diff against tags in the open file.
156
+ 3. Rebuild only dirty nodes; relink dependents; GC orphans Scenic owns.
157
+
158
+ That makes `scenic watch` feel like hot reload — edit a material param, only that material
159
+ recompiles, viewport updates in place. Untagged (user-made) datablocks are never touched.
160
+
161
+ ## Runtime & CLI
162
+
163
+ Two execution homes, same code:
164
+
165
+ - **Headless**: `pip install scenic bpy` → runs anywhere Python runs (CI, render farm, server).
166
+ - **In-Blender**: thin addon that hosts the watch loop so you get live viewport feedback while
167
+ editing scripts in your editor.
168
+
169
+ ```
170
+ scenic build scenes.py -t hero_shot -p rows=5 -o hero.blend
171
+ scenic render scenes.py -t hero_shot -o hero.png
172
+ scenic watch scenes.py -t hero_shot # rebuild on save (headless or into a live Blender)
173
+ scenic docs scenes.py # catalog: every component + its JSON Schema
174
+ scenic serve scenes.py --port 8000 # literally a FastAPI app: POST params → .blend/.glb/PNG
175
+ ```
176
+
177
+ `serve` is the punchline: because components are typed and validated, wrapping them in real
178
+ FastAPI endpoints is nearly free — a scene-generation microservice from the same file.
179
+
180
+ `docs` doubles as **LLM tool schemas**: every component already has a name, description
181
+ (docstring), and JSON Schema. An agent can discover and call the whole catalog with validated
182
+ arguments instead of emitting freehand bpy.
183
+
184
+ ## Libraries (the APIRouter analog)
185
+
186
+ ```python
187
+ # scenic_scifi/ (a pip package)
188
+ lib = Library(name="scifi", prefix="scifi")
189
+
190
+ @lib.asset()
191
+ def corridor(...): ...
192
+
193
+ # user code
194
+ app.include(lib)
195
+ ```
196
+
197
+ Asset packs become versioned pip dependencies. This is how an ecosystem forms.
198
+
199
+ ## Module layout
200
+
201
+ ```
202
+ scenic/
203
+ core/ # Scenic app, registry, Depends resolution, Pydantic base specs, hashing, rng
204
+ geometry/ # primitives, mesh-from-data, modifier stack, geometry-nodes wrapper
205
+ shading/ # node DSL, material/texture specs
206
+ animation/ # timeline DSL, tracks, drivers, NLA
207
+ rigging/ # bones, constraints, IK, controls, skinning
208
+ staging/ # scene, camera, lights, world, render settings, compositor (later)
209
+ compile/ # THE ONLY bpy importer: compiler, reconciler, context guard, naming, GC
210
+ cli/ # build/render/watch/docs/serve
211
+ ```
212
+
213
+ ## Prior art (and the open slot)
214
+
215
+ - **Infinigen** (Princeton): huge procedural bpy codebase — proof this works at scale, but it's
216
+ a research monolith, not a reusable typed framework.
217
+ - **Geometry Script**: lovely, but geometry-nodes-only.
218
+ - **bpy wheel**: makes headless distribution trivial — the enabler, not a competitor.
219
+ - Nothing occupies "typed, composable, validated app framework over bpy." That's the slot.
220
+
221
+ ## Risks / honest costs
222
+
223
+ - **API surface is enormous.** Mitigation: the spec layer is deliberately partial; `raw=True`
224
+ is the pressure valve from day one. Coverage grows by demand, not completionism.
225
+ - **Blender version drift** (4.x renames enums/sockets regularly). Mitigation: compiler is the
226
+ only bpy-touching module; version shims live there; CI matrix over bpy wheels.
227
+ - **Reconciler correctness** is the hardest subsystem. Ship v1 as "wipe managed collection +
228
+ full rebuild" (still fast headless); make it incremental in v2.
229
+
230
+ ## MVP roadmap
231
+
232
+ 1. ✅ **v0.1 — the spine**: `Scenic` app, `@asset/@material/@scene`, Pydantic specs, `Depends`,
233
+ compiler for meshes/primitives/modifiers/Principled materials, `scenic build/render`,
234
+ full-rebuild semantics, seeded rng.
235
+ 2. ✅ **v0.2 — the feel**: shading node DSL with socket piping, `scenic watch` + in-Blender
236
+ addon, `scenic docs`. (Geometry nodes wrapper deferred to v0.3.)
237
+ 3. ✅ **v0.3 — motion**: timeline DSL (`.at().loc().ease()`), animation components, slotted
238
+ actions (bpy 5.0) with 4.2 legacy fallback; incremental reconciler with mesh/material
239
+ dedup by content hash. (Drivers, NLA, geometry nodes deferred to v0.4.)
240
+ 4. ✅ **v0.4 — ecosystem**: drivers (scripted expressions + cross-object variables),
241
+ `Library`/`include`, `scenic serve` (FastAPI over components; PNG/blend/glb responses),
242
+ glTF export. (Rigging, NLA, geometry nodes deferred to v0.5.)
243
+ 5. ✅ **v0.5 — bodies**: rigging primitives (bones, chains, IK/constraints, auto-weight
244
+ skinning via `.skinned()`), pose-bone Timeline keys (`t.at(f).bone("ctrl").loc(...)`),
245
+ NLA strips via `.play()`. (Geometry nodes, humanoid preset deferred to v0.6.)
246
+ 6. ✅ **v0.6 — procedural**: geometry nodes wrapper (scatter/instancing DSL attached via
247
+ the GeoNodes modifier), humanoid rig preset (19-bone biped, 4 IK chains).
248
+
249
+ The original roadmap is complete. Beyond it: node-tree exposure of modifier inputs,
250
+ cross-object driver/constraint targets, physics specs, USD export, `scenic diff`.
251
+
252
+ ## Open questions
253
+
254
+ - Name. (`scenic` is a placeholder — `scenic` on PyPI may be taken.)
255
+ - Is LLM-facing schema export a first-class goal from v0.1? (Given Mixar, probably yes — it
256
+ changes how much effort goes into docstrings/descriptions early.)
257
+ - Minimum Blender/bpy version to target (bpy wheel requires Python 3.11 for 4.x — pinning to
258
+ 4.2 LTS is the safe floor).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Satyam Kumar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,258 @@
1
+ Metadata-Version: 2.4
2
+ Name: scenic-bpy
3
+ Version: 0.6.0
4
+ Summary: A FastAPI-style framework for building Blender scenes from typed, composable components.
5
+ Project-URL: Repository, https://github.com/satyam-fp/scenic-bpy
6
+ Author: Satyam Kumar
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: 3d,blender,bpy,procedural,scene-generation
10
+ Requires-Python: >=3.11
11
+ Requires-Dist: pydantic>=2.5
12
+ Provides-Extra: blender
13
+ Requires-Dist: bpy>=4.2; extra == 'blender'
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=8; extra == 'dev'
16
+ Provides-Extra: serve
17
+ Requires-Dist: fastapi>=0.110; extra == 'serve'
18
+ Requires-Dist: uvicorn>=0.29; extra == 'serve'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # scenic
22
+
23
+ A FastAPI-style framework for building Blender scenes with `bpy`: declare typed, composable
24
+ components; scenic compiles them into a `.blend`. No UI, no clicks — just Python.
25
+
26
+ | Procedural rust (shader DSL) | Geometry-node scatter |
27
+ |---|---|
28
+ | ![rust crates](docs/images/rust_crates.jpg) | ![meadow](docs/images/meadow.jpg) |
29
+
30
+ | Driver-powered orbit | IK arm + auto-skinning |
31
+ |---|---|
32
+ | ![orbit](docs/images/orbit.jpg) | ![arm wave](docs/images/arm.gif) |
33
+
34
+ Every image above is the output of a spec file in [`examples/`](examples), rendered headless
35
+ with `scenic render` — no Blender UI was opened.
36
+
37
+ ```python
38
+ from scenic import Scenic, Depends, Field
39
+ from scenic.geometry import Cube, Bevel
40
+ from scenic.shading import PrincipledBSDF
41
+ from scenic.staging import Scene, Camera, Sun
42
+
43
+ app = Scenic()
44
+
45
+ @app.material()
46
+ def steel(roughness: float = Field(0.3, ge=0, le=1)):
47
+ return PrincipledBSDF(metallic=1.0, roughness=roughness)
48
+
49
+ @app.asset()
50
+ def crate(size: float = 1.0, mat=Depends(steel)):
51
+ return Cube(size=size).modify(Bevel(width=0.03, segments=3)).shade(mat)
52
+
53
+ @app.scene()
54
+ def main(box=Depends(crate)):
55
+ return Scene(
56
+ objects=[box],
57
+ camera=Camera(location=(4, -4, 3), look_at=(0, 0, 0)),
58
+ lights=[Sun(energy=3, rotation=(50, 0, 30))],
59
+ )
60
+ ```
61
+
62
+ ```bash
63
+ scenic build scenes.py -o out/main.blend # compile to a .blend
64
+ scenic render scenes.py -o out/main.png -p size=2 # render a still, override params
65
+ scenic docs scenes.py --json # component catalog as JSON Schemas
66
+ ```
67
+
68
+ ## How it works
69
+
70
+ Builders never call bpy. They return **specs** — immutable Pydantic models. The compiler
71
+ (`scenic.compile`, the only module that imports bpy) realizes specs into datablocks. That split
72
+ means params are validated before anything mutates a scene, tests run without Blender, scenes
73
+ serialize to JSON, and equal specs dedup to shared datablocks (two crates with the same `steel`
74
+ share one material).
75
+
76
+ See [DESIGN.md](DESIGN.md) for the full architecture and roadmap.
77
+
78
+ ## Install
79
+
80
+ ```bash
81
+ pip install -e ".[dev]" # framework + tests (no Blender needed)
82
+ pip install -e ".[blender]" # + the bpy wheel for headless build/render
83
+ ```
84
+
85
+ Requires Python 3.11+. Runs headless via the `bpy` wheel or inside Blender 4.2+.
86
+
87
+ ## Shader node DSL
88
+
89
+ Materials are node graphs written as expressions — sockets pipe with `>>`, scalar arithmetic
90
+ becomes Math nodes, and the whole graph is still an immutable, JSON-serializable spec:
91
+
92
+ ```python
93
+ from scenic.shading import Bump, ColorRamp, Mix, NoiseTexture, PrincipledBSDF
94
+
95
+ @app.material()
96
+ def rust_metal(rust: float = Field(0.5, ge=0, le=1)):
97
+ steel = PrincipledBSDF(base_color=(0.55, 0.56, 0.58), metallic=1.0, roughness=0.35)
98
+ corrosion = PrincipledBSDF(
99
+ base_color=NoiseTexture(scale=24).fac >> ColorRamp(stops=((0.3, (0.22, 0.06, 0.02)), (0.7, (0.45, 0.18, 0.06)))),
100
+ roughness=0.85,
101
+ normal=NoiseTexture(scale=30, detail=8).fac >> Bump(strength=0.4),
102
+ )
103
+ mask = NoiseTexture(scale=6, detail=6).fac >> ColorRamp(stops=((0.35, "black"), (0.65, "white")))
104
+ return Mix(fac=mask * rust, a=steel, b=corrosion)
105
+ ```
106
+
107
+ ## Animation
108
+
109
+ Keyframes are authored on a `Timeline` and attached to objects with `.animate()`.
110
+ `.ease()` shapes the motion *into* the keys you just set — what animators mean:
111
+
112
+ ```python
113
+ from scenic.animation import Timeline
114
+
115
+ @app.animation()
116
+ def drop_and_squash(height: float = 4.0, land: int = 18):
117
+ t = Timeline()
118
+ t.at(1).loc(z=height)
119
+ t.at(land).loc(z=0.5).ease("BOUNCE", "EASE_OUT") # arrive with a bounce
120
+ t.at(land + 3).scale(1.2, 1.2, 0.6) # squash
121
+ t.at(land + 9).scale(1, 1, 1).ease("BACK", "EASE_OUT")
122
+ return t
123
+
124
+ ball = Sphere(radius=0.5).animate(drop_and_squash(height=6))
125
+ ```
126
+
127
+ ```bash
128
+ scenic render examples/bounce.py -o out/bounce.mp4 # full frame range
129
+ ```
130
+
131
+ Rotations are authored in degrees. The compiler targets Blender 5.0's slotted
132
+ actions with a 4.2 legacy fallback. The pip `bpy` wheel can't encode video, so
133
+ `.mp4` output renders PNG frames and assembles them with system ffmpeg.
134
+
135
+ ## Live reload
136
+
137
+ ```bash
138
+ scenic watch scenes.py -o out/main.blend # rebuilds on every save
139
+ ```
140
+
141
+ Builds are **incremental**: every managed datablock carries a content-hash key, and
142
+ rebuilds only create/remove what changed (a no-op rebuild of a 30-object scene is ~4 ms;
143
+ identical geometry and materials are shared datablocks).
144
+
145
+ For a live viewport inside Blender, install `scenic/addon.py` (requires
146
+ `pip install scenic-bpy` into Blender's Python): a **Scenic** tab appears in the
147
+ 3D-viewport sidebar with *Build Once* and *Watch* — only the managed "Scenic"
148
+ collection is ever touched.
149
+
150
+ ## Libraries
151
+
152
+ `Library` is the `APIRouter` analog — a mountable pack of components, distributable as a
153
+ pip package:
154
+
155
+ ```python
156
+ from scenic import Library
157
+
158
+ parts = Library("scifi")
159
+
160
+ @parts.asset()
161
+ def corridor(...): ...
162
+
163
+ app.include(parts) # available as "scifi.corridor"
164
+ ```
165
+
166
+ ## Drivers
167
+
168
+ Scripted expressions on any channel; variables reference other objects by name:
169
+
170
+ ```python
171
+ moon = IcoSphere(radius=0.2).named("moon").drive("location", "cos(frame / 24) * 3", index=0)
172
+ sat = Sphere(radius=0.1).drive("location", "moon_z + 2", index=2,
173
+ variables=(Var("moon_z", target="moon", transform="LOC_Z"),))
174
+ ```
175
+
176
+ ## Serving scenes over HTTP
177
+
178
+ ```bash
179
+ pip install "scenic-bpy[serve]"
180
+ scenic serve scenes.py --port 8000
181
+ ```
182
+
183
+ Every `@app.scene()` becomes `POST /scenes/{name}/render` (PNG) and
184
+ `POST /scenes/{name}/build?format=blend|glb`, with request validation and OpenAPI
185
+ docs generated from the same schemas the CLI uses. `scenic build -o scene.glb`
186
+ exports glTF directly too.
187
+
188
+ ## Rigging
189
+
190
+ Declarative bones, IK, and constraints; meshes bind to a parent armature with
191
+ automatic weights:
192
+
193
+ ```python
194
+ from scenic.rigging import Armature, Bone, IK, Rig, chain
195
+
196
+ @app.rig()
197
+ def arm_rig():
198
+ upper = Bone("upper", head=(0, 0, 0), tail=(0, 0.06, 0.5))
199
+ fore = Bone("fore", head=(0, 0.06, 0.5), tail=(0, 0, 0.95), parent="upper", connected=True)
200
+ ctrl = Bone("ctrl", head=(0, 0, 0.95), tail=(0, 0.22, 0.95), deform=False)
201
+ return Rig(bones=(upper, fore, ctrl),
202
+ constraints=(IK(bone="fore", target="ctrl", chain_count=2),))
203
+
204
+ armature = Armature(arm_rig()).animate(wave()).with_children(
205
+ tube().skinned() # ARMATURE_AUTO weights
206
+ )
207
+ ```
208
+
209
+ Pose bones key through the same Timeline: `t.at(16).bone("ctrl").loc(x=0.45)`.
210
+ `chain("upper", "fore", lengths=(0.5, 0.45))` lays connected runs head-to-tail.
211
+
212
+ ## NLA
213
+
214
+ Layer or sequence clips instead of baking one action:
215
+
216
+ ```python
217
+ obj.play(walk_cycle(), at=1, repeat=4).play(blink(), at=10, track="face", blend="ADD")
218
+ ```
219
+
220
+ ## Geometry nodes
221
+
222
+ Procedural scattering and instancing with the same piping DSL, attached as a modifier:
223
+
224
+ ```python
225
+ from scenic.geometry import GeoNodes
226
+ from scenic.geonodes import (DistributePoints, IcoSphereMesh, InputGeometry,
227
+ InstanceOnPoints, RandomVector, SetMaterial)
228
+
229
+ rocks = (
230
+ InputGeometry()
231
+ >> DistributePoints(density=0.8, seed=3)
232
+ >> InstanceOnPoints(
233
+ instance=IcoSphereMesh(radius=0.14, subdivisions=1) >> SetMaterial(material=rock_mat()),
234
+ scale=RandomVector(min=(0.4, 0.4, 0.3), max=(1.6, 1.6, 1.0)).value,
235
+ )
236
+ )
237
+ field = Plane(size=14).modify(GeoNodes(rocks))
238
+ ```
239
+
240
+ `humanoid(height=1.8)` from `scenic.rigging` gives a ready 19-bone biped with IK
241
+ arms/legs and hand/foot controls.
242
+
243
+ ## Status (v0.6)
244
+
245
+ | Area | State |
246
+ |---|---|
247
+ | Typed components, `Depends`, validation | ✅ |
248
+ | Primitives, modifiers, camera/lights/world/render | ✅ |
249
+ | Shader node DSL (`>>` piping, textures, ramps, mix, bump) | ✅ |
250
+ | Timeline animation DSL, animation components, mp4 render | ✅ |
251
+ | Incremental reconciler, shared mesh/material datablocks | ✅ |
252
+ | Drivers, `Library`/`include`, `scenic serve`, glTF export | ✅ |
253
+ | Rigging (bones, chains, IK/constraints, auto-skinning), NLA | ✅ |
254
+ | Geometry nodes (scatter/instance DSL), humanoid preset | ✅ |
255
+ | `scenic build / render / docs / watch / serve`, in-Blender addon | ✅ |
256
+
257
+ The original DESIGN.md roadmap is complete. Works on Blender/bpy 4.2 through 5.0
258
+ (socket renames and the slotted-action API are handled with fallbacks).