/** * @author mschuetz / http://mschuetz.at * * adapted from THREE.OrbitControls by * * @author qiao / https://github.com/qiao * @author mrdoob / http://mrdoob.com * @author alteredq / http://alteredqualia.com/ * @author WestLangley / http://github.com/WestLangley * @author erich666 / http://erichaines.com * * * */ import * as THREE from "../../libs/three.js/build/three.module.js"; import {MOUSE} from "../defines.js"; import {Utils} from "../utils.js"; import {EventDispatcher} from "../EventDispatcher.js"; import cameraLight from "../utils/cameraLight.js"; export class FirstPersonControls extends EventDispatcher { constructor (viewer, viewport) { super(); this.viewer = viewer; this.renderer = viewer.renderer; this.scene = viewer.scene; this.rotationSpeed = 200; this.moveSpeed = 10; this.setCurrentViewport({hoverViewport:viewport, force:true}) //this.currentViewport = viewport this.keys = { FORWARD: ['W'.charCodeAt(0), 38], BACKWARD: ['S'.charCodeAt(0), 40], LEFT: ['A'.charCodeAt(0), 37], RIGHT: ['D'.charCodeAt(0), 39], UP: ['Q'.charCodeAt(0)], DOWN: ['E'.charCodeAt(0)], //SHIFT : [16], ALT : [18], Rotate_LEFT : ['L'.charCodeAt(0)], Rotate_RIGHT : ['J'.charCodeAt(0)], Rotate_UP : ['K'.charCodeAt(0)], Rotate_DOWN : ['I'.charCodeAt(0)], }; this.fadeFactor = 50; this.yawDelta = 0; this.pitchDelta = 0; this.translationDelta = new THREE.Vector3(0, 0, 0); this.translationWorldDelta = new THREE.Vector3(0, 0, 0); this.tweens = []; //this.enableChangePos = true this.viewer.addEventListener('camera_changed',(e)=>{ this.setFPCMoveSpeed(e.viewport) }) let drag = (e) => { if(!this.enabled)return let viewport = e.drag.dragViewport; if(!viewport)return let camera = viewport.camera let mode = e.drag.mouse === MOUSE.LEFT && (!e.drag.dragViewport || e.drag.dragViewport.name == 'MainView') ? 'rotate' : 'pan' let moveSpeed = this.currentViewport.getMoveSpeed(); if (e.drag.startHandled === undefined) {///??????? e.drag.startHandled = true; this.dispatchEvent({type: 'start'}); } if (mode == 'rotate') {//旋转 (为什么开启调试时旋转很慢?) //来自panoramaControl updateRotation let _matrixWorld = camera.matrixWorld camera.matrixWorld = new THREE.Matrix4;//unproject 前先把相机置于原点 var e1 = new THREE.Vector3(e.drag.pointerDragStart.x,e.drag.pointerDragStart.y,-1).unproject(camera) , t = new THREE.Vector3(e.drag.pointer.x,e.drag.pointer.y,-1).unproject(camera) , i = Math.sqrt(e1.x * e1.x + e1.z * e1.z) , n = Math.sqrt(t.x * t.x + t.z * t.z) , o = Math.atan2(e1.y, i) , a = Math.atan2(t.y, n); this.pitchDelta += o - a //上下旋转 e1.y = 0, t.y = 0; var s = Math.acos(e1.dot(t) / e1.length() / t.length()); if(!isNaN(s)){ var yawDelta = s //左右旋转 e.drag.pointerDragStart.x > e.drag.pointer.x && (yawDelta *= -1) this.yawDelta += yawDelta } e.drag.pointerDragStart.copy(e.drag.pointer) camera.matrixWorld = _matrixWorld ; } else if (mode == 'pan') {//平移 if(viewport.unableChangePos)return if(camera.type == "OrthographicCamera"){ /* let ViewWidthPX = viewport.width * viewer.renderer.domElement.clientWidth let ViewHeightPX = viewport.height * viewer.renderer.domElement.clientHeight let cameraViewWidth = camera.right * 2 let cameraViewHeight = camera.top * 2; moveVec.set(-1 * e.drag.mouseDelta.x * cameraViewWidth / ViewWidthPX, e.drag.mouseDelta.y * cameraViewHeight / ViewHeightPX , 0).applyQuaternion(camera.quaternion) */ let moveVec = Utils.getOrthoCameraMoveVec(e.drag.pointerDelta, camera )//最近一次移动向量 let handleState = window.viewer.modules.Alignment.handleState if(viewport.alignment && handleState && viewport.alignment[handleState] && e.drag.intersectStart.pointcloud){ this.dispatchEvent({ type : "transformPointcloud", intersectPoint: e.intersectPoint.orthoIntersect, intersectStart: e.drag.intersectStart.orthoIntersect, moveVec, pointcloud: e.drag.intersectStart.pointcloud, }) }else{ this.translationWorldDelta.add(moveVec.negate()) } }else{ if(e.drag.intersectStart){//如果拖拽着点云 if(e.drag.z == void 0){//拖拽开始 let pointerStartPos2d = e.drag.intersectStart.location.clone().project(camera);//识别到的点云点的位置 e.drag.z = pointerStartPos2d.z //记录z,保持拖拽物体到屏幕距离不变,所以z深度不变 e.drag.projectionMatrixInverse = camera.projectionMatrixInverse.clone() //防止吸附到最近点上(因为鼠标所在位置并非识别到的点云点的位置,需要得到鼠标所在位置的3d坐标。) let pointerStartPos2dReal = new THREE.Vector3(e.drag.pointerDragStart.x,e.drag.pointerDragStart.y, e.drag.z); e.drag.translateStartPos = pointerStartPos2dReal.clone().unproject(camera); /* this.viewer.dispatchEvent({ type: 'dragPanBegin', projectionMatrixInverse : e.drag.projectionMatrixInverse }); */ } //拖拽的过程中将projectionMatrixInverse替换成开始拖拽时的,因为near、far一直在变,会导致unproject计算出的3d坐标改变很大而闪烁。 var _projectionMatrixInverse = camera.projectionMatrixInverse; camera.projectionMatrixInverse = e.drag.projectionMatrixInverse; let newPos2d = new THREE.Vector3(e.drag.pointer.x,e.drag.pointer.y, e.drag.z ); let newPos3d = newPos2d.clone().unproject(camera); let moveVec = newPos3d.clone().sub( e.drag.translateStartPos /* e.drag.intersectStart.location */ );//移动相机,保持鼠标下的位置永远不变,所以用鼠标下的新位置减去鼠标下的原始位置 camera.projectionMatrixInverse = _projectionMatrixInverse this.translationWorldDelta.copy(moveVec.negate()) //这里没法用add,原因未知,会跳动 }else{ //如果鼠标没有找到和点云的交点,就假设移动整个模型(也可以去扩大范围寻找最近点云) /* let center = viewer.scene.pointclouds[0].position; let radius = camera.position.distanceTo(center); let ratio = radius * Math.tan(THREE.Math.degToRad(camera.fov)/2) / 1000 */ /* let speed = this.currentViewport.getMoveSpeed() if(FirstPersonControls.boundPlane){ speed = FirstPersonControls.boundPlane.distanceToPoint(this.currentViewport.position) speed = Math.max(1 , speed) } */ let lastIntersect = viewport.lastIntersect ? (viewport.lastIntersect.location || viewport.lastIntersect) : viewer.bound.center //该viewport的最近一次鼠标和点云的交点 let speed = camera.position.distanceTo(lastIntersect) let fov = cameraLight.getHFOVForCamera(camera, camera.aspect, 1) let ratio = speed * Math.tan(THREE.Math.degToRad(fov)/2) this.translationDelta.x -= e.drag.pointerDelta.x * ratio this.translationDelta.z -= e.drag.pointerDelta.y * ratio } } } //最好按ctrl可以变为dollhouse的那种旋转 }; let drop = e => { if(!this.enabled)return this.dispatchEvent({type: 'end'}); }; let scroll = (e) => { if(!this.enabled)return this.setCurrentViewport(e) if(this.currentViewport.unableChangePos)return let camera = e.hoverViewport.camera let speed = this.currentViewport.getMoveSpeed() || 1 if(camera.type == "OrthographicCamera"){ let ratio if (e.delta < 0) { ratio = 0.9 } else if (e.delta > 0) { ratio = 1.1 } let zoom = camera.zoom * ratio let limit = Potree.config.OrthoCameraLimit.zoom zoom = THREE.Math.clamp(zoom, limit.min,limit.max ) if(camera.zoom != zoom){ camera.zoom = zoom camera.updateProjectionMatrix() } }else{ var direction = this.currentViewport.view.direction.clone(); var vec = direction.multiplyScalar(speed * 7) if (e.delta < 0) { this.translationWorldDelta.copy(vec.negate()) } else if (e.delta > 0) { this.translationWorldDelta.copy(vec) } } }; let dblclick = (e) => { if(!this.enabled)return if(!Potree.settings.dblToFocusPoint)return;//调试时才可双击 if(Potree.settings.displayMode == 'showPointCloud'/* !viewer.images360.isAtPano() */) this.zoomToLocation(e.mouse); }; this.viewer.addEventListener('global_drag', drag); this.viewer.addEventListener('global_drop', drop); this.viewer.addEventListener('global_mousewheel', scroll); this.viewer.addEventListener('global_dblclick', dblclick); this.viewer.addEventListener('startDragging', this.setCurrentViewport.bind(this)) /* this.viewer.addEventListener('enableChangePos', (e)=>{ if(!this.enabled)return this.enableChangePos = e.canLeavePano }) */ } setEnable(enabled){ this.enabled = enabled; } setFPCMoveSpeed(viewport){ if(viewport.camera.type == 'OrthographicCamera'){ let s = 1 / viewport.camera.zoom viewport.setMoveSpeed(s) }else{ if(viewport == viewer.mainViewport && FirstPersonControls.boundPlane){ let s = FirstPersonControls.boundPlane.distanceToPoint(viewer.mainViewport.view.position) s = Math.sqrt(s) / 10; s = Math.max(FirstPersonControls.standardSpeed , s) viewer.setMoveSpeed(s) } } } setCurrentViewport(o={}){//add if(!this.enabled && !o.force)return if(o.hoverViewport && this.currentViewport != o.hoverViewport ){ this.currentViewport = o.hoverViewport if(this.currentViewport.camera.type == 'OrthographicCamera'){ this.lockElevationOri = true this.lockRotation = true }else{ this.lockElevationOri = false this.lockRotation = false } //this.viewer.setMoveSpeed(this.currentViewport.radius/100); this.setFPCMoveSpeed(this.currentViewport) } } setScene (scene) { this.scene = scene; } stop(){ this.yawDelta = 0; this.pitchDelta = 0; this.translationDelta.set(0, 0, 0); } zoomToLocation(mouse){ if(!this.enabled)return let camera = this.scene.getActiveCamera(); /* let I = Utils.getMousePointCloudIntersection( mouse, camera, this.viewer, this.scene.pointclouds); */ var I = this.viewer.inputHandler.intersectPoint if (!I) { return; } let targetRadius = 0; { let minimumJumpDistance = 0.2; let domElement = this.renderer.domElement; let ray = Utils.mouseToRay(this.viewer.inputHandler.pointer, camera); let {origin, direction} = this.viewer.inputHandler.getMouseDirection() let raycaster = new THREE.Raycaster(); raycaster.ray.set(origin, direction); let nodes = I.pointcloud.nodesOnRay(I.pointcloud.visibleNodes, ray); let nodes2 = I.pointcloud.nodesOnRay(I.pointcloud.visibleNodes, raycaster.ray); let lastNode = nodes[nodes.length - 1]; let radius = lastNode.getBoundingSphere(new THREE.Sphere()).radius; targetRadius = Math.min(this.scene.view.radius, radius); targetRadius = Math.max(minimumJumpDistance, targetRadius); } let d = this.scene.view.direction.multiplyScalar(-1); let cameraTargetPosition = new THREE.Vector3().addVectors(I.location, d.multiplyScalar(targetRadius)); // TODO Unused: let controlsTargetPosition = I.location; let animationDuration = 600; let easing = TWEEN.Easing.Quartic.Out; { // animate let value = {x: 0}; let tween = new TWEEN.Tween(value).to({x: 1}, animationDuration); tween.easing(easing); this.tweens.push(tween); let startPos = this.scene.view.position.clone(); let targetPos = cameraTargetPosition.clone(); let startRadius = this.scene.view.radius; let targetRadius = cameraTargetPosition.distanceTo(I.location); tween.onUpdate(() => { let t = value.x; this.scene.view.position.x = (1 - t) * startPos.x + t * targetPos.x; this.scene.view.position.y = (1 - t) * startPos.y + t * targetPos.y; this.scene.view.position.z = (1 - t) * startPos.z + t * targetPos.z; this.scene.view.radius = (1 - t) * startRadius + t * targetRadius; this.viewer.setMoveSpeed(this.scene.view.radius / 2.5); }); tween.onComplete(() => { this.tweens = this.tweens.filter(e => e !== tween); }); tween.start(); } } update (delta) { if(!this.enabled)return let view = this.currentViewport.view { // cancel move animations on user input let changes = [ this.yawDelta, this.pitchDelta, this.translationDelta.length(), this.translationWorldDelta.length() ]; let changeHappens = changes.some(e => Math.abs(e) > 0.001); if (changeHappens && this.tweens.length > 0) { this.tweens.forEach(e => e.stop()); this.tweens = []; } } { // accelerate while input is given let ih = this.viewer.inputHandler; let moveForward = this.keys.FORWARD.some(e => ih.pressedKeys[e]); let moveBackward = this.keys.BACKWARD.some(e => ih.pressedKeys[e]); let moveLeft = this.keys.LEFT.some(e => ih.pressedKeys[e]); let moveRight = this.keys.RIGHT.some(e => ih.pressedKeys[e]); let moveUp = this.keys.UP.some(e => ih.pressedKeys[e]); let moveDown = this.keys.DOWN.some(e => ih.pressedKeys[e]); let rotateLeft = this.keys.Rotate_LEFT.some(e => ih.pressedKeys[e]); let rotateRight = this.keys.Rotate_RIGHT.some(e => ih.pressedKeys[e]); let rotateUp = this.keys.Rotate_UP.some(e => ih.pressedKeys[e]); let rotateDown = this.keys.Rotate_DOWN.some(e => ih.pressedKeys[e]); this.lockElevation = this.lockElevationOri || this.keys.ALT.some(e => ih.pressedKeys[e]); if(!this.lockRotation){ if(rotateLeft){ this.yawDelta -= 0.01 }else if(rotateRight){ this.yawDelta += 0.01 } if(rotateUp){ this.pitchDelta -= 0.01 }else if(rotateDown){ this.pitchDelta += 0.01 } } if(!this.currentViewport.unableChangePos){ if(this.lockElevation){ let dir = view.direction; dir.z = 0; dir.normalize(); if (moveForward && moveBackward) { this.translationWorldDelta.set(0, 0, 0); } else if (moveForward) { this.translationWorldDelta.copy(dir.multiplyScalar(this.currentViewport.getMoveSpeed())); } else if (moveBackward) { this.translationWorldDelta.copy(dir.multiplyScalar(-this.currentViewport.getMoveSpeed())); } }else{ if (moveForward && moveBackward) { this.translationDelta.y = 0; } else if (moveForward) { this.translationDelta.y = this.currentViewport.getMoveSpeed(); } else if (moveBackward) { this.translationDelta.y = -this.currentViewport.getMoveSpeed(); } } if (moveLeft && moveRight) { this.translationDelta.x = 0; } else if (moveLeft) { this.translationDelta.x = -this.currentViewport.getMoveSpeed(); } else if (moveRight) { this.translationDelta.x = this.currentViewport.getMoveSpeed(); } if (moveUp && moveDown) { this.translationWorldDelta.z = 0; } else if (moveUp) { this.translationWorldDelta.z = this.currentViewport.getMoveSpeed(); } else if (moveDown) { this.translationWorldDelta.z = -this.currentViewport.getMoveSpeed(); } } } { // apply rotation let yaw = view.yaw; let pitch = view.pitch; yaw += this.yawDelta /* * delta; */ pitch += this.pitchDelta/* * delta; */ view.yaw = yaw; view.pitch = pitch; this.yawDelta = 0 this.pitchDelta = 0 } if(this.translationWorldDelta.length()>0) { // console.log('translationDelta') } { // apply translation view.translate( this.translationDelta.x, /* * delta, */ this.translationDelta.y, /* * delta, */ this.translationDelta.z, /* * delta */ ); this.translationDelta.set(0,0,0) //if(this.translationWorldDelta.length())console.log(translationWorldDelta) view.translateWorld( this.translationWorldDelta.x /* * delta */, this.translationWorldDelta.y /* * delta */, this.translationWorldDelta.z /* * delta */ ); this.translationWorldDelta.set(0,0,0) } { // set view target according to speed //view.radius = 1 * this.currentViewport.getMoveSpeed(); /* if(viewer.bound) view.radius = view.position.distanceTo(viewer.bound.center) let speed = view.radius/100; this.viewer.setMoveSpeed(speed); */ //this.setMoveSpeed() } { // decelerate over time let attenuation = Math.max(0, 1 - this.fadeFactor * delta); /* this.yawDelta *= attenuation; this.pitchDelta *= attenuation; this.translationDelta.multiplyScalar(attenuation); this.translationWorldDelta.multiplyScalar(attenuation);*/ } } };