Skip to content

Commit 1c272da

Browse files
TMHSDigitalclaude
andcommitted
fix(showcase): chopping-block quality pass: no lid, steel head, real billets and chips
The block read as a canister with a pale lid, the axe head as a black hole, the billets as striped planks and the chips as confetti. - End grain only on faces that look up (geometry normal z); the top chamfer and rim chips take the bark's cambium brown. Top weathered grey-brown instead of fresh pine. - Iron albedo ~0.1 -> ~0.3, roughness 0.30-0.55: the metallic head was mirroring the black stage. - Render-only dressing: each billet its own object, bark smooth and split flat, sawn ends ringed about its own pith; chips are tilted wedges, thick at the struck end, some with bark on top. Materials and dressing only; the asset mesh is untouched. Budgets pass on 5.2.1, 5.1.2 and 4.5.11 (4.5 render path too); every falsifier exits as before: --skip-decimate 9, --stray-vert 15, --lift-z 16, --fat-haft 17, --float-rivets 18, --round-band 18, --round-haft 19. Contact-sheet luma 0.266; asset sheet regenerated. Link gate: 122 pages, 0 failures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent b22173c commit 1c272da

7 files changed

Lines changed: 359 additions & 165 deletions

File tree

-232 Bytes
Loading
-54 Bytes
Loading

‎docs/gallery/chopping-block/index.html‎

Lines changed: 207 additions & 84 deletions
Large diffs are not rendered by default.
-220 Bytes
Loading

‎showcase/chopping-block/README.md‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,22 @@ bows away from the bit, and hooks toward it at the knob.
1616

1717
The hero sets the block to work: three split billets lie at its foot, each a
1818
quarter of a small round laid on one split face or the other so some show
19-
bark and some the split, among sixteen seeded chips. The dressing is
19+
bark and some the split, among twelve seeded chips. Each billet is its own
20+
object, smooth on the bark and flat on the split, with sawn ends ringed
21+
about its own pith; the chips are wedges, thick at the struck end and
22+
feathered at the tip, tilted as they fell, some with bark on top. Flat
23+
boxes read as confetti, and one striped mesh made the billets look like planks. The dressing is
2024
render-only with its own materials (the block's materials sample its baked
2125
normal map through UVs the dressing does not have). It is not part of the
2226
asset: no budget reads it, the export does not carry it, and the asset sheet
2327
renders the block alone.
2428

29+
The end grain only covers faces that look up. The top chamfer and rim chips
30+
lean out toward the bark and take its cambium brown; pale end grain wrapping
31+
down them drew a lid on a canister. The top is weathered grey-brown rather
32+
than fresh pine, and the iron's albedo sits near 0.3: at 0.1 the metallic
33+
head mirrored the black stage and read as a hole in the frame.
34+
2535
It asserts **budget conformance** of the generated result. It does not
2636
witness an API contract. "It rendered without error" is not a check.
2737

‎showcase/chopping-block/chopping_block.py‎

