import * as THREE from "../../../libs/three.js/build/three.module.js"; import {ClipTask, ClipMethod, CameraMode, LengthUnits, ElevationGradientRepeat} from "../../defines.js"; import {TagTool} from "../objects/tool/TagTool.js"; import Compass from "../objects/tool/Compass.js"; import AxisViewer from "../objects/tool/AxisViewer.js"; import {ExtendScene} from '../../viewer/ExtendScene.js' import {transitions, easing, lerp} from '../utils/transitions.js' import {Renderer} from "../../PotreeRendererNew.js"; //import {PotreeRenderer} from "../viewer/PotreeRenderer.js"; import {EDLRenderer} from "../../viewer/EDLRendererNew.js"; import {HQSplatRenderer} from "../../viewer/HQSplatRenderer.js"; import {MapViewer} from "./map/MapViewer.js"; import {NavigationCube} from '../../viewer/NavigationCube.js' //import {MapView} from "../viewer/map.js"; import {ProfileWindow, ProfileWindowController} from "../../viewer/profile.js"; import {Features} from "../../Features.js"; import {Message} from "../../utils/Message.js"; import {Sidebar} from "../../viewer/sidebarNew.js"; import {AnnotationTool} from "../../utils/AnnotationTool.js"; import {MeasuringTool} from "../objects/tool/MeasuringTool.js"; import CursorDeal from '../utils/CursorDeal.js' import Common from '../utils/Common.js' import browser from '../utils/browser.js' import SplitScreen from '../utils/SplitScreen.js' import cameraLight from "../utils/cameraLight.js"; import math from "../utils/math.js"; import {MeshDraw} from '../utils/DrawUtil.js' import {UoMService} from '../utils/UnitConvert.js' import {ClippingTool} from "../../utils/ClippingTool.js"; import {TransformationTool} from "../../utils/TransformationToolNew.js"; import {Utils} from "../../utils.js"; import {BoxVolume} from "../../utils/VolumeNew.js"; import {VolumeTool} from "../../utils/VolumeTool.js"; import {Images360} from "../modules/panos/Images360.js"; import {Clip} from '../modules/clipModel/Clip.js' import {Alignment} from "../modules/datasetAlignment/Alignment.js"; import {SiteModel} from "../modules/siteModel/SiteModel.js"; import MergeEditor from "../modules/mergeModel/MergeEditor.js"; import {RouteGuider} from '../modules/route/RouteGuider.js' import {Clipping} from '../modules/clipping/Clipping.js' import ParticleEditor from '../modules/Particles/ParticleEditor.js' import CamAniEditor from '../modules/CameraAnimation/CamAniEditor.js' import PanoEditor from '../modules/panoEdit/panoEditor.js' import VolumeComputer from '../modules/VolumeCompute/VolumeComputer.js' import {InputHandler} from "../../navigation/InputHandlerNew.js"; import Magnifier from "../objects/Magnifier.js"; import Reticule from "../objects/Reticule.js"; import Viewport from "../viewer/Viewport.js" import {ViewerBase} from "../viewer/viewerBase.js" import {OBJLoader} from "../../../libs/three.js/loaders/OBJLoader.js"; import {MTLLoader} from "../../../libs/three.js/loaders/MTLLoader.js"; import {GLTFLoader} from "../../../libs/three.js/loaders/GLTFLoader.js"; import {Loader3DTiles} from '../../../libs/three.js/3dtiles/three-loader-3dtiles.esm.js'; import {PLYLoader} from "../../../libs/three.js/loaders/PLYLoader.js"; import SSAARenderPass from "../materials/postprocessing/SSAARenderPass.js" import EffectComposer from '../materials/postprocessing/EffectComposer.js' import {ShaderPass} from '../materials/postprocessing/ShaderPass.js' import RenderPass from '../materials/postprocessing/RenderPass.js' import FXAAShader from "../materials/postprocessing/FXAAShader.js" import OutlinePass from "../materials/postprocessing/OutlinePass.js" import BasicMaterial from '../materials/BasicMaterial.js' //import {RoomEnvironment} from './RoomEnvironment.js' import {FirstPersonControls} from "../../navigation/FirstPersonControlsNew.js"; import {OrbitControls} from "../../navigation/OrbitControlsNew.js"; //import {VRControls} from "../../navigation/VRControlsNew.js"; import { ClassificationScheme } from "../../materials/ClassificationScheme.js"; import { VRButton } from '../../../libs/three.js/extra/VRButton.js'; import DxfLoader from '../../loader/DxfLoader.js' const manager = new THREE.LoadingManager(); let loaders = {} let mapArea; let shelterHistory = [] Potree.isIframeChild = window.parent!=window //子页面 /* if(Potree.isIframeChild){ let rootWin = Common.getRootWindow() rootWin && rootWin.viewer.dispatchEvent({type:'createIframe', window}) //给祖先页面发送信息,得不到可能跨域了或无viewer } */ /* window.addEventListener('focus',()=>{ console.log('focus',window.winIndex) }) window.addEventListener('blur',()=>{ console.log('blur',window.winIndex) }) */ export class Viewer extends ViewerBase{ constructor(domElement, mapArea_, args = {}){ super(domElement, $.extend(args,{name:'mainViewer', antialias:true, preserveDrawingBuffer:true})); //注:viewer因为要分屏,尤其是四屏,preserveDrawingBuffer需要为true, 否则无法局部clear window.viewer = this mapArea = mapArea_ if(Potree.settings.editType == "pano" || Potree.settings.editType == "merge"){ this.modules = { Alignment, SiteModel, volumeComputer: new VolumeComputer //暂时 } Potree.settings.useDepthTex = false if(Potree.settings.editType == "pano" ){ this.modules.PanoEditor = PanoEditor }else if(Potree.settings.editType == "merge"){ this.modules.MergeEditor = MergeEditor this.modules.CamAniEditor = CamAniEditor } }else{ this.modules = { Clip, Alignment, SiteModel, RouteGuider : new RouteGuider, clipping: new Clipping, ParticleEditor, CamAniEditor, volumeComputer: new VolumeComputer, MergeEditor } } { Potree.timeCollect = { //median预置一个中等值,以防止cpu过低的设备首次卡顿 //depthSampler : {minCount:400, median: 1}, //旧的 实时从图片中获取色值的用时,但是现在直接从data中取 depthSamChangeImg:{minCount:200, median: 25} //换图的用时。 需要一开始就start } for(let i in Potree.timeCollect){ Potree.timeCollect[i].measures = []; Potree.timeCollect[i].sum = 0; } setTimeout(()=>{ for(let i in Potree.timeCollect){ Potree.timeCollect[i].start = true } /* setTimeout(()=>{ console.log('timeCollect', Potree.timeCollect.depthSampler.median, Potree.timeCollect.depthSampler.ave, Potree.timeCollect.depthSampler.measures.length) },10000) */ },2000) } this.navigateMode = 'free' // 'panorama'; 'free'自由模式是只显示点云或者未进入到漫游点, this.isEdit = true this.waitQueue = [] this.unitConvert = new UoMService(); this.visible = true this.fpVisiDatasets = [] this.atDatasets = [] this.objs = new THREE.Object3D this.testMaxNodeCount = 0 this.tiles3dVisiVCount = 0 //this.lastPos = new THREE.Vector3(Infinity,Infinity,Infinity) //------------- var supportExtFragDepth = !!Potree.Features.EXT_DEPTH.isSupported(this.renderer.getContext()) ;//iphoneX居然不支持 //这意味着边缘增强和测量线遮挡失效 if(!supportExtFragDepth)console.error('ExtFragDepth unsupported! 边缘增强和测量线遮挡失效') this.guiLoaded = false; this.guiLoadTasks = []; this.onVrListeners = []; this.messages = []; this.elMessages = $(`
`); $(domElement).append(this.elMessages); this.fakeMeasure = {}; try{ if(!Potree.settings.isOfficial) { // generate missing dom hierarchy if ($(domElement).find('#potree_map').length === 0) { let potreeMap = $(` `); $(domElement).append(potreeMap); } if ($(domElement).find('#potree_description').length === 0) { let potreeDescription = $(`
`); $(domElement).append(potreeDescription); } if ($(domElement).find('#potree_annotations').length === 0) { let potreeAnnotationContainer = $(`
`); $(domElement).append(potreeAnnotationContainer); } if ($(domElement).find('#potree_quick_buttons').length === 0) { let potreeMap = $(`
`); $(domElement).append(potreeMap); } //add { $(domElement).append($("
")); if(!mapArea && Potree.settings.editType != 'merge'){ mapArea = $("
"); $(domElement).append(mapArea); mapArea = mapArea[0]; } } /* let domRoot = this.renderer.domElement.parentElement; let elAttach = $(""); elAttach.css({ position : "absolute", right : '10%', bottom: '20px', zIndex: "10000", fontSize:'1em', color:"black", background:'rgba(255,255,255,0.8)', }) let state = false elAttach.on("click", () => { window.buttonFunction && window.buttonFunction() }); domRoot.appendChild(elAttach[0]); */ } this.tiles3dMemoryUsage = 0 this.pointCloudLoadedCallback = args.onPointCloudLoaded || function () {}; // if( /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) { // defaultSettings.navigation = "Orbit"; // } this.server = null; this.fov = 60; this.isFlipYZ = false; this.useDEMCollisions = false; this.generateDEM = false; this.minNodeSize = 30; //允许加载的node的最小可见像素宽度。越大越省性能 this.edlStrength = 1.0; this.edlRadius = 1.4; this.edlOpacity = 1.0; this.useEDL = false; this.description = ""; this.classifications = ClassificationScheme.DEFAULT; this.moveSpeed = 1; this.lengthUnit = LengthUnits.METER; this.lengthUnitDisplay = LengthUnits.METER; this.showBoundingBox = false; this.showAnnotations = true; this.freeze = false; this.elevationGradientRepeat = ElevationGradientRepeat.CLAMP; this.filterReturnNumberRange = [0, 7]; this.filterNumberOfReturnsRange = [0, 7]; this.filterGPSTimeRange = [-Infinity, Infinity]; this.filterPointSourceIDRange = [0, 65535]; this.potreeRenderer = null; this.edlRenderer = null; this.pRenderer = null; this.scene = null; this.sceneVR = null; this.overlay = null; this.overlayCamera = null; this.inputHandler = null; this.controls = null; this.clippingTool = null; this.transformationTool = null; this.navigationCube = null; this.compass = null; this.skybox = null; this.clock = new THREE.Clock(); this.background = null; this.buffers = new Map if(args.noDragAndDrop){ }else{ this.initDragAndDrop(); } if(typeof Stats !== "undefined"){ this.stats = new Stats(); this.stats.showPanel( 0 ); // 0: fps, 1: ms, 2: mb, 3+: custom document.body.appendChild( this.stats.dom ); } { this.overlay = new THREE.Scene(); this.overlayCamera = new THREE.OrthographicCamera( 0, 1, 1, 0, -1000, 1000 ); } this.pRenderer = new Renderer(this.renderer); { let near = 2.5; let far = 10.0; let fov = 90; this.shadowTestCam = new THREE.PerspectiveCamera(90, 1, near, far); this.shadowTestCam.position.set(3.50, -2.80, 8.561); this.shadowTestCam.lookAt(new THREE.Vector3(0, 0, 4.87)); } let scene = new ExtendScene(this.renderer); { // create VR scene this.sceneVR = new THREE.Scene(); // let texture = new THREE.TextureLoader().load(`${Potree.resourcePath}/images/vr_controller_help.jpg`); // let plane = new THREE.PlaneBufferGeometry(1, 1, 1, 1); // let infoMaterial = new THREE.MeshBasicMaterial({map: texture}); // let infoNode = new THREE.Mesh(plane, infoMaterial); // infoNode.position.set(-0.5, 1, 0); // infoNode.scale.set(0.4, 0.3, 1); // infoNode.lookAt(0, 1, 0) // this.sceneVR.add(infoNode); // window.infoNode = infoNode; } this.setScene(scene); //add: for 截图时抗锯齿 { /* this.composer = new EffectComposer( this.renderer ); this.ssaaRenderPass = new SSAARenderPass(0x000000, 0); this.composer.addPass( this.ssaaRenderPass ); */ //this.ssaaRenderPass.useCopy = true //this.ssaaRenderPass.renderToScreen = true; //this.ssaaRenderPass.needsSwap = false //见 https://threejs.org/examples/?q=AA#webgl_postprocessing_fxaa 效果和SSAA差不多,都对透明不太友好。 /* let outlinePass = this.outlinePass = new OutlinePass( ); this.ssaaRenderPass.addPass(outlinePass) */ //ssaa有透明度、发黑的问题所以不用了 //-------------------------- this.composer = new EffectComposer( this.renderer ); this.composer.scaleRatio = 4 //将底图和测量线绘制在一张高倍贴图上,for测量线不模糊 this.composer.readTarget = true //把底图和测量线一起fxaa /* const renderPass = new RenderPass(); //renderPass.clearColor = new THREE.Color( 0,0,0 ); //renderPass.clearAlpha = 0; renderPass.clear = !this.composer.readTarget this.composer.addPass( renderPass ); */ //for 融合页面 let outlinePass = this.outlinePass = new OutlinePass( ); outlinePass.renderToScreen = true //这样更流畅,不用ssaa了,缺点是outline有锯齿 outlinePass.enabled = false this.composer.addPass( outlinePass ); outlinePass.edgeStrength = 4 outlinePass.edgeGlow = 0 outlinePass.visibleEdgeColor = new THREE.Color("#09a1b3") /* this.fxaaPass = new ShaderPass( FXAAShader ); this.fxaaPass.readTarget = true //add this.fxaaPass.setSize = function(width, height){ this.material.uniforms[ 'resolution' ].value.x = 1 / ( width ) ; this.material.uniforms[ 'resolution' ].value.y = 1 / ( height ) ; } this.fxaaPass.renderToScreen = true; this.composer.addPass( this.fxaaPass ); */ //抗锯齿截图 效果时而好时而不好,文字比较模糊 //这两个暂时不能一起用。目前刚好不需要一起用 } { this.mainViewport = new Viewport( this.scene.view, this.scene.cameraP, { left:0, bottom:0, width:1, height: 1, name:'MainView' }) this.viewports = [this.mainViewport] Potree.settings.showCompass && (this.compass = new Compass(Potree.settings.compassDom, this.mainViewport)); //this.axis = new AxisViewer(this.mainViewport, this.renderArea ) this.magnifier = new Magnifier(this); this.reticule = new Reticule(this) this.scene.scene.add(this.magnifier) this.scene.scene.add(this.reticule) if(Potree.settings.editType != "pano" && Potree.settings.editType != 'merge'){ this.mapViewer = new MapViewer(mapArea/* $('#mapGaode')[0] */) } this.inputHandler = new InputHandler(this, this.scene.scene); this.inputHandler.containsMouse = true//初始化,使键盘事件在mainViewer有效 //this.inputHandler.setScene(this.scene); //this.inputHandler.addInputListener(this);//add this.clippingTool = new ClippingTool(this); this.transformationTool = new TransformationTool(this); this.navigationCube = new NavigationCube(this); this.navigationCube.visible = false; this.createControls(); this.clippingTool.setScene(this.scene); let onPointcloudAdded = (e) => { if (this.scene.pointclouds.length === 1) { let speed = e.pointcloud.boundingBox.getSize(new THREE.Vector3()).length(); speed = speed / 2000; this.setMoveSpeed(speed); } }; let onVolumeRemoved = (e) => { this.inputHandler.deselect(e.volume); }; this.addEventListener('scene_changed', (e) => { this.inputHandler.setScene(e.scene); this.clippingTool.setScene(this.scene); if(!e.scene.hasEventListener("pointcloud_added", onPointcloudAdded)){ e.scene.addEventListener("pointcloud_added", onPointcloudAdded); } if(!e.scene.hasEventListener("volume_removed", onPointcloudAdded)){ e.scene.addEventListener("volume_removed", onVolumeRemoved); } }); this.scene.addEventListener("volume_removed", onVolumeRemoved); this.scene.addEventListener('pointcloud_added', onPointcloudAdded); } { // set defaults this.setFOV(60); this.setEDLEnabled(false); this.setEDLRadius(3); this.setEDLStrength(0.01); this.setEDLOpacity(1.0); this.setPointBudget(1*1000*1000); this.setShowBoundingBox(false); this.setFreeze(false); this.setControls(this.fpControls/* orbitControls */); this.setBackground( new THREE.Color(Potree.config.background),1 /* 'gradient' */ ); this.scaleFactor = 1; this.loadSettingsFromURL(); } // start rendering! //if(args.useDefaultRenderLoop === undefined || args.useDefaultRenderLoop === true){ //requestAnimationFrame(this.loop.bind(this)); //} this.renderer.setAnimationLoop(this.loop.bind(this)); this.loadGUI = this.loadGUI.bind(this); this.annotationTool = new AnnotationTool(this); this.measuringTool = new MeasuringTool(this); //this.profileTool = new ProfileTool(this); this.volumeTool = new VolumeTool(this); this.tagTool = new TagTool(this); //----------- CursorDeal.init(this, this.mapViewer ? [this, this.mapViewer] : [this])//ADD if(Potree.settings.editType == "pano"){ this.modules.PanoEditor.init() }else if(Potree.settings.editType == "merge"){ this.modules.MergeEditor.init() }else{ this.modules.SiteModel.init() this.modules.ParticleEditor.init() } this.modules.Alignment.init() this.images360 = new Images360(this); this.scene.scene.add(this.objs); loaders = { objLoader : new OBJLoader( manager ), mtlLoader : new MTLLoader( manager ), glbLoader : new GLTFLoader(undefined, this.renderer, Potree.settings.libsUrl ), plyLoader : new PLYLoader( manager ), dxfLoader : new DxfLoader() } //add test /* const environment = new RoomEnvironment(); const pmremGenerator = new THREE.PMREMGenerator( this.renderer ); this.scene.scene.environment = pmremGenerator.fromScene( environment ).texture; */ //----------- this.modules.volumeComputer && this.modules.volumeComputer.init() }catch(e){ this.onCrash(e); } //-----------------------add---------------------------------------------------- /* { let ratio this.addEventListener('resize',(e)=>{ if(ratio != e.deviceRatio){ //因为devicePixelRatio会影响到点云大小,所以改变时计算下点云大小 viewer.scene.pointclouds.forEach(p => { p.changePointSize() }) } ratio = e.deviceRatio }) } */ { let pointDensity = '' Object.defineProperty(Potree.settings , "pointDensity",{ get: function() { return pointDensity }, set: (density)=>{ if(density){ let config = Potree.config.pointDensity[density]; let pointBudget = config.pointBudget; if(density == 'magnifier'){//尽可能不变pointBudget,否则点云可能会闪烁,因点云被释放又加载,如SS-t-7DUfWAUZ3V pointBudget = Math.max(Potree.pointBudget, Potree.config.pointDensity['magnifier'].pointBudget) }else if(this.magnifier.visible){//放大镜打开时要保证最低点云数量(全景模式没点) pointBudget = Math.max(pointBudget, Potree.config.pointDensity['magnifier'].pointBudget) } viewer.setMinNodeSize(config.minNodeSize || Potree.config.minNodeSize) viewer.setPointBudget(pointBudget ); pointDensity = density this.setPointLevels() this.dispatchEvent('pointDensityChanged') } } }) let UserPointDensity = '' Object.defineProperty(Potree.settings , "UserPointDensity",{ get: function() { return UserPointDensity }, set: (density)=>{ if(UserPointDensity != density){ if(Potree.settings.displayMode == 'showPointCloud' && this.viewports.length != 4){//漫游模式和四屏时都有自己的pointDensity Potree.settings.pointDensity = density } UserPointDensity = density this.dispatchEvent('UserPointDensityChanged') } } }) } { let cameraFar = Potree.settings.cameraFar Object.defineProperty(Potree.settings , "cameraFar",{ get: function() { return cameraFar }, set: (far)=>{ if(far != cameraFar){ if(Potree.settings.displayMode != 'showPanos' && !this.fixCamFar){ this.mainViewport.camera.far = far; this.mainViewport.camera.updateProjectionMatrix() } cameraFar = far } } }) } /* this.reticule.addEventListener('update',(e)=>{ this.needRender = true if(this.mapViewer && this.mapViewer.attachedToViewer)this.mapViewer.needRender = true //分屏时mapViewer也有reticule }) */ this.reticule.addEventListener('update',(e)=>{ this.reticule.hoverViewport && (this.reticule.hoverViewport.needRender = true) this.lazyRenderViewports() if(this.mapViewer && this.mapViewer.attachedToViewer && (this.mapViewer.viewports[0].needRender || this.needRender ) ) this.mapViewer.needRender = true //分屏时mapViewer也有reticule }) this.addEventListener('pointcloud_changed',(e)=>{ if(this.screenshoting){ this.viewports.filter(e=>!e.noPointcloud).forEach(e =>e.needRender = true) } else this.lazyRenderViewports() }) this.addEventListener('allLoaded', ()=>{ setTimeout(this.testPointcloudsMaxLevel.bind(this), 1000) //延迟一丢丢,等画面出现 this.scene.pointclouds.forEach(pointcloud=>{ pointcloud.addEventListener('isVisible',(e)=>{//是否显示该点的mesh(不显示也能走) //console.log('pointcloud isVisible', this.id, e.visible) if(e.reason != 'displayMode' && e.reason != 'overlinePass'){ this.updateModelBound('visibleChanged') } this.dispatchEvent('pointcloud_changed') }) pointcloud.material.addEventListener('material_property_changed',()=>{ this.dispatchEvent('pointcloud_changed') }) }) window.addEventListener('unfocusPage',(e)=>{ console.log('unfocusPage',document.title) this.setDisplay(false) }) window.addEventListener('focusPage',(e)=>{ console.log('focusPage',document.title) this.setDisplay(true) }) }) if(Potree.settings.editType != 'pano' && Potree.settings.editType != 'merge'){ this.addEventListener('switchFloorplanSelect',(e)=>{//进入平面图设置后 切换选中的数据集 this.selectedFloorplan = e.pointcloud; //绝对显示 this.updateFpVisiDatasets() let pointclouds; if(e.pointcloud){ pointclouds = [e.pointcloud] }else if(this.fpVisiDatasets.length){ pointclouds = this.fpVisiDatasets } pointclouds && this.mapViewer.fitToDatasets(pointclouds) }) this.modules.SiteModel.bus.addEventListener('FloorChange',(e)=>{ this.updateFpVisiDatasets() this.updatePanosVisibles(e.currentFloor) //问:编辑空间模型时,需不需要改为显示当前选择的楼层。因为若所在楼层和选中的不一致,修改选中楼层的轮廓却改不了marker显示很奇怪,尤其刚好在一个建筑内时。 }) this.mapViewer.mapLayer.addEventListener('floorplanLoaded',()=>{ this.updateCadVisibles(this.fpVisiDatasets, true) //加载完成后重新更新下 }) /* this.modules.Clip.bus.addEventListener('updateSelectedDatasets',()=>{ this.updateFpVisiDatasets() }) */ } { let updated, zoomLevel let update = (e)=>{ if(e.type == 'updateModelBound' || e.viewport == this.mainViewport && (e.changeInfo.positionChanged || zoomLevel != this.images360.zoomLevel)){ zoomLevel = this.images360.zoomLevel; //对updateMarkerVisibles有影响 //e.changeInfo.positionChanged && shelterHistory.clear() //清空 (e.type == 'updateModelBound' || e.changeInfo.positionChanged) && this.updateDatasetAt() //更新所在数据集 if(Potree.settings.ifShowMarker && Potree.settings.editType != 'merge'){ Common. intervalTool.isWaiting('updateMarkerVisibles', ()=>{ if(!this.mainViewport.view.isFlying() ){ this.updateMarkerVisibles() } },500) } } } this.addEventListener('camera_changed', update) this.addEventListener('updateModelBound', update) this.addEventListener('showMarkerChanged',()=>{ this.updatePanosVisibles(this.modules.SiteModel.currentFloor) this.updateMarkerVisibles() }) /* if(!Potree.Features.EXT_DEPTH.isSupported()){ this.images360.addEventListener('endChangeMode',(e)=>{ if(e.mode == 'showPanos'){ this.updateMarkerVisibles() } }) */ this.images360.addEventListener('getNeighbourAuto',(e)=>{ if(/* Potree.settings.displayMode == 'showPanos' && */e.panos.includes(this.images360.currentPano)){ Common.intervalTool.isWaiting('updateMarkerVisibles', ()=>{ this.updateMarkerVisibles() },500) } }) /* } */ } { let interval document.addEventListener('visibilitychange',(e)=>{ let v = !document.hidden //console.warn('visibilitychange', v ) this.dispatchEvent({type:'pageVisible', v } ) if(this.screenshoting && !v){//截图过程中离开页面需要照常loop。但是尽量别离开页面,效果只能达到90% interval = setInterval(()=>{ //console.log('force loop', Date.now()) if(this.screenshoting){ //截图完成时如果还没回来就不loop this.loop(Date.now()) } },50) //实际>1000, 浏览器会放慢定时器,放慢多少不确定 }else if(v && interval){ clearInterval(interval) } }) } /* if(!Potree.isIframeChild){ window.winIndex = 0; let index = 1; this.addEventListener('createIframe',(e)=>{//创建了子页面 let child = e.window; child.winIndex = index ++; }) //不知道删除iframe时是否那些模型还在内存里,需要释放吗? 如果要需要加一个事件 } */ /* { let setInteract = ()=>{ this.interacted = true //标记这一帧用户有操作屏幕 } this.addEventListener('global_mousedown', setInteract) this.addEventListener('global_touchmove', setInteract) this.addEventListener('global_mousewheel', setInteract) } {//针对数据集偏离中心很远后产生的精度损失而抖动 this.addEventListener('camera_changed', e => { if(e.viewport == this.mainViewport && (e.changeInfo.positionChanged)){ if(Potree.settings.editType != 'merge' && Potree.settings.editType != 'pano'){ if(!this.mainViewport.view.isFlying() ){ let pos = this.mainViewport.view.position.clone() // this.mainViewport.camera.getWorldPosition(new THREE.Vector3) if(pos.lengthSq() > 25000000){ Common.intervalTool.isWaiting('moveWorldCenter', ()=>{ pos = this.mainViewport.view.position.clone() this.mainViewport.view.translateWorld(-pos.x, -pos.y, -pos.z) this.scene.scene.position.sub(pos) console.log('变位置', pos) this.scene.pointclouds.forEach(cloud=>{ Alignment.setMatrix(cloud) }) },500) } } } } }) }*/ } lazyRenderViewports(updateCount){ //本来要写this.needRender = true的, 但在分四个屏时防止太卡而排队render let viewports = this.viewports if(viewports.length == 1 || this.needRender){ return this.needRender = true } const maxWaitTime = 200 let now = Date.now() let maxRenderCount = 1 viewports.forEach(e=>{ if(now - e.lastRenderTime > maxWaitTime){ e.needRender = true } }) let renderCount = viewports.filter(e=>e.needRender).length //console.log('renderCount', renderCount) let list = viewports.filter(e=>!e.needRender).sort((a,b)=>a.lastRenderTime - b.lastRenderTime)//没有准备render的,按上次渲染时间排序,作为候补 if(renderCount < maxRenderCount){ //还有名额,补齐 list.slice(0, maxRenderCount - renderCount).forEach(e=>{ e.needRender = true }) }else if(list[0] && now - list[0].lastRenderTime > maxWaitTime){//名额不足时,考虑候补队列第一个是否超时,超时的话也渲染 list[0].needRender = true } } ifPointBlockedByIntersect(point , panoId, soon ){//点是否被遮挡 let ifShelter let now = Date.now() let extraPanoId = panoId != void 0 if(!this.shelterCount)return let history = shelterHistory.find(e=>e.point.equals(point)) let cameraPos = this.mainViewport.view.position.clone() if(panoId == void 0){ if(this.images360.isAtPano(0.05)){ panoId = this.images360.currentPano.id } } if(history){ if(panoId != void 0){ ifShelter = history.panos[panoId]; }else{ if(history.notAtPano.cameraPos && history.notAtPano.cameraPos.equals(cameraPos)){ ifShelter = history.notAtPano.ifShelter } } let index = shelterHistory.indexOf(history) //先取出,稍后放回 shelterHistory.splice(index, 1) }else{//新增 history = {point, panos:{}, notAtPano:{}} const minCount = 100 if(shelterHistory.length > minCount){//去除最早的 let old while(old = shelterHistory[0], now - old.lastTime > 1000){//因为不知热点个数,所以需要加上时间限制,超过时间才能删。 if(old == history || shelterHistory.length == minCount)break; shelterHistory.splice(0,1) //console.log('delete') } } } if(ifShelter == void 0){ delete history.waitCompute if(this.mainViewport.view.isFlying()){ return useLastResult() } if(panoId != void 0){ let pano = this.images360.getPano(panoId) if((soon || this.shelterCount.byTex=0; i--){ let history = shelterHistory[i]; if(history.waitCompute){ if(history.waitCompute.panoId != void 0){ if(!history.waitCompute.forceGet && (history.waitCompute.panoId != this.images360.currentPano.id || !this.images360.isAtPano(0.1))){ delete history.waitCompute //取消计算 }else{ if(this.images360.currentPano.depthTex){ if(byTex >= maxTexCount)break byTex ++ let ifShelter = !!viewer.inputHandler.ifBlockedByIntersect({point:history.point, margin:Potree.config.shelterMargin, useDepthTex:true, viewport:this.mainViewport } ) history.panos[this.images360.currentPano.id] = ifShelter history.ifShelter = ifShelter delete history.waitCompute //console.log('补1', history.point.toArray()) }else{ if(this.images360.currentPano.pointcloud.hasDepthTex){ //先等待加载完深度图 }else{ waitCloud.push(history) } } } }else{ waitCloud.push(history) } } } let maxCloudCount if(byTex < maxTexCount && waitCloud.length ){ maxCloudCount = this.lastFrameChanged ? Common.getBestCount('shelterMaxCloud', 0, 2, 4, 8 /* ,true */ ) : 5; let waitCloud2 = [] if(maxCloudCount){ for(let i=0; ie.point) let result = Common.batchHandling.getSlice('shelterByCloud', list, {maxUseCount:maxCloudCount,useEquals:true, stopWhenAllUsed:true} ) //iphonex稳定后大概在7-10。 //list.length>0 && console.log('list',list, maxCloudCount) result.list.forEach(e=>{ let history = waitCloud2.find(a=>a.point.equals(e)) let ifShelter = !!viewer.inputHandler.ifBlockedByIntersect({point:history.point, margin: Potree.config.shelterMargin , pickWindowSize:3, viewport:this.mainViewport} ) if(history.waitCompute.cameraPos){ history.notAtPano = {cameraPos: history.waitCompute.cameraPos , ifShelter } }else{ history.panos[this.images360.currentPano.id] = ifShelter } history.ifShelter = ifShelter byCloud++ //console.log('补2', history.point.toArray()) delete history.waitCompute }) } } if(byTex || byCloud){ //console.log('shelterComputed',byTex,byCloud, maxTexCount, maxCloudCount) Common.intervalTool.isWaiting('shelterComputed', ()=>{ //console.log('shelterComputed update') this.dispatchEvent('shelterComputed') },340) } } updateDatasetAt(force){//更新所在数据集 let fun = ()=>{ let currPos = viewer.mainViewport.view.position //if(force || !currPos.equals(this.lastPos)){ //this.lastPos.copy(currPos) var at = this.scene.pointclouds.filter(e=> (e.visible || e.unvisibleReasons && e.unvisibleReasons.length == 1 && e.unvisibleReasons[0].reason == 'displayMode') && e.ifContainsPoint(currPos) ) if(Common.getDifferenceSet(at, this.atDatasets).length){ //console.log('atDatasets', at) this.atDatasets = at if(Potree.settings.editType != 'pano' && Potree.settings.editType != 'merge'){ this.updateFpVisiDatasets() if(!viewer.modules.SiteModel.currentFloor){ this.updatePanosVisibles() //所在数据集的点位显示 } } this.dispatchEvent({type:'pointcloudAtChange',pointclouds:at}) } force = false } //} if(force)fun() else Common.intervalTool.isWaiting('atWhichDataset', fun , 300) } updatePanosVisibles(currentFloor){//显示当前楼层的所有panos if(!Potree.settings.ifShowMarker)return let inEntity = currentFloor //console.error('updatePanosVisibles', currentFloor) viewer.images360.panos.forEach(pano=>{ let visible = inEntity ? inEntity.panos.includes(pano) : this.atDatasets.some(e=>e.panos.includes(pano)) Potree.Utils.updateVisible(pano, 'buildingChange', visible, 2) }) this.dispatchEvent('content_changed') } //注:非official的没有获取sitemodel的信息所以不执行楼层判断,marker显示不对是正常的 /* 2023.11.24 针对部分场景的楼层很矮,很容易就到楼层外了,且不能修改楼高。如SG-t-9NdCpxrUPLL#/,采取如下措施: 1 如果不在任何一楼层,但在某个数据集中,就显示该数据集所有的marker 2 如果当前楼层无,则显示上一次的currentFloor的marker (目前先使用方案1) */ updateMarkerVisibles(){//限制显示的marker个数,因镜头内marker多的时候可能会卡 if(!Potree.settings.ifShowMarker)return const minRadius = 8 * this.images360.zoomLevel, //当视线垂直于marker时的最小可见距离,此范围内可见的pano绝对可见 maxRadius = 50 * this.images360.zoomLevel, //当视线垂直于marker时的最大可见距离,此范围外绝对不可见 hopeCount = browser.isMobile() ? 8 : 15 //期望达到的真实可见的marker数 let sheltered = (pano)=>{ if(/* Potree.settings.displayMode == 'showPanos' && !Potree.Features.EXT_DEPTH.isSupported() && */this.images360.isAtPano() && !this.mainViewport.view.isFlying()){ return !this.images360.currentPano.neighbours.includes(pano) && this.images360.currentPano != pano //起初因不支持EXT_DEPTH时无法用depthTex遮住marker, 后为了减少绘制,都判断 } } let panoMap = new Map //先记录想要设置为可见的 let set = ()=>{//最后确定设置 let count = 0 viewer.images360.panos.forEach(pano=>{ let v = panoMap.get(pano).visible v && count++ Potree.Utils.updateVisible(pano.marker, 'limitMarkerShow', v ) }) //console.log('updateMarkerVisibles marker显示个数', count) this.dispatchEvent('content_changed') } let isWithinDis = (pano,maxDis)=>{//是否marker到相机的距离 没有超出可视距离。可视距离考虑上倾斜角,倾斜越大可视距离越短 let camPos = viewer.mainViewport.camera.position let o = panoMap.get(pano) o.dis = o.dis || camPos.distanceTo(pano.marker.position) o.sin = o.sin || Math.sqrt(Math.abs(camPos.z - pano.marker.position.z) / o.dis) //和地面夹角的sin。 按公式是不加Math.sqrt的,但是这样大马路上在贴近地面时算出的个数非常少,所以增大点…… return o.dis < maxDis * o.sin } viewer.images360.panos.forEach(pano=>{//minRadius内的记录为可见 let o = {} panoMap.set(pano, o) if(pano.visible && !sheltered(pano) && isWithinDis(pano, minRadius)){ o.visible = true; } }) //不超过hopeCount的话,可以直接确定设置 if(viewer.images360.panos.filter(pano=> panoMap.get(pano).visible ).length >= hopeCount)return set() //距离超过maxRadius就绝对不可见 let insideOutCirle = viewer.images360.panos.filter(pano=> pano.visible && !sheltered(pano) && isWithinDis(pano, maxRadius)) if(insideOutCirle.length <= hopeCount){ insideOutCirle.forEach(pano=>panoMap.get(pano).visible = true ) return set() } //数量超过hopeCount时,根据距离排序 insideOutCirle.sort((a,b)=>{return panoMap.get(a).dis - panoMap.get(b).dis }) let slice = insideOutCirle.slice(0,hopeCount) slice.forEach(pano=>panoMap.get(pano).visible = true ) set() } updateFpVisiDatasets(){ let Clip = this.modules.Clip let SiteModel = this.modules.SiteModel let Alignment = this.modules.Alignment var currentFloor = SiteModel.currentFloor; /* if(Clip.editing){//下载页面已经改为和普通时一样,根据位置判断 this.updateCadVisibles(Clip.selectedDatasets) }else */if(this.selectedFloorplan){//平面图设置中 或 地理设置 let pointclouds = [this.selectedFloorplan] this.updateCadVisibles(pointclouds) }else if(SiteModel.editing || Alignment.editing){//只显示勾选的,也就是显示的点云的 let pointclouds = this.scene.pointclouds.filter(p => Potree.Utils.getObjVisiByReason(p,'datasetSelection') ); this.updateCadVisibles(pointclouds) //this.updatePanosVisibles(currentFloor/* , pointclouds */) }else{ let pointclouds = currentFloor ? this.findPointcloudsAtFloor(currentFloor) : [] if(pointclouds.length == 0){ if(this.focusDatasets){ pointclouds = this.focusDatasets } } if(pointclouds.length == 0){//如果当前不在任何楼层或楼层中无数据集,就用当前所在数据集 pointclouds = this.atDatasets } this.updateCadVisibles(pointclouds) //this.updatePanosVisibles(currentFloor/* , pointclouds */) } } /* findPointcloudsAtFloor(entity){//找当前楼层需要显示哪些数据集。 //数据集的belongToEntity 在这个entity内(否则会出现点击数据集飞过去平面图却不显示)。or 如果数据集有漫游点的话,需要包含>20%的漫游点。 (防止重叠体积很大但其实一个漫游点都不包含) //重叠体积>50% 或 包含>50%的漫游点 const ratio1 = 0.2, ratio2 = 0.5, ratio3 = 0.95 var lowScores = [] var pointclouds = viewer.scene.pointclouds.filter(e=>{ let score = 0 if(e.belongToEntity && (e.belongToEntity == entity || e.belongToEntity.buildParent == entity)){//条件1 若该数据集挂载到该楼层 或 该数据集挂载到的房间属于该楼层(这样能显示该层所有房间) return true } if(e.panos.length){//条件2 var insidePanos = e.panos.filter(a=>entity.ifContainsPoint(a.position)); let panoCountRatio = insidePanos.length / e.panos.length if(panoCountRatio > ratio2)return true score += panoCountRatio } //条件3 let volume = entity.intersectPointcloudVolume(e); let volumeRatio = volume / entity.getVolume(true) //注:hole加入计算 if(volumeRatio > ratio3){ //ratio3要高一些,因为点云bounding可能很大,包含很多无点云的空间。即使整个数据集包含entity都不一定看起来在数据集中。(千万要防止两层楼都显示了) return true }else{ score += volumeRatio } lowScores.push({score, pointcloud:e}) }) if(pointclouds.length == 0){//从低分项挑一个出来。 lowScores.sort((a,b)=>{return a.score - b.score}) if(lowScores[0].score > 0.4){ pointclouds = [lowScores[0].pointcloud] } } return pointclouds } */ findPointcloudsAtFloor(entity){//找当前楼层需要显示哪些数据集。 //数据集的belongToEntity 在这个entity内(否则会出现点击数据集飞过去平面图却不显示)。or 如果数据集有漫游点的话,需要包含>20%的漫游点。 (防止重叠体积很大但其实一个漫游点都不包含) //重叠体积>50% 或 包含>50%的漫游点 const ratio1 = 0.2, ratio2 = 0.5, ratio3 = 0.95 var lowScores = [] var pointclouds = viewer.scene.pointclouds.filter(e=>{ let score = 0 if(e.belongToEntity && (e.belongToEntity == entity || e.belongToEntity.buildParent == entity)){//条件1 若该数据集挂载到该楼层 或 该数据集挂载到的房间属于该楼层(这样能显示该层所有房间) return true } if(e.panos.length){//条件2 var insidePanos = e.panos.filter(a=>entity.ifContainsPoint(a.position)); let panoCountRatio = insidePanos.length / e.panos.length if(panoCountRatio > ratio2)return true score += panoCountRatio * 2 } //条件3 let coverHeightRatio = entity.coverPointcloudHeight(e,true)//重叠高度占楼层高度的比率。 if(coverHeightRatio < 0.2) return let {toEntity,toPointcloud} = entity.intersectPointcloudArea(e,true); //重叠面积占比 let coverAreaRatio = Math.max(toEntity,toPointcloud);//占数据集俯视面积的比率 和 占楼层的面积比率,挑一个大的。 (有的数据集被划分多个楼层,所以每层都应该显示该数据集的图。还有的数据集比较大,可能包含多个建筑,则看占楼层面积) if(coverAreaRatio < 0.2) return score += coverAreaRatio * coverHeightRatio lowScores.push({score, pointcloud:e}) }) /* if(pointclouds.length == 0){//从低分项挑一个出来。 lowScores.sort((a,b)=>{return a.score - b.score}) if(lowScores[0].score > 0.4){ pointclouds = [lowScores[0].pointcloud] } } */ lowScores.forEach(e=>{//一个楼层里可以包含许多个数据集 调试SG-t-ds27ym7xzjJ if(e.score > 0.5){ pointclouds.push(e.pointcloud) } }) return pointclouds } updateCadVisibles(visiClouds, force){ //console.log('visiClouds',visiClouds) let oldVisi = this.fpVisiDatasets var visiClouds = this.fpVisiDatasets = visiClouds if(!force){ var difference = Common.getDifferenceSet(oldVisi , visiClouds) if(difference.length == 0)return } //console.log('visiClouds',visiClouds.map(e=>e.name)) viewer.scene.pointclouds.forEach(pointcloud=>{ var floorplan = viewer.mapViewer.mapLayer.getFloorplan(pointcloud.dataset_id) var visi = visiClouds.includes(pointcloud) if(floorplan){ Potree.Utils.updateVisible(floorplan.objectGroup, 'buildingChange', visi) }/* else if(!visi){ let changeVisi = (e)=>{ Potree.Utils.updateVisible(e.floorplan.objectGroup, 'buildingChange', this.fpVisiDatasets.includes(pointcloud)) viewer.mapViewer.mapLayer.removeEventListener('floorplanLoaded', changeVisi) console.log('updateCadVisibles加载后更改显示',e) } viewer.mapViewer.mapLayer.addEventListener('floorplanLoaded', changeVisi) } */ //已经添加了全局的 floorplanLoaded后会updateCadVisibles,这段就删了 }) viewer.mapViewer.mapLayer.needUpdate = true //可能需要更新加载的level程度 viewer.mapViewer.needRender = true //若上句不触发加载也要立即重新绘制 } //促使点云加载出最高级别 testPointcloudsMaxLevel(){ //所有点云都无需testMaxNodeLevel 就停止 let camera_changed, count = 0 let test = (e={})=>{ camera_changed = true let camera = e.camera || this.scene.getActiveCamera() Common.intervalTool.isWaiting('testPointcloudsMaxLevel', ()=>{ if(!camera_changed && count>50 || Potree.settings.displayMode == 'showPanos' || this.pauseTestMaxLevel )return //只有当camera_changed后才继续循环, 除了最开始几次需要连续加载下 camera_changed = false count ++; //console.log('testPointcloudsMaxLevel中', camera.type /* count */) let oldCount = this.testMaxNodeCount var success = true viewer.scene.pointclouds.forEach(e=>{ var wait = e.testMaxNodeLevel(camera) if(wait){ success = false; } }) /* if(oldCount<=Potree.config.testNodeCount1 && this.testMaxNodeCount>Potree.config.testNodeCount1 ){//差不多等当前所在数据集nodeMaxLevel加载出来 viewer.scene.pointclouds.forEach(e=>{e.changePointSize()}) //重新更新一下大小。因之前用的是nodeMaxLevelPredict (防止刚开始因nodeMaxLevel没涨完,导致过大的点云突然出现 } */ if(!success)return true //没有全部加载完,继续循环 else { this.removeEventListener('camera_changed',test) console.log('testPointcloudsMaxLevel结束') } }, count<10 ? 250 : 500) } this.addEventListener('camera_changed',test) test() /* 检验: viewer.scene.pointclouds.sort((a,b)=>a.nodeMaxLevelPredict.min - b.nodeMaxLevelPredict.min).forEach(e=>console.log(e.nodeMaxLevel, e.nodeMaxLevelPredict.min)) */ } setPointLevels(){ this.scene.pointclouds.forEach(e=>{ e.setPointLevel() }) } onCrash(error){ console.error(error) $(this.renderArea).empty(); if ($(this.renderArea).find('#potree_failpage').length === 0) { let elFailPage = $(`

