【可视化学习】86-从入门到放弃WebGL(十七)
发表于:2024-09-04 |

前言

本篇主要讲解透视相机轨道控制器。

透视相机轨道控制器

透视相机的位移轨道

透视相机的位移轨道和正交相机的位移轨道是相同原理的,都是对相机视点和目标点的平移。

代码实现

接下来咱们直接说一下代码实现。在学习过正交相机轨道控制器之后,学习起来透视相机轨道控制器就会轻松很多。

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
//相机平移方向
//鼠标水平运动时,按照相机本地坐标的x轴平移相机
const mx = new Vector3().setFromMatrixColumn(matrix, 0)
//鼠标水平运动时,按照相机本地坐标的y轴,或者-z轴平移相机
const myOrz = new Vector3()
if (screenSpacePanning) {
//y轴,正交相机中默认
myOrz.setFromMatrixColumn(matrix, 1)
} else {
//-z轴,透视相机中默认
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)
)
//'xy','x','y'
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

/* 1.基于鼠标拖拽距离计算旋转量 */
// 鼠标位移距离在画布中的占比
const ratioY = -y / clientHeight
//基于高度的x位置比-用于旋转量的计算
const ratioBaseHeight = x / clientHeight
//位移量
const ratioLen = new Vector2(ratioBaseHeight, ratioY).length()
//旋转量
const angle = ratioLen * pi2

/* 2.将鼠标在画布中的位移量转目标平面位移量 */
//视线长度:相机视点到目标点的距离
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

/* 3.将鼠标在目标平面中的位移量转世界坐标,并从中提取鼠标在世界坐标系中的位移方向 */
// 相机本地坐标系的x,y轴
const mx = new Vector3().setFromMatrixColumn(matrix, 0)
const my = new Vector3().setFromMatrixColumn(matrix, 1)
// 将鼠标在相机世界的x,y轴向的位移量转换为世界坐标位
const vx = mx.clone().multiplyScalar(distanceLeft)
const vy = my.clone().multiplyScalar(distanceUp)
//鼠标在世界坐标系中的位移方向-x轴
const moveDir = vx.clone().add(vy).normalize()

/* 4.基于位移方向和视线获取旋转轴 */
//目标点到视点的单位向量-z轴
const eyeDir = position.clone().sub(target).normalize()
//基于位移方向和视线获取旋转轴-上方向y轴
const axis = moveDir.clone().cross(eyeDir)

/* 5.基于旋转轴和旋转量更新四元数 */
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()
}

结语

本篇文章就先到这里了,债见~

上一篇:
帮后端同事爬数据记录
下一篇:
【可视化学习】85-3D后期效果(一)