diff --git a/Art/blender/hero/hero-facade-2026-11-07.blend b/Art/blender/hero/hero-facade-2026-11-07.blend index 8d91f5a..0147c6f 100644 --- a/Art/blender/hero/hero-facade-2026-11-07.blend +++ b/Art/blender/hero/hero-facade-2026-11-07.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cadc2469e3e9fb41dc379a1c61489054533819478fa74befecaa37adcfdadd97 -size 158985 +oid sha256:22d94a59b1ee5d77a72ee2b2096a0a73de3fc594a6e7d80cd46fbb05faa71e9e +size 208756 diff --git a/Art/blender/hero/hero-facade-2026-11-07.json b/Art/blender/hero/hero-facade-2026-11-07.json index 00d70b6..12420c1 100644 --- a/Art/blender/hero/hero-facade-2026-11-07.json +++ b/Art/blender/hero/hero-facade-2026-11-07.json @@ -37,6 +37,15 @@ ], "evidence": "6 columns at 4.96 m centres (sibling notes x measured frontage)" }, + { + "object": "Hero_Flute_x120", + "dims_m": [ + 0.064, + 0.064, + 6.53 + ], + "evidence": "photographs: fluted shafts; 20 grooves per column modelled as shallow cylinders in the shadow tone rather than boolean cut grooves" + }, { "object": "Hero_ColumnBase_x6", "dims_m": [ @@ -104,10 +113,10 @@ "object": "Hero_MottoBand", "dims_m": [ 22.75, - 0.08, + 0.1, 0.85 ], - "evidence": "photographs: motto panel above the colonnade" + "evidence": "photographs: motto panel above the colonnade, now an inset panel with a moulded surround rather than a flat stripe" }, { "object": "Hero_Medallion_x2", diff --git a/CHANGELOG.md b/CHANGELOG.md index e188b4e..6753db6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,42 @@ Format: [Keep a Changelog](https://keepachangelog.com/) style, versions follow [ Nothing yet. +## [0.1.5] — 2026-11-09 + +Façade finishing. The kit gets the hero detail it was missing, and a class of silent defect is found and gated: the map had been rendering with **604 of 620 mesh actors unmaterialed** without a single error. + +### Added + +- **Scene audit** (`Tools/ue_audit_scene.py`): fails when any staged mesh has no material, when a frontage building is more than ±1 m off its verified footprint, or when two actors share a label. Wired into `Tools/render_screenshots.sh`, which now runs the whole staging chain in the correct order and stops on an audit failure. +- **`Tools/ue_placement.py`**: one source of truth for staged positions and the verified frontage spans, with `span_error` / `describe_span_failure` validators. +- **Hero fluting**: 20 grooves per column as shallow shadow-tone cylinders (120 pieces) rather than boolean cuts. +- **Presence of references for the retail run**: `Art/reference/retail-neighbours/` (6800 Lee Drug and its neighbour). Local only, not committed. + +### Changed + +- **Motto band is a real recess** now — moulded surround 6 cm proud, panel 12 cm behind — instead of an 8 cm dark stripe. Hero: 148 parts / 3 044 tris → 271 / 5 492, still 14.50 m. +- **Kit layout for wide and tall buildings**: bays are capped at 9 with the module widened (6922 was 13 bays across 65.8 m — a window every 4.96 m on a 37 m block), and the storey spread is derived from the available height so the top band always lands inside the volume. + +### Fixed + +- **604 of 620 mesh actors rendered untextured.** `ensure_material` deleted and recreated each site material on every run, orphaning every reference held by the kit pieces and the theatre trim; the map looked only slightly flat. Materials are now updated in place. +- **The hero could be teleported 13 m east onto the wrong side of the boulevard.** `ue_hero_facade_import.py` carried its own copy of the pre-`0.1.0` hero position; running the importer alone moved the building, invisible in a render and visible only as a reference-score drop. Placement now has one definition and the import asserts the staged span (`span ok: X -14644..-12028`). +- **53 duplicated kit actor labels** (both shop piers of a bay shared one name). + +### Verified + +- Scene audit: **0 failures** — every mesh materialed, all 7 verified spans within tolerance, no duplicate labels. +- Packaged `-qaselftest`: walk, reset, pause/unpause, seam transect 1456 samples / 0 holes, wall probe 0/35, façade probe 0/9 — all PASS. +- **1440p re-measured with the kit**: 2.76 ms average (**363 fps**), p99 3.64 ms, against `0.1.0`'s 2.42 ms (414 fps) — the 625 kit actors and 8 textures cost about 12 % and leave roughly six times the 60 fps floor. Series at `Docs/perf/0.1.5-kit-walk-frametimes.csv`. +- 78 tool tests pass. + +### Known limitations + +- **Retail neighbours 6822 and 6806–6820 are not modelled**: Commons holds no usable reference for either, and substituting the El Capitan Theatre's photographs would misattribute their façades. Tracked in `TODO.md`. +- Cornice dentil spacing, abacus and plinth dimensions remain recorded assumptions. +- Textures are generated, not authored — no decals, signage text or weathering maps. +- The vision pre-check stays advisory; it has rejected shots the owner approved. + ## [0.1.4] — 2026-11-09 Building façades. The block goes from flat extruded boxes with flat colours to coursed, textured surfaces assembled from a reusable module kit, and the El Capitan Theatre gets the marquee and blade sign that make it read as itself. @@ -172,7 +208,8 @@ The first playable release: a walkable exterior demo of the **6840 Hollywood Blv - Kerb height (0.13 m as built vs 0.15 m assumed), kerb radii, driveway aprons and graded crossing ramps are deferred to `0.1.1`; crossing ramps are kerb flushes. - One block only; no vehicles, NPCs, audio, or settings UI yet. -[Unreleased]: https://git.allucanget.biz/zwitschi/gig-line/compare/v0.1.4...main +[Unreleased]: https://git.allucanget.biz/zwitschi/gig-line/compare/v0.1.5...main +[0.1.5]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.5 [0.1.4]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.4 [0.1.3]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.3 [0.1.2]: https://git.allucanget.biz/zwitschi/gig-line/releases/tag/v0.1.2 diff --git a/Docs/build.md b/Docs/build.md index accf4a7..e6dd2da 100644 --- a/Docs/build.md +++ b/Docs/build.md @@ -86,8 +86,13 @@ The hero block and the demo level are built by a fixed sequence. Each step write | North import | `Tools/ue_import_north_buildings.py` | `/Game/Hollywood/North/*` | | Hero import | `Tools/ue_hero_facade_import.py` | `/Game/Hollywood/ElCapitan/SM_HeroFacade` | | Demo map | `Tools/ue_build_demo_map.py` | `/Game/Hollywood/DemoMap` | +| Façade kit assembly | `Tools/ue_apply_facade_kit.py` | kit actors in `DemoMap` | +| Theatre trim | `Tools/ue_stage_theatre_trim.py` | marquee + blade sign actors | +| Scene audit | `Tools/ue_audit_scene.py` | materials, placement spans, labels (gate) | | Review shots | `Tools/ue_build_review_map.py` + `Tools/render_screenshots.sh` | `Screenshots/` | +**Order matters.** `ue_build_demo_map.py` rebuilds the site materials, so any actor staged by another tool must be (re)staged _after_ it, or it keeps a reference to a material that no longer exists — that is how 604 actors once ended up untethered from their materials while the map only looked slightly flat. `Tools/render_screenshots.sh` runs the full chain in the right order and gates on `ue_audit_scene.py`. + Every Blender exporter calls [`Tools/fbx_export_preset.py`](../Tools/fbx_export_preset.py) (`ue_metric_v1` / `ue_character_v1`) instead of repeating the FBX argument block — one place for the scale conversion, the axis mapping and `mesh_smooth_type="FACE"` (without which every Unreal import warns about missing smoothing groups and round geometry arrives faceted). Two rules that the pipeline enforces rather than documents: diff --git a/Docs/perf/0.1.5-kit-walk-frametimes.csv b/Docs/perf/0.1.5-kit-walk-frametimes.csv new file mode 100644 index 0000000..1b6a112 --- /dev/null +++ b/Docs/perf/0.1.5-kit-walk-frametimes.csv @@ -0,0 +1,728 @@ +frame,ms +0,2.599 +1,2.877 +2,2.709 +3,2.646 +4,3.168 +5,2.729 +6,2.524 +7,2.726 +8,2.639 +9,2.814 +10,2.819 +11,2.708 +12,2.822 +13,2.703 +14,2.365 +15,2.796 +16,2.899 +17,2.972 +18,2.908 +19,2.356 +20,2.781 +21,2.574 +22,3.148 +23,2.306 +24,2.668 +25,2.855 +26,2.588 +27,3.321 +28,3.419 +29,3.121 +30,3.391 +31,2.355 +32,2.679 +33,3.463 +34,2.614 +35,2.922 +36,2.429 +37,2.830 +38,2.842 +39,2.782 +40,3.252 +41,2.731 +42,2.577 +43,2.363 +44,2.786 +45,2.547 +46,2.594 +47,2.652 +48,2.836 +49,2.792 +50,2.552 +51,2.792 +52,2.718 +53,3.333 +54,2.165 +55,2.880 +56,2.613 +57,2.460 +58,2.919 +59,2.770 +60,2.551 +61,2.622 +62,2.712 +63,2.818 +64,3.155 +65,2.472 +66,2.861 +67,2.651 +68,2.573 +69,2.892 +70,2.689 +71,2.729 +72,2.668 +73,2.753 +74,2.812 +75,2.606 +76,2.686 +77,2.842 +78,2.660 +79,2.903 +80,2.486 +81,2.591 +82,3.626 +83,2.455 +84,2.475 +85,2.922 +86,2.603 +87,2.728 +88,3.057 +89,2.731 +90,3.632 +91,1.950 +92,2.694 +93,3.020 +94,2.230 +95,2.796 +96,2.707 +97,2.807 +98,2.658 +99,2.697 +100,2.453 +101,3.051 +102,2.758 +103,2.662 +104,2.599 +105,2.733 +106,2.751 +107,2.894 +108,2.501 +109,2.753 +110,2.873 +111,2.439 +112,2.634 +113,2.682 +114,2.913 +115,2.692 +116,2.735 +117,2.758 +118,2.726 +119,2.907 +120,2.568 +121,2.764 +122,2.815 +123,2.530 +124,2.684 +125,2.719 +126,2.846 +127,2.879 +128,2.647 +129,2.770 +130,2.477 +131,2.657 +132,2.675 +133,2.917 +134,2.700 +135,2.493 +136,2.678 +137,2.786 +138,3.004 +139,2.697 +140,2.642 +141,2.624 +142,2.684 +143,2.717 +144,2.743 +145,2.740 +146,2.825 +147,2.849 +148,2.991 +149,2.297 +150,3.084 +151,2.459 +152,3.214 +153,2.260 +154,2.550 +155,2.621 +156,2.660 +157,2.739 +158,2.574 +159,3.078 +160,2.735 +161,2.607 +162,2.793 +163,2.798 +164,2.584 +165,2.637 +166,2.627 +167,2.726 +168,2.656 +169,2.788 +170,2.674 +171,2.782 +172,2.766 +173,2.613 +174,2.724 +175,2.770 +176,2.709 +177,2.700 +178,2.600 +179,2.686 +180,2.747 +181,2.841 +182,2.601 +183,2.666 +184,2.846 +185,2.599 +186,2.806 +187,3.126 +188,2.521 +189,2.634 +190,2.716 +191,2.584 +192,2.714 +193,2.923 +194,2.277 +195,2.706 +196,2.606 +197,2.758 +198,2.747 +199,2.958 +200,2.798 +201,2.387 +202,2.596 +203,2.861 +204,2.801 +205,2.521 +206,2.869 +207,2.884 +208,2.515 +209,2.592 +210,3.723 +211,2.474 +212,2.736 +213,2.774 +214,2.769 +215,3.007 +216,2.768 +217,2.716 +218,2.694 +219,2.722 +220,2.862 +221,2.587 +222,2.833 +223,2.792 +224,2.746 +225,2.585 +226,2.741 +227,2.585 +228,2.802 +229,2.674 +230,2.742 +231,2.718 +232,2.881 +233,2.707 +234,2.585 +235,2.810 +236,2.948 +237,2.718 +238,2.773 +239,2.571 +240,2.788 +241,2.702 +242,2.634 +243,2.788 +244,2.678 +245,2.914 +246,2.527 +247,2.637 +248,2.877 +249,2.693 +250,2.644 +251,2.670 +252,2.700 +253,2.877 +254,2.638 +255,2.617 +256,2.774 +257,2.660 +258,2.731 +259,2.671 +260,2.838 +261,2.726 +262,2.671 +263,2.744 +264,2.735 +265,2.684 +266,3.850 +267,2.120 +268,2.669 +269,2.729 +270,2.712 +271,2.817 +272,2.926 +273,2.678 +274,2.786 +275,2.927 +276,2.493 +277,2.846 +278,2.610 +279,2.644 +280,2.577 +281,2.939 +282,2.727 +283,2.595 +284,2.819 +285,2.778 +286,2.774 +287,2.628 +288,2.711 +289,2.699 +290,2.715 +291,2.763 +292,2.710 +293,2.892 +294,2.721 +295,2.575 +296,3.608 +297,2.811 +298,2.739 +299,2.751 +300,2.608 +301,2.913 +302,3.052 +303,2.862 +304,2.653 +305,2.727 +306,2.646 +307,2.888 +308,3.338 +309,2.743 +310,2.639 +311,2.681 +312,2.846 +313,2.766 +314,3.184 +315,2.834 +316,2.728 +317,2.581 +318,2.770 +319,3.351 +320,3.157 +321,2.508 +322,2.835 +323,2.754 +324,2.411 +325,3.398 +326,2.802 +327,2.611 +328,3.058 +329,2.407 +330,2.640 +331,3.645 +332,2.729 +333,2.580 +334,2.902 +335,2.523 +336,3.005 +337,3.173 +338,2.840 +339,2.610 +340,2.576 +341,2.706 +342,3.405 +343,2.849 +344,2.678 +345,2.796 +346,2.782 +347,2.636 +348,3.475 +349,2.798 +350,2.745 +351,2.564 +352,2.792 +353,2.714 +354,3.496 +355,2.841 +356,2.465 +357,2.708 +358,2.692 +359,2.934 +360,3.304 +361,2.561 +362,2.689 +363,2.749 +364,2.780 +365,2.940 +366,3.025 +367,2.706 +368,2.881 +369,2.619 +370,2.716 +371,3.133 +372,2.874 +373,2.816 +374,2.414 +375,2.737 +376,2.797 +377,2.651 +378,2.920 +379,3.032 +380,2.606 +381,2.710 +382,2.649 +383,2.700 +384,3.469 +385,2.224 +386,2.567 +387,2.733 +388,2.511 +389,2.950 +390,3.979 +391,2.798 +392,2.487 +393,2.740 +394,2.741 +395,2.718 +396,3.291 +397,2.769 +398,2.697 +399,2.796 +400,2.641 +401,2.652 +402,3.151 +403,2.673 +404,2.741 +405,2.606 +406,2.665 +407,2.859 +408,2.739 +409,2.586 +410,2.910 +411,2.745 +412,2.698 +413,2.670 +414,2.797 +415,2.829 +416,2.643 +417,2.776 +418,2.772 +419,2.530 +420,2.567 +421,2.826 +422,2.782 +423,2.738 +424,2.640 +425,2.967 +426,2.600 +427,3.020 +428,2.502 +429,2.642 +430,2.625 +431,2.770 +432,2.659 +433,2.717 +434,2.698 +435,2.780 +436,2.992 +437,2.686 +438,2.560 +439,3.048 +440,2.721 +441,2.507 +442,2.694 +443,2.695 +444,2.892 +445,3.443 +446,2.633 +447,2.908 +448,2.823 +449,2.346 +450,2.599 +451,2.914 +452,2.774 +453,3.391 +454,2.384 +455,2.585 +456,2.667 +457,2.704 +458,2.674 +459,2.755 +460,2.718 +461,2.804 +462,2.583 +463,2.694 +464,2.797 +465,3.079 +466,2.434 +467,2.723 +468,3.671 +469,2.211 +470,2.567 +471,2.701 +472,2.717 +473,2.657 +474,2.581 +475,2.889 +476,3.006 +477,2.595 +478,2.703 +479,2.498 +480,2.694 +481,2.915 +482,2.655 +483,2.686 +484,2.833 +485,2.717 +486,2.723 +487,2.451 +488,2.913 +489,2.784 +490,2.684 +491,2.651 +492,2.809 +493,2.819 +494,2.676 +495,2.851 +496,2.775 +497,2.638 +498,2.520 +499,2.824 +500,2.930 +501,2.757 +502,2.608 +503,2.808 +504,2.508 +505,2.819 +506,2.654 +507,2.748 +508,2.705 +509,2.798 +510,2.559 +511,2.694 +512,2.821 +513,2.706 +514,2.612 +515,2.928 +516,2.475 +517,2.830 +518,2.658 +519,2.784 +520,2.572 +521,2.935 +522,2.688 +523,2.600 +524,2.675 +525,2.828 +526,2.774 +527,2.848 +528,2.764 +529,2.379 +530,2.962 +531,2.989 +532,2.528 +533,2.639 +534,2.906 +535,2.543 +536,2.582 +537,2.949 +538,2.813 +539,2.504 +540,2.754 +541,2.638 +542,2.695 +543,2.662 +544,2.922 +545,2.616 +546,2.810 +547,2.675 +548,2.551 +549,2.894 +550,2.754 +551,2.632 +552,2.748 +553,2.827 +554,2.751 +555,2.759 +556,2.684 +557,2.766 +558,2.948 +559,2.461 +560,2.621 +561,2.772 +562,2.985 +563,2.582 +564,2.897 +565,2.574 +566,2.661 +567,2.742 +568,2.797 +569,2.632 +570,2.755 +571,2.878 +572,2.759 +573,3.812 +574,2.651 +575,2.862 +576,2.577 +577,2.634 +578,3.173 +579,2.418 +580,2.714 +581,2.746 +582,2.794 +583,2.774 +584,2.583 +585,3.659 +586,3.222 +587,2.489 +588,2.868 +589,2.558 +590,2.586 +591,2.599 +592,2.930 +593,2.401 +594,2.540 +595,2.889 +596,2.733 +597,2.664 +598,2.877 +599,2.588 +600,2.677 +601,2.897 +602,2.672 +603,2.780 +604,2.740 +605,2.780 +606,2.493 +607,2.794 +608,2.631 +609,2.768 +610,2.870 +611,2.626 +612,2.862 +613,2.602 +614,2.868 +615,2.627 +616,2.638 +617,2.702 +618,2.904 +619,2.918 +620,2.487 +621,3.225 +622,2.350 +623,2.820 +624,2.837 +625,2.408 +626,2.593 +627,3.526 +628,2.554 +629,2.746 +630,2.684 +631,2.729 +632,2.762 +633,2.768 +634,3.034 +635,2.514 +636,2.732 +637,2.739 +638,2.692 +639,2.832 +640,2.810 +641,2.484 +642,2.632 +643,3.284 +644,2.311 +645,3.079 +646,2.656 +647,2.683 +648,2.622 +649,2.668 +650,2.779 +651,2.796 +652,2.874 +653,2.427 +654,2.644 +655,2.872 +656,2.867 +657,2.404 +658,2.917 +659,2.773 +660,2.857 +661,2.789 +662,2.443 +663,2.651 +664,2.839 +665,2.814 +666,2.575 +667,2.796 +668,2.709 +669,2.873 +670,2.670 +671,2.936 +672,2.496 +673,2.608 +674,2.641 +675,2.793 +676,2.765 +677,2.736 +678,2.679 +679,2.987 +680,2.884 +681,2.469 +682,2.774 +683,2.948 +684,2.598 +685,3.478 +686,2.856 +687,4.623 +688,3.038 +689,2.507 +690,2.717 +691,2.682 +692,2.807 +693,2.778 +694,2.699 +695,2.802 +696,2.605 +697,2.650 +698,2.789 +699,2.791 +700,3.015 +701,2.539 +702,2.790 +703,2.698 +704,2.637 +705,2.657 +706,2.800 +707,2.720 +708,2.702 +709,2.845 +710,2.583 +711,2.780 +712,2.782 +713,2.593 +714,2.695 +715,2.747 +716,2.871 +717,2.671 +718,2.676 +719,2.991 +720,2.727 +721,2.576 +722,2.539 +723,2.770 +724,2.712 +725,2.818 +726,2.615 diff --git a/Docs/release-checks.md b/Docs/release-checks.md index ad2eb14..db26a74 100644 --- a/Docs/release-checks.md +++ b/Docs/release-checks.md @@ -127,6 +127,22 @@ Run on the packaged build (`GigLineQA`, Development, Vulkan SM5) with `-qaselfte Committed frame-time series: `Docs/perf/0.1.0-walk-frametimes.csv`. +### `0.1.5` re-measure with the façade kit in the map + +Same route, same preset, packaged Development build, **2560×1440**: + +| Metric | `0.1.0` | `0.1.5` (kit + textures) | +| ------- | ----------------: | -----------------------: | +| Average | 2.42 ms (414 fps) | **2.76 ms (363 fps)** | +| p50 | 2.38 ms | 2.73 ms | +| p95 | 2.71 ms | 3.17 ms | +| p99 | 3.01 ms | 3.64 ms | +| Max | 4.00 ms | 4.62 ms | + +The **625 kit actors and 8 generated textures cost about 12 %** of frame time and still leave **363 fps average, p99 3.64 ms** — roughly six times the 60 fps floor. Frame-time series: `Docs/perf/0.1.5-kit-walk-frametimes.csv`. + +**Measurement trap, again:** runs that reported ~105 fps were two game instances overlapping (a previous launch had not exited). Confirm a single instance (`pgrep -af Binaries/Linux/GigLine`) before trusting a figure — 363 fps reproduced twice on a clean run. + ## `0.1.0` — Fresh checkout build — record (2026-11-08) Followed the documented steps in [`Docs/build.md`](build.md) exactly, into a clean directory with no prior state: diff --git a/Game/GigLine/Content/Hollywood/DemoMap.umap b/Game/GigLine/Content/Hollywood/DemoMap.umap index 2b1a82e..9b1cfcc 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:9ca76190a48710324d82892f5f56e423f123156f1b8e83740e37dbe3f7aa418d -size 958399 +oid sha256:979c81662cbb7cce37da5401a873c85b56a8da9f13a1e27790bd10f8098c54a4 +size 920287 diff --git a/Game/GigLine/Content/Hollywood/ElCapitan/SM_HeroFacade.uasset b/Game/GigLine/Content/Hollywood/ElCapitan/SM_HeroFacade.uasset index a0e205b..a318e07 100644 --- a/Game/GigLine/Content/Hollywood/ElCapitan/SM_HeroFacade.uasset +++ b/Game/GigLine/Content/Hollywood/ElCapitan/SM_HeroFacade.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b506386f8694a3788ec68d0f1feb9b4a6fa604282014d593514e3cefaa247f4 -size 142674 +oid sha256:ab0c6c8ddbc2a9086be3c474166b7fe152f2e0a0aa88cf55500feec21a4e10cf +size 186727 diff --git a/Game/GigLine/Content/Hollywood/Site/M_Asphalt.uasset b/Game/GigLine/Content/Hollywood/Site/M_Asphalt.uasset index 83426cf..a8aa8b3 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:fb97fb7403d331fff1a29884eb1edef8c0bbbe50e6fb33cfbe9ff9c696d24e30 -size 7876 +oid sha256:42651b3f341042ff5b59565061b40e10f742cb040ad32159cc2497baaa53241f +size 9818 diff --git a/Game/GigLine/Content/Hollywood/Site/M_Concrete.uasset b/Game/GigLine/Content/Hollywood/Site/M_Concrete.uasset index e587855..4990334 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:9c93e7576378561a245689f414d1046cff9e3de14329c8773f38e72abf736e76 -size 7881 +oid sha256:ac79d443e78c66dfdd0e6dcc826acb516157bb3b099643ca46a48a11ade12fd1 +size 9823 diff --git a/Game/GigLine/Content/Hollywood/Site/M_FacadeBanner.uasset b/Game/GigLine/Content/Hollywood/Site/M_FacadeBanner.uasset index d10f0e7..96db4a4 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_FacadeBanner.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_FacadeBanner.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e163fe98ecb0da87e920e5f685f4698392c7b00f5c0edb4d3497d61d2eeac98c -size 8667 +oid sha256:ed6a9b1306a3f54006dd09d33edea45635af4445f88f9b7b021a6be96226d0f8 +size 11054 diff --git a/Game/GigLine/Content/Hollywood/Site/M_FacadeGlass.uasset b/Game/GigLine/Content/Hollywood/Site/M_FacadeGlass.uasset index 961b4b6..674550a 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_FacadeGlass.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_FacadeGlass.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f24d1f4d483cf6896103250cf5726f4ab258ecb14386d4e6fd5786ee00263b8 -size 8662 +oid sha256:d8c25c5cf5e02e8ae7eddb42d6ab3ab5eb1868021ea6373a4e01676223daf7fe +size 11049 diff --git a/Game/GigLine/Content/Hollywood/Site/M_FacadeStone.uasset b/Game/GigLine/Content/Hollywood/Site/M_FacadeStone.uasset index 2e46754..b52bd8f 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:f8d3097b908c53a6c63dd8c2320794212362d1f5e4b1b1ff65a3e0f50e70c895 -size 12131 +oid sha256:ee7e3321ede0f26ff779baa0dbfb1cba79682c00c3c604a57a6ff511df1e1a96 +size 16924 diff --git a/Game/GigLine/Content/Hollywood/Site/M_FacadeStoneShadow.uasset b/Game/GigLine/Content/Hollywood/Site/M_FacadeStoneShadow.uasset index 9f382ec..beb0987 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_FacadeStoneShadow.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_FacadeStoneShadow.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:853bb97a0318147126cd22b3bf3b2e6781e9605aadd1ace75c9accbd4e04a3d6 -size 12161 +oid sha256:a63d50ddbe2b8674fa37ec42936e02ae20f604503966bb0eb9acd072c2061508 +size 16954 diff --git a/Game/GigLine/Content/Hollywood/Site/M_Ground.uasset b/Game/GigLine/Content/Hollywood/Site/M_Ground.uasset index 6201c75..79b8458 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:8a8fbad4e48bbf53f1f3dbc3d19fdb497ade7a70e108e0f009829176dcb098b0 -size 7871 +oid sha256:f24e2319667b421615b371015edf38ae1645049a29a4ed07e79f8f780161b4ec +size 9813 diff --git a/Game/GigLine/Content/Hollywood/Site/M_KerbConcrete.uasset b/Game/GigLine/Content/Hollywood/Site/M_KerbConcrete.uasset index dd99703..cc43899 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:9add1813503a7727aea65bb55249ea2eae84a47a349f2ee6f011eaddb6a4d592 -size 7901 +oid sha256:27dad21ae40278bc8f85b71f816b26f63cebf3032cf6f026dc827770ae70c112 +size 9843 diff --git a/Game/GigLine/Content/Hollywood/Site/M_RoadPaint.uasset b/Game/GigLine/Content/Hollywood/Site/M_RoadPaint.uasset index c62ba86..a9892bc 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:5b08b4bc9d63293bd4a2b2db4e6a70c6854b309a579d7121fb065ea3d9834051 -size 7886 +oid sha256:dd1f661a78dc559fe32a7eba5b8de1c3c11eb02db915c6af75698233064f3bfd +size 9828 diff --git a/Game/GigLine/Content/Hollywood/Site/M_Stucco.uasset b/Game/GigLine/Content/Hollywood/Site/M_Stucco.uasset index 26e7b21..9620116 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:81dae9d6c50131436e01dacd27bacfbeb6186405d9d70c1b2858f51dff158df5 -size 8637 +oid sha256:79afcede88edfbdbca48cc80d414f90824c1b3c29f4e0d6fa20bcfd83956add4 +size 11024 diff --git a/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset b/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset index 8dfa296..98a9441 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:a3ca6de1695fb13f5435977ffa8c8330c16075cc21a2a78fef17d1c1ee0ee7cd +oid sha256:60e5397f227314e5d59fd3783e9c31713db9ebcbdc29a34aa44cddb305d76ff9 size 357901 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_BC.uasset b/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_BC.uasset index c70868b..eba8aec 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_BC.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_BC.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03f6ce0b402dd3a4cbfb93ca704ff81c1ac782d1a1f34929342c0cf499eecc19 +oid sha256:c5d7a3acd47b38355b6903b962d6b938ff1f5ddc602923f8e944c29a4674dee2 size 70879 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_R.uasset b/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_R.uasset index 966fd6c..3bd6c08 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_R.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_Asphalt_R.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad6d2595d987f3e4045995aebf9e3f7845ad229beed37308e9d8f4c7c33e1106 +oid sha256:2454b2c9f7d9950d68af0402b8553acc3aa64aab1faee95fe71fe2b2efe4710e size 29011 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_Concrete_BC.uasset b/Game/GigLine/Content/Hollywood/Textures/T_Concrete_BC.uasset index 8085bdb..d463d4e 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_Concrete_BC.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_Concrete_BC.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:918cc5acf1447085dab73d989c89ac1423fe1cba599ae566d5524cc26076d1a8 +oid sha256:782166930730273c3bc82ac7be18f084436d65da79bcff316a4f171e77e6ef93 size 58568 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_Concrete_R.uasset b/Game/GigLine/Content/Hollywood/Textures/T_Concrete_R.uasset index cdaaede..27ff573 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_Concrete_R.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_Concrete_R.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae968668a57242ffa65b81297f4dcd8e3280111b9f3470e1ebd4fd92eaee4a88 +oid sha256:805d8f4ad82682c0d2409cf46e8a62a65438a38073b4a307ff498b3de912a696 size 17517 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_BC.uasset b/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_BC.uasset index c132640..ebb8dce 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_BC.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_BC.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6549cec0e7c5cc24781c484d9901c2ceb3c7a9db25e74352ee1d0f8f619c90cf +oid sha256:2a0d33fee2e6475e7c0392c838a9d7b132c6c95db66f44f39247f1e6ea3ce456 size 40610 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_R.uasset b/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_R.uasset index d9e5552..c55cf87 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_R.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_StoneAshlar_R.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad209b9388a8aed7cea08b267eeab17abec06fbaa392b31378117f7d828fd9c5 +oid sha256:c3ba7d4166902628d5ca37b7d6f164d8e4bf8a58990777a49fe3871640645c6c size 18770 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_Stucco_BC.uasset b/Game/GigLine/Content/Hollywood/Textures/T_Stucco_BC.uasset index 5e443c4..71ce6ce 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_Stucco_BC.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_Stucco_BC.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:516debf8a3076a5cf3fcc8e4ebc2e63d5bc6b290ec9865eb2f47118cea9c7e3f +oid sha256:f435587872a1f21006d6cbc466b01728110b8574ed8384287f414bf332b62cd6 size 62746 diff --git a/Game/GigLine/Content/Hollywood/Textures/T_Stucco_R.uasset b/Game/GigLine/Content/Hollywood/Textures/T_Stucco_R.uasset index 8ab670b..7711f4e 100644 --- a/Game/GigLine/Content/Hollywood/Textures/T_Stucco_R.uasset +++ b/Game/GigLine/Content/Hollywood/Textures/T_Stucco_R.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69b431dd98f073b4987a3019cb8cc22ba23055663eb986a03be57104b2c2e1f2 +oid sha256:92a72806044679f1ddccf944775d8c2c1a131006030c17480788c70404734d13 size 17541 diff --git a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSign.uasset b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSign.uasset index 9805a83..d557461 100644 --- a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSign.uasset +++ b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSign.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:360a19294a103b52b4c0b1e5af5befd9a55fc9471443a703f6e39948d9eed962 +oid sha256:5152da3143f8b6b00f3e595d5adf07f65b260a2f32032140450a34538767f14e size 72330 diff --git a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSignBracket.uasset b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSignBracket.uasset index 7b89f33..d18ceec 100644 --- a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSignBracket.uasset +++ b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSignBracket.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:262d2eaac6f9e2ec2cd5217c68cd9ac4882e075b5d52e6d09dcc96e3886f56a8 +oid sha256:b3a7c47afac32e8601e9ac4649c2e16663dfda8d409241527bd1d2c7bbd7c32e size 72466 diff --git a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSignFrame.uasset b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSignFrame.uasset index 990106b..e0fdde9 100644 --- a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSignFrame.uasset +++ b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_BladeSignFrame.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:516633d806cd4c97bc409b27ffea4533211a798e3811c45c2d6f09176ebd6fc5 +oid sha256:f135922b7e89057917176a516fd283f1b739e9c6960892b8838daabd67eecfe0 size 72406 diff --git a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeCanopy.uasset b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeCanopy.uasset index 6ce02c6..b09cfb1 100644 --- a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeCanopy.uasset +++ b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeCanopy.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e55efd7aee1bb44a6968bf9f82a431798631d20bfafa53d4198c9d436ca4848e +oid sha256:e3aaf3835733ebdaed780d33e8ee6c5ef2047e7f53de03c2984aafcde75830aa size 72370 diff --git a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeFascia.uasset b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeFascia.uasset index 5c3ac79..bbf4643 100644 --- a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeFascia.uasset +++ b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeFascia.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9554845054a637f8101c6d4fe9f85bce8fd7fe5a532768f4b345151b628a5f6d +oid sha256:6e42c24ba5e098c001a4b7ef3a79ab7ca4077a6c29647a7f85037e8a7e97a07a size 72438 diff --git a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeSoffit.uasset b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeSoffit.uasset index 33e2f67..f352bf4 100644 --- a/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeSoffit.uasset +++ b/Game/GigLine/Content/Hollywood/Theatre/SM_Trim_MarqueeSoffit.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0f89567a63c72fde986c42ffcb0577b672ece498d81159250c985fd1915a7863 +oid sha256:ffdf61d494359fea1c003dda276fc8c25b943bca7b9dd209c41381705a1ca13f size 72413 diff --git a/Game/GigLine/Content/Test/ReviewMap.umap b/Game/GigLine/Content/Test/ReviewMap.umap index 562ce47..22da558 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:3d7897fdd23c11963990f71530a4630f8e2067503498c5906b13e232a79819e7 +oid sha256:24297b54b7f130738e1b29acf2c806522a191d185fce1164e52da1d744d81bdd size 28811 diff --git a/MEMORY.md b/MEMORY.md index 2df3716..de67cf7 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -436,3 +436,90 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. scope is cut deliberately and recorded in `CHANGELOG.md`, never the frame rate. Walkability outranks realism in one direction: a block the player falls through is not a demo. + +## 2026-11-09 — 0.1.5 start: retail reference + kit layout maths + +- Commons holds exactly **one** usable reference for the retail run: + `Art/reference/retail-neighbours/01-lee-drug-and-neighbor.jpg` (Gb321, CC BY-SA + 4.0, 1920×953) — 6800 Lee Drug **and** its neighbour at street level, shopfronts + and parapets visible (vision pre-check: yes). Nothing for 6822 or 6806–6820, so + those two façades still have no reference and stay blocked. +- **Kit layout was wrong for tall/wide buildings**, not just ugly: bays were + `round(width / 496)`, so 6922 (65.8 m wide, 37.1 m tall) got **13 bays** — a + window every 4.96 m across the whole frontage — and the storey count came out at + 11 × 310 cm = 34.1 m against a 35.7 m window zone, leaving bare wall under the + cornice. Fixed: `MAX_BAYS = 9` (module widens instead of subdividing), and the + storey spread is derived from the available height (`usable / storeys`) so the top + band always lands inside the building. Result: 6922 9 × 11, theatre 8 × 6, + 6806 7 × 3, 6800 4 × 3, TCL 9 × 5. + +## 2026-11-09 — hero fluting + motto band; and a silent placement divergence + +- Fluting: 20 grooves per column as thin shadow-tone cylinders set into the shaft + (120 pieces) rather than boolean cuts — the eye reads them in silhouette and + grazing light, booleans would cost far more for that. Motto band is now a real + recess: surround 6 cm proud, panel 12 cm behind (was an 8 cm dark stripe). + Hero 148 parts/3 044 tris -> 271/5 492, still 14.50 m. +- **The real find:** `ue_hero_facade_import.py` had its own hero position + `(-12024.5, -1450, 0)` — the pre-`0.1.0` value, before the frontage placement fix + moved the hero to `(-13336, +1307)`. Running the importer on its own therefore + teleported the hero **13 m east and onto the north side of the boulevard**, and + nothing complained: a displaced building still renders. The only symptom was the + reference score dropping 0.69 -> 0.56 with the 3x3 composition correlation + flipping +0.54 -> -0.08. +- Fix: `Tools/ue_placement.py` is now the single source of truth (hero position, + verified frontage spans, `span_error`/`describe_span_failure`). Both + `ue_hero_facade_import.py` and `ue_build_demo_map.py` import it, the import asserts + the staged span and logs `PLACEMENT ERROR` when it does not match, and tests assert + the stale literal cannot come back. Spawn-view score back to **0.72** (best yet). +- Lesson to keep: **duplicated placement constants drift silently.** The metric that + caught it was the composition correlation, not a visual review. + +## 2026-11-09 — 1440p re-measured with the kit: 363 fps, ~12% cost + +- 2560×1440, Epic preset, TSR, VSync off, no frame generation: **2.76 ms average + (363 fps)**, p50 2.73, p95 3.17, p99 3.64, max 4.62 — against `0.1.0`'s 2.42 ms + (414 fps). The 625 kit actors + 8 generated textures cost about **12 %** and leave + ~6× the 60 fps floor. +- **Variance trap confirmed a third time:** runs reporting ~105 fps (9.5 ms) were + two overlapping game instances — an earlier launch had not exited. `pgrep -af + Binaries/Linux/GigLine` should show one wrapper + one binary; 363 fps reproduced + twice on a clean run. Always count instances before believing a perf number. +- Series committed at `Docs/perf/0.1.5-kit-walk-frametimes.csv`; comparison table in + `Docs/release-checks.md`. + +## 2026-11-09 — 604 unmaterialed actors: material rebuild was delete+recreate + +- The "blockout cleaning pass" turned out to need **material and placement hygiene, + not more geometry**. `Tools/ue_audit_scene.py` (new) found **604 of 620 mesh actors + with no material**: kit pieces and theatre trim all untextured. +- **Cause:** `ensure_material` in `ue_build_demo_map.py` deleted and recreated each + `/Game/Hollywood/Site/M_*` asset every run ("so a look change cannot be masked by a + stale asset"). That orphane every reference: actors staged by other tools pointed at + deleted assets and fell back to the default material. The map looked only *slightly* + flat — no error, no warning. +- **Fix:** update the material **in place** — load it, `MaterialEditingLibrary. + delete_all_material_expressions(mat)`, rebuild the graph, recompile, save. Note + `Material.expressions` is protected from Python; use the library's deleter. +- **Ordering rule now documented and enforced:** the demo builder rebuilds materials, + so kit and theatre staging must run *after* it. `render_screenshots.sh` now runs + demo map → kit assembly → theatre trim → scene audit → shot cameras → capture, and + **fails on a non-zero audit count**. +- Second find: 53 duplicated kit labels (both shop piers of a bay shared one name). +- Scene audit after the fixes: **0 failures** (all materialed, all spans verified, no + duplicate labels). Packaged QA still PASS; spawn-view reference score 0.72. + +## 2026-11-09 — v0.1.5 released + +- All actionable `0.1.5` work is done: hero fluting + recessed motto band, kit layout + for wide/tall buildings, placement single-source-of-truth, 1440p re-measure + (363 fps), and the material/placement hygiene that the "blockout cleaning" task + actually needed (604 unmaterialed actors, 53 duplicate labels) with a new scene + audit gate. +- Only item carried forward is the **retail frontage (6822 / 6806–6820 / 6800)**, + blocked because Commons holds no usable reference for two of the three buildings + and substituting the theatre's photographs would misattribute the façades. It now + lives in a `0.1.6` Retail frontage patch. +- Precedent applied again: tag when the stated scope is done, carry blocked scope + into the next patch rather than letting a release wait indefinitely on missing + reference photography. diff --git a/ROADMAP.md b/ROADMAP.md index de7162b..00c8232 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -8,7 +8,7 @@ Development roadmap for this project: milestone goals, scope, decisions, and rel ## Status at a glance — 2026-11-08 15:30 -Workstation, geo pipeline, and engine validation are done. The art pipeline has a first blockout, terrain, and street in metres, in one shared coordinate frame. **Project bootstrap is complete**: `Game/GigLine` builds, packages, and both content-acceptance tests (static-mesh scale, MPFB skeletal character) pass. **`v0.1.0` released 2026-11-08**: the walkable demo, hero façade, verified block frontage, first-person controls, ODbL attribution, and every release check pass (traversal, placement, collision, 60 fps at 1440p, fresh-checkout build, packaged build with controls). Next: the `0.1.1`–`0.1.4` patch series (collision detail, scale/façade corrections, packaging fixes, real building façades), then `0.2.0` (player body, third-person camera, animation). +Workstation, geo pipeline, and engine validation are done. The art pipeline has a first blockout, terrain, and street in metres, in one shared coordinate frame. **Project bootstrap is complete**: `Game/GigLine` builds, packages, and both content-acceptance tests (static-mesh scale, MPFB skeletal character) pass. **`v0.1.0` released 2026-11-08**: the walkable demo, hero façade, verified block frontage, first-person controls, ODbL attribution, and every release check pass (traversal, placement, collision, 60 fps at 1440p, fresh-checkout build, packaged build with controls). Next: the `0.1.1`–`0.1.4` patch series (collision detail, scale/façade corrections, packaging fixes, real building façades), **Next:** `0.1.6` retail frontage (6822, 6806–6820, 6800 — blocked on reference photography for two of the three), then `0.2.0` (player body, third-person camera, animation). | Area | State | | --- | --- | diff --git a/Screenshots/demo/review-demo-crossing.png b/Screenshots/demo/review-demo-crossing.png index 6a19fc7..b49e26e 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:9cd5184c2c1f932e5262322c6ee21d9fae24f4af534ed6b641c11e0cd722952b -size 3079101 +oid sha256:c7729a7ef0b10c5827f3c5b4167e12ba03ca6aa81a8a1da0c55f9fb122897694 +size 2992299 diff --git a/Screenshots/demo/review-demo-north-buildings.png b/Screenshots/demo/review-demo-north-buildings.png index de0eca2..c655726 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:153b25b43f293ebf793fa8d0d685b0ef0be2a94267a03937180e4bcd602ed561 -size 2442501 +oid sha256:a854cc82d6a1ad84173664076a076f649c8dfbf38bb7d887f7d6444a7dc4e57c +size 2417601 diff --git a/Screenshots/demo/review-demo-spawn-view.png b/Screenshots/demo/review-demo-spawn-view.png index da3f6bf..f868673 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:9feb51f299584f7c7e76e69bc1e05e381b9cad381e5b0375e3430c1ea5fcaac6 -size 2946077 +oid sha256:e06925a4ca759e762c3ad01375cc098e22fdab065878a85269a63d62b9ba7256 +size 2505879 diff --git a/Screenshots/demo/review-demo-street-level.png b/Screenshots/demo/review-demo-street-level.png index 77452f8..21ad29f 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:2fdec8a67016806bbff3591f449bc16c19df35df2ae98cb18c38c15d0b91a6a5 -size 2865770 +oid sha256:e0412f034f6ac55928521c14278043f5626ce32ecaaca7e178ec3efd011efc22 +size 2705233 diff --git a/TODO.md b/TODO.md index 00f551b..d55e52d 100644 --- a/TODO.md +++ b/TODO.md @@ -9,19 +9,15 @@ Actionable task list for this project. This file is the single, central place fo - When all steps in a version section are done: write a `CHANGELOG.md` entry, then remove the version section from this file. - Release gate: a version is tagged only on the owner's explicit go-ahead once its release checks pass, not automatically when the checkboxes close. -## 0.1.5 — Façade finishing (patch) +## 0.1.6 — Retail frontage (patch) -Suggested blender skills: **references** (reference-image-match against the photographs), **historical-worlds** / **archviz** (1920s Hollywood Blvd neoclassical detail), **materials** and **lookdev** for the remaining surface work. - -The `0.1.4` kit, materials and theatre trim are signed off; these are the geometry items that were carried forward rather than grown inside that patch. +Suggested blender skills: **references** (reference-image-match), **archviz**, **historical-worlds** for the 1920s–1990s retail mix on this run. ### Patch tasks -- [ ] **Retail neighbours (6822 Budget, 6806–6820, 6800 Lee Drug) from their own references.** The 18-frame set is the El Capitan Theatre, so it cannot stand in for these façades. Fetch or shoot per-building references first, then model the shopfront line and upper storeys rather than relying on the kit alone. -- [ ] **Hero façade: fluting and the motto band as geometry.** The colonnade is plain cylinders and the motto/medallions are flat panels; both are legible in the reference photography. -- [ ] **Blosm blockout cleaning pass.** Roofs and facades remain untextured volumes and the `setupScript` styles stay unused; with the kit carrying the elevations, this is now cleanup rather than a second façade system. -- [ ] **Re-measure 1440p performance** with 625 kit actors and 8 generated textures in the map. `0.1.0` measured 414 fps average, so there is headroom, but it has not been re-run since. -- [ ] Tag `v0.1.5` and write `CHANGELOG.md` entry; then remove this section. +- [ ] **Model 6822 Budget, 6806–6820 and 6800 Lee Drug from their own references.** `Art/reference/retail-neighbours/` holds 6800 and its neighbour (Gb321, CC BY-SA 4.0); Commons has nothing usable for 6822 or 6806–6820, so those two need photography before they can be modelled honestly. Do not substitute the El Capitan Theatre's 18-frame set — it would misattribute the façades. +- [ ] Review every façade change with `Tools/render_screenshots.sh --verify` before tagging. +- [ ] Tag `v0.1.6` and write `CHANGELOG.md` entry; then remove this section. ## 0.2.0 — MPFB player body, third-person camera, basic animation (weeks 7–9) diff --git a/Tools/README.md b/Tools/README.md index 2ef7335..0a54062 100644 --- a/Tools/README.md +++ b/Tools/README.md @@ -22,6 +22,8 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b | `ue_stage_theatre_trim.py` | Imports the trim to `/Game/Hollywood/Theatre` and stages one actor per piece in the DemoMap with site materials; clears a previous run first. | | `ue_apply_facade_kit.py` | Lays the kit over a building's street face from its own bounding box: bay count = round(width / 4.96), storeys from the height, shopfronts on the ground floor, windows above, pilasters on bay boundaries, cornice and parapet at the top. Clears a previous application first; unique labels per building. | | `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_audit_scene.py` | Gate on the DemoMap: no unmaterialed actor, every verified frontage span within tolerance (`ue_placement`), no duplicate actor labels. Writes `/tmp/gigline/scene-audit.txt`; `render_screenshots.sh --verify` fails on a non-zero failure count. Catches the class of defect that still renders plausibly. | +| `ue_placement.py` | Single source of truth for where staged actors belong in the DemoMap (hero position, verified frontage spans) plus `span_error`/`describe_span_failure` validators. Exists because the hero position was written twice and drifted — the importer kept the pre-`0.1.0` value and moved the hero 13 m east onto the wrong side of the boulevard. No `unreal` import, so it is unit-tested. | | `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. | | `sibling_asset_audit.py` | Reads the sibling project's prop library from GLB headers into `Art/props/reuse-manifest.json`; copies nothing | diff --git a/Tools/hero_facade_build.py b/Tools/hero_facade_build.py index 964a419..415f453 100644 --- a/Tools/hero_facade_build.py +++ b/Tools/hero_facade_build.py @@ -174,6 +174,20 @@ def build(): cap_z0 = HEIGHT - ENTABLATURE_H - PARAPET_H - CAPITAL_H cylinder(f"Hero_Column_{i + 1}", x, -0.55, shaft_z0, cap_z0, COLUMN_D / 2.0, stone) + + # Fluting: 20 shallow grooves around the shaft, as the photographs show on + # the colonnade. Each flute is a thin cylinder set into the shaft surface; + # modelling them as grooves would need booleans on 6 columns for detail the + # eye reads only in silhouette and grazing light, so they are added mass in + # the *shadow* direction instead — cheap, and it breaks the flat highlight. + flutes = 20 + flute_r = 0.032 + for f in range(flutes): + ang = (2.0 * math.pi * f) / flutes + fx = x + (COLUMN_D / 2.0 - flute_r * 0.4) * math.cos(ang) + fy = -0.55 + (COLUMN_D / 2.0 - flute_r * 0.4) * math.sin(ang) + cylinder(f"Hero_Flute_{i + 1}_{f + 1}", fx, fy, shaft_z0 + 0.05, + cap_z0 - 0.05, flute_r, stone_dark, vertices=6) # Capital block, then the square abacus that carries the architrave. # The abacus takes the top 0.14 m of the capital zone so the colonnade # still ends at HEIGHT - ENTABLATURE_H - PARAPET_H (proportion unchanged). @@ -185,6 +199,9 @@ def build(): 1.30, 1.30, abacus_h, stone) note("Hero_Column_x6", (COLUMN_D, COLUMN_D, HEIGHT - ENTABLATURE_H - PARAPET_H - PEDESTAL_H), f"6 columns at {COLUMN_BAY} m centres (sibling notes x measured frontage)") + note("Hero_Flute_x120", (0.064, 0.064, cap_z0 - shaft_z0), + "photographs: fluted shafts; 20 grooves per column modelled as shallow " + "cylinders in the shadow tone rather than boolean cut grooves") note("Hero_ColumnBase_x6", (1.36, 1.36, 0.22), "assumption: Ionic base moulding; photographs show a wider base at the shaft foot") note("Hero_Abacus_x6", (1.30, 1.30, 0.14), @@ -223,10 +240,22 @@ def build(): FRONTAGE_W, 1.2, PARAPET_H, stone) note("Hero_Parapet", (FRONTAGE_W, 1.2, PARAPET_H), "assumption: parapet carries the motto band") - # Motto band: a recessed panel across the parapet, as the photographs show. - cube("Hero_MottoBand", 0.0, -1.32, HEIGHT - PARAPET_H / 2.0, - FRONTAGE_W - 3.0, 0.08, 0.85, stone_dark) - note("Hero_MottoBand", (FRONTAGE_W - 3.0, 0.08, 0.85), "photographs: motto panel above the colonnade") + # Motto band: a recessed panel across the parapet with a moulded surround, as + # the photographs show. The panel was a 8 cm stripe of dark stone; it is now a + # real recess — surround frame standing 6 cm proud, panel set 12 cm behind it — + # so it reads as an inscription panel from the street instead of a paint line. + motto_z = HEIGHT - PARAPET_H / 2.0 + motto_w = FRONTAGE_W - 3.0 + for x_off in (-(motto_w / 2.0 + 0.16), (motto_w / 2.0 + 0.16)): + cube(f"Hero_MottoSurround_v{int(x_off > 0)}", x_off, -1.13, motto_z, + 0.32, 0.26, 1.15, stone) + for z_off in (-0.575, 0.575): + cube(f"Hero_MottoSurround_h{int(z_off > 0)}", 0.0, -1.13, motto_z + z_off, + motto_w + 0.64, 0.26, 0.32, stone) + cube("Hero_MottoBand", 0.0, -1.06, motto_z, motto_w, 0.10, 0.85, stone_dark) + note("Hero_MottoBand", (motto_w, 0.10, 0.85), + "photographs: motto panel above the colonnade, now an inset panel with a " + "moulded surround rather than a flat stripe") # Medallions either side of the inscription: discs on the parapet face, not # pancakes on the coping (see disc_on_wall). diff --git a/Tools/render_screenshots.sh b/Tools/render_screenshots.sh index 6a9fd90..1d34493 100755 --- a/Tools/render_screenshots.sh +++ b/Tools/render_screenshots.sh @@ -20,18 +20,29 @@ PROJ="$REPO/Game/GigLine/GigLine.uproject" VERIFY="no" [ "${1:-}" = "--verify" ] && VERIFY="yes" -echo "== 1/3 stage the demo shot cameras ==" -cd "$ENGINE" -./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" \ - -run=pythonscript -script="$REPO/Tools/ue_build_review_map.py" \ - -nullrhi -unattended -nopause -stdout +run_cmdlet() { + local script="$1" label="$2" + echo "== $label ==" + ( cd "$ENGINE" && ./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" \ + -run=pythonscript -script="$REPO/Tools/$script" \ + -nullrhi -unattended -nopause -stdout ) +} + +# Order matters. The demo builder rebuilds the site materials, so any actor staged +# by another tool must be (re)staged after it or it keeps a reference to a material +# that no longer exists - that is how 604 actors ended up untextured once. +run_cmdlet ue_build_demo_map.py "1/5 rebuild the demo map (materials, site, frontage)" +run_cmdlet ue_apply_facade_kit.py "2/5 assemble the facade kit" +run_cmdlet ue_stage_theatre_trim.py "3/5 stage the theatre trim" +run_cmdlet ue_audit_scene.py "4/6 audit the scene (materials, placement, labels)" +run_cmdlet ue_build_review_map.py "5/6 stage the demo shot cameras" tail -3 /tmp/ue-review-map-result.txt 2>/dev/null || true -echo "== 2/3 capture the demo map ==" +echo "== 6/6 capture the demo map ==" ./Engine/Binaries/Linux/UnrealEditor-Cmd "$PROJ" /Game/Hollywood/DemoMap \ -game -windowed -ResX=1920 -ResY=1080 -nopause -nosplash -stdout -echo "== 3/3 collect the demo screenshots ==" +echo "== collect the demo screenshots ==" mkdir -p "$REPO/Screenshots/demo" rm -f "$REPO"/Screenshots/demo/review-*.png found=0 @@ -47,6 +58,14 @@ done [ "$found" = "1" ] || { echo "ERROR: no demo screenshots found"; exit 1; } ls -1 "$REPO/Screenshots/demo" +# The scene audit is a gate: a wrong-material or misplaced actor is exactly the +# kind of failure that still renders plausibly. +if grep -q "failure(s) ==" /tmp/gigline/scene-audit.txt 2>/dev/null; then + grep -E "FAIL|failure\(s\)" /tmp/gigline/scene-audit.txt | tail -5 + grep -q "== 0 failure(s) ==" /tmp/gigline/scene-audit.txt || { + echo "ERROR: scene audit failed — see /tmp/gigline/scene-audit.txt"; exit 1; } +fi + if [ "$VERIFY" = "yes" ]; then echo "== verify: does each shot show what it should? ==" cd "$REPO" diff --git a/Tools/tests/test_ue_placement.py b/Tools/tests/test_ue_placement.py new file mode 100644 index 0000000..6835a51 --- /dev/null +++ b/Tools/tests/test_ue_placement.py @@ -0,0 +1,86 @@ +"""Tests for Tools/ue_placement.py — the single source of truth for placement. + +A regression test for a real bug: `ue_hero_facade_import.py` carried its own hero +position (the pre-0.1.0 one) while `ue_build_demo_map.py` staged the verified one. +Running the importer moved the hero 13 m east and onto the north side of the +boulevard, and nothing complained because a displaced building still renders. +""" + +import os +import sys +import unittest + +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +sys.path.insert(0, os.path.join(ROOT, "Tools")) + +import ue_placement # noqa: E402 + + +class HeroPlacementTest(unittest.TestCase): + def test_hero_actor_name(self): + self.assertEqual(ue_placement.HERO_ACTOR, "Hero_Facade") + + def test_hero_location_is_the_verified_building_line(self): + x, y, z = ue_placement.HERO_LOCATION_CM + self.assertEqual((x, y, z), (-13336.0, 1307.0, 0.0)) + # North is -Y in Unreal, so the hero on the south side must have Y > 0. + self.assertGreater(y, 0.0, "hero must be on the south side (Y > 0)") + + def test_hero_span_is_verified(self): + # The mesh spans -1308..+1308 cm in X about its origin. + lo = ue_placement.HERO_LOCATION_CM[0] - 1308.0 + hi = ue_placement.HERO_LOCATION_CM[0] + 1308.0 + exp_min, exp_max, ok = ue_placement.span_error("hero", lo, hi) + self.assertTrue(ok) + # The mesh spans exactly the verified footprint, so the two agree. + self.assertAlmostEqual(exp_min, lo, delta=ue_placement.SPAN_TOLERANCE_CM) + self.assertAlmostEqual(exp_max, hi, delta=ue_placement.SPAN_TOLERANCE_CM) + + +class SpanValidatorTest(unittest.TestCase): + def test_misplaced_hero_is_reported(self): + # The exact failure observed: the stale import position put the hero here. + lo, hi = -13332.0, -10716.0 + exp_min, exp_max, ok = ue_placement.span_error("hero", lo, hi) + self.assertFalse(ok) + message = ue_placement.describe_span_failure("hero", lo, hi) + self.assertIn("does not match the verified", message) + self.assertIn(f"{exp_min:.0f}", message) + + def test_correct_span_reports_nothing(self): + self.assertEqual(ue_placement.describe_span_failure("hero", -14644, -12028), "") + + def test_within_tolerance_passes(self): + # 99 cm off is inside the ±100 cm tolerance. + self.assertTrue(ue_placement.span_error("6922", -24276 + 99, -17700 - 99)[2]) + + def test_outside_tolerance_fails(self): + self.assertFalse(ue_placement.span_error("6922", -24276 + 101, -17700)[2]) + + def test_unknown_building_raises(self): + with self.assertRaises(KeyError): + ue_placement.span_error("no-such-building", 0, 0) + + def test_every_frontage_span_is_ordered(self): + for name, (lo, hi) in ue_placement.FRONTAGE_SPANS_CM.items(): + self.assertLess(lo, hi, f"{name} span is inverted") + + +class SingleSourceOfTruthTest(unittest.TestCase): + """The import script and the demo builder must not carry their own literals.""" + + def test_import_script_uses_the_shared_constant(self): + src = open(os.path.join(ROOT, "Tools", "ue_hero_facade_import.py"), + encoding="utf-8").read() + self.assertIn("ue_placement.HERO_LOCATION_CM", src) + self.assertNotIn("(-12024.5, -1450.0, 0.0)", src, + "the stale pre-0.1.0 hero position must not come back") + + def test_demo_builder_uses_the_shared_constant(self): + src = open(os.path.join(ROOT, "Tools", "ue_build_demo_map.py"), + encoding="utf-8").read() + self.assertIn("ue_placement.HERO_LOCATION_CM", src) + + +if __name__ == "__main__": + unittest.main() diff --git a/Tools/ue_apply_facade_kit.py b/Tools/ue_apply_facade_kit.py index 5606b91..42c5091 100644 --- a/Tools/ue_apply_facade_kit.py +++ b/Tools/ue_apply_facade_kit.py @@ -123,12 +123,28 @@ def apply_to_actor(actor, kit, materials=None, prefix=None): 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 = max(1, int((height - SHOP_FLOOR_CM) // STOREY_CM)) + # 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: @@ -140,11 +156,13 @@ def apply_to_actor(actor, kit, materials=None, prefix=None): 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)): + 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_{bay}", kit_material(piece)): + 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), @@ -153,7 +171,7 @@ def apply_to_actor(actor, kit, materials=None, prefix=None): # 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 + 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 diff --git a/Tools/ue_audit_scene.py b/Tools/ue_audit_scene.py new file mode 100644 index 0000000..cf05e54 --- /dev/null +++ b/Tools/ue_audit_scene.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""Audit the DemoMap for staged actors that would render wrong. + +Checks that are cheap and catch the failures this project actually had: + +1. **No unmaterialed actors.** 604 actors once rendered with the default material + because the demo builder deleted and recreated the site materials, orphaning + every reference; the map looked merely "a bit flat". +2. **Placement spans.** Each verified frontage building is probed against + `Tools/ue_placement.py`; a building 13 m off its footprint still renders fine, + which is exactly why it needs a number, not an eye. +3. **No duplicate actor labels.** Two buildings' kit pieces sharing a label made + the outliner ambiguous and hid one of them. + + UnrealEditor-Cmd -run=pythonscript \ + -script=Tools/ue_audit_scene.py -nullrhi -unattended -nopause -stdout + +Result file: `/tmp/gigline/scene-audit.txt`. Exit code from the commandlet is +always 0, so **read the file** — `visual_loop.py --verify` does not depend on it. +""" + +from __future__ import annotations + +import collections +import os +import sys + +import unreal + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import ue_placement # noqa: E402 + +MAP = "/Game/Hollywood/DemoMap" +RESULT = "/tmp/gigline/scene-audit.txt" + +lines: list[str] = [] +failures = 0 + + +def log(message: str) -> None: + lines.append(message) + print(message) + + +def fail(message: str) -> None: + global failures + failures += 1 + log("FAIL " + message) + + +def main() -> None: + if not unreal.EditorLevelLibrary.load_level(MAP): + log(f"could not load {MAP}") + return + + actors = unreal.EditorLevelLibrary.get_all_level_actors() + + # 1. materials + unmaterialed = [] + for actor in actors: + comp = actor.get_component_by_class(unreal.StaticMeshComponent) + if not comp or not comp.static_mesh: + continue + if comp.get_material(0) is None: + unmaterialed.append(actor.get_actor_label()) + log(f"mesh actors: {sum(1 for a in actors if a.get_component_by_class(unreal.StaticMeshComponent))}") + if unmaterialed: + fail(f"{len(unmaterialed)} actor(s) with no material, e.g. {unmaterialed[:6]}") + else: + log("OK every staged mesh has a material") + + # 2. placement spans + for actor in actors: + label = actor.get_actor_label() + key = None + if label.startswith("Frontage_SM_"): + name = label[len("Frontage_SM_"):] + key = {"Hollywood_6922": "6922", "Hollywood_6902": "6902", + "ElCapitanTheatre": "6838", "Budget_6822": "6822", + "Hollywood_6806": "6806", "LeeDrug_6800": "6800"}.get(name) + elif label == ue_placement.HERO_ACTOR: + key = "hero" + if not key: + continue + origin, extent = actor.get_actor_bounds(False) + lo, hi = origin.x - extent.x, origin.x + extent.x + problem = ue_placement.describe_span_failure(key, lo, hi) + if problem: + fail(problem) + else: + log(f"OK {key}: X {lo:.0f}..{hi:.0f}") + + # 3. duplicate labels + counts = collections.Counter(a.get_actor_label() for a in actors) + duplicates = {k: v for k, v in counts.items() if v > 1} + if duplicates: + sample = list(duplicates.items())[:6] + fail(f"{len(duplicates)} duplicated actor label(s), e.g. {sample}") + else: + log("OK no duplicate actor labels") + + log(f"== {failures} failure(s) ==") + + +try: + main() +except Exception as exc: # noqa: BLE001 - surface 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") diff --git a/Tools/ue_build_demo_map.py b/Tools/ue_build_demo_map.py index 33e5f2b..4a57c66 100644 --- a/Tools/ue_build_demo_map.py +++ b/Tools/ue_build_demo_map.py @@ -23,6 +23,8 @@ import unreal MAP = "/Game/Hollywood/DemoMap" # Repository root, so the texture generator can be invoked by path. ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import ue_placement # noqa: E402 RESULT = "/tmp/gigline/demo-map.txt" lines = [] @@ -116,14 +118,23 @@ def ensure_material(mat_name, spec, textures=None): does not read as one flat specular value. """ path = f"/Game/Hollywood/Site/{mat_name}" - if unreal.EditorAssetLibrary.does_asset_exist(path): - # Rebuilt every run so a look change cannot be masked by a stale asset. - unreal.EditorAssetLibrary.delete_asset(path) - - mat = unreal.AssetToolsHelpers.get_asset_tools().create_asset( - mat_name, "/Game/Hollywood/Site", unreal.Material, unreal.MaterialFactoryNew()) lib = unreal.MaterialEditingLibrary + # Update the existing material IN PLACE rather than deleting and recreating it. + # Deleting the asset to force a look rebuild silently orphaned every reference: + # after one demo-builder run, all 598 kit actors and the theatre trim pointed at + # deleted materials and rendered with the default one (604 actors with no + # material), while the map itself looked merely "a bit flat". + mat = unreal.EditorAssetLibrary.load_asset(path) + if mat is None: + mat = unreal.AssetToolsHelpers.get_asset_tools().create_asset( + mat_name, "/Game/Hollywood/Site", unreal.Material, unreal.MaterialFactoryNew()) + else: + # Clear the old graph so a removed node cannot survive a look change. + # `Material.expressions` is protected from Python, so use the editing + # library's own deleter rather than reading the array. + lib.delete_all_material_expressions(mat) + def node(cls): return lib.create_material_expression(mat, cls) @@ -401,7 +412,7 @@ def build(): # the NORTH side of the street, which only looked right while it stood alone. hero = unreal.EditorAssetLibrary.load_asset("/Game/Hollywood/ElCapitan/SM_HeroFacade") if hero: - actor = spawn(unreal.StaticMeshActor, unreal.Vector(-13336.0, 1307.0, 0.0), "Hero_Facade") + actor = spawn(unreal.StaticMeshActor, unreal.Vector(*ue_placement.HERO_LOCATION_CM), ue_placement.HERO_ACTOR) if actor: actor.static_mesh_component.set_static_mesh(hero) actor.static_mesh_component.set_mobility(unreal.ComponentMobility.STATIC) diff --git a/Tools/ue_hero_facade_import.py b/Tools/ue_hero_facade_import.py index 49e4e8b..46f0559 100644 --- a/Tools/ue_hero_facade_import.py +++ b/Tools/ue_hero_facade_import.py @@ -19,14 +19,19 @@ import unreal sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import ue_common # noqa: E402 +import ue_placement # noqa: E402 FBX = "/tmp/gigline/hero-facade.fbx" DEST = "/Game/Hollywood/ElCapitan" -ACTOR_LABEL = "Hero_Facade" +ACTOR_LABEL = ue_placement.HERO_ACTOR LEVEL = "/Game/Hollywood/DemoMap" # Unreal centimetres: south-east corner of the façade, on the building line. -PLACEMENT_CM = (-12024.5, -1450.0, 0.0) +# Placement lives in Tools/ue_placement.py. It used to be a literal here, and it +# drifted from ue_build_demo_map.py: this file kept the pre-0.1.0 position +# (-12024.5, -1450) while the builder staged (-13336, 1307), so running the importer +# on its own moved the hero 13 m east and onto the north side of the boulevard. +PLACEMENT_CM = ue_placement.HERO_LOCATION_CM RESULT = "/tmp/gigline/hero-facade-import.txt" lines = [] @@ -118,6 +123,17 @@ def measure_and_place(): comp.set_static_mesh(asset) comp.set_mobility(unreal.ComponentMobility.STATIC) log(f"placed {ACTOR_LABEL} at {PLACEMENT_CM} (cm) on {LEVEL}") + + # Prove it landed on its verified footprint, not merely that it spawned. + box = asset.get_bounding_box() if asset else None + if box: + lo = box.min.x + PLACEMENT_CM[0] + hi = box.max.x + PLACEMENT_CM[0] + problem = ue_placement.describe_span_failure("hero", lo, hi) + if problem: + log(f"PLACEMENT ERROR: {problem}") + else: + log(f"span ok: X {lo:.0f}..{hi:.0f}") _make_map_writable(LEVEL) saved = unreal.EditorLoadingAndSavingUtils.save_map( unreal.EditorLevelLibrary.get_editor_world(), LEVEL) diff --git a/Tools/ue_placement.py b/Tools/ue_placement.py new file mode 100644 index 0000000..9d20228 --- /dev/null +++ b/Tools/ue_placement.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Single source of truth for where staged actors belong in the DemoMap. + +This exists because the same value was written in two places and they drifted: +`ue_hero_facade_import.py` kept the pre-`0.1.0` position `(-12024.5, -1450)` while +`ue_build_demo_map.py` staged the hero at `(-13336, 1307)`. Running the importer on +its own therefore moved the hero **13 m east and onto the north side of the +boulevard** — and nothing complained, because a displaced building still renders. + +The values are Unreal centimetres, absolute world space. Every one of them is +verified against `Geo/verification/2026-11-06-el-capitan-block.md` and probed by +`Tools/visual_loop.py --verify` (via the demo screenshot expectations) and by the +world-span probes recorded in `MEMORY.md`. + +Keep this file boring: constants and one validator, no `unreal` import, so it can +be unit-tested on the host. +""" + +from __future__ import annotations + +# The hero façade sits INSIDE the frontage gap between 6902 (ends at -17749) and +# the El Capitan Theatre (starts at -12082), so its actor origin is the measured +# building line. The mesh is authored with its front plane 7 cm ahead of the actor +# origin, which is why the X here is the frontage centre and Y is the building line. +HERO_ACTOR = "Hero_Facade" +HERO_LOCATION_CM = (-13336.0, 1307.0, 0.0) + +# Tolerance for the span probe: the verified footprints are within ~1 m. +SPAN_TOLERANCE_CM = 100.0 + +# Expected world span per frontage actor (min X, max X), from the block +# verification notes. Used to prove a staged building is on its own footprint +# rather than merely present. +FRONTAGE_SPANS_CM = { + "6922": (-24276.0, -17700.0), + "6902": (-17749.0, -14591.0), + "hero": (-14644.0, -12028.0), + "6838": (-12082.0, -8210.0), + "6822": (-7854.0, -7252.0), + "6806": (-6608.0, -3307.0), + "6800": (-3320.0, -1437.0), +} + + +def span_error(name: str, observed_min: float, observed_max: float): + """Return (expected_min, expected_max, ok) for an observed building span. + + `name` is a key of `FRONTAGE_SPANS_CM`; `ok` is False when either edge is more + than `SPAN_TOLERANCE_CM` from the verified value. + """ + if name not in FRONTAGE_SPANS_CM: + raise KeyError(f"unknown building {name!r}; known: {sorted(FRONTAGE_SPANS_CM)}") + exp_min, exp_max = FRONTAGE_SPANS_CM[name] + ok = (abs(observed_min - exp_min) <= SPAN_TOLERANCE_CM + and abs(observed_max - exp_max) <= SPAN_TOLERANCE_CM) + return exp_min, exp_max, ok + + +def describe_span_failure(name: str, observed_min: float, observed_max: float) -> str: + """A one-line message for a misplaced building, or "" when it is correct.""" + exp_min, exp_max, ok = span_error(name, observed_min, observed_max) + if ok: + return "" + return (f"{name}: span X {observed_min:.0f}..{observed_max:.0f} does not match the " + f"verified {exp_min:.0f}..{exp_max:.0f} (±{SPAN_TOLERANCE_CM:.0f} cm)")