OpenGL: Shaders

I. Overview

In the previous article "Drawing a Triangle", we covered vertex shaders and fragment shaders, and also got a preliminary understanding of how to write and use a shader

Shaders are the soul of OpenGL

Following in the footsteps of official learning documents, write a special article to understand shaders in detail and comprehensively

In a basic sense, a shader is just a program that converts input into output. It is also a very independent program that runs on the GPU for a specific part of the graphics rendering pipeline. They cannot communicate with each other.

The only communication between them is through their respective inputs and outputs

Two. GLSL

Shaders are written in a C-like language called GLSL and include some useful features for vector and matrix operations

Official reference documents:

    https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.1.20.pdf

    docs.gl

A shader program will include the following points:

  • statement version
  • Input variables, output variables, Uniform global variables
  • main() function

The main() function processes all input variables and outputs the results to output variables

code snippet: 

#version version_number           //声明版本

in type in_variable_name;         //输入变量1
in type in_variable_name;         //输入变量2

out type out_variable_name;       //输出变量

uniform type uniform_name;        //全局变量

int main()                        //main()函数
{
    // 处理输入并进行一些图形操作
    ...
    // 输出处理过的结果到输出变量
    out_variable_name = weird_stuff_we_processed; 
}

For vertex shaders, each input variable is also called a vertex attribute

There is an upper limit to the vertex attributes that can be declared in OpenGL, which is generally determined by the hardware

OpenGL ensures that at least 16 4-component vertex attributes are available, but some hardware may allow more

The specific upper limit can be obtained by querying GL_MAX_VERTEX_ATTRIBS :

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

 Normally, 16 will be returned, which is enough in most cases

3. Data type 

3.1 GLSL data types

Basic data types: int, float, double, uint, bool

Two container types: Vector (Vector), Matrix (Matrix)

3.2 Vectors

vecn: the default vector containing n float components

bvecn: a vector containing n bool components

ivecn: a vector containing n int components

uvecn: a vector containing n unsigned int components

dvecn: a vector containing n double components

Most of the time using vecn is enough

Operations on vectors in GLSL:

Get components: vec.x, vec.y, vec.z, vec.w

Reorganization operation:

        vec2 someVec;

        vec4 differentVec = someVec.xyxx;

        vec3 anotherVec = differentVec.zyw;

        vec4 otherVec = someVec.xxxx + anotherVec.yxzy;

Pass parameter operation:

        vec2 vect = vec2(0.5, 0.7);

        vec4 result = vec4(vect, 0.0, 0.0);

        vec4 otherResult = vec4(result.xyz, 1.0);

4. Input and output

GLSL defines the in and out keywords to implement the input and output of the shader

Each shader uses these two keywords to set the input and output, as long as an output variable matches the input of the next shader stage, it will be passed on

The input and output of vertex shaders and fragment shaders are slightly different:

The vertex shader needs to provide an additional layout identifier for its input so we can link it to the vertex data

The fragment shader needs a vec4 color output variable, because the fragment shader needs to generate a final output color

If you intend to send data from one shader to another

An output must be declared in the sender shader and a similar input in the receiver shader

When the type and name are the same, OpenGL will link the two variables together, and data can be sent between them

Example: Let the vertex shader decide the color for the fragment shader

Vertex shader code:

#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); // 把输出变量设置为暗红色
}

Fragment shader code:

#version 330 core
out vec4 FragColor;
in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)
void main()
{
   FragColor = vertexColor;
}

The vertex shader and fragment shader are linked together through the variable vertexColor

Compile and run:

Successfully sent data from vertex shader to fragment shader.

Let's take it a step further and see if we can send a color directly to the fragment shader from within the application!

5.Uniform

Uniform is a way to send data from an application in the CPU to a shader in the GPU

Uniform is a global variable , which means:

        (1). We can define them in any shader

        (2). It is unique in each shader program object and can be accessed by any shader of the shader program at any stage

        (3). No matter what the uniform value is set to, uniform will keep their data until they are reset or updated

Example: Set the color of the triangle by uniform

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

This Uniform has not been assigned yet

First, you need to use the glGetUniformLocation() function to get the index/position value of the Uniform property in the shader, and then you can update its value

