实验2——三角形的显示

复制粘贴运行

我只会复制和粘贴,哪里会这个?

可是,这个实验,单纯的CV也不是不行……

 release:

debug:

 在实验1里,release模式下是报错的。

在实验2里,两个模式 的环境,都按实验1的路径设置的,都正常了?

前面,刚把动态lib,静态lib给“为什么不设置DLL也能运行”勉强糊弄过去;

这里,又冒出来个debug和release都用一个lib还都正常?lib不是分debug和release吗?

实际和预期不符,还是个计算机方面的问题

所以,自己用cmake编译一个试试吧……

看看到底如何,虽然已经能跑了

cmake编译lib

照着指导书搞吧,这次不用预编译好的了,尝试自己编译一下

新版的没有32位的啊……

 终于在这里找到一样的版本了

但是不知道得下载多久…… 

下完了,18年的cmake没有VS2019……还报错

 因为这个?

(33条消息) cmake按要求配置好点configure后却一直报:cmake Error in Configuration process,project files maybe invalid_随心Lc的博客-CSDN博客_cmake configure出错

直接换最新的64位cmake吧……

依旧不快

 下完了,这是正常的吗?

编译完了,但是,只有X64的?

 这里说可以直接新建一个X86

 照着搞了,报错,那就64位的吧……

实验指导书,为什么让32位的?不知道。

 lib还分位数?越搞越复杂。

用这个64位的,重配一下环境试试。

还是得x64对x64:

然后,实测,debug和release的lib,混用也正常……玄学。。

如何cmake出32位的项目?

设置这个吗?试试吧……

 win32了?

试验一下:

 重新生成:

 改为x64

 恢复正常

上次那个debug和release通用,是不是因为没有重新生成项目啊?

报错

cmake的总结 

算了吧,反正release也不常用。。

先这样吧,能跑就行。

至少下了一个cmake,然后知道了cmake如何生成32/64位的VS项目

多关注关注代码吧,VAO,VBO什么的。

感觉核心是OpenGL如何画图,这个GLFW充其量是个和窗口有关的外边的壳……

 GLFW初体验-阿里云开发者社区 (aliyun.com)

新陈代谢

上面那个阿里云的链接,里面配完环境,还找了个代码跑了一下

我也跑了一下,报错了

 为什么呢?

昨天下载的时候,好像确实选了一下core。。

这下是真真实实的感受到了什么叫新陈代谢了。

新版OpenGL就这么取代了旧版OpenGL

引用:

(33条消息) glfw+glad配置后glbegin未定义标识符错误_dook33的博客-CSDN博客_未定义标识符gl

代码的解释

计算机图形学_中国大学MOOC(慕课) (icourse163.org)

直接CV,也不大行,还是尝试看一看具体内容吧

核心模式,直接抛弃了之前旧OpenGL的那个模式?VAO,VBO,着色器……

这个慕课,看样子是跳过固定管线,直接从现代的OpenGL开始了……

太强了。不是我这样的小白搞得来的。 能力有限,跟着混一混吧……看看热闹

这个现代流程和我脑子里那个古早的流程完全不是一回事

 OpenGL是个大的状态机,这个状态,又叫作上下文

GLAD是管理函数指针的。图形学+指针=劝退

 《现代》

我只会传统的。

 核心模式,要求用VAO

VAO,我只知道它一次传一大串数据到GPU

就听明白了这一句…… 

 VAO,VBO,是把数据发送到显卡GPU上的。之后,就是用shader,来处理这些数据

  

 

以后用着色器就可以了,所以这些用不到的就删除了 

 

跟着视频又看了一遍代码,加了点注释

/***
 * 例程  绘制三角形 (MAKE后运行时可删除ALL_BUILD,也可以将Task-triangle设为默认启动工程)
 * 步骤:
 * 1-初始化:   GLFW窗口,GLAD。
 * 2-数据处理: 给定顶点数据,生成并绑定VAO&VBO(准备在GPU中进行处理),设置顶点属性指针(本质上就是告诉OpenGL如何处理数据)。
 * 3-着色器:   给出顶点和片段着色器,然后链接为着色器程序,渲染时使用着色器程序。
 * 4-渲染:     清空缓冲,绑定纹理,使用着色器程序,绘制三角形,交换缓冲区检查触发事件后释放资源
 */

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

 // 三角形的顶点数据
const float triangle[] = {
    //     ---- 位置 ----    
         -0.5f, -0.5f, 0.0f,   // 左下
          0.5f, -0.5f, 0.0f,   // 右下
          0.0f,  0.5f, 0.0f    // 正上
};

// 屏幕宽,高
int screen_width = 1280;
int screen_height = 720;

