<template> <div id="Tunnel" ref="threeDom"></div> </template> <script setup name="Tunnel"> import { ref, reactive, toRefs, onMounted } from "vue"; import * as THREE from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; //控制器 import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; // gltf加载器 import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader"; import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader"; import { Water } from "three/examples/jsm/objects/Water.js"; import { CSS3DRenderer, CSS3DObject, } from "three/examples/jsm/renderers/CSS3DRenderer.js"; import gsap from "gsap"; import { CSSRulePlugin } from "gsap/CSSRulePlugin"; const show = ref(""); const width = ref(null); const height = ref(null); const Camera = ref(null); const Renderer = ref(null); const Controls = ref(null); const granaryArr = ref([]); const LabelRenderer = ref(null); const animationFrameId = ref(null); let Twater = null; const labelData = ref([ { name: "风速风向仪", id: `YuanDian1`, position: { x: 0, y: 0, z: 2.5, }, icon: "/Three/icon/fsfx_icon.png", data: [ { name: "方向", value: "东北风", unit: "", }, { name: "风速", value: "1.3", unit: "m/s", }, ], }, { name: "静力水准仪", id: `YuanDian2`, position: { x: -46, y: 0, z: 2.5, }, icon: "/Three/icon/jlszy_icon.png", data: [ { name: "挠度变化值", value: "-0.01", unit: "mm", }, { name: "温度", value: "16.3", unit: "℃", }, ], }, { name: "静力水准仪", id: `YuanDian3`, position: { x: -46, y: 0, z: -12.6, }, icon: "/Three/icon/jlszy_icon.png", data: [ { name: "挠度变化值", value: "-0.01", unit: "mm", }, { name: "温度", value: "16.3", unit: "℃", }, ], }, { name: "应变计", id: `YuanDian4`, position: { x: 0, y: 7.5, z: 0, }, icon: "/Three/icon/ybj_icon.png", data: [ { name: "应变量", value: "17", unit: "με", }, { name: "温度", value: "16.3", unit: "℃", }, ], }, { name: "应变计", id: `YuanDian5`, position: { x: 0, y: 7.5, z: -10, }, icon: "/Three/icon/ybj_icon.png", data: [ { name: "应变量", value: "17", unit: "με", }, { name: "温度", value: "16.3", unit: "℃", }, ], }, { name: "加速度计", id: `YuanDian6`, position: { x: 0, y: 0, z: -5, }, icon: "/Three/icon/jsdj_icon.png", data: [ { name: "加速度", value: "1.42", unit: "mm/s2", }, { name: "风速", value: "1.3", unit: "m/s", }, ], }, // { // name: '风速风向仪', // id: `YuanDian1`, // position: { // x: 0, // y: 1, // z: 2.5, // }, // OnSrc: '/Three/icon/BJ_On.png', // OffSrc: '/Three/icon/BJ_Off.png', // icon: '/Three/icon/fsfx_icon.png', // status: false, // boxShow: true, // value1: '--', // value2: '--', // value3: '--', // deviceCode: '', // stCode: 'HP0104201130000469', // }, // { // name: "二号泵机", // id: `YuanDian2`, // position: { // x: 3, // y: 230, // z: -180, // }, // OnSrc: "/Three/icon/BJ_On.png", // OffSrc: "/Three/icon/BJ_Off.png", // status: false, // boxShow: true, // value1: "--", // value2: "--", // value3: "--", // deviceCode: "", // stCode: "HP0104201130000469", // }, // { // name: "三号泵机", // id: `YuanDian3`, // position: { // x: 195, // y: 230, // z: -180, // }, // OnSrc: "/Three/icon/BJ_On.png", // OffSrc: "/Three/icon/BJ_Off.png", // status: false, // boxShow: true, // value1: "--", // value2: "--", // value3: "--", // deviceCode: "", // stCode: "HP0104201130000469", // }, ]); const deviceCode = ref(null); const stCode = ref(null); const MiMa = ref(null); const dialogVisible = ref(false); const loading = ref(false); const status = ref(false); const timer = ref(null); const Scene = new THREE.Scene(); const clock = new THREE.Clock(); const threeDom = ref(null); const cameraPosition = { // x: 457.66301930145875, // y: 397.5473109836439, // z: 943.8703438418463, x: -2200.221584999469584, y: 200.590211176632594, z: 600.665579736149148, }; const cameraLookat = { x: 1, y: 1, z: -700, }; /** * 初始化模态对话框 * @param {string} name - 模型的名称 * @param {string} url - 模型的URL */ const initModal = (name, url) => { const Gltfloader = new GLTFLoader(); var dracoLoader = new DRACOLoader(); // dracoLoader.setDecoderPath("https://zhzz.hongshan.gov.cn:8865/file/hongshan/Three_Gltf/"); //设置解压库文件路径 dracoLoader.setDecoderPath("/draco/"); //设置解压库文件路径 Gltfloader.setDRACOLoader(dracoLoader); Gltfloader.load( url, (gltf) => { gltf.scene.name = name; gltf.scene.scale.set(1, 1, 1); // gltf.scene.position.x = -70; gltf.scene.position.y = 2; cameraReset(cameraPosition, cameraLookat); Scene.add(gltf.scene); Scene.traverse(function (child) { if (child.isMesh) { child.frustumCulled = false; child.material.side = THREE.DoubleSide; granaryArr.value.push(child); } }); }, function (xhr) { console.log(Math.floor((xhr.loaded / xhr.total) * 100)); } ); }; /** * 恢复相机位置和视角 * * 此函数通过GSAP动画库来调整相机的位置和视角,以实现 Smooth Transition * * @param {Object} position - 相机的新位置,包含x, y, z坐标 * @param {Object} lookAt - 相机的新视角目标,包含x, y, z坐标,目前未使用 * @param {number} time - 动画过渡时间,单位为秒,默认为3秒 */ const cameraReset = (position, lookAt, time = 3) => { gsap.to(Camera.value.position, { x: position.x, y: position.y, z: position.z, duration: time, ease: "power4.out", }); // gsap.to(Camera.value.lookAt, { // x: lookAt.x, // y: lookAt.y, // z: lookAt.z, // duration: time, // ease: "power4.out", // }); gsap.to(Controls.value.target, { x: lookAt.x, y: lookAt.y, z: lookAt.z, duration: time, ease: "power4.out", }); }; /** * 初始化渲染器 * 该函数负责创建和配置Three.js的WebGL渲染器,包括设置视口大小、抗锯齿、阴影、像素比、色调映射等 */ const initRenderer = () => { width.value = document.getElementById("Tunnel").clientWidth; height.value = document.getElementById("Tunnel").clientHeight; Renderer.value = new THREE.WebGLRenderer({ antialias: true, // alpha: true, //开启alpha }); Renderer.value.shadowMap.enabled = true; Renderer.value.setPixelRatio(window.devicePixelRatio); Renderer.value.setSize(width.value, height.value, true); // Renderer.value.toneMapping = THREE.ACESFilmicToneMapping; Renderer.value.toneMappingExposure = 1; // 曝光系数 threeDom.value.appendChild(Renderer.value.domElement); }; /** * 初始化标签渲染器 * 该函数用于创建和配置一个CSS3D渲染器,用于渲染标签对象 * 它设置了渲染器的大小,位置,并将其添加到三维场景的DOM元素中 */ const initLabelRenderer = () => { LabelRenderer.value = new CSS3DRenderer(); LabelRenderer.value.setSize(width.value, height.value); LabelRenderer.value.domElement.style.position = "absolute"; LabelRenderer.value.domElement.style.top = "0px"; threeDom.value.appendChild(LabelRenderer.value.domElement); // LabelRenderer.value.domElement.addEventListener("click", meshOnClick); }; /** * 初始化摄像机 * 创建一个透视摄像机,并设置其位置 */ const initCamera = () => { Camera.value = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 4000 ); Camera.value.position.set(500, 500, 500); }; /** * 初始化控制对象 * 设置相机控制属性,以实现特定的相机行为 */ const initControls = () => { Controls.value = new OrbitControls(Camera.value, LabelRenderer.value.domElement); //上下翻转的最大角度 Controls.value.maxPolarAngle = 1.5; // //上下翻转的最小角度 Controls.value.minPolarAngle = 0.2; //是否允许缩放 Controls.value.enableZoom = false; // 使动画循环使用时阻尼或自转 意思是否有惯性 Controls.value.enableDamping = false; // 动态阻尼系数 就是鼠标拖拽旋转灵敏度 Controls.value.dampingFactor = 0.04; // 是否可以旋转 Controls.value.enableRotate = true; // 是否可以缩放与速度 Controls.value.enableZoom = true; // 设置相机距离原点的最远距离 Controls.value.minDistance = 50; // 设置相机距离原点的最远距离 Controls.value.maxDistance = 3000; // 是否开启右键拖拽 Controls.value.enablePan = true; // AxesHelper:辅助观察的坐标系 // const axesHelper = new THREE.AxesHelper(3000); // Scene.add(axesHelper); }; /** * 初始化灯光 * 创建并添加环境光到场景中,以模拟全局照明效果 */ const initLight = () => { // 设置场景的背景颜色,即天空颜色 Scene.background = new THREE.Color(0x999999); // 设置背景,可以更换为你想要的颜色 // 设置天空的雾气颜色和雾气近距离 // Scene.fog = new THREE.Fog(0xffffff, 50, 200); // 距离10开始,到200结束 // Scene.fog = new THREE.Fog(0x1784f9, 50, 2000); // 距离10开始,到200结束 // 创建平行光源 // const directionalLight = new THREE.DirectionalLight(0xffffff, 0.6); // const directionalLight = new THREE.DirectionalLight(0x1784f9, 0.6); // directionalLight.position.set(-2500, 1, 1); // Scene.add(directionalLight); // 创建环境光 // const ambientLight = new THREE.AmbientLight('#ffffff', 0.8); // const ambientLight = new THREE.AmbientLight('#1784f9', 0.8); // Scene.add(ambientLight); // 创建平行光辅助线 // const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 0.5); // Scene.add(directionalLightHelper); // 创建点光源 // const PointLight = new THREE.PointLight(0x1784f9, 1, 300); // PointLight.position.set(100, -100, 10); // Scene.add(PointLight); }; /** * 初始化天空盒 * 使用PMREMGenerator生成环境贴图,并设置为场景的环境光和背景 * 通过RGBELoader加载HDR纹理,用于创建环境贴图 * 注意:此函数未使用TextureLoader加载背景图片,而是使用HDR纹理 */ const initSky = () => { var pmremGenerator = new THREE.PMREMGenerator(Renderer.value); // 使用hdr作为背景色 pmremGenerator.compileEquirectangularShader(); new RGBELoader().load("/Gltf/Skey_1K.hdr", function (texture) { const envMap = pmremGenerator.fromEquirectangular(texture).texture; envMap.isPmremTexture = true; Scene.environment = envMap; Scene.background = envMap; pmremGenerator.dispose(); }); // new THREE.TextureLoader().load("/Gltf/bg.png", function (texture) { // Scene.background = texture; // }); }; /** * 渲染函数,用于不断更新和渲染场景 */ const Render = () => { animationFrameId.value = requestAnimationFrame(Render); Controls.value.update(); // 轨道控制器的更新 Renderer.value.clear(); // 清除画布 updateLabelOrientation(Camera.value.position); Renderer.value.render(Scene, Camera.value); LabelRenderer.value.render(Scene, Camera.value); const delta = clock.getDelta(); console.log(Camera.value); }; // 创建气泡窗 function createLable() { labelData.value.forEach((item, index) => { let labelCSS3D = Scene.getObjectByName(`YuanDian${index + 1}`); if (labelCSS3D === undefined) { let lableDiv = document.getElementById(`YuanDian${index + 1}`); lableDiv.style.display = "block"; labelCSS3D = new CSS3DObject(lableDiv); labelCSS3D.position.set(item.position.x, item.position.y, item.position.z); labelCSS3D.scale.set(0.1, 0.1, 0.1); labelCSS3D.name = `YuanDian${index + 1}`; // labelCSS3D.rotation.y = - Math.PI / 6; // 30度转换为弧度 Scene.add(labelCSS3D); } else { labelCSS3D.visible = true; } }); } /** * 更新所有气泡框的方向,使其始终面向相机 */ function updateLabelOrientation(cameraPosition) { labelData.value.forEach((item, index) => { let labelCSS3D = Scene.getObjectByName(`YuanDian${index + 1}`); if (labelCSS3D) { const direction = new THREE.Vector3(); direction.subVectors(cameraPosition, labelCSS3D.position).normalize(); const angle = Math.atan2(direction.x, direction.z); labelCSS3D.rotation.y = angle; // 调整角度使其始终面向相机 } }); } onBeforeMount(() => { // initGltfFloor(); // initModal('ChangJing', '/Gltf/ChuWangCheng.gltf'); initModal("yjlSD", "/Gltf/yjlSD.gltf"); }); onMounted(() => { nextTick(() => { if (document.readyState === "complete") { // createLable(); // createWaterLevel(); } }); initRenderer(); initLabelRenderer(); initCamera(); initControls(); initLight(); initSky(); // createLable(); // LoadWater(); Render(); }); onBeforeUnmount(() => { // document.removeEventListener('click', meshOnClick); // window.removeEventListener("resize", onWindowResize, false); Scene.traverse((e) => { if (e.BufferGeometry) e.BufferGeometry.dispose(); if (e.material) { if (Array.isArray(e.material)) { e.material.forEach((m) => { m.dispose(); }); } else { e.material.dispose(); } } if (e.isMesh) { e.remove(); } }); Scene.remove(); Renderer.value.dispose(); Renderer.value.content = null; clearInterval(timer.value); // 清理渲染器和相机 Camera.value = null; cancelAnimationFrame(animationFrameId.value); }); </script> <style lang="scss" scoped> #Tunnel { width: 100%; height: 100%; } </style>