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 @@ -605,8 +605,8 @@ The compound-collision contract prop pipelines (engines generally,
FiveM/GTA-style prop workflows specifically) ingest: one convex hull piece
per part group, built with `bmesh.ops.convex_hull` from a coarse
`sec(π/n)`-inflated cage — never the dense render mesh, whose hull measures
368 faces, over the 255-face per-piece engine budget. Closed-form plane
tests prove containment (5.9e-08), convexity, watertightness, outward
380 faces, over the 255-face per-piece engine budget. Closed-form plane
tests prove containment (4.4e-08), convexity, watertightness, outward
winding, and Euler characteristic 2 per piece. Proud details cost cage rows;
concave grooves are free.

Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ Not committed; target list for the next content version. (v0.3.0 shipped the smo
- 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)
- ~~GAMMA_CROSS blend-curve witness~~ **SHIPPED** as `examples/vse-gamma-cross/` — the cross blends in a gamma-0.5 space: `((1-t)·√A + t·√B)²` with `t = (frame − start)/duration`, never 1 inside the effect; mid-cross dips 0.115 below the sRGB lerp from crimson/teal (closed form (0.341, 0.349, 0.463) confirmed per frame); AgX-default sampling poisons the fit (0.146 red-channel error, `view_transform='Standard'` mandatory); deleting a consumed input orphans-and-deletes the effect — follow-up to `vse-cut-list`
- Falsy `bpy_prop_collection` trap snippet: an empty collection is falsy, so `editor.strips or editor.sequences` silently falls through to the legacy accessor on an empty timeline — always branch on `hasattr`; likely generalizes across the API (found authoring `vse-cut-list`)
- ~~Collision compound witness~~ **SHIPPED** as `examples/collision-hull-proxy/` — game-prop collision as a compound of convex pieces, each a `bmesh.ops.convex_hull` of a coarse `sec(π/n)`-inflated cage (containment 5.9e-08, watertight, positive signed volume, Euler 2, per-piece 255-face budget: body 70, caps 60×3, compound 250); a hull of the dense render mesh measures 374 faces — over budget — which is why pipelines hull cages; proud details cost cage rows, concave grooves are free; byte-identical on 4.5.11 and 5.1.2
- ~~Collision compound witness~~ **SHIPPED** as `examples/collision-hull-proxy/` — game-prop collision as a compound of convex pieces, each a `bmesh.ops.convex_hull` of a coarse `sec(π/n)`-inflated cage (containment 4.4e-08, watertight, positive signed volume, Euler 2, per-piece 255-face budget: body 70, caps 60×3, compound 250); a hull of the dense render mesh measures 380 faces — over budget — which is why pipelines hull cages; proud details cost cage rows, concave grooves are free; byte-identical on 4.5.11 and 5.1.2
- ~~Custom-normals / shade-by-angle witness~~ **SHIPPED** as `examples/custom-normals-shade/` — the post-4.1 shading contract: `use_auto_smooth`/`use_custom_normals`/`calc_normals` are AttributeError on BOTH 4.5.11 and 5.1.2; `set_sharp_from_angle` sharp sets match an independent dihedral recompute exactly (188/388 edges, 3 meshes); evaluated normal welds/splits exact (0.0 dev); custom split normals survive depsgraph evaluation within int16 quantization (1.407e-04, not float-exact); **divergence**: the legacy `shade_auto_smooth` operator CANCELS headless on 4.5 ("Asset loading is unfinished", mesh untouched, no exception) while 5.1 FINISHES with the Smooth-by-Angle NODES modifier — the data API is the portable path
- prop-origin-transform witness — origin to base center, `transform_apply` through the data API, delta transforms, `matrix_parent_inverse` so parented children do not teleport; closed forms: post-apply scale exactly (1,1,1), local bbox min Z == 0, world bbox unchanged (builds on `parent-inverse-orrery`, does not duplicate it)
- mesh-hygiene-audit witness — the engine-ingest checklist as executable contract: no ngons, no loose vertices, no non-manifold edges, no zero-area faces, consistent outward winding; closed forms via Euler characteristic and exact edge-face incidence counts
Expand Down
Binary file modified docs/gallery/assets/collision-hull-proxy-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/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.
30 changes: 17 additions & 13 deletions docs/gallery/collision-hull-proxy/index.html

Large diffs are not rendered by default.

