import * as THREE from 'three'; //导入整个 three.js核心库 import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; //导入控制器模块,轨道控制器 import { CSS3DRenderer, CSS3DSprite } from 'three/examples/jsm/renderers/CSS3DRenderer.js'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js'; // 用于模型边缘高亮 import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'; import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'; import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass.js'; import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js'; import * as TWEEN from '@tweenjs/tween.js'; import { getBottomMaterial } from './material.js'; import GltfModelManager from '@/views/largeScreen/three/GltfModelManager'; import ComponentHandle from '@/utils/ComponentHandle'; import usageReal from '@/views/largeScreen/dialog/usage-real.vue'; const dracoLoader = new DRACOLoader(); // 设置draco路径 dracoLoader.setDecoderPath('/draco/'); dracoLoader.setDecoderConfig({ type: 'js' }); // 定义一个 class类 class renderModel { constructor(selector) { this.container = document.querySelector(selector); // 相机 this.camera; // 场景 this.scene; //渲染器 this.renderer; // 控制器 this.controls; // 3d文字渲染器 this.css3DRenderer = null; // 3d文字控制器 this.css3dControls = null; // 模型 this.model; // 室内模型1 this.model1; // 室内模型2 this.model2; // 环境光 this.ambientLight; //模型平面 this.planeGeometry; this.outlineObjs = []; this.sharedComposer = null; this.sharedOutlinePass = null; this.sharedBloomPass = null; this.isComposerInitialized = false; this.raycaster = new THREE.Raycaster(); this.mouse = new THREE.Vector2(); this.autoRotate = true; this.labels = []; this.roomLabelGroup = null; } controlRotate(bool) { this.autoRotate = bool; if (!bool) { this.model.rotation.z = 0; } } // 初始化共享后处理系统 initSharedPostProcessing() { if (this.isComposerInitialized) return; // 创建共享的EffectComposer this.sharedComposer = new EffectComposer(this.renderer); this.sharedComposer.renderTarget1.texture.outputColorSpace = THREE.sRGBEncoding; this.sharedComposer.renderTarget2.texture.outputColorSpace = THREE.sRGBEncoding; this.sharedComposer.renderTarget1.texture.encoding = THREE.sRGBEncoding; this.sharedComposer.renderTarget2.texture.encoding = THREE.sRGBEncoding; // 添加基础渲染通道 const renderPass = new RenderPass(this.scene, this.camera); this.sharedComposer.addPass(renderPass); // 创建共享的OutlinePass this.sharedOutlinePass = new OutlinePass(new THREE.Vector2(window.innerWidth, window.innerHeight), this.scene, this.camera); // 设置轮廓效果参数 this.sharedOutlinePass.edgeStrength = 5.0; // 边框的亮度 this.sharedOutlinePass.edgeGlow = 0.3; // 光晕[0,1] this.sharedOutlinePass.edgeThickness = 1.0; // 边框宽度 // this.sharedOutlinePass.pulsePeriod = 3; // 呼吸闪烁的速度 // this.sharedOutlinePass.visibleEdgeColor.set(0x00ff00); // 呼吸显示的颜色 // this.sharedOutlinePass.hiddenEdgeColor.set(0x000000); // 呼吸消失的颜色 // this.sharedOutlinePass.clear = true; this.sharedComposer.addPass(this.sharedOutlinePass); // 创建共享的UnrealBloomPass this.sharedBloomPass = new UnrealBloomPass(); this.sharedBloomPass.strength = 0.1; this.sharedBloomPass.radius = 0; this.sharedBloomPass.threshold = 1; this.sharedComposer.addPass(this.sharedBloomPass); this.isComposerInitialized = true; // 监听窗口大小变化,更新后处理分辨率 window.addEventListener('resize', () => { if (this.sharedOutlinePass) { this.sharedOutlinePass.resolution.set(window.innerWidth, window.innerHeight); } }); } // 优化后的模型高亮方法 outlineObj(selectedObjects) { // 初始化共享后处理系统 this.initSharedPostProcessing(); // 更新选中对象 if (this.sharedOutlinePass) { this.sharedOutlinePass.selectedObjects = selectedObjects; // 如果之前有高亮对象,先清除 if (this.outlineObjs.length > 0) { this.outlineObjs = []; } // 添加当前共享的composer到数组 this.outlineObjs.push(this.sharedComposer); } } // 清理高亮效果 clearHighlight() { if (this.sharedOutlinePass) { this.sharedOutlinePass.selectedObjects = []; this.outlineObjs = []; } } // 初始化加载模型方法 init(loaded) { //初始化场景 this.initScene(); //初始化相机 this.initCamera(); //初始化渲染器 this.initRender(); // 创建灯光 this.createLight(); //初始化控制器,控制摄像头,控制器一定要在渲染器后 this.initControls(); this.setFBXModel(loaded); this.setRoomModel(); //监听场景大小改变,跳转渲染尺寸 window.addEventListener('resize', this.onWindowResizes.bind(this)); //场景渲染 this.sceneAnimation(); // 添加鼠标点击事件 window.addEventListener('click', this.onMouseClick.bind(this)); } initGltfModel() { //初始化场景 this.initScene(); //初始化相机 this.initCamera(); //初始化渲染器 this.initRender(); // 创建灯光 this.createLight(); //初始化控制器,控制摄像头,控制器一定要在渲染器后 this.initControls(); this.setGlbModel(); //监听场景大小改变,跳转渲染尺寸 window.addEventListener('resize', this.onWindowResizes.bind(this)); //场景渲染 this.sceneAnimation(); // 添加鼠标点击事件 window.addEventListener('click', this.onMouseClick.bind(this)); } onMouseClick(event) { // 计算鼠标在标准化设备坐标中的位置 (-1 to +1) this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1; this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1; // 更新射线 this.raycaster.setFromCamera(this.mouse, this.camera); // 计算射线与场景中物体的交点 const intersects = this.raycaster.intersectObjects(this.scene.children, true); if (intersects.length > 0) { const point = intersects[0].point; const object = intersects[0].object; console.log(`Clicked on object: ${object.name} at (${point.x}, ${point.y}, ${point.z})`); // 示例:点击时高亮对象 // this.outlineObj([object]); } } //创建场景 initScene() { this.scene = new THREE.Scene(); this.scene.background = new THREE.Color(0x000000); //底部平面 const geometry = new THREE.PlaneGeometry(1, 1); // 创建材质,可以设置贴图 const material = getBottomMaterial( '#ffffff', // 设置模型的颜色 require('@/assets/images/models/bg.png'), 1, 5 ); // 创建平面网格 const plane = new THREE.Mesh(geometry, material); plane.scale.set(1000, 1000, 1000); plane.position.y = 0; const plane2 = plane.clone(); plane2.material = getBottomMaterial( '#000000', // 设置模型的颜色 require('@/assets/images/models/bg.png'), 2, 5 ); this.scene.add(plane2); this.scene.add(plane); } // 创建相机 initCamera() { const { clientHeight, clientWidth } = this.container; this.camera = new THREE.PerspectiveCamera(18, clientWidth / clientHeight, 0.1, 10000); } // 创建渲染器 initRender() { this.renderer = new THREE.WebGLRenderer({ logarithmicDepthBuffer: true, antialias: true, // true/false表示是否开启反锯齿 alpha: true, // true/false 表示是否可以设置背景色透明 // precision: 'mediump', // highp/mediump/lowp 表示着色精度选择 // premultipliedAlpha: true // true/false 表示是否可以设置像素深度(用来度量图像的分辨率) // preserveDrawingBuffer: false, // true/false 表示是否保存绘图缓冲 // physicallyCorrectLights: true, // true/false 表示是否开启物理光照 }); //设置屏幕像素比 this.renderer.setPixelRatio(window.devicePixelRatio); //渲染的尺寸大小 const { clientHeight, clientWidth } = this.container; this.renderer.setSize(clientWidth, clientHeight); this.renderer.toneMapping = Number(THREE.LinearToneMapping); this.renderer.toneMappingExposure = Math.pow(2, 0.0); this.container.appendChild(this.renderer.domElement); // 创建一个CSS3渲染器CSS3DRenderer this.css3DRenderer = new CSS3DRenderer(); this.css3DRenderer.setSize(clientWidth, clientHeight); // HTML标签
外面父元素叠加到canvas画布上且重合 this.css3DRenderer.domElement.style.position = 'absolute'; this.css3DRenderer.domElement.style.top = '0px'; this.css3DRenderer.domElement.style.zIndex = '9999'; //设置.pointerEvents=none,解决HTML元素标签对threejs canvas画布鼠标事件的遮挡 this.css3DRenderer.domElement.style.pointerEvents = 'none'; this.css3DRenderer.domElement.style.className = 'css3DRenderer'; this.container.appendChild(this.css3DRenderer.domElement); } // 创建光源 createLight() { const light1 = new THREE.AmbientLight('#FFFFFF', 1); this.scene.add(light1); const light2 = new THREE.DirectionalLight('#FFFFFF', 0.8 * Math.PI); light2.position.set(0.5, 0, 0.866); // ~60º this.scene.add(light2); } initControls() { this.controls = new OrbitControls(this.camera, this.renderer.domElement); this.controls.maxPolarAngle = Math.PI / 1.1; this.controls.minPolarAngle = Math.PI / 2; this.controls.minAzimuthAngle = 0; this.controls.maxAzimuthAngle = 0; this.controls.maxDistance = 2000; //标签控制器 this.css3dControls = new OrbitControls(this.camera, this.css3DRenderer.domElement); this.css3dControls.enablePan = false; this.css3dControls.enableDamping = true; this.controls.mouseButtons = { LEFT: THREE.MOUSE.PAN, MIDDLE: THREE.MOUSE.DOLLY, RIGHT: THREE.MOUSE.ROTATE }; } animate() { this.renderer.render(this.scene, this.camera); this.controls.update(); this.css3DRenderer.render(this.scene, this.camera); TWEEN.update(); if (this.model && this.autoRotate) { this.model.rotation.z += 0.001; // 每帧绕y轴旋转0.01弧度 } } // 使用动画器不断更新场景 sceneAnimation() { this.renderer.setAnimationLoop(this.animate.bind(this)); } flyTo(target, scale = 1, duration = 1000) { const targetPosition = new THREE.Vector3().copy(target.position); const targetFocus = new THREE.Vector3().copy(target.targetContent); // 移动相机位置 new TWEEN.Tween(this.camera.position) .to(targetPosition, target.time || 500) .easing(TWEEN.Easing.Quadratic.InOut) .onUpdate(() => { this.controls.update(); // 更新控制器 }) .start(); // 移动控制器目标 new TWEEN.Tween(this.controls.target) .to(targetFocus, target.time || 500) .easing(TWEEN.Easing.Quadratic.InOut) .onUpdate(() => { this.controls.update(); // 更新控制器 }) .start(); new TWEEN.Tween(this.model.scale) .to( { x: scale, y: scale, z: scale }, duration ) .easing(TWEEN.Easing.Quadratic.InOut) .start(); } tag3D(name) { // 创建div元素(作为标签) let div = document.createElement('div'); div.innerHTML = name; div.classList.add('tag3d'); //div元素包装为CSS3模型对象CSS3DObject let label = new CSS3DSprite(div); div.style.pointerEvents = 'none'; //避免HTML标签遮挡三维场景的鼠标事件 //缩放CSS3DObject模型对象 label.scale.set(0.15, 0.15, 0.15); //根据相机渲染范围控制HTML 3D标签尺寸 label.rotateY(Math.PI / 2); //控制HTML标签CSS3对象姿态角度 return label; //返回CSS3模型标签 } createDialog(html) { //div元素包装为CSS3模型对象CSS3DSprite const element = document.createElement('div'); element.className = 'customDialog'; element.appendChild(html); const dialog = new CSS3DSprite(element); element.style.pointerEvents = 'none'; //避免HTML标签遮挡三维场景的鼠标事件 // 设置HTML元素标签在three.js世界坐标中位置 // label.position.set(x, y, z); //缩放CSS3DSprite模型对象 dialog.scale.set(0.15, 0.15, 0.15); //根据相机渲染范围控制HTML 3D标签尺寸 dialog.rotateY(Math.PI / 2); //控制HTML标签CSS3对象姿态角度 return dialog; //返回CSS3模型标签 } /** * 设置加载模型居中 * {Object} object 模型对象 */ setModelPosition(object) { object.updateMatrixWorld(); // 获得包围盒得min和max const box = new THREE.Box3().setFromObject(object); // 返回包围盒的中心点 const center = box.getCenter(new THREE.Vector3()); object.position.x += object.position.x - center.x; object.position.y += object.position.y - center.y; object.position.z = 0; } setGlbModel() { new GltfModelManager().loadModel('model-road-pv', '/models/model-road-pv.glb').then(object => { this.calcMeshCenter(object.scene); this.model = object.scene; const selectedObjects = []; object.scene.traverse(function (child) { if (child.isMesh) { child.material.emissive = child.material.color; child.material.emissiveMap = child.material.map; } // console.log(child.name) // if (["solar_cell097001", "solar_cell097002", "solar_cell097003", "solar_cell097004"].includes(child.name)) { // selectedObjects.push(child); // } }); // 设置相机位置 this.camera.position.set(-20, -652, 500); this.controls.target.set(-20, -20, 0); this.model.scale.set(2, 2, 2); // 设置相机坐标系 this.camera.lookAt(0, 0, 0); // 将模型添加到场景中去 this.scene.add(this.model); // 高亮收集的对象 // if (selectedObjects.length > 0) { // this.outlineObj(selectedObjects); // } }); } setFBXModel(loaded) { var fbxLoader = new GLTFLoader(); fbxLoader.load('/models/main.glb', object => { this.calcMeshCenter(object.scene); this.model = object.scene; object.scene.traverse(function (child) { if (child.isMesh) { child.material.emissive = child.material.color; child.material.emissiveMap = child.material.map; } }); // 设置相机位置 this.camera.position.set(-17.701772776272723, -728.8405392761424, 374.20604159261217); this.controls.target.set(-17.701772776272723, -307.6331500346563, 148.44436360369687); this.model.scale.set(0.8, 0.8, 0.8); // 设置相机坐标系 this.camera.lookAt(0, 0, 0); // 将模型添加到场景中去 this.scene.add(this.model); loaded&&loaded() }); } setModelDisplay(type, areaInfo) { if (type == 'model1' || type == 'model2') { this.camera.position.set(2.9287885309866817, -403.9781890137868, 376.33849737114133); this.controls.target.set(-22.9298060627659623, 15.311141772539067, 64.637911374941055); const targetFocus = new THREE.Vector3(-22.9298060627659623, 15.311141772539067, 24.637911374941055); new TWEEN.Tween(this.controls.target) .to(targetFocus, 500) .easing(TWEEN.Easing.Quadratic.InOut) .onUpdate(() => { this.controls.update(); // 更新控制器 }) .start(); this.addRoomDialog( type, ComponentHandle.createComponent({ component: usageReal, props: { areaCode: areaInfo.value } }), `${areaInfo.name}${type == 'model1' ? '负一楼水泵' : '室内主机'}`, type == 'model1' ? { x: -3, y: 8, z: 0 } : { x: -3, y: 3, z: 0 } ); } if (type == 'model1') { new TWEEN.Tween(this.model1.scale) .to( { x: 10, y: 10, z: 10 }, 500 ) .easing(TWEEN.Easing.Quadratic.InOut) .start(); } if (type == 'model2') { new TWEEN.Tween(this.model2.scale) .to( { x: 15, y: 15, z: 15 }, 500 ) .easing(TWEEN.Easing.Quadratic.InOut) .start(); } } modelChange(type, areaInfo) { if (this.roomLabelGroup) { this.model1.remove(this.roomLabelGroup); this.model2.remove(this.roomLabelGroup); this.roomLabelGroup.clear(); } this.setModelDisplay(type, areaInfo); if (type == 'model') { this.model1.visible = false; this.model2.visible = false; this.model.visible = true; this.labels.forEach(item => { item.visible = true; }); } else if (type == 'model1') { this.model.visible = false; this.labels.forEach(item => { item.visible = false; }); this.model2.visible = false; this.model1.visible = true; } else if (type == 'model2') { this.model.visible = false; this.labels.forEach(item => { item.visible = false; }); this.model1.visible = false; this.model2.visible = true; } } setRoomModel() { const loader = new GLTFLoader(); loader.setDRACOLoader(dracoLoader); loader.load('/models/model1.glb', object => { this.calcMeshCenter(object.scene); const perfectMaterial = new THREE.MeshStandardMaterial({ color: 0xcccccc, // side: THREE.DoubleSide // 防止面片反向不可见 }); this.model1 = object.scene; this.model1.scale.set(10, 10, 10); this.model1.rotation.x = Math.PI / 2; this.model1.rotation.y = Math.PI / 6; // 将模型添加到场景中去 this.model1.visible = false; this.scene.add(this.model1); }); loader.load('/models/model2.glb', object => { this.calcMeshCenter(object.scene); object.scene.traverse(child => { if (child.isMesh) { if (child.name == '平面' || child.name == '平面001') { child.material = new THREE.MeshStandardMaterial({ color: '#B0C4E9', side: THREE.DoubleSide }); } child.material.needsUpdate = true; } }); this.model2 = object.scene; this.model2.scale.set(15, 15, 15); this.model2.rotation.x = Math.PI / 2; this.model2.rotation.y = -Math.PI / 3; // 将模型添加到场景中去 this.model2.visible = false; this.scene.add(this.model2); }); } onWindowResizes() { if (!this.container) return false; const { clientHeight, clientWidth } = this.container; //调整屏幕大小 this.camera.aspect = clientWidth / clientHeight; // 摄像机宽高比例 this.camera.updateProjectionMatrix(); //相机更新矩阵,将3d内容投射到2d面上转换 this.renderer.setSize(clientWidth, clientHeight); this.css3DRenderer.setSize(clientWidth, clientHeight); } calcMeshCenter(group) { /** * 包围盒全自动计算:模型整体居中 */ var box3 = new THREE.Box3(); // 计算层级模型group的包围盒 // 模型group是加载一个三维模型返回的对象,包含多个网格模型 box3.expandByObject(group); // 计算一个层级模型对应包围盒的几何体中心在世界坐标中的位置 var center = new THREE.Vector3(); box3.getCenter(center); // 重新设置模型的位置,使之居中。 group.position.x = 0; group.position.y = 0; group.position.z = 17; } addRoomDialog(modelType, html, labelName, position) { const { x, y, z } = position; if (this.roomLabelGroup && this[modelType]) { this[modelType].remove(this.roomLabelGroup); this.roomLabelGroup.clear(); } const label3D = this.tag3D(labelName); const dialog3D = this.createDialog(html); if (modelType == 'model1') { label3D.scale.set(0.015, 0.015, 0.015); dialog3D.scale.set(0.015, 0.015, 0.15); label3D.position.set(x - 0.2, y - 1.8, z); } else { label3D.position.set(x - 0.2, y - 1.5, z); label3D.scale.set(0.011, 0.011, 0.011); dialog3D.scale.set(0.011, 0.011, 0.11); } // 3. 创建一个组来容纳它们 const group = new THREE.Group(); dialog3D.position.set(x, y, z); group.add(label3D); group.add(dialog3D); this[modelType].add(group); this.roomLabelGroup = group; // 保存引用 } addDialog(html, labelName, position) { const { x, y, z } = position; const label3D = this.tag3D(labelName); const dialog3D = this.createDialog(html); //设置标签名称 label3D.position.y += y; label3D.position.x += x; label3D.position.z += z; dialog3D.position.copy(label3D.position); dialog3D.position.z = z + 20; this.model && this.model.add(label3D); this.model && this.model.add(dialog3D); this.labels.push(label3D); this.labels.push(dialog3D); } } export default renderModel;