"""Surfaces for the Tiger, the Mako's and the Spearfish's method (models/tender-spearfish/build/surfaces_spearfish.py), run on Tender_Tiger.blend after the build: blender -b Tender_Tiger.blend --python build/tg_surfaces.py (generated by build/tg_make_surfaces.py from this head + the Spearfish's helper functions and run, with this head's screen_uvs, seam_value and dress_glass) * hard surfaces take tiling sets from the user's own Season 2 packs (already in the Unreal project), read in place from models/vessel-interior/textures: ScratchedMetal under the paints, StainedSteel for bare metal, FloorPanel_A on the cockpit deck, Glass_DirtRough as a smudge mask, T_Scratch_Norm for fine scratches. The seats take the CC0 leather scan the captain's quarters brought in (models/vessel-interior/source_textures/leather_white). Nothing is downloaded. * two maps baked from the model in Cycles per atlas: Masks (R occlusion, G edges, B crevices and seams) and BevelNormal (hard edges rounded 12 mm, the shell's plate seam as a groove). Exterior atlas 4096 px, cabin atlas 2048 px. * the colours follow the user's picture: a yellow-orange paint under a clear coat on the shell, brow, collar and the orange fins, a red jaw with black gill grooves, blue-grey big fins, a lit grey recess, a blue-grey capsule over a dark belly, a navy canopy frame and intake, a black belly box; worn as a working machine is (chips at exposed edges, grime in crevices, smudges in the coat; no rust). Two UV layers on every mesh (the order matters for Unreal, which builds its tangents from the first): UVMap (UV0) a unique unwrap in the part's square of its atlas (the baked maps); the screens map onto T_Tiger_Screens. UV_Tile (UV1) metres, box-projected (the tiling sets). """ import bpy, bmesh, math, os, time, json import numpy as np from mathutils import Vector OUT = os.path.dirname(bpy.data.filepath) HERE = os.path.join(OUT, "build") TEX = os.path.join(OUT, "textures") os.makedirs(TEX, exist_ok=True) VI = os.path.normpath(os.path.join(OUT, "..", "vessel-interior")) PACK = os.path.join(VI, "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) FLOOR = ("FloorPanel_A/T_FloorPanel_A_diff.png", "FloorPanel_A/T_FloorPanel_A_Rough.png", "FloorPanel_A/T_FloorPanel_A_normals.png") LEATHER = tuple(os.path.join(VI, "source_textures", "leather_white", f) for f in ("leather_white_diff_2k.jpg", "leather_white_rough_2k.jpg", "leather_white_nor_gl_2k.jpg")) DIRT = "Base/Glass/T_Glass_DirtRough.png" SCRATCH_N = "Base/Glass/T_Scratch_Norm.png" REPORT = {} t0 = time.time() for _im in list(bpy.data.images): if _im.name.startswith("T_Tiger_"): bpy.data.images.remove(_im) for _f in os.listdir(TEX): if _f.startswith("T_Tiger_") and _f.count(".") > 1: os.remove(os.path.join(TEX, _f)) ATLASES = { "Exterior": dict(size=4096, rects={"SM_Tiger_Hull": (0.0, 0.0, 0.86), "SM_Tiger_Glass": (0.86, 0.0, 0.14), "SM_Tiger_ToolArm": (0.86, 0.14, 0.07)}), "Cabin": dict(size=2048, rects={"SM_Tiger_Cabin": (0.0, 0.0, 1.0)}), } BEVEL_R, EDGE_R = 0.012, 0.020 SEAM_W0, SEAM_W1, SEAM_DEPTH = 0.0110, 0.0040, 0.0050 _PAINT = dict(dirt=0.02, dirt_tile=2.4, wear_col=(0.15, 0.155, 0.16), wear_rough=0.45) SURF = { "TIGER | Hull paint": dict(set=SCR, tint=(0.86, 0.36, 0.035), metal=0.0, tile=1.20, shade=(0.96, 1.03), rough=(0.34, 0.26), normal=0.04, grime=0.14, wear=0.12, coat=1.0, coat_rough=0.05, coat_dirt=0.04, **_PAINT), "TIGER | Jaw paint": dict(set=SCR, tint=(0.46, 0.036, 0.012), metal=0.0, tile=1.20, shade=(0.96, 1.03), rough=(0.36, 0.28), normal=0.04, grime=0.10, wear=0.08, coat=0.5, coat_rough=0.06, coat_dirt=0.04, **_PAINT), "TIGER | White plate": dict(set=SCR, tint=(0.80, 0.81, 0.78), metal=0.0, tile=0.60, shade=(0.96, 1.03), rough=(0.36, 0.30), normal=0.04, grime=0.12, wear=0.10, coat=0.6, coat_rough=0.06, coat_dirt=0.04, **_PAINT), "TIGER | Fin grey": dict(set=SCR, tint=(0.20, 0.28, 0.34), metal=0.0, tile=1.00, shade=(0.94, 1.05), rough=(0.40, 0.30), normal=0.06, dirt=0.03, dirt_tile=2.0, grime=0.18, wear=0.22, wear_col=(0.42, 0.43, 0.44), wear_rough=0.36, coat=0.6, coat_rough=0.08, coat_dirt=0.05), "TIGER | Recess grey": dict(set=(STN[0], SCR[1], SCR[2]), tint=(0.22, 0.30, 0.37), metal=0.0, tile=0.90, shade=(0.92, 1.08), rough=(0.46, 0.36), normal=0.10, dirt=0.05, dirt_tile=1.9, grime=0.22, wear=0.20, wear_col=(0.30, 0.31, 0.32), wear_rough=0.36), "TIGER | Capsule grey": dict(set=(STN[0], SCR[1], SCR[2]), tint=(0.085, 0.13, 0.17), metal=0.0, tile=0.90, shade=(0.92, 1.08), rough=(0.44, 0.34), normal=0.10, dirt=0.05, dirt_tile=1.9, grime=0.18, wear=0.25, wear_col=(0.30, 0.31, 0.32), wear_rough=0.36, coat=0.35, coat_rough=0.10, coat_dirt=0.06), "TIGER | Lower hull": dict(set=(STN[0], SCR[1], SCR[2]), tint=(0.030, 0.040, 0.055), metal=0.0, tile=0.90, shade=(0.92, 1.08), rough=(0.50, 0.38), normal=0.12, dirt=0.05, dirt_tile=1.9, grime=0.15, wear=0.30, wear_col=(0.26, 0.27, 0.28), wear_rough=0.36, coat=0.35, coat_rough=0.10, coat_dirt=0.06), "TIGER | Graphite": dict(set=(STN[0], SCR[1], SCR[2]), tint=(0.028, 0.031, 0.036), metal=0.0, tile=0.70, shade=(0.92, 1.08), rough=(0.55, 0.42), normal=0.15, dirt=0.06, dirt_tile=1.9, grime=0.15, wear=0.40, wear_col=(0.30, 0.31, 0.32), wear_rough=0.36), "TIGER | Frame": dict(set=SCR, tint=(0.010, 0.026, 0.105), metal=0.0, tile=0.60, shade=(0.94, 1.05), rough=(0.30, 0.22), normal=0.04, dirt=0.03, dirt_tile=1.5, grime=0.10, wear=0.10, wear_col=(0.30, 0.31, 0.32), wear_rough=0.36, coat=0.8, coat_rough=0.04, coat_dirt=0.05), "TIGER | Intake": dict(set=(STN[0], SCR[1], SCR[2]), tint=(0.018, 0.028, 0.068), metal=0.0, tile=0.60, shade=(0.92, 1.08), rough=(0.40, 0.30), normal=0.10, dirt=0.05, dirt_tile=1.5, grime=0.30, wear=0.0), "TIGER | Trim": dict(set=(STN[0], SCR[1], SCR[2]), tint=(0.008, 0.010, 0.016), metal=0.0, tile=0.60, shade=(0.92, 1.08), rough=(0.38, 0.30), normal=0.10, dirt=0.05, dirt_tile=1.5, grime=0.10, wear=0.20, wear_col=(0.30, 0.31, 0.32), wear_rough=0.36), "TIGER | Black gloss": dict(set=(None, SCR[1], None), tint=(0.006, 0.007, 0.009), metal=0.0, tile=0.30, shade=(1.0, 1.0), rough=(0.14, 0.06), normal=0.0, dirt=0.05, dirt_tile=0.5, grime=0.0, wear=0.0, coat=1.0, coat_rough=0.03, coat_dirt=0.05), "TIGER | Groove": dict(set=(None, SCR[1], None), tint=(0.008, 0.006, 0.006), metal=0.0, tile=0.30, shade=(1.0, 1.0), rough=(0.55, 0.40), normal=0.0, dirt=0.05, dirt_tile=0.5, grime=0.0, wear=0.0), "TIGER | Steel": dict(set=STN, tint=(0.60, 0.61, 0.63), metal=1.0, tile=0.90, shade=(0.93, 1.05), rough=(0.24, 0.32), normal=0.50, normal_map=SCRATCH_N, dirt=0.05, dirt_tile=0.9, grime=0.20, wear=0.0), "TIGER | Cabin panel": dict(set=SCR, tint=(0.012, 0.018, 0.036), metal=0.0, tile=0.90, shade=(0.90, 1.05), rough=(0.62, 0.42), normal=0.30, dirt=0.04, dirt_tile=2.0, grime=0.25, wear=0.15, wear_col=(0.45, 0.46, 0.47), wear_rough=0.40), "TIGER | Cabin floor": dict(set=FLOOR, tint=(0.05, 0.06, 0.08), metal=0.6, tile=1.00, shade=(0.80, 1.10), rough=(0.55, 0.35), normal=0.80, dirt=0.07, dirt_tile=1.5, grime=0.30, wear=0.0), "TIGER | Seat": dict(set=LEATHER, tint=(0.012, 0.020, 0.040), metal=0.0, tile=0.55, shade=(0.80, 1.15), rough=(0.62, 0.40), normal=0.6, dirt=0.04, dirt_tile=1.2, grime=0.25, wear=0.0, abs_paths=True), "TIGER | Seat red": dict(set=LEATHER, tint=(0.22, 0.025, 0.010), metal=0.0, tile=0.55, shade=(0.80, 1.15), rough=(0.62, 0.40), normal=0.6, dirt=0.04, dirt_tile=1.2, grime=0.25, wear=0.0, abs_paths=True), "TIGER | Yellow marking": dict(set=SCR, tint=(0.80, 0.50, 0.025), metal=0.0, tile=0.60, shade=(0.94, 1.04), rough=(0.40, 0.32), normal=0.06, dirt=0.06, dirt_tile=1.4, grime=0.22, wear=0.50, wear_col=(0.50, 0.51, 0.52), wear_rough=0.36, coat=0.6, coat_rough=0.06, coat_dirt=0.1), "TIGER | Bay white": dict(set=SCR, tint=(0.70, 0.71, 0.72), metal=0.0, tile=0.60, shade=(0.94, 1.05), rough=(0.45, 0.35), normal=0.05, dirt=0.04, dirt_tile=1.4, grime=0.20, wear=0.10, wear_col=(0.40, 0.41, 0.42), wear_rough=0.36), } GLOW = {"TIGER | Lamp light": ((1.0, 0.95, 0.88), 3.0), "TIGER | Lens": ((1.0, 0.33, 0.06), 6.0), "TIGER | Display": ((0.30, 0.86, 0.95), 2.2)} GLASSES = {"TIGER | Glass": (0.62, 0.88, 0.96), "TIGER | Glass upper": (0.86, 0.94, 0.98), "TIGER | Bay glass": (0.10, 0.15, 0.20)} SCREEN_INK = (0.35, 0.90, 1.0) # cyan instruments, as the picture's cockpit glows; amber for alerts SCREEN_ALERT = (1.0, 0.55, 0.10) def screen_uvs(o): """every Screen plate: its display face (the biggest face looking at the pilot: the wide display across the bulkhead looks forward, the console's two look up and aft) gets a third of the sheet; the plate's other faces a dark corner""" me = o.data names = [m.name for m in me.materials] si = names.index("TIGER | Screen") uvl = me.uv_layers["UVMap"].data bm = bmesh.new(); bm.from_mesh(me); bm.faces.ensure_lookup_table() todo = {f.index for f in bm.faces if f.material_index == si} islands = [] while todo: seed = todo.pop(); isl = [seed]; stack = [seed] while stack: f = bm.faces[stack.pop()] for e in f.edges: for g in e.link_faces: if g.index in todo: todo.discard(g.index); isl.append(g.index); stack.append(g.index) islands.append(isl) bm.free() def centre(ids): return sum((me.polygons[i].center for i in ids), Vector()) / len(ids) islands.sort(key=lambda isl: (round(centre(isl).y, 1), centre(isl).x)) n_disp = 0 for k, isl in enumerate(islands): c = centre(isl) D = Vector((0, -1, 0)) if c.y > -0.8 else Vector((0, 0.5, 0.866)) if len(isl) > 2: # the wide display: one curved strip, all its faces one picture third = 1 right = Vector((1, 0, 0)) up = Vector((0, 0, 1)) cos_ = [me.vertices[me.loops[li].vertex_index].co for i in isl for li in range(me.polygons[i].loop_start, me.polygons[i].loop_start + me.polygons[i].loop_total)] us = [q.dot(right) for q in cos_]; vs = [q.dot(up) for q in cos_] for i in isl: p = me.polygons[i] for li in range(p.loop_start, p.loop_start + p.loop_total): q = me.vertices[me.loops[li].vertex_index].co uvl[li].uv = ((third + 0.04 + 0.92 * (max(us) - q.dot(right)) / max(1e-6, max(us) - min(us))) / 3.0, 0.04 + 0.92 * (q.dot(up) - min(vs)) / max(1e-6, max(vs) - min(vs))) n_disp += 1 continue amax = max(me.polygons[i].area for i in isl) best = max(isl, key=lambda i: me.polygons[i].normal.dot(D) + (0.0 if me.polygons[i].area > 0.5 * amax else -2.0)) third = (k % 2) * 2 for i in isl: p = me.polygons[i] if i == best: nrm = p.normal right = Vector((1, 0, 0)) if abs(nrm.x) < 0.8 else Vector((0, 1, 0)) right = (right - right.dot(nrm) * nrm).normalized() up = nrm.cross(right).normalized() cos_ = [me.vertices[me.loops[li].vertex_index].co for li in range(p.loop_start, p.loop_start + p.loop_total)] us = [q.dot(right) for q in cos_]; vs = [q.dot(up) for q in cos_] for li, q in zip(range(p.loop_start, p.loop_start + p.loop_total), cos_): uvl[li].uv = ((third + 0.04 + 0.92 * (q.dot(right) - min(us)) / max(1e-6, max(us) - min(us))) / 3.0, 0.04 + 0.92 * (q.dot(up) - min(vs)) / max(1e-6, max(vs) - min(vs))) n_disp += 1 else: for li in range(p.loop_start, p.loop_start + p.loop_total): uvl[li].uv = (0.004, 0.004) return n_disp # ------------------------------------------------------------------ the shell's plate seam, drawn per pixel from its line # The picture draws one dark seam on the shell: along the upper flank under the dorsal fin, then down round the front plate's # back edge to the shell's lower edge (cast from the picture's (334, 242) .. (481, 290) onto the shell); both flanks (|x|). SEAM3D = [(0.303, 0.047, 1.053), (0.322, 0.162, 1.074), (0.337, 0.282, 1.095), (0.37, 0.534, 1.108), (0.329, 0.923, 1.128), (0.409, 1.017, 1.042), (0.486, 1.029, 0.925), (0.54, 1.009, 0.814), (0.583, 0.955, 0.693)] def seam_value(nt, loc_x=0): """0..1: how much of the shell's plate seam this point is (3D distance to the seam's line, mirrored)""" N = nt.nodes L = nt.links def mth(op, a, b=None, clamp=False): n = N.new('ShaderNodeMath'); n.operation = op; n.use_clamp = clamp for i, v in enumerate((a, b)): if v is None: continue if isinstance(v, (int, float)): n.inputs[i].default_value = v else: L.new(v, n.inputs[i]) return n.outputs[0] def vm(op, a, b=None): n = N.new('ShaderNodeVectorMath'); n.operation = op for i, v in enumerate((a, b)): if v is None: continue if isinstance(v, (tuple, list)): n.inputs[i].default_value = v else: L.new(v, n.inputs[i]) return n.outputs["Value"] if op in ('LENGTH', 'DOT_PRODUCT') else n.outputs["Vector"] geo = N.new('ShaderNodeNewGeometry') sp = N.new('ShaderNodeSeparateXYZ'); L.new(geo.outputs["Position"], sp.inputs[0]) cm = N.new('ShaderNodeCombineXYZ') L.new(mth('ABSOLUTE', sp.outputs["X"]), cm.inputs[0]); L.new(sp.outputs["Y"], cm.inputs[1]); L.new(sp.outputs["Z"], cm.inputs[2]) P = cm.outputs[0] dmin = None for a, b in zip(SEAM3D, SEAM3D[1:]): AB = (b[0] - a[0], b[1] - a[1], b[2] - a[2]) L2 = max(1e-9, sum(c * c for c in AB)) pa = vm('SUBTRACT', P, a) t = mth('DIVIDE', vm('DOT_PRODUCT', pa, AB), L2, clamp=True) sc = N.new('ShaderNodeVectorMath'); sc.operation = 'SCALE'; sc.inputs[0].default_value = AB; L.new(t, sc.inputs["Scale"]) d = vm('LENGTH', vm('SUBTRACT', pa, sc.outputs["Vector"])) dmin = d if dmin is None else mth('MINIMUM', dmin, d) mr = N.new('ShaderNodeMapRange'); mr.interpolation_type = 'SMOOTHSTEP' mr.inputs["From Min"].default_value = SEAM_W0; mr.inputs["From Max"].default_value = SEAM_W1 mr.inputs["To Min"].default_value = 0.0; mr.inputs["To Max"].default_value = 1.0 L.new(dmin, mr.inputs["Value"]) return mr.outputs["Result"] def dress_glass(mat, tint): """the canopy as the picture paints it: clear and cyan-tinted where the cockpit shows, milky pale above a line from the nose up to the frame; a thin shell (low IOR, little distortion), fingerprints and dust in the sheen""" 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.7), "smudges and dust").outputs["Color"] b.inputs["Base Color"].default_value = tuple(tint) + (1,) b.inputs["Transmission Weight"].default_value = {"TIGER | Glass": 0.92, "TIGER | Glass upper": 0.62, "TIGER | Bay glass": 0.55}.get(mat.name, 1.0) b.inputs["IOR"].default_value = 1.12 b.inputs["Coat Weight"].default_value = 0.6 b.inputs["Coat IOR"].default_value = 1.6 b.inputs["Coat Roughness"].default_value = 0.02 t.put(b.inputs["Roughness"], t.ranged(dirt, 0.0, 0.03, "smudged", 0.0, 1.0)) mat.diffuse_color = tuple(tint) + (0.35,) try: mat.surface_render_method = 'DITHERED' except Exception: pass