Three.js-Raycaster.setFromCamera源码分析

Raycaster.setFromCamera(coords, camera)

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

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

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

Three.jsWebGL)中,相机可视区域变换到裁剪空间的坐标范围是 − 1 , 1 3 -1, 1^3 −1,13,而NDC坐标的范围是 − 1 , 1 2 -1, 1^2 −1,12,因此,裁剪空间和NDC坐标的x分量和y分量是一致的。

首先,我们要明确一点,射线的方向平行于裁剪空间的 Z Z Z轴。这对于正交相机和透视相机会有很大的不同。

1. 正交相机

对于正交相机来说,它射出的射线方向始终都是相机"看向"的方向,也就是相机空间中的 ( 0 , 0 , − 1 ) (0, 0, -1) (0,0,−1)表示的方向,现在我们需要将这个方向从相机坐标变换到世界坐标。

怎么变换一个方向

我们常见的顶点位置变换为: P ′ = M ⋅ P P'=M \cdot P P′=M⋅P。其中 P P P是变换前的坐标, P ′ P' P′是变换后的坐标, M M M是变换矩阵,我们这里变换的是向量表示的位置。

但是,如果向量 P P P表示一个方向,使用同样的变换 M M M,变换后的方向是什么?

向量 P P P表示一个方向,其实隐含的意思就是,起点为原点 O O O,终点为 P P P的向量 V = P − O V=P-O V=P−O。那么,我们将这两个点都进行变换,则为变换后的方向 V ′ = P ′ − O ′ V'=P'-O' V′=P′−O′。
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} 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();

}

计算正交相机发射的射线方向

就像上面我们说的, ( 0 , 0 , − 1 ) (0, 0, -1) (0,0,−1)是在相机空间中,投射的方向,需要推这个向量进行变换。我们只需要知道相机的世界变换矩阵 m a t r i x W o r l d c a m e r a matrixWorld_{camera} matrixWorldcamera,然后变换即可。

javascript 复制代码
// camera已知

const direction = new Vector3(0, 0, -1).transformDirection(camera.matrixWorld)

计算正交相机发射的射线起点

在相机空间中,相机的坐标是 ( 0 , 0 , 0 ) (0, 0, 0) (0,0,0),只需要对齐次坐标 ( 0 , 0 , 0 , 1 ) (0, 0, 0, 1) (0,0,0,1)应用投影变换即可得到相机在裁剪空间中的坐标。

因为我们已经知道了NDC的坐标,就像前面说的,其实已经知道了射线起点在裁剪空间中的 x x x和 y y y坐标(等于NDC坐标),现在要计算未知的 z z z坐标。

我们知道,相机的投影变换在 Z Z Z轴上将相机空间的 n e a r near near和 f a r far far变换为了 − 1 -1 −1和 1 1 1。而相机在相机空间的原点,所以我们关心的是在这个过程中, 0 0 0变换后的结果是什么? 即:
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} M2×2near1=−11
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} M2×2far1=11

我们现在需要求 z z z,可以写为:
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} M2×201=z1

关键点就是求出 M 2 × 2 M_{2 \times 2} M2×2:
设 , M = a b c d 设,M=\begin{bmatrix} a&b\\c&d \end{bmatrix} 设,M=acbd

则可列方程
{ 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} ⎩ ⎨ ⎧a⋅near+b=−1c⋅near+d=1a⋅far+b=1c⋅far+d=1

解得:
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} M=far−near20near−farnear+far1

则:
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} z1=far−near20near−farnear+far101
z = n e a r + f a r n e a r − f a r z=\frac{near+far}{near-far} z=near−farnear+far

现在,我们已经知道了射线起点在裁剪空间得坐标 o r i g i n c l i p origin_{clip} originclip,现在需要将它变换为世界坐标:

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} 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. 透视相机

对于透视相机来说,它的射线的起点就是相机的世界坐标。

那怎么求射线的方向呢?我们目前已经知道了射线的起点,只要再随便知道一个射线上的点,就可以计算出射线的方向。不要忘了,我们已经知道,在裁剪空间中的 x x x和 y y y了,射线是平行于裁剪空间的,我们随便去一个 − 1 , 1 -1, 1 −1,1之间的 z z z即可(在这个区间内,一定不会和相机的 z z 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 );

  }
}
相关推荐
Avan_菜菜3 小时前
AI 能写代码了,为什么我反而开始要求它先写文档?
前端·github·ai编程
JieE2126 小时前
LeetCode 226. 翻转二叉树|JS 递归超详细拆解,二叉树入门经典题
javascript·算法
JieE2126 小时前
LeetCode 104. 二叉树的最大深度|递归思路超详细拆解
javascript·算法
爱勇宝7 小时前
鸿蒙生态的下半场:开发者不只要能开发,还要能赚钱
android·前端·程序员
IT_陈寒10 小时前
SpringBoot这个自动配置坑我跳了三次
前端·人工智能·后端
kyriewen10 小时前
我用 AI 一周写完了整个项目,上线第一天就崩了——这是我踩过最贵的 5 个坑
前端·javascript·ai编程
Larcher10 小时前
AI Loop:让AI像人一样自主完成任务的核心机制
javascript·人工智能·设计模式
默_笙10 小时前
🃏 JS 只有 8 种数据类型,但我花了 2 天才搞懂 null 和 undefined 的区别
javascript
牧艺10 小时前
从零到协同:构建类飞书在线文档系统的五个技术重难点
前端·人工智能
jump_jump11 小时前
流式 HTML:从 htmx 片段装配到浏览器原生增量渲染
javascript·性能优化·前端工程化