: 阅读量 次
极速狂飙 - 3D赛车游戏技术文档
极速狂飙 - 3D赛车游戏技术文档
基于 Three.js 构建的浏览器3D赛车游戏完整技术说明
版本: v1.1.0 | 更新日期: 2026-07-10
📋 项目概述
项目简介
《极速狂飙》是一款基于 Three.js 构建的 3D 赛车游戏,具备完整的物理引擎、赛道系统、圈速计时和车辆控制系统。支持 GLB 模型加载,实现了真实的轮胎滚动和转向效果。
技术栈
| 技术 |
版本 |
用途 |
| Three.js |
0.160.0 |
3D 渲染引擎 |
| jQuery |
3.7.1 |
DOM 操作与事件绑定 |
| GLTFLoader |
- |
GLB/GLTF 模型加载 |
| HTML5 Canvas |
- |
小地图渲染 |
| CSS3 |
- |
界面样式与动画 |
项目结构
极速狂飙/
├── index.html # 主游戏文件(包含所有逻辑)
├── models/ # 3D 模型目录
│ ├── Toyota-RAV4.glb
│ ├── Ford-F150.glb
│ ├── FORD-Mustang2019.glb
│ └── FORD-Mustang2016.glb
├── js/
│ ├── three.module.js # Three.js 核心库
│ └── jquery-3.7.1.min.js # jQuery 库
└── css/
├── google-fonts.css # Google 字体样式
└── font-awesome.min.css # Font Awesome 图标库
🏗️ 系统架构
核心模块关系图
┌─────────────────────────────────────────────────────────────────┐
│ 游戏主循环 │
│ requestAnimationFrame │
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 物理引擎 │ │ 渲染系统 │ │ HUD 系统 │
│ │ │ │ │ │
│ • 速度计算 │ │ • 3D 场景渲染 │ │ • 速度表 │
│ • 转向控制 │ │ • 相机跟随 │ │ • 圈速显示 │
│ • 路面检测 │ │ • 阴影处理 │ │ • 小地图 │
│ • 轮胎控制 │ │ • 模型加载 │ │ • 路面指示 │
│ • 圈速检测 │ │ • 光照系统 │ │ • 档位显示 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└─────────────────────┼─────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────┐
│ 数据模型 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 车辆状态: { pos, heading, speed } │ │
│ │ 赛道数据: { controlPoints, curve, segments } │ │
│ │ 游戏状态: { lapCount, lapTime, bestLap, gameState } │ │
│ │ 输入状态: { up, down, left, right, space } │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
游戏状态机
┌─────────┐ 点击"开始比赛" ┌───────────┐ 倒计时结束 ┌──────────┐
│ MENU │ ─────────────────► │ COUNTDOWN │ ──────────────► │ PLAYING │
│ 菜单 │ │ 倒计时 │ │ 游戏中 │
└─────────┘ └───────────┘ └──────────┘
▲ │ │
│ │ │
│ 点击"再来一局" │ 按 ESC 键 │
└────────────────────────────────┼────────────────────────────┘
▼
┌──────────┐ 点击"继续" ┌──────────┐
│ PAUSED │ ──────────────► │ PLAYING │
│ 暂停中 │ │ 游戏中 │
└──────────┘ └──────────┘
│
│ 完成 3 圈
▼
┌──────────┐
│ FINISHED │
│ 已完成 │
└──────────┘
🎮 游戏设计
游戏规则
| 规则项 |
说明 |
| 比赛圈数 |
3 圈 |
| 计时方式 |
从出发开始计时 |
| 最佳单圈 |
自动记录最快圈速 |
| 路面影响 |
草地速度 = 赛道速度 × 90% |
| 漂移模式 |
空格键激活,转向更灵敏 |
控制方案
| 按键 |
功能 |
说明 |
| ↑ |
加速 |
持续按住加速 |
| ↓ |
刹车/倒车 |
减速或倒车 |
| ← |
左转 |
转向左侧 |
| → |
右转 |
转向右侧 |
| Space |
漂移模式 |
按住进入漂移 |
| ESC |
暂停/继续 |
切换暂停状态 |
| 鼠标拖拽 |
视角旋转 |
拖拽画布旋转视角 |
| 滚轮 |
缩放 |
滚动缩放视角 |
🏎️ 车辆系统
物理参数配置
const CFG = {
// ========== 速度限制 ==========
maxSpeed: 75, // 赛道最大速度 (约 270 km/h)
maxReverse: -18, // 倒车最大速度 (约 -65 km/h)
// ========== 加减速性能 ==========
accel: 28, // 加速度 (单位/秒²)
brake: 45, // 刹车减速度 (单位/秒²)
// ========== 摩擦力 ==========
friction: 0.985, // 赛道摩擦力系数
grassFriction: 0.97, // 草地摩擦力系数
driftFriction: 0.997, // 漂移摩擦力系数
// ========== 转向控制 ==========
turnSpeed: 1.8, // 基础转向速度 (弧度/秒)
driftTurnMul: 1.6, // 漂移转向倍率
// ========== 比赛规则 ==========
totalLaps: 3, // 总圈数
};
物理更新循环
function updatePhysics(dt) {
// 1. 加速/刹车
if (keys.up) carState.speed += CFG.accel * dt;
if (keys.down) {
carState.speed > 0.5
? (carState.speed -= CFG.brake * dt)
: (carState.speed -= CFG.accel * 0.4 * dt);
}
// 2. 摩擦力计算
const isDrift = keys.space;
const onTrack = getSurfaceInfo();
if (isDrift) {
carState.speed *= CFG.driftFriction;
} else if (!onTrack) {
carState.speed *= CFG.grassFriction;
} else {
carState.speed *= Math.pow(CFG.friction, dt * 60);
}
// 3. 速度限制
const maxSpd = onTrack ? CFG.maxSpeed : CFG.maxSpeed * GRASS_SPEED_RATIO;
carState.speed = Math.max(CFG.maxReverse, Math.min(maxSpd, carState.speed));
if (Math.abs(carState.speed) < 0.08) carState.speed = 0;
// 4. 转向计算
const sf = Math.min(1, Math.abs(carState.speed) / 10);
const tm = isDrift ? CFG.driftTurnMul : 1;
const sign = carState.speed >= 0 ? 1 : -1;
const ta = CFG.turnSpeed * tm * sf * dt * sign;
if (keys.left) carState.heading += ta;
if (keys.right) carState.heading -= ta;
// 5. 位置更新
carState.pos.x += Math.sin(carState.heading) * carState.speed * dt;
carState.pos.z += Math.cos(carState.heading) * carState.speed * dt;
// 6. 软边界约束
const {t, distSq} = getClosestT(carState.pos);
const maxD = CFG.trackWidth / 2 + 800;
if (Math.sqrt(distSq) > maxD) {
const cp = trackCurve.getPointAt(t);
carState.pos.copy(cp).add(
carState.pos.clone().sub(cp).normalize().multiplyScalar(maxD)
);
carState.speed *= 0.2;
}
}
车辆模型系统
默认赛车(内置)
使用 Three.js 基础几何体构建:
| 部件 |
几何体 |
材质颜色 |
| 底盘 |
BoxGeometry |
#222222 |
| 车身 |
BoxGeometry |
#ff2200 |
| 车顶 |
BoxGeometry |
#dd1800 |
| 车窗 |
BoxGeometry |
#556677 (玻璃) |
| 轮胎 |
CylinderGeometry |
#1a1a1a |
| 轮毂 |
CylinderGeometry |
#aaaaaa |
| 头灯 |
BoxGeometry |
#ffffee (自发光) |
| 尾灯 |
BoxGeometry |
#ff0000 (自发光) |
GLB 模型加载
async function loadGLBModel(path) {
try {
const gltf = await gltfLoader.loadAsync(path);
const model = gltf.scene;
// 自动缩放模型
const box = new THREE.Box3().setFromObject(model);
const size = box.getSize(new THREE.Vector3());
const maxDim = Math.max(size.x, size.y, size.z);
const scale = 10.0 / maxDim;
model.scale.setScalar(scale);
// 居中处理
const center = box.getCenter(new THREE.Vector3());
model.position.sub(center.multiplyScalar(scale));
model.position.y = 0.45;
return model;
} catch (error) {
console.error('GLB 加载失败:', error);
return null;
}
}
轮胎控制系统
轮胎识别策略
| 策略 |
方法 |
说明 |
| 名称匹配 |
查找包含 wheel/tire/rim 的节点 |
适用于标准命名模型 |
| 几何体识别 |
检测圆柱体/圆环体 Mesh |
适用于复杂模型 |
| 位置聚类 |
K-means 聚类找 4 个角落 |
适用于任意模型 |
| 顶点筛选 |
取最小顶点数的 Mesh |
备选方案 |
轮胎分组算法(K-means 聚类)
function findWheelGroups(meshes) {
// 1. 收集所有 Mesh 的世界位置
const positions = meshes.map(m => m.worldPos);
// 2. K-means 聚类 (K=4)
const clusters = [];
const threshold = 1.5; // 距离阈值
positions.forEach(pos => {
let found = false;
for (const cluster of clusters) {
const dist = distance(pos, cluster.center);
if (dist < threshold) {
cluster.meshes.push(pos);
updateCenter(cluster);
found = true;
break;
}
}
if (!found) {
clusters.push({
center: { x: pos.x, z: pos.z },
meshes: [pos]
});
}
});
// 3. 取最大的 4 个组作为轮胎
clusters.sort((a, b) => b.meshes.length - a.meshes.length);
return clusters.slice(0, 4);
}
轮胎旋转控制
function updateWheels(dt) {
const speed = carState.speed;
const isDrift = keys.space;
// 1. 滚动速度 (所有轮胎)
const rollMul = isDrift ? 4.0 : 2.5;
const rollSpeed = speed * dt * rollMul;
// 2. 前轮转向角度
const speedFactor = Math.min(1, Math.abs(speed) / 30);
const turnInput = (keys.left ? 1 : 0) - (keys.right ? 1 : 0);
const maxAngle = isDrift ? 0.7 : 0.5;
const turnAngle = turnInput * maxAngle * speedFactor;
// 3. 更新所有轮胎滚动
wheelData.all.forEach(wheel => {
wheel.rotation.x += rollSpeed;
});
// 4. 更新前轮转向
wheelData.front.forEach(wheel => {
const base = wheel.userData?.baseRotation?.y || 0;
wheel.rotation.y = base + turnAngle;
});
}
🛤️ 赛道系统
赛道生成
// 控制点定义 (17 个点形成闭环)
const trackCPs = [
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(45, 0, -5),
new THREE.Vector3(90, 0, -15),
new THREE.Vector3(120, 0, -45),
new THREE.Vector3(125, 0, -85),
new THREE.Vector3(105, 0, -115),
new THREE.Vector3(65, 0, -125),
new THREE.Vector3(35, 0, -105),
new THREE.Vector3(15, 0, -75),
new THREE.Vector3(-15, 0, -55),
new THREE.Vector3(-35, 0, -75),
new THREE.Vector3(-55, 0, -105),
new THREE.Vector3(-80, 0, -90),
new THREE.Vector3(-90, 0, -55),
new THREE.Vector3(-85, 0, -15),
new THREE.Vector3(-60, 0, 2),
new THREE.Vector3(-25, 0, 5),
];
// 创建 CatmullRom 曲线 (闭环)
const trackCurve = new THREE.CatmullRomCurve3(
trackCPs,
true, // 闭环
'catmullrom', // 曲线类型
0.5 // 张力参数
);
赛道组件
| 组件 |
实现方式 |
功能 |
| 路面 |
BufferGeometry + Canvas 纹理 |
主要赛道表面 |
| 路肩 |
红白相间网格 |
赛道边界标识 |
| 起跑线 |
棋盘格纹理 |
比赛起点标记 |
| 路灯 |
CylinderGeometry + SphereGeometry |
赛道照明 |
| 树木 |
InstancedMesh (150 棵) |
环境装饰 |
圈速检测系统
function checkLap(currentT) {
// 象限检测法
const quadrant = Math.floor(currentT * 4) % 4;
quadrantFlags[quadrant] = true;
// 检测是否完整通过所有象限
if (lastTrackT > 0.85 && currentT < 0.15 &&
quadrantFlags.every(f => f)) {
lapCount++;
quadrantFlags.fill(false);
const now = performance.now();
const lapTime = now - lapStartTime;
lapStartTime = now;
if (lapTime < bestLapTime) bestLapTime = lapTime;
if (lapCount >= CFG.totalLaps) {
finishRace();
} else {
showToast(`第 ${lapCount} 圈完成 ${formatTime(lapTime)}`);
}
}
lastTrackT = currentT;
}
🎥 相机系统
视角预设
| 视角 |
俯仰角 (rad) |
距离 |
特点 |
| 追踪 (Chase) |
0.32 |
25 |
默认跟车视角 |
| 近跟 (Close) |
0.22 |
10 |
近距离跟随 |
| 俯视 (Top) |
1.45 |
50 |
鸟瞰视角 |
| 车头 (Hood) |
1.50 |
2.2 |
第一人称 |
相机跟随算法
function updateCamera() {
// 平滑插值
camTheta += (camTargetTheta - camTheta) * CFG.cameraLerp;
camPhi += (camTargetPhi - camPhi) * CFG.cameraLerp;
camDist += (camTargetDist - camDist) * CFG.cameraLerp;
// 计算偏移位置
const offset = new THREE.Vector3(
Math.sin(camTheta) * Math.cos(camPhi) * camDist,
Math.sin(camPhi) * camDist,
Math.cos(camTheta) * Math.cos(camPhi) * camDist
);
// 目标点:车辆位置上方
const lookTarget = carGroup.position.clone().add(
new THREE.Vector3(0, 1.2, 0)
);
// 应用位置
camera.position.lerp(lookTarget.clone().add(offset), 0.09);
camera.lookAt(lookTarget);
}
交互控制
| 交互方式 |
控制效果 |
| 鼠标拖拽 |
旋转视角 (Theta/Phi) |
| 滚轮 |
缩放距离 |
| 触摸拖拽 |
旋转视角 (移动端) |
| 按钮切换 |
快速切换预设视角 |
📊 HUD 系统
界面组件布局
┌──────────────────────────────────────────────────────────────┐
│ [视角按钮] [暂停按钮] [圈速信息] │
│ 追踪 近跟 俯视 车头 ⏸ 圈数: 1/3 │
│ 当前: 00:12.345 │
│ 最佳: 00:11.234 │
│ 总时: 00:12.345 │
│ │
│ │
│ 速度表 │
│ ┌─────────┐ │
│ │ 128 │ [小地图] │
│ │ KM/H │ ┌────────┐ │
│ │ ██████░░ │ │ 🏁 │ │
│ │ 6档 │ └────────┘ │
│ └─────────┘ │
│ [路面指示: 🏁 赛道 100%] │
└──────────────────────────────────────────────────────────────┘
HUD 更新逻辑
function updateHUD() {
// 速度更新
const kmh = Math.abs(Math.round(carState.speed * 3.6));
$('#speed-value').text(kmh);
$('#speed-fill').css('width', Math.min(100, kmh / 270 * 100) + '%');
// 档位计算
let gear = 'N';
if (carState.speed < -0.5) gear = 'R';
else if (carState.speed > 0.5) {
if (kmh < 40) gear = '1';
else if (kmh < 80) gear = '2';
else if (kmh < 130) gear = '3';
else if (kmh < 180) gear = '4';
else if (kmh < 230) gear = '5';
else gear = '6';
}
$('#gear-display').text(gear);
// 圈速更新
const now = performance.now();
if (gameState === 'playing') {
$('#lap-time').text(formatTime(now - lapStartTime));
$('#total-time').text(formatTime(now - raceStartTime));
}
if (bestLapTime < Infinity) {
$('#best-time').text(formatTime(bestLapTime));
}
// 路面状态
const si = $('#surface-indicator');
if (getSurfaceInfo()) {
si.removeClass('grass').addClass('track')
.html('<i class="fas fa-road"></i> 赛道 100%');
} else {
si.removeClass('track').addClass('grass')
.html(`<i class="fas fa-seedling"></i> 草地 90%速度`);
}
}
小地图实现
function drawMinimap() {
const w = 300, h = 300;
mmCtx.clearRect(0, 0, w, h);
// 1. 计算赛道边界并映射到画布
// 2. 绘制赛道路径
mmCtx.strokeStyle = '#666';
mmCtx.lineWidth = 4;
mmCtx.beginPath();
for (let i = 0; i <= 200; i++) {
const p = trackCurve.getPointAt(i / 200);
const mx = (p.x - tCx) * mScale + w / 2;
const my = (p.z - tCz) * mScale + h / 2;
i === 0 ? mmCtx.moveTo(mx, my) : mmCtx.lineTo(mx, my);
}
mmCtx.closePath();
mmCtx.stroke();
// 3. 绘制车辆位置和朝向
const cx = (carState.pos.x - tCx) * mScale + w / 2;
const cy = (carState.pos.z - tCz) * mScale + h / 2;
mmCtx.strokeStyle = '#ff4d00';
mmCtx.lineWidth = 2;
mmCtx.beginPath();
mmCtx.moveTo(cx, cy);
mmCtx.lineTo(cx + Math.sin(carState.heading) * 8,
cy + Math.cos(carState.heading) * 8);
mmCtx.stroke();
// 4. 绘制车辆点
mmCtx.fillStyle = '#ff4d00';
mmCtx.beginPath();
mmCtx.arc(cx, cy, 4, 0, Math.PI * 2);
mmCtx.fill();
}
🔧 性能优化
渲染优化
| 优化技术 |
实现方式 |
性能收益 |
| 实例化渲染 |
InstancedMesh (树木) |
减少 Draw Call |
| 阴影优化 |
限制阴影贴图分辨率 2048×2048 |
提升帧率 |
| 帧率控制 |
Math.min(clock.getDelta(), 0.05) |
防止卡顿 |
| 视锥体裁剪 |
Three.js 自动处理 |
减少不可见物体渲染 |
内存管理
function replaceCarModel(newModel) {
// 清理旧模型资源
while (carGroup.children.length) {
const child = carGroup.children[0];
// 释放几何体
if (child.geometry) {
child.geometry.dispose();
}
// 释放材质
if (child.material) {
if (Array.isArray(child.material)) {
child.material.forEach(m => m.dispose());
} else {
child.material.dispose();
}
}
carGroup.remove(child);
}
// 添加新模型
carGroup.add(newModel);
}
性能监控
function showFPS() {
let frames = 0;
let lastTime = performance.now();
setInterval(() => {
const now = performance.now();
const fps = frames / ((now - lastTime) / 1000);
console.log(`FPS: ${fps.toFixed(1)}`);
frames = 0;
lastTime = now;
}, 1000);
// 在动画循环中: frames++;
}
🐛 调试工具
控制台调试命令
// 1. 查看模型结构
function debugModelStructure() {
const model = carGroup.children[0];
if (!model) {
console.log('❌ 没有加载模型');
return;
}
console.log('📦 模型:', model.name || '未命名');
model.traverse((child) => {
if (child.isMesh) {
console.log(' 🟦 Mesh:', child.name || '未命名',
'位置:', child.position);
}
});
}
// 2. 检查轮胎识别
function debugWheels() {
console.log('📊 轮胎数据:');
console.log(' 总数:', wheelData.all.length);
console.log(' 前轮:', wheelData.front.length);
console.log(' 后轮:', wheelData.rear.length);
wheelData.all.forEach((w, i) => {
console.log(` 轮胎${i+1}: `, w.position);
});
}
// 3. 检查赛道位置
function debugTrackPosition() {
const {t, distSq} = getClosestT(carState.pos);
console.log(`📍 赛道位置: t=${t.toFixed(3)}, 距离=${Math.sqrt(distSq).toFixed(2)}m`);
}
// 4. 性能监控
function showFPS() {
let frames = 0;
let lastTime = performance.now();
setInterval(() => {
const now = performance.now();
const fps = frames / ((now - lastTime) / 1000);
console.log(`📊 FPS: ${fps.toFixed(1)}`);
frames = 0;
lastTime = now;
}, 1000);
// 在动画循环中: frames++;
}
📦 部署指南
环境要求
| 要求 |
版本 |
| 浏览器 |
Chrome 80+, Firefox 75+, Safari 13+ |
| WebGL |
2.0 支持 |
| 本地服务器 |
任意 HTTP 服务器 (用于加载 GLB 模型) |
快速开始
# 使用 Python 启动本地服务器
python -m http.server 8000
# 使用 Node.js 的 serve
npx serve
# 使用 PHP 内置服务器
php -S localhost:8000
# 访问
http://localhost:8000
目录结构要求
project/
├── index.html # 主游戏文件
├── models/ # GLB 模型必须在此目录
│ └── *.glb
├── js/
│ ├── three.module.js
│ └── jquery-3.7.1.min.js
└── css/
├── google-fonts.css
└── font-awesome.min.css
常见问题
| 问题 |
解决方案 |
| GLB 模型加载失败 |
确保 models/ 目录存在且有 .glb 文件 |
| 跨域错误 |
使用本地服务器,不要直接打开 HTML |
| 黑屏 |
检查浏览器 WebGL 支持 |
| 性能问题 |
降低阴影分辨率或关闭阴影 |
🔮 未来扩展规划
短期计划 (v1.2)
中期计划 (v2.0)
长期计划 (v3.0)
📚 技术参考
相关链接
依赖版本
Three.js: 0.160.0
jQuery: 3.7.1
Font Awesome: 4.7.0
Google Fonts: Orbitron, Rajdhani
📝 更新日志
v1.1.0 (2026-07-10)
新增功能:
- ✅ 智能轮胎识别系统 (K-means 聚类)
- ✅ 轮胎分组功能
- ✅ 默认加载 GLB 模型
- ✅ 完整的调试工具集
- ✅ 性能监控工具
优化改进:
- 🔧 优化轮胎识别算法
- 🔧 改进模型加载流程
- 🔧 完善错误处理
v1.0.0 (2026-07-09)
初始版本:
- ✅ 完整赛道系统 (CatmullRom 曲线)
- ✅ 物理引擎 (速度/转向/摩擦)
- ✅ GLB 模型支持
- ✅ 轮胎控制系统
- ✅ 圈速计时 (3圈)
- ✅ 多视角相机 (4种预设)
- ✅ 小地图
- ✅ HUD 界面
- ✅ 鼠标/键盘/触摸控制
附注: 本文档随游戏代码同步更新,如有不一致之处,以代码为准。