在设计物体表面时,很多时候我们不满足于一种颜色或者几种简单颜色,我们希望是丰富多彩的图案,或者说我们提供给它的图片。这样一个顶点一个顶点的去指定那是行不通了,我们不可能把所有顶点用数字去表达出来,必须用一种新的方式去设置颜色。这就是纹理,像用一张画去贴在物体的表面一样,这样就不用指定太多的点,只需要设置“边界”就可以了。我们把这种行为叫做映射。 我们不可能随便映射,我们必须告诉程序三个东西:1.纹理图片剪裁多少(边界坐标位置)2.纹理图片对应的3d面的边界(顶点坐标位置)3将纹理图片的坐标与3d面的坐标一一对应。 (一)纹理图片设定:
和标准化坐标一样,纹理的坐标也是从0——1,不过这回没有负值了,不管图片有多么大,我们都将它们认为是在0-1的图片
坐标系中,其中左下顶点是原点。我们需要三个点构成一个面,所以我们找三个顶点作为表示边界的数据。
float texCoords[] = {
0.0f, 0.0f, // 左下角
1.0f, 0.0f, // 右下角
0.5f, 1.0f // 上中
};
(二)处理坐标超出(0,1)设定,纹理环绕方式:
不同于openGL的标准化坐标,在超出(-1,1)时候,直接不显示,当纹理坐标超出(0,1)的时候,纹理处理会将图片进行变
化,使图片将贴合的那个面完全包含,OpenGL默认的行为是重复这个纹理图像(我们基本上忽略浮点纹理坐标的整数部分)。
接下来介绍一个函数,它用来设置种情况,它要放在你创建了一个纹理对象,并且绑定到当前上下文的后面,保证它对这个纹理对象起作用。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
第一个参数是表示2d渲染,第二个参数表示这是对那个坐标轴进行的(可以猜测这样可以有好多的组合情况发生),
最后一个参数需要我们传递一个环绕方式,这里设置的是GL_MIRRORED_REPEAT,和GL_REPEAT一样,但每次重复图片是镜像放置的。
(大多数情况都是这样,但有时候我们还需要指定填充颜色,这个我们一般用这个函数的变形函数,没办法c语言不支持重载)
(三)处理放大,缩小情况,纹理过滤: 我们对于物体不可避免要应对放大和缩小的情况。首先我们屏幕的像素点数量是不可能改变的,也就是说,有的时候当你缩小物体的时候
你需要用更小的像素点数量去承载图片,放大的时候你需要用更多的像素点去表现物体,这就是纹理过滤的问题。此外还有一个情况就是当一
个分辨率很高的物体,在很远的地方。我们只能用很少的像素点去表现它。这一系列问题都是这个纹理过滤的原由 这里需要三个属性作为分析。1.纹理像素 2.纹理顶点 3.屏幕像素
首先纹理像素,就是一张图片不断放大后,能发现它是由一个一个点组成的,这就是纹理像素,它由拍摄这张图片的仪器决定,照片大小不变
它不变。
之后是纹理顶点,这个从(0,1)的绝对坐标组,不受分辨率影响,所以所以OpenGL需要知道怎样将纹理像素(Texture Pixel,也叫Texel,
译注1)映射到纹理坐标。
最后是屏幕像素,OpenGL根据纹理顶点坐标,查找纹理图片上的像素,再根据纹理像素判断分析,提取出一个颜色值,放置到屏幕像素上。
纹理过滤主要就是考虑如何分析判断。目前提供两种过滤GL_NEAREST(颗粒状的图案),GL_LINEAR(更平滑的图案)。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
(四)处理物体远近情况,多级渐远纹理:
接下来我们考虑之前说的一个问题,当一个纹理分辨率很高的物体,在很远的地方,我们只能用很少的屏幕像素点去表示它。
OpenGL从高分辨率纹理中为这些片段获取正确的颜色值就很困难,因为它需要对一个跨过纹理很大部分的片段只拾取一个纹理颜色。在小物
体上这会产生不真实的感觉,对它们使用高分辨率纹理浪费内存的问题。这就是用多级渐远纹理的原因。
这是一种用“空间换时间”的手段。多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL会使用不同的多级渐远纹理,
即最适合物体的距离的那个。
注意每个二分之一的图片它们的纹理像素只有之前的四分之一多。这一步必须在加载图片成功之后再执行。
这个负责函数是glGenerateMipmaps(),此外也可以为这种缩小或者放大,指定纹理过滤形式。
glGenerateMipmap(GL_TEXTURE_2D);
这个参数跟VBO,VAO类似,代表了绑定在这个属性上的纹理对象。
(五)加载与创建纹理:
接下来我们要把存储在文件中的图片转化成二进制流,让OpenGL识别,由于图片格式有很多种,我们要写很多读取函数去读,
这些函数我们当然不必自己去写,引用一个开源的支持多种流行格式的图像加载库就好了。stb_image.h库是我们用的。
我们引用一个stbi_load函数去加载图片文件好了。
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
第一个参数接受一个图像文件的位置,剩下三个是宽度、高度和颜色通道的个数,最后一个填0.暂时不管。
(六)生成纹理:
使用前面载入的图片数据生成一个纹理
接下来就是生成纹理了,首先我们需要一个纹理对象作为处理对象,毕竟前面我们一直在做图片的工作,那些属性的设置都需要一个
对象去承载它们。
//ID引用生成对象
unsigned int texture;
glGenTextures(1, &texture);
//绑定2d纹理目标(属性),之前的哪些纹理过滤也是这个时候用
glBindTexture(GL_TEXTURE_2D, texture);
//使用前面载入的图片数据生成一个纹理,用的生成函数为glTexImage2D,用处就是根据数据生成纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
//第一个参数指定了纹理目标(Target)。第二个参数为纹理指定多级渐远纹理的级别。这里我们填0,也就是基本级别(不自动生成)。
//第三个参数告诉OpenGL我们希望把纹理储存为何种格式。我们的图像只有RGB值,因此我们也把纹理储存为RGB值
//第四个和第五个参数设置最终的纹理的宽度和高度。我们之前加载图像的时候储存了它们,所以我们使用对应的变量。
//下个参数应该总是被设为0(历史遗留的问题)。
//第七第八个参数定义了源图的格式和数据类型。我们使用RGB值加载这个图像,并把它们储存为char(byte)数组,我们将会传入对应值。
//最后一个参数是真正的图像数据。
生成纹理和多纹渐进纹理之后,我们就不需要图片数据了,这时释放它们,不要占用内存了 stbi_image_free(data);
(七)将纹理对象传给着色器,显示到屏幕上:
严格来讲之前我们只是去处理了图片数据和纹理对象,如果我们想看到纹理的样子,我们就必须用着色器去显示。
我们要把纹理对象传递到着色器内部,然后告诉它如何去显示到3d图形的每个点上。
顶点着色器:
#version 330 core layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;
out vec3 ourColor;
out vec2 TexCoord; void main() {
gl_Position = vec4(aPos, 1.0);
ourColor = aColor;
TexCoord = aTexCoord;
} 把数据(纹理数据坐标)读进来,传到片段着色器,此外我们这个时候要从程序CPU把纹理对象传入着色器了,这个最好用之前的uniform
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
void main() {
FragColor = texture(ourTexture, TexCoord);
}
texture函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是对应的纹理坐标。
texture函数会使用之前设置的纹理参数对相应的颜色值进行采样。这个片段着色器的输出就是纹理的(插值)纹理坐标上的(过滤后的)颜色。 那怎么把纹理对象赋值给着色器呢,这个不用咱们去做,咱们只需要绑定纹理对象,绑定VAO,就可调用绘制函数了,纹理对象会自动传入的。
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);