"""Parametric hero façade: 6840 Hollywood Blvd / El Capitan Entertainment Centre. Builds the north elevation in the project frame (X east, Y north, Z up, metres, origin Hollywood x Highland) and exports it with the named FBX preset `ue_metric_v1`, so it imports into Unreal at 1 m = 100 uu. blender -b --factory-startup --python Tools/hero_facade_build.py -- \ --out /tmp/gigline/hero-facade.fbx \ --save-blend Art/blender/hero/hero-facade-2026-11-07.blend \ --report Art/blender/hero/hero-facade-2026-11-07.json Every dimension traces to a recorded measurement or a named assumption; the report writes both, so a later pass can see what was evidence and what was invented: width 25.75 m measured, OSM way 424024147 north edge height 14.50 m tag `height` on the same way 6 columns, 4.96 m sibling venue notes, cross-checked against the 25.75 m frontage (5 bays + one column = 25.70 m) three portals photographs: three separate openings, each with its own short flight, not one 12 m grand stair everything else assumption, listed in the report Quieter choices are deliberate: a blockout for the demo, not a scanned facade. Detail that the photographs do not resolve (moulding profiles, window rhythm) is left abstract rather than invented in fine detail. """ import argparse import json import math import os import sys import bpy # --- measured / derived ------------------------------------------------------ FRONTAGE_W = 25.75 BODY_D = 12.0 # street-side mass only; the building runs 54.9 m deep HEIGHT = 14.5 COLUMN_COUNT = 6 COLUMN_BAY = 4.96 FRONTAGE_Y = 0.0 # front plane sits at the building line of the block frame # --- assumptions ------------------------------------------------------------- # Calibrated against the 2009 façade photograph: the entablature starts a little # over half the total height, and the parapet carries the motto at about three # quarters. The first pass had the enframed wall carrying 5.7 m of blank stone # above the capitals, which read as a warehouse rather than a temple. PEDESTAL_H = 1.45 # column pedestal top; entrance platform sits on it COLUMN_D = 0.95 # Ionic shaft diameter at the base CAPITAL_H = 0.95 ENTABLATURE_H = 3.00 # band between the capitals and the motto panel # 3.40 -> 3.00 on 2026-11-09: the photographs put the # colonnade at 50-60 % of the 14.5 m elevation, and our # 3.40/2.20 entablature+parapet left it at 47.6 %. Thinning # both lifts the colonnade to 7.70 m = 53.1 %, inside the band. PARAPET_H = 1.80 # motto panel and cornice above the entablature (see above) PORTAL_W = 3.10 # each of the three door openings PORTAL_H = 3.40 STEP_TREAD = 0.34 STEP_RISE = 0.15 STEP_WIDTH = 3.60 def cube(name, x, y, z, dx, dy, dz, material=None): bpy.ops.mesh.primitive_cube_add(size=1.0, location=(x, y, z)) obj = bpy.context.active_object obj.name = name obj.scale = (dx, dy, dz) bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) if material: obj.data.materials.append(material) return obj def cylinder(name, x, y, z0, z1, radius, material, vertices=24, smooth=True): bpy.ops.mesh.primitive_cylinder_add( vertices=vertices, radius=radius, depth=(z1 - z0), location=(x, y, (z0 + z1) / 2.0), ) obj = bpy.context.active_object obj.name = name if material: obj.data.materials.append(material) if smooth: # Round parts must shade smoothly *around* the axis while keeping the # caps creased. Without this the columns read as faceted prisms, and the # FBX carries no smoothing groups at all (Unreal warns about it). bpy.ops.object.shade_smooth_by_angle(angle=math.radians(35.0)) return obj def disc_on_wall(name, x, y, z, radius, thickness, material=None, vertices=24): """A round disc mounted vertically on a wall face. `cylinder()` builds along Z, so a "medallion" made with it is a pancake lying flat on top of the parapet rather than a disc on its face — which is what the first build did (1.1 m across in Y, 0.02 m thick in Z, sitting at the coping). This rotates the cylinder 90 degrees about X so the axis points along Y and the disc faces the street. """ obj = cylinder(name, x, y, z - thickness / 2.0, z + thickness / 2.0, radius, material, vertices=vertices) obj.rotation_euler = (math.radians(90.0), 0.0, 0.0) bpy.ops.object.select_all(action="DESELECT") obj.select_set(True) bpy.context.view_layer.objects.active = obj bpy.ops.object.transform_apply(location=False, rotation=True, scale=True) return obj def make_material(name, rgba, roughness=0.75): mat = bpy.data.materials.new(name) mat.use_nodes = True bsdf = mat.node_tree.nodes.get("Principled BSDF") bsdf.inputs["Base Color"].default_value = rgba bsdf.inputs["Roughness"].default_value = roughness return mat def build(): scene = bpy.context.scene for obj in list(bpy.data.objects): bpy.data.objects.remove(obj, do_unlink=True) stone = make_material("M_Facade_Stone", (0.78, 0.74, 0.66, 1.0), 0.85) stone_dark = make_material("M_Facade_StoneShadow", (0.62, 0.59, 0.53, 1.0), 0.85) glass = make_material("M_Facade_GlassDark", (0.05, 0.06, 0.07, 1.0), 0.25) banner = make_material("M_Facade_Banner", (0.66, 0.06, 0.05, 1.0), 0.9) half_w = FRONTAGE_W / 2.0 lines = [] parts = [] def note(name, dims, evidence): parts.append({"object": name, "dims_m": [round(d, 3) for d in dims], "evidence": evidence}) lines.append(f"[hero] {name:22} {dims[0]:6.2f} x {dims[1]:6.2f} x {dims[2]:6.2f} m ({evidence})") # --- main mass --------------------------------------------------------- # The mass sits behind the colonnade; its front face is the colonnade line. # Inset so the entablature (1.5 m deep) and parapet (1.2 m deep) sit inside the # measured 25.75 m frontage instead of overhanging it. inset = 0.75 cube("Hero_Mass", 0.0, -BODY_D / 2.0 - 0.5, HEIGHT / 2.0, FRONTAGE_W - 2 * inset, BODY_D, HEIGHT, stone_dark) note("Hero_Mass", (FRONTAGE_W - 2 * inset, BODY_D, HEIGHT), "measured frontage x assumption depth x measured height, inset behind the cornice") # --- stylobate and steps ---------------------------------------------- # A plinth from the pavement to the pedestal shelf, with three door flights. # The stylobate is built later as piers around the three door openings. # --- Ionic colonnade --------------------------------------------------- col_xs = [] span = (COLUMN_COUNT - 1) * COLUMN_BAY for i in range(COLUMN_COUNT): x = -span / 2.0 + i * COLUMN_BAY col_xs.append(x) cube(f"Hero_Pedestal_{i + 1}", x, -0.62, PEDESTAL_H + 0.27, 1.24, 1.24, 0.54, stone) # Base moulding: a taller, wider cylinder where the shaft meets the # pedestal. Without it the shaft starts straight off a square plinth, # which is not how an Ionic base reads in silhouette. base_z0 = PEDESTAL_H + 0.55 base_h = 0.22 cylinder(f"Hero_ColumnBase_{i + 1}", x, -0.55, base_z0, base_z0 + base_h, 0.68, stone, vertices=20) shaft_z0 = base_z0 + base_h cap_z0 = HEIGHT - ENTABLATURE_H - PARAPET_H - CAPITAL_H cylinder(f"Hero_Column_{i + 1}", x, -0.55, shaft_z0, cap_z0, COLUMN_D / 2.0, stone) # Capital block, then the square abacus that carries the architrave. # The abacus takes the top 0.14 m of the capital zone so the colonnade # still ends at HEIGHT - ENTABLATURE_H - PARAPET_H (proportion unchanged). abacus_h = 0.14 cylinder(f"Hero_Capital_{i + 1}", x, -0.55, cap_z0, HEIGHT - ENTABLATURE_H - PARAPET_H - abacus_h, 0.62, stone, vertices=16) cube(f"Hero_Abacus_{i + 1}", x, -0.55, HEIGHT - ENTABLATURE_H - PARAPET_H - abacus_h / 2.0, 1.30, 1.30, abacus_h, stone) note("Hero_Column_x6", (COLUMN_D, COLUMN_D, HEIGHT - ENTABLATURE_H - PARAPET_H - PEDESTAL_H), f"6 columns at {COLUMN_BAY} m centres (sibling notes x measured frontage)") note("Hero_ColumnBase_x6", (1.36, 1.36, 0.22), "assumption: Ionic base moulding; photographs show a wider base at the shaft foot") note("Hero_Abacus_x6", (1.30, 1.30, 0.14), "assumption: square abacus under the architrave, as Ionic capitals carry") # --- entablature and parapet ------------------------------------------ ent_z0 = HEIGHT - ENTABLATURE_H - PARAPET_H # Lower entablature band (architrave) standing proud of the wall, so the # elevation reads as layered stone rather than one flat plane. cube("Hero_Architrave", 0.0, -1.55, ent_z0 + 0.42, FRONTAGE_W, 1.65, 0.84, stone) note("Hero_Architrave", (FRONTAGE_W, 1.65, 0.84), "assumption: stacked entablature bands") # Cornice under the parapet. cube("Hero_Cornice", 0.0, -1.6, HEIGHT - PARAPET_H - 0.18, FRONTAGE_W, 1.75, 0.36, stone) note("Hero_Cornice", (FRONTAGE_W, 1.75, 0.36), "assumption: projecting cornice line") cube("Hero_Entablature", 0.0, -0.75, ent_z0 + ENTABLATURE_H / 2.0, FRONTAGE_W, 1.5, ENTABLATURE_H, stone) note("Hero_Entablature", (FRONTAGE_W, 1.5, ENTABLATURE_H), "assumption: entablature depth") cube("Hero_Parapet", 0.0, -0.75, HEIGHT - PARAPET_H / 2.0, FRONTAGE_W, 1.2, PARAPET_H, stone) note("Hero_Parapet", (FRONTAGE_W, 1.2, PARAPET_H), "assumption: parapet carries the motto band") # Motto band: a recessed panel across the parapet, as the photographs show. cube("Hero_MottoBand", 0.0, -1.32, HEIGHT - PARAPET_H / 2.0, FRONTAGE_W - 3.0, 0.08, 0.85, stone_dark) note("Hero_MottoBand", (FRONTAGE_W - 3.0, 0.08, 0.85), "photographs: motto panel above the colonnade") # Medallions either side of the inscription: discs on the parapet face, not # pancakes on the coping (see disc_on_wall). for i, x in enumerate((-6.2, 6.2), start=1): disc_on_wall(f"Hero_Medallion_{i}", x, -1.30, HEIGHT - PARAPET_H / 2.0, 0.55, 0.04, stone_dark, vertices=20) note("Hero_Medallion_x2", (1.1, 0.04, 1.1), "photographs: two stone discs beside the motto, mounted on the parapet face") # --- entrances --------------------------------------------------------- # Three portals between column pairs, each with its own short flight. The # doorway is cut INTO the stylobate face rather than laid on it: the plinth is # modelled as three segments so the opening is a real recess, and the portals # stand proud of the wall behind the colonnade so they read from the street. portal_xs = (-COLUMN_BAY, 0.0, COLUMN_BAY) plinth_front = -2.4 # front face of the plinth, in the facade frame plinth_back = 0.0 gap = PORTAL_W # The plinth is four piers between and beside the openings, plus a band over # them, so each door is a real recess instead of a panel on a solid face. piers = [ (-(FRONTAGE_W / 2.0), portal_xs[0] - gap / 2.0), (portal_xs[0] + gap / 2.0, portal_xs[1] - gap / 2.0), (portal_xs[1] + gap / 2.0, portal_xs[2] - gap / 2.0), (portal_xs[2] + gap / 2.0, (FRONTAGE_W / 2.0)), ] for pi, (x0, x1) in enumerate(piers, start=1): cube(f"Hero_Stylobate_{pi}", (x0 + x1) / 2.0, -1.2, PEDESTAL_H / 2.0, x1 - x0, 2.4, PEDESTAL_H, stone) # Band above the openings, closing the plinth to full height between the doors. cube("Hero_Stylobate_LintelBand", 0.0, -1.2, PORTAL_H + (PEDESTAL_H - PORTAL_H) / 2.0, (portal_xs[-1] + gap / 2.0) - (portal_xs[0] - gap / 2.0), 2.4, PEDESTAL_H - PORTAL_H, stone) note("Hero_Stylobate", (FRONTAGE_W, 2.4, PEDESTAL_H), "assumption: pedestal height; four piers and a lintel band leave three recesses") for i, x in enumerate(portal_xs, start=1): # Recessed doorway: a dark reveal set back inside the plinth, 1.2 m deep so # it reads as an opening from any angle. depth = 1.2 cube(f"Hero_Portal_{i}", x, plinth_front + depth / 2.0 + 0.05, PORTAL_H / 2.0, PORTAL_W, depth, PORTAL_H, glass) # Stone surround, standing 0.1 m proud of the plinth face. for side, sx in (("L", -1.0), ("R", 1.0)): # Jambs stop at the pavement: a 0.15 m overhang below Z 0 would sink the # whole facade into the ground plane. cube(f"Hero_PortalJamb_{i}{side}", x + sx * (PORTAL_W / 2.0 + 0.15), plinth_front - 0.10, (PORTAL_H + 0.3) / 2.0, 0.3, 0.2, PORTAL_H + 0.3, stone) cube(f"Hero_PortalLintel_{i}", x, plinth_front - 0.10, PORTAL_H + 0.15, PORTAL_W + 0.6, 0.2, 0.3, stone) # Door grille: mullions across the opening, dark metal read at distance. for m in range(1, 4): cube(f"Hero_PortalMullion_{i}_{m}", x - PORTAL_W / 2.0 + m * PORTAL_W / 4.0, plinth_front + 0.12, PORTAL_H / 2.0, 0.09, 0.06, PORTAL_H, stone_dark) # Sign panel over the doorway, as the photographs show. cube(f"Hero_PortalSign_{i}", x, plinth_front - 0.16, PORTAL_H + 0.75, 0.7, 0.06, 0.9, banner) # Photographs show four to five risers per portal; the count follows the # pedestal height rather than inheriting the old "eight steps" claim. steps = max(3, min(5, int(round((PEDESTAL_H - 0.2) / STEP_RISE)))) # Steps descend from the plinth to the pavement; the lowest tread must land # exactly on Z 0, never below it, so the flight meets the ground plane. steps = max(3, min(steps, int(round(PEDESTAL_H / STEP_RISE)))) for st in range(steps): top = PEDESTAL_H - st * STEP_RISE height = top if st == steps - 1 else STEP_RISE cube(f"Hero_Step_{i}_{st + 1}", x, plinth_front - 0.14 - st * STEP_TREAD - STEP_TREAD / 2.0 + 0.28, top - height / 2.0, STEP_WIDTH, STEP_TREAD, height, stone) note(f"Hero_PortalFlight_{i}", (PORTAL_W, STEP_TREAD * steps, PEDESTAL_H - 0.2), f"{steps} steps at {STEP_RISE:.2f} m rise / {STEP_TREAD:.2f} m tread " f"(assumption; photos show 4-5, the old notes said 8)") # --- banners ----------------------------------------------------------- # Three vertical event banners hung from the column line, as the recordings show. for i, x in enumerate(portal_xs, start=1): cube(f"Hero_Banner_{i}", x, -1.05, 7.9, 1.55, 0.06, 5.4, banner) note("Hero_Banner_x3", (1.55, 0.06, 5.4), "photographs: JIMMY KIMMEL LIVE banners on the column line") # --- windows ----------------------------------------------------------- # Two ranks of windows between the columns and behind the colonnade. # Y must sit on the wall plane: the mass front face is at Y -0.50 and the # columns stand proud to -1.02. These were at -1.98, i.e. 1.44 m in FRONT of # the wall and ahead of the column line, so the glazing floated in the # colonnade instead of reading as openings behind it (visible in the # review-hero-facade-* shots). WINDOW_Y puts the pane 6 cm proud of the wall, # safely behind the columns. WINDOW_Y = -0.56 for rank, z in enumerate((4.4, 8.2), start=1): for i, x in enumerate(col_xs[:-1], start=1): wx = x + COLUMN_BAY / 2.0 cube(f"Hero_Window_{rank}_{i}", wx, WINDOW_Y, z, 2.0, 0.08, 1.9, glass) note("Hero_Window_x10", (2.0, 0.08, 1.9), "photographs: window bands behind the colonnade") return parts, lines def export_fbx(path): os.makedirs(os.path.dirname(path), exist_ok=True) # Named export preset "ue_metric_v1" - same settings as Tools/scale_test_export.py. bpy.ops.export_scene.fbx( filepath=path, use_selection=False, object_types={"MESH"}, apply_unit_scale=True, apply_scale_options="FBX_SCALE_NONE", axis_forward="-Y", axis_up="Z", use_space_transform=True, use_mesh_modifiers=True, # "FACE" exports smoothing groups; without it every import logs # "No smoothing group information was found for this mesh". mesh_smooth_type="FACE", path_mode="AUTO", bake_space_transform=False, ) print(f"[hero] exported FBX preset ue_metric_v1 -> {path}") def main(): argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] parser = argparse.ArgumentParser() parser.add_argument("--out", default="/tmp/gigline/hero-facade.fbx") parser.add_argument("--save-blend", default=None) parser.add_argument("--report", default=None) args = parser.parse_args(argv) parts, lines = build() for line in lines: print(line) # Bounds check: the facade must be exactly the measured frontage and height. import mathutils mn = mathutils.Vector((1e9, 1e9, 1e9)) mx = mathutils.Vector((-1e9, -1e9, -1e9)) for obj in bpy.context.scene.objects: if obj.type != "MESH": continue for corner in obj.bound_box: world = obj.matrix_world @ mathutils.Vector(corner) mn = mathutils.Vector((min(mn[i], world[i]) for i in range(3))) mx = mathutils.Vector((max(mx[i], world[i]) for i in range(3))) size = mx - mn print(f"[hero] bounds: {size.x:.2f} x {size.y:.2f} x {size.z:.2f} m " f"(x {mn.x:.2f}..{mx.x:.2f}, y {mn.y:.2f}..{mx.y:.2f}, z {mn.z:.2f}..{mx.z:.2f})") if abs(size.z - HEIGHT) > 0.05: print(f"[hero] WARNING: height {size.z:.2f} m does not match the {HEIGHT} m tag") if args.save_blend: os.makedirs(os.path.dirname(args.save_blend), exist_ok=True) bpy.ops.wm.save_as_mainfile(filepath=args.save_blend) print(f"[hero] saved {args.save_blend}") if args.report: os.makedirs(os.path.dirname(args.report), exist_ok=True) with open(args.report, "w") as handle: json.dump( { "asset": "hero-facade", "frame": "project frame, metres, X east Y north Z up", "bounds_m": {"min": [round(v, 3) for v in mn], "max": [round(v, 3) for v in mx], "size": [round(v, 3) for v in size]}, "parts": parts, }, handle, indent=2, ) print(f"[hero] wrote {args.report}") export_fbx(args.out) main()