🎮ArcadeLab

3D Car Racing - Three.js

by ApexWizard89
676 lines20.9 KB🛠️ Three.js (3D graphics)
▶ Play
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Car Racing - Three.js</title>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body { overflow: hidden; background: #111; font-family: 'Segoe UI', Arial, sans-serif; color: #fff; }
  #ui {
    position: absolute; top: 0; left: 0; width: 100%; height: 100%;
    pointer-events: none; z-index: 20;
  }
  #speedo {
    position: absolute; bottom: 40px; right: 40px;
    width: 160px; height: 160px; border-radius: 50%;
    background: radial-gradient(circle, #1a1a1a 60%, #000);
    border: 6px solid #333; display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    box-shadow: 0 0 30px rgba(0,0,0,0.8);
  }
  #speed-val {
    font-size: 48px; font-weight: 800; color: #0f0;
    text-shadow: 0 0 10px #0f0;
  }
  #speed-unit { font-size: 14px; color: #aaa; margin-top: -5px; }
  #gear {
    position: absolute; bottom: 50px; left: 40px;
    font-size: 36px; font-weight: bold; color: #ff0;
    text-shadow: 0 0 8px #ff0;
  }
  #lap-info {
    position: absolute; top: 25px; left: 30px;
    font-size: 22px; line-height: 1.6;
    background: rgba(0,0,0,0.55); padding: 12px 20px; border-radius: 10px;
    border: 1px solid #444;
  }
  #position {
    position: absolute; top: 25px; right: 30px;
    font-size: 28px; font-weight: bold; color: #0ff;
    background: rgba(0,0,0,0.55); padding: 10px 18px; border-radius: 10px;
  }
  #countdown {
    position: absolute; top: 40%; left: 50%; transform: translate(-50%,-50%);
    font-size: 120px; font-weight: 900; color: #ff0;
    text-shadow: 0 0 40px #ff0; opacity: 0; transition: opacity 0.2s;
  }
  #message {
    position: absolute; top: 55%; left: 50%; transform: translate(-50%,-50%);
    font-size: 36px; font-weight: bold; color: #fff;
    text-shadow: 0 0 15px #000; opacity: 0; transition: opacity 0.3s;
  }
  #minimap {
    position: absolute; bottom: 40px; left: 40px;
    width: 180px; height: 180px; background: rgba(0,0,0,0.7);
    border: 3px solid #555; border-radius: 8px; overflow: hidden;
  }
  #controls-hint {
    position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
    text-align: center; background: rgba(0,0,0,0.85); padding: 40px 60px;
    border-radius: 16px; border: 2px solid #0f0; z-index: 50; cursor: pointer;
  }
  #controls-hint h1 { font-size: 42px; color: #0f0; margin-bottom: 20px; }
  #controls-hint p { font-size: 18px; margin: 8px 0; color: #ccc; }
</style>
</head>
<body>
<div id="controls-hint">
  <h1>3D CAR RACING</h1>
  <p>Arrow Keys / WASD – Drive</p>
  <p>Space – Handbrake</p>
  <p>Click to Start</p>
</div>

<div id="ui" style="display:none;">
  <div id="lap-info">
    <div>LAP: <span id="lap">1</span> / 3</div>
    <div>TIME: <span id="time">00:00.0</span></div>
    <div>BEST: <span id="best">--:--.-</span></div>
  </div>
  <div id="position">P1</div>
  <div id="speedo">
    <div id="speed-val">0</div>
    <div id="speed-unit">km/h</div>
  </div>
  <div id="gear">N</div>
  <div id="countdown">3</div>
  <div id="message"></div>
  <canvas id="minimap" width="180" height="180"></canvas>
</div>

<script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
  }
}
</script>
<script type="module">
import * as THREE from 'three';

// ==================== AUDIO ====================
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function engineSound(speed) {
  // simple continuous tone will be handled in loop
}
function playBeep(freq, dur) {
  if (audioCtx.state === 'suspended') audioCtx.resume();
  const o = audioCtx.createOscillator();
  const g = audioCtx.createGain();
  o.frequency.value = freq;
  o.type = 'square';
  g.gain.setValueAtTime(0.15, audioCtx.currentTime);
  g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + dur);
  o.connect(g); g.connect(audioCtx.destination);
  o.start(); o.stop(audioCtx.currentTime + dur);
}

