跟着LearnOpenGL学习4--着色器

news2024/12/23 10:25:20

文章目录

  • 一、前言
  • 二、GLSL
    • 2.1、着色器结构
    • 2.2、数据类型
      • 2.2.1、向量
    • 2.3、输入与输出
    • 2.4、uniform
    • 2.5、更多属性
  • 三、着色器类

一、前言

在之前绘制三角形的博文中,我们已经接触到了着色器,但是肯定有许多疑问,本文来详细了解一下着色器!

着色器(Shader)是运行在GPU上的小程序,这些小程序为图形渲染管线的某个特定部分而运行;从基本意义上来说,着色器只是一种把输入转化为输出的程序。着色器也是一种非常独立的程序,因为它们之间不能相互通信;它们之间唯一的沟通只有通过输入和输出。


二、GLSL

着色器是使用一种叫GLSL的类C语言写成的,GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。


2.1、着色器结构

一个典型的着色器结构如下所示:

#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.2、数据类型

  • 和其他编程语言一样,GLSL有数据类型可以来指定变量的种类;
  • GLSL中包含C等其它语言大部分的默认基础数据类型:intfloatdoubleuintbool
  • GLSL也有两种容器类型,分别是向量(Vector)和矩阵(Matrix);

2.2.1、向量

GLSL中的向量是一个可以包含有2、3或者4个分量的容器,分量的类型可以是前面默认基础类型的任意一个。

它们可以是下面的形式(n代表分量的数量):

类型含义
vecn包含n个float分量的默认向量
bvecn包含n个bool分量的向量
ivecn包含n个int分量的向量
uvecn包含n个unsigned int分量的向量
dvecn包含n个double分量的向量

大多数时候我们使用vecn,因为float足够满足大多数要求了;

向量分量访问
一个向量的分量可以通过vec.x这种方式获取,这里x是指这个向量的第一个分量。你可以分别使用.x.y.z.w来获取它们的第1、2、3、4个分量;

GLSL也允许你对颜色使用rgba,或是对纹理坐标使用stpq访问相同的分量;

向量重组
向量这一数据类型也允许一些有趣而灵活的分量选择方式,叫做重组(Swizzling)。重组允许这样的语法:

vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;

可以使用上面4个字母任意组合来创建一个和原来向量一样长的(同类型)新向量,只要原来向量有那些分量即可;然而,你不允许在一个vec2向量中去获取.z元素;

向量传参构造

我们也可以把一个向量作为一个参数传给不同的向量构造函数,以减少需求参数的数量:

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

2.3、输入与输出

虽然着色器是各自独立的小程序,但是它们都是一个整体的一部分,出于这样的原因,我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递。

GLSL定义了inout关键字专门来实现这个目的。每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。但在顶点和片段着色器中会有点不同。

顶点着色器应该接收的是一种特殊形式的输入,否则就会效率低下。顶点着色器的输入特殊在,它从顶点数据中直接接收输入。为了定义顶点数据该如何管理,我们使用location这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。

我们已经在前面的教程看过这个了,layout (location = 0)。顶点着色器需要为它的输入提供一个额外的layout标识,这样我们才能把它链接到顶点数据。

另一个例外是片段着色器,它需要一个vec4颜色输出变量,因为片段着色器需要生成一个最终输出的颜色。如果你在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色)。

所以,如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。例如:

顶点着色器

#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;
}
  • 可以看到在顶点着色器中声明了一个vertexColor变量作为vec4输出,并在片段着色器中声明了一个类似的vertexColor
  • 由于它们名字相同且类型相同,片段着色器中的vertexColor就和顶点着色器中的vertexColor链接了;

2.4、uniform

uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同:

  • uniform是全局的(Global),全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问;
  • 无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新;

在着色器中添加uniform关键字,在类型和变量名前,来声明一个GLSL的uniform

#version 330 core
out vec4 FragColor;

uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量

void main()
{
    FragColor = ourColor;
}

注意:如果你声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误;

