Skip to content

Commit 8b83b01

Browse files
TMHSDigitalclaude
andcommitted
fix(examples): redesign car-mirror-symmetry as a smooth creased body with alloy wheels
The 13-point loft read faceted and lumpy at gallery size. After check() asserts every closed-form count on the Mirror-only stack, the --output path now stacks a level-2 Subdivision Surface modifier after MirrorHalf, so the welded whole is subdivided and the centerline stays on the plane. A crease_edge attribute keeps the panel breaks crisp: material borders crease fully and the fascia cap outlines half-crease. The body shades hard only at material borders. Other changes: - A chrome window-sill strip under the side glass, assigned by construction position (ring band 6 under the cabin). - Candy-red paint over a faint metallic base with a softened clear coat. - Half-metallic alloy rims, so the five spokes read on the dark stage. - A 72-groove tread bump on the tires, masked to the outward-facing band. - Relit: a larger key and a dim overhead softbox. The rim light is large, dim and steep: a small bright rim blew the roof out to a clipped white slab, and a shallow one printed its terminator line across the floor. The body datablock and every check are unchanged (676/1289/614 half, eval 1248/2474/1228). Evidence (live runs, local): - E:/Blender-Developer-Tools/.scratch/blender-5.2.1-windows-x64/blender.exe (Blender 5.2.1 LTS): check-only exit 0, body half=676/1289/614 centerline=104, eval=1248/2474/1228, euler=2, manifold, sym_dev=0, bbox_asym=0; all 8 mirrored parts pass. --no-mirror exits 5 ("evaluated body has 676 verts != 2n-c = 1248"). - E:/Blender-Developer-Tools/.scratch/blender-4.5.11-windows-x64/blender.exe (Blender 4.5.11 LTS): check-only exit 0 with the same numbers; the --output render path also exits 0 and matches the 5.2.1 still. - Framing: fill x=0.731 y=0.783, margins l=0.141 r=0.128 b=0.072 t=0.144. aq_ok (edge90 0.035). - Contact sheet (docs/gallery/contact-sheets/car-mirror-symmetry-contact-sheet.webp), generated on the worktree base with the previous armature-bend hero: candidate luma 0.234 / stage 0.192 / warmth +0.229 / sat 0.252, against damped-track-aim 0.151/0.145/+0.277/0.231 and bmesh-gear 0.236/0.177/+0.303/0.250. The candidate is in family on every criterion. - Asset sheet (docs/gallery/asset-sheets/car-mirror-symmetry.webp): beside the hydrant, drone, well, wheelbarrow and shelf, the smooth car with alloy wheels does not read as the least-designed object. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent f0500ce commit 8b83b01

8 files changed

Lines changed: 132 additions & 26 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1079,7 +1079,7 @@ smoothed render inverts on sight if the panel binding does.
10791079
</tr>
10801080
<tr>
10811081
<td width="46%" valign="middle">
1082-
<a href="examples/car-mirror-symmetry/"><img src="examples/car-mirror-symmetry/preview.webp" alt="Car mirror symmetry: a red stylized hatchback on a dark studio floor, tinted glass, black pillars and arch cladding, spoked wheels, and a matched headlamp and door-mirror pair either side of one centered grille — lofted as one half and completed by the Mirror modifier evaluated through the depsgraph, proving 2n-c counts, exact negated-X partners, a welded watertight centerline, and wheels mirrored about origins on the plane" /></a>
1082+
<a href="examples/car-mirror-symmetry/"><img src="examples/car-mirror-symmetry/preview.webp" alt="Car mirror symmetry: a smooth candy-red stylized hatchback on a dark studio floor, tinted glass, black pillars and arch cladding, a chrome window sill, silver five-spoke alloy wheels, and a matched headlamp and door-mirror pair either side of one centered grille — lofted as one half and completed by the Mirror modifier evaluated through the depsgraph, proving 2n-c counts, exact negated-X partners, a welded watertight centerline, and wheels mirrored about origins on the plane" /></a>
10831083
</td>
10841084
<td valign="middle">
10851085

-1.43 KB
Loading
4.68 KB
Loading
-10.8 KB
Loading

