Skip to content

Commit c34861b

Browse files
TMHSDigitalclaude
andcommitted
fix(showcase): redesign fence-kit hero as a weathered three-section run
The hero now shows the kit doing its job. Three linked duplicates of the one checked mesh stand at the 1.60 m tile pitch, so every joint in the picture is the tile fit the budgets assert: paired posts meet band to band, KIT_CLEAR apart. The run stands in a render-only packed-earth strip, and the wood weathers in broad silver-grey patches (object-space noise) over the per-member tone and grain. Geometry, budgets and falsifiers are unchanged. The strip and duplicates are staging, and the asset sheet judges the single FenceLow section. scripts/asset_sheet.py gains SELECT rows for fence-kit, wooden-ladder and terrain-scatter, which had none, so the asset-sheet gate could not run on them. 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, --lift-z 16, --short-shoes 16, --short-brace 17, --gap-rails 18, --long-rails 19, --float-brace 20, --wide-tile 21, all as documented. - --output renders on both versions. Framing (5.2.1): fill x=0.894 y=0.678, margins l=0.066 r=0.041 b=0.133 t=0.189. - Contact sheet: candidate luma 0.230 / stage 0.213 / warmth +0.135 / sat 0.184, 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. Every axis is inside the calibration band. - Asset sheet (docs/gallery/asset-sheets/fence-kit.webp): the single section reads on a par with the well and the wheelbarrow and is not the least-designed object in the lineup. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent c95e0ce commit c34861b

9 files changed

