Raycaster.setFromCamera(coords, camera)
这个方式是根据相机和“屏幕”上的一个位置,设置射线对象(类型为Ray)。需要设置的属性有两个:
- 射线的起点
origin: Vector3 - 射线的方向
direction: Vector3
首先,要明白的是setFromCamera接收的coords是一个二维向量,表示一个NDC(标准设备坐标)。现在,我们需要根据这个坐标和相机,计算出一条场景世界坐标中的一条射线。
在Three.js(WebGL)中,相机可视区域变换到裁剪空间的坐标范围是,而NDC坐标的范围是,因此,裁剪空间和NDC坐标的x分量和y分量是一致的。
首先,我们要明确一点,射线的方向平行于裁剪空间的轴。这对于正交相机和透视相机会有很大的不同。
1. 正交相机
对于正交相机来说,它射出的射线方向始终都是相机“看向”的方向,也就是相机空间中的表示的方向,现在我们需要将这个方向从相机坐标变换到世界坐标。
怎么变换一个方向
我们常见的顶点位置变换为:。其中是变换前的坐标,是变换后的坐标,是变换矩阵,我们这里变换的是向量表示的位置。
但是,如果向量表示一个方向,使用同样的变换,变换后的方向是什么?
向量表示一个方向,其实隐含的意思就是,起点为原点,终点为的向量。那么,我们将这两个点都进行变换,则为变换后的方向。
可以看到,其实就是变换矩阵左上角的子矩阵和三维向量相乘。
在Three.js中可以找到相关源码:
// src/math/Vector3.js
transformDirection( m ) {
// input: THREE.Matrix4 affine matrix
// input: 一个仿射变换矩阵m
// vector interpreted as a direction
// 将当前三维向量当作方向
// 相当于四维向量[x, y, z, 0]
const x = this.x, y = this.y, z = this.z;
const e = m.elements;
// 矩阵是按列存储的,矩阵元素和下表的关系为
// 0 4 8 12
// 1 5 9 13
// 2 6 10 14
// 3 7 11 15
// 这里就是我们上边推导的公式
this.x = e[ 0 ] * x + e[ 4 ] * y + e[ 8 ] * z;
this.y = e[ 1 ] * x + e[ 5 ] * y + e[ 9 ] * z;
this.z = e[ 2 ] * x + e[ 6 ] * y + e[ 10 ] * z;
// 向量归一化
return this.normalize();
}
计算正交相机发射的射线方向
就像上面我们说的,是射线在相机空间中的方向。现在需要将射线在相机空间中的方向,变换到世界空间。我们只需要知道相机的世界变换矩阵,然后变换即可。
// camera已知
const direction = new Vector3(0, 0, -1).transformDirection(camera.matrixWorld)
计算正交相机发射的射线起点
当从某一个NDC坐标发射一条射线时,我们已经知道了NDC的坐标,也就是射线起点在裁剪空间中的和坐标(等于NDC坐标),现在要计算未知的坐标。
我们知道,相机的投影变换在轴上将相机空间的和变换为了和。而相机在相机空间的原点,所以我们关心的是在这个过程中,变换后的结果是什么?
即:
我们现在需要求,可以写为:
关键点就是求出:
则可列方程
解得:
则:
现在,我们已经知道了射线起点在裁剪空间得坐标,现在需要将它变换为世界坐标:
先通过投影矩阵的逆矩阵,从裁剪空间变换到相机空间,再通过相机的世界变换矩阵变换到世界坐标。这个方法在Vector3中的实现如下:
// src/math/Vector3
unproject( camera ) {
return this.applyMatrix4( camera.projectionMatrixInverse ).applyMatrix4( camera.matrixWorld );
}
最终,求射线起点的代码如下:
// coords和camera已知
const z = (camera.near + camera.far) / (camera.near - camera. far);
const origin = new Vector3(coords.x, coords.y, z).unproject(camera);
小结
我们直接看一下setFromCamera的源码:
// src/core/Raycaster
setFromCamera( coords, camera ) {
if ( camera.isPerspectiveCamera ) {
// 透视相机我们下面讨论
} else if ( camera.isOrthographicCamera ) {
this.ray.origin.set( coords.x, coords.y, ( camera.near + camera.far ) / ( camera.near - camera.far ) ).unproject( camera ); // set origin in plane of camera
this.ray.direction.set( 0, 0, - 1 ).transformDirection( camera.matrixWorld );
this.camera = camera;
} else {
console.error( 'THREE.Raycaster: Unsupported camera type: ' + camera.type );
}
}
2. 透视相机
对于透视相机来说,它的射线的起点就是相机的世界坐标。
那怎么求射线的方向呢?我们目前已经知道了射线的起点,只要再随便知道一个射线上的点,就可以计算出射线的方向。
我们已经知道,在裁剪空间中的和了(NDC坐标),因为射线是平行于裁剪空间的,所以我们随便在裁剪空间中取一个之间的即可,Three.js源码中的取值是0.5。
为什么取的范围?因为在相机空间中的原点(相机位置)不在的范围内;经过投影变换后,也就是说在相机位置的坐标,在投影空间中一定不在的范围内。这样,我们在把在裁剪空间中随便选的一点变换到世界空间后,一定不会和起点(即,相机在世界空间的位置)重合。
直接看源码:
setFromCamera( coords, camera ) {
// src/core/Raycaster
if ( camera.isPerspectiveCamera ) {
this.ray.origin.setFromMatrixPosition( camera.matrixWorld ); // 将相机的世界坐标作为起点
// 在裁剪空间中的射线上取一点转换到世界坐标 然后和起点相减求出方向
this.ray.direction.set( coords.x, coords.y, 0.5 ).unproject( camera ).sub( this.ray.origin ).normalize();
this.camera = camera;
} else if ( camera.isOrthographicCamera ) {
// 上面已经介绍过了
} else {
console.error( 'THREE.Raycaster: Unsupported camera type: ' + camera.type );
}
}