OpenGL自制3D游戏引擎入门V

Shader I

一、概述

  1. 着色器(Shader)是运行在GPU上的程序,为图形渲染管线的某个特定部分而运行,从基本意义上来说,着色器只是一种把输入转换为输出的程序,其运行非常独立,因为它们之间不能相互通信,只能通过输入输出进行交流
  2. 着色器由GLSL这种类C语言编写而成,其是为图形计算量身定制,对于矩阵和向量有非常多的特性

二、GLSL

  1. 着色器的开头总是声明版本号,接着是输入输出变量,uniform以及main函数,每个着色器的入口点都是main函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中。下面是一个典型的着色器结构:

     #version version_number
     in type in_variable_name;
     in type in_variable_name;
    
     out type out_variable_name;
    
     uniform type uniform_name;
    
     int main()
     {
     // 处理输入并进行一些图形操作
     ...
     // 输出处理过的结果到输出变量
     out_variable_name = weird_stuff_we_processed;
     }
    
  2. 当我们特别谈论 Vertex Shader 的时候,每个输入变量叫做顶点属性(Vertex Attribute),我们能够声明的顶点属性是有上限的,一般由硬件决定,OpenGL确保至少16个含4分量的顶点属性可用,有些硬件允许更多的顶点属性,可用通过 GL_MAX_VERTEX_ATTRIBS 来获取具体上限

     int nrAttributes;
     glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
     std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
    

三、数据类型

  1. 基础数据类型:int,float,double,uint,bool 两种容器类型Vertex,Matrix

  2. 向量是一个可以包含多个分量的容器,有以下一些类型

    • vecn 包含n个float分量的向量
    • bvecn 包含n个bool分量的向量
    • ivecn uvecn dvecn以此类推
  3. 向量的分量可以通过 vec.x 这种方式取得,$.x\ .y\ .z\ .w$分别取得其中第一、二、三、四个分量
    (GLSL允许对颜色使用rgba,对纹理使用stpq来访问对应的分量)

  4. 向量允许灵活的分量选择方式,这种方式叫做重组(Swizzling)(Tip:但不允许vec2.z这样的分量获取方式)

     vec2 someVec;
     vec4 differentVec = someVec.xyxx;
     vec3 anotherVec = differentVec.zyw;
     vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
    
  5. 当然也可以把向量作为参数传递给不同的向量构造函数

     vec2 vect = vec2(0.5, 0.7);
     vec4 result = vec4(vect, 0.0, 0.0);
     vec4 otherResult = vec4(result.xyz, 1.0);
    
  6. 虽然着色器是相对独立的小程序,但又作为一个整体的一部分,于是我们要求每个着色器都有输入输出,这样才能够进行数据交流和传递,GLSL定义了 in , out 关键字设定输入输出,但在顶点和片段着色器中会有不同表现

顶点着色器

    #version 330 core
    layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0

    out vec4 vertexColor; // 为片段着色器指定一个颜色输出

    void main()
    {
        gl_Position = vec4(aPos, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数
        vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色
    }

片段着色器

    #version 330 core
    out vec4 FragColor;

    in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)

    void main()
    {
        FragColor = vertexColor;
    }
  1. 可以看到顶点着色器的输入比较特殊,它从顶点数据中直接接受输入,为了定义顶点数据该如何管理,我们使用 location 这一元数据指定输入变量,这样我们就可以在CPU上配置顶点属性,顶点着色器需要为它提供一个额外的 layout标识,这样我们才能把它链接到顶点数据
  2. 另外一个例子是片段着色器,它需要一个vec4颜色输出变量,因为片段着色器需要生成一个最终的颜色,如果不在片段着色器中定义输出颜色,OpenGL会把物体渲染为黑色或白色
  3. 如果我们打算从一个着色器发送数据,我们必须在发送着色器中声明一个输出,在接收方着色器中声明一个类似的输入,当类型和名字都一样的时候,OpenGL会将两个变量链接到一起,它们之间就能发送数据(见上例 vertexColor 变量)

