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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -917,7 +917,7 @@ those post-limit weights onto the arm as a colour attribute.
</tr>
<tr>
<td width="46%" valign="middle">
<a href="examples/triangulate-tangents/"><img src="examples/triangulate-tangents/preview.webp" alt="Triangulate and tangents: a machined steel buckler on a dark cradle - brushed grooves circulating the boss with an anisotropic sweep - proving the mikktspace tangent field a normal map depends on" /></a>
<a href="examples/triangulate-tangents/"><img src="examples/triangulate-tangents/preview.webp" alt="Triangulate and tangents: a machined steel buckler on a walnut stand - pointed brass boss, domed face cut with concentric lathe grooves, riveted brass rim - proving the mikktspace tangent field a normal map depends on" /></a>
</td>
<td valign="middle">

Expand Down
6 changes: 4 additions & 2 deletions docs/VISUAL-STYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,10 @@ the default stage.
describe — and the falsification pass is satisfied by naming why no visual
difference exists. Precedents: `vertex-weight-limit` (the prune preserves
the pose by design, so the 4-influence cap is numeric) and
`triangulate-tangents` (the ring sweep is UV/bump-driven, so a
tangent-field break does not move pixels). This is for contracts that
`triangulate-tangents` (the lathe grooves are a bump read off UV v and
the anisotropic streak follows the renderer's own UV-map tangent, so a
break in the `calc_tangents()` loop data the check reads does not move
pixels). This is for contracts that
genuinely cannot be seen, not for scenes that were not designed hard
enough: where a visual witness is possible, it is required.
- Presentation may be staged; evidence may not. When the witnessed artifact
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/triangulate-tangents-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.
2 changes: 1 addition & 1 deletion docs/gallery/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,7 @@ <h2><a href="vertex-weight-limit/">vertex-weight-limit</a></h2>
</article>
<article class="card" data-tags="mesh uv">
<a class="card-media" href="triangulate-tangents/" tabindex="-1" aria-hidden="true">
<img src="assets/triangulate-tangents-hero.webp" alt="A round silver buckler leaning back on a low stand, its face brushed in circular grooves with an anisotropic highlight." loading="lazy" decoding="async" />
<img src="assets/triangulate-tangents-hero.webp" alt="A steel buckler on a walnut stand, turned three-quarter: pointed brass boss, domed face cut with two bands of concentric lathe grooves, brass rim ringed with steel rivets." loading="lazy" decoding="async" />
</a>
<div class="card-body">
<h2><a href="triangulate-tangents/">triangulate-tangents</a></h2>
Expand Down
337 changes: 234 additions & 103 deletions docs/gallery/triangulate-tangents/index.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion examples/gallery.json
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@
"name": "triangulate-tangents",
"dir": "examples/triangulate-tangents",
"teaches": "A machined buckler verifying the tangent-space contract a game engine's normal mapping depends on. Deterministic triangulation, unit orthogonal tangent frames, and the edge/UV-delta formula matching mikktspace within welding tolerance.",
"alt": "A round silver buckler leaning back on a low stand, its face brushed in circular grooves with an anisotropic highlight.",
"alt": "A steel buckler on a walnut stand, turned three-quarter: pointed brass boss, domed face cut with two bands of concentric lathe grooves, brass rim ringed with steel rivets.",
"witnessesFix": "calc_tangents aborts on any ngon; planar UVs on a cylindrical wall collapse tangents onto normals; a MeshUVLoopLayer handle held across calc_tangents dangles on 4.5 (471 phantom flips, silent exit 0) while the mikktspace math is byte-identical on both versions.",
"hero": "docs/gallery/assets/triangulate-tangents-hero.webp",
"preview": "examples/triangulate-tangents/preview.webp",
Expand Down
62 changes: 38 additions & 24 deletions examples/triangulate-tangents/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Triangulate + Tangents

A runnable example that builds a machined buckler — lathed dome with polar
UVs, unwrapped rim and underside strips, a fanned back cap — and verifies
A runnable example that builds a machined buckler — one closed lathe mesh:
turned boss, flange, domed face and rolled rim with polar UVs, unwrapped rim
and underside strips, fanned back and apex caps — and verifies
the tangent-space contract a game engine's normal mapping depends on,
following [`mesh-editing-and-bmesh`](../../skills/mesh-editing-and-bmesh/SKILL.md).

Expand All @@ -14,47 +15,58 @@ the exported normal maps are baked against.
hazards around it.

