3D Car Racing - Three.js
by ApexWizard89676 lines20.9 KB🛠️ Three.js (3D graphics)
<!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.