链接顶点属性
顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还的确意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。
我们的顶点缓冲数据会被解析为下面这样子:
- l 位置数据被储存为32位(4字节)浮点值。
- l 每个位置包含3个这样的值。
- l 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(TightlyPacked)。
- l 数据中第一个值在缓冲开始的位置。
有了这些信息我们就可以使用glVertexAttribPointer()函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)了:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0);
glVertexAttribPointer()函数的参数如下:
- 第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为0。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入0。
- 第二个参数指定顶点属性的大小。顶点属性是一个vec3,它由3个值组成,所以大小是3。
- 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。
- 第四个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。
- 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个float之后,我们把步长设置为3 * sizeof(float)。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。
- 第六个参数的类型是void*,所以需要我们进行强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。
补充:每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用glVetexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。由于在调用glVetexAttribPointer之前绑定的是先前定义的VBO对象,顶点属性0现在会链接到它的顶点数据。
现在已经定义了OpenGL该如何解释顶点数据,我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。自此,所有东西都已经设置好了:我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中,建立了一个顶点和一个片段着色器,并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。在OpenGL中绘制一个物体,代码会像是这样:
// 0. 复制顶点数组到缓冲中供OpenGL使用 glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 1. 设置顶点属性指针 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // 2. 当我们渲染一个物体时要使用着色器程序 glUseProgram(shaderProgram); // 3. 绘制物体 …
每当我们绘制一个物体的时候都必须重复这一过程,当数据量比较大的时候,我们就需要使用到顶点数组对象了。
顶点数组对象
顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常发明糊弄阿曼,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中(OpenGL的核心模式要求我们使用VAO,所以它知道该如何处理我们的顶点输入。如果我们绑定VAO失败,OpenGL会拒绝绘制任何东西)。
一个顶点数组对象会储存以下这些内容:
- l glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
- l 通过glVertexAttribPointer设置的顶点属性配置。
- l 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。
创建一个VAO和创建一个VBO很类似:
unsigned int VAO; glGenVertexArrays(1, &VAO);
要想使用VAO,要做的只是使用glBindVertexArray绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。类似代码如下:
// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: .. // 1. 绑定VAO glBindVertexArray(VAO); // 2. 把顶点数组复制到缓冲中供OpenGL使用 glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 3. 设置顶点属性指针 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); [...] // ..:: 绘制代码(渲染循环中) :: .. // 4. 绘制物体 glUseProgram(shaderProgram); glBindVertexArray(VAO); …
到此,过程就这么多了!前面做的一切都是等待绘制那一刻,一个储存了我们顶点属性配置和应使用的VBO的顶点数组对象。一般当你打算绘制多个物体时,你首先要生成/配置所有的VAO(和必须的VBO及属性指针),然后储存它们供后面使用。当我们打算绘制物体的时候就拿出相应的VAO,绑定它(VAO),绘制完物体后,再解绑它(VAO)。
绘制三角形
绘制我们想要的物体,OpenGL给我们提供了glDrawArrays()函数,它使用当前激活的着色器,之前定义的顶点属性配置,和VBO的顶点数据(通过VAO间接绑定)来绘制图元。
glUseProgram(shaderProgram); glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3);
glDrawArrays()函数:
- 第一个参数是我们打算绘制的OpenGL图元的类型。由于我们在一开始时说过,我们希望绘制的是一个三角形,这里传递GL_TRIANGLES给它。
- 第二个参数指定了顶点数组的起始索引,我们这里填0。
- 第三个参数(最后一个)指定我们打算绘制多少个顶点,这里是3(我们只从我们的数据中渲染一个三角形,它只有3个顶点长)。
Demo
下载地址:http://download.csdn.net/download/qq21497936/10171739
程序源码
在《OpenGL学习笔记(七):创建第一个Qt5.9.3 OpenGL工程模版(与平台无关)》上增加了三段代码,个人认为非常好解释了VAO,VBO,SHADERPROGRAM的使用,可搜索网页内搜索“共三段”查看。
《OpenGL学习笔记(七):创建第一个Qt5.9.3 OpenGL工程模版(与平台无关)》:http://blog.csdn.net/qq21497936/article/details/78884842
#include <iostream> #include <process.h> #include <GLFW/glfw3.h> #include <glad/glad.h> void framebuffer_size_callback(GLFWwindow* window, int width, int height) { glViewport(0, 0, width, height); } // 在GLFW中实现一些输入控制,使用GLFW的glfwGetKey函数 void processInput(GLFWwindow *window) { if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true); } int main() { glfwInit(); /* 请确认您的系统支持OpenGL3.3或更高版本,否则此应用有可能会 崩溃或者出现不可预知的错误。如果想要查看OpenGL版本的话, 在Linux上运行glxinfo,或者在Windows上使用其它的工具(例如 OpenGL Extension Viewer)。如果你的OpenGL版本低于3.3,检 查一下显卡是否支持OpenGL 3.3+(不支持的话你的显卡真的太老 了),并更新你的驱动程序,有必要的话请更新显卡。 */ glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 如果使用的是Mac OS X系统,你还需要加下面这行代码到你的 // 初始化代码中这些配置才能起作用 //glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // 创建一个窗口对象,这个窗口对象存放了所有和窗口相关的数据, // 而且会被GLFW的其他函数频繁地用到 // glfwCreateWindow函数需要窗口的宽和高作为它的前两个参数。 // 第三个参数表示这个窗口的名称(标题),最后两个参数暂时忽略。 // 函数会返回一个GLFWwindow对象,我们会在其它的GLFW操作中使用到。 GLFWwindow* window = glfwCreateWindow(800, 600, "QQ:21497936", NULL, NULL); if (window == NULL) { std::cout << "Failed to create GLFW window" << std::endl; glfwTerminate(); return -1; } glfwMakeContextCurrent(window); // GLAD是用来管理OpenGL的函数指针的,所以在调用任何OpenGL的函数 // 之前我们需要初始化GLAD。 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { std::cout << "Failed to initialize GLAD" << std::endl; return -1; } // 还需要注册这个函数,告诉GLFW我们希望每当窗口调整大小的时候调用这个函数。 glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); // 我们必须告诉OpenGL渲染窗口的尺寸大小,即视口(Viewport), // 这样OpenGL才只能知道怎样根据窗口大小显示数据和坐标。 // 可以通过调用glViewport函数来设置窗口的维度(Dimension)。 // OpenGL幕后使用glViewport中定义的位置和宽高进行2D坐标的转 // 换,将OpenGL中的位置坐标转换为你的屏幕坐标。例如,OpenGL // 中的坐标(-0.5, 0.5)有可能(最终)被映射为屏幕中的坐标(200,450)。 // 注意,处理过的OpenGL坐标范围只为-1到1,因此我们事实上 // 将(-1到1)范围内的坐标映射到(0, 800)和(0, 600)。 glViewport(0, 0, 600, 600); // 在我们主动关闭它之前不断绘制图像并能够接受用户输入。因此, // 需要在程序中添加一个while循环,我们可以把它称之为渲染循环(Render Loop), // 它能在我们让GLFW退出前一直保持运行。 /* 新增代码 第一段/共三段----------------------------------START */ // 1.顶点着色器 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" "}"; // 2.片段着色器 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" "}"; // 3.创建顶点着色器并编译 int success; char infoLog[512]; int vertexShader = glCreateShader(GL_VERTEX_SHADER); glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); glCompileShader(vertexShader); glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); if(!success) { glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; } // 4.创建片段着色器并编译 int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader); glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); if(!success) { glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl; } // 5.连接着色器 int shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success); if(!success) { glGetShaderInfoLog(shaderProgram, 512, NULL, infoLog); std::cout << "ERROR::SHADER::PROGRAM::COMPILATION_FAILED\n" << infoLog << std::endl; } // 6.删除已经使用的着色器(因为后面不再需要绑定了) glDeleteShader(vertexShader); glDeleteShader(fragmentShader); // 7.顶点数据 float vertices[] = { -0.5f, 0.0f, 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 0.866f, 0.0f, }; // 8.使用VAO和VBO unsigned int VBO, VAO; // glGenVertexArrays() 创建一个顶点数组对象 // 第一个参数:需要创建的缓存数量 // 第二个参数:存储单一ID或多个ID的GLuint变量或数组的地址。 glGenVertexArrays(1, &VAO); // glGenBuffers() 创建一个缓存对象并且返回缓存对象的标示符。 glGenBuffers(1, &VBO); // 顶点对象创建之后,在使用缓存对象之前,需要将缓存对象连接到相应的缓存上。 // glBindBuffer()有1个参数:buffer。(绑定和解绑的顺序很重要,勿更改) glBindVertexArray(VAO); // 缓存对象创建之后,在使用缓存对象之前,需要将缓存对象连接到相应的缓存上。 // glBindBuffer()有2个参数:target与buffer。(绑定和解绑的顺序很重要,勿更改) glBindBuffer(GL_ARRAY_BUFFER, VBO); // 当缓存初始化之后,使用glBufferData()将顶点数据拷贝到缓存对象 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 设置顶点属性指针,glVertexAttribPointer()函数告诉OpenGL该如何解析顶点数据 // 顶点属性位置 顶点属性大小 数据的类型 是否被标准化 步长 偏移 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void *)0); // glEnableVertexAttribArray()以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的 glEnableVertexAttribArray(0); // 解绑缓存着色器(绑定和解绑的顺序很重要,勿更改) glBindBuffer(GL_ARRAY_BUFFER, 0); // 解绑顶点着色器(绑定和解绑的顺序很重要,勿更改) glBindVertexArray(0); /* 新增代码 第一段/共三段----------------------------------END */ while (!glfwWindowShouldClose(window)) { // 捕捉输入 processInput(window); // 清空颜色缓冲 // 添加渲染指令,背景RGBA的A不生效, 更改为白色 glClearColor(1.0f, 1.0f, 1.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT); /* 新增代码 第二段/共三段----------------------------------START */ // 当我们渲染一个物体时要使用着色器程序 glUseProgram(shaderProgram); // 激活VAO表示的顶点缓存 glBindVertexArray(VAO); // 每次重新更新视口,如果不这样,拉升窗口后,会变形。 // 你把你要观察的视景体映射到你的视口就没有问题,但是如果你不设置视景体, // opengl默认会以屏幕坐标,就是屏幕中心为原点,到四周的距离为1的这个范围 // 作为视景体的观察部分映射到你设置的视口上。 // 可以尝试,屏蔽此句,然后运行程序拉升窗口试试效果,更好理解, // 每次设置600 600 但是实际上 窗口大小已经是改变大小后的窗口了 // 所以 总是只能显示 600*600的实际视口 尽管窗口可能是 1024*768 glViewport(0, 0, 600, 600); // 绘制三角形:三角形 数组起始索引 绘制多少个顶点 glDrawArrays(GL_TRIANGLES, 0, 3); // 绘制三角形:线条 数组起始索引 绘制多少个顶点 // glDrawArrays(GL_LINES, 0 , 2); /* 新增代码 第二段/共三段----------------------------------END */ // 检查并调用事件,交换缓冲(执行交换缓存才会更新新数据到界面) glfwSwapBuffers(window); glfwPollEvents(); } /* 新增代码 第三段/共三段----------------------------------END */ glDeleteVertexArrays(1, &VAO); glDeleteBuffers(1, &VBO); /* 新增代码 第三段/共三段----------------------------------END */ // 当渲染循环结束后我们需要正确释放/删除之前的分配的所有资源。 glfwTerminate(); return 0; }
原博主博客地址:http://blog.csdn.net/qq21497936
本文章博客地址:http://blog.csdn.net/qq21497936/article/details/78888286