OpenGL之着色器

着色器

 

我们要写一个三角形,需要经理上图流程,其中顶点着色器片段着色器需要我们自己写。

着色器基本上只是一个程序, 不同点在于它是运行在我们的gpu上,在我们的显卡上,而不是像C++程序一样运行在cpu上。

(1)顶点着色器
获取了每一个我们想要渲染的顶点的调用。
这个例子上,我我们有三个顶点,这就意味着顶点着色器会调用三次,每个顶点各一次。一个顶点着色器的基本目的就是告诉opengl你想要那个顶点在显示器的哪里,简单的说就是窗口的哪里。

(2)片段着色器

片段着色器会为每一个像素运行一次光栅化,光栅化实际上是画在屏幕上。
我们的窗口实际上是由像素组成的。

(3)顶点着色器和片段着色器的区别

现在你可以注意到这两者之间有一点不一样,顶点着色器调用三次,片段着色器调用成百上千次,这取决于我们的三角形在我们的屏幕上占用了多大空间,如果你有一个细小的三角形,在你的窗口上一个真的很小的三角形,那可能会调用额外的50次。如果你有一个巨大的三角形,这充满了你的屏幕,这可能是有一百万像素或者50万像素,那就意味着片段着色器要调用50万次。

需要用着色器语言GLSL(OpenGL Shading Language)编写顶点着色器,然后进行编译。

 

#version 330 core表示OpenGL的版本号及使用核心模式。(对输入数据,只传输未处理)。
in关键字,在顶点着色器中声明所有的输入顶点属性(Input Vertex Attribute)。顶点都为3D坐标,因此创建vec3类型的输入变量aPos。
顶点着色器需要将数据赋值给预定义的gl_Position变量(顶点着色器的输出,vec4类型,w分量设置为1.0f)。我们同样也通过layout (location = 0)设定了输入变量的位置值。

片段着色器所做的是计算像素最后的颜色输出
片段着色器只需要一个输出变量,这个变量是一个4分量向量,它表示的是最终的输出颜色,我们应该自己将其计算出来。声明输出变量可以使用out关键字,这里我们命名为FragColor。下面,我们将一个Alpha值为1.0(1.0代表完全不透明)的橘黄色的vec4赋值给颜色输出。

(4)编译着色器

现在,我们暂时将顶点着色器的源代码硬编码在代码文件顶部的C风格字符串中:

复制代码
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"; 

const char *fragmentShaderSource = "#version 330 core\n" 
"out vec4 FragColor;\n" 
"void main()\n" 
"{\n" 
" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n" 
"}\n\0";
复制代码

为了让OpenGL使用着色器,必须在运行时从源码中动态编译着色器。首先创建着色器对象。 
各个阶段的着色器需要通过着色器程序对象链接起来。着色器程序对象是多个着色器组合的最终链接版本。
将着色器链接到程序时,会将每个着色器的输出链接到下一个着色器的输入。如果输出和输入不匹配,会出现链接错误。

我们首先要做的是创建一个着色器对象,注意还是用ID来引用的。所以我们储存这个顶点着色器为unsigned int,然后用glCreateShader创建这个着色器:

unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);

我们把需要创建的着色器类型以参数形式提供给glCreateShader。由于我们正在创建一个顶点着色器,传递的参数是GL_VERTEX_SHADER

下一步我们把这个着色器源码附加到着色器对象上,然后编译它:

glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

glShaderSource函数把要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量,这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL

(5)着色器程序

着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。
创建一个程序对象很简单:

unsigned int shaderProgram;
shaderProgram = glCreateProgram();

 

 glCreateProgram函数创建一个程序,并返回新创建程序对象的ID引用。现在我们需要把之前编译的着色器附加到程序对象上,然后用glLinkProgram链接它们:

glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

 

 得到的结果就是一个程序对象,我们可以调用glUseProgram函数,用刚创建的程序对象作为它的参数,以激活这个程序对象:

glUseProgram(shaderProgram);

 

 对了,在把着色器对象链接到程序对象以后,记得删除着色器对象,我们不再需要它们了:

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

 

代码示例:

复制代码
// GLAD的include文件包含所需的OpenGL头文件(如GL/GL.h),因此确保在其他需要OpenGL的头文件(如GLFW)之前包含GLAD。就是#include <glad/glad.h> 放在最前面
#include <glad/glad.h> 
#include <GLFW/glfw3.h>
#include <iostream>
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
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";

const char* fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";

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

