From 9a3784683f7bdcf175c5fad1aecee422852c35e7 Mon Sep 17 00:00:00 2001 From: Zwitschi Date: Sun, 8 Nov 2026 19:00:41 +0100 Subject: [PATCH] feat(materials): masonry coursing, glass/banner materials, per-slot assignment, telephoto elevations 0.1.4 materials pass plus the camera fix levelling exposed. Materials (Tools/ue_build_demo_map.py): - ensure_material() gains optional masonry coursing (world-space course height, joint band, joint tint) and roughness variation, so facades read as coursed stone/stucco instead of one flat specular plane. - WorldPosition is in centimetres: course heights in metres are scaled by 100 (the first attempt divided by 0.55 and the coursing was invisible). - connect_material_expressions returns False rather than raising, so every link now goes through a link() helper that logs failures; UE has no MaterialExpressionLess, so the joint mask is Frac -> Divide -> OneMinus -> Clamp. - New materials: M_FacadeStoneShadow, M_FacadeGlass (specular 0.62 ~ IOR 1.5), M_FacadeBanner. - Material assignment is now per SLOT keyed on the imported slot material name. It was slot-0 only, which on the hero is the banner, so stone, shadow stone and glazing still wore flat Blender materials. Hero now 4/4 slots. Cameras (GigLineShotActor + ue_build_review_map): - Optional per-shot ShotFieldOfView; the shot tuples take a 4th element. - Elevations use a 42-55 deg lens from across the street: with the cameras levelled (0.1.2 follow-up) the building was small in frame, and tilting to fill it is what breaks the verticals. hero-facade-front sky 39% -> 20%, demo-spawn-view 23% -> 16%. Screenshots re-rendered for the whole set. --- Game/GigLine/Content/Hollywood/DemoMap.umap | 4 +- .../Content/Hollywood/Site/M_Asphalt.uasset | 2 +- .../Content/Hollywood/Site/M_Concrete.uasset | 2 +- .../Hollywood/Site/M_FacadeBanner.uasset | 3 + .../Hollywood/Site/M_FacadeGlass.uasset | 3 + .../Hollywood/Site/M_FacadeStone.uasset | 4 +- .../Hollywood/Site/M_FacadeStoneShadow.uasset | 3 + .../Content/Hollywood/Site/M_Ground.uasset | 2 +- .../Hollywood/Site/M_KerbConcrete.uasset | 2 +- .../Content/Hollywood/Site/M_RoadPaint.uasset | 2 +- .../Content/Hollywood/Site/M_Stucco.uasset | 4 +- .../Content/Hollywood/Site/Terrain_DEM.uasset | 2 +- Game/GigLine/Content/Test/ReviewMap.umap | 4 +- .../Source/GigLine/GigLineShotActor.cpp | 10 ++ .../GigLine/Source/GigLine/GigLineShotActor.h | 10 ++ MEMORY.md | 25 +++ 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 +- Tools/ue_build_demo_map.py | 147 ++++++++++++++++-- Tools/ue_build_review_map.py | 15 +- 32 files changed, 243 insertions(+), 57 deletions(-) create mode 100644 Game/GigLine/Content/Hollywood/Site/M_FacadeBanner.uasset create mode 100644 Game/GigLine/Content/Hollywood/Site/M_FacadeGlass.uasset create mode 100644 Game/GigLine/Content/Hollywood/Site/M_FacadeStoneShadow.uasset diff --git a/Game/GigLine/Content/Hollywood/DemoMap.umap b/Game/GigLine/Content/Hollywood/DemoMap.umap index 86b0f4d..21ed7b4 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:3d9a16b41394efe0b040d919d1ffa8c9f06b5967cdb4a4306c3a04aa49fdba10 -size 54464 +oid sha256:5544cf164533ce61534d98075bbf41e6fcaa91f340492c82ce10154a75028013 +size 55058 diff --git a/Game/GigLine/Content/Hollywood/Site/M_Asphalt.uasset b/Game/GigLine/Content/Hollywood/Site/M_Asphalt.uasset index ee81ce5..14f0f6c 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_Asphalt.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_Asphalt.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b3b5a17f3a916bb27858051e5ba88b4266a420898128e3fb26577b4ca92ff15 +oid sha256:8f56c5b314d6f7844f2833beefe709676368ccbd2a45ae30f48ec0ae83da4af2 size 7876 diff --git a/Game/GigLine/Content/Hollywood/Site/M_Concrete.uasset b/Game/GigLine/Content/Hollywood/Site/M_Concrete.uasset index 30f1d14..2cc2114 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_Concrete.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_Concrete.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35ef03e01d6cc6aee00d7bdea4063213b1b3f4ec717838f6694dcdfe85e593a8 +oid sha256:2471e65a5e1809aa78abdb6e52de01e72a426ed9c825c2d90e7a0f8bf1dda400 size 7881 diff --git a/Game/GigLine/Content/Hollywood/Site/M_FacadeBanner.uasset b/Game/GigLine/Content/Hollywood/Site/M_FacadeBanner.uasset new file mode 100644 index 0000000..44e5528 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Site/M_FacadeBanner.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:832e10baec763e12727cd6be226ea1fb9ced63aadaf55f25cd5323a7a8fec8c4 +size 8667 diff --git a/Game/GigLine/Content/Hollywood/Site/M_FacadeGlass.uasset b/Game/GigLine/Content/Hollywood/Site/M_FacadeGlass.uasset new file mode 100644 index 0000000..dd46ad6 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Site/M_FacadeGlass.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be4d972ad3655d1c9ec383b69f66c7ead7c8279e9425dfc442dfd7c7393e78a8 +size 8662 diff --git a/Game/GigLine/Content/Hollywood/Site/M_FacadeStone.uasset b/Game/GigLine/Content/Hollywood/Site/M_FacadeStone.uasset index 99136e1..0e63bee 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_FacadeStone.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_FacadeStone.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:092d4108e04ea0a90902249a2ff29480a61ce0001963fdbb7fffa7788facee46 -size 7896 +oid sha256:bf20102e53609e517647200ad44528284dbc5bd26e00d1b41467e3db2ca3c6f1 +size 12131 diff --git a/Game/GigLine/Content/Hollywood/Site/M_FacadeStoneShadow.uasset b/Game/GigLine/Content/Hollywood/Site/M_FacadeStoneShadow.uasset new file mode 100644 index 0000000..f1d76f5 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Site/M_FacadeStoneShadow.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8876871cf45828b2da703e3082bc3bb5a2fb63263958ca5314b4072fe7418ec1 +size 12161 diff --git a/Game/GigLine/Content/Hollywood/Site/M_Ground.uasset b/Game/GigLine/Content/Hollywood/Site/M_Ground.uasset index fea4b51..3317c51 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_Ground.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_Ground.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6bb3fc7e0ba1c40897838d8ab01b053f467da130e54c19a91e46b61a15b09e2b +oid sha256:adb033875c629572815f0d27b94a16bdc3b869344419ad7343b59c8ccdc1e850 size 7871 diff --git a/Game/GigLine/Content/Hollywood/Site/M_KerbConcrete.uasset b/Game/GigLine/Content/Hollywood/Site/M_KerbConcrete.uasset index a3c6748..1becb71 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_KerbConcrete.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_KerbConcrete.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef17b5d0ed660a13a60b294a9d36ab99787336b2655e4f350fcc2780e5fc5d04 +oid sha256:715ec7945cf7eb69f07755252ccc4b39ea1cbfad1a55eddb99616517ef5def42 size 7901 diff --git a/Game/GigLine/Content/Hollywood/Site/M_RoadPaint.uasset b/Game/GigLine/Content/Hollywood/Site/M_RoadPaint.uasset index 7e38022..91ddd02 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_RoadPaint.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_RoadPaint.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60d614ddb19422a8fcca5474f731ef5640f7d7d60c6ef59780914c15f8367388 +oid sha256:dcfda0b94f7141564391069629682cac71c9152100038a5e927a2355c89c869a size 7886 diff --git a/Game/GigLine/Content/Hollywood/Site/M_Stucco.uasset b/Game/GigLine/Content/Hollywood/Site/M_Stucco.uasset index de5a3bb..9f20fe3 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_Stucco.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_Stucco.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38bbe13637e5a27860b2b0cb2d75d9802446a8ee481825de46bd04bbf0b6f586 -size 7871 +oid sha256:940807fd433a5eab6726c830b4c54874a4acf3c352e415e6de71f0f6ed1b4ede +size 8637 diff --git a/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset b/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset index 263ef74..1dc6b23 100644 --- a/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22e56a42763de7a73b4d8974a3e8709d89ebdc666bde2ce5d21dea699cee3198 +oid sha256:a7b7c866693db2f80334baeb66af3ff2585368a503af806c3d78cae71f328b17 size 357901 diff --git a/Game/GigLine/Content/Test/ReviewMap.umap b/Game/GigLine/Content/Test/ReviewMap.umap index a36c204..14c9200 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:3c34637f8b929cab7a68725f550f65ebdfac790cb5af5e2349ef661dccd8faa0 -size 28706 +oid sha256:6bef39374cc710661ddf2f15194d28b298bfa0d0adcf25668740712381c2ba29 +size 28811 diff --git a/Game/GigLine/Source/GigLine/GigLineShotActor.cpp b/Game/GigLine/Source/GigLine/GigLineShotActor.cpp index ba0cc15..878011c 100644 --- a/Game/GigLine/Source/GigLine/GigLineShotActor.cpp +++ b/Game/GigLine/Source/GigLine/GigLineShotActor.cpp @@ -1,6 +1,7 @@ #include "GigLineShotActor.h" #include "Camera/CameraActor.h" +#include "Camera/CameraComponent.h" #include "Engine/World.h" #include "GameFramework/PlayerController.h" #include "UnrealClient.h" @@ -83,6 +84,15 @@ void AGigLineShotActor::NextShot() Params.ObjectFlags |= RF_Transient; CurrentCamera = GetWorld()->SpawnActor( ACameraActor::StaticClass(), ShotLocations[CurrentShot], ShotRotations[CurrentShot], Params); + if (ShotFieldOfView.IsValidIndex(CurrentShot) && ShotFieldOfView[CurrentShot] > 0.0f) + { + if (UCameraComponent* Cam = CurrentCamera->GetCameraComponent()) + { + // Long lens for elevations: fills the frame from across the street + // without pitching the camera, so verticals stay parallel. + Cam->SetFieldOfView(ShotFieldOfView[CurrentShot]); + } + } PlayerController->SetViewTarget(CurrentCamera); SettleCountdown = SettleSeconds; diff --git a/Game/GigLine/Source/GigLine/GigLineShotActor.h b/Game/GigLine/Source/GigLine/GigLineShotActor.h index 9aa0777..4578fca 100644 --- a/Game/GigLine/Source/GigLine/GigLineShotActor.h +++ b/Game/GigLine/Source/GigLine/GigLineShotActor.h @@ -36,6 +36,16 @@ public: UPROPERTY(EditAnywhere, BlueprintReadWrite) TArray ShotNames; + /** + * Optional per-shot field of view (degrees), same index as ShotLocations. + * Zero or missing entries keep the engine default (90 deg). Elevation shots + * taken from across the street need a long lens: a level camera 45 m out with + * the default FOV leaves the building small in frame, and tilting it up to fill + * the frame is what breaks the verticals. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite) + TArray ShotFieldOfView; + /** Seconds to let the view target settle before the capture. Resolution comes from the runner's -ResX/-ResY. */ UPROPERTY(EditAnywhere, BlueprintReadWrite) float SettleSeconds = 0.35f; diff --git a/MEMORY.md b/MEMORY.md index a126f8c..1006f57 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -525,3 +525,28 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. usage and a **traps** table; `setup.md` marks the fresh-checkout build verified and documents the two optional API keys; architecture 07 + combined doc corrected the build-output path. + +## 2026-11-09 — 0.1.4 materials + architecture-camera pass + +- **Material graphs**: `ensure_material()` now supports masonry coursing + (`courses_m`) and roughness variation. **UE's `WorldPosition` is in + CENTIMETRES**, so a course height in metres must be ×100 — the first attempt + divided by 0.55 and produced coursing 100× too fine to see. +- **`connect_material_expressions` returns False instead of raising**, so an + unknown pin name leaves an input unconnected and the material silently compiles + with a default (a mask of 0 is invisible). All links now go through a `link()` + helper that logs failures. There is also **no `MaterialExpressionLess`**; use + `Frac → Divide → OneMinus → Clamp` for a soft threshold band, or + `MaterialExpressionStep`/`SmoothStep` if a hard one is wanted. +- **Material assignment was slot-0-only**, which on the hero is the *banner* — the + stone, shadowed stone and glazing kept their flat Blender materials. Assignment + is now per slot, keyed on the imported slot material's name + (`glass`/`banner`/`stoneshadow`/`stone`/`roof`/`wall`). Hero: 4/4 slots. +- New materials: `M_FacadeStoneShadow` (same coursing, a stop darker), + `M_FacadeGlass` (roughness 0.14, specular 0.62 ≈ IOR 1.5 expressed without a + refraction pass), `M_FacadeBanner` (the one saturated surface). +- **Camera FOV**: levelling the elevation cameras left the building small in frame + (39 % sky). Added optional per-shot `ShotFieldOfView` to `AGigLineShotActor` and + a 4th element on the shot tuples; a long lens (42–55°) fills the frame from + across the street while the camera stays level, so verticals stay parallel. + `review-hero-facade-front` sky 39 → 20 %, `demo-spawn-view` 23 → 16 %. diff --git a/Screenshots/demo/review-demo-crossing.png b/Screenshots/demo/review-demo-crossing.png index 72ccf7a..4476642 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:380647ec83dcdc1d1796a3f6a033dfcb86d634c8312faf0c4332c795a12dcc81 -size 2592475 +oid sha256:a21bd2e7d32099ee8490de63551e817e144ce74cade34dbf7854bfc125a899d0 +size 2756050 diff --git a/Screenshots/demo/review-demo-north-buildings.png b/Screenshots/demo/review-demo-north-buildings.png index 05d5445..83da020 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:f01bd1af993306a5de88397e6f0bcc79f64f0167b06353e54b8cc0719ff4881a -size 2261865 +oid sha256:f93538fc3aa3d3b0a83c5b0ba9637445ef66a2fba845e0dd3f39fc04ddac06ac +size 2265651 diff --git a/Screenshots/demo/review-demo-spawn-view.png b/Screenshots/demo/review-demo-spawn-view.png index ad2ad70..4a928e0 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:63d49011bcf0ab52b96c4ec7b27eeba3d27de3bbbf951a8f8e0ac09117995f3e -size 2628687 +oid sha256:d4a781b779e59facdcd0275cc147ea4f9a5cf9f36c9061c400eca79f69aceb2d +size 2843274 diff --git a/Screenshots/demo/review-demo-street-level.png b/Screenshots/demo/review-demo-street-level.png index f07d483..2da3f96 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:d87896644e71e7a66b18fb9997d76d51a05cce5fa6fc97e3f698a45258d0b306 -size 2468639 +oid sha256:744bb6b3ddcf3130e11d93c1ba6eb0b71e5968629d2517401cd36ecc74e020c8 +size 2704752 diff --git a/Screenshots/review-character.png b/Screenshots/review-character.png index 82f9181..d1c2dc1 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:44df56de845bde433eb1d6de2294462c9933d8f6a800a4f3cdc01793c5b1497b -size 2528497 +oid sha256:e794bb187f5f1cd27cfcdf89a504ce22a33a529cc3244ba5381af930c33e778d +size 2539235 diff --git a/Screenshots/review-ground-contact.png b/Screenshots/review-ground-contact.png index 4dbada6..7c5785b 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:9043e2e90ffead898262bae5c6f8ad0bbc700c5f52aa398ae6f486a858ac4af0 -size 1749002 +oid sha256:586c717e6066aed87c6f7caf1f566789c5ecf17cecf786cd7894a8e4d75afeb8 +size 1756397 diff --git a/Screenshots/review-hero-facade-front.png b/Screenshots/review-hero-facade-front.png index d523315..ccb004b 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:c8c70fa7806c12a0cdbf2196b5f52f82f402e1b806f067e48a98970a94393e41 -size 1436443 +oid sha256:e6ad9425e1cda90135358752c655a70a08e0d449629d1f5075b31fcc272eb601 +size 2297053 diff --git a/Screenshots/review-hero-facade-oblique.png b/Screenshots/review-hero-facade-oblique.png index 6428303..410d1ff 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:c53e905de74ffb3783f2895ed667e352179b7bc411246b3b5bcdae88a5b299f9 -size 1306447 +oid sha256:9731498fc8054b94de923cb311f38c31be95af642786bf64f6015db357823559 +size 1516067 diff --git a/Screenshots/review-hero-portals.png b/Screenshots/review-hero-portals.png index c54cf5b..4f8adb4 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:f7455b6cc2a5a81ed1848956c41555a7775b705c24a34a0cbd8e0d0717e8d499 -size 1321848 +oid sha256:8d02821b21a4b61fed206bb5a8f38ff3b1ab27a0cc16e26cfd0639eb1f5a32ab +size 1327638 diff --git a/Screenshots/review-npc-prefab.png b/Screenshots/review-npc-prefab.png index 74c6464..38979c2 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:99501ac2edfa22a64a7156fbc298930db64997008d7534189f0ee9a208b9506a -size 1735511 +oid sha256:c8e3664310a7e70ff816b8e677f2d6c4ce2a31bb8e0f07e048f9df3467be4de0 +size 1737260 diff --git a/Screenshots/review-overview.png b/Screenshots/review-overview.png index 0da3826..2b947d0 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:f40eda489a19e01f3e0e6428940e4da7ee6865c7397e4e9bbe64f6e7a84b4af0 -size 2488592 +oid sha256:c028e5e01bf9d46f33b7a903f50923ccaee537112d9bb519bb4376766ca6beff +size 2619516 diff --git a/Screenshots/review-player-placement.png b/Screenshots/review-player-placement.png index 102d17f..8b45239 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:f66e9a9e157c4663b971d15dcdfc8d3b67d64584c9f92a0d5ff207aac7637d37 -size 2019699 +oid sha256:a4a003f484e9e048c1c73f6d5bfac87c1db332b1aae02eddf7f7c913af098a9a +size 2027282 diff --git a/Screenshots/review-scale-cubes.png b/Screenshots/review-scale-cubes.png index 3d27366..0ce7ec0 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:be833e2571fa2311499bbd806d32984c4a0b7bcaa33af2ff362fc6c94f553bff -size 1937905 +oid sha256:f8026dd45a8fdb6580c3a0e82aad50518a1d0b27fc9a2a93e419924df651c72e +size 1938060 diff --git a/Screenshots/review-shadow-compare.png b/Screenshots/review-shadow-compare.png index 3888105..1b0cd7a 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:75c80b08b7905b9e6c9df673151c2b0d11362c3a433b402e22240b66c647181b -size 1726246 +oid sha256:dcb80cdfaca40ab3ec68df835e1c31aa66ac8f24bea0eb44e928946b2a9b378d +size 1752868 diff --git a/Tools/ue_build_demo_map.py b/Tools/ue_build_demo_map.py index c7d907a..4dcd7bb 100644 --- a/Tools/ue_build_demo_map.py +++ b/Tools/ue_build_demo_map.py @@ -65,6 +65,13 @@ def ensure_material(mat_name, spec): spec keys: dark, bright (RGB tuples), roughness, specular, noise_scale (world units per noise cycle; smaller = larger features). + + Optional `courses_m` adds **masonry coursing**: horizontal joint lines at a + given course height in world metres, plus a per-course horizontal offset so + the vertical joints stagger like real ashlar. Optional `joint_tint` is the + colour the joints darken toward, and `roughness_variation` lerps roughness + between `roughness` and a rougher value using the same noise, so the surface + does not read as one flat specular value. """ path = f"/Game/Hollywood/Site/{mat_name}" if unreal.EditorAssetLibrary.does_asset_exist(path): @@ -88,6 +95,20 @@ def ensure_material(mat_name, spec): n.set_editor_property("r", float(value)) return n + def link(src, dst, pin=""): + """Connect, and say so when it fails. + + `connect_material_expressions` returns False rather than raising, so an + unknown pin name silently leaves an input unconnected and the material + compiles with a default value — a coursing mask of 0 is invisible, which + is exactly how the first attempt failed. + """ + ok = lib.connect_material_expressions(src, "", dst, pin) + if not ok: + log(f"material {mat_name}: FAILED to connect {src.__class__.__name__} " + f"-> {dst.__class__.__name__}.{pin or '(default)'}") + return ok + # world position -> noise -> lerp(dark, bright) -> base colour wp = node(unreal.MaterialExpressionWorldPosition) scale = scalar(spec.get("noise_scale", 0.004)) @@ -108,10 +129,57 @@ def ensure_material(mat_name, spec): lib.connect_material_expressions(c3(spec["dark"]), "", lerp, "A") lib.connect_material_expressions(c3(spec["bright"]), "", lerp, "B") lib.connect_material_expressions(noise, "", lerp, "Alpha") - lib.connect_material_property(lerp, "", unreal.MaterialProperty.MP_BASE_COLOR) - lib.connect_material_property( - scalar(spec.get("roughness", 0.85)), "", unreal.MaterialProperty.MP_ROUGHNESS) + # --- optional masonry coursing ----------------------------------------- + # A world-space course mask: the fractional part of Z / course height is a + # sawtooth per course; zeroing it gives a joint line every course. The row + # index shifts each course sideways by half a brick, so vertical joints + # stagger. All of it is world-space, so every building shares the coursing + # without needing its own UVs. + base_color_expr = lerp + if spec.get("courses_m"): + # WorldPosition is in CENTIMETRES, so a course height given in metres must + # be scaled by 100 here; getting this wrong makes the coursing 100x too + # fine to see at all (the first attempt did exactly that). + course_cm = float(spec["courses_m"]) * 100.0 + row = node(unreal.MaterialExpressionDivide) + link(wp, row, "A") + link(c3((course_cm, course_cm, course_cm)), row, "B") + saw = node(unreal.MaterialExpressionFrac) + link(row, saw) + # Joint mask without a comparison node (UE has no Less): 1 - saw/joint is + # 1 at the course boundary and 0 once past the joint width; clamped to 0..1 + # so it is a linear band rather than a hard step. + div = node(unreal.MaterialExpressionDivide) + link(saw, div, "A") + # joint_width is a fraction of one course (0.11 ~= a 6 cm joint on a + # 55 cm course), which is the space `saw` lives in after the divide. + link(scalar(spec.get("joint_width", 0.11)), div, "B") + inv = node(unreal.MaterialExpressionOneMinus) + link(div, inv) + joint = node(unreal.MaterialExpressionClamp) + link(inv, joint) + tinted = node(unreal.MaterialExpressionLinearInterpolate) + link(lerp, tinted, "A") + link(c3(spec.get("joint_tint", (0.34, 0.31, 0.27))), tinted, "B") + link(joint, tinted, "Alpha") + base_color_expr = tinted + + lib.connect_material_property(base_color_expr, "", unreal.MaterialProperty.MP_BASE_COLOR) + + # Roughness: lerp(base, base + variation, noise) so the highlight breaks up. + variation = float(spec.get("roughness_variation", 0.0)) + if variation > 0.0: + rough_lerp = node(unreal.MaterialExpressionLinearInterpolate) + lib.connect_material_expressions( + scalar(spec.get("roughness", 0.85)), "", rough_lerp, "A") + lib.connect_material_expressions( + scalar(min(1.0, spec.get("roughness", 0.85) + variation)), "", rough_lerp, "B") + lib.connect_material_expressions(noise, "", rough_lerp, "Alpha") + lib.connect_material_property(rough_lerp, "", unreal.MaterialProperty.MP_ROUGHNESS) + else: + lib.connect_material_property( + scalar(spec.get("roughness", 0.85)), "", unreal.MaterialProperty.MP_ROUGHNESS) try: lib.connect_material_property( scalar(spec.get("specular", 0.3)), "", unreal.MaterialProperty.MP_SPECULAR) @@ -278,10 +346,35 @@ def build(): roughness=0.98, specular=0.15, noise_scale=0.003), "M_RoadPaint": dict(dark=(0.74, 0.74, 0.72), bright=(0.86, 0.86, 0.84), roughness=0.62, specular=0.35, noise_scale=0.02), - "M_FacadeStone": dict(dark=(0.62, 0.58, 0.50), bright=(0.74, 0.70, 0.62), - roughness=0.72, specular=0.32, noise_scale=0.005), - "M_Stucco": dict(dark=(0.52, 0.48, 0.42), bright=(0.66, 0.62, 0.55), - roughness=0.86, specular=0.25, noise_scale=0.004), + # Ashlar coursing at 0.55 m (a 0.10 m band joint), the reading the 2009 + # facade photograph gives for the hero's stone; roughness breaks up so the + # wall is not one specular plane. + "M_FacadeStone": dict(dark=(0.62, 0.58, 0.50), bright=(0.76, 0.72, 0.64), + roughness=0.70, specular=0.32, noise_scale=0.005, + courses_m=0.55, joint_width=0.11, + joint_tint=(0.40, 0.37, 0.32), roughness_variation=0.12), + # Rendered/stucco frontage: no coursing, but a fine tooth and a wider + # roughness range than before, because flat stucco is what read as plastic. + "M_Stucco": dict(dark=(0.50, 0.46, 0.40), bright=(0.68, 0.64, 0.57), + roughness=0.76, specular=0.25, noise_scale=0.0025, + roughness_variation=0.20), + # Stone in shadow: the hero's recessed wall behind the colonnade. Same + # coursing as the face, a stop darker, so depth reads without a light. + "M_FacadeStoneShadow": dict(dark=(0.40, 0.37, 0.32), bright=(0.52, 0.49, 0.43), + roughness=0.78, specular=0.26, noise_scale=0.005, + courses_m=0.55, joint_width=0.11, + joint_tint=(0.28, 0.26, 0.23), + roughness_variation=0.10), + # Glazing: low roughness, strong specular. A plausible IOR for glass sits + # near 1.5, which UE expresses through specular ~0.5-0.6 on an opaque + # surface (there is no refraction pass at blockout fidelity). + "M_FacadeGlass": dict(dark=(0.030, 0.045, 0.055), bright=(0.075, 0.095, 0.110), + roughness=0.14, specular=0.62, noise_scale=0.01, + roughness_variation=0.06), + # The JIMMY KIMMEL banners: the one saturated surface on the elevation. + "M_FacadeBanner": dict(dark=(0.32, 0.05, 0.07), bright=(0.55, 0.10, 0.12), + roughness=0.72, specular=0.22, noise_scale=0.02, + roughness_variation=0.12), } SURFACE_MATERIALS = { "Terrain_DEM": "M_Ground", @@ -337,15 +430,45 @@ def build(): LABEL_MATERIALS = {"Hero_Facade": "M_FacadeStone", "North_TCL_Chinese": "M_Stucco", "North_Madame_Tussauds": "M_Stucco"} + + # Assign per SLOT, by what the slot's imported material is called, not just + # slot 0: the hero mesh's slot 0 is the banner, so the stone, the shadowed + # stone and the glazing were all still wearing their flat Blender materials. + SLOT_RULES = ( + ("glass", "M_FacadeGlass"), + ("banner", "M_FacadeBanner"), + ("stoneshadow", "M_FacadeStoneShadow"), + ("stone", "M_FacadeStone"), + ("roof", "M_Concrete"), + ("wall", "M_Stucco"), + ) + + def slot_material_name(slot_mesh_material): + if not slot_mesh_material: + return None + key = slot_mesh_material.get_name().lower() + for needle, target in SLOT_RULES: + if needle in key: + return target + return None + assigned = 0 for actor in unreal.EditorLevelLibrary.get_all_level_actors(): - mat_name = LABEL_MATERIALS.get(actor.get_actor_label()) - if not mat_name or mat_name not in built: - continue + fallback = LABEL_MATERIALS.get(actor.get_actor_label()) comp = actor.get_component_by_class(unreal.StaticMeshComponent) - if comp and comp.static_mesh: - comp.set_material(0, built[mat_name]) + if not comp or not comp.static_mesh: + continue + slots = comp.static_mesh.get_editor_property("static_materials") + touched = 0 + for index, slot in enumerate(slots): + mesh_mat = slot.get_editor_property("material_interface") + target = slot_material_name(mesh_mat) or fallback + if target and target in built: + comp.set_material(index, built[target]) + touched += 1 + if touched: assigned += 1 + log(f"materials {actor.get_actor_label()}: {touched}/{len(slots)} slots") log(f"assigned look materials to {assigned} facade/background actors") # --- surrounding block frontage --------------------------------------- diff --git a/Tools/ue_build_review_map.py b/Tools/ue_build_review_map.py index 583c037..cf570bc 100644 --- a/Tools/ue_build_review_map.py +++ b/Tools/ue_build_review_map.py @@ -240,9 +240,9 @@ try: # verticals stay parallel. The old shot looked up ~4 deg from 4.2 m at a # 7.6 m target, which converged the columns — the one thing an # architectural elevation must not do (archviz: "straight verticals"). - ("hero-facade-front", unreal.Vector(hero_x + 200, -3200, 760), unreal.Vector(hero_x, hero_y + 400, 760)), + ("hero-facade-front", unreal.Vector(hero_x + 200, -3200, 760), unreal.Vector(hero_x, hero_y + 400, 760), 42.0), # oblique from the north-west, reading the colonnade in depth - ("hero-facade-oblique", unreal.Vector(hero_x - 2200, -3400, 620), unreal.Vector(hero_x + 300, hero_y, 700)), + ("hero-facade-oblique", unreal.Vector(hero_x - 2200, -3400, 620), unreal.Vector(hero_x + 300, hero_y, 700), 55.0), ] # --- review shot cameras + save ------------------------------------------- # Order matters: the review scene is staged in the default (unsaved) world. @@ -256,6 +256,12 @@ try: "shot_rotations", [unreal.MathLibrary.find_look_at_rotation(c[1], c[2]) for c in cams]) shots.set_editor_property("shot_names", [c[0] for c in cams]) + # Optional 4th element: field of view in degrees. 0 keeps the default 90. + # Elevations shot from across the street use a long lens so the building + # fills the frame without pitching the camera (archviz: straight + # verticals, telephoto for elevations). + shots.set_editor_property( + "shot_field_of_view", [float(c[3]) if len(c) > 3 else 0.0 for c in cams]) shots.set_editor_property("quit_when_done", True) log(f"staged: ShotSequencer with {len(cams)} cameras: {', '.join(c[0] for c in cams)}") @@ -282,7 +288,7 @@ try: # the colonnade's underside. # Level horizon for the same reason: a block elevation is read by its # verticals, and a pitched camera makes them lean. - ("demo-spawn-view", unreal.Vector(-13336, -3100, 620), unreal.Vector(-13336, 1300, 620)), + ("demo-spawn-view", unreal.Vector(-13336, -3100, 620), unreal.Vector(-13336, 1300, 620), 48.0), # Street level, from the north sidewalk: the old camera sat at Y -400, # inside the facade body (which spans Y -1457..-200), so it looked at # the unlit back face. Y -2600 is on the sidewalk in front of the lit @@ -309,6 +315,9 @@ try: seq.set_editor_property("shot_rotations", [unreal.MathLibrary.find_look_at_rotation(c[1], c[2]) for c in demo_shots]) seq.set_editor_property("shot_names", [c[0] for c in demo_shots]) + seq.set_editor_property( + "shot_field_of_view", + [float(c[3]) if len(c) > 3 else 0.0 for c in demo_shots]) # quit after the demo shots so a dedicated DemoMap capture run # terminates cleanly (render_screenshots.sh runs both maps). seq.set_editor_property("quit_when_done", True)