Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ The content base (counts are CI-enforced against README.md and the manifest):
API contract with deterministic checks, exit non-zero on failure, and
optionally render a still via `--output`. Each is executed headless on
Blender 4.5 LTS and 5.1 by `blender-smoke.yml`; its render ships in the
site gallery. Anatomy and authoring rules: copy `examples/bmesh-gear/`.
site gallery. Anatomy and authoring rules: copy `examples/bmesh-gear/`;
the render look is specified in `docs/VISUAL-STYLE.md`.

## Repository structure

Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ gallery's source of truth. When authoring a new one, copy the anatomy of
wire all of: gallery.json entry, `.cursor-plugin/plugin.json` examples array (CI-gated),
a `blender-smoke.yml` step, a README gallery row, hero webp (1280×720) in
`docs/gallery/assets/` + preview webp (1200×675), then run `python scripts/build_gallery.py`.
Renders must conform to the gallery look spec at `docs/VISUAL-STYLE.md`.

## Development Workflow

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ that actually runs. Browse them in the
<table>
<tr>
<td width="46%" valign="middle">
<a href="examples/swatch-grid/"><img src="examples/swatch-grid/preview.webp" alt="Swatch grid: a row of six shaded spheres - gold and copper metals, red and blue plastics, an emissive orange, and a matte white - on a reflective studio floor, rendered with EEVEE" /></a>
<a href="examples/swatch-grid/"><img src="examples/swatch-grid/preview.webp" alt="Swatch grid: a three-by-two grid of shaded spheres - polished gold and brushed copper metals, red and blue plastics, an emissive orange, and a matte white - against a dark studio backdrop, rendered with EEVEE" /></a>
</td>
<td valign="middle">

Expand Down Expand Up @@ -193,7 +193,7 @@ verts = 72, faces = 54, and `Set Material` carries the lime accent.
</tr>
<tr>
<td width="46%" valign="middle">
<a href="examples/shape-key-blend/"><img src="examples/shape-key-blend/preview.webp" alt="Shape-key blend: a violet truncated pyramid on a dark studio floor, lifted and flared by a relative Tall shape key at value 0.5" /></a>
<a href="examples/shape-key-blend/"><img src="examples/shape-key-blend/preview.webp" alt="Shape-key blend: three violet blocks on a dark studio floor showing a relative Tall shape key at values 0, 0.25, and 0.5 - plain cube to lifted, flared truncated pyramid, left to right" /></a>
</td>
<td valign="middle">

Expand Down
77 changes: 77 additions & 0 deletions docs/VISUAL-STYLE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Gallery Visual Style

The spec for every example render that ships in the gallery. New examples must
conform; deviations are defects. Calibration references: `armature-bend`,
`color-attribute-wheel`, `grease-pencil-rosette`, `compositor-glare`,
`damped-track-aim`, `parent-inverse-orrery`.

## The look in one sentence

A dark staged studio — near-black floor and back wall, one shaped warm key, a
warm light pool raking the backdrop — with a saturated hero subject filling
the frame.

## Color management

- `scene.view_settings.view_transform = 'Standard'`, always, with a comment.
AgX (the 4.x/5.x default) desaturates hero materials toward pastel and
lifts the stage toward grey — the washed look of the pre-audit gallery
traces to renders that never set a transform.
- Standard does not compress highlights: tune light energies so nothing
clips. If a region reads pure white at full size, the key is too hot.

## Stage

- Floor: 30-unit `create_grid` plane. Wall: a copy at `y ≈ 7–9`, rotated 90°
about X. Both share one Principled material:
`Base Color (0.03, 0.032, 0.037)`, `Roughness 0.7`.
- World background: `(0.02, 0.021, 0.025)`.
- The floor/wall seam is acceptable only where it falls into shadow; never
let it cross the subject.

## Lighting (all AREA lights)