Lines changed: 95 additions & 11 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ Budget-conformance props. Not examples. Conventions: [`showcase/README.md`](show
117117
<a href="showcase/street-lantern/"><img src="showcase/street-lantern/preview.webp" width="24%" alt="Street lantern: an iron post with aged brass collars, a knee brace tenoned into post and arm, and a muntined amber cage on a dark studio floor" /></a>
118118
<a href="showcase/treasure-chest/"><img src="showcase/treasure-chest/preview.webp" width="24%" alt="Treasure chest: a slatted wooden chest with iron bands, corner brackets, and an open lid on a dark studio floor" /></a>
119119
<a href="showcase/terrain-scatter/"><img src="showcase/terrain-scatter/preview.webp" width="24%" alt="Terrain scatter: a Geometry Nodes dirt hill tile with bevelled masonry rocks on a dark studio floor" /></a>
120-
<a href="showcase/fence-kit/"><img src="showcase/fence-kit/preview.webp" width="24%" alt="Fence kit: a grained timber post-and-rail section with pyramidal caps, a diagonal board brace, and iron shoes and bands on a dark studio floor" /></a>
120+
<a href="showcase/fence-kit/"><img src="showcase/fence-kit/preview.webp" width="24%" alt="Fence kit: a run of three weathered post-and-rail sections on a packed-earth strip, joined post to post at the 1.60 m tile pitch, with pyramidal caps, diagonal board braces, and iron bands" /></a>
121121
<a href="showcase/watchtower/"><img src="showcase/watchtower/preview.webp" width="24%" alt="Watchtower: a grained timber lookout with X-braces, hatch ladder, a plank platform on deck girts, and a coursed shake roof on a dark studio floor" /></a>
122122
<a href="showcase/cart/"><img src="showcase/cart/preview.webp" width="24%" alt="Cart: a two-wheel wooden cart with spoked wheels, slatted bed, and shafts on a dark studio floor" /></a>
123123
<a href="showcase/park-bench/"><img src="showcase/park-bench/preview.webp" width="24%" alt="Park bench: a wrought-iron bench whose round-bar legs curl into scrolled feet, with wooden slats and a reclined back, on a dark studio floor" /></a>
55.7 KB
Loading
14.3 KB
Loading
4.11 KB
Loading

‎scripts/asset_sheet.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@
6767
"grain-sacks": (r"^SacksLow$", None),
6868
"rope-bridge": (r"^BridgeLow$", None),
6969
"wheelbarrow": (r"^BarrowLow$", None),
70+
# the hero shows a three-section run; the sheet judges the one asset
71+
"fence-kit": (r"^FenceLow$", None),
72+
"wooden-ladder": (r"^LadderLow$", None),
73+
"terrain-scatter": (r"^TerrainLow$", None),
7074
"triangulate-tangents": (r"^Buckler(\.Rivets)?$", None),
7175
"car-mirror-symmetry": (r"^(CarBody|Wheel(Front|Rear)|Headlamp|Taillamp|Grille"
7276
r"|DoorMirror|DoorHandle(Front|Rear))$", None),

‎showcase/fence-kit/README.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ rail and housed 12 mm into both posts. It clears the bottom rail at the left
1717
post and the top rail at the right one. Rails and kick tenon into the post
1818
volume.
1919

20+
**Hero.** The still shows the kit doing its job: three linked duplicates of
21+
the one checked mesh at the 1.60 m pitch, so every joint in the picture is
22+
the tile fit the budgets assert — the paired posts meet band to band,
23+
`KIT_CLEAR` apart. The run stands in a render-only packed-earth strip, and
24+
the wood weathers in broad silver-grey patches (object-space noise) over
25+
the per-member tone and grain. The strip and the duplicates are staging;
26+
the asset sheet judges the single `FenceLow` section.
27+
2028
It asserts **budget conformance** of the generated result. It does not
2129
witness an API contract. "It rendered without error" is not a check. This
2230
piece does not re-witness the `modular-kit-snap` snap contract.

‎showcase/fence-kit/fence_kit.py‎

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -590,9 +590,9 @@ def wood_material(name):
590590
nt.links.new(shift.outputs["Vector"], noise.inputs["Vector"])
591591
ramp = nt.nodes.new("ShaderNodeValToRGB")
592592
ramp.color_ramp.elements[0].position = 0.30
593-
ramp.color_ramp.elements[0].color = (0.12, 0.052, 0.018, 1.0)
593+
ramp.color_ramp.elements[0].color = (0.085, 0.052, 0.030, 1.0)
594594
ramp.color_ramp.elements[1].position = 0.72
595-
ramp.color_ramp.elements[1].color = (0.38, 0.18, 0.065, 1.0)
595+
ramp.color_ramp.elements[1].color = (0.30, 0.19, 0.105, 1.0)
596596
nt.links.new(noise.outputs["Fac"], ramp.inputs["Fac"])
597597
gain = nt.nodes.new("ShaderNodeMath")
598598
gain.operation = "MULTIPLY_ADD"
@@ -605,7 +605,23 @@ def wood_material(name):
605605
_sock(mix.inputs, "Factor_Float").default_value = 1.0
606606
nt.links.new(ramp.outputs["Color"], _sock(mix.inputs, "A_Color"))
607607
nt.links.new(gain.outputs["Value"], _sock(mix.inputs, "B_Color"))
608-
nt.links.new(_sock(mix.outputs, "Result_Color"), bsdf.inputs["Base Color"])
608+
# Weathering: outdoor timber silvers in broad patches, not uniformly.
609+
# A coarse object-space noise pulls the grain toward weathered grey.
610+
silver = nt.nodes.new("ShaderNodeTexNoise")
611+
silver.inputs["Scale"].default_value = 2.4
612+
silver.inputs["Detail"].default_value = 4.0
613+
nt.links.new(coord.outputs["Object"], silver.inputs["Vector"])
614+
patch = nt.nodes.new("ShaderNodeMapRange")
615+
patch.inputs["From Min"].default_value = 0.42
616+
patch.inputs["From Max"].default_value = 0.68
617+
patch.inputs["To Max"].default_value = 0.55
618+
nt.links.new(silver.outputs["Fac"], patch.inputs["Value"])
619+
weather = nt.nodes.new("ShaderNodeMix")
620+
weather.data_type = "RGBA"
621+
_sock(weather.inputs, "B_Color").default_value = (0.20, 0.19, 0.17, 1.0)
622+
nt.links.new(patch.outputs["Result"], _sock(weather.inputs, "Factor_Float"))
623+
nt.links.new(_sock(mix.outputs, "Result_Color"), _sock(weather.inputs, "A_Color"))
624+
nt.links.new(_sock(weather.outputs, "Result_Color"), bsdf.inputs["Base Color"])
609625
rough = nt.nodes.new("ShaderNodeMapRange")
610626
rough.inputs["To Min"].default_value = 0.72
611627
rough.inputs["To Max"].default_value = 0.52
@@ -1246,8 +1262,64 @@ def render_still(low, wood, tex, path, engine):
12461262
ob.hide_render = True
12471263
ob.hide_viewport = True
12481264

1249-
# Level on the floor: an X tilt sinks one face of the shoes.
1250-
low.rotation_euler.z = math.radians(-16.0)
1265+
# The hero is the kit doing its job: three sections at the tile pitch,
1266+
# linked duplicates of the one checked mesh, so every joint in the
1267+
# picture is the tile fit the check asserts (posts meet band to band,
1268+
# KIT_CLEAR apart). A run parent carries the yaw; level on the floor,
1269+
# since an X tilt sinks one face of the shoes.
1270+
run = bpy.data.objects.new("FenceRun", None)
1271+
scene.collection.objects.link(run)
1272+
run.rotation_euler.z = math.radians(-24.0)
1273+
sections = [low]
1274+
for i in (-1, 1):
1275+
dup = bpy.data.objects.new(f"{low.name}.Run{i:+d}", low.data)
1276+
scene.collection.objects.link(dup)
1277+
dup.location.x = i * TILE
1278+
sections.append(dup)
1279+
for ob in sections:
1280+
ob.parent = run
1281+
1282+
# A render-only packed-earth strip the run stands in: a low bevelled
1283+
# slab with a worn centre, so the sections read as installed, not as
1284+
# props parked on a studio floor. Not part of the asset or its budgets.
1285+
path_me = bpy.data.meshes.new("PathStrip")
1286+
bm = bmesh.new()
1287+
try:
1288+
bmesh.ops.create_cube(bm, size=1.0)
1289+
for v in bm.verts:
1290+
v.co.x *= TILE * 3 + 0.9
1291+
v.co.y *= 0.78
1292+
v.co.z = (v.co.z + 0.5) * 0.02 - 0.019
1293+
bmesh.ops.bevel(bm, geom=sorted(bm.edges, key=lambda e: e.index), offset=0.012,
1294+
segments=2, affect="EDGES", profile=0.5, clamp_overlap=True)
1295+
bm.to_mesh(path_me)
1296+
finally:
1297+
bm.free()
1298+
for poly in path_me.polygons:
1299+
poly.use_smooth = True
1300+
pmat = bpy.data.materials.new("PackedEarth")
1301+
pmat.use_nodes = True
1302+
pnt = pmat.node_tree
1303+
pb = pnt.nodes["Principled BSDF"]
1304+
pnoise = pnt.nodes.new("ShaderNodeTexNoise")
1305+
pnoise.inputs["Scale"].default_value = 9.0
1306+
pnoise.inputs["Detail"].default_value = 8.0
1307+
pramp = pnt.nodes.new("ShaderNodeValToRGB")
1308+
pramp.color_ramp.elements[0].position = 0.35
1309+
pramp.color_ramp.elements[0].color = (0.035, 0.026, 0.018, 1.0)
1310+
pramp.color_ramp.elements[1].position = 0.70
1311+
pramp.color_ramp.elements[1].color = (0.11, 0.085, 0.055, 1.0)
1312+
pnt.links.new(pnoise.outputs["Fac"], pramp.inputs["Fac"])
1313+
pnt.links.new(pramp.outputs["Color"], pb.inputs["Base Color"])
1314+
pb.inputs["Roughness"].default_value = 0.92
1315+
pbump = pnt.nodes.new("ShaderNodeBump")
1316+
pbump.inputs["Strength"].default_value = 0.35
1317+
pnt.links.new(pnoise.outputs["Fac"], pbump.inputs["Height"])
1318+
pnt.links.new(pbump.outputs["Normal"], pb.inputs["Normal"])
1319+
path_me.materials.append(pmat)
1320+
strip = bpy.data.objects.new("PathStrip", path_me)
1321+
scene.collection.objects.link(strip)
1322+
strip.parent = run
12511323

12521324
floor_me = bpy.data.meshes.new("Floor")
12531325
bm = bmesh.new()
@@ -1291,12 +1363,12 @@ def light(name, loc, energy, size, col, rot):
12911363
light("Wedge", (2.4, 4.2, 4.1), 640.0, 5.5, (1.0, 0.70, 0.40), (-70, 0, 198))
12921364

12931365
cam_data = bpy.data.cameras.new("Cam")
1294-
cam_data.lens = 50.0
1366+
cam_data.lens = 45.0
12951367
cam = bpy.data.objects.new("Cam", cam_data)
1296-
cam.location = (2.50, -3.46, 1.95)
1368+
cam.location = (1.30, -5.60, 1.70)
12971369
scene.collection.objects.link(cam)
12981370
aim = bpy.data.objects.new("Aim", None)
1299-
aim.location = (0.0, 0.0, 0.58)
1371+
aim.location = (0.42, -0.18, 0.55)
13001372
scene.collection.objects.link(aim)
13011373
con = cam.constraints.new("TRACK_TO")
13021374
con.target = aim
@@ -1324,7 +1396,7 @@ def light(name, loc, energy, size, col, rot):
13241396
scene.view_settings.view_transform = "Standard"
13251397

13261398
fcode = gallery_framing.check_framing(
1327-
scene, cam, hero=[low], elements=[low], stage=[floor, wall],
1399+
scene, cam, hero=sections, elements=sections, stage=[floor, wall, strip],
13281400
)
13291401
if fcode:
13301402
return fcode

‎showcase/fence-kit/preview.webp‎

13.4 KB
Loading

‎showcase/gallery.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@
115115
"name": "fence-kit",
116116
"dir": "showcase/fence-kit",
117117
"teaches": "A procedural post-and-rail fence section that fits its 1.60 m tile, with one-shell iron bands and a board brace housed in both posts, through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.",
118-
"alt": "A section of grained timber post-and-rail fence with a diagonal board brace across the rails and dark iron bands and shoes on both posts.",
118+
"alt": "A run of three weathered post-and-rail fence sections on a packed-earth strip, joined post to post at the tile pitch, each with a diagonal board brace and dark iron bands.",
119119
"witnessesFix": "Recomputed: 1012 tris, two materials with 116 metal and 390 wood faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.596×0.134×1.232 m inside the 1.60 m tile, grounded zmin, 2 iron shoes, brace housed 12 mm in both posts and bearing 2 mm on the middle rail, rail-to-post gap 0.93 mm, rail span 1.362 m, LOD ratios in band (5.2 COLLAPSE leaner on LOD2), convex collider 24 tris, hygiene all zero, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z/--short-shoes 16; --short-brace 17; --gap-rails 18; --long-rails 19; --float-brace 20; --wide-tile 21.",
120120
"hero": "docs/gallery/assets/fence-kit-hero.webp",
121121
"preview": "showcase/fence-kit/preview.webp",

0 commit comments

Comments
 (0)