int main() {
    // 第一部分,初始化GLFW和GLAD,分为4个步骤进行/
    
    // 初始化GLFW
    glfwInit();                                                     // 初始化GLFW
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);                  // OpenGL版本为3.3,主次版本号均设为3
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);  // 使用核心模式(无需向后兼容性)
    //glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);            // 如果使用的是Mac OS X系统,需加上这行
    glfwWindowHint(GLFW_RESIZABLE, false);						    // 不可改变窗口大小

    // 创建窗口(宽、高、窗口名称)
    auto window = glfwCreateWindow(screen_width, screen_height, "Triangle", nullptr, nullptr);
    if (window == nullptr) {                                        // 如果窗口创建失败,输出Failed to Create OpenGL Context
        std::cout << "Failed to Create OpenGL Context" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);                                 // 将窗口的上下文设置为当前线程的主上下文

    // 初始化GLAD,加载OpenGL函数指针地址的函数
    // 在调用任何OpenGL函数之前
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // 指定当前视口尺寸(前两个参数为左下角位置,后两个参数是渲染窗口宽、高)
    glViewport(0, 0, screen_width, screen_height);



    // 第二部分,数据处理,通过VAO,VBO,把顶点数据发送到显卡GPU上,并设置属性指针,告诉GPU如何解释这些数据/
    
    // 生成并绑定VAO和VBO
    GLuint vertex_array_object; // == VAO   核心模式要求必须使用VAO
    glGenVertexArrays(1, &vertex_array_object);//先生成
    glBindVertexArray(vertex_array_object);//再绑定

    GLuint vertex_buffer_object; // == VBO
    glGenBuffers(1, &vertex_buffer_object);//先生成
    glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object);//再绑定
    // 将顶点数据绑定至当前默认的缓冲中
    glBufferData(GL_ARRAY_BUFFER, sizeof(triangle), triangle, GL_STATIC_DRAW);

    // 设置顶点属性指针——告知OpenGL,如何解释这些数据
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    // 解绑VAO和VBO    为了代码更规范
    glBindVertexArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // 第三部分,着色器,用来处理GPU里的数据//
    
    // 顶点着色器和片段着色器源码【GLSL】
    const char* vertex_shader_source =
        "#version 330 core\n"
        "layout (location = 0) in vec3 aPos;\n"           // 位置变量的属性位置值为0
        "void main()\n"
        "{\n"
        "    gl_Position = vec4(aPos, 1.0);\n" //内建变量
        "}\n\0";
    const char* fragment_shader_source =
        "#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";

    // 生成并编译着色器
        // 顶点着色器
        int vertex_shader = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vertex_shader, 1, &vertex_shader_source, NULL);
        glCompileShader(vertex_shader);
        int success;
        char info_log[512];
        // 检查着色器是否成功编译,如果编译失败,打印错误信息
        glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &success);
        if (!success)
        {
            glGetShaderInfoLog(vertex_shader, 512, NULL, info_log);
            std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << info_log << std::endl;
        }
        // 片段着色器
        int fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fragment_shader, 1, &fragment_shader_source, NULL);
        glCompileShader(fragment_shader);
        // 检查着色器是否成功编译,如果编译失败,打印错误信息
        glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &success);
        if (!success)
        {
            glGetShaderInfoLog(fragment_shader, 512, NULL, info_log);
            std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << info_log << std::endl;
        }

    // 链接顶点和片段着色器至一个着色器程序
    int shader_program = glCreateProgram();
    glAttachShader(shader_program, vertex_shader);
    glAttachShader(shader_program, fragment_shader);
    glLinkProgram(shader_program);
    // 检查着色器是否成功链接,如果链接失败,打印错误信息
    glGetProgramiv(shader_program, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shader_program, 512, NULL, info_log);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << info_log << std::endl;
    }

    // 删除着色器    需要的着色器程序已经拿到了,这个没用了,就删除了
    glDeleteShader(vertex_shader);
    glDeleteShader(fragment_shader);

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

    // 第4部分,渲染//

    // 渲染循环
    while (!glfwWindowShouldClose(window)) {

        // 清空颜色缓冲
        glClearColor(0.0f, 0.34f, 0.57f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // 使用着色器程序  【之前链接好的那个】
        glUseProgram(shader_program);

        // 绘制三角形
        glBindVertexArray(vertex_array_object);                                    // 绑定VAO
        glDrawArrays(GL_TRIANGLES, 0, 3);                                          // 绘制三角形
        glBindVertexArray(0);                                                      // 解除绑定

        // 交换缓冲并且检查是否有触发事件(比如键盘输入、鼠标移动等)
        glfwSwapBuffers(window);//双缓冲
        glfwPollEvents();
    }

    // 第五部分,善后工作

    // 删除VAO和VBO
    glDeleteVertexArrays(1, &vertex_array_object);
    glDeleteBuffers(1, &vertex_buffer_object);

    // 清理所有的资源并正确退出程序
    glfwTerminate();
    return 0;
}

 五个部分,绘制出了三角形

 挺清晰的,现代OpenGL

VAO和VBO的介绍

百度,启动

网页1

可以理解为VBO就是显存中的一个存储区域,可以保持大量的顶点属性信息。并且可以开辟很多个VBO,每个VBO在OpenGL中有它的唯一标识ID,这个ID对应着具体的VBO的显存地址,通过这个ID可以对特定的VBO内的数据进行存取操作。

