210 lines
8.2 KiB
Python
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()
|