Binary file modified docs/gallery/contact-sheets/collision-hull-proxy-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/custom-normals-shade-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.
10 changes: 5 additions & 5 deletions docs/gallery/custom-normals-shade/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,7 @@ <h2>Source</h2>
<span class="c"># semi-gloss painted steel: rough enough to read as paint, glossy enough</span>
<span class="c"># that a highlight exposes every normal discontinuity — matte would hide</span>
<span class="c"># the very shading differences this render exists to show</span>
steel = make_material(<span class="s">&quot;OliveDrab&quot;</span>, (<span class="n">0.31</span>, <span class="n">0.35</span>, <span class="n">0.15</span>), <span class="n">0.8</span>, <span class="n">0.26</span>)
steel = make_material(<span class="s">&quot;OliveDrab&quot;</span>, (<span class="n">0.30</span>, <span class="n">0.345</span>, <span class="n">0.13</span>), <span class="n">0.8</span>, <span class="n">0.26</span>)
accent = make_material(<span class="s">&quot;FuelMarker&quot;</span>, (<span class="n">0.55</span>, <span class="n">0.06</span>, <span class="n">0.03</span>), <span class="n">0.3</span>, <span class="n">0.30</span>)

<span class="c"># the checked base can IS the by-angle variant; the two failure modes are</span>
Expand Down Expand Up @@ -712,16 +712,16 @@ <h2>Source</h2>