- **Deterministic triangulation and the engine basis.** `calc_loop_triangles`
yields the closed-form count (two tris per quad + the cap fan, 720), and
every loop tangent is unit length (err 1.3e-07) and exactly orthogonal to
its loop normal (err 6.0e-08), with the bitangent exactly
yields the closed-form count (two tris per quad + the two cap fans, 4992),
and every loop tangent is unit length (err 1.3e-07) and exactly orthogonal
to its loop normal (err 9.9e-08), with the bitangent exactly
`bitangent_sign * (normal x tangent)` (err 0.0). One ngon anywhere in the
mesh and `calc_tangents` **aborts the whole call** — the back cap is an
explicit fan for exactly that reason.
mesh and `calc_tangents` **aborts the whole call** — both caps are
explicit fans for exactly that reason.
- **The tangent frame follows the UVs.** On smooth-field triangles the
per-loop tangents match the independently derived edge/UV-delta formula
within mikktspace's vertex-welding tolerance (measured 2.3e-06, tol 0.15);
within mikktspace's vertex-welding tolerance (measured 3.3e-02, tol 0.15 —
the face is smooth-shaded with sharp edges at the machined breaks, so
mikktspace genuinely welds frames across shared vertices);
a flipped frame inside a smooth field is never legal (0 measured). At UV
seams the frame orientation is implementation-defined — 42 seam flips, 9
at chart seams, identical on both versions — and planar UVs on a
seams the frame orientation is implementation-defined — 150 seam flips, 12
at chart seams, identical on all three versions — and planar UVs on a
cylindrical wall are degenerate: they collapse the tangent onto the
normal (dot 0.998, measured and fixed by unwrapping strips).
- **The reference-lifetime hazard (the real divergence).** A
`MeshUVLoopLayer` handle held across `calc_tangents()` dangles: on
Blender 4.5 reads through it return tangent floats, not UVs — measured
while authoring as a phantom **471 flipped frames and 385 phantom seam
positions, exit 0, a silent wrong answer**. On 5.1 the same stale read
survives by memory-layout luck. The mikktspace math itself is
byte-identical on 4.5.11 and 5.1.2; the apparent version divergence was
positions, exit 0, a silent wrong answer** (on the current buckler: 2829
phantom seam flips over 2497 phantom seam positions, still exit 0). On
5.1 and 5.2 the same stale read survives by memory-layout luck. The
mikktspace math itself is identical on 4.5.11, 5.1.2 and 5.2.1; the
apparent version divergence was
entirely the corrupt handle. Never hold layer handles across
CustomData-reallocating calls — re-fetch by name.

**What each check catches on failure:** a remeshed asset breaking the
triangulation count (probe: dropped profile ring, exit 3, 624 vs 720); a
wrong tangent formula in the independent derivation (probe: swapped du/dv,
exit 7, weld 1.459); and the stale-handle read (probe: no re-fetch — corrupt
measurements on 4.5, correct values on 5.1 by luck).
exit 7, weld 1.460); and the stale-handle read (probe: no re-fetch — corrupt
measurements on 4.5, correct values on 5.1/5.2 by luck).

**Version witness:** mikktspace output is identical on Blender 4.5.11 LTS
and 5.1.2 (same weld deviations, same seam flips). The `calc_tangents(uvmap=)`
**Version witness:** mikktspace output is identical on Blender 4.5.11 LTS,
5.1.2 and 5.2.1 LTS (same weld deviation, same seam flips). The `calc_tangents(uvmap=)`
signature is stable. The lifetime hazard above is the only behavioral split,
and it corrupts measurements rather than raising.

The render shows the buckler on its cradle: brushed grooves circulating the
boss with the anisotropic sweep riding them — the circulating tangent field
made visible. The staging is derived from the posed mesh: the stand rests on
the floor, the rim's lowest vertex sits 1 cm into its top, and a kickstand
strut runs from the buckler's back (found by a local-space ray) to the floor
behind it.
The render shows the buckler turned three-quarter on a walnut stand: a
pointed brass boss on a riveted blued-steel flange, a turned-steel dome, and
a brass rolled rim ringed with steel rivets. Materials are assigned per lathe
ring. The dome's lathe grooves are a bump read straight off UV v (the
radius), so they are exact circles, with no procedural distortion. Its
anisotropic highlight follows the renderer's own UV-map tangent, so it
fans out radially from the boss. This is a numeric-only contract
(docs/VISUAL-STYLE.md): the renderer derives its tangents from the UVs, so a
break in the `calc_tangents()` loop data the check reads does not move
pixels. The staging is derived from the posed mesh: the stand rests on the
floor, the rim's lowest vertex sits 1 cm into its top, and a kickstand strut
runs from the buckler's back (found by a local-space ray) to the floor behind
it, angled away from the camera.

## Run

Expand All @@ -73,7 +85,8 @@ blender --background --python triangulate_tangents.py -- --output buckler.png --
## Exit codes

Per-script sequential checks. `9` is a valid check code; there is no rule
against it. `10` is the shared framing helper.
against it. `10` is the shared framing helper, `11` the shared asset-quality
helper.

| Code | Meaning |
| --- | --- |
Expand All @@ -87,6 +100,7 @@ against it. `10` is the shared framing helper.
| 7 | Tangents deviate from the edge/UV closed form |
| 8 | Flipped tangent inside a clean triangle |
| 10 | Gallery framing violation; also `--output` produced no file |
| 11 | Asset-quality floor violation (`--output` only; shared helper) |

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
Binary file modified examples/triangulate-tangents/preview.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