diff --git a/Art/blender/hero/hero-facade-2026-11-07.blend b/Art/blender/hero/hero-facade-2026-11-07.blend new file mode 100644 index 0000000..1bfcf57 --- /dev/null +++ b/Art/blender/hero/hero-facade-2026-11-07.blend @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f52c2388c9a5e1e27e975f463bb740fa1efcb6e2343be6b9a779d177f0ca06a8 +size 125299 diff --git a/Art/blender/hero/hero-facade-2026-11-07.json b/Art/blender/hero/hero-facade-2026-11-07.json new file mode 100644 index 0000000..413059d --- /dev/null +++ b/Art/blender/hero/hero-facade-2026-11-07.json @@ -0,0 +1,131 @@ +{ + "asset": "hero-facade", + "frame": "project frame, metres, X east Y north Z up", + "bounds_m": { + "min": [ + -13.02, + -12.5, + 0.0 + ], + "max": [ + 13.02, + 0.07, + 14.5 + ], + "size": [ + 26.04, + 12.57, + 14.5 + ] + }, + "parts": [ + { + "object": "Hero_Mass", + "dims_m": [ + 24.25, + 12.0, + 14.5 + ], + "evidence": "measured frontage x assumption depth x measured height, inset behind the cornice" + }, + { + "object": "Hero_Stylobate", + "dims_m": [ + 25.75, + 2.4, + 1.45 + ], + "evidence": "assumption: pedestal height" + }, + { + "object": "Hero_Column_x6", + "dims_m": [ + 0.95, + 0.95, + 9.25 + ], + "evidence": "6 columns at 4.96 m centres (sibling notes x measured frontage)" + }, + { + "object": "Hero_Entablature", + "dims_m": [ + 25.75, + 1.5, + 2.2 + ], + "evidence": "assumption: entablature depth" + }, + { + "object": "Hero_Parapet", + "dims_m": [ + 25.75, + 1.2, + 1.6 + ], + "evidence": "assumption: parapet carries the motto band" + }, + { + "object": "Hero_MottoBand", + "dims_m": [ + 22.75, + 0.08, + 0.85 + ], + "evidence": "photographs: motto panel above the colonnade" + }, + { + "object": "Hero_Medallion_x2", + "dims_m": [ + 1.1, + 1.1, + 0.02 + ], + "evidence": "photographs: two stone discs beside the motto" + }, + { + "object": "Hero_PortalFlight_1", + "dims_m": [ + 3.1, + 1.7, + 1.25 + ], + "evidence": "5 steps at 0.15 m rise / 0.34 m tread (assumption; photos show 4-5, the old notes said 8)" + }, + { + "object": "Hero_PortalFlight_2", + "dims_m": [ + 3.1, + 1.7, + 1.25 + ], + "evidence": "5 steps at 0.15 m rise / 0.34 m tread (assumption; photos show 4-5, the old notes said 8)" + }, + { + "object": "Hero_PortalFlight_3", + "dims_m": [ + 3.1, + 1.7, + 1.25 + ], + "evidence": "5 steps at 0.15 m rise / 0.34 m tread (assumption; photos show 4-5, the old notes said 8)" + }, + { + "object": "Hero_Banner_x3", + "dims_m": [ + 1.55, + 0.06, + 5.4 + ], + "evidence": "photographs: JIMMY KIMMEL LIVE banners on the column line" + }, + { + "object": "Hero_Window_x10", + "dims_m": [ + 2.0, + 0.08, + 1.9 + ], + "evidence": "photographs: window bands behind the colonnade" + } + ] +} \ No newline at end of file diff --git a/Game/GigLine/Content/Hollywood/DemoMap.umap b/Game/GigLine/Content/Hollywood/DemoMap.umap new file mode 100644 index 0000000..5c9f7bf --- /dev/null +++ b/Game/GigLine/Content/Hollywood/DemoMap.umap @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06a972a5caa2653c11926ae4992c8d43d2ec7312433ef7ed13fd28f6999372e8 +size 8413 diff --git a/Game/GigLine/Content/Hollywood/ElCapitan/M_Facade_Banner.uasset b/Game/GigLine/Content/Hollywood/ElCapitan/M_Facade_Banner.uasset new file mode 100644 index 0000000..ccfd89f --- /dev/null +++ b/Game/GigLine/Content/Hollywood/ElCapitan/M_Facade_Banner.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:643f9d1ee9ccaafab5e38fe201b5e96b6a0fe3e12142072e520c931b7f461740 +size 56660 diff --git a/Game/GigLine/Content/Hollywood/ElCapitan/M_Facade_GlassDark.uasset b/Game/GigLine/Content/Hollywood/ElCapitan/M_Facade_GlassDark.uasset new file mode 100644 index 0000000..b243860 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/ElCapitan/M_Facade_GlassDark.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbd6dd990a3d39af901d6c9b0d695ca5a1064f10666361e32e1f1f8e54caaa67 +size 56708 diff --git a/Game/GigLine/Content/Hollywood/ElCapitan/M_Facade_Stone.uasset b/Game/GigLine/Content/Hollywood/ElCapitan/M_Facade_Stone.uasset new file mode 100644 index 0000000..1add959 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/ElCapitan/M_Facade_Stone.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b0ce1c87b5c6196f5485f4b7072bbf18cdb9e8a3e10793f88c4010fe74133da +size 56644 diff --git a/Game/GigLine/Content/Hollywood/ElCapitan/M_Facade_StoneShadow.uasset b/Game/GigLine/Content/Hollywood/ElCapitan/M_Facade_StoneShadow.uasset new file mode 100644 index 0000000..2875a66 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/ElCapitan/M_Facade_StoneShadow.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d62f8f568b0bc9d15506d4cb0367ac28f337cadb5375d67e1112aeda08effcf1 +size 56740 diff --git a/Game/GigLine/Content/Hollywood/ElCapitan/SM_HeroFacade.uasset b/Game/GigLine/Content/Hollywood/ElCapitan/SM_HeroFacade.uasset new file mode 100644 index 0000000..25a563e --- /dev/null +++ b/Game/GigLine/Content/Hollywood/ElCapitan/SM_HeroFacade.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2cf4addfe8eb255a98ca93f56f102858ecec4deeea3fb4e51e85e98417c2e602 +size 113730 diff --git a/Game/GigLine/Content/Test/ReviewMap.umap b/Game/GigLine/Content/Test/ReviewMap.umap index 1833ad6..d661444 100644 --- a/Game/GigLine/Content/Test/ReviewMap.umap +++ b/Game/GigLine/Content/Test/ReviewMap.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d28cebbe7ab99b8d2de3f8f1c6d49bac2dc7c65a32bd39871b8f4c3e6967e152 -size 22566 +oid sha256:1f1611b2f0bce1874b08e54a43b14b85ca667e83b0e7b0267802417052ac8f32 +size 24198 diff --git a/ROADMAP.md b/ROADMAP.md index b4f37e3..34714ac 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -52,12 +52,12 @@ Status detail in [Docs/setup.md](Docs/setup.md); pins in [Docs/toolchain.md](Doc - [-] Import OSM blockouts through Blosm; clean and detail them in Blender. Imported 2026-11-06 (`Art/blender/blockout/blockout-2026-11-06.blend`, 86 meshes, scale 1:1 against OSM, re-framed into EPSG:26911 with a 0.0 cm affine residual); cleaning and detailing not started. - [-] Preserve geographic coordinates in metadata; define explicit meter→centimeter conversion to local Unreal coordinates. The frame is defined (`Geo/manifests/local-origin.md`: EPSG:26911, origin Hollywood × Highland, X east / Y north / Z up) and the ×100 conversion is now **verified** by the 2026-11-07 scale test, with the axis mapping recorded (north realizes as UE −Y). Left: build the production export preset for the actual site meshes using these settings. -### Asset production +### Asset production - active - gig-line-asset-production - [x] Establish a consistent scale test, named export presets, and repeatable import settings. Scale test = the three-cube calibration scene (`Art/blender/scaletest/`, regenerable via `Tools/scale_test_export.py`); named export preset `ue_metric_v1` (FBX settings in that script); named import preset `ue_metric_v1` (`FbxImportUI` settings in `Tools/ue_scale_test_import.py`). Both scripts are the templates for production asset export/import. - [-] Collect reference photography for manual façade work. Local only, never committed: `Art/reference/` is gitignored. **72 images in eleven folders**: hero façades to 4k, the El Capitan Theatre exterior series, premiere-night staging, Ovation Hollywood, the north side, and the Lee Drug neighbour. Gaps largely closed by the Flickr/web-search pass: the hero entrance is now documented (three portals with individual short flights — the inherited "eight steps, 12 m wide" is overturned), and the 6806–6822 retail run appears in an oblique 2013 frame. Still missing: a clean isolated frame of the door recess and a night frame of 6840 itself. - [x] Download reference photography for the two north-side buildings inside the 50 m buffer — TCL Chinese Theatre (OSM way 424105908, 6925 Hollywood Blvd) and Madame Tussaud's (way 424105889, 6933 Hollywood Blvd). Done 2026-11-06 with `Tools/commons_fetch.py` against the Wikimedia Commons API: eleven frames in the local `Art/reference/north-chinese/` and `Art/reference/north-tussauds/`, provenance in each `sidecar.json`, nothing committed. -- [ ] Model hero façade for the El Capitan Entertainment Centre site. +- [-] Model hero façade for the El Capitan Entertainment Centre site. **First pass built 2026-11-07** (`Tools/hero_facade_build.py` → `Art/blender/hero/hero-facade-2026-11-07.blend` + JSON part report with evidence per part → `Tools/ue_hero_facade_import.py` → `/Game/Hollywood/ElCapitan/SM_HeroFacade`, 1892 tris, 4 materials, placed on the measured building line). Six columns at 4.96 m over the 25.75 m frontage, plinth, three portal flights (5 risers, not the old 8), entablature 2.2 m, parapet 1.6 m with motto band and medallions. **Screenshot review pending** (`Screenshots/review-04`, `review-05`). Next pass: portal recesses read from the street, banner and window placement, then materials. - [ ] Model surrounding block frontage. - [ ] Model simplified background buildings directly across Hollywood Boulevard. - [-] Build sidewalks, roads, and collision meshes. Terrain, road, sidewalks and kerbs exist (`Art/blender/terrain/`, `street/`) and are assembled in `site/site-2026-11-06.blend`; collision and details (crossing markings, kerb radii, aprons) are not built. Ground must follow the DEM: 3.94 m fall along the block, ~2 % crown between the gutters at ±10–11 m from the centreline, north sidewalk higher than south. Kerb height is an unresolved assumption — the DEM does not resolve it (`Geo/verification/2026-11-06-cross-sections.md`). diff --git a/Screenshots/README.md b/Screenshots/README.md index 74355d4..f7a89c0 100644 --- a/Screenshots/README.md +++ b/Screenshots/README.md @@ -65,6 +65,38 @@ judged interactively. Launch the editor, press Play on `Test/ReviewMap`, and confirm W/A/S/D move in the expected directions, the mouse looks around with no acceleration, and Space jumps once per press. +## review-04-hero-facade-front.png — generated 2026-11-07, approval: ☐ pending + +![Straight-on three-quarter view of the hero facade blockout: six fluted columns on pedestals under a plain entablature, parapet band above, plinth below, standing against a dark floor](review-04-hero-facade-front.png) + +New: the **hero façade blockout** for 6840 Hollywood Blvd — six columns at +4.96 m centres across the measured 25.75 m frontage, entablature 2.2 m, +parapet 1.6 m, plinth and three portal flights. Straight on from the north +sidewalk, looking south. + +What it establishes: frontage width, column rhythm and the vertical stack +(plinth → colonnade → entablature → parapet) all read as the photographs do +in silhouette. + +What it does **not** have yet: the banners, windows, portal recesses and motto +band are modelled but sit *behind* the colonnade, so from this distance the +elevation reads as a plain white mass. The materials are flat colours from +Blender, not look-developed. Do not judge surfaces here — that is the +materials pass. + +## review-05-hero-facade-oblique.png — generated 2026-11-07, approval: ☐ pending + +![Oblique view from the north-west showing the facade's volume, the colonnade in depth, and the review scene's small floor and cubes far to the left](review-05-hero-facade-oblique.png) + +Oblique from the north-west. Shows the building's depth (12 m modelled body), +the column line standing proud of the mass, and the scale relative to the +review floor and character far to the left. + +**Known issue, recorded rather than hidden:** the façade stands on the review +floor at Z 0 but is 25 m away from the original review scene, so it appears to +float over the horizon band. That is the review scene's limitation, not the +asset's; the real map will have the terrain under it. + ## What to look for - **Character**: proportions, bind pose, skin material readability. diff --git a/Tools/hero_facade_build.py b/Tools/hero_facade_build.py new file mode 100644 index 0000000..82952b4 --- /dev/null +++ b/Tools/hero_facade_build.py @@ -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() diff --git a/Tools/ue_build_review_map.py b/Tools/ue_build_review_map.py index 691201c..c8c778f 100644 --- a/Tools/ue_build_review_map.py +++ b/Tools/ue_build_review_map.py @@ -94,6 +94,23 @@ try: c.static_mesh_component.set_static_mesh(m) log(f"staged: {name} at ({x}, 600, 50)") + # Hero facade, if the import has run: two views a reviewer needs — straight on + # from the north sidewalk, and the oblique street view from further west. + hero = unreal.EditorAssetLibrary.load_asset("/Game/Hollywood/ElCapitan/SM_HeroFacade") + # The imported mesh spans Y -7..+1250 cm in local space (front plane at -7, the + # body behind it) and is centred on X. To put the front plane on the block's + # building line the actor goes at Y = building line + 7 cm. + hero_x = -12024.5 + 1302.0 + hero_y = -1450.0 + 7.0 + if hero: + actor = spawn(unreal.StaticMeshActor, unreal.Vector(hero_x, hero_y, 0.0), "Hero_Facade") + if actor: + actor.static_mesh_component.set_static_mesh(hero) + actor.static_mesh_component.set_mobility(unreal.ComponentMobility.STATIC) + log("staged: hero facade at the measured building line") + else: + log("MISSING: /Game/Hollywood/ElCapitan/SM_HeroFacade (run Tools/ue_hero_facade_import.py)") + shots = spawn(unreal.GigLineShotActor, unreal.Vector(0, 0, 300), "ShotSequencer") if shots: shots.set_editor_property("shot_locations", [ @@ -103,6 +120,12 @@ try: # first-person eye height, just south-east of the character: this is the # view a player has at spawn, capsule and camera stack in frame unreal.Vector(120, -260, 165), + # straight on from the north sidewalk at eye height. No camera may sit + # inside the facade body (Y -1443..+250 cm), so both stand north of the + # front plane and look south into the colonnade. + unreal.Vector(hero_x + 200, -3200, 420), + # oblique from the north-west, reading the colonnade in depth + unreal.Vector(hero_x - 2200, -3400, 620), ]) shots.set_editor_property("shot_rotations", [ unreal.MathLibrary.find_look_at_rotation( @@ -113,11 +136,16 @@ try: unreal.Vector(200, 100, 200), unreal.Vector(0, 600, 50)), unreal.MathLibrary.find_look_at_rotation( unreal.Vector(120, -260, 165), unreal.Vector(-200, 600, 90)), + unreal.MathLibrary.find_look_at_rotation( + unreal.Vector(hero_x + 200, -3200, 420), unreal.Vector(hero_x, hero_y + 400, 760)), + unreal.MathLibrary.find_look_at_rotation( + unreal.Vector(hero_x - 2200, -3400, 620), unreal.Vector(hero_x + 300, hero_y, 700)), ]) shots.set_editor_property("shot_names", - ["overview", "character", "scale-cubes", "first-person"]) + ["overview", "character", "scale-cubes", "first-person", + "hero-facade-front", "hero-facade-oblique"]) shots.set_editor_property("quit_when_done", True) - log("staged: ShotSequencer with 4 cameras") + log("staged: ShotSequencer with 6 cameras") except Exception as exc: # noqa log(f"staging failed: {exc!r}") diff --git a/Tools/ue_hero_facade_import.py b/Tools/ue_hero_facade_import.py new file mode 100644 index 0000000..455aab0 --- /dev/null +++ b/Tools/ue_hero_facade_import.py @@ -0,0 +1,106 @@ +# Imports the hero façade FBX into Unreal with the named preset `ue_metric_v1` +# and places it at the measured site position. +# +# /Engine/Binaries/Linux/UnrealEditor-Cmd -run=pythonscript \ +# -script=/Tools/ue_hero_facade_import.py -nullrhi -unattended -nopause -stdout +# +# Placement follows Geo/manifests/local-origin.md and the blockout frame: +# project X = UTM easting - 376515.389 (east +) +# project Y = -(UTM northing - 3774224.083) in Unreal, because the FBX preset +# maps Blender +Y (north) to Unreal -Y and Blender +X to Unreal +X. The building +# line of the hero block sits at Unreal Y = -1450 cm (14.5 m south of the road +# centreline, from the street profile), and the hero's east edge at +# X = -12024.5 cm (projected control point in local-origin.md). + +import unreal + +FBX = "/tmp/gigline/hero-facade.fbx" +DEST = "/Game/Hollywood/ElCapitan" +ACTOR_LABEL = "Hero_Facade" +LEVEL = "/Game/Hollywood/DemoMap" + +# Unreal centimetres: south-east corner of the façade, on the building line. +PLACEMENT_CM = (-12024.5, -1450.0, 0.0) + +RESULT = "/tmp/gigline/hero-facade-import.txt" +lines = [] + + +def log(msg): + lines.append(str(msg)) + + +def import_fbx(): + task = unreal.AssetImportTask() + task.filename = FBX + task.destination_path = DEST + task.destination_name = "SM_HeroFacade" + task.automated = True + task.save = True + task.replace_existing = True + + ui = unreal.FbxImportUI() + ui.import_mesh = True + ui.import_materials = True + ui.import_textures = False + ui.import_as_skeletal = False + smd = ui.static_mesh_import_data + smd.import_uniform_scale = 1.0 + smd.combine_meshes = True + smd.convert_scene_unit = False + smd.force_front_x_axis = False + smd.normal_import_method = unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS + task.options = ui + + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True) + + +def measure_and_place(): + asset = unreal.EditorAssetLibrary.load_asset(f"{DEST}/SM_HeroFacade") + if asset is None: + log(f"MISSING {DEST}/SM_HeroFacade") + return + box = asset.get_bounding_box() + mn, mx = box.min, box.max + log(f"SM_HeroFacade bounds cm: size=({mx.x - mn.x:.1f}, {mx.y - mn.y:.1f}, {mx.z - mn.z:.1f}) " + f"min=({mn.x:.1f}, {mn.y:.1f}, {mn.z:.1f})") + # StaticMaterial is a struct wrapper: the material is its material_interface. + names = [] + for sm in asset.get_editor_property("static_materials"): + mat = sm.get_editor_property("material_interface") + names.append(mat.get_name() if mat else "None") + log(f"materials: {names}") + + # Place on the demo map if it exists; otherwise create it from the empty level. + if unreal.EditorAssetLibrary.does_asset_exist(LEVEL): + unreal.EditorLevelLibrary.load_level(LEVEL) + else: + if not unreal.EditorLevelLibrary.new_level(LEVEL): + log(f"could not create {LEVEL}") + return + for actor in unreal.EditorLevelLibrary.get_all_level_actors(): + if actor.get_actor_label() == ACTOR_LABEL: + unreal.EditorLevelLibrary.destroy_actor(actor) + actor = unreal.EditorLevelLibrary.spawn_actor_from_class( + unreal.StaticMeshActor, unreal.Vector(*PLACEMENT_CM)) + actor.set_actor_label(ACTOR_LABEL) + comp = actor.static_mesh_component + comp.set_static_mesh(asset) + comp.set_mobility(unreal.ComponentMobility.STATIC) + log(f"placed {ACTOR_LABEL} at {PLACEMENT_CM} (cm) on {LEVEL}") + saved = unreal.EditorLoadingAndSavingUtils.save_map( + unreal.EditorLevelLibrary.get_editor_world(), LEVEL) + log(f"save_map {LEVEL}: {saved}") + + +try: + import_fbx() + measure_and_place() +except Exception as exc: # noqa: BLE001 + import traceback + log(f"FAILED: {type(exc).__name__}: {exc}") + log(traceback.format_exc()) + +with open(RESULT, "w") as handle: + handle.write("\n".join(lines) + "\n")