基于ThreeJs实现3D车机大屏及自动避障效果
之前接到一个需求,说是给某个无人车做个啥配套的演示代码,但是仅仅只是提供一个Demo,所以细节上很多地方并没有深究。
不过虽然仅仅是一个Demo,但是其核心的功能基本都实现了:
- 道路生成
- 道路上障碍物生成(包括固定障碍物、车辆等)
- 车辆行进效果展示
- 避障道路切换展示
实现方案
使用Three.js实现3D车机大屏效果,先安装 Three.js
pnpm install three
我这里使用的版本是 v0.150.1。
初始化场景
通过 THREE.Scene() 对象生成简单的背景,以及环境光部分。
注意:环境光如果没有特殊的需求用默认的也没问题。
scene = new THREE.Scene()
scene.background = new THREE.Color(0xE8E8E8)
// 添加环境光
// const ambientLight = new THREE.AmbientLight(0xffffff, 0.8)
// scene.add(ambientLight)
初始化部分直接创建渲染器:
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(rendererCanvas.value.clientWidth, rendererCanvas.value.clientHeight)
renderer.setPixelRatio(window.devicePixelRatio)
renderer.shadowMap.enabled = false // 避免proxy错误
rendererCanvas.value.appendChild(renderer.domElement)
创建道路
通过 THREE.PlaneGeometry 对象创建道路,这里我是直接写死的,因为不牵扯屏幕适配问题。
const roadGeometry = new THREE.PlaneGeometry(24, 200) // 参数分别是 width / height
const roadMaterial = new THREE.MeshBasicMaterial({ color: 0x555555 })
road = new THREE.Mesh(roadGeometry, roadMaterial)
road.rotation.x = -Math.PI / 2
scene.add(road)
增加车道标线,这里我不是分开创建的,而是统一创建完以后再划分。
const lineGeometry = new THREE.PlaneGeometry(0.15, 2)
const lanePositions = [-8, -4, 0, 4, 8] // 5条车道分隔线,形成6个车道
lanePositions.forEach(x => {
for (let i = -50; i < 50; i += 4) { // 虚线间隔
const line = new THREE.Mesh(lineGeometry, dashedLineMaterial)
line.position.set(x, 0.01, i)
line.rotation.x = -Math.PI / 2
scene.add(line)
}
})
创建车辆
这里需要注意,一般来说更建议使用具体的车辆模型进行展示,效果更好。
这里因为我确实是没找到合适的车辆模型,就简单画了一个。
const carGroup = new THREE.Group()
// 车身
const bodyGeometry = new THREE.BoxGeometry(2, 1, 4)
// ...
carGroup.add(body)
// 车窗
const windowGeometry = new THREE.BoxGeometry(1.8, 0.8, 2)
// ...
carGroup.add(window)
// 车轮
const wheelGeometry = new THREE.CylinderGeometry(0.3, 0.3, 0.2, 16)
// ...
carGroup.add(wheel)
// 将车辆添加到画布
scene.add(carGroup)
这里需要注意,主体车辆行驶过程中需要增加跟随。
camera = new THREE.PerspectiveCamera(75, rendererCanvas.value.clientWidth / rendererCanvas.value.clientHeight, 0.1, 1000)
// 第三人称跟随视角
camera.position.set(0, 8, -15)
camera.lookAt(0, 0, 0)
创建障碍物
基本与创建车辆类似,但是是随机在车道上进行创建障碍物。
另外如果是车辆的话增加一个行驶速度。
const obstacle = new THREE.Group()
// 主体
const geometry = new THREE.BoxGeometry(type.size.w, type.size.h, type.size.d)
const material = new THREE.MeshBasicMaterial({ color: type.color })
const body = new THREE.Mesh(geometry, material)
obstacle.add(body)
// 随机位置(6车道系统,增加间距)
const lanes = [-10, -6, -2, 2, 6, 10] // 6个车道的中心位置
const lane = lanes[Math.floor(Math.random() * lanes.length)]
obstacle.position.set(lane, 0, Math.random() * 60 + 20) // 增加生成距离
// 添加移动速度
obstacle.userData = {
speed: Math.random() * 0.02 + 0.01,
type: type.type
}
障碍物检测 & 寻找安全车道
障碍物的检测和寻找安全车道效果基本上相同,都是在前后距离范围内检测是否存在障碍物,如果存在那么就认定障碍物检测成功。
如果当前车道存在,而其他车道不存在,则认定其他车道为安全车道。
// 检查是否在同一车道且在前方
if (Math.abs(obstacleX - carX) < 1.5 && // 同一车道范围
obstacleZ > carZ && // 在前方
obstacleZ - carZ < detectionDistance) { // 在检测距离内
return true
}
车道变换效果
这里需要注意,变换车道效果0.5s即完成,尤其是提示部分。
如果是实际项目此处应该配合真正的传感器和计算完成时间计算。
const deltaX = targetX - currentX
if (Math.abs(deltaX) > 0.1) {
car.position.x += Math.sign(deltaX) * laneChangeSpeed
} else {
// 变道完成
car.position.x = targetX
currentLane = targetLane
// 0.5秒后重置状态
setTimeout(() => {
if (!isChangingLane) {
laneChangeStatus.value = '正常行驶'
}
}, 500)
}
其他部分就没什么了,主要是运行过程中需要循环进行变道、道路标线的更新、障碍物的更新。
当然,我推荐你在卸载页面的时候释放掉资源。不释放也没啥太大影响倒是。
全部代码
<template>
<div class="car-three" ref="rendererCanvas">
<div class="controls">
<div class="speed-info">速度: {{ speed.toFixed(1) }} km/h</div>
<div class="obstacle-count">障碍物数量: {{ obstacleCount }}</div>
<div class="lane-info">当前车道: {{ currentLaneDisplay }}</div>
<div class="lane-change-status" :class="{ active: isChangingLaneDisplay }">
{{ laneChangeStatus }}
</div>
</div>
</div>
</template>
<script setup>
import * as THREE from 'three';
import { ref, onMounted, onBeforeUnmount } from 'vue'
const rendererCanvas = ref(null)
const speed = ref(60) // 车速 km/h
const obstacleCount = ref(0)
const currentLaneDisplay = ref(3) // 显示用的车道号(1-6)
const isChangingLaneDisplay = ref(false)
const laneChangeStatus = ref('正常行驶')
let scene, camera, renderer
let car, road, obstacles = []
let clock = new THREE.Clock()
// 车辆控制
let carSpeed = 0.05
let roadOffset = 0
let currentLane = 2 // 当前车道索引(0-5对应6个车道)
let targetLane = 2 // 目标车道索引
let isChangingLane = false // 是否正在变道
let laneChangeSpeed = 0.02 // 变道速度
onMounted(() => {
initScene()
createRoad()
createCar()
createObstacles()
setupCamera()
animate()
})
const initScene = () => {
// 初始化场景
scene = new THREE.Scene()
scene.background = new THREE.Color(0xE8E8E8) // 灰白色背景
// 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(rendererCanvas.value.clientWidth, rendererCanvas.value.clientHeight)
renderer.setPixelRatio(window.devicePixelRatio)
renderer.shadowMap.enabled = false // 避免proxy错误
rendererCanvas.value.appendChild(renderer.domElement)
// 添加环境光
// const ambientLight = new THREE.AmbientLight(0xffffff, 0.8)
// scene.add(ambientLight)
}
const createRoad = () => {
// 创建道路(6车道)
const roadGeometry = new THREE.PlaneGeometry(24, 200)
const roadMaterial = new THREE.MeshBasicMaterial({ color: 0x555555 })
road = new THREE.Mesh(roadGeometry, roadMaterial)
road.rotation.x = -Math.PI / 2
scene.add(road)
// 创建车道标线
const lineGeometry = new THREE.PlaneGeometry(0.15, 2)
const lineMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff })
const dashedLineMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff })
// 车道分隔线(虚线)- 创建多个短线段
const lanePositions = [-8, -4, 0, 4, 8] // 5条车道分隔线,形成6个车道
lanePositions.forEach(x => {
for (let i = -50; i < 50; i += 4) { // 虚线间隔
const line = new THREE.Mesh(lineGeometry, dashedLineMaterial)
line.position.set(x, 0.01, i)
line.rotation.x = -Math.PI / 2
scene.add(line)
}
})
// 道路边界线(实线)
const borderGeometry = new THREE.PlaneGeometry(0.2, 200)
const leftBorder = new THREE.Mesh(borderGeometry, lineMaterial)
leftBorder.position.set(-12, 0.01, 0)
leftBorder.rotation.x = -Math.PI / 2
scene.add(leftBorder)
const rightBorder = new THREE.Mesh(borderGeometry, lineMaterial)
rightBorder.position.set(12, 0.01, 0)
rightBorder.rotation.x = -Math.PI / 2
scene.add(rightBorder)
}
const createCar = () => {
// 创建主车(简化的车辆模型)
const carGroup = new THREE.Group()
// 车身
const bodyGeometry = new THREE.BoxGeometry(2, 1, 4)
const bodyMaterial = new THREE.MeshBasicMaterial({ color: 0x87CEEB }) // 浅蓝色主车
const body = new THREE.Mesh(bodyGeometry, bodyMaterial)
body.position.y = 0.5
carGroup.add(body)
// 车窗
const windowGeometry = new THREE.BoxGeometry(1.8, 0.8, 2)
const windowMaterial = new THREE.MeshBasicMaterial({ color: 0xE8E8E8, transparent: true, opacity: 0.5 })
const window = new THREE.Mesh(windowGeometry, windowMaterial)
window.position.y = 1.2
carGroup.add(window)
// 车轮
const wheelGeometry = new THREE.CylinderGeometry(0.3, 0.3, 0.2, 16)
const wheelMaterial = new THREE.MeshBasicMaterial({ color: 0x222222 })
const wheels = [
{ x: -0.8, z: 1.2 },
{ x: 0.8, z: 1.2 },
{ x: -0.8, z: -1.2 },
{ x: 0.8, z: -1.2 }
]
wheels.forEach(pos => {
const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial)
wheel.position.set(pos.x, 0.3, pos.z)
wheel.rotation.z = Math.PI / 2
carGroup.add(wheel)
})
car = carGroup
car.position.set(-2, 0, -5) // 主车位于右侧车道
scene.add(car)
}
// 检测前方障碍物
const detectFrontObstacle = () => {
const carX = car.position.x
const carZ = car.position.z
const detectionDistance = 15 // 检测距离
const laneWidth = 4 // 车道宽度
// 检查前方是否有障碍物
for (let obstacle of obstacles) {
const obstacleX = obstacle.position.x
const obstacleZ = obstacle.position.z
// 检查是否在同一车道且在前方
if (Math.abs(obstacleX - carX) < 1.5 && // 同一车道范围
obstacleZ > carZ && // 在前方
obstacleZ - carZ < detectionDistance) { // 在检测距离内
return true
}
}
return false
}
// 寻找安全车道
const findSafeLane = () => {
const lanes = [-10, -6, -2, 2, 6, 10] // 6个车道位置
const carZ = car.position.z
const checkDistance = 20 // 安全检查距离
for (let i = 0; i < lanes.length; i++) {
if (i === currentLane) continue // 跳过当前车道
let isSafe = true
const laneX = lanes[i]
// 检查这个车道是否安全
for (let obstacle of obstacles) {
const obstacleX = obstacle.position.x
const obstacleZ = obstacle.position.z
// 检查前后一定距离内是否有障碍物
if (Math.abs(obstacleX - laneX) < 1.5 && // 在目标车道
Math.abs(obstacleZ - carZ) < checkDistance) { // 在安全检查范围内
isSafe = false
break
}
}
if (isSafe) {
return i // 返回安全车道索引
}
}
return currentLane // 如果没有安全车道,保持当前车道
}
// 执行车道变换
const performLaneChange = () => {
if (isChangingLane) {
const lanes = [-10, -6, -2, 2, 6, 10]
const targetX = lanes[targetLane]
const currentX = car.position.x
// 平滑移动到目标车道
const deltaX = targetX - currentX
if (Math.abs(deltaX) > 0.1) {
car.position.x += Math.sign(deltaX) * laneChangeSpeed
} else {
// 变道完成
car.position.x = targetX
currentLane = targetLane
isChangingLane = false
laneChangeStatus.value = '变道完成'
isChangingLaneDisplay.value = false
// 0.5秒后重置状态
setTimeout(() => {
if (!isChangingLane) {
laneChangeStatus.value = '正常行驶'
}
}, 500)
}
}
// 更新显示的车道号
currentLaneDisplay.value = currentLane + 1 // 车道号从1开始
}
const createObstacles = () => {
// 创建随机障碍物(统一灰白色)
const obstacleTypes = [
{ type: 'car', color: 0xD3D3D3, size: { w: 2, h: 1, d: 4 } },
{ type: 'car', color: 0xC0C0C0, size: { w: 2, h: 1, d: 4 } },
{ type: 'truck', color: 0xB8B8B8, size: { w: 2.5, h: 2, d: 6 } },
{ type: 'pedestrian', color: 0xA8A8A8, size: { w: 0.5, h: 1.7, d: 0.5 } }
]
// 创建初始障碍物(减少数量)
for (let i = 0; i < 4; i++) {
createRandomObstacle(obstacleTypes)
}
obstacleCount.value = obstacles.length
}
const createRandomObstacle = (types) => {
const type = types[Math.floor(Math.random() * types.length)]
const obstacle = new THREE.Group()
// 主体
const geometry = new THREE.BoxGeometry(type.size.w, type.size.h, type.size.d)
const material = new THREE.MeshBasicMaterial({ color: type.color })
const body = new THREE.Mesh(geometry, material)
body.position.y = type.size.h / 2
obstacle.add(body)
// 如果是车辆,添加车轮
if (type.type === 'car' || type.type === 'truck') {
const wheelGeometry = new THREE.CylinderGeometry(0.25, 0.25, 0.15, 12)
const wheelMaterial = new THREE.MeshBasicMaterial({ color: 0x222222 })
const wheelPositions = [
{ x: -type.size.w * 0.3, z: type.size.d * 0.3 },
{ x: type.size.w * 0.3, z: type.size.d * 0.3 },
{ x: -type.size.w * 0.3, z: -type.size.d * 0.3 },
{ x: type.size.w * 0.3, z: -type.size.d * 0.3 }
]
wheelPositions.forEach(pos => {
const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial)
wheel.position.set(pos.x, 0.25, pos.z)
wheel.rotation.z = Math.PI / 2
obstacle.add(wheel)
})
}
// 随机位置(6车道系统,增加间距)
const lanes = [-10, -6, -2, 2, 6, 10] // 6个车道的中心位置
const lane = lanes[Math.floor(Math.random() * lanes.length)]
obstacle.position.set(lane, 0, Math.random() * 60 + 20) // 增加生成距离
// 添加移动速度
obstacle.userData = {
speed: Math.random() * 0.02 + 0.01,
type: type.type
}
obstacles.push(obstacle)
scene.add(obstacle)
}
const setupCamera = () => {
camera = new THREE.PerspectiveCamera(75, rendererCanvas.value.clientWidth / rendererCanvas.value.clientHeight, 0.1, 1000)
// 第三人称跟随视角
camera.position.set(0, 8, -15)
camera.lookAt(0, 0, 0)
}
const animate = () => {
requestAnimationFrame(animate)
const deltaTime = clock.getDelta()
// 检测前方障碍物并执行变道
if (!isChangingLane && detectFrontObstacle()) {
const safeLane = findSafeLane()
if (safeLane !== currentLane) {
targetLane = safeLane
isChangingLane = true
laneChangeStatus.value = '检测到障碍物,正在变道...'
isChangingLaneDisplay.value = true
} else {
laneChangeStatus.value = '前方有障碍物,无安全车道'
}
} else if (!isChangingLane) {
laneChangeStatus.value = '正常行驶'
isChangingLaneDisplay.value = false
}
// 执行变道动作
performLaneChange()
// 更新车辆位置(模拟前进)
roadOffset += carSpeed
// 更新道路标线位置
scene.children.forEach(child => {
if (child.material && child.material.color.getHex() === 0xffffff && child.position.y === 0.01) {
child.position.z -= carSpeed
if (child.position.z < -50) {
child.position.z += 100
}
}
})
// 更新障碍物位置
obstacles.forEach((obstacle, index) => {
obstacle.position.z -= carSpeed + obstacle.userData.speed
// 如果障碍物移出视野,重新生成
if (obstacle.position.z < -30) {
scene.remove(obstacle)
obstacles.splice(index, 1)
// 创建新的障碍物(统一灰白色)
const obstacleTypes = [
{ type: 'car', color: 0xD3D3D3, size: { w: 2, h: 1, d: 4 } },
{ type: 'car', color: 0xC0C0C0, size: { w: 2, h: 1, d: 4 } },
{ type: 'truck', color: 0xB8B8B8, size: { w: 2.5, h: 2, d: 6 } },
{ type: 'pedestrian', color: 0xA8A8A8, size: { w: 0.5, h: 1.7, d: 0.5 } }
]
createRandomObstacle(obstacleTypes)
}
})
// 更新速度显示
speed.value = 60 + Math.sin(Date.now() * 0.001) * 10 // 模拟速度变化
obstacleCount.value = obstacles.length
// 摄像机跟随(考虑变道)
camera.position.x = car.position.x
camera.position.z = car.position.z - 15
camera.position.y = 8
camera.lookAt(car.position.x, car.position.y + 2, car.position.z + 5)
renderer.render(scene, camera)
}
onBeforeUnmount(() => {
// 清理资源
obstacles.forEach(obstacle => {
scene.remove(obstacle)
})
if (car) scene.remove(car)
if (road) scene.remove(road)
// 清理几何体和材质
scene.traverse((child) => {
if (child.geometry) {
child.geometry.dispose()
}
if (child.material) {
if (Array.isArray(child.material)) {
child.material.forEach(material => material.dispose())
} else {
child.material.dispose()
}
}
})
renderer.dispose()
if (rendererCanvas.value && renderer.domElement) {
rendererCanvas.value.removeChild(renderer.domElement)
}
})
</script>
<style scoped>
.car-three {
width: 100%;
height: 100%;
}
.controls {
position: absolute;
top: 20px;
left: 20px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 15px;
border-radius: 8px;
font-family: Arial, sans-serif;
z-index: 100;
}
.speed-info, .obstacle-count {
margin: 5px 0;
font-size: 14px;
font-weight: bold;
}
.speed-info {
color: #00ff00;
}
.obstacle-count {
color: #ffaa00;
}
.lane-info {
color: #00aaff;
}
.lane-change-status {
color: #ffff00;
font-weight: bold;
}
.lane-change-status.active {
color: #ff6600;
animation: blink 1s infinite;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0.5; }
}
</style>