<span class="c"># key/fill/rim/wedge per docs/VISUAL-STYLE.md</span>
light(<span class="s">&quot;Key&quot;</span>, (-<span class="n">4.0</span>, -<span class="n">5.0</span>, <span class="n">6.0</span>), <span class="n">520.0</span>, <span class="n">4.5</span>, (<span class="n">1.0</span>, <span class="n">0.96</span>, <span class="n">0.9</span>), (<span class="n">50</span>, <span class="n">0</span>, -<span class="n">38</span>))
light(<span class="s">&quot;Fill&quot;</span>, (<span class="n">5.5</span>, -<span class="n">3.5</span>, <span class="n">2.5</span>), <span class="n">120.0</span>, <span class="n">9.0</span>, (<span class="n">0.75</span>, <span class="n">0.85</span>, <span class="n">1.0</span>), (<span class="n">65</span>, <span class="n">0</span>, <span class="n">55</span>))
light(<span class="s">&quot;Fill&quot;</span>, (<span class="n">5.5</span>, -<span class="n">3.5</span>, <span class="n">2.5</span>), <span class="n">150.0</span>, <span class="n">9.0</span>, (<span class="n">0.75</span>, <span class="n">0.85</span>, <span class="n">1.0</span>), (<span class="n">65</span>, <span class="n">0</span>, <span class="n">55</span>))
light(<span class="s">&quot;Rim&quot;</span>, (<span class="n">1.5</span>, <span class="n">4.5</span>, <span class="n">4.0</span>), <span class="n">340.0</span>, <span class="n">3.0</span>, (<span class="n">0.6</span>, <span class="n">0.78</span>, <span class="n">1.0</span>), (-<span class="n">58</span>, <span class="n">0</span>, <span class="n">170</span>))
light(<span class="s">&quot;Wedge&quot;</span>, (<span class="n">2.5</span>, <span class="n">5.5</span>, <span class="n">4.0</span>), <span class="n">400.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>))
<span class="c"># the shading audit light: a tall strip whose reflection runs down each</span>
<span class="c"># can face — it stair-steps on flat shading, warps at the rim on</span>
<span class="c"># smooth-everything, and stays straight with crisp edges on by-angle</span>
ld = bpy.data.lights.new(<span class="s">&quot;Strip&quot;</span>, <span class="s">&#x27;AREA&#x27;</span>)
ld.energy = <span class="n">460.0</span>
ld.energy = <span class="n">400.0</span>
ld.shape = <span class="s">&#x27;RECTANGLE&#x27;</span>
ld.size = <span class="n">1.0</span>
ld.size = <span class="n">1.35</span>
ld.size_y = <span class="n">8.0</span>
ld.color = (<span class="n">0.9</span>, <span class="n">0.95</span>, <span class="n">1.0</span>)
strip = bpy.data.objects.new(<span class="s">&quot;Strip&quot;</span>, ld)
Expand All @@ -743,7 +743,7 @@ <h2>Source</h2>

scene.render.engine = <span class="s">&#x27;CYCLES&#x27;</span> <span class="k">if</span> engine == <span class="s">&#x27;cycles&#x27;</span> <span class="k">else</span> eevee_engine_id()
<span class="k">if</span> engine == <span class="s">&#x27;cycles&#x27;</span>:
scene.cycles.samples = <span class="n">48</span>
scene.cycles.samples = <span class="n">64</span>
<span class="k">else</span>:
<span class="k">try</span>:
scene.eevee.taa_render_samples = <span class="n">64</span>
Expand Down
4 changes: 2 additions & 2 deletions docs/gallery/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -541,8 +541,8 @@ <h2><a href="light-link-studio/">light-link-studio</a></h2>
</a>
<div class="card-body">
<h2><a href="collision-hull-proxy/">collision-hull-proxy</a></h2>
<p class="teaches">A fire hydrant street prop inside its compound collision shell: four convex pieces hulled by bmesh.ops.convex_hull from a coarse inflated cage. The dense render mesh is never hulled - its hull would measure 368 faces, over the 255-face per-piece engine budget. Closed-form plane tests prove containment, convexity, watertightness, outward winding, and Euler characteristic 2 per piece.</p>
<p class="witnesses"><span class="tag">witnesses</span> A hull of the dense render mesh measures 368 faces, over the 255-face per-piece budget - the cage is a coarser lathe with rings inflated by sec(pi/n) so each cage ring circumscribes its render ring exactly (containment 5.9e-08). Proud details cost cage rows; concave grooves are free under the hull. Piece budgets: body 70, caps 60+60+60, compound 250.</p>
<p class="teaches">A fire hydrant street prop inside its compound collision shell: four convex pieces hulled by bmesh.ops.convex_hull from a coarse inflated cage. The dense render mesh is never hulled - its hull would measure 380 faces, over the 255-face per-piece engine budget. Closed-form plane tests prove containment, convexity, watertightness, outward winding, and Euler characteristic 2 per piece.</p>
<p class="witnesses"><span class="tag">witnesses</span> A hull of the dense render mesh measures 380 faces, over the 255-face per-piece budget - the cage is a coarser lathe with rings inflated by sec(pi/n) so each cage ring circumscribes its render ring exactly (containment 4.4e-08). Proud details cost cage rows; concave grooves are free under the hull. Piece budgets: body 70, caps 60+60+60, compound 250.</p>
<a class="card-link" href="collision-hull-proxy/">View example <span aria-hidden="true">&rarr;</span></a>
</div>
</article>
Expand Down
10 changes: 5 additions & 5 deletions examples/collision-hull-proxy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,27 @@ per-piece face limit (255 is the common cap). The example builds that
structure and derives every property from closed forms, per piece:

- **Containment.** Every render-mesh vertex lies on the inner side of every
face plane of its piece's hull — max excursion **5.9e-08** (tol 2e-4). This
face plane of its piece's hull — max excursion **4.4e-08** (tol 2e-4). This
holds by construction, not luck: the cage is a coarse lathe whose rings are
inflated by `sec(π/n)`, so each n-gon cage ring *circumscribes* its render
ring exactly. Proud details (a rim, a nut) must be paid for in cage rows;
concave details (the grooves) are free — the hull bridges over them. That
trade-off is the collision-authoring lesson.
- **Convexity.** The same plane test restricted to the hull's own vertices
(5.0e-08) — what a convex solver assumes when it uploads the piece.
(4.8e-08) — what a convex solver assumes when it uploads the piece.
- **Watertight + manifold.** Every hull edge borders exactly two faces, and
V − E + F = 2: a convex hull is a topological sphere.
- **Outward winding.** Positive signed volume by the divergence theorem
(body piece 0.968858) — inverted collision is a physics no-op.
(body piece 1.022381) — inverted collision is a physics no-op.
- **Budget.** Every piece ≤ 255 faces: body 70, pumper 60, side caps 60+60;
the compound totals 250. The naive approach fails this — a hull of the
*dense render mesh* measures **368 faces**, over budget, which is why real
*dense render mesh* measures **380 faces**, over budget, which is why real
pipelines hull a cage, never the render mesh.

**What each check catches on failure:** an artist editing the render mesh
after the hull was generated (probe: pumper lug stretched 2×, exit 3,
measured escape 0.730000); inverted hull winding (probe: polygons flipped,
exit 5, signed volume −0.968858, the exact negation); a non-watertight piece
exit 5, signed volume −1.022381, the exact negation); a non-watertight piece
(probe: one face deleted, exit 4, three border edges).