四、Uniform

  1. 除了VAOs能够从CPU向GPU输入信息,uniform也是一种从CPU中的应用向GPU中的着色器发送数据的方式

  2. uniform和顶点属性有些不同,uniform是全局的,全局则意味着uniform变量在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问,同时uniform在被设置数值后会一直保存它们的数据直至被重置或更新

  3. 我们可以在一个着色器中添加uniform关键字至变量名前来声明一个GLSL的uniform

     #version 330 core
     out vec4 FragColor;
    
     uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量
    
     void main()
     {
         FragColor = ourColor;
     }
    

Tip:如果声明了一个uniform变量但在GLSL中没有用过,编译器会静默移除这个变量,导致编译出的版本不包含它,可能会引发诸多麻烦的错误

  1. 此时uniform变量还为空,我们需要在着色器中得到uniform的索引然后更新它的值

     float timeValue = glfwGetTime();
     float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
     int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
     glUseProgram(shaderProgram);
     glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
    
  • 使用 glGetUniformLocation 函数查询 uniform ourColor 的位置,只要提供需要查询的着色器程序和uniform名字即可(返回值为-1时代表没有找到这个位置值)
  • 使用 glUniform4f 函数设置 uniform 值(Tip:查询uniform地址不要求之前使用过着色器程序,但更新一个uniform前必须调用 glUseProgram 函数使用程序)
  • OpenGL核心是一个C库,不支持重载,因此函数参数不同时需要为其定义新的函数,glUniform函数就是一个经典的例子,这个函数有特定的后缀:
    • f 函数需要一个float作为它的值
    • ui 函数需要一个unsigned int作为它的值
    • 4f 函数需要四个float作为它的值
    • fv 函数需要一个向量/数组作为它的值
    • 以此类推
  1. 在循环中实现颜色改变

     while(!glfwWindowShouldClose(window))
     {
     // 输入
     processInput(window);
    
     // 渲染
     // 清除颜色缓冲
     glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
     glClear(GL_COLOR_BUFFER_BIT);
    
     // 记得激活着色器
     glUseProgram(shaderProgram);
    
     // 更新uniform颜色
     float timeValue = glfwGetTime();
     float greenValue = sin(timeValue) / 2.0f + 0.5f;
     int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
     glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
    
     // 绘制三角形
     glBindVertexArray(VAO);
     glDrawArrays(GL_TRIANGLES, 0, 3);
    
     // 交换缓冲并查询IO事件
     glfwSwapBuffers(window);
     glfwPollEvents();
     }
    

五、再来一个栗子

  1. 制作一个三个角颜色不同且渐变的三角形

     float vertices[] = {
     // 位置              // 颜色
     0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,   // 右下
     -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,   // 左下
     0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f    // 顶部
     };
    
  2. 可以看到现在既要发送顶点位置,又要发送颜色信息,所以需要调整顶点着色器

     #version 330 core
     layout (location = 0) in vec3 aPos;   // 位置变量的属性位置值为 0 
     layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1
    
     out vec3 ourColor; // 向片段着色器输出一个颜色
    
     void main()
     {
     gl_Position = vec4(aPos, 1.0);
     ourColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色
     }
    
  3. 由于不再使用uniform传递片段颜色,现在使用 ourColor 输出变量,所以需要调整片段着色器

     #version 330 core
     out vec4 FragColor;  
     in vec3 ourColor;
    
     void main()
     {
     FragColor = vec4(ourColor, 1.0);
     }
    
  4. 由于新增了颜色信息,使用更新glVertexAttribPointer函数更新顶点格式

     // 顶点属性
     glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
     glEnableVertexAttribArray(0);
     // 颜色属性
     glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
     glEnableVertexAttribArray(1);
    
(下接 OpenGL入门VI)

白梅 Pid:63740442


OpenGL自制3D游戏引擎入门V
https://baifabaiquan.cn/2021/03/23/OpenGL入门V/
作者
白发败犬
发布于
2021年3月23日
许可协议