前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >(实时)渲染管线(pipeline)

(实时)渲染管线(pipeline)

原创
作者头像
Zero Two
发布2024-07-09 01:27:34
1340
发布2024-07-09 01:27:34
举报
文章被收录于专栏:Real-Time Rendering

References: 《Unity Shader 入门精要》 《Real-Time Rendering》

我也是一个学习者,如果有不对的地方请务必指出,感谢🙇

假设一个工具需要4个步骤才能够完成,那么一个人只能完成了全部的4个步骤后才能继续进行下一个工具的生产。但如果引入另外的3个人,每个人只负责一个步骤,那么一个人只需要完成一个步骤就可以进行下一个工具的生产。

理想情况下,将一个非流水线分为n个流水线,且每个阶段耗费时间相同的话,将会使整个系统得到n倍速度的提升。

渲染管线(理论)

那么将上面的概念应用到图形渲染中,就是渲染管线(pipeline)。

简单来说,渲染管线可以这样描述

下面我们将要解释渲染管线的逻辑或者说理论架构 (实际实现肯定有所不同),渲染管线可以简单分为4个阶段

应用阶段(application)、几何处理阶段(geometry processing)、光栅化阶段(rasterization)和像素处理阶段(pixel processing)

渲染管线可以并行执行,每个阶段依赖于前一个阶段的结果。每个阶段本身也可以继续流水线的细分,也可以并行化的执行。

应用阶段

这个阶段由应用主导,因此通常由CPU负责实现,也就是我们开发者具有这个阶段的绝对控制权。

但一些应用阶段的任务也可以让GPU以计算着色器(computer shader)的独立模式来执行 该阶段也可以划分为流水线

这个阶段的主要任务有:

  1. 准备场景数据、例如摄像机的位置、视锥体、场景中的模型、光源等信息
  2. 粗粒度剔除(culling),将不可见的物体剔除出去,减少不必要的计算。
  3. 设置每个模型的渲染状态,包括但不限于材质、纹理、shader等。

这一阶段最重要的是输出渲染所需的几何信息,即渲染图元(rendering primitives),通俗来讲,渲染图元可以是点、线、三角形等。这些渲染图元会传递到下一个阶段---几何阶段。

虽然主要任务就是输出渲染图元,但有些任务也可以在该阶段进行:

  1. 碰撞检测(collision detection)
  2. 处理设备或者其他来源输入
  3. 渲染管线无法处理的一切问题

几何阶段

从几何阶段开始的剩余3个阶段一般都是在GPU上运行。

几何阶段处理所有与几何相关的事情,例如决定需要绘制的图元是什么,怎么绘制它们,在哪里绘制它们。

几何阶段的一个重要任务是把顶点坐标变换到屏幕空间中,再交给光栅器处理。

光栅化阶段

光栅化阶段会利用上一阶段传递的数据来产生屏幕上的像素,并渲染出最终的图像。它需要对上一个阶段得到的逐顶点的数据(例如纹理坐标、顶点颜色)进行插值,然后进行逐像素处理。

这一阶段也可以分为更小的流水线阶段

像素处理阶段

渲染管线(GPU)

前文讲述的是简单的渲染管线的理论部分,下面要讲解的GPU管线,是从硬件的角度出发,来实现这个渲染管线。

CPU与GPU之间的通信

渲染管线的第一个阶段就是在CPU上运行的,而之后的阶段都需要在GPU上运行,所以CPU与GPU的通信就尤为重要。

应用阶段大致分为下面3个阶段:

  1. 把数据加载到显存中
  2. 设置渲染状态
  3. 调用Draw call

将数据加载到显存中

所有渲染所需的数据都需要从硬盘(Hard Disk Drive,HDD)加载到系统内存(Random Access Memory,RAM)。然后,网格和纹理等数据又被加载到显卡上的存储空间——显存(Video Random Access Memory,VRAM)中。显卡对于显存的访问速度更快,而且大多数显卡没有RAM的直接访问权限。

将数据加载到显存中后,RAM的数据就可以删除了。但是对于某些数据来说,CPU仍需要访问它们(例如需要网格数据进行碰撞检测),那么这些数据就不应删除。