**Version witness:** output is byte-identical on Blender 4.5.11 LTS and
Expand Down
16 changes: 10 additions & 6 deletions examples/collision-hull-proxy/collision_hull_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,17 @@
(0.37, 0.205), # neck
(0.35, 0.320), # barrel
(0.346, 0.545),
(0.334, 0.555), # barrel groove (concave)
(0.334, 0.575),
(0.328, 0.555), # barrel groove (concave)
(0.328, 0.575),
(0.346, 0.590),
(0.345, 0.880), # barrel top
(0.348, 0.930),
(0.336, 0.950), # upper groove (concave)
(0.336, 1.000),
(0.348, 1.030),
(0.36, 1.090), # shoulder under bonnet
(0.388, 1.110), # bonnet brim (proud — paid for with a cage row)
(0.362, 1.132), # under-brim tuck (concave)
(0.35, 1.150), # bonnet dome (concave curve — cage keeps every row)
(0.30, 1.220),
(0.21, 1.280),
Expand All @@ -87,14 +89,15 @@
(0.37, 0.205),
(0.345, 0.880),
(0.36, 1.090),
(0.388, 1.110), # the brim is proud: it must appear in the cage
(0.35, 1.150),
(0.30, 1.220),
(0.21, 1.280),
(0.10, 1.315),
(0.00, 1.325),
]

NUT_PROFILE = [(0.09, 1.325), (0.072, 1.40), (0.0, 1.40)]
NUT_PROFILE = [(0.115, 1.325), (0.095, 1.44), (0.0, 1.44)]

# Side caps at z=0.76, pumper (bigger) at z=0.66. A cap's outlet direction
# equals its azimuth (degrees from +X); the camera sits near azimuth 292, so
Expand Down Expand Up @@ -198,6 +201,7 @@ def build_hydrant():
red = make_material("EnamelRed", (0.48, 0.035, 0.022), 0.15, 0.30)
yellow = make_material("EnamelYellow", (0.82, 0.53, 0.05), 0.15, 0.32)
iron = make_material("CastIron", (0.05, 0.052, 0.06), 0.9, 0.48)
iron_light = make_material("CastIronLight", (0.14, 0.145, 0.16), 0.85, 0.42)

inflate_body = 1.0 / math.cos(math.pi / CAGE_SEG_BODY)
inflate_cap = 1.0 / math.cos(math.pi / CAGE_SEG_CAP)
Expand All @@ -211,7 +215,7 @@ def build_hydrant():
if p.center.z > 1.09:
p.material_index = 1
nut = lathe_object("Nut", NUT_PROFILE, 5)
nut.data.materials.append(iron)
nut.data.materials.append(iron_light)
body_cage = lathe_object("BodyCage", CAGE_BODY_PROFILE, CAGE_SEG_BODY, inflate_body)
nut_cage = lathe_object("NutCage", NUT_PROFILE, CAGE_SEG_NUT, inflate_nut)
groups = [{"name": "body", "render": [body, nut], "cage": [body_cage, nut_cage]}]
Expand Down Expand Up @@ -439,7 +443,7 @@ def light(name, loc, energy, size, col, rot):

# key/fill/rim/wedge per docs/VISUAL-STYLE.md
light("Key", (-3.5, -4.5, 5.5), 500.0, 4.5, (1.0, 0.96, 0.9), (48, 0, -35))
light("Fill", (5.0, -3.5, 2.5), 110.0, 9.0, (0.75, 0.85, 1.0), (65, 0, 50))
light("Fill", (5.0, -3.5, 2.5), 130.0, 9.0, (0.75, 0.85, 1.0), (65, 0, 50))
light("Rim", (1.5, 4.5, 3.5), 300.0, 3.0, (0.6, 0.78, 1.0), (-55, 0, 170))
light("Wedge", (2.5, 5.5, 4.0), 380.0, 6.0, (1.0, 0.76, 0.5), (-68, 0, 190))
# small warm glint off the camera-left shoulder: lifts the pumper cap face
Expand All @@ -459,7 +463,7 @@ def light(name, loc, energy, size, col, rot):

