Skip to content

Commit 0739862

Browse files
TMHSDigitalclaude
andcommitted
fix(showcase): redesign shipping-crate with stencilled cargo marks and a three-quarter hero
The crate read as a plain plank box, and its hero showed the long side edge-on. Its front long side (-Y) now carries stencilled cargo marks, PORT ROYAL over NO 17. They are surface only: a 5x7 bitmap font painted into a small object-space image, cubic-filtered and thresholded into rounded stencil strokes, bitten by overspray noise, and masked to the -Y faces. Each line is centred on one long slat, found by replaying the same seeded _span_layout that places the slats. The 64 mm cap height fits inside the board, so the baseline runs along it. The image node is deselected so the normal bake never targets it. The hero turns the crate +15 degrees (was -48). The camera sits at about -51 degrees azimuth, so the stencilled side is 24 degrees off the view axis and the handle end 66 degrees. The key is brighter (680 -> 880 W) and the stage 60 m. Geometry and every budget are unchanged. Review fix, folded in: an earlier cut let both lines straddle a slat gap and turned the crate only to -20 degrees. That left the side 59 degrees off-axis, and the lettering read as sloping across the slats. The cause was diagnosed by overlaying the slat centrelines projected through the hero camera, and by a test render painting full-width bars at each computed slat centre. The bars landed on the boards, so the mapping was correct and the fix is placement plus view angle. 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 3388, LOD r1/r2 0.5000/0.2196, faces wood 1170 / metal 904, collider 116 tris, export 274624 / 274680 B. - Falsifiers, identical on both: --skip-decimate 9, --stray-vert 15, --lift-z 16, --short-skids 16, --float-handle 17, --omit-slats 18, --float-nails 18, --straddle-handle 20. 5.1.2 not re-run this pass. - Framing: fill y 0.722, margins l 0.206 r 0.238 b 0.089 t 0.189. - Contact sheet: candidate luma 0.216 / stage 0.195 / warmth +0.240 / sat 0.208, against armature-bend 0.243/0.192/-0.144/0.237, damped-track-aim 0.237/0.218/+0.105/0.178, bmesh-gear 0.236/0.177/+0.303/0.250. - Asset sheet (docs/gallery/asset-sheets/shipping-crate.webp): lettering follows its boards in the panel camera too, and the crate is not the least-designed object in the lineup. - Also corrects the gallery witnessesFix, which still quoted the first pass (2508 tris, 280 metal faces, collider 76) against the measured 3388 / 904 / 116. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent a5fed1d commit 0739862

8 files changed

