Back to Components
Traffic Run Game with Three.js - 3D Driving Game
Component

Traffic Run Game with Three.js - 3D Driving Game

CodewithLord
January 30, 2026

An advanced 3D traffic avoidance game built with Three.js. Features a circular dual-track racing circuit with AI-controlled vehicles, collision detection, player vehicle physics, dynamic lighting with shadows, and procedural texture generation using HTML Canvas. Includes orthographic camera perspective, vehicle spawning mechanics, and responsive controls.

🧠 Description

This project showcases a sophisticated 3D racing game built with Three.js, a powerful JavaScript 3D library.

Players navigate a circular dual-track racing circuit, accelerating and decelerating to avoid collisions with randomly spawned traffic vehicles.

The game features an orthographic camera perspective, dynamic AI-controlled cars and trucks with varying speeds, realistic shadow rendering, and procedurally generated textures using HTML Canvas.

The circuit design utilizes complex mathematical curves to create a figure-eight style track with inner and outer lanes, complete with visual lane markings, curbs, and environmental details like trees.

Perfect for learning advanced Three.js concepts including geometries, materials, lighting, shadows, collision detection, and game loop architecture.


💻 HTML Code


1<!DOCTYPE html> 2<html lang="en"> 3 4 <head> 5 <meta charset="UTF-8"> 6 <title>Traffic Run Game with Three.js</title> 7 <link rel="stylesheet" href="./style.css"> 8 9 </head> 10 11 <body> 12 <div id="score">Press UP</div> 13 14<div id="controls"> 15 <div id="buttons"> 16 <button id="accelerate"> 17 <svg width="30" height="30" viewBox="0 0 10 10"> 18 <g transform="rotate(0, 5,5)"> 19 <path d="M5,4 L7,6 L3,6 L5,4" /> 20 </g> 21 </svg> 22 </button> 23 <button id="decelerate"> 24 <svg width="30" height="30" viewBox="0 0 10 10"> 25 <g transform="rotate(180, 5,5)"> 26 <path d="M5,4 L7,6 L3,6 L5,4" /> 27 </g> 28 </svg> 29 </button> 30 </div> 31 <div id="instructions"> 32 Press UP to start. Avoid collision with other vehicles by accelerating 33 or decelerating with the UP and DOWN keys. 34 </div> 35</div> 36 37<div id="results"> 38 <div class="content"> 39 <h1>You hit another vehicle</h1> 40 <p>To reset the game press R</p> 41 42 <a id="result-youtube" target="_top" href="https://youtu.be/JhgBwJn1bQw"> 43 <div class="youtube"></div> 44 <span>Learn how to build this game with Three.js on YouTube</span> 45 </a> 46 47 48 </div> 49</div> 50 51<a id="youtube-main" class="youtube" target="_top" href="https://youtube.com/codewithlord"> 52 <span>Learn Three.js</span> 53</a> 54<div id="youtube-card"> 55 Learn Three.js while building this game on YouTube 56</div> 57 <script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/r126/three.min.js'></script><script src="./script.js"></script> 58 59 </body> 60 61</html> 62

HTML Structure Explanation

The HTML structure is minimal, relying on JavaScript to dynamically build the Three.js scene:

Key Elements:

  • #score - Displays current lap count and game status
  • #controls - Container for control buttons and instructions
    • #buttons - Contains accelerate and decelerate buttons with SVG arrow icons
    • #instructions - Game controls and tutorial text
  • #results - Modal overlay displayed when player collides with vehicle
    • Links to YouTube tutorial for learning
    • Social media follow link
  • #youtube-main - Fixed position YouTube logo for video tutorial access
  • #youtube-card - Tooltip card displayed on hover

Three.js library loaded from CDN (version r126) before the script.js file.

Canvas element created dynamically by Three.js renderer and appended to body.

Minimal markup allows full control over rendering through JavaScript and Three.js.


🎨 CSS Code


1@import url("https://fonts.googleapis.com/css?family=Press+Start+2P"); 2 3body { 4 margin: 0; 5 padding: 0; 6 color: white; 7 font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; 8} 9 10button { 11 outline: none; 12 cursor: pointer; 13 border: none; 14 box-shadow: 3px 5px 0px 0px rgba(0, 0, 0, 0.75); 15} 16 17a, 18a:visited { 19 color: inherit; 20} 21 22#score { 23 position: absolute; 24 font-family: "Press Start 2P", cursive; 25 font-size: 0.9em; 26 color: white; 27 transform: translate(-50%, -50%); 28 opacity: 0.9; 29 max-width: 100px; 30 text-align: center; 31 line-height: 1.6em; 32} 33 34#controls { 35 position: absolute; 36 bottom: 50px; 37 left: 50px; 38 display: none; 39} 40 41#controls #buttons { 42 width: 80px; 43 opacity: 0; 44 transition: opacity 2s; 45} 46 47#controls #instructions { 48 margin-left: 20px; 49 max-width: 300px; 50 background-color: rgba(0, 0, 0, 0.2); 51 padding: 20px; 52 opacity: 0; 53 transition: opacity 2s; 54} 55 56#controls button { 57 width: 100%; 58 height: 40px; 59 background-color: white; 60 border: 1px solid black; 61 margin-bottom: 10px; 62} 63 64#results { 65 position: absolute; 66 align-items: center; 67 justify-content: center; 68 height: 100%; 69 width: 100%; 70 background-color: rgba(20, 20, 20, 0.75); 71 display: none; 72 z-index: 51; 73} 74 75#results .content { 76 max-width: 350px; 77 padding: 50px; 78 border-radius: 20px; 79} 80 81#result-youtube { 82 display: flex; 83 background-color: white; 84 padding: 20px; 85 color: black; 86 text-decoration: none; 87 cursor: pointer; 88} 89 90#result-youtube span { 91 margin-top: 5px; 92 margin-left: 20px; 93} 94 95.youtube, 96#youtube-card { 97 display: none; 98 color: black; 99} 100 101#youtube-main { 102 opacity: 0; 103 transition: opacity 2s; 104} 105 106@media (min-height: 425px) { 107 #score { 108 font-size: 1.5em; 109 max-width: 150px; 110 } 111 112 #controls { 113 display: flex; 114 } 115 116 /** Youtube logo by https://codepen.io/alvaromontoro */ 117 .youtube { 118 z-index: 50; 119 width: 100px; 120 min-width: 100px; 121 height: 70px; 122 position: fixed; 123 bottom: 50px; 124 right: 50px; 125 transform: scale(0.8); 126 transition: transform 0.5s; 127 display: block; 128 background: red; 129 border-radius: 50% / 11%; 130 } 131 132 #results .youtube { 133 position: relative; 134 right: 0; 135 bottom: 0; 136 } 137 138 .youtube:hover, 139 .youtube:focus { 140 transform: scale(0.9); 141 color: black; 142 } 143 144 .youtube::before { 145 content: ""; 146 display: block; 147 position: absolute; 148 top: 7.5%; 149 left: -6%; 150 width: 112%; 151 height: 85%; 152 background: red; 153 border-radius: 9% / 50%; 154 } 155 156 .youtube::after { 157 content: ""; 158 display: block; 159 position: absolute; 160 top: 20px; 161 left: 40px; 162 width: 45px; 163 height: 30px; 164 border: 15px solid transparent; 165 box-sizing: border-box; 166 border-left: 30px solid white; 167 } 168 169 .youtube span { 170 font-size: 0; 171 position: absolute; 172 width: 0; 173 height: 0; 174 overflow: hidden; 175 } 176 177 .youtube:hover + #youtube-card { 178 z-index: 49; 179 display: block; 180 position: fixed; 181 bottom: 42px; 182 width: 300px; 183 background-color: white; 184 right: 40px; 185 padding: 25px 130px 25px 25px; 186 } 187}

