可可西

三维图形渲染管线

三维图形渲染管线(Graphics pipeline)就是将三维场景转化为一幅二维图像的过程。 

图像中物体所处位置及外形由其几何数据和摄像机的位置共同决定,物体外表是受到其材质属性、光源、纹理及着色模型所影响。

管线过程由3个大的阶段组成:

 

Application(应用程序阶段)

运行在CPU上,能被开发者完全控制,该过程所做操作包括:

① 准备场景数据

     a. 加载模型:Mesh、Material、Shader、Texture(硬盘 --> 内存 --> 显存)

     b. 摄像机(位置、朝向、视锥体)

     c. 光源(位置、类型等参数信息)

② 裁剪和剔除 :视锥裁剪、遮挡剔除 (Occlusion Culling)

③ 计算模型视图矩阵

④ 设置渲染状态(RenderState)

渲染管线内部维护着一些状态值。在我们调用渲染API函数进行绘制之前我们需要设置这些状态值。

这些状态值指导GPU如何渲染我们传递到显存的模型和纹理数据。我们称这些状态值为“渲染状态(Render States) ”。

渲染状态包括Shader、Texture、Material、Light内部定义的各种状态等

最后,发起DrawCall调用

 

Geometry(几何阶段)

负责与每个渲染图元打交道,进行逐顶点、逐多边形的操作。

其重要任务是把顶点坐标变换到带有深度的屏幕空间中,再交给光栅器进行处理。

可进一步分割成:模型视图变换,顶点着色,[曲面细分],[几何着色],投影,裁剪及屏幕映射

 

模型变换

将模型从模型空间(Model Space,又称本地空间或局部空间,Local Space,或称物体空间,Object Space)变换到世界空间(World Space)

模型的Postion乘以Model矩阵,就可以从本地空间变换到世界空间

注1:在模型空间和世界空间中,D3D为左手系,OpenGL为右手系  【数学中使用的是右手系】

注2:在模型空间和世界空间中,UE为左手系(x向前,y向右,z向上),Unity为左手系(z向前,x向右,y向上)

注3:左手系中顺时针为旋转正方向,而右手系中逆时针为旋转正方向

 

视图变换

将各个模型从世界空间变换到眼空间(Eye Space,以摄像机为原点,又称观察空间,View Space、摄像机空间)

注1:在眼空间中,D3D为左手系,OpenGL为右手系

注2:在眼空间中,UE为左手系,与D3D一致;Unity为右手系,与OpenGL一致

通常会把这两个变换矩阵结合成ModelView矩阵,并将这个过程称之为模型视图变换

模型的Postion乘以ModelView矩阵,就可以从本地空间变换到观察空间

 

投影变换

将各个模型从眼空间变换到裁剪空间(Homogeneous Space,又称齐次裁剪空间)

这里用到的矩阵为投影矩阵(DIP矩阵,又称裁剪矩阵),按照投影类型分为透视投影与正交投影

通过近平面(Near)、远平面(Far)、垂直视场角(Vertical Field of View,FOV)和屏幕纵横比(Aspect Ratio,也叫屏幕宽高比)来定义视锥体

转换到裁剪空间的目的是为了方便高效地对图元进行裁剪

模型的Postion乘以ModelViewProject矩阵,就可以从本地空间变换到投影空间

 

顶点着色器(Vertex Shader)

主要功能是修改顶点属性。如:通过传入模型视图矩阵(MVP)进行顶点空间变换(位置属性)、逐顶点光照(颜色属性)、纹理坐标变换(uv属性)等

顶点着色器的处理单元是顶点,也就是说,输入进来的每个顶点都会调用一次顶点着色器。

顶点着色器只能对输入顶点的相关属性进行修改、创建和忽略,不可以创建或销毁任何顶点,而且无法得到顶点与顶点间的关系。

输入一般是一个变换矩阵和一个相对坐标;输出为裁剪空间的齐次坐标(这个是VS必须完成的任务)及每个顶点所附带的其他属性,如颜色、纹理坐标

 