- **Key** — warm white `(1.0, 0.96, 0.9)`, upper left, ~`(-4, -5, 6)`,
size 4–6, 300–700 W depending on subject albedo. It must shape: visible
falloff across the subject, a readable cast shadow.
- **Fill** — cool `(0.75, 0.85, 1.0)`, low right, size ~9, ~100 W. Just
enough that shadow sides stay legible; if the image looks flat, the fill
is too strong.
- **Rim** — cool `(0.6, 0.78, 1.0)`, behind, 200–400 W, to lift silhouettes
off the backdrop.
- **Wedge** — the signature: warm `(1.0, 0.76, 0.5)`, 200–500 W, size 5–7,
raking the back wall so a soft pool of light sits behind the subject.
Place it **between the subject and the wall**. A grazing area light draws
a hard terminator line across any flat surface it skims — if a stray
bright band crosses the subject, a light is grazing it.

## Materials

- Hero materials are designed, never Principled defaults: saturated base
colors, roughness chosen (0.3–0.6 glossy, 0.7+ matte), optional faint
emission (~0.1) when the subject carries color data that must read exactly.
- Surfaces that display flat color data (attribute fills, pixel buffers) go
fully matte: `Specular IOR Level = 0`, otherwise the wall/floor horizon
reflects as a line across the face.

## Framing

- The subject fills roughly 70–90 % of the frame in at least one axis.
Nothing that matters may touch or cross the frame edge; no featureless
quadrant of empty wall or floor.
- Camera: 45–55 mm lens, slightly above subject height, aimed with a
`TRACK_TO` constraint at an empty on the subject — a chosen angle, not the
default. Flat subjects present toward the camera (lean or tilt them);
progressions read left to right.
- The image must still read in under a second at thumbnail scale, and it
must witness the API: if the contract failed, the render should visibly
break.

## Output

- Render 1280×720 PNG (`taa_render_samples`/`cycles.samples` 32–64).
- Gallery assets: hero webp 1280×720 and preview webp 1200×675, quality 85
(load the PNG in Blender, `save(filepath=..., quality=85)`, `scale()` for
the preview — this preserves pixels without re-applying color management).
- After touching any example: `python scripts/build_gallery.py`, and update
the README gallery-row alt text if the composition changed.
Binary file modified docs/gallery/assets/curve-bevel-arc-hero.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/gallery/assets/depsgraph-export-hero.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/gallery/assets/driver-wave-hero.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/gallery/assets/gn-instance-grid-hero.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/gallery/assets/shape-key-blend-hero.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/gallery/assets/swatch-grid-hero.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/gallery/assets/turntable-hero.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/gallery/color-attribute-wheel/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ <h1>color-attribute-wheel</h1>
<img src="../assets/color-attribute-wheel-hero.webp" alt="color-attribute-wheel render" width="1280" height="720" />
</button>
<p class="zoom-hint">Rendered headless by the example itself — click to zoom.</p>
<div class="callout"><span class="tag">witnesses</span> CORNER-domain attributes are sized to loop count, not vertex count; the check asserts that, that active_color points at it, and that the Attribute node is actually linked to Base Color, not just present.</div>
<div class="callout"><span class="tag">witnesses</span> CORNER-domain attributes are sized to loop count, not vertex count; the check asserts the sizing, that active_color points at it, and that the Attribute node is actually linked to Base Color, not just present.</div>
<div class="runline">
<pre id="runCmd">blender --background --python examples/color-attribute-wheel/color_attribute_wheel.py --</pre>
<button class="copy-btn" id="copyRun" type="button">Copy</button>
Expand Down
25 changes: 18 additions & 7 deletions docs/gallery/curve-bevel-arc/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -335,8 +335,8 @@ <h2>Source</h2>
fmat = bpy.data.materials.new(<span class="s">&quot;Studio&quot;</span>)
fmat.use_nodes = <span class="k">True</span>
fb = fmat.node_tree.nodes[<span class="s">&quot;Principled BSDF&quot;</span>]
fb.inputs[<span class="s">&quot;Base Color&quot;</span>].default_value = (<span class="n">0.055</span>, <span class="n">0.06</span>, <span class="n">0.07</span>, <span class="n">1.0</span>)
fb.inputs[<span class="s">&quot;Roughness&quot;</span>].default_value = <span class="n">0.5</span>
fb.inputs[<span class="s">&quot;Base Color&quot;</span>].default_value = (<span class="n">0.03</span>, <span class="n">0.032</span>, <span class="n">0.037</span>, <span class="n">1.0</span>)
fb.inputs[<span class="s">&quot;Roughness&quot;</span>].default_value = <span class="n">0.7</span>
floor_me.materials.append(fmat)
floor = bpy.data.objects.new(<span class="s">&quot;Floor&quot;</span>, floor_me)
scene.collection.objects.link(floor)
Expand All @@ -347,7 +347,7 @@ <h2>Source</h2>

