Three.js-Raycaster.setFromCamera源码分析

Raycaster.setFromCamera(coords, camera)

这个方式是根据相机和"屏幕"上的一个位置,设置射线对象(类型为Ray)。需要设置的属性有两个:

  1. 射线的起点origin: Vector3
  2. 射线的方向direction: Vector3

首先,要明白的是setFromCamera接收的coords是一个二维向量,表示一个NDC(标准设备坐标)。现在,我们需要根据这个坐标和相机,计算出一条场景世界坐标中的一条射线。

Three.jsWebGL)中,相机可视区域变换到裁剪空间的坐标范围是 <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 );

  }
}
相关推荐
爱喝水的小鼠2 分钟前
Vue3(一) Vite创建Vue3工程,选项式API与组合式API;setup的使用;Vue中的响应式ref,reactive
前端·javascript·vue.js
小晗同学3 分钟前
Vue 实现高级穿梭框 Transfer 封装
javascript·vue.js·elementui
WeiShuai18 分钟前
vue-cli3使用DllPlugin优化webpack打包性能
前端·javascript
forwardMyLife22 分钟前
element-plus的面包屑组件el-breadcrumb
javascript·vue.js·ecmascript
ice___Cpu24 分钟前
Linux 基本使用和 web 程序部署 ( 8000 字 Linux 入门 )
linux·运维·前端
JYbill26 分钟前
nestjs使用ESM模块化
前端
加油吧x青年1 小时前
Web端开启直播技术方案分享
前端·webrtc·直播
吕彬-前端1 小时前
使用vite+react+ts+Ant Design开发后台管理项目(二)
前端·react.js·前端框架
小白小白从不日白2 小时前
react hooks--useCallback
前端·react.js·前端框架
恩婧2 小时前
React项目中使用发布订阅模式
前端·react.js·前端框架·发布订阅模式