Skip to content

Commit e81385b

Browse files
TMHSDigitalclaude
andcommitted
fix(showcase): restage wooden-ladder on a board wall with a hung rope coil
The ladder now leans on a render-only barn wall of staggered weathered boards instead of a flat grey block. A hand-coiled hemp rope hangs over the second-highest rung. The rung is found from the mesh the way rung_pitch finds rungs, and the coil is a teardrop laid from a three-lobed section, hung forward of the rung and away from the wall. The wood ramp is desaturated toward weathered timber. Geometry, budgets and falsifiers are unchanged; the wall and the coil are staging only. Evidence (live runs, local): - E:/Blender-Developer-Tools/.scratch/blender-5.2.1-windows-x64/blender.exe (Blender 5.2.1 LTS) and .../blender-4.5.11-windows-x64/blender.exe (Blender 4.5.11 LTS): default exit 0 on both. Falsifiers on both: --skip-decimate 9, --stray-vert 15, --twin-sole 15, --lift-z 16, --fat-rungs 17, --short-stile 18, --drift-rungs 19, all as documented. - --output renders on both versions (Cycles for the hero). Framing (5.2.1): fill y=0.883, margins l=0.416 r=0.416 b=0.044 t=0.072. - Contact sheet: candidate luma 0.211 / stage 0.202 / warmth +0.091 / sat 0.171, against armature-bend 0.243/0.192/-0.144/0.237, damped-track-aim 0.237/0.218/+0.105/0.178, and bmesh-gear 0.236/0.177/+0.303/0.250. Inside the band. - Asset sheet (docs/gallery/asset-sheets/wooden-ladder.webp): verdict is borderline. The bare ladder, thin by nature, is the plainest object in that lineup at sheet scale. Its model is unchanged here. Richer rung joinery such as lashings would break the exact 16-shell parts budget and has only 264 triangles of headroom, so it is left as a follow-up. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent c34861b commit e81385b

8 files changed

