案例效果截图如下:
主场景三维逻辑代码如下:
<template><div class="whole"><!-- threejs画布 --><div id="threejs" ref="threejs"></div><!-- 污水厂模型加载进度条 --><a-progress:stroke-color="{from: '#00F5FF',to: '#4169E1',}":percent="0.0"trailColor="#E8E8E8"status="active"class="progress"/><!-- 标签组件 --><Label></Label><!-- 巡检数据展示面板--><div class="inspectPanel a-fadein" v-show="inspectPanelShow"><div class="panelTitle" id="panelTitle">曝气池</div><div class="panelData"><div class="left"><div class="leftTitle">介绍</div><div class="segment"></div><div class="describe" id="describe"></div></div><div class="right"><div class="rightTitle">数据记录</div><div class="segment"></div><div class="record"><div class="main" id="panelData"></div></div></div></div></div><!-- 巡检中 返回和状态按钮 --><div class="inspect" v-show="props['selectedMenu'] === 'inspect'"><div class="common" @click="endInspect"><div class="return_icon" style=""></div> 返回</div><div class="common" @click="inspectStateChange"><div :class="inspectState ? 'stop_icon' : 'continue_icon'"></div>{{ inspectState ? '暂停' : '继续' }}</div></div><!-- 巡检进度条 --><progressBar v-show="props['selectedMenu'] === 'inspect'" :schedule="schedule" :inspectState="inspectState" @progressBarChange="progressBarChange"></progressBar><!-- 巡检速度控制条 --><speedControlBar v-show="props['selectedMenu'] === 'inspect'" :speed="speed" :inspectState="inspectState" @controlBarChange="controlBarChange"></speedControlBar></div> </template><script setup> import { ref, onMounted, watch } from 'vue'; // 引入threejs import * as THREE from 'three'; // 基础配置文件——场景、灯光、相机等 import { scene, renderer, css2DRender, camera, controls } from './base/index.js'; // 添加污水厂模型函数 import { addSewageModel } from './addSewageModel/index.js'; // 添加人物模型函数 import { addPeopleModel, WalkAction } from './addPeopleModel/index.js'; // 引入tween.js,用来创建动画 import TWEEN from '@tweenjs/tween.js'; // 引入标签组件 import Label from './label/index.vue'; // 引入人物2D标签、CSS2D渲染器、标签初始化函数和建筑标签组对象 import { css2DPeopleLabel, initLabel, buildLabelGroup } from './label/index.js'; // 引入创建水面函数 import { createWaterPlane, waterPlaneGroup } from './waterPlane/index.js'; import { inspectPathArr, inspectIndex, inspectPathIndex, inspectState, inspectPanelShow, inspectLinePointGroup, openInspection, inspectionParams } from './inspection/index.js'; import progressBar from './progressBar/index.vue'; import speedControlBar from './speedControlBar/index.vue'; // 引入RGB加载器 import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js'; import { setPoolMaterial } from './poolMaterial/index'; import Stats from 'three/examples/jsm/libs/stats.module.js'; // 首页传值 const props = defineProps(['craftAnimationStatus', // 工艺动画状态,为true时开启播放相应的工艺动画'craftAnimationType', // 工艺动画类型,根据此值决定动画的类型'selectedMenu', // 首页底部菜单按钮选中项 ]); // 传递事件 const emit = defineEmits(['closeInspect', 'craftAnimationEnd']);// threejs容器 const threejs = ref(); // 污水厂模型 let sewageModel = null; // 人物模型 let people = null; // 人物动画播放器 let animationMixer = null; // 当前巡检进度百分比值 let schedule = ref(0); // 巡检的速度 let speed = ref(0);let stats; onMounted(async () => {//创建stats对象stats = new Stats();threejs.value.appendChild(stats.domElement);threejs.value.appendChild(renderer.domElement);threejs.value.appendChild(css2DRender.domElement);const rgbeLoader = new RGBELoader();// 环境贴图let envMap = await rgbeLoader.loadAsync('./envMap.hdr');createEnvironment(envMap);// 异步加载污水厂模型sewageModel = await addSewageModel(envMap);// 添加人物模型、人物动画播放器const { peopleGroup, mixer } = await addPeopleModel();people = peopleGroup;// 相机添加到人物模型中 people.add(camera);animationMixer = mixer;// 允许人物模型产生阴影people.castShadow = true;scene.add(sewageModel, people, inspectLinePointGroup);// 创建水面 createWaterPlane(sewageModel, envMap);// 设置水池材质 setPoolMaterial(sewageModel);// 开始循环渲染 render();// 播放首次进入动画 eventAnimation(); });watch(() => props['craftAnimationStatus'],(e) => {if (e) {// 重置水面透明度waterPlaneGroup.children.map((obj) => {obj.material.uniforms.alpha.value = 1.0;});craftAnimation(props['craftAnimationType']);}} ); watch(() => props['selectedMenu'],(e) => {// 巡检开启if (e === 'inspect') {// 相机角度重置camera.rotation.x = -0.9662198328141542;camera.rotation.y = 0.0004725006116027576;camera.rotation.z = 0.0006839146449353786;// 相机位置重置camera.position.set(0.103, 179.349, 123.908);// 相机观察点重置camera.lookAt(0, 1.7, 0);// 设置相机位置在人物模型后方camera.position.set(0, -5, -1);// camera.position.set(0, 1.4, -1);// 禁止相机控件旋转平移和缩放controls.enableRotate = false;controls.enablePan = false;controls.enableZoom = false;controls.target.set(0, 1.7, 0);controls.update();// 每次开启巡检时,将巡检项目索引和项目索引都重置,从第一个项目开始巡检inspectIndex.value = 0;inspectPathIndex.value = 0;// 人物步行动画开始播放 WalkAction.play();// 人物标签开启显示css2DPeopleLabel.visible = true;// 巡检标线开启显示inspectLinePointGroup.children[inspectIndex.value].visible = true;// 建筑物标签关闭显示buildLabelGroup.children.map((item) => {item.visible = false;});} else if (e !== 'craft') {// 相机角度重置camera.rotation.x = -0.9662198328141542;camera.rotation.y = 0.0004725006116027576;camera.rotation.z = 0.0006839146449353786;// 相机位置重置camera.position.set(0.103, 179.349, 123.908);// 相机观察点重置camera.lookAt(0, 0, 0);controls.target.set(0, 0, 0);controls.update();// 重置水面透明度水面颜色waterPlaneGroup.children.map((obj) => {obj.material.uniforms.alpha.value = 1.0;obj.material.uniforms.waterColor.value = obj.color;});}} );const clock = new THREE.Clock(); // 设置渲染帧率30FPS,默认情况下requestAnimationFrame在60帧左右,控制帧率优化性能 const FPS = 30; // 间隔多长时间渲染一次 const renderT = 1 / FPS; // 执行一次renderer.render,timeS重新置0 let timeS = 0; // 渲染循环 function render() {stats.update();// 循环渲染 renderer.render(scene, camera);// 获取两帧渲染间隔时间const T = clock.getDelta();timeS = timeS + T;animationMixer.update(T);if (timeS > renderT) {TWEEN.update();// renderer.render每执行一次,timeS置0timeS = 0;// css2D标签渲染 css2DRender.render(scene, camera);// 水面波纹动画渲染waterPlaneGroup.children.map((item) => {item.material.uniforms['time'].value += T / 6;});// 巡检时标线和拐点动画if (inspectLinePointGroup.children[inspectIndex.value] && props['selectedMenu'] === 'inspect') {inspectLinePointGroup.children[inspectIndex.value].children.map((item) => {if (item.name === '标线') {item.material.map.offset.x -= 0.03;} else if (item.name === '拐点') {item.rotation.y += 0.02;}});}// 巡检动画if (props['selectedMenu'] === 'inspect' && inspectState.value) {openInspection(people, controls);schedule.value = inspectPathIndex.value;// 巡检速度不断更新if (inspectPathArr[inspectIndex.value]) {speed.value = inspectPathArr[inspectIndex.value].speed;}console.log('巡检动画');}}requestAnimationFrame(render); } // 巡检状态变化事件 function inspectStateChange() {// 巡检的状态切换inspectState.value = !inspectState.value;// 关闭巡检数据面板的显示inspectPanelShow.value = false;if (inspectState.value) {// 人物动画开始播放 WalkAction.play();if (inspectPathIndex.value >= 100) {// 巡检项目索引加1inspectIndex.value += 1;// 巡检标记线组对象开启显示inspectLinePointGroup.children.map((item, index) => {if (index === inspectIndex.value) {item.visible = true;} else {item.visible = false;}});}} else {// 人物动画停止播放 WalkAction.stop();}if (inspectPathIndex.value >= 100) {// 巡检项目路径索引重新置零inspectPathIndex.value = 0;}// 巡检项目索引值超过巡检路径数组时,表示已经巡检完最后一项,调用endInspect()结束巡检if (inspectIndex.value > inspectPathArr.length - 1) {endInspect();} } // 结束巡检 function endInspect() {// 人物位置重置people.position.set(0, 0, 0);// 人物角度重置people.rotation.y = 0;people.rotation.x = 0;people.rotation.z = 0;// 相机位置重置camera.position.set(0.103, 179.349, 123.908);// 开启相机控件旋转平移和缩放controls.enableRotate = true;controls.enablePan = true;controls.enableZoom = true;// 相机控件观察点重置controls.target.set(0, 1.7, 0);// 相机控件更新 controls.update();// 巡检状态重置为trueinspectState.value = true;// 关闭巡检数据面板显示inspectPanelShow.value = false;// 人物标签隐藏显示css2DPeopleLabel.visible = false;// 巡检标记线组对象隐藏显示inspectLinePointGroup.children.map((item) => {item.visible = false;});// 建筑物标签开启显示buildLabelGroup.children.map((item) => {item.visible = true;});// 巡检速度重置inspectPathArr.map((item) => {item.speed = inspectionParams[item.name].speed;});// 关闭巡检emit('closeInspect'); } // 巡检进度条变化事件 function progressBarChange(e) {inspectPathIndex.value = e; } // 巡检速度条变化事件 function controlBarChange(speed) {inspectPathArr[inspectIndex.value].speed = 0.4 * (speed * 0.01); } // 工艺动画 function craftAnimation(type) {// 重置水面透明度水面颜色waterPlaneGroup.children.map((obj) => {obj.material.uniforms.alpha.value = 1.0;obj.material.uniforms.waterColor.value = obj.color;});// 禁止相机控件旋转平移和缩放// controls.enableRotate = false;// controls.enablePan = false;// controls.enableZoom = false;// 精确曝气动画if (type === 'aeration') {const name = '南北生物池水面';// 水面世界坐标位置const position = sewageModel.getObjectByName(name).getWorldPosition(new THREE.Vector3());// 开启动画,视角切换到水面处new TWEEN.Tween(camera.position).to({ x: -113.85, y: 7.67, z: 43.59 }, 1500).easing(TWEEN.Easing.Sinusoidal.InOut).onUpdate(() => {controls.target.copy(new THREE.Vector3(-113, 2, 30));controls.update();})// 动画执行完成后.onComplete(() => {// 获取水面模型const waterPlane = waterPlaneGroup.getObjectByName(name);// 加载气泡纹理const texture = new THREE.TextureLoader().load('./bubbles.png');// 球体(气泡)材质,map气泡贴图模仿气泡效果const material = new THREE.MeshPhysicalMaterial({map: texture,color: '#fff',transparent: true,opacity: 0.6,});// 球体(气泡)组对象const sphereGroup = new THREE.Group();// 创建box3包围盒计算水面模型尺寸const box3 = new THREE.Box3();box3.expandByObject(waterPlane);// 根据水面尺寸计算出球体(气泡)出现的范围const x = ((box3.max.x - box3.min.x) / 2).toFixed(3) - '';const z = ((box3.max.z - box3.min.z) / 2).toFixed(3) - '' - 0.1;// 循环创建多个球体(气泡)for (let i = 0; i <= 2000; i++) {// 指定随机大小创建球形几何体const sphere = new THREE.SphereGeometry(Math.random() * 0.03 + 0.05);const mesh = new THREE.Mesh(sphere, material);// 随机旋转一定角度mesh.rotateX(Math.random() * Math.PI);// 设置位置 mesh.position.copy(position);// y值置空mesh.position.y = 0;// 随机在增加一定值,使气泡在不同的位置出现mesh.position.x += Math.random() * (x - -x) + -x;mesh.position.y += Math.random() * 2;mesh.position.z += Math.random() * (z - -z) + -z;// 随机气泡上升的速度值mesh.speed = Math.random() * 0.04 + 0.04;sphereGroup.add(mesh);}scene.add(sphereGroup);// 此变量用作循环动画和销毁动画 let bubbleRiseAnimationId;// 气泡上升动画function bubbleRise() {bubbleRiseAnimationId = requestAnimationFrame(bubbleRise);sphereGroup.children.map((item) => {item.position.y += item.speed;if (item.position.y >= position.y) item.position.y = 0;});}bubbleRise();// 水面默认的透明度let alpha = waterPlane.material.uniforms.alpha.value;const color1 = new THREE.Color('#87CEFA');const color2 = waterPlane.material.uniforms.waterColor.value;// 此变量用作循环动画和销毁动画 let waterPlaneAnimationId;// 水面逐渐透明动画function waterPlaneTransparent() {waterPlaneAnimationId = requestAnimationFrame(waterPlaneTransparent);// 透明度大于0.3则不断降低透明度if (alpha >= 0.3) {alpha -= 0.01;waterPlane.material.uniforms.alpha.value = alpha;const newColor = color1.clone().lerp(color2.clone(), alpha);waterPlane.material.uniforms.waterColor.value = newColor;}// 透明度小于0.3else {// 延迟一定秒数后移除气泡组对象setTimeout(() => {// scene.remove(sphereGroup);}, 3000);// 传递事件告知动画执行完毕emit('craftAnimationEnd');// 销毁水面透明动画和气泡上升动画 cancelAnimationFrame(waterPlaneAnimationId);// cancelAnimationFrame(bubbleRiseAnimationId); }}waterPlaneTransparent();}).start();}// 精确加药if (type === 'dosing') {const name = '东加药管2-2';// 加药管位置const position = sewageModel.getObjectByName(name).getWorldPosition(new THREE.Vector3());// 将位置偏移一下到出水口position.y -= 0.138;position.z += 0.22;// 开启Tweenjs动画,将视角切换到加药管处new TWEEN.Tween(camera.position).to({ x: 57.16, y: 2.09, z: 6.53 }, 1500).easing(TWEEN.Easing.Sinusoidal.InOut).onUpdate(() => {controls.target.copy(position);controls.update();})// 视角切换完成后.onComplete(() => {// 创建一个位置数组,因为这个加药管有多个出水口,每个位置对应一个出水口const posArr = [];// 当前出水口位置先push到数组里去 posArr.push(position);// 获取左侧出水口位置for (let i = 1; i <= 4; i++) {const pos = position.clone();pos.x += i * 0.765;posArr.push(pos);}// 获取右侧出水口位置for (let i = 1; i <= 4; i++) {const pos = position.clone();pos.x -= i * 0.765;posArr.push(pos);}// 创建球形几何体,模仿水滴const sphereGeometry = new THREE.SphereGeometry(0.005, 16, 16);const sphereMaterial = new THREE.MeshPhongMaterial({color: '#afeeee',});// 创建球体数组,存储所有的球体const sphereArr = [];// 每个出水管球体数量const numSpheres = 300;// 遍历posArr位置数组,给每个出水管创建球体posArr.map((pos) => {for (let i = 0; i < numSpheres; i++) {const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);// 默认将其隐藏起来,因为随机高度球体会高于出水管sphere.visible = false;// 赋值出水管位置 sphere.position.copy(pos);// 球体高度在加上一个随机值sphere.position.y += Math.random() * 2; // 不同的初始高度// 设置球体下落速度sphere.velocity = Math.random() * 0.02 + 0.01; // 随机下落速度 sphereArr.push(sphere);scene.add(sphere);}});// 此变量用作循环动画和销毁动画 let animationFrameId1;function animation1() {animationFrameId1 = requestAnimationFrame(animation1);// 遍历球体数组sphereArr.forEach((sphere) => {if (sphere.position.y <= position.y) {sphere.visible = true;}sphere.position.y -= sphere.velocity;if (sphere.position.y <= 0.3) {// 当球体下落到一定位置时sphere.position.y = position.y; // 重新置于顶端 }});}animation1();const waterPlane = waterPlaneGroup.getObjectByName('东西生物池-东水面1');let alpha = waterPlane.material.uniforms.alpha.value;const color1 = new THREE.Color('#87CEFA');const color2 = waterPlane.material.uniforms.waterColor.value;// 此变量用作循环动画和销毁动画 let animationFrameId2;function animation2() {animationFrameId2 = requestAnimationFrame(animation2);if (alpha >= 0.5) {alpha -= 0.006;waterPlane.material.uniforms.alpha.value = alpha;const newColor = color1.clone().lerp(color2.clone(), alpha);waterPlane.material.uniforms.waterColor.value = newColor;} else {emit('craftAnimationEnd');cancelAnimationFrame(animationFrameId2);}}animation2();}).start();}// 污泥回流if (type === 'sludge') {// 二沉池模型名称数组const sinkPoolNameArr = ['二沉池3水面','二沉池3水面001','二沉池4水面','二沉池4水面001',// "初沉池水面1",// "初沉池水面1001", ];// 生物池模型名称数组const organismPoolNameArr = ['南北生物池水面', '东西生物池-东水面1', '东西生物池-东水面2', '东西生物池-西水面1', '东西生物池-西水面2'];// 获取二沉池模型const sinkPoolArr = [];sinkPoolNameArr.map((name) => {sinkPoolArr.push(waterPlaneGroup.getObjectByName(name));});// 获取生物池模型const organismPoolArr = [];organismPoolNameArr.map((name) => {const organismPool = waterPlaneGroup.getObjectByName(name);organismPool.material.uniforms.alpha.value = 0.3;organismPool.visible = false;organismPool.userData.y = organismPool.clone().position.y;organismPool.position.y = 0;organismPoolArr.push(organismPool);});// x: -10.84, y: 289.89, z: 276.17// 开启动画,视角切换到整个污水厂new TWEEN.Tween(camera.position).to({ x: 100, y: 100, z: 180 }, 1500).easing(TWEEN.Easing.Sinusoidal.InOut).onUpdate(() => {controls.target.set(100, 0, -30);controls.update();}).onComplete(() => {// 此变量用作循环动画和销毁动画 let waterPlaneAnimationId;// 水面逐渐透明动画function waterPlaneTransparent() {waterPlaneAnimationId = requestAnimationFrame(waterPlaneTransparent);sinkPoolArr.map((item) => {let alpha = item.material.uniforms.alpha.value;// 透明度大于0.3则不断降低透明度if (alpha >= 0.3) {alpha -= 0.01;item.material.uniforms.alpha.value = alpha;}});if (sinkPoolArr[sinkPoolArr.length - 1].material.uniforms.alpha.value < 0.3) {// 传递事件告知动画执行完毕// emit("craftAnimationEnd"); cancelAnimationFrame(waterPlaneAnimationId);waterLevelRise();}}waterPlaneTransparent();// 此变量用作循环动画和销毁动画 let waterLevelRiseAnimationId;function waterLevelRise() {waterLevelRiseAnimationId = requestAnimationFrame(waterLevelRise);organismPoolArr.map((item) => {item.visible = true;const yPos = item.userData.y;let alpha = item.material.uniforms.alpha.value;if (item.position.y < yPos) {item.position.y += 0.01;}if (alpha < 1) {alpha += 0.02;item.material.uniforms.alpha.value = alpha;}if (item.position.y >= yPos && alpha >= 1) {// 传递事件告知动画执行完毕emit('craftAnimationEnd');cancelAnimationFrame(waterLevelRiseAnimationId);}});}}).start();} } // 首次进入动画 function eventAnimation() {new TWEEN.Tween(camera.position).to({ x: 0.103, y: 179.349, z: 123.908 }, 2000).easing(TWEEN.Easing.Sinusoidal.InOut).onUpdate(() => {controls.target.set(0, 0, 0);controls.update();}).onComplete(() => {// 初始化标签 initLabel(sewageModel);// 将人物标签添加到人物模型中people.children[0].add(css2DPeopleLabel);// 设置位置在人物模型头顶css2DPeopleLabel.position.set(0, 2.2, 0);// 设置合适大小css2DPeopleLabel.scale.set(0.1, 0.1, 0.1);// 人物标签默认隐藏显示css2DPeopleLabel.visible = false;}).start(); } function createEnvironment(texture) {// scene.environment = texture;// hdr作为环境贴图生效,设置.mapping为EquirectangularReflectionMappingtexture.mapping = THREE.EquirectangularReflectionMapping;// 创建一个巨大球体作为整个天空环境const sphere = new THREE.SphereGeometry(1000, 512, 512);const material = new THREE.MeshBasicMaterial({map: texture,side: THREE.DoubleSide,});const mesh = new THREE.Mesh(sphere, material);mesh.position.y -= 100;scene.add(mesh); } </script> <style lang='less'> @import './index.less'; </style>
需要全部完整案例源码,访问百度网盘:
链接:https://pan.baidu.com/s/1Ib5nq3MueA-6-OfnVvmqJg
提取码:pgml