diff --git a/flow-state/src/venues/ride.js b/flow-state/src/venues/ride.js index 6f590b1..13705be 100644 --- a/flow-state/src/venues/ride.js +++ b/flow-state/src/venues/ride.js @@ -468,6 +468,7 @@ export const ride = { roughness: 0.15, metalness: 0.7, transparent: opacity < 0.999, opacity, })); + cabin.name = 'cabin'; cabin.position.set(0, 0.5, -0.2); instance.car.add(cabin); instance.bodyMeshes.push(cabin); @@ -532,41 +533,64 @@ 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 — + // Roof display: the viz at a size worth looking at. A wraparound + // glass band over the cabin, 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). + // camera actually sees. Two placements were tried first: a raked + // windshield across the cabin front (edge-on and invisible from + // the side angles the orbit favours) and a low wraparound band + // (buried inside tall procedural bodies — the chassis varies per + // track and no fixed glass clears all of it). Above the roofline + // nothing occludes it from any chase angle, and the curve still + // presents a face to the lens everywhere: abstract viz has no + // handedness, so the mirrored rear view costs nothing. 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, + side: T.DoubleSide, }); - 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); + // Cylinder side: x = r·sinθ, z = r·cosθ, so θ = π centres the + // band on -z (forward) and ±100° wraps it around both flanks. + // Sized to read at chase distance: the car is ~2.5 units filmed + // from 4-6 away, so anything smaller is a rumour at card scale. + // Floated above the body's highest point + margin, measured off + // what was just built — the chassis varies per track and a fixed + // height gets buried (measured). Crew heads (≤0.92 with bob, + // built below) are floored in. Static per arrangement: the key + // carries no per-section seeds, so it never moves mid-video. + instance.car.updateMatrixWorld(true); + const measBox = new T.Box3(); + for (const n of ['chassis', 'cabin']) { + const o = instance.car.getObjectByName(n); + if (o) measBox.expandByObject(o); + } + const bandY = Math.max( + 1.25, (measBox.isEmpty() ? 0.7 : measBox.max.y) + 0.22); + const shield = new T.Mesh( + new T.CylinderGeometry(1.1, 1.1, 0.6, 24, 1, true, Math.PI - 1.745, 3.49), + shieldMat); + shield.position.set(0, bandY, -0.32); 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. + // Masts seating the band on the cabin top. Sized from the + // measured band height so they always meet it. 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); + { + const postH = Math.max(0.1, (bandY - 0.3) - 0.66); + for (const side of [-1, 1]) { + const pillar = new T.Mesh(new T.BoxGeometry(0.05, postH, 0.05), pillarMat); + pillar.position.set(side * 0.7, 0.66 + postH / 2, -1.17); + pillar.name = 'pillar'; + instance.car.add(pillar); + instance.bodyMeshes.push(pillar); + } } // Headlight cones: two small translucent wedges so chase mode