// ==================== SCENE ====================
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
scene.fog = new THREE.FogExp2(0x87CEEB, 0.012);

const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 500);
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: "high-performance" });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.1;
document.body.appendChild(renderer.domElement);

// Lights
const ambient = new THREE.AmbientLight(0xffffff, 0.45);
scene.add(ambient);
const sun = new THREE.DirectionalLight(0xfff4e0, 1.15);
sun.position.set(80, 120, 60);
sun.castShadow = true;
sun.shadow.mapSize.set(2048, 2048);
sun.shadow.camera.near = 10;
sun.shadow.camera.far = 300;
sun.shadow.camera.left = -120;
sun.shadow.camera.right = 120;
sun.shadow.camera.top = 120;
sun.shadow.camera.bottom = -120;
sun.shadow.bias = -0.0003;
scene.add(sun);

// Hemisphere for nicer sky light
const hemi = new THREE.HemisphereLight(0x87CEEB, 0x445522, 0.35);
scene.add(hemi);

// ==================== TRACK ====================
const trackPoints = [];
const trackWidth = 14;
const segments = 80;

// Create a nice racing circuit (figure-8 inspired + long straights)
for (let i = 0; i <= segments; i++) {
  const t = (i / segments) * Math.PI * 2;
  const x = Math.sin(t) * 70 + Math.sin(t * 2) * 18;
  const z = Math.cos(t) * 50 + Math.cos(t * 3) * 8;
  trackPoints.push(new THREE.Vector3(x, 0, z));
}

// Road geometry
const roadShape = new THREE.Shape();
roadShape.moveTo(-trackWidth/2, 0);
roadShape.lineTo(trackWidth/2, 0);
roadShape.lineTo(trackWidth/2, 0.1);
roadShape.lineTo(-trackWidth/2, 0.1);
roadShape.closePath();

const roadGeo = new THREE.ExtrudeGeometry(roadShape, {
  steps: segments,
  bevelEnabled: false,
  extrudePath: new THREE.CatmullRomCurve3(trackPoints, true)
});
const roadMat = new THREE.MeshStandardMaterial({
  color: 0x333333,
  roughness: 0.75,
  metalness: 0.1
});
const road = new THREE.Mesh(roadGeo, roadMat);
road.receiveShadow = true;
scene.add(road);

// Center line (dashed)
const centerLineMat = new THREE.MeshBasicMaterial({ color: 0xffff00 });
for (let i = 0; i < segments; i += 2) {
  const p1 = trackPoints[i];
  const p2 = trackPoints[(i+1) % segments];
  const mid = p1.clone().lerp(p2, 0.5);
  const dir = p2.clone().sub(p1).normalize();
  const len = p1.distanceTo(p2) * 0.6;
  const line = new THREE.Mesh(
    new THREE.BoxGeometry(0.35, 0.05, len),
    centerLineMat
  );
  line.position.copy(mid);
  line.position.y = 0.12;
  line.lookAt(p2);
  scene.add(line);
}

// Edge lines
const edgeMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
function addEdge(offset) {
  for (let i = 0; i < segments; i++) {
    const p = trackPoints[i];
    const next = trackPoints[(i+1)%segments];
    const dir = next.clone().sub(p).normalize();
    const side = new THREE.Vector3(-dir.z, 0, dir.x).multiplyScalar(offset);
    const pos = p.clone().add(side);
    const edge = new THREE.Mesh(new THREE.BoxGeometry(0.4, 0.08, 2.2), edgeMat);
    edge.position.copy(pos);
    edge.position.y = 0.1;
    edge.lookAt(next.clone().add(side));
    scene.add(edge);
  }
}
addEdge(trackWidth/2 - 0.3);
addEdge(-(trackWidth/2 - 0.3));

