前言
本篇主要讲解透视相机轨道控制器。
透视相机轨道控制器
透视相机的位移轨道
透视相机的位移轨道和正交相机的位移轨道是相同原理的,都是对相机视点和目标点的平移。
代码实现
接下来咱们直接说一下代码实现。在学习过正交相机轨道控制器之后,学习起来透视相机轨道控制器就会轻松很多。
1.建透视相机
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const eye = new Vector3(0, 0.5, 1) const target = new Vector3(0, 0, -2.5) const up = new Vector3(0, 1, 0)
const [fov, aspect, near, far] = [ 45, canvas.width / canvas.height, 1, 20 ] const camera = new PerspectiveCamera(fov, aspect, near, far) camera.position.copy(eye) camera.lookAt(target) camera.updateWorldMatrix(true)
|
2.在正交相机的位移轨道的基础上改一下pan方法
将鼠标在画布中的位移量转目标平面位移量
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const {matrix,position,up}=camera const {clientWidth,clientHeight}=canvas
const sightLen = position.clone().sub(target).length()
const halfFov = fov * Math.PI / 360
const targetHeight = sightLen * Math.tan(halfFov) * 2
const ratio = targetHeight / clientHeight
const distanceLeft = x * ratio const distanceUp = y * ratio
|
注:目标平面是过视点,平行于裁剪面的平面,也就是如上图中过target的那个平面。
将鼠标在目标平面中的位移量转世界坐标
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
const mx = new Vector3().setFromMatrixColumn(matrix, 0)
const myOrz = new Vector3() if (screenSpacePanning) { myOrz.setFromMatrixColumn(matrix, 1) } else { myOrz.crossVectors(up, mx) }
const vx = mx.clone().multiplyScalar(-distanceLeft) const vy = myOrz.clone().multiplyScalar(distanceUp) panOffset.copy(vx.add(vy))
|
透视相机的缩放轨道
透视相机缩放是通过视点按照视线的方向,接近或者远离目标点来实现的。
代码实现
我们可以直接在正交相机缩放轨道的基础上做一下修改。
1 2 3
| function dolly(dollyScale) { camera.position.lerp(target, 1 - dollyScale) }
|
lerp ( v : Vector3, alpha : Float ) 按比例取两点之间的插值
其源码如下:
1 2 3 4 5 6
| lerp( v, alpha ) { this.x += ( v.x - this.x ) * alpha; this.y += ( v.y - this.y ) * alpha; this.z += ( v.z - this.z ) * alpha; return this; }
|
dollyScale:(位移之后视点与目标点的距离)/(位移前,视点与与目标点的距离)
1-dollyScale:(视点即将位移的距离)/(位移前,视点于与目标点的距离)
透视相机缩放轨道的基本实现原理就是这么简单。
然而,后面我们还得用球坐标对相机进行旋转,球坐标是已经涵盖了相机视点位的。
因此,我们还可以直接把相机视点位写进球坐标里。
球坐标缩放
1.像正交相机的旋转轨道那样,定义球坐标对象。
1 2 3 4
| const spherical = new Spherical() .setFromVector3( camera.position.clone().sub(target) )
|
2.修改旋转方法
1 2 3
| function dolly(dollyScale) { spherical.radius*=dollyScale }
|
3.更新方法也和正交相机的旋转轨道一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| function update() { target.add(panOffset) camera.position.add(panOffset)
const rotateOffset = new Vector3() .setFromSpherical(spherical) camera.position.copy( target.clone().add(rotateOffset) )
camera.lookAt(target) camera.updateMatrixWorld(true) pvMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse, )
spherical.setFromVector3( camera.position.clone().sub(target) ) panOffset.set(0, 0, 0)
render() }
|
透视相机的旋转轨道
透视相机的旋转轨道和正交相机的实现原理都是一样的,可以用球坐标系实现,也可以用轨迹球实现。
基于球坐标系的旋转轨道
可直接参考正交相机基于球坐标系的旋转轨道来写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| const spherical = new Spherical() .setFromVector3( camera.position.clone().sub(target) )
const rotateDir = 'xy'
……
canvas.addEventListener('pointermove', ({ clientX, clientY }) => { dragEnd.set(clientX, clientY) switch (state) { case 'pan': pan(dragEnd.clone().sub(dragStart)) break case 'rotate': rotate(dragEnd.clone().sub(dragStart)) break } dragStart.copy(dragEnd) }) ……
function rotate({ x, y }) { const { clientHeight } = canvas const deltaT = pi2 * x / clientHeight const deltaP = pi2 * y / clientHeight if (rotateDir.includes('x')) { spherical.theta -= deltaT } if (rotateDir.includes('y')) { const phi = spherical.phi - deltaP spherical.phi = Math.min( Math.PI * 0.99999999, Math.max(0.00000001, phi) ) } update() }
function update() { target.add(panOffset) camera.position.add(panOffset)
const rotateOffset = new Vector3() .setFromSpherical(spherical) camera.position.copy( target.clone().add(rotateOffset) )
camera.lookAt(target) camera.updateMatrixWorld(true) pvMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse, )
spherical.setFromVector3( camera.position.clone().sub(target) ) panOffset.set(0, 0, 0)
render() }
|
对于轨迹球的旋转轨道
基于正交相机轨迹球旋转的代码略作调整即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| const quaternion = new Quaternion()
function rotate({ x, y }) { const { matrix, position, fov } = camera const { clientHeight } = canvas
const ratioY = -y / clientHeight const ratioBaseHeight = x / clientHeight const ratioLen = new Vector2(ratioBaseHeight, ratioY).length() const angle = ratioLen * pi2
const sightLen = position.clone().sub(target).length() const halfFov = fov * Math.PI / 360 const targetHeight = sightLen * Math.tan(halfFov) * 2 const ratio = targetHeight / clientHeight const distanceLeft = x * ratio const distanceUp = -y * ratio
const mx = new Vector3().setFromMatrixColumn(matrix, 0) const my = new Vector3().setFromMatrixColumn(matrix, 1) const vx = mx.clone().multiplyScalar(distanceLeft) const vy = my.clone().multiplyScalar(distanceUp) const moveDir = vx.clone().add(vy).normalize()
const eyeDir = position.clone().sub(target).normalize() const axis = moveDir.clone().cross(eyeDir)
quaternion.setFromAxisAngle(axis, angle)
update() }
function update() { target.add(panOffset) camera.position.add(panOffset)
const rotateOffset = camera.position.clone() .sub(target) .applyQuaternion(quaternion)
camera.position.copy( target.clone().add(rotateOffset) ) camera.up.applyQuaternion(quaternion)
camera.lookAt(target) camera.updateMatrixWorld(true) pvMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse, )
quaternion.setFromRotationMatrix(new Matrix4()) panOffset.set(0, 0, 0)
render() }
|
结语
本篇文章就先到这里了,债见~