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:
@@ -121,6 +121,13 @@ export function planVenueShots(cues, sections, archetype, style, isJump, rng, ga
|
||||
// history would break every streak on its third cue, since all cues in a
|
||||
// section share the target.
|
||||
let sectionSize = null, sectionTarget = null, sectionIndex = -1;
|
||||
// Planted shots (passerby/billboard) hold the camera still while the car
|
||||
// does the travelling — one cue of that is a beat, a whole section of it
|
||||
// is a stuck camera with the car looping through the same frame. The
|
||||
// plant opens its section; the section's remaining cues fall back to the
|
||||
// screen/room rotation. (The section still counts as planted in
|
||||
// sectionTargets, so the adjacency rules stay conservative.)
|
||||
let sectionPlants = 0;
|
||||
const sectionTargets = [];
|
||||
for (const cue of cues) {
|
||||
const section = sections[cue.sectionIndex] || {};
|
||||
@@ -135,6 +142,7 @@ export function planVenueShots(cues, sections, archetype, style, isJump, rng, ga
|
||||
const sharpCut = !!cue.hardCut && marks.length > 0;
|
||||
if (cue.sectionIndex !== sectionIndex || sharpCut) {
|
||||
sectionIndex = cue.sectionIndex;
|
||||
sectionPlants = 0;
|
||||
const prevMark = marks.length ? marks[marks.length - 1] : null;
|
||||
sectionSize = locked ? 'normal' : pickSize(prevMark, energy, story, style, rng);
|
||||
if (sharpCut && prevMark && sectionSize === prevMark.size) {
|
||||
@@ -143,7 +151,7 @@ export function planVenueShots(cues, sections, archetype, style, isJump, rng, ga
|
||||
if (sharpCut && prevMark) {
|
||||
// Flip the look direction outright: screen↔room, so the new
|
||||
// composition cannot be the old one from a step aside.
|
||||
// (Passerby has no opposite — re-draw it instead.)
|
||||
// (Passerby/billboard have no opposite — re-draw instead.)
|
||||
const pt = prevMark.target;
|
||||
sectionTarget = pt === 'screen' ? 'room'
|
||||
: pt === 'room' ? 'screen'
|
||||
@@ -152,15 +160,28 @@ export function planVenueShots(cues, sections, archetype, style, isJump, rng, ga
|
||||
sectionTarget = pickTarget(prevMark, energy, story, rng,
|
||||
sectionTargets.map((t) => ({ target: t })), archetype);
|
||||
}
|
||||
// A billboard shot wants car + board in frame: force wide.
|
||||
if (sectionTarget === 'billboard' && !locked) sectionSize = 'wide';
|
||||
sectionTargets.push(sectionTarget);
|
||||
}
|
||||
const progress = n > 1 ? cue.index / (n - 1) : 0.5;
|
||||
const gaze = gazeFrom && gazeFrom[cue.index] ? gazeFrom[cue.index] : [0, 0];
|
||||
const mark = placeMark(staging, sectionSize, sectionTarget, jump, rng, progress, gaze, roomSide);
|
||||
// Only the section's first planted cue keeps the plant; later ones
|
||||
// rejoin the rotation, alternating away from whatever just played so
|
||||
// the cut out of the plant always changes the composition.
|
||||
let cueTarget = sectionTarget;
|
||||
if (cueTarget === 'passerby' || cueTarget === 'billboard') {
|
||||
sectionPlants++;
|
||||
if (sectionPlants > 1) {
|
||||
const pt = marks.length ? marks[marks.length - 1].target : null;
|
||||
cueTarget = pt === 'screen' ? 'room' : 'screen';
|
||||
}
|
||||
}
|
||||
const mark = placeMark(staging, sectionSize, cueTarget, jump, rng, progress, gaze, roomSide);
|
||||
|
||||
marks.push({
|
||||
index: cue.index,
|
||||
size: sectionSize, target: sectionTarget,
|
||||
size: sectionSize, target: cueTarget,
|
||||
pos: mark.pos,
|
||||
look: mark.look,
|
||||
jump: jump ? jump.arrangement : null,
|
||||
@@ -216,12 +237,29 @@ function pickTarget(previous, energy, story, rng, history, archetype = null) {
|
||||
const tension = story.tension ?? 0.5;
|
||||
// Ride-only: a passerby section plants the camera at the roadside and lets
|
||||
// the car drive through the frame. Offered on breaths (setup/resolution,
|
||||
// quiet passages) — never twice running, never at the climax.
|
||||
if (archetype === 'ride' && act !== 'climax' && streak !== 'passerby'
|
||||
// quiet passages) — never twice running, never right after a billboard
|
||||
// section, never at the climax: two planted shots back to back read as
|
||||
// stuck, in either order (the billboard rule below guards the mirror).
|
||||
// The draw is consumed even when adjacency blocks the plant so the rng
|
||||
// stream — and every plan without a blocked draw — stays exactly as
|
||||
// before. (A streak check cannot do this: sectionTargets holds one entry
|
||||
// per section, so single-cue plant sections never form a streak of two.)
|
||||
const passerbyDraw = archetype === 'ride' && act !== 'climax'
|
||||
&& (act === 'setup' || act === 'resolution' || energy < 0.42)
|
||||
&& rng.bool(0.45)) {
|
||||
&& rng.bool(0.45);
|
||||
if (passerbyDraw && last2[last2.length - 1] !== 'passerby'
|
||||
&& last2[last2.length - 1] !== 'billboard') {
|
||||
return 'passerby';
|
||||
}
|
||||
// Ride-only: a billboard section aims a static camera at the roadside
|
||||
// billboard while the car drives through the frame in front of it. Rarer
|
||||
// than passerby (at most a couple per video), same breath-only rule, never
|
||||
// adjacent to a passerby — two planted shots back to back read as stuck.
|
||||
if (archetype === 'ride' && act !== 'climax' && !streak
|
||||
&& (act === 'setup' || act === 'resolution' || energy < 0.42)
|
||||
&& last2[last2.length - 1] !== 'passerby' && rng.bool(0.3)) {
|
||||
return 'billboard';
|
||||
}
|
||||
let pick;
|
||||
if (act === 'setup') pick = 'room';
|
||||
else if (act === 'resolution') pick = rng.bool(0.3) ? 'screen' : 'room';
|
||||
@@ -243,9 +281,25 @@ function placeMark(staging, size, target, jump, rng, progress = 0.5, gaze = null
|
||||
// Shoulder side follows the video's roomSide: one consistent side all song.
|
||||
if (target === 'passerby') {
|
||||
const side = roomSide || 1;
|
||||
// Planted a step back and up from the lane so the drive-by whoosh
|
||||
// does not swallow the frame, aimed between the approach (far +z)
|
||||
// and the pass point: the car grows head-on in the first third,
|
||||
// crosses near centre mid-cue, and the tail holds road + glow.
|
||||
return {
|
||||
pos: [side * 3.6, 1.2, -2],
|
||||
look: [-side * 1.0, 0.2, 8],
|
||||
pos: [side * 4.4, 1.5, -2],
|
||||
look: [-side * 0.5, 0.3, 3],
|
||||
};
|
||||
}
|
||||
if (target === 'billboard') {
|
||||
// Anchor where billboards live (far shoulder, panel height): the
|
||||
// module aims at the live panel each frame, so this only bends the
|
||||
// look-path sweep toward a sensible place for neighboring cues.
|
||||
// Forced wide in the planner (see below) — a billboard shot wants
|
||||
// the car AND the board in frame.
|
||||
const side = roomSide || 1;
|
||||
return {
|
||||
pos: [side * 3.6, 1.4, 0],
|
||||
look: [-side * 9.0, 3.0, -20],
|
||||
};
|
||||
}
|
||||
const clampTo = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||
@@ -342,10 +396,11 @@ export function venueShotAt(marks, cueIndex, frame, cueStartFrame, cueEndFrame =
|
||||
// the look (the furniture swapped; the path would glide through the old
|
||||
// room). The reported target stays the cue's (HUD motive), the look itself
|
||||
// is where the path is.
|
||||
// Passerby holds its plant: the camera is a bystander, and any drift of
|
||||
// the look would read as a pan away from the road. driveT (0→1 across the
|
||||
// cue) is the car's cue — the module drives it from far +z to far -z on it.
|
||||
const look = cur.target === 'passerby'
|
||||
// Passerby/billboard hold their plants: the camera is a bystander, and
|
||||
// any drift of the look would read as a pan away from the road. driveT
|
||||
// (0→1 across the cue) is the car's cue — the module drives it from far
|
||||
// +z to far -z on it (passerby) or aims at the live panel (billboard).
|
||||
const look = (cur.target === 'passerby' || cur.target === 'billboard')
|
||||
? cur.look.slice()
|
||||
: lookPathAt(marks, progress, cur.jump !== null ? cur : null);
|
||||
const span = Math.max(1, (cueEndFrame ?? (cueStartFrame + cur.easeFrames)) - cueStartFrame);
|
||||
@@ -371,8 +426,13 @@ export function venueShotAt(marks, cueIndex, frame, cueStartFrame, cueEndFrame =
|
||||
// measures. Jump cues still cut instantly (furniture swapped).
|
||||
const t = clamp01((frame - cueStartFrame) / Math.max(1, span * 1.25));
|
||||
const e = t * t * (3 - 2 * t);
|
||||
// A planted camera holds its position too — the bystander does not drift
|
||||
// down the road while the car passes. (The look already holds above; the
|
||||
// module adds handheld micro on top.)
|
||||
const planted = cur.target === 'passerby' || cur.target === 'billboard';
|
||||
return {
|
||||
pos: [lerp(prev.pos[0], cur.pos[0], e), lerp(prev.pos[1], cur.pos[1], e), lerp(prev.pos[2], cur.pos[2], e)],
|
||||
pos: planted ? cur.pos.slice()
|
||||
: [lerp(prev.pos[0], cur.pos[0], e), lerp(prev.pos[1], cur.pos[1], e), lerp(prev.pos[2], cur.pos[2], e)],
|
||||
look,
|
||||
size: t < 1 ? `${prev.size}→${cur.size}` : cur.size,
|
||||
target: cur.target,
|
||||
|
||||
Reference in New Issue
Block a user