当数据加载完毕后,开发者就要通过CPU来设置渲染状态,从而告诉GPU该如何使用这些数据渲染。

设置渲染状态

渲染状态可以简单理解为场景中的网格是怎样被渲染的,使用了什么着色器、光源属性、纹理材质等。如果不更改渲染状态,那么所有的网格都将使用同一种渲染状态。

准备好上述工作后,CPU就需要调用一个渲染命令来按照给好的数据以及渲染状态来渲染。而这个命令就是Draw Call。

调用Draw Call

Draw Call是一个命令,发送方是CPU而接收方是GPU。这个命令仅仅指向一个需要被渲染的图元列表,而不包含任何材质(等着色信息),因为已经设置了渲染状态。

当给定了一个Draw Call时,GPU就会根据渲染状态和所有输入的顶点数据进行计算,最终输出成屏幕上显示的那些像素。

GPU管线

GPU的渲染过程就是GPU管线。

对于几何阶段、光栅化阶段与像素处理阶段,开发者并不拥有它们的绝对控制权,无法完全控制这三个阶段的实现细节,但GPU仍然给了开发者很多的控制权。

它们还可以分为若干个更小的流水线阶段,每个阶段GPU都提供了不同的可配置性或可编程性。

绿色代表了这个阶段是完全可编程的,虚线边框代表了这个阶段是可选的。黄色代表了这个阶段是可配置的,但是非可编程的,例如我们可以为合并阶段设置不同的混合模式,但是无法完全对其进行编程控制。蓝色代表了这些阶段中的功能是完全固定的

在几何阶段中,顶点着色器(Vertex Shader)是完全可编程的,它通常用于实现顶点的空间变换、顶点着色等功能。曲面细分着色器(Tessellation)是一个可选的着色器,用于细分图元。几何着色器(Geometry Shader)是一个可选的着色器,可以用于执行逐图元的操作,或者产生更多的图元。下一阶段是裁剪(Clipping),这一阶段的目的是将那些不在摄影机可视空间内的顶点裁剪掉,并剔除某些三角图元的面片。几何阶段的最后一个是屏幕映射(Screen Mapping),它负责将每个图元的坐标转换到屏幕坐标系中。

光栅化阶段中,三角形设置与遍历(Triangle Setup & Traversal)是固定的。

像素处理阶段中,像素着色器或片元着色器(pixel shader or fragment shader)是完全可编程的,用于实现逐片元的着色操作。(计算每个片元/像素的颜色)。合并(merge)负责很多重要的操作,如修改颜色、深度缓冲、混合等,具有很高的配置性。

几何阶段

顶点着色器

顶点着色器的输入来自于CPU,它的处理单位是顶点,每个输入进来的顶点都会调用一次顶点着色器。顶点着色器本身不能创建或销毁任何顶点,并且无法得到顶点与顶点之间的关系,正是因为这样的相互独立性,GPU可以对这些顶点进行并行化处理。

顶点着色器主要完成的工作是坐标变换与逐顶点光照,除此之外,还可以输出后续阶段所需数据如法线、纹理坐标等等。

坐标变换,就是对顶点的坐标进行某种变换。顶点着色器可以在这一步改变顶点的位置,这在动画中是非常有用的,例如可以用来模拟布料、水面等。

不论如何进行坐标变换,顶点着色器必须完成的一个工作是将顶点坐标从模型空间转换到齐次裁剪空间之后再由硬件做透视除法后,最终得到归一化的设备坐标(Normalized Device Coordinates,NDC)。

坐标变换设计到多个步骤,后面再详细展开,这里更多讲解渲染管线本身

上图中给出的NDC坐标范围是OpenGL同时也是Unity的NDC,它的z分量范围在-1, 1之间,而在DirectX中,NDC的z分量范围是0, 1。

顶点着色器可以有不同的输出方式,最常见的输出路径是经光栅化后交给片元着色器处理。而在现代的Shader Model中,它还可以把数据发送给曲面细分着色器或几何着色器。

裁剪