Lines changed: 141 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -927,14 +927,16 @@ def grain_material():
927927
nt,
928928
wave.outputs["Fac"],
929929
(
930-
(0.30, (0.600, 0.450, 0.265)),
931-
(0.72, (0.520, 0.370, 0.205)),
932-
(0.95, (0.400, 0.268, 0.140)),
930+
(0.30, (0.400, 0.300, 0.190)),
931+
(0.72, (0.335, 0.238, 0.140)),
932+
(0.95, (0.255, 0.168, 0.090)),
933933
),
934934
)
935935
weather = noise(nt, coord, 6.0, 4.0, 0.55)
936+
# A working block's top is weathered grey-brown, not fresh-sawn pine:
937+
# the pale face read as a cheese wheel set on a stump.
936938
base = mix_color(
937-
nt, rings, (0.330, 0.250, 0.170), remap(nt, weather, 0.45, 0.75, 0.0, 0.45)
939+
nt, rings, (0.235, 0.205, 0.170), remap(nt, weather, 0.40, 0.75, 0.20, 0.65)
938940
)
939941
# Radial checks at the angles the geometry notches, widening toward the
940942
# rim and fading out before the pith, as a drying check does.
@@ -957,8 +959,16 @@ def grain_material():
957959
line = remap(nt, nearest, 0.0, 1.0, 1.0, 0.0)
958960
fade = remap(nt, rad, 0.22 * BLOCK_R, 0.55 * BLOCK_R, 0.0, 1.0)
959961
crack = math_node(nt, "MULTIPLY", line, fade)
962+
cracked = mix_color(nt, base, (0.050, 0.032, 0.020), crack)
963+
# Only faces that look up are end grain. The top chamfer and the rim
964+
# chips lean out toward the bark, and pale end grain wrapping down them
965+
# drew a lid on a canister: there they take the bark's cambium brown.
966+
geo = nt.nodes.new("ShaderNodeNewGeometry")
967+
gsep = nt.nodes.new("ShaderNodeSeparateXYZ")
968+
nt.links.new(geo.outputs["Normal"], gsep.inputs["Vector"])
969+
side = remap(nt, gsep.outputs["Z"], 0.80, 0.93, 1.0, 0.0)
960970
nt.links.new(
961-
mix_color(nt, base, (0.050, 0.032, 0.020), crack), bsdf.inputs["Base Color"]
971+
mix_color(nt, cracked, (0.085, 0.055, 0.032), side), bsdf.inputs["Base Color"]
962972
)
963973
nt.links.new(
964974
remap(nt, wave.outputs["Fac"], 0.0, 1.0, 0.58, 0.74), bsdf.inputs["Roughness"]
@@ -978,15 +988,17 @@ def iron_material():
978988
nt,
979989
blot,
980990
(
981-
(0.40, (0.085, 0.082, 0.079)),
982-
(0.60, (0.140, 0.136, 0.131)),
983-
(0.72, (0.130, 0.100, 0.076)),
984-
(0.82, (0.180, 0.096, 0.052)),
991+
(0.40, (0.205, 0.200, 0.192)),
992+
(0.60, (0.330, 0.322, 0.310)),
993+
(0.72, (0.215, 0.165, 0.120)),
994+
(0.82, (0.240, 0.130, 0.070)),
985995
),
986996
),
987997
bsdf.inputs["Base Color"],
988998
)
989-
nt.links.new(remap(nt, blot, 0.35, 0.8, 0.34, 0.60), bsdf.inputs["Roughness"])
999+
# Albedo lifted from ~0.1 to ~0.3: at 0.1 a metallic head mirrored the
1000+
# black stage and the axe read as a hole cut in the frame.
1001+
nt.links.new(remap(nt, blot, 0.35, 0.8, 0.30, 0.55), bsdf.inputs["Roughness"])
9901002
nt.links.new(remap(nt, blot, 0.66, 0.80, 1.0, 0.35), bsdf.inputs["Metallic"])
9911003
return mat
9921004

@@ -1792,96 +1804,145 @@ def wire_normal(mat, tex):
17921804

17931805

