Skip to content

Commit 2d9f5c5

Browse files
TMHSDigitalclaude
andcommitted
fix(showcase): redesign signpost as a lettered three-finger crossroads post
The signpost read as one bare post with two blank boards. It now carries a third finger (FORD, 0.46 m, lower, pointing back the way of the first), and each board's destination and distance is painted on its front face: MILLBROOK 2, OAKHAM 5, FORD 1. The lettering is surface only. A 5x7 bitmap font is painted into a small image covering the boards in object space, cubic-filtered and thresholded so the glyphs round into brush strokes, masked to the front (-Y) faces and worn with the paint. The image node is deselected so the normal bake never targets it. The key and fill lights are raised to bring the hero into the calibration luma band. Adds asset_sheet.py SELECT rows for signpost, shipping-crate and hitching-post, which had none, so the asset-sheet gate can run on them. Evidence (live runs, local): - .scratch/blender-5.2.1-windows-x64/blender.exe (Blender 5.2.1 LTS) and .scratch/blender-4.5.11-windows-x64/blender.exe (Blender 4.5.11 LTS): default exit 0 on both. base_tris 432 (declared 320-480; was 376), mat faces metal 126 / board 33 / wood 60, bbox (1.2600, 0.1420, 1.5510) unchanged, collider 56 tris (<= 180), boards=3 board_gap 0.005 strap_gap 0.003 board_dy 0.028, hygiene all zero. LOD r1/r2: 4.5.11 0.5000/0.2176, 5.2.1 0.4537/0.1620. - Falsifiers, identical on both versions: --skip-decimate 9, --stray-vert 15, --lift-z 16, --short-shoe 16, --gap-board 17 (gap 0.115), --float-strap 18 (gap 0.056), --yaw-boards 19 (dy 0.0936). 5.1.2 was not re-run in this pass. - Framing: fill y 0.872, margins l 0.394 r 0.347 b 0.039 t 0.089. - Contact sheet: candidate luma 0.198 / stage 0.191 / warmth +0.102 / sat 0.095, against armature-bend 0.243/0.192, damped-track-aim 0.237/0.218 and bmesh-gear 0.236/0.177. Saturation is low because the subject is cream paint and brown timber. - Asset sheet (docs/gallery/asset-sheets/signpost.webp): the lettered post is not the least-designed object beside the hydrant, drone, well, wheelbarrow and shelf. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 71b4f2b commit 2d9f5c5

9 files changed

