图形渲染管线与shader(更新中)
恢(zheng)复(shi)更(kai)新(shi)后的第一天。
唔…不如来说说图形管线和着色器的事吧。趁着刚给别人讲完。(说来还是一段惨痛的经历…TAT)
(请自行脑补分割线)
着色器,也就是平时各种地方说的shader,是开发者为了开发需求而为GPU(图形处理器)编写的一个小程序。GPU有着高度并行化的特点,但运算的能力就没有CPU那么厉害,无法完成较为复杂的运算。但同时,它往往拥有CPU无法企及的庞大的计算单元数量(Iris 540:48,GTX 980Ti : 2560),这些单元都可以独立完成相关的计算,想想如果有几百个这样的单元在同时计算,计算的效率势必飞涨。不过这样的计算所能完成的工作也是有限的,但是由于图形计算高度的分离性,这种经过特殊设计的硬件成为了"大杀器"。如果把CPU比成一组学识渊博的教授的话,GPU可能更像一操场的中学生 —— 但应付常见的图形计算,老教授们渊博的学识…并没有什么卯月。
我们可以为这样的硬件编写短小的程序,也就是shader。这些程序一般进行的是纯粹的数学计算和简单的逻辑处理,所以通常行数不会超过三位数,但行行是精华,可能某一行的一个算式就是几十年的研究结果。
Unity中支持好几种不同类型的shader,它们被称之为"Surface shader"、"Unlit shader"一类。Unlit shader是其中最"本质"性的一个,它和OpenGL/DirectX/…这类底层图形API所使用的shader没有什么本质性的区别。
(Unity中shader支持的语言是Cg和HLSL)
不想在这里涉及太多具体实现的细节。那么就让我们开始吧,从冷冰冰的数据如何变成屏幕上的一帧帧图像开始。
(一)图形渲染管线
(简化了的图形渲染管线模型)
"图形渲染管线"(Graphics rendering pipeline)指的是从数据被输入GPU开始,到最后渲染成为图像所经历的一系列有序过程的总称。
但首先,我们要能够告诉GPU,我们要画些什么在屏幕上。现在,人们采用顶点+面的方式来描述。我们把一个个复杂的几何体表示为一组面的集合,就像这样:
尽管这样不能完美的表示一个曲面,但对于骗过我们自己的眼睛来讲这足够了。这个茶壶成为了被一个个四边形包围起来的物体,如果我们存下这些四边形的顶点信息,我们就能够根据这些信息画出一个一模一样的"茶壶"。
由于空间中三点确定一个平面,我们不使用四边形,而使用三角形来"包围"一个物体。有了它们各个顶点的数据,我们就可以根据这些数据去绘制我们需要的图形。这些数据被传入GPU,再由GPU进行计算,变成图像呈现给我们。在这些数据中,大多数情况下不单包含顶点的位置信息,往往还有顶点的法向量("朝向",这在计算光照时十分有用。单有位置不能判断出某个顶点的朝向,进而无法判断出这个面上某个点对应屏幕上的像素究竟该不该被照向某个方向的光打亮),纹理坐标(用来给几何体贴图,详见后续)等。
有了这些信息,我们需要去把它计算成对应于屏幕上的坐标。在这里需要用到空间上的变换,把一个个坐标点换成相对于眼睛位置、朝向的坐标,并经过透视变换,变成对应于屏幕的坐标点。
(物体的变换过程,注意透视变换产生的"拉伸")
在这之后,我们把这些点组装成一个个面,再计算出每个面对应于屏幕上的那些像素。然后根据传入的顶点数据给这些像素分配颜色,最终呈现在屏幕上(或保存在图像中)。一般情况下,这一整套流程要在一秒钟内执行60次以上 —— 通常有万计的顶点;这便是GPU高度并行化可怕的计算效率。
(二)顶点着色器(Vertex shader)
在数据被传入后,经过简单的处理,第一个阶段(stage)便是顶点着色器。上文提到"着色器"即shader,是我们为GPU编写的独立的小程序。它分为几种不同的类型,分别用于整个渲染流程的不同阶段。在这一阶段,我们这一段小程序的目的是把输入的顶点位置转化为相对于屏幕的顶点位置(经过透视变换),并可处理数据向后续阶段传递 —— 我们可以很轻松的在后续阶段的shader中拿到之前阶段shader产生的数据。