//漫游模式相机控制器 /** * 功能:主要是在漫游模式下控制相机的方向(相机的移动只是做了事件触发,逻辑不在此) * * 原理: * @ 以camera本地坐标系作为参考建立球坐标系,有关球坐标系 https://en.wikipedia.org/wiki/Spherical_coordinate_system * @ target作为camera的视点通过鼠标交互在球坐标系运动,然后调用THREE的API:camera.LookAt(target)校正相机方向 * * @param {*} camera 被控制的相机 */ import * as THREE from '../lib/three.module.js' import math from '../utils/math.js' import common from '../utils/common.js' import { MouseButton, ControlEvents } from '../utils/enum.js' const rotationAfterMoveMultiplier = 40 const rotationAfterMoveHistoryCount = 5 const insideLookLimitUp = 90 //25 const insideLookLimitDown = -90 //-25 const rotationFriction = 0.05 const rotationAccelerationInside = 4.5 class PanoramaTool extends THREE.EventDispatcher { constructor(application) { super() this.application = application this.name = 'panorama' this.target = new THREE.Vector3(0, 0, 0) //相机视点,鼠标交互主要影响的对象 this.lookVector = new THREE.Vector3() //相机方向,以单位向量表示 this.lookSpeed = 0.05 //没发现下文用到??? this.rotationAcc = new THREE.Vector2() //旋转角加速度 this.rotationSpeed = new THREE.Vector2() //旋转角速度 this.speed = 1 // 相机拖拽旋转速度 /** * 球坐标系的相关参数lat,lon 与 phi,theta 两种表示形式 * 注:少了半径参数,因为是用于约束相机的方向,半径长短在此没有意义,单位1即可,体现在方向向量lookVector上 */ this.lat = 0 //纬度,角度表示,直观 this.lon = 0 //经度,角度表示,直观 this.phi = 0 //phi,标准球坐标系的参数,弧度表示,用于进行直接计算 this.theta = 0 //theta,标准球坐标系的参数,弧度表示,用于进行直接计算 this.locked = !1 //是否锁定 /** * 交互行为相关,有鼠标点击与触摸,点击或触摸的地方在此约定统称为交互点 */ this.pointer = new THREE.Vector2(0, 0) //交互点的屏幕坐标,有别于DOM坐标,在此存放NDC坐标。(NDC,三维常用坐标系,二维坐标,整个屏幕映射范围(-1,1),屏幕中心为原点,+Y朝上,+X朝右) this.pointersLimit = 2 //触摸事件的触摸点的限制个数 this.pointers = [] //存储交互点的坐标 this.rotationDifference = new THREE.Vector2() //记录帧之间的要进行的旋转量 this.rotationHistory = [] //记录一次拖拽过程中每帧产生的rotationDifference,用于拖拽完成后计算平均值进而得到惯性角速度 this.pointerDragOn = !1 //拖拽的标记,用于处理各种交互行为下的冲突问题 this.pointerDragStart = new THREE.Vector2(0, 0) //拖拽开始位置,也作为两帧之间前一帧的坐标位置 this.pinchDistance = 0 //触控下,“捏合”交互下,两触摸点的距离 this.moveStart = new THREE.Vector2() //交互点移动行为开始的最表 this.moveTolerance = 0.01 //产生拖拽行为的鼠标移动最小阈值,用于解决点击,和其他行为的误触操作 this.translate = new THREE.Vector3() this._animate = this.animate.bind(this) this._onPointerDown = this.onPointerDown.bind(this) this._onPointerMove = this.onPointerMove.bind(this) this._onPointerUp = this.onPointerUp.bind(this) this._onMouseWheel = this.onMouseWheel.bind(this) this._onContextMenu = this.onContextMenu.bind(this) } /** * 启用状态 */ usable() { return !this.locked } activate() { //add const application = this.application const container = application.container application.addEventListener('animation', this._animate) container.addEventListener('pointerdown', this._onPointerDown) container.addEventListener('pointermove', this._onPointerMove) container.addEventListener('pointerup', this._onPointerUp) container.addEventListener('wheel', this._onMouseWheel, false) container.addEventListener('contextmenu', this._onContextMenu, false) //application.addEventListener('scene', this._onScene) this.lookAt(null, this.getDirection()) } deactivate() { const application = this.application const container = application.container //container.removeEventListener('wheel', this._onWheel, false) application.removeEventListener('animation', this._animate) container.removeEventListener('pointerdown', this._onPointerDown) container.removeEventListener('pointermove', this._onPointerMove) container.removeEventListener('pointerup', this._onPointerUp) container.removeEventListener('wheel', this._onMouseWheel) container.removeEventListener('contextmenu', this._onContextMenu, false) //application.removeEventListener('scene', this._onScene) } getDirection() { //not by lon lat return new THREE.Vector3(0, 0, -1).applyQuaternion(this.application.camera.quaternion) } /** * 根据新的方向向量计算所指向的球面坐标(lat,lon),用到了笛卡尔坐标系转球面坐标系的数学方法 * 注:THREE 的 Vector3 与 Spherical 两个数学类有互转的方法 * @param {THREE.Vector3} direction 方向向量 */ lookAt(target, dir) { //compute lon lat let camera = this.application.camera dir = dir || camera.position.clone().sub(target) /** * 以下全为笛卡尔坐标->球座标,不多赘述 */ var i = Math.atan(dir.y / dir.x) i += dir.x < 0 ? Math.PI : 0 i += dir.x > 0 && dir.y < 0 ? 2 * Math.PI : 0 this.lon = -THREE.MathUtils.radToDeg(i) - 180 var n = Math.sqrt(dir.x * dir.x + dir.y * dir.y), o = Math.atan(dir.z / n) this.lat = THREE.MathUtils.radToDeg(o) } /** * 记录一次拖拽旋转开始时的一些状态 * @param {number} clientX 屏幕坐标 * @param {number} clientY 屏幕坐标 */ startRotationFrom(clientX, clientY) { //以屏幕中心为原点,得到pointer在屏幕的百分比 var mouse = common.handelPadding(clientX, clientY, this.application.container) math.convertScreenPositionToNDC(this.pointer, mouse, this.application.container.clientWidth, this.application.container.clientHeight) this.pointerDragOn = !0 this.pointerDragStart.copy(this.pointer) this.moveStart.copy(this.pointer) this.rotationHistory = [] this.rotationSpeed.set(0, 0) } onMouseOver(mouseEvent) { !this.pointerDragOn || (0 !== mouseEvent.which && 0 !== mouseEvent.buttons) || this.onMouseUp(mouseEvent) } onTouchStart(pointerEvent) { if (this.usable()) { pointerEvent.preventDefault() pointerEvent.stopPropagation() switch (pointerEvent.touches.length) { case 1: this.startRotationFrom(pointerEvent.touches[0].clientX, pointerEvent.touches[0].clientY) break case 2: var t = (pointerEvent.touches[0].clientX - pointerEvent.touches[1].clientX) / window.innerWidth, i = (pointerEvent.touches[0].clientY - pointerEvent.touches[1].clientY) / window.innerHeight this.pinchDistance = Math.sqrt(t * t + i * i) } //this.emit(ControlEvents.InputStart, 'touch') } } onPointerDown(pointerEvent) { if (this.usable()) { if ('touch' === pointerEvent.pointerType) { if (this.pointers.length < this.pointersLimit) { this.pointers.push({ id: pointerEvent.pointerId, clientX: pointerEvent.clientX, clientY: pointerEvent.clientY }) } pointerEvent.touches = this.pointers this.onTouchStart(pointerEvent) //this.emit(ControlEvents.InputStart, 'pointer') } else { this.onMouseDown(pointerEvent) } } } onMouseDown(mouseEvent) { if (this.usable()) { mouseEvent.preventDefault() mouseEvent.stopPropagation() switch (mouseEvent.button) { case MouseButton.LEFT: this.startRotationFrom(mouseEvent.clientX, mouseEvent.clientY) break case MouseButton.RIGHT: this.panStart = true break } //this.emit(ControlEvents.InputStart, 'mouse') } } /** * 根据两帧交互点坐标之间的差值,计算两帧角度差值(rotationDifference)用于旋转 * 1.将两次交互点坐标分别映射到3D空间 * 2.通过两坐标在XY平面上投影,分别计算与X轴夹角,再求差值作为竖直方向角度差值(rotationDifference.y) * 3.通过两坐标在XZ平面上投影,分别计算与X轴夹角,再求差值作为水平方向角度差值(rotationDifference.x) */ updateRotation() { if (this.usable() && this.pointerDragOn) { let camera = this.application.camera camera.matrixWorld = new THREE.Matrix4() //许钟文加 unproject前先把相机置于原点 (player的cameras里的panorama是不更新matrixworld的,只有player的camera才更新。 为了其他的camera加) //两交互点在3D空间的坐标 var pointerDragStart3D = new THREE.Vector3(this.pointerDragStart.x, this.pointerDragStart.y, -1).unproject(camera), pointer3D = new THREE.Vector3(this.pointer.x, this.pointer.y, -1).unproject(camera), //两交互点分别到原点的长度 pointerDragStart3DLength = Math.sqrt(pointerDragStart3D.x * pointerDragStart3D.x + pointerDragStart3D.z * pointerDragStart3D.z), pointer3DLength = Math.sqrt(pointer3D.x * pointer3D.x + pointer3D.z * pointer3D.z), //通过Math.atan2计算在XY面上与X轴的夹角弧度。 //注:因为 z = -1,所以两者到原点的长度近似为x分量(数值的大小也不需要绝对对应) anglePointerDragStart3DToX = Math.atan2(pointerDragStart3D.y, pointerDragStart3DLength), //近似为 anglePointerDragStart3DToX = Math.atan2( pointerDragStart3D.y, pointerDragStart3D.x ) anglePointer3DToX = Math.atan2(pointer3D.y, pointer3DLength) //近似为 anglePointer3DToX = Math.atan2( pointer3D.y, pointer3D.x ) camera.updateMatrix() camera.updateMatrixWorld() //算出两者角度差,作为竖直方向角度差值(rotationDifference.y) this.rotationDifference.y = THREE.MathUtils.radToDeg(anglePointerDragStart3DToX - anglePointer3DToX) //y分量清零,原向量等价于在XZ轴上的投影向量 pointerDragStart3D.y = 0 pointer3D.y = 0 //归一化(/length),求两者夹角作为 //判断方向,最后记为水平方向角度差值(rotationDifference.x) var anglePointerDragStart3DToPointer3D = Math.acos(pointerDragStart3D.dot(pointer3D) / pointerDragStart3D.length() / pointer3D.length()) // isNaN(s) || (this.rotationDifference.x = THREE.MathUtils.radToDeg(s), // this.pointerDragStart.x < this.pointer.x && (this.rotationDifference.x *= -1)), if (!isNaN(anglePointerDragStart3DToPointer3D)) { this.rotationDifference.x = THREE.MathUtils.radToDeg(anglePointerDragStart3DToPointer3D) if (this.pointerDragStart.x < this.pointer.x) { this.rotationDifference.x *= -1 } } this.rotationDifference.multiplyScalar(this.speed) //console.log(pointerDragStart3DLength,pointer3DLength) } } onMouseMove(mouseEvent) { if (this.usable()) { var mouse = common.handelPadding(mouseEvent.clientX, mouseEvent.clientY, this.application.container) math.convertScreenPositionToNDC(this.pointer, mouse, this.application.container.clientWidth, this.application.container.clientHeight) /* if (this.pointerDragOn) { if (Math.abs(this.pointer.x - this.moveStart.x) > this.moveTolerance || Math.abs(this.pointer.y - this.moveStart.y) > this.moveTolerance) { //this.emit(ControlEvents.Move, 'mouse') } } */ //add this.updateRotation() if (this.panStart) { let delta = new THREE.Vector2().subVectors(this.pointer, this.pointerDragStart) let speed = 30 this.translate.set(-delta.x * speed, -delta.y * speed, 0) } //更新pointerDragStart记录当前帧坐标,用于下一帧求帧差值 this.pointerDragStart.copy(this.pointer) } } onTouchMove(pointerEvent) { if (this.usable()) { //this.emit(ControlEvents.Move, 'touch') switch (pointerEvent.touches.length) { case 1: var mouse = common.handelPadding(pointerEvent.touches[0].clientX, pointerEvent.touches[0].clientY, this.application.container) math.convertScreenPositionToNDC(this.pointer, mouse, this.application.container.clientWidth, this.application.container.clientHeight) break case 2: var offsetX = (pointerEvent.touches[0].clientX - pointerEvent.touches[1].clientX) / window.innerWidth, offsetY = (pointerEvent.touches[0].clientY - pointerEvent.touches[1].clientY) / window.innerHeight, n = this.pinchDistance - Math.sqrt(offsetX * offsetX + offsetY * offsetY) if (Math.abs(n) > 0.01) { //this.emit(ControlEvents.InteractionDirect) //this.emit(ControlEvents.Pinch, n) this.pinchDistance -= n } } } } onPointerMove(pointerEvent) { if (this.usable()) { if ('touch' === pointerEvent.pointerType) { this.pointers.forEach(function(t) { if (pointerEvent.pointerId === t.id) { t.clientX = pointerEvent.clientX t.clientY = pointerEvent.clientY } }) pointerEvent.touches = this.pointers this.onTouchMove(pointerEvent) } else { this.onMouseMove(pointerEvent) } } } endRotation() { this.pointerDragOn = !1 var averageVector = math.averageVectors(this.rotationHistory) this.rotationSpeed.set(averageVector.x * rotationAfterMoveMultiplier, averageVector.y * rotationAfterMoveMultiplier) } onTouchEnd(pointerEvent) { if (this.usable()) { pointerEvent.preventDefault() pointerEvent.stopPropagation() this.endRotation() } } onMouseUp(mouseEvent) { if (this.usable()) { mouseEvent.preventDefault() mouseEvent.stopPropagation() this.endRotation() this.panStart = false } } onPointerUp(pointerEvent) { if (this.usable()) { if ('touch' === pointerEvent.pointerType) { this.pointers.forEach( function(t, i) { pointerEvent.pointerId === t.id && this.pointers.splice(i, 1) }.bind(this) ) pointerEvent.touches = this.pointers this.onTouchEnd(pointerEvent) } else { this.onMouseUp(pointerEvent) } } } /** * 主循环更新,主要通过物理上的刚体旋转行为(角位移,角速度,角加速度,摩擦等)计算得到新的相机视点target,主要是每帧瞬时的状态 * * updateRotation()计算每帧对应的旋转量 rotationDifference * * 角位移:rotationDifference与原本lon,lat (等价于phi,theta)累加,得到新的角位移 * 角速度:(rotationDifference数组的平均值 * 速度因子rotationAccelerationInside + 角加速度) - 摩擦rotationFriction。 * * target坐标:新的角位移计算出新的球坐标,转换计算后的球坐标到笛卡尔坐标系 * * @param { number } deltaTime 帧间隔时间。 注:关于帧间隔时间,是个有关物理计算的很重要的值,用于保持物理量与绝对时间的对应而不受帧率的的干扰,下文计算角速度用到。更多请见 https://blog.csdn.net/ChinarCSDN/article/details/82914420 */ animate(event = {}) { let deltaTime = event.delta if (this.locked) return //if(settings.vrEnabled) return; const camera = this.application.camera // 求出新的rotationDifference //this.updateRotation() //记录一组rotationDifference 用于求角速度 rotationSpeed。注:见 endRotation() for (this.rotationHistory.push(this.rotationDifference.clone()); this.rotationHistory.length > rotationAfterMoveHistoryCount; ) { this.rotationHistory.shift() } //计算角位移(交互影响下的) this.lon += this.rotationDifference.x this.lat += this.rotationDifference.y this.rotationDifference.set(0, 0) //计算角速度 this.rotationSpeed.x = this.rotationSpeed.x * (1 - rotationFriction) + this.rotationAcc.x * rotationAccelerationInside this.rotationSpeed.y = this.rotationSpeed.y * (1 - rotationFriction) + this.rotationAcc.y * rotationAccelerationInside //计算角位移(交互后,物理定律影响下的) this.lon += this.rotationSpeed.x * deltaTime this.lat += this.rotationSpeed.y * deltaTime this.lat = Math.max(insideLookLimitDown, Math.min(insideLookLimitUp, this.lat)) //通过预定义的俯仰角最大最小范围(insideLookLimitUp、insideLookLimitDown)来限制俯仰角。 注:这种数学计算很常见,因此API也很常见(clamp),等价于 this.lat = THREE.MathUtils.clamp( this.lat, constants.insideLookLimitDown, settings.insideLookLimitUp ); //转换为标准球坐标参数形式,并最终转换为笛卡尔坐标系下 this.phi = THREE.MathUtils.degToRad(90 - this.lat) this.theta = THREE.MathUtils.degToRad(this.lon) this.lookVector.x = -Math.sin(this.phi) * Math.cos(this.theta) this.lookVector.z = Math.cos(this.phi) this.lookVector.y = Math.sin(this.phi) * Math.sin(this.theta) if (this.translate.length() != 0) { console.log(this.translate.clone()) let vec = this.translate.clone().applyQuaternion(camera.quaternion) camera.position.add(vec) this.translate.set(0, 0, 0) camera.updateMatrix() //update完才能 lookAt } //求taget坐标: 当前相机位置 + 方向向量(对于此处旋转来说距离并无意义,方向向量的1即可) this.target.copy(this.lookVector).add(camera.position) //THREE的API来更新相机旋转。注:lookAt是四阶矩阵比较常见的API,因此此PanoramaControls计算流程,不算与THREE耦合 camera.lookAt(this.target) camera.updateMatrix() this.application.notifyObjectsChanged(camera, this) } /** * 滚轮行为: 触发自定义事件 */ onMouseWheel(wheelEvent) { if (this.usable()) { //var t = wheelEvent.wheelDelta || -wheelEvent.detail //this.emit(ControlEvents.InteractionDirect) //this.emit(ControlEvents.Scroll, t) var delta = 0 if (event.wheelDelta) { // WebKit / Opera / Explorer 9 delta = -event.wheelDelta * 0.005 } else if (event.detail) { // Firefox delta = -0.02 * event.detail } if (delta !== 0) { this.translate.set(0, 0, delta) } //displayMode } } /** * 键盘按下:触发自定义事件 */ onKeyDown(keyboardEvent) { if (!this.player.$app.config.useShortcutKeys) { return } if (this.usable()) { if (keyboardEvent.metaKey || keyboardEvent.ctrlKey) { } else { keyboardEvent.preventDefault() this.handleKeyDown(keyboardEvent.which) } } } handleKeyDown(keyValue) { var t = function(e, t) { this.rotationAcc[e] = t }.bind(this) //this.emit(ControlEvents.InteractionKey) var i = !0 switch (keyValue) { case Keys.LEFTARROW: case Keys.J: t('x', -1) break case Keys.RIGHTARROW: case Keys.L: t('x', 1) break case Keys.I: t('y', 1) break case Keys.K: t('y', -1) break default: i = !1 } //i && this.emit(ControlEvents.Move, 'key') } onKeyUp(keyboardEvent) { if (this.usable()) { keyboardEvent.preventDefault() keyboardEvent.stopPropagation() this.handleKeyUp(keyboardEvent.which) } } handleKeyUp(keyValue) { switch (keyValue) { case Keys.LEFTARROW: case Keys.J: case Keys.RIGHTARROW: case Keys.L: this.rotationAcc.x = 0 break case Keys.I: case Keys.K: this.rotationAcc.y = 0 } } onContextMenu(event) { event.preventDefault() } /** * 给定角加速度,使开始旋转。 注:类似给定力推 */ startRotating(e, t) { e && (this.rotationAcc.x = e) t && (this.rotationAcc.y = t) } /** * 通过物理定律来终止旋转 */ stopRotating(e) { e && (this.rotationSpeed.x = this.rotationSpeed.y = 0) this.rotationAcc.set(0, 0) } reset() { this.pointerDragOn = !1 this.rotationAcc.set(0, 0) this.rotationSpeed.set(0, 0) this.pointers = [] } } export { PanoramaTool }