![2466708ba7dbeae4aadc67ca1fd4b891.png](https://i-blog.csdnimg.cn/blog_migrate/9dafde4d309b8a300fa1f07b6bc90900.png)
Overview
在Unity中实现光栅化渲染只是为了深入理解光栅化渲染的流程和细节,使用的算法大多没有经过优化,使用到Unity的功能:
- CommandBuffer显示光栅化结果
- 数学计算
- 模型加载
- 纹理加载
需要注意的是:
- Unity中的世界空间是左手坐标系,观察空间是右手坐标系
- Unity中三角形顶点顺序为顺时针的当作正面
- Unity中向量是行矩阵
光栅化渲染是一种古老的技术,从1960年到1980年间开发出来,也是现在GPU用来生成3D图形的技术,近几年NVIDIA才推出了支持光线跟踪的GPU。虽然它很古老,但是没有被废弃,从GPU诞生以来,虽然各种硬件技术不断发展,但是基本的光栅化技术并没有太多变化。
光栅化和光线跟踪这两种算法都是为了解决可见性和着色问题,但是算法的思想是完全相反的。光线跟踪首先遍历图像上的每一个像素,朝每一个像素发射一条射线,然后遍历每个几何体,射线与几何体相交距离最近的点就是可见的点。而光栅化首先将每个几何体投影到屏幕上,然后遍历每个像素,把被几何体覆盖的像素填充。
相对于光线跟踪,光栅化是个很优雅的算法,光线跟踪处理光线和几何体求交是复杂的,光栅化的两步都是非常简单和快速的。
投影
光栅化的第一步就是把3D空间的顶点变换到屏幕空间,这一步变换就叫投影,图形API中一般由Model、View、Projection Transform来表示这一步。
投影分为正交投影和透视投影,透视投影会模拟人眼的效果,有近大远小的特点,正交投影则没有。透视投影还有一条特性就是保留了直线,但是没有保留距离。
正交投影在空间中表示成一个长方体,在长方体内的顶点能被显示在屏幕上。第一步把长方体缩放成范围在[-1,1]的正方体,然后变换到屏幕空间,也就是[0,width][0,height]。
透视投影表示的空间是一个方平截头体,也叫视锥体,可以看成把视锥体挤压成长方体(也就是投影到近平面),然后再进行正交投影。关于投影的推导:
litmin:渲染管线中的坐标变换
litmin:Tips of Transformation
Edge Function
有了屏幕上的顶点,就能组装成图元,最常用的就是三角形,如果可以不填充像素,只画三角形的边缘,这种模式叫做WireFrame,可以使用Bresenham算法来画直线。
![d01fa275bb43c91c224cd6be111c2ede.png](https://i-blog.csdnimg.cn/blog_migrate/d5dbd883148659ab63c21d69c480623f.jpeg)
如果需要填充三角形覆盖的像素,就需要把位于三角形内的像素填充上颜色。我们可以使用向量的叉积来判断一个点在一条直线的左边还是右边,如果一个点在三角形的三条边的同一侧,那这个点就在三角形内 。
![d46b0a8d3a8d9932a14c780d44e0ed96.png](https://i-blog.csdnimg.cn/blog_migrate/791a7f48139722a5e3ad08c7e553e79a.png)
向量叉乘的方向在右手坐标系中使用右手定则确定,在左手坐标系中使用左手定则确定。
这里需要注意不管是在右手坐标系还是左手坐标系,叉乘向量的数值结果是一样的,只不过表示的绝对向量方向不同,Unity中是左手坐标系,遵循左手定则。
private
Depth Buffer
当画多个三角形时,如果确定显示哪个三角形呢?
画家算法(油画家)是一种方法,首先绘制较远的物体,然后绘制较近的物体,这样近的就能覆盖远的。
![2d4dfb65d0d8b34f5456bc257e97f5fb.png](https://i-blog.csdnimg.cn/blog_migrate/3d8a6178a7962f85d68a8cbd159e7fee.png)
当多个几何体互相重叠的时候,画家算法就处理不了。画家算法现在还被用在透明物体上。
深度缓冲可以解决这种问题,深度缓冲就是一个屏幕大小的浮点数数组,初始化为一个非常大的值,在屏幕上画一个像素时,比较这个点的深度和深度缓冲中对应位置的深度,如果这个点的深度比较小,就表明这个点离相机更近,就可以画这个像素,同时更新深度缓冲。如果这个点的深度大,就表明这个点离相机更远,被其他物体遮挡,就不能画这个像素。
![c649720b745690b4878c25c67a465ee6.png](https://i-blog.csdnimg.cn/blog_migrate/0ff4aa48a028ced32c772980f074e576.png)
Shading
确定好哪些像素需要填充后,就要给这些像素一个颜色,这个步骤就叫shading(着色),所以shader就叫着色器 。如果给每一个三角形指定一个颜色,那到这一步就可以显示出一个纯色的三角形来了。这种每一个面计算一个颜色就叫Flat Shading,逐顶点计算叫Gouraud Shading,逐像素计算叫Phong Shading,这个Phong Shading指的不是那个光照模型,也是那个大佬提出来的,所以就叫这个了。
逐顶点计算颜色时,我们可以给每个顶点一个颜色,但是三角形内的像素点该是什么颜色呢?同样,在逐像素计算时,每个像素计算所需要的属性又怎么来呢?这就需要插值得到。
![3559f998ca5608d1d72e760ac5a64e01.png](https://i-blog.csdnimg.cn/blog_migrate/62c18bff4a787b25c98100a95bc30e57.png)
重心坐标插值
重心坐标就是以三角形的三个顶点组成的坐标系表示的坐标,三角形的平面上任意一点都可以用重心坐标和三个顶点来表示(V0,V1,V2是三角形的三个顶点):
![c37531a2a730d469a5b410af490b10d8.png](https://i-blog.csdnimg.cn/blog_migrate/4ccf98ebb32b08d07e1c4d604474e11e.png)
重心坐标可以是任意值,但是对于三角形内的顶点,它们的范围是[0,1],而且它们的和等于1:
![475ad8108f8ed81786f1694a95861cd6.png](https://i-blog.csdnimg.cn/blog_migrate/79525697159db1b72bebbe2698098151.png)
重心坐标计算:
![f55bb578dad544a1be82b9a384d81cb7.png](https://i-blog.csdnimg.cn/blog_migrate/bc5ba0e76aff1d39ffe85a70461eb071.png)
三角形V0_V1_V2的面积:
![78709d2e3b56f8699cf80c1f7beac1ab.png](https://i-blog.csdnimg.cn/blog_migrate/388949f4f0cdc91c42d212e6be52f660.png)
如果一点P在三角形内,那三角形的面积就是P跟另外三个顶点组成的三个三角形面积的和:
![b5cbe8fc8ab3111220d3a51a762b107a.png](https://i-blog.csdnimg.cn/blog_migrate/829dc3ae80c1b5799d8adde1a90db7ad.png)
点P开始在V1V2这条边上,慢慢朝V0移动:
![788f646ae020cbdc6f736a0610083671.png](https://i-blog.csdnimg.cn/blog_migrate/8d5d01ae8a8591e6637533847772a5aa.png)
![9d505950db10a05e4ead3f8013775b82.png](https://i-blog.csdnimg.cn/blog_migrate/489ea35aae667f9905e9bd6bcde21d98.png)
![01013f4f987d78c8762141953c94d814.png](https://i-blog.csdnimg.cn/blog_migrate/f96e8bc5b257f5bab81f1e33b9dbf7ea.png)
当P在V1V2上时,P的坐标就是V1和V2的线性插值:
![562432faca0247a96ab464a372a5599f.png](https://i-blog.csdnimg.cn/blog_migrate/388527cf0e91a9d0dc8ab26b6f4fb0c9.png)
用重心坐标来表示,其中λ0 = 0:
![6f5aae8afb819fe6d269c95cf7e59ca0.png](https://i-blog.csdnimg.cn/blog_migrate/01c26d70b1d09e6af92bf3972aa7dcb2.png)
P距离V1比较近,因此λ1比λ2大,而且绿色三角形比紫色三角形要大,红色三角形不可见,所以λ0等于0.所以重心坐标和三角形的面积之间可能有某种关系,也就是λ0和红色三角形相关,λ1和绿色三角形相关,λ2和紫色三角形相关。
当P移动到V0的位置,λ0等于1,λ1,λ2等于0,P = V0,红色三角形的面积就等于大三角形的面积。
从以上观察中可以发现,重心坐标跟这个顶点的对边与P组成的三角形的面积有关。
这是凭感觉推导的计算公式,还有更严谨的数学推导:
![b4d6159c5deab1e5ed3bdfcdbbf56d6d.png](https://i-blog.csdnimg.cn/blog_migrate/da13d40a8542ca834813699d86f9a953.png)
既然这个点的位置可以插值三个顶点的位置得到,那三角形内一个点的其他属性也可以通过这样来插值三个顶点的属性得到,例如颜色:
![14232bfcf30af94f9c71a90eeb354fd1.png](https://i-blog.csdnimg.cn/blog_migrate/ee2204b067744f197b3960b94e29417c.png)
计算重心坐标需要的每个三角形的面积都可以用判断点是否在三角形内时叉乘的结果,因为叉乘向量的模就是两个向量组成的平行四边形的面积。
透视矫正插值
如果使用透视投影,直接使用重心坐标插值UV,就会得到这样的结果:
![cc734eff8281acb17457387153c52352.png](https://i-blog.csdnimg.cn/blog_migrate/016f6ba19b3077e960fbcd62d74cad23.png)
正确的结果应该是:
![b3dffe2798f7568e6dd6f91ce0c9d369.png](https://i-blog.csdnimg.cn/blog_migrate/52fdb8c61b1a5867e8d9401d0c816992.png)
深度插值矫正
如果投影后的点的z存储的就是在View Space下的z,插值三角形内的一个点的深度就是:
![0d254b013f791adaff0283354b36ff5e.png](https://i-blog.csdnimg.cn/blog_migrate/3f4ccbe391cda25fcfe9956b1b92f0e8.png)
这样做结果是不对的,因为投影后的z在屏幕空间不是线性的。
![aebd05d2a25c59a0da22e0b3088ea344.png](https://i-blog.csdnimg.cn/blog_migrate/ee620cafb58734491fecdd842b05df11.png)
图中绿线表示近平面,距离原点为1,已知P投影到近平面的点是(0,1),我们的目标是求出点P的z坐标,两个点投影到近平面,它们的x坐标可以求出来:
![229f3c26ebd4a4fb39b837e00213aec4.png](https://i-blog.csdnimg.cn/blog_migrate/f78a2d66a84df1b446f88547430c1b25.png)
直接使用插值计算出来P的z坐标:
![435aad727bb832500ee6fa93a3631b16.png](https://i-blog.csdnimg.cn/blog_migrate/82e6d700c659db29313f1a60e52931a2.png)
很显然结果是不对的。下面推导正确的公式:
![4c2ef1aeaa6fe2eb2d1c735fccbe1944.png](https://i-blog.csdnimg.cn/blog_migrate/f0f245daa85afaba4c27e005c30b29af.png)
在View Space有两个点P0(X0,Z0)P1(X1,Z1),投影到近平面的点是S0、S1,S(X,1)是S0S1上一点,对应的View Space下点P(X,Z),t和q定义如下:
![451675232778ef4add21fe37999b699c.png](https://i-blog.csdnimg.cn/blog_migrate/9a965560e6e130aaa3f71928ad06cf82.png)
还可以写成:
![fc8f957e204379b16189344cfb7c9490.png](https://i-blog.csdnimg.cn/blog_migrate/4546848bc0d336616b297afc0dc51646.png)
P点的X、Z坐标:
![814aa966bfb7f2a2077f3ff63f598aff.png](https://i-blog.csdnimg.cn/blog_migrate/5011349fb2fbf9654d09f2cbc2ef3d87.png)
S表示S点的x坐标:
![f3204e362c3ba7d3a49c9c396a887c67.png](https://i-blog.csdnimg.cn/blog_migrate/3adc3e30b1d875d8816dd6656190feee.png)
![2c3a4d0dd365a38fb71e7f0d6293b675.png](https://i-blog.csdnimg.cn/blog_migrate/bba9b3b8ccd3a4c24c6c5a88bce8847f.png)
把上面等式代入:
![f3e5a6d541d4db610a3f210b1f76407f.png](https://i-blog.csdnimg.cn/blog_migrate/81aa2f4bb6995ac27de98b03c651066c.png)
再代入S0和S1:
![39a3058475c85f8db23de6e95a65d27d.png](https://i-blog.csdnimg.cn/blog_migrate/b1ada1ec217b84540916b13c8d904b99.png)
![5156ec58ac0fd1855b30c74a9d1f9c5b.png](https://i-blog.csdnimg.cn/blog_migrate/c34cdb200a461556abebde8a90236c5e.png)
再把左边的Z替换:
![d3254265a8bd251e711aad663eede18a.png](https://i-blog.csdnimg.cn/blog_migrate/cb2dff2214f23c15847b49770ba4775f.png)
![3e7046075cc696c5bcfa3b338be5df0b.png](https://i-blog.csdnimg.cn/blog_migrate/dd3cacefff2dd935bb8e20316027e0dd.png)
化简的出来t:
![cad05758a5a3a72e754920d35b5787dd.png](https://i-blog.csdnimg.cn/blog_migrate/6cb2795a270c5c5b14fd88de971020b2.png)
![626e42d957ccb44159576ae18579ad5b.png](https://i-blog.csdnimg.cn/blog_migrate/49c74ecd87d17fd9642877eddcf1a33e.png)
把t代入Z的插值公式:
![5719f6a6504e98ad2de3d2b5fbf18318.png](https://i-blog.csdnimg.cn/blog_migrate/2471acfb09e9f472005b712fa52c7f34.png)
推导过程总结出来就是:已知观察空间中深度由两个顶点插值的系数,求出投影点X坐标插值系数和深度插值的关系。
在屏幕空间中Z的倒数是线性的,这就是为什么图形API中投影变换后深度值变为倒数。
顶点属性插值矫正
跟深度值一样,其他属性也不能直接插值。
![f6d4a1fc3fdb3256ada743479e35f149.png](https://i-blog.csdnimg.cn/blog_migrate/fffcc0cb43ea0f908f517425a229381c.png)
有一个正方体,点P在正方体的中心,V1颜色是(1,1,1),V2颜色是(0,0,0),很显然P的颜色应该是(0.5,0.5,0.5)。
![506fa4d071e629b581f50c789d4170a1.png](https://i-blog.csdnimg.cn/blog_migrate/350c81c1567aeee6c629229a61bcd8af.png)
当以不同的角度观察时,P就不在中心了,这是透视投影导致的,因为透视投影保留了直线,但是没有保留距离。因为重心坐标是在屏幕空间计算的,只考虑了xy,但是插值的点其实是在3D空间中,只需要先把它还原回3D空间中再插值就可以了。透视投影中x、y需要除以z来投影到近平面,乘以z就可以还原到3D空间,所以正确的插值应该是:
![d2af061c4cd3385c19892b5c6e933deb.png](https://i-blog.csdnimg.cn/blog_migrate/7f468cb8d11230bdca203c65cd8eff40.png)
数学推导:
![7b1f10b335cdc3b63e68b1445d1b59ed.png](https://i-blog.csdnimg.cn/blog_migrate/7a628f52ed5a9ab50f8267199d9f67b9.png)
三角形上两个点的深度为Z0,Z1,颜色为C0,C1,我们可以用线性插值来求出这两个点之间的一点P的深度值,对于颜色值也可以使用相同的线性插值求出来,所以有:
![18462f7c248026bbed89bdce3ab2dbaa.png](https://i-blog.csdnimg.cn/blog_migrate/c0cd0459aed59ac0d361336a3253bcbc.png)
因为P的深度值的倒数在屏幕空间是线性的:
![e5bc069bbc2ad988dcaf7287ea5468e1.png](https://i-blog.csdnimg.cn/blog_migrate/dfe3c7d61aabd5a1a5d698fd56f920f5.png)
代入第一个等式的左边,化简(先乘Z0Z1):
![09745598686bc80f26ad6a58e3e52ee3.png](https://i-blog.csdnimg.cn/blog_migrate/82b9f1d511fb03c7f30d6bcd96b8565e.png)
求出C:
![e31c31499f65dedbfc1d1e3d6adff01e.png](https://i-blog.csdnimg.cn/blog_migrate/27b698ce636dd249b01fbcc1ac707ab2.png)
用1/Z0Z1化简:
![6049932ee5961c12866d17ff0f4473d4.png](https://i-blog.csdnimg.cn/blog_migrate/ec6a039bad4e7c9a26872758a2df4efc.png)
透视矫正插值和深度插值的特殊处理都是透视投影才需要的,使用正交投影,直接用重心坐标来插值深度和顶点属性都是正确的。
Face Culling
可以指定不渲染三角形的正面或者背面,通过两条边叉乘的向量的方向来确定是正面还是背面。
// Cull,Unity中三角形顺时针为正面
Lighting
插值完成后,使用各个属性计算光照,这里使用简单的Blinn Phong光照模型,最终结果:
![7a4634a625145177b7c3f6271cb1102f.png](https://i-blog.csdnimg.cn/blog_migrate/a5302aa8d6f026e197d67e97256fcce0.png)
Reference:
https://www.scratchapixel.com/lessons/3d-basic-rendering/rasterization-practical-implementation/overview-rasterization-algorithmwww.scratchapixel.com