计算机图形学(OPENGL):着色器

本文同时发布在我的个人博客上:https://dragon_boy.gitee.io

  请多多参考原文:https://learnopengl.com/Getting-started/Shaders

着色器

  就像上篇文章所说,着色器是存储在GPU上的小程序,这些程序会在渲染管线的特定阶段使用。同时着色器是相互独立的,只能通过输入和输出来进行交流。着色器由GLSL编写,下面来详细说明一下。

GLSL

  着色器由类似于C的GLSL语言编写,一种专门用来处理图形的语言,包含许多有用的对于向量和矩阵的操作。
  一个典型的着色器通常包含版本声明,输入和输出变量声明,在主函数中处理输入和输出:

#version version_number
in type in_variable_name;
in type in_variable_name;

out type out_varialble_name;

uniform type uniform_name;

void main()
{
  // 处理输入和一些图形处理
  ...
  // 处理输出
  out_variable_name = what_we_processed;
}

  在顶点着色器中,每个输入的变量被称为顶点属性。由于硬件的限制,我们可以声明的顶点属性数量是有限制的。OpenGL保证至少有16个4元属性可以使用,当然,我们可以通过以下代码获取最大顶点属性数量:

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

数据结构

  GLSL拥有许多类C的数据类型:int, float, double, uint, bool,当然,还有两个很重要的数据类型:vector和matrix,向量和矩阵,这里先介绍向量。

向量

  在GLSL中,向量有这么几种类型:vecn,bvecn,ivecn,uvecn,dvecn。从缩写就可以知道类型,这里不赘述。大多数时候只需要使用vecn即可。
  针对vecn,我们可以通过.x,.y,.z和.w来访问向量的属性,除此之外,对于颜色和贴图信息,可以分别使用rgba和stpq来访问对应的属性。其次,向量类型允许我们混用,大致如下:

vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 another Vec = differentVec.zyw;
vec4 otherVec = someVec.xxx + another.yxzy;

  当然,是不允许赋予某一个向量不存在的属性的,例如二维向量不接受.z属性。同时,向量也可以作为属性来组装为新的向量:

vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);

输入和输出

  GLSL通过in和out关键字来定义输入和输出。顶点着色器必须要有输入,同时需要layout (location = x)来定义输入数据的位置;片元着色器需要有一个类型为vec4的颜色输出变量。
  一个典型的顶点着色器如下:

#version 330 core
layout (location = 0) in vec3 aPos;

out vec4 vertexColor;

void main()
{
  gl_position = vec4(aPos, 1.0);
  vertexColor = vec4(0.5, 0.0, 0.0, 1.0);
}

  一个典型的片元着色器如下:

#version 330 core
out vec4 FragColor;

in vec4 vertexColor;

void main()
{
  FragColor = vertexColor;
}

  如果我们将上述的着色器代码替换到上篇文章中,结果应该是这样:


Uniform

  uniform是另一种不同于in与out的关键字。uniform定义的是全局变量,例如在片元着色器中,我们这样使用uniform:

# version 330 core
out vec4 FragColor;

uniform vec4 ourColor;

void main()
{
  FragColor = ourColor;
}

  定义之后,这个uniform变量还是空的,为了赋值,我们需要先获取这个变量的位置。下面我们为ourColor赋值,让它随时间变化:

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);

  首先我们通过glfwGetTime获取运行时的时间,然后通过sin将值转换为-1到1的范围,接着/2.0f+0.5f将值变换到0到1(颜色的范围);接着通过glGetUniformLocation获取着色器程序中的uniform变量名的位置;在激活着色器程序后就可以通过glUniform4f设置该uniform变量的值了,我们将greenValue赋给G通道。(注意,glUniform后缀4f代表变量的类型,有4个参数,每个都是float类型,其它的类型依此类推)
  我们需要将上述代码放到渲染循环中,渲染循环如下:

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);
  
    // 交换缓冲及事件响应
    glfwSwapBuffers(window);
    glfwPollEvents();
}

  运行上述代码会得到类似这样的结果(请自行查看视频)
  由上可见,uniform非常适合用来设置每帧都会进行变化的变量。

为顶点添加颜色属性

  我们针对上篇文章的代码,为每个顶点添加一个RGB颜色属性:

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   
};

  由于更新的顶点的属性,那么接着修改一下顶点着色器,片元着色器不需要修改:

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;

out vec3 ourColor;