int main() {
    // 初始化GLFW,只有初始化完成之后才能够使用GLFW的函数
    glfwInit();
    // GLFW配置设置
    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  

    // 创建窗口 大小和名称
    GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
    if (window == NULL) {
        std::cout << "Failed to create GLFW window" << std::endl;
        // 此函数销毁所有剩余的窗口和光标
        glfwTerminate();
        return -1;
    }
    //GLFW将窗口的上下文设置为当前线程的上下文
    glfwMakeContextCurrent(window);
    // 告诉GLFW我们希望每当窗口调整大小的时候调用这个函数
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
    //GLAD
    // glad: 加载所有OpenGL函数指针
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }
#pragma region 着色器

    // 创建和编译着色器程序 
//顶点着色器
    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, 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;
    }
    // 片段着色器
    unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);
    // 检查编译错误
    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success) {
        glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
    }
    //着色器程序
    unsigned int shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);
    //链接错误检查
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
    }
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

#pragma endregion

    //创建VBO和VAO对象,并赋予ID
    unsigned int VBO, VAO;
    //创建1个VAO对象
    glGenVertexArrays(1, &VAO);
    //创建1个VBO对象
    glGenBuffers(1, &VBO);
    //绑定VBO和VAO对象
    glBindVertexArray(VAO);
    //缓冲对象如果绑定的是顶点属性则用:GL_ARRAY_BUFFER
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    //为当前绑定到target的缓冲区对象创建一个新的数据存储。
    //如果data不是NULL,则使用来自此指针的数据初始化数据存储
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    //上面我们将数据存到了缓冲区对象中,下面就需要
    //告知Shader(着色器)如何解析缓冲里的属性值
    //第一个属性是从哪个位置开始解析
    //第二个属性是每个顶点属性由几个组合而成
    //第三个属性是数组中每个元素的类型
    //第四个表示是否标准化,这里暂时不需要,需要注意只有整型值才会有效,如果浮点型的数据不会起作用
    //第五个是步长,因为我们这个是数组中每3个元素组成一个顶点属性,而且是float类型
    //第六个是偏移量,我们这里为0,就是从0开始读取数组中的数据的
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    //开启VAO管理的第一个属性值
    glEnableVertexAttribArray(0);
    //作为习惯,用完之后将VBO,VAO解绑,需要的时候可以重新绑定
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0); 
    // 渲染循环 一个循环就是一帧 只要是窗体不关闭,就会一直循环
    while (!glfwWindowShouldClose(window)) {
        processInput(window);

        // 在这里,我们将屏幕设置为了类似黑板的深蓝绿色
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f); //状态设置
        // 调用glClear函数,清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色。
        glClear(GL_COLOR_BUFFER_BIT); //状态使用

#pragma region 绘制三角形

// 首先,要选择使用哪个着色器 每个着色器调用和渲染调用都会使用这个程序对象
        glUseProgram(shaderProgram);

        // 需要注意的是前面我们已经解绑了VAO,所以现在是无法解析数据的,所以我们需要重新绑定,
        // 至于数据我们已经存到缓冲区了
        glBindVertexArray(VAO);
        // 从数据数组中indexwei0处开始读取,每三个做一个三角形的顶点。第三个参数是说一共绘制三个顶点数据(每个顶点由vertices数组中的3个元素组成)
        glDrawArrays(GL_TRIANGLES, 0, 3);
#pragma endregion

        // glfw: 交换缓冲区 该函数在指定窗口的前后缓冲区交换
        // 前缓冲区:屏幕上显示的图像
        // 后缓冲区:正在渲染的图像
        // glfwSwapBuffers函数会交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲),
        // 它在这一迭代中被用来绘制,并且将会作为输出显示在屏幕上
        glfwSwapBuffers(window);
        // 轮询IO事件(按键按下 / 释放、鼠标移动等)通过下面方法就可以是得窗体对鼠标做出的动作做出反应,比如关闭,移动窗体等
        glfwPollEvents();
    }
    // glfw: 回收前面分配的GLFW先关资源. 一定要注意,只有关闭窗体之后才会跳出while循环走到这一步!!!
    glfwTerminate();

    // 在while循环退出后释放内存:
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shaderProgram);

    return 0;
}
// glfwGetKey函数:需要一个窗口以及一个按键作为输入;函数将会返回这个按键是否正在被按下
void processInput(GLFWwindow* window)
{
    // 如果按下了ESC键,设置窗体的关闭标志为true,代表窗体可以退出
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

// 当改变窗口的大小的时候,视口也应该被调整。我们可以对窗口注册一个回调函数(Callback Function),它会在每次窗口大小被调整的时候被调用
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // 设置窗口维度
    // glViewport(前两参数为窗口左下角位置,3.宽度,4.高度)
    glViewport(0, 0, width, height);
}
复制代码

 结果:

 

 

 参考:

 https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/#_5

posted @   安静点--  阅读(1002)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
历史上的今天:
2021-09-04 数据结构之归并排序
2021-09-04 数据结构之插入排序
2021-09-04 数据结构之选择排序
点击右上角即可分享
微信分享提示