Chapter 1 The Graphics Rendering Pipeline
欢迎来到万恶之源——渲染管线,这是一切罪恶的开始
这玩意的主要功能就是决定在给定虚拟相机、三维物体、光源、照明模式,以及纹理等诸多条件的情况下,生成或绘制一幅二维图像的过程
(二向箔打击懂不懂啊)
1.1 Architecture
在rtr4中,是将图形渲染管线分成了四个阶段,(而在rtr3和入门精要中之分成了三个阶段)
• 应用程序阶段
• 几何阶段
• 光栅化阶段
• 逐片元阶段
几个要点:
• 每个阶段本身也可能是一条管线,如图中的几何阶段所示。此外,还可以对有的阶段进行全部或者部分的并行化处理,如图中的光栅化阶段。应用程序阶段虽然是一个单独的过程,但是依然可以对之进行管线化或者并行化处理。
• 最慢的管线阶段决定绘制速度,即图像的更新速度,这种速度一般用 FPS 来表示,也就是每秒绘制的图像数量,或者用 Hz 来表示。
1.2 The Application Stage
应用程序阶段是通过软件方式来实现的阶段,开发者能够对该阶段发生的情况进行完全控制,可以改变实际性能。正因应用程序阶段是软件方式实现,因此不能像几何和光栅化阶段那样继续分为若干个子阶段。但为了提高性能,该阶段还是可以在几个并行处理器上同时执行。在 CPU 设计上,称这种形式为超标量体系(superscalar)结构,因为它可以在同一阶段同一时间做不同的几件事情。
应用阶段大致可分为三个阶段(CPU->GPU):
- 把数据加载在显存(CPU ->RAM-> VRAM)
2.设置渲染状态
3.调用Draw Call
- CPU通过调用Draw Call 来告诉GPU开始进行一个渲染过程,一个Draw Call 会指向本次调用需要渲染的图元列表(DC就是一个命令,CPU是发起方,GPU是接收方)
1.2.1什么是Draw Call
①就是 CPU调用图形API
②深入理解Draw Call的一些问题
(a).问题一: CPU GPU 是如何实现并行工作的?
解决方法就是使用命令缓冲区 (Command Buffer)
(b).问题二:为什么 Draw Call 多了会影响帧率?
如果 Draw Call 的数批太多, CPU 就会把大献时间花费在提交 Draw Call 上,造成 CPU 的过载。
(c).问题三:如何减少 Draw Call?
有很多方法。其中一个:批处理
把很多小的 DrawCall 合并成一个大的Draw Call, 这就是批处理的思想。如图
1.2.2什么是OpenGL、DirectX
API(Application Programming Interface,应用程序接口)是一些预先定义的接口(如函数、HTTP接口),或指软件系统不同组成部分衔接的约定。 用来提供应用程序与开发人员基于某软件或硬件得以访问的一组例程,而又无需访问源码,或理解内部工作机制的细节。
① API (Application Programming Interface) :应用编程接口
【应用编程接口】是指电脑操作系统或程序库提供给应用程序调用使用的代码,其主要目的是让应用程序开发人员得以调用一组例程功能,而无须考虑其底层的源代码为何、或理解其内部工作机制的细节。
②两者都是图形API,要开发者直接访问 GPU 件非常麻烦的事情,我们可能需要和各种寄存器、显存打交道,而图形API在这些硬件的基础上实现了一层抽象。
//渲染命令即Draw Call
通常视线的方法有碰撞检测,加速算法,输入检测,动画,力反馈以及纹理动画,变换仿真,几何变形,以及一些不在其他阶段执行的计算,如层次视锥剪裁等加速算法就可以在这里实现
主要任务:在应用程序阶段的末端,将需要在屏幕上(具体形式取决于具体输入设备)显示出来绘制的几何体(也就是绘制图元,rendering primitives,如点、线、矩形等)输入到绘制管线的下一个阶段。
总结:将需要绘制图元输入到绘制管线的下一个阶段,以及实现一些软件方式来实现的阶段
1.3 The Geometry Processing Stage
几何阶段主要负责大部分多边形操作和顶点操作
1.模型视点变换 Model and View Transform
在显示过程中,模型通常需要变换到若干不同的空间或坐标系中,目的是从模型坐标变换到世界空间坐标,让所有模型都位于同一个空间中。为了便于后续投影和剪裁,这一步必须对相机和所有模型进行视点变换,变换目的就是要把相机放在原点,然后进行试点校准,变换后实际位置和方向就依赖于当前的API,我们称之为相机空间或观察空间
【总结】模型和视图变换阶段分为模型变换和视图变换。模型变换的目的是将模型变换到适合渲染的空间当中,而视图变换的目的是将摄像机放置于坐标原点,方便后续步骤的操作。
2 顶点着色 Vertex Shading
确定材质上的光照效果的这种操作被称为着色,着色过程涉及在对象上的各个点处计算着色方程,而一部分计算是在模型顶点上执行的,而其他计算可以在每像素光栅化期间执行。可以在每个顶点处存储各种材料数据,结算完成后,会被发送到光栅化阶段进行插值操作
【总结】顶点着色阶段的目的在于确定模型上顶点处材质的光照效果。这里引入顶点着色器(Vertex Shader),顶点数据来自于CPU,主要工作是将:顶点坐标从模型空间转换到齐次空间,逐顶点光照
o.pos = mul(UNITY_MVP,v.position)
GPU在每个输入的网格顶点上都会调用顶点着色器,顶点着色器必须进行顶点的坐标变换,同时还可以计算和输出顶点的颜色
顶点着色器会将模型顶点的位置变换到其次裁剪坐标空间下,进行输出后再由硬件做透视除法得到NDC空间下的坐标
在齐次裁剪空间的基础上进行透视除法(perspective division)或称齐次除法(homogeneous division),得到的坐标叫做NDC空间坐标。
补充渲染流水线:
模型数据-顶点着色 - 曲面细分 - 几何着色器 - 裁剪 - NDC空间 - 屏幕空间 - 光栅阶段 - 帧缓存
其中曲面细分和几何着色器是可选项,而从裁剪空间---NDC空间---屏幕空间一般由底层帮我们自动完成。我们的顶点着色器只需要把顶点转换到齐次裁剪空间就可(就是一般所说的输出的positionCS)空间。
透视除法:就是将齐次裁剪空间坐标positionCS的X,Y,Z分量都除以W分量。
我们假设在NDC空间中,经过透视除法后的点为:那么x’,y’,z’的取值范围则在【-1,1】区间内。反推会齐次裁剪空间的公式:NDC空间到屏幕空间:顶点着色器我们的输出在齐次裁剪,那么片段着色器的输入是什么呢?不是齐次裁剪空间,也不是NDC空间,而是屏幕空间。在Unity中屏幕左下角像素坐标为(0,0),右上角像素坐标为(pixelWidth,pixelHeight),由于当前x,y的坐标范围为【-1,1】,因此是一个缩放的过程,可以用下面公式变换:
ScreenX = NDCx * pixelWidth/2 + pixelWidth/2;
ScreenY = NDCy * pixelWidth/2 + pixelWidth/2;
ScreenZ的值通常为Clipz/Clipw,存入深度缓冲,但也不是必须。
注意上面所说的屏幕空间和使用unity内置函数o.screenPos = ComputeScreenPos(o.vertex)不同,内置函数直接从齐次裁剪空间转换,没有经过透视除法转换到NDC空间的过程,所以如果使用内置函数,需要自己在片段着色器中不上透视除法。
如果需要从屏幕空间转换到NDC空间,可以使用内置函数ComputeScreenPos(o.vertex),求得ScreenPos后进行下面公式转换:
float4 ndcPos = (o.screenPos / o.screenPos.w) * 2 - 1;
3 投影 Projection
在光照处理之后,渲染系统就会开始进行投影操作,将视体变换到一个对角顶点分别是(-1,-1,-1)和(1,1,1)单位立方体,目前,主要由两种投影方法:正交投影,透视投影
两种投影方式的主要异同点:
• 正交投影。正交投影的可视体通常是一个矩形,正交投影可以把这个视体变换为单位立方体。正交投影的主要特性是平行线在变换之后彼此之间仍然保持平行,这种变换是平移与缩放的组合。
• 透视投影。相比之下,透视投影比正交投影复杂一些。在这种投影中,越远离摄像机的物体,它在投影后看起来越小。更进一步来说,平行线将在地平线处会聚。透视投影的变换其实就是模拟人类感知物体的方式。
• 正交投影和透视投影都可以通过 4 x 4 的矩阵来实现,在任何一种变换之后,都可以认为模型位于归一化处理之后的设备坐标系中。
【总结】投影阶段就是将模型从三维空间投射到了二维的空间中的过程。
4 裁剪 Clipping
主要工作:裁剪掉那些不在摄像机视野范围的物体(不需要被处理)
只有在单位立方体的图元才需要被继续处理。因此,完全在单位立方体外部的图元(红色三角形)被舍弃,完全在单位立方体内部的图元(绿色三角形)将被保留。和单位立方体相交的图元(黄色三角形)会被裁剪,新的顶点会被生成,原来在外部的顶点会被舍弃
【总结】裁剪阶段的目的,就是对部分位于视体内部的图元进行裁剪操作。
5 屏幕映射 Screen Mapping
主要工作:把每个图元的坐标转换到屏幕坐标系 (Screen Coordinates) 下
1.4 光栅化阶段The Rasterizer Stage
光栅化阶段即从二维顶点所处的屏幕空间(都包含深度值及各种与相关的着色信息)到屏幕上的像素的转化。
把一堆三角形变成真正的图(打散成像素)
1.4.1三角形设定 Triangle Setup
来源:三角网格的顶点数据
主要工作:计算三角网格表示的数据
1.4.2三角形遍历(Triangle Traversal)
主要工作:检查每个像素是否被一个三角网格所覆盖。如果被覆盖的话,就会生成一个片元 (fragment) 。 而这样一个找到哪些像素被三角网格覆盖的过程就 是三角形遍历,这个阶段也被称为扫描变换
三角形遍历的过程。根据几何阶段输出的顶点信息,最终得到该三角网格覆盖的像素位置。对应像素会生成一个片元,而片元中的状态是对三个顶点的信息进行插值得到的。例如,对图中三个顶点的深度进行插值得到其重心位置对应的片元的深度值为-10.0
【总结】找到哪些采样点或像素在三角形中的过程通常叫三角形遍历(TriangleTraversal)或 扫描转换(scan conversion)。
1.4.3片元着色器(Fragment Shader)
//DirectX中叫作像素着色器(Pixel Shader)
输入:输入是上一个阶段对顶点信息插值得到的结果
输出:是一个或者多个颜色值
根据上一步插值后的片元信息,片元着色器计算该片元的输出颜色
像素着色阶段是在可编程 GPU 内执行的,在这一阶段有大量的技术可以使用,其中最常见, 最重要的技术之一就是纹理贴图(Texturing)
1.4.4 融合Merging/逐片元操作 Per-Fragment Operations
主要工作: a.决定每个片元的可见性
b.如果一个片元通过了所有的测试 就需要把这个片元的颜色值和已经存储在颜色缓冲区颜色进行合并(混合)。
这个阶段还负责可见性问题的处理。这意味着当绘制完整场景的时候,颜色缓冲器中 应该还包含从相机视点处可以观察到的场景图元。对于大多数图形硬件来说,这个过程是通 过 Z 缓冲(也称深度缓冲器)算法来实现的。Z 缓冲算法非常简单,具有 O(n)复杂度(n 是需要绘制的像素数量),只要对每个图元计算出相应的像素 z 值,就可以使用这种方法:
Z 缓冲器和颜色缓冲器形状大小一样,每个像素都存储着一个 z 值,这个 z 值是从相机到最近图元之间的距离。每次将一个图元绘制为相应像素时,需要计算像素位置处图元的 z 值,并 与同一像素处的 z 缓冲器内容进行比较。如果新计算出的 z 值,远远小于 z 缓冲器中的 z 值, 那么说明即将绘制的图元与相机的距离比原来距离相机最近的图元还要近。这样,像素的 z 值和颜色就由当前图元对应的值和颜色进行更新。反之,若计算出的 z 值远远大于 z 缓冲器 中的 z 值,那么 z 缓冲器和颜色缓冲器中的值就无需改变。
//对于不透明物体,开发者可以关闭混合(blend)操作,这样片元着色器计算得到的颜色值会直接覆盖掉颜色缓冲区的像素区。但对于半透明物体,我们就需要用混合操作来让这个物体看起来是透明的
而当图元通过光栅化阶段之后,从相机视点处看到的东西就可以在荧幕上显示出来。为了避 免观察者体验到对图元进行处理并发送到屏幕的过程,图形系统一般使用了双缓冲(double buffering)机制,这意味着屏幕绘制是在一个后置缓冲器(backbuffer)中以离屏的方式进行 的。一旦屏幕已在后置缓冲器中绘制,后置缓冲器中的内容就不断与已经在屏幕上显示过的前置缓冲器中的内容进行交换。注意,只有当不影响显示的时候,才进行交换。
【总结】主要任务是合成当前存储于缓冲器的由之前的像素着色阶段产生的片段颜色,还负责可见性问题的处理
Unity中不透明物体的渲染顺序:从前往后
也就是说深度小的先渲染,其次再渲染深度大的
Unity中透明物体的渲染顺序:从后往前(类似画家算法,会造成 OverDraw)