从模型到屏幕展示这中间发生了什么(图形渲染管线)

96 阅读9分钟

简介

做游戏的,那么就不能不来了解渲染管线了,理解 “渲染管线的过程”,其重要性,相对于 web 来说,就如理解“从输入URL到页面展示这中间发生了什么”。 说到渲染管线可能我们会遇到两个不同的概念:功能性阶段硬件阶段。 其实两者都描述了渲染管线的组成,不过是角度和侧重点不同。

  • 功能性阶段:通常是指渲染管线的抽象阶段,描述了渲染过程需要完成的主要任务,是我们按照渲染过程中实现的基本功能划分概念。
  • 硬件阶段: 也称之为GPU硬件管线阶段,是指实际在GPU硬件上执行的阶段。这些阶段可能会和上述的功能性阶段所对应,但也可能不同,因为为它们是根据硬件的设计和优化来定义的。例如,一些现代GPU可能会并行处理多个阶段,或者在硬件上实现一些特定的优化。

两者的概念,前者能更便于我们理解渲染管线的过程,而后者能帮助我们如何最有效的去利用 GPU硬件资源。这里下文我们从功能性阶段去理解,如下图:

图形渲染一般分为了,四个重要阶段,理解这四个阶段都做了什么,才能清晰的认识到图形是如何展示在我们屏幕上的!

  • 应用阶段(Application)
  • 几何阶段(Geometry Processing)
  • 光栅化阶段(Rasterization)
  • 像素阶段(Pixel Processing)

一、应用阶段 (Application)

1、是什么:

这个阶段通常是在 CPU 发生,处理与渲染相关的应用逻辑,比如我们的碰撞检查,物理效果的模拟等等之类。如今我们游戏开发一般都是使用到游戏引擎比如 cocos / Unity 等,引擎中很多这类操作 CPU 的底层操作都帮我们处理好了,使得我们只需要专注效果。

当所需要计算的都计算完之后,在应用阶段的末端,这些计算好的数据也就是 渲染图元(rendering primitive)就会被发送到 GPU 进行处理

在渲染管线中,我们常说应用阶段是可控的,主要是因为我们在开发中有比较大的自由度来决定如何处理和准备渲染数据。比如我们最常接触到的分层渲染来降低 DrawCall,简单来说我们可以通过修改节点的排列顺序,避免打断合批,或者修改渲染顺序,优化渲染命令排序来减少发送绘制操作等,另外比如物理模拟和碰撞中的时候,我们把物体的包围盒改为包围球,也能提高性能。这些都是在CPU,在应用阶段进行的。而其他阶段通常都是在GPU 上进行,受限于不同厂家GPU和图形API 的限制,实际开发者能操作的并不多~

二、几何阶段(Geometry Processing)

1、是什么:

这个过程通常发生在 GPU 上,将上一阶段的 渲染图元进行 逐三角形(per-triangle)和逐顶点(per-vertex)操作,输出屏幕空间的顶点信息,说人话就是将 3D模型数据转换为屏幕可渲染的2D图元,同时也是大多数情况下,成为渲染性能瓶颈的地方。

2、细分过程:

细分执行过程如上图,可以划分为四步:

  • 顶点着色(vertex shading)

    • 在顶点着色中,有两个主要任务,计算顶点位置,以及计算那些开发人员想要作为顶点数据进行输出的任何参数(计算输出颜色:确定材质上光照效果)。
    • 计算顶点位置中,主要是需要把物体的模型空间 -> 世界空间 -> 视图空间
  • 投影(projection)

    • 在这里就是将摄像机的视图空间转化为裁剪空间,为后续裁剪和投射二维做准备。在顶点转化的过程中,引入了齐次坐标系,运用 W分量来衡量订点到摄像机之间的距离。而常见投影方式包含 投射投影(NDC中,W代表与摄像机的距离,近小远大, 常见3D中) 和 正交投影(NDC中,W等于1),
    • 实现透视变形(fov控制)
    • 完成视锥体到立方体的映射
    • 计算顶点位置中,主要是需要将 视图空间 -> 裁剪空间
  • 裁剪(clipping)

    • 只有那些完全或者部分位于视图空间的图元才会被发送到下一个阶段处理,然后绘制到屏幕上。在这里,对于部分处于视图空间内的,会被视图空间裁剪,裁剪之后生产新的订点,替代原本视图空间外的订点。
    • 裁剪过程会通过 投影矩阵 将可视空间转化为一个 x、y、z 三个坐标都在 [-1,1] 区间内的 标准立方体 , 所有图元都依据该立方体进行裁剪,最后通过 透视除法 将其转化为 标准化设备坐标
    • 计算顶点位置中,主要是需要将 裁剪空间 -> NDC 空间
  • 屏幕映射(screen mapping)

    • 这一步输入的坐标是三维坐标系的坐标,屏幕映射的过程是把每个图元的 x,y 坐标转换到屏幕坐标下
    • 计算顶点位置中,主要是需要将 NDC 空间 -> 屏幕空间