‎examples/car-mirror-symmetry/README.md‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,24 @@ not a pixel witness — the hood and roof are nearly flat across the plane,
5959
so an unwelded seam renders within 12/255 of the welded one; the `2n − c`
6060
count is what catches it.
6161

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
62+
Render notes: the 13-point loft reads faceted at gallery size, so the
63+
`--output` path — after `check()` has asserted every closed-form count on
64+
the Mirror-only stack — stacks a Subdivision Surface modifier (level 2)
65+
**after** `MirrorHalf`. Order matters: subdividing the half first would pull
66+
the centerline off the plane; subdividing the welded whole keeps it exact.
67+
Panel breaks stay crisp through a `crease_edge` attribute: every material
68+
border (glass, pillars, arch cladding, the chrome window-sill strip)
69+
creases fully and the fascia cap outlines half-crease, so the nose keeps
70+
its shape. The body is smooth-shaded with hard shading only at material
71+
borders; the separate parts keep a 40° dihedral split, and their boundary
72+
edges on the plane take the angle to their own mirror image,
73+
`acos(1 − 2nₓ²)`, so centerlines shade smooth. The paint is candy red over
74+
a faint metallic base under a softened clear coat; the rim light is large
75+
and dim and set steep — a small bright rim mirrors in the roof as a clipped
76+
white slab, and a shallow one prints its own terminator line across the
77+
floor. Tires carry a 72-groove tread bump masked to the outward-facing
78+
band, and the rims are a half-metallic alloy (a full metal mirrors the
79+
black stage and the spokes vanish). The
6780
previous faceted loft had a thin pink-white streak on the hood by the
6881
windshield: not a seam (it sat at `|x|` 0.11–0.38, on the mirrored half, and
6982
all weld checks passed) but a sliver facet — the loft's greenhouse points

‎examples/car-mirror-symmetry/car_mirror_symmetry.py‎