CSS Breakdown

Typography & General Styling

Imports "Press Start 2P" font from Google Fonts for retro arcade aesthetic.

Body background is dark (implied by Three.js canvas).

Buttons have inset shadow for 3D depth effect.

Links inherit text color for consistent theming.

Score Display

#score positioned absolutely in viewport using transform for perfect centering.

Retro pixel font with 0.9em base size scaled to 1.5em on larger screens.

Opacity 0.9 ensures readability while showing game underneath.

Max-width 100px prevents overflow on small screens.

Control Panel

#controls positioned bottom-left with flexbox layout.

Initially hidden (display: none) on screens smaller than 425px.

Buttons and instructions fade in over 2 seconds using opacity transition.

Buttons

White background with black border, 40px height, full width of container.

SVG arrow icons scale with button size.

Box shadow creates pressed button appearance.

Results Modal

Full-screen overlay with 75% black background opacity.

Centers content using flexbox (align-items, justify-content).

Z-index 51 ensures modal appears above all other elements.

Hidden by default (display: none), shown via JavaScript on collision.

YouTube Logo

Creatively styled using CSS shapes (red background, pseudo-elements).

Hidden by default, only visible on screens taller than 425px.

Position fixed in bottom-right corner.

Scale transforms on hover for interactive feedback.

Pseudo-elements create play button icon without images.

Responsive Design

Media query at 425px height threshold:

  • Shows score at larger 1.5em size
  • Reveals control buttons with fade-in effect
  • Enables YouTube logo and hover tooltips

Z-Index Hierarchy

  • Results modal: 51 (topmost)
  • YouTube logo: 50
  • YouTube tooltip: 49
  • Default content: implicit 0

This layering ensures modals appear above interactive elements.


⚙️ JavaScript Code


