Tech Details

Car Game 3D - Day 18

Three.js + Racing Physics

Table of Contents

01 - Three.js Scene Setup

The game uses Three.js for all 3D rendering. We set up a scene with fog for depth perception and proper lighting for the neon aesthetic.

Scene Initialization

// Create the 3D scene scene = new THREE.Scene(); scene.background = new THREE.Color(0x0a0a1a); scene.fog = new THREE.Fog(0x0a0a1a, 100, 400); // Perspective camera for racing view camera = new THREE.PerspectiveCamera( 70, // Field of view 1, // Aspect ratio (updated on resize) 0.1, // Near clipping plane 1000 // Far clipping plane ); // WebGL renderer with shadows renderer = new THREE.WebGLRenderer({ canvas, antialias: true }); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap;

Lighting Setup

The scene uses ambient light for base illumination plus a directional "moon" light for subtle shadows. Point lights are added throughout the track for the neon glow effect.

// Ambient light - purple tint for night feel const ambient = new THREE.AmbientLight( 0x404080, // Purple-tinted 0.4 // Low intensity ); scene.add(ambient); // Moonlight for subtle shadows const moon = new THREE.DirectionalLight( 0x8888ff, 0.3 ); moon.position.set(50, 100, 50); scene.add(moon);
Performance Tip

Fog helps hide distant objects and gives depth to the scene while also improving performance by allowing Three.js to skip rendering objects beyond the fog distance.

02 - Procedural Track Generation

The track is procedurally generated as an oval with added variation using sine waves. Each segment creates road geometry with perpendicular edge calculations.

Track Point Calculation

// Generate track points around an oval for (let i = 0; i <= TRACK_SEGMENTS; i++) { const angle = (i / TRACK_SEGMENTS) * Math.PI * 2; // Oval radii const radiusX = TRACK_RADIUS * 1.5; const radiusZ = TRACK_RADIUS; // Add variation with sine wave const wobble = Math.sin(angle * 4) * 10; const x = Math.cos(angle) * (radiusX + wobble); const z = Math.sin(angle) * (radiusZ + wobble); trackPoints.push({ x, z, angle }); }

Road Segment Geometry

Each segment is a quad created from two triangles. We calculate perpendicular vectors to extend the road width in both directions.

// Calculate perpendicular direction for road width const dx = p2.x - p1.x; const dz = p2.z - p1.z; const len = Math.sqrt(dx * dx + dz * dz); const perpX = -dz / len; // Rotate 90 degrees const perpZ = dx / len; // Create road quad (6 vertices, 2 triangles) const vertices = new Float32Array([ // Triangle 1 p1.x + perpX * width, 0.1, p1.z + perpZ * width, p1.x - perpX * width, 0.1, p1.z - perpZ * width, p2.x + perpX * width, 0.1, p2.z + perpZ * width, // Triangle 2 p1.x - perpX * width, 0.1, p1.z - perpZ * width, p2.x - perpX * width, 0.1, p2.z - perpZ * width, p2.x + perpX * width, 0.1, p2.z + perpZ * width, ]);

Perpendicular Vector Calculation

Direction (dx, dz)
->
Normalize
->
Rotate 90deg
->
(-dz/len, dx/len)

03 - Car Physics System

The car uses a simplified arcade physics model with acceleration, friction, and speed-dependent steering. This creates a responsive but believable driving feel.

Physics State

const carState = { x: 0, z: TRACK_RADIUS - 10, rotation: 0, speed: 0, maxSpeed: 2.5, acceleration: 0.05, braking: 0.08, deceleration: 0.015, turnSpeed: 0.045, friction: 0.985, driftFactor: 0 };

Movement Update Loop

