一、纹理
纹理坐标:用来标明该从纹理图像的哪个部分采样
为了能够把纹理映射(Map)到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标,之后在图形的其它片段上进行片段插值。
采样:使用纹理坐标获取纹理颜色
纹理坐标在x和y轴上,范围为0到1之间(注意我们使用的是2D纹理图像)。纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1),即纹理图片的右上角。
我们为三角形指定了3个纹理坐标点。如上图所示,我们希望三角形的左下角对应纹理的左下角,因此我们把三角形左下角顶点的纹理坐标设置为(0, 0);三角形的上顶点对应于图片的上中位置所以我们把它的纹理坐标设置为(0.5, 1.0);同理右下方的顶点设置为(1, 0)。我们只要给顶点着色器传递这三个纹理坐标就行了,接下来它们会被传片段着色器中,它会为每个片段进行纹理坐标的插值。
纹理坐标:
1 float texCoords[] = { 2 0.0f, 0.0f, // 左下角 3 1.0f, 0.0f, // 右下角 4 0.5f, 1.0f // 上中 5 };
二、纹理环绕方式
环绕方式 | 描述 |
---|---|
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
是等价的):
1 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT); 2 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
第一个参数:指定了纹理目标;我们使用的是2D纹理,因此纹理目标是GL_TEXTURE_2D。
第二个参数:指定设置的选项与应用的纹理轴。配置的是WRAP
选项,并且指定S
和T
轴。
最后一个参数:环绕方式(Wrapping)
如果我们选择GL_CLAMP_TO_BORDER选项,我们还需要指定一个边缘的颜色。这需要使用glTexParameter函数的fv
后缀形式,用GL_TEXTURE_BORDER_COLOR作为它的选项,并且传递一个float数组作为边缘的颜色值:
1 float borderColor[] = { 1.0f, 1.0f, 0.0f, 1.0f }; 2 glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
三、纹理过滤
纹理像素:Texture Pixel也叫Texel,打开一张图片不断放大时,会发现它是由无数像素点组成的,这个点就是纹理像素。
纹理坐标:给模型顶点设置的那个数组,OpenGL以这个顶点的纹理坐标数据去查找纹理图像上的像素,然后进行采样提取纹理像素的颜色。
1>GL_NEAREST(也叫邻近过滤,Nearest Neighbor Filtering)是OpenGL默认的纹理过滤方式。
当设置为GL_NEAREST的时候,OpenGL会选择中心点最接近纹理坐标的那个像素。下图中你可以看到四个像素,加号代表纹理坐标。左上角那个纹理像素的中心距离纹理坐标最近,所以它会被选择为样本颜色:
2>GL_LINEAR(也叫线性过滤,(Bi)linear Filtering)它会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。下图中你可以看到返回的颜色是邻近像素的混合色:
那么这两种纹理过滤方式有怎样的视觉效果呢?让我们看看在一个很大的物体上应用一张低分辨率的纹理会发生什么吧(纹理被放大了,每个纹理像素都能看到):
GL_NEAREST产生了颗粒状的图案,我们能够清晰看到组成纹理的像素,而GL_LINEAR能够产生更平滑的图案,很难看出单个的纹理像素。GL_LINEAR可以产生更真实的输出.
使用场合:放大或缩小纹理时
缩小:邻近过滤
放大:线性过滤
1 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 2 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
四、多级渐远纹理
简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。多级渐远纹理主要是使用在纹理被缩小的情况下。
OpenGL有一个glGenerateMipmaps函数,在创建完一个纹理后调用它OpenGL就会为每个纹理图像创建一系列的多级渐远纹理。
指定不同多级渐远纹理级别之间的过滤方式,你可以使用下面四个选项中的一个代替原有的过滤方式:
过滤方式 | 描述 |
---|---|
GL_NEAREST_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样 |
GL_LINEAR_MIPMAP_NEAREST | 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样 |
GL_NEAREST_MIPMAP_LINEAR | 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样 |
GL_LINEAR_MIPMAP_LINEAR | 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样 |
1 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
五、加载与创建纹理
使用纹理之前第一件事就是把纹理加载到我们的应用中。
stb_image.h库:一个支持多种流行格式的图像加载库。
要是用它,需要使用它的stbi_load函数:
1 int width,height,nrChannels;//图像的宽度、高度和颜色通道的个数 2 unsigned char *data = stbi_load("container.jpg",&width,&height,&nrChannels,0);
六、生成纹理
纹理也是使用id引用。
1 unsigned int texture; 2 glGenTexture(1,&texture);
我们需要绑定它,让之后任何的纹理指令都可以配置当前绑定的纹理:
glBindTexture(GL_TEXTURE_2D,texture)
现在纹理已经绑定了,我们可以使用前面载入的图片数据生成一个纹理了。纹理可以通过glTexImage2D来生成:
1 glTexImage2D(GL_TEXTURE_2D,0,GL_RGB,width,height,0,GL_RGB,GL_UNSIGNED_BYTE,data); 2 glGenerateMipmap(GL_TEXTURE_2D);
第一个参数:纹理目标。
第二个参数:指定多级渐远纹理的级别。0就是基本级别。
第三个参数:告诉openGL把纹理储存为何种格式。图像只有RGB值,因此把纹理储存为RGB值。
第四、五个参数:纹理的宽度、高度。
第六个参数:一直设为0.
第七个参数:源图的格式。
第八个参数:数据类型。我们使用RGB值加载这个图像,并把它们储存为char
(byte)数组,我们将会传入对应值。
最后一个参数:真正的图像数据。
当调用glTexImage2D时,当前绑定的纹理对象就会被附加上纹理图像。生成纹理之后调用glGenerateMipmap,这会为当前绑定的纹理自动生成所有需要的多级渐远纹理。
释放内存:
stbi_image_free(data);
生成纹理的过程:
1 unsigned int texture; 2 glGenTextures(1, &texture); 3 glBindTexture(GL_TEXTURE_2D, texture); 4 // 为当前绑定的纹理对象设置环绕、过滤方式 5 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); 6 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); 7 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 8 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 9 // 加载并生成纹理 10 int width, height, nrChannels; 11 unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0); 12 if (data) 13 { 14 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data); 15 glGenerateMipmap(GL_TEXTURE_2D); 16 } 17 else 18 { 19 std::cout << "Failed to load texture" << std::endl; 20 } 21 stbi_image_free(data);
七、应用纹理
使用纹理坐标更新顶点数据。
1 float vertices[] = { 2 // ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 - 3 -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下 4 0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下 5 0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上 6 -0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上 7 };
因为在顶点数组中添加了额外的顶点属性,所以必须告诉openGl新的顶点格式。
1 glVertexAttribPointer(2,2,GL_FLOAT,GL_FALSE,8*sizeof(float),(void*)(6*sizeof(float))); 2 glEnable(2);
接着调整着色器:
1>顶点着色器
1 #version 330 core 2 layout(location=0)in vec3 aPos; 3 layout(location=1)in vec3 aColor; 4 layout(location=2)in vec2 aTexCoord; 5 6 out vec3 ourColor; 7 out vec2 TexCoord; 8 9 void main(){ 10 gl_Position = vec4(aPos,1.0); 11 ourColor = aColor; 12 TexCoord = aTexCoord; 13 }
2>片元着色器
GLSL有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler),它以纹理类型作为后缀,比如sampler1D
、sampler3D
,或在我们的例子中的sampler2D
。我们可以简单声明一个uniform sampler2D
把一个纹理添加到片段着色器中,稍后我们会把纹理赋值给这个uniform。
1 #version 330 core 2 out vec4 FragColor; 3 4 in vec3 ourColor; 5 in vec2 TexCoord; 6 7 uniform sampler2D ourTexture; 8 9 void main(){ 10 FragColor = texture(ourTexture,TexCoord); 11 }
texture()是GLSL内建的函数,第一个参数是纹理采样器,第二个参数是对应的纹理坐标。
现在只剩下在调用glDrawElements之前绑定纹理了,它会自动把纹理赋值给片段着色器的采样器:
1 glBindTexture(GL_TEXTURE_2D, texture); 2 glBindVertexArray(VAO); 3 glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
八、纹理单元
纹理单元:一个纹理的位置值。(一个纹理的默认纹理单元是0)
目的:让我们在着色器中可以使用多个纹理。
通过把纹理单元赋值给采样器,只要我们激活对应的纹理单元,我们可以一次绑定多个纹理。
1 glActiveTexture(GL_TEXTURE0);//在绑定纹理之前先激活纹理单元 2 glBindTexture(GL_TEXTURE_2D,texture);
激活纹理单元之后,glBindTexture会绑定这个纹理到当前的纹理单元。GL_TEXTURE0默认总是被激活,所以绑定一个纹理的时候,激不激活无所谓。
之后编辑片元着色器来接受另一个采样器
1 #version 330 core 2 .... 3 uniform sampler2D texture1; 4 uniform sampler2D texture2; 5 6 void main(){ 7 FragColor=mix(texture(texture1,TexCoord),texture(texture2,TexCoord),0.2); 8 }
最终输出颜色是两个纹理的结合。GLSL内建的mix函数需要接受两个值作为参数,并根据第三个参数进行线性参数。
1>若第三个值是0.0,则会返回第一个输入;
2>1.0,则会返回第二个输入;
3>0.2,则会返回80%的第一个输入和20%第二个输入,即返回两个纹理的混合;
还需要通过使用glUniformli设置采样器,告诉openGL每个uniform采样器属于哪个纹理单元,设置一次即可,所以把这个放在循环渲染的前边。
1 glUniform1i(glGetUniformLocation(ourShader.ID,"texture1"),0);//手动设置方式
或者ourShader.setInt("texture2",1);//使用着色器设置
完成后纹理会上下颠倒,加入下边会翻转y轴一行代码即可。
1 stbi_set_flip_vertically_on_load(true);