关于Web3D你可能需要知道的图形流水线渲染过程

735 阅读9分钟

前言

秋招结束后总结性地写了一篇 用 Three.js 写了一个涵盖了大部分基础3D功能的综合场景 - 掘金 (juejin.cn),我以为很小众的Web3D方向居然有很多人感兴趣。

接下来我会继续根据当时的一些笔记写关于Web3D的总结性文章,今天这篇文章主要是关于图形流水线或者说渲染过程,了解这些过程对渲染性能优化可能会有帮助。

作者水平有限,如有错误欢迎评论区指正


CPU和GPU的特性

为什么写图形流水线时要先扯CPU和GPU呢?

因为在渲染流程中,CPU与GPU像一对搭档一样通力合作渲染图像,缺一不可。在运算过程中,CPU负责计算和扔计算好的数据给GPU,GPU调动一个个如工人一般的计算单元对这些数据进行处理,最后组装需要显示的图像

GPU相对与CPU来说,最主要的两个特点就是流水线渲染和并行计算。

无论是CPU送给GPU的顶点数据,还是GPU光栅生成器产生的像素数据都是互不相关的,可以并行地独立处理(一帧画面是由数以万计的像素组成的)。而且顶点数据(xyzw),像素数据(RGBA)一般都用四元数表示,适合于并行计算。同时,由于每个像素的处理互不相关,其处理过程可以使用 流水线处理 的方式来提高处理效率,也就是说每个阶段都会把前一个阶段的输出作为该阶段的输入。

下面要讲的过程就是 图形流水线 或者说 渲染管线(Render Pipeline),下图就是渲染一个彩色三角形的过程示意图,希望借助这幅图来先给读者建立起一个感性的认知

12.jpg


1:应用阶段(Application Stage)

这一部分可不视为 “流水线” 的一部分,但也是交予GPU处理前的准备阶段

这是一个由CPU主要负责的阶段,且完全由开发人员掌控。在这个阶段,CPU将决定递给GPU什么样的数据(譬如渲染目标场景中的灯光、场景的模型、摄像机的位置),有时候还会对这些数据进行处理(譬如只递给GPU可以被摄像机看见的元素,其他不可见的元素被剔除(culling) 出去),并且告诉GPU这些数据的渲染状态(譬如纹理、材质、着色器等)。在应用阶段主要用的是高级程序语言而不是着色器语言进行处理。

2:顶点着色器(Vertex Shader):确定形状的点(位置和颜色)

这部分后开始进入几何阶段(Geometry Stage),这一个由GPU主导的阶段。也就是说,从这个阶段开始,我们进入了上文所说的“流水线”。

  • 顶点着色器的输入: 该阶段输入的是顶点数据(Vertex Data),顶点数据是一系列顶点的集合。其中包括 顶点位置数据、顶点颜色、法向量数据、光方向、光源位置、光颜色等数据。

  • 顶点着色器的作用:

  1. 执行顶点着色器程序对顶点进行变换计算,比如顶点位置坐标执行进行旋转、平移等矩阵变换,变换后新的顶点坐标作为顶点着色器的输出和下一步的输入。

    顺便多说一句,许多步骤可以看做对象在不同坐标系下表示之间的变换。比如,在虚拟照相机成像模式中,首先把对象从其被定义的坐标系下的表示,把图形传输到输出设备时也会涉及变换。坐标系的多次变换就是矩阵的相乘,最后还要对几何数据进行一个投影变换,所以很适合用流水线体系结构。

  2. 除了第一点所说的几何变换矩阵改变的是顶点位置,同样的,光线数据改变的是顶点的颜色数据,对顶点颜色的指派可以简单到由程序指定一种颜色,也可以复杂到利用真实的光照模型进行计算。

    开发者可以在这个阶段计算每个顶点的光照信息,计算光照、阴影等,当然除了计算光照,其他与顶点颜色相关的操作都可以在这个阶段里进行。值得一提的是,这里仅仅是“信息处理”,还不是真正的着色,可以理解为“为接下来的着色计算提供一些信息”。

  • 顶点着色器的输出:

    该阶段输出的是经过修改或者是经过变换顶点数据(Vertex Data),顶点数据是一系列顶点的集合。其中包括 顶点位置数据、顶点颜色、法向量数据、光方向、光源位置、光颜色等数据。

