|
1 | 1 | # Car Mirror Symmetry |
2 | 2 |
|
3 | | -A runnable example that builds a generic hatchback as **one half** — a loft of |
4 | | -14 stations, each a 9-point half-ring from the bottom centerline out to the |
5 | | -roof centerline — and completes it with a **Mirror modifier**, evaluated |
| 3 | +A runnable example that builds a stylized hatchback as **one half** — a loft |
| 4 | +of 52 stations, each a 13-point half-ring from the bottom centerline out to |
| 5 | +the roof centerline — and completes it with a **Mirror modifier**, evaluated |
6 | 6 | through the depsgraph, following |
7 | 7 | [`depsgraph-and-evaluated-data`](../../skills/depsgraph-and-evaluated-data/SKILL.md). |
8 | | -Wheels and lamps are separate objects mirrored the idiomatic way: the object |
9 | | -origin sits **on** the symmetry plane and the mesh data is offset — mirror |
10 | | -mirrors about the object's own origin, so you offset the data, never the |
11 | | -object. |
| 8 | +Wheels, headlamps, taillamps, grille, door mirrors and door handles are |
| 9 | +separate objects mirrored the idiomatic way: the object origin sits **on** |
| 10 | +the symmetry plane and the mesh data is offset — mirror mirrors about the |
| 11 | +object's own origin, so you offset the data, never the object. The grille is |
| 12 | +the one part authored *across* the plane: a half profile whose end points lie |
| 13 | +exactly on `x = 0`, welded into one grille by the merge. |
12 | 14 |
|
13 | 15 | **What it witnesses:** the original datablock keeps only the authored half |
14 | 16 | while the depsgraph carries the mirrored whole, and both directions are |
15 | 17 | closed forms: |
16 | 18 |
|
17 | | -- **Original holds the half.** Body datablock is exactly 126 verts / 231 |
18 | | - edges / 106 faces (14 × 9, loft closed forms), with exactly 28 centerline |
19 | | - verts (probe: modifier applied into data → exit 3, datablock reads |
20 | | - `(224, 434, 212)`). |
21 | | -- **Evaluated is the welded whole.** Exactly `2n − c = 224` verts (probe: |
22 | | - `use_mirror_merge = False` → exit 5, **252** verts — the doubled seam; |
23 | | - probe: one centerline vert pulled off the plane → exit 4, **27 ≠ 28** |
24 | | - welded; probe: mirror axis off → exit 5, **126** — the half-car). The |
| 19 | +- **Original holds the half.** Body datablock is exactly 676 verts / 1289 |
| 20 | + edges / 614 faces (52 × 13, loft closed forms), with exactly 104 |
| 21 | + centerline verts (probe: modifier applied into data → exit 3, datablock |
| 22 | + reads `(1248, 2474, 1228)`). |
| 23 | +- **Evaluated is the welded whole.** Exactly `2n − c = 1248` verts (probe: |
| 24 | + `use_mirror_merge = False` → exit 5, **1352** verts — the doubled seam; |
| 25 | + probe: one centerline vert pulled off the plane → exit 4, **103 ≠ 104** |
| 26 | + welded; probe: mirror axis off → exit 5, **676** — the half-car). The |
25 | 27 | evaluated shell is watertight (every edge borders 2 faces) with Euler |
26 | 28 | characteristic **2** — the two halves weld into a topological sphere. |
27 | 29 | - **Exact ±X partners.** Every evaluated vertex has a partner at negated X: |
28 | 30 | measured deviation **0.000e+00** (mirror copies exactly; tol 2e-5 guards |
29 | 31 | float32), evaluated bbox symmetric (`min.x == −max.x`). |
30 | | -- **Mirrored parts.** Wheels (96/81 half → 192/162 evaluated) and lamps |
31 | | - (8/6 → 16/12) each double across the plane with partner deviation 0.0 — |
32 | | - and each object origin reads `x == 0`. |
| 32 | +- **Mirrored parts.** Offset parts double exactly: wheels 390/361 → |
| 33 | + 780/722, headlamp 64/50 → 128/100, taillamp and each handle 48/34 → |
| 34 | + 96/68, door mirror 80/66 → 160/132, partner deviation 0.0, every origin |
| 35 | + `x == 0` (probe: headlamp origin at `x = 0.01` → exit 12; probe: front |
| 36 | + wheel's mirror axis off → exit 14, `(390, 361)`). The split grille |
| 37 | + authors 36 verts with 8 on the plane and must evaluate to `2n − 8 = 64` |
| 38 | + with exactly 8 on-plane verts (probe: grille merge off → exit 14, **72** |
| 39 | + verts / 16 on the plane — two grilles; probe: one grille end point moved |
| 40 | + to `x = 0.004`, beyond the merge threshold → exit 13, 7 ≠ 8 on the plane). |
33 | 41 |
|
34 | 42 | **Why the checks target the modifier, not the base:** mutating a non-plane |
35 | 43 | base vertex cannot break the ±X pairing — the evaluated set is always |
36 | 44 | `half ∪ mirror(half)`, symmetric by construction. Realistic failures live in |
37 | 45 | the modifier (merge off, axis off, modifier applied into data, origin off |
38 | 46 | the plane), which is what the probes break. |
39 | 47 |
|
40 | | -**Version witness:** check output is byte-identical on Blender 4.5.11 LTS and |
41 | | -5.1.2. Mirror, `evaluated_get` / `to_mesh` / `to_mesh_clear` (no argument — |
42 | | -passing the mesh raises TypeError on both), and `TRACK_TO` constraint |
43 | | -behavior are stable across the pair; only the EEVEE engine id is |
44 | | -version-gated. |
| 48 | +**Version witness:** check output is byte-identical on Blender 4.5.11 LTS, |
| 49 | +5.1.2 and 5.2.1 LTS. Mirror, `evaluated_get` / `to_mesh` / `to_mesh_clear` |
| 50 | +(no argument — passing the mesh raises TypeError on both), `BVHTree`, and |
| 51 | +`TRACK_TO` constraint behavior are stable across them; only the EEVEE |
| 52 | +engine id is version-gated. |
45 | 53 |
|
46 | 54 | The render is the proof: hide the mirror and the still is literally half a |
47 | | -car — halved windshield and hood at the centerline, one headlamp. Render |
48 | | -notes: the loft is faceted by design (flat shading, no bevel modifier — the |
49 | | -modifier stack is Mirror only, so counts stay closed-form); the window band |
50 | | -is glass by construction class (steepest roof-rise slope is the windshield, |
51 | | -steepest drop the rear window, ring segment 5 the side windows), and the |
52 | | -glass is dielectric — metallic glass mirrors the key light and renders the |
53 | | -windshield as a hot salmon slab. The camera stands on the key light's side |
54 | | -of the car: from the other side it saw only the flank the key cannot reach, |
55 | | -and the whole visible side rendered near-black. The car is mirror-symmetric, |
56 | | -so the composition is the same either way. The bright streak along the cowl |
57 | | -below the windshield is the key's highlight in a crease that faces it. |
| 55 | +car — one headlamp, one door mirror, half a grille, a hood cut open at the |
| 56 | +centerline. The camera stands on the key light's side, high enough to look |
| 57 | +down the hood centerline so both halves read at once. The weld itself is |
| 58 | +not a pixel witness — the hood and roof are nearly flat across the plane, |
| 59 | +so an unwelded seam renders within 12/255 of the welded one; the `2n − c` |
| 60 | +count is what catches it. |
| 61 | + |
| 62 | +Render notes: the loft is smooth-shaded with sharp edges set on the |
| 63 | +authored half by dihedral angle (35°) and material border — no bevel or |
| 64 | +weighted-normal modifier, so the stack stays Mirror only and every count |
| 65 | +stays closed-form. Boundary edges on the plane take the angle to their own |
| 66 | +mirror image, `acos(1 − 2nₓ²)`, so the centerline shades smooth. The |
| 67 | +previous faceted loft had a thin pink-white streak on the hood by the |
| 68 | +windshield: not a seam (it sat at `|x|` 0.11–0.38, on the mirrored half, and |
| 69 | +all weld checks passed) but a sliver facet — the loft's greenhouse points |
| 70 | +collapsing onto the hood — flat-shaded and facing the key almost head on |
| 71 | +(`n · −key = 0.967`). The redesigned ring lerps a hood layout into a cabin |
| 72 | +layout, so no such sliver exists. Panel classes are by construction |
| 73 | +position: underbody, rocker and arch cladding are trim; side glass is the |
| 74 | +sill-to-glass-top segment under the roof, broken by a trim B-pillar; the |
| 75 | +drip rail becomes the A-pillar; windshield and hatch glass are the roof |
| 76 | +bands where the cabin factor ramps. The glass is a dark dielectric whose |
| 77 | +base colour ramps toward steel blue at grazing angles (Layer Weight → |
| 78 | +Color Ramp) — on a near-black stage there is little to reflect, and flat |
| 79 | +dark glass read as dull paint. Lamps, grille, mirrors and handles are |
| 80 | +closed superellipse pods seated on the skin by a `BVHTree` ray cast, with a |
| 81 | +chrome bezel and an inset lens rather than flat boxes. |
58 | 82 |
|
59 | 83 | ## Run |
60 | 84 |
|
@@ -90,10 +114,12 @@ against it. |
90 | 114 | | 10 | Mirror partner deviation above tolerance | |
91 | 115 | | 11 | Evaluated bbox not symmetric about X | |
92 | 116 | | 12 | Mirrored-part origin off the plane | |
93 | | -| 13 | Mirrored-part datablock is not the authored half | |
94 | | -| 14 | Mirrored-part evaluated counts did not double | |
| 117 | +| 13 | Mirrored-part datablock is not the authored half (verts, faces, on-plane verts) | |
| 118 | +| 14 | Mirrored-part evaluated counts ≠ `2n − weld` / `2f`, or on-plane verts ≠ weld | |
95 | 119 | | 15 | Mirrored-part partner check failed | |
96 | 120 | | 16 | Mirrored-part evaluated mesh stayed on one side | |
| 121 | +| 17 | Gallery framing violation (`--output` path only; `gallery_framing` returns 10, remapped) | |
| 122 | +| 18 | Asset-quality floor violation (`--output` path only; `gallery_asset_quality` returns 11, remapped) | |
97 | 123 |
|
98 | 124 | The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS |
99 | 125 | (5.1 on the weekly cron, the `needs-5.1` PR label, or manual dispatch). |
|
0 commit comments