1 2 3window.focus(); // Capture keys right away (by default focus is on editor) 4 5// Pick a random value from an array 6function pickRandom(array) { 7 return array[Math.floor(Math.random() * array.length)]; 8} 9 10// The Pythagorean theorem says that the distance between two points is 11// the square root of the sum of the horizontal and vertical distance's square 12function getDistance(coordinate1, coordinate2) { 13 const horizontalDistance = coordinate2.x - coordinate1.x; 14 const verticalDistance = coordinate2.y - coordinate1.y; 15 return Math.sqrt(horizontalDistance ** 2 + verticalDistance ** 2); 16} 17 18const vehicleColors = [ 19 0xa52523, 20 0xef2d56, 21 0x0ad3ff, 22 0xff9f1c /*0xa52523, 0xbdb638, 0x78b14b*/ 23]; 24 25const lawnGreen = "#67C240"; 26const trackColor = "#546E90"; 27const edgeColor = "#725F48"; 28const treeCrownColor = 0x498c2c; 29const treeTrunkColor = 0x4b3f2f; 30 31const wheelGeometry = new THREE.BoxBufferGeometry(12, 33, 12); 32const wheelMaterial = new THREE.MeshLambertMaterial({ color: 0x333333 }); 33const treeTrunkGeometry = new THREE.BoxBufferGeometry(15, 15, 30); 34const treeTrunkMaterial = new THREE.MeshLambertMaterial({ 35 color: treeTrunkColor 36}); 37const treeCrownMaterial = new THREE.MeshLambertMaterial({ 38 color: treeCrownColor 39}); 40 41const config = { 42 showHitZones: false, 43 shadows: true, // Use shadow 44 trees: true, // Add trees to the map 45 curbs: true, // Show texture on the extruded geometry 46 grid: false // Show grid helper 47}; 48 49let score; 50const speed = 0.0017; 51 52const playerAngleInitial = Math.PI; 53let playerAngleMoved; 54let accelerate = false; // Is the player accelerating 55let decelerate = false; // Is the player decelerating 56 57let otherVehicles = []; 58let ready; 59let lastTimestamp; 60 61const trackRadius = 225; 62const trackWidth = 45; 63const innerTrackRadius = trackRadius - trackWidth; 64const outerTrackRadius = trackRadius + trackWidth; 65 66const arcAngle1 = (1 / 3) * Math.PI; // 60 degrees 67 68const deltaY = Math.sin(arcAngle1) * innerTrackRadius; 69const arcAngle2 = Math.asin(deltaY / outerTrackRadius); 70 71const arcCenterX = 72 (Math.cos(arcAngle1) * innerTrackRadius + 73 Math.cos(arcAngle2) * outerTrackRadius) / 74 2; 75 76const arcAngle3 = Math.acos(arcCenterX / innerTrackRadius); 77 78const arcAngle4 = Math.acos(arcCenterX / outerTrackRadius); 79 80const scoreElement = document.getElementById("score"); 81const buttonsElement = document.getElementById("buttons"); 82const instructionsElement = document.getElementById("instructions"); 83const resultsElement = document.getElementById("results"); 84const accelerateButton = document.getElementById("accelerate"); 85const decelerateButton = document.getElementById("decelerate"); 86const youtubeLogo = document.getElementById("youtube-main"); 87 88setTimeout(() => { 89 if (ready) instructionsElement.style.opacity = 1; 90 buttonsElement.style.opacity = 1; 91 youtubeLogo.style.opacity = 1; 92}, 4000); 93 94// Initialize ThreeJs 95// Set up camera 96const aspectRatio = window.innerWidth / window.innerHeight; 97const cameraWidth = 960; 98const cameraHeight = cameraWidth / aspectRatio; 99 100const camera = new THREE.OrthographicCamera( 101 cameraWidth / -2, // left 102 cameraWidth / 2, // right 103 cameraHeight / 2, // top 104 cameraHeight / -2, // bottom 105 50, // near plane 106 700 // far plane 107); 108 109camera.position.set(0, -210, 300); 110camera.lookAt(0, 0, 0); 111 112const scene = new THREE.Scene(); 113 114const playerCar = Car(); 115scene.add(playerCar); 116 117renderMap(cameraWidth, cameraHeight * 2); // The map height is higher because we look at the map from an angle 118 119// Set up lights 120const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); 121scene.add(ambientLight); 122 123const dirLight = new THREE.DirectionalLight(0xffffff, 0.6); 124dirLight.position.set(100, -300, 300); 125dirLight.castShadow = true; 126dirLight.shadow.mapSize.width = 1024; 127dirLight.shadow.mapSize.height = 1024; 128dirLight.shadow.camera.left = -400; 129dirLight.shadow.camera.right = 350; 130dirLight.shadow.camera.top = 400; 131dirLight.shadow.camera.bottom = -300; 132dirLight.shadow.camera.near = 100; 133dirLight.shadow.camera.far = 800; 134scene.add(dirLight); 135 136// const cameraHelper = new THREE.CameraHelper(dirLight.shadow.camera); 137// scene.add(cameraHelper); 138 139if (config.grid) { 140 const gridHelper = new THREE.GridHelper(80, 8); 141 gridHelper.rotation.x = Math.PI / 2; 142 scene.add(gridHelper); 143} 144 145// Set up renderer 146const renderer = new THREE.WebGLRenderer({ 147 antialias: true, 148 powerPreference: "high-performance" 149}); 150renderer.setSize(window.innerWidth, window.innerHeight); 151if (config.shadows) renderer.shadowMap.enabled = true; 152document.body.appendChild(renderer.domElement); 153 154reset(); 155 156function reset() { 157 // Reset position and score 158 playerAngleMoved = 0; 159 score = 0; 160 scoreElement.innerText = "Press UP"; 161 162 // Remove other vehicles 163 otherVehicles.forEach((vehicle) => { 164 // Remove the vehicle from the scene 165 scene.remove(vehicle.mesh); 166 167 // If it has hit-zone helpers then remove them as well 168 if (vehicle.mesh.userData.hitZone1) 169 scene.remove(vehicle.mesh.userData.hitZone1); 170 if (vehicle.mesh.userData.hitZone2) 171 scene.remove(vehicle.mesh.userData.hitZone2); 172 if (vehicle.mesh.userData.hitZone3) 173 scene.remove(vehicle.mesh.userData.hitZone3); 174 }); 175 otherVehicles = []; 176 177 resultsElement.style.display = "none"; 178 179 lastTimestamp = undefined; 180 181 // Place the player's car to the starting position 182 movePlayerCar(0); 183 184 // Render the scene 185 renderer.render(scene, camera); 186 187 ready = true; 188} 189 190function startGame() { 191 if (ready) { 192 ready = false; 193 scoreElement.innerText = 0; 194 buttonsElement.style.opacity = 1; 195 instructionsElement.style.opacity = 0; 196 youtubeLogo.style.opacity = 1; 197 renderer.setAnimationLoop(animation); 198 } 199} 200 201function positionScoreElement() { 202 const arcCenterXinPixels = (arcCenterX / cameraWidth) * window.innerWidth; 203 scoreElement.style.cssText = ` 204 left: ${window.innerWidth / 2 - arcCenterXinPixels * 1.3}px; 205 top: ${window.innerHeight / 2}px 206 `; 207} 208 209function getLineMarkings(mapWidth, mapHeight) { 210 const canvas = document.createElement("canvas"); 211 canvas.width = mapWidth; 212 canvas.height = mapHeight; 213 const context = canvas.getContext("2d"); 214 215 context.fillStyle = trackColor; 216 context.fillRect(0, 0, mapWidth, mapHeight); 217 218 context.lineWidth = 2; 219 context.strokeStyle = "#E0FFFF"; 220 context.setLineDash([10, 14]); 221 222 // Left circle 223 context.beginPath(); 224 context.arc( 225 mapWidth / 2 - arcCenterX, 226 mapHeight / 2, 227 trackRadius, 228 0, 229 Math.PI * 2 230 ); 231 context.stroke(); 232 233 // Right circle 234 context.beginPath(); 235 context.arc( 236 mapWidth / 2 + arcCenterX, 237 mapHeight / 2, 238 trackRadius, 239 0, 240 Math.PI * 2 241 ); 242 context.stroke(); 243 244 return new THREE.CanvasTexture(canvas); 245} 246 247function getCurbsTexture(mapWidth, mapHeight) { 248 const canvas = document.createElement("canvas"); 249 canvas.width = mapWidth; 250 canvas.height = mapHeight; 251 const context = canvas.getContext("2d"); 252 253 context.fillStyle = lawnGreen; 254 context.fillRect(0, 0, mapWidth, mapHeight); 255 256 // Extra big 257 context.lineWidth = 65; 258 context.strokeStyle = "#A2FF75"; 259 context.beginPath(); 260 context.arc( 261 mapWidth / 2 - arcCenterX, 262 mapHeight / 2, 263 innerTrackRadius, 264 arcAngle1, 265 -arcAngle1 266 ); 267 context.arc( 268 mapWidth / 2 + arcCenterX, 269 mapHeight / 2, 270 outerTrackRadius, 271 Math.PI + arcAngle2, 272 Math.PI - arcAngle2, 273 true 274 ); 275 context.stroke(); 276 277 context.beginPath(); 278 context.arc( 279 mapWidth / 2 + arcCenterX, 280 mapHeight / 2, 281 innerTrackRadius, 282 Math.PI + arcAngle1, 283 Math.PI - arcAngle1 284 ); 285 context.arc( 286 mapWidth / 2 - arcCenterX, 287 mapHeight / 2, 288 outerTrackRadius, 289 arcAngle2, 290 -arcAngle2, 291 true 292 ); 293 context.stroke(); 294 295 // Extra small 296 context.lineWidth = 60; 297 context.strokeStyle = lawnGreen; 298 context.beginPath(); 299 context.arc( 300 mapWidth / 2 - arcCenterX, 301 mapHeight / 2, 302 innerTrackRadius, 303 arcAngle1, 304 -arcAngle1 305 ); 306 context.arc( 307 mapWidth / 2 + arcCenterX, 308 mapHeight / 2, 309 outerTrackRadius, 310 Math.PI + arcAngle2, 311 Math.PI - arcAngle2, 312 true 313 ); 314 context.arc( 315 mapWidth / 2 + arcCenterX, 316 mapHeight / 2, 317 innerTrackRadius, 318 Math.PI + arcAngle1, 319 Math.PI - arcAngle1 320 ); 321 context.arc( 322 mapWidth / 2 - arcCenterX, 323 mapHeight / 2, 324 outerTrackRadius, 325 arcAngle2, 326 -arcAngle2, 327 true 328 ); 329 context.stroke(); 330 331 // Base 332 context.lineWidth = 6; 333 context.strokeStyle = edgeColor; 334 335 // Outer circle left 336 context.beginPath(); 337 context.arc( 338 mapWidth / 2 - arcCenterX, 339 mapHeight / 2, 340 outerTrackRadius, 341 0, 342 Math.PI * 2 343 ); 344 context.stroke(); 345 346 // Outer circle right 347 context.beginPath(); 348 context.arc( 349 mapWidth / 2 + arcCenterX, 350 mapHeight / 2, 351 outerTrackRadius, 352 0, 353 Math.PI * 2 354 ); 355 context.stroke(); 356 357 // Inner circle left 358 context.beginPath(); 359 context.arc( 360 mapWidth / 2 - arcCenterX, 361 mapHeight / 2, 362 innerTrackRadius, 363 0, 364 Math.PI * 2 365 ); 366 context.stroke(); 367 368 // Inner circle right 369 context.beginPath(); 370 context.arc( 371 mapWidth / 2 + arcCenterX, 372 mapHeight / 2, 373 innerTrackRadius, 374 0, 375 Math.PI * 2 376 ); 377 context.stroke(); 378 379 return new THREE.CanvasTexture(canvas); 380} 381 382function getLeftIsland() { 383 const islandLeft = new THREE.Shape(); 384 385 islandLeft.absarc( 386 -arcCenterX, 387 0, 388 innerTrackRadius, 389 arcAngle1, 390 -arcAngle1, 391 false 392 ); 393 394 islandLeft.absarc( 395 arcCenterX, 396 0, 397 outerTrackRadius, 398 Math.PI + arcAngle2, 399 Math.PI - arcAngle2, 400 true 401 ); 402 403 return islandLeft; 404} 405 406function getMiddleIsland() { 407 const islandMiddle = new THREE.Shape(); 408 409 islandMiddle.absarc( 410 -arcCenterX, 411 0, 412 innerTrackRadius, 413 arcAngle3, 414 -arcAngle3, 415 true 416 ); 417 418 islandMiddle.absarc( 419 arcCenterX, 420 0, 421 innerTrackRadius, 422 Math.PI + arcAngle3, 423 Math.PI - arcAngle3, 424 true 425 ); 426 427 return islandMiddle; 428} 429 430function getRightIsland() { 431 const islandRight = new THREE.Shape(); 432 433 islandRight.absarc( 434 arcCenterX, 435 0, 436 innerTrackRadius, 437 Math.PI - arcAngle1, 438 Math.PI + arcAngle1, 439 true 440 ); 441 442 islandRight.absarc( 443 -arcCenterX, 444 0, 445 outerTrackRadius, 446 -arcAngle2, 447 arcAngle2, 448 false 449 ); 450 451 return islandRight; 452} 453 454function getOuterField(mapWidth, mapHeight) { 455 const field = new THREE.Shape(); 456 457 field.moveTo(-mapWidth / 2, -mapHeight / 2); 458 field.lineTo(0, -mapHeight / 2); 459 460 field.absarc(-arcCenterX, 0, outerTrackRadius, -arcAngle4, arcAngle4, true); 461 462 field.absarc( 463 arcCenterX, 464 0, 465 outerTrackRadius, 466 Math.PI - arcAngle4, 467 Math.PI + arcAngle4, 468 true 469 ); 470 471 field.lineTo(0, -mapHeight / 2); 472 field.lineTo(mapWidth / 2, -mapHeight / 2); 473 field.lineTo(mapWidth / 2, mapHeight / 2); 474 field.lineTo(-mapWidth / 2, mapHeight / 2); 475 476 return field; 477} 478 479function renderMap(mapWidth, mapHeight) { 480 const lineMarkingsTexture = getLineMarkings(mapWidth, mapHeight); 481 482 const planeGeometry = new THREE.PlaneBufferGeometry(mapWidth, mapHeight); 483 const planeMaterial = new THREE.MeshLambertMaterial({ 484 map: lineMarkingsTexture 485 }); 486 const plane = new THREE.Mesh(planeGeometry, planeMaterial); 487 plane.receiveShadow = true; 488 plane.matrixAutoUpdate = false; 489 scene.add(plane); 490 491 // Extruded geometry with curbs 492 const islandLeft = getLeftIsland(); 493 const islandMiddle = getMiddleIsland(); 494 const islandRight = getRightIsland(); 495 const outerField = getOuterField(mapWidth, mapHeight); 496 497 // Mapping a texture on an extruded geometry works differently than mapping it to a box 498 // By default it is mapped to a 1x1 unit square, and we have to stretch it out by setting repeat 499 // We also need to shift it by setting the offset to have it centered 500 const curbsTexture = getCurbsTexture(mapWidth, mapHeight); 501 curbsTexture.offset = new THREE.Vector2(0.5, 0.5); 502 curbsTexture.repeat.set(1 / mapWidth, 1 / mapHeight); 503 504 // An extruded geometry turns a 2D shape into 3D by giving it a depth 505 const fieldGeometry = new THREE.ExtrudeBufferGeometry( 506 [islandLeft, islandRight, islandMiddle, outerField], 507 { depth: 6, bevelEnabled: false } 508 ); 509 510 const fieldMesh = new THREE.Mesh(fieldGeometry, [ 511 new THREE.MeshLambertMaterial({ 512 // Either set a plain color or a texture depending on config 513 color: !config.curbs && lawnGreen, 514 map: config.curbs && curbsTexture 515 }), 516 new THREE.MeshLambertMaterial({ color: 0x23311c }) 517 ]); 518 fieldMesh.receiveShadow = true; 519 fieldMesh.matrixAutoUpdate = false; 520 scene.add(fieldMesh); 521 522 positionScoreElement(); 523 524 if (config.trees) { 525 const tree1 = Tree(); 526 tree1.position.x = arcCenterX * 1.3; 527 scene.add(tree1); 528 529 const tree2 = Tree(); 530 tree2.position.y = arcCenterX * 1.9; 531 tree2.position.x = arcCenterX * 1.3; 532 scene.add(tree2); 533 534 const tree3 = Tree(); 535 tree3.position.x = arcCenterX * 0.8; 536 tree3.position.y = arcCenterX * 2; 537 scene.add(tree3); 538 539 const tree4 = Tree(); 540 tree4.position.x = arcCenterX * 1.8; 541 tree4.position.y = arcCenterX * 2; 542 scene.add(tree4); 543 544 const tree5 = Tree(); 545 tree5.position.x = -arcCenterX * 1; 546 tree5.position.y = arcCenterX * 2; 547 scene.add(tree5); 548 549 const tree6 = Tree(); 550 tree6.position.x = -arcCenterX * 2; 551 tree6.position.y = arcCenterX * 1.8; 552 scene.add(tree6); 553 554 const tree7 = Tree(); 555 tree7.position.x = arcCenterX * 0.8; 556 tree7.position.y = -arcCenterX * 2; 557 scene.add(tree7); 558 559 const tree8 = Tree(); 560 tree8.position.x = arcCenterX * 1.8; 561 tree8.position.y = -arcCenterX * 2; 562 scene.add(tree8); 563 564 const tree9 = Tree(); 565 tree9.position.x = -arcCenterX * 1; 566 tree9.position.y = -arcCenterX * 2; 567 scene.add(tree9); 568 569 const tree10 = Tree(); 570 tree10.position.x = -arcCenterX * 2; 571 tree10.position.y = -arcCenterX * 1.8; 572 scene.add(tree10); 573 574 const tree11 = Tree(); 575 tree11.position.x = arcCenterX * 0.6; 576 tree11.position.y = -arcCenterX * 2.3; 577 scene.add(tree11); 578 579 const tree12 = Tree(); 580 tree12.position.x = arcCenterX * 1.5; 581 tree12.position.y = -arcCenterX * 2.4; 582 scene.add(tree12); 583 584 const tree13 = Tree(); 585 tree13.position.x = -arcCenterX * 0.7; 586 tree13.position.y = -arcCenterX * 2.4; 587 scene.add(tree13); 588 589 const tree14 = Tree(); 590 tree14.position.x = -arcCenterX * 1.5; 591 tree14.position.y = -arcCenterX * 1.8; 592 scene.add(tree14); 593 } 594} 595 596function getCarFrontTexture() { 597 const canvas = document.createElement("canvas"); 598 canvas.width = 64; 599 canvas.height = 32; 600 const context = canvas.getContext("2d"); 601 602 context.fillStyle = "#ffffff"; 603 context.fillRect(0, 0, 64, 32); 604 605 context.fillStyle = "#666666"; 606 context.fillRect(8, 8, 48, 24); 607 608 return new THREE.CanvasTexture(canvas); 609} 610 611function getCarSideTexture() { 612 const canvas = document.createElement("canvas"); 613 canvas.width = 128; 614 canvas.height = 32; 615 const context = canvas.getContext("2d"); 616 617 context.fillStyle = "#ffffff"; 618 context.fillRect(0, 0, 128, 32); 619 620 context.fillStyle = "#666666"; 621 context.fillRect(10, 8, 38, 24); 622 context.fillRect(58, 8, 60, 24); 623 624 return new THREE.CanvasTexture(canvas); 625} 626 627function Car() { 628 const car = new THREE.Group(); 629 630 const color = pickRandom(vehicleColors); 631 632 const main = new THREE.Mesh( 633 new THREE.BoxBufferGeometry(60, 30, 15), 634 new THREE.MeshLambertMaterial({ color }) 635 ); 636 main.position.z = 12; 637 main.castShadow = true; 638 main.receiveShadow = true; 639 car.add(main); 640 641 const carFrontTexture = getCarFrontTexture(); 642 carFrontTexture.center = new THREE.Vector2(0.5, 0.5); 643 carFrontTexture.rotation = Math.PI / 2; 644 645 const carBackTexture = getCarFrontTexture(); 646 carBackTexture.center = new THREE.Vector2(0.5, 0.5); 647 carBackTexture.rotation = -Math.PI / 2; 648 649 const carLeftSideTexture = getCarSideTexture(); 650 carLeftSideTexture.flipY = false; 651 652 const carRightSideTexture = getCarSideTexture(); 653 654 const cabin = new THREE.Mesh(new THREE.BoxBufferGeometry(33, 24, 12), [ 655 new THREE.MeshLambertMaterial({ map: carFrontTexture }), 656 new THREE.MeshLambertMaterial({ map: carBackTexture }), 657 new THREE.MeshLambertMaterial({ map: carLeftSideTexture }), 658 new THREE.MeshLambertMaterial({ map: carRightSideTexture }), 659 new THREE.MeshLambertMaterial({ color: 0xffffff }), // top 660 new THREE.MeshLambertMaterial({ color: 0xffffff }) // bottom 661 ]); 662 cabin.position.x = -6; 663 cabin.position.z = 25.5; 664 cabin.castShadow = true; 665 cabin.receiveShadow = true; 666 car.add(cabin); 667 668 const backWheel = new Wheel(); 669 backWheel.position.x = -18; 670 car.add(backWheel); 671 672 const frontWheel = new Wheel(); 673 frontWheel.position.x = 18; 674 car.add(frontWheel); 675 676 if (config.showHitZones) { 677 car.userData.hitZone1 = HitZone(); 678 car.userData.hitZone2 = HitZone(); 679 } 680 681 return car; 682} 683 684function getTruckFrontTexture() { 685 const canvas = document.createElement("canvas"); 686 canvas.width = 32; 687 canvas.height = 32; 688 const context = canvas.getContext("2d"); 689 690 context.fillStyle = "#ffffff"; 691 context.fillRect(0, 0, 32, 32); 692 693 context.fillStyle = "#666666"; 694 context.fillRect(0, 5, 32, 10); 695 696 return new THREE.CanvasTexture(canvas); 697} 698 699function getTruckSideTexture() { 700 const canvas = document.createElement("canvas"); 701 canvas.width = 32; 702 canvas.height = 32; 703 const context = canvas.getContext("2d"); 704 705 context.fillStyle = "#ffffff"; 706 context.fillRect(0, 0, 32, 32); 707 708 context.fillStyle = "#666666"; 709 context.fillRect(17, 5, 15, 10); 710 711 return new THREE.CanvasTexture(canvas); 712} 713 714function Truck() { 715 const truck = new THREE.Group(); 716 const color = pickRandom(vehicleColors); 717 718 const base = new THREE.Mesh( 719 new THREE.BoxBufferGeometry(100, 25, 5), 720 new THREE.MeshLambertMaterial({ color: 0xb4c6fc }) 721 ); 722 base.position.z = 10; 723 truck.add(base); 724 725 const cargo = new THREE.Mesh( 726 new THREE.BoxBufferGeometry(75, 35, 40), 727 new THREE.MeshLambertMaterial({ color: 0xffffff }) // 0xb4c6fc 728 ); 729 cargo.position.x = -15; 730 cargo.position.z = 30; 731 cargo.castShadow = true; 732 cargo.receiveShadow = true; 733 truck.add(cargo); 734 735 const truckFrontTexture = getTruckFrontTexture(); 736 truckFrontTexture.center = new THREE.Vector2(0.5, 0.5); 737 truckFrontTexture.rotation = Math.PI / 2; 738 739 const truckLeftTexture = getTruckSideTexture(); 740 truckLeftTexture.flipY = false; 741 742 const truckRightTexture = getTruckSideTexture(); 743 744 const cabin = new THREE.Mesh(new THREE.BoxBufferGeometry(25, 30, 30), [ 745 new THREE.MeshLambertMaterial({ color, map: truckFrontTexture }), 746 new THREE.MeshLambertMaterial({ color }), // back 747 new THREE.MeshLambertMaterial({ color, map: truckLeftTexture }), 748 new THREE.MeshLambertMaterial({ color, map: truckRightTexture }), 749 new THREE.MeshLambertMaterial({ color }), // top 750 new THREE.MeshLambertMaterial({ color }) // bottom 751 ]); 752 cabin.position.x = 40; 753 cabin.position.z = 20; 754 cabin.castShadow = true; 755 cabin.receiveShadow = true; 756 truck.add(cabin); 757 758 const backWheel = Wheel(); 759 backWheel.position.x = -30; 760 truck.add(backWheel); 761 762 const middleWheel = Wheel(); 763 middleWheel.position.x = 10; 764 truck.add(middleWheel); 765 766 const frontWheel = Wheel(); 767 frontWheel.position.x = 38; 768 truck.add(frontWheel); 769 770 if (config.showHitZones) { 771 truck.userData.hitZone1 = HitZone(); 772 truck.userData.hitZone2 = HitZone(); 773 truck.userData.hitZone3 = HitZone(); 774 } 775 776 return truck; 777} 778 779function HitZone() { 780 const hitZone = new THREE.Mesh( 781 new THREE.CylinderGeometry(20, 20, 60, 30), 782 new THREE.MeshLambertMaterial({ color: 0xff0000 }) 783 ); 784 hitZone.position.z = 25; 785 hitZone.rotation.x = Math.PI / 2; 786 787 scene.add(hitZone); 788 return hitZone; 789} 790 791function Wheel() { 792 const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial); 793 wheel.position.z = 6; 794 wheel.castShadow = false; 795 wheel.receiveShadow = false; 796 return wheel; 797} 798 799function Tree() { 800 const tree = new THREE.Group(); 801 802 const trunk = new THREE.Mesh(treeTrunkGeometry, treeTrunkMaterial); 803 trunk.position.z = 10; 804 trunk.castShadow = true; 805 trunk.receiveShadow = true; 806 trunk.matrixAutoUpdate = false; 807 tree.add(trunk); 808 809 const treeHeights = [45, 60, 75]; 810 const height = pickRandom(treeHeights); 811 812 const crown = new THREE.Mesh( 813 new THREE.SphereGeometry(height / 2, 30, 30), 814 treeCrownMaterial 815 ); 816 crown.position.z = height / 2 + 30; 817 crown.castShadow = true; 818 crown.receiveShadow = false; 819 tree.add(crown); 820 821 return tree; 822} 823 824accelerateButton.addEventListener("mousedown", function () { 825 startGame(); 826 accelerate = true; 827}); 828decelerateButton.addEventListener("mousedown", function () { 829 startGame(); 830 decelerate = true; 831}); 832accelerateButton.addEventListener("mouseup", function () { 833 accelerate = false; 834}); 835decelerateButton.addEventListener("mouseup", function () { 836 decelerate = false; 837}); 838window.addEventListener("keydown", function (event) { 839 if (event.key == "ArrowUp") { 840 startGame(); 841 accelerate = true; 842 return; 843 } 844 if (event.key == "ArrowDown") { 845 decelerate = true; 846 return; 847 } 848 if (event.key == "R" || event.key == "r") { 849 reset(); 850 return; 851 } 852}); 853window.addEventListener("keyup", function (event) { 854 if (event.key == "ArrowUp") { 855 accelerate = false; 856 return; 857 } 858 if (event.key == "ArrowDown") { 859 decelerate = false; 860 return; 861 } 862}); 863 864function animation(timestamp) { 865 if (!lastTimestamp) { 866 lastTimestamp = timestamp; 867 return; 868 } 869 870 const timeDelta = timestamp - lastTimestamp; 871 872 movePlayerCar(timeDelta); 873 874 const laps = Math.floor(Math.abs(playerAngleMoved) / (Math.PI * 2)); 875 876 // Update score if it changed 877 if (laps != score) { 878 score = laps; 879 scoreElement.innerText = score; 880 } 881 882 // Add a new vehicle at the beginning and with every 5th lap 883 if (otherVehicles.length < (laps + 1) / 5) addVehicle(); 884 885 moveOtherVehicles(timeDelta); 886 887 hitDetection(); 888 889 renderer.render(scene, camera); 890 lastTimestamp = timestamp; 891} 892 893function movePlayerCar(timeDelta) { 894 const playerSpeed = getPlayerSpeed(); 895 playerAngleMoved -= playerSpeed * timeDelta; 896 897 const totalPlayerAngle = playerAngleInitial + playerAngleMoved; 898 899 const playerX = Math.cos(totalPlayerAngle) * trackRadius - arcCenterX; 900 const playerY = Math.sin(totalPlayerAngle) * trackRadius; 901 902 playerCar.position.x = playerX; 903 playerCar.position.y = playerY; 904 905 playerCar.rotation.z = totalPlayerAngle - Math.PI / 2; 906} 907 908function moveOtherVehicles(timeDelta) { 909 otherVehicles.forEach((vehicle) => { 910 if (vehicle.clockwise) { 911 vehicle.angle -= speed * timeDelta * vehicle.speed; 912 } else { 913 vehicle.angle += speed * timeDelta * vehicle.speed; 914 } 915 916 const vehicleX = Math.cos(vehicle.angle) * trackRadius + arcCenterX; 917 const vehicleY = Math.sin(vehicle.angle) * trackRadius; 918 const rotation = 919 vehicle.angle + (vehicle.clockwise ? -Math.PI / 2 : Math.PI / 2); 920 vehicle.mesh.position.x = vehicleX; 921 vehicle.mesh.position.y = vehicleY; 922 vehicle.mesh.rotation.z = rotation; 923 }); 924} 925 926function getPlayerSpeed() { 927 if (accelerate) return speed * 2; 928 if (decelerate) return speed * 0.5; 929 return speed; 930} 931 932function addVehicle() { 933 const vehicleTypes = ["car", "truck"]; 934 935 const type = pickRandom(vehicleTypes); 936 const speed = getVehicleSpeed(type); 937 const clockwise = Math.random() >= 0.5; 938 939 const angle = clockwise ? Math.PI / 2 : -Math.PI / 2; 940 941 const mesh = type == "car" ? Car() : Truck(); 942 scene.add(mesh); 943 944 otherVehicles.push({ mesh, type, speed, clockwise, angle }); 945} 946 947function getVehicleSpeed(type) { 948 if (type == "car") { 949 const minimumSpeed = 1; 950 const maximumSpeed = 2; 951 return minimumSpeed + Math.random() * (maximumSpeed - minimumSpeed); 952 } 953 if (type == "truck") { 954 const minimumSpeed = 0.6; 955 const maximumSpeed = 1.5; 956 return minimumSpeed + Math.random() * (maximumSpeed - minimumSpeed); 957 } 958} 959 960function getHitZonePosition(center, angle, clockwise, distance) { 961 const directionAngle = angle + clockwise ? -Math.PI / 2 : +Math.PI / 2; 962 return { 963 x: center.x + Math.cos(directionAngle) * distance, 964 y: center.y + Math.sin(directionAngle) * distance 965 }; 966} 967 968function hitDetection() { 969 const playerHitZone1 = getHitZonePosition( 970 playerCar.position, 971 playerAngleInitial + playerAngleMoved, 972 true, 973 15 974 ); 975 976 const playerHitZone2 = getHitZonePosition( 977 playerCar.position, 978 playerAngleInitial + playerAngleMoved, 979 true, 980 -15 981 ); 982 983 if (config.showHitZones) { 984 playerCar.userData.hitZone1.position.x = playerHitZone1.x; 985 playerCar.userData.hitZone1.position.y = playerHitZone1.y; 986 987 playerCar.userData.hitZone2.position.x = playerHitZone2.x; 988 playerCar.userData.hitZone2.position.y = playerHitZone2.y; 989 } 990 991 const hit = otherVehicles.some((vehicle) => { 992 if (vehicle.type == "car") { 993 const vehicleHitZone1 = getHitZonePosition( 994 vehicle.mesh.position, 995 vehicle.angle, 996 vehicle.clockwise, 997 15 998 ); 999 1000 const vehicleHitZone2 = getHitZonePosition( 1001 vehicle.mesh.position, 1002 vehicle.angle, 1003 vehicle.clockwise, 1004 -15 1005 ); 1006 1007 if (config.showHitZones) { 1008 vehicle.mesh.userData.hitZone1.position.x = vehicleHitZone1.x; 1009 vehicle.mesh.userData.hitZone1.position.y = vehicleHitZone1.y; 1010 1011 vehicle.mesh.userData.hitZone2.position.x = vehicleHitZone2.x; 1012 vehicle.mesh.userData.hitZone2.position.y = vehicleHitZone2.y; 1013 } 1014 1015 // The player hits another vehicle 1016 if (getDistance(playerHitZone1, vehicleHitZone1) < 40) return true; 1017 if (getDistance(playerHitZone1, vehicleHitZone2) < 40) return true; 1018 1019 // Another vehicle hits the player 1020 if (getDistance(playerHitZone2, vehicleHitZone1) < 40) return true; 1021 } 1022 1023 if (vehicle.type == "truck") { 1024 const vehicleHitZone1 = getHitZonePosition( 1025 vehicle.mesh.position, 1026 vehicle.angle, 1027 vehicle.clockwise, 1028 35 1029 ); 1030 1031 const vehicleHitZone2 = getHitZonePosition( 1032 vehicle.mesh.position, 1033 vehicle.angle, 1034 vehicle.clockwise, 1035 0 1036 ); 1037 1038 const vehicleHitZone3 = getHitZonePosition( 1039 vehicle.mesh.position, 1040 vehicle.angle, 1041 vehicle.clockwise, 1042 -35 1043 ); 1044 1045 if (config.showHitZones) { 1046 vehicle.mesh.userData.hitZone1.position.x = vehicleHitZone1.x; 1047 vehicle.mesh.userData.hitZone1.position.y = vehicleHitZone1.y; 1048 1049 vehicle.mesh.userData.hitZone2.position.x = vehicleHitZone2.x; 1050 vehicle.mesh.userData.hitZone2.position.y = vehicleHitZone2.y; 1051 1052 vehicle.mesh.userData.hitZone3.position.x = vehicleHitZone3.x; 1053 vehicle.mesh.userData.hitZone3.position.y = vehicleHitZone3.y; 1054 } 1055 1056 // The player hits another vehicle 1057 if (getDistance(playerHitZone1, vehicleHitZone1) < 40) return true; 1058 if (getDistance(playerHitZone1, vehicleHitZone2) < 40) return true; 1059 if (getDistance(playerHitZone1, vehicleHitZone3) < 40) return true; 1060 1061 // Another vehicle hits the player 1062 if (getDistance(playerHitZone2, vehicleHitZone1) < 40) return true; 1063 } 1064 }); 1065 1066 if (hit) { 1067 if (resultsElement) resultsElement.style.display = "flex"; 1068 renderer.setAnimationLoop(null); // Stop animation loop 1069 } 1070} 1071 1072window.addEventListener("resize", () => { 1073 console.log("resize", window.innerWidth, window.innerHeight); 1074 1075 // Adjust camera 1076 const newAspectRatio = window.innerWidth / window.innerHeight; 1077 const adjustedCameraHeight = cameraWidth / newAspectRatio; 1078 1079 camera.top = adjustedCameraHeight / 2; 1080 camera.bottom = adjustedCameraHeight / -2; 1081 camera.updateProjectionMatrix(); // Must be called after change 1082 1083 positionScoreElement(); 1084 1085 // Reset renderer 1086 renderer.setSize(window.innerWidth, window.innerHeight); 1087 renderer.render(scene, camera); 1088});

