import * as THREE from 'three'; import {GLTFLoader} from "three/addons/loaders/GLTFLoader.js"; import List from '../../config/routeList.json' import texture1 from "../../assets/arrow2.png"; export const FlyGLTFClass = { constructor(options){ this.webgl = options.webgl; this.view = options.view; this.mixer = options.mixer; this.airCraftMixer = options.airCraftMixer; this.clock = options.clock; this.airCraftClock = options.airCraftClock; this.path = options.path; //路径点 this.pathProgress = options.pathProgress; //路径速度 this.pathCurve = options.pathCurve; //路径曲线 this.targetObject = options.targetObject; //路径飞行的目标对象 this._camera = options._camera; this.raycaster = options.raycaster; // 射线投射器 this.mouse = options.mouse; // 鼠标位置 this.callBackFunction = options.callBackFunction; // 回调函数 this.mouseClickEvent = null; this.uavid = options.uavid; }, initialize(){ this.mixer = new THREE.AnimationMixer(); this.airCraftMixer = new THREE.AnimationMixer(); this.clock = new THREE.Clock(); this.airCraftClock = new THREE.Clock(); this.renderer = new THREE.WebGLRenderer({ context:this.gl, premultipliedAlpha: false }); this.renderer.setPixelRatio(window.devicePixelRatio); this.renderer.setViewport(0,0,this.view.width,this.view.height); this.renderer.autoClear = false; this.renderer.autoClearDepth = false; this.renderer.autoClearColor = false; let originalSetRenderTarget = this.renderer.setRenderTarget.bind(this.renderer); this.renderer.setRenderTarget = function(target){ originalSetRenderTarget(target); if(target){ this.bindRenderTarget() } } this.scene = new THREE.Scene(); let cam = this.camera; this._camera = new THREE.PerspectiveCamera(cam.fovY,cam.aspect,cam.near,cam.far); //添加坐标轴辅助工具 const axesHelper = new THREE.AxesHelper(1); axesHelper.position.set(1000000,1000000,1000000); this.scene.add(axesHelper); const grid = new THREE.GridHelper(30,10,0xf0f0f0,0xffffff); this.scene.add(grid); this.ambient = new THREE.AmbientLight(0xffffff,5); this.scene.add(this.ambient); this.loadGLTF(); //初始化路径曲线 //this.initPathCurve(); this.resetWebGLState(); this.mouseClickEvent = this.view.on("click", this.onMouseClick.bind(this)); }, loadGLTF(){ const loader = new GLTFLoader(); let that = this; loader.load('public/gltf/mtwrj.glb', function (gltf) { gltf.scene.scale.set(6, 6, 6); that.scene.add(gltf.scene); that.targetObject = gltf.scene; // 添加动画 that.mixer = new THREE.AnimationMixer(gltf.scene); const AnimationAction = that.mixer.clipAction(gltf.animations[0]); AnimationAction.play(); // 创建信息面板 // const panelGeometry = new THREE.PlaneGeometry(3.5, 3.5); // 面板大小 // const canvas = document.createElement('canvas'); // const context = canvas.getContext('2d'); // // 加载背景图片 // const backgroundImage = new Image(); // backgroundImage.src = 'public/imgs/serviceBackground.png'; // 替换为你的图片路径 // backgroundImage.onload = () => { // // 确保背景透明 // context.clearRect(0, 0, canvas.width, canvas.height); // 清除所有内容,包含黑色 // context.fillStyle = 'rgba(0, 0, 0, 0)'; // 设置透明背景 // context.fillRect(0, 0, canvas.width, canvas.height); // 填充透明背景 // // context.save(); // context.scale(-1, 1); // context.translate(-canvas.width, 0); // // 绘制背景图片,填满整个 canvas // context.drawImage(backgroundImage, 0, 0, canvas.width, canvas.height); // // // // 绘制文字信息 // // context.fillStyle = '#fff'; // // context.font = 'normal 15px Arial'; // // context.fillText('无人机信息:',40, 25); // // context.fillText('无人机名称: 无人机1号', 40, 50); // // context.fillText('所属企业: 美团', 40, 70); // // context.fillText('速度: 100km/h', 40, 90); // // context.fillText('高度: 200m', 40, 110); // // context.fillText('状态: 正常',40, 130); // // // const textInfo = [ // { text: '无人机信息: ', color: '#fff', font: '15px Arial', offsetX:40, offsetY: 25 }, // { text: '无人机名称:'+this.panelInfo.data.name, color: '#fff', font: '15px Arial', offsetX:40, offsetY: 50 }, // { text: '所属企业: 美团: ', color: '#fff', font: '15px Arial', offsetX:40, offsetY: 70 }, // { text: '速度: '+Math.floor(this.panelInfo.data.speed * 3.6)+'km/h',color: '#fff', font: '15px Arial', offsetX:40, offsetY: 90 }, // { text: '高度: '+ this.panelInfo.data.altitude+'m', color: '#fff', font: '15px Arial', offsetX:40, offsetY: 110 }, // { text: '状态: 正常',color: '#fff',font: '15px Arial', offsetX:40, offsetY: 130 } // ]; // // 绘制文字 // textInfo.forEach((line) => { // context.fillStyle = line.color; // context.font = line.font; // context.fillText(line.text, line.offsetX, line.offsetY); // }); // // // // 标记纹理需要更新 // texture.needsUpdate = true; // }; // // const texture = new THREE.CanvasTexture(canvas); // const panelMaterial = new THREE.MeshBasicMaterial({ map: texture, side: THREE.DoubleSide, transparent: true }); // panelGeometry.scale(-1,1,1); // const panelMesh = new THREE.Mesh(panelGeometry, panelMaterial); // // 将面板附加到无人机模型 // panelMesh.position.set(0, 2.5, 0); // 调整面板位置到无人机上方 // gltf.scene.add(panelMesh); // // // 保存信息面板,以便更新 // that.infoPanel = { canvas, context, texture, mesh: panelMesh }; }); }, // 点击事件处理函数 onMouseClick(event) { // 获取点击位置的屏幕坐标 let mouse = [] this.webgl.toRenderCoordinates( this.view, [event.mapPoint.x, event.mapPoint.y, event.mapPoint.z], 0, this.view.spatialReference, mouse, 0, 1 ); const mousePoint = new THREE.Vector3(mouse[0], mouse[1], mouse[2]); // 获取摄像机位置 const cameraPosition = new THREE.Vector3().setFromMatrixPosition(this._camera.matrixWorld); // 计算射线方向 const rayDirection = new THREE.Vector3().subVectors(mousePoint, cameraPosition).normalize(); // 设置射线起点和方向 this.raycaster.set(cameraPosition, rayDirection); //this.visualizeRay() // 检测射线与网格的相交 const intersects = this.raycaster.intersectObject(this.targetObject); if (intersects.length > 0) { this.callBackFunction(this.uavid); } else { console.log("未选中任何无人机"); } }, // 设置面板类似 setPanelInfo(uavInfo) { this.panelInfo = uavInfo; }, // 更新面板信息 updateInfoPanel() { if (this.infoPanel) { const { canvas, context, texture ,mesh } = this.infoPanel; // 加载背景图片 const backgroundImage = new Image(); const topImage = new Image(); const bottomImage = new Image(); let typeInfo = ""; switch (this.panelInfo.type){ case "conflict": backgroundImage.src = 'public/imgs/基本信息框-红.png'; // 替换为你的图片路径 topImage.src = 'public/imgs/warningBackground_time.png'; // 替换为你的图片路径 bottomImage.src = 'public/imgs/warningBackground_conflict.png'; // 替换为你的图片路径 typeInfo = "异常" break; case "electricityWarning": backgroundImage.src = 'public/imgs/基本信息框-红.png'; // 替换为你的图片路径 typeInfo = "电池电量低" break; case "routeDeviate": backgroundImage.src = 'public/imgs/基本信息框-红.png'; // 替换为你的图片路径 typeInfo = "异常" break; case "noFlyZone": backgroundImage.src = 'public/imgs/基本信息框-红.png'; // 替换为你的图片路径 typeInfo = "异常" break; case "collision": backgroundImage.src = 'public/imgs/基本信息框-红.png'; // 替换为你的图片路径 typeInfo = "建筑物过近" break; case "clearZone": backgroundImage.src = 'public/imgs/基本信息框-红.png'; // 替换为你的图片路径 typeInfo = "净空区" break; default: backgroundImage.src = 'public/imgs/基本信息框.png'; // 替换为你的图片路径 typeInfo = "正常" break; } // backgroundImage.src = 'public/imgs/serviceBackground.png'; // 替换为你的图片路径 if(this.panelInfo.type == "conflict" && this.panelInfo.action == "wait"){ topImage.onload = () => { bottomImage.onload = () => { // 确保背景透明 context.clearRect(0, 0, canvas.width, canvas.height); // 清除所有内容,包含黑色 context.fillStyle = 'rgba(0, 0, 0, 0)'; // 设置透明背景 context.fillRect(0, 0, canvas.width, canvas.height); // 填充透明背景 // 绘制顶部图片 const topImageHeight = canvas.height * 0.2; // 设定顶部图片高度为 canvas 高度的 20% context.drawImage(topImage, 0, 0, canvas.width, topImageHeight); // 绘制底部图片 const bottomImageHeight = canvas.height * 0.7; // 设定底部图片高度为 canvas 高度的 20% context.drawImage(bottomImage, 0, canvas.height - bottomImageHeight, canvas.width, bottomImageHeight); const textInfo = [ { text: '暂停飞行原地等待:' + this.panelInfo.data.waitTime +'秒', color: '#fff', font: 'bold 16px Arial', offsetX:50, offsetY: 20 }, { text: '距预测撞点前方: ', color: '#fff', font: '20px Verdana', offsetX:70, offsetY: 90 }, { text: Math.floor(this.panelInfo.data.danger_path_distance * 1) + '米', color: '#ff0000', font: 'bold 22px Courier New', offsetX:120, offsetY: 120 } ]; // 绘制文字 textInfo.forEach((line) => { context.fillStyle = line.color; context.font = line.font; context.fillText(line.text, line.offsetX, line.offsetY); }); // 标记纹理需要更新 texture.needsUpdate = true; } } }else if(this?.panelInfo?.type == "routeDeviate"){ //获取当前下标index let index = this.panelInfo.index * 1 +Math.floor(this.panelInfo.data.distance * 1 / 10) * 1 + 1; let targetPoint = []; this.webgl.toRenderCoordinates( this.view, [List.originList[index].mapX, List.originList[index].mapY, List.originList[index].mapZ], 0, this.view.spatialReference, targetPoint, 0, 1 ); this.targetPosition = new THREE.Vector3(targetPoint[0], targetPoint[1], targetPoint[2]); backgroundImage.onload = () => { // 确保背景透明 context.clearRect(0, 0, canvas.width, canvas.height); // 清除所有内容,包含黑色 context.fillStyle = 'rgba(0, 0, 0, 0)'; // 设置透明背景 context.fillRect(0, 0, canvas.width, canvas.height); // 填充透明背景 // 绘制背景图片,填满整个 canvas context.drawImage(backgroundImage, 0, 0, canvas.width, canvas.height); const textInfo = [ { text: '计划偏离: ', color: '#fff', font: '15px Arial', offsetX:40, offsetY: 25 }, { text: '偏离距离: '+Math.floor(this.panelInfo.data.distance* 1) +'米', color: '#ff0000', font: 'bold 22px Courier New', offsetX:70, offsetY: 60 }, { text: '无人机正在偏离计划航线飞行', color: '#fff', font: '18px Arial', offsetX:40, offsetY: 90 }, { text: '请及时调整飞行路线', color: '#fff', font: '18px Arial', offsetX:40, offsetY: 120 } ]; // 绘制文字 textInfo.forEach((line) => { context.fillStyle = line.color; context.font = line.font; context.fillText(line.text, line.offsetX, line.offsetY); }); // 标记纹理需要更新 texture.needsUpdate = true; }; }else{ backgroundImage.onload = () => { // 确保背景透明 context.clearRect(0, 0, canvas.width, canvas.height); // 清除所有内容,包含黑色 context.fillStyle = 'rgba(0, 0, 0, 0)'; // 设置透明背景 context.fillRect(0, 0, canvas.width, canvas.height); // 填充透明背景 // 绘制背景图片,填满整个 canvas context.drawImage(backgroundImage, 0, 0, canvas.width, canvas.height); const textInfo = [ { text: '无人机信息: ', color: '#fff', font: '15px Arial', offsetX:40, offsetY: 25 }, { text: '无人机名称:'+this.panelInfo.data.name, color: '#fff', font: '15px Arial', offsetX:40, offsetY: 50 }, { text: '所属企业: 美团: ', color: '#fff', font: '15px Arial', offsetX:40, offsetY: 70 }, { text: '速度: '+Math.floor(this.panelInfo.data.speed * 3.6)+'km/h', color: '#fff', font: '15px Arial', offsetX:40, offsetY: 90 }, { text: '高度: '+ this.panelInfo.data.altitude+'m', color: '#fff', font: '15px Arial', offsetX:40, offsetY: 110 }, { text: `状态: ${typeInfo}`, color: '#fff', font: '15px Arial', offsetX:40, offsetY: 130 } ]; // 绘制文字 textInfo.forEach((line) => { context.fillStyle = line.color; context.font = line.font; context.fillText(line.text, line.offsetX, line.offsetY); }); // 标记纹理需要更新 texture.needsUpdate = true; }; } mesh.up.set(1, 0, 0); // 保持面板的 "上" 方向为 Y 轴正方向 mesh.lookAt(this._camera.position); } }, getClosestPointOnCurve(position, curve) { let closestPoint = null; let minDistance = Infinity; const points = curve.getPoints(100); points.forEach(point => { const distance = position.distanceTo(point); if (distance < minDistance) { minDistance = distance; closestPoint = point; } }); return closestPoint; }, setPathCurve(path,duration){ this.path = path; this.pathProgress = 0; // 重置路径进度 this.startTime = Date.now(); // 路径开始时间 this.duration = duration || 2500; if(this.path.length > 1){ //用于存储路径的点 const points = this.path.map(point => { const renderPoint = [0,0,0]; this.webgl.toRenderCoordinates( this.view, point, 0, this.view.spatialReference, renderPoint, 0, 1 ); return new THREE.Vector3(renderPoint[0],renderPoint[1],renderPoint[2]); }); //创建一个CurvePath来容纳多条直线路径 this.pathCurve = new THREE.CurvePath(); //将路径点的相邻俩点组成一条直线添加到pathCurve中 for(let i=0;i= 1) { this.pathProgress = 1; // 确保进度不超过 1 } try{ const position = this.pathCurve.getPointAt(this.pathProgress); const tangent = this.pathCurve.getTangentAt(this.pathProgress); this.targetObject.position.copy(position); this.targetObject.quaternion.setFromUnitVectors( new THREE.Vector3(0, 0, 1), tangent ); }catch(e){ console.log("this.pathProgress",this.pathProgress) } } //this.targetObject.rotation.x = -Math.PI/2; this.targetObject.rotation.y = 0; this.targetObject.rotation.z = -Math.PI/2; if (this.recommandPathLineGroup) { this.map.offset.y -= 0.1; } }, updateRecommandPath() { // 如果已存在曲线或管道,将其从场景中移除 if (this.recommandPathLineGroup) { this.scene.remove(this.recommandPathLineGroup); } let startPoint = this.pathCurve.getPointAt(this.pathProgress); let endPoint = this.targetPosition; // 创建两个点之间的中间控制点 const midPoint1 = new THREE.Vector3( (2 * startPoint.x + endPoint.x) / 3, (2 * startPoint.y + endPoint.y) / 3 + 5, // 可调整中间点的高度 (2 * startPoint.z + endPoint.z) / 3 ); const midPoint2 = new THREE.Vector3( (startPoint.x + 2 * endPoint.x) / 3, (startPoint.y + 2 * endPoint.y) / 3 + 5, // 可调整中间点的高度 (startPoint.z + 2 * endPoint.z) / 3 ); // 使用 CatmullRomCurve3 创建一条平滑曲线 const curve = new THREE.CatmullRomCurve3([ startPoint, midPoint1, midPoint2, endPoint ]); // 设置传送带的宽度 const width = 5; // 创建纹理 let textureLoader = new THREE.TextureLoader(); this.map = textureLoader.load(texture1); // 贴图颜色空间校正 this.map.encoding = THREE.sRGBEncoding; this.map.wrapS = THREE.RepeatWrapping; this.map.wrapT = THREE.RepeatWrapping; this.map.repeat.set(1, 1); // 材质使用 MeshBasicMaterial(无需光源) const material = new THREE.MeshBasicMaterial({ map: this.map, transparent: true, opacity: 1, side: THREE.DoubleSide, }); // 创建一个组来存放所有的分段 this.recommandPathLineGroup = new THREE.Group(); // 动态分段数计算 const pathLength = curve.getLength(); // 获取路径总长度 const segmentLength = 5; // 每段的目标长度 const segments = Math.max(2, Math.ceil(pathLength / segmentLength)); // 确保至少分成2段 const step = 1 / segments; for (let i = 0; i < segments; i++) { const t1 = i * step; const t2 = (i + 1) * step; // 获取当前段的起点和终点 const point1 = curve.getPointAt(t1); const point2 = curve.getPointAt(t2); // 创建平面 const planeGeometry = new THREE.PlaneGeometry(width, segmentLength); const plane = new THREE.Mesh(planeGeometry, material); // 设置平面的位置和旋转 plane.position.copy(point1.clone().add(point2).multiplyScalar(0.5)); // 平面中心 plane.lookAt(point2); // 让平面朝向下一个点 plane.rotateX(Math.PI / 2); // 使平面与路径对齐 plane.rotateY(Math.PI / 2); // 使平面与路径对齐 // 将平面添加到组中 this.recommandPathLineGroup.add(plane); } // 将组添加到场景 this.scene.add(this.recommandPathLineGroup); return curve; // 返回生成的曲线对象 }, render(){ let cam = this.camera; //需要调整相机的视角 this._camera.position.set(cam.eye[0], cam.eye[1], cam.eye[2]); this._camera.up.set(cam.up[0], cam.up[1], cam.up[2]); this._camera.lookAt(new THREE.Vector3(cam.center[0], cam.center[1], cam.center[2])); // Projection matrix can be copied directly this._camera.projectionMatrix.fromArray(cam.projectionMatrix); this.updateInfoPanel(); // 更新面板信息 if (this.mixer) { this.mixer.update(this.clock.getDelta()); } if (this.airCraftMixer) { this.airCraftMixer.update(this.airCraftClock.getDelta()); } this.updatePosition(); this.renderer.state.reset(); this.bindRenderTarget(); this.renderer.render(this.scene, this._camera); // as we want to smoothly animate the ISS movement, immediately request a re-render this.requestRender(); // cleanup this.resetWebGLState(); } }