17941806
def _dressing_materials():
1795-
"""Split face and bark for the render-only kindling and chips. Their own
1796-
materials: the block's are wired to its baked normal map through UVs the
1797-
dressing does not have."""
1798-
def mat(name, base, rough, stripes):
1807+
"""Split face, end grain, bark for the render-only kindling and chips.
1808+
Their own materials: the block's are wired to its baked normal map
1809+
through UVs the dressing does not have. All object-space, and each billet
1810+
is its own object, so its end rings centre on its own pith."""
1811+
def mat(name):
17991812
m = bpy.data.materials.new(name)
18001813
m.use_nodes = True
18011814
nt = m.node_tree
1802-
b = nt.nodes["Principled BSDF"]
1803-
b.inputs["Roughness"].default_value = rough
1804-
coord = nt.nodes.new("ShaderNodeTexCoord")
1805-
wave = nt.nodes.new("ShaderNodeTexWave")
1806-
wave.wave_type = "BANDS"
1807-
wave.bands_direction = "Z" if stripes else "X"
1808-
wave.inputs["Scale"].default_value = 6.0 if stripes else 2.0
1809-
wave.inputs["Distortion"].default_value = 6.0
1810-
wave.inputs["Detail"].default_value = 3.0
1811-
nt.links.new(coord.outputs["Object"], wave.inputs["Vector"])
1812-
ramp = nt.nodes.new("ShaderNodeValToRGB")
1813-
ramp.color_ramp.elements[0].color = tuple(c * 0.72 for c in base) + (1.0,)
1814-
ramp.color_ramp.elements[1].color = base + (1.0,)
1815-
nt.links.new(wave.outputs["Fac"], ramp.inputs["Fac"])
1816-
nt.links.new(ramp.outputs["Color"], b.inputs["Base Color"])
1817-
return m
1818-
split = mat("KindlingSplit", (0.62, 0.46, 0.28), 0.72, True)
1819-
bark = mat("KindlingBark", (0.16, 0.11, 0.075), 0.9, False)
1820-
return split, bark
1815+
return m, nt, nt.nodes["Principled BSDF"], nt.nodes.new("ShaderNodeTexCoord")
1816+
1817+
# Split face: fibres torn along the billet (noise squeezed across it).
1818+
split, nt, b, tc = mat("KindlingSplit")
1819+
fib = noise(nt, mapping(nt, tc.outputs["Object"], scale=(0.06, 1.0, 1.0)), 60.0, 5.0, 0.6)
1820+
nt.links.new(ramp(nt, fib, ((0.35, (0.30, 0.21, 0.12)), (0.52, (0.47, 0.34, 0.20)),
1821+
(0.70, (0.58, 0.44, 0.27)))), b.inputs["Base Color"])
1822+
b.inputs["Roughness"].default_value = 0.78
1823+
bump = nt.nodes.new("ShaderNodeBump")
1824+
bump.inputs["Strength"].default_value = 0.5
1825+
nt.links.new(fib, bump.inputs["Height"])
1826+
nt.links.new(bump.outputs["Normal"], b.inputs["Normal"])
1827+
1828+
# End grain: rings about the billet's own axis (local X), darker than the
1829+
# split face, as a sawn end is.
1830+
ends, nt, b, tc = mat("KindlingEnd")
1831+
wave = nt.nodes.new("ShaderNodeTexWave")
1832+
wave.wave_type = "RINGS"
1833+
wave.rings_direction = "SPHERICAL"
1834+
wave.inputs["Scale"].default_value = 26.0
1835+
wave.inputs["Distortion"].default_value = 2.0
1836+
nt.links.new(mapping(nt, tc.outputs["Object"], scale=(0.0, 1.0, 1.0)), wave.inputs["Vector"])
1837+
nt.links.new(ramp(nt, wave.outputs["Fac"], ((0.3, (0.48, 0.34, 0.19)),
1838+
(0.9, (0.33, 0.22, 0.12)))),
1839+
b.inputs["Base Color"])
1840+
b.inputs["Roughness"].default_value = 0.72
1841+
1842+
# Bark: plated and fissured, the block's palette.
1843+
bark, nt, b, tc = mat("KindlingBark")
1844+
plates = noise(nt, mapping(nt, tc.outputs["Object"], scale=(2.0, 9.0, 9.0)), 6.0, 8.0, 0.64)
1845+
nt.links.new(ramp(nt, plates, ((0.40, (0.020, 0.013, 0.008)), (0.55, (0.075, 0.048, 0.028)),
1846+
(0.80, (0.150, 0.100, 0.062)))), b.inputs["Base Color"])
1847+
b.inputs["Roughness"].default_value = 0.88
1848+
bump = nt.nodes.new("ShaderNodeBump")
1849+
bump.inputs["Strength"].default_value = 0.8
1850+
nt.links.new(plates, bump.inputs["Height"])
1851+
nt.links.new(bump.outputs["Normal"], b.inputs["Normal"])
1852+
return split, ends, bark
18211853

18221854