Lines changed: 155 additions & 13 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ Budget-conformance props. Not examples. Conventions: [`showcase/README.md`](show
133133
<a href="showcase/wall-torch/"><img src="showcase/wall-torch/preview.webp" width="24%" alt="Wall torch: a dressed stone plaque with an iron plate and arm holding a cup, a leaning wooden torch with a pitch-wrapped head and a bright flame, on a dark studio floor" /></a>
134134
<a href="showcase/tavern-stool/"><img src="showcase/tavern-stool/preview.webp" width="24%" alt="Tavern stool: a round wooden seat on turned splayed legs with stretchers at two heights and iron ferrules on flat treads, on a dark studio floor" /></a>
135135
<a href="showcase/iron-cauldron/"><img src="showcase/iron-cauldron/preview.webp" width="24%" alt="Iron cauldron: a round-bellied cast-iron pot hanging by its bail from a timber tripod on a dark studio floor" /></a>
136-
<a href="showcase/wooden-ladder/"><img src="showcase/wooden-ladder/preview.webp" width="24%" alt="Wooden ladder: a raked timber ladder with six evenly spaced rungs and iron shoes, leaning against a dark wall section" /></a>
136+
<a href="showcase/wooden-ladder/"><img src="showcase/wooden-ladder/preview.webp" width="24%" alt="Wooden ladder: a raked timber ladder with six evenly spaced turned rungs and iron shoes, leaning on a weathered barn-board wall with a coil of hemp rope hung over a rung" /></a>
137137
<a href="showcase/hay-bale/"><img src="showcase/hay-bale/preview.webp" width="24%" alt="Hay bale: a bound straw bale with two sisal twine belts on a dark studio floor" /></a>
138138
<a href="showcase/crate-stack/"><img src="showcase/crate-stack/preview.webp" width="24%" alt="Crate stack: three slatted shipping crates stacked and yawed, each with iron corner straps and runners, on a dark studio floor" /></a>
139139
<a href="showcase/stone-archway/"><img src="showcase/stone-archway/preview.webp" width="24%" alt="Stone archway: a semicircular masonry arch with coursed piers, projecting imposts, nine voussoirs and a proud keystone, on a dark studio floor" /></a>
49.1 KB
Loading
3.68 KB
Loading
2.91 KB
Loading

‎showcase/gallery.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@
326326
"name": "wooden-ladder",
327327
"dir": "showcase/wooden-ladder",
328328
"teaches": "A procedural timber ladder with raked tapered stiles, six turned rungs, ferrule caps, and level shoe plates through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.",
329-
"alt": "A wooden ladder with tapered stiles and evenly spaced round rungs, leaning against a dark wall section.",
329+
"alt": "A timber ladder with tapered stiles and six evenly spaced turned rungs, leaning on a weathered board wall with a coil of hemp rope hung over its second-highest rung.",
330330
"witnessesFix": "Recomputed: 2536 tris, two materials with 612 wood and 736 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 0.490×0.376×1.487 m, zmin 0, hygiene and coplanar pairs 0, tenon clearance 8 mm, shoe bite 20 mm, rung pitch even to 0.002 m, LOD ratios in band, convex collider 24 tris, non-empty glTF. --skip-decimate exits 9; --stray-vert and --twin-sole exit 15; --lift-z exits 16; --fat-rungs exits 17; --short-stile exits 18; --drift-rungs exits 19.",
331331
"hero": "docs/gallery/assets/wooden-ladder-hero.webp",
332332
"preview": "showcase/wooden-ladder/preview.webp",

‎showcase/wooden-ladder/README.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,16 @@ level shoe plates under sleeves that follow the rake. Then the shipped
77
pipeline: unique-cell UVs, Cycles high-to-low normal bake, LOD chain,
88
convex collider, Unity glTF export.
99

10+
**Hero.** The ladder leans on a render-only barn wall of staggered
11+
weathered boards (a Brick texture turned onto the wall plane, with a
12+
stretched noise for grain), and a hand-coiled rope hangs over the
13+
second-highest rung. The rung is found the way `rung_pitch` finds rungs:
14+
the wooden shells wide in X and short in Z. The coil is a teardrop laid
15+
from a three-lobed section, per the showcase rope rule. It hangs forward of
16+
the rung, away from the wall. The wall and the coil are staging, not part
17+
of the asset, its budgets or its export. The asset sheet judges the bare
18+
`LadderLow`.
19+
1020
It asserts **budget conformance** of the generated result. It does not
1121
witness an API contract. "It rendered without error" is not a check.
1222

6.83 KB
Loading

‎showcase/wooden-ladder/wooden_ladder.py‎

Lines changed: 143 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -990,9 +990,9 @@ def wood_material(name):
990990
nt.links.new(shift.outputs["Vector"], noise.inputs["Vector"])
991991
ramp = nt.nodes.new("ShaderNodeValToRGB")
992992
ramp.color_ramp.elements[0].position = 0.30
993-
ramp.color_ramp.elements[0].color = (0.12, 0.052, 0.018, 1.0)
993+
ramp.color_ramp.elements[0].color = (0.10, 0.056, 0.028, 1.0)
994994
ramp.color_ramp.elements[1].position = 0.72
995-
ramp.color_ramp.elements[1].color = (0.38, 0.18, 0.065, 1.0)
995+
ramp.color_ramp.elements[1].color = (0.33, 0.19, 0.095, 1.0)
996996
nt.links.new(noise.outputs["Fac"], ramp.inputs["Fac"])
997997
gain = nt.nodes.new("ShaderNodeMath")
998998
gain.operation = "MULTIPLY_ADD"
@@ -1423,6 +1423,136 @@ def wire_normal(mat, tex):
14231423
nt.links.new(nrm.outputs["Normal"], bsdf.inputs["Normal"])
14241424

14251425

1426+
def barn_wall_material():
1427+
"""Render-only lean surface: staggered weathered boards, dark seams."""
1428+
mat = bpy.data.materials.new("BarnBoards")
1429+
mat.use_nodes = True
1430+
nt = mat.node_tree
1431+
bsdf = nt.nodes["Principled BSDF"]
1432+
coord = nt.nodes.new("ShaderNodeTexCoord")
1433+
# Brick rows run along the texture's Y; turn the wall's Z onto it.
1434+
mapping = nt.nodes.new("ShaderNodeMapping")
1435+
mapping.inputs["Rotation"].default_value = (math.radians(90.0), 0.0, 0.0)
1436+
nt.links.new(coord.outputs["Object"], mapping.inputs["Vector"])
1437+
boards = nt.nodes.new("ShaderNodeTexBrick")
1438+
boards.offset = 0.37
1439+
boards.inputs["Scale"].default_value = 1.0
1440+
boards.inputs["Brick Width"].default_value = 1.35
1441+
boards.inputs["Row Height"].default_value = 0.17
1442+
boards.inputs["Mortar Size"].default_value = 0.006
1443+
boards.inputs["Mortar Smooth"].default_value = 0.3
1444+
boards.inputs["Bias"].default_value = 0.0
1445+
boards.inputs["Color1"].default_value = (0.070, 0.052, 0.038, 1.0)
1446+
boards.inputs["Color2"].default_value = (0.105, 0.080, 0.058, 1.0)
1447+
boards.inputs["Mortar"].default_value = (0.008, 0.007, 0.006, 1.0)
1448+
nt.links.new(mapping.outputs["Vector"], boards.inputs["Vector"])
1449+
grain = nt.nodes.new("ShaderNodeTexNoise")
1450+
grain.inputs["Scale"].default_value = 6.0
1451+
grain.inputs["Detail"].default_value = 8.0
1452+
stretch = nt.nodes.new("ShaderNodeMapping")
1453+
stretch.inputs["Scale"].default_value = (0.12, 1.0, 6.0)
1454+
nt.links.new(coord.outputs["Object"], stretch.inputs["Vector"])
1455+
nt.links.new(stretch.outputs["Vector"], grain.inputs["Vector"])
1456+
mix = nt.nodes.new("ShaderNodeMix")
1457+
mix.data_type = "RGBA"
1458+
mix.blend_type = "MULTIPLY"
1459+
_sock(mix.inputs, "Factor_Float").default_value = 0.55
1460+
nt.links.new(boards.outputs["Color"], _sock(mix.inputs, "A_Color"))
1461+
nt.links.new(grain.outputs["Color"], _sock(mix.inputs, "B_Color"))
1462+
nt.links.new(_sock(mix.outputs, "Result_Color"), bsdf.inputs["Base Color"])
1463+
bsdf.inputs["Roughness"].default_value = 0.88
1464+
bump = nt.nodes.new("ShaderNodeBump")
1465+
bump.inputs["Strength"].default_value = 0.6
1466+
bump.inputs["Distance"].default_value = 0.004
1467+
nt.links.new(boards.outputs["Fac"], bump.inputs["Height"])
1468+
nt.links.new(bump.outputs["Normal"], bsdf.inputs["Normal"])
1469+
return mat
1470+
1471+
1472+
def rope_coil(low, turns=3, loop_r=0.135, rope_r=0.012):
1473+
"""A hand-coiled rope hung over the second-highest rung (ladder frame).
1474+
1475+
Rungs are read off the mesh the way ``rung_pitch`` reads them: the
1476+
wooden shells wide across X and short in Z. The coil's loops hang in
1477+
the plane across the rung, their tops draped over it, and are laid
1478+
from a three-lobed section turned one step per ring, as the showcase
1479+
rope rule asks.
1480+
"""
1481+
me = low.data
1482+
rungs = []
1483+
for g in _vertex_shells(me):
1484+
pts = [me.vertices[i].co for i in g]
1485+
if (max(p.x for p in pts) - min(p.x for p in pts) > 0.25
1486+
and max(p.z for p in pts) - min(p.z for p in pts) < 0.08):
1487+
rungs.append(pts)
1488+
rungs.sort(key=lambda pts: sum(p.z for p in pts))
1489+
rung = rungs[-2]
1490+
rz = sum(p.z for p in rung) / len(rung)
1491+
ry = sum(p.y for p in rung) / len(rung)
1492+
rr = (max(p.z for p in rung) - min(p.z for p in rung)) / 2.0
1493+
# The wall stands on the side the rails rake toward; the coil hangs
1494+
# forward of the rung, away from it, so it never meets the boards.
1495+
top_band = [v.co.y for v in me.vertices if v.co.z > max(p.z for p in rung) + 0.3]
1496+
foot_band = [v.co.y for v in me.vertices if v.co.z < 0.08]
1497+
wall_side = 1.0 if sum(top_band) / len(top_band) > sum(foot_band) / len(foot_band) else -1.0
1498+
ztop = rz + rr + rope_r
1499+
x0 = 0.035
1500+
1501+
steps, sides = 36 * turns, 9
1502+
pitch = 2.3 * rope_r
1503+
1504+
def centre(i):
1505+
# a hanging loop is a teardrop: pinched over the rung, full below;
1506+
# it reaches far forward of the rung and barely behind it
1507+
t = i / 36.0 * 2.0 * math.pi
1508+
fan = 1.0 - 0.06 * (i / steps) # turns read as separate
1509+
s = math.sin(t)
1510+
reach = (loop_r * 0.9 if s > 0 else loop_r * 0.28) * fan
1511+
y = ry - wall_side * reach * s
1512+
z = ztop - loop_r * 1.35 * fan * (1.0 - math.cos(t))
1513+
return Vector((x0 + pitch * i / 36.0, y, z))
1514+
1515+
bm = bmesh.new()
1516+
try:
1517+
rings = []
1518+
for i in range(steps + 1):
1519+
c = centre(i)
1520+
tang = (centre(i + 1) - centre(i - 1)).normalized()
1521+
n = tang.cross(Vector((1.0, 0.0, 0.0))).normalized()
1522+
b = tang.cross(n)
1523+
ring = []
1524+
for k in range(sides):
1525+
a = 2 * math.pi * k / sides + i * (2 * math.pi / sides)
1526+
lobe = 1.0 + 0.16 * math.cos(3 * (2 * math.pi * k / sides))
1527+
ring.append(bm.verts.new(c + rope_r * lobe * (n * math.cos(a) + b * math.sin(a))))
1528+
rings.append(ring)
1529+
for r0, r1 in zip(rings, rings[1:]):
1530+
for k in range(sides):
1531+
f = bm.faces.new((r0[k], r0[(k + 1) % sides], r1[(k + 1) % sides], r1[k]))
1532+
f.smooth = True
1533+
bm.faces.new(list(reversed(rings[0])))
1534+
bm.faces.new(rings[-1])
1535+
cme = bpy.data.meshes.new("RopeCoil")
1536+
bm.to_mesh(cme)
1537+
finally:
1538+
bm.free()
1539+
rmat = bpy.data.materials.new("Hemp")
1540+
rmat.use_nodes = True
1541+
rnt = rmat.node_tree
1542+
rb = rnt.nodes["Principled BSDF"]
1543+
rnoise = rnt.nodes.new("ShaderNodeTexNoise")
1544+
rnoise.inputs["Scale"].default_value = 90.0
1545+
rnoise.inputs["Detail"].default_value = 6.0
1546+
rramp = rnt.nodes.new("ShaderNodeValToRGB")
1547+
rramp.color_ramp.elements[0].color = (0.16, 0.115, 0.065, 1.0)
1548+
rramp.color_ramp.elements[1].color = (0.40, 0.31, 0.18, 1.0)
1549+
rnt.links.new(rnoise.outputs["Fac"], rramp.inputs["Fac"])
1550+
rnt.links.new(rramp.outputs["Color"], rb.inputs["Base Color"])
1551+
rb.inputs["Roughness"].default_value = 0.9
1552+
cme.materials.append(rmat)
1553+
return bpy.data.objects.new("RopeCoil", cme)
1554+
1555+
14261556
def render_still(low, wood, tex, path, engine):
14271557
scene = bpy.context.scene
14281558
wire_normal(wood, tex)
@@ -1447,22 +1577,24 @@ def render_still(low, wood, tex, path, engine):
14471577
try:
14481578
bmesh.ops.create_cube(bm, size=1.0)
14491579
for v in bm.verts:
1450-
v.co.x *= 1.4
1580+
v.co.x *= 3.8
14511581
v.co.y = top_y + lean * (v.co.y + 0.5) * 0.10
1452-
v.co.z = (v.co.z + 0.5) * 1.75
1582+
v.co.z = (v.co.z + 0.5) * 1.95
14531583
bm.to_mesh(panel_me)
14541584
finally:
14551585
bm.free()
1456-
pmat = bpy.data.materials.new("LeanWall")
1457-
pmat.use_nodes = True
1458-
pb = pmat.node_tree.nodes["Principled BSDF"]
1459-
pb.inputs["Base Color"].default_value = (0.034, 0.032, 0.031, 1.0)
1460-
pb.inputs["Roughness"].default_value = 0.85
1461-
panel_me.materials.append(pmat)
1586+
panel_me.materials.append(barn_wall_material())
14621587
panel = bpy.data.objects.new("LeanWall", panel_me)
14631588
panel.rotation_euler.z = low.rotation_euler.z
14641589
scene.collection.objects.link(panel)
14651590

1591+
# A laid-rope coil hung over the second-highest rung: the ladder in use,
1592+
# not in a catalogue. Render-only, parented to the ladder so it turns
1593+
# with it; not part of the asset, its budgets or its export.
1594+
coil = rope_coil(low)
1595+
coil.parent = low
1596+
scene.collection.objects.link(coil)
1597+
14661598
floor_me = bpy.data.meshes.new("Floor")
14671599
bm = bmesh.new()
14681600
try:
@@ -1540,7 +1672,7 @@ def light(name, loc, energy, size, col, rot):
15401672
scene.view_settings.view_transform = "Standard"
15411673

15421674
fcode = gallery_framing.check_framing(
1543-
scene, cam, hero=[low], elements=[low], stage=[floor, wall, panel],
1675+
scene, cam, hero=[low], elements=[low, coil], stage=[floor, wall, panel],
15441676
)
15451677
if fcode:
15461678
return fcode

0 commit comments

Comments
 (0)