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 b1f80dc..fdc3b59 100644 Binary files a/src/assets/nightdrive-districts.glb and b/src/assets/nightdrive-districts.glb differ 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)],