Unity 中的 ScreenPointToRay 方法

在 Unity 射线检测中,常常会用到 Camera.ScreenPointToRay 方法。这个方法很简单,传入一个屏幕上的像素坐标,返回一条在世界空间下从 Camera 的近裁剪面出发穿过屏幕上的像素坐标点的射线。

ScreenPointToRay 方法

Resulting ray is in world space, starting on the near plane of the camera and going through position's (x,y) pixel coordinates on the screen (position.z is ignored).

ScreenPointToRay 方法生成一条从近裁剪面出发,穿过屏幕像素坐标点的一条射线。该方法除了传入一个屏幕像素坐标作为参数以外,还有一个重载方法,需要多传入一个 Camera.MonoOrStereoscopicEye 类型的参数,用于指定使用哪一种 Camera eye。通常在立方体渲染会用到,这里不做过多解析。

Camera 的一些属性

接下来主要看看 Ray ScreenPointToRay(Vector3 pos) 如何得到一条射线。由于计算会涉及到 Camera 的一些属性,所以先来简单了解一下下面这几个 Camera 属性的定义。

  • Field Of View(当 Projection 设置为 Perspective 生效)

摄像机视野角度的宽度,沿 Y 轴方向扩张。

  • Viewport Rect,包含 X、Y、W 和 H(范围均在 0 - 1)

用于指定 Camera 渲染的图像绘制在屏幕的位置和区域大小。其中 X 和 Y 是摄像机视图在屏幕上绘制的左下角坐标;W 和 H 是视图所在的宽和高。

  • Clipping Planes,包含 Near 和 Far。

裁剪面距离摄像机的距离。Near 是近裁剪面距离摄像机对象的距离;Far 是远裁剪面距离摄像机对象的距离。

  • Target Display

目标展示的窗口展示器,比如手机屏幕、平板屏幕等,和物理分辨率有关系。

如何计算得到屏幕像素点的射线

了解了 Camera 上面的这几个属性后,接着就来计算一下近裁剪面在世界空间下的大小和位置。在接下来的计算中,Camera 的投影模式如未提及,则默认为 Perspective

首先配置 Camera 的相关参数: 将 Field Of View 设置为 60;Clipping Planes 的 Near 和 Far 分别设置为 1 和 20;Viewport Rect 中的 X、Y、W、H 分别使用默认参数 0、0、1、1;Target Display 设置为 Display1(此时真实分辨率为 200*200);Camera Transform 的 Position 为 (0,1,-10)。

上面的配置完成后,取屏幕像素坐标 (0, 0) 处作为目标点作为参数传入 ScreenPointToRay 方法(需要的参数是 Vector3 类型,z 轴会被忽略),运行 Unity 看看返回结果 Ray 的信息。

1
2
Ray ray = Camera.main.ScreenPointToRay(new Vector3(0, 0, 0));
Debug.Log("Ray origin: " + ray.origin + ", Ray direction: " + ray.direction);

运行后 Console 打印如下所示:

unity_camera_screen_point_to_ray_1.png

屏幕像素坐标 (0, 0) 处使用 ScreenPointToRay 方法得到的 Ray origin 值是 (-0.6,0.4,-9.0),direction 值为 (-0.4,-0.4,0.8),这些值是如何计算得到的了?

此时 Camera 的视锥体截面图大致如下:

unity_camera_screen_point_to_ray_3.png

对于近裁剪面的高度,已知 FOV 和 Near,简单的三角函数就能求得。如下:

\[ nearClipPanelHeight = tan(\frac{FOV}{2}) \times Near \times 2 \]

远裁剪面的高度也可以使用类似的方式求得,也就是:

\[ farClipPanelHeight = tan(\frac{FOV}{2}) \times Far \times 2 \]

那近裁剪面的宽度如何求得了? 我在 Unity 中 Camera 的横纵比由其参数 Target Display 所在的真实比例和 Viewport Rect 中的 W 和 H 属性共同决定,因为在上面我们将 Viewport Rect 设置为默认值,所以 Camera 的横纵比就是其参数 Target Display 所在的显示视图的比例(如果设置了 Viewport Rect 中的值不是默认值,那么计算横纵比也要将 Viewport Rect 考虑进去)。此时 Target Display 的 Display1 分辨率为 200*200,所以 Camera 的横纵比就是 1:1。因此得到近裁剪面的宽度就是其高度,远裁剪面也类似得到。

\[ nearClipPanelWidth = nearClipPanelHeight \]

\[ farClipPanelWidth = farClipPanelHeight \]

Camera 的横纵比在渲染管线中进行至投影变换(观察空间 - 裁剪空间)一步时也要用到。同样是是通过横纵比计算得到变换中需要用到的投影矩阵,将顶点从观察空间变换到裁剪空间,然后进行裁剪剔除。

在计算得到近裁剪面的宽高之后,根据 Camera Transform 在世界空间下的位置和距离 Camera 的距离(Near),就可以求得近裁剪面上个点在世界空间下的坐标。如下图:

unity_camera_screen_point_to_ray_3.png

此时计算近裁剪面在世界空间下左下角的坐标:

\[ pos = (\frac{-nearClipPanelWidth}{2}, 1 - \frac{nearClipPanelHeight}{2}, Camera.z + Near) \]

在渲染管线的屏幕映射这一步中,将裁剪空间中的三维顶点坐标投影到屏幕上(将视锥体中的三维坐标映射到屏幕上的二维像素坐标)。首先使用齐次除法将裁剪空间变换到一个立方体中,坐标范围是[-1,1];然后将这些经过齐次除法变换过之后的坐标映射为屏幕像素坐标(缩放)。

屏幕左下角像素坐标为(0,0),右上角为(pixelWidth,piexelHeight)。这里以上面计算所得近裁剪面中左下角的点为例,通过一些列变换来到屏幕映射齐次除法过后的的立方体中,此时它对应着立方体中(-1,-1,-1)位置处的点,将这个坐标映射到屏幕像素坐标就是(0,0),z 分量被用作深度缓冲。

所以当使用 Camera.ScreenPointToRay 方法计算屏幕像素坐标(0,0)处的射线时,实际上就是计算的从 Camera 出发,穿过近裁剪面左下角处的一条射线。上面计算的世界坐标下近裁剪面左下角坐标也就是 ScreenPointToRay 方法在屏幕像素坐标(0,0)处得到的射线的 ray.origin 值,同时该方法计算得到的 ray.direction 也就是从 Camera 朝近裁剪面左下角点的方向值(归一化后的结果)。

ViewportPointToRay 方法

Camera 类还有一个 ViewportPointToRay 方法,这个方法同 ScreenPointToRay 类似也是计算某点的一条射线,不同的是该方法使用 Viewport space 中的坐标来计算的(Viewport space 中的坐标: 屏幕映射中齐次除法之后,将坐标从[-1,1]缩放为[0,1]就是 Viewport space 下的坐标,所以该坐标是相对于 Camera 的,并且已经归一化)。