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.
- scenic_bpy-0.6.0/.github/workflows/ci.yml +36 -0
- scenic_bpy-0.6.0/.github/workflows/publish.yml +23 -0
- scenic_bpy-0.6.0/.gitignore +9 -0
- scenic_bpy-0.6.0/DESIGN.md +258 -0
- scenic_bpy-0.6.0/LICENSE +21 -0
- scenic_bpy-0.6.0/PKG-INFO +258 -0
- scenic_bpy-0.6.0/README.md +238 -0
- scenic_bpy-0.6.0/docs/images/arm.gif +0 -0
- scenic_bpy-0.6.0/docs/images/meadow.jpg +0 -0
- scenic_bpy-0.6.0/docs/images/orbit.jpg +0 -0
- scenic_bpy-0.6.0/docs/images/rust_crates.jpg +0 -0
- scenic_bpy-0.6.0/examples/arm.py +88 -0
- scenic_bpy-0.6.0/examples/bounce.py +58 -0
- scenic_bpy-0.6.0/examples/hero_shot.py +88 -0
- scenic_bpy-0.6.0/examples/meadow.py +87 -0
- scenic_bpy-0.6.0/examples/orbit.py +82 -0
- scenic_bpy-0.6.0/pyproject.toml +32 -0
- scenic_bpy-0.6.0/scenic/__init__.py +16 -0
- scenic_bpy-0.6.0/scenic/addon.py +133 -0
- scenic_bpy-0.6.0/scenic/animation/__init__.py +8 -0
- scenic_bpy-0.6.0/scenic/animation/drivers.py +50 -0
- scenic_bpy-0.6.0/scenic/animation/specs.py +52 -0
- scenic_bpy-0.6.0/scenic/animation/timeline.py +113 -0
- scenic_bpy-0.6.0/scenic/cli/__init__.py +0 -0
- scenic_bpy-0.6.0/scenic/cli/main.py +260 -0
- scenic_bpy-0.6.0/scenic/compile/__init__.py +37 -0
- scenic_bpy-0.6.0/scenic/compile/compiler.py +820 -0
- scenic_bpy-0.6.0/scenic/compile/plan.py +46 -0
- scenic_bpy-0.6.0/scenic/core/__init__.py +5 -0
- scenic_bpy-0.6.0/scenic/core/app.py +25 -0
- scenic_bpy-0.6.0/scenic/core/component.py +133 -0
- scenic_bpy-0.6.0/scenic/core/hashing.py +6 -0
- scenic_bpy-0.6.0/scenic/core/library.py +21 -0
- scenic_bpy-0.6.0/scenic/core/registry.py +58 -0
- scenic_bpy-0.6.0/scenic/core/rng.py +31 -0
- scenic_bpy-0.6.0/scenic/core/spec.py +74 -0
- scenic_bpy-0.6.0/scenic/geometry/__init__.py +19 -0
- scenic_bpy-0.6.0/scenic/geometry/modifiers.py +59 -0
- scenic_bpy-0.6.0/scenic/geometry/objects.py +117 -0
- scenic_bpy-0.6.0/scenic/geometry/primitives.py +106 -0
- scenic_bpy-0.6.0/scenic/geonodes/__init__.py +29 -0
- scenic_bpy-0.6.0/scenic/geonodes/nodes.py +282 -0
- scenic_bpy-0.6.0/scenic/rigging/__init__.py +26 -0
- scenic_bpy-0.6.0/scenic/rigging/presets.py +62 -0
- scenic_bpy-0.6.0/scenic/rigging/specs.py +141 -0
- scenic_bpy-0.6.0/scenic/serve.py +77 -0
- scenic_bpy-0.6.0/scenic/shading/__init__.py +32 -0
- scenic_bpy-0.6.0/scenic/shading/materials.py +14 -0
- scenic_bpy-0.6.0/scenic/shading/nodes.py +405 -0
- scenic_bpy-0.6.0/scenic/staging/__init__.py +23 -0
- scenic_bpy-0.6.0/scenic/staging/specs.py +107 -0
- scenic_bpy-0.6.0/tests/test_animation.py +91 -0
- scenic_bpy-0.6.0/tests/test_component.py +87 -0
- scenic_bpy-0.6.0/tests/test_core.py +25 -0
- scenic_bpy-0.6.0/tests/test_drivers.py +37 -0
- scenic_bpy-0.6.0/tests/test_example.py +36 -0
- scenic_bpy-0.6.0/tests/test_geonodes.py +96 -0
- scenic_bpy-0.6.0/tests/test_library.py +67 -0
- scenic_bpy-0.6.0/tests/test_nodes.py +100 -0
- scenic_bpy-0.6.0/tests/test_plan.py +59 -0
- scenic_bpy-0.6.0/tests/test_rigging.py +89 -0
- 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,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).
|
scenic_bpy-0.6.0/LICENSE
ADDED
|
@@ -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
|
+
|  |  |
|
|
29
|
+
|
|
30
|
+
| Driver-powered orbit | IK arm + auto-skinning |
|
|
31
|
+
|---|---|
|
|
32
|
+
|  |  |
|
|
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).
|