这个uniform现在还是空的,我们还没有给它添加任何数据;我们首先需要找到着色器中uniform属性的索引/位置值。当我们得到uniform的索引/位置值后,我们就可以更新它的值了。这次我们不去给像素传递单独一个颜色,而是让它随着时间改变颜色:

float timeValue = glfwGetTime();	//获取运行的秒数
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;	//让颜色在0.0到1.0之间改变,最后将结果储存到greenValue里
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");	//查询uniform ourColor的位置值
//查询uniform地址不要求你之前使用过着色器程序,但是更新一个uniform之前你必须先使用程序(调用glUseProgram);
//因为它是在当前激活的着色器程序中设置uniform的。
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);	//设置uniform值

实例:通过uniform修改片段着色器的输出值

在这里插入图片描述

#include "mainwindow.h"
#include <QApplication>

//在包含GLFW的头文件之前包含了GLAD的头文件;
//GLAD的头文件包含了正确的OpenGL头文件(例如GL/gl.h);
//所以需要在其它依赖于OpenGL的头文件之前包含GLAD;
#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <iostream>
#include <math.h>

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

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

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"
    "uniform vec4 ourColor;\n"
    "void main()\n"
    "{\n"
    "   FragColor = ourColor;\n"
    "}\n\0";

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    MainWindow w;

    //初始化GLFW
    //--------------------
    glfwInit();

    //配置GLFW
    //--------------------
    //告诉GLFW使用的OpenGL本是3.3
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    //告诉GLFW使用的是核心模式(Core-profile)
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    //创建一个新的OpenGL环境和窗口
    //-----------------------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();    //glfw销毁窗口喝OpenGL环境,并释放资源
        return -1;
    }

    //设置参数window中的窗口所关联的OpenGL环境为当前环境
    //-----------------------------------
    glfwMakeContextCurrent(window);

    //设置窗口尺寸改变大小时的回调函数(窗口尺寸发送改变时会自动调用)
    //-----------------------------------
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    //glad加载系统相关的OpenGL函数指针
    //---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    //顶点着色器
    //---------------------------------------------------------------------
    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);

    //顶点数据
    //---------------------------------------------------------------------
    float vertices[] = {
         0.5f,  0.5f, 0.0f,  // top right
         0.5f, -0.5f, 0.0f,  // bottom right
        -0.5f, -0.5f, 0.0f,  // bottom left
        -0.5f,  0.5f, 0.0f   // top left
    };
    unsigned int indices[] = {  //绘制索引
        0, 1, 3,  //第1个三角形
        1, 2, 3   //第2个三角形
    };

    unsigned int VBO, VAO, EBO;
    glGenVertexArrays(1, &VAO);     //创建顶点数组对象
    glGenBuffers(1, &VBO);          //创建顶点缓冲对象
    glGenBuffers(1, &EBO);          //创建元素缓冲对象

    glBindVertexArray(VAO);         //绑定VAO

    glBindBuffer(GL_ARRAY_BUFFER, VBO);     //将VBO与GL_ARRAY_BUFFER缓冲区绑定
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);  //将顶点数据复制到GL_ARRAY_BUFFER缓冲区,之后可通过VBO进行操作

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);     //将EBO与GL_ELEMENT_ARRAY_BUFFER缓冲区绑定
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);    //将索引复制到GL_ELEMENT_ARRAY_BUFFER缓冲区,之后可通过EBO进行操作

    //设定顶点属性指针
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    //调用glVertexAttribPointer将VBO注册为顶点属性的绑定顶点缓冲对象,因此之后我们可以安全地解除绑定
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    //解除对VAO的绑定
    glBindVertexArray(0);

    //启用线框模式
    //glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

    //渲染循环
    //我们可不希望只绘制一个图像之后我们的应用程序就立即退出并关闭窗口;
    //我们希望程序在我们主动关闭它之前不断绘制图像并能够接受用户输入;
    //因此,我们需要在程序中添加一个while循环,它能在我们让GLFW退出前一直保持运行;
    //------------------------------------------------------------------------------
    while (!glfwWindowShouldClose(window))  //如果用户准备关闭参数window所指定的窗口,那么此接口将会返回GL_TRUE,否则将会返回GL_FALSE
    {
        //用户输入
        //------------------------------------------------------------------------------
        processInput(window);   //检测是否有输入

        //渲染指令
        //------------------------------------------------------------------------------
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        //更新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);

        //绘制三角形
        glUseProgram(shaderProgram);        //激活着色器程序对象
        glBindVertexArray(VAO);             //绑定VAO
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);    //绘制三角形
        // glBindVertexArray(0);            //解绑VAO

        //告诉GLFW检查所有等待处理的事件和消息,包括操作系统和窗口系统中应当处理的消息。如果有消息正在等待,它会先处理这些消息再返回;否则该函数会立即返回
        //---------------------------------------------------------------------------------------------------------------------------------
        glfwPollEvents();

        //请求窗口系统将参数window关联的后缓存画面呈现给用户(双缓冲绘图)
        //------------------------------------------------------------------------------
        glfwSwapBuffers(window);
    }

    //释放资源
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);
    glDeleteProgram(shaderProgram);

    //glfw销毁窗口喝OpenGL环境,并释放资源(之后必须再次调用glfwInit()才能使用大多数GLFW函数)
    //------------------------------------------------------------------
    glfwTerminate();


    w.show();

    return a.exec();
}