曲面细分着色器(Tessellation Shader)

用于细分图元,分为3个阶段。

a. Tessellation Control Shader(D3D中叫Hull Shader),负责把控后续阶段的初始化操作,例如细化程度(可编程)

b. 处理Control阶段的输出,细化Patch数据(D3D中叫做Tesselator)(不可编程)

c. Tessellation Evaluation Shader(D3D中叫Domain Shader)的输入为Patch数据;输出数据为顶点着色器所应输出的数据,但是是批量的(可编程)

 

几何着色器(Geometry Shader)

输入是1个图元,输出是N个图元(N>=0)

通过Shader程序可以指定Geometry Shader对顶点信息进行增减。还有,因为实际增减的是图元顶点,所以对各种的线段、多边形、粒子等图元也可以进行增减。

利用Geometry Shader的各种方法被创造出来,因为可以自由的生成多边形,那么就可以在地面上生长出草的多边形,或者让3D角色生长出毛发等是最基本的使用方法。

在游戏中,还可以把不需要做逻辑交互处理的例如火花等特效的表现,使用Geometry Shader来生成。

注:Geometry Shader通常是在display driver中实现的,也就是说其实是由CPU负责计算,当重新返回GPU的VS时,对流水线的影响很大,所以Geometry Shader的实际效能并不高,甚至是非常低

 

裁剪

在裁剪空间中,将那些不在视景体中的顶点裁剪(Clip)掉

同时满足以下3个条件的顶点表明在视景体内,否则就是在视景体外

−Wc<=Xc<=Wc

−Wc<=Yc<=Wc

−Wc<=Zc<=Wc(OpenGL)、0<=Zc<=Wc(D3D)

 

在此阶段可添加自定义的裁剪面;还可设置背面剔除(Back-Face Culling),来剔除那些背对摄像机的图元。

注:通常情况下,三角形的3个顶点逆时针顺序(couter-clockwise,ccw)排列时,我们认为是正面(front-face);而顺时针(clockwise,cw)排序时,我们会认为是背面(back-face)。

 

屏幕映射

进行透视除法得到CVV(canonical view volume,规则观察体),并将每个图元在CVV的NDC坐标(Normalized Device Coordinates,归一化设备坐标,标准化设备坐标)的x、y分量转换到屏幕空间

需要注意的是,在NDC为左手系坐标,x、y的范围为[-1.0, 1.0];而z的范围OpenGL中为[-1.0, 1.0],D3D中为[0.0, 1.0]

最后,将NDC坐标进一步转换成屏幕坐标和z深度值

注1:D3D9的原点为左上角像素的左上角,D3D10+的原点为左上角像素的中心,x轴向右,y轴向下;OpenGL的原点为左下角像素的中心,x轴向右,y轴向上

注2:D3D直接将NDC的z作为深度值;OpenGL会将NDC的(z+1)/2来作为深度值,都归一化到[0.0, 1.0]

注3:屏幕坐标,UE与D3D一致,Unity与OpenGL一致

 

Rasterizer(光栅化)

对上个阶段得到的图元各顶点进行插值(z深度值、法线方向、纹理坐标、颜色等)来产生屏幕上的像素,并渲染出最终的图像。

光栅化的任务主要是决定每个渲染图元中的哪些像素应该被绘制在屏幕上

三角形设置

对三个顶点插值计算三角形边上的像素

三角形遍历

扫描三角形边上的像素来插值计算整个三角形内的像素

片元着色器(Fragment Shader)

D3D中叫Pixel Shader。逐片元的进行着色计算(即逐像素光照)。该阶段可以完成很多重要的渲染技术  如:纹理采样(Texture Sampling)

逐像素、逐顶点光照差异性主要体现在对于非精细模型,在执行逐顶点光照时,由于点距较大,在进行颜色线性插值的过程中,无法精细平滑过渡,导致效果变差。

另外逐像素光照可以在渲染时添加并不存在的表面细节。如通过bump贴图或normal贴图,在原本平坦的表面表现出近似的凹凸效果。

