📋 项目概述
本项目基于 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)赋值时,会产生旋转轴干扰,导致异常运动。
解决方案:
- 先保存当前所有轴的旋转值
- 使用
rotation.set(x, y, z)一次性设置所有轴 - 避免使用
setRotationFromEuler,因为它会重置所有旋转值
难点 2:车轮驱动整车移动
问题:如何根据车轮旋转速度计算出车辆的实际移动距离?
解决方案:
javascript
// 移动距离 = 车轮旋转角度 × 车轮半径
const moveDistance = wheelRotationSpeed.value * wheelRadius
难点 3:转向时的圆周运动
问题:车辆转向时需要既旋转车身,又沿圆弧轨迹移动。
解决方案:
- 先更新车辆的
rotation.y(车身旋转) - 再根据新的朝向计算移动方向向量
- 应用车辆的四元数旋转到方向向量
- 更新车辆位置
🎨 UI 交互设计
滑块控件
- 旋转速度控制 :范围
-0.2到0.2,步长0.01 - 转向角度控制 :范围
-1到1,步长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()
})
🚀 功能扩展建议
- 车辆物理引擎:集成 Cannon.js 或 Ammo.js 实现真实的物理碰撞
- 多车辆支持:支持场景中加载多个车辆模型
- 路径规划:实现自动驾驶路径跟随功能
- 材质编辑器:支持实时修改材质属性(颜色、粗糙度、金属度)
- 动画录制:支持录制车辆运动轨迹并导出动画
- VR 支持:集成 WebXR 实现虚拟现实体验
📝 总结
本项目通过 Vue 3 + Three.js 实现了一个功能完整的 3D 车辆模型交互系统,涵盖了:
✅ GLB 模型加载与渲染
✅ 节点结构可视化
✅ 车轮旋转动画
✅ 车辆移动与转向
✅ 节点属性实时编辑
✅ 自定义地面纹理
核心技术亮点:
- 使用 UUID 映射表实现快速节点查找
- 避免欧拉角冲突的旋转控制方案
- 车轮驱动整车移动的物理计算
- 可配置的转向轴和滚动轴系统
希望本文能够帮助你理解 Three.js 在 3D 交互应用中的实践技巧!