Lines changed: 112 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,8 @@ def _assign_body_materials(body):
394394
B-pillar interrupts it, paint C-pillar ends it); the drip rail above it
395395
is trim and becomes the A-pillar through the windshield; the roof bands
396396
are glass exactly where the cabin factor ramps (windshield, hatch)."""
397-
PAINT, GLASS, TRIM = (MATERIALS.index(m) for m in ("Paint", "Glass", "Trim"))
397+
PAINT, GLASS, TRIM, CHROME = (MATERIALS.index(m)
398+
for m in ("Paint", "Glass", "Trim", "Chrome"))
398399
for poly in body.data.polygons:
399400
i, k = divmod(poly.index, RING - 1)
400401
if i >= N_ST - 1: # cap ngons (front/rear fascia)
@@ -405,6 +406,8 @@ def _assign_body_materials(body):
405406
cab = cabin(ym) > 1e-6
406407
if k <= 3:
407408
poly.material_index = TRIM # underbody, rocker, arch cladding
409+
elif k == 6 and cab and ym < Y_CPILLAR:
410+
poly.material_index = CHROME # window-sill strip under the side glass
408411
elif k == 7 and cab and ym < Y_CPILLAR:
409412
in_b = Y_BPILLAR[0] <= ym <= Y_BPILLAR[1]
410413
poly.material_index = TRIM if in_b else GLASS
@@ -591,11 +594,13 @@ def principled(name):
591594
m.use_nodes = True
592595
return m.node_tree.nodes["Principled BSDF"]
593596
p = principled("Paint")
594-
p.inputs["Base Color"].default_value = (0.50, 0.016, 0.02, 1.0)
595-
p.inputs["Metallic"].default_value = 0.0
596-
p.inputs["Roughness"].default_value = 0.38
597-
p.inputs["Coat Weight"].default_value = 1.0 # clear coat: crisp light streaks
598-
p.inputs["Coat Roughness"].default_value = 0.06
597+
# candy red over a faint metallic base, under a glassy clear coat: the coat
598+
# carries the crisp strip-light streaks that describe the body's curvature
599+
p.inputs["Base Color"].default_value = (0.46, 0.012, 0.018, 1.0)
600+
p.inputs["Metallic"].default_value = 0.25
601+
p.inputs["Roughness"].default_value = 0.34
602+
p.inputs["Coat Weight"].default_value = 0.7
603+
p.inputs["Coat Roughness"].default_value = 0.10
599604
g = principled("Glass")
600605
# opaque dark dielectric, glossy (metallic glass mirrors the key across
601606
# the whole windshield as a hot slab). On a near-black stage there is
@@ -622,12 +627,49 @@ def principled(name):
622627
ch.inputs["Metallic"].default_value = 1.0
623628
ch.inputs["Roughness"].default_value = 0.2
624629
tire = principled("Tire")
625-
tire.inputs["Base Color"].default_value = (0.016, 0.016, 0.018, 1.0)
626-
tire.inputs["Roughness"].default_value = 0.82
630+
tire.inputs["Base Color"].default_value = (0.018, 0.018, 0.02, 1.0)
631+
tire.inputs["Roughness"].default_value = 0.78
632+
# tread: 72 transverse grooves around the axle (object X), bumped only
633+
# where the face looks outward from the axle, so the sidewalls stay clean
634+
tn = bpy.data.materials["Tire"].node_tree
635+
tc = tn.nodes.new("ShaderNodeTexCoord")
636+
mp = tn.nodes.new("ShaderNodeMapping")
637+
mp.inputs["Rotation"].default_value = (0.0, math.radians(90.0), 0.0)
638+
tn.links.new(tc.outputs["Object"], mp.inputs["Vector"])
639+
grad = tn.nodes.new("ShaderNodeTexGradient")
640+
grad.gradient_type = 'RADIAL' # angle about the axle, 0..1
641+
tn.links.new(mp.outputs["Vector"], grad.inputs["Vector"])
642+
643+
def math_node(op, a=None, b=None):
644+
n = tn.nodes.new("ShaderNodeMath")
645+
n.operation = op
646+
for i, v in enumerate((a, b)):
647+
if v is None:
648+
continue
649+
if isinstance(v, float):
650+
n.inputs[i].default_value = v
651+
else:
652+
tn.links.new(v, n.inputs[i])
653+
return n.outputs[0]
654+
655+
groove = math_node('LESS_THAN',
656+
math_node('FRACT', math_node('MULTIPLY', grad.outputs["Fac"], 72.0)),
657+
0.34)
658+
sep = tn.nodes.new("ShaderNodeSeparateXYZ")
659+
tn.links.new(tc.outputs["Normal"], sep.inputs["Vector"])
660+
tread_face = math_node('LESS_THAN', math_node('ABSOLUTE', sep.outputs["X"]), 0.5)
661+
bump = tn.nodes.new("ShaderNodeBump")
662+
bump.inputs["Strength"].default_value = 0.9
663+
bump.inputs["Distance"].default_value = 0.02
664+
tn.links.new(math_node('MULTIPLY', groove, tread_face), bump.inputs["Height"])
665+
tn.links.new(bump.outputs["Normal"], tire.inputs["Normal"])
627666
rim = principled("Rim")
628-
rim.inputs["Base Color"].default_value = (0.62, 0.64, 0.68, 1.0)
629-
rim.inputs["Metallic"].default_value = 1.0
630-
rim.inputs["Roughness"].default_value = 0.3
667+
# machined alloy: bright, slightly brushed, so the five spokes read
668+
# against the dark spoke gaps even in the thumbnail
669+
# (half metallic: a full metal on a near-black stage mirrors the black)
670+
rim.inputs["Base Color"].default_value = (0.70, 0.71, 0.74, 1.0)
671+
rim.inputs["Metallic"].default_value = 0.5
672+
rim.inputs["Roughness"].default_value = 0.26
631673
head = principled("Headlamp")
632674
head.inputs["Base Color"].default_value = (0.8, 0.84, 0.9, 1.0)
633675
head.inputs["Roughness"].default_value = 0.1
@@ -671,6 +713,40 @@ def _shade_smooth(ob, angle_deg=35.0):
671713
bm.free()
672714

673715

716+
def _subdivide_body(body):
717+
"""Render-path refinement: Catmull-Clark after the Mirror, with creases.
718+
719+
The 13-point loft is exactly the closed form the check counts, and it
720+
reads faceted at gallery size. A Subdivision Surface modifier stacked
721+
AFTER MirrorHalf smooths the welded whole (subdividing the half first
722+
would pull the centerline off the plane). Panel breaks stay crisp:
723+
material borders (glass, pillars, cladding, chrome sill) crease fully and
724+
the fascia cap outlines half-crease, so the nose and tail keep their
725+
shape instead of melting. Runs after check(), so the counts it asserts
726+
are the loft's, untouched."""
727+
bm = bmesh.new()
728+
try:
729+
bm.from_mesh(body.data)
730+
cl = (bm.edges.layers.float.get("crease_edge")
731+
or bm.edges.layers.float.new("crease_edge"))
732+
caps = {f for f in bm.faces if len(f.verts) > 4}
733+
for e in bm.edges:
734+
lf = e.link_faces
735+
c = 0.0
736+
if len(lf) == 2 and lf[0].material_index != lf[1].material_index:
737+
c = 1.0
738+
elif any(f in caps for f in lf):
739+
c = 0.5
740+
e[cl] = c
741+
bm.to_mesh(body.data)
742+
finally:
743+
bm.free()
744+
sub = body.modifiers.new("Refine", 'SUBSURF')
745+
sub.levels = 2
746+
sub.render_levels = 2
747+
sub.use_creases = True
748+
749+
674750
def render_still(objs, path, engine):
675751
# Shared Layer 1 gates (render path only) — see gallery_framing.py
676752
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir))
@@ -682,7 +758,10 @@ def render_still(objs, path, engine):
682758
_finish_materials()
683759
body = objs["body"]
684760
parts = [body] + [w for w, *_ in objs["mirrored"]]
685-
_shade_smooth(body)
761+
# subdivided body: hard shading only at material borders (a dihedral split
762+
# would print the loft's facets back onto the smoothed surface)
763+
_shade_smooth(body, 179.0)
764+
_subdivide_body(body)
686765
for ob in parts[1:]:
687766
_shade_smooth(ob, 40.0)
688767

