three.js开发智慧园区

技术栈基于vue3+vite,项目地址

<template>
	<div id="jindu-text-con" v-if="progressBarShow">
		正在加载模型请稍等:<span id="jindu-text">{
   
   { progressText }}</span>
		<div class="jindu-con">
			<div id="jindu" :style="{ width: progressText }"></div>
		</div>
	</div>
	<video id="videoContainer"></video>
	<div id="container" ref="container"></div>
	<div class="operate-box">
		<el-button type="warning" @click="onReset">场景重置</el-button>
		<el-button type="warning" @click="onChangeTime">{
   
   { timeText }}</el-button>
	</div>
</template>
<script setup>
	import {
		ref,
		onMounted
	} from 'vue'
	import * as THREE from 'three'
	import Viewer from '@/common/threeModules/Viewer'
	import SkyBoxs from '@/common/threeModules/SkyBoxs'
	import Lights from '@/common/threeModules/Lights'
	import ModelLoader from '@/common/threeModules/ModelLoader'
	import Labels from '@/common/threeModules/Labels'
	import {
		Water
	} from 'three/examples/jsm/objects/Water2'
	import * as BufferGeometryUtils from 'three/addons/utils/BufferGeometryUtils.js'

	import gsap from 'gsap'

	let viewer = null
	let cityv1 = null
	let car = null
	let carLabel = null
	let officeLabel = null
	let officeBuild = null
	let oldOfficeBuild = {}
	let curve = null
	let Mesh26 = null
	let timeen = {}
	let modelLoader = null
	let labelIns = null // 标签实例
	let laboratoryBuild = {}
	let videoTextTure = null // 视频纹理
	let curFloorName = '' // 当前鼠标点击选中的楼层name
	let modelMoveName = '' // 当前鼠标移动过程中选中的模型name
	let selectedFloorName = '' // 已经选中过的楼层name
	let isSplit = false // 楼体是否分层
	let lastIndex // 记录上一次点击的楼层index
	let skyBoxs = null
	const sceneList = ['实验楼']
	const TimeNums = {
		day: '白天模式',
		night: '夜间模式'
	}

	let progress = 0 // 物体运动时在运动路径的初始位置,范围0~1
	const velocity = 0.001 // 影响运动速率的一个值,范围0~1,需要和渲染频率结合计算才能得到真正的速率
	const officeFloorList = Array(6)
		.fill(0)
		.map((item, index) => `zuo${index}`) // 办公室楼层

	const isopen = ref(false)
	const progressText = ref('0%')
	const progressBarShow = ref(true)
	const isDriver = ref(false)
	const timeText = ref(TimeNums.night)
	onMounted(() => {
		init()
	})
	const init = () => {
		viewer = new Viewer('container')
		skyBoxs = new SkyBoxs(viewer) // 创建天空盒
		viewer.camera.position.set(17, 10, 52)
		viewer.controls.maxPolarAngle = Math.PI / 2.1 // 限制controls的上下角度范围

		// viewer.renderer.setPixelRatio(window.devicePixelRatio * 2)

		viewer.renderer.shadowMap.enabled = true
		viewer.renderer.shadowMap.type = THREE.PCFSoftShadowMap

		const lights = new Lights(viewer)
		const ambientLight = lights.addAmbientLight() // 添加环境光
		ambientLight.setOption({
			color: 0xffffff,
			intensity: 1 // 环境光强度
		})
		ambientLight.light.name = 'AmbientLight'
		// 添加平行光
		lights.addDirectionalLight([100, 100, -10], {
			color: 'rgb(253,253,253)',
			intensity: 3,
			castShadow: true // 是否投射阴影
		})
		const spotLights = new THREE.Group()
		spotLights.name = 'SpotLights'
		spotLights.add(initSpotLight(10, 32, -30))
		spotLights.add(initSpotLight(-2.5, 32, -30))
		spotLights.add(initSpotLight(-15, 32, -30))
		spotLights.add(initSpotLight(22.5, 32, -30))
		viewer.scene.add(spotLights)

		modelLoader = new ModelLoader(viewer)

		labelIns = new Labels(viewer)

		viewer.addAxis()

		// 添加状态检测
		viewer.addStats()
		// 初始化视频纹理
		initVideoTexture()
		// 初始化车辆
		loadCar()
		// 初始化停车场栅栏
		initFence()
		// 加载办公大厅
		loadOfficeBuild()
		// 加载实验楼
		loadLaboratoryBuild()
		// 加载广告牌
		loadBillBoard()
		// 加载人
		loadPeople()
		// 加载路灯
		loadLamp()
		// 加载树
		loadTree()
		// 加载水池
		loadSwimmingPool()
		// 办公楼鼠标移动效果
		officeMouseMove()
		// 办公楼点击
		officeFloorClick()
	}
	const onChangeTime = () => {
		const ambientLight = viewer.scene.getObjectByName('AmbientLight')
		const directionalLight = viewer.scene.getObjectByProperty(
			'type',
			'DirectionalLight'
		)
		const spotLights = viewer.scene.getObjectsByProperty('type', 'SpotLight')
		// console.log(viewer.scene)
		if (timeText.value === TimeNums.night) {
			skyBoxs.setSkybox('night')
			timeText.value = '白天模式'
			ambientLight.intensity = 0.3
			directionalLight.visible = false
			spotLights.forEach((spotLight) => {
				spotLight.visible = true
			})
		} else {
			skyBoxs.setSkybox('day')
			timeText.value = '夜间模式'
			ambientLight.intensity = 1.0
			directionalLight.visible = true
			spotLights.forEach((spotLight) => {
				spotLight.visible = false
			})
		}
	}
	/**
	 * 初始化视频纹理
	 */
	const initVideoTexture = () => {
		const video = document.getElementById('videoContainer')
		video.src = '/video/bi.mp4'
		video.autoplay = 'autoplay'
		video.loop = 'loop'
		video.muted = 'muted'
		videoTextTure = new THREE.VideoTexture(video)
	}

	/**
	 * 加载聚光灯
	 */
	const initSpotLight = (x, y, z) => {
		const spotLightGroup = new THREE.Group()
		const spotLight = new THREE.SpotLight()
		const spotLightHelper = new THREE.SpotLightHelper(spotLight)
		spotLightGroup.add(spotLight)
		spotLightGroup.add(spotLightHelper)
		spotLightGroup.add(spotLight.target)

		// spotLight.castShadow = true;
		spotLight.position.set(x, y, z)
		spotLight.target.position.set(x, y - 2, z - 1)
		spotLight.penumbra = 0.8

		spotLight.visible = false
		spotLightHelper.visible = false

		return spotLightGroup
	}

	/**
	 * 加载人
	 */
	const loadPeople = () => {
		modelLoader.loadModelToScene('/glb/ren.glb', (model) => {
			model.openCastShadow()
			model.object.position.set(13, 0, 15)
			model.object.name = '人'
			model.startAnimal(1)
			model.cloneModel([25, 0, 29]).startAnimal()
		})
	}
	/**
	 * 加载路灯
	 */
	const loadLamp = () => {
		modelLoader.loadModelToScene('/glb/lightpostDouble.glb', (model) => {
			model.openCastShadow()
			model.object.position.set(23, 0, 29)
			model.object.scale.set(1, 3, 1)
			model.object.name = '路灯'
			model.cloneModel([20, 0, 29])
			model.cloneModel([17, 0, 29])
			model.cloneModel([14, 0, 29])
			model.cloneModel([9, 0, 29])
			model.cloneModel([6, 0, 29])
		})
	}

	/**
	 * 初始化停车场栅栏
	 */
	const initFence = () => {
		modelLoader.loadModelToScene(
			'/glb/city-v1.glb',
			(model) => {
				model.object.name = 'cityv1'
				model.openCastShadow() // 开启投射阴影
				model.openReceiveShadow() // 开启接收阴影
				model.object.children.forEach((item) => {
					// 门口栅栏动画
					if (item.name === 'Mesh26') {
						Mesh26 = item
						gsap.to(item.scale, {
							x: item.scale.x / 8,
							duration: 5,
							ease: 'power1.inOut',
							onComplete: () => {
								makeCurve()
								isopen.value = true
							}
						})
					}
				})
				timeen = {
					fun: moveOnCurve,
					content: car
				}
				viewer.addAnimate(timeen)
				cityv1 = model.object.clone()
			},
			(pgs) => {
				pgs = Math.floor(pgs * 100)
				progressText.value = pgs + '%'
				if (pgs === 100) {
					progressBarShow.value = false
				}
			},
			(error) => {
				console.log('----加载city-v1.glb报错---', error)
			}
		)
	}
	/**
	 * 加载广告牌
	 */
	const loadBillBoard = () => {
		modelLoader.loadModelToScene('/glb/billboard.glb', (model) => {
			model.openCastShadow() // 开启投射阴影
			// model.openReceiveShadow()
			model.object.position.set(4, -20, -35)
			model.object.rotateY(-Math.PI / 2)
			model.object.scale.set(2.7, 2.7, 2.7)
			model.object.name = '广告牌'
			const object6 = model.object.getObjectByName('Object_6')
			object6.material = new THREE.MeshBasicMaterial({
				map: videoTextTure,
				side: THREE.DoubleSide,
				transparent: true
			})
			// const bbox = model.getBox()
		})
	}
	/**
	 * 加载办公大厅
	 */
	const loadOfficeBuild = () => {
		modelLoader.loadModelToScene('/glb/officeBuild.glb', (model) => {
			// console.log('----model----', model)
			officeBuild = model
			officeBuild.openCastShadow()
			officeBuild.openReceiveShadow()
			//旋转360度
			officeBuild.object.rotation.y = Math.PI
			officeBuild.object.position.set(16, 0, -5)
			officeBuild.object.scale.set(0.2, 0.2, 0.2)
			officeBuild.object.name = '办公大厅'
			officeBuild.object.children.forEach((item) => {
				item.name = item.name.replace('zuo', '')
				if (item.name === 'ding') {
					item.name = 6
				}
				item.name--
			})
			officeBuild.object.children
				.sort((a, b) => a.name - b.name)
				.forEach((v) => {
					v.name = 'zuo' + v.name
				})
			officeBuild.forEach((child) => {
				if (child.isMesh) {
					child.frustumCulled = false // 关闭投射阴影
					child.material.emissive = child.material.color // 设置材质颜色
					child.material.emissiveMap = child.material.map // 设置材质贴图
					child.material.emissiveIntensity = 1.2 // 设置材质强度
					child.material.envmap = viewer.scene.background // 设置环境贴图
				}
			})
			oldOfficeBuild = officeBuild.object.clone()
			const buildBox = officeBuild.getBox()
			officeLabel = labelIns.addCss2dLabel({
					x: buildBox.max.x / 2,
					y: buildBox.max.y,
					z: buildBox.max.z
				},
				`<span class="label">${model.object.name}</span>`
			)
			// 添加标签动画
			gsap.to(labelIns.label.position, {
				y: buildBox.max.y + 2,
				repeat: -1, // 循环播放
				yoyo: true, // 循环播放
				duration: 2, // 播放时间
				ease: 'Bounce.inOut'
			})
		})
	}
	/**
	 * 办公楼鼠标移动效果
	 */
	const officeMouseMove = () => {
		// TODO: 做一个节流
		viewer.startSelectEvent('mousemove', false, (model) => {
			if (curFloorName) {
				viewer.stopSelectEvent()
			}
			if (model.parent?.parent?.name === '办公大厅' && !isSplit) {
				officeFloorList.forEach((item) => {
					if (item === model.parent.name) {
						modelMoveName = item
						if (curFloorName === modelMoveName) {
							// 如果当前选中的楼层和鼠标移动选中的楼层相同,则不给当前选中的楼层改变材质,仍保持原来的材质
							return
						}
						officeBuild.object.getObjectByName(item).traverse((child) => {
							if (child.isMesh) {
								child.material = new THREE.MeshPhongMaterial({
									color: 'yellow',
									transparent: true,
									opacity: 0.8,
									emissive: child.material.color, // 设置材质颜色
									emissiveMap: child.material.map, // 设置材质贴图
									emissiveIntensity: 3 // 设置材质强度
								})
							}
						})
					} else {
						if (!isSplit) {
							const oldModel = oldOfficeBuild.getObjectByName(item)
							officeBuild.object.getObjectByName(item)?.traverse((child) => {
								if (child.isMesh) {
									// 将未选中的楼层赋予之前的材质
									child.material = oldModel.getObjectByName(child.name).material
								}
							})
						} else {
							// 未选中的楼层设置为蓝色
							// officeBuild.object.getObjectByName(item).traverse((child) => {
							//   if (child.isMesh && child.parent.name !== curFloorName) {
							//     child.material = new THREE.MeshPhongMaterial({
							//       color: new THREE.Color('#123ca8'),
							//       transparent: true,
							//       opacity: 0.5,
							//       emissiveMap: child.material.map,
							//     })
							//   }
							// })
						}
					}
				})
			}
		})
	}
	/**
	 * 办公楼点击
	 */
	const officeFloorClick = () => {
		viewer.renderer.domElement.addEventListener('click', (e) => {
			const rayCaster = new THREE.Raycaster()
			const mouse = new THREE.Vector2()
			// 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
			mouse.x = (e.offsetX / viewer.renderer.domElement.clientWidth) * 2 - 1
			mouse.y = -(e.offsetY / viewer.renderer.domElement.clientHeight) * 2 + 1
			// 通过摄像机和鼠标位置更新射线
			rayCaster.setFromCamera(mouse, viewer.camera)
			// TODO: 第一个参数是否需要指定模型
			const intersects = rayCaster.intersectObject(viewer.scene, true) // 计算物体和射线的焦点
			if (intersects.length > 0 && modelMoveName) {
				const model = intersects[0].object.parent
				if (model.name.includes('zuo')) {
					if (!isSplit) {
						// 隐藏车和标签
						// TODO: 找到更方便的模型指定方式,而不是每次都遍历查找
						carLabel.visible = false
						officeLabel.visible = false
						viewer.scene.children.find(
							(item) => item.name === '快递车'
						).visible = false
						viewer.scene.children.find((o) => o.name == 'cityv1').visible = false
						viewer.scene.children.find((o) => o.name == '树').visible = false
						// 实验楼材质变化
						sceneList.forEach((item) => {
							viewer.scene.children
								.find((o) => o.name == item)
								.traverse((child) => {
									child.material = new THREE.MeshPhongMaterial({
										color: new THREE.Color('rgba(7,32,96,0.76)'),
										transparent: true,
										opacity: 0.1,
										wireframe: true,
										depthWrite: true // 无法被选择,鼠标穿透
									})
								})
						})
						gsap.to(viewer.scene.children.find((o) => o.name === '人').rotation, {
							y: Math.PI, // 旋转角度
							duration: 2,
							ease: 'power1.inOut',
							onComplete: () => {
								isSplit = true
							}
						})
					}
					selectOffice(model)
				} else {
					if (!isSplit) {
						const oldModel = oldOfficeBuild.getObjectByName(modelMoveName)
						officeBuild.object
							.getObjectByName(modelMoveName)
							.traverse(function(child) {
								if (child.isMesh) {
									child.material = oldModel.getObjectByName(child.name).material
								}
							})
					}
				}
			}
		})
	}

	const selectOffice = (model) => {
		curFloorName = model.name
		const oldModel = oldOfficeBuild.getObjectByName(curFloorName)
		// 找到当前点击的楼层
		const modelSelectIndex = officeFloorList.findIndex(
			(item) => item === curFloorName
		)
		if (modelSelectIndex === lastIndex) return
		if (!isSplit) {
			// 楼体还未分层的时候要做的事
			officeBuild.object.children.forEach((child, index) => {
				if (child.name === curFloorName) {
					// 当前楼层附着原本材质
					child.children.forEach((ol) => {
						ol.material = oldModel.getObjectByName(ol.name).material
					})
				}
				// if (child.name === curFloorName) {
				//   // 当前楼层附着原本材质
				//   child.children.forEach(ol => {
				//     ol.material = oldModel.getObjectByName(ol.name).material
				//   })
				// } else {
				//   // 其余楼层附着蓝色透明材质
				//   child.children.forEach(Mesh => {
				//     Mesh.material = new THREE.MeshPhongMaterial({
				//       color: new THREE.Color('#123ca8'),
				//       transparent: true,
				//       opacity: 0.5,
				//       emissiveMap: Mesh.material.map,
				//     })
				//   })
				// }
				if (index > 0) {
					isSplit = true
					gsap.to(child.position, {
						y: child.position.y + index * 10,
						duration: 2,
						ease: 'power1.inOut'
					})
				}

				// if (!model.userData.position && index > modelSelectIndex) {
				//   gsap.to(child.position, {
				//     y: !child.userData.position ? child.position.y + 25 : child.position.y,
				//     duration: 2,
				//     ease: "power1.inOut",
				//     onComplete: () => {
				//       child.userData.position = true
				//     },
				//   })
				// }
				// if (model.userData.position && index <= modelSelectIndex) {
				//   if (child.userData.position) {
				//     gsap.to(child.position, {
				//       y: oldOfficeBuild.getObjectByName(child.name).position.y,
				//       // z: child.position.z - 40,
				//       duration: 2,
				//       ease: "power1.inOut",
				//       onComplete: () => {
				//         child.userData.position = false
				//       },
				//     });
				//   }
				// }
			})
		} else {
			// TODO:点击快了之后会出现错乱;
			// 楼体分层之后点击抽出楼层
			officeBuild.object.children.forEach((child, index) => {
				if (index === lastIndex) {
					// 将上一次抽出的楼层归位
					gsap.to(child.position, {
						z: child.position.z + 40,
						duration: 2,
						ease: 'power1.inOut'
					})
				}
				if (child.name === curFloorName) {
					gsap.to(child.position, {
						z: child.position.z - 40,
						duration: 2,
						ease: 'power1.inOut',
						onComplete: () => {
							lastIndex = index
						}
					})
				}
			})
		}

		gsap.to(viewer.controls.target, {
			x: 12,
			y: 0,
			z: -5,
			duration: 2,
			ease: 'power1.inOut',
			onComplete: () => {}
		})
		// gsap.to(viewer.camera.position, {
		//   x: 12,
		//   y: 18,
		//   z: 38,
		//   duration: 2,
		//   ease: "power1.inOut",
		//   onComplete: () => {
		//   },
		// });
	}
	/**
	 * 加载实验楼
	 */
	const loadLaboratoryBuild = () => {
		modelLoader.loadModelToScene('/glTF/laboratoryBuild.gltf', (model) => {
			// 合批
			const geometryArr = []
			const materialArr = []
			// 获取几何体/材质数组
			model.object.traverse((item) => {
				item.updateMatrixWorld(true)
				if (item.isMesh) {
					item.geometry.applyMatrix4(item.matrixWorld)
					geometryArr.push(item.geometry)
					materialArr.push(item.material)
				}
			})
			const geometryMerged = BufferGeometryUtils.mergeGeometries(
				geometryArr,
				true
			)

			const meshMerged = new THREE.Mesh(geometryMerged, materialArr)

			model.object.remove(model.object.children[0])
			model.object.add(meshMerged)

			meshMerged.castShadow = true
			meshMerged.receiveShadow = true
			model.object.rotateY(Math.PI / 2)
			model.object.position.set(-17, 0, 5)
			model.object.scale.set(0.7, 0.7, 0.7)
			model.object.name = '实验楼'

			laboratoryBuild = model.object.clone()
			const bbox = model.getBox()

			labelIns.addCss2dLabel({
					x: bbox.max.x,
					y: bbox.max.y,
					z: bbox.max.z
				},
				`<span class="label">${model.object.name}</span>`
			)

			// 添加标签动画
			gsap.to(labelIns.label.position, {
				y: bbox.max.y + 2,
				repeat: -1, // 循环播放
				yoyo: true, // 循环播放
				duration: 2, // 播放时间
				ease: 'Bounce.inOut'
			})
		})
	}
	/**
	 * 加载车辆
	 */
	const loadCar = () => {
		modelLoader.loadModelToScene('/glTF/car13.gltf', (model) => {
			car = model
			model.openCastShadow()
			model.openReceiveShadow()
			model.object.position.set(11.5, 0, 18)
			model.object.scale.set(1, 1, 1)
			model.object.name = '快递车'

			const spotLight = new THREE.SpotLight()

			model.object.add(spotLight)
			model.object.add(spotLight.target)

			spotLight.angle = Math.PI / 4
			spotLight.position.set(0, 2, 2)
			spotLight.target.position.set(0, 1, 3)
			spotLight.penumbra = 0.8

			spotLight.castShadow = true
			// spotLight.shadow.radius = 5 // PCFSS不支持radius
			spotLight.shadow.mapSize.width = 1024
			spotLight.shadow.mapSize.height = 1024
			spotLight.shadow.camera.near = 0.1
			spotLight.shadow.camera.far = 100
			spotLight.shadow.camera.bias = 0.005 // 去除摩尔纹、伪影

			spotLight.visible = false

			let boxx = model.getBox()
			// 加载车的标签
			carLabel = labelIns.addCss2dLabel({
					x: boxx.max.x,
					y: boxx.max.y + 2,
					z: boxx.max.z
				},
				`<span class="label">${model.object.name}</span>`
			)
		})
	}
	/**
	 * 加载树
	 */
	const loadTree = () => {
		modelLoader.loadModelToScene('glTF/tree_animate/new-scene.gltf', (model) => {
			model.openCastShadow()
			model.object.position.set(8, 0, 16)
			model.object.scale.set(0.08, 0.08, 0.08)
			model.object.name = '树'
			model.startAnimal()
		})
	}

	/**
	 * 加载水池
	 */
	const loadSwimmingPool = () => {
		modelLoader.loadModelToScene('/glb/pool.glb', (model) => {
			model.openCastShadow()
			model.openReceiveShadow()
			model.object.position.set(12, 1, -16)
			model.object.scale.set(0.6, 0.5, 0.6)
			model.object.name = '水池'

			const waterTexLoader = new THREE.TextureLoader()
			const oldWater = model.object.getObjectByName('voda_0')
			const waterMesh = new Water(oldWater.children[0].geometry, {
				textureWidth: 512,
				textureHeight: 512,
				color: 0xeeeeff,
				flowDirection: new THREE.Vector2(1, 1),
				scale: 1,
				normalMap0: waterTexLoader.load('/images/Water_1_M_Normal.jpg'),
				normalMap1: waterTexLoader.load('/images/Water_2_M_Normal.jpg')
			})
			waterMesh.name = '动态水'
			oldWater.remove(oldWater.children[0])
			oldWater.add(waterMesh)
		})
	}

	/**
	 * 物体沿线移动方法
	 */
	const moveOnCurve = (model) => {
		if (curve && car) {
			if (progress <= 1 - velocity) {
				let carObj = model.object
				let boxx = model.getBox()
				carLabel.position.set(boxx.max.x, boxx.max.y + 2, boxx.max.z)
				if (
					carObj.position.z.toFixed(2) >= 28.0 &&
					carObj.position.z.toFixed(2) <= 28.1
				) {
					if (isopen.value) {
						gsap.to(Mesh26.scale, {
							x: Mesh26.scale.x * 8,
							duration: 5,
							ease: 'power1.inOut',
							onComplete: () => {
								isopen.value = false
							}
						})
					} else {
						gsap.to(Mesh26.scale, {
							x: Mesh26.scale.x / 8,
							duration: 5,
							ease: 'power1.inOut',
							onComplete: () => {
								isopen.value = true
								viewer.addAnimate(timeen)
							},
							onStart: () => {
								viewer.removeAnimate(timeen)
							}
						})
					}
				}

				const point = curve.getPointAt(progress) // 获取样条曲线指定点坐标
				const pointBox = curve.getPointAt(progress + velocity) // 获取样条曲线指定点坐标

				if (point && pointBox) {
					carObj.position.set(point.x, point.y, point.z)
					//因为这个模型加载进来默认面部是正对Z轴负方向的,所以直接lookAt会导致出现倒着跑的现象,这里用重新设置朝向的方法来解决。
					carObj.lookAt(pointBox.x, pointBox.y, pointBox.z)
					if (isDriver.value) {
						viewer.camera.position.set(point.x, point.y + 2, point.z)
						viewer.camera.lookAt(pointBox.x, pointBox.y + 2, pointBox.z)
						// viewer.controls.position0.set(point.x, point.y + 2, point.z) //非必要,场景有控件时才加上
						//       viewer.controls.target.set(pointBox.x, pointBox.y + 2, pointBox.z) //非必要,场景有控件时才加上
					}

					const offsetAngle = 22 // 目标移动时的朝向偏移
					const mtx = new THREE.Matrix4() // 创建一个4维矩阵
					mtx.lookAt(carObj.position, pointBox, carObj.up) // 设置朝向
					mtx.multiply(
						new THREE.Matrix4().makeRotationFromEuler(
							new THREE.Euler(0, offsetAngle, 0)
						)
					)
					const toRot = new THREE.Quaternion().setFromRotationMatrix(mtx) //计算出需要进行旋转的四元数值
					carObj.quaternion.slerp(toRot, 0.2)
				}
				progress += velocity
			} else {
				progress = 0
			}
		}
	}
	const makeCurve = () => {
		// 从一系列的点创建一条平滑的三维样条曲线
		curve = new THREE.CatmullRomCurve3([
			new THREE.Vector3(11.5, 0, 18),
			new THREE.Vector3(11.5, 0, 34),
			new THREE.Vector3(35, 0, 34),
			new THREE.Vector3(35, 0, 31),
			new THREE.Vector3(11.5, 0, 31)
		])
		curve.curveType = 'catmullrom' // 曲线类型
		curve.closed = true // 是否封闭曲线
		curve.tension = 0 // 设置线的张力,0为无弧度折线

		// 为曲线添加材质在场景中显示出来,不显示也不会影响运动轨迹,相当于一个Helper
		const points = curve.getPoints(0.1) // 获取曲线上的点
		const geometry = new THREE.BufferGeometry().setFromPoints(points) // 创建几何体
		const material = new THREE.LineBasicMaterial({
			color: 0xff0000
		}) // 线材质
		const curveObject = new THREE.Line(geometry, material) // 线
		curveObject.position.y = -1
		viewer.scene.add(curveObject)
	}
	const onReset = () => {
		gsap.to(viewer.camera.position, {
			x: 17,
			y: 10,
			z: 52,
			duration: 2,
			ease: 'Bounce.inOut'
		})
		gsap.to(viewer.scene.children.find((o) => o.name == '人').rotation, {
			y: 0,
			duration: 2,
			ease: 'power1.inOut'
		})
		carLabel.visible = true
		officeLabel.visible = true
		viewer.scene.children.find((o) => o.name === '快递车').visible = true
		viewer.scene.children.find((o) => o.name === '树').visible = true
		viewer.scene.children.find((o) => o.name === 'cityv1').visible = true
		viewer.scene.children[
			viewer.scene.children.findIndex((o) => o.name == '实验楼')
		] = laboratoryBuild.clone()
		viewer.scene.children[
			viewer.scene.children.findIndex((o) => o.name == '办公大厅')
		] = officeBuild.object = oldOfficeBuild.clone()
		curFloorName = ''
		modelMoveName = null
		isSplit = false
		lastIndex = null
		selectedFloorName = ''
		officeMouseMove()
	}
</script>
<style lang="less" scoped>
	#container {
		height: 100vh;
		width: 100%;
		overflow: hidden;
		background-color: #f0f0f0;
		// position: relative;
	}

	#videoContainer {
		position: absolute;
		top: 0px;
		left: 0px;
		z-index: 100;
		visibility: hidden;
	}

	.jindu-con {
		width: 300px;
		height: 10px;
		border-radius: 50px;
		background-color: white;
		margin-top: 10px;
		overflow: hidden;
	}

	#jindu {
		height: inherit;
		background-color: #007bff;
		width: 0;
	}

	#jindu-text-con {
		width: 300px;
		position: absolute;
		left: 0;
		right: 0;
		margin: 0 auto;
		top: 15%;
		text-align: center;
		background-color: rgba(255, 255, 255, 0.5);
		padding: 10px;
	}

	:deep(.label) {
		padding: 20px;
		background: #123ca8;
		color: aliceblue;
		border-radius: 5px;
		cursor: pointer;
	}

	.operate-box {
		position: absolute;
		z-index: 100;
		bottom: 20px;
		width: 100%;
		display: flex;
		flex-direction: row;
		justify-content: center;
	}
</style>

猜你喜欢

转载自blog.csdn.net/n_229397218/article/details/143205520