void main()
{
  gl_Position = vec4(aPos, 1.0);
  ourColor = aColor;
}

  同位置属性一样,我们为颜色属性定义一个位置,同时将颜色赋值给ourColor输出。
  由于增加了颜色属性,VBO中的数据得到了更新,我们需要重新配置一下顶点属性指针。



  如上图,我们可以得到颜色,属性的位置,大小,步长,偏移等属性,配置如下:

// 位置属性
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);

  运行修改后的代码,可以得到如下结果:



  我们明明只设置了三个顶点的颜色,为什么会得到上面的结果呢?就像之前提到过的,在片元着色器之前会有一个光栅化的阶段,之后三角形将有许多许多的像素点组成,每个像素点会根据原来在三角形的位置插值赋予一个颜色,,所以会造成这种混合的效果。

编写Shader类

  为了方便接下来的学习,我们将部分复用代码整合起来编写一个Shader类,以方便之后的程序的编写。这里使用C++的一些自带库进行编写。
  和大多数头文件的编写一样,我们创建Shader.h文件,并加入以下代码:

#ifndef SHADER_H
#define SHADER_H

#include 
  
#include 
#include 
#include 
#include 
  

class Shader
{
public:
     // 程序ID
    unsigned int ID;
  
    // 构造方法,读取着色器代码路径
    Shader(const char* vertexPath, const char* fragmentPath);
    // 定义着色器程序激活方法
    void use();
    // 一些设置uniform变量的方法
    void setBool(const std::string &name, bool value) const;  
    void setInt(const std::string &name, int value) const;   
    void setFloat(const std::string &name, float value) const;
};
  
#endif

  实现构造方法:

Shader(const char* vertexPath, const char* fragmentPath)
{
    // 1、着色器代码路径获取代码
    std::string vertexCode;
    std::string fragmentCode;
    std::ifstream vShaderFile;
    std::ifstream fShaderFile;
    // 确保抛出错误
    vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
    fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
    try 
    {
        // 打开文件
        vShaderFile.open(vertexPath);
        fShaderFile.open(fragmentPath);
        std::stringstream vShaderStream, fShaderStream;
        // 读取文件缓冲至文件流
        vShaderStream << vShaderFile.rdbuf();
        fShaderStream << fShaderFile.rdbuf();       
        // 关闭文件
        vShaderFile.close();
        fShaderFile.close();
        // 保存文件流中的代码
        vertexCode   = vShaderStream.str();
        fragmentCode = fShaderStream.str();     
    }
    catch(std::ifstream::failure e)
    {
        std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
    }
    const char* vShaderCode = vertexCode.c_str();
    const char* fShaderCode = fragmentCode.c_str();
    // 2.编译着色器
  unsigned int vertex, fragment;
  int success;
  char infoLog[512];
   
  // 顶点着色器
  vertex = glCreateShader(GL_VERTEX_SHADER);
  glShaderSource(vertex, 1, &vShaderCode, NULL);
  glCompileShader(vertex);
  // 打印错误信息
  glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
  if(!success)
  {
      glGetShaderInfoLog(vertex, 512, NULL, infoLog);
      std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
  };
  
  // 片元着色器(同顶点着色器)
  [...]
  
  // 着色器程序
  ID = glCreateProgram();
  glAttachShader(ID, vertex);
  glAttachShader(ID, fragment);
  glLinkProgram(ID);
  // 打印错误信息
  glGetProgramiv(ID, GL_LINK_STATUS, &success);
  if(!success)
  {
      glGetProgramInfoLog(ID, 512, NULL, infoLog);
      std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
  }
  // 删除着色器
  glDeleteShader(vertex);
  glDeleteShader(fragment);

  实现use方法:

void use() 
{ 
    glUseProgram(ID);
}  

  实现设置uniform变量方法:

void setBool(const std::string &name, bool value) const
{         
    glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); 
}
void setInt(const std::string &name, int value) const
{ 
    glUniform1i(glGetUniformLocation(ID, name.c_str()), value); 
}
void setFloat(const std::string &name, float value) const
{ 
    glUniform1f(glGetUniformLocation(ID, name.c_str()), value); 
}

  在编写完后,在主cpp文件中,我们便可以这样使用Shader类:

Shader ourShader("shader.vs", "shader.fs");  // 注意,文件的后缀名不需要一致,可以随意自定义
...  //一些设置后
while(...)
{
  ...  //一些设置后
  ourShader.use();
  ourShader.setFloat("someUniform", 1.0f);
  DrawStuff();
}

  这里给出原文的Shader类的代码:Shader.h,以及使用了Shader.h的一个例子的代码:Code。

  最后,请多多关注原文内容:https://learnopengl.com/Getting-started/Shaders

你可能感兴趣的:(计算机图形学(OPENGL):着色器)