e.pos3d)
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.pos3d.equals(e))
let ifShelter = !!viewer.inputHandler.ifBlockedByIntersect({pos3d:history.pos3d, margin: Potree.config.shelterMargin , pickWindowSize:3} )
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.pos3d.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
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
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
viewer.images360.panos.forEach(pano=>{
let visible = currentFloor && currentFloor.panos.includes(pano)
Potree.Utils.updateVisible(pano, 'buildingChange', visible, 2)
})
} //注:非official的没有获取sitemodel的信息所以不执行楼层判断,marker显示不对是正常的
updateMarkerVisibles(){//限制显示的marker个数,因镜头内marker多的时候可能会卡
if(!Potree.settings.ifShowMarker)return
if(this.mainViewport.camera.type == 'OrthographicCamera'){
viewer.images360.panos.forEach(pano=>{
Potree.Utils.updateVisible(pano.marker, 'limitMarkerShow', true )
})
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)
}
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()
}
/*
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
if(panoCountRatio < ratio1){
score += panoCountRatio//return false
}
}
//条件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
}
updateCadVisibles(visiClouds, force){
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)
}
//已经添加了全局的 floorplanLoaded后会updateCadVisibles,这段就删了
})
viewer.mapViewer.mapLayer.needUpdate = true //可能需要更新加载的level程度
viewer.mapViewer.needRender = true //若上句不触发加载也要立即重新绘制
} */
//促使点云加载出最高级别
testPointcloudsMaxLevel(){ //所有点云都无需testMaxNodeLevel 就停止
let camera_changed, count = 0, camera
let test = (e={})=>{
camera_changed = true
camera = e.camera || this.scene.getActiveCamera()
Common.intervalTool.isWaiting('testPointcloudsMaxLevel', ()=>{
if(!camera_changed && count>50 || Potree.settings.displayMode == 'showPanos' )return //只有当camera_changed后才继续循环, 除了最开始几次需要连续加载下
camera_changed = false
count ++;
//console.log('testPointcloudsMaxLevel中',count)
let oldCount = this.testMaxNodeCount
var success = true
viewer.scene.pointclouds.forEach(e=>{
var wait = e.testMaxNodeLevel(camera)
if(wait){
success = false;
}
})
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){
$(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) {
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.scene.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;
} */
/* 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, dur){
view = view || this.scene.view
view.setCubeView("top")
this.fitToScreen(1, dur);
};
setBottomView(view, dur){
view = view || this.scene.view
view.yaw = -Math.PI;
view.pitch = Math.PI / 2;
this.fitToScreen(1, dur);
};
setFrontView(view, dur){
view = view || this.scene.view
view.yaw = 0;
view.pitch = 0;
this.fitToScreen(1, dur);
};
setBackView(view, dur){
view = view || this.scene.view
view.yaw = Math.PI;
view.pitch = 0;
this.fitToScreen(1, dur);
};
setLeftView(view, dur){
view = view || this.scene.view
view.yaw = -Math.PI / 2;
view.pitch = 0;
this.fitToScreen(1, dur);
};
setRightView (view, dur) {
view = view || this.scene.view
view.yaw = Math.PI / 2;
view.pitch = 0;
this.fitToScreen();
};
flipYZ () {
this.isFlipYZ = !this.isFlipYZ;
// TODO flipyz
console.log('TODO');
}
setCameraMode(mode){
this.scene.cameraMode = mode;
for(let pointcloud of this.scene.pointclouds) {
pointcloud.material.useOrthographicCamera = mode == CameraMode.ORTHOGRAPHIC;
}
this.updateScreenSize({forceUpdateSize:true})//reset camera.left and projectionMatrix
}
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;
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 )
// set clip volumes in material
for(let pointcloud of visiblePointClouds){
let clipBoxes_in2 = [], clipBoxes_out2 = [], highlightBoxes2 = []
if(pointcloud.dataset_id == Potree.settings.originDatasetId){ //实时裁剪只对初始数据集有效
clipBoxes_in2 = clipBoxes_in, clipBoxes_out2 = clipBoxes_out, highlightBoxes2 = highlightBoxes
}
pointcloud.material.setClipBoxes(bigClipInBox, clipBoxes_in2, clipBoxes_out2, highlightBoxes2);
}
}
{
for(let pointcloud of visiblePointClouds){
pointcloud.material.elevationGradientRepeat = this.elevationGradientRepeat;
}
}
{ // update navigation cube
this.navCubeViewer.update(delta);
}
this.updateAnnotations();
this.transformationTool.update();
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, 0 );
} else if (background === 'gradient') {
renderer.setClearColor(0x000000, 0);
} 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
}
renderDefault(params_={}){
if(!this.visible || this.paused )return
let pRenderer = this.getPRenderer();
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)return */
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
viewports.forEach(view=>{
let params = $.extend({},params_);
params.viewport = view
//if(!params.target){
params.camera = params.camera || view.camera;
params.extraEnableLayers = view.extraEnableLayers
params.cameraLayers = view.cameraLayers
//}
var left,bottom,width,height
{
left = Math.ceil(renderSize.x * view.left)
bottom = Math.ceil(renderSize.y * view.bottom)
if(params_.target){//有target时最好viewport是专门建出来的
width = Math.ceil(renderSize.x * view.width) //target的大小可能和viewport不同,比如截图,这时会更改viewport大小
height = Math.ceil(renderSize.y * view.height)
}else{
width = view.resolution.x // 用的是client的width和height
height = view.resolution.y
}
if(width == 0 || height == 0)return
let scissorTest = view.width<1 || view.height<1
if(params_.target){
params_.target.viewport.set(left, bottom, width, height);
scissorTest && params_.target.scissor.set(left, bottom, width, height);
params_.target.scissorTest = scissorTest
this.renderer.setRenderTarget(params_.target)
}else{
this.renderer.setViewport(left, bottom, width, height) //规定视口,影响图形变换(画布的使用范围)
scissorTest && this.renderer.setScissor( left, bottom, width, height );//规定渲染范围
this.renderer.setScissorTest( scissorTest );//开启WebGL剪裁测试功能,如果不开启,.setScissor方法设置的范围不起作用 | width==1且height==1时开启会只有鼠标的地方刷新,很奇怪
}
}
if(needSResize){
this.ifEmitResize( { viewport:view} )
}
viewer.dispatchEvent({type: "render.begin", viewer: viewer, viewport:view, params });
view.beforeRender && view.beforeRender()
if(view.render){
if(!view.render($.extend({}, params, {
renderer:this.renderer, clear:this.clear.bind(this), resize:null,
renderBG:this.renderBG.bind(this), force:true //viewer content_change时map也直接渲染吧 //!view.noPointcloud //如果要渲染点云,必须也一直渲染地图,否则地图会被覆盖(点云目前未能获取是否改变,也可能有其他动态物体,所以还是一直渲染的好)
})))return
}else{
this.clear(params)
pRenderer.clearTargets(params);
this.renderBG(view)
if(Potree.settings.notAdditiveBlending){
params.renderBeforeCloud = true
this.renderOverlay1(params) //先渲染不透明的model。 但drawedModelOnRT时这里提前多渲染了一遍
}
}
if(!view.noPointcloud ){
this.updateViewPointcloud(params.camera, view.resolution, true)
pRenderer.render(params); //渲染点云 skybox
}
if(Potree.settings.notAdditiveBlending){ // 融合页面才用到
params.renderBeforeCloud = false
this.renderOverlay1(params)
this.renderOverlay2(params)
}else{
this.renderOverlay(params)
}
view.afterRender && view.afterRender()
this.dispatchEvent({type: "render.end", viewer: this, viewport:view });
view.needRender = false
})
/* if(params_.screenshot){ //抗锯齿
params_.target.viewport.set(0, 0, params_.target.width, params_.target.height);
//scissorTest && params_.target.scissor.set(left, bottom, width, height);
params_.target.scissorTest = false
this.renderer.setRenderTarget(params_.target)
this.composer.render();
this.renderer.setRenderTarget(params_.target) //本想再画一层标签,但是viewport总是出错
} */
this.renderer.setRenderTarget(null)
viewer.scene.pointclouds[0] && this.addFakeMeasure('visibleNodes', viewer.scene.pointclouds[0].visibleNodes.length )//
this.addFakeMeasure('numVisiblePoints', Potree.numVisiblePoints/100000)//十万 numVisiblePoints和帧率成反比(若每一帧都render的话),和render用时成正比 (y=kn+b)。但visibleNodes个数也影响,多的话也更卡。visibleNodes和numVisiblePoints不成正比,少的visibleNodes可能numVisiblePoints多
viewer.addTimeMark('renderDefault','end')
}
renderBG(view){
let background = view.background || viewer.background;
let backgroundOpacity = view.backgroundOpacity == void 0 ? viewer.backgroundOpacity : view.backgroundOpacity//如果想完全透明,只需要backgroundOpacity为0
if(backgroundOpacity != 0){//绘制背景
if(background === "skybox"){
//限制相机到原点的距离。
let skyCamera = view.camera.type == "OrthographicCamera" ? viewer.skybox.cameraOrtho : viewer.skybox.camera
let safeRatio = 0.02;
let safeWidth = Potree.config.skyboxBgWidth * safeRatio / 2; //相机只能在这个范围内移动
if(!view.skyboxFixPos){ //允许不在全景图中心,允许位移造成一定程度畸变
let dir = new THREE.Vector3().subVectors(view.camera.position, viewer.bound.center);
let length = dir.length()
const moveMax = 100;
let aimRadius = easing.easeOutQuart(Math.min(length, moveMax) , 0, safeWidth, moveMax) //(x, startY, wholeY, maxX) 当自变量为0-moveMax时,相机位移量为0-safeWidth
dir.multiplyScalar(aimRadius/length)
skyCamera.position.copy(dir)
}else{
skyCamera.position.set(0,0,0)
}
skyCamera.rotation.copy(view.camera.rotation);
skyCamera.aspect = view.camera.aspect;
if(view.camera.type == "OrthographicCamera"){ //调节zoom
skyCamera.left = view.camera.left; skyCamera.right = view.camera.right; skyCamera.top = view.camera.top; skyCamera.bottom = view.camera.bottom
let a = Potree.config.skyboxBgWidth / 2 - safeWidth
let minY = Math.max(skyCamera.right / a, skyCamera.top / a, view.skyboxMinZoom||0); //能够使skybox铺满画布的最小zoom. 提示:越远zoom越小
let maxY = Math.max(20, minY) ;//自定义一个 不会超过的最大实际zoom
//view.camera.zoom自变量的变化范围:
let minX = 1
let maxX = 80
let x = THREE.Math.clamp(view.camera.zoom - minX, minX, maxX)
skyCamera.zoom = easing.easeOutCubic(x-minX, minY, maxY-minY, maxX-minX) //自变量范围从0开始,所以减去minX
//pos的范围先不管了 其实aimRadius是有误的,但效果还行
}else{
skyCamera.fov = view.camera.fov;
skyCamera.zoom = 1
}
view.skyboxRenderFun && view.skyboxRenderFun()
skyCamera.updateProjectionMatrix();
skyCamera.updateMatrixWorld()
viewer.renderer.render(viewer.skybox.scene, skyCamera);
}else if(background === 'gradient'){
viewer.scene.cameraBG.layers.set(Potree.config.renderLayers.bg);
viewer.renderer.render(viewer.scene.scene, viewer.scene.cameraBG);
}else if(background === 'overlayColor'){//在不clear的前提下加一层背景色
viewer.scene.bg2.material.color.copy(view.backgroundColor)
viewer.scene.bg2.material.opacity = view.backgroundOpacity
viewer.scene.cameraBG.layers.set(Potree.config.renderLayers.bg2);
viewer.renderer.render(viewer.scene.scene, viewer.scene.cameraBG);
}
}
//全景图的部分和点云有点相关就不移动到这了。但是如果是showPanos模式,就不要渲染背景了。
}
/*
关于透明度:
由于点云和mesh是分开渲染的,且材质很不一样,所以透明和blend有冲突
1 如果点云的blend是AdditiveBlending,也就是普通的叠加模式。
则半透明点云的depthTest和depthWrite都为false
这时候mesh要后渲染,且depthWrite不能为false(除非depthTest也为false),否则将被点云遮住。
2 如果点云的blend是普通型
则半透明点云的depthTest和depthWrite都为true。(为何depthWrite不能像mesh一样为false, 否则点云自身透明会错乱,可能因为太多points了)
这时候若mesh全部先渲染,则 透过depthWrite为false的半透明mesh看不透明点云,mesh会完全被点云遮住。但是透明的物体就是depthWrite要为false,否则也会完全遮住点云
即使是后渲染半透明的mesh,若透过点云看mesh,mesh会完全被点云遮住(为什么之前遇到过 透过点云看mesh,点云会显示不出)
最终选择是先渲染不透明的mesh,然后点云,然后透明的mesh。虽然点云对mesh透明会失效。
*/
renderOverlay(params){
viewer.addTimeMark('renderOverlay','start')
this.renderOverlay1(params)
this.renderOverlay2(params)
viewer.addTimeMark('renderOverlay','end')
}
renderOverlay1(params){
let camera = params.camera ? params.camera : this.scene.getActiveCamera();
this.reticule.updateAtViewports(params.viewport)
this.renderer.setRenderTarget(params.target||null)
//为什么要在点云之后渲染,否则透明失效 、 会被点云覆盖
let cameraLayers
if(params.cameraLayers) cameraLayers = params.cameraLayers
else{
if(params.viewport.name == "mapViewport" )cameraLayers = ['bothMapAndScene', 'light']
else {
cameraLayers = ['sceneObjects', 'light', 'bothMapAndScene' ];
if(!params.drawedModelOnRT){
cameraLayers.push('model')
}
}
}
if(cameraLayers.length){
Potree.Utils.setCameraLayers(camera, cameraLayers, params.extraEnableLayers) //透明贴图层 skybox 、reticule marker 不能遮住测量线
if('renderBeforeCloud' in params){
this.scene.scene.traverse((object)=>{
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,所以被当做不透明了
}
this.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 camera = params.camera ? params.camera : this.scene.getActiveCamera();
//清除深度 !!!!
this.renderer.clearDepth();
if(!params.magnifier){
//测量线
this.dispatchEvent({type: "render.pass.perspective_overlay", camera, screenshot:params.screenshot});
if(!params.screenshot && params.viewport.name != "mapViewport" ){
Potree.Utils.setCameraLayers(camera, ['magnifier']) //magnifier 遮住测量线
this.renderer.render(this.scene.scene, camera);
}
}
if(params.viewport.name != "mapViewport" ) {
Potree.Utils.setCameraLayers(camera, ['volume','transformationTool'])
this.renderer.render(this.clippingTool.sceneVolume, camera); //official 可以删
this.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 */
render(params={}){//add params
viewer.addTimeMark('render','start')
const vrActive = this.renderer.xr.isPresenting;
//Potree.settings.useRTPoint = !(SiteModel.editing && SiteModel.selected && SiteModel.selected.buildType == 'room' )//空间模型的房间选中材质是需要depth的,这时候需要绘制两次点云
Potree.settings.pointEnableRT = this.scene.measurements.length > 0 || !Potree.settings.useRTPoint
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{
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.type == 'measure' || info.map
if(Potree.settings.displayMode == 'showPanos' && viewer.scene.view.isFlying('pos')){//如果在飞,飞完再截图
info.getImageDeferred = getImageDeferred , info.finishDeferred = finishDeferred
let f = ()=>{
this.startScreenshot(info, width, height, compressRatio)
}
viewer.scene.view.addEventListener('flyingDone', f, {once:true})
return {getImagePromise:getImageDeferred.promise(), finishPromise:finishDeferred.promise()}
}
var sid = Date.now()
//抗锯齿待加 1 post处理 2截图大张再抗锯齿缩小
console.log('startScreenshot: '+sid)
let updateCamera = ()=>{
this.viewports.forEach(e=>{
e.view.applyToCamera(e.camera) //因为fly时只更新了view所以要强制更新下camera
this.dispatchEvent({ //update map and sprite
type: "camera_changed",
camera: e.camera,
viewport : e,
changeInfo:{positionChanged:true,changed:true}
})
})
}
let screenshot = ()=>{
let pose
useMap && (viewer.mapViewer.needRender = true)
this.needRender = true
let { dataUrl } = viewerMaster.makeScreenshot( new THREE.Vector2(width,height), null, compressRatio );
if(!Potree.settings.isOfficial){
Common.downloadFile(dataUrl, 'screenshot.jpg')
}
var finish = ()=>{
oldStates.viewports.forEach(old=>{//恢复相机
var viewport = viewports.find(v=>v.name == old.name);
viewport.left = old.left;
viewport.width = old.width;
viewport.view.copy(old.view)
viewport.view.applyToCamera(viewport.camera);
})
viewer.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})
console.log('screenshot done: '+sid)
}
{//恢复:
if(info.type == 'measure'){
this.scene.measurements.forEach(e=>Potree.Utils.updateVisible(e, 'screenshot',true))
info.measurement.setSelected(false, 'screenshot')
}
this.images360.panos.forEach(pano=>{
Potree.Utils.updateVisible(pano, 'screenshot', true)
})
if(info.hideMeasures){
viewer.scene.measurements.forEach((e)=>{
Potree.Utils.updateVisible(e, 'screenshot', true)
})
}else{
viewer.scene.measurements.forEach((e)=>{
e.edgeLabels.forEach(label=>{
label.backgroundColor.a = label._oldA ;//透明的抗锯齿渲染会变黑,所以去除透明
label.updateTexture()
})
})
}
Potree.Utils.updateVisible(this.reticule, 'screenshot', true)
if(useMap){
Potree.Utils.updateVisible(this.mapViewer.cursor, 'screenshot', true)
if(oldStates.attachedToViewer != this.mapViewer.attachedToViewer){
if(info.type == 'measure'){
this.mapViewer.attachToMainViewer(false )
}
}
mapViewport.camera.zoom = oldStates.mapZoom
mapViewport.camera.updateProjectionMatrix()
}
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 = [mainViewport];
let oldStates = {
viewports : [mainViewport.clone()],
pano: Potree.settings.displayMode == 'showPanos' ? viewer.images360.currentPano : null,
}
if(useMap){
mapViewport = this.mapViewer.viewports[0]
viewports.push(mapViewport)
oldStates.viewports.push(mapViewport.clone())
oldStates.attachedToViewer = this.mapViewer.attachedToViewer
oldStates.mapZoom = mapViewport.camera.zoom
Potree.Utils.updateVisible(this.mapViewer.cursor, 'screenshot', false)//令mapCursor不可见
}
if(info.hideMarkers){
this.images360.panos.forEach(pano=>{//令漫游点不可见
Potree.Utils.updateVisible(pano, 'screenshot', false)
})
}
if(info.hideMeasures){
viewer.scene.measurements.forEach((e)=>{
Potree.Utils.updateVisible(e, 'screenshot', false)
})
}else{
viewer.scene.measurements.forEach((e)=>{
e.edgeLabels.forEach(label=>{
label._oldA = label.backgroundColor.a
label.backgroundColor.a = 1 ;//透明的抗锯齿渲染会变黑,所以去除透明
label.updateTexture()
})
})
}
Potree.Utils.updateVisible(this.reticule, 'screenshot', false)//令reticule不可见
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){
this.mapViewer.attachToMainViewer(true, 'measure', 0.5 )
}
viewer.updateScreenSize({forceUpdateSize:true, width, height}) //更新viewports相机透视 使focusOnObject在此窗口大小下
let begin = ()=>{
useMap = this.mapViewer.attachedToViewer
updateCamera()
let waitTime = Potree.settings.displayMode == 'showPointCloud' ? 500 : 0 //等点云加载 网速差的话还是加载稀疏 是否要用最高质量点云
if(useMap){
let waitMap = ()=>{
//console.log('waitMap: '+sid)
this.mapViewer.waitLoadDone(screenshot.bind(this))//等待地图所有加载完
}
setTimeout(waitMap.bind(this), waitTime)
}else{
setTimeout(screenshot.bind(this), waitTime)
}
}
let {promise}= this.focusOnObject(info.measurement, 'measure', 0, {basePanoSize:1024} )//注意:不同角度截图 得到三维的会不一样,因为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)
//this.updateFpVisiDatasets()
//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)
if(!floorplanShowed && this.mapViewer.attachedToViewer){
this.mapViewer.attachToMainViewer(false) //取消分屏
viewer.updateScreenSize({forceUpdateSize:true, width, height}) //更新viewports相机透视
let {promise} = this.focusOnObject(info.measurement, 'measure', 0, {basePanoSize:1024} )//因画面比例更改,重新focus
promise.done(()=>{
begin()
})
}else{
begin()
}
})
}else{
screenshot()
}
/*
测量线的截图因为要调用分屏的,会改变画面
但是普通截图的话,不会改变画面
*/
return {getImagePromise:getImageDeferred.promise(), finishPromise:finishDeferred.promise()}
}
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 || o.camera || viewer.scene.getActiveCamera()
let cameraPos = camera.position.clone()
let boundSize
/* if(camera.type == 'OrthographicCamera'){
return console.error('focusOnObject暂不支持OrthographicCamera。因情况复杂,请视情况使用splitScreenTool.viewportFitBound')
}
*/
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.3;
}else{
bound = boundingBox.applyMatrix4(inv);
scale = 1.0//0.9;
}
boundSize = bound.getSize(new THREE.Vector3)
{
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'){
if(o.dontChangeCamDir) {
//必须在模型外部(如点击测量线时)
this.mainViewport.targetPlane.setFromNormalAndCoplanarPoint(this.mainViewport.view.direction, this.bound.center )
this.mainViewport.targetPlane.projectPoint(target, this.mainViewport.shiftTarget ) //得到target在中心面的投影
let pos = this.splitScreen.getPosOutOfModel(this.mainViewport, this.bound.size) //得到观察位置(shiftTarget投影到模型外部)
dis = pos.distanceTo(target)
}else{
dis = boundSize.length()
}
}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 = 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') {
target.copy(object.getCenter())
if(!o.dontChangeCamDir){
//试试改变位置(方向),直视测量线。能避免倾斜角度造成的非常不居中、以及看不到面的情况
if(object.points.length>2/* && window.focusMeasureFaceToIt */){
let facePlane = object.facePlane || new THREE.Plane().setFromCoplanarPoints(...object.points.slice(0,3) )
let normal = facePlane.normal.clone()
let angle = this.scene.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 )
if(this.mapViewer/* .attachedToViewer */){
//console.log('mapFocusOn: '+target.toArray())
const minBound = new THREE.Vector2(4,4)//针对垂直线,在地图上只有一个点
//原始的bound
let boundOri = new THREE.Box3()
object.points.forEach(e=>{
boundOri.expandByPoint(e)
})
let boundSizeOri = boundOri.getSize(new THREE.Vector3)
let boundSizeMap = boundSizeOri.clone().multiplyScalar(2)
boundSizeMap.x = Math.max(minBound.x, boundSizeMap.x )
boundSizeMap.y = Math.max(minBound.y, boundSizeMap.y )
this.mapViewer.moveTo(target.clone(), boundSizeMap, duration)
}
if(Potree.settings.displayMode == 'showPointCloud'){ //点云
let minDis = 0.3;
if(o.checkIntersect){
let checkIntersect = ( )=>{
let intersect = this.inputHandler.ifBlockedByIntersect({pos3d: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({pos3d: 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: true//o.checkIntersect
})
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
//出现过到达位置后测量线标签闪烁的情况
}
} 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({pos3d:position, margin:0, cameraPos:target} /* {pos3d: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.scene.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.startCamera && o.endCamera){
viewer.scene.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.scene.view.moveOrthoCamera(this.mainViewport, { endPosition:position,
target: o.dontChangeCamDir?null:target,
boundSize,
endYaw:o.endYaw, endPitch:o.endPitch,
callback:()=>{
//console.log('focusOnObjectSuccess: '+object.name, type)
deferred.resolve()
},
}, duration)
}else{
viewer.scene.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
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.scene.view.setView({position, target, duration })
}
o.dontMoveMap || viewer.mapViewer.fitToPointcloud(pointcloud, duration)
}
return true
}
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(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.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.update()
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
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,
})
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()
})
}
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
})
this.ifAllLoaded()
}
setView(o={}){
let callback = ()=>{
if(o.displayMode){
Potree.settings.displayMode = o.displayMode
}
o.callback && o.callback()
}
if(o.pano != void 0){//pano 权重高于 position
this.images360.flyToPano(o)
}else{
this.scene.view.setView($.extend({},o, {callback}))
}
}
//设置点云为标准模式
setPointStandardMat(state, pointDensity, fitPointsize){
console.log('setPointStandardMat',state)
if(state){
if(this.pointStatesBefore){
return console.error('已设置过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
}
/* addObjectTest1(){//加水管
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)
}
}
*/
/* createRoomEv(){
const environment = new RoomEnvironment();
const pmremGenerator = new THREE.PMREMGenerator( this.renderer );
}
*/
async loadModel(fileInfo, done, onProgress_, onError){
console.log('开始加载', Common.getNameFromURL(fileInfo.name) )
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_ /* , total, url */)=>{
fileInfo_ = fileInfo_ || fileInfo
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_.name), '耗时(ms)', fileInfo_.loadCostTime, /* 模型数据量:' + weight + 'M' */)
if(fileInfo_.fileType == '3dTiles'){
let tileset = object.runtime.getTileset()
//TileHeader: tileset.root
//参见另一个工程 TileRenderer.js preprocessNode
//let boundingVolume = tileset.root.boundingVolume //这个坐标位置几万…… let data = boundingVolume.halfAxes //但这个似乎是premultiply( transform );过后的,可能需还原下
let json = tileset.tileset
let box = json.root.boundingVolume.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 );
//中心点居然没用。可能是漏用了什么信息,也许这和LVBADUI_qp是散的有关。
console.log('3d tiles json',json)
json.root.refine = 'ADD';
json.refine = 'ADD';
}else {
Potree.Utils.setObjectLayers(object,'model')
object.traverse( ( child )=>{
if ( child instanceof THREE.Mesh || child instanceof THREE.Points ) {
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) || 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(child.material instanceof THREE.MeshStandardMaterial){
child.material.roughness = 0.6
child.material.metalness = 0.3
}
}
} );
}
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
}
done && done(object)
}
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'){ //暂时不支持数组
loaders.mtlLoader.load( fileInfo.mtlurl , (materials)=>{
materials.preload();
loaders.objLoader.setMaterials( materials ).load(fileInfo.objurl, (object, total)=>{
loadDone(object/* , total, fileInfo.objurl */)
})
} , 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: 50,
maxDepth: 100,
maximumMemoryUsage: 700, //缓存大小。 若太小,密集的tile反复加载很卡
//debug:true,
parent: this.scene.scene
},
})
console.log(result)
result.model.runtime = result.runtime
loadDone(result.model/* , null, fileInfo.url */)
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)
})
}
}
removeObj(object){
this.objs.remove(object);
if(Potree.settings.boundAddObjs){
this.updateModelBound()
}
}
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
}
})
*/
}
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
}
}
*/