Skip to content

Commit f5effd7

Browse files
TMHSDigitalclaude
andcommitted
fix(examples): redesign light-link-studio as two porcelain chess kings under one linked key
After the check passes on its matte spheres, the still hides them and stands two identical lathed porcelain chess kings (gilt collar and cross, one shared material) in the hero and decoy collections. The only difference between the kings is the linked key, recoloured amber for the still: the LINKED king glows warm, the UNLINKED king stays cool grey. The shared fill and rim are pulled down for the still only, since glossy porcelain picks up far more of them than the matte check spheres. The still now renders denoised (64 samples) and runs the gallery_framing gate (exit 10), which it previously skipped. Check logic is unchanged. Evidence (live runs, local): - E:/Blender-Developer-Tools/.scratch/blender-5.2.1-windows-x64/blender.exe (Blender 5.2.1 LTS): check exit 0, linked hero=0.862 decoy=0.232 ratio=3.7x; unlinked rise=238%, hero_drift=0.1%. --output exit 0. --skip-link exits 5 ("receiver_collection assignment did not read back"). - E:/Blender-Developer-Tools/.scratch/blender-4.5.11-windows-x64/blender.exe (Blender 4.5.11 LTS): check and --output exit 0, same numbers, framing_ok. - Framing: fill x=0.756 y=0.822; margins l=0.122 r=0.122 b=0.128 t=0.050. - Contact sheet: candidate luma 0.208 / stage 0.163 / warmth +0.275 / sat 0.242, 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.237/0.252/+0.092/0.153. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 5a29c6f commit f5effd7

7 files changed

Lines changed: 115 additions & 14 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@ consumed-input compositing contract, and a save/reload round-trip.
507507
</tr>
508508
<tr>
509509
<td width="46%" valign="middle">
510-
<a href="examples/light-link-studio/"><img src="examples/light-link-studio/preview.webp" alt="Light link studio: a blazing orange hero sphere and a dim steel decoy staged as specimens on turned-metal pedestals, LINKED and UNLINKED placards on each plinth, a warm floor inlay glowing under the hero only, a warm light pool raking the wall behind - one key light restricted to the hero collection" /></a>
510+
<a href="examples/light-link-studio/"><img src="examples/light-link-studio/preview.webp" alt="Light link studio: two identical porcelain chess kings on dark plinths, the LINKED king glowing under an amber key with a warm floor inlay beneath it, the UNLINKED king cool grey beside it - one key light restricted to the hero collection" /></a>
511511
</td>
512512
<td valign="middle">
513513

-32.4 KB
Loading
1.57 KB
Loading

‎examples/gallery.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@
419419
"name": "light-link-studio",
420420
"dir": "examples/light-link-studio",
421421
"teaches": "One key, one hero: a light linked to a receiver collection lights only the hero, proven by two pixel renders in one pass. Linked: 3.6x luminance ratio; unlinked in the same check: the decoy rises 233% while the hero holds at 0.3% drift.",
422-
"alt": "Two spheres on pedestals: a brightly lit orange sphere labelled LINKED and a dark grey sphere labelled UNLINKED.",
422+
"alt": "Two identical porcelain chess kings on dark plinths: the left, labelled LINKED, glows under an amber key; the right, labelled UNLINKED, stays cool grey.",
423423
"witnessesFix": "The API is obj.light_linking on the light OBJECT (ld.light_linking is an AttributeError). Luminance is sampled at each sphere's projected center; EEVEE honors linking too (probe-measured 3.8x on 4.5.11 EEVEE Next and 5.1.2 EEVEE), Cycles pinned for deterministic samples.",
424424
"hero": "docs/gallery/assets/light-link-studio-hero.webp",
425425
"preview": "examples/light-link-studio/preview.webp",

‎examples/light-link-studio/README.md‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,16 @@ below 50%), and a non-surgical restriction (exit 8, hero drift above 5%).
4141
Blender 4.5 LTS and 5.1 (3.7x vs 3.6x linked; rise 238% vs 233% — inside the
4242
rise noise described above).
4343

