Skip to content

Commit 9123f54

Browse files
TMHSDigitalclaude
andcommitted
fix(examples): redesign modular-kit-snap surfaces as painted, tread-plated, hazard-striped kit
The wall panels were metallic 0.6 teal at low roughness and read as glass. They are now matte painted steel with noise wear. The walkway plate gets a procedural diamond-plate bump, the trim rails get diagonal hazard stripes, the rib frames get a bronze-steel paint, and the far door is less blown out. Every texture reads Object coordinates, and every kit part's origin is the segment pivot, so the patterns are identical per instance and continuous across a snapped joint, like the geometry. Geometry and checks are unchanged. Also fixes a latent crash: _mat_cache outlived read_factory_settings, so --output plus --falsify in one run handed the second scene freed materials ("StructRNA of type Material has been removed"). render_still now clears it. Evidence (live runs, local): - .scratch/blender-5.2.1-windows-x64/blender.exe (Blender 5.2.1 LTS): check exit 0 (boundary=16v/16e, joint yz=0, bbox_dev=0); render gates framing_deviation 5.0584 <= cap 8.0, aq_ok (materials n=8, edge90 0.001). --close-camera exits 10 (deviation 30.9012). - .scratch/blender-4.5.11-windows-x64/blender.exe (Blender 4.5.11 LTS): check exit 0; --output plus --falsify both render (exit 0 after the cache fix; exit 1 before it). - Contact sheet: candidate luma 0.254 / stage 0.258 / warmth +0.224 / sat 0.197 against armature-bend 0.243/0.192/-0.144/0.237, damped-track-aim 0.151/0.145/+0.277/0.231, and bmesh-gear 0.236/0.177/+0.303/0.250. The stage runs brighter because the interior corridor walls are the backdrop (existing framing deviation). - Asset sheet (docs/gallery/asset-sheets/modular-kit-snap.webp): the segment reads as designed beside the hydrant, drone, well, wheelbarrow and shelf. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 2228491 commit 9123f54

8 files changed