渲染图元只要有一部分在可视空间内,整个渲染图元就会进入渲染管线处理,但不在可视空间的部分不会影响渲染结果,计算这部分会消耗资源。而裁剪就负责将不在可视空间外的部分处理掉,使用新的顶点来代替

屏幕映射

屏幕映射的任务是,将每个图元的x和y坐标转换到屏幕坐标系,屏幕坐标系是一个二维坐标系,它和我们用于显示画面的分辨率有很大关系。

假如现在需要将场景渲染到一个窗口上,窗口左下角为$$(x_1, y_1)$$,右上角为$$(x_2, y_2)$$,而我们输入的坐标范围是$$-1, 1$$(NDC范围),那么这个映射的过程就是一个缩放的过程。

而z坐标不会做任何处理,但z坐标与屏幕坐标系构成了窗口坐标系。这些值会被传递到光栅化阶段。屏幕映射得到的屏幕坐标决定了这个顶点对应屏幕上的哪个像素以及距离这个像素有多远(z轴)。

屏幕坐标在OpenGL和DirectX之间有一定差异。

微软的窗口都使用了DirectX这样的坐标系统,符合我们的阅读方式:从左到右,从上到下;并且很多图像文件的存储方式也是如此。开发者要时刻小心这样的差异。

光栅化阶段

光栅化阶段分为三角形设置(triangle set up,也叫做图元装配,primitive assembly)和三角形遍历(triangle traversal)。

三角形设置

几何阶段的顶点都是独立的,而在三角形设置阶段,会将这些顶点组装为三角形;并且计算出三角形的边界框,以确定大概有哪些部分的像素会被三角形所覆盖。

三角形遍历

该阶段会根据每个三角形的边界框,检查每个像素是否被一个三角网格所覆盖,如果覆盖的话就生成一个片元(fragment)。这一过程也被称为扫描变换(Scan Conversion)

同时,该阶段会使用三个顶点的顶点信息对生成的每个片元进行插值计算,计算出每个片元的各种信息(如纹理坐标、深度、法线等等)

最后会输出一个片元序列。需要注意的是,片元不等于像素,因为它包含了更多信息,如屏幕坐标、深度、法线、纹理坐标等等。

像素处理阶段

片元着色器

片元着色器的输入是上阶段计算出来的每个片元的插值信息,输出一个或多个颜色值。

这一阶段可以完成很多重要的渲染技术,其中最重要的技术之一就是纹理采样。为了完成纹理采样,首先需要在几何阶段输出每个顶点的纹理坐标,在光栅化阶段对三个顶点的纹理坐标插值得到每个片元的纹理坐标。

片元着色器的每个片元的计算也是独立的,也就是执行片元着色器时,不会向其他执行片元着色器的片元发送自己的任何结果。(当然有例外情况)

合并

逐片元操作(Per-Fragment Operations)是OpenGL中的说法,DirectX中叫做输出合并阶段(Output-Merger)。

模板测试和深度测试是比较复杂的过程,不同的图形API的实现细节也不同,下面是最基础的测试---模板测试和深度测试的实现过程。

模板测试(Stencil Test),与其对应的是模板缓冲(Stencil Buffer),如果开启的模板测试,则

模板测试一般用来限制渲染区域。

如果一个片元通过了模板测试,那么它会继续进行深度测试(Depth Test)

注意,只有通过了深度测试,并且开启了深度写入才可以修改深度缓冲区。深度测试与透明效果密切相关。

当一个片元通过了两个测试后,它就可以进行合并。

渲染的过程不是一口气完成的,而是物体一个接着一个画到屏幕上的,每个像素的颜色信息保存在了颜色缓冲中,当我们进行这次渲染时,颜色缓冲中往往有上一次渲染的颜色结果。我们是直接覆盖掉上次的结果,还是要利用它(比如实现透明效果),是合并要解决的问题。

如果关闭混合模式,片元的颜色就会直接覆盖掉颜色缓冲的颜色,也就得不到透明效果。

开启混合模式,两种颜色值会进行混合操作,这里的混合操作也有好几种方式。

Early-Z技术

