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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -971,7 +971,7 @@ concave grooves are free.
</tr>
<tr>
<td width="46%" valign="middle">
<a href="examples/custom-normals-shade/"><img src="examples/custom-normals-shade/preview.webp" alt="Custom normals and shade by angle: three olive-drab jerry can props with pressed X ribs and red spout rings on a dark studio floor - one faceted flat, one smeared by smooth-everything, one crisp with correct hard edges - proving the post-4.1 shading contract" /></a>
<a href="examples/custom-normals-shade/"><img src="examples/custom-normals-shade/preview.webp" alt="Custom normals and shade by angle: three olive-drab jerry cans with X-pressed panels, triple handles and red-sealed spouts on a dark studio floor - one faceted flat, one smeared glossy by smooth-everything, one crisp by-angle with its sharp edges traced in thin cyan lines - proving the post-4.1 shading contract" /></a>
</td>
<td valign="middle">

Expand All @@ -984,7 +984,7 @@ on **both** 4.5 LTS and 5.1. `set_sharp_from_angle` marks sharp exactly the
edges an independent dihedral recompute predicts; evaluated loop normals
weld across smooth edges and split by the dihedral across sharp ones;
custom split normals survive depsgraph evaluation within their int16
quantization (1.407e-04, not float-exact). Documents the legacy
quantization (3.904e-05, not float-exact). Documents the legacy
`shade_auto_smooth` operator trap: CANCELLED headless on 4.5, FINISHED
with the Smooth-by-Angle modifier on 5.1.

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/assets/custom-normals-shade-hero.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
717 changes: 536 additions & 181 deletions docs/gallery/custom-normals-shade/index.html

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions docs/gallery/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -706,12 +706,12 @@ <h2><a href="collision-hull-proxy/">collision-hull-proxy</a></h2>
</article>
<article class="card" data-tags="mesh attributes">
<a class="card-media" href="custom-normals-shade/" tabindex="-1" aria-hidden="true">
<img src="assets/custom-normals-shade-hero.webp" alt="Three olive jerry cans with embossed X ribs, shaded flat, smooth and smooth-by-angle." loading="lazy" decoding="async" />
<img src="assets/custom-normals-shade-hero.webp" alt="Three olive jerry cans with X-pressed panels, triple handles and red-sealed spouts: faceted flat, glossy-smeared smooth-everywhere, and crisp by-angle with its sharp edges traced in thin cyan lines." loading="lazy" decoding="async" />
</a>
<div class="card-body">
<h2><a href="custom-normals-shade/">custom-normals-shade</a></h2>
<p class="teaches">A jerry can prop shaded three ways to prove the post-4.1 shading contract: hard edges are mesh data, landing exactly where the dihedral crosses. Face smooth flags plus a sharp_edge attribute, verified against an independently recomputed dihedral test, and per-loop custom normals surviving depsgraph evaluation within their int16 storage quantization (1.407e-04, not float-exact).</p>
<p class="witnesses"><span class="tag">witnesses</span> use_auto_smooth, use_custom_normals and calc_normals are AttributeError on BOTH 4.5 LTS and 5.1 - AI code still emits them. The legacy shade_auto_smooth operator CANCELS headless on 4.5 (asset load never finishes; mesh untouched; no exception) while 5.1 FINISHES with the Smooth by Angle NODES modifier. By-angle sharp sets match the independent dihedral recompute exactly (188 of 388 edges over 3 meshes).</p>
<p class="teaches">A jerry can prop shaded three ways to prove the post-4.1 shading contract: hard edges are mesh data, landing exactly where the dihedral crosses. Face smooth flags plus a sharp_edge attribute, verified against an independently recomputed dihedral test, and per-loop custom normals surviving depsgraph evaluation within their int16 storage quantization (3.904e-05 over 8196 loops, not float-exact).</p>
<p class="witnesses"><span class="tag">witnesses</span> use_auto_smooth, use_custom_normals and calc_normals are AttributeError on BOTH 4.5 LTS and 5.1 - AI code still emits them. The legacy shade_auto_smooth operator CANCELS headless on 4.5 (asset load never finishes; mesh untouched; no exception) while 5.1 FINISHES with the Smooth by Angle NODES modifier. By-angle sharp sets match the independent dihedral recompute exactly (1304 of 4738 edges over 3 meshes).</p>
<a class="card-link" href="custom-normals-shade/">View example<span class="sr-only"> custom-normals-shade</span> <span aria-hidden="true">&rarr;</span></a>
</div>
</article>
Expand Down
67 changes: 43 additions & 24 deletions examples/custom-normals-shade/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# Custom Normals + Shade by Angle