【ID,显存地址,指针?有计算机内味儿了】

还有个图,这个图画的挺漂亮的,虽然看不懂。

只能看明白这里面有一堆指针。

引用:

(33条消息) openGL关于VAO和VBO和EBO的区分和理解_在路上的菜鸡的博客-CSDN博客_vao vbo

网页2 

这个里面有代码的解释:

能看明白的一共是这么几句解释VBO的代码:

//声明一个int作为VBO对象的id
unsigned int VBO;
//生成一个Buffer对象,id为VBO
glGenBuffers(1, &VBO);
//将缓冲对象绑定到顶点缓冲对象,相当于指定了这个缓冲对象是啥类型的
glBindBuffer(GL_ARRAY_BUFFER, VBO); 
//传数据给VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//设置如何解释这些数据
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
//开启顶点属性
glEnableVertexAttribArray(0);

具体解释如下:

VBO也就是一个OpenGL里的结构体,它用来存一些顶点数据方便一次性给GPU内存多发一点。

首先,按照惯例,先生成一个缓冲对象(注意这里还不是顶点缓冲对象哦),得到它的id:

unsigned int VBO;//声明一个int作为VBO对象的id
glGenBuffers(1, &VBO);//生成一个Buffer对象,id为VBO

这一步就是将缓冲对象绑定到顶点缓冲对象,相当于就是指定了这个缓冲对象是啥类型的

glBindBuffer(GL_ARRAY_BUFFER, VBO);  

id,,和上个链接好像呼应上了

现在,我们任何在GL_ARRAY_BUFFER上的缓冲调用都是对VBO这个对象起作用的。这里有点难理解吧?我再来举例子。

C语言总学过吧
我们声明一个变量是用 int a;
给他初始化后就能用 addOne(a)各种函数来调用了。

OpenGL里呢,没有那么方便,因为他的函数很多是直接调用“类型”的,就这样的:
addOne(int),你传一个a进去,不认得啊。
所以呢要首先bind(int,a)把a跟int这个类型绑定,告诉它现在int就是专指a了!然后addOne(int)这个函数呢,就知道是给a加一了。简单理解,这个例子里int就是GL_ARRAY_BUFFER,a就是VBO。

下一步,是这句:

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

把我们之前的那个一维数组,传给了GL_ARRAY_BUFFER对象,注意它已经被我们的VBO给绑定了哦,所以就传给了VBO对象。

这个函数的介绍如下:

  1. 它的第一个参数是目标缓冲的类型:顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。
  2. 第二个参数指定传输数据的大小(以字节为单位);用一个简单的sizeof计算出顶点数据大小就行。
  3. 第三个参数是我们希望发送的实际数据。
  4. 第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式:
  • GL_STATIC_DRAW :数据不会或几乎不会改变。
  • GL_DYNAMIC_DRAW:数据会被改变很多。
  • GL_STREAM_DRAW :数据每次绘制时都会改变。

说那么多,其实就一句话,把一个一维数组,给了一个结构体来存储。

然后:一个简简单单的一维数组,咋让顶点着色器知道这代表了三个顶点呢?用这两句:

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

 第一行是告诉OpenGL如何解释数据,第二行是启用顶点属性。
简单来说就是一维数组是如何变成三维点的。注意这里一个点的信息只有位置坐标,之后还会有颜色、贴图等信息,这样就要修改一个单位的大小和单位之间的偏移量了。

引用自:

(33条消息) 是人都能懂,通俗解释VBO和VAO在OpenGL中的作用_kiss_the_rain86的博客-CSDN博客_opengl vbo

网页3 

 这个里面有完整点的代码

VBOBBuffer之意,用来存储顶点数据;VAOAArray,但我认为理解为 Attribute(属性) 之意更好,意思是 Buffer(VBO)的属性

即,我们用VBO来存储数据,而用VAO来告诉计算机这些数据分别有什么属性、起什么作用。

《自动》

 VAO如何解释数据?这么解释:

当数据来自同一数组时:

float buffer = {
   	//顶点坐标(3个一组)              //顶点颜色(3个一组)              //纹理坐标(2个一组)
    0.5f,  0.5f, 0.0f,                1.0f, 0.0f, 0.0f,               1.0f, 1.0f,
    0.5f, -0.5f, 0.0f,                0.0f, 1.0f, 0.0f,               1.0f, 0.0f,
   -0.5f, -0.5f, 0.0f,                0.0f, 0.0f, 1.0f,               0.0f, 0.0f,
   -0.5f,  0.5f, 0.0f,                1.0f, 1.0f, 0.0f,               0.0f, 1.0f
};


//把对VBO中数据的描述存到了一个VAO中
//在一些复杂的OpenGL程序中,VBO可能会有多个,但VAO只有一个

//vertex coord
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

// color attribute
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

// texture coord attribute
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);

当数据来自不同的数组时: 

 

一个VAO来解释多个VBO的操作流程就如下 