Lines changed: 113 additions & 16 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1141,7 +1141,7 @@ check first caught the weld) and [`mesh-hygiene-audit`](examples/mesh-hygiene-au
11411141
</tr>
11421142
<tr>
11431143
<td width="46%" valign="middle">
1144-
<a href="examples/modular-kit-snap/"><img src="examples/modular-kit-snap/preview.webp" alt="Modular kit snap: a corridor run of four tiling segments — dark steel walls with blue-gray panels, orange trim rails, and ceiling light strips converging on a glowing amber doorway, with every joint seamless because the boundary verts snap exactly to the tile grid" /></a>
1144+
<a href="examples/modular-kit-snap/"><img src="examples/modular-kit-snap/preview.webp" alt="Modular kit snap: a corridor run of four tiling segments — dark steel walls with painted teal panels, diamond-plate walkways, hazard-striped orange trim rails, and ceiling light strips converging on a glowing amber doorway, with every joint seamless because the boundary verts snap exactly to the tile grid" /></a>
11451145
</td>
11461146
<td valign="middle">
11471147

8.06 KB
Loading
19.6 KB
Loading
56.9 KB
Loading

‎examples/gallery.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,7 @@
572572
"name": "modular-kit-snap",
573573
"dir": "examples/modular-kit-snap",
574574
"teaches": "A tiling corridor kit whose open-end boundary verts snap to the tile grid, so instances at 4 m multiples join with zero gap or overlap.",
575-
"alt": "A sci-fi corridor built from snapped modular segments, with glass wall panels, orange trim rails and a lit doorway at the far end.",
575+
"alt": "A sci-fi corridor built from snapped modular segments, with painted teal wall panels, diamond-plate walkways, hazard-striped trim rails and a lit doorway at the far end.",
576576
"witnessesFix": "16 boundary verts on x∈{0,4} within 1e-6 m, linked-duplicate joint deviation 0.0, bbox == declared tile; unsnapped probe fails at 3e-3 m.",
577577
"hero": "docs/gallery/assets/modular-kit-snap-hero.webp",
578578
"preview": "examples/modular-kit-snap/preview.webp",

‎examples/modular-kit-snap/README.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ is now an assembly (backing plate + inset panel + four bolt heads), the
6666
palette shifted teal-slate so the card does not twin with
6767
`lightmap-uv-channel`'s warm cart, and mean luminance was brought into the
6868
calibration range (82.7 → 66.1, ceiling 77.7) — no luminance deviation
69-
needed.
69+
needed. The surface language is procedural and reads **Object** coordinates:
70+
every part's origin is the segment pivot, so painted-panel wear, the walkway's
71+
diamond-plate bump and the diagonal hazard stripes on the trim rails are
72+
functions of segment-local position — identical in every instance and
73+
continuous across a snapped joint, exactly like the geometry.
7074

7175
**Framing deviation:** the still is an interior corridor run — the envelope
7276
surrounds the camera on five sides and the tiling joints are the proof, so

‎examples/modular-kit-snap/modular_kit_snap.py‎

Lines changed: 106 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -399,29 +399,118 @@ def fail(code, msg):
399399
# Render
400400
# ---------------------------------------------------------------------------
401401

402-
def make_material(name, rgb, rough=0.45, metallic=0.6, emit=None, estr=0.0):
402+
def make_material(name, rgb, rough=0.45, metallic=0.6, emit=None, estr=0.0,
403+
wear=0.0, tread=False, stripes=None):
404+
"""Principled material; optional procedural surface language.
405+
406+
Every texture reads Object coordinates, and every kit part's origin is the
407+
segment pivot, so the patterns are functions of segment-local position:
408+
identical in each instance and continuous across a snapped joint, exactly
409+
like the geometry. wear mottles colour and roughness; tread adds a
410+
diamond-plate bump; stripes=(rgb_b, period) paints diagonal hazard bands.
411+
"""
403412
mat = bpy.data.materials.new(name)
404413
mat.use_nodes = True
405-
b = mat.node_tree.nodes["Principled BSDF"]
414+
nt = mat.node_tree
415+
b = nt.nodes["Principled BSDF"]
406416
b.inputs["Base Color"].default_value = (*rgb, 1.0)
407417
b.inputs["Roughness"].default_value = rough
408418
b.inputs["Metallic"].default_value = metallic
409419
if emit is not None:
410420
sock = b.inputs.get("Emission Color") or b.inputs["Emission"]
411421
sock.default_value = (*emit, 1.0)
412422
b.inputs["Emission Strength"].default_value = estr
423+
if not (wear or tread or stripes):
424+
return mat
425+
coord = nt.nodes.new("ShaderNodeTexCoord").outputs["Object"]
426+
color_out = None
427+
if stripes:
428+
rgb_b, period = stripes
429+
wave = nt.nodes.new("ShaderNodeTexWave")
430+
wave.wave_type = "BANDS"
431+
wave.bands_direction = "DIAGONAL"
432+
# Blender wave phase is 20 * scale * coordinate: one band per `period` metres
433+
wave.inputs["Scale"].default_value = 2.0 * math.pi / (20.0 * period)
434+
wave.inputs["Distortion"].default_value = 0.0
435+
nt.links.new(coord, wave.inputs["Vector"])
436+
step = nt.nodes.new("ShaderNodeMath")
437+
step.operation = "GREATER_THAN"
438+
step.inputs[1].default_value = 0.5
439+
nt.links.new(wave.outputs["Fac"], step.inputs[0])
440+
mix = nt.nodes.new("ShaderNodeMix")
441+
mix.data_type = "RGBA"
442+
mix.inputs[6].default_value = (*rgb, 1.0)
443+
mix.inputs[7].default_value = (*rgb_b, 1.0)
444+
nt.links.new(step.outputs["Value"], mix.inputs[0])
445+
color_out = mix.outputs[2]
446+
if wear:
447+
noise = nt.nodes.new("ShaderNodeTexNoise")
448+
noise.inputs["Scale"].default_value = 3.5
449+
noise.inputs["Detail"].default_value = 8.0
450+
nt.links.new(coord, noise.inputs["Vector"])
451+
dirt = nt.nodes.new("ShaderNodeMix")
452+
dirt.data_type = "RGBA"
453+
dirt.blend_type = "MULTIPLY"
454+
dirt.inputs[0].default_value = wear
455+
if color_out is not None:
456+
nt.links.new(color_out, dirt.inputs[6])
457+
else:
458+
dirt.inputs[6].default_value = (*rgb, 1.0)
459+
ramp = nt.nodes.new("ShaderNodeValToRGB")
460+
ramp.color_ramp.elements[0].position = 0.38
461+
ramp.color_ramp.elements[0].color = (0.45, 0.42, 0.38, 1.0)
462+
ramp.color_ramp.elements[1].position = 0.68
463+
ramp.color_ramp.elements[1].color = (1.0, 1.0, 1.0, 1.0)
464+
nt.links.new(noise.outputs["Fac"], ramp.inputs["Fac"])
465+
nt.links.new(ramp.outputs["Color"], dirt.inputs[7])
466+
color_out = dirt.outputs[2]
467+
rmap = nt.nodes.new("ShaderNodeMapRange")
468+
rmap.inputs["To Min"].default_value = rough + 0.15
469+
rmap.inputs["To Max"].default_value = rough - 0.10
470+
nt.links.new(noise.outputs["Fac"], rmap.inputs["Value"])
471+
nt.links.new(rmap.outputs["Result"], b.inputs["Roughness"])
472+
if color_out is not None:
473+
nt.links.new(color_out, b.inputs["Base Color"])
474+
if tread:
475+
# diamond plate: two crossed wave bands make a lozenge lattice bump
476+
bump = nt.nodes.new("ShaderNodeBump")
477+
bump.inputs["Strength"].default_value = 1.0
478+
bump.inputs["Distance"].default_value = 0.012
479+
mul = nt.nodes.new("ShaderNodeMath")
480+
mul.operation = "MULTIPLY"
481+
for i, rot in enumerate((0.785, -0.785)):
482+
mapping = nt.nodes.new("ShaderNodeMapping")
483+
mapping.inputs["Rotation"].default_value = (0.0, 0.0, rot)
484+
nt.links.new(coord, mapping.inputs["Vector"])
485+
wv = nt.nodes.new("ShaderNodeTexWave")
486+
wv.wave_type = "BANDS"
487+
wv.bands_direction = "X"
488+
wv.wave_profile = "SAW"
489+
wv.inputs["Scale"].default_value = 6.0
490+
wv.inputs["Distortion"].default_value = 0.0
491+
nt.links.new(mapping.outputs["Vector"], wv.inputs["Vector"])
492+
gt = nt.nodes.new("ShaderNodeMath")
493+
gt.operation = "GREATER_THAN"
494+
gt.inputs[1].default_value = 0.72
495+
nt.links.new(wv.outputs["Fac"], gt.inputs[0])
496+
nt.links.new(gt.outputs["Value"], mul.inputs[i])
497+
nt.links.new(mul.outputs["Value"], bump.inputs["Height"])
498+
nt.links.new(bump.outputs["Normal"], b.inputs["Normal"])
413499
return mat
414500

415501

502+
# (name, rgb, roughness, metallic, emission rgb, emission strength, extras)
416503
SEGMENT_MATS = {
417-
"Shell": ("Shell", (0.075, 0.095, 0.115), 0.5, 0.8, None, 0.0),
418-
"Rib": ("Rib", (0.05, 0.05, 0.06), 0.45, 0.85, None, 0.0),
419-
"FloorPlate": ("FloorPlate", (0.06, 0.075, 0.09), 0.6, 0.7, None, 0.0),
420-
"Panel": ("Panel", (0.10, 0.21, 0.26), 0.42, 0.6, None, 0.0),
421-
"PanelBack": ("PanelBack", (0.06, 0.10, 0.13), 0.5, 0.75, None, 0.0),
422-
"PanelBolts": ("PanelBolts", (0.35, 0.38, 0.42), 0.3, 0.9, None, 0.0),
423-
"Trim": ("Trim", (0.85, 0.35, 0.08), 0.35, 0.3, None, 0.0),
424-
"Light": ("LightStrip", (0.30, 0.25, 0.18), 0.5, 0.2, (1.0, 0.75, 0.4), 8.0),
504+
"Shell": ("Shell", (0.055, 0.062, 0.072), 0.55, 0.55, None, 0.0, dict(wear=0.55)),
505+
"Rib": ("Rib", (0.16, 0.13, 0.09), 0.42, 0.85, None, 0.0, dict(wear=0.5)),
506+
"FloorPlate": ("FloorPlate", (0.20, 0.205, 0.21), 0.38, 0.9, None, 0.0,
507+
dict(wear=0.6, tread=True)),
508+
"Panel": ("Panel", (0.05, 0.12, 0.125), 0.62, 0.05, None, 0.0, dict(wear=0.45)),
509+
"PanelBack": ("PanelBack", (0.045, 0.05, 0.058), 0.48, 0.7, None, 0.0, dict(wear=0.4)),
510+
"PanelBolts": ("PanelBolts", (0.62, 0.58, 0.50), 0.3, 1.0, None, 0.0, {}),
511+
"Trim": ("Trim", (0.90, 0.42, 0.05), 0.45, 0.1, None, 0.0,
512+
dict(wear=0.35, stripes=((0.02, 0.02, 0.022), 0.16))),
513+
"Light": ("LightStrip", (0.30, 0.25, 0.18), 0.5, 0.2, (1.0, 0.78, 0.48), 14.0, {}),
425514
}
426515

427516

@@ -433,8 +522,8 @@ def mat_for(suffix):
433522
revision instantiated a duplicate material per part per segment)."""
434523
key = suffix.split(".")[0]
435524
if key not in _mat_cache:
436-
name, rgb, rough, metal, emit, estr = SEGMENT_MATS[key]
437-
_mat_cache[key] = make_material(name, rgb, rough, metal, emit, estr)
525+
name, rgb, rough, metal, emit, estr, extra = SEGMENT_MATS[key]
526+
_mat_cache[key] = make_material(name, rgb, rough, metal, emit, estr, **extra)
438527
return _mat_cache[key]
439528

440529

@@ -513,7 +602,8 @@ def build_bulkhead(sc):
513602
Not part of the kit — the corridor's destination for this still."""
514603
frame_mat = make_material("Bulkhead", (0.06, 0.06, 0.07), rough=0.4, metallic=0.85)
515604
door_mat = make_material("BulkheadDoor", (0.30, 0.16, 0.07), rough=0.5,
516-
metallic=0.3, emit=(1.0, 0.42, 0.12), estr=0.5)
605+
metallic=0.3, emit=(1.0, 0.42, 0.12), estr=0.22,
606+
wear=0.5)
517607
inner_y = WIDTH / 2 - WALL
518608
end_x = TILE * 4
519609
parts = []
@@ -560,6 +650,9 @@ def render_still(path, engine, falsify=False, close_camera=False):
560650
steps, so every joint reads as a seam — the render-scale exaggeration of
561651
the failure the check measures at 3 mm (probe_unsnapped)."""
562652
bpy.ops.wm.read_factory_settings(use_empty=True)
653+
# the factory reset freed every cached material; --output then --falsify
654+
# in one run would otherwise hand the second scene dead RNA references
655+
_mat_cache.clear()
563656
sc = bpy.context.scene
564657

565658
all_parts = []
19.1 KB
Loading

0 commit comments

Comments
 (0)