作者:王子饼干 ——镇江虹视游戏科技有限公司联合创始人、《多洛可小镇》制作人
原文链接:UnityShader入门精要笔记3:空间变换
原文发布时间:2020年3月1日
文章类型: 授权转载
我成功,是因为我志在成功,未尝踌躇。——拿破仑
前面说到了渲染流水线是一个处理流程,它要做的事情就是把原始的模型数据,纹理数据,摄像机数据等经过一系列的处理然后变成屏幕上的一幅图,或者说一帧画面。从本质性上来说,模型数据都是3D数据,而一帧画面则是一个2D数据。这就表示渲染流水线存在一些空间上的转换。而事实上,这其实是渲染流水线主要操心的问题之一。
在开始对一些繁重的概念进行理解之前,我建议大家做一点简单的初中几何,没有特殊的意义,只是单纯的练练手而已。
现在我们构建了一个坐标系,并且在其中绘制了一个梯形,我们假设,延长梯形的两条边,可以在原点处交汇。且梯形的上底距离原点距离为near,简写为N,而梯形的下底距离原点为far,简写为F。
我们的问题是,在梯形中有一个点A,将它和原定O连起来形成线段AO,会交梯形的上底于S,求S的坐标。
这个问题其实很简单,甚至有的人只要稍微想一想就可以做出来了。但是我还是希望大家拿出纸和笔,和我一起,做一道简单的数学题,纯当是学习或者工作之余的消遣。
我们可以用比较几何的方式来解决这个问题,我们先过A点做x轴的垂线。就可以得到一个相似三角形。
我们要求的东西其实就是红色阴影部分标记出的小三角形的两直角边的长度,于是我们可以啪嗒啪嗒的搞出来一个等式。
由于我们的S点又在距离原点N的线段上。因而可以确定S点的横坐标为N。
通过这两个等式,我们可以借由A点坐标和N来表示出S点的坐标为
如果已经对坐标变换稍有基础的话,就可以很轻松的明白,我们刚才所做的事情其实就是把视椎体(Frustum)中的坐标映射到一个二维的屏幕上,相对于坐标变换,坐标映射此刻更加符合我们所做的事情。这其实是整个渲染流程的倒数第二步。当然啦,最后一步其实就是把我们计算得到的这个原始的二维的图像再填充到目标屏幕里(本质上就是做一个屏幕的缩放以适应目标设备)。
这么说来,我们有了一个新的线索,那就是视椎体。什么是视椎体,并且我们又为什么需要视椎体呢?
在Unity当中,这就是视椎体,视椎体的范围和深度是由相机的参数来定义的。
其中,Field of View决定了相机张开的角度,而Near和Far的话,也就是对应了我们之前做的那个题目中的Near和Far。
说起来复杂,其实很简单。理由是因为现代化科技比较落后,显卡不能同时渲染我们创造的世界的所有的对象。所以制作一个视椎体,把视椎体之外的内容(人眼看不到的内容)裁减掉。但是话不能说的过于刻薄,虽然确确实实有这方面的理由,但是不论我们的显卡是否有足够的算力同一时间渲染所有对象,我们在游戏开发当中,视角必然有一个范围。所以,不论裁剪还是不裁剪,我们看到的范围都是有限的。
由于它也像是一个空间,肯定要为它起一个有辨识度的名字。那就是裁剪空间。
说出来你可能不信,我们所谓的裁剪空间其实也就是那块视椎体。它其实是视角空间中的一个梯台。换句话说,它是视角空间中的一个子空间,只不过是一个有限的空间,所以,我们很自然的得到了一个新的线索,它就是视角空间。
视角空间是以摄像机为原点的空间,从这个名字上来看,视角空间很像是摄像机能看到的空间,但其实我们可以换种说法,即视角空间很像是摄像机能够探索的空间。视角空间使用了右手坐标系,这使得摄像机的正前方指的是-z的方向,这并非什么特殊的设置,也不是说它一定得用右手坐标系,只是这样做符合OpenGL的传统。
当我们有了视角空间的坐标,就可以为它增加一个值为1的w分量,然后通过投影矩阵,转换为裁剪空间坐标,另外,由于增加了w分量,所以我们的裁剪空间也可以称为齐次裁剪空间。
虽然,这很容易联想到,但是我们还是要说一声为什么要有视角空间,这是因为,齐次裁剪空间是在视角空间中定义的,如果没有视角空间,就很难计算出投影矩阵。
在视角空间之前,我们必然有一个存放着所有模型的空间,以备视角空间来探索。你可以在这个空间找到所有的模型。这个空间是最不需要解释理由的。所有的空间都是它的子空间,可以说,世界空间是我们构造一个图形引擎,首先需要创造的空间。(说是创造,其实是用该空间的坐标系来表示一个模型的位置)
考虑一件小事,假设我们需要用变形着色器对一个mesh的表面进行变形,你该怎么办?
我们可以找到一个模型的位置,然后对它进行顶点的扭曲。当然啦,为了标记出这个模型的位置,我们首先需要它的坐标,以及它的大小。然后框选出该模型的位置,对它进行变形。以免该变形效果影响到世界空间的其他物体。如果我们有一万个模型需要变形,那设定模型的位置和它的大小就成了一个非常繁重的工作。不如,不如我们以模型的本身为原点,构造出一个模型空间,任何操作只对模型空间的顶点和表面有效,然后增加一个模型空间到世界空间的矩阵,这样就很轻松的避免了太多冗余的工作。
对了,最后也是第一个空间,那就是模型空间。至此,我们大概的了解了一个渲染流水线所需要处理的大部分的空间。最后,我们再梳理一下,通过下面这张图。
其实可以一口气念完
我们有模型空间的坐标,可以通过模型转世界矩阵,将其转换为一个世界空间坐标。在通过WorldToCamera矩阵,将其转换为一个视角空间坐标,随后为其增加一个分量w,变成齐次视角空间坐标,通过投影矩阵,转换为齐次裁剪空间坐标,在齐次裁剪空间完成裁剪工作,然后再除以w分量,变成归一化设备空间坐标。然后进行二维映射,再进行屏幕映射。
完成~
至于为什么除以w分量可以转换为归一化设备空间坐标,这么复杂的事情,还是大家自己去学习一遍吧,OpenGL投影映射原理
在最后的部分之前,我们先解决一下为什么要有归一化设备空间坐标这回事。其实主要是为了将所有的坐标都映射到-1,1的范围内,以便于投射到OpenGL或者DirectX的设备上。(传统艺能)
前面提到了很多矩阵,诸如模型转世界空间矩阵和WorldToCamera矩阵这个可以在《UnityShader入门精要》87页找到
至于投影矩阵,则可以在《UnityShader入门精要》的89页找到
这些内置变量都放在了UnityCG.cginc中,Unity会在渲染开始前,把所有的数据都输入到这些变量中,以便于你使用。当然啦,通过各种矩阵的混合操作,UnityCG.cginc中也定义了很多方便的函数,比如UnityObjectToClipPos函数,可以直接将模型转换为裁剪空间坐标。
其实还有很多东西要讲,但是本系列的笔记着重于效果实现,前期的理论知识会选择一些我个人认为比较重要的部分来说。