// Barriers
const barrierMat = new THREE.MeshStandardMaterial({ color: 0xcc2222, roughness: 0.6 });
const barrierMat2 = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.6 });
for (let i = 0; i < segments; i += 1) {
  const p = trackPoints[i];
  const next = trackPoints[(i+1)%segments];
  const dir = next.clone().sub(p).normalize();
  const side = new THREE.Vector3(-dir.z, 0, dir.x);
  [-1, 1].forEach((s, idx) => {
    const pos = p.clone().add(side.clone().multiplyScalar(s * (trackWidth/2 + 1.2)));
    const bar = new THREE.Mesh(new THREE.BoxGeometry(1.8, 1.1, 2.5), idx === 0 ? barrierMat : barrierMat2);
    bar.position.copy(pos);
    bar.position.y = 0.55;
    bar.lookAt(next.clone().add(side.clone().multiplyScalar(s * (trackWidth/2 + 1.2))));
    bar.castShadow = true;
    scene.add(bar);
  });
}

// Grass ground
const grassGeo = new THREE.PlaneGeometry(400, 400);
const grassMat = new THREE.MeshStandardMaterial({ color: 0x3a7d2c, roughness: 0.95 });
const grass = new THREE.Mesh(grassGeo, grassMat);
grass.rotation.x = -Math.PI/2;
grass.position.y = -0.05;
grass.receiveShadow = true;
scene.add(grass);

// Trees
function createTree(x, z) {
  const group = new THREE.Group();
  const trunk = new THREE.Mesh(
    new THREE.CylinderGeometry(0.4, 0.55, 3.5, 6),
    new THREE.MeshStandardMaterial({ color: 0x5c3a21 })
  );
  trunk.position.y = 1.75;
  trunk.castShadow = true;
  group.add(trunk);
  const leaves = new THREE.Mesh(
    new THREE.ConeGeometry(2.8, 5, 7),
    new THREE.MeshStandardMaterial({ color: 0x1e6b2e })
  );
  leaves.position.y = 5.2;
  leaves.castShadow = true;
  group.add(leaves);
  group.position.set(x, 0, z);
  scene.add(group);
}
for (let i = 0; i < 45; i++) {
  const ang = Math.random() * Math.PI * 2;
  const r = 95 + Math.random() * 60;
  createTree(Math.cos(ang)*r, Math.sin(ang)*r);
}

// Start/Finish banner
const banner = new THREE.Mesh(
  new THREE.BoxGeometry(trackWidth + 4, 0.4, 0.4),
  new THREE.MeshStandardMaterial({ color: 0xff0000 })
);
banner.position.copy(trackPoints[0]);
banner.position.y = 4.5;
scene.add(banner);
const poleL = new THREE.Mesh(new THREE.CylinderGeometry(0.15,0.15,5,8), new THREE.MeshStandardMaterial({color:0x333}));
poleL.position.set(trackPoints[0].x - trackWidth/2 - 1.5, 2.5, trackPoints[0].z);
scene.add(poleL);
const poleR = poleL.clone();
poleR.position.x = trackPoints[0].x + trackWidth/2 + 1.5;
scene.add(poleR);

