diff --git a/flow-state/src/venues/ride.js b/flow-state/src/venues/ride.js index 2b9bf37..6f590b1 100644 --- a/flow-state/src/venues/ride.js +++ b/flow-state/src/venues/ride.js @@ -42,13 +42,18 @@ export const ride = { // Venue RTT: the compositor calls this with the finished viz texture after // compositing the viz stack into vizTarget. The radio screen samples it, // so the viz plays ON the dash rather than composited over the venue — - // and so does the roadside billboard when the seed placed one. + // and so does the windshield below and the roadside billboard when the + // seed placed one. bindVizTexture(instance, texture) { if (!instance || !texture) return; if (instance.radioMat && instance.radioMat.emissiveMap !== texture) { instance.radioMat.emissiveMap = texture; instance.radioMat.needsUpdate = true; } + if (instance.shieldMat && instance.shieldMat.emissiveMap !== texture) { + instance.shieldMat.emissiveMap = texture; + instance.shieldMat.needsUpdate = true; + } if (instance.billMat && instance.billMat.emissiveMap !== texture) { instance.billMat.emissiveMap = texture; instance.billMat.needsUpdate = true; @@ -339,7 +344,7 @@ export const ride = { skyMesh: sky.mesh, skyMat: sky.mat, crew, ambient, key, tail, - radioMat: null, radioMesh: null, billPanel: null, + radioMat: null, radioMesh: null, shieldMat: null, shieldMesh: null, billPanel: null, bodyMeshes: [], seed, venueSpec: venueSpec || null, @@ -527,6 +532,43 @@ export const ride = { instance.radioMesh = radio; instance.bodyMeshes.push(radio); + // Windshield: the viz at a size worth looking at. A raked plane + // across the cabin front, same emissiveMap feed as the radio — + // the dash screen is for the crew, this is the one the chase + // camera actually sees. Front hemisphere only (single-sided): + // from behind it reads as dark glass, like a real windshield. + // Clears the dash (top 0.87 at z ≥ -1.09) and the crew (z ≥ -0.4). + const shieldMat = new T.MeshStandardMaterial({ + color: new T.Color(0.02, 0.02, 0.02), + emissive: new T.Color(1, 1, 1), + emissiveIntensity: 1.0, + roughness: 0.4, metalness: 0.2, + }); + const shield = new T.Mesh(new T.PlaneGeometry(1.44, 0.44), shieldMat); + shield.position.set(0, 0.8, -1.14); + // Face up-forward: lookAt aims +z at the target, so the normal + // lands ~23° off vertical — a raked windshield, not a billboard. + // Car group is at identity in build, so local == world here. + shield.lookAt(0, 3.0, -6.0); + shield.name = 'windshield'; + instance.car.add(shield); + instance.shieldMat = shieldMat; + instance.shieldMesh = shield; + instance.bodyMeshes.push(shield); + // A-pillars so the glass sits in the car instead of hovering over + // it: thin struts from the cabin top to the glass top corners. + const pillarMat = new T.MeshStandardMaterial({ + color: new T.Color(0.05, 0.05, 0.06), roughness: 0.6, metalness: 0.3, + }); + for (const side of [-1, 1]) { + const pillar = new T.Mesh(new T.BoxGeometry(0.05, 0.37, 0.05), pillarMat); + pillar.position.set(side * 0.7, 0.83, -1.1); + pillar.rotation.x = 0.31; + pillar.name = 'pillar'; + instance.car.add(pillar); + instance.bodyMeshes.push(pillar); + } + // Headlight cones: two small translucent wedges so chase mode // reads. Kept short and faint — long cones filled the frame. const beamMat = new T.MeshBasicMaterial({ @@ -791,6 +833,13 @@ export const ride = { instance.radioMat.emissive.setRGB(1, 1, 1); instance.radioMat.emissiveIntensity = (params.light || 0.85) * (0.7 + l * 0.7); } + // Windshield glow, same feed, slightly restrained — it is 4x the + // radio's area and would blow out at the same intensity. + if (instance.shieldMat) { + const l = vState ? vState.light : 1; + instance.shieldMat.emissive.setRGB(1, 1, 1); + instance.shieldMat.emissiveIntensity = (params.light || 0.85) * (0.55 + l * 0.55); + } // Ground follows horizon like the stage field. instance.road.position.y = -1.15 + (spaceHorizon - 0.5) * 0.6;