@@ -711,19 +790,33 @@ def render_still(objs, path, engine):
711790
world.node_tree.nodes["Background"].inputs["Color"].default_value = (0.02, 0.021, 0.025, 1.0)
712791
scene.world = world
713792

714-
def light(name, loc, energy, size, col, rot):
793+
def light(name, loc, energy, size, col, rot=None, at=None, size_y=None):
715794
ld = bpy.data.lights.new(name, 'AREA')
716795
ld.energy = energy; ld.size = size; ld.color = col
796+
if size_y is not None:
797+
ld.shape = 'RECTANGLE'
798+
ld.size_y = size_y
717799
ob = bpy.data.objects.new(name, ld)
718800
ob.location = loc
719-
ob.rotation_euler = tuple(math.radians(a) for a in rot)
801+
if at is not None:
802+
ob.rotation_euler = (Vector(at) - Vector(loc)).to_track_quat('-Z', 'Y').to_euler()
803+
else:
804+
ob.rotation_euler = tuple(math.radians(a) for a in rot)
720805
scene.collection.objects.link(ob)
721806

722-
# default-stage rig per docs/VISUAL-STYLE.md
723-
light("Key", (-4.0, -5.0, 6.0), 520.0, 5.0, (1.0, 0.96, 0.9), (48, 0, -35))
724-
light("Fill", (5.0, -3.5, 2.5), 110.0, 9.0, (0.75, 0.85, 1.0), (65, 0, 50))
725-
light("Rim", (3.0, 4.5, 5.0), 320.0, 4.0, (0.6, 0.78, 1.0), (-55, 0, 155))
726-
light("Wedge", (2.5, 5.5, 4.0), 420.0, 6.0, (1.0, 0.76, 0.5), (-68, 0, 190))
807+
# default-stage rig per docs/VISUAL-STYLE.md, plus a long overhead strip:
808+
# its reflection runs down the clear-coated hood and roof as one crisp
809+
# streak, which is what makes a car body read as a car body
810+
light("Key", (-4.0, -5.0, 6.0), 480.0, 7.5, (1.0, 0.95, 0.88), (48, 0, -35))
811+
light("Fill", (5.0, -3.5, 2.5), 55.0, 9.0, (0.75, 0.85, 1.0), (65, 0, 50))
812+
# rim: big and dim, so the clear-coated roof mirrors it as a sheen, not a
813+
# clipped white slab (a small bright rim blows the roof out)
814+
# (steep, so the light's own plane meets the floor behind the back wall:
815+
# a shallow area light prints a hard terminator line across the floor)
816+
light("Rim", (2.5, 3.0, 4.5), 220.0, 7.0, (0.62, 0.78, 1.0), at=(0.0, 0.3, 0.8))
817+
light("Strip", (-1.6, -0.3, 4.6), 110.0, 2.2, (1.0, 0.98, 0.95),
818+
at=(-0.6, -0.3, 0.0), size_y=6.0)
819+
light("Wedge", (2.5, 5.5, 4.0), 520.0, 6.0, (1.0, 0.72, 0.45), (-68, 0, 190))
727820

728821
aim = bpy.data.objects.new("Aim", None)
729822
aim.location = (0.0, -0.3, 0.62)
4.07 KB
Loading

‎examples/gallery.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,7 @@
530530
"name": "car-mirror-symmetry",
531531
"dir": "examples/car-mirror-symmetry",
532532
"teaches": "A stylized hatchback lofted as one half (52 stations, 13-point rings) and completed by the Mirror modifier, evaluated through the depsgraph. Wheels, lamps, grille, door mirrors and handles mirror about object origins parked on the symmetry plane; the grille is authored as a half and welded on it.",
533-
"alt": "A red stylized hatchback in front three-quarter view with tinted glass, black pillars, spoked wheels, and matched headlamps and door mirrors either side of one centered grille.",
533+
"alt": "A smooth candy-red stylized hatchback in front three-quarter view with tinted glass, black pillars, a chrome window sill, silver five-spoke alloy wheels, and matched headlamps and door mirrors either side of one centered grille.",
534534
"witnessesFix": "Mirror is a modifier, not applied data: the original keeps the authored half while the evaluated mesh is the welded, watertight, exactly symmetric whole.",
535535
"hero": "docs/gallery/assets/car-mirror-symmetry-hero.webp",
536536
"preview": "examples/car-mirror-symmetry/preview.webp",

0 commit comments

Comments
 (0)