The JavaScript engine orchestrates all Three.js rendering, game logic, and physics. Key systems include:

Three.js Scene Setup

  • Camera: Orthographic projection (not perspective) for isometric view

    • Width: 960 pixels, adjusts height based on aspect ratio
    • Position: (0, -210, 300) for angled top-down view
    • Looking at origin (0, 0, 0)
  • Lighting:

    • Ambient light (white, 0.6 intensity) provides baseline illumination
    • Directional light (sun-like) with shadow mapping enabled
    • Shadow camera covers 800×700px area with 1024×1024 resolution
  • Renderer: WebGL with anti-aliasing, high performance preference

Game Track System

Complex mathematical track design using circular arcs:

  • Two circular tracks with radius 225px and width 45px
  • Inner radius: 180px, Outer radius: 270px
  • Figure-eight layout with two circular sections offset by arcCenterX

Procedural Texture Generation

Canvas-based textures created dynamically:

  • Line Markings: Dashed lane divider lines drawn on canvas
  • Curbs Texture: Grass areas with colored stripe patterns
  • Car Textures: Windshield and side window details painted on canvas
  • Truck Textures: Different cabin and cargo area designs

All textures applied as THREE.CanvasTexture for performance.

Vehicle System

Two vehicle types with distinct geometry:

Cars:

  • Body (main box, 60×30×15 units)
  • Cabin (33×24×12 units with textured windows)
  • Two wheels (front and back)
  • Random colors from vehicleColors array

