Skip to content

Commit 810d88f

Browse files
TMHSDigitalclaude
andcommitted
fix(showcase): anvil quality pass, forged body instead of stacked boxes
The anvil read as boxes glued in a stack (a sharp slab, a step block and a cone horn on a stem), foreshortened end-on on an orange toy bucket, with a block-on-a-stick hammer beside it. - Waist spreads to 0.54 of the face length (was 0.22) on a softer upper flare over seven rings, so the body carries the face. - Slab, table and foot chamfered; hole mouths stay sharp. - Horn grows from the table, top continuous, belly sweeping down (six rings, fewer than the waist's seven so waist_reach still finds it). - Staves weathered oak; hammer lofted with face, eye and peen blade on an oval waisted handle; hero turns the stand to 30 deg to show the profile. 3052 -> 3148 tris, 854 -> 902 metal faces, collider 134 -> 126, all in band; body 0.621 x 0.180, waist reach 0.8572 unchanged. Same on 5.2.1, 5.1.2 and 4.5.11 (4.5 render too); falsifiers match the pre-change run exactly (9, 15, 16, 16, 17, 18, 19). Contact-sheet luma 0.2095; asset sheet regenerated. Link gate: 122 pages, 0 failures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent f6f6995 commit 810d88f

9 files changed

Lines changed: 305 additions & 74 deletions

File tree

‎docs/gallery/anvil/index.html‎

Lines changed: 177 additions & 36 deletions
Large diffs are not rendered by default.
-856 Bytes
Loading
-1.74 KB
Loading
-704 Bytes
Loading

‎docs/gallery/index.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1174,7 +1174,7 @@ <h2><a href="wheelbarrow/">wheelbarrow</a></h2>
11741174
<div class="card-body">
11751175
<h2><a href="anvil/">anvil</a></h2>
11761176
<p class="teaches">A procedural blacksmith anvil on a timber stump through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.</p>
1177-
<p class="witnesses"><span class="tag">witnesses</span> Recomputed: 3052 tris, two materials with 1040 wood and 854 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.621×0.414×0.489 m, LOD ratios in band, convex collider 134 tris, hygiene 0, 16 grounded staves, hoop bite 0.003 m, foot-on-head gap 0.003 m, waist corner reach 0.8572, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z/--short-staves 16; --float-anvil 17; --round-band 18; --round-waist 19.</p>
1177+
<p class="witnesses"><span class="tag">witnesses</span> Recomputed: 3148 tris, two materials with 1040 wood and 902 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.621×0.414×0.489 m, LOD ratios in band, convex collider 126 tris, hygiene 0, 16 grounded staves, hoop bite 0.003 m, foot-on-head gap 0.003 m, waist corner reach 0.8572, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z/--short-staves 16; --float-anvil 17; --round-band 18; --round-waist 19.</p>
11781178
<a class="card-link" href="anvil/">View showcase piece<span class="sr-only"> anvil</span> <span aria-hidden="true">&rarr;</span></a>
11791179
</div>
11801180
</article>

‎showcase/anvil/README.md‎

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ materials, UVs, evaluated LOD, collider, or export file.
2828

2929
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
3030
| --- | --- | --- |
31-
| Base triangles | 2500–4500 | 3052 / 3052 / 3052 |
31+
| Base triangles | 2500–4500 | 3148 / 3148 / 3148 |
3232
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.5000 |
33-
| LOD2 ratio | 0.10–0.35 of base | 0.2195 / 0.2195 / 0.2195 |
34-
| Materials | exactly 2 distinct; ≥400 wood, ≥400 metal faces | 2 slots; 1040 / 854 |
33+
| LOD2 ratio | 0.10–0.35 of base | 0.2198 / 0.2198 / 0.2198 |
34+
| Materials | exactly 2 distinct; ≥400 wood, ≥400 metal faces | 2 slots; 1040 / 902 |
3535
| UVs | in `0..1`, AABB overlap ≤ 1e-5 | in range, overlap 0 |
3636
| Outer AABB | (0.621, 0.414, 0.489) m ± 0.015 | (0.6210, 0.4137, 0.4890), zmin 0 |
37-
| Collider tris | ≤ 280 | 134 |
38-
| Export | written, size > 0 | 248532 / 248532 / 248516 bytes |
37+
| Collider tris | ≤ 280 | 126 |
38+
| Export | written, size > 0 | 254976 / 254976 / 254960 bytes |
3939

4040
Base triangles dropped from **6616 to 2988** in the quality pass:
4141
overlapping face boxes and a beveled superellipse horn became one
@@ -77,7 +77,7 @@ Recomputed from the generated mesh, not asserted about the script.
7777

7878
The waist was lofted from ellipses, so under the rectangular face it read
7979
as a turned funnel, not a forged anvil. It is now a superellipse section
80-
(exponent 4.5) through five stations that flare from the foot in to a
80+
(exponent 4.5) through seven stations that flare from the foot in to a
8181
pinch and out to the body. **Waist form** measures corner reach at the
8282
narrowest ring: how far a section vertex fills its bounding rectangle's
8383
corner. `--round-waist` restores the ellipse and exits 19 (0.7071).
@@ -94,6 +94,27 @@ render-only cross-peen hammer lies on the floor beside the stump; and a
9494
low warm point light off frame stands in for the forge. The hammer is in
9595
the framing gate's element set, so it has to clear the frame edges too.
9696

97+
A later pass fixed the forging's read, which had been a stack of glued
98+
boxes:
99+
- The waist spreads to 0.54 of the face's length under it, on a softer
100+
flare above the pinch (power 1.35, was 2). At 0.22 it was a stem under
101+
a mushroom cap.
102+
- The slab, table and foot chamfer their corners (4 mm on the low mesh).
103+
Edges with both ends inside the slab outline are hole mouths and stay
104+
sharp.
105+
- The horn grows out of the table: its top runs on from the table top and
106+
its belly sweeps down over six rings. That is still fewer than the
107+
waist's seven, so the waist-form audit keeps finding the waist. A thin
108+
cone on the table's end face read as a funnel on a brick.
109+
- The staves are weathered oak rather than orange pine.
110+
- The hammer is lofted: a chamfered face, a swelled eye, and a peen that
111+
thins into a blade, on an oval, waisted handle.
112+
- The hero turns the stand to 30 degrees so the anvil shows its profile.
113+
At -42 its long axis pointed almost at the camera.
114+
115+
1942 faces, 3148 tris (was 3052), collider 126 (was 134); every budget and
116+
falsifier is unchanged.
117+
97118
### Falsifiers
98119

99120
Each violates one named budget. All seven were run on 4.5.11, 5.1.2 and

‎showcase/anvil/anvil.py‎

Lines changed: 99 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,7 @@ def build_anvil_iron(bm, z_head, float_anvil, bevel_offset, bevel_segments, roun
496496

497497
pr_half = PRITCHEL_R
498498
pr_cx = 0.5 * (x_prit0 + x_prit1)
499+
before = set(bm.verts)
499500
add_slotted_slab(
500501
bm,
501502
x_heel, x_face1,
@@ -521,37 +522,68 @@ def build_anvil_iron(bm, z_head, float_anvil, bevel_offset, bevel_segments, roun
521522
METAL_IDX,
522523
)
523524

524-
z_mid = 0.5 * ((z_body0 + 0.004) + (z_face - TABLE_DROP))
525-
horn_rings = [
526-
oval_ring(x_table1 - 0.018, hy * 0.80, 0.018, z_mid),
527-
oval_ring(x_table1 + 0.040, hy * 0.58, 0.015, z_mid - 0.004),
528-
oval_ring(x_table1 + 0.095, hy * 0.34, 0.011, z_mid - 0.010),
529-
oval_ring(x_table1 + 0.150, hy * 0.16, 0.008, z_mid - 0.016),
530-
oval_ring(x_horn1, 0.011, 0.008, z_mid - 0.022),
531-
]
532-
horn_verts, _faces = loft_open(bm, horn_rings, METAL_IDX, cap0=True, cap1=True)
533-
534525
add_box_solid(
535526
bm,
536527
-FOOT_XY[0] * 0.5, FOOT_XY[0] * 0.5,
537528
-FOOT_XY[1] * 0.5, FOOT_XY[1] * 0.5,
538529
z_foot0, z_foot1 + 0.004,
539530
METAL_IDX,
540531
)
541-
pinch_hy = 0.028
542-
pinch_hx = 0.055
532+
# Forged, not sawn: the face, table and foot were sharp-cornered boxes
533+
# and the anvil read as blocks glued in a stack. Chamfer their corners
534+
# (the hardy and pritchel holes keep sharp mouths: an edge with both ends
535+
# strictly inside the slab's outline is a hole edge and is left alone).
536+
if bevel_offset > 0.0:
537+
new = set(bm.verts) - before
538+
539+
def in_hole(v):
540+
x, y = v.co.x, v.co.y
541+
return x_heel + 1e-6 < x < x_face1 - 0.007 and abs(y) < hy - 1e-6
542+
543+
edges = [
544+
e for e in {e for v in new for e in v.link_edges}
545+
if len(e.link_faces) == 2 and e.calc_face_angle() > math.radians(60.0)
546+
and not (in_hole(e.verts[0]) and in_hole(e.verts[1]))
547+
]
548+
bmesh.ops.bevel(
549+
bm, geom=edges, offset=min(bevel_offset, 0.004), segments=bevel_segments,
550+
profile=0.5, affect="EDGES", clamp_overlap=True,
551+
)
552+
553+
# The horn grows out of the table: its top runs on from the table top and
554+
# its belly sweeps down, tapering to a blunt point. A thin cone set on
555+
# the table's end face read as a funnel stuck on a brick.
556+
z_top = z_face - TABLE_DROP
557+
horn_rings = []
558+
for dx, ry, rz, drop in (
559+
(-0.018, 0.86, 0.021, 0.000),
560+
(0.030, 0.70, 0.019, 0.002),
561+
(0.075, 0.52, 0.015, 0.005),
562+
(0.120, 0.36, 0.012, 0.009),
563+
(0.165, 0.22, 0.009, 0.013),
564+
):
565+
horn_rings.append(oval_ring(x_table1 + dx, hy * ry, rz, z_top - rz - drop))
566+
horn_rings.append(oval_ring(x_horn1, 0.009, 0.006, z_top - 0.006 - 0.019))
567+
horn_verts, _faces = loft_open(bm, horn_rings, METAL_IDX, cap0=True, cap1=True)
568+
569+
pinch_hy = 0.030
570+
pinch_hx = 0.060
543571
t_pinch = 0.48
544572
foot_hx, foot_hy = FOOT_XY[0] * 0.42, FOOT_XY[1] * 0.46
545-
body_hx, body_hy = FACE_LEN * 0.22, hy * 0.42
573+
# The waist carries the face: it spreads to most of the face's length
574+
# under it. At 0.22 of the face it was a stem under a mushroom cap.
575+
body_hx, body_hy = FACE_LEN * 0.54, hy * 0.84
546576
expo = 2.0 if round_waist else WAIST_P
547577
real_waist = []
548-
for t in (0.0, 0.24, t_pinch, 0.74, 1.0):
578+
for t in (0.0, 0.14, 0.30, t_pinch, 0.64, 0.82, 1.0):
549579
# concave flare: quadratic from the pinch out to each end
550580
if t <= t_pinch:
551581
k = ((t_pinch - t) / t_pinch) ** 2
552582
hx, hy_w = pinch_hx + (foot_hx - pinch_hx) * k, pinch_hy + (foot_hy - pinch_hy) * k
553583
else:
554-
k = ((t - t_pinch) / (1.0 - t_pinch)) ** 2
584+
# Shoulders: a softer power above the pinch, so the body swells
585+
# up into the face instead of flaring only at its last ring.
586+
k = ((t - t_pinch) / (1.0 - t_pinch)) ** 1.35
555587
hx, hy_w = pinch_hx + (body_hx - pinch_hx) * k, pinch_hy + (body_hy - pinch_hy) * k
556588
z = z_foot1 + (z_body0 + 0.002 - z_foot1) * t
557589
ring = []
@@ -1039,9 +1071,11 @@ def wood_material(name):
10391071
nt.links.new(shift.outputs["Vector"], noise.inputs["Vector"])
10401072
ramp = nt.nodes.new("ShaderNodeValToRGB")
10411073
ramp.color_ramp.elements[0].position = 0.30
1042-
ramp.color_ramp.elements[0].color = (0.12, 0.052, 0.018, 1.0)
1074+
# Smithy-weathered oak, not varnished pine: the saturated orange staves
1075+
# read as a toy bucket under Standard view.
1076+
ramp.color_ramp.elements[0].color = (0.085, 0.050, 0.026, 1.0)
10431077
ramp.color_ramp.elements[1].position = 0.72
1044-
ramp.color_ramp.elements[1].color = (0.38, 0.18, 0.065, 1.0)
1078+
ramp.color_ramp.elements[1].color = (0.29, 0.18, 0.095, 1.0)
10451079
nt.links.new(noise.outputs["Fac"], ramp.inputs["Fac"])
10461080
gain = nt.nodes.new("ShaderNodeMath")
10471081
gain.operation = "MULTIPLY_ADD"
@@ -1454,28 +1488,60 @@ def polish_face(mat):
14541488

14551489

14561490
def build_hammer(scene, steel, wood):
1457-
"""Render only: a cross-peen hammer lying on the floor against the stump."""
1491+
"""Render only: a cross-peen hammer lying on the floor against the stump.
1492+
1493+
The head is lofted along its own axis (Y) from a chamfered striking face
1494+
through a swelled eye to a peen that thins across the handle into a
1495+
blade; the handle is oval, waisted behind the head and swelling to the
1496+
grip. A box on a cylinder read as a mallet from a toy set.
1497+
"""
14581498
me = bpy.data.meshes.new("Hammer")
14591499
bm = bmesh.new()
14601500
try:
1461-
res = bmesh.ops.create_cone(bm, cap_ends=True, segments=12, radius1=0.013,
1462-
radius2=0.015, depth=0.34)
1463-
for v in res["verts"]:
1464-
v.co = Vector((v.co.z, v.co.y, v.co.x)) # handle along +X
1465-
head = bmesh.ops.create_cube(bm, size=1.0)
1466-
for v in head["verts"]:
1467-
v.co = Vector((v.co.x * 0.036 + 0.17, v.co.y * 0.115, v.co.z * 0.036))
1468-
head_faces = {f for v in head["verts"] for f in v.link_faces}
1501+
def octo(hx, hz):
1502+
c = 0.3
1503+
return [(hx, hz * (1 - c)), (hx * (1 - c), hz), (-hx * (1 - c), hz),
1504+
(-hx, hz * (1 - c)), (-hx, -hz * (1 - c)), (-hx * (1 - c), -hz),
1505+
(hx * (1 - c), -hz), (hx, -hz * (1 - c))]
1506+
1507+
def loft(rings, mat):
1508+
vs = [[bm.verts.new(p) for p in r] for r in rings]
1509+
n = len(vs[0])
1510+
for a_, b_ in zip(vs, vs[1:]):
1511+
for i in range(n):
1512+
j = (i + 1) % n
1513+
f = bm.faces.new((a_[i], a_[j], b_[j], b_[i]))
1514+
f.material_index = mat
1515+
for ring, rev in ((vs[0], True), (vs[-1], False)):
1516+
f = bm.faces.new(list(reversed(ring)) if rev else ring)
1517+
f.material_index = mat
1518+
return vs
1519+
1520+
# head: (y, half-x across the handle, half-z)
1521+
head = ((-0.060, 0.0150, 0.0150), (-0.056, 0.0172, 0.0172),
1522+
(-0.030, 0.0158, 0.0158), (0.000, 0.0185, 0.0185),
1523+
(0.022, 0.0160, 0.0170), (0.046, 0.0070, 0.0175),
1524+
(0.058, 0.0022, 0.0165))
1525+
loft([[Vector((0.17 + x, y, z)) for x, z in octo(hx, hz)] for y, hx, hz in head], 1)
1526+
# handle along +X, into the eye
1527+
handle = ((-0.170, 0.0150, 0.0110), (-0.140, 0.0165, 0.0120),
1528+
(-0.040, 0.0140, 0.0100), (0.100, 0.0120, 0.0088),
1529+
(0.165, 0.0135, 0.0095), (0.180, 0.0130, 0.0092))
1530+
rings = []
1531+
for x, ry, rz in handle:
1532+
rings.append([Vector((x, ry * math.cos(a), rz * math.sin(a)))
1533+
for a in (i * 2.0 * math.pi / 12 for i in range(12))])
1534+
loft(rings, 0)
1535+
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
14691536
for f in bm.faces:
1470-
f.material_index = 1 if f in head_faces else 0
1471-
f.smooth = f not in head_faces
1537+
f.smooth = f.material_index == 0
14721538
bm.to_mesh(me)
14731539
finally:
14741540
bm.free()
14751541
me.materials.append(wood)
14761542
me.materials.append(steel)
14771543
ob = bpy.data.objects.new("Hammer", me)
1478-
ob.location = (0.27, 0.10, 0.036)
1544+
ob.location = (0.27, 0.10, 0.0185)
14791545
ob.rotation_euler = (0.0, 0.0, math.radians(62.0))
14801546
scene.collection.objects.link(ob)
14811547
return ob
@@ -1488,7 +1554,10 @@ def render_still(low, _wood, _tex, path, engine):
14881554
ob.hide_render = True
14891555
ob.hide_viewport = True
14901556

1491-
low.rotation_euler.z = math.radians(-42.0)
1557+
# Profile to the lens: the camera sits at azimuth -56 degrees, and at -42
1558+
# the anvil's long axis pointed almost straight at it, foreshortening a
1559+
# 0.62 m forging into a stub. At 30 the horn points to frame right.
1560+
low.rotation_euler.z = math.radians(30.0)
14921561
low.rotation_euler.x = math.radians(2.0)
14931562

14941563
floor_me = bpy.data.meshes.new("Floor")

‎showcase/anvil/preview.webp‎

-1.75 KB
Loading

‎showcase/gallery.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@
197197
"category": "village",
198198
"teaches": "A procedural blacksmith anvil on a timber stump through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.",
199199
"alt": "A forged-steel anvil with a hammer-polished face and a flared, squared waist on a hooped timber stump, a cross-peen hammer on the floor and forge light on its flank.",
200-
"witnessesFix": "Recomputed: 3052 tris, two materials with 1040 wood and 854 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.621×0.414×0.489 m, LOD ratios in band, convex collider 134 tris, hygiene 0, 16 grounded staves, hoop bite 0.003 m, foot-on-head gap 0.003 m, waist corner reach 0.8572, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z/--short-staves 16; --float-anvil 17; --round-band 18; --round-waist 19.",
200+
"witnessesFix": "Recomputed: 3148 tris, two materials with 1040 wood and 902 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.621×0.414×0.489 m, LOD ratios in band, convex collider 126 tris, hygiene 0, 16 grounded staves, hoop bite 0.003 m, foot-on-head gap 0.003 m, waist corner reach 0.8572, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z/--short-staves 16; --float-anvil 17; --round-band 18; --round-waist 19.",
201201
"hero": "docs/gallery/assets/anvil-hero.webp",
202202
"preview": "showcase/anvil/preview.webp",
203203
"tags": [

0 commit comments

Comments
 (0)