44-
The render is the contract at a glance: the spheres staged as studio
45-
specimens on matching turned-metal pedestals — the blazing orange hero over
46-
its LINKED placard with the warm floor inlay marking the linked light's
47-
footprint, the cold steel decoy over UNLINKED with its inlay dark, a warm
48-
pool raking the wall behind them — one key, one hero.
44+
The render is the contract at a glance, on designed subjects: after the
45+
check passes on the spheres, the still hides them and stands two
46+
**identical** lathed porcelain chess kings (gilt collar and cross, one
47+
shared material) in the hero and decoy collections. The only difference
48+
between the kings is the linked key, recoloured amber for the still: the
49+
LINKED king glows warm over its placard and the warm floor inlay, the
50+
UNLINKED king stays cool grey. The shared fill and rim are pulled down for
51+
the still only, because glossy porcelain picks up far more of them than the
52+
matte check spheres do. The still's render path gates framing through
53+
`examples/gallery_framing.py` (exit 10) before writing.
4954

5055
## Run
5156

@@ -77,6 +82,7 @@ against it.
7782
| 7 | Decoy rise below gate when unlinked |
7883
| 8 | Hero drifted across the unlink |
7984
| 9 | `--output` produced no file |
85+
| 10 | Framing gate violation on the `--output` path (`gallery_framing`) |
8086

8187
The `blender-smoke` workflow runs the check on Blender 5.2 LTS and 4.5 LTS
8288
(5.1 on the weekly cron, the `needs-5.1` PR label, or manual dispatch).

‎examples/light-link-studio/light_link_studio.py‎

Lines changed: 102 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@
3333
import bpy, bmesh, sys, os, math, argparse, tempfile, shutil
3434
from mathutils import Matrix
3535

36+
# Shared Layer 1 framing measurement (render path only) — see
37+
# gallery_framing.py for the __file__-relative import shim this relies on.
38+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir))
39+
import gallery_framing # noqa: E402
40+
3641
HERO_RGB = (0.85, 0.30, 0.08) # hazard orange
3742
DECOY_RGB = (0.30, 0.34, 0.40) # cold steel
3843
KEY_ENERGY = 560.0
@@ -375,7 +380,85 @@ def placard(sc, text, x, plate_m, stand_m, text_m):
375380
sc.collection.objects.link(ob)
376381

377382