Lines changed: 169 additions & 7 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ Budget-conformance props. Not examples. Conventions: [`showcase/README.md`](show
109109
<summary><strong>36 showcase pieces</strong> — click to expand the preview grid</summary>
110110

111111
<p align="center">
112-
<a href="showcase/shipping-crate/"><img src="showcase/shipping-crate/preview.webp" width="24%" alt="Shipping crate: a wooden slat crate with iron corner brackets on a dark studio floor, warm wedge on the back wall" /></a>
112+
<a href="showcase/shipping-crate/"><img src="showcase/shipping-crate/preview.webp" width="24%" alt="Shipping crate: a wooden slat crate stencilled PORT ROYAL and NO 17, with iron corner brackets and an end handle, on a dark studio floor, warm wedge on the back wall" /></a>
113113
<a href="showcase/stone-well/"><img src="showcase/stone-well/preview.webp" width="24%" alt="Stone well: a round well of varied grey stones with a grained wooden pyramid roof on posts, rope and a bucket on rusted iron hoops, on a dark studio floor" /></a>
114114
<a href="showcase/wooden-barrel/"><img src="showcase/wooden-barrel/preview.webp" width="24%" alt="Wooden barrel: a 20-stave wine-cask with a head of four boards, chord-lofted iron hoops, and a bung on a dark studio floor" /></a>
115115
<a href="showcase/campfire/"><img src="showcase/campfire/preview.webp" width="24%" alt="Campfire: a two-course ring of rounded fieldstones around a tripod of logs and glowing ash on a dark studio floor, warm wedge on the back wall" /></a>
63.9 KB
Loading
16.9 KB
Loading
4.08 KB
Loading

‎showcase/gallery.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
"name": "shipping-crate",
1010
"dir": "showcase/shipping-crate",
1111
"teaches": "A procedural shipping crate through UVs, bake, LOD, collider, and Unity glTF, asserting recomputed budgets rather than an API contract.",
12-
"alt": "A grained wooden shipping crate with nailed iron corner straps, horizontal planks and a metal bail handle mounted on one end board.",
13-
"witnessesFix": "Recomputed: 2508 tris, two materials with 1170 wood and 280 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.146×0.726×0.612 m, grounded zmin, 3 skids, handle-end and slat-post join 0, mesh hygiene all zero, LOD ratios in band (5.2 COLLAPSE leaner on LOD2), convex collider 76 tris, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z/--short-skids 16; --float-handle 17; --omit-slats 18.",
12+
"alt": "A grained wooden shipping crate in three-quarter view, PORT ROYAL and NO 17 stencilled along its side slats, with nailed iron corner straps and a bail handle on one end.",
13+
"witnessesFix": "Recomputed: 3388 tris, two materials with 1170 wood and 904 metal faces, UVs in 0..1 with zero AABB overlap, outer AABB 1.146×0.726×0.612 m, grounded zmin, 3 skids, handle-end and slat-post join 0, mesh hygiene all zero, LOD ratios in band (5.2 COLLAPSE leaner on LOD2), convex collider 116 tris, non-empty glTF. --skip-decimate exits 9; --stray-vert 15; --lift-z/--short-skids 16; --float-handle 17; --omit-slats 18.",
1414
"hero": "docs/gallery/assets/shipping-crate-hero.webp",
1515
"preview": "showcase/shipping-crate/preview.webp",
1616
"tags": [

‎showcase/shipping-crate/README.md‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ materials, UVs, evaluated LOD, collider, or export file.
3434
| UVs | in `0..1`, AABB overlap ≤ 1e-5 | in range, overlap 0 |
3535
| Outer AABB | (1.146, 0.726, 0.612) m ± 0.015 | (1.1460, 0.7260, 0.6120), zmin 0 |
3636
| Collider tris | ≤ 220 | 116 |
37-
| Export | written, size > 0 | 273816 / 273816 / 273800 bytes |
37+
| Export | written, size > 0 | 274624 (4.5.11) / 274680 (5.2.1) bytes |
3838

3939
Base triangles rose from **624 to 2508** in the quality pass: the old
4040
beveled cube with glued-on slats became a post-and-slat crate with
@@ -104,6 +104,30 @@ The right-angle-edge budget from `crate-stack` is not applied here: the
104104
handle bail's pipe ends are square discs buried in the mounting plates,
105105
so "every box is chamfered" does not hold for this piece.
106106

107+
### Third pass (stencil, hero angle)
108+
109+
The crate read as a plain plank box: nothing on it said what it was for.
110+
Its front long side (-Y) now carries stencilled cargo marks, PORT ROYAL
111+
over NO 17. They are surface only: a 5x7 bitmap font painted into a small
112+
image covering the side in object space, cubic-filtered and thresholded
113+
into rounded stencil strokes, bitten by a fine overspray noise, and masked
114+
to the -Y faces. Each line is centred on one long slat, replayed from the
115+
same seeded `_span_layout` that places the slats, and its 64 mm cap height
116+
fits inside the board, so the baseline runs along the board. The first cut
117+
let both lines straddle a gap, which in perspective read as lettering
118+
sloping across the slats. The image node is deselected so the normal bake
119+
never targets it. Geometry, triangles, material face counts and every budget
120+
are unchanged; the glTF grows by about 800 bytes.
121+
122+
The hero turned the crate -48 degrees, which put the long side
123+
edge-on. The camera sits at about -51 degrees azimuth, so the crate now
124+
turns +15 degrees: the stencilled side is 24 degrees off the view axis
125+
and the handle end 66 degrees, a three-quarter view led by the
126+
lettering. An intermediate -20 degrees left the side 59 degrees
127+
off-axis, where the stencil foreshortened into a steep italic. The key
128+
is brighter and the stage is 60 m (the 14 m grid could show its
129+
edge).
130+
107131
### Falsifiers
108132

109133
Each violates one named budget. All nine were run on 4.5.11, 5.1.2 and
15.2 KB
Loading

‎showcase/shipping-crate/shipping_crate.py‎

Lines changed: 141 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,138 @@
125125
PLANK_TONE_JITTER = 0.28
126126
TONE_SEED = 29
127127

128+
# Stencilled cargo marks on the front long side (-Y). Surface only: a 5x7
129+
# bitmap font painted into a small image covering the side in object space,
130+
# cubic-filtered and thresholded into rounded stencil strokes, masked to the
131+
# -Y faces. Each line is centred on one long slat (index from the bottom,
132+
# replayed from the same seeded _span_layout that places the slats) and is
133+
# shorter than the slat, so its baseline runs along the board rather than
134+
# across a gap: a line straddling a gap reads as sloping in perspective.
135+
STENCIL_LINES = (("PORT ROYAL", 0.0, 3), ("NO 17", 0.0, 1))
136+
STENCIL_PX = 0.0092 # metres per font pixel: 7 px = 64 mm < slat
137+
STENCIL_REGION = (-0.47, 0.47, 0.10, 0.56) # object-space x0, x1, z0, z1
138+
139+
# 5x7 bitmap glyphs, top row first; only the characters STENCIL_LINES uses.
140+
GLYPHS = {
141+
"A": ("01110", "10001", "10001", "11111", "10001", "10001", "10001"),
142+
"L": ("10000", "10000", "10000", "10000", "10000", "10000", "11111"),
143+
"N": ("10001", "11001", "10101", "10011", "10001", "10001", "10001"),
144+
"O": ("01110", "10001", "10001", "10001", "10001", "10001", "01110"),
145+
"P": ("11110", "10001", "10001", "11110", "10000", "10000", "10000"),
146+
"R": ("11110", "10001", "10001", "11110", "10100", "10010", "10001"),
147+
"T": ("11111", "00100", "00100", "00100", "00100", "00100", "00100"),
148+
"Y": ("10001", "10001", "01010", "00100", "00100", "00100", "00100"),
149+
"1": ("00100", "01100", "00100", "00100", "00100", "00100", "01110"),
150+
"7": ("11111", "00001", "00010", "00100", "01000", "01000", "01000"),
151+
" ": ("00000",) * 7,
152+
}
153+
154+
155+
def long_slat_centres():
156+
"""World-z centre of each long side slat, bottom to top.
157+
158+
Replays the seeded draws build_crate_mesh makes before the long slats
159+
(floor, then lid), so the stencil lands on the boards actually built.
160+
"""
161+
ix, _iy, iz = INNER
162+
rng = random.Random(SLAT_SEED)
163+
_span_layout(N_FLOOR, ix, rng)
164+
_span_layout(N_LID, ix + POST * 0.5, rng)
165+
long_c, _long_w = _span_layout(N_LONG, iz - 0.04, rng)
166+
return [SKID_H + 0.02 + iz / 2.0 + c for c in long_c]
167+
168+
169+
def stencil_image():
170+
"""Paint STENCIL_LINES into a greyscale image covering STENCIL_REGION."""
171+
x0, x1, z0, z1 = STENCIL_REGION
172+
w = int(round((x1 - x0) / STENCIL_PX))
173+
h = int(round((z1 - z0) / STENCIL_PX))
174+
buf = [0.0] * (w * h)
175+
slat_z = long_slat_centres()
176+
for text, cx, board in STENCIL_LINES:
177+
cz = slat_z[board]
178+
cols = len(text) * 6 - 1
179+
px0 = int(round((cx - x0) / STENCIL_PX - cols / 2.0))
180+
pz0 = int(round((cz - z0) / STENCIL_PX - 3.5))
181+
for k, ch in enumerate(text):
182+
rows = GLYPHS[ch]
183+
for r in range(7):
184+
for c in range(5):
185+
if rows[r][c] == "1":
186+
px, pz = px0 + k * 6 + c, pz0 + (6 - r)
187+
if 0 <= px < w and 0 <= pz < h:
188+
buf[pz * w + px] = 1.0
189+
img = bpy.data.images.new("CrateStencil", w, h, alpha=False)
190+
img.colorspace_settings.name = "Non-Color"
191+
img.pixels.foreach_set([c for v in buf for c in (v, v, v, 1.0)])
192+
return img
193+
194+
195+
def add_stencil(mat):
196+
"""Spray STENCIL_LINES over the wood colour on the -Y faces."""
197+
nt = mat.node_tree
198+
bsdf = nt.nodes["Principled BSDF"]
199+
base = bsdf.inputs["Base Color"].links[0].from_socket
200+
coord = nt.nodes.new("ShaderNodeTexCoord")
201+
x0, x1, z0, z1 = STENCIL_REGION
202+
sep = nt.nodes.new("ShaderNodeSeparateXYZ")
203+
nt.links.new(coord.outputs["Object"], sep.inputs["Vector"])
204+
uu = nt.nodes.new("ShaderNodeMapRange")
205+
uu.inputs["From Min"].default_value = x0
206+
uu.inputs["From Max"].default_value = x1
207+
nt.links.new(sep.outputs["X"], uu.inputs["Value"])
208+
vv = nt.nodes.new("ShaderNodeMapRange")
209+
vv.inputs["From Min"].default_value = z0
210+
vv.inputs["From Max"].default_value = z1
211+
nt.links.new(sep.outputs["Z"], vv.inputs["Value"])
212+
uvw = nt.nodes.new("ShaderNodeCombineXYZ")
213+
nt.links.new(uu.outputs["Result"], uvw.inputs["X"])
214+
nt.links.new(vv.outputs["Result"], uvw.inputs["Y"])
215+
img = nt.nodes.new("ShaderNodeTexImage")
216+
img.image = stencil_image()
217+
img.interpolation = "Cubic"
218+
img.extension = "CLIP"
219+
# never the bake target: the normal bake writes into the active, selected
220+
# image node of each material, and this one is paint
221+
img.select = False
222+
nt.links.new(uvw.outputs["Vector"], img.inputs["Vector"])
223+
stroke = nt.nodes.new("ShaderNodeMapRange")
224+
stroke.inputs["From Min"].default_value = 0.26
225+
stroke.inputs["From Max"].default_value = 0.44
226+
nt.links.new(img.outputs["Color"], stroke.inputs["Value"])
227+
# overspray: a fine noise eats into the paint so it reads as sprayed
228+
spray = nt.nodes.new("ShaderNodeTexNoise")
229+
spray.inputs["Scale"].default_value = 90.0
230+
spray.inputs["Detail"].default_value = 4.0
231+
nt.links.new(coord.outputs["Object"], spray.inputs["Vector"])
232+
bite = nt.nodes.new("ShaderNodeMapRange")
233+
bite.inputs["From Min"].default_value = 0.30
234+
bite.inputs["From Max"].default_value = 0.55
235+
bite.inputs["To Min"].default_value = 0.72
236+
bite.inputs["To Max"].default_value = 1.0
237+
nt.links.new(spray.outputs["Fac"], bite.inputs["Value"])
238+
nsep = nt.nodes.new("ShaderNodeSeparateXYZ")
239+
nt.links.new(coord.outputs["Normal"], nsep.inputs["Vector"])
240+
front = nt.nodes.new("ShaderNodeMath")
241+
front.operation = "LESS_THAN"
242+
front.inputs[1].default_value = -0.9
243+
nt.links.new(nsep.outputs["Y"], front.inputs[0])
244+
fac = nt.nodes.new("ShaderNodeMath")
245+
fac.operation = "MULTIPLY"
246+
nt.links.new(stroke.outputs["Result"], fac.inputs[0])
247+
nt.links.new(front.outputs["Value"], fac.inputs[1])
248+
fac2 = nt.nodes.new("ShaderNodeMath")
249+
fac2.operation = "MULTIPLY"
250+
nt.links.new(fac.outputs["Value"], fac2.inputs[0])
251+
nt.links.new(bite.outputs["Result"], fac2.inputs[1])
252+
paint = nt.nodes.new("ShaderNodeMix")
253+
paint.data_type = "RGBA"
254+
_sock(paint.inputs, "B_Color").default_value = (0.022, 0.020, 0.019, 1.0)
255+
nt.links.new(fac2.outputs["Value"], _sock(paint.inputs, "Factor_Float"))
256+
nt.links.new(base, _sock(paint.inputs, "A_Color"))
257+
nt.links.new(_sock(paint.outputs, "Result_Color"), bsdf.inputs["Base Color"])
258+
nt.nodes.active = bsdf
259+
128260

129261
def eevee_engine_id():
130262
"""EEVEE id: 'BLENDER_EEVEE' on 5.0+, 'BLENDER_EEVEE_NEXT' on 4.2-4.5.
@@ -719,6 +851,7 @@ def wood_material(name):
719851
def crate_materials():
720852
"""(wood, iron): shared by the check, the render and inspection."""
721853
wood = wood_material("CrateWood")
854+
add_stencil(wood)
722855
metal = principled(
723856
"CrateMetal", (0.17, 0.165, 0.155, 1.0), 0.80, 0.46,
724857
noise_scale=18.0, wear=(0.20, 0.085, 0.032, 1.0),
@@ -1436,13 +1569,18 @@ def render_still(low, wood, tex, path, engine):
14361569
ob.hide_render = True
14371570
ob.hide_viewport = True
14381571

1439-
low.rotation_euler.z = math.radians(-48.0)
1572+
# The camera sits at about -51 deg azimuth. Turning the crate +15 deg puts
1573+
# the stencilled long side (-Y) 24 deg off the view axis and the handle
1574+
# end (+X) 66 deg off it: a three-quarter view led by the lettering. At
1575+
# -48 the side was edge-on; at -20 it was 59 deg off-axis and the
1576+
# stencil foreshortened into a steep italic that read as a mapping error.
1577+
low.rotation_euler.z = math.radians(15.0)
14401578
low.rotation_euler.x = math.radians(4.0)
14411579

14421580
floor_me = bpy.data.meshes.new("Floor")
14431581
bm = bmesh.new()
14441582
try:
1445-
bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=14.0)
1583+
bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=60.0)
14461584
bm.to_mesh(floor_me)
14471585
finally:
14481586
bm.free()
@@ -1476,7 +1614,7 @@ def light(name, loc, energy, size, col, rot):
14761614
ob.rotation_euler = tuple(math.radians(a) for a in rot)
14771615
scene.collection.objects.link(ob)
14781616

1479-
light("Key", (-3.6, -5.0, 5.8), 680.0, 4.0, (1.0, 0.94, 0.86), (50, 0, -36))
1617+
light("Key", (-3.6, -5.0, 5.8), 880.0, 4.0, (1.0, 0.94, 0.86), (50, 0, -36))
14801618
light("Fill", (5.0, -3.6, 2.6), 48.0, 8.0, (0.72, 0.82, 1.0), (62, 0, 50))
14811619
light("Wedge", (2.4, 4.2, 4.1), 640.0, 5.5, (1.0, 0.70, 0.40), (-70, 0, 198))
14821620

0 commit comments

Comments
 (0)