Skip to content

Commit f0500ce

Browse files
TMHSDigitalclaude
andcommitted
fix(examples): redesign png-exr-alpha as thin-bezel monitors on a walnut console
The boxy displays on struts and the grey plinth are replaced with the image-pixels-testcard monitor style: a thin polymer case with a rear housing, a machined neck and base plate, and a power LED. Both monitors stand on a walnut-topped console, lightly toed in, with brass desk plates (FLOAT -> PNG / FLOAT -> EXR) in front of them. The painted card trim shrinks from 10 px to 3 px so the physical case is the bezel. Card data, checks and exit codes are unchanged. Evidence (live runs, local): - .scratch/blender-5.2.1-windows-x64/blender.exe (Blender 5.2.1 LTS): check-only exit 0; --opaque-alpha exits 5 ("float->PNG max RGB err 0.0000076 < floor 0.9 - false-unpremul damage missing"); --output exit 0. - .scratch/blender-4.5.11-windows-x64/blender.exe (Blender 4.5.11 LTS): check-only exit 0; --output exit 0 (framing_ok). - Framing: fill x=0.725 y=0.667, margins l=0.059 r=0.028 b=0.089 t=0.044. - Contact sheet (docs/gallery/contact-sheets/png-exr-alpha-contact-sheet.webp): candidate luma 0.299 / stage 0.191 / warmth +0.061 / sat 0.201, against armature-bend 0.279/0.213/-0.428/0.144, damped-track-aim 0.151/0.145/+0.277/0.231 and bmesh-gear 0.236/0.177/+0.303/0.250. Stage and saturation sit inside the set. Luma is the highest because the self-lit data screens are the subject. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 4a4d25c commit f0500ce

7 files changed

Lines changed: 120 additions & 74 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ non-destructive path.
317317
</tr>
318318
<tr>
319319
<td width="46%" valign="middle">
320-
<a href="examples/png-exr-alpha/"><img src="examples/png-exr-alpha/preview.webp" alt="PNG vs EXR alpha: two framed verification displays on a shared plinth in a dark studio — left the float→PNG false-unpremul mangling clamps dark rows to white, right the EXR-clean straight buffer, nameplates reading FLOAT → PNG and FLOAT → EXR under a warm wall pool" /></a>
320+
<a href="examples/png-exr-alpha/"><img src="examples/png-exr-alpha/preview.webp" alt="PNG vs EXR alpha: two thin-bezel monitors on a walnut-topped console in a dark studio — left the float→PNG false-unpremul mangling clamps dark rows to white, right the EXR-clean straight buffer, brass desk plates reading FLOAT → PNG and FLOAT → EXR" /></a>
321321
</td>
322322
<td valign="middle">
323323

12.4 KB
Loading
6.01 KB
Loading

‎examples/gallery.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@
287287
"name": "png-exr-alpha",
288288
"dir": "examples/png-exr-alpha",
289289
"teaches": "Float-image PNG save trap — float_buffer=True Image.save() writes RGBA16 and unpremultiplies as if associated-alpha, clamping straight-authored dark values at low alpha to white (closed-form err 0.98 at RGB 0.02 / a=1/255).",
290-
"alt": "Two monitors on a plinth labelled FLOAT to PNG and FLOAT to EXR, showing alpha and colour bar strips side by side.",
290+
"alt": "Two thin-bezel monitors on a walnut console with brass desk plates reading FLOAT to PNG and FLOAT to EXR; the PNG screen washes its dark rows to white while the EXR screen keeps every bar.",
291291
"witnessesFix": "float→PNG IHDR is RGBA16 and matches q16(min(1, c/q16(a))); OpenEXR round-trips within 1e-5; byte→PNG is straight RGBA8; EXR color_mode=RGB drops alpha to opaque.",
292292
"hero": "docs/gallery/assets/png-exr-alpha-hero.webp",
293293
"preview": "examples/png-exr-alpha/preview.webp",

‎examples/png-exr-alpha/README.md‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,13 @@ treat a 4.4 run as a 4.5 substitute). The only version gate in the file is
5151
the EEVEE engine id for the optional render (`BLENDER_EEVEE_NEXT` on 4.x,
5252
`BLENDER_EEVEE` on 5.x).
5353

