docs
This commit is contained in:
@@ -1,5 +1,19 @@
|
||||
<!--
|
||||
* @Description: 模型漫游(巡检业务)
|
||||
* @Description: Demo 7 - 模型漫游(巡检业务)
|
||||
* @功能说明: 实现相机沿预设路径自动巡检,模拟无人机/机器人巡检业务
|
||||
* @核心技术:
|
||||
* 1. 路径插值动画 - 使用 lerp(线性插值)在路径点之间平滑移动
|
||||
* 2. SmoothStep 缓动 - 使用平滑步进函数实现加速减速效果
|
||||
* 3. 相机目标联动 - 相机位置和观察目标同步插值
|
||||
* 4. OrbitControls 状态切换 - 自动模式禁用控制器,手动模式启用
|
||||
* @业务场景:
|
||||
* - 建筑/工厂自动巡检
|
||||
* - 虚拟导览
|
||||
* - 安防监控路线演示
|
||||
* @技术要点:
|
||||
* - 13个路径点构成完整巡检循环
|
||||
* - 支持慢速(0.003)和快速(0.008)两种巡检速度
|
||||
* - 使用 Fog 雾效增强空间深度感
|
||||
* @Autor: 刘 相卿
|
||||
* @Date: 2025-11-17 16:07:33
|
||||
* @LastEditors: 刘 相卿
|
||||
@@ -11,148 +25,174 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
||||
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
||||
import { onMounted, onUnmounted, ref, shallowRef } from 'vue'
|
||||
|
||||
const threeRef = shallowRef<HTMLDivElement>()
|
||||
const camera = shallowRef<THREE.PerspectiveCamera>()
|
||||
const scene = shallowRef<THREE.Scene>()
|
||||
const controls = shallowRef<OrbitControls>()
|
||||
const renderer = shallowRef<THREE.WebGLRenderer>()
|
||||
// ========== 响应式状态 ==========
|
||||
const threeRef = shallowRef<HTMLDivElement>() // Canvas 容器
|
||||
const camera = shallowRef<THREE.PerspectiveCamera>() // 透视相机
|
||||
const scene = shallowRef<THREE.Scene>() // 场景对象
|
||||
const controls = shallowRef<OrbitControls>() // 轨道控制器
|
||||
const renderer = shallowRef<THREE.WebGLRenderer>() // WebGL 渲染器
|
||||
|
||||
// 漫游状态
|
||||
const tourMode = ref<'none' | 'slow' | 'fast'>('none')
|
||||
let tourTime = 0
|
||||
const initialCameraPosition = new THREE.Vector3(0, 5, 15)
|
||||
const initialCameraTarget = new THREE.Vector3(0, 0, 0)
|
||||
// ========== 漫游状态管理 ==========
|
||||
const tourMode = ref<'none' | 'slow' | 'fast'>('none') // 巡检模式:无、慢速、快速
|
||||
let tourTime = 0 // 巡检动画时间累加器
|
||||
const initialCameraPosition = new THREE.Vector3(0, 5, 15) // 相机初始位置
|
||||
const initialCameraTarget = new THREE.Vector3(0, 0, 0) // 相机初始观察目标
|
||||
|
||||
// 巡检路径点位 - 模拟进入建筑内部进行详细巡检
|
||||
// 包含位置和对应的观察目标点
|
||||
// ========== 巡检路径定义 ==========
|
||||
/**
|
||||
* 路径点接口
|
||||
* 定义相机在巡检过程中的位置和观察目标
|
||||
*/
|
||||
interface PathPoint {
|
||||
position: THREE.Vector3
|
||||
lookAt: THREE.Vector3
|
||||
position: THREE.Vector3 // 相机位置
|
||||
lookAt: THREE.Vector3 // 相机观察目标(朝向)
|
||||
}
|
||||
|
||||
/**
|
||||
* 巡检路径点数组
|
||||
*
|
||||
* 巡检路线设计:
|
||||
* 1. 外部俯视 → 2. 正面接近 → 3. 进入前厅 →
|
||||
* 4-5. 右侧巡检 → 6. 右后方 → 7. 后方中央 →
|
||||
* 8-9. 左侧巡检 → 10. 左前方 → 11-12. 中央回顾 →
|
||||
* 13. 退出俯视
|
||||
*
|
||||
* 形成完整的顺时针巡检循环
|
||||
*/
|
||||
const inspectionPath: PathPoint[] = [
|
||||
// 1. 外部俯视全景
|
||||
// 1. 外部俯视全景 - 建立整体认知
|
||||
{ position: new THREE.Vector3(0, 8, 15), lookAt: new THREE.Vector3(0, 0, 0) },
|
||||
// 2. 从正面接近入口
|
||||
// 2. 从正面接近入口 - 降低高度,准备进入
|
||||
{ position: new THREE.Vector3(0, 3, 10), lookAt: new THREE.Vector3(0, 2, 0) },
|
||||
// 3. 进入前厅区域
|
||||
// 3. 进入前厅区域 - 穿过入口
|
||||
{ position: new THREE.Vector3(0, 2, 5), lookAt: new THREE.Vector3(0, 2, -2) },
|
||||
// 4. 右侧区域巡检
|
||||
// 4. 右侧区域巡检 - 检查右侧设施
|
||||
{ position: new THREE.Vector3(5, 2, 3), lookAt: new THREE.Vector3(8, 2, 0) },
|
||||
// 5. 深入右侧内部
|
||||
// 5. 深入右侧内部 - 详细检查右侧深处
|
||||
{ position: new THREE.Vector3(8, 2.5, 0), lookAt: new THREE.Vector3(10, 2, -3) },
|
||||
// 6. 右后方区域
|
||||
// 6. 右后方区域 - 转向后方
|
||||
{ position: new THREE.Vector3(6, 2, -5), lookAt: new THREE.Vector3(3, 2, -8) },
|
||||
// 7. 后方中央区域
|
||||
// 7. 后方中央区域 - 检查后方设施
|
||||
{ position: new THREE.Vector3(0, 2.5, -8), lookAt: new THREE.Vector3(0, 2, -5) },
|
||||
// 8. 左后方区域
|
||||
// 8. 左后方区域 - 转向左侧
|
||||
{ position: new THREE.Vector3(-6, 2, -5), lookAt: new THREE.Vector3(-3, 2, -8) },
|
||||
// 9. 深入左侧内部
|
||||
// 9. 深入左侧内部 - 详细检查左侧深处
|
||||
{ position: new THREE.Vector3(-8, 2.5, 0), lookAt: new THREE.Vector3(-10, 2, -3) },
|
||||
// 10. 左侧区域巡检
|
||||
// 10. 左侧区域巡检 - 检查左侧设施
|
||||
{ position: new THREE.Vector3(-5, 2, 3), lookAt: new THREE.Vector3(-8, 2, 0) },
|
||||
// 11. 返回中央区域
|
||||
// 11. 返回中央区域 - 回到中心
|
||||
{ position: new THREE.Vector3(0, 2, 0), lookAt: new THREE.Vector3(0, 3, -5) },
|
||||
// 12. 中央上升视角
|
||||
// 12. 中央上升视角 - 俯瞰全局
|
||||
{ position: new THREE.Vector3(0, 5, 2), lookAt: new THREE.Vector3(0, 0, 0) },
|
||||
// 13. 退出并俯视
|
||||
// 13. 退出并俯视 - 完成巡检,恢复初始视角
|
||||
{ position: new THREE.Vector3(0, 8, 12), lookAt: new THREE.Vector3(0, 0, 0) },
|
||||
]
|
||||
|
||||
/**
|
||||
* 初始化 Three.js 场景
|
||||
* 创建场景、相机、渲染器、灯光、地面等基础元素
|
||||
*/
|
||||
function initModel() {
|
||||
const width = threeRef.value!.clientWidth
|
||||
const height = threeRef.value!.clientHeight
|
||||
|
||||
// 创建场景
|
||||
// ===== 创建场景 =====
|
||||
scene.value = new THREE.Scene()
|
||||
|
||||
// 创建天空盒
|
||||
// ===== 创建天空盒(立方体背景)=====
|
||||
// 使用6个面的立方体模拟天空
|
||||
const skyboxGeometry = new THREE.BoxGeometry(500, 500, 500)
|
||||
const skyboxMaterials = [
|
||||
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // right
|
||||
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // left
|
||||
new THREE.MeshBasicMaterial({ color: 0x0F0F1E, side: THREE.BackSide }), // top
|
||||
new THREE.MeshBasicMaterial({ color: 0x000000, side: THREE.BackSide }), // bottom
|
||||
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // front
|
||||
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // back
|
||||
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // 右侧(深蓝灰)
|
||||
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // 左侧
|
||||
new THREE.MeshBasicMaterial({ color: 0x0F0F1E, side: THREE.BackSide }), // 顶部(更深)
|
||||
new THREE.MeshBasicMaterial({ color: 0x000000, side: THREE.BackSide }), // 底部(黑色)
|
||||
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // 前侧
|
||||
new THREE.MeshBasicMaterial({ color: 0x1A1A2E, side: THREE.BackSide }), // 后侧
|
||||
]
|
||||
const skybox = new THREE.Mesh(skyboxGeometry, skyboxMaterials)
|
||||
scene.value.add(skybox)
|
||||
|
||||
scene.value.background = new THREE.Color(0x000000)
|
||||
// 添加雾效增强深度感
|
||||
scene.value.background = new THREE.Color(0x000000) // 场景背景色(黑色)
|
||||
// 添加雾效增强深度感和空间层次
|
||||
// Fog(颜色, 起始距离, 结束距离)
|
||||
scene.value.fog = new THREE.Fog(0x000000, 10, 50)
|
||||
|
||||
// 创建透视相机 PerspectiveCamera(视野角度, 宽高比, 近截面, 远截面)
|
||||
// ===== 创建透视相机 =====
|
||||
// PerspectiveCamera(视野角度, 宽高比, 近截面, 远截面)
|
||||
camera.value = new THREE.PerspectiveCamera(
|
||||
60,
|
||||
60, // 视野角度 60°(较窄,适合建筑巡检)
|
||||
width / height,
|
||||
0.1,
|
||||
1000,
|
||||
0.1, // 近裁剪面
|
||||
1000, // 远裁剪面
|
||||
)
|
||||
camera.value.position.copy(initialCameraPosition)
|
||||
camera.value.position.copy(initialCameraPosition) // 设置初始位置
|
||||
|
||||
// 创建渲染器
|
||||
renderer.value = new THREE.WebGLRenderer({ antialias: true })
|
||||
// ===== 创建 WebGL 渲染器 =====
|
||||
renderer.value = new THREE.WebGLRenderer({ antialias: true }) // 抗锯齿
|
||||
renderer.value.setSize(width, height)
|
||||
renderer.value.setPixelRatio(window.devicePixelRatio)
|
||||
renderer.value.setPixelRatio(window.devicePixelRatio) // 高清屏适配
|
||||
// 开启阴影贴图
|
||||
renderer.value.shadowMap.enabled = true
|
||||
renderer.value.shadowMap.type = THREE.PCFSoftShadowMap
|
||||
renderer.value.shadowMap.type = THREE.PCFSoftShadowMap // PCF 软阴影
|
||||
threeRef.value!.appendChild(renderer.value.domElement)
|
||||
|
||||
// 创建轨道控制器
|
||||
// ===== 创建轨道控制器 =====
|
||||
controls.value = new OrbitControls(camera.value, renderer.value.domElement)
|
||||
controls.value.enableDamping = true // 启用阻尼(惯性)
|
||||
controls.value.dampingFactor = 0.05
|
||||
controls.value.minDistance = 2
|
||||
controls.value.maxDistance = 50
|
||||
controls.value.maxPolarAngle = Math.PI / 2 * 1.8 // 限制垂直旋转角度
|
||||
controls.value.enableDamping = true // 启用阻尼(惯性效果)
|
||||
controls.value.dampingFactor = 0.05 // 阻尼系数
|
||||
controls.value.minDistance = 2 // 最小缩放距离
|
||||
controls.value.maxDistance = 50 // 最大缩放距离
|
||||
controls.value.maxPolarAngle = Math.PI / 2 * 1.8 // 限制垂直旋转角度(防止翻转)
|
||||
|
||||
// 添加环境光
|
||||
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 0.6)
|
||||
// ===== 灯光系统 =====
|
||||
|
||||
// 环境光 - 提供基础照明
|
||||
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 0.6) // 白色,强度 0.6
|
||||
scene.value.add(ambientLight)
|
||||
|
||||
// 添加主平行光(模拟太阳光)
|
||||
const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 1.2)
|
||||
directionalLight.position.set(10, 15, 10)
|
||||
directionalLight.castShadow = true
|
||||
// 主平行光(模拟太阳光)
|
||||
const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 1.2) // 强度 1.2
|
||||
directionalLight.position.set(10, 15, 10) // 光源位置(右上方)
|
||||
directionalLight.castShadow = true // 投射阴影
|
||||
|
||||
// 配置阴影相机范围(正交投影)
|
||||
directionalLight.shadow.camera.left = -20
|
||||
directionalLight.shadow.camera.right = 20
|
||||
directionalLight.shadow.camera.top = 20
|
||||
directionalLight.shadow.camera.bottom = -20
|
||||
directionalLight.shadow.mapSize.width = 2048
|
||||
directionalLight.shadow.mapSize.width = 2048 // 阴影贴图分辨率
|
||||
directionalLight.shadow.mapSize.height = 2048
|
||||
scene.value.add(directionalLight)
|
||||
|
||||
// 添加辅助光源(从另一个方向)
|
||||
// 辅助光源(填充光)- 从另一个方向照亮暗部
|
||||
const fillLight = new THREE.DirectionalLight(0xFFFFFF, 0.5)
|
||||
fillLight.position.set(-10, 10, -10)
|
||||
fillLight.position.set(-10, 10, -10) // 左上后方
|
||||
scene.value.add(fillLight)
|
||||
|
||||
// 添加地面
|
||||
const groundGeometry = new THREE.PlaneGeometry(100, 100)
|
||||
// ===== 创建地面 =====
|
||||
const groundGeometry = new THREE.PlaneGeometry(100, 100) // 100x100 的平面
|
||||
const groundMaterial = new THREE.MeshStandardMaterial({
|
||||
color: 0x999999,
|
||||
roughness: 0.8,
|
||||
metalness: 0.2,
|
||||
color: 0x999999, // 灰色
|
||||
roughness: 0.8, // 粗糙度(不光滑)
|
||||
metalness: 0.2, // 金属度(略有金属感)
|
||||
})
|
||||
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
|
||||
ground.rotation.x = -Math.PI / 2
|
||||
ground.position.y = 0
|
||||
ground.receiveShadow = true
|
||||
ground.rotation.x = -Math.PI / 2 // 旋转 90° 使其水平
|
||||
ground.position.y = 0 // 位于 Y=0 平面
|
||||
ground.receiveShadow = true // 接收阴影
|
||||
scene.value.add(ground)
|
||||
|
||||
// 加载模型
|
||||
// ===== 加载 GLTF 模型 =====
|
||||
const loader = new GLTFLoader()
|
||||
loader.load('/mzjc_bansw.glb', (gltf) => {
|
||||
const model = gltf.scene
|
||||
|
||||
// 遍历模型,启用阴影
|
||||
// 遍历模型所有子对象,启用阴影
|
||||
model.traverse((child) => {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
child.castShadow = true
|
||||
child.receiveShadow = true
|
||||
child.castShadow = true // 投射阴影
|
||||
child.receiveShadow = true // 接收阴影
|
||||
}
|
||||
})
|
||||
|
||||
@@ -161,46 +201,56 @@ function initModel() {
|
||||
console.error('模型加载失败:', error)
|
||||
})
|
||||
|
||||
/**
|
||||
* 动画循环函数
|
||||
*
|
||||
* 职责:
|
||||
* 1. 处理巡检动画逻辑(相机路径插值)
|
||||
* 2. 更新轨道控制器
|
||||
* 3. 渲染场景
|
||||
*/
|
||||
function animate() {
|
||||
requestAnimationFrame(animate)
|
||||
requestAnimationFrame(animate) // 递归调用
|
||||
|
||||
// 巡检漫游效果
|
||||
// ===== 巡检漫游动画逻辑 =====
|
||||
if (tourMode.value !== 'none' && camera.value && controls.value) {
|
||||
// 根据模式确定移动速度
|
||||
const speed = tourMode.value === 'slow' ? 0.003 : 0.008 // 慢速或快速
|
||||
tourTime += speed
|
||||
tourTime += speed // 累加时间
|
||||
|
||||
const totalPoints = inspectionPath.length
|
||||
const totalProgress = tourTime % totalPoints
|
||||
const currentIndex = Math.floor(totalProgress)
|
||||
const nextIndex = (currentIndex + 1) % totalPoints
|
||||
const t = totalProgress - currentIndex
|
||||
const totalPoints = inspectionPath.length // 路径点总数
|
||||
const totalProgress = tourTime % totalPoints // 当前进度(0 到 totalPoints 循环)
|
||||
const currentIndex = Math.floor(totalProgress) // 当前路径点索引
|
||||
const nextIndex = (currentIndex + 1) % totalPoints // 下一个路径点索引(循环)
|
||||
const t = totalProgress - currentIndex // 两点之间的插值参数(0-1)
|
||||
|
||||
// 使用平滑的缓动函数
|
||||
const smoothT = t * t * (3 - 2 * t) // smoothstep
|
||||
// 使用平滑的缓动函数(SmoothStep)
|
||||
// smoothstep: 在 0 和 1 处导数为 0,产生平滑的加速减速效果
|
||||
const smoothT = t * t * (3 - 2 * t)
|
||||
|
||||
// 获取当前和下一个路径点
|
||||
const currentPoint = inspectionPath[currentIndex]!
|
||||
const nextPoint = inspectionPath[nextIndex]!
|
||||
|
||||
// 插值位置
|
||||
// 插值计算相机位置
|
||||
const newPosition = new THREE.Vector3()
|
||||
newPosition.lerpVectors(currentPoint.position, nextPoint.position, smoothT)
|
||||
camera.value.position.copy(newPosition)
|
||||
|
||||
// 插值观察目标
|
||||
// 插值计算观察目标
|
||||
const newTarget = new THREE.Vector3()
|
||||
newTarget.lerpVectors(currentPoint.lookAt, nextPoint.lookAt, smoothT)
|
||||
controls.value.target.copy(newTarget)
|
||||
|
||||
// 禁用控制器的用户交互
|
||||
// 巡检模式下禁用用户控制
|
||||
controls.value.enabled = false
|
||||
}
|
||||
else if (controls.value) {
|
||||
// 非漫游模式时启用控制器
|
||||
// 非巡检模式时启用用户控制
|
||||
controls.value.enabled = true
|
||||
}
|
||||
|
||||
// 更新控制器
|
||||
// 更新轨道控制器(处理阻尼等)
|
||||
controls.value?.update()
|
||||
|
||||
// 执行渲染
|
||||
@@ -208,13 +258,16 @@ function initModel() {
|
||||
renderer.value.render(scene.value, camera.value)
|
||||
}
|
||||
}
|
||||
animate()
|
||||
animate() // 启动动画循环
|
||||
|
||||
// 窗口自适应
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener('resize', handleResize)
|
||||
}
|
||||
|
||||
// 处理窗口大小变化
|
||||
/**
|
||||
* 处理窗口大小变化
|
||||
* 更新相机宽高比和渲染器尺寸
|
||||
*/
|
||||
function handleResize() {
|
||||
if (!threeRef.value || !camera.value || !renderer.value)
|
||||
return
|
||||
@@ -224,55 +277,66 @@ function handleResize() {
|
||||
|
||||
// 更新相机宽高比
|
||||
camera.value.aspect = width / height
|
||||
camera.value.updateProjectionMatrix()
|
||||
camera.value.updateProjectionMatrix() // 必须调用以应用更改
|
||||
|
||||
// 更新渲染器尺寸
|
||||
renderer.value.setSize(width, height)
|
||||
renderer.value.setPixelRatio(window.devicePixelRatio)
|
||||
}
|
||||
|
||||
// 慢速巡检漫游
|
||||
/**
|
||||
* 启动慢速巡检
|
||||
* 速度:0.003,适合详细观察
|
||||
*/
|
||||
function startSlowTour() {
|
||||
tourMode.value = 'slow'
|
||||
tourTime = 0
|
||||
tourTime = 0 // 重置时间,从第一个路径点开始
|
||||
}
|
||||
|
||||
// 快速巡检漫游
|
||||
/**
|
||||
* 启动快速巡检
|
||||
* 速度:0.008,适合快速浏览
|
||||
*/
|
||||
function startFastTour() {
|
||||
tourMode.value = 'fast'
|
||||
tourTime = 0
|
||||
tourTime = 0 // 重置时间,从第一个路径点开始
|
||||
}
|
||||
|
||||
// 停止漫游
|
||||
/**
|
||||
* 停止巡检
|
||||
* 恢复相机到初始位置,重新启用用户控制
|
||||
*/
|
||||
function stopTour() {
|
||||
tourMode.value = 'none'
|
||||
tourTime = 0
|
||||
if (camera.value && controls.value) {
|
||||
camera.value.position.copy(initialCameraPosition)
|
||||
controls.value.target.copy(initialCameraTarget)
|
||||
controls.value.enabled = true
|
||||
camera.value.position.copy(initialCameraPosition) // 恢复初始位置
|
||||
controls.value.target.copy(initialCameraTarget) // 恢复初始目标
|
||||
controls.value.enabled = true // 重新启用用户控制
|
||||
controls.value.update()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(initModel)
|
||||
// ===== 生命周期钩子 =====
|
||||
onMounted(initModel) // 组件挂载时初始化
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理事件监听
|
||||
window.removeEventListener('resize', handleResize)
|
||||
// 销毁控制器
|
||||
controls.value?.dispose()
|
||||
// 销毁渲染器
|
||||
renderer.value?.dispose()
|
||||
// 组件卸载时清理资源
|
||||
window.removeEventListener('resize', handleResize) // 移除事件监听
|
||||
controls.value?.dispose() // 销毁控制器
|
||||
renderer.value?.dispose() // 销毁渲染器
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="threeRef" class="w-full h-full" />
|
||||
<!-- 导航链接 -->
|
||||
<router-link to="/three/8" class="position-fixed left-20px top-20px">
|
||||
漫游 修改材质(设备告警业务)
|
||||
</router-link>
|
||||
<!-- 控制面板 -->
|
||||
<div class="position-fixed right-20px top-20px flex flex-col gap-12px">
|
||||
<!-- 巡检控制按钮组 -->
|
||||
<div class="flex gap-12px">
|
||||
<button
|
||||
:class="{ 'bg-blue-500 text-white': tourMode === 'slow' }"
|
||||
@@ -296,6 +360,7 @@ onUnmounted(() => {
|
||||
停止漫游
|
||||
</button>
|
||||
</div>
|
||||
<!-- 状态提示 -->
|
||||
<div v-if="tourMode !== 'none'" class="bg-white px-12px py-8px rounded shadow text-sm">
|
||||
正在进行{{ tourMode === 'slow' ? '慢速' : '快速' }}巡检...
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user