378-
def render_still(sc, path):
383+
# Render-only chess king, lathed from an (r, z) profile standing on z=0.
384+
# Faces between KING_BAND z-limits take the gilt material (index 1).
385+
KING_PROFILE = [
386+
(0.0, 0.0), (0.34, 0.0), (0.355, 0.025), (0.345, 0.065), (0.29, 0.085),
387+
(0.30, 0.115), (0.26, 0.135), (0.21, 0.19), (0.165, 0.31), (0.14, 0.45),
388+
(0.145, 0.55), (0.22, 0.585), (0.235, 0.615), (0.175, 0.64),
389+
(0.165, 0.67), (0.20, 0.79), (0.25, 0.89), (0.258, 0.915), (0.215, 0.94),
390+
(0.155, 0.99), (0.075, 1.02), (0.0, 1.03),
391+
]
392+
KING_BAND = (0.57, 0.65)
393+
KING_SEGS = 48
394+
395+
396+
def chess_king(sc, name, x, z0, coll, body_m, gilt_m):
397+
me = bpy.data.meshes.new(name)
398+
bm = bmesh.new()
399+
try:
400+
rings = []
401+
for r, z in KING_PROFILE:
402+
if r == 0.0:
403+
rings.append([bm.verts.new((0.0, 0.0, z))])
404+
else:
405+
rings.append([bm.verts.new((r * math.cos(2 * math.pi * s / KING_SEGS),
406+
r * math.sin(2 * math.pi * s / KING_SEGS), z))
407+
for s in range(KING_SEGS)])
408+
for a, b in zip(rings, rings[1:]):
409+
zmid = 0.5 * (a[0].co.z + b[0].co.z)
410+
idx = 1 if KING_BAND[0] <= zmid <= KING_BAND[1] else 0
411+
for s in range(KING_SEGS):
412+
t = (s + 1) % KING_SEGS
413+
if len(a) == 1:
414+
f = bm.faces.new((a[0], b[t], b[s]))
415+
elif len(b) == 1:
416+
f = bm.faces.new((a[s], a[t], b[0]))
417+
else:
418+
f = bm.faces.new((a[s], a[t], b[t], b[s]))
419+
f.smooth = True
420+
f.material_index = idx
421+
# gilt cross finial: an upright and a crossbar
422+
for dims, cz in (((0.055, 0.055, 0.24), 1.13), ((0.19, 0.075, 0.055), 1.17)):
423+
res = bmesh.ops.create_cube(bm, size=1.0)
424+
for v in res["verts"]:
425+
v.co = (v.co.x * dims[0], v.co.y * dims[1], v.co.z * dims[2] + cz)
426+
for f in {f for v in res["verts"] for f in v.link_faces}:
427+
f.material_index = 1
428+
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
429+
bm.to_mesh(me)
430+
finally:
431+
bm.free()
432+
me.set_sharp_from_angle(angle=math.radians(45.0))
433+
me.materials.append(body_m)
434+
me.materials.append(gilt_m)
435+
ob = bpy.data.objects.new(name, me)
436+
ob.location = (x, 0.0, z0)
437+
coll.objects.link(ob)
438+
return ob
439+
440+
441+
def render_still(sc, path, key, hero, decoy):
442+
"""The gallery still stages the contract on designed subjects: the checked
443+
spheres are hidden and two identical porcelain chess kings stand in their
444+
collections. The kings share one material, so the only difference between
445+
them is the linked key — recoloured amber for the still (the check ran
446+
first, on the untouched scene)."""
447+
porcelain = _principled("Porcelain", (0.66, 0.64, 0.60), 0.22)
448+
gilt = _principled("Gilt", (0.78, 0.56, 0.24), 0.28, metallic=1.0)
449+
kings = []
450+
for src in (hero, decoy):
451+
src.hide_render = True
452+
coll = src.users_collection[0]
453+
kings.append(chess_king(sc, "King" + src.name, src.location.x, 0.70,
454+
coll, porcelain, gilt))
455+
key.data.color = (1.0, 0.48, 0.16)
456+
key.data.energy = KEY_ENERGY * 1.2
457+
# the unlinked king must sit near the fill floor as the check measured it
458+
# on the matte decoy sphere; porcelain reflects far more of the shared
459+
# fill and rim, so both are pulled down for the still only
460+
sc.objects["Fill"].data.energy = FILL_ENERGY * 0.45
461+
sc.objects["Rim"].data.energy = 70.0
379462
plate_m = _principled("PlateBlack", (0.03, 0.032, 0.038), 0.6)
380463
stand_m = _principled("StandMetal", (0.16, 0.17, 0.20), 0.3, metallic=0.9)
381464
# faint self-glow: the UNLINKED placard must read even in the decoy's dark
@@ -386,16 +469,26 @@ def render_still(sc, path):
386469
# the gallery still is the linked state, on Cycles for the same
387470
# deterministic sampling as the check
388471
sc.render.engine = 'CYCLES'
389-
sc.cycles.samples = 48
390-
sc.cycles.use_denoising = False
472+
sc.cycles.samples = 64
473+
sc.cycles.use_denoising = True
391474
sc.render.resolution_x = 1280
392475
sc.render.resolution_y = 720
393476
sc.render.resolution_percentage = 100
394477
sc.render.image_settings.file_format = 'PNG'
395478
sc.render.filepath = path
396479
sc.view_settings.view_transform = 'Standard'
480+
bpy.context.view_layer.update()
481+
# Layer 1 framing gate before the beauty render (exit 10 on violation)
482+
stage = [o for o in sc.objects if o.name.startswith(("Floor", "Wall", "Inlay"))]
483+
placards = [o for o in sc.objects if o.name.startswith(("Plate", "PlacardText"))]
484+
peds = [o for o in sc.objects if o.name.startswith("Ped")]
485+
fcode = gallery_framing.check_framing(sc, sc.camera, hero=kings + peds,
486+
elements=kings + peds + placards,
487+
stage=stage)
488+
if fcode:
489+
return fcode
397490
bpy.ops.render.render(write_still=True)
398-
return os.path.exists(path) and os.path.getsize(path) > 0
491+
return 0 if os.path.exists(path) and os.path.getsize(path) > 0 else 9
399492

400493

401494
def main():
@@ -415,9 +508,11 @@ def main():
415508
return code
416509

417510
if args.output:
418-
if not render_still(sc, os.path.abspath(args.output)):
419-
print("ERROR: render produced no file", file=sys.stderr)
420-
return 9
511+
rcode = render_still(sc, os.path.abspath(args.output), key, hero, decoy)
512+
if rcode:
513+
if rcode == 9:
514+
print("ERROR: render produced no file", file=sys.stderr)
515+
return rcode
421516
print(f"rendered still {args.output}")
422517

423518
print("light-link-studio OK")
-19.6 KB
Loading

0 commit comments

Comments
 (0)