//检测是否有输入
//---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
    if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)   //ESC键,退出
        glfwSetWindowShouldClose(window, true);
}

//给glfw窗口注册的尺寸改变回调函数
//---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}

可以看到,uniform对于设置一个在渲染迭代中会改变的属性是一个非常有用的工具,它也是一个在程序和着色器间数据交互的很好工具,但假如我们打算为每个顶点设置一个颜色的时候该怎么办?这种情况下,我们就不得不声明和顶点数目一样多的uniform了。在这一问题上更好的解决方案是在顶点属性中包含更多的数据,这是我们接下来要做的事情。


2.5、更多属性

在前面的教程中,我们了解了如何填充VBO、配置顶点属性指针以及如何把它们都储存到一个VAO里;

这次,我们同样打算把颜色数据加进顶点数据中。我们将把颜色数据添加为3个float值至vertices数组;

我们将把三角形的三个角分别指定为红色、绿色和蓝色:

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

由于现在有更多的数据要发送到顶点着色器,我们有必要去调整一下顶点着色器,使它能够接收颜色值作为一个顶点属性输入。需要注意的是我们用layout标识符来把aColor属性的位置值设置为1:

#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设置为我们从顶点数据那里得到的输入颜色
}

由于我们不再使用uniform来传递片段的颜色了,现在使用ourColor输出变量,我们必须再修改一下片段着色器:

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

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

因为我们添加了另一个顶点属性,并且更新了VBO的内存,我们就必须重新配置顶点属性指针。更新后的VBO内存中的数据现在看起来像这样:
在这里插入图片描述
知道了现在使用的布局,我们就可以使用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);
  • 步长值为6 * sizeof(float),为获得数据队列中下一个属性值,我们必须向右移动6个float,其中3个是位置值,另外3个是颜色值;
  • 偏移量为(void*)(3* sizeof(float)),对于每个顶点来说,位置顶点属性在前,所以它的偏移量是0。颜色属性紧随位置数据之后,所以偏移量就是3 * sizeof(float)

完整代码
在这里插入图片描述

#include "mainwindow.h"
#include <QApplication>

