Download static/car.js from Death242/void: direct link, hf CLI and curl.
- Browser
- Download file 37.3 kB
-
https://huggingface.co/spaces/Death242/void/resolve/main/static/car.js
- Command line
-
hf download hf://spaces/Death242/void/static/car.js
-
curl -L -o car.js https://huggingface.co/spaces/Death242/void/resolve/main/static/car.js
37.3 kB
| // The VOID car (shape inspired by the McLaren 765LT), modelled in code: the body and canopy are lofted from side-profile curves | |
| // (a rounded superellipse cross-section at every station), the wheel wells are carved out of | |
| // the sides, and a blueprint of grid lines can be swept away by a clipping plane to "build" it. | |
| import * as THREE from "three"; | |
| import { mergeGeometries } from "three/addons/utils/BufferGeometryUtils.js"; | |
| const X0 = -2.3, LEN = 4.6; // rear tip to nose, along +x | |
| const WHEELS = [1.42, -1.38]; // front and rear axle x (a 2.8 m wheelbase) | |
| const WR = 0.36, ARCH = 0.44, WELL = 0.62; // wheel radius, arch radius, how far in the wells are carved | |
| // cosine interpolation through [u, value] keys (u from 0 at the rear to 1 at the nose) | |
| function curve(keys, u) { | |
| if (u <= keys[0][0]) return keys[0][1]; | |
| for (let i = 1; i < keys.length; i++) { | |
| const [u1, v1] = keys[i], [u0, v0] = keys[i - 1]; | |
| if (u <= u1) { const s = (1 - Math.cos(Math.PI * (u - u0) / (u1 - u0))) / 2; return v0 + (v1 - v0) * s; } | |
| } | |
| return keys[keys.length - 1][1]; | |
| } | |
| const spow = (v, p) => Math.sign(v) * Math.pow(Math.abs(v), p); | |
| // long-tail proportions: a tall, blunt tail, wide rear haunches, a pinched waist where the door | |
| // scoop is, wide front fenders and a low, narrow nose | |
| const BODY = { | |
| x0: X0, len: LEN, n: 6, tumble: .3, | |
| top: [[0, .84], [.03, .93], [.12, .96], [.25, .96], [.4, .9], [.55, .84], [.66, .8], [.78, .68], [.9, .54], [.97, .44], [1, .36]], | |
| bot: [[0, .3], [.04, .2], [.12, .16], [.88, .16], [.96, .2], [1, .28]], | |
| half: [[0, .86], [.05, .95], [.18, 1.0], [.35, .96], [.5, .91], [.62, .93], [.8, .99], [.93, .93], [1, .62]], | |
| ends: [.012, .06], fenders: true, scoop: true, | |
| }; | |
| // a cab-forward teardrop: the screen starts far forward, the roof peaks over the doors and the | |
| // glass runs back in a long taper over the engine | |
| const CANOPY = { | |
| x0: -1.35, len: 2.35, n: 4, tumble: .42, | |
| top: [[0, .92], [.12, 1.0], [.3, 1.12], [.5, 1.19], [.65, 1.17], [.8, 1.06], [.92, .92], [1, .8]], | |
| bot: [[0, .72], [1, .72]], | |
| half: [[0, .42], [.2, .6], [.5, .72], [.75, .72], [.92, .64], [1, .5]], | |
| ends: [.15, .06], fenders: false, | |
| }; | |
| // the other cars in the garage, lofted the same way | |
| export const MODELS = { | |
| hydra: { name: "Hydra LTE242", body: BODY, canopy: CANOPY }, | |
| vandal: { name: "Vandal GT", note: "Muscle fastback", body: { x0: X0, len: LEN, n: 8, tumble: .2, fenders: true, scoop: false, ends: [.02, .03], | |
| top: [[0, .92], [.06, .98], [.2, 1.0], [.35, .98], [.5, .95], [.62, .93], [.75, .9], [.9, .84], [.98, .74], [1, .66]], bot: [[0, .32], [.05, .22], [.12, .18], [.88, .18], [.96, .22], [1, .3]], | |
| half: [[0, .86], [.06, .93], [.2, .96], [.5, .92], [.8, .94], [.95, .9], [1, .72]] }, | |
| canopy: { x0: -1.25, len: 2.0, n: 4, tumble: .35, ends: [.2, .08], fenders: false, top: [[0, .98], [.15, 1.05], [.35, 1.25], [.6, 1.28], [.8, 1.2], [.95, 1.0], [1, .95]], bot: [[0, .88], [1, .88]], half: [[0, .6], [.3, .72], [.7, .72], [1, .6]] } }, | |
| wraith: { name: "Wraith X", note: "Wedge hypercar", body: { x0: X0, len: LEN, n: 5, tumble: .32, fenders: true, scoop: true, ends: [.01, .04], | |
| top: [[0, .8], [.05, .86], [.2, .88], [.4, .84], [.6, .74], [.8, .6], [.95, .45], [1, .34]], bot: [[0, .26], [.04, .16], [.12, .13], [.88, .13], [.96, .16], [1, .22]], | |
| half: [[0, .9], [.05, 1.0], [.2, 1.02], [.4, .96], [.6, .95], [.82, .98], [.95, .9], [1, .55]] }, | |
| canopy: { x0: -.9, len: 2.3, n: 4, tumble: .45, ends: [.2, .05], fenders: false, top: [[0, .82], [.2, 1.0], [.5, 1.07], [.75, 1.0], [.95, .8], [1, .7]], bot: [[0, .68], [1, .62]], half: [[0, .38], [.3, .62], [.6, .66], [.9, .55], [1, .45]] } }, | |
| titan: { name: "Titan RS", note: "Super SUV", body: { x0: X0, len: LEN, n: 10, tumble: .12, fenders: true, scoop: false, ends: [.02, .03], | |
| top: [[0, 1.15], [.05, 1.2], [.6, 1.2], [.75, 1.12], [.9, 1.0], [1, .9]], bot: [[0, .38], [.05, .28], [.12, .26], [.88, .26], [.95, .28], [1, .36]], half: [[0, .92], [.05, .98], [.5, 1.0], [.95, .98], [1, .85]] }, | |
| canopy: { x0: -2.15, len: 3.3, n: 10, tumble: .15, ends: [.02, .12], fenders: false, top: [[0, 1.15], [.05, 1.62], [.7, 1.66], [.85, 1.5], [1, 1.2]], bot: [[0, 1.1], [1, 1.1]], half: [[0, .86], [.5, .9], [1, .86]] } }, | |
| cab: { name: "Big Apple Cab", note: "A yellow cab with a jet kit", body: { x0: X0, len: LEN, n: 9, tumble: .15, fenders: true, scoop: false, ends: [.02, .03], | |
| top: [[0, .9], [.05, .95], [.25, .96], [.35, .95], [.65, .93], [.8, .9], [.95, .84], [1, .76]], bot: [[0, .32], [.05, .22], [.12, .18], [.88, .18], [.96, .22], [1, .3]], half: [[0, .86], [.05, .92], [.5, .93], [.95, .9], [1, .75]] }, | |
| canopy: { x0: -1.35, len: 2.5, n: 8, tumble: .2, ends: [.1, .1], fenders: false, top: [[0, .95], [.15, 1.35], [.75, 1.38], [.9, 1.2], [1, .95]], bot: [[0, .9], [1, .9]], half: [[0, .75], [.5, .82], [1, .75]] } }, | |
| }; | |
| // a (nu+1) x nv grid of points on the lofted surface | |
| function loft(spec, nu, nv) { | |
| const P = new Float32Array((nu + 1) * nv * 3); | |
| for (let i = 0; i <= nu; i++) { | |
| const u = i / nu, x = spec.x0 + spec.len * u; | |
| const t = curve(spec.top, u), b = curve(spec.bot, u), w = curve(spec.half, u); | |
| const c = (t + b) / 2, h = (t - b) / 2; | |
| const e = Math.pow(Math.max(0, Math.min(1, u / spec.ends[0], (1 - u) / spec.ends[1])), .35); | |
| for (let j = 0; j < nv; j++) { | |
| const th = (j / nv) * Math.PI * 2, cz = Math.cos(th), sy = Math.sin(th); | |
| let z = w * spow(cz, 2 / spec.n), y = h * spow(sy, 2 / spec.n); | |
| if (sy > 0) z *= 1 - spec.tumble * sy * sy; | |
| if (spec.fenders && sy > 0) for (const xw of WHEELS) y += (xw > 0 ? .2 : .14) * Math.exp(-(((x - xw) / .55) ** 2)) * Math.abs(cz) ** 3; | |
| z *= e; y = c + y * e; | |
| if (spec.scoop && Math.abs(cz) > .3) { // the big intake carved into each door, deepest at its front | |
| const xr = (x + .35) / .62, yr = (y - .6) / .17; | |
| if (Math.abs(xr) < 1 && Math.abs(yr) < 1) z *= 1 - .17 * (1 - xr * xr) * Math.sqrt(1 - yr * yr) * (1.15 - .3 * xr); | |
| } | |
| if (spec.fenders) for (const xw of WHEELS) { | |
| if ((x - xw) ** 2 + (y - WR) ** 2 < ARCH * ARCH && Math.abs(z) > WELL) z = Math.sign(z) * WELL; | |
| } | |
| const k = (i * nv + j) * 3; P[k] = x; P[k + 1] = y; P[k + 2] = z; | |
| } | |
| } | |
| return { P, nu, nv }; | |
| } | |
| function surface({ P, nu, nv }) { | |
| const idx = []; | |
| for (let i = 0; i < nu; i++) for (let j = 0; j < nv; j++) { | |
| const a = i * nv + j, b = i * nv + (j + 1) % nv, c = (i + 1) * nv + j, d = (i + 1) * nv + (j + 1) % nv; | |
| idx.push(a, c, b, b, c, d); | |
| } | |
| const g = new THREE.BufferGeometry(); | |
| g.setAttribute("position", new THREE.BufferAttribute(P, 3)); | |
| g.setIndex(idx); g.computeVertexNormals(); | |
| return g; | |
| } | |
| // blueprint: every few rings across the car, and every few lines along it | |
| function gridLines({ P, nu, nv }, ringStep, lineStep) { | |
| const pts = [], at = (i, j) => { const k = (i * nv + j) * 3; return [P[k], P[k + 1], P[k + 2]]; }; | |
| for (let i = 0; i <= nu; i += ringStep) for (let j = 0; j < nv; j++) pts.push(...at(i, j), ...at(i, (j + 1) % nv)); | |
| for (let j = 0; j < nv; j += lineStep) for (let i = 0; i < nu; i++) pts.push(...at(i, j), ...at(i + 1, j)); | |
| const g = new THREE.BufferGeometry(); | |
| g.setAttribute("position", new THREE.Float32BufferAttribute(pts, 3)); | |
| return g; | |
| } | |
| export function buildCar(model = "hydra") { | |
| const M = MODELS[model] || MODELS.hydra, BS = M.body, CS = M.canopy; | |
| const car = new THREE.Group(); | |
| const solid = new THREE.Group(), wire = new THREE.Group(); | |
| car.add(solid, wire); | |
| // the reveal: solid shows behind the scan plane, blueprint in front of it | |
| const solidClip = new THREE.Plane(new THREE.Vector3(-1, 0, 0), -3); | |
| const wireClip = new THREE.Plane(new THREE.Vector3(1, 0, 0), 3); | |
| const clip = [solidClip]; | |
| const flakes = (() => { // a fine noise normal map: the metallic flake that sparkles under the clear coat | |
| const c = document.createElement("canvas"); c.width = c.height = 256; const g = c.getContext("2d"), img = g.createImageData(256, 256); | |
| for (let i = 0; i < 256 * 256; i++) { const a = Math.random() * 6.28, r = Math.random() * .55; img.data[i * 4] = 128 + Math.cos(a) * r * 127; img.data[i * 4 + 1] = 128 + Math.sin(a) * r * 127; img.data[i * 4 + 2] = 255; img.data[i * 4 + 3] = 255; } | |
| g.putImageData(img, 0, 0); const t = new THREE.CanvasTexture(c); t.wrapS = t.wrapT = THREE.RepeatWrapping; t.repeat.set(30, 30); return t; | |
| })(); | |
| const paint = new THREE.MeshPhysicalMaterial({ color: 0x0d0d10, metalness: .6, roughness: .32, normalMap: flakes, normalScale: new THREE.Vector2(.12, .12), clearcoat: 1, clearcoatRoughness: .03, envMapIntensity: 1.5, clippingPlanes: clip }); | |
| const glass = new THREE.MeshPhysicalMaterial({ color: 0x0a0d12, metalness: .2, roughness: .02, clearcoat: 1, envMapIntensity: 1.8, transparent: true, opacity: .6, depthWrite: false, clippingPlanes: clip }); // tinted, so you can see the cabin | |
| const carbon = new THREE.MeshStandardMaterial({ color: 0x0c0c0e, metalness: .4, roughness: .55, clippingPlanes: clip }); | |
| const rubber = new THREE.MeshStandardMaterial({ color: 0x0a0a0a, roughness: .9, clippingPlanes: clip }); | |
| const rim = new THREE.MeshStandardMaterial({ color: 0x121215, metalness: 1, roughness: .16, envMapIntensity: 1.6, clippingPlanes: clip }); | |
| const caliper = new THREE.MeshStandardMaterial({ color: 0xc1121f, metalness: .3, roughness: .4, clippingPlanes: clip }); | |
| const front = new THREE.MeshBasicMaterial({ color: new THREE.Color(0xe8fbff).multiplyScalar(3), toneMapped: false, clippingPlanes: clip }); | |
| const rear = new THREE.MeshBasicMaterial({ color: new THREE.Color(0xff2a3a).multiplyScalar(2.6), toneMapped: false, clippingPlanes: clip }); | |
| const trim = new THREE.MeshPhysicalMaterial({ color: 0xc1121f, metalness: .4, roughness: .3, clearcoat: 1, clippingPlanes: clip }); // the red accents | |
| const ti2 = new THREE.MeshStandardMaterial({ color: 0x8d9099, metalness: 1, roughness: .25, side: THREE.DoubleSide, clippingPlanes: clip }); | |
| const pipeOf = () => new THREE.CylinderGeometry(.06, .06, .2, 20, 1, true).rotateZ(Math.PI / 2); | |
| const lineMat = new THREE.LineBasicMaterial({ color: 0xeef0f5, transparent: true, opacity: .75, clippingPlanes: [wireClip] }); | |
| const bodyHi = loft(BS, 220, 96), canHi = loft(CS, 90, 64); | |
| const bodyMesh = new THREE.Mesh(surface(bodyHi), paint), canMesh = new THREE.Mesh(surface(canHi), glass); | |
| solid.add(bodyMesh, canMesh); | |
| wire.add(new THREE.LineSegments(gridLines(loft(BS, 110, 48), 5, 4), lineMat), new THREE.LineSegments(gridLines(loft(CS, 40, 32), 5, 4), lineMat)); | |
| const topAt = u => curve(BS.top, u), halfAt = u => curve(BS.half, u); | |
| const add = (geo, mat, x, y, z, parent = solid) => { const m = new THREE.Mesh(geo, mat); m.position.set(x, y, z); parent.add(m); return m; }; | |
| // details: eye-socket headlights, hood vents, splitter, side blades and sills, flying | |
| // buttresses, engine louvers, a full-width wing, a mesh tail with four high exhausts, a diffuser | |
| const beam = (a, b, w, h, mat) => { const m = new THREE.Mesh(new THREE.BoxGeometry(w, a.distanceTo(b), h), mat); m.position.copy(a).add(b).multiplyScalar(.5); m.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), b.clone().sub(a).normalize()); solid.add(m); return m; }; | |
| const exhausts = []; | |
| if (model === "hydra") { | |
| for (const s of [-1, 1]) { | |
| const sock = add(new THREE.SphereGeometry(.2, 28, 14), carbon, 1.97, .52, s * .62); | |
| sock.scale.set(.7, .42, 1.15); sock.rotation.y = s * .55; sock.rotation.x = s * .25; | |
| const led = add(new THREE.BoxGeometry(.03, .022, .3), front, 2.04, .56, s * .64); led.rotation.y = s * .6; led.rotation.x = s * -.35; | |
| add(new THREE.BoxGeometry(.03, .02, .12), front, 2.07, .49, s * .5).rotation.y = s * .25; | |
| const vent = add(new THREE.BoxGeometry(.36, .012, .24), carbon, 1.5, .64, s * .3); vent.rotation.z = -.43; | |
| const blade = add(new THREE.BoxGeometry(.62, .07, .02), carbon, .88, .4, s * .975); blade.rotation.z = .22; | |
| add(new THREE.BoxGeometry(2.05, .07, .03), carbon, -.05, .21, s * .955); | |
| add(new THREE.BoxGeometry(2.05, .012, .034), trim, -.05, .248, s * .956); // red pinstripe on the sill | |
| beam(new THREE.Vector3(-.45, 1.1, s * .6), new THREE.Vector3(-2.05, .95, s * .86), .07, .11, paint); | |
| const tl = add(new THREE.BoxGeometry(.03, .03, .56), rear, -2.29, .82, s * .52); tl.rotation.x = s * .05; | |
| add(new THREE.BoxGeometry(.03, .2, .03), rear, -2.28, .72, s * .8); | |
| } | |
| for (let k = 0; k < 6; k++) add(new THREE.BoxGeometry(.06, .015, .92), carbon, -1.42 - k * .11, .975, 0); | |
| add(new THREE.BoxGeometry(.3, .03, 1.62), carbon, 2.06, .175, 0); | |
| add(new THREE.BoxGeometry(.02, .032, 1.62), trim, 2.2, .175, 0); // red splitter lip | |
| const wing = add(new THREE.BoxGeometry(.38, .03, 1.94), carbon, -2.12, 1.02, 0); wing.rotation.z = .12; | |
| for (const z of [-.97, .97]) add(new THREE.BoxGeometry(.42, .16, .02), trim, -2.1, .98, z); | |
| add(new THREE.BoxGeometry(.04, .32, 1.5), carbon, -2.3, .58, 0); | |
| for (let k = -3; k <= 3; k++) add(new THREE.BoxGeometry(.6, .2, .025), carbon, -2.06, .25, k * .2).rotation.z = -.42; | |
| const ti = new THREE.MeshStandardMaterial({ color: 0x6d7079, metalness: 1, roughness: .3, side: THREE.DoubleSide, clippingPlanes: clip }); | |
| const pipe = new THREE.CylinderGeometry(.05, .05, .22, 24, 1, true).rotateZ(Math.PI / 2), soot = new THREE.CircleGeometry(.045, 20).rotateY(-Math.PI / 2); | |
| for (const y of [.6, .72]) for (const z of [-.065, .065]) { add(pipe, ti, -2.28, y, z); add(soot, rubber, -2.37, y, z); } | |
| for (const y of [.6, .72]) for (const z of [-.065, .065]) exhausts.push([-2.38, y, z]); | |
| } else { | |
| // shared details: headlights, tail lights, a splitter, sills, mirrors, twin exhausts | |
| const nose = X0 + LEN, ny = topAt(.96) - .1, rearY = topAt(.03) - .12; | |
| for (const s of [-1, 1]) { | |
| add(new THREE.BoxGeometry(.05, .07, .36), front, nose - .1, ny, s * halfAt(.94) * .62).rotation.y = s * .35; | |
| add(new THREE.BoxGeometry(.05, .08, .5), rear, X0 + .03, rearY, s * halfAt(.04) * .62); | |
| add(new THREE.BoxGeometry(2.1, .07, .03), carbon, 0, curve(BS.bot, .5) + .05, s * (halfAt(.5) - .02)); | |
| const mir = add(new THREE.BoxGeometry(.12, .08, .16), paint, CS.x0 + CS.len * .82, curve(CS.bot, .8) + .06, s * (curve(CS.half, .8) + .1)); mir.rotation.y = s * .2; | |
| add(pipeOf(), ti2, X0 + .02, curve(BS.bot, .05) + .1, s * .35); exhausts.push([X0 - .08, curve(BS.bot, .05) + .1, s * .35]); | |
| } | |
| add(new THREE.BoxGeometry(.3, .03, halfAt(.97) * 1.6), carbon, nose - .2, curve(BS.bot, .97) + .02, 0); | |
| add(new THREE.BoxGeometry(.02, .032, halfAt(.97) * 1.6), trim, nose - .06, curve(BS.bot, .97) + .02, 0); | |
| add(new THREE.BoxGeometry(.02, .05, halfAt(.02) * 1.4), rear, X0 + .01, rearY + .07, 0); | |
| if (model === "cab") { // the roof sign and the checker stripe | |
| const signT = (() => { const c = document.createElement("canvas"); c.width = 256; c.height = 64; const g = c.getContext("2d"); g.fillStyle = "#fff6d0"; g.fillRect(0, 0, 256, 64); g.fillStyle = "#111"; g.font = "900 40px sans-serif"; g.textAlign = "center"; g.fillText("TAXI", 128, 48); const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace; return t; })(); | |
| const sign = add(new THREE.BoxGeometry(.55, .22, .9), new THREE.MeshBasicMaterial({ map: signT, toneMapped: false, color: new THREE.Color(1.6, 1.6, 1.6), clippingPlanes: clip }), CS.x0 + CS.len * .45, curve(CS.top, .45) + .1, 0); | |
| sign.rotation.y = Math.PI / 2; | |
| } | |
| } | |
| add(new THREE.BoxGeometry(3.6, .03, 1.7), carbon, -.1, .16, 0); // flat floor | |
| // wheels: a rounded tyre, a deep-dish rim with twin spokes and a centre lock, a brake disc | |
| // (all spinning) and a fixed caliper | |
| const wheels = []; | |
| const V2 = (x, y) => new THREE.Vector2(x, y); | |
| const tyreG = new THREE.LatheGeometry([V2(.265, -.15), V2(.32, -.156), V2(.35, -.145), V2(.362, -.115), V2(WR + .004, 0), V2(.362, .115), V2(.35, .145), V2(.32, .156), V2(.265, .15)], 72).rotateX(Math.PI / 2); | |
| const barrelG = new THREE.LatheGeometry([V2(.262, .15), V2(.27, .13), V2(.268, -.13), V2(.24, -.14)], 64).rotateX(Math.PI / 2); | |
| const lipG = new THREE.TorusGeometry(.266, .009, 8, 64); | |
| const discG = new THREE.CylinderGeometry(.2, .2, .03, 48).rotateX(Math.PI / 2), hatG = new THREE.CylinderGeometry(.09, .09, .06, 32).rotateX(Math.PI / 2); | |
| const nutG = new THREE.CylinderGeometry(.045, .045, .05, 6).rotateX(Math.PI / 2); | |
| const spokeG = new THREE.BoxGeometry(.026, .2, .028).translate(0, .155, 0); | |
| const disc = new THREE.MeshStandardMaterial({ color: 0x5a5c62, metalness: .8, roughness: .45, clippingPlanes: clip, emissive: 0xff4a10, emissiveIntensity: 0 }); // glows when the brakes are hammered | |
| const wireWheel = new THREE.EdgesGeometry(new THREE.CylinderGeometry(WR, WR, .3, 28).rotateX(Math.PI / 2), 20); | |
| for (const xw of WHEELS) for (const s of [-1, 1]) { | |
| const spin = new THREE.Group(); spin.position.set(xw, WR, s * .83); solid.add(spin); | |
| spin.add(new THREE.Mesh(tyreG, rubber), new THREE.Mesh(barrelG, carbon)); | |
| const lip = new THREE.Mesh(lipG, rim); lip.position.z = s * .14; spin.add(lip); | |
| const d = new THREE.Mesh(discG, disc); d.position.z = -s * .04; spin.add(d); | |
| const hat = new THREE.Mesh(hatG, rim); hat.position.z = s * .04; spin.add(hat); | |
| const nut = new THREE.Mesh(nutG, caliper); nut.position.z = s * .12; spin.add(nut); | |
| for (let k = 0; k < 5; k++) for (const off of [-.11, .11]) { // five twin spokes, dished in toward the hub | |
| const sp = new THREE.Mesh(spokeG, rim); sp.position.z = s * .1; | |
| sp.rotation.z = k / 5 * Math.PI * 2 + off; sp.rotation.x = s * .12; spin.add(sp); | |
| } | |
| const cal = add(new THREE.BoxGeometry(.24, .13, .08), caliper, xw - .16, WR + .13, s * .7); cal.rotation.z = .65; | |
| const wl = new THREE.LineSegments(wireWheel, lineMat); wl.position.set(xw, WR, s * .83); wire.add(wl); | |
| wheels.push(spin); | |
| } | |
| // the glowing scan plane that sweeps along the car during the build | |
| const scanTex = (() => { | |
| const c = document.createElement("canvas"); c.width = 4; c.height = 128; | |
| const g = c.getContext("2d"), gr = g.createLinearGradient(0, 0, 0, 128); | |
| gr.addColorStop(0, "rgba(255,255,255,0)"); gr.addColorStop(.5, "rgba(255,255,255,1)"); gr.addColorStop(1, "rgba(255,255,255,0)"); | |
| g.fillStyle = gr; g.fillRect(0, 0, 4, 128); return new THREE.CanvasTexture(c); | |
| })(); | |
| const scan = new THREE.Mesh(new THREE.PlaneGeometry(2.6, 1.6), new THREE.MeshBasicMaterial({ map: scanTex, transparent: true, opacity: .0, blending: THREE.AdditiveBlending, depthWrite: false, side: THREE.DoubleSide, toneMapped: false })); | |
| scan.rotation.y = Math.PI / 2; scan.rotation.z = Math.PI / 2; scan.position.y = .7; car.add(scan); | |
| // flight kit: four thrusters that swing from straight down (lift-off) to straight back (cruise), | |
| // and two swept wings that slide out of the sides, with nav lights on the tips | |
| const housing = new THREE.MeshStandardMaterial({ color: 0x18181c, metalness: .85, roughness: .3 }); | |
| const core = new THREE.MeshBasicMaterial({ color: new THREE.Color(0xdff6ff).multiplyScalar(4), transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, toneMapped: false }); | |
| const outer = new THREE.MeshBasicMaterial({ color: new THREE.Color(0x4f8dff).multiplyScalar(2.2), transparent: true, opacity: .55, blending: THREE.AdditiveBlending, depthWrite: false, toneMapped: false, side: THREE.DoubleSide }); | |
| const nozG = new THREE.CylinderGeometry(.12, .16, .26, 24, 1, true), ringG = new THREE.TorusGeometry(.15, .025, 8, 28).rotateX(Math.PI / 2); | |
| const flameG = new THREE.ConeGeometry(.14, 1, 20, 1, true).rotateX(Math.PI).translate(0, -.5, 0); | |
| const thrusters = []; | |
| for (const [x, y] of [[.9, .3], [-.95, .38]]) for (const s of [-1, 1]) { | |
| const pivot = new THREE.Group(); pivot.position.set(x, y, s * 1.0); car.add(pivot); | |
| const noz = new THREE.Mesh(nozG, housing); noz.position.y = -.05; | |
| const ring = new THREE.Mesh(ringG, front); ring.position.y = -.18; | |
| const fo = new THREE.Mesh(flameG, outer), fc = new THREE.Mesh(flameG, core); | |
| fo.position.y = fc.position.y = -.18; fc.scale.set(.45, 1, .45); | |
| pivot.add(noz, ring, fo, fc); pivot.scale.setScalar(0); | |
| thrusters.push({ pivot, fo, fc, seed: Math.random() * 10 }); | |
| } | |
| const glowLight = new THREE.PointLight(0x6aa8ff, 0, 9, 2); glowLight.position.set(0, -.4, 0); car.add(glowLight); | |
| const wingShape = new THREE.Shape([[.7, 0], [-1.05, 0], [-1.35, 2.25], [-.75, 2.25]].map(([a, b]) => new THREE.Vector2(a, b))); | |
| const wingG = new THREE.ExtrudeGeometry(wingShape, { depth: .05, bevelEnabled: true, bevelThickness: .015, bevelSize: .02, bevelSegments: 2 }).rotateX(-Math.PI / 2); | |
| const wings = []; | |
| for (const s of [-1, 1]) { | |
| const w = new THREE.Mesh(wingG, paint); w.position.set(-.15, .6, s * .85); w.scale.set(1, 1, 0); car.add(w); | |
| const nav = new THREE.Mesh(new THREE.SphereGeometry(.05, 12, 8), new THREE.MeshBasicMaterial({ color: new THREE.Color(s > 0 ? 0x22ff66 : 0xff2233).multiplyScalar(3), toneMapped: false })); | |
| nav.position.set(-1.1, .04, -2.25); w.add(nav); | |
| const edge = new THREE.Mesh(new THREE.BoxGeometry(1.8, .02, .02), front); edge.position.set(-.18, .06, -.02); w.add(edge); | |
| wings.push({ w, s, nav }); | |
| } | |
| // fewer draw calls: the fixed detail parts (lights, vents, splitter, sills...) and each wheel's parts are merged per material | |
| const mergeStatic = (grp, keep) => { | |
| const by = new Map(); | |
| for (const o of [...grp.children]) { | |
| if (!o.isMesh || keep.has(o) || o.children.length || o.material.map) continue; | |
| o.updateMatrix(); const g = o.geometry.index ? o.geometry.toNonIndexed() : o.geometry.clone(); g.applyMatrix4(o.matrix); | |
| for (const k of Object.keys(g.attributes)) if (k !== "position" && k !== "normal") g.deleteAttribute(k); | |
| if (!g.attributes.normal) g.computeVertexNormals(); | |
| if (!by.has(o.material)) by.set(o.material, []); by.get(o.material).push(g); grp.remove(o); | |
| } | |
| for (const [m, list] of by) { const mesh = new THREE.Mesh(mergeGeometries(list), m); mesh.castShadow = true; grp.add(mesh); } | |
| }; | |
| mergeStatic(solid, new Set([bodyMesh, canMesh])); | |
| for (const w of wheels) mergeStatic(w, new Set()); | |
| // ---- garage parts: spoilers, liveries, exhaust flames ---- | |
| const spoilers = new THREE.Group(); solid.add(spoilers); | |
| // active aero: a carbon wing stowed in the tail that rises at speed and flips up as an air brake under hard braking | |
| const aero = new THREE.Group(), aeroWing = new THREE.Group(); solid.add(aero); aero.add(aeroWing); | |
| { const y0 = topAt(.05), w = Math.max(1.4, halfAt(.05) * 1.9), x = X0 + .3; | |
| aero.position.set(x, y0 - .02, 0); | |
| const blade = new THREE.Mesh(new THREE.BoxGeometry(.42, .03, w), carbon); blade.position.x = -.06; aeroWing.add(blade); | |
| for (const z of [-1, 1]) { const ep = new THREE.Mesh(new THREE.BoxGeometry(.46, .14, .02), carbon); ep.position.set(-.06, .03, z * w / 2); aeroWing.add(ep); } | |
| const ledG = new THREE.Mesh(new THREE.BoxGeometry(.02, .012, w * .9), rear); ledG.position.set(-.27, 0, 0); aeroWing.add(ledG); | |
| for (const z of [-.3, .3]) { const st = new THREE.Mesh(new THREE.BoxGeometry(.05, .4, .04).translate(0, -.2, 0), carbon); st.position.set(0, 0, z * w); aeroWing.add(st); } | |
| aero.userData = { lift: 0, tilt: 0 }; } | |
| function setSpoiler(k) { | |
| spoilers.clear(); | |
| const y0 = topAt(.04), w = Math.max(1.5, halfAt(.05) * 2), x = X0 + .18; | |
| const part = (geo, m, px, py, pz, rz = 0) => { const o = new THREE.Mesh(geo, m); o.position.set(px, py, pz); o.rotation.z = rz; spoilers.add(o); o.castShadow = true; return o; }; | |
| if (k === "ducktail") part(new THREE.BoxGeometry(.32, .05, w * .9), paint, x + .05, y0 + .04, 0, .28); | |
| if (k === "gt") { part(new THREE.BoxGeometry(.45, .035, w + .25), carbon, x - .05, y0 + .42, 0, .1); for (const z of [-1, 1]) { part(new THREE.BoxGeometry(.08, .42, .03), carbon, x, y0 + .21, z * w * .3); part(new THREE.BoxGeometry(.5, .2, .02), trim, x - .05, y0 + .42, z * (w + .25) / 2); } } | |
| if (k === "swan") { part(new THREE.BoxGeometry(.5, .04, w + .3), carbon, x - .1, y0 + .55, 0, .14); for (const z of [-1, 1]) { part(new THREE.BoxGeometry(.06, .6, .03), carbon, x + .05, y0 + .3, z * w * .28, -.35); part(new THREE.BoxGeometry(.55, .24, .02), trim, x - .1, y0 + .55, z * (w + .3) / 2); } } | |
| if (k === "rally") { part(new THREE.BoxGeometry(.4, .04, w * .95), paint, x + .25, topAt(.12) + .12, 0, .05); for (const z of [-1, 1]) part(new THREE.BoxGeometry(.32, .14, .02), paint, x + .25, topAt(.12) + .06, z * w * .46); } | |
| mergeStatic(spoilers, new Set()); | |
| } | |
| const liveryMat = new THREE.MeshPhysicalMaterial({ color: 0xffffff, roughness: .3, clearcoat: 1, side: THREE.DoubleSide, clippingPlanes: clip, polygonOffset: true, polygonOffsetFactor: -2 }); | |
| const livery = new THREE.Group(); solid.add(livery); | |
| const LP = bodyHi.P, LNV = bodyHi.nv, LNU = bodyHi.nu, pt = (i, j) => { const k = (i * LNV + ((j % LNV) + LNV) % LNV) * 3; return [LP[k], LP[k + 1], LP[k + 2]]; }; | |
| const patch = (i0, i1, js, push) => { // a strip of the body surface, lifted a hair off the paint | |
| const pos = []; | |
| for (let i = i0; i < i1; i++) for (let a = 0; a < js.length - 1; a++) { | |
| const q = [pt(i, js[a]), pt(i + 1, js[a]), pt(i + 1, js[a + 1]), pt(i, js[a + 1])].map(([x, y, z]) => [x, y + push[1], z + Math.sign(z) * push[2]]); | |
| pos.push(...q[0], ...q[1], ...q[2], ...q[0], ...q[2], ...q[3]); | |
| } | |
| const g = new THREE.BufferGeometry(); g.setAttribute("position", new THREE.Float32BufferAttribute(pos, 3)); g.computeVertexNormals(); | |
| livery.add(new THREE.Mesh(g, liveryMat)); | |
| }; | |
| const T4 = LNV / 4; | |
| function setLivery(k, hex) { | |
| livery.clear(); liveryMat.color.set(hex || "#ffffff"); | |
| if (k === "stripes") { patch(4, LNU - 2, [T4 - 5, T4 - 4, T4 - 3], [0, .006, 0]); patch(4, LNU - 2, [T4 + 3, T4 + 4, T4 + 5], [0, .006, 0]); } | |
| if (k === "center") patch(4, LNU - 2, [T4 - 2, T4 - 1, T4, T4 + 1, T4 + 2], [0, .006, 0]); | |
| if (k === "side") { patch(20, LNU - 30, [-1, 0, 1], [0, 0, .006]); patch(20, LNU - 30, [LNV / 2 - 1, LNV / 2, LNV / 2 + 1], [0, 0, .006]); } | |
| if (k === "race") { | |
| patch(4, LNU - 2, [T4 - 5, T4 - 4, T4 - 3], [0, .006, 0]); patch(4, LNU - 2, [T4 + 3, T4 + 4, T4 + 5], [0, .006, 0]); | |
| const c = document.createElement("canvas"); c.width = c.height = 128; const g = c.getContext("2d"); g.fillStyle = "#fff"; g.beginPath(); g.arc(64, 64, 60, 0, 7); g.fill(); g.fillStyle = "#111"; g.font = "900 70px sans-serif"; g.textAlign = "center"; g.fillText("42", 64, 89); | |
| const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace; | |
| for (const side of [0, LNV / 2]) { const [x, y, z] = pt(Math.round(LNU * .5), side); const d = new THREE.Mesh(new THREE.CircleGeometry(.2, 24), new THREE.MeshBasicMaterial({ map: t, transparent: true, clippingPlanes: clip })); d.position.set(x, y + .02, z + Math.sign(z) * .012); d.rotation.y = z > 0 ? 0 : Math.PI; livery.add(d); } | |
| } | |
| mergeStatic(livery, new Set()); | |
| } | |
| const flameMat = new THREE.MeshBasicMaterial({ color: new THREE.Color(0x4fa8ff).multiplyScalar(3), transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, toneMapped: false }); | |
| const flames = exhausts.map(([x, y, z]) => { const f = new THREE.Mesh(new THREE.ConeGeometry(.07, 1, 12, 1, true).rotateZ(Math.PI / 2).translate(-.5, 0, 0), flameMat); f.position.set(x, y, z); f.visible = false; solid.add(f); return f; }); | |
| const bodyBase = bodyMesh.geometry.attributes.position.array.slice(), canBase = canMesh.geometry.attributes.position.array.slice(); | |
| const wear = { total: 0, burn: 0, cracked: false }; let paintHex = "#0d0d10"; | |
| const crackTex = () => { // a starburst of cracks on the screen | |
| const c = document.createElement("canvas"); c.width = c.height = 256; const g = c.getContext("2d"); g.fillStyle = "#9aa4ae"; g.fillRect(0, 0, 256, 256); | |
| g.strokeStyle = "rgba(255,255,255,.95)"; g.lineWidth = 1.2; | |
| for (let n = 0; n < 3; n++) { const ox = 60 + Math.random() * 140, oy = 60 + Math.random() * 140; for (let k = 0; k < 14; k++) { let x = ox, y = oy, a = Math.random() * 6.28; g.beginPath(); g.moveTo(x, y); for (let j = 0; j < 7; j++) { a += (Math.random() - .5) * .7; x += Math.cos(a) * 14; y += Math.sin(a) * 14; g.lineTo(x, y); } g.stroke(); } for (let r = 8; r < 50; r += 12) { g.beginPath(); g.arc(ox, oy, r, 0, 6.3); g.stroke(); } } | |
| const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace; t.wrapS = t.wrapT = THREE.RepeatWrapping; t.repeat.set(1.5, 1.5); return t; | |
| }; | |
| // ---- finishing details: panel seams (doors, hood), LED light strips, number plates ---- | |
| { | |
| const seamMat = new THREE.MeshBasicMaterial({ color: 0x050505, side: THREE.DoubleSide, clippingPlanes: clip, polygonOffset: true, polygonOffsetFactor: -2, transparent: true, opacity: .85 }); | |
| const seamPatch = (i0, js, push) => { | |
| const pos = []; | |
| for (let a = 0; a < js.length - 1; a++) { const q = [pt(i0, js[a]), pt(i0 + 1, js[a]), pt(i0 + 1, js[a + 1]), pt(i0, js[a + 1])].map(([x, y, z]) => [x, y + push[1], z + Math.sign(z) * push[2]]); pos.push(...q[0], ...q[1], ...q[2], ...q[0], ...q[2], ...q[3]); } | |
| const g = new THREE.BufferGeometry(); g.setAttribute("position", new THREE.Float32BufferAttribute(pos, 3)); solid.add(new THREE.Mesh(g, seamMat)); | |
| }; | |
| const sideJ = (c) => Array.from({ length: 11 }, (_, k) => c - 5 + k); // the side of the car from the sill up to the waist | |
| for (const u of [.36, .55]) { const i = Math.round(LNU * u); seamPatch(i, sideJ(0), [0, 0, .004]); seamPatch(i, sideJ(LNV / 2), [0, 0, .004]); } | |
| const hoodI = Math.round(LNU * .8); seamPatch(hoodI, Array.from({ length: 13 }, (_, k) => T4 - 6 + k), [0, .004, 0]); | |
| // LED strips: a light bar across the nose and a full-width tail bar | |
| const nose = X0 + LEN; | |
| const drl = new THREE.Mesh(new THREE.BoxGeometry(.03, .025, halfAt(.93) * 1.5), front); drl.position.set(nose - .14, topAt(.93) - .02, 0); solid.add(drl); | |
| const tail = new THREE.Mesh(new THREE.BoxGeometry(.03, .04, halfAt(.03) * 1.7), rear); tail.position.set(X0 + .02, topAt(.03) - .07, 0); solid.add(tail); | |
| // number plates | |
| const plateT = (() => { const c = document.createElement("canvas"); c.width = 256; c.height = 64; const g = c.getContext("2d"); g.fillStyle = "#f4f4f0"; g.fillRect(0, 0, 256, 64); g.strokeStyle = "#111"; g.lineWidth = 4; g.strokeRect(3, 3, 250, 58); g.fillStyle = "#111"; g.font = "900 40px Inter, sans-serif"; g.textAlign = "center"; g.fillText("VOID·242", 128, 47); const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace; return t; })(); | |
| const plateM = new THREE.MeshStandardMaterial({ map: plateT, roughness: .4, metalness: .2, clippingPlanes: clip }); | |
| const pf = new THREE.Mesh(new THREE.PlaneGeometry(.52, .13), plateM); pf.position.set(nose + .005, curve(BS.bot, .98) + .14, 0); pf.rotation.y = Math.PI / 2; solid.add(pf); | |
| const pr = new THREE.Mesh(new THREE.PlaneGeometry(.52, .13), plateM); pr.position.set(X0 - .005, curve(BS.bot, .03) + .22, 0); pr.rotation.y = -Math.PI / 2; solid.add(pr); | |
| } | |
| return { | |
| group: car, wheels, clip, glowLight, materials: { carbon, front }, model, name: M.name, | |
| setSpoiler, setLivery, | |
| setTint(k) { const T = { light: [0x1a2a3a, .32, .1], dark: [0x0a0d12, .6, .1], limo: [0x030405, .9, .1], mirror: [0x9aa4b0, .88, .95] }[k] || [0x0a0d12, .6, .1]; glass.color.setHex(T[0]); glass.opacity = T[1]; glass.metalness = T[2]; }, | |
| setFlameColor(hex) { flameMat.color.set(hex).multiplyScalar(3); }, | |
| setFlame(power, time) { for (const f of flames) { f.visible = power > .02; f.scale.set(.4 + power * (1.1 + .3 * Math.sin(time * 60 + f.position.z * 9)), 1 + power * .6, 1 + power * .6); } }, | |
| // power 0..1 is flame size, angle 0 = pointing down, 1 = pointing back, deploy 0..1 brings them out | |
| setThrust(power, angle, deploy, time) { | |
| for (const t of thrusters) { | |
| t.pivot.scale.setScalar(Math.max(.0001, deploy)); t.pivot.visible = deploy > .005; | |
| t.pivot.rotation.z = -angle * Math.PI / 2; | |
| const f = power * (.82 + .18 * Math.sin(time * 41 + t.seed) * Math.sin(time * 23 + t.seed * 3)); | |
| t.fo.scale.set(1 + power * .3, Math.max(.001, f * 2.6), 1 + power * .3); | |
| t.fc.scale.set(.45, Math.max(.001, f * 1.5), .45); | |
| t.fo.visible = t.fc.visible = power > .01; | |
| } | |
| glowLight.intensity = power * 30; | |
| }, | |
| setWings(t, time) { | |
| for (const { w, s, nav } of wings) { | |
| w.scale.z = Math.max(.0001, t) * -s; w.rotation.x = s * (1 - t) * .4; w.visible = t > .01; | |
| nav.visible = t > .6 && (Math.sin(time * 6 + s) > -.2); | |
| } | |
| }, | |
| setReveal(t) { | |
| const x = t >= .999 ? 1e5 : -2.7 + 5.4 * t; // once built, never clip (the car drives far from the origin) | |
| solidClip.constant = x; wireClip.constant = -x; | |
| scan.position.x = x; scan.material.opacity = t > .01 && t < .99 ? .55 : 0; | |
| wire.visible = t < .999; | |
| }, | |
| setPaint(hex) { paint.color.set(hex); paintHex = hex; if (wear.burn) paint.color.lerp(new THREE.Color(0x141210), wear.burn); }, | |
| // damage you can see: dents pushed into the bodywork where the hit was, a cracking windscreen, scorched paint | |
| damage(local, power) { | |
| wear.total += power; | |
| for (const [mesh, kk] of [[bodyMesh, 1], [canMesh, .6]]) { | |
| const pos = mesh.geometry.attributes.position, r = .55 + Math.min(1.1, power * .025), depth = Math.min(.32, power * .006) * kk; | |
| let hit = false; | |
| for (let i = 0; i < pos.count; i++) { | |
| const x = pos.getX(i), y = pos.getY(i), z = pos.getZ(i), d = Math.hypot(x - local.x, (y - local.y) * 1.4, z - local.z); | |
| if (d > r) continue; | |
| // pushed in toward the middle of the car (and back from the nose or tail on a head-on hit) | |
| const f = (1 - d / r) ** 2 * depth, dy = .6 - y, dz = -z, l = Math.hypot(dy, dz) || 1, ax = Math.abs(local.x) > 1.5 ? -Math.sign(local.x) * 1.4 : 0; | |
| pos.setXYZ(i, x + ax * f, y + dy / l * f * .7, z + dz / l * f * 1.3); hit = true; | |
| } | |
| if (hit) { pos.needsUpdate = true; mesh.geometry.computeVertexNormals(); } | |
| } | |
| if (wear.total > 25 && !wear.cracked) { wear.cracked = true; glass.map = crackTex(); glass.needsUpdate = true; } | |
| wear.burn = Math.min(.85, Math.max(wear.burn, (wear.total - 60) / 160)); if (wear.burn > 0) { paint.color.set(paintHex).lerp(new THREE.Color(0x141210), wear.burn); paint.roughness = Math.max(paint.roughness, .3 + wear.burn * .6); } | |
| }, | |
| wreck() { wear.burn = .9; paint.color.set(0x161412); paint.roughness = .95; paint.clearcoat = 0; glass.opacity = .25; if (!wear.cracked) { wear.cracked = true; glass.map = crackTex(); glass.needsUpdate = true; } spoilers.visible = false; }, | |
| repair() { | |
| bodyMesh.geometry.attributes.position.array.set(bodyBase); bodyMesh.geometry.attributes.position.needsUpdate = true; bodyMesh.geometry.computeVertexNormals(); | |
| canMesh.geometry.attributes.position.array.set(canBase); canMesh.geometry.attributes.position.needsUpdate = true; canMesh.geometry.computeVertexNormals(); | |
| wear.total = 0; wear.burn = 0; wear.cracked = false; glass.map = null; glass.needsUpdate = true; paint.color.set(paintHex); spoilers.visible = true; | |
| }, | |
| get wear() { return wear; }, | |
| setFinish(f) { | |
| const F = { gloss: [.6, .32, 1, .12], satin: [.4, .5, .35, .06], matte: [.15, .8, 0, 0] }[f] || [.6, .32, 1, .12]; | |
| paint.metalness = F[0]; paint.roughness = F[1]; paint.clearcoat = F[2]; paint.normalScale.setScalar(F[3]); | |
| }, | |
| setAccent(hex) { caliper.color.set(hex); }, | |
| // brakes: the tail lights flare and the discs glow when you brake hard at speed | |
| setBrakes(k, heat) { rear.color.setRGB(2.6 * (1 + k * 1.2), .42 * (1 + k * 1.2), .58 * (1 + k)); disc.emissiveIntensity += ((heat || 0) * 2.2 - disc.emissiveIntensity) * .08; }, | |
| // the active wing: up above 110 km/h, air-brake tilt when braking hard; it stays stowed if a fixed spoiler is fitted | |
| setAero(kmh, braking, dt) { const A = aero.userData, on = !spoilers.children.length; aero.visible = on; if (!on) return; | |
| A.lift += ((kmh > 110 || braking > .5 && kmh > 60 ? 1 : 0) - A.lift) * Math.min(1, dt * 3); A.tilt += ((braking > .5 && kmh > 60 ? 1 : 0) - A.tilt) * Math.min(1, dt * 5); | |
| aeroWing.position.y = .03 + A.lift * .38; aeroWing.rotation.z = .08 + A.tilt * .95; aeroWing.visible = A.lift > .02; }, | |
| setTrim(hex) { trim.color.set(hex); }, | |
| setRim(hex) { rim.color.set(hex); }, | |
| setLight(hex) { front.color.set(hex).multiplyScalar(3); }, | |
| }; | |
| } | |