""" Rebuilds one of the pack's own demo maps in Blender from kit_export/maps/__.json. blender -b --factory-startup --python build/bl_demo_map.py -- [--no-lights] [--no-decals] Two uses: it proves the Unreal -> Blender transform (the modules have to close up without gaps), and it shows how the pack's author composes rooms, which is what the vessel interior is modelled on. (Copy of the Anchorage script; map names may be given as Pack__Map.) """ import glob import json import math import os import sys import bpy sys.dont_write_bytecode = True # leave no __pycache__ next to the build scripts from mathutils import Matrix sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import bl_kit # noqa: E402 from bl_kit import GAME, KIT_DIR, Kit, get_collection, ue_rows_to_matrix # noqa: E402 LIGHT_FIX = Matrix.Rotation(math.radians(-90.0), 4, "Y") # Blender lights look down -Z, Unreal's along +X CAM_FIX = Matrix(((0, 0, -1, 0), (-1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 0, 1))) UNITLESS_TO_WATT = 0.02 SKIP_PACKS = ("UE4_Assets", "NatureMaterials_Ice") 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 def add_light(l, name, col): cls = l["comp_class"] if "Spot" in cls: kind = "SPOT" elif "Rect" in cls: kind = "AREA" elif "Directional" in cls: kind = "SUN" elif "Point" in cls: kind = "POINT" else: 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 kind == "SUN": data.energy = max(0.1, inten * 0.3) elif "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 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 = ue_rows_to_matrix(l["matrix"]) @ LIGHT_FIX ob["ue_intensity"] = inten ob["ue_attenuation_radius"] = float(l.get("attenuation_radius") or 0.0) col.objects.link(ob) return ob _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] sy, sz = size[1] / 100.0, size[2] / 100.0 me = bpy.data.meshes.new("DECAL_%gx%g" % (2 * key[0], 2 * key[1])) # Unreal local (y, z) -> Blender local (-y, z); decal UV: u = z, v = y (v down in Unreal, up in Blender) pts = [(-1, -1), (1, -1), (1, 1), (-1, 1)] # (y_ue, z_ue) in half-extents verts = [(0.0, -y * sy, z * sz) for y, z in pts] me.from_pydata(verts, [], [(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.update() _decal_meshes[key] = me return me def add_decal(kit, d, name, col, lift=0.004): me = decal_mesh(d["size"]) ob = bpy.data.objects.new(name, me) m = ue_rows_to_matrix(d["matrix"]) # lift the plane a few mm against the projection direction so it does not fight the surface it lies on xaxis = m.to_3x3().col[0].normalized() m.translation = m.translation - xaxis * lift ob.matrix_world = m col.objects.link(ob) # the quad is shared by every decal of that size; the material sits on the object if not me.materials: me.materials.append(None) 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 return ob def build(map_name, kit=None, want_lights=True, want_decals=True, parent_collection=None, log=print): hits = (glob.glob(os.path.join(KIT_DIR, "maps", "%s.json" % map_name)) # "Pack__Map" (names repeat between packs) 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) kit = kit or Kit() root = get_collection("DEMO | " + map_name, parent_collection) c_mesh = get_collection("DEMO | %s | meshes" % map_name, root) c_light = get_collection("DEMO | %s | lights" % map_name, root) c_decal = get_collection("DEMO | %s | decals" % map_name, root) c_cam = get_collection("DEMO | %s | cameras" % map_name, root) n = {"mesh": 0, "skipped": 0, "light": 0, "decal": 0, "cam": 0, "hidden": 0} for a in data["actors"]: if a.get("hidden") or a.get("hidden_in_game"): n["hidden"] += 1 continue for m in a.get("meshes", []): path = m["mesh"] if not path.startswith(GAME) or path[len(GAME):].split("/")[0] in SKIP_PACKS or not m.get("visible", True): n["skipped"] += 1 continue rec = kit.meshes.get(path) if rec is None: n["skipped"] += 1 continue defaults = [s["material"] for s in rec["slots"]] overrides = {i: mp for i, mp in enumerate(m["materials"]) if i < len(defaults) and mp and mp != defaults[i]} mats = m.get("instances") or [m["matrix"]] for rows in mats: ob = kit.place(path, ue_rows_to_matrix(rows), c_mesh, name=a["label"], overrides=overrides) if ob is not None: ob["ue_actor"] = a["name"] n["mesh"] += 1 if want_lights: for l in a.get("lights", []): if l.get("visible", True) and add_light(l, a["label"], c_light): n["light"] += 1 if want_decals: for d in a.get("decals", []): add_decal(kit, d, a["label"], c_decal) n["decal"] += 1 for c in a.get("cameras", []): if a["class"] not in ("CameraActor", "CineCameraActor"): continue cam = bpy.data.cameras.new(a["label"]) cam.sensor_fit = "HORIZONTAL" cam.angle = math.radians(float(c.get("fov") or 90.0)) ob = bpy.data.objects.new(a["label"], cam) ob.matrix_world = ue_rows_to_matrix(c["matrix"]) @ CAM_FIX c_cam.objects.link(ob) n["cam"] += 1 log("demo map %s: %s" % (map_name, n)) return root if __name__ == "__main__": argv = sys.argv[sys.argv.index("--") + 1:] name, out = argv[0], argv[1] bpy.ops.wm.read_factory_settings(use_empty=True) build(name, want_lights="--no-lights" not in argv, want_decals="--no-decals" not in argv) os.makedirs(os.path.dirname(out), exist_ok=True) bpy.ops.wm.save_as_mainfile(filepath=out, compress=True) bpy.ops.file.make_paths_relative() bpy.ops.wm.save_mainfile(compress=True) print("saved", out, "objects", len(bpy.data.objects), "meshes", len(bpy.data.meshes), "materials", len(bpy.data.materials))