import * as THREE from 'three' export type CarModel = { root: THREE.Group body: THREE.Group wheels: Array frontWheels: Array wing: THREE.Group rearNozzles: Array liftNozzles: Array tailLight: THREE.MeshStandardMaterial } /** A generic, stylized wedge roadster built from primitives. Local forward is +Z. */ export function buildCar(): CarModel { const root = new THREE.Group() const body = new THREE.Group() root.add(body) const paint = new THREE.MeshPhysicalMaterial({ color: 0xf3f1ec, metalness: 0.15, roughness: 0.32, clearcoat: 1, clearcoatRoughness: 0.12, }) const dark = new THREE.MeshStandardMaterial({ color: 0x1b1c20, metalness: 0.4, roughness: 0.6 }) const titanium = new THREE.MeshStandardMaterial({ color: 0x9aa0a8, metalness: 1, roughness: 0.28 }) const glass = new THREE.MeshPhysicalMaterial({ color: 0xd9a441, metalness: 0.9, roughness: 0.08, clearcoat: 1, transparent: true, opacity: 0.92, }) // Side profile (x = length, y = height), extruded across the width. const profile = new THREE.Shape() const pts: Array<[number, number]> = [ [2.25, 0.3], [2.32, 0.46], [1.5, 0.66], [0.55, 0.86], [-0.9, 0.9], [-1.85, 0.8], [-2.22, 0.74], [-2.28, 0.42], [-2.1, 0.24], [2.0, 0.22], ] profile.moveTo(pts[0]![0], pts[0]![1]) for (const [x, y] of pts.slice(1)) profile.lineTo(x, y) profile.closePath() const width = 1.92 const shellGeo = new THREE.ExtrudeGeometry(profile, { depth: width - 0.24, bevelEnabled: true, bevelThickness: 0.12, bevelSize: 0.08, bevelSegments: 3, steps: 1, }) shellGeo.translate(0, 0, -(width - 0.24) / 2) shellGeo.rotateY(-Math.PI / 2) const shell = new THREE.Mesh(shellGeo, paint) shell.castShadow = true shell.receiveShadow = true body.add(shell) // Canopy with gold-tinted glass. const canopyShape = new THREE.Shape() canopyShape.moveTo(1.05, 0.7) canopyShape.quadraticCurveTo(0.1, 1.32, -1.1, 0.95) canopyShape.lineTo(-1.2, 0.82) canopyShape.lineTo(1.05, 0.7) const canopyGeo = new THREE.ExtrudeGeometry(canopyShape, { depth: 1.1, bevelEnabled: true, bevelThickness: 0.06, bevelSize: 0.06, bevelSegments: 3, }) canopyGeo.translate(0, 0, -0.55) canopyGeo.rotateY(-Math.PI / 2) const canopy = new THREE.Mesh(canopyGeo, glass) canopy.castShadow = true body.add(canopy) // Side intakes and titanium accents. for (const side of [-1, 1]) { const intake = new THREE.Mesh(new THREE.BoxGeometry(0.06, 0.18, 0.9), dark) intake.position.set(side * 1.0, 0.52, -0.7) body.add(intake) const trim = new THREE.Mesh(new THREE.BoxGeometry(0.03, 0.03, 3.6), titanium) trim.position.set(side * 1.0, 0.3, 0.1) body.add(trim) } // Concealed headlight slits. const headMat = new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0xdff4ff, emissiveIntensity: 2.2 }) for (const side of [-1, 1]) { const head = new THREE.Mesh(new THREE.BoxGeometry(0.55, 0.035, 0.05), headMat) head.position.set(side * 0.6, 0.47, 2.29) head.rotation.x = 0.25 body.add(head) } // Full-length tail light bar. const tailLight = new THREE.MeshStandardMaterial({ color: 0x330000, emissive: 0xff2a2a, emissiveIntensity: 1.6 }) const tail = new THREE.Mesh(new THREE.BoxGeometry(1.86, 0.05, 0.05), tailLight) tail.position.set(0, 0.66, -2.31) body.add(tail) // Diffuser. const diffuser = new THREE.Mesh(new THREE.BoxGeometry(1.6, 0.14, 0.4), dark) diffuser.position.set(0, 0.28, -2.1) body.add(diffuser) // Active rear wing (deploys in track mode). const wing = new THREE.Group() const blade = new THREE.Mesh(new THREE.BoxGeometry(1.9, 0.04, 0.42), paint) blade.castShadow = true blade.position.y = 0.36 wing.add(blade) for (const side of [-1, 1]) { const post = new THREE.Mesh(new THREE.BoxGeometry(0.05, 0.36, 0.14), titanium) post.position.set(side * 0.55, 0.18, 0) wing.add(post) } wing.position.set(0, 0.62, -1.85) body.add(wing) // Thruster nozzles: two rear plus eight lift nozzles around the car (ten total). const nozzleGeo = new THREE.CylinderGeometry(0.07, 0.11, 0.14, 12, 1, true) const nozzleMat = new THREE.MeshStandardMaterial({ color: 0x2a2b30, metalness: 0.9, roughness: 0.3, side: THREE.DoubleSide }) const rearNozzles: Array = [] for (const side of [-1, 1]) { const n = new THREE.Mesh(nozzleGeo, nozzleMat) n.rotation.x = Math.PI / 2 n.position.set(side * 0.38, 0.4, -2.36) body.add(n) rearNozzles.push(n) } const liftNozzles: Array = [] const liftSpots: Array<[number, number]> = [ [-0.85, 1.75], [0.85, 1.75], [-0.95, 0.6], [0.95, 0.6], [-0.95, -0.6], [0.95, -0.6], [-0.85, -1.75], [0.85, -1.75], ] for (const [x, z] of liftSpots) { const n = new THREE.Mesh(nozzleGeo, nozzleMat) n.position.set(x, 0.18, z) body.add(n) liftNozzles.push(n) } // Super-light wheels with thin spokes. const tireGeo = new THREE.CylinderGeometry(0.36, 0.36, 0.3, 28) tireGeo.rotateZ(Math.PI / 2) const tireMat = new THREE.MeshStandardMaterial({ color: 0x111114, roughness: 0.9 }) const rimMat = new THREE.MeshStandardMaterial({ color: 0xc9ccd2, metalness: 1, roughness: 0.22 }) const wheels: Array = [] const frontWheels: Array = [] for (const [x, z] of [[-0.9, 1.45], [0.9, 1.45], [-0.92, -1.45], [0.92, -1.45]] as const) { const steer = new THREE.Group() steer.position.set(x, 0.36, z) const spin = new THREE.Group() steer.add(spin) const tire = new THREE.Mesh(tireGeo, tireMat) tire.castShadow = true spin.add(tire) const side = Math.sign(x) const hub = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.06, 0.04, 12).rotateZ(Math.PI / 2), rimMat) hub.position.x = side * 0.16 spin.add(hub) for (let i = 0; i < 10; i++) { const spoke = new THREE.Mesh(new THREE.BoxGeometry(0.015, 0.29, 0.025), rimMat) spoke.position.set(side * 0.155, 0, 0) const pivot = new THREE.Group() pivot.rotation.x = (i / 10) * Math.PI * 2 spoke.position.y = 0.14 pivot.add(spoke) spin.add(pivot) } const rimRing = new THREE.Mesh(new THREE.TorusGeometry(0.28, 0.02, 6, 32).rotateY(Math.PI / 2), rimMat) rimRing.position.x = side * 0.155 spin.add(rimRing) root.add(steer) wheels.push(spin) if (z > 0) frontWheels.push(steer) } body.position.y = 0.04 return { root, body, wheels, frontWheels, wing, rearNozzles, liftNozzles, tailLight } }