Skip to content

Commit 6ca8abd

Browse files
TMHSDigitalclaude
andcommitted
fix(examples): stand the bake-normal panels in plinths
Render path only. Both panels balanced on an edge and the leaning plate pierced the floor; each now stands in a low plinth, lifted so its lowest evaluated point seats 35 mm in the slot. The solidify rim wears plain bronze rather than the baked map. Bake statistics (frac 0.7211, MAD 0.09356) and exit codes unchanged on all three binaries. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com>
1 parent 5174e81 commit 6ca8abd

6 files changed

Lines changed: 132 additions & 7 deletions

File tree

318 Bytes
Loading

‎docs/gallery/bake-normal-high-to-low/index.html‎

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@
1616
<meta property="og:image" content="https://tmhsdigital.github.io/Blender-Developer-Tools/gallery/assets/bake-normal-high-to-low-hero.webp" />
1717
<meta property="og:image:width" content="1280" />
1818
<meta property="og:image:height" content="720" />
19-
<meta property="og:image:alt" content="A baked tangent-space normal map shown as a lilac card beside a bronze plate wearing it, with a radial embossed pattern." />
19+
<meta property="og:image:alt" content="A baked tangent-space normal map shown as a lilac card beside a bronze plate wearing it, with a radial embossed pattern, both standing in low display plinths." />
2020
<meta property="og:site_name" content="Blender Developer Tools" />
2121
<meta name="twitter:card" content="summary_large_image" />
2222
<meta name="twitter:title" content="bake-normal-high-to-low — Examples — Blender Developer Tools" />
2323
<meta name="twitter:description" content="A collapse-decimated hatch plate receiving a Cycles cage-baked tangent normal map from a ribbed high-poly source" />
2424
<meta name="twitter:image" content="https://tmhsdigital.github.io/Blender-Developer-Tools/gallery/assets/bake-normal-high-to-low-hero.webp" />
25-
<meta name="twitter:image:alt" content="A baked tangent-space normal map shown as a lilac card beside a bronze plate wearing it, with a radial embossed pattern." />
25+
<meta name="twitter:image:alt" content="A baked tangent-space normal map shown as a lilac card beside a bronze plate wearing it, with a radial embossed pattern, both standing in low display plinths." />
2626
<style>
2727
/* fonts are deployed by the landing build (docs/fonts/) */
2828
@font-face { font-family: 'Barlow Condensed'; font-weight: 600; font-display: swap;
@@ -268,8 +268,8 @@ <h1>bake-normal-high-to-low</h1>
268268
<p>A collapse-decimated hatch plate receiving a Cycles cage-baked tangent normal map from a ribbed high-poly source</p>
269269
</header>
270270
<main id="main">
271-
<button class="detail-hero" id="heroZoom" type="button" aria-label="View full size: A baked tangent-space normal map shown as a lilac card beside a bronze plate wearing it, with a radial embossed pattern.">
272-
<img src="../assets/bake-normal-high-to-low-hero.webp" alt="A baked tangent-space normal map shown as a lilac card beside a bronze plate wearing it, with a radial embossed pattern." width="1280" height="720" />
271+
<button class="detail-hero" id="heroZoom" type="button" aria-label="View full size: A baked tangent-space normal map shown as a lilac card beside a bronze plate wearing it, with a radial embossed pattern, both standing in low display plinths.">
272+
<img src="../assets/bake-normal-high-to-low-hero.webp" alt="A baked tangent-space normal map shown as a lilac card beside a bronze plate wearing it, with a radial embossed pattern, both standing in low display plinths." width="1280" height="720" />
273273
</button>
274274
<p class="zoom-hint">Rendered headless by the example itself — click to zoom.</p>
275275
<div class="callout"><span class="tag">witnesses</span> Statistical gates not byte-identity: detail frac 0.7211 / MAD 0.09356 vs flat 0.0000 / 0.00277; --flat-source exits 5; type=NORMAL not bake_type; RNA identical on 4.5.11, 5.1.2, 5.2.1</div>
@@ -284,6 +284,7 @@ <h1>bake-normal-high-to-low</h1>
284284
<ul><li><strong>Detail bake is not flat.</strong> <code>frac &gt;= 0.40</code> and <code>MAD &gt;= 0.05</code> (measured 0.7211 / 0.09356 on 4.5.11, 5.1.2, and 5.2.1). Catches an inactive Image Texture node, reversed selection, or EEVEE/GPU mis-setup that writes a blank map. MAD floor is half the measured hatch value, still ~30× a flat bake.</li><li><strong>Flat control is flat.</strong> <code>frac &lt;= 0.05</code> and <code>MAD &lt;= 0.03</code> (measured 0.0000 / 0.00277). Catches a noisy or wrongly-typed bake that would also satisfy the detail gates.</li><li><strong>Monotonic gap.</strong> <code>detail_frac - flat_frac &gt;= 0.30</code>. The two maps must separate; a tolerance wide enough to pass both would have no discriminating power.</li><li><strong><code>--flat-source</code> is the falsifier.</strong> Skips the ribs and still runs the detail gates. Must exit 5. Analogous to <code>--same-axis</code> in <a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/export-preset-axis"><code>export-preset-axis</code></a>.</li></ul>
285285
<p>Neighbor of <a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/lod-decimate-chain"><code>lod-decimate-chain</code></a> (the LOD is the cage target; collapse keeps UVs) and <a href="https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/examples/image-pixels-testcard"><code>image-pixels-testcard</code></a> (<code>save_render</code>, not <code>Image.save()</code>, if you persist the datablock). UV transfer and atlas packing are out of scope.</p>
286286
<p>The still stages the baked map as an unlit card beside the LOD wearing it. If the bake were flat, the card would be uniform <code>(128, 128, 255)</code> periwinkle and the plate would shade like the undisplaced cage.</p>
287+
<p>Staging is render-only. Each panel stands in a low display plinth, lifted so its lowest evaluated point sits 35 mm in the slot. Both panels used to balance on an edge, and the leaning plate pierced the floor. The solidify rim wears plain bronze instead of the baked map. The slight waviness along the plate&#x27;s border is the bake itself, normal-map shading inside the UV margin, and is left as the map produces it. Bake statistics are unchanged: frac 0.7211, MAD 0.09356 on all three binaries.</p>
287288
<p>Operator RNA (<code>type=&#x27;NORMAL&#x27;</code>, <code>use_selected_to_active</code>, <code>cage_extrusion</code>, <code>cage_object</code> as a string, <code>normal_space=&#x27;TANGENT&#x27;</code>, <code>margin_type</code>) is identical on 4.5.11, 5.1.2, and 5.2.1 — no shim.</p>
288289
<h2>Run</h2>
289290
<pre><code># Cheap correctness check (no render) - the CI check:
@@ -686,6 +687,55 @@ <h2>Source</h2>
686687
<span class="k">return</span> ob
687688

688689

690+
PLINTH_H = <span class="n">0.10</span>
691+
PLINTH_SEAT = <span class="n">0.035</span>
692+
PLINTH_PAD = <span class="n">0.08</span>
693+
694+
695+
<span class="k">def</span> seat_on_plinth(ob, name):
696+
<span class="s">&quot;&quot;&quot;Stand a display panel in a low plinth by its own lowest point.
697+
698+
Render-only staging. The panel is lifted so its lowest evaluated vertex
699+
sits PLINTH_SEAT inside a dark slab sized from the panel&#x27;s footprint.
700+
Both panels used to balance on an edge, and the leaning plate&#x27;s lower
701+
edge pierced the floor.
702+
&quot;&quot;&quot;</span>
703+
bpy.context.view_layer.update()
704+
dg = bpy.context.evaluated_depsgraph_get()
705+
ev = ob.evaluated_get(dg)
706+
me = ev.to_mesh()
707+
<span class="k">try</span>:
708+
pts = [ev.matrix_world @ v.co <span class="k">for</span> v <span class="k">in</span> me.vertices]
709+
<span class="k">finally</span>:
710+
ev.to_mesh_clear()
711+
ob.location.z += (PLINTH_H - PLINTH_SEAT) - min(p.z <span class="k">for</span> p <span class="k">in</span> pts)
712+
base = [p <span class="k">for</span> p <span class="k">in</span> pts <span class="k">if</span> p.z &lt; min(q.z <span class="k">for</span> q <span class="k">in</span> pts) + <span class="n">0.15</span>]
713+
x0 = min(p.x <span class="k">for</span> p <span class="k">in</span> base) - PLINTH_PAD
714+
x1 = max(p.x <span class="k">for</span> p <span class="k">in</span> base) + PLINTH_PAD
715+
y0 = min(p.y <span class="k">for</span> p <span class="k">in</span> base) - PLINTH_PAD
716+
y1 = max(p.y <span class="k">for</span> p <span class="k">in</span> base) + PLINTH_PAD
717+
slab = bpy.data.meshes.new(name)
718+
bm = bmesh.new()
719+
<span class="k">try</span>:
720+
bmesh.ops.create_cube(bm, size=<span class="n">1.0</span>)
721+
<span class="k">for</span> v <span class="k">in</span> bm.verts:
722+
v.co.x = (x0 + x1) / <span class="n">2</span> + v.co.x * (x1 - x0)
723+
v.co.y = (y0 + y1) / <span class="n">2</span> + v.co.y * (y1 - y0)
724+
v.co.z = PLINTH_H / <span class="n">2</span> + v.co.z * PLINTH_H
725+
bmesh.ops.bevel(bm, geom=list(bm.edges), offset=<span class="n">0.012</span>, segments=<span class="n">2</span>,
726+
affect=<span class="s">&quot;EDGES&quot;</span>, clamp_overlap=<span class="k">True</span>)
727+
bm.to_mesh(slab)
728+
<span class="k">finally</span>:
729+
bm.free()
730+
pmat = bpy.data.materials.new(name + <span class="s">&quot;Mat&quot;</span>)
731+
pmat.use_nodes = <span class="k">True</span>
732+
paint_principled(pmat, (<span class="n">0.045</span>, <span class="n">0.042</span>, <span class="n">0.04</span>, <span class="n">1.0</span>), <span class="n">0.0</span>, <span class="n">0.55</span>)
733+
slab.materials.append(pmat)
734+
plinth = bpy.data.objects.new(name, slab)
735+
bpy.context.scene.collection.objects.link(plinth)
736+
<span class="k">return</span> plinth
737+
738+
689739
<span class="k">def</span> render_still(low, mat, tex, path, engine):
690740
scene = bpy.context.scene
691741
<span class="k">for</span> ob <span class="k">in</span> list(scene.objects):
@@ -694,13 +744,22 @@ <h2>Source</h2>
694744
ob.hide_viewport = <span class="k">True</span>
695745

696746
wire_normal_map(mat, tex)
747+
<span class="c"># The solidify rim is the plate&#x27;s edge, not baked surface: give it plain</span>
748+
<span class="c"># bronze. Wearing the map, it sampled the UV border and rendered ragged.</span>
749+
rim = bpy.data.materials.new(<span class="s">&quot;BronzeRim&quot;</span>)
750+
rim.use_nodes = <span class="k">True</span>
751+
paint_principled(rim, (<span class="n">0.22</span>, <span class="n">0.13</span>, <span class="n">0.07</span>, <span class="n">1.0</span>), <span class="n">0.58</span>, <span class="n">0.44</span>)
752+
low.data.materials.append(rim)
697753
solid = low.modifiers.new(<span class="s">&quot;SolidifyDisplay&quot;</span>, <span class="s">&quot;SOLIDIFY&quot;</span>)
698754
solid.thickness = THICKNESS
699755
solid.offset = <span class="n">1.0</span>
756+
solid.material_offset_rim = <span class="n">1</span>
700757
low.rotation_euler.x = math.radians(<span class="n">72.0</span>)
701758
low.location = (<span class="n">1.20</span>, <span class="n">0.0</span>, <span class="n">1.05</span>)
702759
card = make_map_card(tex.image)
703760
card.location = (-<span class="n">1.50</span>, <span class="n">0.0</span>, <span class="n">1.05</span>)
761+
plinths = [seat_on_plinth(ob, name) <span class="k">for</span> ob, name <span class="k">in</span>
762+
((card, <span class="s">&quot;CardPlinth&quot;</span>), (low, <span class="s">&quot;PlatePlinth&quot;</span>))]
704763

705764
floor_me = bpy.data.meshes.new(<span class="s">&quot;Floor&quot;</span>)
706765
bm = bmesh.new()
@@ -780,7 +839,7 @@ <h2>Source</h2>
780839
scene,
781840
cam,
782841
hero=[card, low],
783-
elements=[card, low],
842+
elements=[card, low] + plinths,
784843
stage=[floor, wall],
785844
)
786845
<span class="k">if</span> fcode:
@@ -840,7 +899,7 @@ <h2>Source</h2>
840899
</main>
841900
<dialog class="lightbox" id="lightbox" aria-label="Full-size render">
842901
<button class="lightbox-close" type="button" autofocus>Close</button>
843-
<img src="../assets/bake-normal-high-to-low-hero.webp" alt="A baked tangent-space normal map shown as a lilac card beside a bronze plate wearing it, with a radial embossed pattern." />
902+
<img src="../assets/bake-normal-high-to-low-hero.webp" alt="A baked tangent-space normal map shown as a lilac card beside a bronze plate wearing it, with a radial embossed pattern, both standing in low display plinths." />
844903
</dialog>
845904
<footer>
846905
<div class="statusbar">
50 Bytes
Loading

‎examples/bake-normal-high-to-low/README.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ The still stages the baked map as an unlit card beside the LOD wearing it.
4141
If the bake were flat, the card would be uniform `(128, 128, 255)` periwinkle
4242
and the plate would shade like the undisplaced cage.
4343

44+
Staging is render-only. Each panel stands in a low display plinth, lifted
45+
so its lowest evaluated point sits 35 mm in the slot. Both panels used to
46+
balance on an edge, and the leaning plate pierced the floor. The
47+
solidify rim wears plain bronze instead of the baked map. The slight
48+
waviness along the plate's border is the bake itself, normal-map shading
49+
inside the UV margin, and is left as the map produces it. Bake
50+
statistics are unchanged: frac 0.7211, MAD 0.09356 on all three binaries.
51+
4452
Operator RNA (`type='NORMAL'`, `use_selected_to_active`, `cage_extrusion`,
4553
`cage_object` as a string, `normal_space='TANGENT'`, `margin_type`) is
4654
identical on 4.5.11, 5.1.2, and 5.2.1 — no shim.

‎examples/bake-normal-high-to-low/bake_normal_high_to_low.py‎

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,55 @@ def make_map_card(image, name="BakeCard"):
378378
return ob
379379

380380

381+
PLINTH_H = 0.10
382+
PLINTH_SEAT = 0.035
383+
PLINTH_PAD = 0.08
384+
385+
386+
def seat_on_plinth(ob, name):
387+
"""Stand a display panel in a low plinth by its own lowest point.
388+
389+
Render-only staging. The panel is lifted so its lowest evaluated vertex
390+
sits PLINTH_SEAT inside a dark slab sized from the panel's footprint.
391+
Both panels used to balance on an edge, and the leaning plate's lower
392+
edge pierced the floor.
393+
"""
394+
bpy.context.view_layer.update()
395+
dg = bpy.context.evaluated_depsgraph_get()
396+
ev = ob.evaluated_get(dg)
397+
me = ev.to_mesh()
398+
try:
399+
pts = [ev.matrix_world @ v.co for v in me.vertices]
400+
finally:
401+
ev.to_mesh_clear()
402+
ob.location.z += (PLINTH_H - PLINTH_SEAT) - min(p.z for p in pts)
403+
base = [p for p in pts if p.z < min(q.z for q in pts) + 0.15]
404+
x0 = min(p.x for p in base) - PLINTH_PAD
405+
x1 = max(p.x for p in base) + PLINTH_PAD
406+
y0 = min(p.y for p in base) - PLINTH_PAD
407+
y1 = max(p.y for p in base) + PLINTH_PAD
408+
slab = bpy.data.meshes.new(name)
409+
bm = bmesh.new()
410+
try:
411+
bmesh.ops.create_cube(bm, size=1.0)
412+
for v in bm.verts:
413+
v.co.x = (x0 + x1) / 2 + v.co.x * (x1 - x0)
414+
v.co.y = (y0 + y1) / 2 + v.co.y * (y1 - y0)
415+
v.co.z = PLINTH_H / 2 + v.co.z * PLINTH_H
416+
bmesh.ops.bevel(bm, geom=list(bm.edges), offset=0.012, segments=2,
417+
affect="EDGES", clamp_overlap=True)
418+
bm.to_mesh(slab)
419+
finally:
420+
bm.free()
421+
pmat = bpy.data.materials.new(name + "Mat")
422+
pmat.use_nodes = True
423+
paint_principled(pmat, (0.045, 0.042, 0.04, 1.0), 0.0, 0.55)
424+
slab.materials.append(pmat)
425+
plinth = bpy.data.objects.new(name, slab)
426+
bpy.context.scene.collection.objects.link(plinth)
427+
return plinth
428+
429+
381430
def render_still(low, mat, tex, path, engine):
382431
scene = bpy.context.scene
383432
for ob in list(scene.objects):
@@ -386,13 +435,22 @@ def render_still(low, mat, tex, path, engine):
386435
ob.hide_viewport = True
387436

388437
wire_normal_map(mat, tex)
438+
# The solidify rim is the plate's edge, not baked surface: give it plain
439+
# bronze. Wearing the map, it sampled the UV border and rendered ragged.
440+
rim = bpy.data.materials.new("BronzeRim")
441+
rim.use_nodes = True
442+
paint_principled(rim, (0.22, 0.13, 0.07, 1.0), 0.58, 0.44)
443+
low.data.materials.append(rim)
389444
solid = low.modifiers.new("SolidifyDisplay", "SOLIDIFY")
390445
solid.thickness = THICKNESS
391446
solid.offset = 1.0
447+
solid.material_offset_rim = 1
392448
low.rotation_euler.x = math.radians(72.0)
393449
low.location = (1.20, 0.0, 1.05)
394450
card = make_map_card(tex.image)
395451
card.location = (-1.50, 0.0, 1.05)
452+
plinths = [seat_on_plinth(ob, name) for ob, name in
453+
((card, "CardPlinth"), (low, "PlatePlinth"))]
396454

397455
floor_me = bpy.data.meshes.new("Floor")
398456
bm = bmesh.new()
@@ -472,7 +530,7 @@ def light(name, loc, energy, size, col, rot):
472530
scene,
473531
cam,
474532
hero=[card, low],
475-
elements=[card, low],
533+
elements=[card, low] + plinths,
476534
stage=[floor, wall],
477535
)
478536
if fcode:
538 Bytes
Loading

0 commit comments

Comments
 (0)