首先,绑定VAO,以告知OpenGL程序该使用这个VAO来对VBO做出解释。

然后,绑定第一个VBO,向这个VBO中写入数据,告知VAO该如何解释这个VBO的信息;
然后,解绑这个VBO。

然后,绑定第二个VBO,向这个VBO中写入数据,并在VAO中保存该如何解释这个VBO的信息;
然后,解绑这个VBO。
……
	unsigned int VBO[2], VAO;
	glGenVertexArrays(1, &VAO);
	glGenBuffers(2, VBO);

//=========================绑定VAO===============================
	glBindVertexArray(VAO); 
//===============================================================

//=======================绑定第一个VBO============================
	glBindBuffer(GL_ARRAY_BUFFER, VBO[0]);
//===============================================================
	 
	glBufferData(GL_ARRAY_BUFFER, sphereVertices.size() * sizeof(float), &sphereVertices[0], GL_STATIC_DRAW); //向第一个VBO中写入数据

//================告知VAO该如何解释第一个VBO的信息=================
	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(0);
//===============================================================

//=======================解绑第一个VBO===========================
	glBindBuffer(GL_ARRAY_BUFFER, 0);
//===============================================================

//=======================绑定第二个VBO============================
	glBindBuffer(GL_ARRAY_BUFFER, VBO[1]);
//===============================================================

	glBufferData(GL_ARRAY_BUFFER, sizeof(texVertrices), texVertrices, GL_STATIC_DRAW);//向第二个VBO中写入数据

//================告知VAO该如何解释第二个VBO的信息=================
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);
	glEnableVertexAttribArray(1);
//=============================================================

引用:

(33条消息) 【图像】【OpenGL】VAO和VBO的关系_西西敏的博客-CSDN博客_vao vbo

网页4 

这个可能偏应用一点: 

一个enable函数的解释

引用:

(33条消息) OpenGL VAO VBO EBO(IBO)的绑定、解绑问题_csu_xiji的博客-CSDN博客

看来,这个VAO+VBO里的VAO,和之前理解的用来加快传数据速度的VAO,好像不大一样。 

一个VAO,两个VBO,两个三角形

 实际运用吧这应该算是

初始

一开始,根据猜测,改写成了这样:

代码:

/***
 * 例程  绘制三角形 (MAKE后运行时可删除ALL_BUILD,也可以将Task-triangle设为默认启动工程)
 * 步骤:
 * 1-初始化:   GLFW窗口,GLAD。
 * 2-数据处理: 给定顶点数据,生成并绑定VAO&VBO(准备在GPU中进行处理),设置顶点属性指针(本质上就是告诉OpenGL如何处理数据)。
 * 3-着色器:   给出顶点和片段着色器,然后链接为着色器程序,渲染时使用着色器程序。
 * 4-渲染:     清空缓冲,绑定纹理,使用着色器程序,绘制三角形,交换缓冲区检查触发事件后释放资源
 */

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

 // 三角形的顶点数据
const float triangle[] = {
    //     ---- 位置 ----    
         -0.5f, -0.5f, 0.0f,   // 左下
          0.5f, -0.5f, 0.0f,   // 右下
          0.0f,  0.5f, 0.0f    // 正上
};

const float triangle2[] = {
    //     ---- 位置 ----    
         0.6f, -0.5f, 0.0f,   // 左下
          0.8f, -0.5f, 0.0f,   // 右下
          0.7f,  0.5f, 0.0f    // 正上
};

// 屏幕宽,高
int screen_width = 1280;
int screen_height = 720;

