Files
three-demos/src/views/three/7/index.vue
2025-11-21 13:30:33 +08:00

369 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
* @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: 相卿
* @LastEditTime: 2025-11-20 16:40:34
-->
<script lang="ts" setup>
import * as THREE from 'three'
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>() // 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) // 相机初始观察目标
// ========== 巡检路径定义 ==========
/**
* 路径点接口
* 定义相机在巡检过程中的位置和观察目标
*/
interface PathPoint {
position: THREE.Vector3 // 相机位置
lookAt: THREE.Vector3 // 相机观察目标(朝向)
}
/**
* 巡检路径点数组
*
* 巡检路线设计:
* 1. 外部俯视 → 2. 正面接近 → 3. 进入前厅 →
* 4-5. 右侧巡检 → 6. 右后方 → 7. 后方中央 →
* 8-9. 左侧巡检 → 10. 左前方 → 11-12. 中央回顾 →
* 13. 退出俯视
*
* 形成完整的顺时针巡检循环
*/
const inspectionPath: PathPoint[] = [
// 1. 外部俯视全景 - 建立整体认知
{ position: new THREE.Vector3(0, 8, 15), lookAt: new THREE.Vector3(0, 0, 0) },
// 2. 从正面接近入口 - 降低高度,准备进入
{ position: new THREE.Vector3(0, 3, 10), lookAt: new THREE.Vector3(0, 2, 0) },
// 3. 进入前厅区域 - 穿过入口
{ position: new THREE.Vector3(0, 2, 5), lookAt: new THREE.Vector3(0, 2, -2) },
// 4. 右侧区域巡检 - 检查右侧设施
{ position: new THREE.Vector3(5, 2, 3), lookAt: new THREE.Vector3(8, 2, 0) },
// 5. 深入右侧内部 - 详细检查右侧深处
{ position: new THREE.Vector3(8, 2.5, 0), lookAt: new THREE.Vector3(10, 2, -3) },
// 6. 右后方区域 - 转向后方
{ position: new THREE.Vector3(6, 2, -5), lookAt: new THREE.Vector3(3, 2, -8) },
// 7. 后方中央区域 - 检查后方设施
{ position: new THREE.Vector3(0, 2.5, -8), lookAt: new THREE.Vector3(0, 2, -5) },
// 8. 左后方区域 - 转向左侧
{ position: new THREE.Vector3(-6, 2, -5), lookAt: new THREE.Vector3(-3, 2, -8) },
// 9. 深入左侧内部 - 详细检查左侧深处
{ position: new THREE.Vector3(-8, 2.5, 0), lookAt: new THREE.Vector3(-10, 2, -3) },
// 10. 左侧区域巡检 - 检查左侧设施
{ position: new THREE.Vector3(-5, 2, 3), lookAt: new THREE.Vector3(-8, 2, 0) },
// 11. 返回中央区域 - 回到中心
{ position: new THREE.Vector3(0, 2, 0), lookAt: new THREE.Vector3(0, 3, -5) },
// 12. 中央上升视角 - 俯瞰全局
{ position: new THREE.Vector3(0, 5, 2), lookAt: new THREE.Vector3(0, 0, 0) },
// 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 }), // 右侧(深蓝灰)
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) // 场景背景色(黑色)
// 添加雾效增强深度感和空间层次
// Fog(颜色, 起始距离, 结束距离)
scene.value.fog = new THREE.Fog(0x000000, 10, 50)
// ===== 创建透视相机 =====
// PerspectiveCamera(视野角度, 宽高比, 近截面, 远截面)
camera.value = new THREE.PerspectiveCamera(
60, // 视野角度 60°较窄适合建筑巡检
width / height,
0.1, // 近裁剪面
1000, // 远裁剪面
)
camera.value.position.copy(initialCameraPosition) // 设置初始位置
// ===== 创建 WebGL 渲染器 =====
renderer.value = new THREE.WebGLRenderer({ antialias: true }) // 抗锯齿
renderer.value.setSize(width, height)
renderer.value.setPixelRatio(window.devicePixelRatio) // 高清屏适配
// 开启阴影贴图
renderer.value.shadowMap.enabled = true
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 // 限制垂直旋转角度(防止翻转)
// ===== 灯光系统 =====
// 环境光 - 提供基础照明
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 0.6) // 白色,强度 0.6
scene.value.add(ambientLight)
// 主平行光(模拟太阳光)
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.height = 2048
scene.value.add(directionalLight)
// 辅助光源(填充光)- 从另一个方向照亮暗部
const fillLight = new THREE.DirectionalLight(0xFFFFFF, 0.5)
fillLight.position.set(-10, 10, -10) // 左上后方
scene.value.add(fillLight)
// ===== 创建地面 =====
const groundGeometry = new THREE.PlaneGeometry(100, 100) // 100x100 的平面
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x999999, // 灰色
roughness: 0.8, // 粗糙度(不光滑)
metalness: 0.2, // 金属度(略有金属感)
})
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
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 // 接收阴影
}
})
scene.value!.add(model)
}, undefined, (error) => {
console.error('模型加载失败:', error)
})
/**
* 动画循环函数
*
* 职责:
* 1. 处理巡检动画逻辑(相机路径插值)
* 2. 更新轨道控制器
* 3. 渲染场景
*/
function animate() {
requestAnimationFrame(animate) // 递归调用
// ===== 巡检漫游动画逻辑 =====
if (tourMode.value !== 'none' && camera.value && controls.value) {
// 根据模式确定移动速度
const speed = tourMode.value === 'slow' ? 0.003 : 0.008 // 慢速或快速
tourTime += speed // 累加时间
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
// 使用平滑的缓动函数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()
// 执行渲染
if (renderer.value && scene.value && camera.value) {
renderer.value.render(scene.value, camera.value)
}
}
animate() // 启动动画循环
// 监听窗口大小变化
window.addEventListener('resize', handleResize)
}
/**
* 处理窗口大小变化
* 更新相机宽高比和渲染器尺寸
*/
function handleResize() {
if (!threeRef.value || !camera.value || !renderer.value)
return
const width = threeRef.value.clientWidth
const height = threeRef.value.clientHeight
// 更新相机宽高比
camera.value.aspect = width / height
camera.value.updateProjectionMatrix() // 必须调用以应用更改
// 更新渲染器尺寸
renderer.value.setSize(width, height)
renderer.value.setPixelRatio(window.devicePixelRatio)
}
/**
* 启动慢速巡检
* 速度0.003,适合详细观察
*/
function startSlowTour() {
tourMode.value = 'slow'
tourTime = 0 // 重置时间,从第一个路径点开始
}
/**
* 启动快速巡检
* 速度0.008,适合快速浏览
*/
function startFastTour() {
tourMode.value = 'fast'
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 // 重新启用用户控制
controls.value.update()
}
}
// ===== 生命周期钩子 =====
onMounted(initModel) // 组件挂载时初始化
onUnmounted(() => {
// 组件卸载时清理资源
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' }"
class="px-16px py-8px rounded hover:bg-blue-400"
@click="startSlowTour"
>
慢速巡检
</button>
<button
:class="{ 'bg-green-500 text-white': tourMode === 'fast' }"
class="px-16px py-8px rounded hover:bg-green-400"
@click="startFastTour"
>
快速巡检
</button>
<button
:class="{ 'bg-red-500 text-white': tourMode === 'none' }"
class="px-16px py-8px rounded hover:bg-red-400"
@click="stopTour"
>
停止漫游
</button>
</div>
<!-- 状态提示 -->
<div v-if="tourMode !== 'none'" class="bg-white px-12px py-8px rounded shadow text-sm">
正在进行{{ tourMode === 'slow' ? '慢速' : '快速' }}巡检...
</div>
</div>
</template>