world = bpy.data.worlds.new(<span class="s">&quot;World&quot;</span>)
world.use_nodes = <span class="k">True</span>
world.node_tree.nodes[<span class="s">&quot;Background&quot;</span>].inputs[<span class="s">&quot;Color&quot;</span>].default_value = (<span class="n">0.008</span>, <span class="n">0.009</span>, <span class="n">0.012</span>, <span class="n">1.0</span>)
world.node_tree.nodes[<span class="s">&quot;Background&quot;</span>].inputs[<span class="s">&quot;Color&quot;</span>].default_value = (<span class="n">0.02</span>, <span class="n">0.021</span>, <span class="n">0.025</span>, <span class="n">1.0</span>)
scene.world = world

aim = bpy.data.objects.new(<span class="s">&quot;Aim&quot;</span>, <span class="k">None</span>)
Expand All @@ -367,14 +367,23 @@ <h2>Source</h2>
lc.track_axis = <span class="s">&#x27;TRACK_NEGATIVE_Z&#x27;</span>
lc.up_axis = <span class="s">&#x27;UP_Y&#x27;</span>

light(<span class="s">&quot;Key&quot;</span>, (-<span class="n">3.5</span>, -<span class="n">4.5</span>, <span class="n">5.5</span>), <span class="n">1500.0</span>, <span class="n">6.0</span>, (<span class="n">1.0</span>, <span class="n">0.98</span>, <span class="n">0.94</span>))
light(<span class="s">&quot;Fill&quot;</span>, (<span class="n">5.0</span>, -<span class="n">3.5</span>, <span class="n">2.5</span>), <span class="n">340.0</span>, <span class="n">8.0</span>, (<span class="n">0.8</span>, <span class="n">0.87</span>, <span class="n">1.0</span>))
light(<span class="s">&quot;Rim&quot;</span>, (<span class="n">1.5</span>, <span class="n">4.5</span>, <span class="n">2.0</span>), <span class="n">480.0</span>, <span class="n">4.0</span>, (<span class="n">1.0</span>, <span class="n">0.75</span>, <span class="n">0.45</span>))
<span class="c"># warm shaped key, faint cool fill, warm wedge on the back wall</span>
<span class="c"># (docs/VISUAL-STYLE.md)</span>
light(<span class="s">&quot;Key&quot;</span>, (-<span class="n">3.5</span>, -<span class="n">4.5</span>, <span class="n">5.5</span>), <span class="n">550.0</span>, <span class="n">4.5</span>, (<span class="n">1.0</span>, <span class="n">0.96</span>, <span class="n">0.9</span>))
light(<span class="s">&quot;Fill&quot;</span>, (<span class="n">5.0</span>, -<span class="n">3.5</span>, <span class="n">2.5</span>), <span class="n">110.0</span>, <span class="n">8.0</span>, (<span class="n">0.75</span>, <span class="n">0.85</span>, <span class="n">1.0</span>))
wedge = bpy.data.lights.new(<span class="s">&quot;Wedge&quot;</span>, <span class="s">&#x27;AREA&#x27;</span>)
wedge.energy = <span class="n">380.0</span>
wedge.size = <span class="n">6.0</span>
wedge.color = (<span class="n">1.0</span>, <span class="n">0.76</span>, <span class="n">0.5</span>)
wob = bpy.data.objects.new(<span class="s">&quot;Wedge&quot;</span>, wedge)
wob.location = (<span class="n">2.5</span>, <span class="n">5.5</span>, <span class="n">4.0</span>)
wob.rotation_euler = (math.radians(-<span class="n">68</span>), <span class="n">0.0</span>, math.radians(<span class="n">190</span>))
scene.collection.objects.link(wob)

cam_data = bpy.data.cameras.new(<span class="s">&quot;Cam&quot;</span>)
cam_data.lens = <span class="n">50.0</span>
cam = bpy.data.objects.new(<span class="s">&quot;Cam&quot;</span>, cam_data)
cam.location = (<span class="n">3.0</span>, -<span class="n">4.2</span>, <span class="n">2.6</span>)
cam.location = (<span class="n">2.4</span>, -<span class="n">3.4</span>, <span class="n">1.7</span>)
scene.collection.objects.link(cam)
scene.camera = cam
track = cam.constraints.new(<span class="s">&#x27;TRACK_TO&#x27;</span>)
Expand All @@ -394,6 +403,8 @@ <h2>Source</h2>
scene.render.resolution_y = <span class="n">720</span>
scene.render.image_settings.file_format = <span class="s">&#x27;PNG&#x27;</span>
scene.render.filepath = path
<span class="c"># AgX would wash the rose tube toward salmon (docs/VISUAL-STYLE.md)</span>
scene.view_settings.view_transform = <span class="s">&#x27;Standard&#x27;</span>
bpy.ops.render.render(write_still=<span class="k">True</span>)
<span class="k">return</span> os.path.exists(path) <span class="k">and</span> os.path.getsize(path) &gt; <span class="n">0</span>

Expand Down
17 changes: 11 additions & 6 deletions docs/gallery/depsgraph-export/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -314,20 +314,20 @@ <h2>Source</h2>
bm.to_mesh(floor_me)
<span class="k">finally</span>:
bm.free()
floor_me.materials.append(principled(<span class="s">&quot;Studio&quot;</span>, (<span class="n">0.24</span>, <span class="n">0.24</span>, <span class="n">0.25</span>, <span class="n">1.0</span>), <span class="n">0.0</span>, <span class="n">0.6</span>))
floor_me.materials.append(principled(<span class="s">&quot;Studio&quot;</span>, (<span class="n">0.03</span>, <span class="n">0.032</span>, <span class="n">0.037</span>, <span class="n">1.0</span>), <span class="n">0.0</span>, <span class="n">0.7</span>))
floor = bpy.data.objects.new(<span class="s">&quot;Floor&quot;</span>, floor_me)
scene.collection.objects.link(floor)
wall = bpy.data.objects.new(<span class="s">&quot;Wall&quot;</span>, floor_me.copy())
wall.data.materials.clear()
wall.data.materials.append(principled(<span class="s">&quot;Wall&quot;</span>, (<span class="n">0.045</span>, <span class="n">0.05</span>, <span class="n">0.055</span>, <span class="n">1.0</span>), <span class="n">0.0</span>, <span class="n">0.6</span>))
wall.data.materials.append(principled(<span class="s">&quot;Wall&quot;</span>, (<span class="n">0.03</span>, <span class="n">0.032</span>, <span class="n">0.037</span>, <span class="n">1.0</span>), <span class="n">0.0</span>, <span class="n">0.7</span>))
wall.location = (<span class="n">0.0</span>, <span class="n">9.0</span>, <span class="n">0.0</span>)
wall.rotation_euler = (math.pi / <span class="n">2</span>, <span class="n">0.0</span>, <span class="n">0.0</span>)
scene.collection.objects.link(wall)

world = bpy.data.worlds.new(<span class="s">&quot;World&quot;</span>)
world.use_nodes = <span class="k">True</span>
world.node_tree.nodes[<span class="s">&quot;Background&quot;</span>].inputs[<span class="s">&quot;Color&quot;</span>].default_value = \
(<span class="n">0.035</span>, <span class="n">0.04</span>, <span class="n">0.05</span>, <span class="n">1.0</span>)
(<span class="n">0.02</span>, <span class="n">0.021</span>, <span class="n">0.025</span>, <span class="n">1.0</span>)
scene.world = world

<span class="k">def</span> light(name, loc, energy, size, col, rot):
Expand All @@ -338,9 +338,11 @@ <h2>Source</h2>
ob.rotation_euler = tuple(math.radians(a) <span class="k">for</span> a <span class="k">in</span> rot)
scene.collection.objects.link(ob)

light(<span class="s">&quot;Key&quot;</span>, (-<span class="n">4.0</span>, -<span class="n">5.0</span>, <span class="n">6.0</span>), <span class="n">1000.0</span>, <span class="n">8.0</span>, (<span class="n">1.0</span>, <span class="n">0.99</span>, <span class="n">0.96</span>), (<span class="n">46</span>, <span class="n">0</span>, -<span class="n">35</span>))
light(<span class="s">&quot;Fill&quot;</span>, (<span class="n">5.0</span>, -<span class="n">3.5</span>, <span class="n">3.0</span>), <span class="n">350.0</span>, <span class="n">9.0</span>, (<span class="n">0.85</span>, <span class="n">0.9</span>, <span class="n">1.0</span>), (<span class="n">62</span>, <span class="n">0</span>, <span class="n">50</span>))
light(<span class="s">&quot;Rim&quot;</span>, (<span class="n">1.5</span>, <span class="n">5.0</span>, <span class="n">4.0</span>), <span class="n">120.0</span>, <span class="n">6.0</span>, (<span class="n">1.0</span>, <span class="n">0.85</span>, <span class="n">0.65</span>), (-<span class="n">70</span>, <span class="n">0</span>, <span class="n">165</span>))
<span class="c"># warm shaped key, faint cool fill, warm wedge on the back wall</span>
<span class="c"># (docs/VISUAL-STYLE.md)</span>
light(<span class="s">&quot;Key&quot;</span>, (-<span class="n">4.0</span>, -<span class="n">5.0</span>, <span class="n">6.0</span>), <span class="n">650.0</span>, <span class="n">5.0</span>, (<span class="n">1.0</span>, <span class="n">0.96</span>, <span class="n">0.9</span>), (<span class="n">46</span>, <span class="n">0</span>, -<span class="n">35</span>))
light(<span class="s">&quot;Fill&quot;</span>, (<span class="n">5.0</span>, -<span class="n">3.5</span>, <span class="n">3.0</span>), <span class="n">120.0</span>, <span class="n">9.0</span>, (<span class="n">0.75</span>, <span class="n">0.85</span>, <span class="n">1.0</span>), (<span class="n">62</span>, <span class="n">0</span>, <span class="n">50</span>))
light(<span class="s">&quot;Wedge&quot;</span>, (<span class="n">2.5</span>, <span class="n">5.5</span>, <span class="n">4.0</span>), <span class="n">380.0</span>, <span class="n">6.0</span>, (<span class="n">1.0</span>, <span class="n">0.76</span>, <span class="n">0.5</span>), (-<span class="n">68</span>, <span class="n">0</span>, <span class="n">190</span>))

cam_data = bpy.data.cameras.new(<span class="s">&quot;Cam&quot;</span>)
cam_data.lens = <span class="n">42.0</span>
Expand All @@ -362,6 +364,9 @@ <h2>Source</h2>
scene.render.resolution_y = <span class="n">720</span>
scene.render.image_settings.file_format = <span class="s">&#x27;PNG&#x27;</span>
scene.render.filepath = path
<span class="c"># AgX would flatten the graphite-vs-green contrast this still hinges on</span>
<span class="c"># (docs/VISUAL-STYLE.md)</span>
scene.view_settings.view_transform = <span class="s">&#x27;Standard&#x27;</span>
bpy.ops.render.render(write_still=<span class="k">True</span>)
<span class="k">return</span> os.path.exists(path) <span class="k">and</span> os.path.getsize(path) &gt; <span class="n">0</span>

Expand Down
Loading
Loading