From c9d603f754b06a7da4b79122d2f0a75107b3f3e0 Mon Sep 17 00:00:00 2001 From: Zwitschi Date: Sun, 8 Nov 2026 19:12:45 +0100 Subject: [PATCH] =?UTF-8?q?feat(kit):=20fa=C3=A7ade=20module=20kit=20at=20?= =?UTF-8?q?the=204.96=20m=20bay,=20assembled=20onto=20eight=20buildings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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). --- Game/GigLine/Content/Hollywood/DemoMap.umap | 4 +- .../Content/Hollywood/Kit/Kit_Coping.uasset | 3 + .../Content/Hollywood/Kit/Kit_Cornice.uasset | 3 + .../Content/Hollywood/Kit/Kit_Door.uasset | 3 + .../Hollywood/Kit/Kit_DoorJamb_L.uasset | 3 + .../Hollywood/Kit/Kit_DoorJamb_R.uasset | 3 + .../Content/Hollywood/Kit/Kit_Lintel.uasset | 3 + .../Content/Hollywood/Kit/Kit_Parapet.uasset | 3 + .../Content/Hollywood/Kit/Kit_Pilaster.uasset | 3 + .../Hollywood/Kit/Kit_ShopFascia.uasset | 3 + .../Hollywood/Kit/Kit_ShopGlass.uasset | 3 + .../Hollywood/Kit/Kit_ShopPier_L.uasset | 3 + .../Hollywood/Kit/Kit_ShopPier_R.uasset | 3 + .../Content/Hollywood/Kit/Kit_Sill.uasset | 3 + .../Hollywood/Kit/Kit_Threshold.uasset | 3 + .../Hollywood/Kit/Kit_WindowBay.uasset | 3 + .../Hollywood/Kit/Kit_WindowGlass.uasset | 3 + .../Hollywood/Kit/M_Kit_cornice.uasset | 3 + .../Content/Hollywood/Kit/M_Kit_door.uasset | 3 + .../Content/Hollywood/Kit/M_Kit_lintel.uasset | 3 + .../Hollywood/Kit/M_Kit_parapet.uasset | 3 + .../Hollywood/Kit/M_Kit_pilaster.uasset | 3 + .../Hollywood/Kit/M_Kit_shopfront.uasset | 3 + .../Content/Hollywood/Kit/M_Kit_sill.uasset | 3 + .../Content/Hollywood/Kit/M_Kit_window.uasset | 3 + Game/GigLine/Content/Test/ReviewMap.umap | 2 +- MEMORY.md | 23 ++ Screenshots/demo/review-demo-crossing.png | 4 +- .../demo/review-demo-north-buildings.png | 4 +- Screenshots/demo/review-demo-spawn-view.png | 4 +- Screenshots/demo/review-demo-street-level.png | 4 +- Screenshots/review-character.png | 4 +- Screenshots/review-ground-contact.png | 4 +- Screenshots/review-hero-facade-front.png | 4 +- Screenshots/review-hero-facade-oblique.png | 4 +- Screenshots/review-hero-portals.png | 4 +- Screenshots/review-npc-prefab.png | 4 +- Screenshots/review-overview.png | 4 +- Screenshots/review-player-placement.png | 4 +- Screenshots/review-scale-cubes.png | 4 +- Screenshots/review-shadow-compare.png | 4 +- TODO.md | 2 +- Tools/README.md | 3 + Tools/facade_kit_build.py | 285 ++++++++++++++++++ Tools/ue_apply_facade_kit.py | 210 +++++++++++++ Tools/ue_import_facade_kit.py | 96 ++++++ 46 files changed, 721 insertions(+), 32 deletions(-) create mode 100644 Game/GigLine/Content/Hollywood/Kit/Kit_Coping.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/Kit_Cornice.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/Kit_Door.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/Kit_DoorJamb_L.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/Kit_DoorJamb_R.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/Kit_Lintel.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/Kit_Parapet.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/Kit_Pilaster.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/Kit_ShopFascia.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/Kit_ShopGlass.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/Kit_ShopPier_L.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/Kit_ShopPier_R.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/Kit_Sill.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/Kit_Threshold.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/Kit_WindowBay.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/Kit_WindowGlass.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/M_Kit_cornice.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/M_Kit_door.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/M_Kit_lintel.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/M_Kit_parapet.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/M_Kit_pilaster.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/M_Kit_shopfront.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/M_Kit_sill.uasset create mode 100644 Game/GigLine/Content/Hollywood/Kit/M_Kit_window.uasset create mode 100644 Tools/facade_kit_build.py create mode 100644 Tools/ue_apply_facade_kit.py create mode 100644 Tools/ue_import_facade_kit.py diff --git a/Game/GigLine/Content/Hollywood/DemoMap.umap b/Game/GigLine/Content/Hollywood/DemoMap.umap index 34b7b1e..53c90ff 100644 --- a/Game/GigLine/Content/Hollywood/DemoMap.umap +++ b/Game/GigLine/Content/Hollywood/DemoMap.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0dc9468e94f868cd76893dd1d8e6cd2ef0b365ce3fa86f3feed5f65a630af9cb -size 55066 +oid sha256:cabcd908ff5149ac64773f34092efbdf7df077165db50438aef1d36e846ecc46 +size 939345 diff --git a/Game/GigLine/Content/Hollywood/Kit/Kit_Coping.uasset b/Game/GigLine/Content/Hollywood/Kit/Kit_Coping.uasset new file mode 100644 index 0000000..47b6cdb --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/Kit_Coping.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8c34895de32da94b4d773b3b7be342c4468295401d4701a3bdc93ed923e9f10 +size 72484 diff --git a/Game/GigLine/Content/Hollywood/Kit/Kit_Cornice.uasset b/Game/GigLine/Content/Hollywood/Kit/Kit_Cornice.uasset new file mode 100644 index 0000000..97fd0f2 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/Kit_Cornice.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01b734f5cf0d08395e924b2c2ba5c6b8fdb08041a474ed24f4fb151323b32dd9 +size 72493 diff --git a/Game/GigLine/Content/Hollywood/Kit/Kit_Door.uasset b/Game/GigLine/Content/Hollywood/Kit/Kit_Door.uasset new file mode 100644 index 0000000..b833aa7 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/Kit_Door.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e50c198203e70bc926f17c056978c5c7a93d1705c30600c397e520c4f4c4f42 +size 72369 diff --git a/Game/GigLine/Content/Hollywood/Kit/Kit_DoorJamb_L.uasset b/Game/GigLine/Content/Hollywood/Kit/Kit_DoorJamb_L.uasset new file mode 100644 index 0000000..dfb83cc --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/Kit_DoorJamb_L.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d554e6f5122f5b7ed9888432e5941522cab5c9a28a2c65b0bf5ba6f2b9c24ec +size 72483 diff --git a/Game/GigLine/Content/Hollywood/Kit/Kit_DoorJamb_R.uasset b/Game/GigLine/Content/Hollywood/Kit/Kit_DoorJamb_R.uasset new file mode 100644 index 0000000..089c170 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/Kit_DoorJamb_R.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e4ac06e8d69b569adda3b120944fa41ab8e4ca1a544fa4263d61b9439552319f +size 72492 diff --git a/Game/GigLine/Content/Hollywood/Kit/Kit_Lintel.uasset b/Game/GigLine/Content/Hollywood/Kit/Kit_Lintel.uasset new file mode 100644 index 0000000..58fc7e7 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/Kit_Lintel.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6437b4b48e40b650f9c13db22bd34979361b2da2521e55aedc3000c0ae416fad +size 72449 diff --git a/Game/GigLine/Content/Hollywood/Kit/Kit_Parapet.uasset b/Game/GigLine/Content/Hollywood/Kit/Kit_Parapet.uasset new file mode 100644 index 0000000..ef8a281 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/Kit_Parapet.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96f42acaadd77b069bc6da62c646183ded8c0c8b2f239121a3d737b86a2b113f +size 72482 diff --git a/Game/GigLine/Content/Hollywood/Kit/Kit_Pilaster.uasset b/Game/GigLine/Content/Hollywood/Kit/Kit_Pilaster.uasset new file mode 100644 index 0000000..bea3a6d --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/Kit_Pilaster.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd3a3956c2384b15e38a67fb2b0f350bbf9d073bf204efe282bc84c2605e5331 +size 72535 diff --git a/Game/GigLine/Content/Hollywood/Kit/Kit_ShopFascia.uasset b/Game/GigLine/Content/Hollywood/Kit/Kit_ShopFascia.uasset new file mode 100644 index 0000000..797f05b --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/Kit_ShopFascia.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2fd1b3f2439e60cb7d3578215543cecd60c7aeb2115099268f44dad470bf91d0 +size 72617 diff --git a/Game/GigLine/Content/Hollywood/Kit/Kit_ShopGlass.uasset b/Game/GigLine/Content/Hollywood/Kit/Kit_ShopGlass.uasset new file mode 100644 index 0000000..de30ffd --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/Kit_ShopGlass.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:680337f0ed0e2b8c58495e539d87766d76f688b979908e39f12e1372d137872b +size 72600 diff --git a/Game/GigLine/Content/Hollywood/Kit/Kit_ShopPier_L.uasset b/Game/GigLine/Content/Hollywood/Kit/Kit_ShopPier_L.uasset new file mode 100644 index 0000000..3515bae --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/Kit_ShopPier_L.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e86da75085c4abf9d8b4eea5a9b77f208f0bb811d3418fb33ca5da2febf436de +size 72612 diff --git a/Game/GigLine/Content/Hollywood/Kit/Kit_ShopPier_R.uasset b/Game/GigLine/Content/Hollywood/Kit/Kit_ShopPier_R.uasset new file mode 100644 index 0000000..5d13aea --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/Kit_ShopPier_R.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35a391ad6bd9dbe9be1ac5ec24848d94d91de3831810748b8b7d4ea2dd1dd663 +size 72620 diff --git a/Game/GigLine/Content/Hollywood/Kit/Kit_Sill.uasset b/Game/GigLine/Content/Hollywood/Kit/Kit_Sill.uasset new file mode 100644 index 0000000..2ea504e --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/Kit_Sill.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1dd748d23ab0caaf1b13f3a85b4a2d11a89e2f6cff311b8326750f517a2e59d +size 72365 diff --git a/Game/GigLine/Content/Hollywood/Kit/Kit_Threshold.uasset b/Game/GigLine/Content/Hollywood/Kit/Kit_Threshold.uasset new file mode 100644 index 0000000..546a234 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/Kit_Threshold.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a54520339bb3453b866e7da7fde9bd5675a3af2b641766df47604e328f46ae8 +size 72465 diff --git a/Game/GigLine/Content/Hollywood/Kit/Kit_WindowBay.uasset b/Game/GigLine/Content/Hollywood/Kit/Kit_WindowBay.uasset new file mode 100644 index 0000000..794f4d5 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/Kit_WindowBay.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:601098d4b531b20be1243c06644cc7ddf70e4e4f3d746c6b5efba080fdf593ac +size 72530 diff --git a/Game/GigLine/Content/Hollywood/Kit/Kit_WindowGlass.uasset b/Game/GigLine/Content/Hollywood/Kit/Kit_WindowGlass.uasset new file mode 100644 index 0000000..e82ebd2 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/Kit_WindowGlass.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:304d251f899f05b113f8011b24dcd9090e9c4f83a9d8658db55a3399ba98ddd3 +size 72565 diff --git a/Game/GigLine/Content/Hollywood/Kit/M_Kit_cornice.uasset b/Game/GigLine/Content/Hollywood/Kit/M_Kit_cornice.uasset new file mode 100644 index 0000000..925853a --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/M_Kit_cornice.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb17b057f18958dd2086800f37fe1c951166aaeb0c7f2b842f2ab5cff786849d +size 56606 diff --git a/Game/GigLine/Content/Hollywood/Kit/M_Kit_door.uasset b/Game/GigLine/Content/Hollywood/Kit/M_Kit_door.uasset new file mode 100644 index 0000000..f140901 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/M_Kit_door.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d92a56b405a8eba2db4e31c5603b77195ba297f0dce70e4ab750a63b84ab1155 +size 56549 diff --git a/Game/GigLine/Content/Hollywood/Kit/M_Kit_lintel.uasset b/Game/GigLine/Content/Hollywood/Kit/M_Kit_lintel.uasset new file mode 100644 index 0000000..d80faf0 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/M_Kit_lintel.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b235d479ea427b0bf4e495f4e74bad0853bdc466d8eaf9a116f49f53905a75e +size 56587 diff --git a/Game/GigLine/Content/Hollywood/Kit/M_Kit_parapet.uasset b/Game/GigLine/Content/Hollywood/Kit/M_Kit_parapet.uasset new file mode 100644 index 0000000..2bd7207 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/M_Kit_parapet.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:550e00ba4d59e75e0f5524dc278b34c8c0bd2dae1ab9f9567bed2b3fd820f7c1 +size 56595 diff --git a/Game/GigLine/Content/Hollywood/Kit/M_Kit_pilaster.uasset b/Game/GigLine/Content/Hollywood/Kit/M_Kit_pilaster.uasset new file mode 100644 index 0000000..4a29ddf --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/M_Kit_pilaster.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae26b34392a8f173a82d5936449a93a882b7cec52eb788793447352e59fe8443 +size 56625 diff --git a/Game/GigLine/Content/Hollywood/Kit/M_Kit_shopfront.uasset b/Game/GigLine/Content/Hollywood/Kit/M_Kit_shopfront.uasset new file mode 100644 index 0000000..0e6215f --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/M_Kit_shopfront.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1610ba046a1b76230aad2f6990bcd506eb3ee479248a442f21fee32ee9337612 +size 56647 diff --git a/Game/GigLine/Content/Hollywood/Kit/M_Kit_sill.uasset b/Game/GigLine/Content/Hollywood/Kit/M_Kit_sill.uasset new file mode 100644 index 0000000..6ce3d70 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/M_Kit_sill.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:821549d7ed69a2c726d7e6fa2dafafa72fb45245a5c0792f532cac52be79c5ef +size 56560 diff --git a/Game/GigLine/Content/Hollywood/Kit/M_Kit_window.uasset b/Game/GigLine/Content/Hollywood/Kit/M_Kit_window.uasset new file mode 100644 index 0000000..7066f64 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Kit/M_Kit_window.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d034b7c099db62f88d9a6df23e6aa7843cf4b90689e90e4836011b9b1e1f88a9 +size 56607 diff --git a/Game/GigLine/Content/Test/ReviewMap.umap b/Game/GigLine/Content/Test/ReviewMap.umap index bd50cc5..4341632 100644 --- a/Game/GigLine/Content/Test/ReviewMap.umap +++ b/Game/GigLine/Content/Test/ReviewMap.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06db40a8beab60daaf548554fc70b85eb4cd4ab1f8371b0e8ccd4b7e33c1648d +oid sha256:d0bb3ac4675c0622116d74233da208c1ba97e686e0377e99ef8a77dc49bcbb24 size 28811 diff --git a/MEMORY.md b/MEMORY.md index 5834e6d..17e55eb 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -568,3 +568,26 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. - The script needs `ROOT` in `ue_build_demo_map.py` and `sys` imported; forgetting either fails with `NameError` and an exit code of 0 from the commandlet (always read the result file under /tmp/gigline). + +## 2026-11-09 — façade module kit (0.1.4) + +- `Tools/facade_kit_build.py` builds the kit at the measured **4.96 m bay** and + **3.10 m storey**: pilaster, sill, lintel, window bay, shopfront bay, door bay, + cornice, parapet + coping = 16 meshes, exported per-object with + `fbx_export_preset` plus a JSON part report (dims + evidence per piece). +- `Tools/ue_import_facade_kit.py` → `/Game/Hollywood/Kit/*`, complex-as-simple + collision (a convex hull would fence off the shopfront glazing). +- `Tools/ue_apply_facade_kit.py` lays the kit out from each building's own bbox: + `bays = round(width / 496 cm)`, storeys from height minus the 310 cm ground + floor. **638 pieces** over six frontage buildings + both north buildings. +- **Bug worth remembering:** the first window piece was modelled entirely at +Y + (into the wall), so every window sat *inside* the solid blockout wall and was + invisible in every render while the placement maths looked right. The kit now + projects the surround 3 cm proud with the glazing 6 cm behind its face — a + solid-wall blockout cannot carry a real recess, so the piece has to project. +- Blender gotcha: clearing the scene *including materials* invalidates handles + already taken ("StructRNA of type Material has been removed"); clear objects and + orphan meshes only, and create the material after the clear. +- The kit meshes are spawned at the world origin with their position baked into + the mesh (like the frontage exports), so placement uses the bbox directly and + must not add the actor location. diff --git a/Screenshots/demo/review-demo-crossing.png b/Screenshots/demo/review-demo-crossing.png index ce2ce2d..76af3a1 100644 --- a/Screenshots/demo/review-demo-crossing.png +++ b/Screenshots/demo/review-demo-crossing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b1aeaed19783379067f81e67c4286bb28017c50c4fa713574ac18370c3846bb -size 2755870 +oid sha256:e38ac20db95e026e2a3031dc4a7044eade0052f21790ce24479e72be0668a6a3 +size 2754407 diff --git a/Screenshots/demo/review-demo-north-buildings.png b/Screenshots/demo/review-demo-north-buildings.png index 1c4503e..3748bc7 100644 --- a/Screenshots/demo/review-demo-north-buildings.png +++ b/Screenshots/demo/review-demo-north-buildings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d83115402d0a44b8b025dc8ba35505bba8fc1410ead6f830df8c819d3020e79 -size 2267060 +oid sha256:632f36c7708e60afa8249341ba8e9c92d27dd486d412af50d91f32394db8d523 +size 2262962 diff --git a/Screenshots/demo/review-demo-spawn-view.png b/Screenshots/demo/review-demo-spawn-view.png index 0eaeefd..fdec6f2 100644 --- a/Screenshots/demo/review-demo-spawn-view.png +++ b/Screenshots/demo/review-demo-spawn-view.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:701203ac6afe5d7897e32414bce1bc09dc85cfd747636104ab5b3cad48306d47 -size 2838039 +oid sha256:fbc59fa80c5d6769f88bf120246cd1d18d885264cbccb91bd9e98fa6e1d31f87 +size 2773580 diff --git a/Screenshots/demo/review-demo-street-level.png b/Screenshots/demo/review-demo-street-level.png index fc88e69..770b478 100644 --- a/Screenshots/demo/review-demo-street-level.png +++ b/Screenshots/demo/review-demo-street-level.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2607aaa8e9333dd033745ebbac6b99b31641049e03982d66d5d1a0b1b086de9d -size 2703487 +oid sha256:4a612caffb70624590152784204d17c0816c6e9dd588b2ea9da60f95cc463830 +size 2676882 diff --git a/Screenshots/review-character.png b/Screenshots/review-character.png index de5a517..838d2c8 100644 --- a/Screenshots/review-character.png +++ b/Screenshots/review-character.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d38dc63e0c09483ec15aaa493b483baa8d6d7d7a2678415fef5519561226ebd -size 2540302 +oid sha256:772930bca1b302578fd771a7d8f5be7da82b5c6566f9efe7344561a148de1635 +size 2536185 diff --git a/Screenshots/review-ground-contact.png b/Screenshots/review-ground-contact.png index 113c7d5..6cb7bd5 100644 --- a/Screenshots/review-ground-contact.png +++ b/Screenshots/review-ground-contact.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2797ca6c909c3956795361fcef86dfa8268007ab3df23adf07be807d0f0e073f -size 1750724 +oid sha256:54c81c33f84515be3c947a3b7d1376eefe9e8d017165487441128d95c258add6 +size 1754197 diff --git a/Screenshots/review-hero-facade-front.png b/Screenshots/review-hero-facade-front.png index d290e35..e03c6e8 100644 --- a/Screenshots/review-hero-facade-front.png +++ b/Screenshots/review-hero-facade-front.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f428e43ac9117e9e45a662a89d82f5b636bd8eebd07cad5de8c8c3894a146b09 -size 2298703 +oid sha256:88d440c07d0220e880af430249f1d2eafa3fe780ab7d5367ca1b299761ea9400 +size 2294756 diff --git a/Screenshots/review-hero-facade-oblique.png b/Screenshots/review-hero-facade-oblique.png index d603bef..90665d9 100644 --- a/Screenshots/review-hero-facade-oblique.png +++ b/Screenshots/review-hero-facade-oblique.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae882d2fb120ce45c4bed16c4e5288400bbbe9e565a93f29768e353ebb280870 -size 1515587 +oid sha256:dfd3ad70d01a466316a64f34cde6cf7f699b6e9cb7bdaee6e26873c5c912b557 +size 1510048 diff --git a/Screenshots/review-hero-portals.png b/Screenshots/review-hero-portals.png index b73d080..9569846 100644 --- a/Screenshots/review-hero-portals.png +++ b/Screenshots/review-hero-portals.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:795bffa54e8efc0bb3aafb47a3cfa6282ea41c3a55a0402fb349c2e7ddbedd53 -size 1321845 +oid sha256:05c63be2a22a2a35bd95c7c7467da67f9556d0ea56395149e860476cdf9f34bd +size 1322804 diff --git a/Screenshots/review-npc-prefab.png b/Screenshots/review-npc-prefab.png index f0c8c75..4bb5b3e 100644 --- a/Screenshots/review-npc-prefab.png +++ b/Screenshots/review-npc-prefab.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12ba9d44bb231629abf2e0a607d22e1c7c940b953b56c089efb8810d8ffce88a -size 1736896 +oid sha256:732b96208e25edc36f6e52cc0b214540d07fc6484d0d1cc76df438ce99b050fc +size 1735007 diff --git a/Screenshots/review-overview.png b/Screenshots/review-overview.png index 0a51b2a..e0452d1 100644 --- a/Screenshots/review-overview.png +++ b/Screenshots/review-overview.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d3ccc49c65e6347e273d07fe7cf72839ad3b4b74c37ab9e4e1d92f4020b4390 -size 2614061 +oid sha256:67bb53592ca9e3bfaae5b5feb4d772fdbc474ed3bedd78b81cb999a44aa4a839 +size 2488342 diff --git a/Screenshots/review-player-placement.png b/Screenshots/review-player-placement.png index c80f9f1..00dc1af 100644 --- a/Screenshots/review-player-placement.png +++ b/Screenshots/review-player-placement.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8ad82386c9504b3e2bdd461c22c57e2e9cb19762aaf794f47dd8472ff01f436 -size 2025973 +oid sha256:3c01fcc7ee234597c8451154ebe9b91bf5aa0720d963b1ee5694835aa5133ad2 +size 2030579 diff --git a/Screenshots/review-scale-cubes.png b/Screenshots/review-scale-cubes.png index 71f4bef..9e1bc57 100644 --- a/Screenshots/review-scale-cubes.png +++ b/Screenshots/review-scale-cubes.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3aa35281f616ad86f1d7b2de6a79b0bf3fd476b8c259dc3d3449c6ef7f3dc960 -size 1942628 +oid sha256:945099a7cf7a5f185ab445fdb0c6906353fa3789ec7eb4f8d573632b3c965d18 +size 1935381 diff --git a/Screenshots/review-shadow-compare.png b/Screenshots/review-shadow-compare.png index aa480e0..161d09f 100644 --- a/Screenshots/review-shadow-compare.png +++ b/Screenshots/review-shadow-compare.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7a03d6be117470d86bd351aa35584350140bcf6ff2aae3b35da79e062672a4c -size 1723008 +oid sha256:99b64891c9b211d9990bfbf39b10f3df73ca3be324461550184d9ee4dec5db17 +size 1711185 diff --git a/TODO.md b/TODO.md index 4388110..e22ff48 100644 --- a/TODO.md +++ b/TODO.md @@ -16,7 +16,7 @@ Suggested blender skills: **procedural-modeling** + **geometry-nodes** (the faç ### Patch tasks - active - 01a1b26b-3c94-707f-9665-2f6b4a79b564 (branch `facades-0.1.4`) - [ ] **Finish the Blosm blockout cleaning/detailing (moved from the deferred `0.1.0` item).** Import state: `Art/blender/blockout/blockout-2026-11-06.blend` (86 meshes, EPSG:26911, 0.0 cm affine residual) with cleaning tooling in `Geo/tools/` and `Art/blender/blockout/blosm_blockout.py`. To do here: (a) untextured roofs/facades → **Replace placeholder materials** below; (b) Blosm `setupScript` façade styles → **Give OSM buildings a façade style** below; (c) the 3 of 15 buildings lacking height data are the `0.1.2` placeholder-heights item. -- [ ] **Define the façade module.** One set of reusable pieces (bay, pilaster, cornice, parapet, window, shopfront, door) at a fixed module, exported with the `ue_metric_v1` preset so every building assembles from the same kit. +- [x] **Define the façade module.** **Done 2026-11-09.** `Tools/facade_kit_build.py` builds the kit at the measured **4.96 m bay**: pilaster, sill, lintel, window bay (surround + inset glazing), shopfront bay (piers, glass, fascia), door bay (jambs, door, threshold), cornice and parapet + coping — 16 meshes, each exported through the shared `ue_metric_v1` preset with a JSON part report recording dims and evidence. `Tools/ue_import_facade_kit.py` imports to `/Game/Hollywood/Kit`. `Tools/ue_apply_facade_kit.py` assembles it from each building's own bounding box: **638 pieces placed across the six frontage buildings and both north buildings** (6922 = 13 bays × 11 storeys, theatre 8 × 5, TCL and Tussaud's 9 × 4, etc.). First pass put the window bays inside the solid wall (invisible) — the kit now projects the surround 3 cm proud with glazing 6 cm behind it. - [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. - [ ] **Give OSM buildings a façade style.** Blosm exposes `scene.blosm.setupScript` (`setup/neoclassical.py` is the example) so a building can carry a real elevation instead of a plain extrusion. Build one style script for the south frontage and one for the north. - [ ] **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. diff --git a/Tools/README.md b/Tools/README.md index f54bb8d..97c90cb 100644 --- a/Tools/README.md +++ b/Tools/README.md @@ -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. | diff --git a/Tools/facade_kit_build.py b/Tools/facade_kit_build.py new file mode 100644 index 0000000..c60e778 --- /dev/null +++ b/Tools/facade_kit_build.py @@ -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()) diff --git a/Tools/ue_apply_facade_kit.py b/Tools/ue_apply_facade_kit.py new file mode 100644 index 0000000..b33f1e3 --- /dev/null +++ b/Tools/ue_apply_facade_kit.py @@ -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 -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") diff --git a/Tools/ue_import_facade_kit.py b/Tools/ue_import_facade_kit.py new file mode 100644 index 0000000..4419907 --- /dev/null +++ b/Tools/ue_import_facade_kit.py @@ -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 -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")