"""Real surfaces for the docking arm. exec'd inside build_dockarm.py (shares its namespace). The user, 2026-10-13: "do it more realistic with good textures". The first arm had one flat colour per material and long glowing lines. Now: * the hard surfaces take tiling texture sets from the user's own Season 2 packs (already in the Unreal project), read in place from models/vessel-interior/textures: ScratchedMetal for the paint, StainedSteel for bare steel and under the dark coating, Glass_DirtRough as a smudge and dust mask, T_Scratch_Norm for fine scratches; * two maps baked from the model itself (Cycles) in one shared atlas, textures/T_DockArm_Masks.png (R ambient occlusion, G edges, B crevices) and textures/T_DockArm_BevelNormal.png (every hard edge rounded by 12 mm): grime sits in the crevices, the paint is worn through on exposed edges, and edges catch the light instead of being knife-sharp; * glow is an accent: the light lines along booms and links are gone, the remaining lights are small and dimmer. Every part has two UV layers: UVMap box projection in metres (the tiling sets; decal quads keep their atlas UVs there) UV_Atlas a unique unwrap inside the part's square of the shared atlas (the two baked maps) """ PACK = os.path.normpath(os.path.join(OUT, "..", "vessel-interior", "textures", "ModSci_Engineer", "Textures")) SCR = ("Base/ScratchedMetal/T_ScratchedMetal_Diff.png", "Base/ScratchedMetal/T_ScratchedMetal_Rough.png", "Base/ScratchedMetal/T_ScratchedMetal_Normal.png") STN = ("Base/StainedSteel/T_StainSteeled_Diff.png", "Base/StainedSteel/T_StainSteeled_Rough.png", None) DIRT = "Base/Glass/T_Glass_DirtRough.png" SCRATCH_N = "Base/Glass/T_Scratch_Norm.png" ATLAS_SIZE = 4096 ATLAS_RECT = {"SM_DockArm_Turret": (0.00, 0.00, 0.60), "SM_DockArm_UpperLink": (0.00, 0.60, 0.30), "SM_DockArm_Mount": (0.30, 0.60, 0.30), "SM_DockArm_Head": (0.60, 0.00, 0.40), "SM_DockArm_LowerLink": (0.60, 0.40, 0.40), "SM_DockArm_Claw": (0.60, 0.80, 0.20)} # What each material is made of. tile: metres per repeat. shade: the diffuse map darkens/lightens the tint between these. # rough: what the roughness map's black and white become. dirt: smudge mask added to the roughness. grime: how far crevices # darken. wear: how readily exposed edges show wear_col (bare metal) instead of the coating. SURF = { "M_DockArm_Paint": dict(set=SCR, tint=(0.60, 0.62, 0.63), metal=0.0, tile=0.90, shade=(0.86, 1.06), rough=(0.62, 0.36), normal=0.35, dirt=0.05, dirt_tile=2.6, grime=0.42, wear=0.85, wear_col=(0.56, 0.57, 0.58), wear_rough=0.40), "M_DockArm_Graphite": dict(set=(STN[0], SCR[1], SCR[2]), tint=(0.040, 0.043, 0.048), metal=0.0, tile=0.70, shade=(0.75, 1.25), rough=(0.68, 0.44), normal=0.30, dirt=0.06, dirt_tile=1.9, grime=0.25, wear=1.0, wear_col=(0.30, 0.31, 0.32), wear_rough=0.36), "M_DockArm_Steel": dict(set=STN, tint=(0.60, 0.61, 0.63), metal=1.0, tile=0.50, shade=(0.60, 1.30), rough=(0.20, 0.46), normal=0.50, normal_map=SCRATCH_N, dirt=0.05, dirt_tile=0.9, grime=0.35, wear=0.0), "M_DockArm_Rubber": dict(set=(None, SCR[1], None), tint=(0.017, 0.017, 0.018), metal=0.0, tile=0.40, shade=(1.0, 1.0), rough=(0.88, 0.64), normal=0.0, dirt=0.08, dirt_tile=1.2, grime=0.20, wear=0.0, dust=0.05), "M_DockArm_SafetyOrange": dict(set=SCR, tint=(0.80, 0.25, 0.03), metal=0.0, tile=0.60, shade=(0.82, 1.08), rough=(0.64, 0.38), normal=0.40, dirt=0.06, dirt_tile=1.4, grime=0.45, wear=1.25, wear_col=(0.50, 0.51, 0.52), wear_rough=0.36), } # ------------------------------------------------------------------ UV layers def uv_layers(o): me = o.data while len(me.uv_layers): me.uv_layers.remove(me.uv_layers[0]) uvm = me.uv_layers.new(name="UVMap") at = me.uv_layers.new(name="UV_Atlas") me.uv_layers.active = at for o2 in bpy.context.view_layer.objects: o2.select_set(False) o.select_set(True) bpy.context.view_layer.objects.active = o bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') bpy.ops.uv.smart_project(angle_limit=math.radians(66), island_margin=0.006, area_weight=0.0, correct_aspect=True, scale_to_bounds=False) bpy.ops.object.mode_set(mode='OBJECT') me = o.data uvm, at = me.uv_layers["UVMap"], me.uv_layers["UV_Atlas"] x0, y0, s = ATLAS_RECT[o.name] pad = 0.012 for d in at.data: d.uv = (x0 + (pad + d.uv[0] * (1 - 2 * pad)) * s, y0 + (pad + d.uv[1] * (1 - 2 * pad)) * s) for p in me.polygons: # metres, by the face's dominant axis n = p.normal ax = 0 if abs(n.x) >= abs(n.y) and abs(n.x) >= abs(n.z) else (1 if abs(n.y) >= abs(n.z) else 2) for li in p.loop_indices: co = me.vertices[me.loops[li].vertex_index].co uvm.data[li].uv = (co.y, co.z) if ax == 0 else ((co.x, co.z) if ax == 1 else (co.x, co.y)) me.uv_layers.active = uvm uvm.active_render = True # ------------------------------------------------------------------ the two baked maps def use_gpu(): sc = bpy.context.scene sc.render.engine = 'CYCLES' try: prefs = bpy.context.preferences.addons['cycles'].preferences for kind in ('OPTIX', 'CUDA', 'HIP', 'ONEAPI'): try: prefs.compute_device_type = kind except Exception: continue (prefs.get_devices if hasattr(prefs, "get_devices") else prefs.refresh_devices)() if any(d.type == kind for d in prefs.devices): for d in prefs.devices: d.use = d.type == kind sc.cycles.device = 'GPU' return kind except Exception as ex: print("GPU not set:", ex) sc.cycles.device = 'CPU' return 'CPU' def bake_atlas(parts, decal_mat, folder): kind = use_gpu() sc = bpy.context.scene N = ATLAS_SIZE masks = bpy.data.images.new("T_DockArm_Masks", N, N, alpha=False) masks.colorspace_settings.name = 'Non-Color' masks.generated_color = (1.0, 0.0, 1.0, 1.0) # nothing baked there: open, no edge, no crevice bevel = bpy.data.images.new("T_DockArm_BevelNormal", N, N, alpha=False) bevel.colorspace_settings.name = 'Non-Color' bevel.generated_color = (0.5, 0.5, 1.0, 1.0) mm = bpy.data.materials.new("_bake_masks") mm.use_nodes = True nt = mm.node_tree nt.nodes.clear() ao = nt.nodes.new('ShaderNodeAmbientOcclusion'); ao.samples = 24; ao.only_local = True; ao.inputs["Distance"].default_value = 0.55 cv = nt.nodes.new('ShaderNodeAmbientOcclusion'); cv.samples = 24; cv.only_local = True; cv.inputs["Distance"].default_value = 0.06 bv = nt.nodes.new('ShaderNodeBevel'); bv.samples = 8; bv.inputs["Radius"].default_value = 0.020 geo = nt.nodes.new('ShaderNodeNewGeometry') dot = nt.nodes.new('ShaderNodeVectorMath'); dot.operation = 'DOT_PRODUCT' nt.links.new(bv.outputs["Normal"], dot.inputs[0]); nt.links.new(geo.outputs["Normal"], dot.inputs[1]) inv = nt.nodes.new('ShaderNodeMath'); inv.operation = 'SUBTRACT'; inv.inputs[0].default_value = 1.0 nt.links.new(dot.outputs["Value"], inv.inputs[1]) edg = nt.nodes.new('ShaderNodeMath'); edg.operation = 'MULTIPLY'; edg.inputs[1].default_value = 3.5; edg.use_clamp = True nt.links.new(inv.outputs[0], edg.inputs[0]) cc = nt.nodes.new('ShaderNodeCombineColor') nt.links.new(ao.outputs["AO"], cc.inputs[0]); nt.links.new(edg.outputs[0], cc.inputs[1]); nt.links.new(cv.outputs["AO"], cc.inputs[2]) em = nt.nodes.new('ShaderNodeEmission') nt.links.new(cc.outputs[0], em.inputs["Color"]) om = nt.nodes.new('ShaderNodeOutputMaterial') nt.links.new(em.outputs[0], om.inputs["Surface"]) ti = nt.nodes.new('ShaderNodeTexImage'); ti.image = masks nt.nodes.active = ti mn = bpy.data.materials.new("_bake_bevel") mn.use_nodes = True nt2 = mn.node_tree b2 = nt2.nodes["Principled BSDF"] bv2 = nt2.nodes.new('ShaderNodeBevel'); bv2.samples = 8; bv2.inputs["Radius"].default_value = 0.012 nt2.links.new(bv2.outputs["Normal"], b2.inputs["Normal"]) ti2 = nt2.nodes.new('ShaderNodeTexImage'); ti2.image = bevel nt2.nodes.active = ti2 sc.cycles.samples = 16 sc.render.bake.margin = 6 sc.render.bake.use_clear = False for p in parts: me = p.data.copy() bm = bmesh.new(); bm.from_mesh(me) names = [m.name for m in me.materials] if decal_mat.name in names: di = names.index(decal_mat.name) bmesh.ops.delete(bm, geom=[f for f in bm.faces if f.material_index == di], context='FACES') for f in bm.faces: f.material_index = 0 bm.to_mesh(me); bm.free() me.materials.clear() me.materials.append(mm) me.uv_layers.active = me.uv_layers["UV_Atlas"] me.uv_layers["UV_Atlas"].active_render = True tmp = bpy.data.objects.new("_bake_" + p.name, me) sc.collection.objects.link(tmp) for o2 in bpy.context.view_layer.objects: o2.select_set(False) tmp.select_set(True) bpy.context.view_layer.objects.active = tmp bpy.ops.object.bake(type='EMIT', margin=6, use_clear=False) me.materials.clear() me.materials.append(mn) bpy.ops.object.bake(type='NORMAL', normal_space='TANGENT', margin=6, use_clear=False) bpy.data.objects.remove(tmp) bpy.data.meshes.remove(me) for im, nm in ((masks, "T_DockArm_Masks"), (bevel, "T_DockArm_BevelNormal")): im.filepath_raw = os.path.join(folder, nm + ".png") im.file_format = 'PNG' im.save() bpy.data.materials.remove(mm) bpy.data.materials.remove(mn) return masks, bevel, kind # ------------------------------------------------------------------ node helper def tex_image(path, colorspace="sRGB"): path = os.path.normpath(path) for im in bpy.data.images: if im.filepath and os.path.normpath(bpy.path.abspath(im.filepath)) == path: return im im = bpy.data.images.load(path) im.colorspace_settings.name = colorspace return im class Tree: def __init__(self, mat): mat.use_nodes = True self.nt = mat.node_tree self.nt.nodes.clear() self.x = -1600 def node(self, kind, label=None, **props): n = self.nt.nodes.new(kind) if label: n.label = label for k, v in props.items(): setattr(n, k, v) n.location = (self.x, 300 - 40 * (len(self.nt.nodes) % 14) * 4) self.x += 90 return n def put(self, sock, v): if isinstance(v, bpy.types.NodeSocket): self.nt.links.new(v, sock) else: sock.default_value = v def math(self, op, a, b=None, clamp=False, label=None): n = self.node('ShaderNodeMath', label, operation=op, use_clamp=clamp) self.put(n.inputs[0], a) if b is not None: self.put(n.inputs[1], b) return n.outputs[0] def ranged(self, v, lo, hi, label=None, f0=0.0, f1=1.0): n = self.node('ShaderNodeMapRange', label) self.put(n.inputs["Value"], v) n.inputs["From Min"].default_value = f0; n.inputs["From Max"].default_value = f1 n.inputs["To Min"].default_value = lo; n.inputs["To Max"].default_value = hi return n.outputs["Result"] def smooth(self, v, e0, e1, label=None): n = self.node('ShaderNodeMapRange', label, interpolation_type='SMOOTHSTEP') self.put(n.inputs["Value"], v) n.inputs["From Min"].default_value = e0; n.inputs["From Max"].default_value = e1 n.inputs["To Min"].default_value = 0.0; n.inputs["To Max"].default_value = 1.0 return n.outputs["Result"] def mixc(self, fac, a, b, label=None, blend='MIX'): n = self.node('ShaderNodeMix', label, data_type='RGBA', blend_type=blend) self.put(n.inputs[0], fac) self.put(n.inputs[6], a if isinstance(a, bpy.types.NodeSocket) else tuple(a) + (1.0,)) self.put(n.inputs[7], b if isinstance(b, bpy.types.NodeSocket) else tuple(b) + (1.0,)) return n.outputs[2] def mixf(self, fac, a, b, label=None): n = self.node('ShaderNodeMix', label, data_type='FLOAT') self.put(n.inputs[0], fac); self.put(n.inputs[2], a); self.put(n.inputs[3], b) return n.outputs[0] def uv(self, name): return self.node('ShaderNodeUVMap', uv_map=name).outputs["UV"] def mapped(self, src, scale, label=None): n = self.node('ShaderNodeMapping', label) self.nt.links.new(src, n.inputs["Vector"]) n.inputs["Scale"].default_value = (scale, scale, scale) return n.outputs["Vector"] def tex(self, im, vec, label=None): n = self.node('ShaderNodeTexImage', label, image=im) self.nt.links.new(vec, n.inputs["Vector"]) return n def atlas_part(t, masks, bevel): """the two baked maps: (ao, edge, crevice) and the rounded-edge normal""" u1 = t.uv("UV_Atlas") mk = t.tex(masks, u1, "baked: R occlusion, G edges, B crevices") sep = t.node('ShaderNodeSeparateColor') t.nt.links.new(mk.outputs["Color"], sep.inputs[0]) bn = t.tex(bevel, u1, "baked: rounded edges") nb = t.node('ShaderNodeNormalMap', "rounded edges", uv_map="UV_Atlas") t.nt.links.new(bn.outputs["Color"], nb.inputs["Color"]) return sep.outputs[0], sep.outputs[1], sep.outputs[2], nb.outputs["Normal"] def dress(mat, r, masks, bevel): """A coated or bare metal surface: tiling set + smudges, grime in crevices, wear on exposed edges, rounded edges.""" t = Tree(mat) b = t.node('ShaderNodeBsdfPrincipled') out = t.node('ShaderNodeOutputMaterial') t.nt.links.new(b.outputs[0], out.inputs["Surface"]) u0 = t.uv("UVMap") mp = t.mapped(u0, 1.0 / r["tile"], "metres per repeat %.2f" % r["tile"]) diff, rough, normal = r["set"] ao, edge, crev, n_bevel = atlas_part(t, masks, bevel) dirt = t.tex(tex_image(os.path.join(PACK, DIRT), "Non-Color"), t.mapped(u0, 1.0 / r["dirt_tile"]), "smudges and dust").outputs["Color"] # colour col = tuple(r["tint"]) if diff: d = t.tex(tex_image(os.path.join(PACK, diff)), mp, "surface, diffuse") bw = t.node('ShaderNodeRGBToBW'); t.nt.links.new(d.outputs["Color"], bw.inputs[0]) shade = t.ranged(bw.outputs[0], r["shade"][0], r["shade"][1], "shade the tint") col = t.mixc(1.0, col, t.node('ShaderNodeCombineColor').outputs[0], blend='MULTIPLY') if False else col vm = t.node('ShaderNodeVectorMath', "tint x shade", operation='SCALE') vm.inputs[0].default_value = tuple(r["tint"]); t.nt.links.new(shade, vm.inputs["Scale"]) col = vm.outputs["Vector"] grime = t.math('MULTIPLY', t.math('POWER', t.math('SUBTRACT', 1.0, crev), 1.4), r["grime"], clamp=True, label="grime in crevices") col = t.mixc(grime, col, (0.02, 0.019, 0.018), "darken crevices") if r.get("dust"): col = t.mixc(t.math('MULTIPLY', dirt, r["dust"]), col, (0.55, 0.53, 0.50), "dust") # roughness ro = t.tex(tex_image(os.path.join(PACK, rough), "Non-Color"), mp, "surface, roughness").outputs["Color"] rr = t.ranged(ro, r["rough"][0], r["rough"][1], "roughness range") rr = t.math('ADD', rr, t.math('MULTIPLY', dirt, r["dirt"]), label="smudges dull the sheen") rr = t.math('ADD', rr, t.math('MULTIPLY', grime, 0.35), clamp=True, label="grime is matte") metal = r["metal"] # wear on exposed edges: the coating rubbed through to the metal under it if r["wear"] > 0: breakup = t.ranged(ro, 1.25, 0.55, "scratches break the wear up") exposed = t.smooth(ao, 0.72, 0.93, "only where the part is exposed") w = t.math('MULTIPLY', t.math('MULTIPLY', edge, breakup), exposed) w = t.smooth(t.math('MULTIPLY', w, r["wear"]), 0.38, 0.72, "worn edge") col = t.mixc(w, col, tuple(r["wear_col"]), "bare metal on worn edges") rr = t.mixf(w, rr, r["wear_rough"]) metal = t.mixf(w, r["metal"], 1.0) t.put(b.inputs["Base Color"], col) t.put(b.inputs["Roughness"], rr) t.put(b.inputs["Metallic"], metal) # normal: rounded edges, plus the set's fine relief nm_file = r.get("normal_map") or normal if nm_file and r["normal"] > 0: dn = t.tex(tex_image(os.path.join(PACK, nm_file), "Non-Color"), mp, "surface, normal") nd = t.node('ShaderNodeNormalMap', "fine relief", uv_map="UVMap") nd.inputs["Strength"].default_value = r["normal"] t.nt.links.new(dn.outputs["Color"], nd.inputs["Color"]) geo = t.node('ShaderNodeNewGeometry') a1 = t.node('ShaderNodeVectorMath', operation='ADD'); t.nt.links.new(n_bevel, a1.inputs[0]); t.nt.links.new(nd.outputs["Normal"], a1.inputs[1]) s1 = t.node('ShaderNodeVectorMath', operation='SUBTRACT'); t.nt.links.new(a1.outputs[0], s1.inputs[0]); t.nt.links.new(geo.outputs["Normal"], s1.inputs[1]) nz = t.node('ShaderNodeVectorMath', "both normals", operation='NORMALIZE'); t.nt.links.new(s1.outputs[0], nz.inputs[0]) t.nt.links.new(nz.outputs[0], b.inputs["Normal"]) else: t.nt.links.new(n_bevel, b.inputs["Normal"]) mat.diffuse_color = tuple(r["tint"]) + (1.0,) mat.roughness = 0.5 mat.metallic = r["metal"] def dress_glow(mat, color, strength): t = Tree(mat) b = t.node('ShaderNodeBsdfPrincipled') out = t.node('ShaderNodeOutputMaterial') t.nt.links.new(b.outputs[0], out.inputs["Surface"]) b.inputs["Base Color"].default_value = (0.02, 0.03, 0.035, 1) b.inputs["Roughness"].default_value = 0.22 b.inputs["Emission Color"].default_value = tuple(color) + (1,) b.inputs["Emission Strength"].default_value = strength mat.diffuse_color = tuple(color) + (1.0,) def dress_glass(mat): """dark lens glass with fingerprints and dust in its sheen""" t = Tree(mat) b = t.node('ShaderNodeBsdfPrincipled') out = t.node('ShaderNodeOutputMaterial') t.nt.links.new(b.outputs[0], out.inputs["Surface"]) dirt = t.tex(tex_image(os.path.join(PACK, DIRT), "Non-Color"), t.mapped(t.uv("UVMap"), 1.0 / 0.6), "smudges and dust").outputs["Color"] b.inputs["Base Color"].default_value = (0.010, 0.012, 0.015, 1) t.put(b.inputs["Roughness"], t.ranged(dirt, 0.03, 0.26)) if "Coat Weight" in b.inputs: b.inputs["Coat Weight"].default_value = 0.4 mat.diffuse_color = (0.02, 0.025, 0.03, 1.0) def dress_decal(mat, atlas_img): t = Tree(mat) b = t.node('ShaderNodeBsdfPrincipled') out = t.node('ShaderNodeOutputMaterial') t.nt.links.new(b.outputs[0], out.inputs["Surface"]) u0 = t.uv("UVMap") d = t.tex(atlas_img, u0, "markings") t.nt.links.new(d.outputs["Color"], b.inputs["Base Color"]) t.nt.links.new(d.outputs["Alpha"], b.inputs["Alpha"]) b.inputs["Roughness"].default_value = 0.55 try: mat.surface_render_method = 'DITHERED' except Exception: pass mat.diffuse_color = (0.72, 0.73, 0.74, 0.0) # ------------------------------------------------------------------ the hose: clear ducting with a spiral rib HOSE_RIBS = 14 # ribs per texture repeat (one repeat = one circumference of hose length, 0.69 m): pitch 4.9 cm def make_duct_textures(folder, n=1024, seed=11): """Clear flexible ducting with a spiral reinforcing rib: colour + alpha, roughness, normal (OpenGL). Tileable, own work.""" rng = np.random.default_rng(seed) u = (np.arange(n) + 0.5) / n U, V = np.meshgrid(u, u) # U around the hose, V along it (one repeat = one circumference) circ = 2 * math.pi * HOSE_R pitch = circ / HOSE_RIBS ph = (V * HOSE_RIBS - U) % 1.0 # the helix: one pitch further along per turn dist = np.abs(ph - 0.5) * pitch # metres from the rib's centre line rib = np.clip((0.0078 - dist) / 0.0030, 0.0, 1.0) rib = rib * rib * (3 - 2 * rib) wr = np.zeros((n, n)) # slack film between the ribs: fine creases along the hose for _ in range(260): cu, cv = rng.random(2) su, sv = rng.uniform(0.010, 0.030), rng.uniform(0.02, 0.07) du = (U - cu + 0.5) % 1.0 - 0.5 dv = (V - cv + 0.5) % 1.0 - 0.5 wr += rng.uniform(-1.0, 1.0) * np.exp(-(du / su) ** 2 - (dv / sv) ** 2) wr /= np.abs(wr).max() film = 1.0 - rib h = 0.0042 * np.exp(-(dist / 0.0052) ** 2) - 0.0016 * film * np.sin(np.pi * ph) + 0.0006 * film * wr # metres px = circ / n gx = (np.roll(h, -1, 1) - np.roll(h, 1, 1)) / (2 * px) gy = (np.roll(h, -1, 0) - np.roll(h, 1, 0)) / (2 * px) ln = np.sqrt(gx ** 2 + gy ** 2 + 1.0) nrm = np.stack([-gx / ln * 0.5 + 0.5, -gy / ln * 0.5 + 0.5, 1.0 / ln * 0.5 + 0.5, np.ones_like(h)], -1) ribc, filmc = np.array([0.74, 0.76, 0.76]), np.array([0.72, 0.84, 0.88]) col = rib[..., None] * ribc + film[..., None] * filmc alpha = rib * 1.0 + film * (0.10 + 0.05 * np.abs(wr)) rgba = np.concatenate([col, alpha[..., None]], -1) rough = rib * 0.46 + film * (0.07 + 0.05 * np.abs(wr)) rgh = np.stack([rough, rough, rough, np.ones_like(h)], -1) out = [] for name, arr, cs, al in (("T_DockArm_Duct_BaseColor", rgba, 'sRGB', True), ("T_DockArm_Duct_Roughness", rgh, 'Non-Color', False), ("T_DockArm_Duct_Normal", nrm, 'Non-Color', False)): if cs == 'sRGB': # the array is linear; the file is sRGB a = arr.copy() c = a[..., :3] a[..., :3] = np.where(c <= 0.0031308, c * 12.92, 1.055 * np.power(np.clip(c, 0, 1), 1 / 2.4) - 0.055) arr = a img = bpy.data.images.new(name, n, n, alpha=al) img.colorspace_settings.name = 'Non-Color' # write the numbers as they are ... img.pixels.foreach_set(arr.astype(np.float32).ravel()) img.filepath_raw = os.path.join(folder, name + ".png") img.file_format = 'PNG' img.save() img.colorspace_settings.name = cs # ... and read them back in the right space if al: img.alpha_mode = 'CHANNEL_PACKED' out.append(img) return out def dress_duct(mat, col_img, rough_img, nrm_img): """Clear film: see-through head-on, full reflections at a glancing angle (Fresnel), opaque ribs, dust in the film.""" t = Tree(mat) out = t.node('ShaderNodeOutputMaterial') u0 = t.uv("UVMap") c = t.tex(col_img, u0, "rib and film, colour + alpha") ro = t.tex(rough_img, u0, "roughness").outputs["Color"] nt_ = t.tex(nrm_img, u0, "rib relief and creases") nm = t.node('ShaderNodeNormalMap', uv_map="UVMap") nm.inputs["Strength"].default_value = 1.0 t.nt.links.new(nt_.outputs["Color"], nm.inputs["Color"]) dirt = t.tex(tex_image(os.path.join(PACK, DIRT), "Non-Color"), t.mapped(u0, 1.0 / 1.7), "dust and scuffs in the film").outputs["Color"] alpha = t.math('ADD', c.outputs["Alpha"], t.math('MULTIPLY', dirt, 0.10), clamp=True, label="dust makes the film a little milky") rr = t.math('ADD', ro, t.math('MULTIPLY', dirt, 0.22), clamp=True) body = t.node('ShaderNodeBsdfPrincipled', "ribs, dust, the film's own tint") t.nt.links.new(c.outputs["Color"], body.inputs["Base Color"]) t.put(body.inputs["Roughness"], rr) t.nt.links.new(nm.outputs["Normal"], body.inputs["Normal"]) clear = t.node('ShaderNodeBsdfTransparent') clear.inputs["Color"].default_value = (0.96, 0.99, 1.0, 1) m1 = t.node('ShaderNodeMixShader', "see-through where the film is clear") t.put(m1.inputs[0], alpha); t.nt.links.new(clear.outputs[0], m1.inputs[1]); t.nt.links.new(body.outputs[0], m1.inputs[2]) gl = t.node('ShaderNodeBsdfGlossy' if hasattr(bpy.types, 'ShaderNodeBsdfGlossy') else 'ShaderNodeBsdfAnisotropic') t.put(gl.inputs["Roughness"], rr) t.nt.links.new(nm.outputs["Normal"], gl.inputs["Normal"]) fr = t.node('ShaderNodeFresnel') fr.inputs["IOR"].default_value = 1.50 t.nt.links.new(nm.outputs["Normal"], fr.inputs["Normal"]) m2 = t.node('ShaderNodeMixShader', "reflections of a plastic film") t.nt.links.new(fr.outputs[0], m2.inputs[0]); t.nt.links.new(m1.outputs[0], m2.inputs[1]); t.nt.links.new(gl.outputs[0], m2.inputs[2]) t.nt.links.new(m2.outputs[0], out.inputs["Surface"]) try: mat.surface_render_method = 'DITHERED' except Exception: pass mat.use_backface_culling = False mat.diffuse_color = (0.78, 0.86, 0.88, 0.35)