This time instead of assigning a fixed color value, let it change the color over time:

//通过glfwGetTime()获取运行的秒数
float timeValue = glfwGetTime();
//使用sin()函数让颜色在0.0到1.0
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
/*查询uniform ourColor的位置值,返回-1表示没能找到
  参数:着色器程序、uniform名字*/
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
//设置uniform变量的值
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

Querying Uniform addresses does not require previous use of shader programs

You must use the program (call glUseProgram) before updating a uniform , because it sets the uniform in the currently active shader program

OpenGL is a C library at its core, so it does not support type overloading. When the function parameters are different, a new function must be defined for it;

glUniform is a typical example. This function identifies the type of uniform with a specific suffix:

        The f function requires a float as its value

        The i function requires an int as its value

        The ui function expects an unsigned int as its value

        The 3f function requires 3 floats as its value

        The fv function expects a float vector/array as its value

After knowing how to set the value of the uniform variable

Now start rendering the triangle dynamically, updating the uniform on each iteration of the rendering loop (so it changes frame by frame)

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

Compile and run, the effect is as follows:

As you can see, uniform is very useful for setting a property whose value changes during rendering iterations

Very convenient for data interaction between programs and shaders

But what if we want to set a color for each vertex?

In this case, you have to declare as many uniforms as there are vertices

But a better solution on this issue would be to include more data in the vertex attributes,

Here's what we're going to do next

6. More attributes

Add color data to vertex data

The three vertices of the triangle are designated red, green, and blue:

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    // 顶部
};

There was only one "position attribute" in the vertex data before, and now there is a new "color attribute"

The code of the vertex shader is also modified accordingly:

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

out vec3 ourColor; // 向片段着色器输出一个颜色

void main()
{
    gl_Position = vec4(aPos, 1.0);
    // 将ourColor设置为我们从顶点数据那里得到的输入颜色
    ourColor = aColor; 
}

The code for the fragment shader should also be modified:

#version 330 core
in vec3 ourColor;
out vec4 FragColor;  

void main()
{
    FragColor = vec4(ourColor, 1.0);
}

A new vertex attribute has been added, and the VBO has also changed:

 Vertex attribute pointers also need to be reallocated:

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

Parameter changes:

  • The color attribute is the second attribute of the vertex, so the first parameter is assigned 1
  • A vertex has two attributes, 6 floats, so the fifth parameter step size is 6*sizeof(float)
  • The offset of the color attribute in each vertex memory is 3 floats, so fill in the last parameter with 3*sizeof(float)

After the code is modified, compile and run:

When you see this triangle, you will have a question:

Why we only set the color of three vertices, but the palette triangle is displayed?

This is the result of Fragment Interpolation in         the fragment shader

        When rendering a triangle, the rasterization stage will divide the triangle into countless pixel fragments for coloring

        The principle of coloring can be understood as "close to Zhu Zechi, close to ink to black" , which is to take the pixel values ​​​​of the surrounding pixels for interpolation filling

        This creates the effect of the palette

7. Package shader class

Until now, all our code has been written in a main.cpp

Shader writing, compiling, testing, linking, calling and other related codes can be completely separated and packaged into files to ensure the readability and portability of the code

Write a shader class header file Shader.h:

#include <glad/glad.h>

#include <string>
#include <fstream>
#include <sstream>
#include <iostream>

using namespace std;

class Shader
{
    public:
    //着色器程序ID
    unsigned int ID;
    
