"""Build the procedural Maze32 command chamber in Blender 4.x. Run from this package: blender --background --python build_command_chamber.py -- --output maze32-command-chamber.blend The scene intentionally uses no downloaded meshes or textures. The north entry is cut from the wall with an Exact Boolean before its trim is added. """ import argparse import math import sys from pathlib import Path import bpy from mathutils import Vector def cli(): values = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] parser = argparse.ArgumentParser() parser.add_argument("--output", type=Path, default=Path("maze32-command-chamber.blend")) parser.add_argument("--renders", type=Path, default=Path("renders")) return parser.parse_args(values) def clear_scene(): bpy.ops.object.select_all(action="SELECT") bpy.ops.object.delete(use_global=False) def material(name, color, metallic=0.0, roughness=0.5, emission=None): result = bpy.data.materials.new(name) result.use_nodes = True node = result.node_tree.nodes["Principled BSDF"] node.inputs["Base Color"].default_value = color node.inputs["Metallic"].default_value = metallic node.inputs["Roughness"].default_value = roughness if emission: node.inputs["Emission Color"].default_value = emission node.inputs["Emission Strength"].default_value = 3.0 return result SLATE = None BRASS = None CYAN = None COLORS = [] def cube(name, location, dimensions, material_ref, bevel=0.0): bpy.ops.mesh.primitive_cube_add(size=1, location=location) obj = bpy.context.object obj.name = name obj.dimensions = dimensions bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) if bevel: modifier = obj.modifiers.new("Soft readable edges", "BEVEL") modifier.width = bevel modifier.segments = 3 obj.data.materials.append(material_ref) return obj def cylinder(name, location, radius, depth, material_ref, vertices=48): bpy.ops.mesh.primitive_cylinder_add(vertices=vertices, radius=radius, depth=depth, location=location) obj = bpy.context.object obj.name = name obj.data.materials.append(material_ref) bevel = obj.modifiers.new("Edge highlights", "BEVEL") bevel.width = 0.03 bevel.segments = 2 return obj def point_camera(camera, target): camera.rotation_euler = (Vector(target) - camera.location).to_track_quat("-Z", "Y").to_euler() def boolean_door(wall): cutter = cube("CUTTER_north-arrival", (0, 5.85, 1.55), (3.0, 0.8, 3.1), SLATE) modifier = wall.modifiers.new("OPENING_north-arrival_EXACT", "BOOLEAN") modifier.operation = "DIFFERENCE" modifier.solver = "EXACT" modifier.object = cutter bpy.context.view_layer.objects.active = wall bpy.ops.object.modifier_apply(modifier=modifier.name) bpy.data.objects.remove(cutter, do_unlink=True) def make_shell(): cube("SHELL_floor", (0, 0, -0.15), (12, 12, 0.3), SLATE, 0.05) cube("SHELL_ceiling", (0, 0, 5.0), (12, 12, 0.35), SLATE, 0.05) cube("SHELL_south_wall", (0, -5.85, 2.5), (12, 0.3, 5.0), SLATE, 0.05) west = cube("SHELL_west_wall", (-5.85, 0, 2.5), (0.3, 12, 5.0), SLATE, 0.05) east = cube("SHELL_east_wall", (5.85, 0, 2.5), (0.3, 12, 5.0), SLATE, 0.05) north = cube("SHELL_north_wall", (0, 5.85, 2.5), (12, 0.3, 5.0), SLATE, 0.05) boolean_door(north) for x in (-1.72, 1.72): cube("TRIM_north-arrival", (x, 5.62, 1.55), (0.18, 0.24, 3.3), BRASS, 0.03) cube("TRIM_arrival-header", (0, 5.62, 3.22), (3.62, 0.24, 0.18), BRASS, 0.03) cube("ENTRY_apron", (0, 6.8, -0.02), (3.0, 2.0, 0.08), BRASS, 0.02) for x, y in ((-4.8, -4.8), (4.8, -4.8), (-4.8, 4.8), (4.8, 4.8)): cylinder("ARCH_column", (x, y, 2.2), 0.28, 4.4, BRASS, 32) def make_command_table(): cylinder("HERO_command_table_base", (0, 0, 0.32), 1.72, 0.64, SLATE) cylinder("HERO_command_table_brass_ring", (0, 0, 0.68), 1.55, 0.12, BRASS) cylinder("HERO_command_table_top", (0, 0, 0.76), 1.42, 0.12, SLATE) cylinder("HERO_hologram_disc", (0, 0, 0.85), 1.16, 0.05, CYAN) for angle in range(0, 360, 45): radians = math.radians(angle) x, y = 0.88 * math.cos(radians), 0.88 * math.sin(radians) marker = cube("HERO_maze_segment", (x, y, 1.0), (0.42, 0.08, 0.3), CYAN, 0.02) marker.rotation_euler[2] = radians + math.pi / 2 cylinder("HERO_maze_core", (0, 0, 1.08), 0.24, 0.48, CYAN, 32) def make_station(name, location, rotation, color): root = bpy.data.objects.new(name, None) bpy.context.collection.objects.link(root) base = cube(name + "_base", location, (1.1, 0.7, 0.18), SLATE, 0.04) body = cube(name + "_body", (location[0], location[1], 0.55), (0.72, 0.42, 0.7), BRASS, 0.04) marker = cylinder(name + "_color_marker", (location[0], location[1] - 0.24, 0.94), 0.19, 0.07, color, 32) for part in (base, body, marker): part.parent = root root.rotation_euler[2] = math.radians(rotation) root["attachment_face"] = "floor" root["semantic_front"] = "inward" def make_ceiling(): cylinder("CEILING_light_ring", (0, 0, 4.74), 2.25, 0.1, BRASS) cylinder("CEILING_beacon", (0, 0, 4.6), 0.32, 0.34, CYAN) for angle in range(0, 360, 90): radians = math.radians(angle) cube("CEILING_radial_trim", (3.5 * math.cos(radians), 3.5 * math.sin(radians), 4.73), (2.5, 0.1, 0.12), BRASS, 0.02).rotation_euler[2] = radians def light(name, location, color, energy, size=2.0): data = bpy.data.lights.new(name, "AREA") data.energy = energy data.color = color[:3] data.shape = "DISK" data.size = size obj = bpy.data.objects.new(name, data) bpy.context.collection.objects.link(obj) obj.location = location obj.rotation_euler = (0, 0, 0) return obj def render_views(camera, output): scene = bpy.context.scene scene.render.engine = "BLENDER_EEVEE_NEXT" scene.render.resolution_x = 1280 scene.render.resolution_y = 720 scene.render.resolution_percentage = 100 scene.render.image_settings.file_format = "PNG" views = { "production": ((9.2, -10.2, 6.7), (0, 0, 1.5)), "corner-nw": ((-5.0, 5.0, 3.7), (0, 0, 1.2)), "corner-ne": ((5.0, 5.0, 3.7), (0, 0, 1.2)), "corner-se": ((5.0, -5.0, 3.7), (0, 0, 1.2)), "corner-sw": ((-5.0, -5.0, 3.7), (0, 0, 1.2)), "ceiling-up": ((0, 0, 0.5), (0, 0, 5.0)), } output.mkdir(parents=True, exist_ok=True) for name, (location, target) in views.items(): camera.location = location point_camera(camera, target) scene.render.filepath = str(output / (name + ".png")) bpy.ops.render.render(write_still=True) def main(): args = cli() clear_scene() global SLATE, BRASS, CYAN, COLORS SLATE = material("MAT_slate", (0.055, 0.075, 0.11, 1), 0.12, 0.68) BRASS = material("MAT_brass", (0.42, 0.22, 0.045, 1), 0.8, 0.28) CYAN = material("MAT_hologram", (0.0, 0.55, 0.75, 1), 0.1, 0.25, (0.0, 0.75, 1.0, 1)) COLORS = [material("MAT_red", (0.85, 0.05, 0.05, 1), emission=(0.85, 0.02, 0.02, 1)), material("MAT_yellow", (0.9, 0.65, 0.02, 1), emission=(0.9, 0.5, 0.0, 1)), material("MAT_green", (0.05, 0.72, 0.25, 1), emission=(0.0, 0.65, 0.18, 1)), material("MAT_blue", (0.05, 0.28, 0.9, 1), emission=(0.02, 0.15, 0.85, 1))] make_shell() make_command_table() make_station("STATION_north", (0, 3.95, 0.09), 180, COLORS[0]) make_station("STATION_east", (3.95, 0, 0.09), 90, COLORS[1]) make_station("STATION_south", (0, -3.95, 0.09), 0, COLORS[2]) make_station("STATION_west", (-3.95, 0, 0.09), -90, COLORS[3]) make_ceiling() camera_data = bpy.data.cameras.new("ProductionCamera") camera = bpy.data.objects.new("ProductionCamera", camera_data) bpy.context.collection.objects.link(camera) camera_data.lens = 35 bpy.context.scene.camera = camera light("Key", (0, 0, 4.5), (0.7, 0.85, 1.0, 1), 900, 5.0) light("Warm arrival", (0, 4.9, 3.8), (1.0, 0.5, 0.15, 1), 650, 2.5) render_views(camera, args.renders) scene = bpy.context.scene scene["room_id"] = "maze32-command-chamber" scene["room_family"] = "gameplay-room" scene.unit_settings.system = "METRIC" scene.unit_settings.scale_length = 1.0 bpy.ops.wm.save_as_mainfile(filepath=str(args.output.resolve())) if __name__ == "__main__": main()