当然,逐像素的计算量要比逐顶点要大

 

逐片元操作

有时也被称为光栅操作(raster operations ,ROP)或混合操作(blend operations),通过设置来淘汰一些不合格的片元以及如何合并问题

如果一个片元通过了所有的测试,新生成的片元才能和颜色缓冲区中已存在的像素颜色进行Alpha混合,并写入颜色缓冲区

Alpha测试

注1:并非所有显卡都支持Alpha测试特性,使用前需要检查显卡是否有该能力

注2:由于大量片元会在该阶段舍弃,Alpha测试可提高含大量透明物件场景的性能

注3:固定管线才会有上面流程的Alpha测试,可编程管线直接在片元着色器或像素着色器里面实现类似的功能,如下所示:

/*** GLSL ***/
void PSMain()
{
    vec4 texColor = texture(g_Texture, g_TexCoord);
    
    // 进行Alpha测试裁剪,texColor.a<0.1f,直接丢弃当前像素
    if (texColor.a < 0.1f)
        discard;
    
    FragColor = texColor;
}
// ---------------------------------------------------------------
/*** HLSL ***/
float4 PSMain(VertexPosHWNormalTex pIn) : SV_Target
{
    float4 texColor = g_Texture.Sample(g_SamLinear, pIn.Tex);
    
    // 进行Alpha测试裁剪,texColor.a<0.1f,直接丢弃当前像素
    clip(texColor.a - 0.1f);

    return texColor;
}

更多信息详见:OpenGl alpha test - How to replace AlphaFunc deprecated? 

 

模板测试

注1:若建立模板缓冲区为8bits,则模板值的范围为:[0, 255]的整数;其初始值为清理模板缓冲区的所设置的值

注2:若在模板测试时,关闭了深度测试,则深度测试始终通过

深度测试

注1:深度值范围(OpenGL和D3D的深度范围为:[0.0, 1.0] )

注2:建立深度缓冲区位数越多,则深度值的精度就会越高;其初始值为清理深度缓冲区的所设置的值

注3:关闭了深度测试,意味着该片元始终通过深度测试

Alpha混合

FrameBuffer(帧缓冲)

帧缓冲器frame buffer):在显卡中硬件实现,用于存放渲染的最终结果。分为:单缓冲、双缓冲(double buffering)、三重缓冲(Triple Buffering)

单缓冲:各个物体的渲染会直接画在屏幕上,效率比较低,由于能看到中间绘制过程,会导致屏幕不断闪烁。一般只用于显示非动态的图像

双缓冲(double buffering):绘制是在一个后备缓冲器(backbuffer)中以离屏的方式进行的。一旦在后备缓冲器中完成绘制,

通过交换指令(D3D为Present、OpenGL为SwapBuffer)就可将后备缓冲器中的内容与已经在屏幕上显示过的前台缓冲器(frontbuffer)中的内容进行交换,使得一个完整的帧显示在屏幕上。

完成交换后,后备缓冲器变为前台缓冲区,而前台缓冲区变为后备缓冲区,为下一帧的绘制工作提前做好准备。

我们将前后缓冲区功能互换的行为成为提交(Presenting)。

由于只是将前台缓冲区的指针和后备缓冲区的指针做一个简单的交换,提交是一个运行速度很快的操作。

// OpenGL单缓冲
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);
glFlush(); //单缓冲的刷新模式;

// OpenGL双缓冲
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB);
glutSwapBuffers(); //双缓冲的刷新模式;

三重缓冲(triple buffering):一个前台缓冲区,两个后备缓冲区。

在开启了VSync垂直同步时,若游戏的FPS低于显示器刷新频率,三重缓冲可缓解卡顿现象,然而由于存在2个后备缓冲区,三重缓冲会导致画面有一帧的延迟。(见下文说明)

显示器

以CRT显示器为例(液晶显示器原理类似),CRT的电子枪从左到右,从上到下进行逐行扫描,扫描完成后显示器就呈现一帧画面,随后电子枪回到初始位置继续下一次扫描。
为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。
当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;
而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。
显示器通常以固定频率(如60HZ)进行刷新,这个刷新率就是 VSync 信号产生的频率。

