File size: 8,014 Bytes
b985a12 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 | // Watch: the 48 s film. Its clock is the song's clock (26 bars at 130 bpm) whenever sound is on,
// so the loop lands on the beat, the ramp gets the build-up and lift-off gets the drop.
import * as THREE from "three";
import { clamp, lerp, smooth, keyed, G, R, LZ, frame, loc, zonesFrom } from "./world.js";
import { BAR, SONG_BARS } from "./sound.js";
import { HEAD } from "./interior.js";
export const TOTAL = SONG_BARS * BAR;
export const CHAPTERS = [
[0, "Build", "Blueprint to carbon."], [2, "Roll out", "Flat out from a standstill."], [4.5, "The loop", "A full 360°. Real loop physics."],
[6, "The ramp", "Foot down. 33° kicker."], [8, "Lift-off", "Four thrusters fire straight down."], [11, "Wings out", "Thrusters swing back. It glides over Dammam."],
[17.5, "Front seat", "Inside the Hydra LTE242."], [20.2, "The screen", "Dashcam and a landing scan within 5 km."], [22, "", ""],
];
export function createWatch(W, car, interior) {
const T = W.track, fr = frame(), basis = new THREE.Matrix4(), q = new THREE.Quaternion();
const dStart = T.D[T.idxX(0)], dX = x => T.D[T.idxX(x, T.roadEnd)];
const RIDE = [[0, dStart], [.065, T.D[T.loopIn]], [.245, T.D[T.loopOut]], [.33, T.D[T.rampIn]], [.385, T.D[T.roadEnd]], [.5, dX(77)], [.64, dX(180)], [.78, dX(420)], [1, dX(880)]];
const rideP = b => b < 2 ? 0 : keyed([[2, 0], [4.5, .065], [6, .245], [7.2, .33], [8, .385], [11, .5], [14, .64], [17.5, .735], [24, 1]], b);
const speedMS = (p, y) => {
if (p < .065) return 30 * Math.pow(smooth(0, .065, p), .6);
if (p < .245) return Math.sqrt(Math.max(30 * 30 - 2 * G * y, 9));
if (p < .33) return lerp(30, 48, smooth(.245, .33, p));
if (p < .385) return Math.sqrt(Math.max(48 * 48 - 2 * G * y, 9));
return keyed([[.385, 47], [.5, 72], [.64, 88], [.78, 84], [1, 81]], p);
};
const thrustDeploy = p => smooth(.355, .395, p), thrustPower = p => keyed([[.37, 0], [.392, 1], [.5, 1], [.56, .5], [.66, .35], [1, .3]], p);
const thrustAngle = p => keyed([[.39, 0], [.47, .12], [.56, .85], [.62, 1]], p), wingsOut = p => smooth(.5, .585, p);
// where the car is at each bar, with real speed changes inside each section: it pulls away from
// rest, slows over the top of the loop and whips through the bottom, builds up the run-up and
// ramp, surges on the thrusters, then glides. w(s) is the speed shape over the section.
const tmp = frame(), V0 = 30, dFilm = T.D[T.idxX(-55)];
const SECTIONS = [
[2, 4.5, dFilm, T.D[T.loopIn], s => Math.sqrt(s + .02)],
[4.5, 6, T.D[T.loopIn], T.D[T.loopOut], s => Math.sqrt(Math.max(.05, 1 - 2 * G * T.sample(lerp(T.D[T.loopIn], T.D[T.loopOut], s), tmp).p.y / (V0 * V0)))],
[6, 8, T.D[T.loopOut], T.D[T.roadEnd], s => .75 + .35 * s],
[8, 11, T.D[T.roadEnd], dX(T.P[T.roadEnd].x + 110), s => Math.sqrt(.25 + 1.4 * s)],
[11, 17.5, dX(T.P[T.roadEnd].x + 110), dX(T.P[T.roadEnd].x + 1150), () => 1],
[17.5, 24, dX(T.P[T.roadEnd].x + 1150), dX(T.P[T.roadEnd].x + 2100), () => 1],
].map(([b0, b1, d0, d1, w]) => {
const N = 240, tau = new Float32Array(N + 1);
for (let i = 1; i <= N; i++) tau[i] = tau[i - 1] + 2 / (w((i - 1) / N) + w(i / N)) / N;
for (let i = 1; i <= N; i++) tau[i] /= tau[N];
return { b0, b1, d0, d1, tau, N };
});
const distAt = b => {
if (b <= 2) return dFilm;
const S = SECTIONS.find(x => b <= x.b1) || SECTIONS[SECTIONS.length - 1];
const u = clamp((b - S.b0) / (S.b1 - S.b0));
let lo = 0, hi = S.N; while (hi - lo > 1) { const m = (lo + hi) >> 1; if (S.tau[m] <= u) lo = m; else hi = m; }
const s = (lo + clamp((u - S.tau[lo]) / Math.max(1e-6, S.tau[hi] - S.tau[lo]))) / S.N;
return lerp(S.d0, S.d1, s);
};
const FILM = 1; // real time
const UP = new THREE.Vector3(0, 1, 0);
const SHOTS = [
[0, .05, f => ({ pos: loc(f, -1, 1.3, 8), look: loc(f, 0, .6, 0), fov: 40 })],
[.06, .065, f => ({ pos: loc(f, -7.5, 2.3, 4.5), look: loc(f, 4, .6, 0), fov: 42 })],
[.085, .23, f => ({ pos: new THREE.Vector3(16, 9, LZ + 25), look: new THREE.Vector3(16, 9, 2).lerp(f.p, .3), fov: 44 })],
[.255, .37, f => ({ pos: f.p.clone().add(new THREE.Vector3(-7, 2, 9)), look: f.p.clone().add(new THREE.Vector3(5, 1.4, 0)), fov: 46 })],
[.395, .48, f => ({ pos: loc(f, -9, -1.8, 7), look: loc(f, 2, 1, 0), fov: 50 })],
[.52, .62, f => ({ pos: loc(f, -4, 2.2, 12), look: loc(f, 1.5, .5, 0), fov: 42 })],
[.665, 1, f => ({ pos: loc(f, -11, 3.2, 2), look: loc(f, 8, 1, 0), fov: 48 })],
];
const shotAt = (p, f) => {
for (let i = 0; i < SHOTS.length; i++) {
const [a, b, fn] = SHOTS[i];
if (p <= b || i === SHOTS.length - 1) {
if (p >= a || i === 0) return fn(f);
const prev = SHOTS[i - 1], w = smooth(prev[1], a, p), A = prev[2](f), B = fn(f);
return { pos: A.pos.lerp(B.pos, w), look: A.look.lerp(B.look, w), fov: lerp(A.fov, B.fov, w) };
}
}
};
const blend = (A, B, w) => ({ pos: A.pos.clone().lerp(B.pos, w), look: A.look.clone().lerp(B.look, w), fov: lerp(A.fov, B.fov, w), up: (A.up || UP).clone().lerp(B.up || UP, w).normalize() });
let prevB = 0;
return {
// everything about the film at time t (seconds)
at(t, time, aspect, fovFor = f => f) {
const b = clamp(t / BAR, 0, SONG_BARS), p = rideP(b);
T.sample(distAt(b), fr);
basis.makeBasis(fr.f, fr.u, fr.s); q.setFromRotationMatrix(basis);
const v = b < 2 ? 0 : Math.min(93, (distAt(b + .02) - distAt(b)) / (.02 * BAR) * FILM), onLoop = fr.i >= T.loopIn && fr.i <= T.loopOut;
const g = onLoop ? v * v / (R * G) + fr.u.y : (p > .385 ? 1 + thrustPower(p) * .25 : 1);
const speed = v * 3.6, dep = thrustDeploy(p), power = thrustPower(p);
// camera
let cam;
if (b < 2) {
const h = b / 2, a = lerp(.75, 1.45, smooth(0, 1, h));
cam = { pos: fr.p.clone().add(new THREE.Vector3(Math.cos(a) * 7.4, lerp(2.2, 1.3, h), Math.sin(a) * 8.2)), look: fr.p.clone().add(new THREE.Vector3(0, .55, 0)), fov: 40 };
cam = blend(cam, shotAt(0, fr), smooth(.8, 1, h));
} else {
cam = shotAt(p, fr);
if (b > 17.2) {
car.group.position.copy(fr.p); car.group.quaternion.copy(q); car.group.updateMatrixWorld(true);
const head = { pos: HEAD.clone().applyMatrix4(car.group.matrixWorld), look: loc(fr, 30, .5, HEAD.z), up: fr.u.clone(), fov: 72 };
const glance = { pos: head.pos, look: interior.screen.getWorldPosition(new THREE.Vector3()), up: fr.u.clone(), fov: 66 };
const sv = interior.screenView(aspect, fovFor(46)), scr = { pos: sv.pos, look: sv.look, up: sv.up, fov: 46 };
if (b < 18.4) cam = blend(cam, head, smooth(17.5, 18.4, b));
else if (b < 20.2) cam = blend(head, glance, smooth(19.4, 20.2, b));
else cam = blend(glance, scr, smooth(20.2, 21.4, b));
}
}
if (b >= 8 && prevB < 8) this.onDrop && this.onDrop();
prevB = b;
let gear = 1; const GEARS = [0, 60, 100, 140, 185, 235, 285, 340];
for (let i = 1; i < GEARS.length; i++) if (speed >= GEARS[i]) gear = i + 1;
gear = Math.min(7, gear);
const rpm = b < 8 && b >= 2 ? 2400 + clamp((speed - GEARS[gear - 1]) / ((GEARS[gear] || 340) - GEARS[gear - 1])) * 5200 : 900;
return {
bar: b, p, reveal: smooth(0, 1.8, b), pos: fr.p.clone(), quat: q.clone(), fr, speed, g, alt: Math.max(0, fr.p.y), vs: v * fr.f.y,
thrust: { power: power * dep, angle: thrustAngle(p), deploy: dep }, wings: wingsOut(p), air: p > .385, onLoop,
cam, inside: b > 18.1, screenOn: b > 18.2, mode: thrustAngle(p) > .9 ? "GLIDE" : p > .385 ? "THRUST" : "DRIVE",
rpm, gear, load: b < 8 ? .8 : .15, engine: b < 8 ? 1 : .15,
zones: zonesFrom(fr.p, fr.f), dream: smooth(22, 22.6, b), fade: smooth(23.4, 24, b),
chapter: CHAPTERS.reduce((c, ch) => (b >= ch[0] ? ch : c), CHAPTERS[0]),
};
},
};
}
|