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:
@@ -133,7 +133,7 @@ export function planVenueShots(cues, sections, archetype, style, isJump, rng, ga
|
||||
const prevMark = marks.length ? marks[marks.length - 1] : null;
|
||||
sectionSize = locked ? 'normal' : pickSize(prevMark, energy, story, style, rng);
|
||||
sectionTarget = pickTarget(prevMark, energy, story, rng,
|
||||
sectionTargets.map((t) => ({ target: t })));
|
||||
sectionTargets.map((t) => ({ target: t })), archetype);
|
||||
sectionTargets.push(sectionTarget);
|
||||
}
|
||||
const progress = n > 1 ? cue.index / (n - 1) : 0.5;
|
||||
@@ -190,11 +190,19 @@ function pickSize(previous, energy, story, style, rng) {
|
||||
* still crosses the frame — the walk passes it — it is just never the mark.
|
||||
* Rotation between the two (never three in a row) is the coverage.
|
||||
*/
|
||||
function pickTarget(previous, energy, story, rng, history) {
|
||||
function pickTarget(previous, energy, story, rng, history, archetype = null) {
|
||||
const last2 = (history || []).slice(-2).map((m) => m.target);
|
||||
const streak = last2.length === 2 && last2[0] === last2[1] ? last2[0] : null;
|
||||
const act = story.act || 'development';
|
||||
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'
|
||||
&& (act === 'setup' || act === 'resolution' || energy < 0.42)
|
||||
&& rng.bool(0.45)) {
|
||||
return 'passerby';
|
||||
}
|
||||
let pick;
|
||||
if (act === 'setup') pick = 'room';
|
||||
else if (act === 'resolution') pick = rng.bool(0.3) ? 'screen' : 'room';
|
||||
@@ -210,6 +218,17 @@ function pickTarget(previous, energy, story, rng, history) {
|
||||
|
||||
/** Absolute camera mark for a (size, target): position + look point. */
|
||||
function placeMark(staging, size, target, jump, rng, progress = 0.5, gaze = null, roomSide = 1) {
|
||||
// Passerby (ride only): plant the camera on the shoulder and look back up
|
||||
// the road. The car enters far, drives to the camera and past it — the
|
||||
// module drives the car through on driveT, so pos/look here are static.
|
||||
// Shoulder side follows the video's roomSide: one consistent side all song.
|
||||
if (target === 'passerby') {
|
||||
const side = roomSide || 1;
|
||||
return {
|
||||
pos: [side * 3.6, 1.2, -2],
|
||||
look: [-side * 1.0, 0.2, 8],
|
||||
};
|
||||
}
|
||||
const clampTo = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
||||
const { box, dolly } = staging;
|
||||
// Room visits alternate sides so consecutive room views face opposite
|
||||
@@ -304,12 +323,20 @@ 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.
|
||||
const look = lookPathAt(marks, progress, cur.jump !== null ? cur : null);
|
||||
// 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'
|
||||
? cur.look.slice()
|
||||
: lookPathAt(marks, progress, cur.jump !== null ? cur : null);
|
||||
const span = Math.max(1, (cueEndFrame ?? (cueStartFrame + cur.easeFrames)) - cueStartFrame);
|
||||
const driveT = clamp01((frame - cueStartFrame) / span);
|
||||
if (!prev || cur.jump !== null) {
|
||||
return {
|
||||
pos: cur.pos.slice(), look,
|
||||
size: cur.size, target: cur.target,
|
||||
dolly: VENUE_SIZE_DOLLY[cur.size] ?? 1.0,
|
||||
drive: driveT,
|
||||
};
|
||||
}
|
||||
// The travel OUTRUNS the cue (1.25x): the camera is always on the way to
|
||||
@@ -319,7 +346,6 @@ export function venueShotAt(marks, cueIndex, frame, cueStartFrame, cueEndFrame =
|
||||
// as churn to the descriptor). Travel-portion differences are small up
|
||||
// close and accumulate far away, which is exactly the gradient direction
|
||||
// measures. Jump cues still cut instantly (furniture swapped).
|
||||
const span = Math.max(1, (cueEndFrame ?? (cueStartFrame + cur.easeFrames)) - cueStartFrame);
|
||||
const t = clamp01((frame - cueStartFrame) / Math.max(1, span * 1.25));
|
||||
const e = t * t * (3 - 2 * t);
|
||||
return {
|
||||
@@ -328,6 +354,7 @@ export function venueShotAt(marks, cueIndex, frame, cueStartFrame, cueEndFrame =
|
||||
size: t < 1 ? `${prev.size}→${cur.size}` : cur.size,
|
||||
target: cur.target,
|
||||
dolly: lerp(VENUE_SIZE_DOLLY[prev.size] ?? 1.0, VENUE_SIZE_DOLLY[cur.size] ?? 1.0, e),
|
||||
drive: driveT,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user