Skip to content

Commit 64b38ef

Browse files
committed
feat: add triangulate-tangents example witnessing the mikktspace contract
A game engine's normal mapping lives or dies by the tangent frame: the basis must be unit and orthogonal, follow the UVs, and carry a consistent bitangent sign. This example verifies all of it on a machined buckler with three UV chart styles, against the independently derived edge/UV formula, plus the two hazards that actually bite: calc_tangents aborting on any ngon, and a MeshUVLoopLayer handle dangling across the call on 4.5 (471 phantom flips, silent exit 0) while the mikktspace math is byte-identical on 4.5.11 and 5.1.2. Signed-off-by: fOuttaMyPaint <TMhospitalitystrategies@gmail.com>
1 parent ec0c933 commit 64b38ef

12 files changed

Lines changed: 1267 additions & 34 deletions

File tree

.cursor-plugin/plugin.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"examples/swatch-grid",
8181
"examples/temp-override-join",
8282
"examples/text-version-stamp",
83+
"examples/triangulate-tangents",
8384
"examples/turntable",
8485
"examples/uv-layer-grid",
8586
"examples/vertex-weight-limit",

.github/workflows/blender-smoke.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,3 +404,15 @@ jobs:
404404
# deform layer, and a pinned Root mount. Exits non-zero on failure.
405405
xvfb-run -a "$BLENDER" --background \
406406
--python examples/vertex-weight-limit/vertex_weight_limit.py --
407+
408+
- name: Shipped example - triangulate + tangents (mikktspace contract)
409+
run: |
410+
set -euo pipefail
411+
# Check only (no render): a lathed buckler with polar/strip/planar
412+
# UV charts; asserts closed-form loop-triangle count, unit and
413+
# orthogonal tangent frames, bitangent == sign*(n x t), tangents
414+
# matching the independent edge/UV-delta formula on smooth-field
415+
# triangles, zero flips off seams, and the re-fetched UV layer
416+
# surviving calc_tangents. Exits non-zero on failure.
417+
xvfb-run -a "$BLENDER" --background \
418+
--python examples/triangulate-tangents/triangulate_tangents.py --

README.md

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
</p>
1919

2020
<p align="center">
21-
<strong>12 skills</strong> &nbsp;&bull;&nbsp; <strong>6 rules</strong> &nbsp;&bull;&nbsp; <strong>2 templates</strong> &nbsp;&bull;&nbsp; <strong>17 snippets</strong> &nbsp;&bull;&nbsp; <strong>26 examples</strong>
21+
<strong>12 skills</strong> &nbsp;&bull;&nbsp; <strong>6 rules</strong> &nbsp;&bull;&nbsp; <strong>2 templates</strong> &nbsp;&bull;&nbsp; <strong>17 snippets</strong> &nbsp;&bull;&nbsp; <strong>27 examples</strong>
2222
</p>
2323

2424
<p align="center">
@@ -36,7 +36,7 @@
3636

3737
## Overview
3838

39-
This repository ships **12 skills, 6 rules, 2 templates, 17 snippets, and 26 runnable examples** for Blender Python development targeting Blender 5.1 (current stable) with Blender 4.5 LTS fallback support.
39+
This repository ships **12 skills, 6 rules, 2 templates, 17 snippets, and 27 runnable examples** for Blender Python development targeting Blender 5.1 (current stable) with Blender 4.5 LTS fallback support.
4040

4141
The content is consumed by AI coding agents (Cursor, Claude Code, any MCP-capable client) when working on Blender add-ons, geometry nodes scripts, batch pipelines, or animation tooling. There is no build step. Edit the markdown and Python files directly.
4242

@@ -472,7 +472,7 @@ round-trips through the raw `POINT` buffer.
472472
</details>
473473

474474
<details>
475-
<summary><strong>Game asset pipeline</strong> — 3 examples</summary>
475+
<summary><strong>Game asset pipeline</strong> — 4 examples</summary>
476476

477477
<table>
478478
<tr>
@@ -525,6 +525,23 @@ boots really carry five influences, no vertex ends over the cap, weights still
525525
sum to one, the pose survives pruning, and the modifier is still exact linear
526526
blend skinning read back from the mesh's own deform layer.
527527

528+
</td>
529+
</tr>
530+
<tr>
531+
<td width="46%" valign="middle">
532+
<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>
533+
</td>
534+
<td valign="middle">
535+
536+
### [triangulate-tangents](examples/triangulate-tangents/)
537+
538+
The normal-mapping tangent-space contract: deterministic triangulation
539+
(`calc_tangents` aborts on any ngon), unit orthogonal tangent frames,
540+
`bitangent == sign * (n x t)`, and mikktspace matching the independent
541+
edge/UV-delta formula on smooth fields. Documents the planar-on-cylinder UV
542+
degeneracy (tangent collapses onto the normal) and the stale layer-handle
543+
hazard that silently corrupts measurements on 4.5.
544+
528545
</td>
529546
</tr>
530547
</table>

