← Public packages
@kentcdodds/roadster
Unofficial fan game: drive a stylized thruster roadster - launch 0-60, boost, hover, flip and hop over traffic.
src/client/car.ts
195 lines · 6.2 KB · TypeScriptimport * as THREE from 'three'
export type CarModel = {
root: THREE.Group
body: THREE.Group
wheels: Array<THREE.Group>
frontWheels: Array<THREE.Group>
wing: THREE.Group
rearNozzles: Array<THREE.Object3D>
liftNozzles: Array<THREE.Object3D>
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<THREE.Object3D> = []
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<THREE.Object3D> = []
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<THREE.Group> = []
const frontWheels: Array<THREE.Group> = []
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 }
}