//在包含GLFW的头文件之前包含了GLAD的头文件;
//GLAD的头文件包含了正确的OpenGL头文件(例如GL/gl.h);
//所以需要在其它依赖于OpenGL的头文件之前包含GLAD;
#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <iostream>

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

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

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

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    //MainWindow w;
    //w.show();


    //初始化GLFW
    //--------------------
    glfwInit();

    //配置GLFW
    //--------------------
    //告诉GLFW使用的OpenGL本是3.3
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    //告诉GLFW使用的是核心模式(Core-profile)
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    //创建一个新的OpenGL环境和窗口
    //-----------------------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();    //glfw销毁窗口喝OpenGL环境,并释放资源
        return -1;
    }

    //设置参数window中的窗口所关联的OpenGL环境为当前环境
    //-----------------------------------
    glfwMakeContextCurrent(window);

    //设置窗口尺寸改变大小时的回调函数(窗口尺寸发送改变时会自动调用)
    //-----------------------------------
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    //glad加载系统相关的OpenGL函数指针
    //---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    //顶点着色器
    //---------------------------------------------------------------------
    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);

    //顶点数据
    //---------------------------------------------------------------------
    float vertices[] = {
        //位置              //颜色
       -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // left
        0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // right
        0.0f,  0.5f, 0.0f, 0.0f, 0.0f, 1.0f  // top
    };

    unsigned int VBO, VAO;
    glGenVertexArrays(1, &VAO);     //创建顶点数组对象
    glGenBuffers(1, &VBO);          //创建顶点缓冲对象

    glBindVertexArray(VAO);         //绑定VAO

    glBindBuffer(GL_ARRAY_BUFFER, VBO);     //将VBO与GL_ARRAY_BUFFER缓冲区绑定
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);  //将顶点数据复制到GL_ARRAY_BUFFER缓冲区,之后可通过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);

    //调用glVertexAttribPointer将VBO注册为顶点属性的绑定顶点缓冲对象,因此之后我们可以安全地解除绑定
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    //解除对VAO的绑定
    glBindVertexArray(0);

    //渲染循环
    //我们可不希望只绘制一个图像之后我们的应用程序就立即退出并关闭窗口;
    //我们希望程序在我们主动关闭它之前不断绘制图像并能够接受用户输入;
    //因此,我们需要在程序中添加一个while循环,它能在我们让GLFW退出前一直保持运行;
    //------------------------------------------------------------------------------
    while (!glfwWindowShouldClose(window))  //如果用户准备关闭参数window所指定的窗口,那么此接口将会返回GL_TRUE,否则将会返回GL_FALSE
    {
        //用户输入
        //------------------------------------------------------------------------------
        processInput(window);   //检测是否有输入

        //渲染指令
        //------------------------------------------------------------------------------
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        //绘制三角形
        glUseProgram(shaderProgram);        //激活着色器程序对象
        glBindVertexArray(VAO);             //绑定VAO
        glDrawArrays(GL_TRIANGLES, 0, 3);   //绘制三角形
        // glBindVertexArray(0);            //解绑VAO

        //告诉GLFW检查所有等待处理的事件和消息,包括操作系统和窗口系统中应当处理的消息。如果有消息正在等待,它会先处理这些消息再返回;否则该函数会立即返回
        //---------------------------------------------------------------------------------------------------------------------------------
        glfwPollEvents();

        //请求窗口系统将参数window关联的后缓存画面呈现给用户(双缓冲绘图)
        //------------------------------------------------------------------------------
        glfwSwapBuffers(window);
    }

    //释放资源
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteProgram(shaderProgram);

    //glfw销毁窗口喝OpenGL环境,并释放资源(之后必须再次调用glfwInit()才能使用大多数GLFW函数)
    //------------------------------------------------------------------
    glfwTerminate();


    return a.exec();
}

//检测是否有输入
//---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
    if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)   //ESC键,退出
        glfwSetWindowShouldClose(window, true);
}

//给glfw窗口注册的尺寸改变回调函数
//---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}

我们给三角形三个顶点分别设置了:红色、绿色、蓝色,渲染出来的三角形如上所示,这是在片段着色器中进行的所谓**片段插值(Fragment Interpolation)**的结果。当渲染一个三角形时,**光栅化(Rasterization)**阶段通常会造成比原指定顶点更多的片段,光栅会根据每个片段在三角形形状上所处相对位置决定这些片段的位置,基于这些位置,它会插值(Interpolate)所有片段着色器的输入变量。

比如说,我们有一个线段,上面的端点是绿色的,下面的端点是蓝色的。如果一个片段着色器在线段的70%的位置运行,它的颜色输入属性就会是一个绿色和蓝色的线性结合;更精确地说就是30%蓝 + 70%绿。

