"""Real surfaces for the sliding pressure door, the hinged door's recipe (models/pressure-door/build/pd_surfaces.py, itself from the docking arm): the user's rule is "realistic with good textures" from the first version. * tiling sets from the user's own Season 2 pack (already in the Unreal project), read in place by relative path from models/vessel-interior/textures/ModSci_Engineer/Textures: ScratchedMetal under the paint, StainedSteel for bare steel, Glass_DirtRough as a smudge mask, T_Scratch_Norm for fine scratches on the steel; * one map baked from the model in Cycles into a shared atlas, textures/T_PDS_Masks.png (R occlusion, G edges, B crevices): a little shade in crevices, a few chips on exposed edges. The ship is clean: low wear, no rust; * generated here: textures/T_PDS_Screens.png (the monitor's display, the leaf keypad, the sign's dark ground; no lettering: the sign's words are a mesh). UV layers (order matters for Unreal, which builds tangents from the first): UVMap (UV0) unique unwrap inside the mesh's square of the shared atlas (the baked masks); screens map into the screen atlas UV_Tile (UV1) metres, box-projected, for the tiling sets The paint is the ship's clean grey-white; in Unreal it is one parameter of the material. run() does everything. """ import bpy, bmesh, math, os, time import numpy as np from mathutils import Vector FOLDER = r"D:\HADAL - The Descent\models\pressure-door-sliding" TEX = os.path.join(FOLDER, "textures") PACK = os.path.normpath(os.path.join(FOLDER, "..", "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_PDSliding_Frame": (0.0, 0.0, 0.5), "SM_PDSliding_Wall": (0.5, 0.0, 0.5), "SM_PDSliding_LeafL": (0.0, 0.5, 0.25), "SM_PDSliding_LeafR": (0.25, 0.5, 0.25), "SM_PDSliding_Monitor": (0.5, 0.5, 0.25), "SM_PDSliding_Camera": (0.75, 0.5, 0.125), "SM_PDSliding_Handle": (0.875, 0.5, 0.125)} EDGE_R = 0.010 SCREEN_REGION = {"monitor": (0.0, 0.5, 0.5, 0.5), "keypad": (0.5, 0.75, 0.25, 0.25), "sign": (0.5, 0.5, 0.5, 0.25)} # x0 y0 w h SURF = { "M_PDS_Paint": dict(set=SCR, tint=(0.43, 0.44, 0.455), metal=0.0, tile=0.90, shade=(0.90, 1.06), rough=(0.48, 0.30), normal=0.30, dirt=0.04, dirt_tile=2.6, grime=0.18, wear=0.25, wear_col=(0.52, 0.53, 0.54), wear_rough=0.38), "M_PDS_Orange": dict(set=SCR, tint=(0.72, 0.25, 0.04), metal=0.0, tile=0.60, shade=(0.90, 1.06), rough=(0.50, 0.32), normal=0.25, dirt=0.04, dirt_tile=1.4, grime=0.15, wear=0.2, wear_col=(0.50, 0.51, 0.52), wear_rough=0.36), "M_PDS_Steel": dict(set=STN, tint=(0.80, 0.81, 0.83), metal=1.0, tile=0.50, shade=(0.85, 1.10), rough=(0.10, 0.24), normal=0.45, normal_map=SCRATCH_N, dirt=0.04, dirt_tile=0.9, grime=0.18, wear=0.0), "M_PDS_Dark": dict(set=(STN[0], SCR[1], SCR[2]), tint=(0.030, 0.033, 0.037), metal=0.0, tile=0.70, shade=(0.87, 1.13), rough=(0.56, 0.36), normal=0.30, dirt=0.06, dirt_tile=1.9, grime=0.12, wear=0.0), "M_PDS_Rubber": dict(set=(None, SCR[1], None), tint=(0.018, 0.019, 0.021), metal=0.0, tile=0.40, shade=(1.0, 1.0), rough=(0.84, 0.62), normal=0.0, dirt=0.08, dirt_tile=1.2, grime=0.10, wear=0.0, dust=0.03), "M_PDS_CableRed": dict(set=(None, SCR[1], None), tint=(0.30, 0.025, 0.018), metal=0.0, tile=0.40, shade=(1.0, 1.0), rough=(0.80, 0.58), normal=0.0, dirt=0.08, dirt_tile=1.2, grime=0.10, wear=0.0, dust=0.03), } GLOW = {"M_PDS_Light": ((1.0, 0.97, 0.92), 14.0, (0.85, 0.85, 0.85)), "M_PDS_LampRed": ((1.0, 0.04, 0.02), 2.5, (0.03, 0.03, 0.03)), "M_PDS_LampGreen": ((0.05, 1.0, 0.18), 2.5, (0.03, 0.03, 0.03)), "M_PDS_SignText": ((0.25, 0.95, 1.0), 3.0, (0.05, 0.08, 0.09))} # emission is kept low on the coloured lamps: under AgX a strong red glow washes out to pink # ------------------------------------------------------------------ UV layers def uv_tile_layer(me): """metres, by the face's dominant axis""" uv = me.uv_layers.get("UV_Tile") or me.uv_layers.new(name="UV_Tile") co = np.zeros(len(me.vertices) * 3, np.float32) me.vertices.foreach_get("co", co) co = co.reshape(-1, 3) out = np.zeros((len(me.loops), 2), np.float32) for p in me.polygons: 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 range(p.loop_start, p.loop_start + p.loop_total): c = co[me.loops[li].vertex_index] out[li] = (c[1], c[2]) if ax == 0 else ((c[0], c[2]) if ax == 1 else (c[0], c[1])) uv.data.foreach_set("uv", out.ravel()) def uv_atlas_layer(ob, rect): me = ob.data if "UVMap" not in me.uv_layers: me.uv_layers.new(name="UVMap") names = [l.name for l in me.uv_layers] if names[0] != "UVMap": keep = {} for l in me.uv_layers: a = np.zeros(len(me.loops) * 2, np.float32) l.data.foreach_get("uv", a) keep[l.name] = a while len(me.uv_layers): me.uv_layers.remove(me.uv_layers[0]) for nm in ("UVMap", "UV_Tile"): me.uv_layers.new(name=nm).data.foreach_set("uv", keep.get(nm, np.zeros(len(me.loops) * 2, np.float32))) me.uv_layers.active = me.uv_layers["UVMap"] me.uv_layers["UVMap"].active_render = True x0, y0, s = rect vl = bpy.context.view_layer for o in vl.objects: o.select_set(False) hidden = ob.hide_get() ob.hide_set(False) ob.select_set(True) vl.objects.active = ob bpy.ops.object.mode_set(mode='EDIT') bpy.ops.mesh.select_all(action='SELECT') margin = 6.0 / (s * ATLAS_SIZE) bpy.ops.uv.smart_project(angle_limit=math.radians(60), island_margin=margin, area_weight=0.0, correct_aspect=True, scale_to_bounds=False) bpy.ops.uv.select_all(action='SELECT') try: bpy.ops.uv.pack_islands(rotate=True, scale=True, margin_method='FRACTION', margin=margin, shape_method='CONCAVE') except Exception as ex: print("pack skipped", ob.name, ex) bpy.ops.object.mode_set(mode='OBJECT') a = np.zeros(len(me.loops) * 2, np.float32) me.uv_layers["UVMap"].data.foreach_get("uv", a) a = a.reshape(-1, 2) pad = 4.0 / (s * ATLAS_SIZE) a = np.array([x0, y0], np.float32) + (pad + np.clip(a, 0, 1) * (1 - 2 * pad)) * s me.uv_layers["UVMap"].data.foreach_set("uv", a.ravel()) ob.hide_set(hidden) def screen_uvs(ob): """faces with M_PDS_Screen map into their region of the screen atlas (UVMap), by the screen record on the root""" me = ob.data names = [m.name for m in me.materials] if "M_PDS_Screen" not in names: return si = names.index("M_PDS_Screen") screens = bpy.data.objects["PressureDoorSliding_Root"]["screens"] M = ob.matrix_world uv = me.uv_layers["UVMap"] for p in me.polygons: if p.material_index != si: continue c = M @ p.center best = min(screens.keys(), key=lambda k: (Vector(screens[k]["c"]) - c).length) sc = screens[best] C, U, Vv, w, h = Vector(sc["c"]), Vector(sc["u"]), Vector(sc["v"]), sc["w"], sc["h"] rx, ry, rw, rh = SCREEN_REGION[sc["region"]] for li in range(p.loop_start, p.loop_start + p.loop_total): q = (M @ me.vertices[me.loops[li].vertex_index].co) - C u = min(max(q.dot(U) / w + 0.5, 0.0), 1.0) v = min(max(q.dot(Vv) / h + 0.5, 0.0), 1.0) uv.data[li].uv = (rx + u * rw, ry + v * rh) # ------------------------------------------------------------------ generated textures def save_image(name, arr, colorspace='sRGB'): h, w = arr.shape[:2] im = bpy.data.images.get(name) if im is None or tuple(im.size) != (w, h): if im: bpy.data.images.remove(im) im = bpy.data.images.new(name, w, h, alpha=False) im.colorspace_settings.name = colorspace # before the pixels: changing it afterwards regenerates the image (black) rgba = np.ones((h, w, 4), np.float32) rgba[..., :3] = arr[..., :3] im.pixels.foreach_set(rgba.ravel()) im.filepath_raw = os.path.join(TEX, name + ".png") im.file_format = 'PNG' im.save() _relative(im) return im def _relative(im): """keep the texture path relative to the .blend (the pack's are): the folder moves as one""" try: im.filepath = bpy.path.relpath(im.filepath_raw) except Exception as ex: print("relpath skipped", im.name, ex) def make_screens(n=1024): """one atlas: the monitor's display (lower-left half... regions as SCREEN_REGION, rows count from the bottom), the leaf keypad, the sign's dark ground. Emissive, sRGB values, no lettering.""" img = np.zeros((n, n, 3), np.float32) yy, xx = np.mgrid[0:n, 0:n] bg, dim, cyan, white = (0.02, 0.07, 0.085), (0.05, 0.22, 0.26), (0.20, 0.80, 0.90), (0.75, 0.9, 0.95) def rect(x0, y0, x1, y1, col): img[(xx >= x0) & (xx < x1) & (yy >= y0) & (yy < y1)] = col # monitor: region x 0..512, y 512..1024 rect(0, 512, 512, 1024, bg) rect(0, 512, 512, 1024, bg) rect(6, 518, 506, 1018, dim); rect(10, 522, 502, 1014, bg) # frame rect(10, 985, 502, 988, dim) # header rule rr = np.hypot(xx - 140, yy - 840) img[(rr > 88) & (rr < 92)] = dim img[(rr > 60) & (rr < 62)] = dim img[(rr > 30) & (rr < 32)] = dim img[(rr < 92) & (np.abs(xx - 140) < 1)] = dim img[(rr < 92) & (np.abs(yy - 840) < 1)] = dim ang = np.arctan2(yy - 840, xx - 140) img[(rr < 90) & (ang > 0.4) & (ang < 0.9)] = (0.08, 0.35, 0.40) # the sweep for k, (bx, by) in enumerate(((190, 900), (240, 790), (120, 760))): img[np.hypot(xx - bx, yy - by) < 5] = cyan for k in range(7): # a list of bars at the right y0 = 940 - k * 48 rect(270, y0, 490, y0 + 26, dim) rect(272, y0 + 2, 272 + int(200 * (0.25 + 0.7 * ((k * 5) % 7) / 6.0)), y0 + 24, (0.10, 0.50, 0.58)) for k in range(5): # a wave trace at the bottom pass xs = np.arange(10, 502) ys = (600 + 40 * np.sin(xs / 18.0) + 15 * np.sin(xs / 5.0)).astype(int) for dx in range(len(xs)): img[ys[dx] - 1:ys[dx] + 2, xs[dx]] = cyan rect(10, 540, 502, 542, dim) for k in range(12): # small readouts rect(20 + k * 40, 548, 48 + k * 40, 560, dim if k % 3 else (0.9, 0.45, 0.1)) # keypad: region x 512..768, y 768..1024 : a status bar and 3 x 4 keys rect(512, 768, 768, 1024, bg) rect(524, 990, 756, 1014, (0.10, 0.45, 0.52)) for r_ in range(4): for c_ in range(3): x0 = 528 + c_ * 78 y0 = 790 + r_ * 48 rect(x0, y0, x0 + 66, y0 + 38, dim) rect(x0 + 3, y0 + 3, x0 + 63, y0 + 35, (0.12, 0.55, 0.62)) # sign: region x 512..1024, y 512..768 : near black with a faint frame rect(512, 512, 1024, 768, (0.012, 0.014, 0.016)) rect(522, 522, 1014, 758, (0.06, 0.16, 0.19)); rect(526, 526, 1010, 754, (0.012, 0.014, 0.016)) return save_image("T_PDS_Screens", img) # ------------------------------------------------------------------ bake def use_gpu(): sc = bpy.context.scene sc.render.engine = 'CYCLES' try: prefs = bpy.context.preferences.addons['cycles'].preferences prefs.compute_device_type = 'HIP' prefs.get_devices() for d in prefs.devices: d.use = d.type == 'HIP' sc.cycles.device = 'GPU' return 'HIP' except Exception as ex: print("GPU not set:", ex) sc.cycles.device = 'CPU' return 'CPU' SKIP_BAKE = {"M_PDS_Screen", "M_PDS_Light", "M_PDS_Lock", "M_PDS_LampRed", "M_PDS_LampGreen", "M_PDS_SignText", "M_PDS_Glass"} def bake_masks(parts, reset=True): """parts: list of (name_in_atlas, mesh). One emit bake of each mesh alone (occlusion counts only the mesh itself).""" t0 = time.time() kind = use_gpu() sc = bpy.context.scene N = ATLAS_SIZE masks = bpy.data.images.get("T_PDS_Masks") if masks is None or tuple(masks.size) != (N, N): if masks: bpy.data.images.remove(masks) masks = bpy.data.images.new("T_PDS_Masks", N, N, alpha=False) masks.colorspace_settings.name = 'Non-Color' if reset: masks.pixels.foreach_set(np.tile(np.array((1.0, 0.0, 1.0, 1.0), np.float32), N * N)) mm = bpy.data.materials.get("_bake_masks") or bpy.data.materials.new("_bake_masks") mm.use_nodes = True nt = mm.node_tree nt.nodes.clear() ao = nt.nodes.new('ShaderNodeAmbientOcclusion'); ao.samples = 16; ao.only_local = True; ao.inputs["Distance"].default_value = 0.45 cv = nt.nodes.new('ShaderNodeAmbientOcclusion'); cv.samples = 16; cv.only_local = True; cv.inputs["Distance"].default_value = 0.05 bv = nt.nodes.new('ShaderNodeBevel'); bv.samples = 8; bv.inputs["Radius"].default_value = EDGE_R 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 sc.cycles.samples = 16 sc.cycles.use_denoising = False sc.render.bake.margin = 6 sc.render.bake.use_clear = False for name, src in parts: me = src.copy() bm = bmesh.new() bm.from_mesh(me) names = [m.name for m in me.materials] kill = [f for f in bm.faces if names[f.material_index] in SKIP_BAKE] bmesh.ops.delete(bm, geom=kill, 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["UVMap"] me.uv_layers["UVMap"].active_render = True tmp = bpy.data.objects.new("_bake_" + 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 try: bpy.ops.object.bake(type='EMIT', margin=6, use_clear=False) finally: bpy.data.objects.remove(tmp) bpy.data.meshes.remove(me) masks.filepath_raw = os.path.join(TEX, "T_PDS_Masks.png") masks.file_format = 'PNG' masks.save() _relative(masks) bpy.data.materials.remove(mm) return masks, dict(device=kind, seconds=round(time.time() - t0, 1), size=N) # ------------------------------------------------------------------ materials (node recipe from the docking arm) 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 try: im.filepath = bpy.path.relpath(path) except Exception: pass return im _RANGE = {} def tex_range(im, lo=3, hi=97): if im.name not in _RANGE: n, ch = im.size[0] * im.size[1], im.channels px = np.empty(n * ch, np.float32) im.pixels.foreach_get(px) px = px.reshape(n, ch) if ch >= 3: c = px[:, :3] if im.colorspace_settings.name == 'sRGB': c = np.where(c <= 0.04045, c / 12.92, ((c + 0.055) / 1.055) ** 2.4) v = c @ np.array([0.2126, 0.7152, 0.0722], np.float32) else: v = px[:, 0] _RANGE[im.name] = (round(float(np.percentile(v, lo)), 4), round(float(np.percentile(v, hi)), 4)) return _RANGE[im.name] class Tree: def __init__(self, mat): mat.use_nodes = True self.nt = mat.node_tree self.nt.nodes.clear() 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) i = len(self.nt.nodes) - 1 n.location = (-2000 + 240 * (i // 4), 500 - 320 * (i % 4)) return n def link(self, a, b): self.nt.links.new(a, b) 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, smooth=False): n = self.node('ShaderNodeMapRange', label) if smooth: n.interpolation_type = 'SMOOTHSTEP' 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 mixc(self, fac, a, b, label=None): n = self.node('ShaderNodeMix', label, data_type='RGBA') 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 dress(mat, r, masks=None): """a coated or bare metal surface: a tiling set + smudges; with the baked masks also shade in crevices and a few chips on exposed edges""" t = Tree(mat) b = t.node('ShaderNodeBsdfPrincipled') out = t.node('ShaderNodeOutputMaterial') t.link(b.outputs[0], out.inputs["Surface"]) u0 = t.uv("UV_Tile") mp = t.mapped(u0, 1.0 / r["tile"], "one repeat = %.2f m" % r["tile"]) diff, rough, normal = r["set"] 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"] ao = edge = crev = None if masks is not None: sep = t.node('ShaderNodeSeparateColor') t.link(t.tex(masks, t.uv("UVMap"), "baked: R occlusion, G edges, B crevices").outputs["Color"], sep.inputs[0]) ao, edge, crev = sep.outputs[0], sep.outputs[1], sep.outputs[2] tint = tuple(r["tint"]) col = tint if diff: im = tex_image(os.path.join(PACK, diff)) lo, hi = tex_range(im) bw = t.node('ShaderNodeRGBToBW') t.link(t.tex(im, mp, "surface, diffuse").outputs["Color"], bw.inputs[0]) sh = t.ranged(bw.outputs[0], r["shade"][0], r["shade"][1], "shade the tint", lo, hi) vm = t.node('ShaderNodeVectorMath', "tint x shade", operation='SCALE') vm.inputs[0].default_value = tint t.link(sh, vm.inputs["Scale"]) col = vm.outputs["Vector"] grime = None if crev is not None and r["grime"] > 0: grime = t.math('MULTIPLY', t.math('POWER', t.math('SUBTRACT', 1.0, crev), 1.4), r["grime"], clamp=True, label="shade in crevices") col = t.mixc(grime, col, (0.02, 0.019, 0.018), "darken crevices") if ao is not None: col = t.mixc(t.ranged(ao, 0.35, 0.0, "occlusion", 0.0, 1.0), col, (0.01, 0.01, 0.01), "occlusion") if r.get("dust"): col = t.mixc(t.math('MULTIPLY', dirt, r["dust"]), col, (0.55, 0.53, 0.50), "dust") rim = tex_image(os.path.join(PACK, rough), "Non-Color") rlo, rhi = tex_range(rim) ro = t.tex(rim, mp, "surface, roughness").outputs["Color"] rr = t.ranged(ro, r["rough"][0], r["rough"][1], "roughness range", rlo, rhi) rr = t.math('ADD', rr, t.math('MULTIPLY', dirt, r["dirt"]), clamp=True, label="smudges dull the sheen") if grime is not None: rr = t.math('ADD', rr, t.math('MULTIPLY', grime, 0.35), clamp=True, label="crevices are matte") metal = r["metal"] if edge is not None and r["wear"] > 0: breakup = t.ranged(ro, 1.25, 0.55, "scratches break the wear up", rlo, rhi) exposed = t.ranged(ao, 0.0, 1.0, "only where the part is exposed", 0.72, 0.93, smooth=True) w = t.math('MULTIPLY', t.math('MULTIPLY', t.math('MULTIPLY', edge, breakup), exposed), r["wear"]) w = t.ranged(w, 0.0, 1.0, "chipped edge", 0.38, 0.72, smooth=True) col = t.mixc(w, col, tuple(r["wear_col"]), "metal under the coating") 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) nm_file = r.get("normal_map") or normal if nm_file and r["normal"] > 0: nd = t.node('ShaderNodeNormalMap', "fine relief", uv_map="UV_Tile") nd.inputs["Strength"].default_value = r["normal"] t.link(t.tex(tex_image(os.path.join(PACK, nm_file), "Non-Color"), mp, "surface, normal").outputs["Color"], nd.inputs["Color"]) t.link(nd.outputs["Normal"], b.inputs["Normal"]) mat.diffuse_color = tuple(r["tint"]) + (1.0,) mat.metallic = r["metal"] def dress_glow(mat, color, strength, base): t = Tree(mat) b = t.node('ShaderNodeBsdfPrincipled') out = t.node('ShaderNodeOutputMaterial') t.link(b.outputs[0], out.inputs["Surface"]) b.inputs["Base Color"].default_value = tuple(base) + (1,) b.inputs["Roughness"].default_value = 0.25 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_lock(mat): """the ring light: a ramp red -> amber -> green, its Fac driven by the rig (node name LockRamp)""" t = Tree(mat) b = t.node('ShaderNodeBsdfPrincipled') out = t.node('ShaderNodeOutputMaterial') t.link(b.outputs[0], out.inputs["Surface"]) ramp = t.node('ShaderNodeValToRGB', "LockRamp") ramp.name = "LockRamp" cr = ramp.color_ramp cr.elements[0].position, cr.elements[0].color = 0.0, (1.0, 0.03, 0.02, 1) cr.elements[1].position, cr.elements[1].color = 1.0, (0.08, 1.0, 0.18, 1) mid = cr.elements.new(0.5) mid.color = (1.0, 0.45, 0.04, 1) ramp.inputs[0].default_value = 0.0 t.link(ramp.outputs["Color"], b.inputs["Emission Color"]) b.inputs["Emission Strength"].default_value = 2.5 b.inputs["Base Color"].default_value = (0.03, 0.03, 0.03, 1) b.inputs["Roughness"].default_value = 0.3 mat.diffuse_color = (1.0, 0.1, 0.05, 1.0) def dress_screen(mat, img): t = Tree(mat) b = t.node('ShaderNodeBsdfPrincipled') out = t.node('ShaderNodeOutputMaterial') t.link(b.outputs[0], out.inputs["Surface"]) tx = t.tex(img, t.uv("UVMap"), "screens") b.inputs["Base Color"].default_value = (0.01, 0.012, 0.014, 1) b.inputs["Roughness"].default_value = 0.08 t.link(tx.outputs["Color"], b.inputs["Emission Color"]) b.inputs["Emission Strength"].default_value = 3.0 mat.diffuse_color = (0.1, 0.5, 0.6, 1) def dress_glass(mat): t = Tree(mat) b = t.node('ShaderNodeBsdfPrincipled') out = t.node('ShaderNodeOutputMaterial') t.link(b.outputs[0], out.inputs["Surface"]) dirt = t.tex(tex_image(os.path.join(PACK, DIRT), "Non-Color"), t.mapped(t.uv("UV_Tile"), 1.0 / 0.5), "smudges").outputs["Color"] b.inputs["Base Color"].default_value = (0.82, 0.90, 0.93, 1) b.inputs["Transmission Weight"].default_value = 1.0 b.inputs["IOR"].default_value = 1.47 t.put(b.inputs["Roughness"], t.ranged(dirt, 0.03, 0.12)) mat.diffuse_color = (0.6, 0.7, 0.75, 0.4) # ------------------------------------------------------------------ all OBJECTS = list(ATLAS_RECT.keys()) def run(bake=True, uvs=True): os.makedirs(TEX, exist_ok=True) t0 = time.time() meshes = [(n, bpy.data.objects[n]) for n in OBJECTS] if uvs: for n, ob in meshes: uv_tile_layer(ob.data) uv_atlas_layer(ob, ATLAS_RECT[n]) screen_uvs(ob) info = {} if bake: masks, info = bake_masks([(n, ob.data) for n, ob in meshes]) else: masks = bpy.data.images.get("T_PDS_Masks") scr = make_screens() for name, r in SURF.items(): if bpy.data.materials.get(name): # a material nothing uses is not saved (the red cables went) dress(bpy.data.materials[name], r, masks) for name, (c, s, base) in GLOW.items(): if bpy.data.materials.get(name): dress_glow(bpy.data.materials[name], c, s, base) dress_lock(bpy.data.materials["M_PDS_Lock"]) dress_screen(bpy.data.materials["M_PDS_Screen"], scr) dress_glass(bpy.data.materials["M_PDS_Glass"]) info["total_s"] = round(time.time() - t0, 1) return info