本文总结了 Real-Time Rendering (第四版) 中第一章到第二章的内容
这里仅总结在书中自己认为或者不懂的要点并结合实际工作学习所需,若内容讲解不详细还请谅解
-
什么是实时渲染(Real-Time Rendering)--- Chapter 1
实时渲染(Real-Time Rendering)可以极快的渲染图像以及 3D 模型,并且可以在此基础上实现流畅的人机交互。然而仅仅有速度和交互并不是实时渲染的全部:实时渲染也包括了对图形硬件的使用。
-
渲染管线中的基本元素和定义 --- Chapter 1
在渲染管线中,我们使用向量(vector)和矩阵(matrix)进行运算。其中的数学运算在此略过。
基于这两个数学元素,我们定义了其他在管线中使用的元素或概念,包括三维模型顶点(vertex),法向量(normal)等实际运用,也可以延伸出线(line)三角形(triangle)和面(face)等概念应用。
在书中,图元(rendering primitives)包括了上面提到的点线面等元素,这些元素构成模型(Model)和物体(Object)。物体和模型又被包括在场景(Scene)中集中渲染,且场景中也可以包括其他属性,比如光源(Light source)等。
-
图像渲染管线(The Graphics Rendering Pipeline)--- Chapter 2
渲染管线将三维模型转换至二维屏幕上,其中经历了四个主要阶段:应用程序阶段(application),几何处理阶段(geometry processing),光栅化阶段(rasterization),像素处理阶段(pixel processing)(如图 1 所示)。这四个阶段在实际管线中可能是平行工作,部分平行或者分阶段工作。其中,应用程序阶段是完全由 CPU 处理,光栅化和像素处理则是完全由 GPU 主导。其中曲面细分(tessellation)和几何着色(geometry shading)这两个用于细化模型和制作粒子特效的可选阶段跳过,有兴趣的朋友可以看一下这篇文章 [2]。
图 1:渲染管线四阶段
- 应用程序阶段(application)
这一阶段使用 CPU 处理应用程序需要处理的事情,比如在游戏引擎中,用户输入,碰撞检测(collision detection)和物理相关的计算会在这个阶段执行。在这个阶段,程序员拥有完全控制权。
在渲染管线中,这一阶段可以是对向下一个阶段输入需要的顶点信息等进行预处理,比如剔除不需要渲染的顶点或者其他加速算法等。在软光栅化过程中,这个阶段同其他三个阶段一样,并没有被分成多个子阶段。但在实际应用中为了提升性能,在这个阶段中会使用多线程计算进行优化。
- 几何处理阶段(geometry processing)
几何处理阶段会对三角形,顶点等图元进行处理以及预处理,这个阶段也可以被分为四个子阶段:顶点着色(vertex shading),投影(projection),裁剪(clipping),屏幕映射(screen mapping)。(这里仅介绍主要步骤)
图 2:几何处理阶段的四个子阶段
- 顶点着色(vertex shading)
这一阶段主要对顶点位置和属性进行处理,包括顶点几何坐标转换和对材质,光等属性进行处理,后者就是常说的着色处理(shading)。程序员使用顶点着色器(vertex shader)对 GPU 进行编程而对顶点进行处理。
- 投影(projection)
将待渲染对象进行视角转换。一般有两种视角:正交(orthographic)和透视(perspective),其中透视投影是模仿人眼视角的投影,遵循近大远小的规则。
- 裁剪(clipping)
将在视方体之外的顶点进行裁剪或者剔除。具体可见我的这一篇随笔。
- 屏幕映射(screen mapping)
在这个阶段会根据屏幕的大小比例将顶点映射到屏幕对应的位置,也就是将之前投影后的坐标中的 X Y 提取出来得到该点在屏幕上的坐标,举例如图三:
图 3:屏幕映射
但在这个阶段(严格来说是投影阶段)后,顶点的深度坐标 Z 不会被丢弃,而是会被传入下一个阶段,用于后面的深度测试。Z 用于判断顶点距离镜头的距离,且将会在像素处理中用于判断某个像素是否在其他像素之前或之后以判断是否需要需要渲染,像素的深度值将会由插值所得。
- 光栅化阶段(rasterization)
在光栅化阶段,渲染器负责处理图元(在书中提到的是三角形)和生成像素数据,再将这些数据传递给像素处理阶段。其中,光栅化阶段也分为两个子阶段,步骤如图四(注意只有前两个是光栅化阶段所做的事情):
图 4:光栅化阶段以及像素处理阶段
-
设置三角形面(triangel setup)
这一阶段负责计算与三角形图元有关的计算,比如点线等,并将这些图元组装成三角形面,这一步也叫图元组装(primitive assembly)
-
三角形遍历(triangle traversal)
在上一阶段生成三角形图元后,渲染器需要判断哪些哪些像素在三角形内。这些在三角形内的图元将会被渲染(填充对应的颜色)。寻找在三角形内的像素的过程被称为三角形遍历。在这个过程中,三角形将会被整体扫描一遍,在三角形内的像素相会在下一阶段被渲染,在这一阶段仅仅是判断在三角形内的像素并标记。在透视视角下,三角形内的像素也会被进行透视校正插值以呈现正确的视角。图解如下:
图 5:三角形遍历,黄色像素在三角形内 [2]
在书中主要提到的是这种简单的像素标记,但这样将整个像素标记的形式很可能使渲染图形产生锯齿。关于抗锯齿的方法可以了解下 Supersampling, Multisampling antialiasing techniques 等抗锯齿技术。
- 像素处理阶段(pixel processing)
在光栅化进行完后,渲染器已经知道哪些像素需要处理并在这个阶段处理像素。这个阶段也分为两个子阶段:像素着色(pixel shading)和合并(merging)。
- 像素着色(pixel shading)
与光栅化完全依赖图像处理硬件不同,在像素着色阶段,程序员可以向在顶点着色一样,使用可以编程的 GPU 核心对像素进行操作,也就是像素着色器(pixel shader),在 OpenGL 中则叫做 片元着色器(fragment shader) 。除此之外,在这个阶段也可以处理贴图(texture)。使用顶点着色器将贴图根据贴图坐标附着在模型上。
- 合并(merging)
从上一个阶段得来的像素的颜色信息将会被存储在颜色缓存(color buffer)里,在这个阶段要结合颜色缓存中的信息和像素着色器中的处理信息生成最终要渲染的片元颜色信息。这个阶段也叫做 ROP (光栅操作管线,raster operations pipeline)。进行颜色混合的过程也叫做 Color Blending。
合并阶段也负责处理像素的可见度(visibility),其中要使用顶点的深度信息进行深度测试(depth test),从之前阶段传递过来的深度信息将会被存储在深度缓存(Z-buffer)内。在不考虑像素透明度的情况下(没有透明物体渲染),渲染器将会比对每一个需要被渲染的像素的深度 Z:在相同的屏幕位置上,越小的 Z 意味着离镜头越近,则这个像素将会被渲染。且这个像素挡住了其他的像素,在这之后的像素则不会出现在屏幕上(有可能被渲染但却被 Z 更小的像素覆盖了)。
然而仅仅简单的使用深度缓冲是没法处理透明物体的渲染:比如渲染一面玻璃和玻璃后面的物体,在这个情况下不能使用简单的遮挡关系来判断像素是否渲染,否则玻璃后的物体像素则会因为被玻璃遮挡而无法渲染。渲染器使用 alpha test 来解决这个问题:在管线中,alpha channel 中的像素透明度信息用于判断像素的透明度,并结合像素的颜色信息和深度信息决定如何渲染或剔除像素。
除了颜色缓存和深度缓存,渲染管线中还有模板缓存(stencil buffer)和帧缓存(frame buffer)等各种缓存用于在渲染到屏幕前对像素进行处理。模板缓存被用于记录渲染图元的位置,可以被用于提升渲染效率和实现效果(比如加给三维物体加阴影和平面反射)。
帧缓存集合了渲染管线的各个缓存,并用于在实际渲染到屏幕上前进行缓冲。帧缓冲的架构一般是双缓冲(double buffering)的,一前一后。前缓冲(front buffer)将图像内容传递到屏幕缓冲(screen buffer)并最终呈现渲染内容,后缓冲(back buffer)则进行下一帧的渲染,当后缓冲完成渲染后则将这一帧内容与前缓冲存储的上一帧内容进行交换。之所以设计双缓冲是为了避免人眼看到渲染图元的过程而导致画面不流畅;渲染管线需要呈现流畅的显示,而单缓冲则有可能会造成画面撕裂或者跳帧等问题,而双缓冲的渲染不会直接显示在屏幕上(离屏渲染)而是在渲染完一帧后再显示,从而解决的单缓冲带来的问题。
-
总结
以上所说的渲染管线流程是总结许多为了实现实时渲染的图形 API 所采用的渲染策略,但一定要知道以上所讲的管线这并非唯一的渲染管线策略:比如电影特效中所使用的离线渲染(offline render)是为了追求效果而舍弃效率的渲染方式,它的渲染方式则和实时渲染不同。还有更久远的无法进行灵活的 GPU 编程的固定渲染管线(fixed-function pipeline)使用的渲染策略也不一样。这里仅需作为一个知识点了解一下。
参考资料:
[1] Real-Time Rendering fourth Edition (大部分内容以及图片来自于此书)