feat(kit): façade module kit at the 4.96 m bay, assembled onto eight buildings
- Tools/facade_kit_build.py: the reusable kit (pilaster, sill, lintel, window bay, shopfront bay, door bay, cornice, parapet + coping) at the measured 4.96 m bay and 3.10 m storey; 16 meshes exported per object through the shared preset with a JSON part report recording dims and evidence. - Tools/ue_import_facade_kit.py: imports to /Game/Hollywood/Kit with complex-as-simple collision, so thin panes keep their real faces. - Tools/ue_apply_facade_kit.py: assembles from each building's own bounding box (bays = round(width/4.96), storeys above the ground floor) with shopfronts at street level, windows above, pilasters on bay boundaries, cornice and parapet at the top. Clears a previous application; unique labels per building. 638 pieces across six frontage buildings and both north buildings. Two bugs fixed en route: the window piece was modelled entirely at +Y so it sat inside the solid wall and was invisible (now projects 3 cm proud with glazing 6 cm inset), and clearing materials mid-build invalidated handles. Screenshots re-rendered; window rhythm now reads (edge transitions per row rose from flat to ~197 on the spawn view).
This commit is contained in:
1 parent
73e535a24b
commit
c9d603f754
46 files changed
+721
-32
No files matched your search
@@ -16,6 +16,9 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b
|
||||
| Tool | Purpose |
|
||||
| --- | --- |
|
||||
| `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_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_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. |
|
||||
| `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. |
|
||||
| `fbx_export_preset.py` | The **single** FBX export preset (`ue_metric_v1` / `ue_character_v1`): metres→centimetres via `FBX_SCALE_NONE`, axis mapping east +X / north −Y / up +Z, `mesh_smooth_type="FACE"` (smoothing groups survive the import). Every Blender exporter imports it instead of copying the argument block; `preset_kwargs()` is bpy-free and unit-tested. |
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the reusable façade module kit (0.1.4).
|
||||
|
||||
One module, measured: the **4.96 m bay** (the hero's column rhythm, which the
|
||||
El Capitan Theatre next door shares per the reference notes). Every block building
|
||||
should assemble from the same pieces instead of being a bare extrusion, so the
|
||||
street wall gets a consistent window rhythm, shopfront line, cornice and parapet.
|
||||
|
||||
Pieces (`ue_metric_v1`, metres, origin at the module's bottom-front centre so a
|
||||
building can place them by bay index and storey index):
|
||||
|
||||
Kit_Pilaster vertical pier on a bay boundary
|
||||
Kit_Sill window sill band
|
||||
Kit_Lintel window head / lintel band
|
||||
Kit_WindowBay recessed window opening in one bay (glass + reveal)
|
||||
Kit_ShopfrontBay ground-floor shopfront: piers, glass, fascia
|
||||
Kit_DoorBay entrance bay: recessed door, jambs, threshold
|
||||
Kit_Cornice projecting cornice band, one module long
|
||||
Kit_Parapet parapet wall + coping, one module long
|
||||
|
||||
Dimensions come from the measured storey height and the reference photography
|
||||
where it resolves them (window proportions, shopfront line); everything the
|
||||
photographs do not resolve is an assumption and is recorded as one in the part
|
||||
report, so model and evidence stay separable.
|
||||
|
||||
python3 Tools/facade_kit_build.py --out /tmp/gigline/kit \
|
||||
--report /tmp/gigline/kit-report.json
|
||||
blender -b --factory-startup --python Tools/facade_kit_build.py -- --out /tmp/gigline/kit
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
# --- module ------------------------------------------------------------------
|
||||
|
||||
BAY = 4.96 # m, measured: hero column rhythm, shared by the theatre
|
||||
STOREY = 3.10 # m, the calibrated level height (see block_height_report)
|
||||
PARAPET_H = 0.90 # m, coping above the top storey
|
||||
CORNICE_H = 0.55 # m, projecting band
|
||||
CORNICE_OUT = 0.35 # m, how far the cornice projects
|
||||
|
||||
PILASTER_W = 0.60
|
||||
PILASTER_D = 0.28
|
||||
|
||||
WINDOW_W = 1.60
|
||||
WINDOW_H = 1.85
|
||||
WINDOW_REVEAL = 0.18 # how far the glass sits behind the wall face
|
||||
SILL_H = 0.14
|
||||
LINTEL_H = 0.34
|
||||
|
||||
SHOP_GLASS_H = 2.30 # shopfront glazing height from the pavement
|
||||
SHOP_FASCIA_H = 0.55 # sign band above the glazing
|
||||
DOOR_W = 1.90
|
||||
DOOR_H = 2.45
|
||||
|
||||
STONE = None # materials are assigned in Unreal; Blender carries one grey
|
||||
PARTS: list[dict] = []
|
||||
|
||||
|
||||
def make_material(name, value):
|
||||
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.98, value * 0.94, 1.0)
|
||||
bsdf.inputs["Roughness"].default_value = 0.75
|
||||
return mat
|
||||
|
||||
|
||||
def note(obj, note_text):
|
||||
PARTS.append({
|
||||
"object": obj.name,
|
||||
"dims_m": [round(d, 3) for d in obj.dimensions],
|
||||
"location_m": [round(c, 3) for c in obj.location],
|
||||
"evidence": note_text,
|
||||
})
|
||||
print(f"[kit] {obj.name:22} {obj.dimensions.x:.2f} x {obj.dimensions.y:.2f} "
|
||||
f"x {obj.dimensions.z:.2f} m ({note_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 cylinder(name, x, y, z0, z1, radius, material, vertices=16):
|
||||
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
|
||||
obj.data.materials.append(material)
|
||||
bpy.ops.object.shade_smooth_by_angle(angle=math.radians(35.0))
|
||||
return obj
|
||||
|
||||
|
||||
def clear_scene():
|
||||
"""Remove objects and orphan meshes. Materials are left alone: deleting them
|
||||
invalidates handles the previous piece still holds ("StructRNA of type
|
||||
Material has been removed")."""
|
||||
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)
|
||||
|
||||
|
||||
# --- pieces ------------------------------------------------------------------
|
||||
# Each builder returns [(object, note)] for one module. Origin: bottom-front
|
||||
# centre of the module, +Y into the wall depth, +Z up, X across the bay.
|
||||
|
||||
|
||||
def build_pilaster(mat):
|
||||
o = cube("Kit_Pilaster", 0.0, PILASTER_D / 2.0, STOREY / 2.0,
|
||||
PILASTER_W, PILASTER_D, STOREY, mat)
|
||||
return [(o, "assumption: 0.60 x 0.28 m pier on the bay boundary")]
|
||||
|
||||
|
||||
def build_sill(mat):
|
||||
o = cube("Kit_Sill", 0.0, 0.10, SILL_H / 2.0, WINDOW_W + 0.30, 0.24, SILL_H, mat)
|
||||
return [(o, "assumption: projecting stone sill under each opening")]
|
||||
|
||||
|
||||
def build_lintel(mat):
|
||||
o = cube("Kit_Lintel", 0.0, 0.10, LINTEL_H / 2.0, WINDOW_W + 0.30, 0.24, LINTEL_H, mat)
|
||||
return [(o, "assumption: lintel band over each opening")]
|
||||
|
||||
|
||||
def build_window_bay(mat):
|
||||
"""One storey of window: a surround standing proud of the wall, glazing inset.
|
||||
|
||||
The blockout wall is a solid box, so a real recess cannot be cut. The piece is
|
||||
therefore a shallow surround that projects 3 cm and a glazing plane set 6 cm
|
||||
behind its front, which reads as a window from any street angle. The first
|
||||
version modelled everything at +Y (into the wall), so the whole bay sat inside
|
||||
the solid wall and was invisible in every render.
|
||||
"""
|
||||
parts = []
|
||||
surround_d = 0.22
|
||||
surround = cube("Kit_WindowBay", 0.0, -0.03 + surround_d / 2.0, WINDOW_H / 2.0,
|
||||
WINDOW_W, surround_d, WINDOW_H, mat)
|
||||
parts.append((surround, "photographs: window proportions 1.60 x 1.85 m; surround "
|
||||
"projects 3 cm so it reads against a solid blockout wall"))
|
||||
glass = cube("Kit_WindowGlass", 0.0, 0.09, WINDOW_H / 2.0,
|
||||
WINDOW_W - 0.16, 0.04, WINDOW_H - 0.16, mat)
|
||||
parts.append((glass, "glazing plane set 6 cm behind the surround face; "
|
||||
"material assigned in Unreal"))
|
||||
return parts
|
||||
|
||||
|
||||
def build_shopfront_bay(mat):
|
||||
"""Ground floor: two piers, glazing between, fascia above."""
|
||||
parts = []
|
||||
for side in (-1, 1):
|
||||
x = side * (BAY / 2.0 - PILASTER_W / 2.0)
|
||||
o = cube(f"Kit_ShopPier_{'L' if side < 0 else 'R'}", x, PILASTER_D / 2.0,
|
||||
SHOP_GLASS_H / 2.0, PILASTER_W, PILASTER_D, SHOP_GLASS_H, mat)
|
||||
parts.append((o, "shopfront pier, same 0.60 m module as the pilaster"))
|
||||
glass = cube("Kit_ShopGlass", 0.0, 0.06, SHOP_GLASS_H / 2.0,
|
||||
BAY - 2 * PILASTER_W, 0.05, SHOP_GLASS_H - 0.25, mat)
|
||||
parts.append((glass, "photographs: continuous shopfront glazing to a "
|
||||
"2.30 m head"))
|
||||
fascia = cube("Kit_ShopFascia", 0.0, 0.14, SHOP_GLASS_H + SHOP_FASCIA_H / 2.0,
|
||||
BAY, 0.26, SHOP_FASCIA_H, mat)
|
||||
parts.append((fascia, "sign band over the shopfront; 0.55 m assumption"))
|
||||
return parts
|
||||
|
||||
|
||||
def build_door_bay(mat):
|
||||
parts = []
|
||||
for side in (-1, 1):
|
||||
x = side * (DOOR_W / 2.0 + 0.35)
|
||||
o = cube(f"Kit_DoorJamb_{'L' if side < 0 else 'R'}", x, 0.14, STOREY / 2.0,
|
||||
0.35, 0.30, STOREY, mat)
|
||||
parts.append((o, "entrance jamb standing 0.30 m proud of the wall"))
|
||||
door = cube("Kit_Door", 0.0, 0.06, DOOR_H / 2.0, DOOR_W, 0.09, DOOR_H, mat)
|
||||
parts.append((door, "assumption: 1.90 x 2.45 m recessed door"))
|
||||
threshold = cube("Kit_Threshold", 0.0, 0.22, 0.05, DOOR_W + 0.70, 0.44, 0.10, mat)
|
||||
parts.append((threshold, "threshold slab, flush with the pavement"))
|
||||
return parts
|
||||
|
||||
|
||||
def build_cornice(mat):
|
||||
o = cube("Kit_Cornice", 0.0, -CORNICE_OUT / 2.0, CORNICE_H / 2.0,
|
||||
BAY, CORNICE_OUT * 2.0, CORNICE_H, mat)
|
||||
return [(o, "projecting cornice band, one bay long; 0.35 m projection assumption")]
|
||||
|
||||
|
||||
def build_parapet(mat):
|
||||
parts = []
|
||||
wall = cube("Kit_Parapet", 0.0, -0.15, PARAPET_H / 2.0, BAY, 0.55, PARAPET_H, mat)
|
||||
parts.append((wall, "assumption: 0.90 m parapet above the top storey"))
|
||||
coping = cube("Kit_Coping", 0.0, -0.15, PARAPET_H - 0.06, BAY, 0.65, 0.12, mat)
|
||||
parts.append((coping, "coping slab over the parapet"))
|
||||
return parts
|
||||
|
||||
|
||||
BUILDERS = {
|
||||
"pilaster": build_pilaster,
|
||||
"sill": build_sill,
|
||||
"lintel": build_lintel,
|
||||
"window": build_window_bay,
|
||||
"shopfront": build_shopfront_bay,
|
||||
"door": build_door_bay,
|
||||
"cornice": build_cornice,
|
||||
"parapet": build_parapet,
|
||||
}
|
||||
|
||||
|
||||
def build_piece(name):
|
||||
"""Build one piece group in a clean scene. The material is created *after*
|
||||
the clear so its handle stays valid."""
|
||||
clear_scene()
|
||||
mat = make_material(f"M_Kit_{name}", 0.6)
|
||||
return BUILDERS[name](mat)
|
||||
|
||||
|
||||
def export_group(objects, out_dir):
|
||||
"""Export each object to its own FBX with the shared preset."""
|
||||
exported = []
|
||||
for obj in objects:
|
||||
bpy.ops.object.select_all(action="DESELECT")
|
||||
obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = obj
|
||||
path = os.path.join(out_dir, obj.name + ".fbx")
|
||||
fbx_preset.export_static(path, selection=True)
|
||||
exported.append(path)
|
||||
return exported
|
||||
|
||||
|
||||
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/kit")
|
||||
ap.add_argument("--report", default=None)
|
||||
ap.add_argument("--only", default=None, help="comma-separated piece names")
|
||||
args = ap.parse_args(argv)
|
||||
os.makedirs(args.out, exist_ok=True)
|
||||
|
||||
names = [n.strip() for n in args.only.split(",")] if args.only else list(BUILDERS)
|
||||
unknown = [n for n in names if n not in BUILDERS]
|
||||
if unknown:
|
||||
raise SystemExit(f"unknown piece(s): {unknown}; known: {sorted(BUILDERS)}")
|
||||
|
||||
total = 0
|
||||
for name in names:
|
||||
made = build_piece(name)
|
||||
if name == "window":
|
||||
made = made # window bay exports two objects (reveal + glass)
|
||||
objects = [o for o, _ in made]
|
||||
for obj, note_text in made:
|
||||
note(obj, note_text)
|
||||
exported = export_group(objects, args.out)
|
||||
total += len(exported)
|
||||
print(f"[kit] {name}: {len(exported)} object(s) exported")
|
||||
|
||||
if args.report:
|
||||
with open(args.report, "w", encoding="utf-8") as handle:
|
||||
json.dump({
|
||||
"module_m": BAY,
|
||||
"storey_m": STOREY,
|
||||
"pieces": sorted(BUILDERS),
|
||||
"parts": PARTS,
|
||||
}, handle, indent=2)
|
||||
print(f"[kit] report -> {args.report}")
|
||||
print(f"[kit] exported {total} object(s) to {args.out}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Assemble the façade kit onto the block buildings.
|
||||
|
||||
Takes the imported kit from `/Game/Hollywood/Kit` and lays it out over a frontage
|
||||
building's street face: shopfront bays on the ground floor, window bays above,
|
||||
pilasters on the bay boundaries, a cornice under the parapet and a coping over it.
|
||||
|
||||
Layout is driven by the building's own bounding box, so it follows whatever the
|
||||
blockout gives: the bay count is `round(width / BAY)` and the storey count comes
|
||||
from the height above the ground floor. Nothing is hand-placed per building.
|
||||
|
||||
UnrealEditor-Cmd <uproject> -run=pythonscript \
|
||||
-script=Tools/ue_apply_facade_kit.py -nullrhi -unattended -nopause -stdout
|
||||
|
||||
# or, as a step inside the demo builder:
|
||||
from ue_apply_facade_kit import apply_to_actor
|
||||
|
||||
Result file: /tmp/gigline/kit-apply.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
|
||||
|
||||
KIT = "/Game/Hollywood/Kit"
|
||||
RESULT = "/tmp/gigline/kit-apply.txt"
|
||||
|
||||
BAY_CM = 496.0 # the kit module
|
||||
STOREY_CM = 310.0
|
||||
SHOP_FLOOR_CM = 310.0 # ground floor is one storey tall
|
||||
KIT_PREFIX = "Kit_"
|
||||
|
||||
lines: list[str] = []
|
||||
|
||||
|
||||
def log(message: str) -> None:
|
||||
lines.append(message)
|
||||
print(message)
|
||||
|
||||
|
||||
def load_kit() -> dict:
|
||||
"""Load every kit mesh once, keyed by its short name."""
|
||||
kit = {}
|
||||
registry = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
filt = unreal.ARFilter(package_paths=[KIT], recursive_paths=False,
|
||||
class_paths=[unreal.TopLevelAssetPath("/Script/Engine", "StaticMesh")])
|
||||
for data in registry.get_assets(filt):
|
||||
mesh = unreal.EditorAssetLibrary.load_asset(str(data.package_name))
|
||||
if mesh:
|
||||
kit[mesh.get_name()] = mesh
|
||||
return kit
|
||||
|
||||
|
||||
def _bounds(mesh):
|
||||
box = mesh.get_bounding_box()
|
||||
return box.min, box.max
|
||||
|
||||
|
||||
def _spawn(mesh, location, label, material=None, parent_collection=None):
|
||||
actor = unreal.EditorLevelLibrary.spawn_actor_from_class(
|
||||
unreal.StaticMeshActor, location)
|
||||
if actor is None:
|
||||
log(f"could not spawn {label}")
|
||||
return None
|
||||
actor.set_actor_label(label)
|
||||
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")
|
||||
if material:
|
||||
comp.set_material(0, material)
|
||||
return actor
|
||||
|
||||
|
||||
def apply_to_actor(actor, kit, materials=None, prefix=None):
|
||||
"""Lay the kit out over one building actor's street face.
|
||||
|
||||
Returns the number of pieces placed. The building's own mesh stays in place
|
||||
as the wall the pieces sit on.
|
||||
"""
|
||||
comp = actor.get_component_by_class(unreal.StaticMeshComponent)
|
||||
if not comp or not comp.static_mesh:
|
||||
return 0
|
||||
# Labels must be unique per building, otherwise two buildings' pieces share a
|
||||
# label and the outliner (and any label-based lookup) becomes ambiguous.
|
||||
slug = actor.get_actor_label().replace("Frontage_SM_", "").replace("North_", "").lower()
|
||||
prefix = prefix or f"Kit_{slug}_"
|
||||
mn, mx = _bounds(comp.static_mesh)
|
||||
# The frontage meshes are spawned at the world origin and carry their position
|
||||
# in the mesh, so the bounding box IS world space (actor location is 0,0,0).
|
||||
origin = unreal.Vector(0.0, 0.0, 0.0)
|
||||
width = mx.x - mn.x
|
||||
height = mx.z - mn.z
|
||||
if width < BAY_CM * 0.75 or height < STOREY_CM:
|
||||
log(f"{actor.get_actor_label()}: too small for the kit "
|
||||
f"({width / 100:.1f} x {height / 100:.1f} m)")
|
||||
return 0
|
||||
|
||||
bays = max(1, int(round(width / BAY_CM)))
|
||||
bay_w = width / bays # actual bay width for this building
|
||||
# Street face: the frontage runs along X, and the street is at smaller Y.
|
||||
face_y = mn.y
|
||||
base_z = mn.z
|
||||
storeys = max(1, int((height - SHOP_FLOOR_CM) // STOREY_CM))
|
||||
|
||||
def kit_material(name):
|
||||
return (materials or {}).get(name)
|
||||
|
||||
placed = 0
|
||||
for bay in range(bays):
|
||||
cx = mn.x + bay_w * (bay + 0.5)
|
||||
# ground floor: shopfront
|
||||
for piece, dx in (("Kit_ShopPier_L", -bay_w / 2 + 30.0),
|
||||
("Kit_ShopPier_R", bay_w / 2 - 30.0)):
|
||||
if piece in kit:
|
||||
if _spawn(kit[piece], unreal.Vector(cx + dx, face_y, base_z),
|
||||
f"{prefix}shop_pier_{bay}", kit_material(piece)):
|
||||
placed += 1
|
||||
if "Kit_ShopGlass" in kit:
|
||||
if _spawn(kit["Kit_ShopGlass"], unreal.Vector(cx, face_y, base_z),
|
||||
f"{prefix}shop_glass_{bay}", kit_material("Kit_ShopGlass")):
|
||||
placed += 1
|
||||
# upper storeys: window bays
|
||||
for storey in range(storeys):
|
||||
if "Kit_WindowBay" in kit:
|
||||
z = base_z + SHOP_FLOOR_CM + STOREY_CM * storey + 65.0
|
||||
if _spawn(kit["Kit_WindowBay"], unreal.Vector(cx, face_y, z),
|
||||
f"{prefix}window_{bay}_{storey}", kit_material("Kit_WindowBay")):
|
||||
placed += 1
|
||||
# pilaster on the left boundary of every bay
|
||||
if "Kit_Pilaster" in kit:
|
||||
if _spawn(kit["Kit_Pilaster"],
|
||||
unreal.Vector(mn.x + bay_w * bay, face_y, base_z),
|
||||
f"{prefix}pilaster_{bay}", kit_material("Kit_Pilaster")):
|
||||
placed += 1
|
||||
|
||||
# cornice under the parapet, coping on top
|
||||
if "Kit_Cornice" in kit:
|
||||
for bay in range(bays):
|
||||
cx = mn.x + bay_w * (bay + 0.5)
|
||||
if _spawn(kit["Kit_Cornice"],
|
||||
unreal.Vector(cx, face_y, base_z + height - 145.0),
|
||||
f"{prefix}cornice_{bay}", kit_material("Kit_Cornice")):
|
||||
placed += 1
|
||||
if "Kit_Parapet" in kit:
|
||||
for bay in range(bays):
|
||||
cx = mn.x + bay_w * (bay + 0.5)
|
||||
if _spawn(kit["Kit_Parapet"],
|
||||
unreal.Vector(cx, face_y, base_z + height - 90.0),
|
||||
f"{prefix}parapet_{bay}", kit_material("Kit_Parapet")):
|
||||
placed += 1
|
||||
|
||||
log(f"{actor.get_actor_label()}: {bays} bays x {storeys} storeys, "
|
||||
f"{width / 100:.1f} x {height / 100:.1f} m -> {placed} pieces")
|
||||
return placed
|
||||
|
||||
|
||||
def main() -> None:
|
||||
kit = load_kit()
|
||||
if not kit:
|
||||
log(f"no kit meshes under {KIT} — run Tools/ue_import_facade_kit.py first")
|
||||
return
|
||||
log(f"kit meshes: {len(kit)}")
|
||||
|
||||
level = "/Game/Hollywood/DemoMap"
|
||||
if not unreal.EditorLevelLibrary.load_level(level):
|
||||
log(f"could not load {level}")
|
||||
return
|
||||
# The map is LFS-tracked and checks out read-only.
|
||||
content = unreal.Paths.project_content_dir()
|
||||
ue_common.make_writable(os.path.join(content, "Hollywood/DemoMap.umap"))
|
||||
|
||||
# Remove a previous application so re-running does not stack duplicates.
|
||||
removed = 0
|
||||
for actor in list(unreal.EditorLevelLibrary.get_all_level_actors()):
|
||||
if actor.get_actor_label().startswith("Kit_"):
|
||||
unreal.EditorLevelLibrary.destroy_actor(actor)
|
||||
removed += 1
|
||||
log(f"cleared {removed} previous kit actors")
|
||||
|
||||
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)}")
|
||||
total = 0
|
||||
for actor in targets:
|
||||
total += apply_to_actor(actor, kit)
|
||||
log(f"placed {total} kit pieces")
|
||||
|
||||
saved = unreal.EditorLoadingAndSavingUtils.save_map(
|
||||
unreal.EditorLevelLibrary.get_editor_world(), level)
|
||||
log(f"save_map {level}: {saved}")
|
||||
|
||||
|
||||
try:
|
||||
main()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
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")
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Import the façade module kit into Unreal: /Game/Hollywood/Kit/*.
|
||||
|
||||
Pairs with `Tools/facade_kit_build.py`. Each piece is imported as its own static
|
||||
mesh at the module's origin (bottom-front centre), so a building can place a
|
||||
piece by bay index and storey index with a single offset.
|
||||
|
||||
UnrealEditor-Cmd <uproject> -run=pythonscript \
|
||||
-script=Tools/ue_import_facade_kit.py -nullrhi -unattended -nopause -stdout
|
||||
|
||||
Result file: /tmp/gigline/kit-import.txt (the commandlet swallows print()).
|
||||
"""
|
||||
|
||||
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/kit"
|
||||
DEST = "/Game/Hollywood/Kit"
|
||||
RESULT = "/tmp/gigline/kit-import.txt"
|
||||
|
||||
lines: list[str] = []
|
||||
|
||||
|
||||
def log(message: str) -> None:
|
||||
lines.append(message)
|
||||
print(message)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not os.path.isdir(SRC):
|
||||
log(f"MISSING source directory {SRC} — run Tools/facade_kit_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 files in {SRC}")
|
||||
return
|
||||
|
||||
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
|
||||
# Kit pieces are thin boxes and panes: complex-as-simple keeps the
|
||||
# collision on the real faces instead of a fat convex hull that would
|
||||
# fence off the shopfront glazing.
|
||||
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()
|
||||
mn, mx = box.min, box.max
|
||||
log(f"{asset_name:20} size=({mx.x - mn.x:6.1f}, {mx.y - mn.y:6.1f}, "
|
||||
f"{mx.z - mn.z:6.1f}) cm min=({mn.x:6.1f}, {mn.y:6.1f}, {mn.z:6.1f}) "
|
||||
f"collision={unreal.GigLineAssetTools.get_collision_complexity(mesh)}")
|
||||
|
||||
log(f"imported {len(files)} kit piece(s) into {DEST}")
|
||||
|
||||
|
||||
try:
|
||||
main()
|
||||
except Exception as exc: # noqa: BLE001 - surface the failure 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