首先,两种测试的测试顺序不是唯一的,并且虽然从逻辑上来说这些测试应该在片元着色器之前进行,但是想象一下,如果片元着色器计算了片元的颜色,但这个片元并没有通过测试被舍弃,那么之前的计算就全部浪费掉了。那么可以让GPU尽可能早知道哪些片元是会被舍弃的,不用计算它们的颜色。将深度测试提前执行(在片元着色器前)技术通常也被称为Early-Z技术。

有时这种操作会与后续的一些操作产生冲突。两个片元A、B,A在B之前,B经过了early-Z被剔除,在像素处理阶段,A渲染为一个透明物体,但因为B已经被剔除,所以渲染结果是错误的。

现代的GPU会判断片元着色器中的操作是否和提前测试发生冲突,如果有冲突则中断提前测试,但会造成性能上的下降。这也是透明度测试会导致性能下降的原因。

双重缓冲(Double Buffering)

渲染一张图像的整个过程是有一定时间的,为了避免让用户看到正在进行光栅化的图元,GPU会使用双重缓冲的策略。对场景的渲染是在幕后进行的,即在后置缓冲(Back Buffer)中,一旦场景已经被渲染到了后置缓冲中,GPU就会交换后置缓冲区与前置缓冲区(Front Buffer)的内容,前置缓冲区的数据会显示到屏幕中。这样就保证用户看到的图像是连续的。

到这里渲染管线的整个过程就简单过了一遍,实际上的渲染管线的实现过程远比上面讲的要复杂的多。

Draw Call

Draw Call本身的含义很简单,就是CPU调用图像编程接口如OpenGL的glDrawElements,以命令GPU进行渲染的操作

如果没有流水线化,那么CPU需要等GPU完成上一个渲染命令后,才能发送新的渲染命令。为了提高效率,我们可以使用命令缓冲区(Command Buffer)。

命令缓冲区包含了一个命令队列,CPU向其添加命令而GPU从中读取命令,两个过程是独立的。这样CPU不需要等待GPU完成渲染命令就可以发送新的渲染命令。

除了Draw Call外还有其他命令种类,如改变渲染状态等。

如果创建10000个小文件,每个文件只有1kb,将它们从一个文件夹复制到另一个文件夹,会发现这个过程要花费很长时间,但单独的一个10MB文件的复制过程就很快。这是因为,10000个小文件包含了10000次复制,每次复制都包含了很多额外操作,这些操作将造成很多额外的性能开销。

到图形渲染中,CPU调用Draw Call之前,都要发送数据、渲染状态和命令等等,这一阶段CPU需要进行很多额外操作,比如检查渲染状态等。当CPU完成了这些准备工作,调用Draw Call时,GPU就可以进行渲染,GPU的渲染能力很强,渲染200个三角网格和渲染2000个渲染网格基本没区别,因此渲染速度要远远快于CPU提交命令的速度,如果Draw Call的数量太多,CPU要进行大量额外操作导致CPU的过载。

减少Draw Call的方法有很多,这里介绍批处理(Batching)方法。

减少Draw Call,一个最直观的方法就是将多个Draw Call合并为一个,比如将要渲染的多个网格合并为一个大网格。但这个合并网格的过程也是需要CPU资源的,因此批处理技术更适合那些静态的物体,如大地、石头等,这些物体只需要合并一次;对于动态物体也可以合并,但因为它们的不断运动,每帧需要重新合并网格后再发给GPU,这对空间和时间都会造成一定影响。

为了减少Draw Call开销:

  1. 尽量避免使用大量很小的网格,当不可避免要使用它们时,看看是否能合并它们。
  2. 避免使用过多的材质。尽量在不同网格之间共用一个材质。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 渲染管线(理论)
    • 应用阶段
      • 几何阶段
        • 光栅化阶段
          • 像素处理阶段
          • 渲染管线(GPU)
            • CPU与GPU之间的通信
              • 将数据加载到显存中
              • 设置渲染状态
              • 调用Draw Call
            • GPU管线
              • 几何阶段
                • 顶点着色器
                • 裁剪
                • 屏幕映射
              • 光栅化阶段
                • 三角形设置
                • 三角形遍历
              • 像素处理阶段
                • 片元着色器
                • 合并
            • Draw Call
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档