""" Reusable units cut out of the pack's own demo maps (Season 2), and the code that places them. The pack's author has composed corridors, junction cubes and a hall in the demo maps. A unit is a list of the items found in one region of such a map (kit meshes with their material overrides, lights, decals), stored in a local frame, so it can be set down anywhere and keeps the author's detailing. demo = Demo("ModSci_Engineer__Example_Dynamic_White") unit = demo.unit(frame, pred) # items for which pred(item, local_position) is true place(kit, unit.items, collection, target_matrix, zone="FORE", lights=light_collection, decals=...) Frames and matrices are Blender ones (metres, right-handed, +Z up); the demo's Unreal matrices are converted on load with bl_kit.ue_rows_to_matrix. """ import glob import json import math import os import bpy from mathutils import Matrix, Vector import bl_kit from bl_kit import KIT_DIR, ue_rows_to_matrix LIGHT_FIX = Matrix.Rotation(math.radians(-90.0), 4, "Y") # Blender lights look down -Z, Unreal's along +X UNITLESS_TO_WATT = 0.02 BAY = 3.6 # Engineer corridor bay DOOR_GAP = 0.35 # a structure ends this far from the door plane it faces # Mesh names (without pack) by role ------------------------------------------------------------------------------ DOORSET = {"SM_Doorway_Small_A", "SM_Doorway_Small_A3", "SM_Door_Small_A", "SM_Door_Small_A_Glass", "SM_Door_Small_UI"} # things that stand or lie loose on the floor: left out where a space is to look tidy LOOSE = {"SM_Case_A", "SM_WaterDrum", "SM_PlasticPallet", "SM_Toolbox", "SM_PortableLight", "SM_WireReel_A", "SM_NitrogenTank_Covered", "SM_RubberMat_Flat", "SM_RubberMat_Rolled", "SM_Wire_Floor_A", "SM_Wire_Floor_B", "SM_Wrench", "SM_Screwdriver", "SM_OxygenTank", "SM_NitrogenTank", "SM_Pipe_Bar", "SM_Pipe_90", "SM_Pipe_90_b", "SM_Pipe_ASV"} def short(path): return path.split("/")[-1] if path else "" def pack(path): parts = path.strip("/").split("/") return parts[1] if len(parts) > 1 else "" def srgb_to_linear(c): c = c / 255.0 return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 class Item(object): """One placed thing of a demo map. kind: 'mesh' | 'light' | 'decal'.""" __slots__ = ("kind", "key", "m", "label", "actor_class", "data", "center") def __init__(self, kind, key, m, label, actor_class, data, center=None): self.kind, self.key, self.m, self.label, self.actor_class, self.data = kind, key, m, label, actor_class, data self.center = center if center is not None else m.translation.copy() def moved(self, t): """A copy transformed by t (applied on the left).""" return Item(self.kind, self.key, t @ self.m, self.label, self.actor_class, self.data, t @ self.center) @property def name(self): return short(self.key) class Unit(object): def __init__(self, items): self.items = items def filtered(self, pred): return Unit([it for it in self.items if pred(it)]) def moved(self, t): return Unit([it.moved(t) for it in self.items]) def __add__(self, other): return Unit(self.items + other.items) def bounds(self): lo = Vector((1e9, 1e9, 1e9)) hi = Vector((-1e9, -1e9, -1e9)) for it in self.items: for i in range(3): lo[i] = min(lo[i], it.center[i]) hi[i] = max(hi[i], it.center[i]) return lo, hi class Demo(object): """A demo map as a flat list of items in the map's own (Blender) coordinates. kit_dir: the kit export the map belongs to (default: this folder's Season 2 export; the Season 1 export of models/anchorage is read the same way).""" _mesh_info = None # mesh records of every kit export read so far (Unreal paths never collide) def __init__(self, map_name, kit_dir=None): kit_dir = kit_dir or KIT_DIR hits = (glob.glob(os.path.join(kit_dir, "maps", "%s.json" % map_name)) or glob.glob(os.path.join(kit_dir, "maps", "*__%s.json" % map_name))) if not hits: raise RuntimeError("no map export for " + map_name) with open(hits[0], "r", encoding="utf-8") as f: data = json.load(f) if Demo._mesh_info is None: Demo._mesh_info = {} if kit_dir not in Demo._loaded: with open(os.path.join(kit_dir, "meshes.json"), "r", encoding="utf-8") as f: Demo._mesh_info.update(json.load(f)) Demo._loaded.add(kit_dir) self.name = map_name self.items = [] for a in data["actors"]: if a.get("hidden") or a.get("hidden_in_game"): continue cls = a.get("class", "") for mc in a.get("meshes", []): path = mc["mesh"] rec = Demo._mesh_info.get(path) if rec is None or not mc.get("visible", True): continue defaults = [s["material"] for s in rec["slots"]] over = {i: mp for i, mp in enumerate(mc["materials"]) if i < len(defaults) and mp and mp != defaults[i]} mn, mx = rec["min"], rec["max"] c_local = Vector(((mn[0] + mx[0]) / 200.0, -(mn[1] + mx[1]) / 200.0, (mn[2] + mx[2]) / 200.0)) for rows in (mc.get("instances") or [mc["matrix"]]): m = ue_rows_to_matrix(rows) self.items.append(Item("mesh", path, m, a["label"], cls, over, m @ c_local)) for l in a.get("lights", []): if l.get("visible", True): self.items.append(Item("light", l["comp_class"], ue_rows_to_matrix(l["matrix"]), a["label"], cls, l)) for d in a.get("decals", []): self.items.append(Item("decal", d["material"] or "", ue_rows_to_matrix(d["matrix"]), a["label"], cls, d)) _loaded = set() def unit(self, frame, pred): """Items for which pred(item, p) holds, p = the item's centre in `frame`; returned in that frame.""" inv = frame.inverted() out = [] for it in self.items: p = inv @ it.center if pred(it, p): out.append(it.moved(inv)) return Unit(out) # --------------------------------------------------------------------------------------------------------------- # placing # --------------------------------------------------------------------------------------------------------------- _decal_meshes = {} def decal_mesh(size): """Quad in the decal's local YZ plane (Unreal projects a decal along its local X).""" key = (round(size[1], 2), round(size[2], 2)) if key in _decal_meshes: return _decal_meshes[key] name = "DECAL_%gx%g" % (2 * key[0], 2 * key[1]) me = bpy.data.meshes.get(name) if me is None: sy, sz = size[1] / 100.0, size[2] / 100.0 me = bpy.data.meshes.new(name) pts = [(-1, -1), (1, -1), (1, 1), (-1, 1)] # (y_ue, z_ue) in half-extents me.from_pydata([(0.0, -y * sy, z * sz) for y, z in pts], [], [(0, 1, 2, 3)]) uv = me.uv_layers.new(name="UVMap") for li, (y, z) in enumerate(pts): uv.data[li].uv = (z * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5)) me.materials.append(None) me.update() _decal_meshes[key] = me return me def add_light(l, name, col, m, scale=1.0): cls = l["comp_class"] kind = "SPOT" if "Spot" in cls else "AREA" if "Rect" in cls else "SUN" if "Directional" in cls else "POINT" if "Point" in cls else None if kind is None or kind == "SUN": return None data = bpy.data.lights.new(name, kind) lc = l.get("light_color") or [255, 255, 255, 255] data.color = tuple(srgb_to_linear(v) for v in lc[:3]) inten = float(l.get("intensity") or 0.0) units = str(l.get("intensity_units", "")) if "CANDELAS" in units: data.energy = inten * 625.0 * UNITLESS_TO_WATT elif "LUMENS" in units: data.energy = inten / (4.0 * math.pi) * 625.0 * UNITLESS_TO_WATT else: data.energy = inten * UNITLESS_TO_WATT data.energy *= scale if kind in ("POINT", "SPOT"): data.shadow_soft_size = max(0.03, float(l.get("source_radius") or 0.0) / 100.0) if kind == "SPOT": outer = float(l.get("outer_cone_angle") or 44.0) inner = float(l.get("inner_cone_angle") or 0.0) data.spot_size = math.radians(min(179.0, 2.0 * outer)) data.spot_blend = max(0.0, min(1.0, 1.0 - inner / max(outer, 1e-3))) if kind == "AREA": data.shape = "RECTANGLE" data.size = max(0.05, float(l.get("source_width") or 64.0) / 100.0) data.size_y = max(0.05, float(l.get("source_height") or 64.0) / 100.0) ob = bpy.data.objects.new(name, data) ob.matrix_world = m @ LIGHT_FIX ob["ue_intensity"] = inten ob["ue_attenuation_radius"] = float(l.get("attenuation_radius") or 0.0) ob["ue_light_class"] = cls ob["ue_light"] = json.dumps({k: v for k, v in l.items() if k not in ("matrix", "visible")}) # the original values col.objects.link(ob) return ob def place(kit, items, col, t, zone="", lights=None, decals=None, parent=None, tidy=False, light_scale=1.0, material_swap=None, stats=None): """Set a unit's items down with the frame matrix t. Returns the mesh objects. lights / decals: collections for them (None leaves them out). tidy: leave out loose floor props. material_swap: {Unreal material path: other path} applied to every slot (colour variants).""" out = [] stats = stats if stats is not None else {} for it in items: m = t @ it.m if it.kind == "mesh": if tidy and it.name in LOOSE: continue over = it.data if material_swap: rec = Demo._mesh_info[it.key] over = dict(over) for i, s in enumerate(rec["slots"]): cur = over.get(i, s["material"]) if cur in material_swap: over[i] = material_swap[cur] ob = kit.place(it.key, m, col, name=it.label, overrides=over) if ob is None: continue if parent is not None: ob.parent = parent ob.matrix_world = m if zone: ob["zone"] = zone if it.name in ("SM_Door_Small_A", "SM_Door_Small_A_Glass", "SM_Door_Small_UI", "SM_Hall_Door_L", "SM_Door_L", "SM_GlassDoor_A", "SM_GlassDoor_B"): ob["ue_role"] = "door_leaf" out.append(ob) stats["mesh"] = stats.get("mesh", 0) + 1 elif it.kind == "light" and lights is not None: ob = add_light(it.data, it.label, lights, m, light_scale) if ob is not None: if parent is not None: ob.parent = parent ob.matrix_world = m @ LIGHT_FIX if zone: ob["zone"] = zone stats["light"] = stats.get("light", 0) + 1 elif it.kind == "decal" and decals is not None: d = it.data me = decal_mesh(d["size"]) ob = bpy.data.objects.new(it.label, me) mm = m.copy() xaxis = mm.to_3x3().col[0].normalized() mm.translation = mm.translation - xaxis * 0.004 # a few mm off the surface it lies on decals.objects.link(ob) if parent is not None: ob.parent = parent ob.matrix_world = mm ob.material_slots[0].link = "OBJECT" ob.material_slots[0].material = kit.material(d["material"]) ob["ue_decal_material"] = d["material"] or "" ob["ue_decal_size"] = list(d["size"]) ob.visible_shadow = False if zone: ob["zone"] = zone stats["decal"] = stats.get("decal", 0) + 1 return out # --------------------------------------------------------------------------------------------------------------- # frames # --------------------------------------------------------------------------------------------------------------- def frame(x, y, z, yaw_deg=0.0): """Frame at a point, +X turned by yaw about Z.""" return Matrix.Translation((x, y, z)) @ Matrix.Rotation(math.radians(yaw_deg), 4, "Z") def shift_x(d): return Matrix.Translation((d, 0.0, 0.0))