feat(facades): 0.1.6 retail frontage first pass — Lee Drug and 6806-6820
Models the two retail buildings beside the hero from their own photograph rather than the generic kit: bronze cornice, mural row, lettering band, stepped Deco parapet and rounded corner tower on 6800 Lee Drug; burgundy fascia, pilasters, white-framed windows and banded parapet on 6806-6820. Reference analysis written before any geometry (Art/blender/retail/ref-analysis.md), every unmeasured proportion carried into the part report as an assumption. 6822 Budget is deferred, not guessed: no photograph of it exists and the 6.0 x 5.5 m sliver cannot be read out of the neighbour shot. Pipeline fixes found on the way: - render_screenshots.sh called ./Engine/... for the capture step while every commandlet used the absolute path; set -e killed the script at 127 *after* the maps were rebuilt, so screenshots silently stayed a day old. - the facade kit applies to every Frontage_*/North_* actor, so it pasted its own language over the new modelled facades; it now skips them, and the trim stages as RetailTrim_*. - pure bay arithmetic moved to Tools/_common.py so it is unit-testable without Blender (4 new tests, 82 total pass). Known gaps recorded in Screenshots/README.md, not hidden: the parapet reads as a blank band, murals and lettering do not resolve at review distance, the tower is a plain cylinder, and the review camera crops the east end.
This commit is contained in:
1 parent
4e703fcfd9
commit
2614d259aa
54 files changed
+715
-50
No files matched your search
@@ -17,7 +17,10 @@ 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 |
|
||||
| `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_retail_detail.py` | Imports the two retail trim meshes into `/Game/Hollywood/Frontage` with complex-as-simple collision — the trim decorates a solid the player already collides with, so it must not add snag points. Staged by the demo builder as `RetailTrim_*`. |
|
||||
| `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`. |
|
||||
| `retail_frontage_build.py` | Retail frontage trim for **6800 Lee Drug** and **6806–6820** (0.1.6): bronze cornice, mural row, lettering band, stepped Deco parapet and rounded corner tower on Lee Drug; burgundy fascia, pilasters, white-framed windows and banded parapet on 6806–6820. Built in world coordinates from the blockout bounds, one joined mesh per building, JSON part report with the evidence behind every piece. `6822 Budget` is **not** built — no photograph exists. |
|
||||
| `retail_frontage_build.py` | Retail frontage trim for **6800 Lee Drug** and **6806–6820** (0.1.6): bronze cornice, mural row, lettering band, stepped Deco parapet and rounded corner tower on Lee Drug; burgundy fascia, pilasters, white-framed windows and banded parapet on 6806–6820. Built in world coordinates from the blockout bounds, one joined mesh per building, JSON part report with the evidence behind every piece. `6822 Budget` is **not** built — no photograph exists. |
|
||||
| `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. |
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Shared helpers that must stay importable outside Blender and Unreal.
|
||||
|
||||
Blender-side builders import `bpy` at module scope, so nothing in them can be unit
|
||||
tested. Pure layout arithmetic lives here instead: the builders import it, and
|
||||
`Tools/tests/` imports it without a 3D application.
|
||||
|
||||
Standard library only.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def bay_centres(x0, x1, margin=0.9, bay=2.2):
|
||||
"""Centres of equal bays across a frontage, inset by `margin` at each end.
|
||||
|
||||
Used by the façade kit and the retail frontage so both use one rhythm. The count
|
||||
is the span divided by the bay module and rounded, so a 33 m frontage at a 2.2 m
|
||||
module gets 14 bays rather than one bay per 2.2 m of wall with a stub at the end.
|
||||
|
||||
Returns [] when the margins consume the whole frontage.
|
||||
"""
|
||||
span = (x1 - x0) - 2.0 * margin
|
||||
if span <= 0:
|
||||
return []
|
||||
count = max(1, int(round(span / bay)))
|
||||
step = span / count
|
||||
return [x0 + margin + step * (i + 0.5) for i in range(count)]
|
||||
@@ -39,7 +39,10 @@ run_cmdlet ue_build_review_map.py "5/6 stage the demo shot cameras"
|
||||
tail -3 /tmp/ue-review-map-result.txt 2>/dev/null || true
|
||||
|
||||
echo "== 6/6 capture the demo map =="
|
||||
./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" /Game/Hollywood/DemoMap \
|
||||
# The engine path is absolute: this step runs from the repo, not from $ENGINE, and a
|
||||
# bare ./Engine/... failed with "No such file or directory" while every commandlet
|
||||
# above succeeded through run_cmdlet's cd.
|
||||
"$ENGINE/Engine/Binaries/Linux/UnrealEditor-Cmd" "$PROJ" /Game/Hollywood/DemoMap \
|
||||
-game -windowed -ResX=1920 -ResY=1080 -nopause -nosplash -stdout
|
||||
|
||||
echo "== collect the demo screenshots =="
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Retail frontage trim for 6800 Lee Drug and 6806–6820 (0.1.6).
|
||||
|
||||
Both buildings are bare OSM extrusions with a flat stucco material; what makes them
|
||||
read as *the* retail run beside the hero is the façade layer: the bronze cornice and
|
||||
stepped Deco parapet on Lee Drug, and the pilaster-and-fascia rhythm on 6806–6820.
|
||||
|
||||
The trim is built **in world coordinates** in the shared frame (metres, X east,
|
||||
Y north, Z from the project datum), so the export carries its position and the
|
||||
Unreal staging step is a single spawn at the origin — the same convention as
|
||||
`Tools/theatre_trim_build.py` and `Tools/export_block_frontage.py`.
|
||||
|
||||
blender -b --factory-startup --python Tools/retail_frontage_build.py -- \
|
||||
--out /tmp/gigline/retail --report /tmp/gigline/retail-report.json
|
||||
|
||||
Bounds are **measured**, not assumed: they are the blockout objects' world bounds
|
||||
(`Lee Drug Building`, `element.019`), which sit at Z 0 with the volume top at 12.10 m.
|
||||
Vertical proportions come from the single reference photograph and are recorded with
|
||||
the assumption in `Art/blender/retail/ref-analysis.md`.
|
||||
|
||||
6822 Budget is deliberately **not** built: there is no photograph of it, and the TODO
|
||||
item says so. Guessing its façade would be the misattribution the item warns against.
|
||||
"""
|
||||
|
||||
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 _common # noqa: E402
|
||||
import fbx_export_preset as fbx_preset # noqa: E402
|
||||
|
||||
# --- measured bounds, from the blockout (world metres) ------------------------
|
||||
# Lee Drug 6800, OSM way 424024149. Front face is the north edge (max Y).
|
||||
LD_X0, LD_X1 = -33.20, -14.37
|
||||
LD_FACE_Y = -14.63
|
||||
LD_H = 12.10
|
||||
|
||||
# 6806-6820, OSM way 424024150.
|
||||
B6_X0, B6_X1 = -66.08, -33.07
|
||||
B6_FACE_Y = -12.52
|
||||
B6_H = 12.10
|
||||
|
||||
# --- proportions, read off the reference photograph ---------------------------
|
||||
# Fractions of the 12.10 m height; see Art/blender/retail/ref-analysis.md for how
|
||||
# they were read and why they are assumptions at ±0.03 H.
|
||||
LD_SHOP_TOP = 0.36 * LD_H # 4.36 m: shopfront glazing / cornice soffit
|
||||
LD_CORNICE_TOP = 0.43 * LD_H # 5.20 m: top of the bronze band
|
||||
LD_MURAL_Z = (0.49 * LD_H, 0.61 * LD_H)
|
||||
LD_LETTER_Z = (0.65 * LD_H, 0.76 * LD_H)
|
||||
LD_PARAPET_Z = 0.82 * LD_H # 9.92 m: bottom of the stepped parapet
|
||||
|
||||
B6_SHOP_TOP = 0.32 * B6_H # 3.87 m
|
||||
B6_FASCIA_TOP = 0.38 * B6_H # 4.60 m
|
||||
B6_WINDOW_Z = (0.45 * B6_H, 0.60 * B6_H)
|
||||
B6_PARAPET_Z = 0.62 * B6_H # 7.50 m
|
||||
|
||||
BAY = 2.20 # mural / bay rhythm on Lee Drug, assumed
|
||||
PROJECT = 0.55 # bronze cornice projection, assumed
|
||||
TOWER_R = 2.10 # Lee Drug corner tower radius, assumed
|
||||
|
||||
PARTS: list[dict] = []
|
||||
|
||||
|
||||
def make_material(name, rgb, rough=0.6, metallic=0.0):
|
||||
"""Principled material with an explicit colour; the export preset carries it as a slot."""
|
||||
mat = bpy.data.materials.new(name)
|
||||
mat.use_nodes = True
|
||||
bsdf = mat.node_tree.nodes.get("Principled BSDF")
|
||||
bsdf.inputs["Base Color"].default_value = (rgb[0], rgb[1], rgb[2], 1.0)
|
||||
bsdf.inputs["Roughness"].default_value = rough
|
||||
bsdf.inputs["Metallic"].default_value = metallic
|
||||
return mat
|
||||
|
||||
|
||||
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 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 cylinder(name, x, y, z, radius, height, material, vertices=28):
|
||||
bpy.ops.mesh.primitive_cylinder_add(vertices=vertices, radius=radius, depth=height,
|
||||
location=(x, y, z))
|
||||
obj = bpy.context.active_object
|
||||
obj.name = name
|
||||
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
||||
obj.data.materials.append(material)
|
||||
return obj
|
||||
|
||||
|
||||
def bay_centres(x0, x1, margin=0.9, bay=BAY):
|
||||
"""Bay centres; the arithmetic lives in `_common` so it is testable without Blender."""
|
||||
return _common.bay_centres(x0, x1, margin=margin, bay=bay)
|
||||
|
||||
|
||||
def note(obj, evidence):
|
||||
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],
|
||||
"evidence": evidence,
|
||||
})
|
||||
print(f"[retail] {obj.name:26} {obj.dimensions.x:6.2f} x {obj.dimensions.y:5.2f} "
|
||||
f"x {obj.dimensions.z:5.2f} m ({evidence})")
|
||||
|
||||
|
||||
def build_lee_drug(mats):
|
||||
"""6800 Lee Drug: bronze cornice, mural row, lettering band, stepped Deco parapet,
|
||||
rounded corner tower."""
|
||||
stucco, bronze, mural, glass, white = (mats["stucco"], mats["bronze"], mats["mural"],
|
||||
mats["glass"], mats["white"])
|
||||
parts = []
|
||||
cx = (LD_X0 + LD_X1) / 2.0
|
||||
width = LD_X1 - LD_X0
|
||||
|
||||
# Bronze cornice: the strongest horizontal line on the building.
|
||||
cornice = cube("LD_Cornice", cx, LD_FACE_Y + PROJECT / 2.0,
|
||||
(LD_SHOP_TOP + LD_CORNICE_TOP) / 2.0,
|
||||
width + 0.4, PROJECT, LD_CORNICE_TOP - LD_SHOP_TOP, bronze)
|
||||
parts.append((cornice, "photographs: projecting bronze cornice over the shopfront; "
|
||||
f"{PROJECT:.2f} m projection assumed"))
|
||||
|
||||
# Shopfront glazing, recessed behind the face, one panel per bay.
|
||||
for i, bx in enumerate(bay_centres(LD_X0, LD_X1, margin=1.2), start=1):
|
||||
panel = cube(f"LD_ShopGlass_{i}", bx, LD_FACE_Y + 0.12, LD_SHOP_TOP / 2.0,
|
||||
BAY - 0.30, 0.10, LD_SHOP_TOP - 0.55, glass)
|
||||
parts.append((panel, "photographs: full-height shopfront glazing; bay width and "
|
||||
"the 0.55 m kickplate are assumed (a bus hides this zone)"))
|
||||
mullion = cube(f"LD_Mullion_{i}", bx - (BAY - 0.30) / 2.0, LD_FACE_Y - 0.02,
|
||||
LD_SHOP_TOP / 2.0, 0.10, 0.16, LD_SHOP_TOP - 0.55, bronze)
|
||||
parts.append((mullion, "assumption: slim mullion at each bay boundary"))
|
||||
|
||||
# Celebrity-portrait panels, one per bay, in the band above the cornice.
|
||||
z0, z1 = LD_MURAL_Z
|
||||
for i, bx in enumerate(bay_centres(LD_X0, LD_X1, margin=1.2), start=1):
|
||||
panel = cube(f"LD_Mural_{i}", bx, LD_FACE_Y - 0.04, (z0 + z1) / 2.0,
|
||||
BAY - 0.75, 0.08, z1 - z0, mural)
|
||||
parts.append((panel, "photographs: row of black-framed portrait panels; count and "
|
||||
"size assumed from the visible row"))
|
||||
|
||||
# Lettering band: a recessed plate, not text. Reproducing the lettering needs a
|
||||
# decal, which is a materials-pass job.
|
||||
lz0, lz1 = LD_LETTER_Z
|
||||
letter = cube("LD_LetterBand", cx, LD_FACE_Y - 0.03, (lz0 + lz1) / 2.0,
|
||||
width - 3.0, 0.06, lz1 - lz0, mural)
|
||||
parts.append((letter, "photographs: large dark lettering across the upper wall; "
|
||||
"modelled as a recessed plate, the text itself is a decal job"))
|
||||
|
||||
# Stepped Deco parapet: wall band plus four horizontal reveals.
|
||||
para = cube("LD_Parapet", cx, LD_FACE_Y + 0.10, (LD_PARAPET_Z + LD_H) / 2.0,
|
||||
width + 0.2, 0.30, LD_H - LD_PARAPET_Z, stucco)
|
||||
parts.append((para, "photographs: parapet steps above the frontage wall; "
|
||||
"thickness assumed"))
|
||||
for i in range(4):
|
||||
z = LD_PARAPET_Z + (LD_H - LD_PARAPET_Z) * (i + 1) / 5.0
|
||||
groove = cube(f"LD_ParapetReveal_{i + 1}", cx, LD_FACE_Y - 0.03, z,
|
||||
width + 0.2, 0.07, 0.10, mural)
|
||||
parts.append((groove, "photographs: four horizontal reveal lines in the parapet; "
|
||||
"count read off the frame"))
|
||||
|
||||
# Rounded corner tower at the east end, stepped twice at the top. The centre
|
||||
# sits INSIDE the footprint so the tower projects 0.35 m from the face instead of
|
||||
# bulging 4 m into the pavement, which the first run did.
|
||||
tower_y = LD_FACE_Y - TOWER_R + 0.35
|
||||
tower = cylinder("LD_CornerTower", LD_X1 - TOWER_R, tower_y,
|
||||
(LD_H - 0.9) / 2.0, TOWER_R, LD_H - 0.9, stucco)
|
||||
parts.append((tower, "photographs: rounded Deco corner tower rising above the "
|
||||
f"frontage; {TOWER_R:.2f} m radius assumed"))
|
||||
# Cap rings stack exactly to the measured 12.10 m top: 11.20 -> 11.75 -> 12.10.
|
||||
cap_z = LD_H - 0.9
|
||||
for i, (dh, radius) in enumerate(((0.55, TOWER_R - 0.35), (0.35, TOWER_R - 0.70)), start=1):
|
||||
cap = cylinder(f"LD_TowerCap_{i}", LD_X1 - TOWER_R, tower_y,
|
||||
cap_z + dh / 2.0, radius, dh, white)
|
||||
parts.append((cap, "photographs: two stepped cap rings on the tower"))
|
||||
cap_z += dh
|
||||
return parts
|
||||
|
||||
|
||||
def build_6806(mats):
|
||||
"""6806–6820: burgundy fascia, brown pilasters, white-framed windows, banded parapet."""
|
||||
stucco, bronze, glass, white, burgundy = (mats["stucco"], mats["bronze"], mats["glass"],
|
||||
mats["white"], mats["burgundy"])
|
||||
parts = []
|
||||
cx = (B6_X0 + B6_X1) / 2.0
|
||||
width = B6_X1 - B6_X0
|
||||
|
||||
fascia = cube("B6_Fascia", cx, B6_FACE_Y - 0.02,
|
||||
(B6_SHOP_TOP + B6_FASCIA_TOP) / 2.0, width, 0.22,
|
||||
B6_FASCIA_TOP - B6_SHOP_TOP, burgundy)
|
||||
parts.append((fascia, "photographs: dark fascia band over the shopfront; colour "
|
||||
"sampled, height assumed"))
|
||||
|
||||
centres = bay_centres(B6_X0, B6_X1, margin=1.0)
|
||||
for i, bx in enumerate(centres, start=1):
|
||||
panel = cube(f"B6_ShopGlass_{i}", bx, B6_FACE_Y + 0.12, B6_SHOP_TOP / 2.0,
|
||||
BAY - 0.25, 0.10, B6_SHOP_TOP - 0.45, glass)
|
||||
parts.append((panel, "photographs: framed shopfront glazing behind the fascia"))
|
||||
# Brown pilaster on every second bay boundary, as the photo shows.
|
||||
if i % 2 == 1:
|
||||
pil = cube(f"B6_Pilaster_{i}", bx, B6_FACE_Y - 0.10,
|
||||
(B6_FASCIA_TOP + B6_PARAPET_Z) / 2.0, 0.40, 0.22,
|
||||
B6_PARAPET_Z - B6_FASCIA_TOP, bronze)
|
||||
parts.append((pil, "photographs: brown pilaster strips dividing the frontage"))
|
||||
|
||||
wz0, wz1 = B6_WINDOW_Z
|
||||
for i, bx in enumerate(centres, start=1):
|
||||
frame = cube(f"B6_WindowFrame_{i}", bx, B6_FACE_Y - 0.06, (wz0 + wz1) / 2.0,
|
||||
1.70, 0.14, wz1 - wz0, white)
|
||||
parts.append((frame, "photographs: white-framed upper windows with security bars; "
|
||||
"count and size assumed"))
|
||||
pane = cube(f"B6_WindowGlass_{i}", bx, B6_FACE_Y + 0.02, (wz0 + wz1) / 2.0,
|
||||
1.46, 0.08, wz1 - wz0 - 0.24, glass)
|
||||
parts.append((pane, "photographs: dark glazing inside each frame"))
|
||||
|
||||
para = cube("B6_Parapet", cx, B6_FACE_Y + 0.08, (B6_PARAPET_Z + B6_H) / 2.0,
|
||||
width + 0.2, 0.26, B6_H - B6_PARAPET_Z, stucco)
|
||||
parts.append((para, "photographs: parapet band above the window storey; thickness assumed"))
|
||||
for i in range(3):
|
||||
z = B6_PARAPET_Z + (B6_H - B6_PARAPET_Z) * (i + 1) / 4.0
|
||||
groove = cube(f"B6_ParapetReveal_{i + 1}", cx, B6_FACE_Y - 0.03, z,
|
||||
width + 0.2, 0.07, 0.09, bronze)
|
||||
parts.append((groove, "photographs: three reveal lines in the parapet; count assumed"))
|
||||
return parts
|
||||
|
||||
|
||||
def join_parts(parts, name):
|
||||
"""Join one building's parts into a single mesh so Unreal imports one asset.
|
||||
|
||||
The per-part dimensions and evidence are recorded before this runs, so joining
|
||||
loses nothing from the report. Materials survive as separate slots.
|
||||
"""
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
for obj, _text in parts:
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = parts[0][0]
|
||||
bpy.ops.object.join()
|
||||
joined = bpy.context.active_object
|
||||
joined.name = name
|
||||
bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
|
||||
return joined
|
||||
|
||||
|
||||
def export_objects(objects, out_dir):
|
||||
count = 0
|
||||
for obj in objects:
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
fbx_preset.export_static(os.path.join(out_dir, "SM_" + obj.name + ".fbx"),
|
||||
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/retail")
|
||||
ap.add_argument("--report", default=None)
|
||||
args = ap.parse_args(argv)
|
||||
os.makedirs(args.out, exist_ok=True)
|
||||
|
||||
clear_scene()
|
||||
mats = {
|
||||
"stucco": make_material("M_Retail_Stucco", (0.86, 0.83, 0.76), rough=0.72),
|
||||
"bronze": make_material("M_Retail_Bronze", (0.20, 0.11, 0.06), rough=0.45, metallic=0.35),
|
||||
"mural": make_material("M_Retail_Mural", (0.035, 0.032, 0.030), rough=0.55),
|
||||
"glass": make_material("M_Retail_Glass", (0.035, 0.045, 0.055), rough=0.15),
|
||||
"white": make_material("M_Retail_White", (0.90, 0.88, 0.84), rough=0.60),
|
||||
"burgundy": make_material("M_Retail_Burgundy", (0.20, 0.09, 0.09), rough=0.55),
|
||||
}
|
||||
|
||||
ld_parts = build_lee_drug(mats)
|
||||
b6_parts = build_6806(mats)
|
||||
for obj, text in ld_parts + b6_parts:
|
||||
note(obj, text)
|
||||
|
||||
# One mesh per building: 85 separate actors over the frontage would cost more
|
||||
# draw calls than the trim is worth, and the staging step stays two spawns.
|
||||
joined = [join_parts(ld_parts, "Retail_LeeDrug"), join_parts(b6_parts, "Retail_6806")]
|
||||
for obj in joined:
|
||||
print(f"[retail] joined {obj.name}: {len(obj.data.vertices)} verts, "
|
||||
f"{len(obj.data.materials)} material slot(s), "
|
||||
f"X {min(v.co.x for v in obj.data.vertices):.2f}.."
|
||||
f"{max(v.co.x for v in obj.data.vertices):.2f}")
|
||||
exported = export_objects(joined, args.out)
|
||||
print(f"[retail] exported {exported} object(s) to {args.out}")
|
||||
|
||||
if args.report:
|
||||
with open(args.report, "w", encoding="utf-8") as handle:
|
||||
json.dump({
|
||||
"scope": ["6800 Lee Drug (way 424024149)", "6806-6820 (way 424024150)"],
|
||||
"deferred": ["6822 Budget (way 424024144): no photograph, not modelled"],
|
||||
"bounds_m": {
|
||||
"lee_drug": [LD_X0, LD_X1, LD_FACE_Y, LD_H],
|
||||
"b6806": [B6_X0, B6_X1, B6_FACE_Y, B6_H],
|
||||
},
|
||||
"reference": "Art/reference/retail-neighbours/01-lee-drug-and-neighbor.jpg "
|
||||
"(Gb321, CC BY-SA 4.0)",
|
||||
"analysis": "Art/blender/retail/ref-analysis.md",
|
||||
"parts": PARTS,
|
||||
}, handle, indent=2)
|
||||
print(f"[retail] report -> {args.report}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Tests for the pure layout helper in Tools/retail_frontage_build.py.
|
||||
|
||||
The builder itself needs Blender; `bay_centres` does not, so the bay rhythm the
|
||||
façade depends on is checkable without starting Blender or Unreal.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from Tools._common import bay_centres
|
||||
|
||||
|
||||
class BayCentreTests(unittest.TestCase):
|
||||
def test_centres_stay_inside_the_margins(self):
|
||||
centres = bay_centres(-33.20, -14.37, margin=1.2)
|
||||
self.assertGreaterEqual(min(centres), -33.20 + 1.2 - 1e-9)
|
||||
self.assertLessEqual(max(centres), -14.37 - 1.2 + 1e-9)
|
||||
|
||||
def test_bays_are_evenly_spaced(self):
|
||||
centres = bay_centres(-66.08, -33.07, margin=1.0)
|
||||
steps = {round(b - a, 6) for a, b in zip(centres, centres[1:])}
|
||||
self.assertEqual(len(steps), 1, f"uneven bay spacing: {steps}")
|
||||
|
||||
def test_bay_count_is_the_round_of_span_over_bay(self):
|
||||
# 33.01 m frontage, 1.0 m margin each end -> 31.01 m -> 14 bays at 2.2 m.
|
||||
self.assertEqual(len(bay_centres(-66.08, -33.07, margin=1.0)), 14)
|
||||
|
||||
def test_degenerate_frontage_returns_nothing(self):
|
||||
self.assertEqual(bay_centres(0.0, 1.0, margin=1.0), [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -235,9 +235,17 @@ def main() -> None:
|
||||
removed += 1
|
||||
log(f"cleared {removed} previous kit actors")
|
||||
|
||||
# The kit is the fallback for buildings that have nothing but a blockout. These
|
||||
# two are modelled from their own photograph in 0.1.6
|
||||
# (Tools/retail_frontage_build.py); pasting the generic kit over them put a
|
||||
# second, contradicting facade language on the same wall.
|
||||
MODELLED = ("Frontage_SM_LeeDrug_6800", "Frontage_SM_Hollywood_6806")
|
||||
targets = [a for a in unreal.EditorLevelLibrary.get_all_level_actors()
|
||||
if a.get_actor_label().startswith(("Frontage_", "North_"))]
|
||||
log(f"frontage actors: {len(targets)}")
|
||||
if a.get_actor_label().startswith(("Frontage_", "North_"))
|
||||
and a.get_actor_label() not in MODELLED]
|
||||
skipped = [a.get_actor_label() for a in unreal.EditorLevelLibrary.get_all_level_actors()
|
||||
if a.get_actor_label() in MODELLED]
|
||||
log(f"frontage actors: {len(targets)} (kit skipped on modelled facades: {skipped})")
|
||||
total = 0
|
||||
for actor in targets:
|
||||
total += apply_to_actor(actor, kit, materials)
|
||||
|
||||
@@ -597,6 +597,25 @@ def build():
|
||||
staged_frontage += 1
|
||||
log(f"staged {staged_frontage} frontage buildings")
|
||||
|
||||
# --- retail frontage trim (0.1.6) -------------------------------------
|
||||
# The two retail buildings beside the hero are modelled from their own
|
||||
# photograph (Art/reference/retail-neighbours/); the trim is laid over the
|
||||
# blockout volume, so it keeps the trim's own sampled materials rather than a
|
||||
# look-dev override. 6822 Budget has no photograph and stays a bare volume.
|
||||
for asset_name, label in (("SM_Retail_LeeDrug", "6800 Lee Drug trim"),
|
||||
("SM_Retail_6806", "6806-6820 trim")):
|
||||
mesh = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Frontage/{asset_name}")
|
||||
if mesh is None:
|
||||
log(f"MISSING trim {asset_name} — run Tools/ue_import_retail_detail.py")
|
||||
continue
|
||||
# RetailTrim_ rather than Frontage_ so the kit assembler leaves it alone.
|
||||
actor = spawn(unreal.StaticMeshActor, unreal.Vector(0, 0, 0), f"RetailTrim_{asset_name}")
|
||||
if actor:
|
||||
comp = actor.static_mesh_component
|
||||
comp.set_static_mesh(mesh)
|
||||
comp.set_mobility(unreal.ComponentMobility.MOVABLE)
|
||||
log(f"staged: {label}")
|
||||
|
||||
# --- player start: north sidewalk, facing the hero frontage -------------
|
||||
# Signs were measured from the imported meshes rather than assumed: the FBX
|
||||
# preset flips north, so in Unreal the road spans Y -1382..+1047, the NORTH
|
||||
|
||||
@@ -300,6 +300,12 @@ try:
|
||||
# on the north sidewalk looking along the crosswalk itself, which is
|
||||
# the pedestrian's view and shows the zebra bars as bars.
|
||||
("demo-crossing", unreal.Vector(-15250, -2500, 400), unreal.Vector(-14956, 400, 150)),
|
||||
# 0.1.6 retail frontage: 6800 Lee Drug (X -3340..-1417, face Y +1467)
|
||||
# and 6806-6820 (X -6618..-3297, face Y +1252). Stand on the north
|
||||
# sidewalk at 2 m, look south with a level horizon so the parapets and
|
||||
# the cornice line read as horizontals. 68 deg covers both frontages at
|
||||
# this distance; the ends crop rather than the elevation leaning.
|
||||
("demo-retail-frontage", unreal.Vector(-4700, -2000, 200), unreal.Vector(-4700, 1400, 560), 68.0),
|
||||
# the two north-side background buildings (TCL, Tussaud's) seen from
|
||||
# the south across Hollywood Blvd. Probed world-space (2026-11-08):
|
||||
# TCL bbox X -22889..-18442, Y -9431..-1802; Tussaud's X -27071..
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# Import the retail frontage trim (6800 Lee Drug, 6806–6820) into Unreal.
|
||||
#
|
||||
# blender -b --factory-startup --python Tools/retail_frontage_build.py -- \
|
||||
# --out /tmp/gigline/retail --report /tmp/gigline/retail-report.json
|
||||
# <engine>/Engine/Binaries/Linux/UnrealEditor-Cmd <uproject> -run=pythonscript \
|
||||
# -script=<repo>/Tools/ue_import_retail_detail.py -nullrhi -unattended -nopause -stdout
|
||||
#
|
||||
# Two meshes arrive, one per building, each already at its world position in the
|
||||
# project frame (the FBX carries the offset), so staging spawns at the origin.
|
||||
# Destination: /Game/Hollywood/Frontage/<name> — same folder as the block frontage
|
||||
# so the demo builder stages them together.
|
||||
#
|
||||
# Result file: /tmp/gigline/retail-import.txt
|
||||
|
||||
import glob
|
||||
import os
|
||||
|
||||
import unreal
|
||||
|
||||
SRC = "/tmp/gigline/retail"
|
||||
DEST = "/Game/Hollywood/Frontage"
|
||||
RESULT = "/tmp/gigline/retail-import.txt"
|
||||
lines = []
|
||||
|
||||
|
||||
def log(msg):
|
||||
lines.append(str(msg))
|
||||
unreal.log(str(msg))
|
||||
|
||||
|
||||
def import_fbx(path, name):
|
||||
task = unreal.AssetImportTask()
|
||||
task.filename = path
|
||||
task.destination_path = DEST
|
||||
task.destination_name = 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
|
||||
task.options = ui
|
||||
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||
|
||||
mesh = unreal.EditorAssetLibrary.load_asset(f"{DEST}/{name}")
|
||||
if mesh is None:
|
||||
log(f"MISSING after import: {DEST}/{name}")
|
||||
return None
|
||||
|
||||
# Trim is decoration on a solid the player already collides with: no collision
|
||||
# of its own, otherwise the cornice and mullions become snag points.
|
||||
try:
|
||||
body = mesh.get_editor_property("body_setup")
|
||||
body.set_editor_property("collision_trace_flag",
|
||||
unreal.CollisionTraceFlag.CTF_USE_COMPLEX_AS_SIMPLE)
|
||||
mesh.set_editor_property("body_setup", body)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
log(f"{name}: collision flag not set ({exc!r})")
|
||||
|
||||
box = mesh.get_bounding_box()
|
||||
mn, mx = box.min, box.max
|
||||
log(f"{name:22} X {mn.x:8.0f}..{mx.x:8.0f} Y {mn.y:7.0f}..{mx.y:7.0f} "
|
||||
f"Z {mn.z:5.0f}..{mx.z:5.0f} slots {len(mesh.get_editor_property('static_materials'))}")
|
||||
return mesh
|
||||
|
||||
|
||||
def main():
|
||||
files = sorted(glob.glob(os.path.join(SRC, "*.fbx")))
|
||||
if not files:
|
||||
log(f"no FBX in {SRC}; run Tools/retail_frontage_build.py first")
|
||||
return
|
||||
for path in files:
|
||||
import_fbx(path, os.path.splitext(os.path.basename(path))[0])
|
||||
unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True)
|
||||
|
||||
|
||||
try:
|
||||
main()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
import traceback
|
||||
log(f"FAILED: {type(exc).__name__}: {exc}")
|
||||
log(traceback.format_exc())
|
||||
|
||||
with open(RESULT, "w") as f:
|
||||
f.write("\n".join(lines) + "\n")
|
||||
@@ -48,6 +48,18 @@
|
||||
"Art/reference/north-chinese/01-grauman-s-chinese-theatre-by-carol-highsmith.jpg"
|
||||
],
|
||||
"expect": "The two north-side buildings (TCL Chinese Theatre, Madame Tussaud's) seen from across Hollywood Boulevard, full height in frame, daylight"
|
||||
},
|
||||
{
|
||||
"name": "demo-retail-frontage",
|
||||
"shot": "Screenshots/demo/review-demo-retail-frontage.png",
|
||||
"size": [
|
||||
1920,
|
||||
1080
|
||||
],
|
||||
"references": [
|
||||
"Art/reference/retail-neighbours/01-lee-drug-and-neighbor.jpg"
|
||||
],
|
||||
"expect": "The retail run beside the hero from across the street: 6800 Lee Drug's bronze cornice, mural row and stepped parapet next to 6806-6820's pilasters and window storey, daylight"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user