Raycaster.setFromCamera(coords, camera)
这个方式是根据相机和"屏幕"上的一个位置,设置射线对象(类型为Ray
)。需要设置的属性有两个:
- 射线的起点
origin: Vector3
- 射线的方向
direction: Vector3
首先,要明白的是setFromCamera
接收的coords
是一个二维向量,表示一个NDC
(标准设备坐标)。现在,我们需要根据这个坐标和相机,计算出一条场景世界坐标中的一条射线。
在Three.js
(WebGL
)中,相机可视区域变换到裁剪空间的坐标范围是 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ − 1 , 1 ] 3 [-1, 1]^3 </math>[−1,1]3,而NDC
坐标的范围是 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ − 1 , 1 ] 2 [-1, 1]^2 </math>[−1,1]2,因此,裁剪空间和NDC
坐标的x
分量和y
分量是一致的。
首先,我们要明确一点,射线的方向平行于裁剪空间的 <math xmlns="http://www.w3.org/1998/Math/MathML"> Z Z </math>Z轴。这对于正交相机和透视相机会有很大的不同。
1. 正交相机
对于正交相机来说,它射出的射线方向始终都是相机"看向"的方向,也就是相机空间中的 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( 0 , 0 , − 1 ) (0, 0, -1) </math>(0,0,−1)表示的方向,现在我们需要将这个方向从相机坐标变换到世界坐标。
怎么变换一个方向
我们常见的顶点位置变换为: <math xmlns="http://www.w3.org/1998/Math/MathML"> P ′ = M ⋅ P P'=M \cdot P </math>P′=M⋅P。其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> P P </math>P是变换前的坐标, <math xmlns="http://www.w3.org/1998/Math/MathML"> P ′ P' </math>P′是变换后的坐标, <math xmlns="http://www.w3.org/1998/Math/MathML"> M M </math>M是变换矩阵,我们这里变换的是向量表示的位置。
但是,如果向量 <math xmlns="http://www.w3.org/1998/Math/MathML"> P P </math>P表示一个方向,使用同样的变换 <math xmlns="http://www.w3.org/1998/Math/MathML"> M M </math>M,变换后的方向是什么?
向量 <math xmlns="http://www.w3.org/1998/Math/MathML"> P P </math>P表示一个方向,其实隐含的意思就是,起点为原点 <math xmlns="http://www.w3.org/1998/Math/MathML"> O O </math>O,终点为 <math xmlns="http://www.w3.org/1998/Math/MathML"> P P </math>P的向量 <math xmlns="http://www.w3.org/1998/Math/MathML"> V = P − O V=P-O </math>V=P−O。那么,我们将这两个点都进行变换,则为变换后的方向 <math xmlns="http://www.w3.org/1998/Math/MathML"> V ′ = P ′ − O ′ V'=P'-O' </math>V′=P′−O′。
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> V ′ = P ′ − O ′ = M ⋅ P − M ⋅ O = M ⋅ ( P − O ) = M ⋅ ( [ P x P y P z 1 ] − [ 0 0 0 1 ] ) = [ i x j x k x t x i y j y k y t y i z j z k z t z 0 0 0 1 ] ⋅ [ P x P y P z 0 ] = [ i x ⋅ P x + j x ⋅ P y + k x ⋅ P z + 0 i y ⋅ P y + j y ⋅ P y + k y ⋅ P z + 0 i z ⋅ P z + j z ⋅ P y + k z ⋅ P z + 0 0 + 0 + 0 + 0 ] \begin{aligned} V'&= P'-O' \\ &= M \cdot P - M \cdot O \\ &= M \cdot (P - O) \\ &= M \cdot (\begin{bmatrix} P_{x}\\ P_{y}\\ P_{z}\\ 1\\ \end{bmatrix} - \begin{bmatrix} 0\\ 0\\ 0\\ 1\\ \end{bmatrix} ) \\ &= \begin{bmatrix} i_{x}&j_{x}&k_{x}&t{x}\\ i_{y}&j_{y}&k_{y}&t{y}\\ i_{z}&j_{z}&k_{z}&t{z}\\ 0&0&0&1\\ \end{bmatrix} \cdot \begin{bmatrix} P_{x}\\ P_{y}\\ P_{z}\\ 0\\ \end{bmatrix} \\ &= \begin{bmatrix} i_{x} \cdot P_{x} + j_{x} \cdot P_{y} + k_{x} \cdot P{z} + 0\\ i_{y} \cdot P_{y} + j_{y} \cdot P_{y} + k_{y} \cdot P{z} + 0\\ i_{z} \cdot P_{z} + j_{z} \cdot P_{y} + k_{z} \cdot P{z} + 0\\ 0+0+0+0\\ \end{bmatrix} \end{aligned} </math>V′=P′−O′=M⋅P−M⋅O=M⋅(P−O)=M⋅(⎣ ⎡PxPyPz1⎦ ⎤−⎣ ⎡0001⎦ ⎤)=⎣ ⎡ixiyiz0jxjyjz0kxkykz0txtytz1⎦ ⎤⋅⎣ ⎡PxPyPz0⎦ ⎤=⎣ ⎡ix⋅Px+jx⋅Py+kx⋅Pz+0iy⋅Py+jy⋅Py+ky⋅Pz+0iz⋅Pz+jz⋅Py+kz⋅Pz+00+0+0+0⎦ ⎤
可以看到,其实就是变换矩阵左上角的子矩阵和三维向量相乘。 在Three.js
中可以找到相关源码:
javascript
// 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();
}
计算正交相机发射的射线方向
就像上面我们说的, <math xmlns="http://www.w3.org/1998/Math/MathML"> ( 0 , 0 , − 1 ) (0, 0, -1) </math>(0,0,−1)是在相机空间中,投射的方向,需要推这个向量进行变换。我们只需要知道相机的世界变换矩阵 <math xmlns="http://www.w3.org/1998/Math/MathML"> m a t r i x W o r l d c a m e r a matrixWorld_{camera} </math>matrixWorldcamera,然后变换即可。
javascript
// camera已知
const direction = new Vector3(0, 0, -1).transformDirection(camera.matrixWorld)
计算正交相机发射的射线起点
在相机空间中,相机的坐标是 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( 0 , 0 , 0 ) (0, 0, 0) </math>(0,0,0),只需要对齐次坐标 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( 0 , 0 , 0 , 1 ) (0, 0, 0, 1) </math>(0,0,0,1)应用投影变换即可得到相机在裁剪空间中的坐标。
因为我们已经知道了NDC
的坐标,就像前面说的,其实已经知道了射线起点在裁剪空间中的 <math xmlns="http://www.w3.org/1998/Math/MathML"> x x </math>x和 <math xmlns="http://www.w3.org/1998/Math/MathML"> y y </math>y坐标(等于NDC
坐标),现在要计算未知的 <math xmlns="http://www.w3.org/1998/Math/MathML"> z z </math>z坐标。
我们知道,相机的投影变换在 <math xmlns="http://www.w3.org/1998/Math/MathML"> Z Z </math>Z轴上将相机空间的 <math xmlns="http://www.w3.org/1998/Math/MathML"> n e a r near </math>near和 <math xmlns="http://www.w3.org/1998/Math/MathML"> f a r far </math>far变换为了 <math xmlns="http://www.w3.org/1998/Math/MathML"> − 1 -1 </math>−1和 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1。而相机在相机空间的原点,所以我们关心的是在这个过程中, <math xmlns="http://www.w3.org/1998/Math/MathML"> 0 0 </math>0变换后的结果是什么? 即:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> M 2 × 2 [ n e a r 1 ] = [ − 1 1 ] \begin{aligned} M_{2 \times 2} \begin{bmatrix} near \\ 1 \\ \end{bmatrix} &= \begin{bmatrix} -1 \\ 1 \\ \end{bmatrix} \end{aligned} </math>M2×2[near1]=[−11]
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> M 2 × 2 [ f a r 1 ] = [ 1 1 ] \begin{aligned} M_{2 \times 2} \begin{bmatrix} far \\ 1 \\ \end{bmatrix} &= \begin{bmatrix} 1 \\ 1 \\ \end{bmatrix} \end{aligned} </math>M2×2[far1]=[11]
我们现在需要求 <math xmlns="http://www.w3.org/1998/Math/MathML"> z z </math>z,可以写为:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> M 2 × 2 [ 0 1 ] = [ z 1 ] \begin{aligned} M_{2 \times 2} \begin{bmatrix} 0 \\ 1 \\ \end{bmatrix} &= \begin{bmatrix} z \\ 1 \\ \end{bmatrix} \end{aligned} </math>M2×2[01]=[z1]
关键点就是求出 <math xmlns="http://www.w3.org/1998/Math/MathML"> M 2 × 2 M_{2 \times 2} </math>M2×2:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 设 , M = [ a b c d ] 设,M=\begin{bmatrix} a&b\\c&d \end{bmatrix} </math>设,M=[acbd]
则可列方程
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> { a ⋅ n e a r + b = − 1 c ⋅ n e a r + d = 1 a ⋅ f a r + b = 1 c ⋅ f a r + d = 1 \begin{cases} a \cdot near + b = -1 \\ c \cdot near + d = 1 \\ a \cdot far + b = 1 \\ c \cdot far + d = 1 \end{cases} </math>⎩ ⎨ ⎧a⋅near+b=−1c⋅near+d=1a⋅far+b=1c⋅far+d=1
解得:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> M = [ 2 f a r − n e a r n e a r + f a r n e a r − f a r 0 1 ] M=\begin{bmatrix} \frac{2}{far-near} & \frac{near+far}{near-far}\\0&1 \end{bmatrix} </math>M=[far−near20near−farnear+far1]
则:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> [ z 1 ] = [ 2 f a r − n e a r n e a r + f a r n e a r − f a r 0 1 ] [ 0 1 ] \begin{bmatrix} z \\ 1 \end{bmatrix}=\begin{bmatrix} \frac{2}{far-near} & \frac{near+far}{near-far}\\0&1 \end{bmatrix} \begin{bmatrix} 0 \\ 1 \end{bmatrix} </math>[z1]=[far−near20near−farnear+far1][01]
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> z = n e a r + f a r n e a r − f a r z=\frac{near+far}{near-far} </math>z=near−farnear+far
现在,我们已经知道了射线起点在裁剪空间得坐标 <math xmlns="http://www.w3.org/1998/Math/MathML"> o r i g i n c l i p origin_{clip} </math>originclip,现在需要将它变换为世界坐标:
<math xmlns="http://www.w3.org/1998/Math/MathML"> o r i g i n w o r l d = M a t r i x W o l r d c a m e r a M a t r i x p r o j e c t i o n − 1 o r i g i n c l i p o origin_{world}=MatrixWolrd_{camera}Matrix_{projection}^{-1}origin_{clipo} </math>originworld=MatrixWolrdcameraMatrixprojection−1originclipo
先通过投影矩阵的逆矩阵,从裁剪空间变换到相机空间,再通过相机的世界变换矩阵变换到世界坐标。这个方法在Vector3
中的实现如下:
javascript
// src/math/Vector3
unproject( camera ) {
return this.applyMatrix4( camera.projectionMatrixInverse ).applyMatrix4( camera.matrixWorld );
}
最终,求射线起点的代码如下:
javascript
// coords和camera已知
const z = (camera.near + camera.far) / (camera.near - camera. far);
const origin = new Vector3(coords.x, coords.y, z).unproject(camera);
小结
我们直接看一下setFromCamera
的源码:
javascript
// 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. 透视相机
对于透视相机来说,它的射线的起点就是相机的世界坐标。
那怎么求射线的方向呢?我们目前已经知道了射线的起点,只要再随便知道一个射线上的点,就可以计算出射线的方向。不要忘了,我们已经知道,在裁剪空间中的 <math xmlns="http://www.w3.org/1998/Math/MathML"> x x </math>x和 <math xmlns="http://www.w3.org/1998/Math/MathML"> y y </math>y了,射线是平行于裁剪空间的,我们随便去一个 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ − 1 , 1 ] [-1, 1] </math>[−1,1]之间的 <math xmlns="http://www.w3.org/1998/Math/MathML"> z z </math>z即可(在这个区间内,一定不会和相机的 <math xmlns="http://www.w3.org/1998/Math/MathML"> z z </math>z重合),Three.js
源码中的取值是0.5
。
直接看源码:
javascript
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 );
}
}