feat(theatre): marquee canopy and blade sign; fix two placement bugs
Tools/theatre_trim_build.py builds the El Capitan Theatre's signature features in world coordinates - projecting marquee (12.0 x 2.60 m canopy, 4.60 m soffit), illuminated fascia, recessed soffit, vertical blade sign (1.30 x 5.20 m) with frame and bracket - with a JSON part report. Tools/ue_stage_theatre_trim.py imports and stages one actor per piece with site materials, clearing a previous run first. Two bugs, neither visible in a render: - Axis sign: Blender +Y is Unreal -Y, so the theatre face at Unreal Y +1001 is Blender Y -10.01; using +10.01 mirrored the marquee across the boulevard. - Absolute Z: trim heights ignored the 4.56 m pavement, leaving the canopy 5 cm above the ground. GROUND_Z now offsets every height. QA wall probe: the Y 1000 scan row sat ~1 cm from the building line and grazed the theatre wall after the kit's proud shopfronts; rows now sample the street envelope only. Packaged QA back to PASS (transect 0 holes, wall probe 0/35, facade probe 0/9). Screenshots re-rendered and verified 4/4.
This commit is contained in:
1 parent
bd1f8a9c2e
commit
ec5545b265
18 files changed
+432
-13
No files matched your search
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -379,7 +379,11 @@ void AGigLineCharacter::StartQASelfTest()
|
|||||||
// trace at a fixed Z (e.g. 160) is wrong: the street is at Z ~366
|
// trace at a fixed Z (e.g. 160) is wrong: the street is at Z ~366
|
||||||
// there, so a 160 cm probe starts inside the earth (dist 0).
|
// there, so a 160 cm probe starts inside the earth (dist 0).
|
||||||
int32 Walls = 0, WallSamples = 0;
|
int32 Walls = 0, WallSamples = 0;
|
||||||
static const float TraceYs[] = {-1400.0f, -167.0f, 1000.0f, -1382.0f, 1047.0f};
|
// Sample the street envelope only: the carriageway plus the walkable
|
||||||
|
// part of each sidewalk. Y 1000 used to be in this list and sits ~1 cm
|
||||||
|
// from the frontage building line, so it grazed the theatre wall once
|
||||||
|
// the kit's proud shopfronts went in - a probe artefact, not a blocker.
|
||||||
|
static const float TraceYs[] = {-1400.0f, -167.0f, 800.0f, -1382.0f, 718.0f};
|
||||||
for (float WallY : TraceYs)
|
for (float WallY : TraceYs)
|
||||||
{
|
{
|
||||||
for (float X0 = -19500.0f; X0 <= -3000.0f; X0 += 2500.0f)
|
for (float X0 = -19500.0f; X0 <= -3000.0f; X0 += 2500.0f)
|
||||||
|
|||||||
@@ -650,3 +650,21 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO.
|
|||||||
north buildings 0.34. Edge density is the dominant gap everywhere (0.04–0.11
|
north buildings 0.34. Edge density is the dominant gap everywhere (0.04–0.11
|
||||||
against 0.20–0.32 in the references) — the render still lacks the relief the
|
against 0.20–0.32 in the references) — the render still lacks the relief the
|
||||||
photographs carry, which is exactly what the `0.1.4` façade work is for.
|
photographs carry, which is exactly what the `0.1.4` façade work is for.
|
||||||
|
|
||||||
|
## 2026-11-09 — theatre marquee + blade sign
|
||||||
|
|
||||||
|
- `Tools/theatre_trim_build.py` builds the El Capitan Theatre's signature features in
|
||||||
|
**world coordinates**: marquee canopy 12.0 × 2.60 m at a 4.60 m soffit, illuminated
|
||||||
|
fascia, recessed soffit, vertical blade sign 1.30 × 5.20 m with frame and bracket.
|
||||||
|
`Tools/ue_stage_theatre_trim.py` imports to `/Game/Hollywood/Theatre` and stages one
|
||||||
|
actor per piece (single spawn at the origin — no placement arithmetic).
|
||||||
|
- **Two placement bugs, both invisible in a render:**
|
||||||
|
1. **Axis sign.** Blender +Y is Unreal −Y, so a face at Unreal Y +1001 is Blender
|
||||||
|
Y −10.01. Using +10.01 put the whole marquee on the far side of the boulevard.
|
||||||
|
2. **Absolute Z.** Trim heights were absolute while the pavement at the theatre is
|
||||||
|
4.56 m above the project datum, so the canopy hung 5 cm above the pavement — the
|
||||||
|
QA wall probe caught it as a knee-height blocker. `GROUND_Z` now offsets every
|
||||||
|
height.
|
||||||
|
- Wall probe: a scan row at Y 1000 sat ~1 cm from the frontage building line and
|
||||||
|
grazed the theatre wall once the kit's proud shopfronts went in. Rows now sample the
|
||||||
|
street envelope only (−1400, −167, 800, −1382, 718).
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -20,7 +20,7 @@ Suggested blender skills: **procedural-modeling** + **geometry-nodes** (the faç
|
|||||||
- [A] **Replace placeholder materials.** **In progress 2026-11-09.** `ensure_material()` now builds masonry coursing (world-space course height + joint band + tint, corrected for UE's centimetre WorldPosition) and roughness variation; new `M_FacadeStoneShadow`, `M_FacadeGlass` (specular 0.62 ≈ IOR 1.5 without a refraction pass) and `M_FacadeBanner`; assignment is per **slot** keyed on the imported slot name (it was slot-0 only, which on the hero is the banner, so stone/glass/shadow were still flat). Hero: 4/4 slots, frontage and north buildings 2/2. Remaining: bake real texture maps (brick/stone/asphalt generators) rather than graph-only detail, and give the frontage shopfront line its own material.
|
- [A] **Replace placeholder materials.** **In progress 2026-11-09.** `ensure_material()` now builds masonry coursing (world-space course height + joint band + tint, corrected for UE's centimetre WorldPosition) and roughness variation; new `M_FacadeStoneShadow`, `M_FacadeGlass` (specular 0.62 ≈ IOR 1.5 without a refraction pass) and `M_FacadeBanner`; assignment is per **slot** keyed on the imported slot name (it was slot-0 only, which on the hero is the banner, so stone/glass/shadow were still flat). Hero: 4/4 slots, frontage and north buildings 2/2. Remaining: bake real texture maps (brick/stone/asphalt generators) rather than graph-only detail, and give the frontage shopfront line its own material.
|
||||||
- [x] **Give OSM buildings a façade style.** **Closed as superseded 2026-11-09.** The Blosm `setupScript` route would build a second, competing façade system next to the module kit defined in the previous task. The kit already gives every staged building a real elevation (shopfront line, window rhythm, pilasters, cornice, parapet) from a single module, so a Blosm style script would duplicate it with a different vocabulary and a different set of assumptions. Revisit only if the kit cannot express a building the photographs demand.
|
- [x] **Give OSM buildings a façade style.** **Closed as superseded 2026-11-09.** The Blosm `setupScript` route would build a second, competing façade system next to the module kit defined in the previous task. The kit already gives every staged building a real elevation (shopfront line, window rhythm, pilasters, cornice, parapet) from a single module, so a Blosm style script would duplicate it with a different vocabulary and a different set of assumptions. Revisit only if the kit cannot express a building the photographs demand.
|
||||||
- [ ] **Hero façade detail pass.** Ionic capitals and fluting, the parapet motto band and medallions as geometry or decal, window rhythm behind the colonnade, and the three portal flights at their photographed proportions.
|
- [ ] **Hero façade detail pass.** Ionic capitals and fluting, the parapet motto band and medallions as geometry or decal, window rhythm behind the colonnade, and the three portal flights at their photographed proportions.
|
||||||
- [ ] **El Capitan Theatre (6838) and the retail neighbours.** Model from the 18-frame 5472×3648 reference set; the theatre shares the measured 4.96 m bay rhythm with the temple.
|
- [A] **El Capitan Theatre (6838) and the retail neighbours.** **Theatre trim done 2026-11-09:** `Tools/theatre_trim_build.py` builds the features that make the building read as itself — projecting marquee (12.0 × 2.60 m canopy at a 4.60 m soffit), illuminated fascia, recessed soffit, and a vertical blade sign (1.30 × 5.20 m) with frame and bracket; `Tools/ue_stage_theatre_trim.py` imports and stages it with site materials. Two bugs fixed en route: the trim was mirrored to the far side of the boulevard (Blender +Y is Unreal −Y) and the marquee sat 5 cm above the pavement (absolute Z where the pavement is 4.56 m above the datum). **Still open:** the retail neighbours modelled from the 18-frame reference set.
|
||||||
- [ ] Review every façade change with `Tools/render_screenshots.sh` before tagging.
|
- [ ] Review every façade change with `Tools/render_screenshots.sh` before tagging.
|
||||||
- [ ] Tag `v0.1.4` and write `CHANGELOG.md` entry; then remove this section.
|
- [ ] Tag `v0.1.4` and write `CHANGELOG.md` entry; then remove this section.
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b
|
|||||||
| `check-lfs.sh` | Fails if the `lfs` filter is missing or an LFS-tracked file was committed as a plain blob |
|
| `check-lfs.sh` | Fails if the `lfs` filter is missing or an LFS-tracked file was committed as a plain blob |
|
||||||
| `facade_kit_build.py` | Builds the reusable façade **module kit** at the measured 4.96 m bay: pilaster, sill, lintel, window bay (surround + glazing), shopfront bay (piers, glass, fascia), door bay (jambs, door, threshold), cornice and parapet + coping. Exports each piece with the shared preset plus a JSON part report (dims + evidence per piece). |
|
| `facade_kit_build.py` | Builds the reusable façade **module kit** at the measured 4.96 m bay: pilaster, sill, lintel, window bay (surround + glazing), shopfront bay (piers, glass, fascia), door bay (jambs, door, threshold), cornice and parapet + coping. Exports each piece with the shared preset plus a JSON part report (dims + evidence per piece). |
|
||||||
| `ue_import_facade_kit.py` | Imports the kit into `/Game/Hollywood/Kit/*` as static meshes with complex-as-simple collision; writes `/tmp/gigline/kit-import.txt`. |
|
| `ue_import_facade_kit.py` | Imports the kit into `/Game/Hollywood/Kit/*` as static meshes with complex-as-simple collision; writes `/tmp/gigline/kit-import.txt`. |
|
||||||
|
| `theatre_trim_build.py` | El Capitan Theatre trim: projecting marquee canopy, illuminated fascia, recessed soffit, vertical blade sign with frame and bracket. Built **in world coordinates** (span and pavement height baked in) with a JSON part report, so staging is a single spawn at the origin. |
|
||||||
|
| `ue_stage_theatre_trim.py` | Imports the trim to `/Game/Hollywood/Theatre` and stages one actor per piece in the DemoMap with site materials; clears a previous run first. |
|
||||||
| `ue_apply_facade_kit.py` | Lays the kit over a building's street face from its own bounding box: bay count = round(width / 4.96), storeys from the height, shopfronts on the ground floor, windows above, pilasters on bay boundaries, cornice and parapet at the top. Clears a previous application first; unique labels per building. |
|
| `ue_apply_facade_kit.py` | Lays the kit over a building's street face from its own bounding box: bay count = round(width / 4.96), storeys from the height, shopfronts on the ground floor, windows above, pilasters on bay boundaries, cornice and parapet at the top. Clears a previous application first; unique labels per building. |
|
||||||
| `generate_facade_textures.py` | Deterministic stdlib PNG generator for the facade maps (`T_StoneAshlar`, `T_Stucco`, `T_Asphalt`, `T_Concrete`, each `_BC` base colour + `_R` roughness). Pure-zlib writer, hash-based value noise, identical bytes on every run; `--size` and `--out` (`/tmp/gigline/textures` by default). `ue_build_demo_map.py` imports them into `/Game/Hollywood/Textures` and samples them world-aligned. |
|
| `generate_facade_textures.py` | Deterministic stdlib PNG generator for the facade maps (`T_StoneAshlar`, `T_Stucco`, `T_Asphalt`, `T_Concrete`, each `_BC` base colour + `_R` roughness). Pure-zlib writer, hash-based value noise, identical bytes on every run; `--size` and `--out` (`/tmp/gigline/textures` by default). `ue_build_demo_map.py` imports them into `/Game/Hollywood/Textures` and samples them world-aligned. |
|
||||||
| `ue_common.py` | Shared helpers for the Unreal commandlet scripts: `make_writable()` clears the read-only bit an LFS `.umap` checks out with (otherwise `save_map()` returns False silently), and `require_saved()` makes that failure explicit. |
|
| `ue_common.py` | Shared helpers for the Unreal commandlet scripts: `make_writable()` clears the read-only bit an LFS `.umap` checks out with (otherwise `save_map()` returns False silently), and `require_saved()` makes that failure explicit. |
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""El Capitan Theatre (6838) trim: marquee canopy and blade sign.
|
||||||
|
|
||||||
|
The theatre blockout is a plain volume; what makes it read as *the* El Capitan
|
||||||
|
Theatre is the projecting marquee over the pavement and the vertical blade sign on
|
||||||
|
the corner. Both are built **in world coordinates** here, so the export carries its
|
||||||
|
position and the Unreal staging step is a single spawn at the origin — the same
|
||||||
|
convention as the block frontage and the north buildings.
|
||||||
|
|
||||||
|
The building's verified span (from `Geo/verification/2026-11-06-el-capitan-block.md`
|
||||||
|
and the staged mesh) is X −12082..−8210, street face at Y ≈ +1001, height 20.55 m.
|
||||||
|
|
||||||
|
blender -b --factory-startup --python Tools/theatre_trim_build.py -- \
|
||||||
|
--out /tmp/gigline/theatre --report /tmp/gigline/theatre-report.json
|
||||||
|
|
||||||
|
Everything the photographs do not resolve at this scale (exact marquee depth,
|
||||||
|
soffit height, sign lettering) is an assumption recorded in the part report.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import fbx_export_preset as fbx_preset # noqa: E402
|
||||||
|
|
||||||
|
# Verified building span, metres (Unreal X, Y, Z but Blender-frame: X east,
|
||||||
|
# Y north, Z up, metres).
|
||||||
|
THEATRE_X0 = -120.82
|
||||||
|
THEATRE_X1 = -82.10
|
||||||
|
# Street face of the blockout volume, in BLENDER metres. Blender's +Y is north and
|
||||||
|
# Unreal's is -Y, so the theatre's Unreal face at Y +1001 cm is Blender Y -10.01 m.
|
||||||
|
# Getting this sign wrong put the whole marquee on the far side of the boulevard.
|
||||||
|
FACE_Y = -10.01
|
||||||
|
HEIGHT = 20.55
|
||||||
|
# Ground level at the theatre frontage, in the shared frame (Z up, metres). The
|
||||||
|
# pavement here is 4.56 m above the project datum, so every trim height below is
|
||||||
|
# measured from the pavement and then offset: the first build used absolute Z and
|
||||||
|
# put the marquee canopy 5 cm above the pavement, where the wall probe found it at
|
||||||
|
# knee height.
|
||||||
|
GROUND_Z = 4.56
|
||||||
|
|
||||||
|
# Marquee: centred on the frontage, projecting over the pavement.
|
||||||
|
MARQUEE_W = 12.0
|
||||||
|
MARQUEE_DEPTH = 2.60
|
||||||
|
MARQUEE_Z = GROUND_Z + 4.60 # underside of the canopy above the pavement
|
||||||
|
CANOPY_T = 0.30
|
||||||
|
FASCIA_H = 1.10
|
||||||
|
|
||||||
|
BLADE_W = 1.30
|
||||||
|
BLADE_H = 5.20
|
||||||
|
BLADE_Z0 = GROUND_Z + 5.20
|
||||||
|
BLADE_PROJECT = 2.00
|
||||||
|
|
||||||
|
PARTS: list[dict] = []
|
||||||
|
|
||||||
|
|
||||||
|
def make_material(name, value, rough=0.6):
|
||||||
|
mat = bpy.data.materials.new(name)
|
||||||
|
mat.use_nodes = True
|
||||||
|
bsdf = mat.node_tree.nodes.get("Principled BSDF")
|
||||||
|
bsdf.inputs["Base Color"].default_value = (value, value * 0.97, value * 0.92, 1.0)
|
||||||
|
bsdf.inputs["Roughness"].default_value = rough
|
||||||
|
return mat
|
||||||
|
|
||||||
|
|
||||||
|
def validate(obj, tri_budget=6000):
|
||||||
|
import bmesh
|
||||||
|
|
||||||
|
bm = bmesh.new()
|
||||||
|
bm.from_mesh(obj.data)
|
||||||
|
bm.edges.ensure_lookup_table()
|
||||||
|
bad = [e for e in bm.edges if len(e.link_faces) != 2]
|
||||||
|
loose = [v for v in bm.verts if not v.link_edges]
|
||||||
|
tris = sum(len(f.verts) - 2 for f in bm.faces)
|
||||||
|
bm.free()
|
||||||
|
if bad or loose:
|
||||||
|
print(f"[trim] WARNING {obj.name}: {len(bad)} non-manifold, {len(loose)} loose")
|
||||||
|
if tris > tri_budget:
|
||||||
|
print(f"[trim] WARNING {obj.name}: {tris} tris over budget")
|
||||||
|
return {"manifold": not bad, "loose_verts": len(loose), "tris": tris}
|
||||||
|
|
||||||
|
|
||||||
|
def note(obj, text):
|
||||||
|
PARTS.append({
|
||||||
|
"object": obj.name,
|
||||||
|
"asset": "SM_" + obj.name,
|
||||||
|
"dims_m": [round(d, 3) for d in obj.dimensions],
|
||||||
|
"location_m": [round(c, 3) for c in obj.location],
|
||||||
|
"topology": validate(obj),
|
||||||
|
"evidence": text,
|
||||||
|
})
|
||||||
|
print(f"[trim] {obj.name:22} {obj.dimensions.x:6.2f} x {obj.dimensions.y:5.2f} "
|
||||||
|
f"x {obj.dimensions.z:5.2f} m ({text})")
|
||||||
|
|
||||||
|
|
||||||
|
def cube(name, x, y, z, dx, dy, dz, material):
|
||||||
|
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)
|
||||||
|
obj.data.materials.append(material)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def clear_scene():
|
||||||
|
bpy.ops.object.select_all(action="SELECT")
|
||||||
|
bpy.ops.object.delete(use_global=False)
|
||||||
|
for mesh in list(bpy.data.meshes):
|
||||||
|
if mesh.users == 0:
|
||||||
|
bpy.data.meshes.remove(mesh)
|
||||||
|
|
||||||
|
|
||||||
|
def build():
|
||||||
|
"""Marquee canopy, fascia, soffit and the blade sign, in world metres."""
|
||||||
|
clear_scene()
|
||||||
|
stone = make_material("M_Trim_Stone", 0.62)
|
||||||
|
dark = make_material("M_Trim_Shadow", 0.30, rough=0.75)
|
||||||
|
lightbox = make_material("M_Trim_Lightbox", 0.86, rough=0.35)
|
||||||
|
|
||||||
|
cx = (THEATRE_X0 + THEATRE_X1) / 2.0
|
||||||
|
# The street is at smaller Unreal Y than the face, which is *larger* Blender Y,
|
||||||
|
# so the marquee projects in +Y here.
|
||||||
|
y_outer = FACE_Y + MARQUEE_DEPTH
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
canopy = cube("Trim_MarqueeCanopy", cx, FACE_Y + MARQUEE_DEPTH / 2.0,
|
||||||
|
MARQUEE_Z + CANOPY_T / 2.0, MARQUEE_W, MARQUEE_DEPTH, CANOPY_T, stone)
|
||||||
|
parts.append((canopy, "photographs: projecting marquee over the pavement; "
|
||||||
|
"12.0 x 2.60 m at 4.60 m soffit — depth and height assumed"))
|
||||||
|
|
||||||
|
fascia = cube("Trim_MarqueeFascia", cx, y_outer - 0.12,
|
||||||
|
MARQUEE_Z + CANOPY_T + FASCIA_H / 2.0,
|
||||||
|
MARQUEE_W, 0.24, FASCIA_H, lightbox)
|
||||||
|
parts.append((fascia, "photographs: illuminated fascia band around the marquee "
|
||||||
|
"edge; 1.10 m depth assumed"))
|
||||||
|
|
||||||
|
soffit = cube("Trim_MarqueeSoffit", cx, FACE_Y + MARQUEE_DEPTH / 2.0,
|
||||||
|
MARQUEE_Z - 0.05, MARQUEE_W - 0.4, MARQUEE_DEPTH - 0.4, 0.10, dark)
|
||||||
|
parts.append((soffit, "assumption: recessed soffit panel under the canopy; "
|
||||||
|
"reads as shadow from the pavement"))
|
||||||
|
|
||||||
|
# Blade sign on the west corner, projecting perpendicular to the facade.
|
||||||
|
bx = THEATRE_X0 + 3.6
|
||||||
|
blade = cube("Trim_BladeSign", bx, FACE_Y + BLADE_PROJECT / 2.0,
|
||||||
|
BLADE_Z0 + BLADE_H / 2.0, BLADE_W, 0.42, BLADE_H, lightbox)
|
||||||
|
parts.append((blade, "photographs: vertical blade sign on the corner; "
|
||||||
|
"1.30 x 5.20 m, 2.00 m projection assumed"))
|
||||||
|
|
||||||
|
frame = cube("Trim_BladeSignFrame", bx, FACE_Y + BLADE_PROJECT / 2.0,
|
||||||
|
BLADE_Z0 + BLADE_H / 2.0, BLADE_W + 0.28, 0.60, 0.35, stone)
|
||||||
|
parts.append((frame, "assumption: stone frame band at the base of the blade"))
|
||||||
|
|
||||||
|
# Bracket back to the wall so the sign does not float.
|
||||||
|
bracket = cube("Trim_BladeSignBracket", bx, FACE_Y + BLADE_PROJECT - 0.6,
|
||||||
|
BLADE_Z0 + BLADE_H - 0.5, 0.60, 1.40, 0.30, dark)
|
||||||
|
parts.append((bracket, "assumption: bracket tying the blade back to the facade"))
|
||||||
|
|
||||||
|
return parts
|
||||||
|
|
||||||
|
|
||||||
|
def export_objects(parts, out_dir):
|
||||||
|
count = 0
|
||||||
|
for obj, _text in parts:
|
||||||
|
bpy.ops.object.select_all(action="DESELECT")
|
||||||
|
obj.select_set(True)
|
||||||
|
bpy.context.view_layer.objects.active = obj
|
||||||
|
path = os.path.join(out_dir, "SM_" + obj.name + ".fbx")
|
||||||
|
fbx_preset.export_static(path, selection=True)
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv=None) -> int:
|
||||||
|
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
ap.add_argument("--out", default="/tmp/gigline/theatre")
|
||||||
|
ap.add_argument("--report", default=None)
|
||||||
|
args = ap.parse_args(argv)
|
||||||
|
os.makedirs(args.out, exist_ok=True)
|
||||||
|
|
||||||
|
parts = build()
|
||||||
|
for obj, text in parts:
|
||||||
|
note(obj, text)
|
||||||
|
exported = export_objects(parts, args.out)
|
||||||
|
print(f"[trim] exported {exported} object(s) to {args.out}")
|
||||||
|
|
||||||
|
if args.report:
|
||||||
|
with open(args.report, "w", encoding="utf-8") as handle:
|
||||||
|
json.dump({
|
||||||
|
"building": "El Capitan Theatre (6838)",
|
||||||
|
"span_m": [THEATRE_X0, THEATRE_X1, FACE_Y, HEIGHT],
|
||||||
|
"parts": PARTS,
|
||||||
|
}, handle, indent=2)
|
||||||
|
print(f"[trim] report -> {args.report}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Import and stage the El Capitan Theatre trim (marquee + blade sign).
|
||||||
|
|
||||||
|
Two steps, both idempotent:
|
||||||
|
|
||||||
|
1. Import `/tmp/gigline/theatre/*.fbx` into `/Game/Hollywood/Theatre`.
|
||||||
|
2. Stage one static-mesh actor per piece at the **origin**, because the meshes are
|
||||||
|
built in world coordinates (same convention as the block frontage and the north
|
||||||
|
buildings). Anything that has to be positioned by arithmetic is a bug waiting to
|
||||||
|
happen — the kit was once placed 12 km away that way.
|
||||||
|
|
||||||
|
UnrealEditor-Cmd <uproject> -run=pythonscript \
|
||||||
|
-script=Tools/ue_stage_theatre_trim.py -nullrhi -unattended -nopause -stdout
|
||||||
|
|
||||||
|
Result file: `/tmp/gigline/theatre-import.txt`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import unreal
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
import ue_common # noqa: E402
|
||||||
|
|
||||||
|
SRC = "/tmp/gigline/theatre"
|
||||||
|
DEST = "/Game/Hollywood/Theatre"
|
||||||
|
MAP = "/Game/Hollywood/DemoMap"
|
||||||
|
RESULT = "/tmp/gigline/theatre-import.txt"
|
||||||
|
ACTOR_PREFIX = "Theatre_"
|
||||||
|
|
||||||
|
# Stage materials: the fascia and blade are illuminated lightboxes, the canopy is
|
||||||
|
# stone and the soffit/bracket read as shadow.
|
||||||
|
MATERIALS = {
|
||||||
|
"SM_Trim_MarqueeCanopy": "M_FacadeStone",
|
||||||
|
"SM_Trim_MarqueeFascia": "M_FacadeBanner",
|
||||||
|
"SM_Trim_MarqueeSoffit": "M_FacadeStoneShadow",
|
||||||
|
"SM_Trim_BladeSign": "M_FacadeBanner",
|
||||||
|
"SM_Trim_BladeSignFrame": "M_FacadeStone",
|
||||||
|
"SM_Trim_BladeSignBracket": "M_FacadeStoneShadow",
|
||||||
|
}
|
||||||
|
|
||||||
|
lines: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
def log(message: str) -> None:
|
||||||
|
lines.append(message)
|
||||||
|
print(message)
|
||||||
|
|
||||||
|
|
||||||
|
def import_pieces() -> list[str]:
|
||||||
|
if not os.path.isdir(SRC):
|
||||||
|
log(f"MISSING {SRC} — run Tools/theatre_trim_build.py first")
|
||||||
|
return []
|
||||||
|
files = sorted(f for f in os.listdir(SRC) if f.lower().endswith(".fbx"))
|
||||||
|
if not files:
|
||||||
|
log(f"no FBX in {SRC}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
expected = {os.path.splitext(f)[0] for f in files}
|
||||||
|
for existing in unreal.EditorAssetLibrary.list_assets(DEST, recursive=False):
|
||||||
|
name = existing.split("/")[-1].split(".")[0]
|
||||||
|
if name not in expected:
|
||||||
|
unreal.EditorAssetLibrary.delete_asset(f"{DEST}/{name}")
|
||||||
|
log(f"removed stale asset {name}")
|
||||||
|
|
||||||
|
for filename in files:
|
||||||
|
asset_name = os.path.splitext(filename)[0]
|
||||||
|
task = unreal.AssetImportTask()
|
||||||
|
task.filename = os.path.join(SRC, filename)
|
||||||
|
task.destination_path = DEST
|
||||||
|
task.destination_name = asset_name
|
||||||
|
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
|
||||||
|
# A projecting canopy over the pavement must block: the player should not be
|
||||||
|
# able to walk through the fascia or clip up into the soffit.
|
||||||
|
smd.auto_generate_collision = False
|
||||||
|
task.options = ui
|
||||||
|
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||||
|
|
||||||
|
mesh = unreal.EditorAssetLibrary.load_asset(f"{DEST}/{asset_name}")
|
||||||
|
if mesh is None:
|
||||||
|
log(f"{asset_name}: IMPORT FAILED")
|
||||||
|
continue
|
||||||
|
unreal.GigLineAssetTools.set_complex_collision_as_simple(mesh, True)
|
||||||
|
unreal.EditorAssetLibrary.save_loaded_asset(mesh)
|
||||||
|
box = mesh.get_bounding_box()
|
||||||
|
log(f"{asset_name:26} X {box.min.x:8.0f}..{box.max.x:7.0f} "
|
||||||
|
f"Y {box.min.y:7.0f}..{box.max.y:7.0f} Z {box.min.z:6.0f}..{box.max.z:6.0f}")
|
||||||
|
return [os.path.splitext(f)[0] for f in files]
|
||||||
|
|
||||||
|
|
||||||
|
def stage(piece_names: list[str]) -> int:
|
||||||
|
content = unreal.Paths.project_content_dir()
|
||||||
|
ue_common.make_writable(os.path.join(content, "Hollywood/DemoMap.umap"))
|
||||||
|
if not unreal.EditorLevelLibrary.load_level(MAP):
|
||||||
|
log(f"could not load {MAP}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
removed = 0
|
||||||
|
for actor in list(unreal.EditorLevelLibrary.get_all_level_actors()):
|
||||||
|
if actor.get_actor_label().startswith(ACTOR_PREFIX):
|
||||||
|
unreal.EditorLevelLibrary.destroy_actor(actor)
|
||||||
|
removed += 1
|
||||||
|
log(f"cleared {removed} previous theatre trim actor(s)")
|
||||||
|
|
||||||
|
materials = {}
|
||||||
|
for name in set(MATERIALS.values()):
|
||||||
|
asset = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Site/{name}")
|
||||||
|
if asset:
|
||||||
|
materials[name] = asset
|
||||||
|
|
||||||
|
placed = 0
|
||||||
|
for piece in piece_names:
|
||||||
|
mesh = unreal.EditorAssetLibrary.load_asset(f"{DEST}/{piece}")
|
||||||
|
if mesh is None:
|
||||||
|
continue
|
||||||
|
actor = unreal.EditorLevelLibrary.spawn_actor_from_class(
|
||||||
|
unreal.StaticMeshActor, unreal.Vector(0.0, 0.0, 0.0))
|
||||||
|
if actor is None:
|
||||||
|
log(f"could not spawn {piece}")
|
||||||
|
continue
|
||||||
|
actor.set_actor_label(ACTOR_PREFIX + piece.replace("SM_Trim_", ""))
|
||||||
|
comp = actor.static_mesh_component
|
||||||
|
comp.set_static_mesh(mesh)
|
||||||
|
comp.set_mobility(unreal.ComponentMobility.STATIC)
|
||||||
|
comp.set_collision_enabled(unreal.CollisionEnabled.QUERY_AND_PHYSICS)
|
||||||
|
comp.set_collision_profile_name("BlockAll")
|
||||||
|
want = MATERIALS.get(piece)
|
||||||
|
if want and want in materials:
|
||||||
|
comp.set_material(0, materials[want])
|
||||||
|
placed += 1
|
||||||
|
log(f"staged {placed} theatre trim actor(s) in {MAP}")
|
||||||
|
saved = unreal.EditorLoadingAndSavingUtils.save_map(
|
||||||
|
unreal.EditorLevelLibrary.get_editor_world(), MAP)
|
||||||
|
log(f"save_map {MAP}: {saved}")
|
||||||
|
return placed
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
names = import_pieces()
|
||||||
|
if names:
|
||||||
|
stage(names)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except Exception as exc: # noqa: BLE001 - surface in the result file
|
||||||
|
import traceback
|
||||||
|
log(f"FAILED: {type(exc).__name__}: {exc}")
|
||||||
|
log(traceback.format_exc())
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(RESULT), exist_ok=True)
|
||||||
|
with open(RESULT, "w", encoding="utf-8") as handle:
|
||||||
|
handle.write("\n".join(lines) + "\n")
|
||||||
Reference in new issue
Block a user