Three.js 实现 3D 车辆模型交互控制技术详解

📋 项目概述

本项目基于 Vue 3 + Vite + Three.js 技术栈,实现了一个功能强大的 GLB 3D 模型查看器,支持车辆模型的节点结构可视化、车轮旋转动画、转向控制、节点属性实时编辑等交互功能。

技术栈

  • Vue 3.5.25 :采用 Composition API(<script setup>)语法
  • Vite 7.3.1:快速构建工具
  • Three.js:3D 渲染引擎
  • GLTFLoader + DRACOLoader:用于加载压缩的 GLB 模型

🎯 核心功能

1. GLB 模型加载与渲染

1.1 场景初始化

使用 Three.js 创建基本的 3D 场景环境:

javascript 复制代码
// 创建场景
scene = new THREE.Scene()
scene.background = new THREE.Color(0x1a1a1a)  // 深灰色背景

// 创建透视相机
camera = new THREE.PerspectiveCamera(
  75,  // 视角(FOV)
  canvasRef.value.clientWidth / canvasRef.value.clientHeight,  // 宽高比
  0.1,  // 近截面
  1000  // 远截面
)
camera.position.set(5, 5, 5)

// 创建 WebGL 渲染器
renderer = new THREE.WebGLRenderer({
  canvas: canvasRef.value,
  antialias: true  // 开启抗锯齿
})
renderer.setPixelRatio(window.devicePixelRatio)  // 支持高分屏
1.2 多光源照明系统

为了让模型有良好的视觉效果,采用多光源组合:

javascript 复制代码
// 环境光(提供基础照明)
const ambientLight = new THREE.AmbientLight(0xffffff, 1.5)
scene.add(ambientLight)

// 主平行光(模拟太阳光)
const directionalLight = new THREE.DirectionalLight(0xffffff, 2.0)
directionalLight.position.set(5, 10, 7.5)
scene.add(directionalLight)

// 补光 1(从另一侧补光)
const fillLight1 = new THREE.DirectionalLight(0xffffff, 0.8)
fillLight1.position.set(-5, 5, -5)
scene.add(fillLight1)

// 补光 2(从背面补光)
const fillLight2 = new THREE.DirectionalLight(0xffffff, 0.6)
fillLight2.position.set(0, 5, -10)
scene.add(fillLight2)
1.3 DRACO 压缩模型加载

为了提高模型加载效率,使用 DRACO 压缩格式:

javascript 复制代码
// 配置 DRACO 解码器
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/')
dracoLoader.setDecoderConfig({ type: 'js' })

// 加载 GLB 模型
const loader = new GLTFLoader()
loader.setDRACOLoader(dracoLoader)
loader.load(
  '/src/assets/car1.glb',
  (gltf) => {
    scene.add(gltf.scene)
    // 自动调整相机位置以适配模型大小
    adjustCameraToModel(gltf.scene)
  }
)
1.4 自动相机适配

根据模型大小自动调整相机位置和视角:

javascript 复制代码
// 计算模型边界包围盒
const box = new THREE.Box3().setFromObject(gltf.scene)
const center = box.getCenter(new THREE.Vector3())  // 模型中心点
const size = box.getSize(new THREE.Vector3())      // 模型尺寸

// 根据模型大小计算相机距离
const maxDim = Math.max(size.x, size.y, size.z)
const fov = camera.fov * (Math.PI / 180)
let cameraZ = Math.abs(maxDim / 2 / Math.tan(fov / 2))
cameraZ *= 2.5  // 增加一些距离

// 设置相机位置和目标
camera.position.set(center.x + cameraZ * 0.5, center.y + cameraZ * 0.5, center.z + cameraZ)
camera.lookAt(center)
controls.target.copy(center)

2. 节点结构可视化

2.1 递归解析节点树

将 Three.js 场景图转换为可视化的节点树结构:

javascript 复制代码
const parseNodes = (object, level = 0) => {
  const nodeInfo = {
    uuid: object.uuid,
    name: object.name || '(unnamed)',
    type: object.type,
    level,
    position: object.position.toArray(),
    rotation: object.rotation.toArray().slice(0, 3),
    scale: object.scale.toArray(),
    visible: object.visible,
    children: []
  }

  // Mesh 类型添加几何体和材质信息
  if (object.isMesh) {
    nodeInfo.geometry = object.geometry.type
    nodeInfo.material = object.material.type
    nodeInfo.vertices = object.geometry.attributes.position.count
  }

  // 递归解析子节点
  if (object.children && object.children.length > 0) {
    nodeInfo.children = object.children.map(child => parseNodes(child, level + 1))
  }

  return nodeInfo
}
2.2 UUID 快速查找映射

