feat: sculpt portfolio into an immersive midnight expressway
Author physical road furniture, layered terrain and atmosphere, projected illuminated route signs, and detailed Blender coachwork. Refine camera mass, wheel contact, chapter composition, and compact reading space while preserving approved content and audio contracts. Include isolated Blender source, before/after and hardware Chrome motion evidence, preservation hashes, validated preview checks, and documented inherited unit failures.
This commit is contained in:
212
src/scene/world/expressway.ts
Normal file
212
src/scene/world/expressway.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import * as THREE from 'three';
|
||||
import type { SceneResourceBag } from '@/scene/resources';
|
||||
import type { RenderQualitySettings } from '@/scene/render-quality';
|
||||
import type { SectionId } from '@/content/types';
|
||||
import { WORLD_DEPTH, WORLD_FAR_Z } from './metrics';
|
||||
|
||||
/** A bounded kit of physical roadside objects. Instance origins wrap behind the
|
||||
* camera; the entire object moves together, so long beams never tear at a seam. */
|
||||
export function createExpressway(
|
||||
resources: SceneResourceBag,
|
||||
quality: RenderQualitySettings,
|
||||
) {
|
||||
const group = new THREE.Group();
|
||||
group.name = 'expressway-architecture';
|
||||
const uniforms = {
|
||||
uDistance: { value: 0 },
|
||||
uEnergy: { value: 0 },
|
||||
uArchitecture: { value: 0 },
|
||||
uWarmth: { value: 0 },
|
||||
};
|
||||
const vertexShader = `
|
||||
uniform float uDistance;
|
||||
uniform float uArchitecture;
|
||||
varying vec3 vNormal;
|
||||
varying vec3 vWorld;
|
||||
varying float vDepth;
|
||||
void main() {
|
||||
vec4 origin = instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0);
|
||||
vec4 p = instanceMatrix * vec4(position, 1.0);
|
||||
p.z += mod(origin.z + uDistance - ${WORLD_FAR_Z.toFixed(1)}, ${WORLD_DEPTH.toFixed(1)}) + ${WORLD_FAR_Z.toFixed(1)} - origin.z;
|
||||
p.y *= 1.0 + uArchitecture * 0.22;
|
||||
vNormal = normalize(mat3(instanceMatrix) * normal);
|
||||
vWorld = p.xyz;
|
||||
vec4 view = modelViewMatrix * p;
|
||||
vDepth = -view.z;
|
||||
gl_Position = projectionMatrix * view;
|
||||
}
|
||||
`;
|
||||
function material(luminous: boolean, coral = false) {
|
||||
return resources.own(
|
||||
'material',
|
||||
new THREE.ShaderMaterial({
|
||||
uniforms,
|
||||
vertexShader,
|
||||
fragmentShader: `
|
||||
uniform float uEnergy;
|
||||
uniform float uWarmth;
|
||||
varying vec3 vNormal;
|
||||
varying vec3 vWorld;
|
||||
varying float vDepth;
|
||||
void main() {
|
||||
float farFade = 1.0 - smoothstep(380.0, 660.0, vDepth);
|
||||
float nearFade = smoothstep(0.0, 9.0, vDepth);
|
||||
vec3 cyan = vec3(0.15, 0.69, 0.76);
|
||||
vec3 coral = vec3(1.0, 0.18, 0.095);
|
||||
float facing = max(0.0, dot(normalize(vNormal), normalize(vec3(-0.5, 0.9, 0.6))));
|
||||
vec3 color = ${luminous ? `mix(${coral ? 'coral' : 'cyan'}, coral, uWarmth * 0.12) * (1.3 + uEnergy * 0.5)` : 'vec3(0.015, 0.027, 0.044) + vec3(0.055, 0.10, 0.135) * pow(facing, 2.0)'};
|
||||
${!luminous ? 'color += cyan * pow(max(0.0, vNormal.x), 2.0) * 0.028;' : ''}
|
||||
if(farFade * nearFade < 0.02) discard;
|
||||
gl_FragColor = vec4(color, farFade * nearFade);
|
||||
#include <tonemapping_fragment>
|
||||
#include <colorspace_fragment>
|
||||
}
|
||||
`,
|
||||
transparent: true,
|
||||
depthWrite: !luminous,
|
||||
depthTest: true,
|
||||
blending: luminous ? THREE.AdditiveBlending : THREE.NormalBlending,
|
||||
}),
|
||||
);
|
||||
}
|
||||
const graphite = material(false);
|
||||
const cyan = material(true);
|
||||
const coral = material(true, true);
|
||||
const box = resources.own('geometry', new THREE.BoxGeometry(1, 1, 1));
|
||||
const dummy = new THREE.Object3D();
|
||||
type Part = {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
sx: number;
|
||||
sy: number;
|
||||
sz: number;
|
||||
rz?: number;
|
||||
};
|
||||
function batch(
|
||||
name: string,
|
||||
parts: Part[],
|
||||
surface: ReturnType<typeof material>,
|
||||
) {
|
||||
const mesh = resources.own(
|
||||
'other',
|
||||
new THREE.InstancedMesh(box, surface, parts.length),
|
||||
);
|
||||
mesh.name = name;
|
||||
mesh.frustumCulled = false;
|
||||
parts.forEach((part, i) => {
|
||||
dummy.position.set(part.x, part.y, part.z);
|
||||
dummy.scale.set(part.sx, part.sy, part.sz);
|
||||
dummy.rotation.set(0, 0, part.rz ?? 0);
|
||||
dummy.updateMatrix();
|
||||
mesh.setMatrixAt(i, dummy.matrix);
|
||||
});
|
||||
mesh.instanceMatrix.needsUpdate = true;
|
||||
group.add(mesh);
|
||||
}
|
||||
const posts: Part[] = [],
|
||||
reflectors: Part[] = [],
|
||||
rails: Part[] = [];
|
||||
// Full density retains a recognisable rhythm even at the lowest quality.
|
||||
const pairs = quality.postEffects ? 56 : 40;
|
||||
for (let i = 0; i < pairs; i++)
|
||||
for (const side of [-1, 1]) {
|
||||
const z = 30 - (i * WORLD_DEPTH) / pairs;
|
||||
posts.push({ x: side * 9.5, y: 0.63, z, sx: 0.22, sy: 1.26, sz: 0.32 });
|
||||
reflectors.push({
|
||||
x: side * 9.48,
|
||||
y: 1.1,
|
||||
z: z + 0.18,
|
||||
sx: 0.14,
|
||||
sy: 0.22,
|
||||
sz: 0.03,
|
||||
});
|
||||
rails.push({
|
||||
x: side * 9.5,
|
||||
y: 0.56,
|
||||
z,
|
||||
sx: 0.16,
|
||||
sy: 0.18,
|
||||
sz: WORLD_DEPTH / pairs + 0.1,
|
||||
});
|
||||
}
|
||||
batch('barrier-posts', posts, graphite);
|
||||
batch('barrier-reflectors', reflectors, coral);
|
||||
batch('continuous-low-rails', rails, graphite);
|
||||
const fins: Part[] = [],
|
||||
lights: Part[] = [],
|
||||
feet: Part[] = [];
|
||||
for (let i = 0; i < 8; i++)
|
||||
for (const side of [-1, 1]) {
|
||||
const z = -24 - (i * WORLD_DEPTH) / 8;
|
||||
// The canted cap is the repeated silhouette: road furniture, not a tunnel.
|
||||
fins.push({ x: side * 12.6, y: 5.0, z, sx: 0.38, sy: 10, sz: 0.64 });
|
||||
fins.push({
|
||||
x: side * 10.45,
|
||||
y: 10.55,
|
||||
z,
|
||||
sx: 0.32,
|
||||
sy: 4.6,
|
||||
sz: 0.64,
|
||||
rz: side * 1.31,
|
||||
});
|
||||
lights.push({
|
||||
x: side * 12.36,
|
||||
y: 5.9,
|
||||
z: z + 0.34,
|
||||
sx: 0.075,
|
||||
sy: 7.5,
|
||||
sz: 0.035,
|
||||
});
|
||||
lights.push({
|
||||
x: side * 10.4,
|
||||
y: 10.4,
|
||||
z: z + 0.34,
|
||||
sx: 0.07,
|
||||
sy: 4.35,
|
||||
sz: 0.04,
|
||||
rz: side * 1.31,
|
||||
});
|
||||
feet.push({ x: side * 12.6, y: 0.24, z, sx: 1.1, sy: 0.48, sz: 1.5 });
|
||||
}
|
||||
batch('swept-signal-pylons', fins, graphite);
|
||||
batch('recessed-pylon-lights', lights, cyan);
|
||||
batch('pylon-foundations', feet, graphite);
|
||||
// Far roadside monolith pairs give the waves an occasional architectural edge.
|
||||
const landmarks: Part[] = [];
|
||||
for (let i = 0; i < 6; i++)
|
||||
for (const side of [-1, 1]) {
|
||||
const z = -110 - (i * WORLD_DEPTH) / 6;
|
||||
for (let j = 0; j < 3; j++) {
|
||||
landmarks.push({
|
||||
x: side * (34 + j * 4.5),
|
||||
y: (15 + j * 6) / 2,
|
||||
z: z - j * 3,
|
||||
sx: 1.2,
|
||||
sy: 15 + j * 6,
|
||||
sz: 2.6,
|
||||
rz: side * 0.1,
|
||||
});
|
||||
}
|
||||
}
|
||||
batch('ridge-observatories', landmarks, graphite);
|
||||
let architecture = 0;
|
||||
return {
|
||||
group,
|
||||
update(
|
||||
distance: number,
|
||||
energy: number,
|
||||
warmth: number,
|
||||
section: SectionId,
|
||||
dt: number,
|
||||
) {
|
||||
const target =
|
||||
section === 'infrastructure' ? 1 : section === 'toolkit' ? 0.65 : 0;
|
||||
architecture += (target - architecture) * (1 - Math.exp(-dt * 0.7));
|
||||
uniforms.uDistance.value = distance;
|
||||
uniforms.uEnergy.value = energy;
|
||||
uniforms.uArchitecture.value = architecture;
|
||||
uniforms.uWarmth.value = warmth;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user