int main() {
    // 第一部分,初始化GLFW和GLAD,分为4个步骤进行/

    // 初始化GLFW
    glfwInit();                                                     // 初始化GLFW
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);                  // OpenGL版本为3.3,主次版本号均设为3
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);  // 使用核心模式(无需向后兼容性)
    //glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);            // 如果使用的是Mac OS X系统,需加上这行
    glfwWindowHint(GLFW_RESIZABLE, false);						    // 不可改变窗口大小

    // 创建窗口(宽、高、窗口名称)
    auto window = glfwCreateWindow(screen_width, screen_height, "Triangle", nullptr, nullptr);
    if (window == nullptr) {                                        // 如果窗口创建失败,输出Failed to Create OpenGL Context
        std::cout << "Failed to Create OpenGL Context" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);                                 // 将窗口的上下文设置为当前线程的主上下文

    // 初始化GLAD,加载OpenGL函数指针地址的函数
    // 在调用任何OpenGL函数之前
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // 指定当前视口尺寸(前两个参数为左下角位置,后两个参数是渲染窗口宽、高)
    glViewport(0, 0, screen_width, screen_height);



    // 第二部分,数据处理,通过VAO,VBO,把顶点数据发送到显卡GPU上,并设置属性指针,告诉GPU如何解释这些数据/

    // 生成并绑定VAO和VBO
    GLuint vertex_array_object; // == VAO   核心模式要求必须使用VAO
    glGenVertexArrays(1, &vertex_array_object);//先生成
    glBindVertexArray(vertex_array_object);//再绑定

    GLuint vertex_buffer_object[2]; // == VBO
    glGenBuffers(2, &vertex_buffer_object[0]);//先生成

    glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object[0]);//再绑定
    glBufferData(GL_ARRAY_BUFFER, sizeof(triangle), triangle, GL_STATIC_DRAW);    // 将顶点数据绑定至当前默认的缓冲中
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);    // 设置顶点属性指针——告知OpenGL,如何解释这些数据
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, 0);

    glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object[1]);//再绑定
    glBufferData(GL_ARRAY_BUFFER, sizeof(triangle2), triangle2, GL_STATIC_DRAW);    // 将顶点数据绑定至当前默认的缓冲中
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);    // 设置顶点属性指针——告知OpenGL,如何解释这些数据
    glEnableVertexAttribArray(1);

    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // 解绑VAO和VBO    为了代码更规范
    glBindVertexArray(0);


    // 第三部分,着色器,用来处理GPU里的数据//

    // 顶点着色器和片段着色器源码【GLSL】
    const char* vertex_shader_source =
        "#version 330 core\n"
        "layout (location = 0) in vec3 aPos;\n"           // 位置变量的属性位置值为0
        "void main()\n"
        "{\n"
        "    gl_Position = vec4(aPos, 1.0);\n" //内建变量
        "}\n\0";
    const char* fragment_shader_source =
        "#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";

    // 生成并编译着色器
        // 顶点着色器
    int vertex_shader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertex_shader, 1, &vertex_shader_source, NULL);
    glCompileShader(vertex_shader);
    int success;
    char info_log[512];
    // 检查着色器是否成功编译,如果编译失败,打印错误信息
    glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(vertex_shader, 512, NULL, info_log);
        std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << info_log << std::endl;
    }
    // 片段着色器
    int fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragment_shader, 1, &fragment_shader_source, NULL);
    glCompileShader(fragment_shader);
    // 检查着色器是否成功编译,如果编译失败,打印错误信息
    glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(fragment_shader, 512, NULL, info_log);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << info_log << std::endl;
    }

    // 链接顶点和片段着色器至一个着色器程序
    int shader_program = glCreateProgram();
    glAttachShader(shader_program, vertex_shader);
    glAttachShader(shader_program, fragment_shader);
    glLinkProgram(shader_program);
    // 检查着色器是否成功链接,如果链接失败,打印错误信息
    glGetProgramiv(shader_program, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shader_program, 512, NULL, info_log);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << info_log << std::endl;
    }

    // 删除着色器    需要的着色器程序已经拿到了,这个没用了,就删除了
    glDeleteShader(vertex_shader);
    glDeleteShader(fragment_shader);

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

    // 第4部分,渲染//

    // 渲染循环
    while (!glfwWindowShouldClose(window)) {

        // 清空颜色缓冲
        glClearColor(0.0f, 0.34f, 0.57f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // 使用着色器程序  【之前链接好的那个】
        glUseProgram(shader_program);

        // 绘制三角形
        glBindVertexArray(vertex_array_object);                                    // 绑定VAO


        glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object[0]);

        glDrawArrays(GL_TRIANGLES, 0, 3);                                          // 绘制三角形

        glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object[1]);

        glDrawArrays(GL_TRIANGLES, 0, 3);                                          // 绘制三角形


        glBindVertexArray(0);                                                      // 解除绑定

        // 交换缓冲并且检查是否有触发事件(比如键盘输入、鼠标移动等)
        glfwSwapBuffers(window);//双缓冲
        glfwPollEvents();
    }

    // 第五部分,善后工作

    // 删除VAO和VBO
    glDeleteVertexArrays(1, &vertex_array_object);
    glDeleteBuffers(1, &vertex_buffer_object[0]);

    // 清理所有的资源并正确退出程序
    glfwTerminate();
    return 0;
}

 运行结果,明显不对……

不对就改嘛,实践——认识——再实践——。。。

小白,所以,百度,启动

修改1

《这个问题和我遇到的差不多》《也使用属性0》

想起了上面的某个图

又想起了代码里的某一行 

 好像能呼应上似的

引用:

渲染一个包含两个 VBO 的 VAO | (1r1g.com)

修改2

这个说是,渲染绘制之前,需要设置一下这个指针

引用:

你如何渲染多个VBO / IBO? - 码客 (oomake.com)

进行修改 

