mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 03:09:57 +00:00
feat(venue): reactive crowd video layer behind the 3D highway (career mode 1/3) (#905)
* feat(venue): reactive crowd video layer behind the 3D highway (career mode PR1) Two crossfading video backdrop planes in the highway_3d venue background style, driven by a new venue-crowd.js state machine that maps v3:live-performance-state to crowd states (bored/neutral/engaged/ecstatic) with 3s stability + 8s dwell hysteresis, plus one-shot reaction stingers on streak milestones and end-of-song accuracy. Inert without a venue pack manifest (career plugin, PR2) or the feedBack-venue-crowd-dev flag — the static bg plate behaves exactly as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(venue-crowd): retry renderer binding + preserve mid-stinger transitions Codex preflight P2s: (1) videos created before highway_3d registered its globals never reached the backdrop planes — binding is now idempotent and retried from start/perf-event/re-activation paths; (2) a crowd-state switch committing while a stinger played was dropped because the machine had already advanced — it is now deferred and played when the stinger ends. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(venue-crowd): per-video load tokens + unbind renderer on stop Codex preflight round 2: (1) the global load token let a stinger cancel a committed loop load on the other layer — tokens are now per-element, and a stinger preempting an in-flight loop on its own layer requeues that loop for when the stinger ends; (2) setManifest(null)/deactivate left the last crowd frame bound and visible over the static plate — stop() now unbinds both layers from the renderer and zeroes the mix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(venue-crowd): flush deferred loop on stinger failure, source accuracy from perf events Codex preflight round 3: (1) a failed/timed-out stinger left a deferred loop switch queued forever; the failure path now flushes it. (2) stats:recorded only carries {filename, arrangement}, so the end-of-song reaction now uses the accuracyPct from the song's last v3:live-performance-state event (a real percentage) instead of a field that never existed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(venue-crowd): requeue mid-crossfade loops preempted by stingers; hard-stop on manifest swap Codex preflight round 4: (1) idleLayer() still points at the fading-in layer during a crossfade, so a stinger firing mid-fade overwrote the new loop with nothing requeued — the fading loop is now tracked and requeued like an in-flight load; (2) swapping venue packs while active now goes through stop() so _stopGen invalidates the old manifest's in-flight loads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(venue-crowd): generation-gate stinger handlers; recrop on video size change Codex preflight round 5: (1) an ended/timeout handler orphaned by stop() could fire into a later stinger's lifecycle on the reused element — handlers now detach unconditionally and carry a generation token; (2) the renderer only re-applied cover-crop on camera aspect changes, so a src swap with a different intrinsic size kept stale repeat/offset — it now recrops when videoWidth/Height change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(venue-crowd): bail loop-fade completion when a stinger preempted the layer Codex preflight round 6: the loop crossfade's completion callback could still run between a stinger's start and its canplaythrough, promoting the stinger's layer to active and pausing the real loop — it now bails when the fading loop was preempted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(venue-crowd): keep rear video layer opaque during crossfades Two half-transparent layers let the static bg plate bleed through (~25% at mid-fade) — visible as a flash of the old still image on every state transition. The crossfade is now always the front layer fading over an opaque rear layer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(venue-crowd): reset active layer with mix on stop Codex preflight: stop() zeroed the mix but left _activeLayer at 1, so a restart flashed layer 0's stale frame until the new loop loaded. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(venue-crowd): reset crowd mood to neutral on song load Codex preflight: a song ending in ecstatic/bored left the next song's crowd stuck in that mood until the hysteresis window passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(venue-crowd): cancel in-flight fade when a stinger preempts it Codex preflight: the orphaned ramp kept pushing the mix toward the layer whose src the stinger had just replaced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(venue-crowd): don't let null accuracy resets wipe the end-of-song value Codex preflight: Number(null) is 0, so idle HUD resets overwrote _lastAccuracyPct before stats:recorded consumed it, suppressing the end-of-song stinger. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(venue-crowd): abort stale stinger state on song load Codex preflight: a stinger straddling a song change could fade back into the previous song's layer or flush its pending loop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(venue-crowd): always detach load listeners, gate only the callback Codex preflight: superseded loads left canplaythrough/error listeners attached to the persistent video elements — unbounded growth over a session. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(venue-crowd): flyover intro with crowd-ambience ducking On song:loaded, an optional pack intro plays once: a camera flyover video (idle layer, one-shot) with bar-crowd ambience audio that ducks out on song:play, near the flyover's landing, or at handoff — whichever first. Machine commits and stingers defer during the intro; stop()/song-change abort it. Packs without an intro behave as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(venue-crowd): fall back to the loop when the intro fails to load Codex preflight: a failed/timed-out intro left the song with no crowd loop at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
8d3db5f42c
commit
e779c72396
@@ -2418,6 +2418,13 @@
|
||||
let _venueSceneAssetsLoaded = false;
|
||||
let _venueSceneLoadFailed = false;
|
||||
const _venueTextureCache = new Map();
|
||||
// Crowd video layers (career mode). venue-crowd.js owns the <video>
|
||||
// elements and the crossfade timing; the renderer only maps them onto
|
||||
// two planes in front of the static plate. _venueCrowdRev bumps on any
|
||||
// element (re)assignment so update() knows to rebind textures.
|
||||
const _venueCrowdVideos = [null, null];
|
||||
let _venueCrowdMix = 0;
|
||||
let _venueCrowdRev = 0;
|
||||
|
||||
function _bgVenueMoodCoeffs(state) {
|
||||
const s = String(state || 'idle').toLowerCase();
|
||||
@@ -2909,6 +2916,20 @@
|
||||
window.h3dVenueSceneSetMood = (state) => {
|
||||
_venueMoodState = String(state || 'idle').toLowerCase();
|
||||
};
|
||||
// Crowd video layers (career mode) — see venue-crowd.js. Layer 0/1 are
|
||||
// two coplanar backdrop planes; mix selects between them (0 → layer 0,
|
||||
// 1 → layer 1) so the caller can crossfade loop videos.
|
||||
window.h3dVenueBackdropSetVideo = (layer, videoEl) => {
|
||||
const i = layer ? 1 : 0;
|
||||
const el = videoEl || null;
|
||||
if (_venueCrowdVideos[i] === el) return;
|
||||
_venueCrowdVideos[i] = el;
|
||||
_venueCrowdRev++;
|
||||
};
|
||||
window.h3dVenueBackdropSetMix = (mix) => {
|
||||
const v = Number(mix);
|
||||
_venueCrowdMix = Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 0;
|
||||
};
|
||||
window.h3dVenueSceneSetInstrumentPov = (input) => {
|
||||
const next = _venueResolvePovFromInput(input);
|
||||
if (_venueInstrumentPov === next) return;
|
||||
@@ -3371,6 +3392,40 @@
|
||||
() => _venueMarkFailed('failed to load small-club bg plate'),
|
||||
);
|
||||
|
||||
// Crowd video planes (career mode): two crossfading layers
|
||||
// just in front of the static plate (which stays mounted as
|
||||
// the no-pack / load-failure fallback). Textures bind lazily
|
||||
// in update() when venue-crowd.js assigns video elements.
|
||||
state.crowd = { layers: [], rev: -1 };
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const geo = new T.PlaneGeometry(1, 1);
|
||||
const mat = new T.MeshBasicMaterial({
|
||||
color: 0xffffff, transparent: true, opacity: 0,
|
||||
depthWrite: false, fog: false,
|
||||
});
|
||||
const mesh = new T.Mesh(geo, mat);
|
||||
mesh.visible = false;
|
||||
// Layer 1 sits nearest so three.js's back-to-front
|
||||
// transparent sort draws it after layer 0.
|
||||
const layer = {
|
||||
mesh, geo, mat, tex: null, videoEl: null,
|
||||
cam: settings.cam,
|
||||
distance: BG_BACKDROP_DISTANCE * (i === 0 ? 1.04 : 1.03),
|
||||
lastAspect: 0, lastVisibleHeight: 0,
|
||||
};
|
||||
layer.applyCoverCrop = function () {
|
||||
if (!layer.videoEl || !layer.tex) return;
|
||||
_bgCoverCrop(
|
||||
layer.tex,
|
||||
layer.videoEl.videoWidth || 0,
|
||||
layer.videoEl.videoHeight || 0,
|
||||
layer.cam.aspect,
|
||||
);
|
||||
};
|
||||
scene.add(mesh);
|
||||
state.crowd.layers.push(layer);
|
||||
}
|
||||
|
||||
const hazeGeo = new T.PlaneGeometry(280 * K, 40 * K);
|
||||
const hazeMat = new T.MeshBasicMaterial({
|
||||
color: 0x101820, transparent: true, opacity: coeffs.haze,
|
||||
@@ -3402,6 +3457,64 @@
|
||||
s.haze.mat.opacity = (s.haze.baseOp || VENUE_HAZE_STEADY)
|
||||
* (coeffs.haze / VENUE_HAZE_STEADY);
|
||||
}
|
||||
if (s.crowd) {
|
||||
// Rebind VideoTextures when venue-crowd.js (re)assigns
|
||||
// elements. VideoTexture samples the element every frame,
|
||||
// so a src change on the same element needs no rebind.
|
||||
if (s.crowd.rev !== _venueCrowdRev) {
|
||||
s.crowd.rev = _venueCrowdRev;
|
||||
s.crowd.layers.forEach((layer, i) => {
|
||||
const el = _venueCrowdVideos[i];
|
||||
if (layer.videoEl === el) return;
|
||||
if (layer.tex) { layer.mat.map = null; layer.tex.dispose(); layer.tex = null; }
|
||||
layer.videoEl = el;
|
||||
layer.lastAspect = 0; // force refit + recrop
|
||||
if (el) {
|
||||
const tex = new T.VideoTexture(el);
|
||||
tex.colorSpace = T.SRGBColorSpace;
|
||||
tex.wrapS = T.ClampToEdgeWrapping;
|
||||
tex.wrapT = T.ClampToEdgeWrapping;
|
||||
tex.minFilter = T.LinearFilter;
|
||||
tex.magFilter = T.LinearFilter;
|
||||
tex.generateMipmaps = false;
|
||||
layer.tex = tex;
|
||||
layer.mat.map = tex;
|
||||
}
|
||||
layer.mat.needsUpdate = true;
|
||||
});
|
||||
}
|
||||
const warm = coeffs.warmth;
|
||||
s.crowd.layers.forEach((layer, i) => {
|
||||
const el = layer.videoEl;
|
||||
// videoWidth === 0 until metadata lands — showing the
|
||||
// plane before that paints a black flash over the plate.
|
||||
const ready = !!el && el.videoWidth > 0;
|
||||
// venue-crowd.js swaps src on the same element (loop ↔
|
||||
// stinger); a new intrinsic size needs a fresh
|
||||
// cover-crop, which _bgFitBackdropPlane only reapplies
|
||||
// on camera aspect changes.
|
||||
if (ready && (layer.lastVidW !== el.videoWidth ||
|
||||
layer.lastVidH !== el.videoHeight)) {
|
||||
layer.lastVidW = el.videoWidth;
|
||||
layer.lastVidH = el.videoHeight;
|
||||
layer.applyCoverCrop();
|
||||
}
|
||||
// Layer 0 (rear) stays fully opaque whenever any of the
|
||||
// fade involves it: two half-transparent layers would
|
||||
// let the static plate behind bleed through (~25% at
|
||||
// mid-fade). The crossfade is therefore layer 1 (front)
|
||||
// fading over an opaque layer 0 — in both directions.
|
||||
const opacity = i === 0
|
||||
? (_venueCrowdMix < 0.999 ? 1 : 0)
|
||||
: _venueCrowdMix;
|
||||
layer.mat.opacity = opacity;
|
||||
layer.mesh.visible = ready && opacity > 0.01;
|
||||
if (layer.mesh.visible) {
|
||||
layer.mat.color.setRGB(warm, warm * 0.98, warm * 0.95);
|
||||
_bgFitBackdropPlane(layer);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
teardown(s) {
|
||||
if (!s) return;
|
||||
@@ -3416,6 +3529,19 @@
|
||||
p.mat.dispose?.();
|
||||
}
|
||||
}
|
||||
// Crowd planes: this style owns the VideoTextures; the
|
||||
// <video> elements belong to venue-crowd.js and survive.
|
||||
if (s.crowd) {
|
||||
for (const layer of s.crowd.layers) {
|
||||
layer.mesh?.parent?.remove(layer.mesh);
|
||||
layer.geo?.dispose?.();
|
||||
if (layer.mat) {
|
||||
layer.mat.map = null;
|
||||
layer.mat.dispose?.();
|
||||
}
|
||||
layer.tex?.dispose?.();
|
||||
}
|
||||
}
|
||||
// Dispose the cached plate textures too — the module-level cache
|
||||
// otherwise keeps every loaded POV plate GPU-resident for the
|
||||
// page lifetime (steady VRAM growth across POV/arrangement swaps).
|
||||
|
||||
Reference in New Issue
Block a user