// ==================== CARS ====================
function createCar(color, isPlayer = false) {
  const group = new THREE.Group();

  // Body
  const bodyMat = new THREE.MeshStandardMaterial({ color, metalness: 0.6, roughness: 0.35 });
  const body = new THREE.Mesh(new THREE.BoxGeometry(2.1, 0.55, 4.4), bodyMat);
  body.position.y = 0.55;
  body.castShadow = true;
  group.add(body);

  // Cabin
  const cabin = new THREE.Mesh(
    new THREE.BoxGeometry(1.7, 0.55, 2.0),
    new THREE.MeshStandardMaterial({ color: 0x111111, metalness: 0.4, roughness: 0.3 })
  );
  cabin.position.set(0, 1.05, -0.3);
  cabin.castShadow = true;
  group.add(cabin);

  // Windows
  const glassMat = new THREE.MeshStandardMaterial({ color: 0x88aacc, transparent: true, opacity: 0.5, metalness: 0.9, roughness: 0.1 });
  const windshield = new THREE.Mesh(new THREE.BoxGeometry(1.55, 0.45, 0.1), glassMat);
  windshield.position.set(0, 1.1, 0.7);
  group.add(windshield);

  // Wheels
  const wheelMat = new THREE.MeshStandardMaterial({ color: 0x111, roughness: 0.8 });
  const wheelGeo = new THREE.CylinderGeometry(0.42, 0.42, 0.35, 12);
  const positions = [
    [-1.05, 0.42, 1.4], [1.05, 0.42, 1.4],
    [-1.05, 0.42, -1.4], [1.05, 0.42, -1.4]
  ];
  positions.forEach(pos => {
    const w = new THREE.Mesh(wheelGeo, wheelMat);
    w.rotation.z = Math.PI/2;
    w.position.set(...pos);
    w.castShadow = true;
    group.add(w);
  });

  // Spoiler for player
  if (isPlayer) {
    const spoiler = new THREE.Mesh(
      new THREE.BoxGeometry(1.9, 0.12, 0.5),
      new THREE.MeshStandardMaterial({ color: 0x222 })
    );
    spoiler.position.set(0, 1.15, -2.1);
    group.add(spoiler);
  }

  // Headlights
  const lightMat = new THREE.MeshStandardMaterial({ color: 0xffffee, emissive: 0xffffaa, emissiveIntensity: 0.6 });
  [-0.7, 0.7].forEach(x => {
    const hl = new THREE.Mesh(new THREE.BoxGeometry(0.35, 0.2, 0.15), lightMat);
    hl.position.set(x, 0.55, 2.2);
    group.add(hl);
  });

  group.castShadow = true;
  return group;
}

const playerCar = createCar(0xe63946, true);
scene.add(playerCar);

const aiCars = [];
const aiColors = [0x2a9d8f, 0xe9c46a, 0x264653, 0xf4a261];
for (let i = 0; i < 4; i++) {
  const car = createCar(aiColors[i]);
  scene.add(car);
  aiCars.push({
    mesh: car,
    progress: 0.05 + i * 0.06,
    speed: 0,
    maxSpeed: 38 + Math.random() * 8,
    offset: (Math.random() - 0.5) * 4
  });
}

// ==================== PLAYER PHYSICS ====================
const player = {
  progress: 0,          // 0 to 1 around track
  speed: 0,
  maxSpeed: 52,
  accel: 28,
  brake: 45,
  friction: 12,
  turnSpeed: 0,
  steering: 0,
  lateral: 0,           // side offset from center
  maxLateral: trackWidth/2 - 1.3,
  lap: 1,
  totalLaps: 3,
  finished: false,
  startTime: 0,
  bestTime: null,
  gear: 'N'
};

const keys = {};
window.addEventListener('keydown', e => { keys[e.code] = true; });
window.addEventListener('keyup', e => { keys[e.code] = false; });

// ==================== TRACK HELPERS ====================
function getPointAt(t) {
  t = ((t % 1) + 1) % 1;
  const idx = t * segments;
  const i = Math.floor(idx);
  const f = idx - i;
  const p1 = trackPoints[i % segments];
  const p2 = trackPoints[(i + 1) % segments];
  return p1.clone().lerp(p2, f);
}
function getTangentAt(t) {
  t = ((t % 1) + 1) % 1;
  const idx = t * segments;
  const i = Math.floor(idx);
  const p1 = trackPoints[i % segments];
  const p2 = trackPoints[(i + 1) % segments];
  return p2.clone().sub(p1).normalize();
}
function getNormalAt(t) {
  const tan = getTangentAt(t);
  return new THREE.Vector3(-tan.z, 0, tan.x);
}

// ==================== UPDATE ====================
let prevTime = performance.now();
let raceStarted = false;
let countdown = 3;
let countdownTimer = 0;
let totalTime = 0;