Trucks:

  • Base platform (100×25×5 units)
  • Cargo section (75×35×40 units)
  • Cabin (25×30×30 units)
  • Three wheels (back, middle, front)

Both cast and receive shadows for realistic rendering.

Player Control System

  • Accelerate key (UP arrow or button) - Increases player speed
  • Decelerate key (DOWN arrow or button) - Decreases player speed
  • Reset key (R) - Restarts game after collision

Speed multipliers applied for acceleration (higher speed) and deceleration (lower speed).

Player Movement

Player vehicle follows circular track with configurable speed:

  • Position calculated using trigonometry (cos, sin) of track radius
  • Rotation aligned to track tangent (totalPlayerAngle - Math.PI/2)
  • Speed modulated by acceleration/deceleration input
  • Global position updated each frame via animation loop

AI Vehicle Spawning

Vehicles spawn randomly on the track:

  • New vehicle added at start and then every 5th lap
  • Random selection between car or truck
  • Random clockwise or counter-clockwise direction
  • Random speed based on vehicle type

Each vehicle tracks: mesh, type, speed, direction, angle.

AI Vehicle Movement

Opponent vehicles move along figure-eight track:

  • Speed varies: cars 0.002-0.004, trucks 0.001-0.002
  • Direction alternates between tracks
  • Position updated using same circular motion math as player
  • Removed from scene when off-screen to optimize performance