三、光栅化阶段

1、是什么:

在上一个阶段对顶点数据进行正确的变换和投影后,当前阶段的目标是,将 几何图元 转换为屏幕像素表示,将连续的几何数据离散化为栅格化的图像,也就是光栅化。

2、细分过程:

  • 三角形设置(triangle set up,也叫做图元装配,primitive assembly)

    • 该阶段会进行 三角形的微分,边界方程等其他计算,将几何阶段输出的三角形顶点数据转换为光栅化所需的预计算信息, 用于下面的 三角形遍历,以及对几何处理阶段产生的各种着色数据进行插值。这个阶段一般会使用固定功能的硬件实现,不受开发者直接控制。
    • 具体而言就是,在上一个阶段获得的是三角形网格的三个顶点,而要想获得三角形的覆盖面,就得把边界信息计算出来,以便判断像素是否在三角形内,同时做属性插值,为纹理映射、光照计算提供精确的插值参数
  • 三角形遍历(triangle traversal)

    • 遍历基于三角形网格三个顶点坐标计算出的 "包围盒" 内的每一个像素,对每一个位于三角形网格内的像素,生成一个片元,
    • 利用三角形三个顶点的属性进行插值,来获得每个片元的属性(颜色插值,纹理坐标插值,法线插值),
    • 片元生成时会插值计算深度值(z-buffer 值) ,用于后续的深度测试,也可能会计算透视修正,保证纹理不会因为投影变形而出现拉伸问题

四、像素处理阶段

1、是什么:

在上一个阶段后,拿到所有目标像素,当前阶段主要做的是对图元内部的的像素进行逐像素的计算和操作。

2、细分过程:

  • 像素着色(pixel shading)

    • 该阶段主要通过传入的片元,计算片元的最终颜色,这个过程中开发人员是能控制,可编程的,常见纹理采样,贴图操作,光线计算等等
  • 合并(merging)

    • 那么在上面拿到输出的颜色后,在合并阶段,本质处理多物体重叠时候显示优先级的问题
    • 深度测试: 对于可见性问题,依赖于 z-buffer(深度缓冲) ,更新的时候选择 z-buffer中的z值最小的,说明离相机最近,会挡住比它大的。
    • 模板测试: 模板测试是图形渲染管线中一种基于掩码(Mask)的像素级过滤技术,它通过一个与屏幕分辨率相同的模板缓冲区(Stencil Buffer)精确控制哪些片元可以写入帧缓冲。可以用来实现遮罩,描边,反射,分区域渲染等效果,具体对片元读取模板值,用设定的比较函数判断是否通过,来决定是保留,丢弃还是更新模板值。
    • 透明测试: 它通过阈值判定决定像素的保留或丢弃,属于逐片元测试阶段的重要操作
    • 混合: 如果片元的 Alpha 值小于 1(透明度),需要和帧缓冲中已有的颜色混合。
  • 颜色写入(color write)

    • 最后一步就是把颜色值写入到帧缓冲中了~

五、补充概念名词

1、图元、几何图元、渲染图元三个的区别:

  • 图元(Primitive): 图元是指渲染的基本图形,在图形渲染管线中,将几何物体定义为点、线和三角形作为基本渲染图元(rendering primitive),任何图像都可以由这三类组合而成,比如我们所见的圆,其实就是数千个三角形组合而成的。
  • 几何图元(Geometry Primitive): 这些是由图元组成的,用于描述对象的几何形状。它们是渲染管线中的输入数据,例如顶点数据、纹理坐标、顶点法线和顶点颜色等。
  • 渲染图元(Rendering Primitive): 这些是在图形渲染过程中实际被处理的图元。在OpenGL中,渲染图元包括了渲染所需的几何信息,如顶点数据、线段、多边形等,并且它们对应绘图界面上可见的实体。

2、什么是片元:

片元(Fragment) 是光栅化过程中生成的潜在像素数据,它代表屏幕空间中的一个候选像素点,携带了所有计算最终像素颜色所需的信息。

核心关系:片元是像素的“胚胎”,只有通过所有测试的片元才能成为像素。

3、渲染管线中有多少次坐标系的转化?

这个问题其实很有意思的,理解这里每次坐标的转化,也是理解整个渲染管线的关键,可以当做一条主干去一步步了解

具体的过程如下,同时包含了每一次转化的方式

image.png

下图我们可以对比看,每一个坐标系的特性

image.png

4、像素阶段有多少种缓冲区

一般有三大核心缓冲 颜色缓冲(Color Buffer)深度缓冲(Depth Buffer / Z-Buffer)模板缓冲(Stencil Buffer), 除了此之外还有累计缓冲,实现多重采样,抗锯齿,HDR合成。Alpha 缓冲,单独存储透明度,等等。

image.png