OGL中纹理分为两种类型,一种是常规的纹理,另一种是缓存纹理。前者和特定的纹理类型相关联,而后者则仅仅作为输出的缓存区域,和Frame buffer object粘合的纹理就是缓存纹理。不过这两者的创建过程很相似,都是利用glGenTexture函数实现纹理ID的创建,然后利用glBindTexture将ID号绑定到指定的纹理对象上。常规纹理绑定的纹理对象是指定的纹理类型,而纹理缓存则绑定到GL_TEXTURE_BUFFER类型的纹理。绑定之后,就可以向纹理存储传递初始化数据。前者通过glTexSubImage1D函数向指定的纹理类型提交数据,而后者则通过glTexBuffer向缓存纹理中提交数据,主要是通过GL_ARRAY_BUFFER为对应的缓存纹理提供存储支持。
Load Texture
上面简单的介绍了纹理的类型,下面主要介绍特定的纹理类型进行纹理加载的步骤。第一步需要为纹理分配指定大小的存储空间。纹理内存的空间并不一定和需要加载的纹理的大小是对应的,主要是为了渲染的时候能够有效的对图片进行放缩处理。因此,在分配内存时需要根据需要创建的纹理的尺寸以及需要创建的纹理的放缩的层次分配足够的存储,一般会比最大的纹理层次所占用的存储空间大1/3。glTexStorage2D函数的第一个参数指定所需要分配的纹理类型,在这里特定是GL_TEXTURE_2D,第二个参数levels指定所需要创建的纹理图像的层次数目,主要是用空间换时间。internalformat参数则用于指定纹理的具体类型,用以控制纹理的内存大小,接下来两个参数指定最顶层的纹理的宽度和高度。需要注意的是这里会默认下一层纹理的大小相对于上一层原来的宽度和高度缩小一半,当然也可以利用glTexSubStorage2D为每一个层次指定纹理的大小。
void glTexStorage2D( GLenum target,
GLsizei levels,
GLenum internalformat,
GLsizei width,
GLsizei height);
利用上述函数分配了内存之后,就可以利用glTexSubImage2D提交数据。glTexSubImage2D主要利用data中的数据初始化已经分配好的纹理存储,并指定提交到纹理存储中的格式format,以及data中数据的类型type;并设置了data数据初始化纹理存储中的范围,由level指定纹理缓存的层次,同时借助xoffset和yoffset设置位移起点,width和height设置范围。data中的数据可以来自于硬盘上的文件,也可以是其他方式将数据给存储到data指针中,并将该数据提交给纹理缓存。需要注意的是,glTexStorage2D和glTexImage2D函数中的width和height并不是具体的长度,而是得到一个ratio,由得到的ratio来得到最终的纹理填充范围。
void glTexSubImage2D(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const GLvoid * data);
在提交了数据之后,一个纹理对象就准备好了。在稍后的绘制过程中就可以利用纹理的颜色信息填充片元,从而实现贴图的效果。然而,由于在纹理对象中的数据和窗口的大小并非一一对应的,因此需要通过检索得到纹理对象中的数据并将这些数据提交到片元着色器中。对纹理对象中的数据的索引并不是以具体的单位进行访问的,而是以比例的形式来进行。为了更好的获取纹理对象中的数据,引入纹理坐标,纹理坐标每一个坐标的范围是[0.0,1.0]的浮点数,按比例截取纹理图像中对应的区域的数据,并将这一部分数据给贴图到对应的坐标区域。比如下面的例子:
static const GLfloat quad_data[] =
{
1.0f, -1.0f,
-1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f,
0.0f, 0.0f,
1.0f, 0.0f,
1.0f, 1.0f,
0.0f, 1.0f
};
数组的前八位数字用于表示绘制区域的顶点坐标,后八位数字则是纹理坐标。也就是将纹理从最左下端的[0.0]到最右上端的[width,height]给贴图到由顶点坐标区域处理得到的绘制区域。利用上面的信息在片元着色器中进行片元着色器颜色输出,最终输出纹理颜色。texture内置函数用于从指定的坐标中获取到纹理的颜色信息。同样需要注意的是纹理坐标定义的也是一个比例,最大值和最小值的差值表示占绘制区域的比例。如果等于0.5则表示占绘制区域的一半,大于1则表示多少个纹理组成绘制区域。
#version 330 core
in vec2 tex_coord;
layout (location = 0) out vec4 color;
uniform sampler2D tex1;
void main(void)
{
color = texture(tex1, tex_coord);
}
过滤以及填充
纹理贴图可能出现纹理图片和时机的窗口大小不一致的情况,在这样的情况下怎样进行纹理贴图操作呢?之前提到过一个纹理的层次,纹理的层次是利用提交的纹理图片生成不同尺度的纹理图片,在贴图的时候就不需要进行大范围的,直接选择一个最近的纹理图片进行小幅度的缩放就可以了,很明显是一种利用空间还时间的方法。但是小幅度的缩放以什么样的方式缩放呢?在OGL的实现中提供了两种方式,一种是GL_LINEAR,另一种是GL_NEAREST方法。前者以线性的方式平滑直到满足填充区域,而后者则是选择和当前像素最邻近的像素作为当前的填充值。当纹理坐标大于1的时候,会出现部分绘制区域不能够被绘制,那么为了填充这部分区域需要设置其他一些属性。总共包括四种填充方式:
GL_CLAMP_TO_EDGE:将纹理填充边缘的像素直接扩展到绘制区域的边缘,从而填满整个绘制区域
GL_CLAMP_TO_BORDER:仅仅将能够利用纹理区域填充的绘制区域进行填充,剩余的绘制区域则忽略不计
GL_REPEAT:将剩余的绘制区域按照纹理区域进行重复填充,直到所有的填充区域都填充满
GL_MIRRORED_REPEAT:以镜面反射的方式利用纹理区域进行绘制区域的填充
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
多纹理渲染
着色器使用多个纹理的时候,根据需要进行适当时机的激活。激活操作由函数glActiveTexture进行,这个函数以一个枚举值作为参数。在激活操作之后,第一个在绑定函数中调用的纹理对象就和前面激活的参数绑定到了一起。下面的例子中,首先激活第0号纹理,然后将tex1和第0号纹理绑定起来,那么在片元着色器中使用的第0号片元的数据就是tex1中的数据。
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, tex1);
设置片元着色器中纹理缓存的编号时,首先得到片元着色器中相应的纹理对象的位置,然后将位置修改为对应的ID号。
glUniform1i(glGetUniformLocation(base_prog, "tex1"), 0);
在片元着色器中就可以利用多个纹理特征进行数据融合了。