From fc897e534cc14f18aac6ca85ac7cb5bff946bc18 Mon Sep 17 00:00:00 2001 From: LucaP Date: Sun, 11 Oct 2026 00:43:25 +0300 Subject: [PATCH] feat: retire the gantry and billboard districts and light the rest as steel --- assets/blender/districts/export-districts.py | 12 +++-- .../districts/nightdrive-districts.asset.json | 10 ++-- src/assets/nightdrive-districts.glb | Bin 83676 -> 67640 bytes src/scene/world/districts.ts | 49 ++++++++---------- 4 files changed, 36 insertions(+), 35 deletions(-) diff --git a/assets/blender/districts/export-districts.py b/assets/blender/districts/export-districts.py index bd02ba4..2137425 100644 --- a/assets/blender/districts/export-districts.py +++ b/assets/blender/districts/export-districts.py @@ -13,16 +13,21 @@ here = Path(__file__).resolve().parent root = here.parents[2] scene = bpy.data.scenes['Roadside Districts'] output = root / 'src/assets/nightdrive-districts.glb' +# Retired in round 6: the overhead gantry and the sign truss now own the road, +# and the billboards' light strips read as garbled text. They stay authored in +# the .blend but are no longer exported. +RETIRED = {'gantry', 'billboard'} +exported = [o for o in scene.objects if o.name.rsplit('_', 1)[0] not in RETIRED] for obj in scene.objects: - obj.select_set(True) + obj.select_set(obj in exported) bpy.ops.export_scene.gltf( filepath=str(output), export_format='GLB', use_selection=True, use_active_scene=True, export_apply=True, export_normals=False, export_texcoords=False, export_materials='NONE', export_animations=False, export_cameras=False, export_lights=False, export_yup=True, ) triangles = 0 -for obj in scene.objects: +for obj in exported: obj.data.calc_loop_triangles() triangles += len(obj.data.loop_triangles) provenance = { @@ -30,7 +35,8 @@ provenance = { 'source': 'nightdrive-districts.blend', 'authorship': 'Original procedural modelling in Blender for this portfolio (author-districts.py); no external assets.', 'editable_scene': 'Roadside Districts', - 'pieces': sorted({o.name.rsplit('_', 1)[0] for o in scene.objects}), + 'pieces': sorted({o.name.rsplit('_', 1)[0] for o in exported}), + 'retired': sorted(RETIRED), 'triangles': triangles, 'attributes': 'POSITION + indices; flat normals derived at runtime', 'export': '../../../src/assets/nightdrive-districts.glb', diff --git a/assets/blender/districts/nightdrive-districts.asset.json b/assets/blender/districts/nightdrive-districts.asset.json index 5cff713..4954753 100644 --- a/assets/blender/districts/nightdrive-districts.asset.json +++ b/assets/blender/districts/nightdrive-districts.asset.json @@ -5,16 +5,18 @@ "editable_scene": "Roadside Districts", "pieces": [ "arch", - "billboard", "container", "crane", - "gantry", "mast", "rack-row", "workshop" ], - "triangles": 5424, + "retired": [ + "billboard", + "gantry" + ], + "triangles": 4392, "attributes": "POSITION + indices; flat normals derived at runtime", "export": "../../../src/assets/nightdrive-districts.glb", - "sha256": "428a212792ac4d8dbf86b9224e9788f49cfc22b3d0dd1b72dfaf33c89f08f189" + "sha256": "4fb4acbd90ccd1ed6cb56362c3c2fca3347e0d6f53d37f19e925478f1be4e252" } diff --git a/src/assets/nightdrive-districts.glb b/src/assets/nightdrive-districts.glb index b1f80dcf6386830c66f6929b4543ecfa08e88e1d..fdc3b59feceff719d5f25e7ffba62f1cfc8a758a 100644 GIT binary patch delta 647 zcmXZWPiWI{6bJAoP1Bk+O_Tmj*EZ??z_8V&$uDUF6EFUo7a0m(IutKM@Svgx{}cuu z#KF+|h2dd}WuOGK|s?|W}+KJ@w?7n`|o z>IfSM1l$M{_>pPGmYQpIoMlwT!gXetA$Y_Tag7;3he_cfBVde8;tlmEo5B`#hRxy+ zb&k!U$O>3yRUDvx2us*zeav$L4srXj9vq~#2aU_)J7?iC)ehO{P|Li9OsIexJi&SD zI$uM(=N^oO#}VTTxDs}8hR@Tz9$xYyYg&FhBC(L7Pw_&~@g+KppMnNeY+-po!&h+# zJJft^0jp6BpJSu=O-)Nr&=xgpN+ak}RpkLzVmdaI5&WWRf&eYCh_+P1M_~wmB^{fI zt9TwS;Zl;`i0gQjyn|I)$8?JBlr^+cz>1>bWSZ`vE~M|Hok-)8DEP-R9)pG|qnRDY znkok!+r(6^g033pT+bl*R@{q;oE-EF508sw+{`67&m=}~@HVHgRSzfBeR!9ZgQjaB zPQwQW<&f!GgrG&!xAF?@Y^NW2pI11`F$p%*GG?=Kh?s`aAJG-Gv)q2{a6#dS>D16v zE4W>dIbz#3rf8~DP*_sKU3xWAR5;tRo!-yKi*m@WdH)a1?T$RM`}G9p7!JYqZc(k8 o(6x%_REg_0rW?9$>;(MN+NkJ1voD@os&^cJv7|G8y*oSk4~!hj-2eap delta 9935 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 DnPuUb diff --git a/src/scene/world/districts.ts b/src/scene/world/districts.ts index 0b6bd51..b4aa0a5 100644 --- a/src/scene/world/districts.ts +++ b/src/scene/world/districts.ts @@ -18,8 +18,6 @@ import { WORLD_DEPTH, WORLD_FAR_Z } from './metrics'; * like the expressway furniture. Districts at rest are hidden, so only the * current chapter's pieces cost draw calls. */ export type DistrictPiece = - | 'gantry' - | 'billboard' | 'rack-row' | 'mast' | 'workshop' @@ -34,7 +32,7 @@ interface Placement { /** Yaw in radians, applied before mirroring. */ ry?: number; s?: number; - /** Extra vertical stretch (gantries clear the chase camera). */ + /** Extra vertical stretch (the arch clears the chase camera). */ sy?: number; } @@ -42,18 +40,10 @@ interface Placement { export const DISTRICT_PRESENCE: Readonly< Record>> > = { - gantry: { - profile: 1, - experience: 1, - infrastructure: 1, - 'selected-builds': 1, - 'additional-work': 1, - toolkit: 1, - }, - billboard: { profile: 1, experience: 1 }, + // Pieces rise fully or not at all: a part-raised piece is squashed flat. 'rack-row': { infrastructure: 1 }, - mast: { infrastructure: 0.75, toolkit: 1 }, - workshop: { 'selected-builds': 1, 'additional-work': 0.5 }, + mast: { infrastructure: 1, toolkit: 1 }, + workshop: { 'selected-builds': 1 }, crane: { 'selected-builds': 1, 'additional-work': 1 }, container: { 'additional-work': 1 }, arch: { contact: 1 }, @@ -67,14 +57,6 @@ const span = (count: number, from: number, to: number) => function placementsFor(piece: DistrictPiece, dense: boolean): Placement[] { const n = (full: number) => (dense ? full : Math.ceil(full / 2)); switch (piece) { - case 'gantry': - return span(n(3), -70, -70 - WORLD_DEPTH).map((z) => ({ x: 0, z, sy: 1.42 })); - case 'billboard': - return span(n(10), -40, -40 - WORLD_DEPTH).map((z, i) => ({ - x: (i % 2 ? 1 : -1) * 16, - z, - ry: (i % 2 ? 1 : -1) * 0.28, - })); case 'rack-row': return span(n(18), -26, -26 - WORLD_DEPTH).flatMap((z, i) => { const side = i % 2 ? 1 : -1; @@ -194,17 +176,28 @@ export function createDistricts( varying float vHeight; void main() { ${FADES} + // Opaque dark steel, like the sign truss: fades dissolve through a + // fixed screen-door pattern instead of alpha, so pieces never read + // as translucent sheets over the sky and the sun. + float cover = fade * smoothstep(0.0, 0.25, vRise); + vec2 cell = mod(floor(gl_FragCoord.xy), 4.0); + float threshold = (mod(cell.x + cell.y * 2.0, 4.0) * 4.0 + mod(cell.y, 2.0) * 2.0 + mod(cell.x, 2.0) + 0.5) / 16.0; + if (cover < threshold) discard; vec3 n = normalize(cross(dFdx(vWorld), dFdy(vWorld))); float key = max(0.0, dot(n, normalize(vec3(-0.45, 0.85, 0.35)))); - float rim = pow(1.0 - abs(n.y), 3.0) * 0.5; - vec3 color = vec3(0.03, 0.016, 0.05) + vec3(0.085, 0.06, 0.14) * key; - color += uAccent * rim * 0.06 * (0.4 + 0.6 * smoothstep(0.0, 12.0, vHeight)); - gl_FragColor = vec4(color, fade * smoothstep(0.0, 0.25, vRise)); + float fill = max(0.0, dot(n, normalize(vec3(0.6, 0.2, -0.4)))); + vec3 color = vec3(0.022, 0.018, 0.034) + vec3(0.07, 0.06, 0.1) * key + vec3(0.02, 0.015, 0.035) * fill; + // Emissive edge: the silhouette catches the chapter colour, and + // the tops of the pieces carry a thin lit band. + float rim = pow(1.0 - abs(dot(n, normalize(cameraPosition - vWorld))), 4.0); + float crown = smoothstep(0.92, 1.0, n.y) * smoothstep(1.0, 6.0, vHeight); + color += uAccent * (rim * 0.22 + crown * 0.05); + gl_FragColor = vec4(color, 1.0); #include #include } `, - transparent: true, + transparent: false, depthWrite: true, side: THREE.DoubleSide, }), @@ -253,7 +246,7 @@ export function createDistricts( const entry = state.get(piece); if (!entry) return; const placements = placementsFor(piece, quality.postEffects); - const accentMix = piece === 'gantry' || piece === 'arch' || piece === 'billboard' ? 1 : 0.35; + const accentMix = piece === 'arch' ? 1 : 0.35; for (const [geometry, material] of [ [body, bodyMaterial(entry.rise)], [light, lightMaterial(entry.rise, accentMix)],