Lines changed: 159 additions & 22 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ Budget-conformance props. Not examples. Conventions: [`showcase/README.md`](show
127127
<a href="showcase/hitching-post/"><img src="showcase/hitching-post/preview.webp" width="24%" alt="Hitching post: a square timber post with a pyramidal cap, cross-arm, iron bands wrapped around it, and two hitching rings on a dark studio floor" /></a>
128128
<a href="showcase/grindstone/"><img src="showcase/grindstone/preview.webp" width="24%" alt="Grindstone: a stone wheel on a timber A-frame with an iron axle, hubs, crank, and tray on a dark studio floor" /></a>
129129
<a href="showcase/hand-pump/"><img src="showcase/hand-pump/preview.webp" width="24%" alt="Hand pump: a matte black cast-iron village pump on a stepped wooden plinth with a spout and handle on a dark studio floor" /></a>
130-
<a href="showcase/signpost/"><img src="showcase/signpost/preview.webp" width="24%" alt="Signpost: a timber post with two painted fingerboards, iron straps, and a pyramidal cap on a dark studio floor" /></a>
130+
<a href="showcase/signpost/"><img src="showcase/signpost/preview.webp" width="24%" alt="Signpost: a timber crossroads post with three painted fingerboards lettered MILLBROOK 2, OAKHAM 5 and FORD 1, iron straps, and a pyramidal cap on a dark studio floor" /></a>
131131
<a href="showcase/chopping-block/"><img src="showcase/chopping-block/preview.webp" width="24%" alt="Chopping block: a bark-sided log round with growth rings and drying checks on its sawn top, a riveted iron hoop, and a felling axe buried in the top, on a dark studio floor" /></a>
132132
<a href="showcase/wooden-bucket/"><img src="showcase/wooden-bucket/preview.webp" width="24%" alt="Wooden bucket: a coopered pail with a three-strand rope bail through iron ear rings and chord-lofted iron hoops on a dark studio floor" /></a>
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>
50.9 KB
Loading
4.3 KB
Loading
-514 Bytes
Loading

‎scripts/asset_sheet.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@
7171
"fence-kit": (r"^FenceLow$", None),
7272
"wooden-ladder": (r"^LadderLow$", None),
7373
"terrain-scatter": (r"^TerrainLow$", None),
74+
"shipping-crate": (r"^CrateLow$", None),
75+
"signpost": (r"^SignpostLow$", None),
76+
"hitching-post": (r"^HitchPostLow$", None),
7477
"triangulate-tangents": (r"^Buckler(\.Rivets)?$", None),
7578
"car-mirror-symmetry": (r"^(CarBody|Wheel(Front|Rear)|Headlamp|Taillamp|Grille"
7679
r"|DoorMirror|DoorHandle(Front|Rear))$", None),

‎showcase/gallery.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,8 +247,8 @@
247247
"name": "signpost",
248248
"dir": "showcase/signpost",
249249
"teaches": "A procedural timber fingerboard signpost through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.",
250-
"alt": "A wooden signpost with two pointed finger boards in worn cream paint and rusted iron bands on the post.",
251-
"witnessesFix": "Recomputed: 376 tris, three materials with 108 metal, 22 board and 60 wood faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.260×0.142×1.551 m at zmin 0, LOD ratios in band (5.2 COLLAPSE leaner on LOD1/LOD2), convex collider 52 tris, non-empty glTF. Hygiene all zero. Board-to-post gap 5 mm, strap gap 3 mm, board AABB Y 28 mm. --skip-decimate exits 9; --stray-vert 15; --lift-z / --short-shoe 16; --gap-board 17; --float-strap 18; --yaw-boards 19.",
250+
"alt": "A wooden crossroads signpost with three pointed cream finger boards painted MILLBROOK 2, OAKHAM 5 and FORD 1, rusted iron collars and bands on the post.",
251+
"witnessesFix": "Recomputed: 432 tris, three materials with 126 metal, 33 board and 60 wood faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.260×0.142×1.551 m at zmin 0, LOD ratios in band (5.2 COLLAPSE leaner on LOD1/LOD2), convex collider 56 tris, non-empty glTF. Hygiene all zero. Board-to-post gap 5 mm, strap gap 3 mm, board AABB Y 28 mm. --skip-decimate exits 9; --stray-vert 15; --lift-z / --short-shoe 16; --gap-board 17; --float-strap 18; --yaw-boards 19.",
252252
"hero": "docs/gallery/assets/signpost-hero.webp",
253253
"preview": "showcase/signpost/preview.webp",
254254
"tags": [

‎showcase/signpost/README.md‎

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
A showcase piece, not an example. Procedural timber fingerboard
44
signpost (square post, pyramidal cap from the post half plus overhang,
5-
two axis-aligned painted boards with wedges cut from the board
6-
section, iron shoe collar and U-wrap straps) then the shipped
5+
three axis-aligned painted boards lettered with their destinations,
6+
with wedges cut from the board section, iron shoe collar and U-wrap straps) then the shipped
77
pipeline: unique-cell UVs, Cycles high-to-low normal bake, LOD chain,
88
convex collider, Unity glTF export.
99

@@ -24,24 +24,24 @@ witness an API contract. "It rendered without error" is not a check.
2424
Hygiene combinatorics match `examples/mesh-hygiene-audit` (copied, not
2525
imported).
2626

27-
Intended size: 1.48 m post, boards 0.62 m and 0.54 m, 0.118 m post
27+
Intended size: 1.48 m post, boards 0.62 m, 0.54 m and 0.46 m, 0.118 m post
2828
stock; outer AABB 1.260 × 0.142 × 1.551 m.
2929

3030
## Budgets
3131

3232
Declared as named constants; every gate **recomputes** from the mesh,
3333
materials, UVs, evaluated LOD, collider, or export file.
3434

35-
| Axis | Declared | Measured (4.5.11 / 5.1.2 / 5.2.1) |
35+
| Axis | Declared | Measured (4.5.11 / 5.2.1) |
3636
| --- | --- | --- |
37-
| Base triangles | 320–480 | 376 / 376 / 376 |
38-
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.5000 / 0.4681 |
39-
| LOD2 ratio | 0.10–0.35 of base | 0.2181 / 0.2181 / 0.1649 |
40-
| Materials | exactly 3 distinct, ≥24 metal, ≥12 board, ≥12 wood | 3 slots, 108 metal, 22 board, 60 wood |
37+
| Base triangles | 320–480 | 432 / 432 |
38+
| LOD1 ratio | 0.32–0.62 of base | 0.5000 / 0.4537 |
39+
| LOD2 ratio | 0.10–0.35 of base | 0.2176 / 0.1620 |
40+
| Materials | exactly 3 distinct, ≥24 metal, ≥12 board, ≥12 wood | 3 slots, 126 metal, 33 board, 60 wood |
4141
| UVs | in `0..1`, AABB overlap ≤ 1e-5 | in range, overlap 0 |
4242
| Outer AABB | (1.260, 0.142, 1.551) m ± 0.015 | (1.2600, 0.1420, 1.5510), zmin 0 |
43-
| Collider tris | ≤ 180 | 52 |
44-
| Export | written, size > 0 | 35648 / 35648 / 35640 bytes |
43+
| Collider tris | ≤ 180 | 56 |
44+
| Export | written, size > 0 | 41168 / 41232 bytes |
4545

4646
Base triangles dropped from **504 to 376** in the quality pass: glued
4747
diamond cubes and a solid shoe block became wedges and a four-plate
@@ -67,6 +67,19 @@ wall's left edge showed in the corner of the hero. The temp `.glb` is
6767
removed after its size is measured. With no new geometric defect, there
6868
is no new budget.
6969

70+
The second pass added a third finger (FORD, 0.46 m, pointing back the
71+
way of the first, below the other two) so the post reads as a crossroads,
72+
and painted each board's destination and distance on its front face.
73+
The lettering is surface: a 5×7 bitmap font painted into a small image
74+
covering the boards in object space, cubic-filtered and thresholded so
75+
the pixel glyphs round into brush strokes, masked to the front (−Y)
76+
faces and worn through with the paint. The third board moved the
77+
budgets it should: base triangles 376 → 432 (inside the declared
78+
320–480), metal faces 108 → 126 for its collar, board faces 22 → 33,
79+
collider 52 → 56 tris, and `boards` in the joint audit 2 → 3. The outer
80+
AABB is unchanged: the new board is shorter than both the others and
81+
sits between existing bands. Every falsifier still exits its code.
82+
7083
### Hygiene
7184

7285
Recomputed from the generated mesh, not asserted about the script.
@@ -93,7 +106,8 @@ Recomputed from the generated mesh, not asserted about the script.
93106
### Falsifiers
94107

95108
Each violates one named budget. All were run on 4.5.11, 5.1.2 and 5.2.1
96-
and returned the same code on each.
109+
and returned the same code on each; after the second pass they were
110+
re-run on 4.5.11 and 5.2.1 and still do.
97111

98112
| Flag | Budget violated | Exit |
99113
| --- | --- | --- |

‎showcase/signpost/preview.webp‎

3.94 KB
Loading

‎showcase/signpost/signpost.py‎

Lines changed: 128 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Game-ready timber signpost — a showcase piece, not an example.
22
33
Asserts budget conformance of a procedural fingerboard signpost (post,
4-
two painted boards, iron shoe and straps) after composing shipped
4+
three painted boards lettered with destinations, iron shoe and straps) after composing shipped
55
pipeline pieces: bmesh construction, UVs, three materials, high-to-low
66
normal bake, LOD chain, convex collider, Unity glTF export.
77
@@ -53,6 +53,18 @@
5353
BOARD_H_B = 0.100
5454
BOARD_A = (1.24, 0.62, 1.0)
5555
BOARD_B = (1.00, 0.54, -1.0)
56+
# Third finger, lower and shorter, pointing back the way of the first: a
57+
# crossroads post reads as a junction, not a pair of arrows.
58+
BOARD_H_C = 0.092
59+
BOARD_C = (0.78, 0.46, 1.0)
60+
61+
# Painted destinations, one per board (A, B, C). Rendered from a 5x7 bitmap
62+
# font into a small lettering image sampled in object space on the boards'
63+
# front (-Y) faces; cubic filtering plus a threshold rounds the pixel
64+
# glyphs into brush strokes. Surface only: no geometry, no budget moves.
65+
BOARD_TEXT = ("MILLBROOK 2", "OAKHAM 5", "FORD 1")
66+
LETTER_PX = 0.0072 # metres per font pixel
67+
LETTER_REGION = (-0.66, 0.66, 0.68, 1.34) # object-space x0, x1, z0, z1
5668
TIP_LEN = 0.070
5769
TIP_BITE = 0.004
5870
BAND_Z = 0.22
@@ -250,6 +262,71 @@ def pack_uvs(bm, margin=0.08):
250262
)
251263