18231855
def add_split_kindling(scene):
18241856
"""Render-only dressing: split billets and chips at the block's foot.
18251857
18261858
A chopping block is read by what it splits. Each billet is a quarter of
1827-
a small round (two split faces meeting at the pith, bark on the arc),
1828-
laid on the floor; chips are thin seeded slivers. None of it is part of
1829-
the asset: no budget reads it, the export does not carry it, and the
1830-
asset sheet renders the block alone.
1859+
a small round (two split faces meeting at the pith, bark on the arc,
1860+
sawn ends showing rings), laid on the floor; chips are tapered wedges
1861+
tilted as they fell. None of it is part of the asset: no budget reads
1862+
it, the export does not carry it, and the asset sheet renders the block
1863+
alone. Returns the dressing objects.
18311864
"""
1832-
split, bark = _dressing_materials()
1865+
split, ends, bark = _dressing_materials()
18331866
rng = random.Random(71)
1834-
me = bpy.data.meshes.new("Kindling")
1835-
me.materials.append(split)
1836-
me.materials.append(bark)
1837-
bm = bmesh.new()
1838-
try:
1839-
def billet(cx, cy, yaw, r, length, roll):
1840-
segs = 6
1867+
obs = []
1868+
1869+
def emit(name, build, loc, rot):
1870+
me = bpy.data.meshes.new(name)
1871+
for m in (split, ends, bark):
1872+
me.materials.append(m)
1873+
bm = bmesh.new()
1874+
try:
1875+
build(bm)
1876+
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
1877+
bm.to_mesh(me)
1878+
finally:
1879+
bm.free()
1880+
ob = bpy.data.objects.new(name, me)
1881+
ob.location = loc
1882+
ob.rotation_euler = rot
1883+
scene.collection.objects.link(ob)
1884+
obs.append(ob)
1885+
1886+
def billet(name, cx, cy, yaw, r, length, roll):
1887+
# Quarter round about local X; roll 0 rests it on one split face,
1888+
# 90 degrees on the other, so one shows bark up and one a split face.
1889+
segs = 10
1890+
1891+
def build(bm):
18411892
rings = []
18421893
for end in (-0.5, 0.5):
1843-
ring = [Vector((end * length, 0.0, 0.0))]
1894+
ring = [bm.verts.new((end * length, 0.0, 0.0))]
18441895
for k in range(segs + 1):
18451896
a = (math.pi / 2.0) * k / segs
1846-
ring.append(Vector((end * length, r * math.cos(a), r * math.sin(a))))
1897+
# a little out of round, as split wood is
1898+
rr = r * (1.0 + 0.04 * math.sin(3.0 * a + end))
1899+
ring.append(bm.verts.new((end * length, rr * math.cos(a), rr * math.sin(a))))
18471900
rings.append(ring)
1848-
rot = Matrix.Rotation(yaw, 3, "Z") @ Matrix.Rotation(roll, 3, "X")
1849-
vs = [[bm.verts.new(rot @ p + Vector((cx, cy, 0.0))) for p in ring] for ring in rings]
1850-
a, b = vs
1901+
a, b = rings
18511902
n = len(a)
18521903
for k in range(n):
18531904
j = (k + 1) % n
18541905
f = bm.faces.new((a[k], a[j], b[j], b[k]))
1855-
# faces between arc vertices carry bark; the two through the
1856-
# pith are split faces
1857-
f.material_index = 1 if (k >= 1 and j >= 2) else 0
1858-
bm.faces.new(list(reversed(a))).material_index = 0
1859-
bm.faces.new(b).material_index = 0
1860-
1861-
# three billets beside the foot, two left and one right;
1862-
# roll 0 or +90 degrees lays a billet on one split face or the other, so\n # some show their bark and some their split face
1863-
billet(-0.45, -0.02, math.radians(62.0), 0.088, 0.31, 0.0)
1864-
billet(-0.43, -0.19, math.radians(38.0), 0.080, 0.28, math.radians(90.0))
1865-
billet(0.42, -0.12, math.radians(118.0), 0.084, 0.29, math.radians(90.0))
1866-
for i in range(16):
1867-
ang = rng.uniform(math.radians(200.0), math.radians(340.0))
1868-
d = rng.uniform(0.31, 0.42)
1906+
arc = k >= 1 and j >= 2
1907+
f.material_index = 2 if arc else 0
1908+
f.smooth = arc
1909+
bm.faces.new(list(reversed(a))).material_index = 1
1910+
bm.faces.new(b).material_index = 1
1911+
1912+
# rolled about local X, then yawed; roll 90 lifts the arc off the floor
1913+
emit(name, build, (cx, cy, 0.0), (roll, 0.0, yaw))
1914+
1915+
billet("Billet.A", -0.45, -0.02, math.radians(62.0), 0.088, 0.31, 0.0)
1916+
billet("Billet.B", -0.43, -0.19, math.radians(38.0), 0.080, 0.28, math.radians(90.0))
1917+
billet("Billet.C", 0.42, -0.12, math.radians(118.0), 0.084, 0.29, math.radians(90.0))
1918+
1919+
def chips(bm):
1920+
for i in range(12):
1921+
ang = rng.uniform(math.radians(205.0), math.radians(335.0))
1922+
d = rng.uniform(0.30, 0.46)
18691923
cx, cy = d * math.cos(ang), d * math.sin(ang)
1870-
sx, sy, sz = rng.uniform(0.020, 0.045), rng.uniform(0.008, 0.018), 0.003
1871-
geo = bmesh.ops.create_cube(bm, size=1.0)
1872-
rot = Matrix.Rotation(rng.uniform(0.0, math.pi), 3, "Z")
1873-
for v in geo["verts"]:
1874-
v.co = rot @ Vector((v.co.x * sx, v.co.y * sy, v.co.z * sz + sz / 2.0)) \
1875-
+ Vector((cx, cy, 0.0))
1876-
for f in {f for v in geo["verts"] for f in v.link_faces}:
1877-
f.material_index = 0
1878-
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
1879-
bm.to_mesh(me)
1880-
finally:
1881-
bm.free()
1882-
ob = bpy.data.objects.new("Kindling", me)
1883-
scene.collection.objects.link(ob)
1884-
return ob
1924+
lx, ly = rng.uniform(0.035, 0.070), rng.uniform(0.016, 0.030)
1925+
t0, t1 = rng.uniform(0.007, 0.011), 0.0018
1926+
# a wedge: full thickness at the struck end, feathered at the tip
1927+
pts = [(-lx / 2, -ly / 2, 0.0), (lx / 2, -ly / 2, 0.0),
1928+
(lx / 2, ly / 2, 0.0), (-lx / 2, ly / 2, 0.0),
1929+
(-lx / 2, -ly / 2, t0), (lx / 2, -ly * 0.4, t1),
1930+
(lx / 2, ly * 0.4, t1), (-lx / 2, ly / 2, t0)]
1931+
tilt = Matrix.Rotation(rng.uniform(-0.25, 0.25), 3, "X")
1932+
rot = Matrix.Rotation(rng.uniform(0.0, math.pi), 3, "Z") @ tilt
1933+
vs = [bm.verts.new(rot @ Vector(p) + Vector((cx, cy, 0.0))) for p in pts]
1934+
lift = -min(v.co.z for v in vs)
1935+
for v in vs:
1936+
v.co.z += lift
1937+
quads = ((0, 3, 2, 1), (4, 5, 6, 7), (0, 1, 5, 4),
1938+
(1, 2, 6, 5), (2, 3, 7, 6), (3, 0, 4, 7))
1939+
for q, quad in enumerate(quads):
1940+
f = bm.faces.new([vs[k] for k in quad])
1941+
# every third chip carries its bark on top
1942+
f.material_index = 2 if (q == 1 and i % 3 == 0) else 0
1943+
1944+
emit("Chips", chips, (0.0, 0.0, 0.0), (0.0, 0.0, 0.0))
1945+
return obs
18851946

18861947

18871948
def render_still(low, wood, tex, path, engine):
@@ -1974,7 +2035,7 @@ def light(name, loc, energy, size, col, rot):
19742035
scene.view_settings.view_transform = "Standard"
19752036

19762037
fcode = gallery_framing.check_framing(
1977-
scene, cam, hero=[low], elements=[low, kindling], stage=[floor, wall],
2038+
scene, cam, hero=[low], elements=[low, *kindling], stage=[floor, wall],
19782039
)
19792040
if fcode:
19802041
return fcode
164 Bytes
Loading

0 commit comments

Comments
 (0)