Potree Encountered An Error

This may happen if your browser or graphics card is not supported.
We recommend to use Chrome or Firefox.

Please also visit webglreport.com and check whether your system supports WebGL.

If you are already using one of the recommended browsers and WebGL is enabled, consider filing an issue report at github,
including your operating system, graphics card, browser and browser version, as well as the error message below.
Please do not report errors on unsupported browsers.


				
			
`); let elErrorMessage = elFailPage.find('#potree_error_console'); elErrorMessage.html(error.stack); $(this.renderArea).append(elFailPage); } throw error; } // ------------------------------------------------------------------------------------ // Viewer API // ------------------------------------------------------------------------------------ setScene (scene) { if (scene === this.scene) { return; } let oldScene = this.scene; this.scene = scene; this.dispatchEvent({ type: 'scene_changed', oldScene: oldScene, scene: scene }); { // Annotations $('.annotation').detach(); // for(let annotation of this.scene.annotations){ // this.renderArea.appendChild(annotation.domElement[0]); // } this.scene.annotations.traverse(annotation => { this.renderArea.appendChild(annotation.domElement[0]); }); if (!this.onAnnotationAdded) { this.onAnnotationAdded = e => { // console.log("annotation added: " + e.annotation.title); e.annotation.traverse(node => { $("#potree_annotation_container").append(node.domElement); //this.renderArea.appendChild(node.domElement[0]); node.scene = this.scene; }); }; } if (oldScene) { oldScene.annotations.removeEventListener('annotation_added', this.onAnnotationAdded); } this.scene.annotations.addEventListener('annotation_added', this.onAnnotationAdded); } }; setControls(controls/* , setSpeed */){ if (controls !== this.controls) { if (this.controls) { this.controls.setEnable(false) //this.inputHandler.removeInputListener(this.controls); this.controls.moveSpeed = this.moveSpeed; //记录 (因为orbit的radius很大,转为firstPerson时要缩小) } this.controls = controls; controls.moveSpeed && this.setMoveSpeed(controls.moveSpeed) //add this.controls.setEnable(true) //this.inputHandler.addInputListener(this.controls); } } getControls () { if(this.renderer.xr.isPresenting){ return this.vrControls; }else{ return this.controls; } } getMinNodeSize () { return this.minNodeSize; }; setMinNodeSize (value) { if (this.minNodeSize !== value) { this.minNodeSize = value; this.dispatchEvent({'type': 'minnodesize_changed', 'viewer': this}); } }; getBackground () { return this.background; } setBackground(bg, src){ /* if (this.background === bg ) { return; } */ if(bg === "skybox"){ if(!src)src = Potree.resourcePath+'/textures/skybox/xingkong.jpg' this.skybox = Utils.loadSkybox(src, this.skybox); } this.background = bg; this.backgroundOpacity = 1//add this.dispatchEvent({'type': 'background_changed', 'viewer': this}); } setDescription (value) { this.description = value; $('#potree_description').html(value); //$('#potree_description').text(value); } getDescription(){ return this.description; } setShowBoundingBox (value) { if (this.showBoundingBox !== value) { this.showBoundingBox = value; this.dispatchEvent({'type': 'show_boundingbox_changed', 'viewer': this}); } }; getShowBoundingBox () { return this.showBoundingBox; }; setMoveSpeed (value) { if (this.getMoveSpeed() !== value) { this.mainViewport.setMoveSpeed(value) this.dispatchEvent({'type': 'move_speed_changed', 'viewer': this, 'speed': value}); } }; getMoveSpeed () { return this.mainViewport.moveSpeed; }; setWeightClassification (w) { for (let i = 0; i < this.scene.pointclouds.length; i++) { this.scene.pointclouds[i].material.weightClassification = w; this.dispatchEvent({'type': 'attribute_weights_changed' + i, 'viewer': this}); } }; setFreeze (value) { value = Boolean(value); if (this.freeze !== value) { this.freeze = value; this.dispatchEvent({'type': 'freeze_changed', 'viewer': this}); } }; getFreeze () { return this.freeze; }; setElevationGradientRepeat(value){ if(this.elevationGradientRepeat !== value){ this.elevationGradientRepeat = value; this.dispatchEvent({ type: "elevation_gradient_repeat_changed", viewer: this}); } } setPointBudget (value) { //pointBudget: 每次刷新显示点数量的最大值。 缓存中的点数量也跟此有关,但大于这个数值。 if (Potree.pointBudget !== value && value) { Potree.pointBudget = parseInt(value); this.dispatchEvent({'type': 'point_budget_changed', 'viewer': this}); } }; getPointBudget () { return Potree.pointBudget; }; setShowAnnotations (value) { if (this.showAnnotations !== value) { this.showAnnotations = value; this.dispatchEvent({'type': 'show_annotations_changed', 'viewer': this}); } } getShowAnnotations () { return this.showAnnotations; } setDEMCollisionsEnabled(value){ if(this.useDEMCollisions !== value){ this.useDEMCollisions = value; this.dispatchEvent({'type': 'use_demcollisions_changed', 'viewer': this}); }; }; getDEMCollisionsEnabled () { return this.useDEMCollisions; }; setEDLEnabled (value) { value = Boolean(value) && Features.SHADER_EDL.isSupported(); if (this.useEDL !== value) { this.useEDL = value; this.dispatchEvent({'type': 'use_edl_changed', 'viewer': this}); this.dispatchEvent('pointcloud_changed') } }; getEDLEnabled () { return this.useEDL; }; setEDLRadius (value) { if (this.edlRadius !== value) { this.edlRadius = value; this.dispatchEvent({'type': 'edl_radius_changed', 'viewer': this}); this.dispatchEvent('pointcloud_changed') } }; getEDLRadius () { return this.edlRadius; }; setEDLStrength (value) { if (this.edlStrength !== value) { this.edlStrength = value; this.dispatchEvent({'type': 'edl_strength_changed', 'viewer': this}); this.dispatchEvent('pointcloud_changed') } }; getEDLStrength () { return this.edlStrength; }; setEDLOpacity (value) { if (this.edlOpacity !== value) { this.edlOpacity = value; this.dispatchEvent({'type': 'edl_opacity_changed', 'viewer': this}); this.dispatchEvent('pointcloud_changed') } }; getEDLOpacity () { return this.edlOpacity; }; setFOV (value) { if (this.fov !== value) { let oldFov = this.fov this.fov = value; this.scene.cameraP.fov = this.fov; //add this.scene.cameraP.updateProjectionMatrix() //add this.dispatchEvent({'type': 'fov_changed', 'viewer': this, oldFov, fov:this.fov}); } }; getFOV () { return this.fov; }; disableAnnotations () { this.scene.annotations.traverse(annotation => { annotation.domElement.css('pointer-events', 'none'); // return annotation.visible; }); }; enableAnnotations () { this.scene.annotations.traverse(annotation => { annotation.domElement.css('pointer-events', 'auto'); // return annotation.visible; }); } setClassifications(classifications){ this.classifications = classifications; this.dispatchEvent({'type': 'classifications_changed', 'viewer': this}); } setClassificationVisibility (key, value) { if (!this.classifications[key]) { this.classifications[key] = {visible: value, name: 'no name'}; this.dispatchEvent({'type': 'classification_visibility_changed', 'viewer': this}); } else if (this.classifications[key].visible !== value) { this.classifications[key].visible = value; this.dispatchEvent({'type': 'classification_visibility_changed', 'viewer': this}); } } toggleAllClassificationsVisibility(){ let numVisible = 0; let numItems = 0; for(const key of Object.keys(this.classifications)){ if(this.classifications[key].visible){ numVisible++; } numItems++; } let visible = true; if(numVisible === numItems){ visible = false; } let somethingChanged = false; for(const key of Object.keys(this.classifications)){ if(this.classifications[key].visible !== visible){ this.classifications[key].visible = visible; somethingChanged = true; } } if(somethingChanged){ this.dispatchEvent({'type': 'classification_visibility_changed', 'viewer': this}); } } setFilterReturnNumberRange(from, to){ this.filterReturnNumberRange = [from, to]; this.dispatchEvent({'type': 'filter_return_number_range_changed', 'viewer': this}); } setFilterNumberOfReturnsRange(from, to){ this.filterNumberOfReturnsRange = [from, to]; this.dispatchEvent({'type': 'filter_number_of_returns_range_changed', 'viewer': this}); } setFilterGPSTimeRange(from, to){ this.filterGPSTimeRange = [from, to]; this.dispatchEvent({'type': 'filter_gps_time_range_changed', 'viewer': this}); } setFilterPointSourceIDRange(from, to){ this.filterPointSourceIDRange = [from, to] this.dispatchEvent({'type': 'filter_point_source_id_range_changed', 'viewer': this}); } setLengthUnit (value) { switch (value) { case 'm': this.lengthUnit = LengthUnits.METER; this.lengthUnitDisplay = LengthUnits.METER; break; case 'ft': this.lengthUnit = LengthUnits.FEET; this.lengthUnitDisplay = LengthUnits.FEET; break; case 'in': this.lengthUnit = LengthUnits.INCH; this.lengthUnitDisplay = LengthUnits.INCH; break; } this.dispatchEvent({ 'type': 'length_unit_changed', 'viewer': this, value: value}); }; setLengthUnitAndDisplayUnit(lengthUnitValue, lengthUnitDisplayValue) { switch (lengthUnitValue) { case 'm': this.lengthUnit = LengthUnits.METER; break; case 'ft': this.lengthUnit = LengthUnits.FEET; break; case 'in': this.lengthUnit = LengthUnits.INCH; break; } switch (lengthUnitDisplayValue) { case 'm': this.lengthUnitDisplay = LengthUnits.METER; break; case 'ft': this.lengthUnitDisplay = LengthUnits.FEET; break; case 'in': this.lengthUnitDisplay = LengthUnits.INCH; break; } this.dispatchEvent({ 'type': 'length_unit_changed', 'viewer': this, value: lengthUnitValue }); }; zoomTo(node, factor, animationDuration = 0){ let view = this.mainViewport.view; let camera = this.scene.cameraP.clone(); camera.rotation.copy(this.scene.cameraP.rotation); camera.rotation.order = "ZXY"; camera.rotation.x = Math.PI / 2 + view.pitch; camera.rotation.z = view.yaw; camera.updateMatrix(); camera.updateMatrixWorld(); camera.zoomTo(node, factor); let bs; if (node.boundingSphere) { bs = node.boundingSphere; } else if (node.geometry && node.geometry.boundingSphere) { bs = node.geometry.boundingSphere; } else { bs = node.boundingBox.getBoundingSphere(new THREE.Sphere()); } bs = bs.clone().applyMatrix4(node.matrixWorld); let startPosition = view.position.clone(); let endPosition = camera.position.clone(); let startTarget = view.getPivot(); let endTarget = bs.center; let startRadius = view.radius; let endRadius = endPosition.distanceTo(endTarget); let easing = TWEEN.Easing.Quartic.Out; { // animate camera position let pos = startPosition.clone(); let tween = new TWEEN.Tween(pos).to(endPosition, animationDuration); tween.easing(easing); tween.onUpdate(() => { view.position.copy(pos); }); tween.start(); } { // animate camera target let target = startTarget.clone(); let tween = new TWEEN.Tween(target).to(endTarget, animationDuration); tween.easing(easing); tween.onUpdate(() => { view.lookAt(target); }); tween.onComplete(() => { view.lookAt(target); this.dispatchEvent({type: 'focusing_finished', target: this}); }); this.dispatchEvent({type: 'focusing_started', target: this}); tween.start(); } }; moveToGpsTimeVicinity(time){ const result = Potree.Utils.findClosestGpsTime(time, viewer); const box = result.node.pointcloud.deepestNodeAt(result.position).getBoundingBox(); const diameter = box.min.distanceTo(box.max); const camera = this.scene.getActiveCamera(); const offset = camera.getWorldDirection(new THREE.Vector3()).multiplyScalar(diameter); const newCamPos = result.position.clone().sub(offset); this.scene.view.position.copy(newCamPos); this.scene.view.lookAt(result.position); } showAbout () { $(function () { $('#about-panel').dialog(); }); }; getGpsTimeExtent(){ const range = [Infinity, -Infinity]; for(const pointcloud of this.scene.pointclouds){ const attributes = pointcloud.pcoGeometry.pointAttributes.attributes; const aGpsTime = attributes.find(a => a.name === "gps-time"); if(aGpsTime){ range[0] = Math.min(range[0], aGpsTime.range[0]); range[1] = Math.max(range[1], aGpsTime.range[1]); } } return range; } fitToScreen (factor = 1, animationDuration = 0) { let box = this.getBoundingBox(this.scene.pointclouds); let node = new THREE.Object3D(); node.boundingBox = box; this.zoomTo(node, factor, animationDuration); this.controls.stop(); }; toggleNavigationCube() { this.navigationCube.visible = !this.navigationCube.visible; /* this.viewports.push({ name:'navigationCube', left:0, bottom:0, width:0.2, }) */ } /* setView(pos, view) { if(!pos) return; switch(pos) { case "F": this.setFrontView(view); break; case "B": this.setBackView(view); break; case "L": this.setLeftView(view); break; case "R": this.setRightView(view); break; case "U": this.setTopView(view); break; case "D": this.setBottomView(view); break; } } */ setTopView(view){ view = view || this.scene.view view.setCubeView("top") this.fitToScreen(); }; setBottomView(){ this.scene.view.yaw = -Math.PI; this.scene.view.pitch = Math.PI / 2; this.fitToScreen(); }; setFrontView(view){ view = view || this.scene.view view.yaw = 0; view.pitch = 0; this.fitToScreen(); }; setBackView(view){ view = view || this.scene.view view.yaw = Math.PI; view.pitch = 0; this.fitToScreen(); }; setLeftView(){ this.scene.view.yaw = -Math.PI / 2; this.scene.view.pitch = 0; this.fitToScreen(); }; setRightView () { this.scene.view.yaw = Math.PI / 2; this.scene.view.pitch = 0; this.fitToScreen(); }; flipYZ () { this.isFlipYZ = !this.isFlipYZ; // TODO flipyz console.log('TODO'); } setCameraMode(mode){ this.scene.cameraMode = mode; viewer.mainViewport.camera = mode == CameraMode.PERSPECTIVE ? this.scene.cameraP : this.scene.cameraO; //改 for(let pointcloud of this.scene.pointclouds) { pointcloud.material.useOrthographicCamera = mode == CameraMode.ORTHOGRAPHIC; } } getProjection(){ const pointcloud = this.scene.pointclouds[0]; if(pointcloud){ return pointcloud.projection; }else{ return null; } } async loadProject(url,done){ const response = await fetch(url); if(response.ok){ const text = await response.text(); const json = JSON5.parse(text); // const json = JSON.parse(text); if(json.type === "Potree"){ Potree.loadProject(viewer, json, done); } }else{ console.warn("未能加载:"+url ) } } saveProject(){ return Potree.saveProject(this); } loadSettingsFromURL(){ if(Utils.getParameterByName("pointSize")){ this.setPointSize(parseFloat(Utils.getParameterByName("pointSize"))); } if(Utils.getParameterByName("FOV")){ this.setFOV(parseFloat(Utils.getParameterByName("FOV"))); } if(Utils.getParameterByName("opacity")){ this.setOpacity(parseFloat(Utils.getParameterByName("opacity"))); } if(Utils.getParameterByName("edlEnabled")){ let enabled = Utils.getParameterByName("edlEnabled") === "true"; this.setEDLEnabled(enabled); } if (Utils.getParameterByName('edlRadius')) { this.setEDLRadius(parseFloat(Utils.getParameterByName('edlRadius'))); } if (Utils.getParameterByName('edlStrength')) { this.setEDLStrength(parseFloat(Utils.getParameterByName('edlStrength'))); } if (Utils.getParameterByName('pointBudget')) { this.setPointBudget(parseFloat(Utils.getParameterByName('pointBudget'))); } if (Utils.getParameterByName('showBoundingBox')) { let enabled = Utils.getParameterByName('showBoundingBox') === 'true'; if (enabled) { this.setShowBoundingBox(true); } else { this.setShowBoundingBox(false); } } if (Utils.getParameterByName('material')) { let material = Utils.getParameterByName('material'); this.setMaterial(material); } if (Utils.getParameterByName('pointSizing')) { let sizing = Utils.getParameterByName('pointSizing'); this.setPointSizing(sizing); } if (Utils.getParameterByName('quality')) { let quality = Utils.getParameterByName('quality'); this.setQuality(quality); } if (Utils.getParameterByName('position')) { let value = Utils.getParameterByName('position'); value = value.replace('[', '').replace(']', ''); let tokens = value.split(';'); let x = parseFloat(tokens[0]); let y = parseFloat(tokens[1]); let z = parseFloat(tokens[2]); this.scene.view.position.set(x, y, z); } if (Utils.getParameterByName('target')) { let value = Utils.getParameterByName('target'); value = value.replace('[', '').replace(']', ''); let tokens = value.split(';'); let x = parseFloat(tokens[0]); let y = parseFloat(tokens[1]); let z = parseFloat(tokens[2]); this.scene.view.lookAt(new THREE.Vector3(x, y, z)); } if (Utils.getParameterByName('background')) { let value = Utils.getParameterByName('background'); this.setBackground(value); } // if(Utils.getParameterByName("elevationRange")){ // let value = Utils.getParameterByName("elevationRange"); // value = value.replace("[", "").replace("]", ""); // let tokens = value.split(";"); // let x = parseFloat(tokens[0]); // let y = parseFloat(tokens[1]); // // this.setElevationRange(x, y); // //this.scene.view.target.set(x, y, z); // } }; // ------------------------------------------------------------------------------------ // Viewer Internals // ------------------------------------------------------------------------------------ createControls () { { // create FIRST PERSON CONTROLS this.fpControls = new FirstPersonControls(this, this.mainViewport); this.fpControls.enabled = false; this.fpControls.addEventListener('start', this.disableAnnotations.bind(this)); this.fpControls.addEventListener('end', this.enableAnnotations.bind(this)); /* this.addEventListener("loadPointCloudDone", ()=>{ let boundPlane = new THREE.Box3() boundPlane.expandByPoint(this.bound.boundingBox.min.clone())//最低高度为bound的最低 boundPlane.expandByPoint(this.bound.boundingBox.max.clone().setZ(this.bound.center.z))//最高高度为bound的中心高度 FirstPersonControls.boundPlane = boundPlane FirstPersonControls.standardSpeed = THREE.Math.clamp( Math.sqrt(this.bound.boundSize.length() )/ 100 , 0.02,0.5); //在这个boundPlane中的速度 }) */ } // { // create GEO CONTROLS // this.geoControls = new GeoControls(this.scene.camera, this.renderer.domElement); // this.geoControls.enabled = false; // this.geoControls.addEventListener("start", this.disableAnnotations.bind(this)); // this.geoControls.addEventListener("end", this.enableAnnotations.bind(this)); // this.geoControls.addEventListener("move_speed_changed", (event) => { // this.setMoveSpeed(this.geoControls.moveSpeed); // }); // } { // create ORBIT CONTROLS this.orbitControls = new OrbitControls(this); this.orbitControls.enabled = false; this.orbitControls.addEventListener('start', this.disableAnnotations.bind(this)); this.orbitControls.addEventListener('end', this.enableAnnotations.bind(this)); } /* { // create EARTH CONTROLS this.earthControls = new EarthControls(this); this.earthControls.enabled = false; this.earthControls.addEventListener('start', this.disableAnnotations.bind(this)); this.earthControls.addEventListener('end', this.enableAnnotations.bind(this)); } { // create DEVICE ORIENTATION CONTROLS this.deviceControls = new DeviceOrientationControls(this); this.deviceControls.enabled = false; this.deviceControls.addEventListener('start', this.disableAnnotations.bind(this)); this.deviceControls.addEventListener('end', this.enableAnnotations.bind(this)); } */ /* { // create VR CONTROLS this.vrControls = new VRControls(this); this.vrControls.enabled = false; this.vrControls.addEventListener('start', this.disableAnnotations.bind(this)); this.vrControls.addEventListener('end', this.enableAnnotations.bind(this)); } */ }; toggleSidebar () { let renderArea = $('#potree_render_area'); let isVisible = renderArea.css('left') !== '0px'; if (isVisible) { renderArea.css('left', '0px'); } else { renderArea.css('left', '300px'); } }; toggleMap () { // let map = $('#potree_map'); // map.toggle(100); if (this.mapView) { this.mapView.toggle(); } }; onGUILoaded(callback){ if(this.guiLoaded){ callback(); }else{ this.guiLoadTasks.push(callback); } } promiseGuiLoaded(){ return new Promise( resolve => { if(this.guiLoaded){ resolve(); }else{ this.guiLoadTasks.push(resolve); } }); } loadGUI(callback){ if(callback){ this.onGUILoaded(callback); } let viewer = this; let sidebarContainer = $('#potree_sidebar_container'); sidebarContainer.load(new URL(Potree.scriptPath + '/' + (Potree.settings.sidebar || 'sidebar1.html')).href, () => { sidebarContainer.css('width', '300px'); sidebarContainer.css('height', '100%'); let imgMenuToggle = document.createElement('img'); imgMenuToggle.src = new URL(Potree.resourcePath + '/icons/menu_button.svg').href; imgMenuToggle.onclick = this.toggleSidebar; imgMenuToggle.classList.add('potree_menu_toggle'); let imgMapToggle = document.createElement('img'); imgMapToggle.src = new URL(Potree.resourcePath + '/icons/map_icon.png').href; imgMapToggle.style.display = 'none'; imgMapToggle.onclick = e => { this.toggleMap(); }; imgMapToggle.id = 'potree_map_toggle'; let elButtons = $("#potree_quick_buttons").get(0); elButtons.append(imgMenuToggle); elButtons.append(imgMapToggle); VRButton.createButton(this.renderer).then(vrButton => { if(vrButton == null){ console.log("VR not supported or active."); return; } this.renderer.xr.enabled = true; let element = vrButton.element; element.style.position = ""; element.style.bottom = ""; element.style.left = ""; element.style.margin = "4px"; element.style.fontSize = "100%"; element.style.width = "2.5em"; element.style.height = "2.5em"; element.style.padding = "0"; element.style.textShadow = "black 2px 2px 2px"; element.style.display = "block"; elButtons.append(element); vrButton.onStart(() => { this.dispatchEvent({type: "vr_start"}); }); vrButton.onEnd(() => { this.dispatchEvent({type: "vr_end"}); }); }); /* this.mapView = new MapView(this); this.mapView.init(); */ i18n.init({ lng: 'en', resGetPath: Potree.resourcePath + '/lang/__lng__/__ns__.json', preload: ['en', 'fr', 'de', 'jp', 'se', 'es', 'zh'], getAsync: true, debug: false }, function (t) { // Start translation once everything is loaded $('body').i18n(); }); $(() => { //initSidebar(this); let sidebar = new Sidebar(this); sidebar.init(); this.sidebar = sidebar; //if (callback) { // $(callback); //} let elProfile = $('
').load(new URL(Potree.scriptPath + '/profile.html').href, () => { $(document.body).append(elProfile.children()); this.profileWindow = new ProfileWindow(this); this.profileWindowController = new ProfileWindowController(this); $('#profile_window').draggable({ handle: $('#profile_titlebar'), containment: $(document.body) }); $('#profile_window').resizable({ containment: $(document.body), handles: 'n, e, s, w' }); $(() => { this.guiLoaded = true; for(let task of this.guiLoadTasks){ task(); } }); }); }); }); return this.promiseGuiLoaded(); } setLanguage (lang) { i18n.setLng(lang); $('body').i18n(); } setServer (server) { this.server = server; } initDragAndDrop(){ function allowDrag(e) { e.dataTransfer.dropEffect = 'copy'; e.preventDefault(); } let dropHandler = async (event) => { console.log(event); event.preventDefault(); for(const item of event.dataTransfer.items){ console.log(item); if(item.kind !== "file"){ continue; } const file = item.getAsFile(); const isJson = file.name.toLowerCase().endsWith(".json"); const isGeoPackage = file.name.toLowerCase().endsWith(".gpkg"); if(isJson){ try{ const text = await file.text(); const json = JSON.parse(text); if(json.type === "Potree"){ Potree.loadProject(viewer, json); } }catch(e){ console.error("failed to parse the dropped file as JSON"); console.error(e); } }else if(isGeoPackage){ const hasPointcloud = viewer.scene.pointclouds.length > 0; if(!hasPointcloud){ let msg = "At least one point cloud is needed that specifies the "; msg += "coordinate reference system before loading vector data."; console.error(msg); }else{ proj4.defs("WGS84", "+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs"); proj4.defs("pointcloud", this.getProjection()); let transform = proj4("WGS84", "pointcloud"); const buffer = await file.arrayBuffer(); const params = { transform: transform, source: file.name, }; const geo = await Potree.GeoPackageLoader.loadBuffer(buffer, params); viewer.scene.addGeopackage(geo); } } } }; $("body")[0].addEventListener("dragenter", allowDrag); $("body")[0].addEventListener("dragover", allowDrag); $("body")[0].addEventListener("drop", dropHandler); } updateAnnotations () { if(!this.visibleAnnotations){ this.visibleAnnotations = new Set(); } this.scene.annotations.updateBounds(); this.scene.cameraP.updateMatrixWorld(); this.scene.cameraO.updateMatrixWorld(); let distances = []; let renderAreaSize = this.renderer.getSize(new THREE.Vector2()); let viewer = this; let visibleNow = []; this.scene.annotations.traverse(annotation => { if (annotation === this.scene.annotations) { return true; } if (!annotation.visible) { return false; } annotation.scene = this.scene; let element = annotation.domElement; let position = annotation.position.clone(); position.add(annotation.offset); if (!position) { position = annotation.boundingBox.getCenter(new THREE.Vector3()); } let distance = viewer.scene.cameraP.position.distanceTo(position); let radius = annotation.boundingBox.getBoundingSphere(new THREE.Sphere()).radius; let screenPos = new THREE.Vector3(); let screenSize = 0; { // SCREEN POS screenPos.copy(position).project(this.scene.getActiveCamera()); screenPos.x = renderAreaSize.x * (screenPos.x + 1) / 2; screenPos.y = renderAreaSize.y * (1 - (screenPos.y + 1) / 2); // SCREEN SIZE if(viewer.scene.cameraMode == CameraMode.PERSPECTIVE) { let fov = Math.PI * viewer.scene.cameraP.fov / 180; let slope = Math.tan(fov / 2.0); let projFactor = 0.5 * renderAreaSize.y / (slope * distance); screenSize = radius * projFactor; } else { screenSize = Utils.projectedRadiusOrtho(radius, viewer.scene.cameraO.projectionMatrix, renderAreaSize.x, renderAreaSize.y); } } element.css("left", screenPos.x + "px"); element.css("top", screenPos.y + "px"); //element.css("display", "block"); let zIndex = 10000000 - distance * (10000000 / this.scene.cameraP.far); if(annotation.descriptionVisible){ zIndex += 10000000; } element.css("z-index", parseInt(zIndex)); if(annotation.children.length > 0){ let expand = screenSize > annotation.collapseThreshold || annotation.boundingBox.containsPoint(this.scene.getActiveCamera().position); annotation.expand = expand; if (!expand) { //annotation.display = (screenPos.z >= -1 && screenPos.z <= 1); let inFrustum = (screenPos.z >= -1 && screenPos.z <= 1); if(inFrustum){ visibleNow.push(annotation); } } return expand; } else { //annotation.display = (screenPos.z >= -1 && screenPos.z <= 1); let inFrustum = (screenPos.z >= -1 && screenPos.z <= 1); if(inFrustum){ visibleNow.push(annotation); } } }); let notVisibleAnymore = new Set(this.visibleAnnotations); for(let annotation of visibleNow){ annotation.display = true; notVisibleAnymore.delete(annotation); } this.visibleAnnotations = visibleNow; for(let annotation of notVisibleAnymore){ annotation.display = false; } } updateMaterialDefaults(pointcloud){ // PROBLEM STATEMENT: // * [min, max] of intensity, source id, etc. are computed as point clouds are loaded // * the point cloud material won't know the range it should use until some data is loaded // * users can modify the range at runtime, but sensible default ranges should be // applied even if no GUI is present // * display ranges shouldn't suddenly change even if the actual range changes over time. // e.g. the root node has intensity range [1, 478]. One of the descendants increases range to // [0, 2047]. We should not automatically change to the new range because that would result // in sudden and drastic changes of brightness. We should adjust the min/max of the sidebar slider. const material = pointcloud.material; const attIntensity = pointcloud.getAttribute("intensity"); if(attIntensity != null && material.intensityRange[0] === Infinity){ material.intensityRange = [...attIntensity.range]; } // const attIntensity = pointcloud.getAttribute("intensity"); // if(attIntensity && material.intensityRange[0] === Infinity){ // material.intensityRange = [...attIntensity.range]; // } // let attributes = pointcloud.getAttributes(); // for(let attribute of attributes.attributes){ // if(attribute.range){ // let range = [...attribute.range]; // material.computedRange.set(attribute.name, range); // //material.setRange(attribute.name, range); // } // } } update(delta, timestamp){ viewer.addTimeMark('update','start') TWEEN.update(timestamp); transitions.update(delta);//写在开头,因为这时候最为固定,计时准确 this.dispatchEvent({ type: 'update_start', delta: delta, timestamp: timestamp}); this.updateScreenSize() //判断是否改变canvas大小 const scene = this.scene; const camera = scene.getActiveCamera(); const visiblePointClouds = this.scene.pointclouds.filter(pc => pc.visible) Potree.pointLoadLimit = Potree.pointBudget * 2; /* const lTarget = camera.position.clone().add(camera.getWorldDirection(new THREE.Vector3()).multiplyScalar(1000)); this.scene.directionalLight.position.copy(camera.position); this.scene.directionalLight.lookAt(lTarget); */ for (let pointcloud of visiblePointClouds) { pointcloud.showBoundingBox = this.showBoundingBox; pointcloud.generateDEM = this.generateDEM; pointcloud.minimumNodePixelSize = this.minNodeSize; let material = pointcloud.material; material.uniforms.uFilterReturnNumberRange.value = this.filterReturnNumberRange; material.uniforms.uFilterNumberOfReturnsRange.value = this.filterNumberOfReturnsRange; material.uniforms.uFilterGPSTimeClipRange.value = this.filterGPSTimeRange; material.uniforms.uFilterPointSourceIDClipRange.value = this.filterPointSourceIDRange; material.classification = this.classifications; material.recomputeClassification(); this.updateMaterialDefaults(pointcloud); } { if(this.showBoundingBox){ let bbRoot = this.scene.scene.getObjectByName("potree_bounding_box_root"); if(!bbRoot){ let node = new THREE.Object3D(); node.name = "potree_bounding_box_root"; this.scene.scene.add(node); bbRoot = node; } let visibleBoxes = []; for(let pointcloud of this.scene.pointclouds){ for(let node of pointcloud.visibleNodes.filter(vn => vn.boundingBoxNode !== undefined)){ let box = node.boundingBoxNode; visibleBoxes.push(box); } } bbRoot.children = visibleBoxes; } } if(this.boundNeedUpdate)this.updateModelBound() //add this.scene.cameraP.fov = this.fov; let controls = this.getControls(); if (controls === this.deviceControls) { this.controls.setScene(scene); this.controls.update(delta); this.scene.cameraP.position.copy(scene.view.position); this.scene.cameraO.position.copy(scene.view.position); } else if (controls !== null) { controls.setScene(scene); controls.update(delta); //更新camera this.viewports.forEach(viewport=>{ if(!viewport.active)return viewport.view.applyToCamera(viewport.camera) }) } this.lastFrameChanged = this.cameraChanged()//判断camera画面是否改变 { // update clip boxes let boxes = []; // volumes with clipping enabled boxes.push(...this.scene.volumes.filter(v => (v.clip && v instanceof BoxVolume))); // profile segments for(let profile of this.scene.profiles){ boxes.push(...profile.boxes); } // Needed for .getInverse(), pre-empt a determinant of 0, see #815 / #816 let degenerate = (box) => box.matrixWorld.determinant() !== 0 && box.clip; let clipBoxes = boxes.filter(degenerate).map( box => { box.updateMatrixWorld(); let boxInverse = box.matrixWorld.clone().invert(); //let boxPosition = box.getWorldPosition(new THREE.Vector3()); return {box: box, inverse: boxInverse/* , position: boxPosition */}; }); //改 let bigClipInBox = clipBoxes.find(e=>e.box.clipTask == ClipTask.SHOW_INSIDE_Big && !e.box.highlight)//裁剪下载 when this.modules.Clip.editing let clipBoxes_in = clipBoxes.filter(e=>e.box.clipTask == ClipTask.SHOW_INSIDE && !e.box.highlight) let clipBoxes_out = clipBoxes.filter(e=>e.box.clipTask == ClipTask.SHOW_OUTSIDE && !e.box.highlight) let highlightBoxes = clipBoxes.filter(e=>e.box.highlight ) //let prismPolygons = this.modules.volumeComputer && this.modules.volumeComputer.entered ? viewer.scene.measurements.filter(e=>(e.measureType == 'MulDistance Ring') && !e.isNew) : [] let prismPolygons = this.modules.volumeComputer ? this.modules.volumeComputer.prisms.filter(e=>!e.isNew && e.visible && !e.dontHighlight) : [] // set clip volumes in material for(let pointcloud of visiblePointClouds){ pointcloud.material.setClipBoxes(bigClipInBox, clipBoxes_in, clipBoxes_out, highlightBoxes, prismPolygons); } } { for(let pointcloud of visiblePointClouds){ pointcloud.material.elevationGradientRepeat = this.elevationGradientRepeat; } } { // update navigation cube this.navigationCube.update(camera.rotation); } this.updateAnnotations(); if(this.mapView){ this.mapView.update(delta); if(this.mapView.sceneProjection){ $( "#potree_map_toggle" ).css("display", "block"); } } this.transformationTool.update(); if(Potree.settings.editType != 'pano' && Potree.settings.editType != 'merge'){ this.modules.ParticleEditor.update(delta) this.mapViewer.update(delta) //地图更新 } this.dispatchEvent({ type: 'update', delta: delta, timestamp: timestamp}); //在有sidebar时耗高cpu,占本update函数80% viewer.addTimeMark('update','end') //add ------ this.reticule.updateVisible() } updateViewPointcloud(camera, areaSize, isViewport){ let result = Potree.updatePointClouds(this.scene.pointclouds, camera, areaSize ); //if(isViewport)return const tStart = performance.now(); const campos = camera.position; let closestImage = Infinity; for(const images of this.scene.orientedImages){ for(const image of images.images){ const distance = image.mesh.position.distanceTo(campos); closestImage = Math.min(closestImage, distance); } } //const tEnd = performance.now(); //改:不根据点云修改视野near far var near = camera.near, far = camera.far if(!camera.limitFar && result.lowestSpacing !== Infinity){ //let near = result.lowestSpacing * 10.0; let far = -this.getBoundingBox().applyMatrix4(camera.matrixWorldInverse).min.z; far = Math.max(far * 1.5, 10000); //near = Math.min(100.0, Math.max(0.01, near)); //near = Math.min(near, closestImage); far = Math.max(far, near + 10000); /* if(near === Infinity){ near = 0.1; } */ //camera.near = near; //为了其他物体的显示,不修改near camera.far = far; } /* if(this.scene.cameraMode == CameraMode.ORTHOGRAPHIC) {//??? camera.near = -camera.far; } */ if(/* near != camera.near || */far != camera.far){ camera.updateProjectionMatrix() } //注:pointcloud.visibleNodes会随着near far自动更新 } getPRenderer(){ if(this.useHQ){ if (!this.hqRenderer) { this.hqRenderer = new HQSplatRenderer(this); } this.hqRenderer.useEDL = this.useEDL; return this.hqRenderer; }else{ /* if (this.useEDL && Features.SHADER_EDL.isSupported()) { if (!this.edlRenderer) { this.edlRenderer = new EDLRenderer(this); } return this.edlRenderer; } else { if (!this.potreeRenderer) { this.potreeRenderer = new PotreeRenderer(this); } return this.potreeRenderer; } */ if (!this.edlRenderer) { this.edlRenderer = new EDLRenderer(this); } return this.edlRenderer; } } renderVR(){//渲染部分没改完 let renderer = this.renderer; renderer.setClearColor(0x550000, 0); renderer.clear(); let xr = renderer.xr; let dbg = new THREE.PerspectiveCamera(); let xrCameras = xr.getCamera(dbg); if(xrCameras.cameras.length !== 2){ return; } let makeCam = this.vrControls.getCamera.bind(this.vrControls); { // clear framebuffer if(viewer.background === "skybox"){ renderer.setClearColor(0xff0000, 1); }else if(viewer.background === "gradient"){ renderer.setClearColor(0x112233, 1); }else if(viewer.background === "black"){ renderer.setClearColor(0x000000, 1); }else if(viewer.background === "white"){ renderer.setClearColor(0xFFFFFF, 1); }else{ renderer.setClearColor(0x000000, 0); } renderer.clear(); } // render background if(this.background === "skybox"){ let {skybox} = this; let cam = makeCam(); skybox.camera.rotation.copy(cam.rotation); skybox.camera.fov = cam.fov; skybox.camera.aspect = cam.aspect; // let dbg = new THREE.Object3D(); let dbg = skybox.parent; // dbg.up.set(0, 0, 1); dbg.rotation.x = Math.PI / 2; // skybox.camera.parent = dbg; // dbg.children.push(skybox.camera); dbg.updateMatrix(); dbg.updateMatrixWorld(); skybox.camera.updateMatrix(); skybox.camera.updateMatrixWorld(); skybox.camera.updateProjectionMatrix(); renderer.render(skybox.scene, skybox.camera); // renderer.render(skybox.scene, cam); }else if(this.background === "gradient"){ // renderer.render(this.scene.sceneBG, this.scene.cameraBG); } this.renderer.xr.getSession().updateRenderState({ depthNear: 0.1, depthFar: 10000 }); let cam = null; let view = null; { // render world scene cam = makeCam(); cam.position.z -= 0.8 * cam.scale.x; cam.parent = null; // cam.near = 0.05; cam.near = viewer.scene.getActiveCamera().near; cam.far = viewer.scene.getActiveCamera().far; cam.updateMatrix(); cam.updateMatrixWorld(); this.scene.scene.updateMatrix(); this.scene.scene.updateMatrixWorld(); this.scene.scene.matrixAutoUpdate = false; let camWorld = cam.matrixWorld.clone(); view = camWorld.clone().invert(); this.scene.scene.matrix.copy(view); this.scene.scene.matrixWorld.copy(view); cam.matrix.identity(); cam.matrixWorld.identity(); cam.matrixWorldInverse.identity(); renderer.render(this.scene.scene, cam); this.scene.scene.matrixWorld.identity(); } for(let pointcloud of this.scene.pointclouds){ let viewport = xrCameras.cameras[0].viewport; pointcloud.material.useEDL = false; pointcloud.screenHeight = viewport.height; pointcloud.screenWidth = viewport.width; // automatically switch to paraboloids because they cause far less flickering in VR, // when point sizes are larger than around 2 pixels // if(Features.SHADER_INTERPOLATION.isSupported()){ // pointcloud.material.shape = Potree.PointShape.PARABOLOID; // } } // render point clouds for(let xrCamera of xrCameras.cameras){ let v = xrCamera.viewport; renderer.setViewport(v.x, v.y, v.width, v.height); // xrCamera.fov = 90; { // estimate VR fov let proj = xrCamera.projectionMatrix; let inv = proj.clone().invert(); let p1 = new THREE.Vector4(0, 1, -1, 1).applyMatrix4(inv); let rad = p1.y let fov = 180 * (rad / Math.PI); xrCamera.fov = fov; } for(let pointcloud of this.scene.pointclouds){ const {material} = pointcloud; material.useEDL = false; } let vrWorld = view.clone().invert(); vrWorld.multiply(xrCamera.matrixWorld); let vrView = vrWorld.clone().invert(); this.pRenderer.render(this.scene.scenePointCloud, xrCamera, null, { viewOverride: vrView, }); } { // render VR scene let cam = makeCam(); cam.parent = null; renderer.render(this.sceneVR, cam); } renderer.resetState(); } clear(params={}){ let background = params.background || this.background; let backgroundOpacity = params.backgroundOpacity == void 0 ? this.backgroundOpacity : params.backgroundOpacity//如果想完全透明,只需要backgroundOpacity为0 let renderer = this.renderer //let gl = renderer.getContext() if(background instanceof THREE.Color){ //add renderer.setClearColor(background, backgroundOpacity); }else if(background === "skybox"){ renderer.setClearColor(0x000000, backgroundOpacity); } else if (background === 'gradient') { renderer.setClearColor(0x000000, backgroundOpacity); } else if (background === 'black') { renderer.setClearColor(0x000000, 1); } else if (background === 'white') { renderer.setClearColor(0xFFFFFF, 1); } else { renderer.setClearColor(background, backgroundOpacity); } params.target || renderer.clear(); } getBuffer(viewport){//根据不同viewport返回rtEDL的texture var buffer = this.buffers.get(viewport) if(!buffer){ buffer = new THREE.WebGLRenderTarget(viewport.resolution.x, viewport.resolution.y, { minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter, format: THREE.RGBAFormat, type: THREE.FloatType }); this.buffers.set(viewport, rtEDL) } return buffer } async renderDefault(params_={}){ if(!this.visible ) return let pRenderer = this.getPRenderer(); let viewports = params_.viewports || this.viewports viewer.addTimeMark('renderDefault','start') //console.log('render', viewports.map(e=>e.name).join(',')) let renderSize if(params_.target){ renderSize = new THREE.Vector2(params_.target.width, params_.target.height) //是画布大小 //可能需要viewer.setSize }else{ renderSize = this.renderer.getSize(new THREE.Vector2()); //是client大小 } ///let needsResize = viewports.length > 1 || params_.resize //去掉原因:因为不需要渲染的viewport不在此中所以无法判断几个viewport for(let i=0; i{ if(object.material){ Potree.Utils.updateVisible(object, 'renderOpa', (params.renderBeforeCloud && (object.material.opacity<1 || !object.material.depthTest) || (!params.renderBeforeCloud) && (object.material.opacity==1 && object.material.depthTest))? false:true) //点云之前渲染的话隐藏半透明的, 点云之后渲染的话隐藏不透明的。 depthTest==false的也最后渲染 } })//ground的材质中opacity为1,所以被当做不透明了 } viewer.dispatchEvent({type: "render.begin2" , name:'scene', viewport:params.viewport }) renderer.render(this.scene.scene, camera); if('renderBeforeCloud' in params){ this.scene.scene.traverse((object)=>{ if(object.material){ Potree.Utils.updateVisible(object, 'renderOpa', true) //恢复 } }) } } this.dispatchEvent({type: "render.pass.scene", viewer: viewer}); } renderOverlay2(params){ let renderer = params.renderer || this.renderer let camera = params.camera ? params.camera : this.scene.getActiveCamera(); //清除深度 !!!! renderer.clearDepth(); if(!params.magnifier){ //测量线 this.dispatchEvent({type: "render.pass.perspective_overlay", camera, screenshot:params.screenshot,viewport:params.viewport, renderer}); if(!params.screenshot && params.viewport.name != "mapViewport" ){ Potree.Utils.setCameraLayers(camera, ['magnifier']) //magnifier 遮住测量线 renderer.render(this.scene.scene, camera); } } if(params.viewport.name != "mapViewport" ) { Potree.Utils.setCameraLayers(camera, ['volume','transformationTool']) //viewer.dispatchEvent({type: "render.begin2" , name:'clip&trans',viewport:params.viewport }) renderer.render(this.clippingTool.sceneVolume, camera); //official 可以删 renderer.render(this.transformationTool.scene, camera); } } setLimitFar(state){//切换是否limitFar viewer.mainViewport.camera.limitFar = !!state if(state){ viewer.mainViewport.camera.near = 0.02; viewer.mainViewport.camera.far = Potree.settings.displayMode == 'showPanos' ? viewer.farWhenShowPano : Potree.settings.cameraFar; viewer.mainViewport.camera.updateProjectionMatrix() } } setClipState(state){//有时候需要暂时关闭下clip state = !!state if(this.clipUnabled == !state)return this.scene.volumes.filter(v=>/* v.clip && */ v instanceof Potree.BoxVolume).map(volume=>{ volume.clip = state Potree.Utils.updateVisible(volume, 'setClipState', state) }) this.clipUnabled = !state } /* 大规模WebGL应用引发浏览器崩溃的几种情况及解决办法 https://blog.csdn.net/weixin_30378311/article/details/94846947 */ async render(params={}){//add params viewer.addTimeMark('render','start') const vrActive = this.renderer.xr.isPresenting; let SiteModel = viewer.modules.SiteModel if(this.screenshoting && !params.screenshot)return //正在截图 let s = SiteModel.editing && SiteModel.selected && (SiteModel.selected.buildType == 'room' || SiteModel.selected.buildType == 'floor') //空间模型的房间选中材质是需要depth的,这时候需要绘制两次点云 Potree.settings.pointEnableRT = !this.screenshoting && (this.scene.measurements.filter(e=>e.visible).length > 0 || s ) if(vrActive){ this.renderVR(); }else{ let specialRender = !!params.viewports let viewports = params.viewports || this.viewports if(!this.needRender){ viewports = viewports.filter(v=>v.needRender) //可以渲染的条件是viewer或viewport的needRender为true } viewports = viewports.filter(v=>v.active) if(viewports.length > 0){ params.viewports = viewports if(this.outlinePass.selectedObjects.length && this.outlinePass.edgeStrength > 0 && !params.screenshot){ let scenes = this.inputHandler.interactiveScenes.concat(this.scene.scene).concat(viewer.scene.scenePointCloud) this.composer.render(scenes, null, this.viewports, this.renderDefault.bind(this)); }else{ await this.renderDefault(params); } } if(!specialRender) this.needRender = false } viewer.addTimeMark('render','end') } startScreenshot(info={}, width=800, height=400, compressRatio ){//add //let deferred = info.deferred || $.Deferred(); let getImageDeferred = info.getImageDeferred || $.Deferred(); let finishDeferred = info.finishDeferred || $.Deferred(); let viewerMaster = info.map ? this.mapViewer : this; //截图主体 let useMap = info.map || info.type == 'measure' || info.type.includes('prism2d') if(Potree.settings.displayMode == 'showPanos' && viewer.mainViewport.view.isFlying('pos')){//如果在飞,飞完再截图 info.getImageDeferred = getImageDeferred , info.finishDeferred = finishDeferred let f = ()=>{ this.startScreenshot(info, width, height, compressRatio) } viewer.mainViewport.view.addEventListener('flyingDone', f, {once:true}) return {getImagePromise:getImageDeferred.promise(), finishPromise:finishDeferred.promise()} } var startTime = Date.now() //抗锯齿待加 1 post处理 2截图大张再抗锯齿缩小 this.pauseTestMaxLevel = true this.screenshoting = true console.log('startScreenshot: '+startTime) let updateCamera = ()=>{ viewports.forEach(e=>{ e.view.applyToCamera(e.camera) //因为fly时只更新了view所以要强制更新下camera this.dispatchEvent({ //update map and sprite, mapChanged type: "camera_changed", camera: e.camera, viewport : e, changeInfo:{positionChanged:true,changed:true} }) }) } let screenshot = async ()=>{ let pose useMap && (viewer.mapViewer.needRender = true) info.beforeScreenshot && info.beforeScreenshot() let render = async ()=>{ this.needRender = true viewports.forEach(e=>e.active = true) if(info.useRenderTarget){ //离屏渲染 有抗锯齿问题、在手机上速度慢 主视图一样会变内容,还是不用这个了 var { dataUrl } = viewerMaster.makeScreenshot( new THREE.Vector2(width,height), null, compressRatio ); }else{ //直接渲染 会改变canvas大小 let canvas = viewerMaster.renderer.domElement //canvas.width = width, canvas.height = height //不需要改canvas大小, 只需要 this.renderer.setSize(width, height ); 前面updateScreenSize已经执行 await viewerMaster.render({ screenshot : true, width , height, resize :true, needWaitLoadPoint:!!info.splitRenderInfo, maxTimeForPointLoad:info.maxTimeForPointLoad }); //需要resize /* if(info.type.includes('prism2d')){//o.map要为true viewer.dispatchEvent({type:'resize', viewport:viewer.mapViewer.viewports[0]}) //借用viewer通知lineMaterial resize viewerMaster.renderOverlay() } */ var dataUrl = canvas.toDataURL('image/png',compressRatio) } return dataUrl } let dataUrl if(info.splitRenderInfo){//为了防崩溃,分区域批次渲染 let {wc , hc} = info.splitRenderInfo let camera = viewer.mainViewport.camera let dataUrls = [] let width_ = Math.ceil(1/wc * width) , height_ = Math.ceil(1/hc * height) //可能加起来比原图大,没事到时候边界会重合 viewer.updateScreenSize({forceUpdateSize:true, width:width_, height:height_, forTarget:info.useRenderTarget}) //更新viewports相机透视 使focusOnObject在此窗口大小下 for(let i=0;i{ oldStates.viewports.forEach(old=>{//恢复相机 var viewport = viewports.find(v=>v.name == old.name); viewport.left = old.left; viewport.bottom = old.bottom; viewport.width = old.width; viewport.height = old.height viewport.view.copy(old.view) //viewport.view.applyToCamera(viewport.camera); if(viewport.camera.isOrthographicCamera){ viewport.camera.zoom = viewport.view.zoom } }) viewerMaster.updateScreenSize({forceUpdateSize:true})//更新像素 /* oldStates.viewports.forEach(old=>{//恢复相机 var viewport = [mapViewport, mainViewport].find(v=>v.name == old.name); this.dispatchEvent({ //update map type: "camera_changed", camera: viewport.camera, viewport : viewport }) }) */ updateCamera() finishDeferred.resolve({dataUrl, pose}) info.map || setTimeout(()=>{ if(!this.screenshoting){ //Potree.settings.pointNoLimit = false Potree.settings.pointDensity = 'high' //console.warn('恢复pointDensity') /* if(viewer.scene.pointclouds[0].material.oldSize_ ){ viewer.scene.pointclouds[0].material.size = viewer.scene.pointclouds[0].material.oldSize_ viewer.scene.pointclouds[0].material.oldSize_ = null } */ } },500) //延迟:避免连续多次截图时释放点云 this.screenshoting = false console.log('screenshot done: ' + startTime, 'costTime', (Date.now() - startTime) + 'ms') } {//恢复: this.backgroundOpacity = oldStates.bgOpacity; this.background = oldStates.background; this.pauseTestMaxLevel = false oldStates.displayMode && (Potree.settings.displayMode = oldStates.displayMode) if(info.type == 'measure' || info.type.includes('prism2d')){ this.modules.SiteModel.pauseUpdateEntity = false this.focusDatasets = null this.scene.measurements.forEach(e=>Potree.Utils.updateVisible(e, 'screenshot',true)) info.type == 'measure' && info.measurement.setSelected(false, 'screenshot') } if(info.type.includes('prism2d')){ //viewer.mapViewer.transparentBG = false; (info.prisms || [info.prism]).forEach(prism=>{ prism.changeStyleForScreenshot(false) if(info.type == 'prism2d-single'){ prism.baseModel && (Potree.Utils.updateVisible(prism.baseModel, 'screenshot', false, 3, 'cancel'), Potree.Utils.setObjectLayers(prism.baseModel, 'model')) prism.setEditState(false) } }) } this.images360.panos.forEach(pano=>{ Potree.Utils.updateVisible(pano, 'screenshot', true) }) Potree.Utils.updateVisible(this.reticule, 'screenshot', true) if(useMap){ Potree.Utils.updateVisible(this.mapViewer.cursor, 'screenshot', true) if(oldStates.attachedToViewer != this.mapViewer.attachedToViewer){ this.mapViewer.attachToMainViewer(!!oldStates.attachedToViewer ) } if(oldStates.attachedToViewer && this.mapViewer.splitDir != oldStates.oldSplitDir){ this.mapViewer.changeSplitScreenDir(oldStates.oldSplitDir) } mapViewport.noPointcloud = oldStates.noPointcloud Potree.Utils.updateVisible(viewer.mapViewer.mapLayer.sceneGroup, 'screenshot-prism', true) } let recover = ()=>{ if(Potree.settings.displayMode == 'showPanos') { viewer.images360.flyToPano({pano:oldStates.pano, duration:0, callback:()=>{ finish() }}) }else{ finish() } } if(info.ifGetPose){ Potree.sdk.scene.getPose().done(pose_ =>{ pose = pose_ getImageDeferred.resolve({dataUrl, pose}) recover() }) }else{ recover() } } }// screenshot end let mapViewport let mainViewport = this.mainViewport let viewports = [], oldStates = { viewports:[], pano: Potree.settings.displayMode == 'showPanos' ? viewer.images360.currentPano : null, bgOpacity : this.backgroundOpacity, background: this.background } if(!info.type.includes('prism2d')){ viewports.push(mainViewport) mainViewport.camera.isOrthographicCamera && (mainViewport.view.zoom = mainViewport.camera.zoom) oldStates.viewports.push(mainViewport.clone()) } if(useMap){ mapViewport = this.mapViewer.viewports[0] mapViewport.view.zoom = mapViewport.camera.zoom viewports.push(mapViewport) oldStates.viewports.push(mapViewport.clone()) oldStates.attachedToViewer = this.mapViewer.attachedToViewer oldStates.noPointcloud = mapViewport.noPointcloud oldStates.displayMode = Potree.settings.displayMode Potree.Utils.updateVisible(this.mapViewer.cursor, 'screenshot', false)//令mapCursor不可见 } viewports.forEach(e=>e.active = false) //暂停渲染 if(info.hideMarkers){ this.images360.panos.forEach(pano=>{//令漫游点不可见 Potree.Utils.updateVisible(pano, 'screenshot', false) }) } Potree.Utils.updateVisible(this.reticule, 'screenshot', false)//令reticule不可见 if(info.bgOpacity != void 0){ this.backgroundOpacity = info.bgOpacity } if(info.background != void 0){ this.background = info.background } Potree.settings.pointDensity = info.pointDensity || 'screenshot' let focusDatasets = (measurements)=>{ this.focusDatasets = [] measurements.forEach(measure=>{ measure.points_datasets.forEach(e=>{ let p = viewer.scene.pointclouds.find(a=>a.dataset_id == e) if(p && !this.focusDatasets.includes(p)){ this.focusDatasets.push(p) } }) }) this.updateFpVisiDatasets() } if(info.type == 'measure'){//要截图双屏 this.scene.measurements.forEach(e=>Potree.Utils.updateVisible(e,'screenshot',e == info.measurement) ) info.measurement.setSelected(true, 'screenshot') //因为分屏后位置才最终确定,才能确定是否显示出floorplan所以先分屏 if(Potree.settings.floorplanEnable){ oldStates.oldSplitDir = this.mapViewer.splitDir this.mapViewer.attachToMainViewer(true, 'measure', 0.5 , {dir:'leftRight'} ) } viewer.updateScreenSize({forceUpdateSize:true, width, height, forTarget:info.useRenderTarget}) //更新viewports相机透视 使focusOnObject在此窗口大小下 let begin = ()=>{ useMap = this.mapViewer.attachedToViewer updateCamera() if(useMap){ let waitMap = ()=>{ console.log('waitMap: '+startTime, Date.now(), this.mapViewer.mapLayer.loadingInProgress ) this.mapViewer.waitLoadDone(screenshot.bind(this))//等待地图所有加载完 } this.waitPointLoad(waitMap,info.maxTimeForPointLoad) }else{ this.waitPointLoad(screenshot,info.maxTimeForPointLoad) } } let {promise} = this.focusOnObject(info.measurement, 'measure', 0, { basePanoSize:1024, gotoBestView:true, //minMapWidth: THREE.Math.clamp(Math.min(viewer.bound.boundSize.x, viewer.bound.boundSize.y) / (20/* this.mapViewer.mapLayer.maps.find(e=>e.name == 'map').disabled ? 6 : 3*/), 2, 25) //有地图的话为了显示出名字需要更大范围 })//注意:不同角度截图 得到三维的会不一样,因为focusOnObject是根据方向的 promise.done(()=>{ //console.log('promise.done') //根据当前位置更新floorplan显示 //console.log('view Pos ', this.mainViewport.view.position.toArray()) this.updateDatasetAt(true) this.modules.SiteModel.updateEntityAt(true, {measure:info.measurement}) //更新测量线所在楼层,展示俯视图 if(!this.modules.SiteModel.inEntity){ focusDatasets([info.measurement]) } this.modules.SiteModel.pauseUpdateEntity = true //console.log('currentFloor', this.modules.SiteModel.currentFloor, 'currentDataset', this.atDatasets ) let floorplanShowed = this.mapViewer.mapLayer.maps.some(e => e.name.includes('floorplan') && e.objectGroup.visible)//如果没有floorplan展示就不分屏(如果focus时飞出数据集从而不展示怎么办。尤其是俯瞰比较长的线时容易这样,或许要根据其所在数据集强制显示) if(!floorplanShowed && this.mapViewer.attachedToViewer){ this.mapViewer.attachToMainViewer(false) //取消分屏 viewer.updateScreenSize({forceUpdateSize:true, width, height, forTarget:info.useRenderTarget}) //更新viewports相机透视 let {promise} = this.focusOnObject(info.measurement, 'measure', 0, {basePanoSize:1024,gotoBestView:true,} )//因画面比例更改,重新focus promise.done(()=>{ begin() }) }else{ begin() } }) }else if(info.type.includes('prism2d')){ let points = [], prisms = info.type == 'prism2d-all' ? info.prisms: [info.prism] this.mapViewer.attachToMainViewer(true, 'screenshot', 1 , {/* dir:'leftRight' */} ) viewer.updateScreenSize({forceUpdateSize:true, width, height, forTarget:info.useRenderTarget}) //更新viewports相机透视 使focusOnObject在此窗口大小下 prisms.forEach(prism=>{ points.push(...prism.points) prism.setSelected(false) prism.changeStyleForScreenshot(true, {hideLabel: info.type != 'prism2d-all', showName: info.type == 'prism2d-all' }) if(info.type == 'prism2d-single') { prism.baseModel && (Potree.Utils.updateVisible(prism.baseModel, 'screenshot', true, 3, 'add'), Potree.Utils.setObjectLayers(prism.baseModel, 'bothMapAndScene')) prism.setEditState(true) } }) this.scene.measurements.forEach(e=>Potree.Utils.updateVisible(e,'screenshot', prisms.includes(e)) ) this.backgroundOpacity = 0 let {promise} = this.focusOnObject({points}, 'prisms2d', 0, { minMapWidth: 0.5 , onlyMap:true, focusBoundCenter:true, boundExpandRatio:1.4 }) promise.done(()=>{ updateCamera() if(info.type == 'prism2d-all'){//全部 带平面图 focusDatasets(prisms) //更新平面图 可能有漏洞,当不在任何一个数据集内的话? this.mapViewer.waitLoadDone(screenshot.bind(this))//等待地图所有加载完 }else{//single mapViewport.noPointcloud = false //显示点云,而非平面图 Potree.settings.displayMode = 'showPointCloud' Potree.Utils.updateVisible(viewer.mapViewer.mapLayer.sceneGroup, 'screenshot-prism', false) this.waitPointLoad(screenshot, 5000) } }) }else{//default let done = ()=>{ updateCamera() if(info.splitRenderInfo){ screenshot() }else{ this.waitPointLoad(screenshot, info.maxTimeForPointLoad) } } if(info.focusObjectInfo){ viewer.updateScreenSize({forceUpdateSize:true, width, height, forTarget:info.useRenderTarget}) //更新viewports相机透视 使focusOnObject在此窗口大小下 let {promise} = this.focusOnObject( ...info.focusObjectInfo) promise.done(()=>{ done() }) }else{ done() } } /* 测量线的截图因为要调用分屏的,会改变画面 但是普通截图的话,不会改变画面 */ return {getImagePromise:getImageDeferred.promise(), finishPromise:finishDeferred.promise()} } async waitPointLoad(done, maxTimeForPointLoad, viewport = viewer.mainViewport, resolution ){ return new Promise((resolve,reject)=>{ let finish let dealDone = ()=>{ viewer.removeEventListener('update',update) viewer.removeEventListener('overPointBudget',decreaseLevel) finish || done && done() finish = true resolve() } let decreaseLevel = (e)=>{ //降点云level 但基本降完也不会再加载低等级的点云了,所以是否直接不降?即使质量不平均 if(e.restQueueSize < 10)return ;//差不多完成了 let pointclouds = viewer.scene.pointclouds.filter(e=>e.visibleNodes.length) console.log('准备decreaseLevel, numVisiblePoints', e.numVisiblePoints, '最小numVisiblePoints占比', 1/pointclouds.length ) pointclouds.forEach(pointcloud=>{ let percent = pointcloud.numVisiblePoints / e.numVisiblePoints //console.log('numVisiblePoints占总比', pointcloud.dataset_id, percent ) if(percent < 1/pointclouds.length)return let old = pointcloud.maxLevel let levels = pointcloud.visibleNodes.map(e=>e.getLevel()) let actMaxLevel = Math.max.apply(null, levels) //实际加载到的最高的node level pointcloud.maxLevel = actMaxLevel - 1 console.warn(pointcloud.dataset_id,'decreaseLevel,新maxLevel', actMaxLevel - 1, '原maxlevel', old ) }) } /* if(Potree.settings.displayMode == 'showPointCloud'){ viewer.updateScreenSize({forceUpdateSize:true, width, height, forTarget:info.useRenderTarget})//需要先setSize才能加载范围内的点云 } */ //updateCamera() //viewer.addEventListener('overPointBudget',decreaseLevel) let update = ()=>{ //前提:已经禁止渲染,仅加载点云 let camera = viewport.forViewOffset ? viewport.forViewOffset.camera : viewport.camera Potree.updatePointClouds(viewer.scene.pointclouds, camera , resolution || viewport.resolution ); } viewer.addEventListener('update',update) update() let maxTime = maxTimeForPointLoad || 2000 document.hidden && (maxTime *= 6) //离开页面后会变成1秒1帧 setTimeout(()=>{ if(Potree.pointsLoading && Potree.settings.displayMode == 'showPointCloud'){//如果还在加载 viewer.addEventListener('pointsLoaded',()=>{ //点云加载完时(不一定准确) if(!finish)console.log('加载完毕', ' numVisiblePoints', Potree.numVisiblePoints) dealDone() },{once:true}) let overTime = ()=>{//超时不候(其实之后等待地图还会再加载几秒) if(document.hidden){ return setTimeout(overTime, maxTime) } if(!finish)console.warn('超时, numVisiblePoints', Potree.numVisiblePoints) dealDone() } setTimeout(overTime, maxTime) }else{ console.log('已经加载完,无需再加载点云 numVisiblePoints', Potree.numVisiblePoints) dealDone() } },200)//先加载一段时间 }) } focusOnObject(object, type, duration, o={} ) { //飞向热点、测量线等 。 //console.log('focusOnObject: ', object, type) let deferred = o.deferred || $.Deferred(); let target = new THREE.Vector3, //相机focus的位置 position = new THREE.Vector3, //相机最终位置 dis; //相机距离目标 duration = duration == void 0 ? 1200 : duration; let camera = o.endCamera || viewer.scene.getActiveCamera() let cameraPos = camera.position.clone() let boundSize if(o.dontChangeCamDir && (o.endYaw == void 0 || o.endPitch == void 0)){ //在俯视时仅靠dir来算不准 o.endYaw = viewer.mainViewport.view.yaw o.endPitch = viewer.mainViewport.view.pitch } let moveMap = (done)=>{ if(this.mapViewer && !o.dontMoveMap){ //console.log('mapFocusOn: '+target.toArray()) const minMapWidth = o.minMapWidth || 2 //截图的时候要显示的地图范围较大,为了显示出地区名字 const minBound = new THREE.Vector2(minMapWidth,minMapWidth)//针对垂直线,在地图上只有一个点 //原始的bound let boundOri if(object.points){ boundOri = new THREE.Box3() object.points.forEach(e=>{ boundOri.expandByPoint(e) }) }else{ boundOri = object.boundingBox } if(o.focusBoundCenter){ boundOri.getCenter(target) } let boundSizeOri = boundOri.getSize(new THREE.Vector3) let boundSizeMap = boundSizeOri.clone().multiplyScalar(o.boundExpandRatio || math.linearClamp(Math.max(boundSizeOri.x, boundSizeOri.y) , [0.5, 30], [2.5, 1.2]))//为了能同时看清测量线和地图,当测量线较短时,扩大margin,防止地图过度放大 boundSizeMap.x = Math.max(minBound.x, boundSizeMap.x ) boundSizeMap.y = Math.max(minBound.y, boundSizeMap.y ) this.mapViewer.moveTo(target.clone(), boundSizeMap, duration, o.margin, null, done) } } if(o.onlyMap){ moveMap(()=>{ deferred.resolve() }) return {promise:deferred.promise()} } let getPosWithFullBound = (points, boundingBox, target, cameraPos )=>{//使boundingBox差不多占满屏幕时的相机到target的距离 // points 和 boundingBox 至少有一个 let scale if(o.dontChangeCamDir){ var inv = camera.matrixWorldInverse; }else{ var cameraTemp = camera.clone() let view = viewer.mainViewport.view.clone(); view.position.copy(cameraPos); view.lookAt(target); if(o.endPitch != void 0){ view.pitch = o.endPitch view.yaw = o.endYaw } view.applyToCamera(cameraTemp) //对镜头的bound var inv = cameraTemp.matrixWorldInverse; } var bound = new THREE.Box3() if(points){//使用points得到的bound更小 //如果points和boundingbox的差别较大,尤其使target和points中心不一致,那么points不一定会刚好在boundingbox内 points.forEach(e=>{ var p = e.clone().applyMatrix4(inv); bound.expandByPoint(p) }) scale = 1.2; }else{ bound = boundingBox.applyMatrix4(inv); scale = 1 //0.9; } boundSize = bound.getSize(new THREE.Vector3) if(o.boundScale){ scale = o.boundScale } { boundSize.x *= scale //稍微放大一些,不然会靠到屏幕边缘 boundSize.y *= scale let min = 0.0001 boundSize.x = Math.max(min, boundSize.x) boundSize.y = Math.max(min, boundSize.y) } if(camera.type == 'OrthographicCamera'){ dis = boundSize.length() / 2 }else{ let aspect = boundSize.x / boundSize.y if(camera.aspect > aspect){//视野更宽则用bound的纵向来决定 dis = boundSize.y/2/ Math.tan(THREE.Math.degToRad(camera.fov / 2)) + boundSize.z/2 }else{ let hfov = cameraLight.getHFOVForCamera(camera, true); dis = boundSize.x/2 / Math.tan(hfov / 2) + boundSize.z/2 } dis += camera.near } dis = Math.max(0.1,dis) //三个顶点以上的由于measure的中心不等于bound的中心,所以点会超出bound外。 且由于视椎近大远小,即使是两个点的,bound居中后线看上去仍旧不居中. //获得相机最佳位置 let dir if(o.dontChangeCamDir){ dir = viewer.mainViewport.view.direction.negate() }else{ dir = new THREE.Vector3().subVectors(cameraPos, target).normalize() } if(o.dontLookUp && dir.z < 0){ dir.negate() } position.copy(target).add(dir.multiplyScalar(dis)) if(false){//打开以检查box if(!this.boundBox){//调试 this.boundBox = new THREE.Mesh(new THREE.BoxGeometry(1,1,1,1)); this.boundBox.material.wireframe = true this.boundBox.up.set(0,0,1) Potree.Utils.setObjectLayers(this.boundBox,'sceneObjects') this.scene.scene.add(this.boundBox); } this.boundBox.position.copy(target) this.boundBox.scale.copy(boundSize) this.boundBox.lookAt(position) } return position } if(this.images360.flying){ let f = ()=>{ this.focusOnObject(object, type, duration, $.extend(o,{deferred})) this.images360.removeEventListener('cameraMoveDone',f) } this.images360.addEventListener('cameraMoveDone',f) return {promise: deferred.promise() } } if (type == 'measure') { if(object.points.length == 0){ return {promise:deferred.resolve()} } target.copy(object.getCenter()) //试试改变位置(方向),直视测量线。能避免倾斜角度造成的非常不居中、以及看不到面的情况 if(o.gotoBestView){//直接沿着法线方向看 if(object.points.length>2){ let facePlane = object.getFacePlane(target) let normal = facePlane.normal.clone(); if(normal.dot(this.mainViewport.view.direction) > 0){ normal.negate() //顺着视线 } //normal.z = Math.max(0.2, normal.z)//尽量不要仰视 cameraPos.copy(target).add(normal) }else{ let lineDir = new THREE.Vector3().subVectors(object.points[0],object.points[1]).normalize() let facePlane = new THREE.Plane().setFromNormalAndCoplanarPoint(lineDir, target) //if(cameraPos.z < target.z + 1)cameraPos.z = target.z + 1//尽量不要仰视 let dir = new THREE.Vector3().subVectors(cameraPos,target).normalize() dir.z = Math.max(dir.z, 0.4); cameraPos.copy(target).add(dir)//尽量不要仰视 let dis = cameraPos.distanceTo(target) dir = lineDir.clone().multiplyScalar(dis) let line = new THREE.Line3().set(cameraPos.clone().add(dir), cameraPos.clone().add(dir.negate())) //过相机位置作到面的垂线 facePlane.intersectLine(line, cameraPos ) //得垂足,作为新的相机位置。就能在测量线中间看测量线 } }else{ if(object.points.length>2){ let facePlane = object.getFacePlane(target) let normal = facePlane.normal.clone() let angle = this.mainViewport.view.direction.angleTo(normal) let minDiff = THREE.Math.degToRad(60) //console.log('angle',angle) if(angle>minDiff && angleMath.PI-maxDiff){//当几乎正对时就不执行 if(angle>Math.PI/2){ //令dir和lineDir成钝角 lineDir.negate() } let dir = new THREE.Vector3().subVectors(camera.position, target).normalize() let mid = new THREE.Vector3().addVectors(lineDir, dir).normalize() //中间法向量(如果刚好dir和lineDir反向,那得到的为零向量,就不移动了,但一般不会酱紫吧) let newDir = new THREE.Vector3().addVectors(dir, mid) cameraPos.copy(target.clone().add(newDir)) } }else{ console.error('measure 没有facePlane points点数还不为2?') } } //截图时直接飞到最佳视角,也就是平视它所在面。其余时候为了避免镜头旋转过大就算了。 position = getPosWithFullBound(object.points, null, target, cameraPos ) moveMap() if(Potree.settings.displayMode == 'showPointCloud'){ //点云 let minDis = 0.3; if(0 ){//如果没有被选中,会被遮挡。 2023.11.15 最好展示全局(尤其截图时),虽然被遮挡,但focus的过程是选中状态全显示的,可以看出所在范围。 let checkIntersect = ( )=>{ let intersect = this.inputHandler.ifBlockedByIntersect({point:position, cameraPos: target})// 不一定准确 if(intersect){ let blockCount = 0, unblockCount = 0, visi; for(let i=0;i= 0.5){ visi = false break } }else{ unblockCount ++; if(unblockCount / object.points.length > 0.5){ visi = true break } } } if(visi == void 0){ visi = unblockCount / object.points.length > 0.5 } let shrink = ()=>{ let dir = new THREE.Vector3().subVectors(position, target).normalize().multiplyScalar(intersect.distance) position.copy(target).add(dir) console.log('checkIntersect newPos', position.clone() ) } if(!visi){//更改位置距离target如果小于最小距离,需要反向。 否则直接缩短距离。 if(intersect.distance < minDis ){ console.log('检测到intersect 反向', intersect.distance ) let position1 = position.clone() let dir = new THREE.Vector3().subVectors(position, target) position.copy(target).sub(dir) let intersect2 = this.inputHandler.ifBlockedByIntersect({point: position, cameraPos:target})// 不一定准确 if(intersect2){ if(intersect2.distance < intersect.distance ){ position.copy(position1)//恢复 } shrink() } }else{ shrink() } } } } checkIntersect() //多折线没有areaPlane 有时候会看向空白区域 - - } }else if(Potree.settings.displayMode == 'showPanos'){//全景 (比较难校准) let target2, dir if( object.measureType.includes('MulDistance')){//因为该线不闭合,可能看向target的方向会没有线,所以换一个target target2 = object.points[Math.round(object.points.length / 2) ]//直接看向中间点 dir = new THREE.Vector3().subVectors(target2, position).normalize() } let pano = viewer.images360.fitPanoTowardPoint({ /*point : target, //不使用目标点来判断是因为缺少measure角度的信息。比如虽然可以靠近线的中心,但是线朝向屏幕,那几乎就是一个点了。 //bestDistance : dis * 0.5, //乘以小数是为了尽量靠近 boundSphere: boundOri.getBoundingSphere(new THREE.Sphere), */ target, dir, point : position, bestDistance : 0 , checkIntersect: o.checkIntersect, gotoBestView: o.gotoBestView }) let result = {promise:deferred.promise() } if(pano && pano.msg){ pano = pano.pano result.msg = pano.msg } if(pano){ viewer.images360.flyToPano({pano, target : target2 || target, duration, deferred, dontMoveMap:true , basePanoSize:o.basePanoSize})//dontMoveMap不要移动map,否则flytopano会自动在map中focus到漫游点的位置,而非测量线了 if(viewer.images360.currentPano == pano){ let dis1 = viewer.images360.currentPano.position.distanceTo(target) let dis2 = position.distanceTo(target) //console.log('dis1 / dis2',dis1 / dis2, 'dis1-dis2', dis1-dis2) return {msg: (dis1 / dis2 > 1.5 && dis1-dis2>10)? 'tooFar' : 'posNoChange', promise : deferred.promise() } } } return result /* if(viewer.images360.currentPano == pano){ let dis1 = viewer.images360.currentPano.position.distanceTo(target) let dis2 = position.distanceTo(target) //console.log('dis1 / dis2',dis1 / dis2, 'dis1-dis2', dis1-dis2) return {msg: (dis1 / dis2 > 1.5 && dis1-dis2>10)? 'tooFar' : 'posNoChange', promise : deferred.promise() } }else{ return {promise : deferred.promise()} } */ //出现过到达位置后测量线标签闪烁的情况 // 测量线截图 全景 最好能放大. 但要确保该位置放大后图片加载完有点困难 } } else if (type == 'tag' || type == 'point') { //dimension = 1 target.copy(object.position) let bestDistance = o.distance || 3 if(!o.dontMoveMap && this.mapViewer){ //console.log('mapFocusOn: '+target.toArray()) this.mapViewer.moveTo(target.clone(), null, duration) } if(Potree.settings.displayMode == 'showPointCloud'){ if(o.dontChangePos){ position.copy(cameraPos) }else{ dis = bestDistance let dir = o.direction ? o.direction.clone().negate() : this.mainViewport.view.direction.negate()// */new THREE.Vector3().subVectors(camera.position, target).normalize() if(o.dontLookUp && dir.z<0) dir.z *= -1 position.copy(target).add(dir.multiplyScalar(dis)) } if(o.sameFloor){//需要在同一楼层 let atFloor = this.modules.SiteModel.pointInWhichEntity(target, 'floor') if(atFloor){ let camFloor = this.modules.SiteModel.pointInWhichEntity(position, 'floor') if(camFloor != atFloor){ let raycaster = new THREE.Raycaster(); let origin = target let dir = new THREE.Vector3().subVectors( position, target ).normalize() raycaster.set(origin, dir) let intersect = Potree.Utils.getIntersect(null, [atFloor.box], null, raycaster) if(intersect){ let dis = THREE.Math.clamp(intersect.distance - 0.2, camera.near, intersect.distance) position.addVectors(origin, dir.multiplyScalar(dis)) console.log('移动到楼层') }else{ console.error('?no intersect?') } } } } if(o.checkIntersect){//识别被点云遮住的话 let intersect //反向查找从target到相机的第一个intersect intersect = this.inputHandler.ifBlockedByIntersect({point:position, margin:0, cameraPos:target} /* {point:target, margin: 0.2, cameraPos:position} */) if(intersect){ position.copy(intersect.location) console.log('移近') } } }else if(Potree.settings.displayMode == 'showPanos'){ let pano = viewer.images360.fitPanoTowardPoint({ point : target, dir : this.mainViewport.view.direction, //尽量不改相机方向,避免镜头晃动 checkIntersect: o.checkIntersect, sameFloor:o.sameFloor, bestDistance, maxDis: o.maxDis //越近越好,但不要太近,bestDistance左右差不多 }) let result = {promise:deferred.promise() } if(pano && pano.msg){ pano = pano.pano result.msg = pano.msg } pano && viewer.images360.flyToPano({pano, target, duration, deferred, dontMoveMap:true , basePanoSize:o.basePanoSize }) return result } }else if(object.boundingBox && type == 'boundingBox'){//使屏幕刚好看全boundingBox target = object.boundingBox.getCenter(new THREE.Vector3) if(o.dir){ //指定方向 cameraPos.copy(target).sub(o.dir) } position = getPosWithFullBound(object.points, object.boundingBox.clone(), target, cameraPos ) if(Potree.settings.displayMode == 'showPanos'){//全景 (比较难校准) let pano = viewer.images360.fitPanoTowardPoint({ point : position, bestDistance : 0 , }) let result = {promise:deferred.promise() } if(pano && pano.msg){ pano = pano.pano result.msg = pano.msg } pano && viewer.images360.flyToPano({pano, target, duration, deferred, dontMoveMap:true , basePanoSize:o.basePanoSize})//dontMoveMap不要移动map,否则flytopano会自动在map中focus到漫游点的位置,而非测量线了 if(!pano){ console.error('no pano') } return result //出现过到达位置后测量线标签闪烁的情况 }else{ /* if(o.dontChangeCamDir){ target = null } */ moveMap() } } if(o.startCamera && o.endCamera){ viewer.mainViewport.view.tranCamera(this.mainViewport, { endPosition:position, target , boundSize, callback:()=>{ //console.log('focusOnObjectSuccess: '+object.name, type) deferred.resolve() }, startCamera:o.startCamera, endCamera:o.endCamera, midCamera:this.scene.cameraBasic, endYaw:o.endYaw, endPitch:o.endPitch }, duration) }else if(camera.type == "OrthographicCamera"){ viewer.mainViewport.view.moveOrthoCamera(this.mainViewport, { endPosition:position, target , boundSize, endYaw:o.endYaw, endPitch:o.endPitch, callback:()=>{ //console.log('focusOnObjectSuccess: '+object.name, type) deferred.resolve() }, }, duration) }else{ viewer.mainViewport.view.setView({position, target, duration, endYaw:o.endYaw, endPitch:o.endPitch, callback:()=>{ //console.log('focusOnObjectSuccess: '+object.name, type) deferred.resolve() } }) } this.dispatchEvent({type:'focusOnObject', CamTarget:target, position}) //给controls发送信息 return {promise:deferred.promise()} } flyToDataset(o={}){ var pointcloud; if(o instanceof THREE.Object3D) pointcloud = o else if(o.pointcloud) pointcloud = o.pointcloud else pointcloud = this.scene.pointclouds.find(p => p.dataset_id == o.id); let duration = o.duration == void 0 ? 1000 : o.duration if(o.focusOnPoint || !pointcloud.panosBound){//focus点云上一点,避免center区域刚好没有点 if(pointcloud.root.geometryNode){ let posArr = pointcloud.root.geometryNode.geometry.attributes.position.array let count = pointcloud.root.geometryNode.geometry.attributes.position.count let index = Math.ceil(count / 2) //随便取一个点 let point = new THREE.Vector3(posArr[index*3+0],posArr[index*3+1],posArr[index*3+2]) //point.applyMatrix4(pointcloud.root.pointcloud.matrixWorld) point.applyMatrix4(pointcloud.root.sceneNode.matrixWorld) viewer.focusOnObject({position:point},'point',duration,{dontChangeCamDir:true, distance:15}) o.dontMoveMap || viewer.mapViewer.fitToPointcloud(pointcloud, duration) console.log('flyToDataset focusOnPoint done') return } } var center = pointcloud.bound.getCenter(new THREE.Vector3); let position let getPano = ()=>{//获取离中心最近的pano let request = [] let rank = [ Images360.scoreFunctions.distanceSquared({position: center}) ] let r = Common.sortByScore(pointcloud.panos, request, rank); if(r.length){ return r[0].item } } if(Potree.settings.displayMode == 'showPanos'){ let pano = getPano() if(pano){ if(pano == this.images360.currentPano) return 'posNoChange' this.images360.flyToPano({ pano }) }else return false }else{ let target position = center if(pointcloud.panosBound){ let panosCenter = pointcloud.panosBound.center //pano集中的地方,也就是真正有点云的地方 position = panosCenter.clone() /* let ratio = 0.2 position.z = center.z * ratio + panosCenter.z * (1-ratio) //因为panos一般比较低,为了不让相机朝下时看不到点云,加一丢丢中心高度 */ let pano = getPano() if(pano){ target = pano.position //针对像隧道一样的场景, 中心点还是panosCenter都在没有点云的地方,所以还是看向其中一个漫游点好。 position.z = target.z //水平, 避免朝上或朝下 } } if(this.modules.Clip.editing){ position.z = center.z //剪裁时在中心高度,因为以点云为重点 this.modules.Clip.bus.dispatchEvent({type:'flyToPos', position, duration }) }else{ if(math.closeTo(position, this.images360.position)) return 'posNoChange' viewer.mainViewport.view.setView({position, target, duration }) } o.dontMoveMap || viewer.mapViewer.fitToPointcloud(pointcloud, duration) } return true } findClosestDatasetOnMap(position){//寻找当前平面图上离某点最近的数据集 let pointclouds = viewer.scene.pointclouds.filter(e=>e.visible); const addScore = viewer.bound.boundSize.length() let r = Potree.Common.sortByScore(pointclouds, [], [(e)=>{//pos2d和bound2d距离排序 let pos3d = position.clone().setZ(e.panosBound ? e.panosBound.center.z : (e.bound.min.z+e.bound.max.z)/2) return - (e.panosBound ? e.panosBound.bounding : e.bound).distanceToPoint(pos3d) },(e)=>{//最有可能的是地图上显示的平面图 return e in viewer.fpVisiDatasets ? addScore : 0 }]); return r[0] && r[0].item } addTimeMark(name, type){ let record = Potree.timeCollect[name] let needRecord = record && record.start && record.measures.length < record.minCount if(needRecord || Potree.measureTimings){ performance.mark(name+"-"+type) if(type == 'end'){ let measure = performance.measure(name,name+"-start",name+"-end"); if(needRecord){ record.measures.push( measure.duration ) record.sum += measure.duration; record.ave = record.sum / record.measures.length; record.measures.sort( (a, b) => a - b ); record.median = record.measures[parseInt(record.measures.length / 2)] } } } } addFakeMeasure(name,duration){//把一些count当做duration来统计 if(!Potree.measureTimings)return if(!this.fakeMeasure[name]){ this.fakeMeasure[name] = [] } let object = { name, duration } this.fakeMeasure[name].push(object) } resolveTimings(timestamp,log){//打印用时。 注:performance手机的精度只到整数位 。 sidebar中监听update时有高cpu的函数所以不要用demo测 if(!this.toggle){ this.toggle = timestamp; } let duration = timestamp - this.toggle; if(duration > 1000.0){ if(log){ let measures = performance.getEntriesByType("measure"); for(let i in this.fakeMeasure){ measures.push(...this.fakeMeasure[i]) } let names = new Set(); for(let measure of measures){ names.add(measure.name); } let groups = new Map(); for(let name of names){ groups.set(name, { measures: [], sum: 0, n: 0, min: Infinity, max: -Infinity }); } for(let measure of measures){ let group = groups.get(measure.name); group.measures.push(measure); group.sum += measure.duration; group.n++; group.min = Math.min(group.min, measure.duration); group.max = Math.max(group.max, measure.duration); } /* let glQueries = Potree.resolveQueries(this.renderer.getContext()); // resolveQueries 无 for(let [key, value] of glQueries){ let group = { measures: value.map(v => {return {duration: v}}), sum: value.reduce( (a, i) => a + i, 0), n: value.length, min: Math.min(...value), max: Math.max(...value) }; let groupname = `[tq] ${key}`; groups.set(groupname, group); names.add(groupname); } */ for(let [name, group] of groups){ group.mean = group.sum / group.n; /* group.measures.sort( (a, b) => a.duration - b.duration ); if(group.n === 1){ group.median = group.measures[0].duration; }else if(group.n > 1){ group.median = group.measures[parseInt(group.n / 2)].duration; } */ let measures = group.measures.slice() measures.sort( (a, b) => a.duration - b.duration ); if(group.n === 1){ group.median = measures[0].duration; }else if(group.n > 1){ group.median = measures[parseInt(group.n / 2)].duration; } } let cn = Array.from(names).reduce( (a, i) => Math.max(a, i.length), 0) + 5; let cmin = 6; let cmed = 6; let cmax = 6; let csam = 4; let message = ` ${"NAME".padEnd(cn)} |` + ` ${"MIN".padStart(cmin)} |` + ` ${"MEDIAN".padStart(cmed)} |` + ` ${"MAX".padStart(cmax)} |` + ` ${"AVE".padStart(cmax)} |` + ` ${"SAMPLES".padStart(csam)} \n`; message += ` ${"-".repeat(message.length) }\n`; names = Array.from(names).sort(); for(let name of names){ let group = groups.get(name); let min = group.min.toFixed(3); let median = group.median.toFixed(3); let max = group.max.toFixed(3); let ave = group.mean.toFixed(3); //add let n = group.n; message += ` ${name.padEnd(cn)} |` + ` ${min.padStart(cmin)} |` + ` ${median.padStart(cmed)} |` + ` ${max.padStart(cmax)} |` + ` ${ave.padStart(cmax)} |` + ` ${n.toString().padStart(csam)}\n`; } message += `\n`; console.log(message); } this.fakeMeasure = {} //clear performance.clearMarks(); performance.clearMeasures(); this.toggle = timestamp; } //注意,console.log本身用时挺高,降4倍时可能占用0.5毫秒,所以不能每帧都打印 } loop(timestamp){ //let startTime = performance.now() //console.log('间隔:' /*, parseInt((startTime - this.lastEndTime)*100 )/100 */) if(this.paused)return if(performance.getEntriesByName("loopWaitNext-start").length)viewer.addTimeMark('loopWaitNext','end') if(this.stats){ this.stats.begin(); } performance.mark('loop-start') ;// 无论有没有reportTimings都要获取,因为getBestCound需要 this.dispatchEvent('loopStart') this.interacted = false this.shelterCount = {byTex:0, byCloud:0, maxByTex: 100, maxByCloud:0 } //清空 因ifPointBlockedByIntersect可能在任何时候触发,所以需要一开始就定义这个,且每次计算最大可计算次数太麻烦了就定义一个吧。 let deltaTime = this.clock.getDelta() this.update(deltaTime, timestamp); this.magnifier.render(); this.render(); this.objs.children.forEach(e=>{ if(e.fileType == '3dTiles'){ e.runtime.update(deltaTime, this.renderer, this.mainViewport.camera) } }) // let vrActive = viewer.renderer.xr.isPresenting; // if(vrActive){ // this.update(this.clock.getDelta(), timestamp); // this.render(); // }else{ // this.update(this.clock.getDelta(), timestamp); // this.render(); // } Potree.framenumber++; //------------- this.images360.tileDownloader.update() this.images360.panoRenderer.update() this.images360.getNeighbours(this.interacted) this.computeShelter() //------------- if(this.stats){ this.stats.end(); } viewer.addTimeMark('loop','end') viewer.addTimeMark('loopWaitNext','start') this.resolveTimings(timestamp, Potree.measureTimings); //Potree.measureTimings = 1 } postError(content, params = {}){ let message = this.postMessage(content, params); message.element.addClass("potree_message_error"); return message; } postMessage(content, params = {}){ let message = new Message(content); let animationDuration = 100; message.element.css("display", "none"); message.elClose.click( () => { message.element.slideToggle(animationDuration); let index = this.messages.indexOf(message); if(index >= 0){ this.messages.splice(index, 1); } }); this.elMessages.prepend(message.element); message.element.slideToggle(animationDuration); this.messages.push(message); if(params.duration !== undefined){ let fadeDuration = 500; let slideOutDuration = 200; setTimeout(() => { message.element.animate({ opacity: 0 }, fadeDuration); message.element.slideToggle(slideOutDuration); }, params.duration) } return message; } getBoundingBox (pointclouds) { //可以直接返回viewer.bound if(!this.bound){ this.updateModelBound() } return this.bound.boundingBox.clone()//this.scene.getBoundingBox(pointclouds); }; updateModelBound(reason){ this.boundNeedUpdate = false this.bound = Utils.computePointcloudsBound(this.scene.pointclouds.filter(pointcloud=> //只求可见 pointcloud.visible || pointcloud.unvisibleReasons && pointcloud.unvisibleReasons.length == 1 && pointcloud.unvisibleReasons[0].reason == 'displayMode' )) if(Potree.settings.boundAddObjs){//加上obj的bound 需要确保都updateMatrixWorld过 this.objs.children.forEach(e=>{ this.bound.boundingBox.union(e.boundingBox.clone().applyMatrix4(e.matrixWorld)) }) this.bound.boundingBox.getSize(this.bound.boundSize) this.bound.boundingBox.getCenter(this.bound.center) } viewer.farWhenShowPano = this.bound.boundSize.length() * 10//全景漫游时要能看到整个skybox 原本*2的但对于距离特远的数据集需要乘大一些否则会黑面 /* let boundPlane = new THREE.Box3() boundPlane.expandByPoint(this.bound.boundingBox.min.clone())//最低高度为bound的最低 boundPlane.expandByPoint(this.bound.boundingBox.max.clone().setZ(this.bound.center.z))//最高高度为bound的中心高度 FirstPersonControls.boundPlane = boundPlane */ //FirstPersonControls.standardSpeed = THREE.Math.clamp( Math.sqrt(this.bound.boundSize.length() )/ 100 , 0.02,0.5); //在这个boundPlane中的速度 viewer.scene.pointclouds.forEach(e=>{//海拔范围 e.material.heightMin = this.bound.boundingBox.min.z e.material.heightMax = this.bound.boundingBox.max.z }) this.dispatchEvent({type:'updateModelBound'}) } waitForLoad(object, isLoadedCallback){//等待加载时显示loading。主要是贴图 this.waitQueue.push({ object, isLoadedCallback, }) //console.warn('waitForLoad',object.id,this.waitQueue.length) 1 === this.waitQueue.length && this.dispatchEvent({type:"loading", show:true}) } ifAllLoaded( ){ if(this.waitQueue.length>0){ this.waitQueue = this.waitQueue.filter(function(e) { return !e.isLoadedCallback() }) } //console.warn('ifAllLoaded', this.waitQueue.length) 0 === this.waitQueue.length && this.dispatchEvent({type:"loading", show:false}) } cancelLoad(object){//add 突然出现还没加载完就被deactivateTiledPano但还在loading的情况,所以加了这个 this.waitQueue = this.waitQueue.filter(function(e) { return e.object != object }) //console.log('cancelLoad', object.id) this.ifAllLoaded() } setView(o={}){ let callback = ()=>{ if(o.displayMode){ Potree.settings.displayMode = o.displayMode } if(!o.pano && o.currentPano )this.images360.currentPano = o.currentPano o.callback && o.callback() } if(o.pano != void 0){//pano 权重高于 position this.images360.flyToPano(o) }else{ this.mainViewport.view.setView($.extend({},o, {callback})) } } //设置点云为标准模式 setPointStandardMat(state, pointDensity, fitPointsize){ console.log('setPointStandardMat',state) if(state){ if(this.pointStatesBefore){ return console.warn('已设置过pointStatesBefore!') } this.pointStatesBefore = { opacity : new Map(), size: new Map(), density:Potree.settings.pointDensity, useEDL:this.getEDLEnabled(), shape: viewer.scene.pointclouds[0].material.shape } viewer.scene.pointclouds.forEach(e=>{ this.pointStatesBefore.opacity.set(e, e.temp.pointOpacity) //因为更改pointDensity时会自动变opacity,所以这项最先获取 this.pointStatesBefore.colorType = e.material.activeAttributeName; fitPointsize && this.pointStatesBefore.size.set(e,e.temp.pointSize) //这项不一定有用,因为会被后期覆盖 }) if(pointDensity)Potree.settings.pointDensity = pointDensity //万一之后切换到全景模式怎么办 if(fitPointsize)Potree.settings.sizeFitToLevel = true viewer.scene.pointclouds.forEach(e=>{ e.material.activeAttributeName = 'rgba'; e.material.shape = Potree.PointShape['SQUARE'] fitPointsize && e.changePointSize(Potree.config.material.realPointSize, true) e.changePointOpacity(1) }) viewer.setEDLEnabled(false) }else{ if(!this.pointStatesBefore){ return console.error('未设置过pointStatesBefore!') } Potree.settings.sizeFitToLevel = false if(pointDensity)Potree.settings.pointDensity = this.pointStatesBefore.pointDensity viewer.scene.pointclouds.forEach(e=>{ e.material.activeAttributeName = this.pointStatesBefore.colorType e.changePointOpacity(this.pointStatesBefore.opacity.get(e)) e.material.shape = this.pointStatesBefore.shape let size = this.pointStatesBefore.size.get(e) if(size) e.changePointSize(size) }) viewer.setEDLEnabled(this.pointStatesBefore.useEDL) this.pointStatesBefore = null } } //调试时显示transformControl来调节object transformObject(object){ let seleted = viewer.inputHandler.selection[0] if(!object){//取消 seleted && viewer.inputHandler.toggleSelection(seleted); return } if(seleted && seleted != object){//要更换,先取消 this.transformObject(null) } if(!object.boundingBox){ object.boundingBox = new THREE.Box3() //任意大小 只是为了显示黄色外框 //??? computeBoundingBox } if(!viewer.inputHandler.selection.includes(object)){ viewer.inputHandler.toggleSelection(object); } } pointInWhichPointcloud(pos){//选择最接近中心的那个 使用boundSphere let result = Common.sortByScore(this.scene.pointclouds,[],[ (pointcloud)=>{ var size = pointcloud.pcoGeometry.tightBoundingBox.getSize(new THREE.Vector3) var center = pointcloud.bound.getCenter(new THREE.Vector3) var length = size.length() / 2 var dis = pos.distanceTo(center); return length / dis //到数据集中心的距离占数据集大小越小越好 } ]) //若要求更准确的话,可以使用ifContainsPoint判断一下是否在bound中 let r = result[0]; return r && r.score > 1 ? result[0].item : null } /* createRoomEv(){ const environment = new RoomEnvironment(); const pmremGenerator = new THREE.PMREMGenerator( this.renderer ); } */ modelLoaded(object, fileInfo_={}, done){//普通模型加载完以后 object.isModel = true let boundingBox = new THREE.Box3() if(fileInfo_.parentInfo){ object.name = fileInfo_.name fileInfo_.parentInfo.loadedCount ++ fileInfo_.parentInfo.modelGroup.add(object) if(fileInfo_.parentInfo.loadedCount == fileInfo_.parentInfo.url.length){ return loadDone(fileInfo_.parentInfo.modelGroup, fileInfo_.parentInfo) }else{ return } } object.name = fileInfo_.name != void 0 ? fileInfo_.name : fileInfo_.type object.fileType = fileInfo_.fileType object.boundingBox = boundingBox //未乘上matrixWorld的本地boundingBox //object.scale.set(1,1,1);//先获取原始的大小时的boundingBox object.opacity = 1 //初始化 记录 object.updateMatrixWorld() if(fileInfo_.id != void 0)object.dataset_id = fileInfo_.id fileInfo_.loadCostTime = Date.now() - fileInfo_.loadStartTime /* let weight = Math.round((total / 1024 / 1024) * 100) / 100;*/ console.log( '加载完毕:', Common.getNameFromURL(fileInfo_.url), '耗时(ms)', fileInfo_.loadCostTime, /* 模型数据量:' + weight + 'M' */) if(fileInfo_.fileType == '3dTiles'){ let tileset = object.runtime.getTileset() //TileHeader: tileset.root //参见另一个工程 TileRenderer.js preprocessNode //这个坐标位置几万…… let data = boundingVolume.halfAxes //但这个似乎是premultiply( transform );过后的,可能需还原下 let json = tileset.tileset let box = json.root.boundingVolume.box if(box){ let center = new THREE.Vector3(box[0],box[1],box[2]) let boundSize = new THREE.Vector3( ) // get the extents of the bounds in each axis let vecX = new THREE.Vector3( box[ 3 ], box[ 4 ], box[ 5 ] ) let vecY = new THREE.Vector3( box[ 6 ], box[ 7 ], box[ 8 ] ); let vecZ = new THREE.Vector3( box[ 9 ], box[ 10 ], box[ 11 ] ); const scaleX = vecX.length(); const scaleY = vecY.length(); const scaleZ = vecZ.length(); /* boundingBox.expandByPoint(center); boundingBox.expandByVector(new THREE.Vector3(scaleX,scaleY,scaleZ)) */ boundingBox.min.set( - scaleX, - scaleY, - scaleZ ); boundingBox.max.set( scaleX, scaleY, scaleZ ); }else if(json.root.boundingVolume.sphere){ let sphereData = json.root.boundingVolume.sphere let center = new THREE.Vector3(...sphereData) let radius = sphereData[3] / 2 /* let sphere = new THREE.Sphere(center, radius) let box = sphere.getBoundingBox() boundingBox.copy(box) */ boundingBox.min.set( - radius, - radius, - radius ); boundingBox.max.set( radius, radius, radius ); }else{ return console.error('json boundingVolume 缺少信息') } //中心点居然没用。可能是漏用了什么信息,也许这和LVBADUI_qp是散的有关。 console.log('3d tiles json',json) json.root.refine = 'ADD'; json.refine = 'ADD'; }else { Potree.Utils.setObjectLayers(object,'model') object.traverse( ( child )=>{ let is = child.isMesh || child instanceof THREE.Points || child.isLine if (is){ child.renderOrder = Potree.config.renderOrders.model; //if(Potree.settings.boundAddObjs){ child.geometry.computeBoundingBox() //console.log(child.matrixWorld.clone()) boundingBox.union(child.geometry.boundingBox.clone().applyMatrix4(child.matrixWorld)) //但感觉如果最外层object大小不为1,要还原下scale再乘 //}//获取在scale为1时,表现出的大小 //Potree.Utils.makeTexDontResize(child.material.map) //console.log(child.name, 'roughness',child.material.roughness,'metalness',child.material.metalness) //暂时用这种材质: if(fileInfo_.unlit && (!(child.material instanceof THREE.MeshBasicMaterial) || object.fileType == 'glb')){ //let material = new THREE.MeshBasicMaterial({map:child.material.map}) let material = new BasicMaterial({map : child.material.map}) //很奇怪glb的图会使原本的MeshBasicMaterial 会偏暗,所以自己重新写 //child.material.dispose() child.material = material } if(fileInfo_.useStandandMat && !(child.material instanceof THREE.MeshStandardMaterial)){ child.material = new THREE.MeshStandardMaterial() } if(child.material instanceof THREE.MeshStandardMaterial){ child.material.roughness = 0.7 child.material.metalness = 0.5 } } } ); } this.objs.add(object) if(fileInfo_.transform){ let setTransfrom = (name)=>{ let value = fileInfo_.transform[name] if(!value)return if(value instanceof Array){ object[name].fromArray(value) }else{ object[name].copy(value) } } setTransfrom('position') setTransfrom('rotation') setTransfrom('scale') } if(fileInfo_.moveWithPointcloud){ object.updateMatrix(); object.matrixAutoUpdate = false object.matrix.premultiply(viewer.scene.pointclouds[0].transformMatrix) //默认跟随第一个数据集 object.matrixWorldNeedsUpdate = true } object.updateMatrixWorld() MergeEditor.getBoundCenter(object) //初始化 done && done(object, fileInfo_) this.dispatchEvent({type:'modelLoaded',model:object}) } async loadModel(fileInfo, done, onProgress_, onError){ console.log('开始加载', Common.getNameFromURL(fileInfo.url) ) let boundingBox = new THREE.Box3() /* if(!Potree.settings.boundAddObjs){ boundingBox.min.set(-0.5,-0.5,-0.5); boundingBox.max.set(0.5,0.5,0.5) } */ if(fileInfo.objurl){ fileInfo.url = fileInfo.objurl, fileInfo.fileType = 'obj' //兼容最早的 } if(fileInfo.url instanceof Array){ if(fileInfo.url.length == 1){ fileInfo.url = fileInfo.url[0] }else{ fileInfo.loadedCount = 0 fileInfo.modelGroup = new THREE.Object3D; //parentGroup.name = fileInfo.title fileInfo.url.forEach((url,i)=>{ let fileInfoS = Common.CloneObject(fileInfo) fileInfoS.url = url fileInfoS.name = 'child-'+i fileInfoS.parentInfo = fileInfo this.loadModel(fileInfoS, done, onProgress_, onError) }) return } } fileInfo.url = Common.dealURL(fileInfo.url) //去除'+' fileInfo.loadStartTime = Date.now() //let fileType = fileInfo.tilesUrl ? '3dTiles' : fileInfo.objurl ? 'obj' : 'glb' let loadDone = (object, fileInfo_ )=>{ this.modelLoaded(object, fileInfo_ || fileInfo , done) } let onProgress = function ( xhr ) { if ( xhr.lengthComputable ) { let percentComplete = xhr.loaded / xhr.total * 100; //console.log( Math.round(percentComplete, 2) + '% downloaded' ); onProgress_ && onProgress_(percentComplete) } }; if(fileInfo.fileType == 'obj'){ //暂时不支持数组 if(fileInfo.mtlurl){ loaders.mtlLoader.load( fileInfo.mtlurl , (materials)=>{ materials.preload(); loaders.objLoader.setMaterials( materials ).load(fileInfo.objurl, (object, total)=>{ loadDone(object/* , total, fileInfo.objurl */) }, onProgress, onError ) } , onProgress, onError ); }else{ loaders.objLoader.load(fileInfo.objurl, (object, total)=>{ loadDone(object) }, onProgress, onError) } }else if(fileInfo.fileType == 'glb'){ loaders.glbLoader.unlitMat = true//!!fileInfo.unlit loaders.glbLoader.load(fileInfo.url, ( gltf, total )=>{ //console.log('loadGLTF', gltf) loadDone(gltf.scene/* , total, fileInfo.url */) }, onProgress, onError) }else if(fileInfo.fileType == 'ply'){ loaders.plyLoader.load( fileInfo.url, (geometry) => { let object console.log('ply加载完毕', geometry) if(!geometry.index){//点云 object = new THREE.Points(geometry, new THREE.PointsMaterial({vertexColors:true, size:0.02})) //141M的点云,intersect费时300ms以上 }else{//mesh object = new THREE.Mesh(geometry) } loadDone(object) }) }else if(fileInfo.fileType == '3dTiles'){ let result = await Loader3DTiles.load({ url: fileInfo.url, gltfLoader : loaders.glbLoader, //renderer: SceneRenderer.renderer options: { //dracoDecoderPath: '../utils/loaders/DRACOLoader/draco', //basisTranscoderPath: '../utils/loaders/KTX2Loader/basis', maximumScreenSpaceError: 30, //如果本身tiles很密很小这个值就不能很大。 //maxDepth: 100, maximumMemoryUsage: 200, //缓存大小。单位M(但实际结果是 2.5*maximumMemoryUsage + 750 。超过2G会崩, 所以应该小于540) 若太小,密集的tile反复加载很卡. (任务管理器刷新网页后若内存不掉就要结束进程否则虚高) //debug:true, parent: this.scene.scene, is4dkk: fileInfo.is4dkk,//是否是4dkk中的模型 updateTime: fileInfo.updateTime, //加后缀防止缓存 }, }) console.log(result) result.model.runtime = result.runtime let loaded = false let tileset = result.runtime.getTileset() tileset.addEventListener('endTileLoading', function (data) {//Tileset3D if (data.loadingCount == 0 && !loaded) { loaded = true; console.log('loaded!!!!!!!!!!!!!') } }); tileset.addEventListener('tileLoaded',(e)=>{ //每一个tile加载完要更改透明度 let opacity = result.model.opacity MergeEditor.changeOpacity(e.tileContent,opacity) //set Layers ? }) { let vi = true Object.defineProperty( result.model, "visible", { get: function() { return vi }, set: function(v) { vi = v tileset.visible = v; //同步,使不加载 tileset.nextForceUpdate = true } }) } loadDone(result.model/* , null, fileInfo.url */) }else if(fileInfo.fileType == 'dxf'){ loaders.dxfLoader.load(fileInfo.url,(object)=>{ loadDone(object) },fileInfo) } } removeModel(model){ this.objs.remove(model); let dispose = (e)=>{ e.geometry && e.geometry.dispose() e.material && e.material.dispose() } model.traverse(e=>{ dispose(e) }) if(Potree.settings.boundAddObjs){ this.updateModelBound() } } setDisplay(state, cause='setDisplay'){//如果创建了iframe,主页的需要隐藏的话需要释放一些内存出来。iframe关闭前也释放下比较保险 state = !!state this.objs.children.forEach(e=>{ if(e.fileType == '3dTiles'){ let tileset = e.runtime.getTileset() Potree.Utils.updateVisible(e, cause, state) if(!state) tileset._cache.trim(); //使下一次update时dispose所有不可见的tiles e.runtime.update(16, this.renderer, this.mainViewport.camera, true) if(state) this.dispatchEvent('content_changed') } }) if(state){ Potree.pointBudget = 6*1000*1000 //先随便写一个, 随后mergeEditor.updateMemoryUsage }else{ Potree.pointBudget = 0 Potree.updatePointClouds(this.scene.pointclouds, this.mainViewport.camera, this.mainViewport.resolution ) this.images360.panoRenderer.disposeIdelTargets() //如果也能清空当前使用的就好了,但是恢复就需要时间 this.images360.depthSampler.clearTexData() } this.dispatchEvent({type:'setDisplay',state}) this.paused = !state } addFire(){ if(Potree.settings.number == 't-CwfhfqJ'){ let position = Potree.Utils.datasetPosTransform({ pointcloud:viewer.scene.pointclouds[0], position: new THREE.Vector3(4.4318,-0.580291847759, -0.78), fromDataset:true }) viewer.modules.ParticleEditor.addParticle( { type:'fire', positions:[position], radius:0.42, height:10, }) viewer.modules.ParticleEditor.addParticle( { type:'smoke', positions: [ new THREE.Vector3().addVectors(position,new THREE.Vector3(0,0,0.3))], positionStyle : 'sphere' , positionRadius : 0.3, sizeTween: [[0, 0.3, 0.9, 1], [0.05, 0.1, 1, 0.8]], opacityBase : 0.2, opacityTween :[ [0, 0.3, 0.7, 0.95, 1], [0, 0.2, 1 , 0.1, 0] ], velocityBase : new THREE.Vector3( 0, 0, 1), velocitySpread : new THREE.Vector3( 0.2, 0.2, -0.3), accelerationBase : 0.2, accelerationSpread : 0.7, radius:0, //particlesPerSecond : 30, particleDeathAge : 3.0, }) viewer.modules.ParticleEditor.addParticle( { type:'explode', name:'fire splash', position: new THREE.Vector3().addVectors(position,new THREE.Vector3(0,0,0.3)), size: 0.1, sizeRange: 0.3, sizeTween:[[0, 0.05, 0.3, 0.45], [0, 0.02, 0.1, 0.05] ], opacityTween: [[0, 0.05, 0.3, 0.45], [1, 1, 0.5, 0]] , speed : 1, //sphere speedRange : 4, radius: 0.1, acceleration : 0.3, accelerationRange : 1, particleSpaceTime:0, strength:4, }) } } addVideo(){ if(Potree.settings.number != 'SS-t-P6zBR73Gke')return var geo = new THREE.PlaneGeometry(1, 1, 1, 1) var videoInfo = this.videoInfo = [ { id: '40-2', url: 'https://laser-oss.4dkankan.com/testdata/SS-t-P6zBR73Gke/temp/poi/2022/05/10/0aabafee-36b8-455d-9c11-0780bf694786.mp4', rotation:[-1.494468618954883, -1.4987317433158989, -3.061254983446741], position:[ 19.801820617361624, 2.884673619844108, -0.03362305858221648], scale:[3.5741423153151763, 2.8738725275578703, 1], }, { id: 40, /* rotation:[-1.534692822378723, 0.01083403560862361, 3.141535283661569], position:[17.2934294239949861, 2.413510747928117, -0.008057029580231356], */ url: 'https://laser-oss.4dkankan.com/testdata/SS-t-P6zBR73Gke/temp/poi/2022/05/09/7896d6ef-a2d6-4fd7-949c-768782a5b484.mp4', rotation:[-1.5487684197910518, 0.021848470169552752, -3.1387534893955236], position:[17.277316608096, 2.0840432922115846, -0.0931149415437065], scale:[2.0821757723834047, 0.6129478480765236, 1], visibles: [40] }, ] let add = (info)=>{ var video = $(``)[0] video.setAttribute("crossOrigin", 'Anonymous') video.src = info.url || Potree.resourcePath+`/video/${Potree.settings.number}/${info.id}.mp4` var map = new THREE.VideoTexture(video); var plane = this.videoPlane = new THREE.Mesh(geo, new THREE.MeshBasicMaterial({ color:"#ffffff", transparent: !0, depthTest:false, opacity:0 , //side:2, map })) plane.position.fromArray(info.position) plane.rotation.fromArray(info.rotation) info.scale && plane.scale.fromArray(info.scale) this.scene.scene.add(plane) info.plane = plane plane.boundingBox = new THREE.Box3(new THREE.Vector3(0,-0.5,0),new THREE.Vector3(1,-0.4,0.2)) video.addEventListener('loadeddata', function(e) { video.play() if(!info.visibles/* ||!viewer.images360.currentPano || info.visibles.includes(viewer.images360.currentPano.id) */){ plane.material.opacity = 1 } info.scale || plane.scale.set(video.videoWidth/1000,video.videoHeight/1000,1) // 1080 * 1920 console.log('video loadeddata', info.id) }) if(info.visibles){ this.images360.addEventListener('flyToPano' ,(e)=>{//飞之前 if(info.visibles.includes(e.toPano.pano.id)){ //出现 setTimeout(()=>{ plane.visible = true; video.currentTime = 0 video.play(); if(video.paused){ var startPlay = ()=>{ plane.visible && video.play() this.removeEventListener('global_mousedown', startPlay) } this.addEventListener('global_mousedown', startPlay) } Potree.settings.zoom.enabled = false transitions.start(lerp.property(plane.material, "opacity", 1 ) , e.toPano.duration*0.4 , ()=>{ }, 0, easing['easeInOutQuad']) }, e.toPano.duration*0.6) //时间上不能和消失的重叠 延迟 }else{ //消失 transitions.start(lerp.property(plane.material, "opacity", 0, ) , e.toPano.duration*0.4, ()=>{ if(!info){ plane.visible = false video.pause() Potree.settings.zoom.enabled = true } }, 0, easing['easeInOutQuad']) } }) } var startPlay = ()=>{ video.play() //video.pause() //video.currentTime = 0.1; this.removeEventListener('global_mousedown', startPlay) } this.addEventListener('global_mousedown', startPlay) Potree.settings.isTest && plane.addEventListener('select',(e)=>{console.log(e)}) } videoInfo.forEach(info=>{ add(info) }) /* this.images360.addEventListener('flyToPano' ,(e)=>{//飞之前 if(Potree.settings.displayMode != 'showPanos') return let info = videoInfo[e.toPano.pano.id] if(info ){ //出现 setTimeout(()=>{ plane.visible = true; plane.position.fromArray(info.position) plane.rotation.fromArray(info.rotation) video.src = Potree.resourcePath+`/video/${Potree.settings.number}/${e.toPano.pano.id}.mp4` video.play(); video.currentTime = 0 Potree.settings.zoom.enabled = false transitions.start(lerp.property(plane.material, "opacity", 1 ) , e.toPano.duration*0.4 , ()=>{ }, 0, easing['easeInOutQuad']) }, e.toPano.duration*0.6) //时间上不能和消失的重叠 延迟 } //消失 transitions.start(lerp.property(plane.material, "opacity", 0, ) , e.toPano.duration*0.4, ()=>{ if(!info){ plane.visible = false video.pause() Potree.settings.zoom.enabled = true } }, 0, easing['easeInOutQuad']) }) this.images360.addEventListener('endChangeMode',(e)=>{ //暂时不处理初始加载时就在有视频的点位上的情况 if(e.mode == 'showPanos'){ let info = videoInfo[this.images360.currentPano.id] if(info ){ //出现 plane.visible = true; plane.position.fromArray(info.position) plane.rotation.fromArray(info.rotation) plane.material.opacity = 0 video.src = Potree.resourcePath+`/video/${Potree.settings.number}/${this.images360.currentPano.id}.mp4` video.play(); video.currentTime = 0 Potree.settings.zoom.enabled = false transitions.start(lerp.property(plane.material, "opacity", 1, (e)=>{console.log('fadeIn',e)}) , 300 , ()=>{ }, 0, easing['easeInOutQuad']) } }else{ plane.visible = false; Potree.settings.zoom.enabled = true } }) */ } /* addTube(){//加水管 自动生成 if(Potree.settings.number == 't-8KbK1JjubE'){ let boundingBox = new THREE.Box3() boundingBox.min.set(-1,-1,-1); boundingBox.max.set(1,1,1) let radius = 0.08; let radialSegments = 5 let radSegments = Math.PI*2 / radialSegments var circlePts = [];//横截面 for(let i=0;i{//height:在path之上的高度,负数代表在path之下 var name = 'cylinder'+count var mat = new THREE.MeshStandardMaterial({color, depthTest:false, roughness:0.4,metalness:0.5})  let linePath = path.map(e=>new THREE.Vector3().copy(e).setZ(e.z+height)) let geo = MeshDraw.getExtrudeGeo( circlePts, null,{ extrudePath:linePath, tension:0.2} ) var mesh = new THREE.Mesh(geo,mat); mesh.name = name window[name] = mesh mesh.boundingBox = boundingBox mesh.matrixAutoUpdate = false mesh.matrix.copy(viewer.scene.pointclouds[0].transformMatrix) mesh.matrixWorldNeedsUpdate = true this.scene.scene.add(mesh); count ++ } let linePath, height //地上管子 黄色 linePath = [{"x":-109.83,"y":-68.33,"z":-7.52},{"x":-95.17,"y":-59.3,"z":-7.38}, {"x":-38.75,"y":-24.01,"z":-6.01},{"x":0.5,"y":0.19,"z":-3.89},{"x":39.29,"y":24.41,"z":-1.31} ,{"x":43.58,"y":27.7,"z":-0.97},{"x":40.22,"y":35.37,"z":-0.67}// 拐弯向右 , {"x":39.18,"y":36.71,"z":0.35},{"x":38.69,"y":36.04,"z":18.04} // 拐弯向上 ] height = radius + 0.05; addMesh('#b86', linePath, height) //地下管子 藍色 linePath = [{"x":-108.24,"y":-70.61,"z":-7.52}, {"x":-57.8,"y":-39.31,"z":-6.72},{"x":-18.8,"y":-15.35,"z":-5.01},{"x":55.87,"y":31.67,"z":-0.04},{"x":110.53,"y":66.48,"z":5.14} ] height = -0.5; addMesh('#48a', linePath, height) } } */ addTube(datas){//加水管 自动生成 for(let number in datas){ if(number == Potree.settings.number){ let data = datas[number] let boundingBox = new THREE.Box3() boundingBox.min.set(-1,-1,-1); boundingBox.max.set(1,1,1) var index = 0 var addMesh = ({color, path, height,fromDataset, radius=0.08, datasetId=Potree.settings.originDatasetId, spaceDis, tension=0.1, visiEntity}={})=>{//height:在path之上的高度,负数代表在path之下 var name = 'tube_'+index let radialSegments = 30//THREE.Math.clamp( Math.round(radius * 800), 10, 40) let radSegments = Math.PI*2 / radialSegments var circlePts = [];//横截面 for(let i=0;i{ e instanceof Array && (e = new THREE.Vector3().fromArray(e)) let pos if(fromDataset){ //获取:JSON.stringify(viewer.scene.measurements[0].dataset_points.map(e=>Potree.math.toPrecision(e.toArray(),3))) pos = Potree.Utils.datasetPosTransform({ fromDataset: true, position:e, datasetId}) }else{ pos = new THREE.Vector3().copy(e) } return pos.setZ(pos.z+height) }) console.log(linePath) let geo = MeshDraw.getExtrudeGeo( circlePts, null,{ extrudePath:linePath, tension, spaceDis} ) var mesh = new THREE.Mesh(geo,mat); mesh.name = name window[name] = mesh mesh.boundingBox = boundingBox mesh.matrixAutoUpdate = false fromDataset || mesh.matrix.copy(viewer.scene.pointclouds[0].transformMatrix) mesh.matrixWorldNeedsUpdate = true this.scene.scene.add(mesh); if(visiEntity){ this.modules.SiteModel.bus.addEventListener('buildingChange',(e)=>{ Potree.Utils.updateVisible(mesh,'isInEntity', e.entity && e.entity.name == visiEntity) }) } } while(index < data.length){ addMesh(data[index]) index++ } } } /* 可能用到的 viewer.scene.measurements.forEach(e=>e.edgeLabels.forEach(e=>Potree.Utils.updateVisible(e,'f',false))) JSON.stringify(viewer.scene.measurements.find(e=>e.visible).dataset_points.map(e=>Potree.math.toPrecision(e.toArray(),3))) */ } addSprite(e){//api let sprite if(e.text != void 0){ sprite = new TextSprite(e ) }else{ let map = texLoader.load(src) e.map = map sprite = new Sprite(e ) } return sprite } }; //------ CLIP 默认clipTask都是clipInside ---------------------- /* 并集相当于加法,交集相当于乘法。 所有结果都能展开成多个乘积相加。 假设有4个clipBoxes,ABCD, 如果是 A*B + C*D ,那么这是最终结果。 如果是 (A+B)*(C+D) = A*C+A*D+B*C+B*D */ /* let Clips = { boxes : [], unionGroups : [], //二维数组。最外层要求并集,里层要求交集(如果只有一个元素就是本身)。总结起来就是要求一堆交集的并集 shaderParams:{}, needsUpdate : true, addClip(box, clipMethod){ //不允许重复 if(this.boxes.includes(box)){ return console.warn('addClip重复添加了box',box) } boxes.push(box) if(clipMethod == 'any'){//并 this.unionGroups.push([box]) }else if(clipMethod == 'all'){//交 this.unionGroups.forEach(mixGroup=>mixGroup.push(box)) } this.needsUpdate = true }, removeClip(box){ if(!this.boxes.includes(box)){ return console.warn('removeClip没有找到该box',box) } var newGroups = []; this.unionGroups.forEach(mixGroup=>{ if(mixGroup.length == 1 && mixGroup[0] == box)return;//直接删除 newGroups.push(mixGroup.filter(e=>e!=box)); }) this.unionGroups = newGroups; this.needsUpdate = true } , clearClip(){ this.boxes = []; this.unionGroups = [] this.needsUpdate = true } , updateShaderParams(){//没写完 - - 见 pointcloud clip.vs //uniform mat4 clipBoxes[num_clipboxes]; //uniform int clipBoxGroupCount; //uniform int mixClipIndices[clipboxGroupItemCount]; //把所有的要求都直接放到数组内 //这里需要转为Float32Array..? 参考material.setClipBoxes let everyClipGroupCount = this.unionGroups.map(e=>e.length) let mixClipIndices = [] this.unionGroups.forEach(e=>{ mixClipIndices.push(...e) }) this.shaderParams = { num_clipboxes : this.boxes.length, clipBoxGroupCount : this.unionGroups.length, everyClipGroupCount, clipBoxIndexCount: mixClipIndices.length, mixClipIndices } } , getShaderParams(){//每次要传递参数到shader中,执行这个就好 if(this.needsUpdate){ this.updateShaderParams() } return this.shaderParams } } */