然后,就修改了 这两部分,死马当活马医而已,因为我是小白,只会百度

    // 第二部分,数据处理,通过VAO,VBO,把顶点数据发送到显卡GPU上,并设置属性指针,告诉GPU如何解释这些数据/

    // 生成并绑定VAO和VBO
    GLuint vertex_array_object; // == VAO   核心模式要求必须使用VAO
    glGenVertexArrays(1, &vertex_array_object);//先生成
    glBindVertexArray(vertex_array_object);//再绑定

    GLuint vertex_buffer_object[2]; // == VBO
    glGenBuffers(2, &vertex_buffer_object[0]);//先生成

    glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object[0]);//再绑定
    glBufferData(GL_ARRAY_BUFFER, sizeof(triangle), triangle, GL_STATIC_DRAW);    // 将顶点数据绑定至当前默认的缓冲中
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);    // 设置顶点属性指针——告知OpenGL,如何解释这些数据
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, 0);

    glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object[1]);//再绑定
    glBufferData(GL_ARRAY_BUFFER, sizeof(triangle2), triangle2, GL_STATIC_DRAW);    // 将顶点数据绑定至当前默认的缓冲中
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);    // 设置顶点属性指针——告知OpenGL,如何解释这些数据
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // 解绑VAO和VBO    为了代码更规范
    glBindVertexArray(0);

    // 渲染循环
    while (!glfwWindowShouldClose(window)) {

        // 清空颜色缓冲
        glClearColor(0.0f, 0.34f, 0.57f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);
        // 使用着色器程序  【之前链接好的那个】
        glUseProgram(shader_program);

        // 绘制三角形
        glBindVertexArray(vertex_array_object);                                    // 绑定VAO


        glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object[0]);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);    // 设置指针位置,以便后续从此处读取数据进行渲染
        glDrawArrays(GL_TRIANGLES, 0, 3);                                          // 绘制三角形

        glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object[1]);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);    // 设置指针位置,以便后续从此处读取数据进行渲染
        glDrawArrays(GL_TRIANGLES, 0, 3);                                          // 绘制三角形


        glBindVertexArray(0);                                                      // 解除绑定

        // 交换缓冲并且检查是否有触发事件(比如键盘输入、鼠标移动等)
        glfwSwapBuffers(window);//双缓冲
        glfwPollEvents();
    }

修改后 

总的代码:

/***
 * 例程  绘制三角形 (MAKE后运行时可删除ALL_BUILD,也可以将Task-triangle设为默认启动工程)
 * 步骤:
 * 1-初始化:   GLFW窗口,GLAD。
 * 2-数据处理: 给定顶点数据,生成并绑定VAO&VBO(准备在GPU中进行处理),设置顶点属性指针(本质上就是告诉OpenGL如何处理数据)。
 * 3-着色器:   给出顶点和片段着色器,然后链接为着色器程序,渲染时使用着色器程序。
 * 4-渲染:     清空缓冲,绑定纹理,使用着色器程序,绘制三角形,交换缓冲区检查触发事件后释放资源
 */

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

 // 三角形的顶点数据
const float triangle[] = {
    //     ---- 位置 ----    
         -0.5f, -0.5f, 0.0f,   // 左下
          0.5f, -0.5f, 0.0f,   // 右下
          0.0f,  0.5f, 0.0f    // 正上
};

const float triangle2[] = {
    //     ---- 位置 ----    
         0.6f, -0.5f, 0.0f,   // 左下
          0.8f, -0.5f, 0.0f,   // 右下
          0.7f,  0.5f, 0.0f    // 正上
};

// 屏幕宽,高
int screen_width = 1280;
int screen_height = 720;