这正是在这个三角形中发生了什么。我们有3个顶点,和相应的3个颜色,从这个三角形的像素来看它可能包含50000左右的片段,片段着色器为这些像素进行插值颜色。如果你仔细看这些颜色就应该能明白了:红首先变成到紫再变为蓝色。片段插值会被应用到片段着色器的所有输入属性上。


三、着色器类

编写、编译、管理着色器是件麻烦事,我们会写一个类来让我们轻松一点;

它可以从硬盘读取着色器,然后编译并链接它们,并对它们进行错误检测;

shader.h

#ifndef SHADER_H
#define SHADER_H

#include <glad/glad.h>; // 包含glad来获取所有的必须OpenGL头文件

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


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;

    void checkCompileErrors(unsigned int shader, std::string type);
};

#endif

  • 着色器类储存了着色器程序的ID;
  • 它的构造器需要顶点和片段着色器源代码的文件路径,这样我们就可以把源码的文本文件储存在硬盘上了;
  • 除此之外,为了让我们的生活更轻松一点,还加入了一些工具函数:use用来激活着色器程序,所有的set…函数能够查询一个unform的位置值并设置它的值。

shader.cpp

#include "shader.h"

// 构造器读取并构建着色器
Shader::Shader(const char* vertexPath, const char* fragmentPath)
{
    // 1. 读取顶点着色器和片段着色器源码
    //================================================================================
    std::string vertexCode;     //顶点着色器源码
    std::string fragmentCode;   //片段着色器源码
    std::ifstream vShaderFile;
    std::ifstream fShaderFile;
    // ensure ifstream objects can throw exceptions:
    vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
    fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
    try
    {
        // open files
        vShaderFile.open(vertexPath);
        fShaderFile.open(fragmentPath);
        std::stringstream vShaderStream, fShaderStream;
        // read file's buffer contents into streams
        vShaderStream << vShaderFile.rdbuf();
        fShaderStream << fShaderFile.rdbuf();
        // close file handlers
        vShaderFile.close();
        fShaderFile.close();
        // convert stream into string
        vertexCode   = vShaderStream.str();
        fragmentCode = fShaderStream.str();
    }
    catch (std::ifstream::failure& e)
    {
        std::cout << "ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ: " << e.what() << std::endl;
    }
    const char* vShaderCode = vertexCode.c_str();
    const char * fShaderCode = fragmentCode.c_str();

    // 2. 编译着色器
    //================================================================================
    unsigned int vertex, fragment;
    // 顶点着色器
    vertex = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertex, 1, &vShaderCode, NULL);
    glCompileShader(vertex);
    checkCompileErrors(vertex, "VERTEX");
    // 片段着色器
    fragment = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragment, 1, &fShaderCode, NULL);
    glCompileShader(fragment);
    checkCompileErrors(fragment, "FRAGMENT");
    // 链接着色器程序
    ID = glCreateProgram();
    glAttachShader(ID, vertex);
    glAttachShader(ID, fragment);
    glLinkProgram(ID);
    checkCompileErrors(ID, "PROGRAM");
    // 链接完成之后删除顶点着色器对象和片段着色器对象
    glDeleteShader(vertex);
    glDeleteShader(fragment);
}

// 使用/激活程序
void Shader::use()
{
    glUseProgram(ID);
}

// uniform工具函数
void Shader::setBool(const std::string &name, bool value) const
{
    glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
}
void Shader::setInt(const std::string &name, int value) const
{
    glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}
void Shader::setFloat(const std::string &name, float value) const
{
    glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}

void Shader::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;
        }
    }
}

着色器类的使用

把着色器类添加到工程中,如下所示:

在这里插入图片描述

把顶点着色器源码存储到shader.vs文件中,把片段着色器存储到shader.fs文件中,如下所示:

在这里插入图片描述
在程序中创建着色器类对象,并在绘制图形前激活着色器程序,完整代码如下:

#include "mainwindow.h"
#include <QApplication>

