普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月6日首页

基于ThreeJs实现3D车机大屏及自动避障效果

作者 李剑一
2026年2月5日 22:00

之前接到一个需求,说是给某个无人车做个啥配套的演示代码,但是仅仅只是提供一个Demo,所以细节上很多地方并没有深究。

无标题视频——使用Clipchamp制作.gif

不过虽然仅仅是一个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>
❌
❌