int main() {
    // 第一部分,初始化GLFW和GLAD,分为4个步骤进行/

    // 初始化GLFW
    glfwInit();                                                     // 初始化GLFW
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);                  // OpenGL版本为3.3,主次版本号均设为3
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);  // 使用核心模式(无需向后兼容性)
    //glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);            // 如果使用的是Mac OS X系统,需加上这行
    glfwWindowHint(GLFW_RESIZABLE, false);						    // 不可改变窗口大小

    // 创建窗口(宽、高、窗口名称)
    auto window = glfwCreateWindow(screen_width, screen_height, "Triangle", nullptr, nullptr);
    if (window == nullptr) {                                        // 如果窗口创建失败,输出Failed to Create OpenGL Context
        std::cout << "Failed to Create OpenGL Context" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);                                 // 将窗口的上下文设置为当前线程的主上下文

    // 初始化GLAD,加载OpenGL函数指针地址的函数
    // 在调用任何OpenGL函数之前
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // 指定当前视口尺寸(前两个参数为左下角位置,后两个参数是渲染窗口宽、高)
    glViewport(0, 0, screen_width, screen_height);



    // 第二部分,数据处理,通过VAO,VBO,把顶点数据发送到显卡GPU上,并设置属性指针,告诉GPU如何解释这些数据/

    // 生成并绑定VAO和VBO
    GLuint vertex_array_object; // == VAO   核心模式要求必须使用VAO
    glGenVertexArrays(1, &vertex_array_object);//先生成
    glBindVertexArray(vertex_array_object);//再绑定

    GLuint vertex_buffer_object[2]; // == VBO
    glGenBuffers(2, &vertex_buffer_object[0]);//先生成

    glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object[0]);//再绑定
    glBufferData(GL_ARRAY_BUFFER, sizeof(triangle), triangle, GL_STATIC_DRAW);    // 将顶点数据绑定至当前默认的缓冲中
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);    // 设置顶点属性指针——告知OpenGL,如何解释这些数据
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, 0);

    glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object[1]);//再绑定
    glBufferData(GL_ARRAY_BUFFER, sizeof(triangle2), triangle2, GL_STATIC_DRAW);    // 将顶点数据绑定至当前默认的缓冲中
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);    // 设置顶点属性指针——告知OpenGL,如何解释这些数据
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // 解绑VAO和VBO    为了代码更规范
    glBindVertexArray(0);


    // 第三部分,着色器,用来处理GPU里的数据//

    // 顶点着色器和片段着色器源码【GLSL】
    const char* vertex_shader_source =
        "#version 330 core\n"
        "layout (location = 0) in vec3 aPos;\n"           // 位置变量的属性位置值为0
        "void main()\n"
        "{\n"
        "    gl_Position = vec4(aPos, 1.0);\n" //内建变量
        "}\n\0";
    const char* fragment_shader_source =
        "#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";

    // 生成并编译着色器
        // 顶点着色器
    int vertex_shader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertex_shader, 1, &vertex_shader_source, NULL);
    glCompileShader(vertex_shader);
    int success;
    char info_log[512];
    // 检查着色器是否成功编译,如果编译失败,打印错误信息
    glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(vertex_shader, 512, NULL, info_log);
        std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << info_log << std::endl;
    }
    // 片段着色器
    int fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragment_shader, 1, &fragment_shader_source, NULL);
    glCompileShader(fragment_shader);
    // 检查着色器是否成功编译,如果编译失败,打印错误信息
    glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(fragment_shader, 512, NULL, info_log);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << info_log << std::endl;
    }

    // 链接顶点和片段着色器至一个着色器程序
    int shader_program = glCreateProgram();
    glAttachShader(shader_program, vertex_shader);
    glAttachShader(shader_program, fragment_shader);
    glLinkProgram(shader_program);
    // 检查着色器是否成功链接,如果链接失败,打印错误信息
    glGetProgramiv(shader_program, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shader_program, 512, NULL, info_log);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << info_log << std::endl;
    }

    // 删除着色器    需要的着色器程序已经拿到了,这个没用了,就删除了
    glDeleteShader(vertex_shader);
    glDeleteShader(fragment_shader);

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

    // 第4部分,渲染//

    // 渲染循环
    while (!glfwWindowShouldClose(window)) {

        // 清空颜色缓冲
        glClearColor(0.0f, 0.34f, 0.57f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);
        // 使用着色器程序  【之前链接好的那个】
        glUseProgram(shader_program);

        // 绘制三角形
        glBindVertexArray(vertex_array_object);                                    // 绑定VAO


        glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object[0]);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);    // 设置指针位置,以便后续从此处读取数据进行渲染
        glDrawArrays(GL_TRIANGLES, 0, 3);                                          // 绘制三角形

        glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object[1]);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);    // 设置指针位置,以便后续从此处读取数据进行渲染
        glDrawArrays(GL_TRIANGLES, 0, 3);                                          // 绘制三角形


        glBindVertexArray(0);                                                      // 解除绑定

        // 交换缓冲并且检查是否有触发事件(比如键盘输入、鼠标移动等)
        glfwSwapBuffers(window);//双缓冲
        glfwPollEvents();
    }

    // 第五部分,善后工作

    // 删除VAO和VBO
    glDeleteVertexArrays(1, &vertex_array_object);
    glDeleteBuffers(1, &vertex_buffer_object[0]);

    // 清理所有的资源并正确退出程序
    glfwTerminate();
    return 0;
}

运行结果 

结果,符合预期

代码,是正经的写法吗?不知道……小白总是一问三不知,我就是小白,所以……先这样吧…… 

一点函数解释

名称

glBindBuffer- 绑定一个命名(ID)的缓冲区对象

C规范

void glBindBuffer(GLenum target,GLuint buffer);

参数

target

指定缓冲区对象绑定的目标。

 符号常量必须为GL_ARRAY_BUFFER或GL_ELEMENT_ARRAY_BUFFER。【只能二选一?】

buffer

指定缓冲区对象的名称(ID)。

描述

glBindBuffer允许您创建或使用命名缓冲区对象。 调用glBindBuffer,目标设置为GL_ARRAY_BUFFERGL_ELEMENT_ARRAY_BUFFER,缓冲区设置为新缓冲区对象的名称,这样就将缓冲区对象名称绑定到目标了。 当缓冲区对象绑定到目标时,该目标的先前绑定将自动中断。

缓冲区对象名称是无符号整数。 0值保留,但每个缓冲区对象目标没有默认缓冲区对象。 相反,缓冲区设置为0可以有效地取消绑定先前绑定的任何缓冲区对象,并恢复该缓冲区对象目标的客户机内存使用情况。 缓冲区对象名称和相应的缓冲区对象内容对于当前GL渲染上下文的共享对象空间是本地的。

引用:

