refactor(tools): group the sixty scripts by what they talk to
One directory with sixty scripts stopped being navigable, and the grouping is also the sandbox boundary:
Tools/unreal/ commandlets and the two map builders, with ue_common.py and ue_placement.py as the
shared imports (they need the engine)
Tools/blender/ builders, exporters, the FBX preset, the MPFB character pipeline, blender_screenshots
(they need the Blender Flatpak)
Tools/review/ the visual loop: render_screenshots.sh, judge_screenshot.py, hero_compare.py,
build_comparison_md.py, e2e_asset_check.py, hero_iterate.sh, visual_loop.py,
visual_targets.json
Tools/gen/ image and texture generation: the ComfyUI driver, deterministic texture writers,
reference fetching
Tools/input/ OS-level input injection
root _common.py, check-lfs.sh, addon_compat_probe.py, sibling_asset_audit.py, tests/
Every moved script now resolves the repository root as ../.. and puts the tools root plus its siblings on
sys.path, so 'import ue_common' still works from unreal/. The test suite bootstrap does the same for the five
folders. render_screenshots.sh takes the folder as a third argument to run_cmdlet, and the e2e driver,
hero_iterate.sh and the comparison generator were repointed.
51 README rows now link to their new location, the README gains a Layout section naming each folder's
sandbox, and the architecture notes and Blender README follow the moves.
Verified: 108 tests pass, prettier is clean, judge_screenshot.py and hero_compare.py run from their new
homes, and the e2e run was aborted mid-way (it had been rebuilding an empty DemoMap for 15 minutes) and is
being rerun against the new layout.
This commit is contained in:
1 parent
f681d83736
commit
4d27109668
168 files changed
+799
-407
No files matched your search
@@ -0,0 +1,268 @@
|
||||
#!/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/unreal/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.dirname(os.path.abspath(__file__))))
|
||||
import ue_common # noqa: E402
|
||||
|
||||
KIT = "/Game/Hollywood/Kit"
|
||||
RESULT = "/tmp/gigline/kit-apply.txt"
|
||||
|
||||
# Material per kit piece. Without this the pieces keep the grey material the FBX
|
||||
# import creates (M_Kit_*), so the glazing is not glass and the shopfronts are not
|
||||
# stone - the kit reads as a plastic scaffold over the buildings.
|
||||
KIT_MATERIALS = {
|
||||
"Kit_WindowGlass": "M_FacadeGlass",
|
||||
"Kit_ShopGlass": "M_FacadeGlass",
|
||||
"Kit_Door": "M_FacadeStoneShadow",
|
||||
"Kit_WindowBay": "M_FacadeStone",
|
||||
"Kit_ShopFascia": "M_FacadeBanner",
|
||||
}
|
||||
KIT_MATERIAL_FALLBACK = "M_FacadeStone"
|
||||
|
||||
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:
|
||||
# Assets are SM_-prefixed (Unreal convention); the layout code refers to
|
||||
# pieces by their short name, so key on the name without the prefix.
|
||||
name = mesh.get_name()
|
||||
kit[name[3:] if name.startswith("SM_") else 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}_"
|
||||
# WORLD-space bounds. The imported meshes carry a building-line-local offset in
|
||||
# their own vertices and the demo builder then positions the actor, so the
|
||||
# asset's bounding box is NOT world space: using it put every kit piece about
|
||||
# 12 km east of its building, floating in empty ground where nothing could see
|
||||
# it. get_actor_bounds() includes the actor transform.
|
||||
origin_w, extent_w = actor.get_actor_bounds(False)
|
||||
mn = unreal.Vector(origin_w.x - extent_w.x, origin_w.y - extent_w.y, origin_w.z - extent_w.z)
|
||||
mx = unreal.Vector(origin_w.x + extent_w.x, origin_w.y + extent_w.y, origin_w.z + extent_w.z)
|
||||
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: the kit module is one window bay (4.96 m). A very wide building gets a
|
||||
# bay count that keeps the rhythm readable rather than one window per 4.96 m of
|
||||
# frontage — 6922 is 65.8 m wide, which is 13 bays, giving a window every 4.96 m
|
||||
# across a 37 m tall block: the rhythm reads as a wall of glass, not a building.
|
||||
# Above MAX_BAYS the module is widened instead, so the bay count stays legible.
|
||||
MAX_BAYS = 9
|
||||
bays = max(1, int(round(width / BAY_CM)))
|
||||
if bays > MAX_BAYS:
|
||||
bays = MAX_BAYS
|
||||
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 never overrun the volume: cap the count so the top band (cornice +
|
||||
# parapet, 145 cm) always lands inside the building.
|
||||
storeys = max(1, int((height - SHOP_FLOOR_CM - 145.0) // STOREY_CM) + 1)
|
||||
top_z = base_z + height
|
||||
# Windows above the shopfront, spread over the available height rather than
|
||||
# assumed to be exactly STOREY_CM apart, so a 37 m block does not leave a gap
|
||||
# of bare wall under the cornice.
|
||||
usable = max(0.0, (top_z - 145.0) - (base_z + SHOP_FLOOR_CM))
|
||||
storey_step = (usable / storeys) if storeys else STOREY_CM
|
||||
|
||||
def kit_material(name):
|
||||
if materials is None:
|
||||
return None
|
||||
want = KIT_MATERIALS.get(name, KIT_MATERIAL_FALLBACK)
|
||||
return materials.get(want)
|
||||
|
||||
placed = 0
|
||||
for bay in range(bays):
|
||||
cx = mn.x + bay_w * (bay + 0.5)
|
||||
# ground floor: shopfront
|
||||
for piece, dx, side in (("Kit_ShopPier_L", -bay_w / 2 + 30.0, "L"),
|
||||
("Kit_ShopPier_R", bay_w / 2 - 30.0, "R")):
|
||||
if piece in kit:
|
||||
# The side belongs in the label: both piers of a bay shared one
|
||||
# label, which left 53 ambiguous pairs in the outliner.
|
||||
if _spawn(kit[piece], unreal.Vector(cx + dx, face_y, base_z),
|
||||
f"{prefix}shop_pier_{side}_{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_step * 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/unreal/ue_import_facade_kit.py first")
|
||||
return
|
||||
log(f"kit meshes: {len(kit)}")
|
||||
|
||||
# Site materials built by Tools/unreal/ue_build_demo_map.py.
|
||||
materials = {}
|
||||
for name in ("M_FacadeStone", "M_FacadeStoneShadow", "M_FacadeGlass",
|
||||
"M_FacadeBanner", "M_Stucco", "M_Concrete"):
|
||||
asset = unreal.EditorAssetLibrary.load_asset(f"/Game/Hollywood/Site/{name}")
|
||||
if asset:
|
||||
materials[name] = asset
|
||||
log(f"kit materials resolved: {len(materials)}/6")
|
||||
|
||||
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")
|
||||
|
||||
# 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/blender/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_"))
|
||||
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)
|
||||
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")
|
||||
Reference in new issue
Block a user