3. 图元装配(Shape Assembly):连接确定形状的点、线、三角形

  • 图元装配的输入:

    该阶段将顶点着色器输出的所有顶点作为输入,并将所有的点装配成指定图元的形状。

    图元(Primitive) 用于表示如何渲染顶点数据,如:点、线、三角形,比如线条图元就是两个顶点确定的一条线段,系统默认宽度1个像素, 三角面图元是一个三角形边界线里面所有区域,不是一个三角形的边线,注意!图元其实不是显示单位,也就是说图元不是所谓的像素点。

  • 图元装配的作用:

  1. 该阶段把图元形式的一系列定点的集合作为输入,通过生产新的顶点,构造出全新的(或者其他的)图元,来生成几何形状。

  2. 如果是WebGL,从程序的角度来看,就是绘制函数drawArrays()或drawElements()的第一个参数绘制模式mode控制顶点如何装配为图元。比如gl.LINES的定义的是把两个顶点装配成一个线条图元,gl.TRIANGLES定义的是三个顶点装配为一个三角面图元,gl.POINTS定义的是一个点域图元(sprites)。

  • 图元装配的输出:

    装配好的图元

4. 光栅化(Rasterization):将几何图形转化为一个个实际的屏幕像素

  • 光栅化的输入:

    图元装配阶段的图元

  • 光栅化的作用:

  1. 该阶段会把图元映射为最终屏幕上相应的像素,生成片元。片元(Fragment) 是渲染一个像素所需要的所有数据(包括插值计算出的颜色值)。

    但需要特别提醒的是,此时片元是没有颜色的,虽然光栅化就是把图元转化为片元,canvas画布上图像的每一个像素都对应一个片元,你可以把片元简单的理解为像素,但是事实上两者并不是一个概念,顶点光栅化得到的原始片元还没有赋予颜色,可以在后面一步的片元着色器中给片元自定义颜色

  2. 顶点的坐标的定义是以世界坐标系作为参照,片元的坐标是以canvas画布窗口坐标系统为参照。也就是说,在这步以后,片元实际上是从“3D”形态的数据转化为了可以在显示屏上显示的“2D”数据(不管是原始的是2D还是3D,屏幕永远都是2D的)

  • 光栅化的输出:

    一个个片元,也可以理解为未赋予颜色的一个个像素

    image.png

5. 片元着色器(Fragment Shader):对有用的屏幕像素点进行着色(插值着色)

  • 片元着色器的输入:

    光栅化输出的片元。读数据的时候,片元着色器和顶点着色器一样是GPU渲染管线上一个可以执行着色器程序的功能单元,顶点着色器处理的是逐顶点处理顶点数据,片元着色器是逐片元处理片元数据。

  • 片元着色器的作用:

  1. 该阶段首先会对输入的片元(Fragment)进行裁切(Clipping)。裁切会丢弃超出视图以外的所有像素,用来提升执行效率。WebGL通过关键字discard还可以实现哪些片元可以被丢弃,被丢弃的片元不会出现在帧缓冲区,自然不会显示在canvas画布上。

  2. 对片元(Fragment)进行着色。

    WebGL通过给内置变量gl_FragColor赋值可以给每一个片元进行着色, 值可以是一个确定的RGBA值,可以是一个和片元位置相关的值,也可以是插值后的顶点颜色。这个阶段是完全可编程的;在收到GPU为这个阶段输入了大量的数据后,程序员可以决定这些片元该着上什么样的颜色,也可以使用纹理贴图进行着色。下图借某篇博文的图片举个例子

    image.png

  • 片元着色器的输出:

    经过裁切着色后的片元,也就是经过筛选后有颜色的像素

6. 测试与混合(Tests and Blending):检查图层深度和透明度,并进行图层混合

如果有用过PS的人,应该比较好理解这个阶段

  • 如果生成的是三维数据,(x,y)坐标相同片元的深度值Z,默认的情况下深度值是gl_FragCoord.z,深度值大表示片元在另一个片元的后面, 深度测试单元会自动抛弃Z值较大的片元,把Z值较小的片元的深度值存入深度缓冲区。也就是说有一些片元可能是不可见的,因为其定义的表面定义在其他表面的后面

  • 该阶段还会检查 alpha 值( alpha 值定义了一个像素的透明度),从而对图层进行混合。

  • 像素值RGBA存入颜色缓冲区,同一个(x,y)位置, 复杂场景往往会有多个片元叠加,会多次逐片元比较,不停地更新替换帧缓冲区中已有的深度值和颜色值。

  • 如果不关闭深度缓冲区或者深度检测单元,融合计算就无法进行。场景中如果同时存在不透明与透明的物体,一般采取的原则是, 先开启深度测试单元,绘制不透明的几何体,然后设置深度缓冲区为只读模式,然后再绘制透明的几何体。这样的话,因为深度缓冲区可以读取,位于不透明片元后面的透明片元后被抛弃,由于深度缓冲区已经不可写入, 每一个(x,y)坐标对应的的深度值不会被更小的替换,保证可以进行融合计算。