Unity坐标系入门
一、坐标系的概念
Unity 世界坐标系采用左手坐标系,大拇指指向X轴(红色),食指指向Y轴(黄色),中指向手心方向歪曲90度表示Z轴(蓝色),同时Z轴也是物体前进方向,下图表示Unity的四种坐标系:
其中GUI和Screen的单位是像素单位,什么是像素单位呢,这里可以先了解一下像素相关知识:
像素:简单地来说,可以理解成一个方块,它是图像的最小单元,不能再分割,但它并没有固定的大小。
屏幕分辨率:指的是由像素组成的点阵,比如说一台电脑的屏幕分辨率为1920*1080,那么该电脑屏幕在横向有1920个像素,纵向有1080个像素。
谈及像素和分辨率,还有一个概念叫做PPI,什么是PPI呢?
PPI:每英寸像素数目,可以理解为屏幕像素密度
因为像素并没有固定的大小,所以,PPI 越高,像素大小越小,也就越清晰。具体的计算公式如下:
所以可以知道,在购买显示器的时候,同是2K屏,24寸的是要比27寸的更清晰一些,因为PPI更高。
回到正题,比如我的电脑的分辨率是1920*1080,那么其X轴最大单位就是1920,Y轴最大单位就是1080。
了解了GUI、世界和屏幕坐标系后,Viewport坐标系其实没什么好说的,只是进行了归一化。
二、Screen坐标系和World坐标系的换算
在进行换算前,需要知道Camera相机的概念,相机又分为正交视图(orthogonal)、透视视图(perspective)。
一般2D游戏采用正交视图,3D游戏采用透视视图。
1、正交视图
2、透视视图
1、2D坐标系换算(正交视图)
在上述示例中用到的天空背景图的分辨率是4096*2048,但是有个地方很奇怪,Unity显示该图分辨率是2048*1024,实际是4096*2048。
如果想要背景图填充满整个屏幕,可以将游戏视图分辨率设置为图片分辨率,然后还需要设置相机大小:
相机大小(size):采用世界坐标系单位,为屏幕半宽。(所以相机大小为2048/2/100=10.24)
Pixels Per Unit:每多少像素为一世界坐标系单位。
这里我们应该知道屏幕坐标系的范围是(0,0)到(4096,2048),那世界坐标系范围是什么呢?
世界坐标系的原点和范围受到相机的影响,若相机的位置为(0,0),并且没有发生旋转,那么世界坐标系的原点就在屏幕正中心,其范围是(-20.48,-10.24)到(20.48,10.24)。
2、3D坐标系换算(透视视图)
1、从世界坐标到屏幕像素
3D坐标系换算相对更复杂一点,如下实例:
相机位于(0,0,-16),且视野角度为60度,帽子位于(0,6,0),且都没有发生旋转,那么其在Y轴上的分辨率既可以推算出来。
同理,其在X轴上的分辨率也可以算出来。
2、从像素坐标到世界坐标
当你用在屏幕上点击的时候,怎么从二维坐标对应到三维坐标了,其实是相机发射了一条射线:
以下内容来自于Unity中的ScreenToPointRay:(该作者的博客写的很不错,推荐看看,不过图片需要FQ)
在 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 打印如下所示:
屏幕像素坐标 (0, 0) 处使用 ScreenPointToRay
方法得到的 Ray origin 值是 (-0.6,0.4,-9.0)
,direction 值为 (-0.4,-0.4,0.8)
,这些值是如何计算得到的了?
此时 Camera 的视锥体截面图大致如下:
对于近裁剪面的高度,已知 FOV 和 Near,简单的三角函数就能求得。如下:
远裁剪面的高度也可以使用类似的方式求得,也就是:
那近裁剪面的宽度如何求得了? 我在 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。因此得到近裁剪面的宽度就是其高度,远裁剪面也类似得到。
Camera 的横纵比在渲染管线中进行至投影变换(观察空间 - 裁剪空间)一步时也要用到。
同样是是通过横纵比计算得到变换中需要用到的投影矩阵,将顶点从观察空间变换到裁剪空间,然后进行裁剪剔除。
在计算得到近裁剪面的宽高之后,根据 Camera Transform 在世界空间下的位置和距离 Camera 的距离(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 朝近裁剪面左下角点的方向值(归一化后的结果)。