scene.render.engine = 'CYCLES' if engine == 'cycles' else eevee_engine_id()
if engine == 'cycles':
scene.cycles.samples = 48
scene.cycles.samples = 64
else:
try:
scene.eevee.taa_render_samples = 64
Expand Down
Binary file modified examples/collision-hull-proxy/preview.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 5 additions & 5 deletions examples/custom-normals-shade/custom_normals_shade.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ def render_still(can, path, engine):
# semi-gloss painted steel: rough enough to read as paint, glossy enough
# that a highlight exposes every normal discontinuity — matte would hide
# the very shading differences this render exists to show
steel = make_material("OliveDrab", (0.31, 0.35, 0.15), 0.8, 0.26)
steel = make_material("OliveDrab", (0.30, 0.345, 0.13), 0.8, 0.26)
accent = make_material("FuelMarker", (0.55, 0.06, 0.03), 0.3, 0.30)

# the checked base can IS the by-angle variant; the two failure modes are
Expand Down Expand Up @@ -504,16 +504,16 @@ def light(name, loc, energy, size, col, rot):

# key/fill/rim/wedge per docs/VISUAL-STYLE.md
light("Key", (-4.0, -5.0, 6.0), 520.0, 4.5, (1.0, 0.96, 0.9), (50, 0, -38))
light("Fill", (5.5, -3.5, 2.5), 120.0, 9.0, (0.75, 0.85, 1.0), (65, 0, 55))
light("Fill", (5.5, -3.5, 2.5), 150.0, 9.0, (0.75, 0.85, 1.0), (65, 0, 55))
light("Rim", (1.5, 4.5, 4.0), 340.0, 3.0, (0.6, 0.78, 1.0), (-58, 0, 170))
light("Wedge", (2.5, 5.5, 4.0), 400.0, 6.0, (1.0, 0.76, 0.5), (-68, 0, 190))
# the shading audit light: a tall strip whose reflection runs down each
# can face — it stair-steps on flat shading, warps at the rim on
# smooth-everything, and stays straight with crisp edges on by-angle
ld = bpy.data.lights.new("Strip", 'AREA')
ld.energy = 460.0
ld.energy = 400.0
ld.shape = 'RECTANGLE'
ld.size = 1.0
ld.size = 1.35
ld.size_y = 8.0
ld.color = (0.9, 0.95, 1.0)
strip = bpy.data.objects.new("Strip", ld)
Expand All @@ -535,7 +535,7 @@ def light(name, loc, energy, size, col, rot):

scene.render.engine = 'CYCLES' if engine == 'cycles' else eevee_engine_id()
if engine == 'cycles':
scene.cycles.samples = 48
scene.cycles.samples = 64
else:
try:
scene.eevee.taa_render_samples = 64
Expand Down
Binary file modified examples/custom-normals-shade/preview.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions examples/gallery.json
Original file line number Diff line number Diff line change
Expand Up @@ -373,8 +373,8 @@
{
"name": "collision-hull-proxy",
"dir": "examples/collision-hull-proxy",
"teaches": "A fire hydrant street prop inside its compound collision shell: four convex pieces hulled by bmesh.ops.convex_hull from a coarse inflated cage. The dense render mesh is never hulled - its hull would measure 368 faces, over the 255-face per-piece engine budget. Closed-form plane tests prove containment, convexity, watertightness, outward winding, and Euler characteristic 2 per piece.",
"witnessesFix": "A hull of the dense render mesh measures 368 faces, over the 255-face per-piece budget - the cage is a coarser lathe with rings inflated by sec(pi/n) so each cage ring circumscribes its render ring exactly (containment 5.9e-08). Proud details cost cage rows; concave grooves are free under the hull. Piece budgets: body 70, caps 60+60+60, compound 250.",
"teaches": "A fire hydrant street prop inside its compound collision shell: four convex pieces hulled by bmesh.ops.convex_hull from a coarse inflated cage. The dense render mesh is never hulled - its hull would measure 380 faces, over the 255-face per-piece engine budget. Closed-form plane tests prove containment, convexity, watertightness, outward winding, and Euler characteristic 2 per piece.",
"witnessesFix": "A hull of the dense render mesh measures 380 faces, over the 255-face per-piece budget - the cage is a coarser lathe with rings inflated by sec(pi/n) so each cage ring circumscribes its render ring exactly (containment 4.4e-08). Proud details cost cage rows; concave grooves are free under the hull. Piece budgets: body 70, caps 60+60+60, compound 250.",
"hero": "docs/gallery/assets/collision-hull-proxy-hero.webp",
"preview": "examples/collision-hull-proxy/preview.webp",
"tags": [
Expand Down
Loading