flyGLTF.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. import * as THREE from 'three';
  2. import {GLTFLoader} from "three/addons/loaders/GLTFLoader.js";
  3. import List from '../../config/routeList.json'
  4. import texture1 from "../../assets/arrow2.png";
  5. export const FlyGLTFClass = {
  6. constructor(options){
  7. this.webgl = options.webgl;
  8. this.view = options.view;
  9. this.mixer = options.mixer;
  10. this.airCraftMixer = options.airCraftMixer;
  11. this.clock = options.clock;
  12. this.airCraftClock = options.airCraftClock;
  13. this.path = options.path; //路径点
  14. this.pathProgress = options.pathProgress; //路径速度
  15. this.pathCurve = options.pathCurve; //路径曲线
  16. this.targetObject = options.targetObject; //路径飞行的目标对象
  17. this._camera = options._camera;
  18. this.raycaster = options.raycaster; // 射线投射器
  19. this.mouse = options.mouse; // 鼠标位置
  20. this.callBackFunction = options.callBackFunction; // 回调函数
  21. this.mouseClickEvent = null;
  22. this.uavid = options.uavid;
  23. },
  24. initialize(){
  25. this.mixer = new THREE.AnimationMixer();
  26. this.airCraftMixer = new THREE.AnimationMixer();
  27. this.clock = new THREE.Clock();
  28. this.airCraftClock = new THREE.Clock();
  29. this.renderer = new THREE.WebGLRenderer({
  30. context:this.gl,
  31. premultipliedAlpha: false
  32. });
  33. this.renderer.setPixelRatio(window.devicePixelRatio);
  34. this.renderer.setViewport(0,0,this.view.width,this.view.height);
  35. this.renderer.autoClear = false;
  36. this.renderer.autoClearDepth = false;
  37. this.renderer.autoClearColor = false;
  38. let originalSetRenderTarget = this.renderer.setRenderTarget.bind(this.renderer);
  39. this.renderer.setRenderTarget = function(target){
  40. originalSetRenderTarget(target);
  41. if(target){
  42. this.bindRenderTarget()
  43. }
  44. }
  45. this.scene = new THREE.Scene();
  46. let cam = this.camera;
  47. this._camera = new THREE.PerspectiveCamera(cam.fovY,cam.aspect,cam.near,cam.far);
  48. //添加坐标轴辅助工具
  49. const axesHelper = new THREE.AxesHelper(1);
  50. axesHelper.position.set(1000000,1000000,1000000);
  51. this.scene.add(axesHelper);
  52. const grid = new THREE.GridHelper(30,10,0xf0f0f0,0xffffff);
  53. this.scene.add(grid);
  54. this.ambient = new THREE.AmbientLight(0xffffff,5);
  55. this.scene.add(this.ambient);
  56. this.loadGLTF();
  57. //初始化路径曲线
  58. //this.initPathCurve();
  59. this.resetWebGLState();
  60. this.mouseClickEvent = this.view.on("click", this.onMouseClick.bind(this));
  61. },
  62. loadGLTF(){
  63. const loader = new GLTFLoader();
  64. let that = this;
  65. loader.load('public/gltf/mtwrj.glb', function (gltf) {
  66. gltf.scene.scale.set(6, 6, 6);
  67. that.scene.add(gltf.scene);
  68. that.targetObject = gltf.scene;
  69. // 添加动画
  70. that.mixer = new THREE.AnimationMixer(gltf.scene);
  71. const AnimationAction = that.mixer.clipAction(gltf.animations[0]);
  72. AnimationAction.play();
  73. // 创建信息面板
  74. // const panelGeometry = new THREE.PlaneGeometry(3.5, 3.5); // 面板大小
  75. // const canvas = document.createElement('canvas');
  76. // const context = canvas.getContext('2d');
  77. // // 加载背景图片
  78. // const backgroundImage = new Image();
  79. // backgroundImage.src = 'public/imgs/serviceBackground.png'; // 替换为你的图片路径
  80. // backgroundImage.onload = () => {
  81. // // 确保背景透明
  82. // context.clearRect(0, 0, canvas.width, canvas.height); // 清除所有内容,包含黑色
  83. // context.fillStyle = 'rgba(0, 0, 0, 0)'; // 设置透明背景
  84. // context.fillRect(0, 0, canvas.width, canvas.height); // 填充透明背景
  85. //
  86. // context.save();
  87. // context.scale(-1, 1);
  88. // context.translate(-canvas.width, 0);
  89. // // 绘制背景图片,填满整个 canvas
  90. // context.drawImage(backgroundImage, 0, 0, canvas.width, canvas.height);
  91. //
  92. // // // 绘制文字信息
  93. // // context.fillStyle = '#fff';
  94. // // context.font = 'normal 15px Arial';
  95. // // context.fillText('无人机信息:',40, 25);
  96. // // context.fillText('无人机名称: 无人机1号', 40, 50);
  97. // // context.fillText('所属企业: 美团', 40, 70);
  98. // // context.fillText('速度: 100km/h', 40, 90);
  99. // // context.fillText('高度: 200m', 40, 110);
  100. // // context.fillText('状态: 正常',40, 130);
  101. //
  102. //
  103. // const textInfo = [
  104. // { text: '无人机信息: ', color: '#fff', font: '15px Arial', offsetX:40, offsetY: 25 },
  105. // { text: '无人机名称:'+this.panelInfo.data.name, color: '#fff', font: '15px Arial', offsetX:40, offsetY: 50 },
  106. // { text: '所属企业: 美团: ', color: '#fff', font: '15px Arial', offsetX:40, offsetY: 70 },
  107. // { text: '速度: '+Math.floor(this.panelInfo.data.speed * 3.6)+'km/h',color: '#fff', font: '15px Arial', offsetX:40, offsetY: 90 },
  108. // { text: '高度: '+ this.panelInfo.data.altitude+'m', color: '#fff', font: '15px Arial', offsetX:40, offsetY: 110 },
  109. // { text: '状态: 正常',color: '#fff',font: '15px Arial', offsetX:40, offsetY: 130 }
  110. // ];
  111. // // 绘制文字
  112. // textInfo.forEach((line) => {
  113. // context.fillStyle = line.color;
  114. // context.font = line.font;
  115. // context.fillText(line.text, line.offsetX, line.offsetY);
  116. // });
  117. //
  118. //
  119. // // 标记纹理需要更新
  120. // texture.needsUpdate = true;
  121. // };
  122. //
  123. // const texture = new THREE.CanvasTexture(canvas);
  124. // const panelMaterial = new THREE.MeshBasicMaterial({ map: texture, side: THREE.DoubleSide, transparent: true });
  125. // panelGeometry.scale(-1,1,1);
  126. // const panelMesh = new THREE.Mesh(panelGeometry, panelMaterial);
  127. // // 将面板附加到无人机模型
  128. // panelMesh.position.set(0, 2.5, 0); // 调整面板位置到无人机上方
  129. // gltf.scene.add(panelMesh);
  130. //
  131. // // 保存信息面板,以便更新
  132. // that.infoPanel = { canvas, context, texture, mesh: panelMesh };
  133. });
  134. },
  135. // 点击事件处理函数
  136. onMouseClick(event) {
  137. // 获取点击位置的屏幕坐标
  138. let mouse = []
  139. this.webgl.toRenderCoordinates(
  140. this.view,
  141. [event.mapPoint.x, event.mapPoint.y, event.mapPoint.z],
  142. 0,
  143. this.view.spatialReference,
  144. mouse,
  145. 0,
  146. 1
  147. );
  148. const mousePoint = new THREE.Vector3(mouse[0], mouse[1], mouse[2]);
  149. // 获取摄像机位置
  150. const cameraPosition = new THREE.Vector3().setFromMatrixPosition(this._camera.matrixWorld);
  151. // 计算射线方向
  152. const rayDirection = new THREE.Vector3().subVectors(mousePoint, cameraPosition).normalize();
  153. // 设置射线起点和方向
  154. this.raycaster.set(cameraPosition, rayDirection);
  155. //this.visualizeRay()
  156. // 检测射线与网格的相交
  157. const intersects = this.raycaster.intersectObject(this.targetObject);
  158. if (intersects.length > 0) {
  159. this.callBackFunction(this.uavid);
  160. } else {
  161. console.log("未选中任何无人机");
  162. }
  163. },
  164. // 设置面板类似
  165. setPanelInfo(uavInfo) {
  166. this.panelInfo = uavInfo;
  167. },
  168. // 更新面板信息
  169. updateInfoPanel() {
  170. if (this.infoPanel) {
  171. const { canvas, context, texture ,mesh } = this.infoPanel;
  172. // 加载背景图片
  173. const backgroundImage = new Image();
  174. const topImage = new Image();
  175. const bottomImage = new Image();
  176. let typeInfo = "";
  177. switch (this.panelInfo.type){
  178. case "conflict":
  179. backgroundImage.src = 'public/imgs/基本信息框-红.png'; // 替换为你的图片路径
  180. topImage.src = 'public/imgs/warningBackground_time.png'; // 替换为你的图片路径
  181. bottomImage.src = 'public/imgs/warningBackground_conflict.png'; // 替换为你的图片路径
  182. typeInfo = "异常"
  183. break;
  184. case "electricityWarning":
  185. backgroundImage.src = 'public/imgs/基本信息框-红.png'; // 替换为你的图片路径
  186. typeInfo = "电池电量低"
  187. break;
  188. case "routeDeviate":
  189. backgroundImage.src = 'public/imgs/基本信息框-红.png'; // 替换为你的图片路径
  190. typeInfo = "异常"
  191. break;
  192. case "noFlyZone":
  193. backgroundImage.src = 'public/imgs/基本信息框-红.png'; // 替换为你的图片路径
  194. typeInfo = "异常"
  195. break;
  196. case "collision":
  197. backgroundImage.src = 'public/imgs/基本信息框-红.png'; // 替换为你的图片路径
  198. typeInfo = "建筑物过近"
  199. break;
  200. case "clearZone":
  201. backgroundImage.src = 'public/imgs/基本信息框-红.png'; // 替换为你的图片路径
  202. typeInfo = "净空区"
  203. break;
  204. default:
  205. backgroundImage.src = 'public/imgs/基本信息框.png'; // 替换为你的图片路径
  206. typeInfo = "正常"
  207. break;
  208. }
  209. // backgroundImage.src = 'public/imgs/serviceBackground.png'; // 替换为你的图片路径
  210. if(this.panelInfo.type == "conflict" && this.panelInfo.action == "wait"){
  211. topImage.onload = () => {
  212. bottomImage.onload = () => {
  213. // 确保背景透明
  214. context.clearRect(0, 0, canvas.width, canvas.height); // 清除所有内容,包含黑色
  215. context.fillStyle = 'rgba(0, 0, 0, 0)'; // 设置透明背景
  216. context.fillRect(0, 0, canvas.width, canvas.height); // 填充透明背景
  217. // 绘制顶部图片
  218. const topImageHeight = canvas.height * 0.2; // 设定顶部图片高度为 canvas 高度的 20%
  219. context.drawImage(topImage, 0, 0, canvas.width, topImageHeight);
  220. // 绘制底部图片
  221. const bottomImageHeight = canvas.height * 0.7; // 设定底部图片高度为 canvas 高度的 20%
  222. context.drawImage(bottomImage, 0, canvas.height - bottomImageHeight, canvas.width, bottomImageHeight);
  223. const textInfo = [
  224. { text: '暂停飞行原地等待:' + this.panelInfo.data.waitTime +'秒', color: '#fff', font: 'bold 16px Arial', offsetX:50, offsetY: 20 },
  225. { text: '距预测撞点前方: ', color: '#fff', font: '20px Verdana', offsetX:70, offsetY: 90 },
  226. { text: Math.floor(this.panelInfo.data.danger_path_distance * 1) + '米', color: '#ff0000', font: 'bold 22px Courier New', offsetX:120, offsetY: 120 }
  227. ];
  228. // 绘制文字
  229. textInfo.forEach((line) => {
  230. context.fillStyle = line.color;
  231. context.font = line.font;
  232. context.fillText(line.text, line.offsetX, line.offsetY);
  233. });
  234. // 标记纹理需要更新
  235. texture.needsUpdate = true;
  236. }
  237. }
  238. }else if(this?.panelInfo?.type == "routeDeviate"){
  239. //获取当前下标index
  240. let index = this.panelInfo.index * 1 +Math.floor(this.panelInfo.data.distance * 1 / 10) * 1 + 1;
  241. let targetPoint = [];
  242. this.webgl.toRenderCoordinates(
  243. this.view,
  244. [List.originList[index].mapX, List.originList[index].mapY, List.originList[index].mapZ],
  245. 0,
  246. this.view.spatialReference,
  247. targetPoint,
  248. 0,
  249. 1
  250. );
  251. this.targetPosition = new THREE.Vector3(targetPoint[0], targetPoint[1], targetPoint[2]);
  252. backgroundImage.onload = () => {
  253. // 确保背景透明
  254. context.clearRect(0, 0, canvas.width, canvas.height); // 清除所有内容,包含黑色
  255. context.fillStyle = 'rgba(0, 0, 0, 0)'; // 设置透明背景
  256. context.fillRect(0, 0, canvas.width, canvas.height); // 填充透明背景
  257. // 绘制背景图片,填满整个 canvas
  258. context.drawImage(backgroundImage, 0, 0, canvas.width, canvas.height);
  259. const textInfo = [
  260. { text: '计划偏离: ', color: '#fff', font: '15px Arial', offsetX:40, offsetY: 25 },
  261. { text: '偏离距离: '+Math.floor(this.panelInfo.data.distance* 1) +'米', color: '#ff0000', font: 'bold 22px Courier New', offsetX:70, offsetY: 60 },
  262. { text: '无人机正在偏离计划航线飞行', color: '#fff', font: '18px Arial', offsetX:40, offsetY: 90 },
  263. { text: '请及时调整飞行路线', color: '#fff', font: '18px Arial', offsetX:40, offsetY: 120 }
  264. ];
  265. // 绘制文字
  266. textInfo.forEach((line) => {
  267. context.fillStyle = line.color;
  268. context.font = line.font;
  269. context.fillText(line.text, line.offsetX, line.offsetY);
  270. });
  271. // 标记纹理需要更新
  272. texture.needsUpdate = true;
  273. };
  274. }else{
  275. backgroundImage.onload = () => {
  276. // 确保背景透明
  277. context.clearRect(0, 0, canvas.width, canvas.height); // 清除所有内容,包含黑色
  278. context.fillStyle = 'rgba(0, 0, 0, 0)'; // 设置透明背景
  279. context.fillRect(0, 0, canvas.width, canvas.height); // 填充透明背景
  280. // 绘制背景图片,填满整个 canvas
  281. context.drawImage(backgroundImage, 0, 0, canvas.width, canvas.height);
  282. const textInfo = [
  283. { text: '无人机信息: ', color: '#fff', font: '15px Arial', offsetX:40, offsetY: 25 },
  284. { text: '无人机名称:'+this.panelInfo.data.name, color: '#fff', font: '15px Arial', offsetX:40, offsetY: 50 },
  285. { text: '所属企业: 美团: ', color: '#fff', font: '15px Arial', offsetX:40, offsetY: 70 },
  286. { text: '速度: '+Math.floor(this.panelInfo.data.speed * 3.6)+'km/h', color: '#fff', font: '15px Arial', offsetX:40, offsetY: 90 },
  287. { text: '高度: '+ this.panelInfo.data.altitude+'m', color: '#fff', font: '15px Arial', offsetX:40, offsetY: 110 },
  288. { text: `状态: ${typeInfo}`, color: '#fff', font: '15px Arial', offsetX:40, offsetY: 130 }
  289. ];
  290. // 绘制文字
  291. textInfo.forEach((line) => {
  292. context.fillStyle = line.color;
  293. context.font = line.font;
  294. context.fillText(line.text, line.offsetX, line.offsetY);
  295. });
  296. // 标记纹理需要更新
  297. texture.needsUpdate = true;
  298. };
  299. }
  300. mesh.up.set(1, 0, 0); // 保持面板的 "上" 方向为 Y 轴正方向
  301. mesh.lookAt(this._camera.position);
  302. }
  303. },
  304. getClosestPointOnCurve(position, curve) {
  305. let closestPoint = null;
  306. let minDistance = Infinity;
  307. const points = curve.getPoints(100);
  308. points.forEach(point => {
  309. const distance = position.distanceTo(point);
  310. if (distance < minDistance) {
  311. minDistance = distance;
  312. closestPoint = point;
  313. }
  314. });
  315. return closestPoint;
  316. },
  317. setPathCurve(path,duration){
  318. this.path = path;
  319. this.pathProgress = 0; // 重置路径进度
  320. this.startTime = Date.now(); // 路径开始时间
  321. this.duration = duration || 2500;
  322. if(this.path.length > 1){
  323. //用于存储路径的点
  324. const points = this.path.map(point => {
  325. const renderPoint = [0,0,0];
  326. this.webgl.toRenderCoordinates(
  327. this.view,
  328. point,
  329. 0,
  330. this.view.spatialReference,
  331. renderPoint,
  332. 0,
  333. 1
  334. );
  335. return new THREE.Vector3(renderPoint[0],renderPoint[1],renderPoint[2]);
  336. });
  337. //创建一个CurvePath来容纳多条直线路径
  338. this.pathCurve = new THREE.CurvePath();
  339. //将路径点的相邻俩点组成一条直线添加到pathCurve中
  340. for(let i=0;i<points.length-1;i++){
  341. const line = new THREE.LineCurve3(points[i],points[i+1]);
  342. this.pathCurve.add(line);
  343. }
  344. //创建几何体以便在场景中可视化路径
  345. const geometry = new THREE.BufferGeometry().setFromPoints(this.pathCurve.getPoints(100));
  346. const material = new THREE.LineBasicMaterial({color:0xff0000,lineWidth:2});
  347. this.pathLine = new THREE.Line(geometry,material);
  348. this.scene.add(this.pathLine);
  349. if (this.recommandPathLineGroup) {
  350. this.scene.remove(this.recommandPathLineGroup);
  351. }
  352. if(this?.panelInfo?.type == "routeDeviate"){
  353. this.updateRecommandPath();
  354. }
  355. }
  356. },
  357. // 获取当前位置和方向信息
  358. getCurrentPositionAndDirection() {
  359. if (this.pathCurve && this.targetObject) {
  360. // 获取当前位置
  361. const position = this.pathCurve.getPointAt(this.pathProgress);
  362. // 获取切线方向
  363. const tangent = this.pathCurve.getTangentAt(this.pathProgress);
  364. // 计算目标朝向(四元数)
  365. this.targetObject.quaternion.setFromUnitVectors(new THREE.Vector3(0, 0, 1), tangent);
  366. // 获取路径进度对应的点
  367. const pathProgress = this.pathProgress;
  368. // 计算路径的总长度和每条线段的长度
  369. let totalLength = 0;
  370. const segmentLengths = [];
  371. const segments = [];
  372. for (let i = 0; i < this.path.length - 1; i++) {
  373. const startPoint = this.path[i];
  374. const endPoint = this.path[i + 1];
  375. const segmentLength = Math.sqrt(
  376. Math.pow(endPoint[0] - startPoint[0], 2) +
  377. Math.pow(endPoint[1] - startPoint[1], 2) +
  378. Math.pow(endPoint[2] - startPoint[2], 2)
  379. );
  380. segmentLengths.push(segmentLength);
  381. totalLength += segmentLength;
  382. // 存储当前线段的起点和终点
  383. segments.push({ start: startPoint, end: endPoint });
  384. }
  385. // 计算当前位置对应的进度在整个路径的比例
  386. let progress = pathProgress * totalLength;
  387. let currentSegmentIndex = 0;
  388. let accumulatedLength = 0;
  389. // 判断当前进度所在的线段
  390. for (let i = 0; i < segmentLengths.length; i++) {
  391. accumulatedLength += segmentLengths[i];
  392. if (progress <= accumulatedLength) {
  393. currentSegmentIndex = i;
  394. break;
  395. }
  396. }
  397. // 获取当前所在的线段
  398. const currentSegment = segments[currentSegmentIndex];
  399. const segmentStart = currentSegment.start;
  400. const segmentEnd = currentSegment.end;
  401. return {
  402. position: position, // 当前坐标
  403. tangent: tangent, // 当前切线方向
  404. rotation: this.targetObject.rotation, // 当前旋转信息(朝向)
  405. pathProgress: this.pathProgress, // 当前路径进度
  406. currentSegment: { start: segmentStart, end: segmentEnd } // 当前所在的线段
  407. };
  408. }
  409. return null;
  410. },
  411. // 设置悬停状态
  412. setHovering(isHovering) {
  413. this.isHovering = isHovering;
  414. },
  415. updatePosition(){
  416. const currentTime = Date.now();
  417. const elapsedTime = currentTime - this.startTime; // 已经过的时间
  418. // 计算路径进度
  419. this.pathProgress = elapsedTime / this.duration;
  420. if (this.pathCurve && this.targetObject) {
  421. // 如果到达终点,重置路径并请求新路径
  422. if (this.pathProgress >= 1) {
  423. this.pathProgress = 1; // 确保进度不超过 1
  424. }
  425. try{
  426. const position = this.pathCurve.getPointAt(this.pathProgress);
  427. const tangent = this.pathCurve.getTangentAt(this.pathProgress);
  428. this.targetObject.position.copy(position);
  429. this.targetObject.quaternion.setFromUnitVectors(
  430. new THREE.Vector3(0, 0, 1), tangent
  431. );
  432. }catch(e){
  433. console.log("this.pathProgress",this.pathProgress)
  434. }
  435. }
  436. //this.targetObject.rotation.x = -Math.PI/2;
  437. this.targetObject.rotation.y = 0;
  438. this.targetObject.rotation.z = -Math.PI/2;
  439. if (this.recommandPathLineGroup) {
  440. this.map.offset.y -= 0.1;
  441. }
  442. },
  443. updateRecommandPath() {
  444. // 如果已存在曲线或管道,将其从场景中移除
  445. if (this.recommandPathLineGroup) {
  446. this.scene.remove(this.recommandPathLineGroup);
  447. }
  448. let startPoint = this.pathCurve.getPointAt(this.pathProgress);
  449. let endPoint = this.targetPosition;
  450. // 创建两个点之间的中间控制点
  451. const midPoint1 = new THREE.Vector3(
  452. (2 * startPoint.x + endPoint.x) / 3,
  453. (2 * startPoint.y + endPoint.y) / 3 + 5, // 可调整中间点的高度
  454. (2 * startPoint.z + endPoint.z) / 3
  455. );
  456. const midPoint2 = new THREE.Vector3(
  457. (startPoint.x + 2 * endPoint.x) / 3,
  458. (startPoint.y + 2 * endPoint.y) / 3 + 5, // 可调整中间点的高度
  459. (startPoint.z + 2 * endPoint.z) / 3
  460. );
  461. // 使用 CatmullRomCurve3 创建一条平滑曲线
  462. const curve = new THREE.CatmullRomCurve3([
  463. startPoint,
  464. midPoint1,
  465. midPoint2,
  466. endPoint
  467. ]);
  468. // 设置传送带的宽度
  469. const width = 5;
  470. // 创建纹理
  471. let textureLoader = new THREE.TextureLoader();
  472. this.map = textureLoader.load(texture1);
  473. // 贴图颜色空间校正
  474. this.map.encoding = THREE.sRGBEncoding;
  475. this.map.wrapS = THREE.RepeatWrapping;
  476. this.map.wrapT = THREE.RepeatWrapping;
  477. this.map.repeat.set(1, 1);
  478. // 材质使用 MeshBasicMaterial(无需光源)
  479. const material = new THREE.MeshBasicMaterial({
  480. map: this.map,
  481. transparent: true,
  482. opacity: 1,
  483. side: THREE.DoubleSide,
  484. });
  485. // 创建一个组来存放所有的分段
  486. this.recommandPathLineGroup = new THREE.Group();
  487. // 动态分段数计算
  488. const pathLength = curve.getLength(); // 获取路径总长度
  489. const segmentLength = 5; // 每段的目标长度
  490. const segments = Math.max(2, Math.ceil(pathLength / segmentLength)); // 确保至少分成2段
  491. const step = 1 / segments;
  492. for (let i = 0; i < segments; i++) {
  493. const t1 = i * step;
  494. const t2 = (i + 1) * step;
  495. // 获取当前段的起点和终点
  496. const point1 = curve.getPointAt(t1);
  497. const point2 = curve.getPointAt(t2);
  498. // 创建平面
  499. const planeGeometry = new THREE.PlaneGeometry(width, segmentLength);
  500. const plane = new THREE.Mesh(planeGeometry, material);
  501. // 设置平面的位置和旋转
  502. plane.position.copy(point1.clone().add(point2).multiplyScalar(0.5)); // 平面中心
  503. plane.lookAt(point2); // 让平面朝向下一个点
  504. plane.rotateX(Math.PI / 2); // 使平面与路径对齐
  505. plane.rotateY(Math.PI / 2); // 使平面与路径对齐
  506. // 将平面添加到组中
  507. this.recommandPathLineGroup.add(plane);
  508. }
  509. // 将组添加到场景
  510. this.scene.add(this.recommandPathLineGroup);
  511. return curve; // 返回生成的曲线对象
  512. },
  513. render(){
  514. let cam = this.camera;
  515. //需要调整相机的视角
  516. this._camera.position.set(cam.eye[0], cam.eye[1], cam.eye[2]);
  517. this._camera.up.set(cam.up[0], cam.up[1], cam.up[2]);
  518. this._camera.lookAt(new THREE.Vector3(cam.center[0], cam.center[1], cam.center[2]));
  519. // Projection matrix can be copied directly
  520. this._camera.projectionMatrix.fromArray(cam.projectionMatrix);
  521. this.updateInfoPanel(); // 更新面板信息
  522. if (this.mixer) {
  523. this.mixer.update(this.clock.getDelta());
  524. }
  525. if (this.airCraftMixer) {
  526. this.airCraftMixer.update(this.airCraftClock.getDelta());
  527. }
  528. this.updatePosition();
  529. this.renderer.state.reset();
  530. this.bindRenderTarget();
  531. this.renderer.render(this.scene, this._camera);
  532. // as we want to smoothly animate the ISS movement, immediately request a re-render
  533. this.requestRender();
  534. // cleanup
  535. this.resetWebGLState();
  536. }
  537. }