function updateCar() { // Acceleration / Braking if (keys.up) carState.speed += carState.acceleration; if (keys.down) carState.speed -= carState.braking; // Speed limits carState.speed = Math.max( -carState.maxSpeed / 2, Math.min(carState.maxSpeed, carState.speed) ); // Friction when not accelerating if (!keys.up && !keys.down) { carState.speed *= carState.friction; } // Speed-dependent steering if (Math.abs(carState.speed) > 0.05) { const turnFactor = carState.speed > 0 ? 1 : -1; const speedFactor = Math.min( Math.abs(carState.speed) / carState.maxSpeed, 1 ); if (keys.left) { carState.rotation += carState.turnSpeed * turnFactor * speedFactor; } if (keys.right) { carState.rotation -= carState.turnSpeed * turnFactor * speedFactor; } } // Apply velocity to position carState.x += Math.sin(carState.rotation) * carState.speed; carState.z += Math.cos(carState.rotation) * carState.speed; }
Arcade vs Simulation

This physics model prioritizes fun over realism. Real car physics involve complex tire friction models, weight transfer, and suspension. For an arcade game, simple friction and speed-clamping work great!

04 - Checkpoint & Lap System

The race uses a checkpoint system to ensure players complete the full track. The player must pass through checkpoints in order to advance.

Checkpoint Detection

function checkCheckpoints() { checkpoints.forEach((cp, index) => { // Distance from car to checkpoint const dx = carState.x - cp.x; const dz = carState.z - cp.z; const dist = Math.sqrt(dx * dx + dz * dz); if (dist < TRACK_WIDTH) { // Must hit checkpoints in order const expectedNext = (lastCheckpoint + 1) % checkpoints.length; if (index === expectedNext) { lastCheckpoint = index; // Check for lap completion if (index === 0) { if (currentLap < totalLaps) { currentLap++; } else { finishRace(); } } } } }); }

Lap Completion Flow

CP 1
->
CP 2
->
...
->
CP 8
->
Start/Finish
->
Lap++

Best Time Persistence

// Load best time from localStorage let bestTime = localStorage.getItem('carGame3dBestTime'); // Save new record if (!bestTime || finalTime < parseFloat(bestTime)) { bestTime = finalTime; localStorage.setItem('carGame3dBestTime', finalTime); }

05 - Camera Follow System

The camera smoothly follows behind the car, creating the classic third-person racing view. Interpolation prevents jarring camera movements.

Smooth Camera Follow

// Calculate ideal camera position const camDist = 25; const camHeight = 12; const targetCamX = carState.x - Math.sin(carState.rotation) * camDist; const targetCamZ = carState.z - Math.cos(carState.rotation) * camDist; // Smooth interpolation (lerp) camera.position.x += (targetCamX - camera.position.x) * 0.05; camera.position.z += (targetCamZ - camera.position.z) * 0.05; camera.position.y = camHeight; // Always look at the car camera.lookAt(carState.x, 2, carState.z);

Car Tilt Effect

When turning, the car tilts slightly to enhance the feeling of speed and momentum.

// Calculate target tilt based on input and speed const targetTilt = (keys.left ? 0.1 : keys.right ? -0.1 : 0) * Math.abs(carState.speed); // Smoothly interpolate towards target carGroup.rotation.z += (targetTilt - carGroup.rotation.z) * 0.1;
Lerp Magic

Linear interpolation (lerp) with a small factor like 0.05 creates smooth transitions. The formula current += (target - current) * factor is used everywhere in game development!

06 - Neon Visual Effects

The game's cyberpunk aesthetic is achieved through careful use of emissive materials, point lights, and glow effects.

Neon Edge Lines

// Create glowing track edges function createEdgeLine(p1, p2, perpX, perpZ, offset, color) { const lineMat = new THREE.LineBasicMaterial({ color }); // Add glow posts at intervals if (Math.random() > 0.9) { const postMat = new THREE.MeshBasicMaterial({ color }); const post = new THREE.Mesh(postGeo, postMat); // Point light for glow effect const light = new THREE.PointLight(color, 0.5, 15); light.position.copy(post.position); scene.add(light); } }

Car Underglow

// Neon underglow lights on car const underglowL = new THREE.PointLight( 0x00ffff, // Cyan 0.5, // Intensity 5 // Distance ); underglowL.position.set(-1.5, 0.2, 0); carGroup.add(underglowL); const underglowF = new THREE.PointLight( 0xff00ff, // Magenta 0.5, 5 ); underglowF.position.set(0, 0.2, 2); carGroup.add(underglowF);

Floating Particles

Animated particles floating in the scene add depth and atmosphere to the cyberpunk world.

// Create floating particles for (let i = 0; i < 50; i++) { const particle = new THREE.Mesh(sphereGeo, particleMat); particle.position.set( (Math.random() - 0.5) * 400, 10 + Math.random() * 30, (Math.random() - 0.5) * 400 ); particle.userData = { speed: 0.5 + Math.random(), offset: Math.random() * Math.PI * 2 }; } // Animate in render loop particle.position.y += Math.sin( Date.now() * 0.001 * particle.userData.speed + particle.userData.offset ) * 0.02;
Performance Note

Point lights are expensive! The game uses them sparingly with low range values. For more complex scenes, consider baked lighting or light probes.