将显卡与显示器的刷新频率通过一个称为VSync的信号同步起来,保证显示器上显示的是一帧完整的画面,来解决Tearing(撕裂)现象(多帧画面同时绘制在显示器上)。

 

假设游戏的FPS是100,显示器的刷新频率是75Hz,显卡将比显示器快1/3;这意味着,在1个显示器刷新周期内,显卡将写入4/3的帧数据,也就是说,下一帧的1/3覆盖在前一帧之上;
当然,随着系统运行,1/3这个比例会发生变化,1/3,2/3,1,1/3,循环;这种帧与帧之间的不完全覆盖重合现象就是撕裂现象。

注:如果帧率足够高,即便图像有撕裂,你也感受不到——这正是PC上如果一个游戏稳定运行有140帧以上,事实上存在撕裂,你也感受不到的原因(PC游戏默认不开Vsync)。
       如果你能看到如此明显的撕裂,只能说明当前帧率极其低了。你通常不太会看到这样的撕裂。你能感受到的只是不顺滑,像上面这张图所展示的通常是快速截取一帧所呈现出来的现象。

D3DPRESENT_PARAMETERS md3dPP;
md3dPP.PresentationInterval = D3DPRESENT_INTERVAL_ONE; // 开启垂直同步

当开启了垂直同步,若游戏FPS高于显示器刷新频率时,显卡会将一部分时间浪费在等待上,显卡必须等待VSync信号的到来才能将绘制好的画面推送给显示器,这也使得游戏的最大FPS下降为显示器的刷新频率

这避免了显卡做一些无用的工作,降低显卡的功耗;然而,VSync技术也有缺点,会导致玩家输入的响应出现延迟;另外,若游戏的FPS低于显示器刷新频率,那么系统的FPS将迅速下降为显示器刷新频率的分数倍上,加剧画面卡顿(Shutter)

Triple Buffering(三重缓冲)可以缓解这一问题,示意图如下:

然而,从上图可以看出由于存在2个后备缓冲区,三重缓冲会导致画面有一帧的延迟

 

垂直同步会将游戏的FPS限制为显示设备的刷新率,其最大的问题是会导致玩家输入延迟,因为它会要求显卡在渲染完毕后等待显示设备去刷新数据。这对对竞技游戏的影响是很大的。

在PC平台上有以下技术来解决:

G-Sync技术:如果你是NVIDIA显卡,且你的显卡和显示器均支持G-Sync,则显示器中的特殊芯片会与显卡进行通讯,使显示器调整其刷新率以匹配显卡的帧率。

FreeSync技术:用于AMD显卡,是基于DP接口(DisplayPort)所支持的自动同步技术(Adaptive-Sync)实现的。实现原理与G-Sync类似,也需要显卡和显示器均支持FreeSync。

                          FreeSync技术目前已被Android手机使用,但没有被大规模普及。更多请参考:Android上的FreeSync(下)实现方法

注:G-Sync和FreeSync都是基于VRR(variable refresh rate)的技术,采用可调整的VSync(垂直同步),让显卡与显示器步调一致。只是实现思路不同。 而思路的差异主要在于成本/兼容性等方面的考虑。 

 

总结

(Vertex Shader) => Clip Space => (透视除法) => NDC => (视口变换) => Screen Space => (Fragment Shader)

 

参考

【《Real-Time Rendering 3rd》 提炼总结】(二) 第二章 · 图形渲染管线 The Graphics Rendering Pipeline

【《Real-Time Rendering 3rd》 提炼总结】(三) 第三章 · GPU渲染管线与可编程着色器

 深入理解OpenGL之投影矩阵推导

《Unity Shader入门精要》随书彩色插图 

UE4:坐标空间术语en) 

Real-Time Rendering(翻译)

细说图形学渲染管线

VSync与屏幕撕裂

posted on 2018-06-04 22:05  可可西  阅读(2371)  评论(0编辑  收藏  举报

导航