(31条消息) GLES2.0中文API-glBindBuffer_flycatdeng的博客-CSDN博客_glbindbuffer

GLuint vbo;
glGenBuffers(1,&vbo);
GLuint vbo[3];
glGenBuffers(3,vbo);

glGenBuffers()函数仅仅是生成一个缓冲对象的名称,这个缓冲对象并不具备任何意义,它仅仅是个缓冲对象,还不是一个顶点数组缓冲,它类似于C语言中的一个指针变量【这个类比挺好的】,我们可以分配内存对象并且用它的名称来引用这个内存对象。OpenGL有很多缓冲对象类型,那么这个缓冲对象到底是什么类型,就要用到下面的glBindBuffer()函数了。

glBindBuffer(GL_ARRAY_BUFFER, VBO);  //VBO变成了一个顶点缓冲类型
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

第一个就是缓冲对象的类型,第二个参数就是要绑定的缓冲对象的名称,也就是我们在上一个函数里生成的名称,使用该函数将缓冲对象绑定到OpenGL上下文环境中以便使用。如果把target绑定到一个已经创建好的缓冲对象,那么这个缓冲对象将为当前target的激活对象;但是如果绑定的buffer值为0,那么OpenGL将不再对当前target使用任何缓存对象。

OpenGL红宝书中给出了一个恰当的比喻:绑定对象的过程就像设置铁路的道岔开关,每一个缓冲类型中的各个对象就像不同的轨道一样,我们将开关设置为其中一个状态,那么之后的列车都会驶入这条轨道。

  • 我要把数据存入顶点缓冲区,但是顶点缓冲区可以有很多缓冲对象,我需要传入哪个呢,于是我就要提前绑定一个,之后,我只要向顶点缓冲区内传入数据,这个数据就会自动进入被绑定的那个对象里面。
  • 之后调用glBufferData()传输所需数据,其中第一个参数就是要制定缓冲类型,根据这个类型锁定当前唯一的目标缓冲。

引用:

(31条消息) glGenBuffers与glBindBuffer理解_莫之的博客-CSDN博客_glgenbuffers

《渲染时》

glVertexAttribPointer_百度百科 (baidu.com)

 试了试,这么搞也能画出来图


    // 第二部分,数据处理,通过VAO,VBO,把顶点数据发送到显卡GPU上,并设置属性指针,告诉GPU如何解释这些数据/

    // 生成并绑定VAO和VBO
    GLuint vertex_array_object; // == VAO   核心模式要求必须使用VAO
    glGenVertexArrays(1, &vertex_array_object);//先生成
    glBindVertexArray(vertex_array_object);//再绑定

    GLuint vertex_buffer_object[2]; // == VBO
    glGenBuffers(2, &vertex_buffer_object[0]);//先生成,相当于C里声明的指针

    glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object[0]);//再绑定, 相当于声明指针指向的变量类型,同时设置一下状态机的状态(火车轨道变轨,变完了后面的车都得按变了的轨道走)
    glBufferData(GL_ARRAY_BUFFER, sizeof(triangle), triangle, GL_STATIC_DRAW);    // 将顶点数据绑定至当前默认的缓冲中
    //glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);    // 设置顶点属性指针——告知OpenGL,如何解释这些数据
    //glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, 0);

    glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object[1]);//再绑定
    glBufferData(GL_ARRAY_BUFFER, sizeof(triangle2), triangle2, GL_STATIC_DRAW);    // 将顶点数据绑定至当前默认的缓冲中
    //glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);    // 设置顶点属性指针——告知OpenGL,如何解释这些数据
    //glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // 解绑VAO和VBO    为了代码更规范
    glBindVertexArray(0);

    // 渲染循环
    while (!glfwWindowShouldClose(window)) {

        // 清空颜色缓冲
        glClearColor(0.0f, 0.34f, 0.57f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);
        // 使用着色器程序  【之前链接好的那个】
        glUseProgram(shader_program);

        // 绘制三角形
        glBindVertexArray(vertex_array_object);                                    // 绑定VAO

        glEnableVertexAttribArray(0);
        glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object[0]);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);    // 【渲染时】设置指针位置,以便后续从此处读取数据进行渲染
        glDrawArrays(GL_TRIANGLES, 0, 3);                                          // 绘制三角形

        glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_object[1]);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);    // 设置指针位置,以便后续从此处读取数据进行渲染
        glDrawArrays(GL_TRIANGLES, 0, 3);                                          // 绘制三角形


        glBindVertexArray(0);                                                      // 解除绑定

        // 交换缓冲并且检查是否有触发事件(比如键盘输入、鼠标移动等)
        glfwSwapBuffers(window);//双缓冲
        glfwPollEvents();
    }

 

 

后记 

状态机 ,铁路岔道开关,所以最早的时候,只渲染不设指针的时候,指针还停留在向VBO里传数据时设置的地址,所以只画出了一个。

能自圆其说,对不对?不知道。小白一个,要求不能太高。

红宝书……好像是个字典。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值