| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476 |
- <template>
- <div class="w-full h-full">
- <canvas id="canvasDom"></canvas>
- <NProgress
- class="absolute top-1/2 left-1/2 transform-translate--50%"
- v-if="percentageFlag"
- type="circle"
- processing
- :percentage="percentage"
- :fill-border-radius="0"
- />
- <RightClick
- v-auth="['mraddPointBtn']"
- v-model="rightClickD.show"
- :flag="rightClickFlag"
- :x="rightClickD.x"
- :y="rightClickD.y"
- :intersection="rightClickD.intersection"
- />
- </div>
- </template>
- <script setup>
- import * as THREE from 'three'
- import { NProgress } from 'naive-ui'
- import { gsap } from 'gsap'
- // 导入控制器 https://threejs.org/docs/index.html#examples/zh/controls/OrbitControls
- import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
- import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
- import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'
- import { onMounted, onUnmounted, provide, ref } from 'vue'
- import { useOutsideHomeStore } from '@/stores/modules/home'
- import { useOutsideSystemStore } from '@/stores/modules/system'
- import { getAutofitScale, $mitt, hasPermission } from '@/utils'
- import storage from '@/utils/storage'
- import RightClick from './components/rightClick.vue'
- const emits = defineEmits(['onGetData'])
- const rightClickD = reactive({
- show: false,
- x: 0,
- y: 0,
- intersection: null
- })
- const rightClickFlag = ref('add')
- const { API_MR_CAMERA_GET, API_MR_VIDEO_LIST_GET } = useRequest()
- const useHomeStore = useOutsideHomeStore()
- const useSystemStore = useOutsideSystemStore()
- const percentage = ref(0)
- const percentageFlag = ref(true)
- const timelineCamera = gsap.timeline()
- const timelineControls = gsap.timeline()
- // 实现视角平滑移动方法
- function translateCamera(cameraPosition, controlsTarget, duration) {
- const _duration = duration ? duration : 1
- return new Promise((resolve) => {
- timelineCamera.to(camera.position, {
- x: cameraPosition.x,
- y: cameraPosition.y,
- z: cameraPosition.z,
- duration: _duration,
- ease: 'power2.inOut',
- onComplete: () => {}
- })
- timelineControls.to(controls.target, {
- x: controlsTarget.x,
- y: controlsTarget.y,
- z: controlsTarget.z,
- duration: _duration,
- ease: 'power2.inOut',
- onComplete: () => {}
- })
- setTimeout(() => {
- resolve()
- }, _duration)
- })
- }
- const sprites = []
- function addSprite(arr = []) {
- arr.forEach(({ x, y, z, id, RtspMain }) => {
- const sprite = new THREE.Sprite(
- new THREE.SpriteMaterial({
- map: new THREE.TextureLoader().load('textures/video-icon.png'),
- color: 0xffffff,
- depthWrite: false, //深度写入属性
- depthTest: false //depthTest属性值默认true,设置为false可以关闭深度测试.
- })
- )
- sprite.vData = {
- id,
- RtspMain
- }
- sprite.position.set(x, y, z)
- sprite.scale.set(0.4, 0.45, 0.5)
- // 设置精灵的锚点为底部中央
- sprite.center.set(0.5, 0)
- scene.add(sprite)
- sprites.push(sprite)
- })
- }
- provide('addSprite', addSprite)
- const raycaster = new THREE.Raycaster()
- const pointer = new THREE.Vector2()
- const addEvent = (dom = window) => {
- dom.addEventListener('click', (event) => {
- if (useHomeStore.temp === 'video') return
- pointer.x = (event.clientX / window.innerWidth) * 2 - 1
- pointer.y = -(event.clientY / window.innerHeight) * 2 + 1
- raycaster.setFromCamera(pointer, camera)
- const intersects = raycaster.intersectObjects(sprites)
- if (intersects.length > 0) {
- console.log('标签被点击!', intersects[0].object)
- const target = intersects[0].object
- translateCamera({ ...camera.position, z: 5 }, target.position, 1).then(() => {
- useHomeStore.temp = 'video'
- useSystemStore.deviceInfo = target.vData
- storage.local.set('d_id', target.vData.id)
- emits('onGetData', target.vData)
- })
- }
- })
- dom.addEventListener('contextmenu', onRightClick)
- }
- function onRightClick(event) {
- event.preventDefault()
- // 隐藏之前的菜单
- // document.getElementById('context-menu').style.display = 'none'
- // 计算鼠标位置
- pointer.x = (event.clientX / window.innerWidth) * 2 - 1
- pointer.y = -(event.clientY / window.innerHeight) * 2 + 1
- // 更新射线投射
- raycaster.setFromCamera(pointer, camera)
- // 检测与模型的交点
- const intersects = raycaster.intersectObjects(scene.children, true)
- if (intersects.length > 0) {
- if (intersects[0].object.type === 'Sprite') {
- rightClickFlag.value = 'del'
- rightClickD.intersection = intersects[0]
- } else {
- rightClickFlag.value = 'add'
- rightClickD.intersection = intersects[0].point
- }
- // 显示右键菜单
- rightClickD.show = true
- rightClickD.x = event.clientX / getAutofitScale()
- rightClickD.y = event.clientY / getAutofitScale()
- // 存储交点信息
- } else {
- // 如果没有点击到物体,隐藏菜单
- rightClickD.show = false
- }
- }
- const destroyedMenu = () => {
- rightClickD.show = false
- }
- document.addEventListener('click', destroyedMenu)
- // 创建场景
- const scene = new THREE.Scene()
- let camera = null
- let renderer = null
- let cube = null
- let axesHelper = null
- let controls = null
- let loader = null
- let heatmapMesh // 用于控制热力图地形的 Mesh
- const init = () => {
- camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000)
- // axesHelper = new THREE.AxesHelper(5)
- renderer = new THREE.WebGLRenderer({
- canvas: document.getElementById('canvasDom'),
- alpha: true
- })
- camera.position.z = 10
- camera.position.y = 2
- // camera.position.x = 2;
- camera.lookAt(0, 0, 0)
- // scene.add(axesHelper)
- controls = new OrbitControls(camera, renderer.domElement)
- controls.enableDamping = true
- controls.dampingFactor = 0.05
- // 加载模型
- percentageFlag.value = true
- // 注意:加载的3d模型 没有灯光是纯黑色 有光才会反射出颜色
- loader = new GLTFLoader()
- loader.load(
- '3d/tiny_city_1k.glb',
- function (gltf) {
- scene.add(gltf.scene)
- percentageFlag.value = false
- gltf.scene.scale.x = 0
- gltf.scene.scale.y = 0
- gltf.scene.scale.z = 0
- gltf.scene.rotation.y = 2
- gsap.to(gltf.scene.rotation, {
- y: 0,
- duration: 1,
- ease: 'power1.inOut'
- })
- gsap.to(gltf.scene.scale, {
- y: 1,
- x: 1,
- z: 1,
- duration: 1,
- ease: 'power1.inOut',
- onComplete: async () => {
- if (
- useSystemStore.permissions &&
- !hasPermission(['mrdevicePointShow'], useSystemStore.permissions)
- ) {
- return
- }
- const res = await API_MR_CAMERA_GET()
- // const sprites = [
- // { x: 1.5, y: 0.7, z: 0 },
- // { x: 1, y: -0.16, z: 0 },
- // { x: -0.5, y: -0.16, z: -1.5 },
- // { x: -0.5, y: -0.8, z: 1 }
- // ]
- const sprites = res.map((item) => ({
- x: item.X,
- y: item.Y,
- z: item.Z,
- id: item.Id,
- RtspMain: item.RtspMain
- }))
- addSprite(sprites)
- }
- })
- },
- function (xhr) {
- percentage.value = Math.floor((xhr.loaded / xhr.total) * 100)
- console.log('加载完成的百分比' + (xhr.loaded / xhr.total) * 100 + '%')
- },
- function (error) {
- console.error(error)
- }
- )
- // 加载hdr环境贴图
- const rgbeLoader = new RGBELoader()
- rgbeLoader.load('textures/sky_linekotsi_01_b_HDRI.hdr', (texture) => {
- texture.mapping = THREE.EquirectangularReflectionMapping
- scene.environment = texture
- scene.background = 'transparent'
- })
- createHeatmapTerrain()
- addEvent(renderer.domElement)
- }
- let animationFrameID = null
- const clock = new THREE.Clock()
- function animate() {
- // cube.rotation.x += 0.01;
- // cube.rotation.y += 0.01;
- renderer.render(scene, camera)
- controls.update()
- animationFrameID = requestAnimationFrame(animate)
- }
- const resize = () => {
- // 更新相机的宽高比
- camera.aspect = window.innerWidth / window.innerHeight
- camera.updateProjectionMatrix()
- // 更新渲染器的尺寸
- renderer.setSize(window.innerWidth, window.innerHeight)
- renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
- }
- window.addEventListener('resize', resize, false)
- // 内存释放
- const cleanupThreeJS = () => {
- // 停止渲染循环(如果有的话) animationFrameID是requestAnimationFrame的返回值
- cancelAnimationFrame(animationFrameID)
- // 遍历并清理场景中的所有物体
- scene.traverse((child) => {
- if (child instanceof THREE.Mesh) {
- if (child.geometry?.dispose) child.geometry.dispose()
- if (child.material?.dispose) child.material.dispose()
- if (child.material?.texture?.dispose) child.material.texture.dispose()
- }
- if (child instanceof THREE.Group) child.clear()
- if (child instanceof THREE.Object3D) child.clear()
- })
- renderer.dispose() // 如果你的renderer对象有dispose方法的话
- }
- function generateHeatmapValue(x, z) {
- // 增加 scale,使波纹更密集
- const scale = 0.4
- // 使用两个振幅不同的波叠加,创造更复杂的起伏
- const value = Math.sin(x * scale) * 0.8 + Math.cos(z * scale * 1.5) * 1.2
- // 我们想要热力值在中心区域更集中、更高。
- // 使用高斯函数或类似的衰减函数,使中心点 (0, 0) 附近的值更高。
- const distanceSq = x * x + z * z
- const centerFactor = Math.exp(-distanceSq * 0.005) // 0.005 控制衰减速度
- // 结合起伏和中心集中度
- const combinedValue = (value + 2) * 0.5 * centerFactor
- // 确保值在 0 到 1 之间
- return Math.max(0.0, Math.min(1.0, combinedValue))
- }
- // 创建热力图地形
- function createHeatmapTerrain() {
- // *** 调整尺寸: 进一步缩小到 35x35,并增加分段数使曲面更平滑 ***
- const width = 35
- const height = 35
- const widthSegments = 80 // 增加分段数
- const heightSegments = 80
- const maxTerrainHeight = 4 // 增加地形最大高度,使起伏更剧烈
- const geometry = new THREE.PlaneGeometry(width, height, widthSegments, heightSegments)
- geometry.rotateX(-Math.PI / 2)
- const positionAttribute = geometry.getAttribute('position')
- const heatmapValues = []
- for (let i = 0; i < positionAttribute.count; i++) {
- const x = positionAttribute.getX(i)
- const z = positionAttribute.getZ(i)
- // 1. 获取热力值 (0 到 1 之间)
- const originalHeatValue = generateHeatmapValue(x, z)
- // 为了让“红色区域不要太红”,我们对用于着色的热力值做轻微压缩/偏移处理:
- // - 将热力值整体缩小到原来的 75%(降低最高端的红色强度)
- // - 加上一个小的偏移以避免完全变暗
- // - 最终上限设置为 0.95,避免出现完全饱和的红色
- const colorHeatValue = Math.min(0.95, Math.max(0.0, originalHeatValue * 0.75 + 0.08))
- heatmapValues.push(colorHeatValue)
- // 2. 根据原始热力值和坐标生成地形高度(高度仍使用 originalHeatValue,使视觉的高低与热力对应)
- const y = originalHeatValue * maxTerrainHeight + 15 // 整体抬高到建筑上方
- positionAttribute.setY(i, y)
- }
- geometry.setAttribute('heatValue', new THREE.Float32BufferAttribute(heatmapValues, 1))
- geometry.computeVertexNormals()
- // 自定义着色器材质(与上次相同,但确保颜色映射和您图片一致)
- const heatmapMaterial = new THREE.ShaderMaterial({
- uniforms: {
- uTime: { value: 0.0 },
- uOpacity: { value: 1.0 } // 全局不透明度控制,可在运行时调整
- },
- transparent: true, // 允许透明
- depthWrite: false, // 关闭深度写入以便正确混合
- blending: THREE.NormalBlending,
- vertexShader: `
- attribute float heatValue;
- varying float vHeatValue;
- void main() {
- vHeatValue = heatValue;
- gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
- }
- `,
- fragmentShader: `
- varying float vHeatValue;
- uniform float uOpacity;
- void main() {
- vec3 color = vec3(0.0);
- // 颜色映射逻辑: 确保渐变从深蓝 (最低值) 到红色 (最高值)
- if (vHeatValue < 0.25) {
- // 深蓝/黑蓝 到 蓝/青 (对应图中最低部分)
- color = mix(vec3(0.0, 0.1, 0.4), vec3(0.0, 0.5, 1.0), vHeatValue * 4.0);
- } else if (vHeatValue < 0.5) {
- // 蓝/青 到 绿色 (对应图中中间部分)
- color = mix(vec3(0.0, 0.5, 1.0), vec3(0.0, 1.0, 0.2), (vHeatValue - 0.25) * 4.0);
- } else if (vHeatValue < 0.75) {
- // 绿色 到 黄色 (对应图中高一些的部分)
- color = mix(vec3(0.0, 1.0, 0.2), vec3(1.0, 1.0, 0.0), (vHeatValue - 0.5) * 4.0);
- } else {
- // 黄色 到 红色/橙色 (对应图中最高峰部分)
- color = mix(vec3(1.0, 1.0, 0.0), vec3(1.0, 0.2, 0.0), (vHeatValue - 0.75) * 4.0);
- }
- // 基于热力值计算每片面的不透明度:低热力值更透明,热值高则更不透明
- float minA = 0.08;
- float maxA = 0.95;
- float localOpacity = mix(minA, maxA, vHeatValue);
- // 将局部不透明度与全局控制相乘,方便外部动态调整整体透明度
- float finalA = clamp(localOpacity * uOpacity, 0.0, 1.0);
- gl_FragColor = vec4(color, finalA);
- }
- `,
- side: THREE.DoubleSide
- })
- heatmapMesh = new THREE.Mesh(geometry, heatmapMaterial)
- heatmapMesh.visible = false
- heatmapMesh.scale.set(0.2, 0.2, 0.2)
- heatmapMesh
- scene.add(heatmapMesh)
- }
- function toggleHeatmapVisibility(visible) {
- if (heatmapMesh) {
- heatmapMesh.visible = visible
- if (visible) {
- translateCamera({ x: 0, y: 10, z: 20 }, { x: 0, y: 0, z: 0 }, 1)
- } else {
- translateCamera({ x: 0, y: 2, z: 10 }, { x: 0, y: 0, z: 0 }, 1)
- }
- }
- }
- watch(
- () => useHomeStore.temp,
- (newV) => {
- if (newV != 'video') {
- translateCamera({ x: 0, y: 2, z: 10 }, { x: 0, y: 0, z: 0 }, 1)
- }
- }
- )
- $mitt.on('onToolsIndex', (v) => {
- if (v.type === 'rlt') {
- toggleHeatmapVisibility(v.selected)
- }
- })
- onMounted(() => {
- init()
- resize()
- animate()
- })
- onUnmounted(() => {
- window.removeEventListener('resize', resize)
- document.removeEventListener('click', destroyedMenu)
- cleanupThreeJS()
- })
- </script>
- <style lang="scss" scoped>
- #canvasDom {
- // position: fixed;
- width: 100%;
- height: 100%;
- }
- </style>
|