function updatePlayer(dt) {
  if (!raceStarted || player.finished) return;

  const accel = keys['ArrowUp'] || keys['KeyW'];
  const brake = keys['ArrowDown'] || keys['KeyS'];
  const left = keys['ArrowLeft'] || keys['KeyA'];
  const right = keys['ArrowRight'] || keys['KeyD'];
  const handbrake = keys['Space'];

  // Acceleration
  if (accel) {
    player.speed += player.accel * dt;
  } else if (brake) {
    player.speed -= player.brake * dt;
  } else {
    player.speed -= player.friction * dt * Math.sign(player.speed || 1) * 0.3;
  }
  if (handbrake) player.speed *= 0.96;

  player.speed = THREE.MathUtils.clamp(player.speed, -12, player.maxSpeed);

  // Steering
  const steerInput = (left ? 1 : 0) - (right ? 1 : 0);
  const steerFactor = THREE.MathUtils.clamp(Math.abs(player.speed) / 20, 0.3, 1);
  player.steering = THREE.MathUtils.lerp(player.steering, steerInput * 1.4 * steerFactor, 6 * dt);

  // Progress along track
  const move = player.speed * dt * 0.012;
  player.progress += move;

  // Lateral movement from steering
  player.lateral += player.steering * player.speed * dt * 0.08;
  player.lateral = THREE.MathUtils.clamp(player.lateral, -player.maxLateral, player.maxLateral);

  // Soft walls
  if (Math.abs(player.lateral) > player.maxLateral - 0.5) {
    player.speed *= 0.92;
  }

  // Lap detection
  if (player.progress >= player.lap) {
    player.lap++;
    if (player.lap > player.totalLaps) {
      player.finished = true;
      const t = totalTime;
      if (!player.bestTime || t < player.bestTime) player.bestTime = t;
      showMessage('FINISHED! Time: ' + formatTime(t));
      document.getElementById('position').textContent = 'P1';
    } else {
      playBeep(880, 0.15);
    }
  }

  // Position car
  const t = player.progress % 1;
  const center = getPointAt(t);
  const normal = getNormalAt(t);
  const tangent = getTangentAt(t);

  playerCar.position.copy(center).add(normal.multiplyScalar(player.lateral));
  playerCar.position.y = 0.35;

  // Rotation
  const lookTarget = center.clone().add(tangent.multiplyScalar(5));
  lookTarget.add(normal.multiplyScalar(player.lateral));
  playerCar.lookAt(lookTarget);
  playerCar.rotation.y += player.steering * 0.15; // visual lean

  // Camera chase
  const camOffset = new THREE.Vector3(0, 5.5, -11);
  camOffset.applyQuaternion(playerCar.quaternion);
  const camTarget = playerCar.position.clone().add(camOffset);
  camera.position.lerp(camTarget, 1 - Math.pow(0.001, dt));
  camera.lookAt(playerCar.position.clone().add(new THREE.Vector3(0, 1.5, 0)));

  // Gear display
  if (player.speed < 1) player.gear = 'N';
  else if (player.speed < 18) player.gear = '1';
  else if (player.speed < 32) player.gear = '2';
  else if (player.speed < 42) player.gear = '3';
  else player.gear = '4';
}

function updateAI(dt) {
  aiCars.forEach((ai, idx) => {
    // simple speed control
    const targetSpeed = ai.maxSpeed * (0.85 + Math.sin(totalTime + idx)*0.08);
    ai.speed = THREE.MathUtils.lerp(ai.speed, targetSpeed, 1.5 * dt);
    ai.progress += ai.speed * dt * 0.012;

    const t = ai.progress % 1;
    const center = getPointAt(t);
    const normal = getNormalAt(t);
    const tangent = getTangentAt(t);

    // slight weaving
    const weave = Math.sin(totalTime * 1.5 + idx * 2) * 1.8 + ai.offset;
    ai.mesh.position.copy(center).add(normal.multiplyScalar(weave));
    ai.mesh.position.y = 0.35;

    const look = center.clone().add(tangent.multiplyScalar(4));
    ai.mesh.lookAt(look);
  });
}

function updateHUD() {
  document.getElementById('speed-val').textContent = Math.abs(Math.round(player.speed * 3.6));
  document.getElementById('gear').textContent = player.gear;
  document.getElementById('lap').textContent = Math.min(player.lap, player.totalLaps);
  document.getElementById('time').textContent = formatTime(totalTime);
  if (player.bestTime) document.getElementById('best').textContent = formatTime(player.bestTime);

  // Position (simple)
  let pos = 1;
  aiCars.forEach(ai => {
    if (ai.progress > player.progress) pos++;
  });
  document.getElementById('position').textContent = 'P' + pos;
}