    // 构造器读取并构建着色器
    Shader(const char* vertexPath, const char* fragmentPath) 
    {
        //1.从filePath中检索顶点 / 片段源代码
        // 顶点和片段着色器代码文件
        ifstream vShaderFile;
        ifstream fShaderFile;
        
        // 顶点和片段着色器源码字符串
        string vertexCode;
        string fragmentCode;
        
        // 确保ifstream对象能够抛出异常
        vShaderFile.exceptions(ifstream::failbit | ifstream::badbit);
        fShaderFile.exceptions(ifstream::failbit | ifstream::badbit);
        
        try 
        {
        // open file
        vShaderFile.open(vertexPath);
        fShaderFile.open(fragmentPath);
        
        // 读取文件缓冲内容到streams
        std::stringstream vShaderStream, fShaderStream;
        vShaderStream << vShaderFile.rdbuf();
        fShaderStream << fShaderFile.rdbuf();
        
        // close file hander
        vShaderFile.close();
        fShaderFile.close();
        
        // stream转换赋值给string字符串,最终string还需要转换成char*指针
        vertexCode = vShaderStream.str();
        fragmentCode = fShaderStream.str();
        }
        catch (std::ifstream::failure& e) 
        {
        std::cout << "ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ: " << e.what() << std::endl;
        }
        
          //string转换成char*
        const char* vShaderCode = vertexCode.c_str();
        const char* fShaderCode = fragmentCode.c_str();
        
        //2.编译shader
        //创建顶点着色器
        unsigned int vertex;
        vertex = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vertex, 1, &vShaderCode, NULL);
        glCompileShader(vertex);
        checkCompileErrors(vertex, "VERTEX");
                 
        //创建片段着色器
        unsigned int fragment;
        fragment = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fragment, 1, &fShaderCode, NULL);
        glCompileShader(fragment);
        checkCompileErrors(fragment, "FRAGMENT");
        
        //创建着色器程序
        ID = glCreateProgram();
        //附件着色器到着色器程序,然后链接着色器程序到OpenGL
        glAttachShader(ID, vertex);
        glAttachShader(ID, fragment);
        glLinkProgram(ID);
        checkCompileErrors(ID, "PROGRAM");
        
        //删除着色器
        glDeleteShader(vertex);
        glDeleteShader(fragment);
    }
    
    // 使用/激活着色器程序
    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);
    }

    private:
    // 检测着色器编译错误和着色器程序链接错误
    void checkCompileErrors(unsigned int shader, std::string type)
    {
        int success;
        char infoLog[1024];
        
        if (type != "PROGRAM")
        {
            glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
            if (!success)
            {
                glGetShaderInfoLog(shader, 1024, NULL, infoLog);
                std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n" << infoLog << "\n ----------------------------------------------------- -- " << std::endl;
            }
        }
        else
        {
            glGetProgramiv(shader, GL_LINK_STATUS, &success);
            if (!success)
            {
                glGetProgramInfoLog(shader, 1024, NULL, infoLog);
                std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n" << infoLog << "\n ----------------------------------------------------- -- " << std::endl;
            }
        }
    }
};

The code about the shader in the main.cpp file can basically be deleted

Just keep shader loading and shader programs using:

#include "Shader.h"

#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);

// 窗口大小
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

int main()
{
    // glfw: 初始化窗口配置
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

    // glfw:创建窗口
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    // glad: load OpenGL所有函数指针
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    //加载着色器文件
    Shader ourShader("assets/shader.vs", "assets/shader.fs");

    // 配置顶点数据buffer和顶点属性
    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    // 顶部
    };

    unsigned int VBO, VAO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    // 绑定VAO先,然后绑定和设置顶点buffer,再配置顶点属性
    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

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

    //激活着色器程序
    ourShader.use();

    // 渲染循环
    while (!glfwWindowShouldClose(window))
    {
        // 处理外设输入
        processInput(window);

        // render
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // 绘制三角形
        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 3);
        // glBindVertexArray(0); // no need to unbind it every time 

        // glfw: 交换缓冲区和轮询IO事件(按键按下/释放、鼠标移动等)
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // 删除分配的所有资源:
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);

    // glfw:终止、清除所有先前分配的GLFW资源。
    glfwTerminate();
    return 0;
}

// glfw:处理窗口事件
void processInput(GLFWwindow* window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

// glfw: 窗口尺寸变化回调
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // 确保视口与新窗口尺寸匹配
    glViewport(0, 0, width, height);
}

As you can see from the code, there are only two calls to the shader in main.cpp:

//load the shader file

Shader ourShader("assets/shader.vs", "assets/shader.fs");

.....

//Activate the shader program

ourShader.use();

.....

Created a new assets folder in the project directory

Create two new txt texts, save the content of the vertex and fragment shaders, and modify the suffixes to vs and fs respectively

The name and suffix can be modified at will, as long as you like it

Compile and run, the result is the same as before:

Guess you like

Origin blog.csdn.net/geyichongchujianghu/article/details/129779727