ROADMAP.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ Not committed; target list for the next content version. (v0.3.0 shipped the smo
106106
- ~~glTF export round-trip witness~~ **SHIPPED** as `examples/gltf-export-roundtrip/``export_yup` bakes `(x,y,z)→(x,z,−y)` into vertex data with no node rotation (probed identical on 4.5.11 and 5.1.2), `export_apply` ships the evaluated mesh (one disk vertex per evaluated loop), TEXCOORD_0 is V-flipped on disk, per-triangle material bindings survive; exporter/importer RNA signatures byte-identical between 4.5.11 and 5.1.2 (guarded against future renames); `Mesh.calc_normals()` removal on 5.x surfaced during authoring
107107
- ~~LOD decimate chain witness~~ **SHIPPED** as `examples/lod-decimate-chain/` — Decimate COLLAPSE evaluated through the depsgraph is non-destructive (obj.data keeps closed-form counts), evaluated tris hit `ratio × base` within 5% (measured 0.0–0.44%), silhouette bbox survives within 1e-3 (measured 7.7e-6); a stacked Decimate halves the effective ratio, an aggressive 0.02 ratio collapses the nose tip — both caught failure modes
108108
- ~~Vertex weight limit witness~~ **SHIPPED** as `examples/vertex-weight-limit/` — the 4-influence engine cap enforced via the data API (`v.groups` + `VertexGroup.remove` + renormalize); unit sums (measured 3e-8), pose preserved (4.9e-3), exact LBS from the mesh's own deform layer (2.7e-7), Root mount pinned
109-
- Triangulate + tangent-space witness: `mesh.calc_loop_triangles` + `calc_tangents` producing the deterministic loop topology and orthonormal tangent basis a game engine's normal mapping depends on — the fourth direction of the game-pipeline arc, identified but not built
109+
- ~~Triangulate + tangent-space witness~~ **SHIPPED** as `examples/triangulate-tangents/``calc_tangents` aborts on any ngon (back cap must be an explicit fan); mikktspace matches the edge/UV-delta formula within welding tolerance on smooth fields (2.3e-6 measured); planar UVs on a cylindrical wall collapse tangents onto normals (dot 0.998); `MeshUVLoopLayer` handle dangles across `calc_tangents()` on 4.5 (471 phantom flips, silent exit 0) while the mikktspace math is byte-identical on 4.5.11 and 5.1.2
110+
- UV-handle lifetime snippet: re-fetch attribute/UV layers by name after any CustomData-reallocating call (`calc_tangents`, `VertexGroup.add`, modifier edits) — held handles dangle silently on 4.5, survive by luck on 5.1 (found authoring `triangulate-tangents`)
110111
- glTF skinned-mesh export witness: follow-up to `gltf-export-roundtrip` + `vertex-weight-limit` — export a rigged mesh (weights + joints) to glTF and round-trip the skin, asserting influence counts and joint bindings survive the format
111112
- Degenerate-bevel weld hazard (snippet or rule): bevel width ≥ half a box dimension creates zero-area faces whose loops weld on glTF export (found authoring `gltf-export-roundtrip`, where the count check caught a 36-vertex weld)
112113
- GAMMA_CROSS blend-curve witness: the cross blend is not the naive linear mix (mid-cross measured (0.341, 0.349, 0.463) from crimson (0.85, 0.10, 0.22) and teal (0.06, 0.75, 0.80)) — derive and assert the gamma-space closed form per frame; follow-up to `vse-cut-list`
17.8 KB
Loading
39.4 KB
Loading

docs/gallery/index.html

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,17 @@ <h2><a href="vertex-weight-limit/">vertex-weight-limit</a></h2>
491491
<a class="card-link" href="vertex-weight-limit/">View example <span aria-hidden="true">&rarr;</span></a>
492492
</div>
493493
</article>
494+
<article class="card" data-tags="mesh uv">
495+
<a class="card-media" href="triangulate-tangents/" aria-label="triangulate-tangents example detail page">
496+
<img src="assets/triangulate-tangents-hero.webp" alt="triangulate-tangents — A machined buckler verifying the tangent-space contract a game engine&#x27;s normal mapping depends on." loading="lazy" decoding="async" />
497+
</a>
498+
<div class="card-body">
499+
<h2><a href="triangulate-tangents/">triangulate-tangents</a></h2>
500+
<p class="teaches">A machined buckler verifying the tangent-space contract a game engine&#x27;s normal mapping depends on. Deterministic triangulation, unit orthogonal tangent frames, and the edge/UV-delta formula matching mikktspace within welding tolerance.</p>
501+
<p class="witnesses"><span class="tag">witnesses</span> 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.</p>
502+
<a class="card-link" href="triangulate-tangents/">View example <span aria-hidden="true">&rarr;</span></a>
503+
</div>
504+
</article>
494505
</div>
495506
</main>
496507
<footer>

0 commit comments

Comments
 (0)