1.1 VAO,VBO,EBO编程路线
VAO顶点数组对象顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。
一个顶点数组对象会储存以下这些内容:
- glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
- 通过glVertexAttribPointer设置的顶点属性配置。
- 通过glVertexAttribPointer调用进行的顶点缓冲对象与顶点属性链接。
要想使用VAO,要做的只是使用生成并绑定VAO。从绑定之后起,我们应该配置对应的VBO和属性指针,之后解绑VAO供之后使用。当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了GLuint VAO; glGenVertexArrays(1, &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(GLfloat), (GLvoid*)0); glEnableVertexAttribArray(0); //4. 解绑VAO glBindVertexArray(0); [...] // ..:: 绘制代(游戏循环中) :: .. // 5. 绘制物体 glUseProgram(shaderProgram); glBindVertexArray(VAO); someOpenGLFunctionThatDrawsOurTriangle(); glBindVertexArray(0);
VBO 顶点缓冲数据属性(位置)- 位置数据被储存为32-bit(4字节)浮点值。
- 每个位置包含3个这样的值。
- 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列。
- 数据中第一个值在缓冲开始的位置。
有了这些信息我们就可以使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)了:
- glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)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个GLfloat之后,我们把步长设置为3
- sizeof(GLfloat)。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注:
这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。- 最后一个参数的类型是GLvoid*,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。我们会在后面详细解释这个参数。
每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用glVetexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。由于在调用glVetexAttribPointer之前绑定的是先前定义的VBO对象,顶点属性0现在会链接到它的顶点数据。现在我们已经定义了OpenGL该如何解释顶点数据,我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。多属性数据详见下。
EBO索引缓冲对象
索引绘制(Indexed Drawing)允许我们在已经存储的顶点中选取不同的顶点来绘制不同的图形,而不需要对对同一个顶点进行重复定义。进行索引绘制需要借助索引缓冲对象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO)。和顶点缓冲对象一样,EBO也是一个缓冲,它专门储存索引,OpenGL调用这些顶点的索引来决定该绘制哪个顶点。首先,我们先要定义(独一无二的)顶点,和绘制出矩形所需的索引:
GLfloat vertices[] = { 0.5f, 0.5f, 0.0f, // 右上角 0.5f, -0.5f, 0.0f, // 右下角 -0.5f, -0.5f, 0.0f, // 左下角 -0.5f, 0.5f, 0.0f // 左上角 }; GLuint indices[] = { // 注意索引从0开始! 0, 1, 3, // 第一个三角形 1, 2, 3 // 第二个三角形 }
创建索引缓冲对象
GLuint EBO; glGenBuffers(1, &EBO);
与VBO类似,我们先绑定EBO然后用glBufferData把索引复制到缓冲里。同样,和VBO类似,我们会把这些函数调用放在绑定和解绑函数调用之间,只不过这次我们把缓冲的类型定义为GL_ELEMENT_ARRAY_BUFFER。
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
要注意的是,我们传递了GL_ELEMENT_ARRAY_BUFFER当作缓冲目标。最后一件要做的事是用glDrawElements来替换glDrawArrays函数,来指明我们从索引缓冲渲染。使用glDrawElements时,我们会使用当前绑定的索引缓冲对象中的索引进行绘制:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glDrawElements函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER目标的EBO中获取索引。这意味着 我们必须在每次要用索引渲染一个物体时绑定相应的EBO,这还是有点麻烦。不过顶点数组对象同样可以保存索引缓冲对象的绑定状态。VAO绑定时正在绑定的索引缓冲对象会被保存为VAO的元素缓冲对象。 绑定VAO的同时也会自动绑定EBO。
当目标是GL_ELEMENT_ARRAY_BUFFER的时候,VAO会储存glBindBuffer的函数调用。这也意味着它也会储存解绑调用,所以确保你没有在解绑VAO之前解绑索引数组缓冲,否则它就没有这个EBO配置了。
VAO,VBO,EBO编程路线如下:
// ..:: 初始化代码 :: .. // 1. 绑定顶点数组对象 glBindVertexArray(VAO); // 2. 把我们的顶点数组复制到一个顶点缓冲中,供OpenGL使用 glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 3. 复制我们的索引数组到一个索引缓冲中,供OpenGL使用 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); // 3. 设定顶点属性指针 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0); glEnableVertexAttribArray(0); // 4. 解绑VAO(不是EBO!) glBindVertexArray(0); [...] // ..:: 绘制代码(循环中) :: .. glUseProgram(shaderProgram); glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0) glBindVertexArray(0);
1.2 顶点位置、颜色
1.3 顶点、片元着色器
典型的着色器语言GLSL( http://blog.csdn.net/ylbs110/article/details/52145794)#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; }
OpenGL确保顶点着色器的输入变量(顶点属性)至少有16个包含4分量的顶点属性可用,但是有些硬件或许允许更多的顶点属性,你可以查询GL_MAX_VERTEX_ATTRIBS来获取具体的上限:GLint Attributes; glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &Attributes); std::cout << "Maximum nr of vertex attributes supported: " << Attributes << std::endl;
输入与输出GLSL定义了in和out关键字专门来实现输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。
顶点着色器的输入特殊在,它从顶点数据中直接接收输入。为了定义顶点数据该如何管理,我们使用location这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。顶点着色器需要为它的输入提供一个额外的layout标识,这样我们才能把它链接到顶点数据。
你也可以忽略layout (location = 0)标识符,通过在OpenGL代码中使用glGetAttribLocation 查询属性位置值(Location),但是我更喜欢在着色器中设置它们,这样会更容易理解而且节省你(和OpenGL)的工作量。
另一个例外是片段着色器,它需要一个vec4颜色输出变量,因为片段着色器需要生成一个最终输出的颜色。如果你在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色)。
顶点着色器#version 330 core layout(location = 0) in vec3 position; // position变量的属性位置值为0 out vec4 vertexColor; // 为片段着色器指定一个颜色输出 void main() { gl_Position = vec4(position, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数 vertexColor = vec4(0.5f, 0.0f, 0.0f, 1.0f); // 把输出变量设置为暗红色 }
片段着色器
in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同) out vec4 color; // 片段着色器输出的变量名可以任意命名,类型必须是vec4 void main() { color = vertexColor; }
Uniform
Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。首先,uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。
如果你声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,记住这点!
片段着色器
#version 330 core shader: out vec4 color; uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量 void main() { color = ourColor; }
应用程序查询uniform变量位置并赋值
GLfloat timeValue = glfwGetTime(); GLfloat greenValue = (sin(timeValue) / 2) + 0.5; GLint vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); glUseProgram(shaderProgram); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
为顶点添加更多属性把颜色数据加进顶点数据中。可以把颜色数据添加为3个float值至vertices数组。我们将把三角形的三个角分别指定为红色、绿色和蓝色:GLfloat 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标识符来把color属性的位置值设置为1:#version 330 core layout(location = 0) in vec3 position; // 位置变量的属性位置值为 0 layout(location = 1) in vec3 color; // 颜色变量的属性位置值为 1 out vec3 ourColor; // 向片段着色器输出一个颜色 void main() { gl_Position = vec4(position, 1.0); ourColor = color; // 将ourColor设置为我们从顶点数据那里得到的输入颜色 }
更新了VBO的内存,我们就必须重新配置顶点属性指针。更新后的VBO内存中的数据现在看起来像这样:知道了现在使用的布局,我们就可以使用glVertexAttribPointer函数更新顶点格式,
// 位置属性 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0); //顶点属性间步长为6,位移偏移为0 glEnableVertexAttribArray(0); // 颜色属性 glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat))); //颜色属性偏移为3 glEnableVertexAttribArray(1);
glVertexAttribPointer函数的前几个参数比较明了。这次我们配置属性位置值为1的顶点属性。颜色值有3个float那么大,我们不去标准化这些值。
由于我们现在有了两个顶点属性,我们不得不重新计算步长值。为获得数据队列中下一个属性值(比如位置向量的下个x分量)我们必须向右移动6个float,其中3个是位置值,另外3个是颜色值。这使我们的步长值为6乘以float的字节数(=24字节)。
同样,这次我们必须指定一个偏移量。对于每个顶点来说,位置顶点属性在前,所以它的偏移量是0。颜色属性紧随位置数据之后,所以偏移量就是3 * sizeof(GLfloat),用字节来计算就是12字节。
1.4 大数据模型绘制(ThunderBird body、glass)
OpenGL蓝宝书中的此模型是通过第三方软件导出的.cpp模型。这里用来作为大数据模型绘制的练习。这里包括四个数组:平面索引数组、顶点数组、法线和纹理数组。由于数组不是基于同样的顶点,为此需要通过平面索引数组来重新处理可以被OpenGL利用的具有相同元素索引的顶点、法线和纹理数组和索引数组。/* This file was produced by Deep Exploration Plugin: CPP Export filter. */ #include "stdafx.h" #include
// 1898 Verticies // 2925 Texture Coordinates // 2716 Normals。 // 3704 Triangles GLushort face_indicies[3704][9] = { // body {6,8,7 ,0,1,2 ,0,1,2 }, {6,9,8 ,0,3,1 ,0,3,1 }, {10,8,11 ,4,1,5 ,4,1,5 }, {10,12,8 ,4,6,1 ,4,6,1 }, {13,12,14 ,7,6,8 ,7,6,8 }, {13,15,12 ,7,9,6 ,7,9,6 }, ... {1797,1817,1760 ,2712,2709,2711 ,2785,2819,2786 }, {1874,1817,1797 ,2713,2709,2712 ,2923,2819,2785 }, {1867,1817,1874 ,2714,2709,2713 ,2908,2819,2923 }, {1816,1817,1867 ,2715,2709,2714 ,2817,2819,2908 } }; GLfloat vertices [1898][3] = { {0.0f,-90.5208f,-5.47392f},{-3.40217e-008f,-41.995f,-13.5968f},{-17.11f,76.7107f,-1.06122f}, ..., {5.70697f,-62.5562f,9.21391f},{5.69512f,-65.3741f,8.88059f},{2.538f,-32.2758f,13.7085f}, {2.30592f,-79.977f,8.47098f},{0.805676f,-80.9146f,8.53326f} }; GLfloat normals [2716][3] = { {-0.689541f,0.152826f,0.707939f},{-0.732465f,0.0511671f,0.678879f},{-0.802999f,0.126122f,0.582482f}, ..., {0.00100489f,-0.0358456f,0.999357f} }; GLfloat textures [2925][2] = { {0.258139f,0.911523f},{0.258937f,0.839553f},{0.264164f,0.91181f}, ..., {0.0627418f,0.839841f},{0.06316f,0.839841f},{0.0628102f,0.830563f} };
类似下面的数组。过滤掉具有相同位置、法线和纹理坐标的顶点,剩下nNumVerts个不同顶点。GLushort *pIndexes = new GLushort[nMaxIndexes]; // Array of indexes Vector3f *pVerts = new Vector3f[nMaxIndexes]; // Array of vertices Vector3f *pNorms = new Vector3f[nMaxIndexes]; // Array of normals Vector2f *pTexCoords = new Vector2f[nMaxIndexes]; // Array of texture coordinates
多物体绘制
// http://www.kdab.com/opengl-in-qt-5-1-part-2/ 点击打开链接
void Scene::initialize() { // Assumes we have a current QOpenGLContext and that // m_shaderProgram is a QOpenGLShaderProgram // Create VAO for first object to render m_vao1 = new QOpenGLVertexArrayObject( this ); m_vao1->create(); m_vao1->bind(); // Setup VBOs and IBO (use QOpenGLBuffer to buffer data, // specify format, usage hint etc). These will be // remembered by the currently bound VAO m_positionBuffer.create(); m_positionBuffer.setUsagePattern( QOpenGLBuffer::StreamDraw ); m_positionBuffer.bind(); m_positionBuffer.allocate( positionData, vertexCount * 3 * sizeof( float ) ); m_shaderProgram.enableAttributeArray( "vertexPosition" ); m_shaderProgram.setAttributeBuffer( "vertexPosition", GL_FLOAT, 0, 3 ); m_colorBuffer.create(); m_colorBuffer.setUsagePattern( QOpenGLBuffer::StaticDraw ); m_colorBuffer.bind(); m_colorBuffer.allocate( colorData, vertexCount * 3 * sizeof( float ) ); m_shaderProgram.enableAttributeArray( "vertexColor" ); m_shaderProgram.setAttributeBuffer( "vertexColor", GL_FLOAT, 0, 3 ); // Repeat for buffers of normals, texture coordinates, // tangents, ... ... // Create VAO for second object to render m_vao2 = new QOpenGLVertexArrayObject( this ); m_vao2->create(); m_vao2->bind(); // Setup VBOs and IBO for next object ... // Rinse and repeat for other objects m_skyBoxVAO = new QOpenGLVertexArrayObject( this ); ... } void Scene::render() { // Clear buffers m_funcs->glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); // Bind shader program, textures for first set of objects m_phongShaderProgram->bind(); ... // Switch to the vertex data for first object and draw it m_vao1->bind(); m_funcs->glDrawElements(...); // Switch to the vertex data for second object and draw it m_vao2->bind(); m_funcs->glDrawElements(...); // Maybe change shader program, textures etc // and draw other objects m_skyboxShaderProgram->bind(); ... m_skyboxVAO->bind(); m_funcs->glDrawElements(...); ... }
1.5 纹理贴图
纹理
纹理是一个2D图片(甚至也有1D和3D的纹理),它可以用来添加物体的细节。除了图像以外,纹理也可以被用来储存大量的数据,这些数据可以发送到着色器上。
纹理坐标
纹理坐标(Texture Coordinate),用来标明该从纹理图像的哪个部分采样(译注:采集片段颜色)。纹理坐标在x和y轴上,范围为0到1之间(注意我们使用的是2D纹理图像)。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。
采样
使用纹理坐标获取纹理颜色叫做采样(Sampling)。纹理环绕方式对超出坐标范围(0,1)的坐标的处理为纹理环绕方式。OpenGL默认的行为是重复这个纹理图像(我们基本上忽略浮点纹理坐标的整数部分)。
环绕方式(Wrapping) 描述 GL_REPEAT 对纹理的默认行为。重复纹理图像。 GL_MIRRORED_REPEAT 和GL_REPEAT一样,但每次重复图片是镜像放置的。 GL_CLAMP_TO_EDGE 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。 GL_CLAMP_TO_BORDER 超出的坐标为用户指定的边缘颜色。
前面提到的每个选项都可以使用glTexParameter*函数对单独的一个坐标轴设置(s、t(如果是使用3D纹理那么还有一个r)它们和x、y、z是等价的):
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT)
第一个参数指定了纹理目标;我们使用的是2D纹理,因此纹理目标是GL_TEXTURE_2D。第二个参数需要我们指定设置的选项与应用的纹理轴。我们打算配置的是WRAP选项,并且指定S和T轴。最后一个参数需要我们传递一个环绕方式,在这个例子中OpenGL会给当前激活的纹理设定纹理环绕方式为GL_MIRRORED_REPEAT。
如果我们选择GL_CLAMP_TO_BORDER选项,我们还需要指定一个边缘的颜色。这需要使用glTexParameter函数的fv后缀形式,用GL_TEXTURE_BORDER_COLOR作为它的选项,并且传递一个float数组作为边缘的颜色值:
float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f }; glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
纹理过滤纹理贴图中纹素的坐标总是以整数定义的,但是如果纹理坐标映射到纹素上的坐标为(152.34,745.14)怎么办?不明智的方案是将这个坐标舍去小数变为(152,745)。这种方法虽然可以有效果但是在某些情形下效果会很差。一个更好的办法是选取该坐标周围纹素2x2的4个坐标 ( (152,745), (153,745), (152,744) 和 (153,744) ) 并根据他们的颜色做线性插值。线性插值必须体现出坐标(152.34,745.14)和四个坐标的相对距离来。距离近的点的颜色对最终纹素的颜色值影响大,越远影响越小,这样就比开始的方法简单多了。
选择最终纹素的方法叫做’过滤‘,最简单的实现小数纹理坐标取整的方法叫做‘最邻近过滤’,更复杂一点的方法叫做‘线性过滤’,另外一种可能遇到的最邻近过滤方法叫做‘点过滤’。OpenGL支持几种不同类型的过滤方式可以选择。通常效果好的过滤方式需要GPU更强大计算能力并且可能对帧率产生影响,选择不同的过滤方式就需要权衡对过滤效果的需求和对目标设备能力的要求了。
邻近过滤
GL_NEAREST(Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。
线性过滤
GL_LINEAR( (Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。
我们需要使用glTexParameter*函数指定过滤方式。这段代码看起来会和纹理环绕方式的设置很相似:glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
多级渐远纹理Mipmap多级渐远纹理(Mipmap)简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,即最适合物体的距离的那个。
在渲染中切换多级渐远纹理级别(Level)时,OpenGL在两个不同级别的多级渐远纹理层之间会产生不真实的生硬边界。就像普通的纹理过滤一样,切换多级渐远纹理级别时你也可以在两个不同多级渐远纹理级别之间使用NEAREST和LINEAR过滤。为了指定不同多级渐远纹理级别之间的过滤方式,你可以使用下面四个选项中的一个代替原有的过滤方式:
过滤方式 描述 GL_NEAREST_MIPMAP_NEAREST 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样 GL_LINEAR_MIPMAP_NEAREST 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样 GL_NEAREST_MIPMAP_LINEAR 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样 GL_LINEAR_MIPMAP_LINEAR 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样 我们使用glTexParameteri将过滤方式设置为前面四种提到的方法之一:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
应用程序中使用纹理映射,需要的步骤:
- 创建纹理对象Texture Object, 为它加载纹素数据
GLuint texture; glGenTextures(1, &texture);
初始绑定的目标决定了创建的纹理类型为GL_TEXTURE_2D. 纹理通过纹理单元(命名为GL_TEXTURE0~GL_TEXTUREi, 最大数GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS至少是80)绑定到OpenGL环境,并在着色器中使用采样器变量来访问。纹理单元GL_TEXTURE0默认 总是被激活,所以该纹理如果之前没有其它激活的纹理单元,默认的纹理单元是0.glBindTexture(GL_TEXTURE_2D, texture);
glActiveTexture(GL_TEXTURE0); //在绑定纹理之前先激活纹理单元 glBindTexture(GL_TEXTURE_2D, texture);
图像数据加载进纹理对象可以从图像直接加载,也可以显示通过数组设置.
第一个参数指定了纹理目标(Target)。设置为GL_TEXTURE_2D意味着会生成与当前绑定的纹理对象在同一个目标上的纹理(任何绑定到GL_TEXTURE_1D和GL_TEXTURE_3D的纹理不会受到影响)。glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image); //加载纹理数据到目标
- 第二个参数为纹理指定多级渐远纹理的级别,如果你希望单独手动设置每个多级渐远纹理的级别的话。这里我们填0,也就是基本级别。
- 第三个参数告诉OpenGL我们希望把纹理储存为何种格式。我们的图像只有RGB值,因此我们也把纹理储存为RGB值。
- 第四个和第五个参数设置最终的纹理的宽度和高度。我们之前加载图像的时候储存了它们,所以我们使用对应的变量。
- 下个参数应该总是被设为0(历史遗留问题)。
- 第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像,并把它们储存为char(byte)数组,我们将会传入对应值。
- 最后一个参数是真正的图像数据。
当调用glTexImage2D时,当前绑定的纹理对象就会被附加上纹理图像。然而,目前只有基本级别(Base-level)的纹理图像被加载了,如果要使用多级渐远纹理,我们必须手动设置所有不同的图像(不断递增第二个参数)。或者,直接在生成纹理之后调用glGenerateMipmap。这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。
glGenerateMipmap(GL_TEXTURE_2D);
注意要设置纹理过滤。调试中发现不设置该项图像总是不显示。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
解绑并释放
glBindTexture(GL_TEXTURE_2D, 0);
当绑定的texture值为0时, OpenGL将删除所有与激活纹理单元关联的绑定。
- 为顶点增加纹理坐标
GLfloat vertices[] = { // ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 - 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 // 左上 };
由于我们添加了一个额外的顶点属性,我们必须告诉OpenGL我们新的顶点格式:glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (GLvoid*)(6 * sizeof(GLfloat)));
glEnableVertexAttribArray(2);
- 接着我们需要调整顶点着色器使其能够接受顶点坐标为一个顶点属性,并把坐标传给片段着色器:
#version 330 core layout(location = 0) in vec3 position; layout(location = 1) in vec3 color; layout(location = 2) in vec2 texCoord; out vec3 ourColor; out vec2 TexCoord; void main() { gl_Position = vec4(position, 1.0f); ourColor = color; TexCoord = texCoord; }
- 纹理图与着色器中的纹理采样器关联
片段着色器应该把输出变量TexCoord作为输入变量。
片段着色器也应该能访问纹理对象,但是我们怎样能把纹理对象传给片段着色器呢?GLSL有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler),它以纹理类型作为后缀,比如sampler1D、sampler3D,或在我们的例子中的sampler2D。我们可以简单声明一个uniform sampler2D把一个纹理添加到片段着色器中,稍后我们会把纹理赋值给这个uniform。
#version 330 core in vec3 ourColor; in vec2 TexCoord; out vec4 color; uniform sampler2D ourTexture; void main() { color = texture(ourTexture, TexCoord); }
我们使用GLSL内建的texture函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是对应的纹理坐标。texture函数会使用之前设置的纹理参数对相应的颜色值进行采样。这个片段着色器的输出就是纹理的(插值)纹理坐标上的(过滤后的)颜色。
现在只剩下在调用glDrawElements之前绑定纹理了,它会自动把纹理texture赋值给片段着色器的采样器ourTexture:
glBindTexture(GL_TEXTURE_2D, texture); glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); glBindVertexArray(0);
- 着色器中使用纹理采样器查询纹素值
- 对于单个纹理,在片元着色器中使用GLSL自带的函数color = texture(ourTexture, TexCoord);即可采样纹理的颜色,即纹素值。
- 对于多重纹理,我们需要在片段着色器中声明多个采样器变量。每个指向不同的纹理单元。
#version 330 core ... uniform sampler2D ourTexture1; uniform sampler2D ourTexture2; void main() { color = mix(texture(ourTexture1, TexCoord), texture(ourTexture2, TexCoord), 0.2); }
最终输出颜色现在是两个纹理的结合。GLSL内建的mix函数需要接受两个值作为参数,并对它们根据第三个参数进行线性插值。。如果第三个值是0.0,它会返回第一个输入;如果是1.0,会返回第二个输入值。0.2会返回80%的第一个输入颜色和20%的第二个输入颜色,即返回两个纹理的混合色。
我们现在需要载入并创建另一个纹理,方法和载入第一个纹理一样。
为了使用第二个纹理(以及第一个),我们必须改变一点渲染流程,先绑定两个纹理到对应的纹理单元,然后定义哪个uniform采样器对应哪个纹理单元:glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, texture1); glUniform1i(glGetUniformLocation(ourShader.Program, "ourTexture1"), 0);
glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, texture2); glUniform1i(glGetUniformLocation(ourShader.Program, "ourTexture2"), 1); glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); glBindVertexArray(0);
注意,我们使用glUniform1i设置uniform采样器的位置值,或者说纹理单元。通过glUniform1i的设置,我们保证每个uniform采样器对应着正确的纹理单元。