//在包含GLFW的头文件之前包含了GLAD的头文件;
//GLAD的头文件包含了正确的OpenGL头文件(例如GL/gl.h);
//所以需要在其它依赖于OpenGL的头文件之前包含GLAD;
#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <iostream>

#include "shader.h"

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

// settings
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    //MainWindow w;
    //w.show();


    //初始化GLFW
    //--------------------
    glfwInit();

    //配置GLFW
    //--------------------
    //告诉GLFW使用的OpenGL本是3.3
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    //告诉GLFW使用的是核心模式(Core-profile)
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    //创建一个新的OpenGL环境和窗口
    //-----------------------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();    //glfw销毁窗口喝OpenGL环境,并释放资源
        return -1;
    }

    //设置参数window中的窗口所关联的OpenGL环境为当前环境
    //-----------------------------------
    glfwMakeContextCurrent(window);

    //设置窗口尺寸改变大小时的回调函数(窗口尺寸发送改变时会自动调用)
    //-----------------------------------
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    //glad加载系统相关的OpenGL函数指针
    //---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

	//创建着色器类对象
    Shader ourShader("C:/Qt_Pro/OpenGL_GLFW/shader/shader.vs","C:/Qt_Pro/OpenGL_GLFW/shader/shader.fs");

    //顶点数据
    //---------------------------------------------------------------------
    float vertices[] = {
        //位置              //颜色
       -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // left
        0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // right
        0.0f,  0.5f, 0.0f, 0.0f, 0.0f, 1.0f  // top
    };

    unsigned int VBO, VAO;
    glGenVertexArrays(1, &VAO);     //创建顶点数组对象
    glGenBuffers(1, &VBO);          //创建顶点缓冲对象

    glBindVertexArray(VAO);         //绑定VAO

    glBindBuffer(GL_ARRAY_BUFFER, VBO);     //将VBO与GL_ARRAY_BUFFER缓冲区绑定
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);  //将顶点数据复制到GL_ARRAY_BUFFER缓冲区,之后可通过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);

    //调用glVertexAttribPointer将VBO注册为顶点属性的绑定顶点缓冲对象,因此之后我们可以安全地解除绑定
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    //解除对VAO的绑定
    glBindVertexArray(0);

    //渲染循环
    //我们可不希望只绘制一个图像之后我们的应用程序就立即退出并关闭窗口;
    //我们希望程序在我们主动关闭它之前不断绘制图像并能够接受用户输入;
    //因此,我们需要在程序中添加一个while循环,它能在我们让GLFW退出前一直保持运行;
    //------------------------------------------------------------------------------
    while (!glfwWindowShouldClose(window))  //如果用户准备关闭参数window所指定的窗口,那么此接口将会返回GL_TRUE,否则将会返回GL_FALSE
    {
        //用户输入
        //------------------------------------------------------------------------------
        processInput(window);   //检测是否有输入

        //渲染指令
        //------------------------------------------------------------------------------
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        //绘制三角形
        ourShader.use();        //激活着色器程序对象
        glBindVertexArray(VAO);             //绑定VAO
        glDrawArrays(GL_TRIANGLES, 0, 3);   //绘制三角形
        // glBindVertexArray(0);            //解绑VAO

        //告诉GLFW检查所有等待处理的事件和消息,包括操作系统和窗口系统中应当处理的消息。如果有消息正在等待,它会先处理这些消息再返回;否则该函数会立即返回
        //---------------------------------------------------------------------------------------------------------------------------------
        glfwPollEvents();

        //请求窗口系统将参数window关联的后缓存画面呈现给用户(双缓冲绘图)
        //------------------------------------------------------------------------------
        glfwSwapBuffers(window);
    }

    //释放资源
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    //glDeleteProgram(shaderProgram);

    //glfw销毁窗口和OpenGL环境,并释放资源(之后必须再次调用glfwInit()才能使用大多数GLFW函数)
    //------------------------------------------------------------------
    glfwTerminate();


    return a.exec();
}

//检测是否有输入
//---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
    if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)   //ESC键,退出
        glfwSetWindowShouldClose(window, true);
}