A runnable example that builds a jerry can prop — rounded-slab shell,
pressed X ribs, spout and cap, three-post handle — and verifies the shading
A runnable example that builds a jerry can prop — a pressed-steel shell
whose front and back carry four raised rounded-triangle panels (the X is
the channel between them), a weld-seam bead round the side band, a triple
carry handle on welded feet, and a spout with red seal, domed cap, cam
lever, hinge and locking pin; olive paint chipped to steel on exposed edges
by a `wear` point attribute — and verifies the shading
contract a game prop's silhouette depends on: which edges read hard and
which read smooth is mesh DATA, carried since Blender 4.1 by face smooth
flags plus a `sharp_edge` attribute,
Expand All @@ -28,18 +32,25 @@ It is not an engine exporter and does not claim engine/FiveM compatibility
- **Shade-by-angle is exact.** `mesh.set_sharp_from_angle(30°)` (which also
sets the face smooth flags — probed on both versions) marks sharp exactly
the edges whose *independently recomputed* dihedral angle crosses the
threshold: Shell 48 of 72 edges, rib 12 of 12, neck 128 of 304 — an exact
set match, not a count approximation.
threshold: Shell 1144 of 4098 edges, Handle 32 of 368, Neck 128 of 272
(1304 of 4738) — an exact set match, not a count approximation.
- **The evaluated shading matches the attribute's promise.** Through
depsgraph evaluation, loop normals across a smooth edge are welded
(deviation 0.0, tol 1e-3) and loop normals across a sharp edge carry
their face normals split by exactly the dihedral (angle error 0.0 rad,
tol 5e-3), all unit length (err 5.6e-08).
(deviation 0.0, tol 1e-3) and loop normals across a sharp edge are split
by the dihedral (angle error 1.898e-03 rad, tol 5e-3 — the curved side
band and the panels' rounded wall corners average a fan whose facets
step at most 22.5 and 10 deg), all unit length (err 1.0e-07).
- **Custom split normals survive depsgraph evaluation** — with a
quantization budget, not float precision: `normals_split_custom_set`
stores per-loop normals in int16, so a 144-loop round-trip reads back
within **1.407e-04** (tol 2e-4), unit length within 1.1e-07. Asserting
float-exact custom normals is a real bug this check catches.
stores per-loop normals in int16, so an 8196-loop round-trip reads back
within **3.904e-05** (tol 2e-4), unit length within 1.7e-07. Asserting
float-exact custom normals is a real bug this check catches. Each corner
requests its face normal tilted 20 deg toward the corner bisector. The
pattern matters: Blender's lnor encoder merges requests within one fan
that sit closer than ~0.81 deg and snaps an in-plane angle under 0.81 deg
to zero. On this shell an index-swept pattern measured the merge
(1.38e-02) and a 144-direction cycle hit the snap on 13 corners
(9.1e-03). Neither is storage precision, so the check keeps clear of both.
- **The divergence: the legacy `shade_auto_smooth` OPERATOR is a
version-split trap.** It builds the Smooth-by-Angle node-group modifier
from a bundled asset. Headless on **4.5 LTS the asset load never
Expand All @@ -50,22 +61,28 @@ It is not an engine exporter and does not claim engine/FiveM compatibility
portable path is the data API above, version-gated here explicitly.

**What each check catches on failure:** author/audit threshold drift
(probe: sharp marks applied at 20° but audited at 30°, exit 5, 4 extra
edges); the two halves of the contract out of sync (probe: `sharp_edge`
set but face smooth flags lost, exit 6, smooth-edge loops split by
3.83e-01); float-exactness assumed of custom normals (probe: tolerance
1e-6, exit 7, measured 1.407e-04); and any future version that resurrects
the legacy API or changes the operator's headless behavior (exit 3/8).

**Version witness:** every value above is identical on Blender 4.5.11 LTS
and 5.1.2 except the `shade_auto_smooth` operator behavior, which is
(probe: sharp marks applied at 20° but audited at 30°, exit 5, 48 extra
edges on the shell); the two halves of the contract out of sync (probe:
`sharp_edge` set but face smooth flags lost, exit 6, smooth-edge loops
split by 3.902e-01); float-exactness assumed of custom normals (probe:
tolerance 1e-6, exit 7, measured 3.904e-05); and any future version that
resurrects the legacy API or changes the operator's headless behavior
(exit 3/8). The render path adds the Layer 1 gates: framing (exit 10) and
asset quality (exit 11).

**Version witness:** every value above is identical on Blender 4.5.11 LTS,
5.1.2 and 5.2.1 LTS except the `shade_auto_smooth` operator behavior, which is
asserted per version (CANCELLED + untouched on 4.5, FINISHED + NODES
modifier on 5.1).
modifier on 5.1 and 5.2).

The render shows the same can shaded three ways — flat (faceted corners),
smooth-everywhere (the smeared AI bug: ribs melt, highlights warp at the
rim), and by-angle (the contract: smooth walls, crisp edges) — under a
strip light whose reflection exposes every normal discontinuity.
The render shows three fresh builds of the same can, one per shading
treatment. Left, flat: the side-band corners, cap and handle break into
facets. Middle, smooth-everywhere (the shade-smooth-and-forget AI habit):
the flat fields smear into gradients and the pressed panels go pillowy.
Right, by-angle (the contract): crisp creases and smooth rounds, with the
`sharp_edge` attribute read back from the mesh and traced as thin cyan
lines, so the viewer sees exactly which edges the data marked hard. A
low strip light makes the highlight shapes diverge between the three.

## Run

Expand Down Expand Up @@ -98,6 +115,8 @@ against it.
| 7 | Custom split normals lost or dequantized in evaluation |
| 8 | `shade_auto_smooth` operator behavior drifted from the version split |
| 9 | `--output` produced no file |
| 10 | Render path: Layer 1 framing violation (`gallery_framing`) |
| 11 | Render path: asset-quality floor violation (`gallery_asset_quality`) |

The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS
(5.1 on the weekly cron, the `needs-5.1` PR label, or manual dispatch).
Expand Down
Loading
Loading