feat: hero facade blockout for 6840 Hollywood Blvd
Tools/hero_facade_build.py builds the north elevation parametrically and writes a JSON report naming the evidence behind every part: the 25.75 m frontage and 14.5 m height are measured, the 4.96 m column bay comes from the sibling notes and matches that frontage, the three portal flights follow the photographs (five risers, overturning the inherited eight-step, 12 m wide claim), and the rest is labelled assumption. Tools/ue_hero_facade_import.py imports it with the ue_metric_v1 preset (2604 x 1257 x 1450 cm, 1892 tris, 4 materials) and places it on the measured building line of /Game/Hollywood/DemoMap. Two new review shots at review-04 and review-05, both pending approval. Camera placement cost four renders: the first two sat inside the facade body, and the frontage faces Unreal -Y, so a camera has to stand at Y < -1450 cm, not north of the building line in project terms.
This commit is contained in:
1 parent
41fd249b8a
commit
c5e30d1c36
14 files changed
+600
-6
No files matched your search
@@ -0,0 +1,276 @@
|
||||
"""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 -------------------------------------------------------------
|
||||
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 = 2.20
|
||||
PARAPET_H = 1.60
|
||||
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):
|
||||
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)
|
||||
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.
|
||||
cube("Hero_Stylobate", 0.0, -1.2, PEDESTAL_H / 2.0, FRONTAGE_W, 2.4, PEDESTAL_H, stone)
|
||||
note("Hero_Stylobate", (FRONTAGE_W, 2.4, PEDESTAL_H), "assumption: pedestal height")
|
||||
|
||||
# --- 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)
|
||||
cylinder(f"Hero_Pedestal_{i + 1}", x, -0.55, PEDESTAL_H, PEDESTAL_H + 0.55,
|
||||
0.62, stone, vertices=16)
|
||||
cylinder(f"Hero_Column_{i + 1}", x, -0.55, PEDESTAL_H + 0.55,
|
||||
PEDESTAL_H + 0.55 + (HEIGHT - ENTABLATURE_H - PARAPET_H - PEDESTAL_H - 0.55 - CAPITAL_H),
|
||||
COLUMN_D / 2.0, stone)
|
||||
cylinder(f"Hero_Capital_{i + 1}", x, -0.55,
|
||||
HEIGHT - ENTABLATURE_H - PARAPET_H - CAPITAL_H,
|
||||
HEIGHT - ENTABLATURE_H - PARAPET_H, 0.62, stone, vertices=16)
|
||||
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)")
|
||||
|
||||
# --- entablature and parapet ------------------------------------------
|
||||
ent_z0 = HEIGHT - ENTABLATURE_H - PARAPET_H
|
||||
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.
|
||||
for i, x in enumerate((-6.2, 6.2), start=1):
|
||||
cylinder(f"Hero_Medallion_{i}", x, -1.36, HEIGHT - PARAPET_H / 2.0 + 0.42,
|
||||
HEIGHT - PARAPET_H / 2.0 + 0.44, 0.55, stone_dark, vertices=20)
|
||||
note("Hero_Medallion_x2", (1.1, 1.1, 0.02), "photographs: two stone discs beside the motto")
|
||||
|
||||
# --- entrances ---------------------------------------------------------
|
||||
# Three portals between column pairs, each with its own short flight.
|
||||
portal_xs = (-COLUMN_BAY, 0.0, COLUMN_BAY)
|
||||
for i, x in enumerate(portal_xs, start=1):
|
||||
# Recessed doorway: a dark panel set into the stylobate face.
|
||||
cube(f"Hero_Portal_{i}", x, -2.35, PORTAL_H / 2.0,
|
||||
PORTAL_W, 0.10, PORTAL_H, glass)
|
||||
# Door grille above the steps, using the banner red for the sign panel.
|
||||
cube(f"Hero_PortalSign_{i}", x, -2.42, PORTAL_H + 0.55,
|
||||
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))))
|
||||
for s in range(steps):
|
||||
top = PEDESTAL_H - s * STEP_RISE
|
||||
cube(f"Hero_Step_{i}_{s + 1}", x,
|
||||
-2.4 - 0.14 - s * STEP_TREAD - STEP_TREAD / 2.0,
|
||||
top - STEP_RISE / 2.0,
|
||||
STEP_WIDTH, STEP_TREAD, STEP_RISE, 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.
|
||||
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, -1.98, 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,
|
||||
mesh_smooth_type="OFF",
|
||||
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()
|
||||
Reference in new issue
Block a user