赞
踩
three.js+vue智慧城市blender建模web3d污水厂数字孪生源码
下面是三维场景主页面逻辑代码:
- <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置0
- timeS = 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) {
- // 巡检项目索引加1
- inspectIndex.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();
- // 巡检状态重置为true
- inspectState.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.3
- else {
- // 延迟一定秒数后移除气泡组对象
- 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为EquirectangularReflectionMapping
- texture.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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。