function formatTime(t) {
  const m = Math.floor(t / 60);
  const s = Math.floor(t % 60);
  const ms = Math.floor((t % 1) * 10);
  return String(m).padStart(2,'0') + ':' + String(s).padStart(2,'0') + '.' + ms;
}

function showMessage(txt) {
  const el = document.getElementById('message');
  el.textContent = txt;
  el.style.opacity = 1;
  setTimeout(() => el.style.opacity = 0, 3500);
}

// Minimap
const mini = document.getElementById('minimap');
const mctx = mini.getContext('2d');
function drawMinimap() {
  mctx.fillStyle = '#1a1a1a';
  mctx.fillRect(0,0,180,180);
  mctx.save();
  mctx.translate(90, 90);
  mctx.scale(0.85, 0.85);

  // track
  mctx.strokeStyle = '#555';
  mctx.lineWidth = 8;
  mctx.beginPath();
  trackPoints.forEach((p, i) => {
    const x = p.x * 0.7, y = p.z * 0.7;
    if (i === 0) mctx.moveTo(x, y); else mctx.lineTo(x, y);
  });
  mctx.closePath();
  mctx.stroke();

  // player
  const pp = getPointAt(player.progress % 1);
  mctx.fillStyle = '#e63946';
  mctx.beginPath();
  mctx.arc(pp.x*0.7, pp.z*0.7, 5, 0, Math.PI*2);
  mctx.fill();

  // AI
  aiCars.forEach((ai, i) => {
    const ap = getPointAt(ai.progress % 1);
    mctx.fillStyle = '#' + aiColors[i].toString(16).padStart(6,'0');
    mctx.beginPath();
    mctx.arc(ap.x*0.7, ap.z*0.7, 4, 0, Math.PI*2);
    mctx.fill();
  });
  mctx.restore();
}

// ==================== MAIN LOOP ====================
function animate() {
  requestAnimationFrame(animate);
  const now = performance.now();
  const dt = Math.min((now - prevTime) / 1000, 0.05);
  prevTime = now;

  if (!raceStarted) {
    countdownTimer += dt;
    if (countdownTimer > 1) {
      countdownTimer = 0;
      countdown--;
      const cd = document.getElementById('countdown');
      if (countdown > 0) {
        cd.textContent = countdown;
        cd.style.opacity = 1;
        playBeep(440, 0.2);
        setTimeout(() => cd.style.opacity = 0, 600);
      } else if (countdown === 0) {
        cd.textContent = 'GO!';
        cd.style.opacity = 1;
        playBeep(880, 0.4);
        setTimeout(() => { cd.style.opacity = 0; raceStarted = true; player.startTime = now; }, 700);
        countdown = -1;
      }
    }
  } else {
    totalTime += dt;
    updatePlayer(dt);
    updateAI(dt);
    updateHUD();
    drawMinimap();
  }

  // place cars initially
  if (!raceStarted) {
    const t = 0.02;
    const center = getPointAt(t);
    const normal = getNormalAt(t);
    playerCar.position.copy(center);
    playerCar.position.y = 0.35;
    playerCar.lookAt(getPointAt(t + 0.02));
    camera.position.set(center.x - 8, 6, center.z - 14);
    camera.lookAt(playerCar.position);

    aiCars.forEach((ai, i) => {
      const at = 0.08 + i * 0.05;
      const c = getPointAt(at);
      const n = getNormalAt(at);
      ai.mesh.position.copy(c).add(n.multiplyScalar(ai.offset));
      ai.mesh.position.y = 0.35;
      ai.mesh.lookAt(getPointAt(at + 0.02));
    });
  }

  renderer.render(scene, camera);
}

// Start
document.getElementById('controls-hint').addEventListener('click', () => {
  document.getElementById('controls-hint').style.display = 'none';
  document.getElementById('ui').style.display = 'block';
  if (audioCtx.state === 'suspended') audioCtx.resume();
  animate();
});

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>

Game Source: 3D Car Racing - Three.js

Creator: ApexWizard89

Libraries: three

Complexity: complex (676 lines, 20.9 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: 3d-car-racing-three-js-apexwizard89" to link back to the original. Then publish at arcadelab.ai/publish.