54-
**Render:** two framed verification displays on a shared plinth — the
55-
left face bakes the closed-form PNG mangling (dark rows flash to white at
56-
low-alpha columns), the right face shows the authored straight buffer
57-
(EXR-clean); nameplates read `FLOAT → PNG` and `FLOAT → EXR`. If the
58-
contract failed, both panels would read the same.
54+
**Render:** two thin-bezel monitors on machined stands, sharing a
55+
walnut-topped console — the left screen bakes the closed-form PNG mangling
56+
(dark rows flash to white at low-alpha columns), the right screen shows the
57+
authored straight buffer (EXR-clean); brass desk plates in front read
58+
`FLOAT → PNG` and `FLOAT → EXR`. If the contract failed, both screens would
59+
read the same. The render path gates framing through
60+
`examples/gallery_framing.py` (exit 10) before writing the still.
5961

6062
## Run
6163

‎examples/png-exr-alpha/png_exr_alpha.py‎

Lines changed: 111 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ def make_gallery_card(name, mangled):
352352
use fill_pattern).
353353
"""
354354
gw, gh = 256, 192
355-
border = 10
355+
border = 3
356356
img = bpy.data.images.new(name, gw, gh, alpha=True, float_buffer=True)
357357
img.colorspace_settings.name = "Non-Color"
358358
px = [0.0] * (gw * gh * 4)
@@ -391,42 +391,46 @@ def make_gallery_card(name, mangled):
391391

392392

393393
def fixture_mats():
394-
"""Designed non-data surfaces: dark polymer bezels, rail-steel struts,
395-
machined nameplates, plinth — no Principled defaults."""
396-
def mat_principled(name, color, metallic, rough):
394+
"""Designed non-data surfaces: polymer monitor cases, machined stands,
395+
a walnut-topped console, brass desk plates — no Principled defaults."""
396+
def mat_principled(name, color, metallic, rough, noise=None):
397397
mat = bpy.data.materials.new(name)
398398
mat.use_nodes = True
399-
bsdf = mat.node_tree.nodes["Principled BSDF"]
399+
nt = mat.node_tree
400+
bsdf = nt.nodes["Principled BSDF"]
400401
bsdf.inputs["Base Color"].default_value = (*color, 1.0)
401402
bsdf.inputs["Metallic"].default_value = metallic
402403
bsdf.inputs["Roughness"].default_value = rough
404+
if noise:
405+
# wear/grain: noise-mottled base color, never a flat slot
406+
scale, amount = noise
407+
tex = nt.nodes.new("ShaderNodeTexNoise")
408+
tex.inputs["Scale"].default_value = scale
409+
tex.inputs["Detail"].default_value = 8.0
410+
ramp = nt.nodes.new("ShaderNodeValToRGB")
411+
ramp.color_ramp.elements[0].color = (*(c * (1 - amount) for c in color), 1.0)
412+
ramp.color_ramp.elements[1].color = (*(min(1.0, c * (1 + amount)) for c in color), 1.0)
413+
nt.links.new(tex.outputs["Fac"], ramp.inputs["Fac"])
414+
nt.links.new(ramp.outputs["Color"], bsdf.inputs["Base Color"])
403415
return mat
404416

417+
glow = mat_principled("PowerLed", (0.0, 0.0, 0.0), 0.0, 0.5)
418+
gb = glow.node_tree.nodes["Principled BSDF"]
419+
gb.inputs["Emission Color"].default_value = (0.10, 0.85, 0.75, 1.0)
420+
gb.inputs["Emission Strength"].default_value = 6.0
405421
return {
406-
"bezel": mat_principled("BezelPolymer", (0.045, 0.048, 0.055), 0.1, 0.5),
407-
"rail": mat_principled("RailSteel", (0.075, 0.08, 0.09), 0.6, 0.45),
408-
"plinth": mat_principled("PlinthSteel", (0.10, 0.11, 0.12), 0.6, 0.5),
409-
"plate": mat_principled("PlateSteel", (0.14, 0.15, 0.17), 0.7, 0.4),
422+
"case": mat_principled("CasePolymer", (0.030, 0.034, 0.042), 0.0, 0.38),
423+
"stand": mat_principled("StandMetal", (0.07, 0.075, 0.085), 0.75, 0.34,
424+
noise=(40.0, 0.25)),
425+
"walnut": mat_principled("ConsoleWalnut", (0.15, 0.065, 0.03), 0.0, 0.42,
426+
noise=(18.0, 0.35)),
427+
"body": mat_principled("ConsoleLacquer", (0.028, 0.029, 0.033), 0.0, 0.55),
428+
"brass": mat_principled("PlateBrass", (0.80, 0.58, 0.26), 1.0, 0.28,
429+
noise=(60.0, 0.12)),
430+
"led": glow,
410431
}
411432

412433

413-
def box_obj(name, dims, loc, mat, rot_x=0.0):
414-
me = bpy.data.meshes.new(name)
415-
bm = bmesh.new()
416-
try:
417-
bmesh.ops.create_cube(bm, size=1.0)
418-
bm.to_mesh(me)
419-
finally:
420-
bm.free()
421-
me.materials.append(mat)
422-
ob = bpy.data.objects.new(name, me)
423-
ob.scale = dims
424-
ob.location = loc
425-
ob.rotation_euler = (rot_x, 0.0, 0.0)
426-
bpy.context.collection.objects.link(ob)
427-
return ob
428-
429-
430434
def face_mesh(name):
431435
"""UV-mapped unit plane (create_grid lies in XY facing +Z; the display
432436
unit rotates it vertical)."""
@@ -467,59 +471,97 @@ def face_mat(name, image):
467471

468472

469473
def caption_mat():
470-
mat = bpy.data.materials.new("CaptionGrey")
474+
# engraved-ink lettering on the brass desk plates
475+
mat = bpy.data.materials.new("CaptionInk")
471476
mat.use_nodes = True
472477
cb = mat.node_tree.nodes["Principled BSDF"]
473-
cb.inputs["Base Color"].default_value = (0.42, 0.44, 0.48, 1.0)
474-
cb.inputs["Metallic"].default_value = 0.2
475-
cb.inputs["Roughness"].default_value = 0.6
476-
sock = cb.inputs.get("Emission Color") or cb.inputs["Emission"]
477-
sock.default_value = (0.38, 0.40, 0.45, 1.0)
478-
cb.inputs["Emission Strength"].default_value = 0.5
478+
cb.inputs["Base Color"].default_value = (0.012, 0.011, 0.010, 1.0)
479+
cb.inputs["Roughness"].default_value = 0.5
479480
return mat
480481

481482

482-
def display_unit(name, image, caption, x, mats, capmat):
483-
"""One verification display: the emissive data face seated proud of a
484-
thick dark bezel, carried by a rear strut and foot, with a machined
485-
nameplate under the bezel — a physical instrument standing on the
486-
shared plinth, not a floating texture."""
483+
def bevel_box(name, dims, loc, mat, bevel=0.0, rot=(0.0, 0.0, 0.0)):
484+
"""A world-dimensioned box (dims baked into the mesh, so a bevel stays
485+
even); a small bevel catches a machined edge line."""
486+
from mathutils import Vector
487+
488+
me = bpy.data.meshes.new(name)
489+
bm = bmesh.new()
490+
try:
491+
bmesh.ops.create_cube(bm, size=1.0)
492+
bmesh.ops.scale(bm, vec=Vector(dims), verts=bm.verts)
493+
bm.to_mesh(me)
494+
finally:
495+
bm.free()
496+
me.materials.append(mat)
497+
ob = bpy.data.objects.new(name, me)
498+
ob.location = loc
499+
ob.rotation_euler = rot
500+
bpy.context.collection.objects.link(ob)
501+
if bevel > 0.0:
502+
mod = ob.modifiers.new("EdgeBevel", "BEVEL")
503+
mod.width = bevel
504+
mod.segments = 3
505+
mod.limit_method = "ANGLE"
506+
me.shade_smooth()
507+
me.set_sharp_from_angle(angle=math.radians(40.0))
508+
return ob
509+
510+
511+
DESK_TOP = 0.62 # console top surface height
512+
513+
514+
def display_unit(name, image, caption, x, yaw_deg, mats, capmat):
515+
"""One verification monitor: the emissive data face inside a thin
516+
polymer case on a machined neck and base plate, with a brass desk plate
517+
in front naming the save path — the image-pixels-testcard monitor, not a
518+
floating texture."""
487519
root = bpy.data.objects.new(name, None)
488-
root.location = (x, 0.0, 0.0)
520+
root.location = (x, 0.0, DESK_TOP)
521+
root.rotation_euler = (0.0, 0.0, math.radians(yaw_deg))
489522
bpy.context.collection.objects.link(root)
490523

491524
def adopt(ob):
492525
ob.parent = root
493526
return ob
494527

495-
# bezel with real thickness; the face sits proud of its front surface
496-
adopt(box_obj(name + "Bezel", (2.64, 0.16, 2.04), (0.0, 0.0, 1.80),
497-
mats["bezel"]))
528+
sw, sh = 2.40, 1.80 # 4:3, matching the 256x192 card
529+
scr_z = 1.38 # screen center above the desk top
498530
face = bpy.data.objects.new(name + "Face", face_mesh(name + "Face"))
499531
bpy.context.collection.objects.link(face)
500532
face.data.materials.append(face_mat(name + "FaceMat", image))
501-
face.location = (0.0, -0.085, 1.80)
533+
face.location = (0.0, -0.001, scr_z)
502534
face.rotation_euler = (math.radians(90), 0.0, 0.0)
503-
face.scale = (1.20, 0.90, 1.0)
535+
face.scale = (sw / 2.0, sh / 2.0, 1.0)
504536
adopt(face)
505-
# rear strut leaning onto the bezel back, foot on the plinth top
506-
adopt(box_obj(name + "Strut", (0.26, 0.30, 1.30), (0.0, 0.42, 1.10),
507-
mats["rail"], rot_x=math.radians(18)))
508-
adopt(box_obj(name + "Foot", (0.60, 0.80, 0.10), (0.0, 0.72, 0.50),
509-
mats["rail"]))
510-
# nameplate bridging bezel bottom and plinth top
511-
adopt(box_obj(name + "Plate", (1.60, 0.06, 0.26), (0.0, -0.03, 0.63),
512-
mats["plate"]))
537+
# thin case: 0.07 bezel around the face, a thicker rear housing
538+
adopt(bevel_box(name + "Case", (sw + 0.14, 0.09, sh + 0.14), (0.0, 0.046, scr_z),
539+
mats["case"], bevel=0.025))
540+
adopt(bevel_box(name + "Housing", (sw * 0.62, 0.16, sh * 0.55),
541+
(0.0, 0.16, scr_z - 0.05), mats["case"], bevel=0.04))
542+
adopt(bevel_box(name + "Neck", (0.26, 0.12, 0.95), (0.0, 0.27, 0.52),
543+
mats["stand"], bevel=0.02))
544+
adopt(bevel_box(name + "Base", (1.10, 0.62, 0.05), (0.0, 0.18, 0.025),
545+
mats["stand"], bevel=0.02))
546+
adopt(bevel_box(name + "Led", (0.05, 0.02, 0.02), (sw / 2.0 - 0.10, -0.004,
547+
scr_z - sh / 2.0 - 0.035), mats["led"]))
548+
# brass tent plate leaning back on the desk, the caption on its face
549+
tilt = math.radians(-24.0)
550+
adopt(bevel_box(name + "Plate", (1.46, 0.035, 0.26), (0.0, -0.58, 0.135),
551+
mats["brass"], bevel=0.008, rot=(tilt, 0.0, 0.0)))
552+
adopt(bevel_box(name + "PlateFoot", (1.40, 0.16, 0.02), (0.0, -0.53, 0.01),
553+
mats["stand"], bevel=0.005))
513554
cu = bpy.data.curves.new(name + "Caption", "FONT")
514555
cu.body = caption
515556
cu.align_x = "CENTER"
516557
cu.align_y = "CENTER"
517558
cu.size = 0.15
518-
cu.extrude = 0.004
559+
cu.extrude = 0.003
519560
cu.materials.append(capmat)
520561
cap = bpy.data.objects.new(name + "Caption", cu)
521-
cap.location = (0.0, -0.065, 0.63)
522-
cap.rotation_euler = (math.radians(90), 0.0, 0.0)
562+
# on the plate's front face: offset along the tilted plate normal
563+
cap.location = (0.0, -0.58 - 0.021 * math.cos(tilt), 0.135 - 0.021 * math.sin(tilt))
564+
cap.rotation_euler = (math.radians(90) + tilt, 0.0, 0.0)
523565
bpy.context.collection.objects.link(cap)
524566
adopt(cap)
525567
return root
@@ -533,13 +575,17 @@ def render_still(path, engine):
533575
png_card = make_gallery_card("PngMangled", mangled=True)
534576
exr_card = make_gallery_card("ExrClean", mangled=False)
535577

536-
# The verification station: two framed displays on a shared plinth —
578+
# The verification station: two monitors on a shared console —
537579
# left bakes the closed-form PNG mangling, right is EXR-clean.
538580
mats = fixture_mats()
539581
capmat = caption_mat()
540-
display_unit("PngDisplay", png_card, "FLOAT → PNG", -1.45, mats, capmat)
541-
display_unit("ExrDisplay", exr_card, "FLOAT → EXR", 1.45, mats, capmat)
542-
box_obj("Plinth", (6.10, 1.80, 0.45), (0.0, 0.35, 0.225), mats["plinth"])
582+
display_unit("PngDisplay", png_card, "FLOAT → PNG", -1.42, 6.0, mats, capmat)
583+
display_unit("ExrDisplay", exr_card, "FLOAT → EXR", 1.42, -6.0, mats, capmat)
584+
# shared console: walnut top slab on a dark lacquered body
585+
bevel_box("ConsoleTop", (6.40, 1.90, 0.08), (0.0, 0.05, DESK_TOP - 0.04),
586+
mats["walnut"], bevel=0.02)
587+
bevel_box("Console", (6.20, 1.70, DESK_TOP - 0.08), (0.0, 0.10, (DESK_TOP - 0.08) / 2),
588+
mats["body"], bevel=0.03)
543589

544590
floor_me = bpy.data.meshes.new("Floor")
545591
bm = bmesh.new()
@@ -593,17 +639,15 @@ def light(name, loc, energy, size, col, target):
593639
(0.0, 9.0, 2.6))
594640

595641
aim = bpy.data.objects.new("Aim", None)
596-
aim.location = (-0.10, 0.2, 1.38)
642+
aim.location = (-0.10, 0.2, 1.12)
597643
scene.collection.objects.link(aim)
598644

599645
# Gentle 3/4 from the right: both faces stay fully readable while the
600-
# bezels, struts, and plinth show their thickness. Aim dropped 0.23 from
601-
# the original 1.65 so the plinth clears the bottom edge (framing gate)
602-
# without changing the distance or the composition.
646+
# cases, stands and console show their thickness.
603647
cam_data = bpy.data.cameras.new("Cam")
604-
cam_data.lens = 50.0
648+
cam_data.lens = 60.0
605649
cam = bpy.data.objects.new("Cam", cam_data)
606-
cam.location = (3.9, -9.3, 3.05)
650+
cam.location = (4.3, -11.0, 3.2)
607651
con = cam.constraints.new("TRACK_TO")
608652
con.target = aim
609653
con.track_axis = "TRACK_NEGATIVE_Z"
@@ -631,11 +675,11 @@ def light(name, loc, energy, size, col, target):
631675
o for o in scene.objects
632676
if o.name.startswith(("PngDisplay", "ExrDisplay")) and o.type in {"MESH", "FONT"}
633677
]
634-
plinth = scene.objects.get("Plinth")
678+
console = [scene.objects[n] for n in ("ConsoleTop", "Console")]
635679
fcode = gallery_framing.check_framing(
636680
scene, cam,
637681
hero=hero,
638-
elements=hero + ([plinth] if plinth else []),
682+
elements=hero + console,
639683
stage=[floor, wall],
640684
)
641685
if fcode:
12.5 KB
Loading

0 commit comments

Comments
 (0)