Files
maze32/docs/3d/maze32-command-chamber/build_command_chamber.py
T

210 lines
8.2 KiB
Python

"""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()