252264

265+
def board_specs():
266+
"""(z, length, sign, height, thickness) per finger board, A B C."""
267+
return (
268+
(BOARD_A[0], BOARD_A[1], BOARD_A[2], BOARD_H_A, BOARD_T),
269+
(BOARD_B[0], BOARD_B[1], BOARD_B[2], BOARD_H_B, BOARD_T * 0.92),
270+
(BOARD_C[0], BOARD_C[1], BOARD_C[2], BOARD_H_C, BOARD_T * 0.90),
271+
)
272+
273+
274+
# 5x7 bitmap glyphs, top row first; only the characters BOARD_TEXT uses.
275+
GLYPHS = {
276+
"A": ("01110", "10001", "10001", "11111", "10001", "10001", "10001"),
277+
"B": ("11110", "10001", "10001", "11110", "10001", "10001", "11110"),
278+
"D": ("11110", "10001", "10001", "10001", "10001", "10001", "11110"),
279+
"F": ("11111", "10000", "10000", "11110", "10000", "10000", "10000"),
280+
"H": ("10001", "10001", "10001", "11111", "10001", "10001", "10001"),
281+
"I": ("11111", "00100", "00100", "00100", "00100", "00100", "11111"),
282+
"K": ("10001", "10010", "10100", "11000", "10100", "10010", "10001"),
283+
"L": ("10000", "10000", "10000", "10000", "10000", "10000", "11111"),
284+
"M": ("10001", "11011", "10101", "10101", "10001", "10001", "10001"),
285+
"O": ("01110", "10001", "10001", "10001", "10001", "10001", "01110"),
286+
"R": ("11110", "10001", "10001", "11110", "10100", "10010", "10001"),
287+
"1": ("00100", "01100", "00100", "00100", "00100", "00100", "01110"),
288+
"2": ("01110", "10001", "00001", "00010", "00100", "01000", "11111"),
289+
"5": ("11111", "10000", "11110", "00001", "00001", "10001", "01110"),
290+
" ": ("00000",) * 7,
291+
}
292+
293+
294+
def lettering_image():
295+
"""Paint BOARD_TEXT into a greyscale image covering LETTER_REGION.
296+
297+
Each board's text is centred on the part of the board clear of its
298+
post collar; one image pixel is one font pixel (LETTER_PX metres).
299+
"""
300+
x0, x1, z0, z1 = LETTER_REGION
301+
w = int(round((x1 - x0) / LETTER_PX))
302+
h = int(round((z1 - z0) / LETTER_PX))
303+
buf = [0.0] * (w * h)
304+
half = POST_W / 2.0
305+
for (z, length, sign, _bh, _bt), text in zip(board_specs(), BOARD_TEXT):
306+
# start clear of the collar plus a margin, so the post does not hide
307+
# the first letters of the far board in a three-quarter view
308+
inner = sign * (half + IRON_T + 0.045)
309+
outer = sign * (half - TENON + length - TIP_LEN)
310+
cx = (inner + outer) / 2.0
311+
cols = len(text) * 6 - 1
312+
px0 = int(round((cx - x0) / LETTER_PX - cols / 2.0))
313+
pz0 = int(round((z - z0) / LETTER_PX - 3.5))
314+
for k, ch in enumerate(text):
315+
rows = GLYPHS[ch]
316+
for r in range(7):
317+
for c in range(5):
318+
if rows[r][c] != "1":
319+
continue
320+
px = px0 + k * 6 + c
321+
pz = pz0 + (6 - r)
322+
if 0 <= px < w and 0 <= pz < h:
323+
buf[pz * w + px] = 1.0
324+
img = bpy.data.images.new("SignpostLettering", w, h, alpha=False)
325+
img.colorspace_settings.name = "Non-Color"
326+
img.pixels.foreach_set([c for v in buf for c in (v, v, v, 1.0)])
327+
return img
328+
329+
253330
def build_signpost_mesh(
254331
name,
255332
bevel_offset,
@@ -305,10 +382,7 @@ def build_signpost_mesh(
305382
yaw = YAW_FAIL if yaw_boards else 0.0
306383
extra = GAP_BOARD if gap_board else 0.0
307384
strap_extra = FLOAT_STRAP if float_strap else 0.0
308-
specs = (
309-
(BOARD_A[0], BOARD_A[1], BOARD_A[2], BOARD_H_A, BOARD_T),
310-
(BOARD_B[0], BOARD_B[1], BOARD_B[2], BOARD_H_B, BOARD_T * 0.92),
311-
)
385+
specs = board_specs()
312386
for z, length, sign, bh, bt in specs:
313387
inner = sign * (half - TENON + extra)
314388
rect_len = length - TIP_LEN
@@ -531,10 +605,56 @@ def board_material(name):
531605
_sock(paint.inputs, "A_Color").default_value = (0.62, 0.56, 0.42, 1.0)
532606
_sock(paint.inputs, "B_Color").default_value = (0.74, 0.68, 0.52, 1.0)
533607
nt.links.new(tone.outputs["Fac"], _sock(paint.inputs, "Factor_Float"))
608+
609+
# Lettering: object-space XZ -> LETTER_REGION -> image, cubic-filtered and
610+
# thresholded into rounded strokes, only on the front (-Y) faces.
611+
x0, x1, z0, z1 = LETTER_REGION
612+
sep = nt.nodes.new("ShaderNodeSeparateXYZ")
613+
nt.links.new(coord.outputs["Object"], sep.inputs["Vector"])
614+
uu = nt.nodes.new("ShaderNodeMapRange")
615+
uu.inputs["From Min"].default_value = x0
616+
uu.inputs["From Max"].default_value = x1
617+
nt.links.new(sep.outputs["X"], uu.inputs["Value"])
618+
vv = nt.nodes.new("ShaderNodeMapRange")
619+
vv.inputs["From Min"].default_value = z0
620+
vv.inputs["From Max"].default_value = z1
621+
nt.links.new(sep.outputs["Z"], vv.inputs["Value"])
622+
uvw = nt.nodes.new("ShaderNodeCombineXYZ")
623+
nt.links.new(uu.outputs["Result"], uvw.inputs["X"])
624+
nt.links.new(vv.outputs["Result"], uvw.inputs["Y"])
625+
letters = nt.nodes.new("ShaderNodeTexImage")
626+
letters.image = lettering_image()
627+
letters.interpolation = "Cubic"
628+
letters.extension = "CLIP"
629+
# never the bake target: the normal bake writes into the active, selected
630+
# image node of each material, and this one is surface paint
631+
letters.select = False
632+
nt.nodes.active = bsdf
633+
nt.links.new(uvw.outputs["Vector"], letters.inputs["Vector"])
634+
stroke = nt.nodes.new("ShaderNodeMapRange")
635+
stroke.inputs["From Min"].default_value = 0.38
636+
stroke.inputs["From Max"].default_value = 0.52
637+
nt.links.new(letters.outputs["Color"], stroke.inputs["Value"])
638+
nsep = nt.nodes.new("ShaderNodeSeparateXYZ")
639+
nt.links.new(coord.outputs["Normal"], nsep.inputs["Vector"])
640+
front = nt.nodes.new("ShaderNodeMath")
641+
front.operation = "LESS_THAN"
642+
front.inputs[1].default_value = -0.9
643+
nt.links.new(nsep.outputs["Y"], front.inputs[0])
644+
ink_fac = nt.nodes.new("ShaderNodeMath")
645+
ink_fac.operation = "MULTIPLY"
646+
nt.links.new(stroke.outputs["Result"], ink_fac.inputs[0])
647+
nt.links.new(front.outputs["Value"], ink_fac.inputs[1])
648+
ink = nt.nodes.new("ShaderNodeMix")
649+
ink.data_type = "RGBA"
650+
_sock(ink.inputs, "B_Color").default_value = (0.045, 0.032, 0.024, 1.0)
651+
nt.links.new(ink_fac.outputs["Value"], _sock(ink.inputs, "Factor_Float"))
652+
nt.links.new(_sock(paint.outputs, "Result_Color"), _sock(ink.inputs, "A_Color"))
653+
534654
mix = nt.nodes.new("ShaderNodeMix")
535655
mix.data_type = "RGBA"
536656
nt.links.new(mask.outputs["Color"], _sock(mix.inputs, "Factor_Float"))
537-
nt.links.new(_sock(paint.outputs, "Result_Color"), _sock(mix.inputs, "A_Color"))
657+
nt.links.new(_sock(ink.outputs, "Result_Color"), _sock(mix.inputs, "A_Color"))
538658
nt.links.new(grain, _sock(mix.inputs, "B_Color"))
539659
nt.links.new(_sock(mix.outputs, "Result_Color"), bsdf.inputs["Base Color"])
540660
return mat
@@ -1144,8 +1264,8 @@ def light(name, loc, energy, size, col, rot):
11441264
ob.rotation_euler = tuple(math.radians(a) for a in rot)
11451265
scene.collection.objects.link(ob)
11461266

1147-
light("Key", (-3.6, -5.0, 5.4), 660.0, 4.0, (1.0, 0.94, 0.86), (50, 0, -36))
1148-
light("Fill", (5.0, -3.4, 2.4), 46.0, 8.0, (0.72, 0.82, 1.0), (62, 0, 50))
1267+
light("Key", (-3.6, -5.0, 5.4), 980.0, 4.0, (1.0, 0.94, 0.86), (50, 0, -36))
1268+
light("Fill", (5.0, -3.4, 2.4), 70.0, 8.0, (0.72, 0.82, 1.0), (62, 0, 50))
11491269
light("Wedge", (2.2, 4.0, 3.8), 600.0, 5.5, (1.0, 0.70, 0.40), (-70, 0, 198))
11501270

11511271
cam_data = bpy.data.cameras.new("Cam")

0 commit comments

Comments
 (0)