Collision Detection System

Hit zone based collision testing:

  • Calculates hit zone position ahead of each vehicle
  • Tests distance between player hit zone and AI hit zone
  • Uses Pythagorean theorem for 2D distance calculation
  • Collision ends game and displays results modal

Game State Management

  • score - Tracks number of completed laps
  • gameInProgress - Boolean flag for active gameplay
  • ready - Indicates game is initialized and waiting for input
  • accelerate, decelerate - Input state flags

Game Loop

RequestAnimationFrame maintains 60 FPS:

  1. Calculate delta time since last frame
  2. Move player car based on input and speed
  3. Calculate current lap count
  4. Spawn new vehicles as needed
  5. Move all AI vehicles
  6. Test collisions
  7. Render scene with camera and lighting

Input Handling

Mouse/Touch buttons and keyboard shortcuts:

  • Accelerate button: mousedown/up events
  • Decelerate button: mousedown/up events
  • Arrow keys: keydown/keyup events
  • R key: triggers game reset

Multiple input methods support both desktop (keyboard) and mobile (touch buttons).

Rendering Pipeline

Three.js WebGL renderer processes:

  1. Applies lights and shadows to all objects
  2. Renders player car at current position
  3. Renders all AI vehicles
  4. Renders track and ground plane
  5. Renders trees and environmental objects
  6. Outputs to canvas element

Advanced Three.js Concepts

The game demonstrates:

  • Orthographic vs Perspective cameras
  • Group objects (vehicles composed of sub-meshes)
  • Material types (Lambert material for realistic lighting)
  • Shadow rendering and shadow maps
  • Canvas textures for procedural graphics
  • Extruded geometries from 2D shapes
  • Complex scene hierarchy with nested objects
  • Dynamic object creation and destruction
  • Responsive camera and renderer resizing

Track Architecture

Mathematical precision ensures:

  • Smooth circular paths using trigonometric calculations
  • Proper arc angles for figure-eight layout
  • Offset track centers (arcCenterX) for dual circuits
  • Collision boundaries based on track geometry

This comprehensive system creates a polished 3D racing game that showcases advanced Three.js capabilities while maintaining smooth 60 FPS performance and intuitive gameplay mechanics.

Love this component?

Explore more components and build amazing UIs.

View All Components