//给glfw窗口注册的尺寸改变回调函数
//---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/518469.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【17】SCI易中期刊推荐——计算机信息系统电子与电气(中科院4区)

💖💖>>>加勒比海带,QQ2479200884<<<💖💖 🍀🍀>>>【YOLO魔法搭配&论文投稿咨询】<<<🍀🍀 ✨✨>>>学习交流 | 温澜潮生 | 合作共赢 | 共同进步<<<✨✨ 📚📚>>>人工智能 | 计算机视觉…

Flink从入门到精通之-09状态编程

Flink从入门到精通之-09状态编程 Flink 处理机制的核心&#xff0c;就是“有状态的流式计算”。我们在之前的章节中也已经多次提到了“状态”&#xff08;state&#xff09;&#xff0c;不论是简单聚合、窗口聚合&#xff0c;还是处理函数的应用&#xff0c;都会有状态的身影出…

Spring Cloud第二季--Spring Cloud Bus

文章目录 Spring Clud Bus什么是总线基本原理 牛刀小试 Spring Clud Bus 在Spring Cloud学习–配置中心&#xff08;Config&#xff09;中实现了集中管理微服务配置、不同环境不同配置、运行期间也可动态调整、配置修改后可以自动更新的需求&#xff0c;但同时也有一个弊端&am…

服务器电源线:同为科技(TOWE)机房工程专用电源延长线

工程机房电源延长线 众所周知&#xff0c;世界上不同国家或地区所使用的插头、插座标准有所不同&#xff0c;在庞大复杂的数据中心计算集群内&#xff0c;需要电源供电才能正常工作&#xff0c;因此&#xff0c;服务器电源线是连接电源分配器和服务器的基本配件。在机房服务器…

如何通过优化服务器提升网站的SEO排名

在当今数字化时代&#xff0c;拥有一个高效、稳定的服务器对于网站的成功至关重要。然而&#xff0c;服务器不仅仅是为了提供网站的基本运行&#xff0c;它还可以对搜索引擎优化(SEO)起到关键作用&#xff0c;于是就有了多IP站群服务器这样对SEO非常友好的服务器。通过优化服务…

Goby 漏洞更新 |Telesquare TLR-2005Ksh 路由器 setSyncTimeHost 命令执行漏洞

漏洞名称&#xff1a;Telesquare TLR-2005Ksh 路由器 setSyncTimeHost 命令执行漏洞 English Name&#xff1a;Telesquare TLR-2005Ksh setSyncTimeHost RCE CVSS core: 9.8 影响资产数&#xff1a;25826 漏洞描述&#xff1a; Telesquare Tlr-2005Ksh是韩国Telesquare公司…

5月12日作业

作业1&#xff1a; 作业2&#xff1a;

make_shared知识点

背景 普通创建shared_ptr的方法如&#xff1a; shared_ptr<int> sp1(new int(11));sp1所开辟的动态内存分为如下两部分 uses是持有该资源shared_ptr数量&#xff0c;weaks表示持有该资源weak_ptr数量。 有可能出现 new int(10) 成功&#xff0c;但是引用计数 ref 的内…

应用程序服务器监控

什么是应用程序服务器监视 为了确保业务应用程序的最佳性能&#xff0c;必须使用应用程序服务器监视工具&#xff0c;以深入了解应用程序的运行状况和正常运行时间。应用程序服务器监视可帮助您识别性能不足的服务器组件以及性能问题的根本原因&#xff0c;修复它们并确保应用…

IDEA 导入 spring 源码

文章目录 前言一、下载源码二、安装 Gragle1. 下载 Gragle2. 配置环境变量 三、导入前准备四、编译源码1. 导入源码2. 我所遇见的问题 五、测试1. 创建 module2. 编写测试代码3. 我所遇到的问题 六、总结 前言 我们在学习 spring 源码的时候&#xff0c;有时候是需要在阅读源码…

【Linux Network】网络版计算器

