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
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ Budget-conformance props. Not examples. Conventions: [`showcase/README.md`](show
<p align="center">
<a href="showcase/shipping-crate/"><img src="showcase/shipping-crate/preview.webp" width="24%" alt="Shipping crate: a wooden slat crate with iron corner brackets on a dark studio floor, warm wedge on the back wall" /></a>
<a href="showcase/stone-well/"><img src="showcase/stone-well/preview.webp" width="24%" alt="Stone well: a round brick well with a shingled pyramid roof on posts, rope and bucket, on a dark studio floor" /></a>
<a href="showcase/wooden-barrel/"><img src="showcase/wooden-barrel/preview.webp" width="24%" alt="Wooden barrel: a 20-stave wine-cask with croze-seated heads, chord-lofted iron hoops, and a bung on a dark studio floor" /></a>
<a href="showcase/wooden-barrel/"><img src="showcase/wooden-barrel/preview.webp" width="24%" alt="Wooden barrel: a 20-stave wine-cask with a head of four boards, chord-lofted iron hoops, and a bung on a dark studio floor" /></a>
<a href="showcase/campfire/"><img src="showcase/campfire/preview.webp" width="24%" alt="Campfire: a two-course stone fire ring with a tripod of logs on a dark studio floor, warm wedge on the back wall" /></a>
<a href="showcase/market-stall/"><img src="showcase/market-stall/preview.webp" width="24%" alt="Market stall: a timber frame with post feet, slatted counter, rafters, and a striped awning on a dark studio floor, warm wedge on the back wall" /></a>
<a href="showcase/street-lantern/"><img src="showcase/street-lantern/preview.webp" width="24%" alt="Street lantern: an iron post with brass collars, a braced arm, and a muntined amber cage on a dark studio floor" /></a>
Expand All @@ -129,7 +129,7 @@ Budget-conformance props. Not examples. Conventions: [`showcase/README.md`](show
<a href="showcase/hand-pump/"><img src="showcase/hand-pump/preview.webp" width="24%" alt="Hand pump: a cast-iron village pump on a wooden plinth with a spout and handle on a dark studio floor" /></a>
<a href="showcase/signpost/"><img src="showcase/signpost/preview.webp" width="24%" alt="Signpost: a timber post with two painted fingerboards, iron straps, and a pyramidal cap on a dark studio floor" /></a>
<a href="showcase/chopping-block/"><img src="showcase/chopping-block/preview.webp" width="24%" alt="Chopping block: a bark-sided log round with growth rings and drying checks on its sawn top, a riveted iron hoop, and a felling axe buried in the top, on a dark studio floor" /></a>
<a href="showcase/wooden-bucket/"><img src="showcase/wooden-bucket/preview.webp" width="24%" alt="Wooden bucket: a coopered pail with a round rope bail through ear rings and chord-lofted iron hoops on a dark studio floor" /></a>
<a href="showcase/wooden-bucket/"><img src="showcase/wooden-bucket/preview.webp" width="24%" alt="Wooden bucket: a coopered pail with a three-strand rope bail through iron ear rings and chord-lofted iron hoops on a dark studio floor" /></a>
<a href="showcase/wall-torch/"><img src="showcase/wall-torch/preview.webp" width="24%" alt="Wall torch: a dressed stone plaque with an iron sconce, wooden haft, and flame on a dark studio floor" /></a>
<a href="showcase/tavern-stool/"><img src="showcase/tavern-stool/preview.webp" width="24%" alt="Tavern stool: a round wooden seat on turned splayed legs with stretchers and iron ferrules on a dark studio floor" /></a>
<a href="showcase/iron-cauldron/"><img src="showcase/iron-cauldron/preview.webp" width="24%" alt="Iron cauldron: a bulged iron pot hanging from a timber tripod on a dark studio floor" /></a>
Expand All @@ -143,7 +143,7 @@ Budget-conformance props. Not examples. Conventions: [`showcase/README.md`](show

[`stone-well`](showcase/stone-well/) — procedural round stone well, shingled roof, windlass, rope and bucket through the same pipeline. Falsifier `--skip-decimate` exits 9.

[`wooden-barrel`](showcase/wooden-barrel/) — procedural 20-stave wine-cask with croze-seated heads and chord-lofted hoops through the same pipeline. Falsifier `--round-band` exits 18.
[`wooden-barrel`](showcase/wooden-barrel/) — procedural 20-stave wine-cask with boarded, croze-seated heads and chord-lofted hoops through the same pipeline. Falsifiers `--round-band` exits 18, `--one-piece-head` exits 17.

[`campfire`](showcase/campfire/) — procedural cobble ring, logs, and ash through the same pipeline. Falsifier `--skip-decimate` exits 9.

Expand Down Expand Up @@ -179,7 +179,7 @@ Budget-conformance props. Not examples. Conventions: [`showcase/README.md`](show

[`chopping-block`](showcase/chopping-block/) — procedural hooped timber stump with an embedded axe through the same pipeline. Falsifier `--skip-decimate` exits 9.

[`wooden-bucket`](showcase/wooden-bucket/) — procedural coopered pail with a round bail through the ear rings through the same pipeline. Falsifier `--float-handle` exits 17.
[`wooden-bucket`](showcase/wooden-bucket/) — procedural coopered pail with a three-strand rope bail through the ear rings through the same pipeline. Falsifiers `--float-handle` and `--sunk-rope` exit 17.

[`wall-torch`](showcase/wall-torch/) — procedural wall-mounted torch sconce with a stone plaque, iron bracket, and flame through the same pipeline. Falsifiers `--skip-decimate` (9), `--stray-vert` (15), `--float-arm` (17).

Expand Down Expand Up @@ -257,7 +257,7 @@ the compositor on and is exactly zero with it off — and that EEVEE has no `use
</tr>
<tr>
<td width="46%" valign="middle">
<a href="examples/color-attribute-wheel/"><img src="examples/color-attribute-wheel/preview.webp" alt="Color attribute wheel: an HSV color wheel disc with a white center fading into a vivid rainbow rim, leaning upright toward the camera on a dark studio floor with a warm light pool behind it" /></a>
<a href="examples/color-attribute-wheel/"><img src="examples/color-attribute-wheel/preview.webp" alt="Color attribute wheel: an HSV color wheel board with a white center fading into a vivid rainbow rim, leaning back on a dark plinth on a dark studio floor with a warm light pool behind it" /></a>
</td>
<td valign="middle">

Expand Down Expand Up @@ -361,7 +361,7 @@ then re-reads its own render and fails unless the pixels prove the flat-vs-check
</tr>
<tr>
<td width="46%" valign="middle">
<a href="examples/wave-displace/"><img src="examples/wave-displace/preview.webp" alt="Wave displace: a glossy sapphire-blue surface displaced into smooth standing-wave dunes against a black backdrop" /></a>
<a href="examples/wave-displace/"><img src="examples/wave-displace/preview.webp" alt="Wave displace: a sapphire-blue tile whose top is displaced into smooth standing-wave dunes, standing on a dark studio floor" /></a>
</td>
<td valign="middle">

Expand Down
Binary file modified docs/gallery/assets/color-attribute-wheel-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/wave-displace-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/wooden-barrel-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/wooden-bucket-hero.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
67 changes: 58 additions & 9 deletions docs/gallery/color-attribute-wheel/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@
<meta property="og:image" content="https://tmhsdigital.github.io/Blender-Developer-Tools/gallery/assets/color-attribute-wheel-hero.webp" />
<meta property="og:image:width" content="1280" />
<meta property="og:image:height" content="720" />
<meta property="og:image:alt" content="A disc painted with a full hue wheel that fades to white at the centre." />
<meta property="og:image:alt" content="A board painted with a full hue wheel that fades to white at the centre, leaning on a dark plinth." />
<meta property="og:site_name" content="Blender Developer Tools" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="color-attribute-wheel — Examples — Blender Developer Tools" />
<meta name="twitter:description" content="The modern color-attributes API — mesh.color_attributes.new() on the CORNER domain, filled by expanding per-vertex HSV across face corners with foreach_get/foreach_set, then wired into a shader Attribute node." />
<meta name="twitter:image" content="https://tmhsdigital.github.io/Blender-Developer-Tools/gallery/assets/color-attribute-wheel-hero.webp" />
<meta name="twitter:image:alt" content="A disc painted with a full hue wheel that fades to white at the centre." />
<meta name="twitter:image:alt" content="A board painted with a full hue wheel that fades to white at the centre, leaning on a dark plinth." />
<style>
/* fonts are deployed by the landing build (docs/fonts/) */
@font-face { font-family: 'Barlow Condensed'; font-weight: 600; font-display: swap;
Expand Down Expand Up @@ -268,8 +268,8 @@ <h1>color-attribute-wheel</h1>
<p>The modern color-attributes API — mesh.color_attributes.new() on the CORNER domain, filled by expanding per-vertex HSV across face corners with foreach_get/foreach_set, then wired into a shader Attribute node.</p>
</header>
<main id="main">
<button class="detail-hero" id="heroZoom" type="button" aria-label="View full size: A disc painted with a full hue wheel that fades to white at the centre.">
<img src="../assets/color-attribute-wheel-hero.webp" alt="A disc painted with a full hue wheel that fades to white at the centre." width="1280" height="720" />
<button class="detail-hero" id="heroZoom" type="button" aria-label="View full size: A board painted with a full hue wheel that fades to white at the centre, leaning on a dark plinth.">
<img src="../assets/color-attribute-wheel-hero.webp" alt="A board painted with a full hue wheel that fades to white at the centre, leaning on a dark plinth." 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 the sizing, that active_color points at it, and that the Attribute node is actually linked to Base Color, not just present.</div>
Expand All @@ -280,6 +280,8 @@ <h1>color-attribute-wheel</h1>
<section class="detail-section md">
<p>A runnable example that builds an HSV color wheel disc entirely with <code>bmesh</code> and colors it with <code>Mesh.color_attributes.new()</code> — the modern attributes API, not the deprecated <code>Mesh.vertex_colors</code> alias AI code keeps reaching for. It witnesses the domain trap that comes with it: a <code>CORNER</code>-domain attribute is sized to <code>len(mesh.loops)</code>, not <code>len(mesh.vertices)</code>, so per-vertex data has to be expanded across face corners before it is written. The material wires the same attribute into a Shader <code>Attribute</code> node (<code>attribute_type=&#x27;GEOMETRY&#x27;</code>) feeding Base Color — the step AI code most often skips, leaving the mesh gray even when the attribute data is correct.</p>
<p><strong>What it witnesses:</strong> a <code>FLOAT_COLOR</code> attribute on the <code>CORNER</code> domain, filled with one <code>foreach_get</code> (loop → vertex index) and one <code>foreach_set</code> (loop color), never a per-loop Python assignment. The check asserts the attribute is sized to the loop count and <em>not</em> the vertex count, that it is <code>color_attributes.active_color</code> (so a renderer or exporter actually picks it up), and that several probe loops match the closed-form HSV value for the vertex they reference. A separate check in the render path confirms the <code>Attribute</code> node is actually linked to Base Color, not just present in the node tree.</p>
<h2>Staging</h2>
<p>The disc is zero-thickness, and the still used to show it as a paper-thin oval hovering 8 cm above the floor. The render path now gives it a render-only Solidify body (5 cm) and stands it at the same 52° lean on a dark plinth, with a strut behind it, easel-style. The height is computed so the lowest point of the rim rests on the plinth. The mesh <code>check()</code> asserts is unchanged: the modifier is added after the check and only in the render path. Measured framing on 5.2.1: fill 0.794 y, every margin ≥ 0.094.</p>
<h2>Run</h2>
<pre><code># Cheap correctness check (no render) — the CI check:
blender --background --python color_attribute_wheel.py --
Expand Down Expand Up @@ -326,6 +328,7 @@ <h2>Source</h2>
&quot;&quot;&quot;</span>
<span class="k">import</span> bpy, bmesh, sys, os, math, colorsys, argparse
<span class="k">from</span> array <span class="k">import</span> array
<span class="k">from</span> mathutils <span class="k">import</span> Vector

RINGS = <span class="n">14</span>
SEGMENTS = <span class="n">72</span>
Expand Down Expand Up @@ -500,8 +503,54 @@ <h2>Source</h2>
<span class="c"># stand the disc up toward the camera like an easel: the wheel is the</span>
<span class="c"># subject, so it should present nearly face-on and fill the frame instead</span>
<span class="c"># of lying foreshortened on the floor.</span>
obj.location = (<span class="n">0.0</span>, <span class="n">0.0</span>, <span class="n">1.34</span>)
obj.rotation_euler = (math.radians(<span class="n">52</span>), <span class="n">0.0</span>, math.radians(<span class="n">10</span>))
<span class="c"># render-only: a Solidify body so the disc reads as a painted board, not</span>
<span class="c"># a zero-thickness sheet, resting on a dark plinth and propped from</span>
<span class="c"># behind by a strut, easel-style, instead of hovering above the floor.</span>
disc_t = <span class="n">0.05</span>
plinth_h = <span class="n">0.12</span>
tilt = math.radians(<span class="n">52</span>)
solid = obj.modifiers.new(<span class="s">&quot;Board&quot;</span>, <span class="s">&#x27;SOLIDIFY&#x27;</span>)
solid.thickness = disc_t
solid.offset = -<span class="n">1.0</span>
<span class="c"># lowest rim point sits R_OUTER * sin(tilt) below the centre, and the</span>
<span class="c"># board&#x27;s back face another disc_t * cos(tilt) below that</span>
obj.location = (<span class="n">0.0</span>, <span class="n">0.0</span>, plinth_h + R_OUTER * math.sin(tilt) + disc_t * math.cos(tilt))
obj.rotation_euler = (tilt, <span class="n">0.0</span>, math.radians(<span class="n">10</span>))

stand_mat = bpy.data.materials.new(<span class="s">&quot;Stand&quot;</span>)
stand_mat.use_nodes = <span class="k">True</span>
sb = stand_mat.node_tree.nodes[<span class="s">&quot;Principled BSDF&quot;</span>]
sb.inputs[<span class="s">&quot;Base Color&quot;</span>].default_value = (<span class="n">0.012</span>, <span class="n">0.012</span>, <span class="n">0.014</span>, <span class="n">1.0</span>)
sb.inputs[<span class="s">&quot;Roughness&quot;</span>].default_value = <span class="n">0.45</span>

<span class="k">def</span> stand_part(name, build):
me = bpy.data.meshes.new(name)
bm = bmesh.new()
<span class="k">try</span>:
build(bm)
bm.to_mesh(me)
<span class="k">finally</span>:
bm.free()
me.materials.append(stand_mat)
ob = bpy.data.objects.new(name, me)
scene.collection.objects.link(ob)
<span class="k">return</span> ob

bpy.context.view_layer.update()
mw = obj.matrix_world
rim_low = mw @ Vector((<span class="n">0.0</span>, -R_OUTER, <span class="n">0.0</span>))
plinth = stand_part(<span class="s">&quot;Plinth&quot;</span>, <span class="k">lambda</span> bm: bmesh.ops.create_cube(bm, size=<span class="n">1.0</span>))
plinth.scale = (<span class="n">1.5</span>, <span class="n">0.9</span>, plinth_h)
plinth.location = (rim_low.x, rim_low.y + <span class="n">0.25</span>, plinth_h / <span class="n">2.0</span>)
plinth.rotation_euler = (<span class="n">0.0</span>, <span class="n">0.0</span>, math.radians(<span class="n">10</span>))
<span class="c"># the strut runs from the board&#x27;s back, above centre, down to the plinth</span>
top = mw @ Vector((<span class="n">0.0</span>, <span class="n">0.45</span>, -disc_t))
foot = Vector((top.x, top.y + <span class="n">0.95</span>, plinth_h))
span = top - foot
strut = stand_part(<span class="s">&quot;Strut&quot;</span>, <span class="k">lambda</span> bm: bmesh.ops.create_cone(
bm, cap_ends=<span class="k">True</span>, segments=<span class="n">16</span>, radius1=<span class="n">0.035</span>, radius2=<span class="n">0.035</span>, depth=span.length))
strut.location = (top + foot) / <span class="n">2.0</span>
strut.rotation_euler = Vector((<span class="n">0.0</span>, <span class="n">0.0</span>, <span class="n">1.0</span>)).rotation_difference(span).to_euler()

floor_me = bpy.data.meshes.new(<span class="s">&quot;Floor&quot;</span>)
bm = bmesh.new()
Expand Down Expand Up @@ -550,13 +599,13 @@ <h2>Source</h2>
light(<span class="s">&quot;Wedge&quot;</span>, (<span class="n">2.0</span>, <span class="n">5.2</span>, <span class="n">3.6</span>), <span class="n">220.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>))

aim = bpy.data.objects.new(<span class="s">&quot;Aim&quot;</span>, <span class="k">None</span>)
aim.location = obj.location
aim.location = obj.location - Vector((<span class="n">0.0</span>, <span class="n">0.0</span>, <span class="n">0.2</span>))
scene.collection.objects.link(aim)

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">0.0</span>, -<span class="n">8.0</span>, <span class="n">2.4</span>)
cam.location = (<span class="n">0.0</span>, -<span class="n">9.4</span>, <span class="n">2.5</span>)
con = cam.constraints.new(<span class="s">&#x27;TRACK_TO&#x27;</span>)
con.target = aim
con.track_axis = <span class="s">&#x27;TRACK_NEGATIVE_Z&#x27;</span>
Expand Down Expand Up @@ -618,7 +667,7 @@ <h2>Source</h2>
</main>
<dialog class="lightbox" id="lightbox" aria-label="Full-size render">
<button class="lightbox-close" type="button" autofocus>Close</button>
<img src="../assets/color-attribute-wheel-hero.webp" alt="A disc painted with a full hue wheel that fades to white at the centre." />
<img src="../assets/color-attribute-wheel-hero.webp" alt="A board painted with a full hue wheel that fades to white at the centre, leaning on a dark plinth." />
</dialog>
<footer>
<div class="statusbar">
Expand Down
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/contact-sheets/wave-displace-contact-sheet.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/contact-sheets/wooden-barrel-contact-sheet.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/contact-sheets/wooden-bucket-contact-sheet.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading