一、背景
Three.js 是一个前端三维图形展示库。它自带了一个三维图形控件,OrbitControls.js,可以控制三维图形的平移、缩放、旋转。
OrbitControls 是通过控制”照相机“(也就是三维形体的观察者)的位置,以实现对三维形体的平移、缩放,和旋转的。
也就是说:
- 通过平移照相机的位置,实现三维形体在显示屏幕上的平移效果
- 通过调整照相机的远近,实现三维形体在显示屏幕上的缩放效果
- 通过旋转照相机的镜头角度和朝向,实现三维形体在显示屏幕上的旋转
但在实际使用中,这样带来一个问题:
- 在平移了屏幕上的形体之后,相当于把”照相机“平移到了形体侧方
- 这样,照相机镜头的指向,即照相机的正视线,不再对准形体中心的,而是被平移到了形体旁边的位置
- 这时,再进行旋转操作,就是把照相机对准形体旁边的位置旋转,造成形体在屏幕上旋转的轨迹非常混乱
为了解决这个问题,我们需要自己开发一套控件,来实现形体平移之后,仍能围绕形体中心进行旋转的效果。
二、方案
基本思路如下:
- 借鉴 OrbitControls 的事件处理框架,接收鼠标操作和触摸屏上的手势,判断用户是要进行旋转、平移,还是缩放。
- 然后通过操作形体,而不是操作照相机的方式,实现旋转、平移、缩放的效果。
借用 OrbitControls 的事件处理框架:
- 监听 pointerdown 事件,收到该事件后
- 把该 pointer (也就是鼠标的某一个键,或者触摸屏上的某一个手指)加入活动 pointer 列表
- 进行 touchstart 处理(触摸屏)
- 如果只有一个活动的 pointer (单指) - 旋转
- handleTouchStartRotate() – 记录当前单指的坐标
- 如果有两个活动的 pointer(双指)- 平移 + 缩放 - handleTouchStartDollyPan()
- handleTouchStartDolly() - 记录当前两指之间的距离
- handleTouchStartPan() - 记录当前两指中间的坐标
- 进行 mousedown 处理(鼠标)
- 开始监听 pointermove 事件,收到该事件后
- 进行 touchmove 处理 (触摸屏)
- 记录该 pointer 的位置
- handleTouchMoveRotate
- 根据当前 pointer 和初始 pointer 的坐标差值,控制旋转
- 用当前 pointer 的坐标值,更新初始坐标值的位置
- handleTouchMoveDollyPan
- handleTouchMoveDolly
- 根据当前两指间距离和初始两指间距离的比例,计算缩放比例
- dolly()
- 用当前两指间距离,更新初始的两指间距离
- handleTouchMovePan
- 计算当前两指的平均坐标值
- 根据当前坐标和初始坐标,计算移动位置
- pan()
- 用当前两指的平均坐标值,更新初始的两指坐标值
- 进行 mousemove 处理(鼠标)
- 开始监听 pointerup 事件,收到该事件后
- 把该事件相关的 pointer 从 pointer 列表中删除
- 如果当前没有活动的 pointer (鼠标所有按键都松开了,触摸屏上没有手指活动)
- 不再监听 pointermove
- 不再监听 pointerup
- 监听 pointercancel 事件
- 移动浏览器会自动发送 pointercancel 事件,这时我们需要在三维图形的画布 (canvas) 的 CSS 中,把 touch-action 属性设为 none
2.2 功能指标
2.2.1 前提
支持的设备
支持 鼠标操作 和 触摸屏操作
- 鼠标:左键拖动 = 旋转,右键拖动 = 平移,滚轮 = 缩放;按住 ctrl / shift / meta 三个键中的任意一个,可以切换左、右键的效果。
- 触摸屏:单指拖动 = 旋转,双指拖动 = 平移,双指 Pinch = 缩放
目前暂时不支持键盘
支持的投影模式
支持 正交照相机 (orthographic camera) 和 透视照相机 (perspective camera) 两种模式
正交照相机没有 “近大远小” 的效果,不能靠移动照相机的位置进行图形的缩放。
2.2.2 缩放
操作:
要求:缩放效果均匀,不会随着放大或者缩小变化得特别快或者特别慢。
限制:最多放大 5 倍,最小缩到 1/5
2.2.3 平移
操作:
- 鼠标:右键拖动 (或者按住 ctrl / shift / meta 三键之一,同时左键拖动)
- 触摸屏:双指拖动
要求:形体跟随 pointer (鼠标或手指)移动,跟随性良好,
限制:形体不得移出 pointer 控制范围之外,不得移到屏幕之外完全不可见。
2.2.4 旋转
操作:
- 鼠标:左键拖动(或者按住 ctrl / shift / meta 三键之一,同时右键拖动)
- 触摸屏:单指拖动
要求:绕形体中心点旋转,形体跟随 pointer ( 鼠标或手指)转动,跟随性好。
限制:上下旋转不超过 2Pi, 左右旋转不超过 2Pi
注意:
- 双指操作时,Pinch 和 拖动可能同时进行,因此,可能产生 “既平移,又缩放” 的效果
- 一些笔记本电脑的触控板,比如 Dell 大多数型号的笔记本电脑,并不支持实际意义上的多点触控,上面的 pinch 实际上产生的是 mousewheel (鼠标滚轮)事件,而对双指拖动没有任何反应。
三、实现
3.1 缩放
| 正交相机 Orthographic | 透视相机 Perspective |
---|
Mouse | 每次事件缩放固定的比例
- deltaY>0,滚轮向下: 缩小
- deltaY<0,滚轮向上: 放大
比例 = 0.95^zoomSpeed | 也可以直接考虑设置 zoom 参数 |
---|
Touch | 根据两次事件指尖距离的比例 (r), 以 r^zoomSpeed 进行缩放
- 指尖距离变大,r>1,放大
- 指尖距离变小,r<1,缩小
| 统一考虑调整 zoom 参数 |
---|
3.2 平移
根据 camera.zoom 的比例,结合 pointer 在屏幕上的位置变化向量,计算物体移动的距离。
物体移动(x, -y) = Pointer(dx, dy) * 视场(x,y) / 画布 (x,y) / camera.zoom
正交相机 视场(x,y) = camera.right - camera.left , camera.top - camera.bottom
透视相机 视场(x,y) = tan(camera.fov/2)*camera.postion.z*2, tan(camera.fov/2)*camera.postion.z*2*camera.aspect
3.3 旋转
每次事件只做步长为 0.05 的旋转,根据 Pointer(x, y) 的移动方向,进行左右,或者上下旋转。
学习网站参考
Discover three.js!
Three.js – JavaScript 3D Library