【OpenGL】OpenGL渲染流程详解

时间:2021-09-21 18:27:35

查阅过很多资料,在OpenGL的整个渲染流程中很多资料上描述的过程大致相同却又略去了很多细节,可以说是每一份资料的描述都不尽相同,或许每个作者所阐述的重点都不太相同,不存在孰对孰错

  • 例如:《OpenGL ES 3.0编程指南》中对可编程管线的流程介绍如下:

    1. VBO/VAO(顶点缓冲区对象或顶点数组对象)
    2. VertexShader(顶点着色器)
    3. rasterization(光栅化)
    4. FragmentShader(片段着色器)
    5. Per-Fragment Operations(逐片段操作)
      • pixelOwnershipTest(像素归属测试)
      • ScissorTest(剪裁测试)
      • StencilTest and DepthTest(模板和深度测试)
      • Blending(混合)
      • dithering(抖动)
    6. Frame Buffer (帧缓冲区)
  • 又例如:《实时计算机图形学》一书中,将图形绘制管线分为三个主要阶段:应用程序阶段、几何阶段、光栅阶段

    1. 应用程序阶段:
      • 主要和CPU、内存进行交互,在此阶段有碰撞检测、场景图建立、空间八叉树更新、视锥剪裁等算法都在此阶段执行
      • 几何体数据(顶点坐标、法向量、纹理坐标、纹理等)这些信息会通过数据总线传送到图形硬件数据总线规范了一个大的集成应用系统中同构系统、异构系统等方面进行数据共享和交换实现方法。 系统间数据交换标准
    2. 几何阶段:
      • 顶点坐标变换、光照、剪裁、投影、屏幕映射 均在GPU进行运算(顶点着色器就在此阶段执行
      • 该阶段的末端会得到经过变换和投影之后的顶点坐标、颜色、纹理坐标,这些数据将作为光栅化阶段的输入进一步处理
    3. 光栅阶段:
      • 光栅化:决定哪些像素被集合图元覆盖的过程(其实就是将图元拆分成多个片段的过程,图元是由几个点组成的点、线、面)
        • 例如:面形状的图元最少3个点(A、B、C三个点)即可组成,那么屏幕是由众多个像素点组成的,那ABC三个点围成的三角形中有多少个像素点呢?那么其实光栅化就是用来分解图元得到这些像素点的过程,当然这其中还有一个重要的过程就是插值,插值出那个图形区域的像素(纹理坐标v_texCoord、颜色等信息)。注意,此时的像素并不是屏幕上的像素,是不带有颜色的。接下来的片段着色器完成上色的工作。
        • 注意:光栅化不是着色的阶段!片段着色器才是给每个像素绘制颜色的阶段,我在很多博客上都看到过错误的讲解,都有提到光栅化是上色的过程
      • 着色器上色

以上,都是关于流程的简要过程,下面会详细的描述整个渲染流程


渲染管线详解

下面这个图取自《OpenGL ES 3.0编程指南》,此流程为可编程管线。
【OpenGL】OpenGL渲染流程详解

  1. VBO/VAO(顶点缓冲区对象或顶点数组对象):
    VBO/VAO(到底是啥,下回讲解)是cpu提供给GPU的顶点信息,包括了顶点的位置、颜色(只是顶点的颜色,和纹理的颜色无关)、纹理坐标(用于纹理贴图)等顶点信息。

  2. VertexShader(顶点着色器):
    顶点着色器是处理VBO/VAO提供的顶点信息的程序。VBO/VAO提供的每个顶点都执行一遍顶点着色器。Uniforms(一种变量类型)在每个顶点保持一致,Attribute每个顶点都不同(可以理解为输入顶点属性)。执行一次VertexShader输出一个Varying(可变变量)和gl_positon。

    • 顶点着色器的输入包括:

      • 着色器程序:描述顶点上执行操作的顶点着色器程序源代码或者可执行文件
      • 顶点着色器输入(或者属性):用顶点数组提供的每个顶点的数据
      • 统一变量(uniform):顶点/片段 着色器使用的不变数据
      • 采样器(Samplers):代表顶点着色器使用纹理的特殊统一变量类型

      【OpenGL】OpenGL渲染流程详解

    • 重点:VertexShader就是顶点着色器编程可以操作的阶段,用于控制顶点坐标的转换过程,片段着色器控制着每个像素颜色的计算过程

    • 该阶段在很多书中被描述为几何阶段:原因是,例如3D模型输入到计算机中是一系列的三维顶点,但最终我们都需要将其转换为能在2D屏幕上可见的二维坐标,所以就需要一个三维坐标转换二维坐标的转换过程,在一些面试中会问到有具体哪些转换步骤,步骤如下:

      先说说坐标类型:
      Object Space(模型坐标空间)
      World Space(世界坐标空间)
      Eye Space(观察坐标空间)
      Clip and Project Space(屏幕坐标空间)

    • 顶点坐标空间变换流程:
      1. Object Space到World Space
        • 模型坐标空间有2层核心含义:
          • 其一:模型文件中的顶点值,这些数据是在建模的时候得到的,例如说用3D MAX建模并导出一个.max文件,文件中包含的数据就是object space coordinate
          • 其二: object space coordinate与任何物体都没有参照关系,这个概念很重要,相当于是一个相对于坐标原点的具体位置,但是导入计算机之后这个模型还应该被赋予一个世界坐标(例如说一个人在中国-北京-*-国旗下)是一个具体的位置,而不是本地坐标(比如说在二楼的最左边第一个房间,这是参照当前这栋楼而言的一个位置),那么从object space coordinate转换到world space coordinate这个变换过程由一个四阶矩阵控制,通常叫做world matrix
        • 光照计算:通常都是在world space coordinate中进行,符合人类常识
        • 顶点法向量:在模型文件中属于object space,然而在GPU的顶点着色器程序中必须将法向量转换到world space中才能使用,需要注意的是这个转换不能使用world matrix这个转置矩阵!!!而是需要使用world matrix的逆矩阵,这个错误非常的难以发现
      2. World Space到Eye Space
        • 这个转换不难理解,同人类一样,对每个人而言,都是从自己的双眼去观察这个世界,我们的眼镜就相当于是eye space,所以计算机也一样,每次的渲染都是从某一个视角去渲染物体,游戏中通常摄像机就是eye space
        • eye space概念:视点(camera、相机、眼睛)为原点,由视线的方向、视角范围、远*面,组成一个梯形的三维空间,我们称之为 viewing frustum(视锥)
        • 【OpenGL】OpenGL渲染流程详解
        • 超出这个梯形范围的物体将会被剔除,也就是说我们看不见,这项技术被称之为Furstum Culling(视锥剪裁)
      3. Eye Space到Clip and Project Space
        • 在视锥体中(那个不规则的梯形)进行剪裁并非易事,所以图形学前辈们精心分析之后,剪裁被安排到一个单位立方体中进行,该立方体的对角顶点分别是(-1,-1,-1)和(1,1,1),称之为规范立方体(简称CVV:Canonical view volume),CVV的*面(梯形较小的矩形面)对应的屏幕像素坐标是(左下角的0,0)
        • 1.把视锥体变换到CVV中,这个过程就是“投影”,投影的方法有2种:正投影(平行投影)和透视投影,一般在游戏里都有这两种类型的摄像机,正投影就好像一个无限延伸的长方体,透视投影就好像是一个梯形
        • 2.确定只有图元完全或者一部分在视锥体内部的时候,才需要进行光栅化
          • 如果图元完全在视锥体内部:该图元直接进入下一个阶段
          • 如果图元部分在视锥体内部:进行剪裁处理,详细的剪裁算法大家自行查阅一下《计算机图形学》第12章第5节
          • 如果图元完全不在视锥体内部:直接剔除

    3.Primitive Assembly(图元装配):
    顶点着色器下一个阶段是图元装配,图元(prmitive)是三角形、直线或者点精灵等几何对象。这个阶段,把顶点着色器输出的顶点组合成图元。

    • 将顶点数据根据Primitive(原始链接关系)还原出网格结构,网格由顶点和索引组成,在此阶段根据索引将顶点链接在一起,组成点、线、面三种不同的图元,之后就是对超出屏幕的三角形进行剪裁,如果三角形ABC三个顶点其中一个点在屏幕的外面,另外两个点在屏幕里面,其实屏幕上看到的应该是个四边形,然后再将这个四边形切成2个小的三角形
    • 简而言之,将顶点着色器计算之后得到的点根据链接关系组成点、线、面(三角形)

4.rasterization(光栅化):

  • 光栅化是将图元转化为一组二维片段的过程,然后,这些片段由片段着色器处理(片段着色器的输入)。这些二维片段代表着可在屏幕上绘制的像素。用于从分配给每个图元顶点的顶点着色器输出生成每个片段值的机制称作插值(Interpolation)。这句不是人话的话解释了一个问题,就是从cpu提供的分散的顶点信息是如何变成屏幕上密集的像素的,图元装配后顶点可以理解成变为图形,光栅化时可以根据图形的形状,插值出那个图形区域的像素(纹理坐标v_texCoord、颜色等信息)。注意,此时的像素并不是屏幕上的像素,是不带有颜色的。接下来的片段着色器完成上色的工作。

    问题1:如果该图元是点线面中的“点”,那么它的坐标为浮点数,但是像素是由整数来表示的,如何正确计算屏幕坐标所对应的像素?
    答: 根据《计算机图形学(第二版)》52页示例:如果一条线段的位置是(10.48,20.51),那么转换到像素的位置则是(10,21)也就是我们说的四舍五入

    问题2:为什么通过三个点就能自动连接成一个三角形,如果有ABCDEFG….很多个点,如何知道ABC三个点需要组成三角形,而不是用AFG组成三角形?
    答:这个问题涉及到划线算法和区域图元填充算法
    划线算法有:DDA算法、Bresenham划线算法
    区域图元填充算法有:扫描线多边形填充算法、边界填充算法

    更多详细的算法内容可以查阅《计算机图形学》第三章

5.FragmentShader(片段着色器):
片段着色器为片段(像素)上的操作实现了通用的可编程方法,光栅化输出的每个片段都执行一遍片段着色器,对光栅化阶段生成每个片段执行这个着色器,生成一个或多个(多重渲染)颜色值作为输出。
【OpenGL】OpenGL渲染流程详解

6.Per-Fragment Operations(逐片段操作)

  • 在该阶段,每个片段都会执行下面的5个操作

【OpenGL】OpenGL渲染流程详解

附上一张中文翻译图,嘿嘿~

【OpenGL】OpenGL渲染流程详解
(1)pixelOwnershipTest(像素归属测试):

  • 这个用来确定帧缓冲区中位置(x,y)的像素是不是归当前上下文所有。例如,如果一个显示帧缓冲区窗口被另一个窗口所遮蔽,则窗口系统可以确定被遮蔽的像素不属于此opengl的上下文,从而不显示这些像素。

(2)ScissorTest(剪裁测试):

  • 如果该片段位于剪裁区域外,则被抛弃

(3)StencilTest and DepthTest(模板和深度测试):

  • 深度测试比较好理解,若片段着色器返回的深度小于缓冲区中的深度,则舍弃。模板测试没有用过,不清楚具体功能,猜测功能应该和名字一样,模板形状内可通过。

(4)Blending(混合):

  • 将新生成的片段颜色值与保存在帧缓冲区的颜色值组合起来,产生新的RGBA。

(5)dithering(抖动):

  • 在逐片段操作阶段的最后,片段要么被拒绝,要么在帧缓冲区(x,y)的某个位置写入片段的颜色,深度或者模板值。写入片段颜色,深度和模板值取决于弃用的相应写入掩码。写入掩码可以更精确的控制写入相关缓冲区的颜色、深度和模板值。例如:可以设置颜色缓冲区的写入掩码,使得任何红色值都不能被写入颜色缓冲区。

最后把产生的片段放到帧缓冲区(前缓冲区或后缓冲区或FBO)中,若不是FBO,则屏幕绘制缓冲区中的片段,产生屏幕上的像素。