123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546 |
- //漫游模式相机控制器
- /**
- * 功能:主要是在漫游模式下控制相机的方向(相机的移动只是做了事件触发,逻辑不在此)
- *
- * 原理:
- * @ 以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 }
|