@@ -552,8 +552,12 @@ <h2>Source</h2>
552552 Columns = rising alpha; top rows = dark mid-tones that PNG clamps to
553553 white; lower rows = primaries that survive. Left panel bakes the
554554 closed-form mangling; right panel shows authored straight RGBA.
555+
556+ A dark bezel is painted into the texture so panel edges read as framed
557+ objects in the studio still (render-only — checks use fill_pattern).
555558 """</ span >
556559 gw, gh = < span class ="n "> 256</ span > , < span class ="n "> 192</ span >
560+ border = < span class ="n "> 10</ span >
557561 img = bpy.data.images.new(name, gw, gh, alpha=< span class ="k "> True</ span > , float_buffer=< span class ="k "> True</ span > )
558562 img.colorspace_settings.name = < span class ="s "> "Non-Color"</ span >
559563 px = [< span class ="n "> 0.0</ span > ] * (gw * gh * < span class ="n "> 4</ span > )
@@ -567,25 +571,32 @@ <h2>Source</h2>
567571 (< span class ="n "> 0.2</ span > , < span class ="n "> 0.35</ span > , < span class ="n "> 1.0</ span > ), < span class ="c "> # blue-leaning; G/B still clamp at low a → wash</ span >
568572 ]
569573 alphas = [< span class ="n "> 1</ span > / < span class ="n "> 255</ span > , < span class ="n "> 8</ span > / < span class ="n "> 255</ span > , < span class ="n "> 32</ span > / < span class ="n "> 255</ span > , < span class ="n "> 64</ span > / < span class ="n "> 255</ span > , < span class ="n "> 128</ span > / < span class ="n "> 255</ span > , < span class ="n "> 1.0</ span > ]
570- col_w = gw // n_cols
571- row_h = gh // len(row_colors)
574+ iw, ih = gw - < span class ="n "> 2</ span > * border, gh - < span class ="n "> 2</ span > * border
575+ col_w = max(< span class ="n "> 1</ span > , iw // n_cols)
576+ row_h = max(< span class ="n "> 1</ span > , ih // len(row_colors))
577+ bezel = (< span class ="n "> 0.04</ span > , < span class ="n "> 0.042</ span > , < span class ="n "> 0.048</ span > )
572578 < span class ="k "> for</ span > y < span class ="k "> in</ span > range(gh):
573- ri = min(y // row_h, len(row_colors) - < span class ="n "> 1</ span > )
574- rgb = row_colors[ri]
575579 < span class ="k "> for</ span > x < span class ="k "> in</ span > range(gw):
576- ci = min(x // col_w, n_cols - < span class ="n "> 1</ span > )
580+ i = (y * gw + x) * < span class ="n "> 4</ span >
581+ < span class ="k "> if</ span > x < border < span class ="k "> or</ span > y < border < span class ="k "> or</ span > x >= gw - border < span class ="k "> or</ span > y >= gh - border:
582+ px[i : i + < span class ="n "> 4</ span > ] = [bezel[< span class ="n "> 0</ span > ], bezel[< span class ="n "> 1</ span > ], bezel[< span class ="n "> 2</ span > ], < span class ="n "> 1.0</ span > ]
583+ < span class ="k "> continue</ span >
584+ xi, yi = x - border, y - border
585+ ri = min(yi // row_h, len(row_colors) - < span class ="n "> 1</ span > )
586+ ci = min(xi // col_w, n_cols - < span class ="n "> 1</ span > )
587+ rgb = row_colors[ri]
577588 a = alphas[ci]
578589 < span class ="k "> if</ span > mangled:
579590 r, g, b, _a = expected_float_png_rgba(*rgb, a)
580591 < span class ="k "> else</ span > :
581592 r, g, b = rgb
582- i = (y * gw + x) * < span class ="n "> 4</ span >
583593 px[i : i + < span class ="n "> 4</ span > ] = [r, g, b, < span class ="n "> 1.0</ span > ]
584594 img.pixels.foreach_set(px)
585595 < span class ="k "> return</ span > img
586596
587597
588598< span class ="k "> def</ span > easel_plane(name, image, loc, rot_z_deg):
599+ < span class ="s "> """Single-plane easel + short stand — no dual-surface z-fight."""</ span >
589600 me = bpy.data.meshes.new(name)
590601 bm = bmesh.new()
591602 < span class ="k "> try</ span > :
@@ -610,19 +621,46 @@ <h2>Source</h2>
610621 tex = nodes.new(< span class ="s "> "ShaderNodeTexImage"</ span > )
611622 tex.image = image
612623 tex.interpolation = < span class ="s "> "Closest"</ span >
624+ links.new(tex.outputs[< span class ="s "> "Color"</ span > ], bsdf.inputs[< span class ="s "> "Base Color"</ span > ])
613625 links.new(tex.outputs[< span class ="s "> "Color"</ span > ], bsdf.inputs[< span class ="s "> "Emission Color"</ span > ])
614- bsdf.inputs[< span class ="s "> "Base Color"</ span > ].default_value = (< span class ="n "> 0.0</ span > , < span class ="n "> 0.0</ span > , < span class ="n "> 0.0</ span > , < span class ="n "> 1.0</ span > )
615626 bsdf.inputs[< span class ="s "> "Roughness"</ span > ].default_value = < span class ="n "> 1.0</ span >
616627 specular_off(bsdf)
617- bsdf.inputs[< span class ="s "> "Emission Strength"</ span > ].default_value = < span class ="n "> 0.95</ span >
628+ < span class ="c "> # Faint emission so clamps read under a dim stage without washing the wall.</ span >
629+ bsdf.inputs[< span class ="s "> "Emission Strength"</ span > ].default_value = < span class ="n "> 0.22</ span >
618630 me.materials.append(mat)
619631
620- obj = bpy.data.objects.new(name, me)
621- obj.location = loc
622- obj.rotation_euler = (math.radians(< span class ="n "> 62</ span > ), < span class ="n "> 0.0</ span > , math.radians(rot_z_deg))
623- obj.scale = (< span class ="n "> 1.15</ span > , < span class ="n "> 1.15</ span > , < span class ="n "> 1.15</ span > )
624- bpy.context.collection.objects.link(obj)
625- < span class ="k "> return</ span > obj
632+ root = bpy.data.objects.new(name, < span class ="k "> None</ span > )
633+ root.location = loc
634+ root.rotation_euler = (math.radians(< span class ="n "> 62</ span > ), < span class ="n "> 0.0</ span > , math.radians(rot_z_deg))
635+ root.scale = (< span class ="n "> 0.88</ span > , < span class ="n "> 0.88</ span > , < span class ="n "> 0.88</ span > )
636+ bpy.context.collection.objects.link(root)
637+
638+ face = bpy.data.objects.new(name + < span class ="s "> "Face"</ span > , me)
639+ bpy.context.collection.objects.link(face)
640+ face.parent = root
641+
642+ stand_me = bpy.data.meshes.new(name + < span class ="s "> "Stand"</ span > )
643+ bm = bmesh.new()
644+ < span class ="k "> try</ span > :
645+ bmesh.ops.create_cube(bm, size=< span class ="n "> 1.0</ span > )
646+ bm.to_mesh(stand_me)
647+ < span class ="k "> finally</ span > :
648+ bm.free()
649+ smat = bpy.data.materials.new(name + < span class ="s "> "StandMat"</ span > )
650+ smat.use_nodes = < span class ="k "> True</ span >
651+ sb = smat.node_tree.nodes[< span class ="s "> "Principled BSDF"</ span > ]
652+ sb.inputs[< span class ="s "> "Base Color"</ span > ].default_value = (< span class ="n "> 0.045</ span > , < span class ="n "> 0.048</ span > , < span class ="n "> 0.055</ span > , < span class ="n "> 1.0</ span > )
653+ sb.inputs[< span class ="s "> "Roughness"</ span > ].default_value = < span class ="n "> 0.45</ span >
654+ < span class ="k "> if</ span > < span class ="s "> "Metallic"</ span > < span class ="k "> in</ span > sb.inputs:
655+ sb.inputs[< span class ="s "> "Metallic"</ span > ].default_value = < span class ="n "> 0.3</ span >
656+ stand_me.materials.append(smat)
657+ stand = bpy.data.objects.new(name + < span class ="s "> "Stand"</ span > , stand_me)
658+ < span class ="c "> # Fully below the card face — never intersects the evidence plane.</ span >
659+ stand.scale = (< span class ="n "> 0.06</ span > , < span class ="n "> 0.06</ span > , < span class ="n "> 0.28</ span > )
660+ stand.location = (< span class ="n "> 0.0</ span > , < span class ="n "> 0.04</ span > , -< span class ="n "> 0.88</ span > )
661+ bpy.context.collection.objects.link(stand)
662+ stand.parent = root
663+ < span class ="k "> return</ span > root
626664
627665
628666< span class ="k "> def</ span > render_still(path, engine):
@@ -631,8 +669,27 @@ <h2>Source</h2>
631669 png_card = make_gallery_card(< span class ="s "> "PngMangled"</ span > , mangled=< span class ="k "> True</ span > )
632670 exr_card = make_gallery_card(< span class ="s "> "ExrClean"</ span > , mangled=< span class ="k "> False</ span > )
633671
634- easel_plane(< span class ="s "> "PngPanel"</ span > , png_card, (-< span class ="n "> 1.22</ span > , < span class ="n "> 0.0</ span > , < span class ="n "> 1.18</ span > ), < span class ="n "> 10</ span > )
635- easel_plane(< span class ="s "> "ExrPanel"</ span > , exr_card, (< span class ="n "> 1.22</ span > , < span class ="n "> 0.0</ span > , < span class ="n "> 1.18</ span > ), -< span class ="n "> 10</ span > )
672+ < span class ="c "> # Mounted easels on a shared dark table — continuous base, no V-tear shadow.</ span >
673+ easel_plane(< span class ="s "> "PngPanel"</ span > , png_card, (-< span class ="n "> 1.15</ span > , < span class ="n "> 0.1</ span > , < span class ="n "> 1.28</ span > ), < span class ="n "> 5</ span > )
674+ easel_plane(< span class ="s "> "ExrPanel"</ span > , exr_card, (< span class ="n "> 1.15</ span > , < span class ="n "> 0.1</ span > , < span class ="n "> 1.28</ span > ), -< span class ="n "> 5</ span > )
675+
676+ table_me = bpy.data.meshes.new(< span class ="s "> "Table"</ span > )
677+ bm = bmesh.new()
678+ < span class ="k "> try</ span > :
679+ bmesh.ops.create_cube(bm, size=< span class ="n "> 1.0</ span > )
680+ bm.to_mesh(table_me)
681+ < span class ="k "> finally</ span > :
682+ bm.free()
683+ tmat = bpy.data.materials.new(< span class ="s "> "TableMat"</ span > )
684+ tmat.use_nodes = < span class ="k "> True</ span >
685+ tb = tmat.node_tree.nodes[< span class ="s "> "Principled BSDF"</ span > ]
686+ tb.inputs[< span class ="s "> "Base Color"</ span > ].default_value = (< span class ="n "> 0.022</ span > , < span class ="n "> 0.024</ span > , < span class ="n "> 0.028</ span > , < span class ="n "> 1.0</ span > )
687+ tb.inputs[< span class ="s "> "Roughness"</ span > ].default_value = < span class ="n "> 0.6</ span >
688+ table_me.materials.append(tmat)
689+ table = bpy.data.objects.new(< span class ="s "> "Table"</ span > , table_me)
690+ table.scale = (< span class ="n "> 1.5</ span > , < span class ="n "> 0.55</ span > , < span class ="n "> 0.08</ span > )
691+ table.location = (< span class ="n "> 0.0</ span > , < span class ="n "> 0.25</ span > , < span class ="n "> 0.28</ span > )
692+ scene.collection.objects.link(table)
636693
637694 floor_me = bpy.data.meshes.new(< span class ="s "> "Floor"</ span > )
638695 bm = bmesh.new()
@@ -644,20 +701,20 @@ <h2>Source</h2>
644701 fmat = bpy.data.materials.new(< span class ="s "> "Studio"</ span > )
645702 fmat.use_nodes = < span class ="k "> True</ span >
646703 fb = fmat.node_tree.nodes[< span class ="s "> "Principled BSDF"</ span > ]
647- fb.inputs[< span class ="s "> "Base Color"</ span > ].default_value = (< span class ="n "> 0.03 </ span > , < span class ="n "> 0.032 </ span > , < span class ="n "> 0.037 </ span > , < span class ="n "> 1.0</ span > )
648- fb.inputs[< span class ="s "> "Roughness"</ span > ].default_value = < span class ="n "> 0.7 </ span >
704+ fb.inputs[< span class ="s "> "Base Color"</ span > ].default_value = (< span class ="n "> 0.02 </ span > , < span class ="n "> 0.021 </ span > , < span class ="n "> 0.025 </ span > , < span class ="n "> 1.0</ span > )
705+ fb.inputs[< span class ="s "> "Roughness"</ span > ].default_value = < span class ="n "> 0.8 </ span >
649706 floor_me.materials.append(fmat)
650707 floor = bpy.data.objects.new(< span class ="s "> "Floor"</ span > , floor_me)
651708 scene.collection.objects.link(floor)
652709 wall = bpy.data.objects.new(< span class ="s "> "Wall"</ span > , floor_me.copy())
653- wall.location = (< span class ="n "> 0.0</ span > , < span class ="n "> 6 .8</ span > , < span class ="n "> 0.0</ span > )
710+ wall.location = (< span class ="n "> 0.0</ span > , < span class ="n "> 8 .8</ span > , < span class ="n "> 0.0</ span > )
654711 wall.rotation_euler = (math.radians(< span class ="n "> 90</ span > ), < span class ="n "> 0.0</ span > , < span class ="n "> 0.0</ span > )
655712 scene.collection.objects.link(wall)
656713
657714 world = bpy.data.worlds.new(< span class ="s "> "World"</ span > )
658715 world.use_nodes = < span class ="k "> True</ span >
659716 world.node_tree.nodes[< span class ="s "> "Background"</ span > ].inputs[< span class ="s "> "Color"</ span > ].default_value = (
660- < span class ="n "> 0.02 </ span > , < span class ="n "> 0.021 </ span > , < span class ="n "> 0.025 </ span > , < span class ="n "> 1.0</ span > ,
717+ < span class ="n "> 0.012 </ span > , < span class ="n "> 0.013 </ span > , < span class ="n "> 0.015 </ span > , < span class ="n "> 1.0</ span > ,
661718 )
662719 scene.world = world
663720
@@ -671,19 +728,19 @@ <h2>Source</h2>
671728 ob.rotation_euler = tuple(math.radians(a) < span class ="k "> for</ span > a < span class ="k "> in</ span > rot)
672729 scene.collection.objects.link(ob)
673730
674- light(< span class ="s "> "Key"</ span > , (-< span class ="n "> 3.8</ span > , -< span class ="n "> 4.8</ span > , < span class ="n "> 5.8</ span > ), < span class ="n "> 360 .0</ span > , < span class ="n "> 4.5</ span > , (< span class ="n "> 1.0</ span > , < span class ="n "> 0.96</ span > , < span class ="n "> 0.9</ span > ), (< span class ="n "> 52</ span > , < span class ="n "> 0</ span > , -< span class ="n "> 32</ span > ))
675- light(< span class ="s "> "Fill"</ span > , (< span class ="n "> 4.8</ span > , -< span class ="n "> 2.8</ span > , < span class ="n "> 1.6</ span > ), < span class ="n "> 70 .0</ span > , < span class ="n "> 9.0</ span > , (< span class ="n "> 0.75</ span > , < span class ="n "> 0.85</ span > , < span class ="n "> 1.0</ span > ), (< span class ="n "> 72</ span > , < span class ="n "> 0</ span > , < span class ="n "> 52</ span > ))
676- light(< span class ="s "> "Rim"</ span > , (< span class ="n "> 0.2</ span > , < span class ="n "> 4.0 </ span > , < span class ="n "> 2.8</ span > ), < span class ="n "> 220 .0</ span > , < span class ="n "> 3.2</ span > , (< span class ="n "> 0.6</ span > , < span class ="n "> 0.78</ span > , < span class ="n "> 1.0</ span > ), (-< span class ="n "> 62</ span > , < span class ="n "> 0</ span > , < span class ="n "> 178</ span > ))
677- light(< span class ="s "> "Wedge"</ span > , (< span class ="n "> 0.5</ span > , < span class ="n "> 5.2 </ span > , < span class ="n "> 2.8 </ span > ), < span class ="n "> 520 .0</ span > , < span class ="n "> 7.0</ span > , (< span class ="n "> 1.0</ span > , < span class ="n "> 0.72</ span > , < span class ="n "> 0.42</ span > ), (-< span class ="n "> 78</ span > , < span class ="n "> 0</ span > , < span class ="n "> 180</ span > ))
731+ light(< span class ="s "> "Key"</ span > , (-< span class ="n "> 3.8</ span > , -< span class ="n "> 4.8</ span > , < span class ="n "> 5.8</ span > ), < span class ="n "> 220 .0</ span > , < span class ="n "> 4.5</ span > , (< span class ="n "> 1.0</ span > , < span class ="n "> 0.96</ span > , < span class ="n "> 0.9</ span > ), (< span class ="n "> 52</ span > , < span class ="n "> 0</ span > , -< span class ="n "> 32</ span > ))
732+ light(< span class ="s "> "Fill"</ span > , (< span class ="n "> 4.8</ span > , -< span class ="n "> 2.8</ span > , < span class ="n "> 1.6</ span > ), < span class ="n "> 40 .0</ span > , < span class ="n "> 9.0</ span > , (< span class ="n "> 0.75</ span > , < span class ="n "> 0.85</ span > , < span class ="n "> 1.0</ span > ), (< span class ="n "> 72</ span > , < span class ="n "> 0</ span > , < span class ="n "> 52</ span > ))
733+ light(< span class ="s "> "Rim"</ span > , (< span class ="n "> 0.2</ span > , < span class ="n "> 4.8 </ span > , < span class ="n "> 2.8</ span > ), < span class ="n "> 120 .0</ span > , < span class ="n "> 3.2</ span > , (< span class ="n "> 0.6</ span > , < span class ="n "> 0.78</ span > , < span class ="n "> 1.0</ span > ), (-< span class ="n "> 62</ span > , < span class ="n "> 0</ span > , < span class ="n "> 178</ span > ))
734+ light(< span class ="s "> "Wedge"</ span > , (< span class ="n "> 0.5</ span > , < span class ="n "> 6.5 </ span > , < span class ="n "> 2.5 </ span > ), < span class ="n "> 450 .0</ span > , < span class ="n "> 7.0</ span > , (< span class ="n "> 1.0</ span > , < span class ="n "> 0.72</ span > , < span class ="n "> 0.42</ span > ), (-< span class ="n "> 78</ span > , < span class ="n "> 0</ span > , < span class ="n "> 180</ span > ))
678735
679736 aim = bpy.data.objects.new(< span class ="s "> "Aim"</ span > , < span class ="k "> None</ span > )
680- aim.location = (< span class ="n "> 0.0</ span > , < span class ="n "> 0.0 </ span > , < span class ="n "> 1.1 </ span > )
737+ aim.location = (< span class ="n "> 0.0</ span > , < span class ="n "> 0.1 </ span > , < span class ="n "> 1.15 </ span > )
681738 scene.collection.objects.link(aim)
682739
683740 cam_data = bpy.data.cameras.new(< span class ="s "> "Cam"</ span > )
684- cam_data.lens = < span class ="n "> 45 .0</ span >
741+ cam_data.lens = < span class ="n "> 50 .0</ span >
685742 cam = bpy.data.objects.new(< span class ="s "> "Cam"</ span > , cam_data)
686- cam.location = (< span class ="n "> 0.0</ span > , -< span class ="n "> 5.4 </ span > , < span class ="n "> 2.2 </ span > )
743+ cam.location = (< span class ="n "> 0.0</ span > , -< span class ="n "> 6.0 </ span > , < span class ="n "> 2.15 </ span > )
687744 con = cam.constraints.new(< span class ="s "> "TRACK_TO"</ span > )
688745 con.target = aim
689746 con.track_axis = < span class ="s "> "TRACK_NEGATIVE_Z"</ span >
0 commit comments