feat: retire the gantry and billboard districts and light the rest as steel

This commit is contained in:
2026-10-11 00:43:25 +03:00
parent 4ff42288a8
commit fc897e534c
4 changed files with 36 additions and 35 deletions

View File

@@ -13,16 +13,21 @@ here = Path(__file__).resolve().parent
root = here.parents[2] root = here.parents[2]
scene = bpy.data.scenes['Roadside Districts'] scene = bpy.data.scenes['Roadside Districts']
output = root / 'src/assets/nightdrive-districts.glb' 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: for obj in scene.objects:
obj.select_set(True) obj.select_set(obj in exported)
bpy.ops.export_scene.gltf( bpy.ops.export_scene.gltf(
filepath=str(output), export_format='GLB', use_selection=True, use_active_scene=True, 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_apply=True, export_normals=False, export_texcoords=False, export_materials='NONE',
export_animations=False, export_cameras=False, export_lights=False, export_yup=True, export_animations=False, export_cameras=False, export_lights=False, export_yup=True,
) )
triangles = 0 triangles = 0
for obj in scene.objects: for obj in exported:
obj.data.calc_loop_triangles() obj.data.calc_loop_triangles()
triangles += len(obj.data.loop_triangles) triangles += len(obj.data.loop_triangles)
provenance = { provenance = {
@@ -30,7 +35,8 @@ provenance = {
'source': 'nightdrive-districts.blend', 'source': 'nightdrive-districts.blend',
'authorship': 'Original procedural modelling in Blender for this portfolio (author-districts.py); no external assets.', 'authorship': 'Original procedural modelling in Blender for this portfolio (author-districts.py); no external assets.',
'editable_scene': 'Roadside Districts', '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, 'triangles': triangles,
'attributes': 'POSITION + indices; flat normals derived at runtime', 'attributes': 'POSITION + indices; flat normals derived at runtime',
'export': '../../../src/assets/nightdrive-districts.glb', 'export': '../../../src/assets/nightdrive-districts.glb',

View File

@@ -5,16 +5,18 @@
"editable_scene": "Roadside Districts", "editable_scene": "Roadside Districts",
"pieces": [ "pieces": [
"arch", "arch",
"billboard",
"container", "container",
"crane", "crane",
"gantry",
"mast", "mast",
"rack-row", "rack-row",
"workshop" "workshop"
], ],
"triangles": 5424, "retired": [
"billboard",
"gantry"
],
"triangles": 4392,
"attributes": "POSITION + indices; flat normals derived at runtime", "attributes": "POSITION + indices; flat normals derived at runtime",
"export": "../../../src/assets/nightdrive-districts.glb", "export": "../../../src/assets/nightdrive-districts.glb",
"sha256": "428a212792ac4d8dbf86b9224e9788f49cfc22b3d0dd1b72dfaf33c89f08f189" "sha256": "4fb4acbd90ccd1ed6cb56362c3c2fca3347e0d6f53d37f19e925478f1be4e252"
} }

Binary file not shown.

View File

@@ -18,8 +18,6 @@ import { WORLD_DEPTH, WORLD_FAR_Z } from './metrics';
* like the expressway furniture. Districts at rest are hidden, so only the * like the expressway furniture. Districts at rest are hidden, so only the
* current chapter's pieces cost draw calls. */ * current chapter's pieces cost draw calls. */
export type DistrictPiece = export type DistrictPiece =
| 'gantry'
| 'billboard'
| 'rack-row' | 'rack-row'
| 'mast' | 'mast'
| 'workshop' | 'workshop'
@@ -34,7 +32,7 @@ interface Placement {
/** Yaw in radians, applied before mirroring. */ /** Yaw in radians, applied before mirroring. */
ry?: number; ry?: number;
s?: number; s?: number;
/** Extra vertical stretch (gantries clear the chase camera). */ /** Extra vertical stretch (the arch clears the chase camera). */
sy?: number; sy?: number;
} }
@@ -42,18 +40,10 @@ interface Placement {
export const DISTRICT_PRESENCE: Readonly< export const DISTRICT_PRESENCE: Readonly<
Record<DistrictPiece, Partial<Record<SectionId, number>>> Record<DistrictPiece, Partial<Record<SectionId, number>>>
> = { > = {
gantry: { // Pieces rise fully or not at all: a part-raised piece is squashed flat.
profile: 1,
experience: 1,
infrastructure: 1,
'selected-builds': 1,
'additional-work': 1,
toolkit: 1,
},
billboard: { profile: 1, experience: 1 },
'rack-row': { infrastructure: 1 }, 'rack-row': { infrastructure: 1 },
mast: { infrastructure: 0.75, toolkit: 1 }, mast: { infrastructure: 1, toolkit: 1 },
workshop: { 'selected-builds': 1, 'additional-work': 0.5 }, workshop: { 'selected-builds': 1 },
crane: { 'selected-builds': 1, 'additional-work': 1 }, crane: { 'selected-builds': 1, 'additional-work': 1 },
container: { 'additional-work': 1 }, container: { 'additional-work': 1 },
arch: { contact: 1 }, arch: { contact: 1 },
@@ -67,14 +57,6 @@ const span = (count: number, from: number, to: number) =>
function placementsFor(piece: DistrictPiece, dense: boolean): Placement[] { function placementsFor(piece: DistrictPiece, dense: boolean): Placement[] {
const n = (full: number) => (dense ? full : Math.ceil(full / 2)); const n = (full: number) => (dense ? full : Math.ceil(full / 2));
switch (piece) { 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': case 'rack-row':
return span(n(18), -26, -26 - WORLD_DEPTH).flatMap((z, i) => { return span(n(18), -26, -26 - WORLD_DEPTH).flatMap((z, i) => {
const side = i % 2 ? 1 : -1; const side = i % 2 ? 1 : -1;
@@ -194,17 +176,28 @@ export function createDistricts(
varying float vHeight; varying float vHeight;
void main() { void main() {
${FADES} ${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))); vec3 n = normalize(cross(dFdx(vWorld), dFdy(vWorld)));
float key = max(0.0, dot(n, normalize(vec3(-0.45, 0.85, 0.35)))); 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; float fill = max(0.0, dot(n, normalize(vec3(0.6, 0.2, -0.4))));
vec3 color = vec3(0.03, 0.016, 0.05) + vec3(0.085, 0.06, 0.14) * key; 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;
color += uAccent * rim * 0.06 * (0.4 + 0.6 * smoothstep(0.0, 12.0, vHeight)); // Emissive edge: the silhouette catches the chapter colour, and
gl_FragColor = vec4(color, fade * smoothstep(0.0, 0.25, vRise)); // 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 <tonemapping_fragment> #include <tonemapping_fragment>
#include <colorspace_fragment> #include <colorspace_fragment>
} }
`, `,
transparent: true, transparent: false,
depthWrite: true, depthWrite: true,
side: THREE.DoubleSide, side: THREE.DoubleSide,
}), }),
@@ -253,7 +246,7 @@ export function createDistricts(
const entry = state.get(piece); const entry = state.get(piece);
if (!entry) return; if (!entry) return;
const placements = placementsFor(piece, quality.postEffects); 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 [ for (const [geometry, material] of [
[body, bodyMaterial(entry.rise)], [body, bodyMaterial(entry.rise)],
[light, lightMaterial(entry.rise, accentMix)], [light, lightMaterial(entry.rise, accentMix)],