Ride: roadside actor figures among the props on some songs
Two seeded slots on ~40% of builds hold figures of the track's own cast, grown on first update like the crew (identity arrives via update), normalized to person-scale with feet planted via measured bounds, palette-recolored per frame, glancing around analytically. Static for the whole video: they stream with their slots and never pop. Verified variance across seeds, pose and planting.
This commit is contained in:
@@ -284,15 +284,28 @@ export const ride = {
|
||||
roadside.name = 'roadside';
|
||||
// 12 slots spaced 8 over [-84, +4]: house / sign / trees / billboard.
|
||||
// Billboard appears at most once, on one side, placed mid-span so it
|
||||
// is on screen for a stretch, not a flash.
|
||||
// is on screen for a stretch, not a flash. On ~40% of builds two
|
||||
// slots instead hold actor figures — the track's own characters
|
||||
// standing among the props (never the billboard slot). Static for
|
||||
// the whole video: figures never pop in mid-shot, they stream past
|
||||
// with their slot like every other prop.
|
||||
const billSlot = 3 + Math.floor(rng() * 6);
|
||||
const billSide = rng() < 0.5 ? -1 : 1;
|
||||
const roadsideActors = rng() < 0.4;
|
||||
const actorSlots = new Set();
|
||||
if (roadsideActors) {
|
||||
while (actorSlots.size < 2) {
|
||||
const s = Math.floor(rng() * 12);
|
||||
if (s !== billSlot) actorSlots.add(s);
|
||||
}
|
||||
}
|
||||
const roadsideMeta = [];
|
||||
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'
|
||||
: actorSlots.has(i) ? 'actor'
|
||||
: i % 3 === 0 ? 'house' : i % 3 === 1 ? 'sign' : 'trees';
|
||||
// Dressing lives far out (houses ±13-18, signs/trees ±9-13): near
|
||||
// items cross the frame in a blink, far items linger and let the
|
||||
@@ -330,8 +343,7 @@ export const ride = {
|
||||
diamond.rotation.z = Math.PI / 4;
|
||||
diamond.rotation.y = side > 0 ? -0.35 : 0.35;
|
||||
g.add(diamond);
|
||||
} else if (kind === 'billboard') {
|
||||
// Two posts + big panel carrying the viz (bound in
|
||||
} else if (kind === 'billboard') { // Two posts + big panel carrying the viz (bound in
|
||||
// bindVizTexture alongside the radio). Faces the road.
|
||||
const pGeo = new T.CylinderGeometry(0.12, 0.12, 3.4, 6);
|
||||
for (const px of [-1.4, 1.4]) {
|
||||
@@ -349,6 +361,13 @@ export const ride = {
|
||||
panel.name = 'billboard';
|
||||
g.add(panel);
|
||||
g.userData.billPanel = panel;
|
||||
} else if (kind === 'actor') {
|
||||
// A figure of the track's own cast, grown on first update
|
||||
// when identity arrives (same pattern as the crew): until
|
||||
// then the slot is an empty group, which never renders.
|
||||
g.userData.actorSlot = true;
|
||||
g.userData.actorBuilt = false;
|
||||
g.userData.phase = rng() * Math.PI * 2;
|
||||
} else {
|
||||
// Trees: 2-3 cones on cylinders, dark green glow.
|
||||
const n = 2 + Math.floor(rng() * 2);
|
||||
@@ -542,9 +561,27 @@ export const ride = {
|
||||
// — the chassis above varies per track, this is what makes every
|
||||
// reading of it a convertible rather than a tractor. Painted
|
||||
// panels join the viz reflection feed (more surface mirroring the
|
||||
// video); dark trim stays matte. All constants: identical on
|
||||
// every track, every run.
|
||||
// video); dark trim stays matte.
|
||||
//
|
||||
// Varied per track, not per run: a private rng fork (the main
|
||||
// stream is untouched, so crew draws don't move) plus the track's
|
||||
// shape and style. Discrete variants only ever swap whole parts
|
||||
// that interpenetrate identically — counts never change mid-video
|
||||
// (arrangement-static, cut-covered like everything structural).
|
||||
{
|
||||
const trimRng = mulberry(((instance.seed >>> 0) ^ 0x7e33d21b) >>> 0);
|
||||
const pick = (n) => Math.floor(trimRng() * n);
|
||||
// Rear wing: none, lip, or full blade.
|
||||
const spoilerKind = trimRng() < 0.3 ? 'none' : trimRng() < 0.72 ? 'blade' : 'lip';
|
||||
// Rally stripes in the accent colour, or bare paint.
|
||||
const stripeKind = trimRng() < 0.4 ? 'none' : trimRng() < 0.7 ? 'center' : 'dual';
|
||||
// Hood intake scoop xor the spine (they share the same turf).
|
||||
const scoop = trimRng() < 0.45;
|
||||
// Exhausts: dual or quad.
|
||||
const quad = trimRng() < 0.4;
|
||||
// Chrome slats follow line weight; tilt rakes the long panels.
|
||||
const slatCount = 2 + Math.min(2, Math.floor(styleWeight * 3));
|
||||
const rake = (shapeTilt / Math.PI - 0.5) * 0.06;
|
||||
const paint = cc(0);
|
||||
const paintMat = () => vizEnvMaterial(new T.MeshStandardMaterial({
|
||||
color: new T.Color(paint[0] * 0.85, paint[1] * 0.85, paint[2] * 0.85),
|
||||
@@ -864,7 +901,7 @@ export const ride = {
|
||||
transparent: opacity < 0.999, opacity,
|
||||
}));
|
||||
m.userData.partIndex = src.userData.partIndex ?? 0;
|
||||
} else {
|
||||
} else {
|
||||
const col = cc(i + 1);
|
||||
m = new T.Mesh(
|
||||
new T.SphereGeometry(0.22, 12, 10),
|
||||
@@ -1074,6 +1111,70 @@ export const ride = {
|
||||
instance.roadside.children[i].visible = opacity > 0.001 && (i < showUpTo || i === forced);
|
||||
}
|
||||
}
|
||||
// Roadside actors: figures of the track's cast standing among the
|
||||
// props on some builds (seeded at build; static all video, so they
|
||||
// stream past with their slot and never pop). Grown here, on first
|
||||
// update, because identity only arrives via update — the crew pattern.
|
||||
// They turn slowly to feel alive; analytic f(t), seek-stable.
|
||||
if (instance.roadside && identity && actorSpec && actorSpec.form) {
|
||||
for (const g of instance.roadside.children) {
|
||||
if (!g.userData.actorSlot) continue;
|
||||
if (!g.userData.actorBuilt) {
|
||||
try {
|
||||
const built = actorToGeometry(actorSpec, identity, T);
|
||||
const fig = new T.Group();
|
||||
fig.name = 'figure';
|
||||
while (built.children.length) {
|
||||
const m = built.children[0];
|
||||
built.remove(m);
|
||||
const pIdx = m.userData.partIndex ?? 0;
|
||||
const pPal = actorSpec.paletteMap
|
||||
? actorSpec.paletteMap[pIdx % actorSpec.paletteMap.length] : pIdx;
|
||||
const c = pal[pPal % pal.length] || pal[0];
|
||||
m.material = new T.MeshStandardMaterial({
|
||||
color: new T.Color(c[0], c[1], c[2]),
|
||||
roughness: 0.5, metalness: 0.08,
|
||||
transparent: opacity < 0.999, opacity,
|
||||
});
|
||||
fig.add(m);
|
||||
}
|
||||
// Feet on the ground, person-scale: normalize the form
|
||||
// to ~1.6 units tall (houses run 2-4) and plant the
|
||||
// lowest point at the roadside level regardless of
|
||||
// the form's own extent or origin. Local box
|
||||
// (unparented) over slot scale: exact.
|
||||
if (fig.children.length) {
|
||||
const _box = new T.Box3().setFromObject(fig);
|
||||
const _h = _box.max.y - _box.min.y;
|
||||
const _s = _h > 1e-6 ? 1.6 / _h : 1;
|
||||
fig.scale.setScalar(_s);
|
||||
fig.position.y = -1.15 / g.scale.x - _s * _box.min.y;
|
||||
}
|
||||
g.add(fig);
|
||||
g.userData.actorFig = fig;
|
||||
instance.needsCompile = true;
|
||||
} catch { /* figure optional; the slot stays empty */ }
|
||||
g.userData.actorBuilt = true;
|
||||
}
|
||||
const fig = g.userData.actorFig;
|
||||
if (fig) {
|
||||
// Glance around rather than spinning: a bystander
|
||||
// watching the road, not a turntable. Analytic f(t).
|
||||
fig.rotation.y = (g.userData.phase || 0) + Math.sin(t * 0.12) * 0.55;
|
||||
for (const m of fig.children) {
|
||||
if (!m.isMesh) continue;
|
||||
const pIdx = m.userData.partIndex ?? 0;
|
||||
const pPal = actorSpec.paletteMap
|
||||
? actorSpec.paletteMap[pIdx % actorSpec.paletteMap.length] : pIdx;
|
||||
const c = pal[pPal % pal.length] || pal[0];
|
||||
m.material.color.setRGB(c[0], c[1], c[2]);
|
||||
m.material.opacity = opacity;
|
||||
m.material.transparent = opacity < 0.999;
|
||||
m.visible = opacity > 0.01;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Dressing takes the palette tint so houses/trees belong to the song;
|
||||
// the billboard glows with the section light (its IMAGE is the viz).
|
||||
if (instance.dressMat) {
|
||||
|
||||
Reference in New Issue
Block a user