为了实现节点的快速查找和编辑,构建 UUID 到 Three.js 对象的映射表:

javascript 复制代码
const buildObjectMap = (object) => {
  sceneObjectsMap.set(object.uuid, object)
  if (object.children && object.children.length > 0) {
    object.children.forEach(child => buildObjectMap(child))
  }
}
2.3 节点搜索功能

支持关键词搜索节点名称:

javascript 复制代码
const searchNodes = () => {
  const keyword = searchKeyword.value.toLowerCase()
  const results = []
  
  const searchInNode = (node) => {
    if (node.name.toLowerCase().includes(keyword)) {
      results.push({
        uuid: node.uuid,
        name: node.name,
        type: node.type,
        path: getNodePath(node)  // 节点路径
      })
    }
    
    if (node.children && node.children.length > 0) {
      node.children.forEach(child => searchInNode(child))
    }
  }
  
  rootNodes.value.forEach(node => searchInNode(node))
  searchResults.value = results
}

3. 车轮旋转动画系统

3.1 车轮节点识别

根据可配置的车轮组名称查找车轮节点:

javascript 复制代码
const findWheelNodes = () => {
  const wheels = []
  const frontWheels = []
  
  // 从输入框获取车轮名称(逗号分隔)
  const wheelNames = wheelNameInput.value.split(',').map(name => name.trim())
  const frontWheelNames = frontWheelNameInput.value.split(',').map(name => name.trim())
  
  sceneObjectsMap.forEach((object, uuid) => {
    const name = object.name
    
    // 查找匹配的车轮组
    if (wheelNames.includes(name)) {
      // 遍历车轮组的子节点,找到实际的车轮 Mesh
      object.traverse((child) => {
        if (child.isMesh || child.type === 'Mesh') {
          wheels.push(child)
          
          // 如果在前轮名称列表中,也添加到前轮数组
          if (frontWheelNames.includes(name)) {
            frontWheels.push(child)
          }
        }
      })
    }
  })
  
  wheelNodes.value = wheels
  frontWheelNodes.value = frontWheels
}
3.2 车轮滚动动画

在动画循环中实现车轮的滚动旋转:

javascript 复制代码
if (wheelRotationSpeed.value !== 0) {
  // 所有车轮进行滚动旋转
  wheelNodes.value.forEach(wheelObj => {
    if (wheelObj) {
      // 根据选择的滚动轴进行旋转
      if (rollingAxis.value === 'x') {
        wheelObj.rotation.x += wheelRotationSpeed.value
      } else if (rollingAxis.value === 'y') {
        wheelObj.rotation.y += wheelRotationSpeed.value
      } else if (rollingAxis.value === 'z') {
        wheelObj.rotation.z += wheelRotationSpeed.value
      }
    }
  })
}

4. 车辆移动与转向控制

4.1 车辆主体定位

查找车辆主体节点(Car_Rig):

javascript 复制代码
const findCarRig = () => {
  sceneObjectsMap.forEach((object, uuid) => {
    if (object.name === 'Car_Rig') {
      carModel.value = object
    }
  })
}
4.2 车轮驱动车辆移动

根据车轮旋转速度计算车辆移动距离:

javascript 复制代码
if (carModel.value) {
  // 车轮旋转速度转换为车辆移动距离
  const wheelRadius = 0.35  // 车轮半径
  const moveDistance = wheelRotationSpeed.value * wheelRadius
  
  // 根据当前朝向移动车辆
  const direction = new THREE.Vector3(0, 0, -1)  // 初始方向
  direction.applyQuaternion(carModel.value.quaternion)  // 应用车辆旋转
  direction.multiplyScalar(moveDistance)  // 乘以移动距离
  
  carModel.value.position.add(direction)  // 更新车辆位置
}
4.3 转向系统实现

实现车辆的转向功能,包括车辆旋转和圆周运动:

javascript 复制代码
if (steeringAngle.value !== 0) {
  // 车辆自身旋转(转向效果)
  carModel.value.rotation.y += steeringAngle.value * Math.abs(wheelRotationSpeed.value) * 0.02
  
  // 根据当前朝向移动车辆(形成圆周运动)
  const direction = new THREE.Vector3(0, 0, -1)
  direction.applyQuaternion(carModel.value.quaternion)
  direction.multiplyScalar(moveDistance)
  
  carModel.value.position.add(direction)
}
4.4 前轮转向角度控制

⚠️ 关键技术点:避免欧拉角冲突

在 Three.js 中,对已存在滚动旋转的车轮对象应用转向时,如果使用 setRotationFromEuler 会强制重置所有轴旋转值,导致原有滚动角度被覆盖。

