demos
This commit is contained in:
@@ -3,11 +3,301 @@
|
|||||||
* @Autor: 刘 相卿
|
* @Autor: 刘 相卿
|
||||||
* @Date: 2025-11-17 16:07:33
|
* @Date: 2025-11-17 16:07:33
|
||||||
* @LastEditors: 刘 相卿
|
* @LastEditors: 刘 相卿
|
||||||
* @LastEditTime: 2025-11-20 11:44:48
|
* @LastEditTime: 2025-11-20 16:40:34
|
||||||
-->
|
-->
|
||||||
<script lang="ts" setup>
|
<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>()
|
||||||
|
const camera = shallowRef<THREE.PerspectiveCamera>()
|
||||||
|
const scene = shallowRef<THREE.Scene>()
|
||||||
|
const controls = shallowRef<OrbitControls>()
|
||||||
|
const renderer = shallowRef<THREE.WebGLRenderer>()
|
||||||
|
|
||||||
|
// 漫游状态
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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) },
|
||||||
|
]
|
||||||
|
|
||||||
|
function initModel() {
|
||||||
|
const width = threeRef.value!.clientWidth
|
||||||
|
const height = threeRef.value!.clientHeight
|
||||||
|
|
||||||
|
// 创建场景
|
||||||
|
scene.value = new THREE.Scene()
|
||||||
|
|
||||||
|
// 创建天空盒
|
||||||
|
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
|
||||||
|
]
|
||||||
|
const skybox = new THREE.Mesh(skyboxGeometry, skyboxMaterials)
|
||||||
|
scene.value.add(skybox)
|
||||||
|
|
||||||
|
scene.value.background = new THREE.Color(0x000000)
|
||||||
|
// 添加雾效增强深度感
|
||||||
|
scene.value.fog = new THREE.Fog(0x000000, 10, 50)
|
||||||
|
|
||||||
|
// 创建透视相机 PerspectiveCamera(视野角度, 宽高比, 近截面, 远截面)
|
||||||
|
camera.value = new THREE.PerspectiveCamera(
|
||||||
|
60,
|
||||||
|
width / height,
|
||||||
|
0.1,
|
||||||
|
1000,
|
||||||
|
)
|
||||||
|
camera.value.position.copy(initialCameraPosition)
|
||||||
|
|
||||||
|
// 创建渲染器
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
scene.value.add(ambientLight)
|
||||||
|
|
||||||
|
// 添加主平行光(模拟太阳光)
|
||||||
|
const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 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)
|
||||||
|
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
|
||||||
|
ground.position.y = 0
|
||||||
|
ground.receiveShadow = true
|
||||||
|
scene.value.add(ground)
|
||||||
|
|
||||||
|
// 加载模型
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
|
||||||
|
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
|
||||||
|
const currentIndex = Math.floor(totalProgress)
|
||||||
|
const nextIndex = (currentIndex + 1) % totalPoints
|
||||||
|
const t = totalProgress - currentIndex
|
||||||
|
|
||||||
|
// 使用平滑的缓动函数
|
||||||
|
const smoothT = t * t * (3 - 2 * t) // smoothstep
|
||||||
|
|
||||||
|
// 获取当前和下一个路径点
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 慢速巡检漫游
|
||||||
|
function startSlowTour() {
|
||||||
|
tourMode.value = 'slow'
|
||||||
|
tourTime = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 快速巡检漫游
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>7</div>
|
<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>
|
</template>
|
||||||
|
|||||||
@@ -1,13 +1,313 @@
|
|||||||
<!--
|
<!--
|
||||||
* @Description: 漫游 + 修改材质(设备告警业务)
|
* @Description: 漫游 + 修改材质(设备告警业务)
|
||||||
* @Autor: 刘 相卿
|
* @Autor: 刘 相卿
|
||||||
* @Date: 2025-11-17 16:07:33
|
* @Date: 2025-11-17 16:07:33
|
||||||
* @LastEditors: 刘 相卿
|
* @LastEditors: 刘 相卿
|
||||||
* @LastEditTime: 2025-11-20 11:44:57
|
* @LastEditTime: 2025-11-20 17:28:46
|
||||||
-->
|
-->
|
||||||
<script lang="ts" setup>
|
<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>()
|
||||||
|
const camera = shallowRef<THREE.PerspectiveCamera>()
|
||||||
|
const scene = shallowRef<THREE.Scene>()
|
||||||
|
const controls = shallowRef<OrbitControls>()
|
||||||
|
const renderer = shallowRef<THREE.WebGLRenderer>()
|
||||||
|
|
||||||
|
// 告警相关
|
||||||
|
const alertMesh = shallowRef<THREE.Mesh>()
|
||||||
|
const originalMaterial = shallowRef<THREE.Material>()
|
||||||
|
const alertOverlay = shallowRef<THREE.Mesh>() // 告警覆盖层
|
||||||
|
const isAlerting = ref(false)
|
||||||
|
let alertTime = 0
|
||||||
|
|
||||||
|
// 相机动画相关
|
||||||
|
let cameraAnimating = false
|
||||||
|
let cameraAnimationProgress = 0
|
||||||
|
const cameraStartPosition = new THREE.Vector3()
|
||||||
|
const cameraStartTarget = new THREE.Vector3()
|
||||||
|
const cameraEndPosition = new THREE.Vector3()
|
||||||
|
const cameraEndTarget = new THREE.Vector3()
|
||||||
|
|
||||||
|
function initModel() {
|
||||||
|
const width = threeRef.value!.clientWidth
|
||||||
|
const height = threeRef.value!.clientHeight
|
||||||
|
|
||||||
|
// 创建场景
|
||||||
|
scene.value = new THREE.Scene()
|
||||||
|
scene.value.background = new THREE.Color(0x1A1A2E)
|
||||||
|
scene.value.fog = new THREE.Fog(0x1A1A2E, 10, 50)
|
||||||
|
|
||||||
|
const axesHelper = new THREE.AxesHelper(5)
|
||||||
|
scene.value.add(axesHelper)
|
||||||
|
|
||||||
|
// 创建透视相机
|
||||||
|
camera.value = new THREE.PerspectiveCamera(
|
||||||
|
60,
|
||||||
|
width / height,
|
||||||
|
0.1,
|
||||||
|
1000,
|
||||||
|
)
|
||||||
|
camera.value.position.set(0, 5, 15)
|
||||||
|
|
||||||
|
// 创建渲染器
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
// 添加环境光
|
||||||
|
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 0.6)
|
||||||
|
scene.value.add(ambientLight)
|
||||||
|
|
||||||
|
// 添加主平行光
|
||||||
|
const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 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)
|
||||||
|
const groundMaterial = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0x555555,
|
||||||
|
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
|
||||||
|
scene.value.add(ground)
|
||||||
|
|
||||||
|
// 加载模型
|
||||||
|
const loader = new GLTFLoader()
|
||||||
|
loader.load('/mzjc_bansw.glb', (gltf) => {
|
||||||
|
const model = gltf.scene
|
||||||
|
|
||||||
|
// 遍历模型,找到 fmz_2_jzf_bs015 并启用阴影
|
||||||
|
model.traverse((child) => {
|
||||||
|
if (child instanceof THREE.Mesh) {
|
||||||
|
child.castShadow = true
|
||||||
|
child.receiveShadow = true
|
||||||
|
|
||||||
|
// 找到告警设备
|
||||||
|
if (child.name === 'fmz_2_jzf_bs015') {
|
||||||
|
alertMesh.value = child
|
||||||
|
// 保存原始材质
|
||||||
|
originalMaterial.value = child.material.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
scene.value!.add(model)
|
||||||
|
}, undefined, (error) => {
|
||||||
|
console.error('模型加载失败:', error)
|
||||||
|
})
|
||||||
|
|
||||||
|
function animate() {
|
||||||
|
requestAnimationFrame(animate)
|
||||||
|
|
||||||
|
// 告警闪烁效果
|
||||||
|
if (isAlerting.value && alertOverlay.value) {
|
||||||
|
alertTime += 0.05
|
||||||
|
// 使用正弦波创建闪烁效果,频率较快
|
||||||
|
const intensity = (Math.sin(alertTime * 5) + 1) / 2 // 0-1之间震荡
|
||||||
|
|
||||||
|
if (alertOverlay.value.material instanceof THREE.Material) {
|
||||||
|
const material = alertOverlay.value.material as THREE.MeshBasicMaterial
|
||||||
|
material.opacity = 0.3 + intensity * 0.4 // 透明度在0.3-0.7之间震荡
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 相机动画
|
||||||
|
if (cameraAnimating && camera.value && controls.value) {
|
||||||
|
cameraAnimationProgress += 0.008 // 控制移动速度
|
||||||
|
|
||||||
|
if (cameraAnimationProgress >= 1) {
|
||||||
|
cameraAnimationProgress = 1
|
||||||
|
cameraAnimating = false
|
||||||
|
controls.value.enabled = true // 动画结束后恢复控制器
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用缓动函数使运动更平滑
|
||||||
|
const t = easeInOutCubic(cameraAnimationProgress)
|
||||||
|
|
||||||
|
// 插值相机位置
|
||||||
|
camera.value.position.lerpVectors(cameraStartPosition, cameraEndPosition, t)
|
||||||
|
|
||||||
|
// 插值观察目标
|
||||||
|
const currentTarget = new THREE.Vector3()
|
||||||
|
currentTarget.lerpVectors(cameraStartTarget, cameraEndTarget, t)
|
||||||
|
controls.value.target.copy(currentTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controls.value) {
|
||||||
|
controls.value.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renderer.value && scene.value && camera.value) {
|
||||||
|
renderer.value.render(scene.value, camera.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animate()
|
||||||
|
|
||||||
|
// 处理窗口大小变化
|
||||||
|
function onResize() {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', onResize)
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', onResize)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓动函数
|
||||||
|
function easeInOutCubic(t: number): number {
|
||||||
|
return t < 0.5 ? 4 * t * t * t : 1 - (-2 * t + 2) ** 3 / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设备报警处理
|
||||||
|
function handleAlert() {
|
||||||
|
if (!alertMesh.value || !camera.value || !controls.value) {
|
||||||
|
console.warn('告警设备或相机未就绪')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换告警状态
|
||||||
|
isAlerting.value = !isAlerting.value
|
||||||
|
|
||||||
|
if (isAlerting.value) {
|
||||||
|
// 开始告警
|
||||||
|
alertTime = 0
|
||||||
|
|
||||||
|
// 创建半透明红色覆盖层
|
||||||
|
const geometry = alertMesh.value.geometry.clone()
|
||||||
|
const overlayMaterial = new THREE.MeshBasicMaterial({
|
||||||
|
color: 0xFF0000,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.2,
|
||||||
|
depthWrite: false,
|
||||||
|
side: THREE.DoubleSide,
|
||||||
|
})
|
||||||
|
|
||||||
|
alertOverlay.value = new THREE.Mesh(geometry, overlayMaterial)
|
||||||
|
// 复制告警物体的变换矩阵
|
||||||
|
alertOverlay.value.position.copy(alertMesh.value.position)
|
||||||
|
// alertOverlay.value.position.y -= 0.6
|
||||||
|
// alertOverlay.value.position.x += 0.6
|
||||||
|
// alertOverlay.value.position.z += 2
|
||||||
|
alertOverlay.value.rotation.copy(alertMesh.value.rotation)
|
||||||
|
alertOverlay.value.scale.copy(alertMesh.value.scale).multiplyScalar(1.01) // 稍微放大避免z-fighting
|
||||||
|
|
||||||
|
// 如果告警物体有父节点,添加到同一父节点
|
||||||
|
if (alertMesh.value.parent) {
|
||||||
|
alertMesh.value.parent.add(alertOverlay.value)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
scene.value?.add(alertOverlay.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算告警设备的世界坐标
|
||||||
|
const worldPosition = new THREE.Vector3()
|
||||||
|
alertMesh.value.getWorldPosition(worldPosition)
|
||||||
|
|
||||||
|
// 计算包围盒来获取设备的大小
|
||||||
|
const box = new THREE.Box3().setFromObject(alertMesh.value)
|
||||||
|
const size = box.getSize(new THREE.Vector3())
|
||||||
|
const maxDim = Math.max(size.x, size.y, size.z)
|
||||||
|
|
||||||
|
// 根据设备大小计算合适的观察距离,使用较小的倍数以靠近物体
|
||||||
|
const distance = Math.max(maxDim * 1.5, 2) // 距离是设备最大尺寸的1.5倍,最小2单位
|
||||||
|
|
||||||
|
// 计算观察方向 - 从设备的斜上前方观察,更接近物体
|
||||||
|
const direction = new THREE.Vector3(-1.2, 1.2, -1.8).normalize()
|
||||||
|
const targetPosition = worldPosition.clone().add(direction.multiplyScalar(distance))
|
||||||
|
|
||||||
|
// 保存当前相机状态
|
||||||
|
cameraStartPosition.copy(camera.value.position)
|
||||||
|
cameraStartTarget.copy(controls.value.target)
|
||||||
|
|
||||||
|
// 设置目标状态 - 相机位置和观察目标
|
||||||
|
cameraEndPosition.copy(targetPosition)
|
||||||
|
cameraEndTarget.copy(worldPosition) // 直接看向设备的世界坐标
|
||||||
|
|
||||||
|
// 开始动画
|
||||||
|
cameraAnimating = true
|
||||||
|
cameraAnimationProgress = 0
|
||||||
|
controls.value.enabled = false // 动画期间禁用控制器
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 停止告警,移除覆盖层
|
||||||
|
if (alertOverlay.value) {
|
||||||
|
alertOverlay.value.parent?.remove(alertOverlay.value)
|
||||||
|
alertOverlay.value.geometry.dispose()
|
||||||
|
if (alertOverlay.value.material instanceof THREE.Material) {
|
||||||
|
alertOverlay.value.material.dispose()
|
||||||
|
}
|
||||||
|
alertOverlay.value = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initModel()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (renderer.value) {
|
||||||
|
renderer.value.dispose()
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>8</div>
|
<div ref="threeRef" class="w-full h-full" />
|
||||||
|
<router-link to="/three/9" 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-red-500': isAlerting }"
|
||||||
|
class="px-16px py-8px bg-blue-500 text-white rounded cursor-pointer hover:bg-blue-600"
|
||||||
|
@click="handleAlert"
|
||||||
|
>
|
||||||
|
{{ isAlerting ? '停止报警' : '设备报警' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user