@@ -217,6 +217,87 @@ def principled(name, base, rough, metal=0.0, noise=None, coat=0.0, emit=None):
217217 return mat
218218
219219
220+ def glaze (name , dark , light ):
221+ """Glazed ceramic: colour pooling between two tones on low-frequency noise,
222+ a clear coat, and a faint roughness ripple so highlights break up."""
223+ mat = bpy .data .materials .new (name )
224+ mat .use_nodes = True
225+ nt = mat .node_tree
226+ b = nt .nodes ["Principled BSDF" ]
227+ b .inputs ["Roughness" ].default_value = 0.22
228+ b .inputs ["Coat Weight" ].default_value = 0.8
229+ b .inputs ["Coat Roughness" ].default_value = 0.06
230+ tex = nt .nodes .new ("ShaderNodeTexNoise" )
231+ tex .inputs ["Scale" ].default_value = 3.5
232+ tex .inputs ["Detail" ].default_value = 4.0
233+ ramp = nt .nodes .new ("ShaderNodeValToRGB" )
234+ ramp .color_ramp .elements [0 ].position = 0.35
235+ ramp .color_ramp .elements [0 ].color = (* dark , 1.0 )
236+ ramp .color_ramp .elements [1 ].position = 0.7
237+ ramp .color_ramp .elements [1 ].color = (* light , 1.0 )
238+ nt .links .new (tex .outputs ["Fac" ], ramp .inputs ["Fac" ])
239+ nt .links .new (ramp .outputs ["Color" ], b .inputs ["Base Color" ])
240+ return mat
241+
242+
243+ def walnut_mat (name ):
244+ """Oiled walnut: banded grain from a distorted wave texture along X,
245+ two-tone ramp, satin roughness (not a mirror)."""
246+ mat = bpy .data .materials .new (name )
247+ mat .use_nodes = True
248+ nt = mat .node_tree
249+ b = nt .nodes ["Principled BSDF" ]
250+ b .inputs ["Coat Weight" ].default_value = 0.15
251+ b .inputs ["Coat Roughness" ].default_value = 0.3
252+ # Object-space coordinates turned 45 degrees about X, so the bands vary
253+ # across both the top and the front face while running along X (the
254+ # plinth's length), the way long-grain stock looks.
255+ coord = nt .nodes .new ("ShaderNodeTexCoord" )
256+ mapping = nt .nodes .new ("ShaderNodeMapping" )
257+ mapping .inputs ["Rotation" ].default_value = (math .radians (45.0 ), 0.0 , 0.0 )
258+ mapping .inputs ["Scale" ].default_value = (0.35 , 1.0 , 1.0 )
259+ nt .links .new (coord .outputs ["Object" ], mapping .inputs ["Vector" ])
260+ wave = nt .nodes .new ("ShaderNodeTexWave" )
261+ wave .wave_type = 'BANDS'
262+ wave .bands_direction = 'Y'
263+ wave .inputs ["Scale" ].default_value = 7.0
264+ wave .inputs ["Distortion" ].default_value = 3.5
265+ wave .inputs ["Detail" ].default_value = 4.0
266+ wave .inputs ["Detail Scale" ].default_value = 2.0
267+ nt .links .new (mapping .outputs ["Vector" ], wave .inputs ["Vector" ])
268+ ramp = nt .nodes .new ("ShaderNodeValToRGB" )
269+ ramp .color_ramp .elements [0 ].color = (0.045 , 0.019 , 0.009 , 1.0 )
270+ ramp .color_ramp .elements [1 ].color = (0.15 , 0.072 , 0.034 , 1.0 )
271+ nt .links .new (wave .outputs ["Fac" ], ramp .inputs ["Fac" ])
272+ nt .links .new (ramp .outputs ["Color" ], b .inputs ["Base Color" ])
273+ mr = nt .nodes .new ("ShaderNodeMapRange" )
274+ mr .inputs ["To Min" ].default_value = 0.42
275+ mr .inputs ["To Max" ].default_value = 0.58
276+ nt .links .new (wave .outputs ["Fac" ], mr .inputs ["Value" ])
277+ nt .links .new (mr .outputs ["Result" ], b .inputs ["Roughness" ])
278+ return mat
279+
280+
281+ def bar_mesh (name , a , b , half_w , half_d , normal ):
282+ """A slim box from point a to point b, half_w wide across the bar in the
283+ plane, half_d thick along *normal*."""
284+ axis = (b - a )
285+ side = axis .cross (normal ).normalized () * half_w
286+ nrm = normal .normalized () * half_d
287+ me = bpy .data .meshes .new (name )
288+ bm = bmesh .new ()
289+ try :
290+ vs = [bm .verts .new (p + s * side + d * nrm )
291+ for p in (a , b ) for s in (- 1 , 1 ) for d in (- 1 , 1 )]
292+ for f in ((0 , 1 , 3 , 2 ), (4 , 6 , 7 , 5 ), (0 , 4 , 5 , 1 ), (2 , 3 , 7 , 6 ), (0 , 2 , 6 , 4 ), (1 , 5 , 7 , 3 )):
293+ bm .faces .new ([vs [i ] for i in f ])
294+ bmesh .ops .recalc_face_normals (bm , faces = bm .faces [:])
295+ bm .to_mesh (me )
296+ finally :
297+ bm .free ()
298+ return me
299+
300+
220301def box_mesh (name , lo , hi ):
221302 me = bpy .data .meshes .new (name )
222303 bm = bmesh .new ()
@@ -233,12 +314,12 @@ def box_mesh(name, lo, hi):
233314def render_still (even_obj , plain_obj , path , engine ):
234315 scene = bpy .context .scene
235316
236- # Sheet faces in a pale enamel , the cut section (Solidify's rim) in
237- # selection orange via material_offset_rim, so the end of each shell
238- # reads as a band whose width is the thickness.
239- sheet = principled ( "SheetEnamel " , (0.50 , 0.56 , 0.62 ), 0.38 , metal = 0.15 , coat = 0.3 )
240- section = principled ("CutSection" , (1.0 , 0.42 , 0.04 ), 0.35 ,
241- emit = ((1.0 , 0.45 , 0.06 ), 0.6 ))
317+ # Sheet faces in a deep teal glaze , the cut section (Solidify's rim) in
318+ # selection- orange enamel via material_offset_rim, so the end of each
319+ # shell reads as a bright band whose width is the thickness.
320+ sheet = glaze ( "TealGlaze " , (0.025 , 0.16 , 0.19 ), ( 0.05 , 0.25 , 0.28 ) )
321+ section = principled ("CutSection" , (1.0 , 0.40 , 0.03 ), 0.3 , coat = 0.5 ,
322+ emit = ((1.0 , 0.42 , 0.05 ), 0.35 ))
242323 for obj in (even_obj , plain_obj ):
243324 obj .data .materials .append (sheet )
244325 obj .data .materials .append (section )
@@ -247,17 +328,24 @@ def render_still(even_obj, plain_obj, path, engine):
247328 mod .material_offset_rim = 1
248329
249330 # Stand each shell on the plinth: lowest evaluated point at the plinth top
250- plinth_top = 0.24
331+ plinth_top = 0.30
251332 plain_obj .location = (- SHELL_GAP , 0.0 , 0.0 )
252333 bpy .context .view_layer .update ()
334+ even_local = evaluated_local_coords (even_obj )
253335 for obj in (even_obj , plain_obj ):
254336 lo = min ((obj .matrix_world @ c ).z for c in evaluated_local_coords (obj ))
337+ if obj is plain_obj :
338+ # the red nominal-thickness line drawn on this shell reaches the
339+ # even shell's deeper copies; keep it above the plinth too
340+ lo = min (lo , min ((obj .matrix_world @ c ).z for c in even_local ))
255341 obj .location .z += plinth_top - lo
256342 bpy .context .view_layer .update ()
257343
258- walnut = principled ("Walnut" , ( 0.13 , 0.055 , 0.025 ), 0.45 , noise = 40.0 , coat = 0.4 )
259- brass = principled ("Brass" , (0.80 , 0.58 , 0.26 ), 0.3 , metal = 1.0 )
344+ walnut = walnut_mat ("Walnut" )
345+ brass = principled ("Brass" , (0.86 , 0.62 , 0.27 ), 0.2 , metal = 1.0 , noise = 60 .0 )
260346 ink = principled ("Engraving" , (0.012 , 0.010 , 0.008 ), 0.7 )
347+ deficit = principled ("Deficit" , (0.85 , 0.04 , 0.05 ), 0.3 , coat = 0.5 ,
348+ emit = ((0.95 , 0.05 , 0.05 ), 0.5 ))
261349 parts = []
262350 pts = profile_points ()
263351
@@ -283,7 +371,7 @@ def render_still(even_obj, plain_obj, path, engine):
283371 try :
284372 h = under .z - plinth_top
285373 res = bmesh .ops .create_cone (bm , cap_ends = True , segments = 16 ,
286- radius1 = 0.022 , radius2 = 0.022 , depth = h )
374+ radius1 = 0.03 , radius2 = 0.03 , depth = h )
287375 for vert in res ["verts" ]:
288376 vert .co += Vector ((under .x , y , plinth_top + h / 2 ))
289377 bm .to_mesh (rod )
@@ -301,28 +389,46 @@ def render_still(even_obj, plain_obj, path, engine):
301389 plinth .data .materials .append (walnut )
302390 scene .collection .objects .link (plinth )
303391 bev = plinth .modifiers .new ("Chamfer" , 'BEVEL' )
304- bev .width = 0.03
305- bev .segments = 2
392+ bev .width = 0.035
393+ bev .segments = 3
306394 parts .append (plinth )
307395
396+ # On the plain shell's cut face, a red line traces where a t-thick shell's
397+ # inner face would run: the even shell's own evaluated front-row copies,
398+ # placed through the plain shell's transform (both shells share one local
399+ # profile). Where the orange band stops short of the red line, that gap
400+ # is the thickness use_even_offset = False loses at each fold.
401+ n = len (even_obj .data .vertices )
402+ target = evaluated_local_coords (even_obj )[n :n + len (pts )]
403+ lift = Vector ((0.0 , - 0.02 , 0.0 ))
404+ line = [plain_obj .matrix_world @ c + lift for c in target ]
405+ for a , b in zip (line , line [1 :]):
406+ ob = bpy .data .objects .new ("NominalLine" , bar_mesh ("NominalLine" , a , b , 0.012 , 0.008 , Y_AXIS ))
407+ ob .data .materials .append (deficit )
408+ scene .collection .objects .link (ob )
409+ parts .append (ob )
410+
308411 # Brass plaques on the plinth's front edge naming each shell's setting
309412 front = - DEPTH / 2 - 0.35
310413 for obj , label in ((plain_obj , "use_even_offset = False" ), (even_obj , "use_even_offset = True" )):
311414 cx = obj .location .x + span / 2
312415 plate = bpy .data .objects .new ("Plaque" , box_mesh (
313- "Plaque" , (cx - 0.95 , front - 0.012 , 0.03 ), (cx + 0.95 , front + 0.01 , plinth_top - 0.03 )))
416+ "Plaque" , (cx - 1.05 , front - 0.016 , 0.035 ), (cx + 1.05 , front + 0.01 , plinth_top - 0.035 )))
314417 plate .data .materials .append (brass )
315418 scene .collection .objects .link (plate )
419+ pbev = plate .modifiers .new ("Chamfer" , 'BEVEL' )
420+ pbev .width = 0.008
421+ pbev .segments = 2
316422 parts .append (plate )
317423 cu = bpy .data .curves .new ("PlaqueText" , 'FONT' )
318424 cu .body = label
319- cu .size = 0.13
425+ cu .size = 0.15
320426 cu .extrude = 0.004
321427 cu .align_x = 'CENTER'
322428 cu .align_y = 'CENTER'
323429 cu .materials .append (ink )
324430 txt = bpy .data .objects .new ("PlaqueText" , cu )
325- txt .location = (cx , front - 0.014 , plinth_top / 2 )
431+ txt .location = (cx , front - 0.018 , plinth_top / 2 )
326432 txt .rotation_euler = (math .radians (90 ), 0.0 , 0.0 )
327433 scene .collection .objects .link (txt )
328434
@@ -369,10 +475,13 @@ def light(name, loc, energy, size, col, aim):
369475 cam_data = bpy .data .cameras .new ("Cam" )
370476 cam_data .lens = 50.0
371477 cam = bpy .data .objects .new ("Cam" , cam_data )
372- cam .location = (centre .x - 1.3 , - 8.5 , 1.75 )
478+ # Low and nearly square-on to the cut ends, so the section bands and the
479+ # gauges across them are the subject; a little height keeps the glazed
480+ # sheet surfaces readable behind them.
481+ cam .location = (centre .x - 0.9 , - 8.6 , 1.35 )
373482 scene .collection .objects .link (cam )
374483 aim = bpy .data .objects .new ("Aim" , None )
375- aim .location = centre + Vector ((0.0 , 0.0 , - 0.2 ))
484+ aim .location = centre + Vector ((0.0 , 0.0 , - 0.08 ))
376485 scene .collection .objects .link (aim )
377486 tr = cam .constraints .new ('TRACK_TO' )
378487 tr .target = aim
0 commit comments