正确做法 :先读取当前各轴 rotation 值,再仅对转向轴增量赋值,最后用 rotation.set() 直接设置:

javascript 复制代码
if (frontWheelNodes.value.length > 0 && steeringAngle.value !== 0) {
  const maxSteeringWheelAngle = Math.PI / 6  // 前轮最大转向角度 30°
  const targetWheelAngle = steeringAngle.value * maxSteeringWheelAngle
  
  frontWheelNodes.value.forEach(wheelObj => {
    if (wheelObj) {
      // 保留当前所有轴的旋转值(保留滚动旋转角度)
      const currentX = wheelObj.rotation.x
      const currentY = wheelObj.rotation.y
      const currentZ = wheelObj.rotation.z
      
      // 根据选择的转向轴设置旋转
      if (steeringAxis.value === 'x') {
        wheelObj.rotation.set(currentX + targetWheelAngle, currentY, currentZ)
      } else if (steeringAxis.value === 'y') {
        wheelObj.rotation.set(currentX, currentY + targetWheelAngle, currentZ)
      } else if (steeringAxis.value === 'z') {
        wheelObj.rotation.set(currentX, currentY, currentZ + targetWheelAngle)
      }
    }
  })
}

5. 节点属性实时编辑

5.1 节点选择

点击节点树中的节点,加载到编辑器:

javascript 复制代码
const selectNode = (uuid) => {
  const object = sceneObjectsMap.get(uuid)
  if (object) {
    selectedNode.value = {
      uuid: object.uuid,
      name: object.name || '(unnamed)',
      type: object.type,
      position: [...object.position.toArray()],
      rotation: [...object.rotation.toArray().slice(0, 3)],
      scale: [...object.scale.toArray()],
      visible: object.visible
    }
  }
}
5.2 实时变换更新

支持实时修改节点的位置、旋转、缩放:

javascript 复制代码
const updateNodeTransform = (property, index, value) => {
  const object = sceneObjectsMap.get(selectedNode.value.uuid)
  const numValue = parseFloat(value)
  
  if (property === 'position') {
    object.position.setComponent(index, numValue)
    selectedNode.value.position[index] = numValue
  } else if (property === 'rotation') {
    object.rotation[['x', 'y', 'z'][index]] = numValue
    selectedNode.value.rotation[index] = numValue
  } else if (property === 'scale') {
    object.scale.setComponent(index, numValue)
    selectedNode.value.scale[index] = numValue
  }
}
5.3 可见性控制

切换节点的显示/隐藏状态:

javascript 复制代码
const updateNodeVisibility = (visible) => {
  const object = sceneObjectsMap.get(selectedNode.value.uuid)
  object.visible = visible
  selectedNode.value.visible = visible
}

6. 地面纹理定制

6.1 Canvas 生成格子纹理

使用 Canvas API 动态生成水泥路面格子纹理:

javascript 复制代码
const changePlaneToConcreteTexture = () => {
  sceneObjectsMap.forEach((object, uuid) => {
    if (object.name === 'Plane' && object.isMesh) {
      // 创建画布纹理
      const canvas = document.createElement('canvas')
      canvas.width = 512
      canvas.height = 512
      const ctx = canvas.getContext('2d')
      
      // 绘制水泥底色
      ctx.fillStyle = '#000'
      ctx.fillRect(0, 0, 512, 512)
      
      // 绘制格子线
      const gridSize = 800
      ctx.strokeStyle = '#606060'
      ctx.lineWidth = 100
      
      // 绘制垂直线和水平线
      for (let x = 0; x <= 512; x += gridSize) {
        ctx.beginPath()
        ctx.moveTo(x, 0)
        ctx.lineTo(x, 512)
        ctx.stroke()
      }
      
      for (let y = 0; y <= 512; y += gridSize) {
        ctx.beginPath()
        ctx.moveTo(0, y)
        ctx.lineTo(512, y)
        ctx.stroke()
      }
      
      // 创建纹理并应用
      const texture = new THREE.CanvasTexture(canvas)
      texture.wrapS = THREE.RepeatWrapping
      texture.wrapT = THREE.RepeatWrapping
      texture.repeat.set(20, 20)
      
      const concreteMaterial = new THREE.MeshStandardMaterial({
        map: texture,
        roughness: 0.9,
        metalness: 0.1,
        side: THREE.DoubleSide
      })
      
      object.material = concreteMaterial
    }
  })
}

7. 轨道控制器

使用 OrbitControls 实现鼠标交互:

javascript 复制代码
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true  // 开启阻尼(惯性)效果
controls.dampingFactor = 0.05  // 阻尼系数

🔧 技术难点与解决方案

难点 1:欧拉角旋转冲突

