OpenGL(入门笔记)--图形渲染管线(Graphics Pipeline)

1,201 阅读8分钟

在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线管理的。

学习目标:

  • 图形渲染管线基础知识;
  • 图形渲染管线的工作流程;

图形渲染管线基础知识

在OpenGL中,任何事物都在3D空间中,而屏幕和窗口等都是2D像素组,所以需要进行转换。

  • 概念:

    • 一堆原始图形数据途经一个输送管道,期间经过各种变化处理 最终出现在屏幕的过程.
    • (图形渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。)
  • 图形渲染管线主要有两方面工作:

    • 把3D坐标转换为2D坐标;
    • 把2D坐标转变为实际的有颜色的像素;

图形渲染管线的工作流程

image.png

  1. 顶点着色器顶点数据(Vertex Data)数组(3D坐标)作为图形渲染管线的输入数据,将3D坐标转换为另一种3D坐标。
  2. 图元装配(Primitive Assembly): 将顶点着色器的输出作为顶点输入,并将所以的顶点装配成指定的图元形状
  3. 几何着色器:图元装配阶段的输出(顶点集合)作为它的输入,产生新顶点构造出新的图元来生成其他形状
  4. 光栅化阶段:几何着色器的输出作为它的输入参数,把图元映射为最终屏幕上相应的像素,生成供片段着色器使用。在此阶段会执行裁切(Clipping)--会丢弃超出视图外的所有像素以此提高执行效率。
  5. 片段着色器计算一个像素的最终颜色。 片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。
  6. Alpha测试和混合(Blending):检测 片段对应的深度和模板(Stencil)值,用来判断这个像素是在其他物体的前面还是后面,决定是否丢弃。检测alpha值(alpha值定义物体的透明度)并对物体进行混合(Blend)。

图形渲染管线非常复杂,它包含很多可配置的部分,我们只需要配置顶点和片段着色器就行。接下来我们学习如何在openGL程序中 编写顶点和片段着色器。

编写着色器程序主要步骤:

1. 顶点输入
2. 顶点着色器 --编译着色器
3. 片段着色器 --编译着色器
4. 将着色器链接到着色器程序对象中
5. 链接顶点属性
  • 顶点输入 顶点数据是一系列顶点的集合。一个顶点(Vertex)是一个3D坐标的数据的集合。而顶点数据是用顶点属性(Vertex Attribute)表示的.
  1. 定义顶点数据 (vertext data)(3D坐标),将顶点数据坐标进行标准化设备坐标(有效范围内 --OpenGL的可见区域)
    • 标准化设备坐标(Normalized Device Coordnates,NDC)
      • 任何坐落在范围外的坐标都会被丢弃/裁剪,不会显示在屏幕上。
      • 标准化设备坐标接着会变换为屏幕空间坐标(Screen -space Coordinates)-- [通过glViewport函数提供的数据,进行视口变换(viewport transform)完成的]
  2. 顶点数据作为输入发送给图形渲染管线的第一个处理阶段:顶点着色器。
  3. 把顶点数据 存储在显卡内存中,使用VBO(顶点缓冲对象)管理。
    • 使用顶点缓冲对象管理内存的原因是一次性可以发送大量数据到显卡上。

    • 顶点缓冲对象(vertex buffer objects,VBO)管理内存-- 在GPU中存储大量的顶点数据。

    • 顶点缓冲对象的创建步骤:
      1. 创建VBO对象;glGenBuffer()

      2. 将VBO对象绑定到顶点缓冲对象中; glBindBuffer()

      3. 将用户输入的顶点数据复制到缓冲内存中; glBufferData()

      • 要点:
        • OpenGL有很多缓冲对象类型,其中顶点缓冲对象类型是GL_ARRAY_BUFFER

        • glBufferData函数--把顶点数据(用户定义的数据) 复制到 缓冲的内存 中。

        • glBufferData:第四个参数,指名显卡如何管理给定的数据:

          1. GL_STATIC_DRAW:数据不会或者几乎不变;
          2. GL_DYNAMIC_DRAW:数据会被改变多次;
          3. GL_STREAM_DRAW:数据每次绘制时都会改变;
          // 创建vbo对象
          unsigned int VBO;//VBO对象id
          glGenBuffer(1,&VBO);
          
          //把新创建的缓冲 绑定到GL_ARRAY_BUFFER目标上
          //GL_ARRAY_BUFFER--顶点缓冲对象的缓冲类型
          glBindBuffer(GL_ARRAY_BUFFER,VBO);
          
          //配置 当前绑定的缓冲(VBO) --调用glBufferData函数 
          glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW)
          
  • 顶点着色器

    • 在现代OpenGL中至少需要设置一个顶点和一个片段着色器。

    • 使用GLSL(OpenGL Shading Language)编写顶点着色器,然后编译这个着色器,最后在程序中使用。


    • 创建顶点着色器

      #version 330 core
      layout (location=0) in vec 3 aPos;
      void main(){
          gl_Position=vec4(aPos.x,aPos.y,aPos.z,1.0)
      }
      
      1. 着色器版本 OpenGL 3.3以及更高版本,使用核心模式。
      2. in关键字 --代表输入顶点属性(Input Vertex Attribute),数据位置location ( layout (location=0) ),使用vec3 三维浮点向量。
      3. 设置顶点着色器的输出;
    • 编译着色器

    const char *vertexShaderSource = "#version 330 core\n" "layout (location = 0) in vec3 aPos;\n" "void main()\n" "{\n" " gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n" "}\0";
    

    为了使用OpenGL使用它,必须在运行时动态编译它的源代码

    • 编译步骤:

      1. 创建着色器对象,使用ID来引用; -- glCreateShader(GL_VERTEX_SHADER);
      2. 把着色器源码 附加到这个着色器对象中; -- glShadderSource();
      3. 编译着色器 -- glCompileShader(vertexShader);
      4. 检测编译状态 -- glGetShaderiv() ```c unsigned int vertexShader; vertexShader=glClreateShader(GL_VERTEX_SHADER);
      glShaderSource(vertexShader,1,&vertexShader,NULL);
      
      glCompileShader(vertexShader);
      
        //检测编译着色器状态 --是否编译成功
        int  success;
        char infoLog[512];
        glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
      
        //如果编译失败 
        if(!success)
        {
            glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
            std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
        }
      ```
      
  • 片段着色器(Fragment Shader)

    • 计算像素最后的颜色输出。

    • 在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)分量,通常缩写为RGBA。当在OpenGL或GLSL中定义一个颜色的时候,我们把颜色每个分量的强度设置在0.0到1.0之间

    • 三种颜色分量的不同调配可以生成超过1600万种不同的颜色!

    • 创建步骤:

      1. 创建片段着色器源码
           #version 330 core out vec4 FragColor; 
           void main() 
           { 
               FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); 
           }
      
      1. 编译片段着色器
         unsigned int fragmentShader;
         fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); 
         glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); 
         glCompileShader(fragmentShader);
      
  • 着色器程序(Shader Program Object)

    • 将多个着色器合并后链接(link)为一个着色器程序对象,然后渲染对象的时候,激活这个着色器程序。
      • 注:在链接 着色器到一个程序时,会把每个着色器的输出 链接 到 下个着色器的输入。 当输入和输出 不匹配的时候, 会链接失败。
    unisigned int shaderProgram;
    shaderProgram=glCreateProgram();
    
    
    glAttrachShader(shaderProgram,vertexShader);
    glAttrachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);
    
    glUserProgram(shaderProgram);
    
    • 已经把输入顶点数据发送给了GPU,并指示了GPU如何在顶点和片段着色器中处理它。

    • OpenGL不知道如何解释内存中的顶点数据,以及它该如何将顶点数据 链接 到 顶点着色器的属性上;

  • 链接顶点属性 (解析顶点数据)

    // 设置顶点属性指针
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0); // 启用顶点属性
    
    • glVertexAttribPointer --参数:
      • 配置的顶点属性 (顶点属性的位置值) --layout(location = 0)
      • 顶点属性的大小 --是一个vec3,它由3个值组成,所以大小是3
      • 指定数据的类型
      • 是否希望数据被标准化(Normalize) --设置为GL_TRUE,所有数据都会被映射到0 -1之间;
      • 步长(Stride) -- 在连续的顶点属性组之间的间隔
      • 位置数据在缓冲中起始位置的偏移量(Offset)
    • 顶点属性默认是禁用的
      • 启用顶点属性 -- glEnableVertexAttribArray
  • 顶点数组对象(Vertex Array Object,VAO) 顶点属性调用都会存储在VAO中,好处就是当配置顶点属性指针时,只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行。

    • VAO的创建
    unsigned int VAO;
    glGenVertexArrays(1,&VAO);
    
    //VAO的使用
    glBindVertexArray(VAO);
    
    
  • 绘制

    • glDrawArrays(GL_TRIANGLES,0,3);
    • 参数:
      1. 指定不同的图元,比如:GL_POINTS;
      2. 指定了顶点数组的起始索引
      3. 绘制点的个数;
  • 索引缓冲对象(Element Buffer Object,EBO)

    • EBO也是一种缓冲,专门存储索引。在OpenGL中调用顶点的索引 就可以决定绘制那些顶点。 -- 为了减少顶点创建的个数(减少重复数据)。
      
      float vertices[] = {
          0.5f, 0.5f, 0.0f, // 右上角 
          0.5f, -0.5f, 0.0f, // 右下角
          -0.5f, -0.5f, 0.0f, // 左下角 
          -0.5f, 0.5f, 0.0f // 左上角 
      }; 
      
      unsigned int indices[] = { // 注意索引从0开始!
          0, 1, 3, // 第一个三角形 
          1, 2, 3 // 第二个三角形
      };进行绘制
      
    • 创建索引缓冲对象 --使用glDrawElements绘制
      unsigned int EBO; 
      glGenBuffers(1, &EBO);
      
      glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); 
      
      //把索引复制到缓冲里
      glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
      glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);