目录 实验目标&#xff1a; 源代码&#xff1a; 实验结果&#xff1a; Linux网络编程✨ 实验目标&#xff1a; 制作一个应用层的简易版的计算器&#xff08;加、减、乘、除、取余&#xff09;&#xff1b; 源代码&#xff1a; makefile .PHONY:all all:CalClient CalServe…

国产仪器 6592A便携式高精度光伏电池伏安特性测试仪

6592A便携式高精度光伏电池伏安特性测试仪&#xff0c;主要用于室外太阳能电池阵列/组件/电池片伏安特性测试&#xff0c;能够方便、快速的测试太阳能电池阵列/组件/电池片在自然光照下的工作特性&#xff0c;可为太阳能电站设计、验收、维护提供测试保障&#xff0c;是电站建设…

sql进阶—— 查询重复数据 查询连续数据

目录 sql语句查询重复的数据 MYSQL 连续性问题求解 binlog 索引 sql语句查询重复的数据 查找所有重复 [标题] 的记录&#xff1a;SELECT * FROM t_info a WHERE ((SELECT COUNT(*) FROM t_info WHERE Title a.Title) > 1) ORDER BY Title DESC删除重复记录1。删除全部…

(9)Qt---网络编程(半双工通信)

目录 1. 复习 1.1 UDP 与TCP 1.2 IP地址与端口号 2. 前期准备 3. 编程内容 1. 复习 1.1 UDP 与TCP UDP TCP 协议相同点&#xff1a;都存在于传输层 TCP&#xff08;即传输控制协议&#xff09;&#xff1a; 是一种面向连接的传输层协议&#xff0c;它能提供高可靠性通信(即…

牛客网面试必刷:BM18 二维数组中的查找

牛客网面试必刷&#xff1a;BM18 二维数组中的查找 前言一、解法1&#xff1a;逐行使用二分搜索二、解法2&#xff1a;线性搜索&#xff08;推荐&#xff09; 前言 二分查找常见的是在一维数据中进行&#xff0c;在我的上一篇文章中已经有介绍。 一维数组查找&#xff1a;BM1…

STM32F4_DHT11数字温湿度传感器

目录 前言 1. DHT11简介 2. DHT11数据结构 3. DHT11的传输时序 3.1 DHT11开始发送数据流程 3.2 主机复位信号和DHT11响应信号 3.3 数字 “0” 信号表示方法 3.4 数字 “1” 信号表示方法 4. 硬件分析 5. 实验程序详解 5.1 main.c 5.2 DHT11.c 5.3 DHT11.h 前言 DH…

c#笔记-泛型

泛型方法 假设我们要编写一个方法&#xff0c;它可以获取任意类型数组中的最大值&#xff0c; 并返回该值。我们可能会这样写&#xff1a; static int GetMax(int[] array) {Array.Sort(array);return array[array.Length - 1]; }这个方法可以实现我们的需求&#xff0c;但是…

【虚拟机】VMware16保姆级安装教程

大家好&#xff0c;我是雷工&#xff01; 工作中需要用到各种各样的工控软件&#xff0c;有时候甚至需要不同版本的软件&#xff0c;但频繁装卸软件比较麻烦&#xff0c;而且像WinCC和博图软件对系统要求比较严格&#xff0c;卸载重装可能就出问题&#xff0c;此时就不得不重装…

MODBUS通讯程序设计流程图

MODBUS通讯一般来说是基于RS485电平的通讯&#xff0c;RS485是半双工&#xff0c;很多单片机工程师做的通讯不是稳定&#xff0c;主要原因是流程没有掌控好。 我以前也犯过和他们一样错误&#xff0c;觉得很容易。在QQ群里&#xff0c;有位老工程师和大家一样犯了同样的毛病。…

宣布!BNB Greenfield 测试网上的部署

我们很高兴地宣布&#xff0c;4EVERLAND Hosting 现在支持在 BNB Greenfield Testnet 上部署&#xff0c;为开发和托管去中心化应用程序 (dApp) 提供更具可扩展性和安全性的选项。 这种集成使项目团队能够轻松且经济地利用 4EVERLAND 在去中心化托管方面的尖端技术和专业知识&…