"""Wall props: game-ready assets made from the user's two kits, for the ship's walls. The user, 2026-10-19, on the pack's "Storage Area Network" box: "I want you to replace every box of that object in ship with inside "MachineParts_Kit" "HardSurfaceKit" objects ... but before doing that do the objects gameready asset with baked textures, do the textures metallic substance in grey tones similar to that boxes that gonna be replaced and the object must fit to the scene like dont put pistons to the wall choose attachable objects to the wall or hangable." The kits' own files are not changed: the parts are appended from them into WallProps.blend, and there each one becomes an asset of its own: - one mesh, its origin at the centre of its back (the kits' floor, z = 0: the face that goes on the wall), - UV0 `UVMap`: its own unwrap (Smart UV Project, packed into 0..1), for its own texture set, - three baked textures, 1024 px: `T_WP__BaseColor` (sRGB), `T_WP__ORM` (R occlusion, G roughness, B metallic), `T_WP__Normal` (tangent space, OpenGL). Baked in Cycles from a grey metal shader on the kit's own maps: the kit's baked occlusion, edge and cavity masks (its atlas UV), its bevel normal (the machine parts), the Season 2 pack's scratched-metal set (UV_Tile, metres). Paint: a metallic grey (each asset its own tone, dark charcoal to gunmetal, as the boxes they replace); bolts and pins bare steel; ports and gaps near black; edges worn to steel, crevices grimy. - one material `MI_WP_` on the three textures, - the kit's collision boxes, renamed `UCX_SM_WP__NN`, parented to it, - an FBX in `fbx/` (Z up, -Y forward, metres, with its UCX). Only parts with a flat back that mount on a wall or hang from one are taken: no hydraulic cylinders, no free-standing tanks or engines. blender.exe -b --factory-startup --python build/wp_build.py -- --stage all (stages: assemble, bake, finish, export, render) """ import json import math import os import random import sys import time import bpy import bmesh from mathutils import Matrix, Vector HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(HERE) MODELS = os.path.dirname(ROOT) BLEND = os.path.join(ROOT, "WallProps.blend") TEX = os.path.join(ROOT, "textures") FBX = os.path.join(ROOT, "fbx") RENDERS = os.path.join(ROOT, "renders") PACK = os.path.join(MODELS, "vessel-interior", "textures", "ModSci_Engineer", "Textures", "Base") KITS = { "MPK": dict(blend=os.path.join(MODELS, "machine-parts-kit", "MachineParts_Kit.blend"), masks=os.path.join(MODELS, "machine-parts-kit", "textures", "T_MPK_Masks.png"), bevel=os.path.join(MODELS, "machine-parts-kit", "textures", "T_MPK_BevelNormal.png")), "HSK": dict(blend=os.path.join(MODELS, "hardsurface-kit", "HardSurfaceKit.blend"), masks=os.path.join(MODELS, "hardsurface-kit", "textures", "T_Kit_Masks.png"), bevel=None), } RES = 1024 # (kit, source object, asset name, paint tone (linear base colour of the metallic grey)) ASSETS = [ ("MPK", "SM_MPK_A03_StackBox", "StackBox", (0.105, 0.108, 0.114)), ("MPK", "SM_MPK_B07_ValveBlock", "ValveBlock", (0.150, 0.154, 0.164)), ("MPK", "SM_MPK_D02_CableBox", "CableBox", (0.085, 0.088, 0.093)), ("MPK", "SM_MPK_D03_PerforatedCan", "PerforatedCan", (0.130, 0.134, 0.142)), ("MPK", "SM_MPK_D04_TriPortBlock", "TriPortBlock", (0.095, 0.098, 0.104)), ("MPK", "SM_MPK_D05_FrameBox", "FrameBox", (0.115, 0.118, 0.125)), ("MPK", "SM_MPK_D06_SlimMechanism", "SlimMechanism", (0.165, 0.170, 0.180)), ("MPK", "SM_MPK_D07_HandleBox", "HandleBox", (0.080, 0.082, 0.087)), ("MPK", "SM_MPK_D09_CubeBox", "CubeBox", (0.125, 0.129, 0.136)), ("MPK", "SM_MPK_E03_GrilleBox", "GrilleBox", (0.100, 0.103, 0.109)), ("HSK", "SM_Kit_C01_VentHousing", "VentHousing", (0.140, 0.144, 0.153)), ("HSK", "SM_Kit_C04_PanelEnclosure", "PanelEnclosure", (0.090, 0.093, 0.098)), ("HSK", "SM_Kit_C05_TwinNeckHousing", "TwinNeckHousing", (0.120, 0.124, 0.131)), ("HSK", "SM_Kit_A08_JunctionBox", "JunctionBox", (0.155, 0.160, 0.169)), ("HSK", "SM_Kit_A09_FilterBowlPlate", "FilterBowlPlate", (0.110, 0.113, 0.120)), ("HSK", "SM_Kit_B09_TwinCanisterYoke", "TwinCanisterYoke", (0.135, 0.139, 0.147)), ] STEEL = (0.46, 0.465, 0.47) DARK = (0.022, 0.023, 0.025) def log(*a): print("[wp]", *a, flush=True) def name_of(short): return "SM_WP_" + short # ---- assemble --------------------------------------------------------------------------------------------------------- def assemble(): for o in list(bpy.data.objects): bpy.data.objects.remove(o, do_unlink=True) sc = bpy.context.scene sc.name = "WallProps" coll = bpy.data.collections.get("WP | Assets") or bpy.data.collections.new("WP | Assets") if coll.name not in sc.collection.children: sc.collection.children.link(coll) ucx_coll = bpy.data.collections.get("WP | Collision (UCX)") or bpy.data.collections.new("WP | Collision (UCX)") if ucx_coll.name not in sc.collection.children: sc.collection.children.link(ucx_coll) out = {} for i, (kit, src, short, tone) in enumerate(ASSETS): K = KITS[kit] with bpy.data.libraries.load(K["blend"], link=False) as (data_from, data_to): want = [src] + [n for n in data_from.objects if n.startswith("UCX_" + src + "_")] data_to.objects = want objs = [o for o in data_to.objects if o is not None] part = next(o for o in objs if o.name.startswith(src)) ucx = [o for o in objs if o is not part] src_mw = part.matrix_world.copy() name = name_of(short) part.name = name part.data = part.data.copy() part.data.name = name for c in list(part.users_collection): c.objects.unlink(part) coll.objects.link(part) part.parent = None # its back at the origin: the kits' parts stand on z = 0 with their origin at the footprint's centre grid = Matrix.Translation(((i % 4) * 0.75, (i // 4) * 0.8, 0.0)) part.matrix_world = grid for k, u in enumerate(sorted(ucx, key=lambda o: o.name)): rel = src_mw.inverted() @ u.matrix_world u.parent = None for c in list(u.users_collection): c.objects.unlink(u) ucx_coll.objects.link(u) u.name = "UCX_%s_%02d" % (name, k) u.data = u.data.copy() u.data.name = u.name u.parent = part u.matrix_parent_inverse = Matrix.Identity(4) u.matrix_local = rel u.hide_render = True u.display_type = 'WIRE' part["wp_kit"] = kit part["wp_source"] = src part["wp_tone"] = list(tone) out[name] = dict(kit=kit, source=src, ucx=len(ucx), slots=[m.name for m in part.data.materials if m]) log("appended", name, "from", src, "ucx", len(ucx)) ucx_coll.hide_render = True return out # ---- UVs ---------------------------------------------------------------------------------------------------------------- def unwrap(ob): me = ob.data if "UV_Bake" in me.uv_layers: me.uv_layers.remove(me.uv_layers["UV_Bake"]) uvb = me.uv_layers.new(name="UV_Bake") me.uv_layers.active = uvb vl = bpy.context.view_layer for o in vl.objects: o.select_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 / RES bpy.ops.uv.smart_project(angle_limit=math.radians(62), 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: log("pack skipped", ob.name, ex) bpy.ops.object.mode_set(mode='OBJECT') return "UV_Bake" # (the layer's RNA pointer is stale after the mode switches: do not read it) # ---- shaders ------------------------------------------------------------------------------------------------------------ _IMG = {} def img(path, colorspace): key = (os.path.normcase(path), colorspace) if key not in _IMG: im = bpy.data.images.load(path, check_existing=False) im.colorspace_settings.name = colorspace _IMG[key] = im return _IMG[key] class T: def __init__(self, mat): mat.use_nodes = True self.nt = mat.node_tree self.nt.nodes.clear() self.i = 0 def n(self, kind, **props): node = self.nt.nodes.new(kind) for k, v in props.items(): setattr(node, k, v) node.location = (-1800 + 220 * (self.i // 6), 600 - 220 * (self.i % 6)) self.i += 1 return node def l(self, a, b): self.nt.links.new(a, b) def put(self, sock, v): if isinstance(v, bpy.types.NodeSocket): self.l(v, sock) else: sock.default_value = v def math(self, op, a, b=None, clamp=False): m = self.n('ShaderNodeMath', operation=op, use_clamp=clamp) self.put(m.inputs[0], a) if b is not None: self.put(m.inputs[1], b) return m.outputs[0] def rng(self, v, lo, hi, f0=0.0, f1=1.0): m = self.n('ShaderNodeMapRange') self.put(m.inputs["Value"], v) m.inputs["From Min"].default_value, m.inputs["From Max"].default_value = f0, f1 m.inputs["To Min"].default_value, m.inputs["To Max"].default_value = lo, hi return m.outputs["Result"] def mixc(self, f, a, b): m = self.n('ShaderNodeMix', data_type='RGBA') self.put(m.inputs[0], f) self.put(m.inputs[6], a if isinstance(a, bpy.types.NodeSocket) else tuple(a) + (1.0,)) self.put(m.inputs[7], b if isinstance(b, bpy.types.NodeSocket) else tuple(b) + (1.0,)) return m.outputs[2] def mixf(self, f, a, b): m = self.n('ShaderNodeMix', data_type='FLOAT') self.put(m.inputs[0], f) self.put(m.inputs[2], a) self.put(m.inputs[3], b) return m.outputs[0] def tex(self, im, uvname, scale=1.0): uv = self.n('ShaderNodeUVMap', uv_map=uvname) v = uv.outputs["UV"] if scale != 1.0: mp = self.n('ShaderNodeMapping') mp.inputs["Scale"].default_value = (scale, scale, scale) self.l(v, mp.inputs["Vector"]) v = mp.outputs["Vector"] t = self.n('ShaderNodeTexImage', image=im) self.l(v, t.inputs["Vector"]) return t def source_material(mat, kind, kit, tone, targets): """the grey metal the bake reads: base colour, occlusion/roughness/metallic and the normal, each as an output the bake can be pointed at (mat["wp_out"]: base, orm, normal)""" K = KITS[kit] t = T(mat) masks = t.tex(img(K["masks"], 'Non-Color'), "UVMap") sep = t.n('ShaderNodeSeparateColor') t.l(masks.outputs["Color"], sep.inputs[0]) ao, edge, cav = sep.outputs[0], sep.outputs[1], sep.outputs[2] tile = 1.0 / 0.9 sd = t.tex(img(os.path.join(PACK, "ScratchedMetal", "T_ScratchedMetal_Diff.png"), 'sRGB'), "UV_Tile", tile) sr = t.tex(img(os.path.join(PACK, "ScratchedMetal", "T_ScratchedMetal_Rough.png"), 'Non-Color'), "UV_Tile", tile) sn = t.tex(img(os.path.join(PACK, "ScratchedMetal", "T_ScratchedMetal_Normal.png"), 'Non-Color'), "UV_Tile", tile) dirt = t.tex(img(os.path.join(PACK, "Glass", "T_Glass_DirtRough.png"), 'Non-Color'), "UV_Tile", 1.0 / 1.6) bw = t.n('ShaderNodeRGBToBW') t.l(sd.outputs["Color"], bw.inputs[0]) shade = t.rng(bw.outputs[0], 0.82, 1.14, 0.15, 0.55) if kind == "paint": base_c, metal, r0, r1 = tone, 0.82, 0.38, 0.58 elif kind == "steel": base_c, metal, r0, r1 = STEEL, 1.0, 0.22, 0.40 else: base_c, metal, r0, r1 = DARK, 0.25, 0.60, 0.78 vm = t.n('ShaderNodeVectorMath', operation='SCALE') vm.inputs[0].default_value = base_c t.l(shade, vm.inputs["Scale"]) col = vm.outputs["Vector"] rough = t.rng(sr.outputs["Color"], r0, r1) rough = t.math('ADD', rough, t.math('MULTIPLY', dirt.outputs["Color"], 0.10), clamp=True) metal_s = metal if kind != "dark": # crevices: grime, matte g = t.math('MULTIPLY', t.math('POWER', t.math('SUBTRACT', 1.0, cav), 1.4), 0.55, clamp=True) col = t.mixc(g, col, (0.03, 0.029, 0.027)) rough = t.math('ADD', rough, t.math('MULTIPLY', g, 0.25), clamp=True) if kind == "paint": # exposed edges worn to bright steel, broken up by the scratches brk = t.rng(sr.outputs["Color"], 1.4, 0.4) w = t.math('MULTIPLY', t.math('MULTIPLY', edge, brk), 0.8) w = t.rng(w, 0.0, 1.0, 0.30, 0.75) col = t.mixc(w, col, STEEL) rough = t.mixf(w, rough, 0.30) metal_s = t.mixf(w, metal, 1.0) # the base colour carries the occlusion lightly (the ORM's R carries it in full) col = t.mixc(t.rng(ao, 0.45, 0.0), col, (0.01, 0.01, 0.01)) # outputs: Emission(base), Emission(ORM), Principled for the normal e_base = t.n('ShaderNodeEmission') t.l(col, e_base.inputs["Color"]) comb = t.n('ShaderNodeCombineColor') t.l(ao, comb.inputs[0]) t.put(comb.inputs[1], rough) t.put(comb.inputs[2], metal_s) e_orm = t.n('ShaderNodeEmission') t.l(comb.outputs[0], e_orm.inputs["Color"]) bsdf = t.n('ShaderNodeBsdfPrincipled') nm = t.n('ShaderNodeNormalMap', uv_map="UV_Tile") nm.inputs["Strength"].default_value = 0.35 t.l(sn.outputs["Color"], nm.inputs["Color"]) nrm = nm.outputs["Normal"] if K["bevel"]: bv = t.tex(img(K["bevel"], 'Non-Color'), "UVMap") nm2 = t.n('ShaderNodeNormalMap', uv_map="UVMap") nm2.inputs["Strength"].default_value = 1.0 t.l(bv.outputs["Color"], nm2.inputs["Color"]) # combine the two: the bevel normal first, the scratches bent on it (a sum, renormalised) add = t.n('ShaderNodeVectorMath', operation='ADD') t.l(nm2.outputs["Normal"], add.inputs[0]) t.l(nrm, add.inputs[1]) geo = t.n('ShaderNodeNewGeometry') sub = t.n('ShaderNodeVectorMath', operation='SUBTRACT') t.l(add.outputs[0], sub.inputs[0]) t.l(geo.outputs["Normal"], sub.inputs[1]) nn = t.n('ShaderNodeVectorMath', operation='NORMALIZE') t.l(sub.outputs[0], nn.inputs[0]) nrm = nn.outputs[0] t.l(nrm, bsdf.inputs["Normal"]) out = t.n('ShaderNodeOutputMaterial') mat["wp_nodes"] = [e_base.name, e_orm.name, bsdf.name, out.name] # the bake targets for key, im in targets.items(): tn = t.n('ShaderNodeTexImage', image=im, name="TARGET_" + key) tn.label = "bake target " + key return mat def route(mat, which): nt = mat.node_tree e_base, e_orm, bsdf, out = (nt.nodes[n] for n in mat["wp_nodes"]) for l in list(out.inputs["Surface"].links): nt.links.remove(l) nt.links.new({"base": e_base, "orm": e_orm, "normal": bsdf}[which].outputs[0], out.inputs["Surface"]) tn = nt.nodes["TARGET_" + which] for n in nt.nodes: n.select = False tn.select = True nt.nodes.active = tn 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: log("GPU not set:", ex) sc.cycles.device = 'CPU' return 'CPU' # ---- bake --------------------------------------------------------------------------------------------------------------------- def bake(only=None): os.makedirs(TEX, exist_ok=True) dev = use_gpu() sc = bpy.context.scene sc.cycles.samples = 4 sc.cycles.use_denoising = False sc.render.bake.margin = 8 sc.render.bake.use_clear = True t0 = time.time() res = {} for kit, src, short, tone in ASSETS: name = name_of(short) if only and name not in only: continue ob = bpy.data.objects[name] me = ob.data unwrap(ob) targets = {} for key, cs in (("base", 'sRGB'), ("orm", 'Non-Color'), ("normal", 'Non-Color')): nm = "T_WP_%s_%s" % (short, {"base": "BaseColor", "orm": "ORM", "normal": "Normal"}[key]) im = bpy.data.images.get(nm) or bpy.data.images.new(nm, RES, RES, alpha=False, float_buffer=False) im.colorspace_settings.name = cs im.generated_color = (0.5, 0.5, 1.0, 1.0) if key == "normal" else (0.2, 0.2, 0.2, 1.0) targets[key] = im # one bake material per slot kind slot_mats = [] for i, m in enumerate(list(me.materials)): nmm = (m.name if m else "").lower() kind = "steel" if "steel" in nmm else ("dark" if "dark" in nmm else "paint") bm_name = "BK_%s_%s_%d" % (short, kind, i) bmat = bpy.data.materials.get(bm_name) or bpy.data.materials.new(bm_name) source_material(bmat, kind, kit, tone, targets) me.materials[i] = bmat slot_mats.append(bmat) vl = bpy.context.view_layer for o in vl.objects: o.select_set(False) ob.hide_set(False) ob.hide_render = False ob.select_set(True) vl.objects.active = ob me.uv_layers.active = me.uv_layers["UV_Bake"] for key in ("base", "orm", "normal"): for bmat in slot_mats: route(bmat, key) if key == "normal": bpy.ops.object.bake(type='NORMAL', normal_space='TANGENT', margin=8, use_clear=True, uv_layer="UV_Bake") else: bpy.ops.object.bake(type='EMIT', margin=8, use_clear=True, uv_layer="UV_Bake") im = targets[key] im.filepath_raw = os.path.join(TEX, im.name + ".png") im.file_format = 'PNG' im.save() res[name] = dict(slots=[m.name for m in slot_mats], textures=[im.name for im in targets.values()]) log("baked", name, "%.1f s" % (time.time() - t0)) return dict(device=dev, assets=res, seconds=round(time.time() - t0, 1)) # ---- finish: one material on the baked set, one UV layer ---------------------------------------------------------------- def finish(): out = {} for kit, src, short, tone in ASSETS: name = name_of(short) ob = bpy.data.objects[name] me = ob.data ims = {} for key in ("BaseColor", "ORM", "Normal"): p = os.path.join(TEX, "T_WP_%s_%s.png" % (short, key)) im = bpy.data.images.get("T_WP_%s_%s" % (short, key)) if im is None: im = bpy.data.images.load(p) im.name = "T_WP_%s_%s" % (short, key) im.filepath = bpy.path.relpath(p) if bpy.data.filepath else p im.source = 'FILE' im.colorspace_settings.name = 'sRGB' if key == "BaseColor" else 'Non-Color' im.reload() ims[key] = im mname = "MI_WP_" + short mat = bpy.data.materials.get(mname) or bpy.data.materials.new(mname) t = T(mat) uv = t.n('ShaderNodeUVMap', uv_map="UVMap") b = t.tex(ims["BaseColor"], "UVMap") o = t.tex(ims["ORM"], "UVMap") nimg = t.tex(ims["Normal"], "UVMap") sep = t.n('ShaderNodeSeparateColor') t.l(o.outputs["Color"], sep.inputs[0]) bsdf = t.n('ShaderNodeBsdfPrincipled') aom = t.n('ShaderNodeMix', data_type='RGBA', blend_type='MULTIPLY') aom.inputs[0].default_value = 0.6 t.l(b.outputs["Color"], aom.inputs[6]) t.l(sep.outputs[0], aom.inputs[7]) t.l(aom.outputs[2], bsdf.inputs["Base Color"]) t.l(sep.outputs[1], bsdf.inputs["Roughness"]) t.l(sep.outputs[2], bsdf.inputs["Metallic"]) nm = t.n('ShaderNodeNormalMap', uv_map="UVMap") t.l(nimg.outputs["Color"], nm.inputs["Color"]) t.l(nm.outputs["Normal"], bsdf.inputs["Normal"]) outn = t.n('ShaderNodeOutputMaterial') t.l(bsdf.outputs[0], outn.inputs["Surface"]) mat["ue_material"] = "/Game/Vessel/WallProps/" + mname mat.diffuse_color = tuple(tone) + (1.0,) mat.metallic = 0.8 mat.roughness = 0.45 # one slot for p in me.polygons: p.material_index = 0 me.materials.clear() me.materials.append(mat) # one UV layer: the bake's, named UVMap keep = [l for l in me.uv_layers if l.name == "UV_Bake"] for l in list(me.uv_layers): if l.name != "UV_Bake": me.uv_layers.remove(l) me.uv_layers["UV_Bake"].name = "UVMap" me.uv_layers.active = me.uv_layers["UVMap"] me.uv_layers["UVMap"].active_render = True me["ue_mesh"] = "/Game/Vessel/WallProps/" + name me["wp_asset"] = short vs = [v.co for v in me.vertices] lo = Vector((min(v.x for v in vs), min(v.y for v in vs), min(v.z for v in vs))) hi = Vector((max(v.x for v in vs), max(v.y for v in vs), max(v.z for v in vs))) me["wp_bounds"] = [list(lo), list(hi)] out[name] = dict(material=mname, bounds=[[round(c, 4) for c in lo], [round(c, 4) for c in hi]], tris=sum(len(p.vertices) - 2 for p in me.polygons), ucx=len([c for c in ob.children if c.name.startswith("UCX_")])) for m in [m for m in bpy.data.materials if m.users == 0]: bpy.data.materials.remove(m) for i in [i for i in bpy.data.images if i.users == 0]: bpy.data.images.remove(i) bpy.ops.wm.save_as_mainfile(filepath=BLEND, relative_remap=True) with open(os.path.join(ROOT, "WallProps_layout.json"), "w", encoding="utf-8") as f: json.dump(dict(units="metres, Z up; origin at the centre of the back (the wall side), the front is +Z", textures="T_WP__BaseColor (sRGB), _ORM (R occlusion, G roughness, B metallic), _Normal (OpenGL)", assets={k: dict(v, kit=next(a[0] for a in ASSETS if name_of(a[2]) == k), source=next(a[1] for a in ASSETS if name_of(a[2]) == k)) for k, v in out.items()}), f, indent=1) return out # ---- export --------------------------------------------------------------------------------------------------------------- def export(): os.makedirs(FBX, exist_ok=True) out = {} for kit, src, short, tone in ASSETS: name = name_of(short) ob = bpy.data.objects[name] mw = ob.matrix_world.copy() ob.matrix_world = Matrix.Identity(4) vl = bpy.context.view_layer for o in vl.objects: o.select_set(False) sel = [ob] + [c for c in ob.children if c.name.startswith("UCX_")] hidden = [] for o in sel: if o.hide_get(): hidden.append(o) o.hide_set(False) o.select_set(True) lc = vl.layer_collection.children.get("WP | Collision (UCX)") was = lc.exclude if lc else None if lc: lc.exclude = False p = os.path.join(FBX, name + ".fbx") bpy.ops.export_scene.fbx(filepath=p, use_selection=True, object_types={'MESH'}, axis_forward='-Y', axis_up='Z', apply_unit_scale=True, apply_scale_options='FBX_SCALE_NONE', mesh_smooth_type='FACE', use_tspace=True, bake_anim=False, path_mode='STRIP') if lc: lc.exclude = was for o in hidden: o.hide_set(True) ob.matrix_world = mw out[name] = os.path.getsize(p) return out # ---- render ---------------------------------------------------------------------------------------------------------------- def render(): os.makedirs(RENDERS, exist_ok=True) sc = bpy.context.scene use_gpu() sc.cycles.samples = 96 sc.cycles.use_denoising = True sc.render.resolution_x, sc.render.resolution_y = 1800, 1200 # the assets stand up as on a wall: front toward -Y stand = Matrix.Rotation(math.radians(90), 4, 'X') keep = {} for i, (kit, src, short, tone) in enumerate(ASSETS): ob = bpy.data.objects[name_of(short)] keep[ob.name] = ob.matrix_world.copy() ob.matrix_world = Matrix.Translation(((i % 8) * 0.62 - 2.17, 0.0, 1.35 - (i // 8) * 0.75)) @ Matrix.Rotation(math.radians(90), 4, 'X') wall = bpy.data.meshes.new("WP_wall") bm = bmesh.new() bmesh.ops.create_grid(bm, x_segments=1, y_segments=1, size=4.0) bm.to_mesh(wall) bm.free() w = bpy.data.objects.new("WP_wall", wall) w.matrix_world = Matrix.Translation((0, 0.003, 0.9)) @ Matrix.Rotation(math.radians(90), 4, 'X') wm = bpy.data.materials.new("WP_wall_mat") wm.use_nodes = True wm.node_tree.nodes["Principled BSDF"].inputs["Base Color"].default_value = (0.5, 0.52, 0.55, 1) wm.node_tree.nodes["Principled BSDF"].inputs["Roughness"].default_value = 0.6 wall.materials.append(wm) sc.collection.objects.link(w) cam_d = bpy.data.cameras.new("WP_cam") cam = bpy.data.objects.new("WP_cam", cam_d) sc.collection.objects.link(cam) cam.location = (0.0, -6.2, 1.0) cam.rotation_euler = (math.radians(90), 0, 0) cam_d.lens = 40 sc.camera = cam for nm, loc, e in (("WP_key", (-2.5, -4.0, 3.0), 600), ("WP_fill", (3.0, -3.5, 1.5), 300)): ld = bpy.data.lights.new(nm, 'AREA') ld.energy = e ld.size = 2.5 lo = bpy.data.objects.new(nm, ld) sc.collection.objects.link(lo) lo.location = loc lo.rotation_euler = (Vector((0, 0, 0.9)) - Vector(loc)).to_track_quat('-Z', 'Y').to_euler() world = bpy.data.worlds.new("WP_world") world.use_nodes = True world.node_tree.nodes["Background"].inputs["Strength"].default_value = 0.15 sc.world = world sc.view_settings.view_transform = 'AgX' p = os.path.join(RENDERS, "WallProps_sheet.png") sc.render.filepath = p bpy.ops.render.render(write_still=True) # tidy the render setup away again; the assets back where they were for o in (w, cam) + tuple(bpy.data.objects[n] for n in ("WP_key", "WP_fill")): bpy.data.objects.remove(o, do_unlink=True) for n, mw in keep.items(): bpy.data.objects[n].matrix_world = mw return p if __name__ == "__main__": argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] stage = argv[argv.index("--stage") + 1] if "--stage" in argv else "all" report = {} t0 = time.time() if stage in ("assemble", "all"): report["assemble"] = assemble() if stage in ("bake", "all"): report["bake"] = bake() if stage in ("finish", "all"): report["finish"] = finish() if stage in ("export", "all"): report["export"] = export() bpy.ops.wm.save_mainfile() if stage in ("render", "all"): report["render"] = render() report["seconds"] = round(time.time() - t0, 1) with open(os.path.join(ROOT, "build_report_%s.json" % stage), "w", encoding="utf-8") as f: json.dump(report, f, indent=1, default=str) log("done", stage, report["seconds"], "s")