import * as THREE from 'three' export const LAYOUT = { runway: { x: 0, z0: -60, z1: 2500, halfWidth: 22 }, launchBox: { x: 0, z0: 8, z1: 22, halfWidth: 4 }, lineup: { x: -95, z: [300, 314, 328, 342, 356], halfLane: 11 }, ramp: { x: 95, z: 220, radius: 12, halfWidth: 4.5, maxAngle: (160 * Math.PI) / 180 }, canyon: { x0: -270, x1: -130, z0: 720, z1: 840, depth: 70 }, kicker: { x: -200, z0: 660, z1: 720, height: 7, halfWidth: 9 }, } export type Obstacle = { min: THREE.Vector3; max: THREE.Vector3 } /** Ground height at x/z. Returns -Infinity over the canyon. */ export function groundHeight(x: number, z: number): number { const c = LAYOUT.canyon if (x > c.x0 && x < c.x1 && z > c.z0 && z < c.z1) return -Infinity const k = LAYOUT.kicker if (Math.abs(x - k.x) < k.halfWidth && z > k.z0 && z < k.z1) { const t = (z - k.z0) / (k.z1 - k.z0) return k.height * t * t * 0.4 + k.height * t * 0.6 } return 0 } export function inCanyonFootprint(x: number, z: number) { const c = LAYOUT.canyon return x > c.x0 && x < c.x1 && z > c.z0 && z < c.z1 } function canvasTexture(w: number, h: number, draw: (g: CanvasRenderingContext2D) => void) { const c = document.createElement('canvas') c.width = w c.height = h const g = c.getContext('2d')! draw(g) const tex = new THREE.CanvasTexture(c) tex.colorSpace = THREE.SRGBColorSpace tex.anisotropy = 8 return tex } function label(text: string, color = '#e9c46a') { const tex = canvasTexture(512, 128, (g) => { g.fillStyle = 'rgba(10,10,20,0.75)' g.beginPath() g.roundRect(4, 4, 504, 120, 24) g.fill() g.strokeStyle = color g.lineWidth = 6 g.stroke() g.fillStyle = '#fff' g.font = '900 56px system-ui, sans-serif' g.textAlign = 'center' g.textBaseline = 'middle' g.fillText(text, 256, 68) }) const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: tex, transparent: true, depthWrite: false })) sprite.scale.set(16, 4, 1) return sprite } export function buildWorld(scene: THREE.Scene, lowGpu = false) { const obstacles: Array = [] // Sky dome with a dusk gradient. const skyGeo = new THREE.SphereGeometry(4000, lowGpu ? 16 : 32, lowGpu ? 8 : 16) const skyMat = new THREE.ShaderMaterial({ side: THREE.BackSide, depthWrite: false, uniforms: {}, vertexShader: 'varying vec3 vP; void main(){ vP = normalize(position); gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }', fragmentShader: `varying vec3 vP; void main(){ float h = vP.y; vec3 horizon = vec3(1.0,0.55,0.32); vec3 mid = vec3(0.42,0.27,0.55); vec3 top = vec3(0.04,0.05,0.16); vec3 c = mix(horizon, mid, smoothstep(0.0,0.18,h)); c = mix(c, top, smoothstep(0.15,0.6,h)); c = mix(vec3(0.18,0.12,0.12), c, smoothstep(-0.05,0.0,h)); float sun = pow(max(dot(normalize(vP), normalize(vec3(-0.6,0.08,1.0))),0.0), 220.0); gl_FragColor = vec4(c + vec3(1.0,0.75,0.45)*sun*1.4, 1.0); }`, }) scene.add(new THREE.Mesh(skyGeo, skyMat)) scene.fog = new THREE.Fog(0x8a5a6a, 300, 2600) // Lighting. scene.add(new THREE.HemisphereLight(0xffd7c0, 0x3a2a30, 1.1)) const sun = new THREE.DirectionalLight(0xffc89a, 2.4) sun.position.set(-60, 90, 120) sun.castShadow = !lowGpu sun.shadow.mapSize.set(2048, 2048) const sc = sun.shadow.camera sc.left = -40 sc.right = 40 sc.top = 40 sc.bottom = -40 sc.near = 1 sc.far = 400 sun.shadow.bias = -0.0005 scene.add(sun) scene.add(sun.target) // Desert ground with a hole for the canyon. const groundTex = canvasTexture(lowGpu ? 256 : 512, lowGpu ? 256 : 512, (g) => { g.fillStyle = '#9b7a5c' g.fillRect(0, 0, 512, 512) for (let i = 0; i < 9000; i++) { const v = Math.random() g.fillStyle = v > 0.5 ? 'rgba(255,230,200,0.06)' : 'rgba(60,35,20,0.08)' g.fillRect(Math.random() * 512, Math.random() * 512, 2 + Math.random() * 4, 2 + Math.random() * 4) } }) groundTex.wrapS = groundTex.wrapT = THREE.RepeatWrapping groundTex.repeat.set(300, 300) const S = 3500 const shape = new THREE.Shape([ new THREE.Vector2(-S, -S), new THREE.Vector2(S, -S), new THREE.Vector2(S, S), new THREE.Vector2(-S, S), ]) const c = LAYOUT.canyon // Shape lives in x/y; after rotating -90deg about X, world z = -y. shape.holes.push(new THREE.Path([ new THREE.Vector2(c.x0, -c.z0), new THREE.Vector2(c.x0, -c.z1), new THREE.Vector2(c.x1, -c.z1), new THREE.Vector2(c.x1, -c.z0), ])) const groundGeo = new THREE.ShapeGeometry(shape) groundGeo.rotateX(-Math.PI / 2) const uv = groundGeo.attributes.uv as THREE.BufferAttribute const pos = groundGeo.attributes.position as THREE.BufferAttribute for (let i = 0; i < uv.count; i++) uv.setXY(i, (pos.getX(i) + S) / (2 * S), (pos.getZ(i) + S) / (2 * S)) const ground = new THREE.Mesh(groundGeo, new THREE.MeshStandardMaterial({ map: groundTex, roughness: 1 })) ground.receiveShadow = true scene.add(ground) // Canyon walls, floor and river. const rock = new THREE.MeshStandardMaterial({ color: 0x7a4a33, roughness: 1 }) const D = c.depth const wallSpecs: Array<[number, number, number, number]> = [ [(c.x0 + c.x1) / 2, c.z0, c.x1 - c.x0, 1], [(c.x0 + c.x1) / 2, c.z1, c.x1 - c.x0, 1], [c.x0, (c.z0 + c.z1) / 2, 1, c.z1 - c.z0], [c.x1, (c.z0 + c.z1) / 2, 1, c.z1 - c.z0], ] for (const [x, z, w, d] of wallSpecs) { const wall = new THREE.Mesh(new THREE.BoxGeometry(w, D, d), rock) wall.position.set(x, -D / 2, z) scene.add(wall) } const floor = new THREE.Mesh(new THREE.PlaneGeometry(c.x1 - c.x0, c.z1 - c.z0).rotateX(-Math.PI / 2), rock) floor.position.set((c.x0 + c.x1) / 2, -D, (c.z0 + c.z1) / 2) scene.add(floor) const river = new THREE.Mesh( new THREE.PlaneGeometry(c.x1 - c.x0, 18).rotateX(-Math.PI / 2), new THREE.MeshStandardMaterial({ color: 0x3b7fa8, roughness: 0.2, metalness: 0.3, emissive: 0x0a2233 }), ) river.position.set((c.x0 + c.x1) / 2, -D + 0.2, (c.z0 + c.z1) / 2) scene.add(river) // Runway. const r = LAYOUT.runway const runwayTex = canvasTexture(256, 1024, (g) => { g.fillStyle = '#2b2b30' g.fillRect(0, 0, 256, 1024) for (let i = 0; i < 4000; i++) { g.fillStyle = `rgba(255,255,255,${Math.random() * 0.04})` g.fillRect(Math.random() * 256, Math.random() * 1024, 2, 2) } g.fillStyle = '#f2f2f2' g.fillRect(6, 0, 6, 1024) g.fillRect(244, 0, 6, 1024) g.fillStyle = '#e9c46a' for (let y = 0; y < 1024; y += 128) g.fillRect(124, y, 8, 64) }) runwayTex.wrapS = runwayTex.wrapT = THREE.RepeatWrapping const len = r.z1 - r.z0 runwayTex.repeat.set(1, len / 60) const runway = new THREE.Mesh( new THREE.PlaneGeometry(r.halfWidth * 2, len).rotateX(-Math.PI / 2), new THREE.MeshStandardMaterial({ map: runwayTex, roughness: 0.85 }), ) runway.position.set(r.x, 0.02, (r.z0 + r.z1) / 2) runway.receiveShadow = true scene.add(runway) // Pads under each challenge area. const padMat = new THREE.MeshStandardMaterial({ color: 0x34343a, roughness: 0.9 }) const pad = (x: number, z: number, w: number, d: number) => { const m = new THREE.Mesh(new THREE.PlaneGeometry(w, d).rotateX(-Math.PI / 2), padMat) m.position.set(x, 0.015, z) m.receiveShadow = true scene.add(m) } pad(LAYOUT.lineup.x, 300, 26, 360) pad(LAYOUT.ramp.x, 160, 14, 130) pad(LAYOUT.kicker.x, 560, 20, 200) pad(LAYOUT.kicker.x, 920, 30, 160) // Launch box. const lb = LAYOUT.launchBox const boxMat = new THREE.MeshBasicMaterial({ color: 0xe9c46a }) for (const z of [lb.z0, lb.z1]) { const line = new THREE.Mesh(new THREE.PlaneGeometry(lb.halfWidth * 2, 0.4).rotateX(-Math.PI / 2), boxMat) line.position.set(lb.x, 0.04, z) scene.add(line) } for (const x of [-lb.halfWidth, lb.halfWidth]) { const line = new THREE.Mesh(new THREE.PlaneGeometry(0.4, lb.z1 - lb.z0).rotateX(-Math.PI / 2), boxMat) line.position.set(lb.x + x, 0.04, (lb.z0 + lb.z1) / 2) scene.add(line) } // Start gantry with three lights. const gantry = new THREE.Group() const steel = new THREE.MeshStandardMaterial({ color: 0x55565c, metalness: 0.8, roughness: 0.4 }) for (const x of [-9, 9]) { const post = new THREE.Mesh(new THREE.BoxGeometry(0.4, 7, 0.4), steel) post.position.set(x, 3.5, 0) gantry.add(post) } const beam = new THREE.Mesh(new THREE.BoxGeometry(18.4, 0.8, 0.6), steel) beam.position.y = 7 gantry.add(beam) const lightMats: Array = [] for (let i = 0; i < 3; i++) { const m = new THREE.MeshStandardMaterial({ color: 0x220000, emissive: 0x000000 }) const bulb = new THREE.Mesh(new THREE.SphereGeometry(0.35, 16, 8), m) bulb.position.set(-1.2 + i * 1.2, 7, -0.4) gantry.add(bulb) lightMats.push(m) } gantry.position.set(0, 0, lb.z1 + 6) scene.add(gantry) // Distance boards along the runway, every 250 m. for (let z = 250; z < r.z1; z += 250) { for (const side of [-1, 1]) { const board = label(`${z} m`, '#9ee7ff') board.scale.set(8, 2, 1) board.position.set(side * (r.halfWidth + 6), 3, z) scene.add(board) } } // 60 mph marker style edge lights. const edgeGeo = new THREE.SphereGeometry(0.18, 8, 6) const edgeMat = new THREE.MeshBasicMaterial({ color: 0xffe6a8 }) const edgeCount = Math.floor(len / 30) * 2 const edges = new THREE.InstancedMesh(edgeGeo, edgeMat, edgeCount) const m4 = new THREE.Matrix4() let ei = 0 for (let z = r.z0; z < r.z1 && ei < edgeCount; z += 30) { for (const side of [-1, 1]) { if (ei >= edgeCount) break m4.makeTranslation(side * (r.halfWidth + 0.6), 0.25, z) edges.setMatrixAt(ei++, m4) } } scene.add(edges) // Traffic lineup: five generic parked vehicles across the lane. const lu = LAYOUT.lineup const vehicleSpecs: Array<{ len: number; h: number; color: number; kind: 'pod' | 'wedge' | 'cab' | 'suv' | 'sedan' }> = [ { len: 4.2, h: 1.5, color: 0xc8b46a, kind: 'pod' }, { len: 5.7, h: 1.9, color: 0xa8adb4, kind: 'wedge' }, { len: 7.2, h: 3.8, color: 0xe8e8e8, kind: 'cab' }, { len: 4.8, h: 1.65, color: 0x2d5fa8, kind: 'suv' }, { len: 4.7, h: 1.45, color: 0xb02a2a, kind: 'sedan' }, ] vehicleSpecs.forEach((spec, i) => { const z = lu.z[i]! const v = buildVehicle(spec.len, spec.h, spec.color, spec.kind) v.position.set(lu.x, 0, z) v.rotation.y = Math.PI / 2 scene.add(v) obstacles.push({ min: new THREE.Vector3(lu.x - spec.len / 2, 0, z - 1.0), max: new THREE.Vector3(lu.x + spec.len / 2, spec.h, z + 1.0), }) }) // Inverted ramp: a curl that tips past vertical. const rp = LAYOUT.ramp const segs = 48 const verts: Array = [] const idx: Array = [] for (let i = 0; i <= segs; i++) { const a = (i / segs) * rp.maxAngle const y = rp.radius * (1 - Math.cos(a)) const z = rp.z + rp.radius * Math.sin(a) const ny = Math.cos(a) const nz = -Math.sin(a) // Offset slightly along the normal so the car sits on the surface. verts.push(rp.x - rp.halfWidth, y - ny * 0.05, z - nz * 0.05, rp.x + rp.halfWidth, y - ny * 0.05, z - nz * 0.05) if (i < segs) { const b = i * 2 idx.push(b, b + 2, b + 1, b + 1, b + 2, b + 3) } } const rampGeo = new THREE.BufferGeometry() rampGeo.setAttribute('position', new THREE.Float32BufferAttribute(verts, 3)) rampGeo.setIndex(idx) rampGeo.computeVertexNormals() const rampMat = new THREE.MeshStandardMaterial({ color: 0xdedbd2, roughness: 0.6, metalness: 0.2, side: THREE.DoubleSide }) const ramp = new THREE.Mesh(rampGeo, rampMat) ramp.castShadow = true ramp.receiveShadow = true scene.add(ramp) // Rails and trusses. const railMat = new THREE.MeshStandardMaterial({ color: 0xe9c46a, emissive: 0x3a2a00, metalness: 0.6, roughness: 0.4 }) for (const side of [-1, 1]) { const curve = new THREE.CatmullRomCurve3( Array.from({ length: 25 }, (_, i) => { const a = (i / 24) * rp.maxAngle return new THREE.Vector3(rp.x + side * (rp.halfWidth + 0.2), rp.radius * (1 - Math.cos(a)) + 0.4 * Math.cos(a), rp.z + rp.radius * Math.sin(a) - 0.4 * Math.sin(a)) }), ) scene.add(new THREE.Mesh(new THREE.TubeGeometry(curve, 64, 0.15, 8), railMat)) for (let i = 2; i <= 24; i += 3) { const a = (i / 24) * rp.maxAngle const y = rp.radius * (1 - Math.cos(a)) const z = rp.z + rp.radius * Math.sin(a) const strut = new THREE.Mesh(new THREE.BoxGeometry(0.3, y, 0.3), steel) strut.position.set(rp.x + side * (rp.halfWidth + 0.3), y / 2, z + 0.3) scene.add(strut) } } // Canyon kicker. const k = LAYOUT.kicker const kSegs = 24 const kv: Array = [] const ki: Array = [] for (let i = 0; i <= kSegs; i++) { const z = k.z0 + ((k.z1 - k.z0) * i) / kSegs const y = groundHeight(k.x, z - 0.001) kv.push(k.x - k.halfWidth, y, z, k.x + k.halfWidth, y, z, k.x - k.halfWidth, 0, z, k.x + k.halfWidth, 0, z) if (i < kSegs) { const b = i * 4 ki.push(b, b + 4, b + 1, b + 1, b + 4, b + 5) // top ki.push(b, b + 2, b + 4, b + 4, b + 2, b + 6) // left side ki.push(b + 1, b + 5, b + 3, b + 3, b + 5, b + 7) // right side } } const last = kSegs * 4 ki.push(last, last + 1, last + 2, last + 2, last + 1, last + 3) const kGeo = new THREE.BufferGeometry() kGeo.setAttribute('position', new THREE.Float32BufferAttribute(kv, 3)) kGeo.setIndex(ki) kGeo.computeVertexNormals() const kicker = new THREE.Mesh(kGeo, new THREE.MeshStandardMaterial({ color: 0xd8d2c4, roughness: 0.7, side: THREE.DoubleSide })) kicker.castShadow = true kicker.receiveShadow = true scene.add(kicker) // Signs. const signs: Array<[string, number, number, number]> = [ ['LAUNCH BOX', 0, 12, 2], ['TRAFFIC HOP', lu.x, 10, 270], ['INVERTED RAMP', rp.x, 30, 205], ['CANYON JUMP', k.x, 14, 630], ] for (const [text, x, y, z] of signs) { const s = label(text) s.position.set(x, y, z) scene.add(s) } // Distant mesas and light towers for depth. const mesaMat = new THREE.MeshStandardMaterial({ color: 0x6b4336, roughness: 1, flatShading: true }) for (let i = 0; i < 40; i++) { const a = (i / 40) * Math.PI * 2 + Math.random() * 0.1 const d = 1400 + Math.random() * 900 const h = 40 + Math.random() * 160 const m = new THREE.Mesh(new THREE.CylinderGeometry(60 + Math.random() * 80, 120 + Math.random() * 140, h, 7), mesaMat) m.position.set(Math.cos(a) * d, h / 2 - 2, 600 + Math.sin(a) * d) scene.add(m) } const towerTop = new THREE.MeshBasicMaterial({ color: 0xfff1c9 }) for (const [x, z] of [[-40, 60], [40, 60], [-60, 330], [140, 250], [-240, 640], [-150, 900]] as const) { const t = new THREE.Mesh(new THREE.BoxGeometry(0.6, 22, 0.6), steel) t.position.set(x, 11, z) scene.add(t) const head = new THREE.Mesh(new THREE.BoxGeometry(3, 1.2, 1), towerTop) head.position.set(x, 22.5, z) scene.add(head) } return { sun, obstacles, lightMats } } function buildVehicle(len: number, h: number, color: number, kind: string) { const g = new THREE.Group() const paint = new THREE.MeshStandardMaterial({ color, metalness: 0.5, roughness: 0.35 }) const glass = new THREE.MeshStandardMaterial({ color: 0x15202a, metalness: 0.8, roughness: 0.1 }) const tire = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.9 }) const w = kind === 'cab' ? 2.5 : 1.95 const add = (m: THREE.Mesh) => { m.castShadow = true m.receiveShadow = true g.add(m) return m } if (kind === 'cab') { add(new THREE.Mesh(new THREE.BoxGeometry(w, h - 0.8, 3.2), paint)).position.set(0, (h - 0.8) / 2 + 0.6, len / 2 - 1.6) add(new THREE.Mesh(new THREE.BoxGeometry(w * 0.98, 0.9, 2.2), glass)).position.set(0, h - 1.2, len / 2 - 0.4) add(new THREE.Mesh(new THREE.BoxGeometry(w * 0.8, 0.5, len - 3.2), paint)).position.set(0, 1.0, -1.6) } else if (kind === 'wedge') { const s = new THREE.Shape() s.moveTo(-len / 2, 0.4) s.lineTo(len / 2, 0.4) s.lineTo(len / 2, 1.0) s.lineTo(0, h) s.lineTo(-len / 2, 1.3) s.closePath() const geo = new THREE.ExtrudeGeometry(s, { depth: w, bevelEnabled: false }) geo.translate(0, 0, -w / 2) geo.rotateY(-Math.PI / 2) add(new THREE.Mesh(geo, paint)) } else { const bodyH = kind === 'pod' ? h * 0.55 : h * 0.5 add(new THREE.Mesh(new THREE.BoxGeometry(w, bodyH, len), paint)).position.y = bodyH / 2 + 0.3 const cabinLen = kind === 'pod' ? len * 0.55 : len * 0.5 add(new THREE.Mesh(new THREE.BoxGeometry(w * 0.9, h - bodyH - 0.3, cabinLen), glass)).position.set(0, bodyH + 0.3 + (h - bodyH - 0.3) / 2, kind === 'suv' ? -0.3 : 0) } for (const x of [-w / 2, w / 2]) { for (const z of [len / 2 - 0.9, -len / 2 + 0.9]) { const t = add(new THREE.Mesh(new THREE.CylinderGeometry(0.38, 0.38, 0.28, 16).rotateZ(Math.PI / 2), tire)) t.position.set(x, 0.38, z) } } return g }