问题:同时对车轮的滚动轴(rotation.x)和转向轴(rotation.z)赋值时,会产生旋转轴干扰,导致异常运动。

解决方案

  1. 先保存当前所有轴的旋转值
  2. 使用 rotation.set(x, y, z) 一次性设置所有轴
  3. 避免使用 setRotationFromEuler,因为它会重置所有旋转值

难点 2:车轮驱动整车移动

问题:如何根据车轮旋转速度计算出车辆的实际移动距离?

解决方案

javascript 复制代码
// 移动距离 = 车轮旋转角度 × 车轮半径
const moveDistance = wheelRotationSpeed.value * wheelRadius

难点 3:转向时的圆周运动

问题:车辆转向时需要既旋转车身,又沿圆弧轨迹移动。

解决方案

  1. 先更新车辆的 rotation.y(车身旋转)
  2. 再根据新的朝向计算移动方向向量
  3. 应用车辆的四元数旋转到方向向量
  4. 更新车辆位置

🎨 UI 交互设计

滑块控件

  • 旋转速度控制 :范围 -0.20.2,步长 0.01
  • 转向角度控制 :范围 -11,步长 0.1

按钮组设计

  • 速度预设按钮:快速前进、前进、停止、后退、快速后退
  • 转向按钮:右转、直行、左转
  • 轴选择按钮:X 轴、Y 轴、Z 轴

样式规范

  • 暗色主题:背景 #1e1e1e,强调色 #61dafb
  • 按钮激活状态:使用不同颜色标识(如转向激活 #10b981
  • 滑块拖动:支持 hover 放大效果

📊 性能优化

1. 响应式窗口调整

监听窗口大小变化,自动调整相机和渲染器:

javascript 复制代码
const handleResize = () => {
  camera.aspect = canvasRef.value.clientWidth / canvasRef.value.clientHeight
  camera.updateProjectionMatrix()
  renderer.setSize(canvasRef.value.clientWidth, canvasRef.value.clientHeight)
}
window.addEventListener('resize', handleResize)

2. 资源清理

组件卸载时正确清理资源:

javascript 复制代码
onUnmounted(() => {
  if (animationId) cancelAnimationFrame(animationId)
  if (renderer) renderer.dispose()
  if (controls) controls.dispose()
})

🚀 功能扩展建议

  1. 车辆物理引擎:集成 Cannon.js 或 Ammo.js 实现真实的物理碰撞
  2. 多车辆支持:支持场景中加载多个车辆模型
  3. 路径规划:实现自动驾驶路径跟随功能
  4. 材质编辑器:支持实时修改材质属性(颜色、粗糙度、金属度)
  5. 动画录制:支持录制车辆运动轨迹并导出动画
  6. VR 支持:集成 WebXR 实现虚拟现实体验

📝 总结

本项目通过 Vue 3 + Three.js 实现了一个功能完整的 3D 车辆模型交互系统,涵盖了:

✅ GLB 模型加载与渲染

✅ 节点结构可视化

✅ 车轮旋转动画

✅ 车辆移动与转向

✅ 节点属性实时编辑

✅ 自定义地面纹理

核心技术亮点

  • 使用 UUID 映射表实现快速节点查找
  • 避免欧拉角冲突的旋转控制方案
  • 车轮驱动整车移动的物理计算
  • 可配置的转向轴和滚动轴系统

希望本文能够帮助你理解 Three.js 在 3D 交互应用中的实践技巧!

相关推荐
萧曵 丶12 小时前
Vue 中父子组件之间最常用的业务交互场景
javascript·vue.js·交互
大模型RAG和Agent技术实践12 小时前
从零构建本地AI合同审查系统:架构设计与流式交互实战(完整源代码)
人工智能·交互·智能合同审核
Amumu1213813 小时前
Vue3扩展(二)
前端·javascript·vue.js
NEXT0613 小时前
JavaScript进阶:深度剖析函数柯里化及其在面试中的底层逻辑
前端·javascript·面试
微祎_14 小时前
Flutter for OpenHarmony:魔方计时器开发实战 - 基于Flutter的专业番茄工作法应用实现与交互设计
flutter·交互
牛奶14 小时前
你不知道的 JS(上):原型与行为委托
前端·javascript·编译原理
牛奶15 小时前
你不知道的JS(上):this指向与对象基础
前端·javascript·编译原理
牛奶15 小时前
你不知道的JS(上):作用域与闭包
前端·javascript·电子书
大江东去浪淘尽千古风流人物16 小时前
【SLAM】Hydra-Foundations 层次化空间感知:机器人如何像人类一样理解3D环境
深度学习·算法·3d·机器人·概率论·slam
pas13616 小时前
45-mini-vue 实现代码生成三种联合类型
前端·javascript·vue.js