Ride: plant the planted shots (passerby/billboard camera + planner fixes)
driveRideCamera checked the passerby/billboard plants after the generic chase branch, which returns on every planned shot — both plants were dead code (camera orbited, only the car drive-through worked). Check them first. Planner fixes for the now-live plants: - a planted section keeps the plant on its first cue only; later cues fall back to the screen/room rotation (a whole section planted is a stuck camera with the car looping through one frame) - venueShotAt holds plant position too, not just the look - no consecutive planted sections in either order; the section-level history never forms streaks for single-cue sections, so gate on the previous section directly (draw-first, rng-neutral elsewhere) - passerby plant back/up, aimed between approach and pass point so the whoosh does not swallow the frame Verified: billboard holds centre-frame with the viz live while the car crosses mid-cue; passerby plant static +/- handheld; tests 24/24, scene lint clean, standard gates unchanged vs HEAD (P9/P12 + census centre/drone fail pre-existing).
This commit is contained in:
@@ -610,21 +610,27 @@ export const ride = {
|
||||
// far +z at cue start, past the lens mid-cue, far -z at the end — and
|
||||
// the world holds still around it. (A scrolling world under a driving
|
||||
// car would double the motion and read as a slide projector.)
|
||||
// Billboard: same planted grammar, but aimed at the roadside
|
||||
// billboard while the car drives through in front of it.
|
||||
const shot = framing && framing.venueShot ? framing.venueShot : null;
|
||||
const passerby = !!shot && shot.target === 'passerby';
|
||||
const driveT = passerby && typeof shot.drive === 'number'
|
||||
const billboard = !!shot && shot.target === 'billboard';
|
||||
const driveT = (passerby || billboard) && typeof shot.drive === 'number'
|
||||
? Math.max(0, Math.min(1, shot.drive)) : null;
|
||||
// World scroll: dashes, posts, roadside, chevrons and streaks stream
|
||||
// past toward +z (the car drives toward -z, so the world comes at the
|
||||
// camera). Frozen during passerby (see above). Parked mode keeps a 12%
|
||||
// camera). Frozen during passerby (see above) and during billboard —
|
||||
// except the billboard slot itself, which keeps its analytic scroll
|
||||
// (at half rate: it's a distant parallax layer, and holding it still
|
||||
// while aimed at it would read as parked). Parked mode keeps a 12%
|
||||
// idle creep plus a floor so the world never reads dead — a frozen
|
||||
// world with a crawling camera measured as "no movement at all".
|
||||
// Analytic in t (not += per frame): a seek must land on the same
|
||||
// positions as playback. The chase rate reads as driving at a glance
|
||||
// (~3x the original).
|
||||
{
|
||||
const dz = passerby ? 0
|
||||
: (instance.chase ? speed * 26 : speed * 26 * 0.12 + 0.25) * rate;
|
||||
const cruise = (instance.chase ? speed * 26 : speed * 26 * 0.12 + 0.25) * rate;
|
||||
const dz = (passerby || billboard) ? 0 : cruise;
|
||||
const kids = instance.dashes.children;
|
||||
for (let i = 0; i < kids.length; i++) {
|
||||
const z0 = -70 + i * 6;
|
||||
@@ -641,9 +647,18 @@ export const ride = {
|
||||
}
|
||||
// Roadside set dressing streams on the same clock: 12 slots spaced
|
||||
// 8 over [-84, +12]. Same wrap math, so houses/signs/trees and the
|
||||
// billboard hold formation with the posts.
|
||||
// billboard hold formation with the posts. Frozen whole during
|
||||
// billboard shots (the snap lands under the cut, like passerby) —
|
||||
// the motion in a billboard shot comes from the car, not the world.
|
||||
const rs = instance.roadside.children;
|
||||
// During a billboard shot the board is seated at a showcase spot
|
||||
// in view of the plant below — analytic in the shot flag, so no
|
||||
// state and no mid-shot drift. (The freeze already snaps the world
|
||||
// to base under the cut; seating the subject somewhere worth
|
||||
// looking at costs nothing extra.)
|
||||
const seatBill = billboard ? billSlotIndex(instance) : -1;
|
||||
for (let i = 0; i < rs.length; i++) {
|
||||
if (i === seatBill) { rs[i].position.z = -8; continue; }
|
||||
const z0 = -84 + i * 8;
|
||||
const span = 96;
|
||||
const z = z0 + t * dz;
|
||||
@@ -691,9 +706,9 @@ export const ride = {
|
||||
const sweepAmp = instance.chase ? 1.1 : 0.15;
|
||||
const sweep = Math.sin(t * 0.133 + instance.seed * 0.001);
|
||||
instance.car.position.x = sweep * sweepAmp;
|
||||
// Passerby drive-through: +12 (far, small) to -14 (far, gone) eased
|
||||
// across the cue. Otherwise the car lives at the origin.
|
||||
if (passerby && driveT !== null) {
|
||||
// Passerby/billboard drive-through: +12 (far, small) to -14 (far,
|
||||
// gone) eased across the cue. Otherwise the car lives at the origin.
|
||||
if ((passerby || billboard) && driveT !== null) {
|
||||
const e = driveT * driveT * (3 - 2 * driveT);
|
||||
instance.car.position.z = 12 + (-14 - 12) * e;
|
||||
} else {
|
||||
@@ -712,7 +727,7 @@ export const ride = {
|
||||
// axis (local x after the geometry pre-rotation). During a
|
||||
// passerby the car covers ~26 units a cue, so the wheels spin at
|
||||
// drive speed rather than cruise speed.
|
||||
const spin = passerby ? t * 14 : t * speed * 6 * rate;
|
||||
const spin = (passerby || billboard) ? t * 14 : t * speed * 6 * rate;
|
||||
for (const w of instance.wheels) {
|
||||
if (w.userData.tire) w.userData.tire.rotation.z = spin;
|
||||
if (w.userData.hub) w.userData.hub.rotation.z = spin;
|
||||
@@ -751,8 +766,11 @@ export const ride = {
|
||||
const n = instance.roadside.children.length;
|
||||
const dense = (arrangement % 2 === 1) || (vState && vState.popMul > 0.8);
|
||||
const showUpTo = dense ? n : Math.max(3, Math.floor(n * 0.45));
|
||||
// A billboard shot with a hidden billboard is an empty road: the
|
||||
// board's own slot is always on while it is the subject.
|
||||
const forced = billboard ? billSlotIndex(instance) : -1;
|
||||
for (let i = 0; i < n; i++) {
|
||||
instance.roadside.children[i].visible = opacity > 0.001 && (i < showUpTo);
|
||||
instance.roadside.children[i].visible = opacity > 0.001 && (i < showUpTo || i === forced);
|
||||
}
|
||||
}
|
||||
// Dressing takes the palette tint so houses/trees belong to the song;
|
||||
@@ -826,6 +844,46 @@ function driveRideCamera({ instance, camera, timeline, framing, personality, ven
|
||||
const lim = { x: 6.0, yLo: -0.2, yHi: 3.2, zLo: -10.0, zHi: 10.0 };
|
||||
const isJump = frame.venueJump !== undefined && frame.venueJump !== null;
|
||||
|
||||
// Passerby: the camera is a planted bystander — static plant, fixed road
|
||||
// point, handheld micro only. The car drives through on driveT (see the
|
||||
// car block below): far +z at cue start, past the lens mid-cue, far -z at
|
||||
// the end. No orbit, no traverse, no tracking corrections — a static
|
||||
// camera is the whole point of the shot. (Checked before the generic
|
||||
// chase branch below, which would otherwise swallow every planned shot.)
|
||||
if (planned && !isJump && planned.target === 'passerby') {
|
||||
camera.position.set(
|
||||
Math.max(-lim.x, Math.min(lim.x, planned.pos[0] + hx * 0.5)),
|
||||
Math.max(lim.yLo, Math.min(lim.yHi, planned.pos[1] + hy * 0.5)),
|
||||
Math.max(lim.zLo, Math.min(lim.zHi, planned.pos[2])),
|
||||
);
|
||||
camera.rotation.z = pCam ? pCam.spin * t * 0.5 : 0;
|
||||
camera.lookAt(planned.look[0], planned.look[1], planned.look[2]);
|
||||
camera.updateMatrixWorld();
|
||||
return;
|
||||
}
|
||||
|
||||
// Billboard: same planted grammar, aimed at the seated panel. The board
|
||||
// holds at its showcase spot (see above) so the plant is static with
|
||||
// handheld micro only; the plant sits on the opposite shoulder so the
|
||||
// car crosses between camera and board on its drive-through.
|
||||
if (planned && !isJump && planned.target === 'billboard') {
|
||||
const bi = billSlotIndex(instance);
|
||||
const bg = bi >= 0 ? instance.roadside.children[bi] : null;
|
||||
const bx = bg ? bg.position.x : 9;
|
||||
// Panel centre in world: group pos + scaled local offset (farK 1.2).
|
||||
const panelPos = [bx, 3.1 * 1.2, -8];
|
||||
const side = bx >= 0 ? -1 : 1;
|
||||
camera.position.set(
|
||||
Math.max(-lim.x, Math.min(lim.x, side * 4.2 + hx * 0.5)),
|
||||
Math.max(lim.yLo, Math.min(lim.yHi, 1.5 + hy * 0.5)),
|
||||
Math.max(lim.zLo, Math.min(lim.zHi, -2)),
|
||||
);
|
||||
camera.rotation.z = pCam ? pCam.spin * t * 0.5 : 0;
|
||||
camera.lookAt(panelPos[0], panelPos[1], panelPos[2]);
|
||||
camera.updateMatrixWorld();
|
||||
return;
|
||||
}
|
||||
|
||||
if (planned && !isJump) {
|
||||
// Wide base: the chase unit works FAR from the car (wides pull back
|
||||
// to ~7) and HIGH (up to ~3). The old 3.4 base parked every shot in
|
||||
@@ -893,23 +951,6 @@ function driveRideCamera({ instance, camera, timeline, framing, personality, ven
|
||||
return;
|
||||
}
|
||||
|
||||
// Passerby: the camera is a planted bystander — static plant, fixed road
|
||||
// point, handheld micro only. The car drives through on driveT (see the
|
||||
// car block below): far +z at cue start, past the lens mid-cue, far -z at
|
||||
// the end. No orbit, no traverse, no tracking corrections — a static
|
||||
// camera is the whole point of the shot.
|
||||
if (planned && !isJump && planned.target === 'passerby') {
|
||||
camera.position.set(
|
||||
Math.max(-lim.x, Math.min(lim.x, planned.pos[0] + hx * 0.5)),
|
||||
Math.max(lim.yLo, Math.min(lim.yHi, planned.pos[1] + hy * 0.5)),
|
||||
Math.max(lim.zLo, Math.min(lim.zHi, planned.pos[2])),
|
||||
);
|
||||
camera.rotation.z = pCam ? pCam.spin * t * 0.5 : 0;
|
||||
camera.lookAt(planned.look[0], planned.look[1], planned.look[2]);
|
||||
camera.updateMatrixWorld();
|
||||
return;
|
||||
}
|
||||
|
||||
// No plan (hand-built looks, tests): legacy traverse, forward look.
|
||||
const baseZ = 3.4;
|
||||
const dolly = baseZ / Math.max(scale, 0.05) - Math.max(0, push);
|
||||
@@ -925,6 +966,20 @@ function driveRideCamera({ instance, camera, timeline, framing, personality, ven
|
||||
camera.updateMatrixWorld();
|
||||
}
|
||||
|
||||
/**
|
||||
* Index of the billboard slot in the roadside group (-1 if none — defensive;
|
||||
* every build places exactly one). Read off roadsideMeta so plan and module
|
||||
* agree without sharing rng streams.
|
||||
*/
|
||||
function billSlotIndex(instance) {
|
||||
const meta = instance && instance.roadsideMeta;
|
||||
if (!meta) return -1;
|
||||
for (let i = 0; i < meta.length; i++) {
|
||||
if (meta[i] && meta[i].kind === 'billboard') return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function mulberry(seed) {
|
||||
let a = seed >>> 0;
|
||||
return function () {
|
||||
|
||||
Reference in New Issue
Block a user