Ride: venue-owned camera (directCamera) + sidewalk passerby shot
Camera architecture: a module with directCamera:true drives its own rig inside update() (analytic f(timeline, seed, framing, venueState)); the shared rig is ignored for that layer only. ModelLayer prefers own camera, Show skips shared injection. Ride migrates its full grammar over (traverse, orbit, car tracking, jumps, clamp); Compositor ride branches removed. Chamber/stage unchanged on the shared path. Sidewalk shot: new ride-only 'passerby' plan target (breaths only, never climax, never twice running) plants a static roadside camera; the module drives the car +12 to -14 on driveT with the world frozen. Verified far-pass-away through the Show path; lint + 24 tests green.
This commit is contained in:
+185
-12
@@ -18,6 +18,11 @@ export const ride = {
|
||||
family: 'structural',
|
||||
kind: 'model',
|
||||
venue: true,
|
||||
// This venue drives its own camera rig outright (see the camera block at
|
||||
// the end of update()). The shared rig is ignored for this layer — kept
|
||||
// for every other model layer. Analytic f(timeline, seed, framing,
|
||||
// venueState) like everything else, so seek === playback still holds.
|
||||
directCamera: true,
|
||||
consumes: ['form', 'ink', 'staging'],
|
||||
traits: ['shape', 'space', 'camera', 'style'],
|
||||
texture: 0.4,
|
||||
@@ -215,7 +220,8 @@ export const ride = {
|
||||
const billSlot = 3 + Math.floor(rng() * 6);
|
||||
const billSide = rng() < 0.5 ? -1 : 1;
|
||||
const roadsideMeta = [];
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const roadsidePopulation = 12;
|
||||
for (let i = 0; i < roadsidePopulation; i++) {
|
||||
const g = new T.Group();
|
||||
const side = i % 2 === 0 ? -1 : 1;
|
||||
const kind = i === billSlot ? 'billboard'
|
||||
@@ -600,15 +606,25 @@ export const ride = {
|
||||
const energyK = vState ? (0.55 + vState.light * 0.65) : 1;
|
||||
const speed = (0.35 + (params.speed || 0.7) * 0.75) * energyK * (instance.chase ? 1 : 0);
|
||||
const rate = 1;
|
||||
// Passerby: the camera is planted, so the CAR does the travelling —
|
||||
// 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.)
|
||||
const shot = framing && framing.venueShot ? framing.venueShot : null;
|
||||
const passerby = !!shot && shot.target === 'passerby';
|
||||
const driveT = passerby && 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). 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).
|
||||
// camera). Frozen during passerby (see above). 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 = (instance.chase ? speed * 26 : speed * 26 * 0.12 + 0.25) * rate;
|
||||
const dz = passerby ? 0
|
||||
: (instance.chase ? speed * 26 : speed * 26 * 0.12 + 0.25) * rate;
|
||||
const kids = instance.dashes.children;
|
||||
for (let i = 0; i < kids.length; i++) {
|
||||
const z0 = -70 + i * 6;
|
||||
@@ -675,6 +691,14 @@ 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) {
|
||||
const e = driveT * driveT * (3 - 2 * driveT);
|
||||
instance.car.position.z = 12 + (-14 - 12) * e;
|
||||
} else {
|
||||
instance.car.position.z = 0;
|
||||
}
|
||||
instance.car.position.y = bob * (instance.chase ? 1 : 0.3);
|
||||
// Lean INTO the sweep (roll) and yaw with it, like a lane change.
|
||||
const leanK = instance.chase ? 1 : 0.2;
|
||||
@@ -685,10 +709,13 @@ export const ride = {
|
||||
if (instance.wheels) {
|
||||
// Rolling spin on the meshes (local axes), not the yawed group:
|
||||
// tire about its hole axis (local z), hub about its cylinder
|
||||
// axis (local x after the geometry pre-rotation).
|
||||
// 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;
|
||||
for (const w of instance.wheels) {
|
||||
if (w.userData.tire) w.userData.tire.rotation.z = t * speed * 6 * rate;
|
||||
if (w.userData.hub) w.userData.hub.rotation.z = t * speed * 6 * rate;
|
||||
if (w.userData.tire) w.userData.tire.rotation.z = spin;
|
||||
if (w.userData.hub) w.userData.hub.rotation.z = spin;
|
||||
}
|
||||
}
|
||||
// Passengers bob gently, out of phase — analytic f(t), seek-stable.
|
||||
@@ -725,7 +752,7 @@ export const ride = {
|
||||
const dense = (arrangement % 2 === 1) || (vState && vState.popMul > 0.8);
|
||||
const showUpTo = dense ? n : Math.max(3, Math.floor(n * 0.45));
|
||||
for (let i = 0; i < n; i++) {
|
||||
instance.roadside.children[i].visible = opacity > 0.01 && (i < showUpTo);
|
||||
instance.roadside.children[i].visible = opacity > 0.001 && (i < showUpTo);
|
||||
}
|
||||
}
|
||||
// Dressing takes the palette tint so houses/trees belong to the song;
|
||||
@@ -748,10 +775,156 @@ export const ride = {
|
||||
// Ground follows horizon like the stage field.
|
||||
instance.road.position.y = -1.15 + (spaceHorizon - 0.5) * 0.6;
|
||||
|
||||
void camera; void framing; void params;
|
||||
// --- camera: this venue's own rig (directCamera). The chase unit
|
||||
// orbits the car once per video while pushing in, tracking the car so
|
||||
// the orbit never loses it. Moved here from Compositor so the venue
|
||||
// owns its whole grammar; the shared rig keeps serving every other
|
||||
// model layer. All analytic f(time, progress, seed) — seek stable.
|
||||
driveRideCamera({ instance, camera, timeline, framing, personality, venueState: vState, THREE: T });
|
||||
|
||||
void framing; void params;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* The ride's own camera rig. Same grammar the shared rig used to serve for
|
||||
* this archetype, owned here now: traverse body (continuous, monotonic — the
|
||||
* thing that arcs) + planned-look head (screen / room) + one orbit per video
|
||||
* on top + car tracking so wide swings keep their subject. Jump sections cut
|
||||
* the body to the arrangement side. Box-clamped, focus-floored, handheld.
|
||||
*/
|
||||
function driveRideCamera({ instance, camera, timeline, framing, personality, venueState }) {
|
||||
const t = timeline.time;
|
||||
const trav = typeof timeline.progress === 'number' ? timeline.progress : 0.5;
|
||||
const frame = framing || { scale: 1, shift: [0, 0] };
|
||||
const shift = frame.shift || [0, 0];
|
||||
const scale = frame.scale || 1;
|
||||
const pCam = personality ? personality.camera : null;
|
||||
const motion = personality ? personality.motion : null;
|
||||
const seed = ((instance.seed >>> 0) || 1);
|
||||
const s1 = Math.sin(seed * 0.00013), s2 = Math.cos(seed * 0.00029);
|
||||
const room = 1.1;
|
||||
const still = motion ? motion.stillness : 0.5;
|
||||
const churn = motion ? motion.churn : 0.25;
|
||||
const planned = frame && frame.venueShot ? frame.venueShot : null;
|
||||
|
||||
const walkDir = s1 >= 0 ? 1 : -1;
|
||||
const walkSpan = (1.1 + still * 0.9) * walkDir;
|
||||
const walk = (trav - 0.5) * 2 * walkSpan
|
||||
+ Math.cos(t * 0.043 + s2 * 4.2) * walkSpan * 0.55
|
||||
+ Math.sin(t * 0.23 + s1 * 3.1) * 0.08
|
||||
+ Math.sin(t * 0.11 + s2 * 5.0) * 0.06;
|
||||
// No journey on this path — drive the whole push off monotonic progress
|
||||
// instead (smoother than the old staged+progress split, same travel).
|
||||
const push = (trav - 0.5) * 3.8 * room;
|
||||
const hx = Math.sin(t * 0.9 + s1 * 7.0) * 0.5 * (0.4 + churn)
|
||||
+ Math.sin(t * 0.23 + s2 * 5.0) * 0.5 * (0.4 + churn);
|
||||
const hy = Math.cos(t * 0.77 + s2 * 9.0) * 0.5 * (0.4 + churn)
|
||||
+ Math.sin(t * 0.31 + s1 * 4.0) * 0.5 * (0.4 + churn);
|
||||
const sway = pCam ? pCam.sway : 0.01;
|
||||
const swayRate = pCam ? pCam.swayRate : 0.09;
|
||||
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;
|
||||
|
||||
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
|
||||
// the car's lap.
|
||||
const baseZ = 5.2;
|
||||
const sizeDolly = (planned.dolly && planned.dolly > 0) ? planned.dolly : 1.0;
|
||||
const dolly = (baseZ / Math.max(scale, 0.05) - Math.max(0, push)) * sizeDolly;
|
||||
const rise = (trav - 0.5) * 1.4 * room;
|
||||
const shiftX = shift[0] * 0.6;
|
||||
const shiftY = shift[1] * 0.35;
|
||||
let px = walk * 1.5 * room + Math.sin(t * swayRate) * sway * 4 + shiftX + hx;
|
||||
let py = 0.8 + rise + Math.cos(t * swayRate * 0.83) * sway * 3 + shiftY + hy;
|
||||
let pz = dolly;
|
||||
// Orbit the car once per video (~200° arc, seeded direction).
|
||||
// Radius tracks the shot dolly (wides orbit far, closes near) with a
|
||||
// floor that keeps the camera out of the bodywork. Orbit-weighted
|
||||
// 65/35 over the traverse so the chase unit actually leaves the car's
|
||||
// lap — 50/50 parked it near the paint.
|
||||
const oDir = s1 >= 0 ? 1 : -1;
|
||||
const oAng = oDir * (0.5 + trav * 3.5);
|
||||
const oRad = Math.max(4.5, dolly * 1.15);
|
||||
px = px * 0.35 + Math.sin(oAng) * oRad * 0.65;
|
||||
pz = pz * 0.35 + Math.cos(oAng) * oRad * 0.65;
|
||||
py = py * 0.35 + (1.2 + Math.sin(trav * 6.28 + s2 * 3.0) * 1.6) * 0.65;
|
||||
// Minimum focus distance from the car (origin): blending can still
|
||||
// land the camera inside the paint. Push out along the xz ray.
|
||||
{
|
||||
const r = Math.hypot(px, pz);
|
||||
const minR = 4.2;
|
||||
if (r < minR && r > 1e-4) {
|
||||
const k = minR / r;
|
||||
px *= k; pz *= k;
|
||||
} else if (r <= 1e-4) {
|
||||
px = minR; pz = 0;
|
||||
}
|
||||
}
|
||||
camera.position.set(
|
||||
Math.max(-lim.x, Math.min(lim.x, px)),
|
||||
Math.max(lim.yLo, Math.min(lim.yHi, py)),
|
||||
Math.max(lim.zLo, Math.min(lim.zHi, pz)),
|
||||
);
|
||||
camera.rotation.z = pCam ? pCam.spin * t * 0.5 : 0;
|
||||
// Track the car: blend the plan's motive look with the car's analytic
|
||||
// position (same formula as the module sweep) so the chase unit keeps
|
||||
// its subject while still panning with motive.
|
||||
const carX = Math.sin(t * 0.133 + ((instance.seed >>> 0) || 1) * 0.001) * 1.1;
|
||||
camera.lookAt(
|
||||
carX * 0.55 + planned.look[0] * 0.45,
|
||||
0.3 * 0.55 + planned.look[1] * 0.45,
|
||||
-0.3 * 0.55 + planned.look[2] * 0.45,
|
||||
);
|
||||
camera.updateMatrixWorld();
|
||||
return;
|
||||
}
|
||||
|
||||
if (planned && isJump) {
|
||||
camera.position.set(
|
||||
Math.max(-lim.x, Math.min(lim.x, planned.pos[0] + hx)),
|
||||
Math.max(lim.yLo, Math.min(lim.yHi, planned.pos[1] + hy)),
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
camera.position.set(
|
||||
Math.max(-lim.x, Math.min(lim.x,
|
||||
walk * 1.5 * room + Math.sin(t * swayRate) * sway * 4 + shift[0] * 0.6 + hx)),
|
||||
0.35 + (trav - 0.5) * 0.9 * room
|
||||
+ Math.cos(t * swayRate * 0.83) * sway * 3 + shift[1] * 0.35 + hy,
|
||||
Math.max(0.6, Math.min(7.5, dolly)),
|
||||
);
|
||||
camera.rotation.z = pCam ? pCam.spin * t * 0.5 : 0;
|
||||
camera.lookAt(camera.position.x * 0.15, camera.position.y * 0.3 - 0.25, camera.position.z - 5);
|
||||
camera.updateMatrixWorld();
|
||||
}
|
||||
|
||||
function mulberry(seed) {
|
||||
let a = seed >>> 0;
|
||||
return function () {
|
||||
|
||||
Reference in New Issue
Block a user