6.OpenGL纹理与帧缓存(笔记)

纹理综述

纹理映射(texture mapping),这一技术允许用户,在着色器中从一种特殊的表类型变量中查找数据,例如颜色值。OpenGL的所有着色阶段都允许我们访问这类纹理贴图(texture map)变量。

OpenGL支持多种纹理格式:一维纹理、三维纹理、立方体映射纹理、缓存纹理、以及数组纹理。数组纹理可以被视为一系列相同维度和格式的纹理切片,然后封装到一个纹理对象之后的结果。

纹理是由纹素(texel)组成的,其中通常包含颜色数据信息;有很多工具将纹理作为一种数据表,用在着色器中进行查询并且用于特定的工作。

使用纹理映射,需要使用以下几种步骤:

  • 创建一个纹理对象并且加载纹素数据。
  • 为顶点数据增加纹理坐标。
  • 如果在着色器中使用纹理采样器,将它关联到纹理贴图。
  • 在着色器中通过纹理采样器获取纹素数据。

基本纹理类型

OpenGL支持很多种纹理对象的类型,包括不同的维度和布局。

每个纹理对象都代表一种可以构成纹理的图片形式。

我们有必要将一个纹理对象看做是一组图片的集合,每张图片都可以独立访问,然后所有图片一起进行操作,而这一过程在概念上是不同于纹理这个词本身的。每张图片都是由一维、二维或者三维纹素的数组构成,多张图片可进行"堆叠",也就是一张图片摞在另一张上面,因此构成了一个被称作mipmap的金字塔形式。

纹理如果是由一维或二维图片切片组成的数组,这叫做数组纹理,数组中的每一个元素都称为一张切片。

立方体映射是一种特殊形式的数组纹理,它只有6的倍数个切片。一张立方体映射纹理总是有6个面,而立方体映射数组中总是有多个立方体映射纹理组成,因此它的总面数是6的倍数。

纹理也可以用来表达多重采样的表面,此时需要用到多重采样的二维或者三维数组纹理类型。多重采样是一种反锯齿的实现方案,它要求每个纹素(或者像素)都记录多个独立的颜色数据,然后再渲染流程当中将这些颜色进行合并得到最终的输出结果。一张多重采样纹理的每个纹素可能会有多个采样值(通常是2~8个)。

使用概述

纹理绑定到OpenGL环境中需要通过纹理单元(texture unit)来完成,它是一个不小于0,不大于设备所支持的最大单元数量的绑定点整数值。

如果环境支持多个纹理单元,多个纹理可以同时绑定到同一个环境当中。一旦纹理绑定到环境中,可以在着色器中通过采样器变量的方式去访问它,该变量需要提前声明,并确保声明的纹理维度和实际情况一致的。

纹理目标和对应的采样器类型
目标(GL_TEXTURE_*)采样器类型维度
1Dsampler1D一维
1D_ARRAYsampler1DArray一维数组
2Dsampler2D二维
2D_ARRAYsampler2DArray二维数组
2D_MULTISAMPLEsampler2DMS二维多重采样
2D_MULTISAMPLE_ARRAYsampler2DMSArray二维多重采样数组
3Dsampler3D三维
CUBEsamplerCube立方体映射纹理
ARRAYsamplerCubeArray立方体映射纹理数组
RECTANGLEsamplerRect二维长方形
BUFFERsamplerBuffer一维缓存
  • 长方形纹理目标(GL_TEXTURE_RECTANGLE)

    它是一种特殊的二维纹理类型,可以表达简单的长方形区域中的纹素集合;它不能有mipmap,也不能构成数组类型。长方形纹理也不支持某些纹理封装的模式。

  • 缓存纹理(GL_TEXTURE_BUFFER)

    它表示任意一维的纹素数组。与长方形纹理类似,它也没有mipmap且无法构成数组。缓存纹理的存储区域(即内存)通常是通过缓存对象来表达。因此,缓存纹理的尺寸最大边界比通常的一维纹理要大得多。

    缓存纹理的存在使得我们可以在任意着色器阶段访问诸如顶点数据这样的内容,而不需要将数据再重新复制到纹理图片中。

创建并初始化纹理

void glCreateTextures(GLenum target,GLsizei n,GLuint *textures);

返回n个当前没有使用的纹理对象名称,并保存到textures数组中。textures中返回的名称不一定是一组连续的整数值。

textures中返回的名称表示n个新创建的纹理,采用默认的状态信息以及target中设置的维度类型(例如一维、二维或者三维)。

0是一个保留的纹理名称,永远不会由glCreateTextures()返回。

void glDeleteTextures(GLsizei n,const GLuint *textures);

删除n个纹理对象,它们的名字被保存为数据textures的元素。被释放后的纹理名称可以再次使用(例如由glCreateTextures()再次分配)。

如果一个当前绑定到环境的纹理被删除了,那么这个绑定点也会被删除,相当于调用glBindTextureUnit()并设置texture参数为0。如果试图删除不存在的纹理名称,或者纹理名称为0,那么命令将被忽略且不会产生错误提示。

GLboolean gllsTexture(GLuint texture);

如果texture是一个已经被创建的纹理的名称,并没有被删除,那么返回GL_TRUE;如果texture为0或者是一个非0值,但是并非是已有的纹理名称,那么返回GL_FLASE。

创建好纹理对象的名称之后,纹理会保持target对应的默认纹理状态,但是没有任何内容。在向纹理传入数据之前,我们需要告诉OpenGL这个纹理的大小是多少。

void glTextureStorage1D(GLuint texture,GLsizei levels,GLenum internalformat,GLsizei width);
void glTextureStorage2D(GLuint texture,GLsizei levels,GLenum internalformat,GLsizei width,GLsizei height);
void glTextureStorage3D(GLuint texture,GLsizei levels,GLenum internalformat,GLsizei width,GLsizei height,GLsizei depth);

函数glTextureStorage1D()、glTextureStorage2D()和glTextureStorage3d()分别负责分配一维、二维以及三维的纹理数据。

而对于某个维度的纹理数组数据的分配而言,通常我们需要把存储空间的维度加1。也就是说,一维数组纹理的分配需要使用glTextureStorage2D(),而二维数组纹理的分配需要使用glTextureStorage3D()。立方体映射纹理可以被认为是与二维数组纹理等价的。

texture:指的是准备分配存储空间的纹理对象的名称。

levels:是分配给纹理的mipmap的层数。第0层也就是纹理的基础层,后续金字塔的每一层都会比之前的层数据要更少。

width、height和depth:表示纹理基础层的宽度、高度和深度值。对于一维数组纹理来说,height就是纹理切片的数量,而对于二维数组纹理来说,depth是切片的数量。对于立方体映射数组,可以使用glTextureStorage3D()并且设置depth为立方体映射表面的数量。在这里,depth应当设置为6的整数倍。

internalformat设置纹理存储时使用的内部数据格式。

纹理一旦分配了空间,那么它就无法被重新分配或释放了,只有纹理自己被删除的时候,才会删除对应的存储空间。

上述接口用来为纹理创建永久的存储空间。纹理存储空间的属性,也就是用来存储给定纹理中的所有纹素(以及所有mipmap层次中的纹素)的内存总量,它根据选定的内部格式和对应的分辨率来决定。

一旦使用上面的函数分配了空间,这个空间是无法被再次定义的。注意,对于纹理的不可变性而言,只有上述存储空间的属性是永久不变的;纹理内容,是可以通过glTextureSubImage2D()函数来修改。

而对于多重采样纹理,可以调用如下接口:

void glTextureStorage2DMultisample(GLuint texture,GLsizei samples,GLenum internalformat,GLsizei width,GLsizei height,GLboolean fixedsamplelocations);
void glTextureStorage3DMultisample(GLuint texture,GLsizei samples,GLenum internalformat,GLsizei width,GLsizei height,GLsizei depth,GLboolean fixedsamplelocations);

texture:用于指定多重采样纹理对象的永久纹理存储空间;

对于glTextureStorage2DMultisample()来说,texture必须是GL_TEXTURE_2D_MULTISAMPLE类型。然后设置二维多重采样纹理的存储空间,width和height:用于设置纹理的尺寸。

对于glTextureStorage3DMultisample()用来设置二维多重采样纹理数组的存储空间,texture必须是GL_TEXTURE_2D_MULTISAMPLE_ARRAY类型。对于二维多重采样纹理数组来说,widh和height:用于设置单张纹理切片的尺寸,depth用来设置数组中切片的数量。

在这两个函数中,samples设置了纹理中采样点的数值。如果fixedsamplelocations为GL_TRUE,OpenGL将会对每个纹素中的同一个采样点使用同一个子纹素位置。如果fixedsamplelocations为GL_FALSE,OpenGL会选择空间上变化的位置来匹配每个纹素中的同一个采样点。

如果要实际使用一个纹理,也就是着色器中读取它的数据,需要将它绑定到纹理单元。

void glBindTextureUnit(GLuint uint,GLuint texture);

这个接口完成了两项工作。首先,如果绑定了一个已经创建的纹理对象,那么这个纹理对象在给定纹理单元unit上会被激活。其次,如果设置绑定名称texture为0,那么OpenGL会删除当前激活的纹理单元上所有已经绑定的对象,也就是不绑定纹理的状态。

如果一个纹理对象已经初始化,那么它的维度信息应该是glCreateTextures()的target参数所设置的,也就是GL_TEXTURE_1DGL_TEXTURE_2DGL_TEXTURE_3DGL_TEXTURE_1D_ARRAYGL_TEXTURE_2D_ARRAYGL_TEXTURE_RECTANGLEGL_TEXTURE_BUFFERGL_TEXTURE_CUBE_MAPGL_TEXTURE_CUBE_MAP_ARRAYGL_TEXTURE_2D_MULTISAMPLE或者GL_TEXTURE_2D_MULTISAMPLE_ARRAY中的其中一个。

如果texture不是0,也不是通过glCreateTextures()创建的名称,那么将产生GL_INVALID_OPERATION错误。如果texture是一个已经存在的纹理对象,但是它的维度信息与target所设置的维度不匹配的话,将产生GL_INVALID_OPERATION错误。

OpenGL支持的纹理单元的最大值,可以通过GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS常量的数值来查询,在OpenGL4.0中应当至少是80个。在glBindTextureUnit()中,unit参数必须设置为0到(当前OpenGL实现所支持的)最大单元减1之间的某个值。当纹理被绑定后,可以通过着色器访问。

代理纹理

对于每一个标准的纹理目标,都可以有一个对应的代理纹理目标。

纹理目标与对应的代理目标
纹理目标(GL_TEXTURE_*)代理纹理目标(GL_PROXY_TEXTURE_*)
1D1D
1D_ARRAY1D_ARRAY
2D2D
2D_ARRAY2D_ARRAY
2D_MULTISAMPLE2D_MULTISAMPLE_ARRAY
3D3D
CUBECUBE
CUBE_ARRAYCUBE_ARRAY
RECTANGLERECTANGLE
BUFFER/

代理纹理目标用来测试OpenGL具体实现的能力,检查是否存在一些特定的限制。

举例来说:

我们可能需要某个OpenGL硬件实现能够支持最大尺寸为16384的纹理(这是OpenGL 4的最小需求)。如果某个环境可以创建16384X16384的大小,内部格式为GL_RGBA8(每个纹素存储4个字节)的纹理,那么这样一个纹理所需的总存储空间至少是1GB。如果还有mipmap或者其他内部存储的需求,那么这个值还会更大。

因此,对于可用纹理存储空间已经不足1GB的环境来说,这样的请求将会失败。如果通过代理纹理目标来请求分配这个纹理的话,硬件系统会告诉用户这个请求对于标准目标而言是否可以成功,或者必然会失败。

如果代理纹理目标对应的纹理分配失败的话,那么虚拟代理目标产生的纹理的宽度和高度都是0。如果查询代理目标的尺寸,就可以知道刚才调用是否成功,以及在实际目标上进行请求是否可以成功。

指定纹理数据

显示设置纹理数据

最简单的方法就是直接在程序中提供图像数据。纹理数据默认的排列方式:从左到右,从上到下排列。

// 下面是一个 8x8 的棋盘格图案,采用 GL_RED 和 GL_UNSIGNED_BYTE 格式的数据
static const GLubyte tex_checkerboard_data[] = 
{
    0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00,
    0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF,
    0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00,
    0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF,
    0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00,
    0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF,
    0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00,
    0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF
};
// 下面的数据表示一个2x2的纹理(红绿蓝黄四种颜色纹素),采用GL_RGBA和GL_FLOAT格式的数据
static const GLfloat tex_color_data[]=
{
    //红色纹素				绿色纹素
    1.0f,0.0f,0.0f,1.0f,	0.0f,1.0f,0.0f,1.0f,
    //蓝色纹素				黄色纹素
    0.0f,0.0f,1.0f,1.0f,	1.0f,1.0f,0.0f,1.0f
};

以下函数将数据载入到纹理对象中,

void glTextureSubImage1D(GLuint texture,GLint level,GLint xoffset,GLsizei width,GLenum format,GLenum type,const void* pixels);
void glTextureSubImage2D(GLuint texture,GLint level,GLint xoffset,GLint yoffset,GLsizei width,GLsizei height,GLenum format,GLenum type,const void* pixels);
void glTextureSubImage3D(GLuint texture,GLint level,GLint xoffset,GLint yoffset,GLint zoffset,GLsizei width,GLsizei height,GLsizei depth,GLenum format,GLenum type,const void* pixels);

替换texture所指定的纹理的某个区域中的数据,使用data所指定的新数据内容。level中设置了需要更新的mipmap层,而format和type参数指定了新的纹理数据的外部存储格式。

data:包含了这个子图像的纹理数据。

width、height和depth(如何存在):是这个子区域的大小,它会替代当前纹理图像的全部或者一部分。

xoffset、yoffset和zoffset(如果存在):分别表示x、y、z三个维度上的纹素偏移量。

target:设置要修改的纹理对象所对应的纹理目标。

如果target表示一维的数组纹理,那么yoffset和height分别指定更新后数组的第一层切片和总的切片数;否则,它们表示的就是纹理坐标。

如果target是二维数组纹理、立方体映射数组,那么zoffset和depth表示更新后数组的第一层切片和总的切片数;否则,它们表示的也是纹理坐标。

函数中指定的更新区域不能包含任何超出原始纹理数组范围的纹素数据。

以下是将数据加载到纹理对象中的过程:

//首先是黑白相间的棋盘格纹理
//分配纹理数据的存储空间
glTextureStorage2D(tex_checkerboard,4,GL_R8,8,8);
//设置纹理数据
glTexturesSunImage2D(tex_checkerboard,			 //纹理
					 0,							 //mipmap层 0
                     0,0,						//x和y偏移
                     8,8,						//宽度和高度
                     GL_RED,GL_UNSIGNED_BYTE,	//格式和类型
                    tex_checkerboard_data);		//数据
//下一个是浮点数的颜色值,分配存储空间
glTextureStorage2D(tex_color,2,GL_RGBA32F,2,2);
//设置纹理数据
glTextureSubImage2D(tex_color,					//纹理
					0,							//mipmap层0
					0,0,						//x和y偏移
                    2,2,						//宽度和高度
                    GL_RGBA,GL_FLOAT,			//格式和类型
                    tex_color_data);			//数据

设置纹理的内部格式,需要和提供的纹理数据相互匹配。对于无符号字节所组成的数组数据,可以使用内部格式GL_R8,也就是单通道的8位格式。对于颜色数据,我们使用GL_RGBA32F,也就是4通道32位浮点数格式。

从缓存中加载纹理

glTextureSubImage2D()的data参数,可以有两种用途,进行纹理数据的设置:

  1. 通过用户程序中存储的自然数据指针,设置纹理数据

  2. 通过绑定到GL_PIXEL_UNPACK_BUFFER目标的缓存对象来完成,作为缓存对象的偏移位置。

    用户程序此时可以将数据存储到缓存对象中,然后再传递给纹理对象。

    如果GL_PIXEL_UNPACK_BUFFER目标没有绑定任何缓存对象,那么data会被解释成一个本地的指针,如果绑定了缓存,那么data会被解释成缓存中的一个偏移位置。

//创建缓存对象
glCreateBuffers(1,&buf);

//将源数据传递到缓存中
glNamedBufferStorage(buf,
                    sizeof(tex_checkboard_data),
                    tex_checkboard_data,
                    0);

//分配纹理数据的存储空间
glTextureStorage2D(texture,4,GL_R8,8,8);

//把缓存绑定到GL_PIXEL_UNPACK_BUFFER
glBindBuffer(GL_PIXEL_UNPACK_BUFFER,buf);

//设置纹理数据
glTextureSubImage2D(texture,			//目标纹理
                   	0,					//mipmap第0层
                    0,0,				//x和y偏移
                    8,8,				//宽度和高度
                    GL_RED,				//格式
                    GL_UNSIGNED_BYTE,	//数据类型
                   NULL);				//数据(缓存中的偏移地址)

使用缓存对象来存储纹理数据的一个主要的好处在于:数据不是立即从缓存对象向纹理进行传输的,而是在着色器请求数据的时候才会执行这一操作。因此应用程序的运行和数据的传输操作可以并行进行。

如果数据在应用程序本地内存中,那么glTextureSubImage2D()需要先对数据进行拷贝,然后函数才会返回,这是不可能并行完成的。不过这种方法好处在于:应用程序在函数返回之后,依然可以自由修改之前传输的data数据。

从文件加载图像

大多数的用户程序,会选择将纹理数据存储到一些特定格式的图像文件当中,例如JPEG、PNG、GIF,或者其他一些格式。OpenGL的纹理可以使用无压缩的像素数据,也可以使用特定算法压缩后的数据。

因此,用户程序需要设法将图像文件解码到文件中,然后OpenGL读取数据并初始化内部的纹理存储空间。

vglLoadImage()的函数,它可以直接读取一个图像文件,并返回内存中的纹素数据,同时会传递其他一些信息,帮助OpenGL对像素数据进行解析,主要包括:

  • 宽度(以像素为单位)
  • 高度(以像素为单位)
  • OpenGL像素格式(例如GL_RGB表式RGB形式的像素点)
  • 建议在纹理中使用的内部格式
  • 纹理中mipmap的层次数量
  • 像素中每个分量的数据类型
  • 图像数据本身

这些数据会存储在vglImageData类型的结构体中,它是在LoadImage.h中定义。

//OpenGL 4.x以及更高版本中所需的mipmap的最大层级数量,对于16K x 16K的纹理也是足够的
#define MAX_TEXTURE_MIPS	14

//每个纹理图像数据的结构体中都会包含一个MAX_TEXTURE_MIPS大小的数组来记录mipmap的信息。
//这个结构体定义了某一级mipmap数据的所有信息
struct vglImageMipData
{
    GLsizei width;			//该级mipmap的宽度
    GLsizei height;			//该级mipmap的高度
    GLsizei	depth;			//该级mipmap的深度
    GLsizeiptr mipStride;	//相邻级别的mipmap在内存中的距离
    GLvoid* data;			//数据指针
};

//主要的图像数据结构体,其中包含了所有必要的OpenGL参数,可以将纹理数据传递到纹理对象中
struct vglImageData
{
    GLenum target;				//纹理目标(二维,立方体映射等)
    GLenum internalFormat;		//推荐的内部格式
    GLenum format;				//内存中的格式
    GLenum type;				//内存中的数据类型(GL_RGB等)
    GLenum swizzle[4];			//RGBA分量的乱序设置(swizzle)
    GLsizei	mipLevels;			//mipmap的层次数量
    GLsizei slices;				//(对于纹理数组)切片的数量
    GLsizeiptr sliceStride;		//纹理数组中相邻切片之间的距离
    GLsizeiptr totalDataSize;	//纹理总共分配的数据大小
    vglImageMipData	mip[MAX_TEXTURE_MIPS];		//实际的mipmap数据
};

对于内存中图像数据的创建、初始化、属性修改和删除操作,定义了两个函数:

void vglLoadImage(const char* filename,vglImageData* image);
void vglUnloadImage(vglImageData* image);

vglLoadImage()负责从磁盘文件中加载图像。filename指定要加载的文件名称。image传入的一个vglImageData结构体地址,如果文件加载成功,图像数据和参数将会被填充进来。如果失败,image将会被清楚。

vglUnloadImage()负责释放上一次成功调用vglLoadImage()消耗的所有资源。

GLuint LoadTexture(const char* filename,GLuint texture,GLboolean generateMips)
{
    vglImageData image;
    int level;
    
    vglLoadImage(filename,&image);
    
    if(texture == 0)
    {
        glCreateTexture(image.target,1,&texture);
    }
    
    swith(image.target)
    {
        case GL_TEXTURE_2D:
        	glTextureStorage2D(texture,
            				  image.mipLevels,
                              image.internalFormat,
                              image.mip[0].width,
                              image.mip[0].height);
        //这里还可以处理其他纹理目标
        default:
        	break;
    }
    
    //假设这是一个二维纹理
    for(level = 0;level < image.mipLevels;++level)
    {
        glTextureSubImage2D(texture,
        					level,
                           0,0,
                           image.mip[level].width,
                           image.mip[level].height,
                           image.format,image.type,
                           image.mip(level).data);
    }
    
    //现在可以卸载图像数据了,因为glTexSubImage2D已经使用了图像,我们在本地不再需要它了
    vglUnloadImage(&image);
    
    return texture;
}

调用vglLoadImage()之后,指定的图像文件中的纹理数据被加载到内存中,同时图像数据的相关参数,将被存储到vglImageData结构体中。

然后需要将图像数据和纹理的维度参数传递给正确的纹理图像函数。首先将纹理分配给一个永久性的对象(例如glTextureStorage2D()),然后将图像数据设置为纹理子图像(例如glTextureSubImage2D())。

上述代码是一个简化版本,事实上这个函数,还可以处理其他维度的图像、数组纹理、立方体映射、压缩纹理,以及其他可以通过vglLoadImage()函数读取的内容。

GLuint vglLoadTexture(const char* filename,GLuint texture,vglImageData* image);

从磁盘加载纹理并将它传递给一个OpenGL纹理对象。

filename:设置了要加载的文本名称。

texture:设置了纹理对象的名称,我们将把数据加载到其中。如果texture是0,vglLoadTexture()会创建一个新的纹理对象来存储图像数据。

image:是vglImageData结构体的地址,可以用来存储函数返回的图像参数数据。如果不是NULL,那么它将负责记录图像的参数数据,并且不会主动释放本地的图像数据,用户需要使用vglUnloadImage()来释放图像相关的所有资源。如果是NULL,那么将使用内部的数据结构体来加载图像,并且自动释放本地的图像数据。

函数成功,返回一个纹理对象名称,纹理图像已经加载到其中。如果texture非0,那么返回值应当与texture相同;否则新建一个纹理对象并返回。函数运行失败,vglLoadTexture()返回0。

获取纹理数据

当我们向纹理中传输了数据之后,可以再次读取数据并传递回用户程序的本地内存中,或者传递给一个缓存对象。

void glGetTextureImage(GLuint texture,GLint level,GLenum format,GLenum type,GLsizei bufSize,void* pixels);

从纹理texture中获取图像数据。

level:表示细节层次的层数。

format和type:表示所需数据的像素格式和数据类型。

pixels:可以被理解为用户内存中的一个地址,用来存储图像数据,或者如果当前有缓存对象绑定到GL_PIXEL_PACK_BUFFER,这里设置的就是图像数据传递到缓存对象时的数据偏移地址。

使用该函数时需要特别小心,具体写入到pixels中的数据是由当前绑定的纹理的维度、format和type参数共同决定的。因此它可能会返回一组巨大的数据,并且OpenGL并不会对用户进行任何边界检查。

此函数从纹理中回读像素数据并不是很高效率的操作。建议使用GL_PIXEL_PACK_BUFFER绑定缓存对象的方式,然后将纹素回读到缓存中,可以再把缓存映射到内存里,从而将像素数据传递给用户程序。

纹理数据的排列布局

多数情况下,图像数据的布局是按照左到右,从上到下的顺序存放在内存中,并且各个纹素之间是紧密排列的。也可以用户自己描述程序中的图像数据布局方式。

void glPixelStorei(GLenum pname,GLint param);
void glPixelStoref(GLenum pname,GLfloat param);

设置像素存储的参数pname以及对应的数值param。param必须是下面的像素解包(unpack)参数名称之一:

GL_UNPACK_ROW_LENGTHGL_UNPACK_SWAP_BYTESGL_UNPACK_SKIP_PIXELSGL_UNPACK_SKIP_ROWSGL_UNPACK_SKIP_IMAGESGL_UNPACK_ALIGNMENTGL_UNPACK_IMAGE_HEIGHT、或GL_UNPACK_LSB_FIRST

或者打包(pack)参数名称之一:

GL_PACK_ROW_LENGTHGL_PACK_SWAP_BYTESGL_PACK_SKIP_PIXELSGL_PACK_SKIP_ROWSGL_PACK_SKIP_IMAGESGL_PACK_ALIGNMENTGL_PACK_IMAGE_HEIGHT,或GL_PACK_LSB_FIRST

解包参数(以GL_UNPACK_开头)设置的是OpenGL从用户内存或者GL_PIXEL_UNPACK_BUFFER中读取数据的布局方式,例如glTextureSubImage2D()的时候。

而打包参数设置的是OpenGL将纹理数据写入内存中的布局方式,例如运行glGetTextureImage()的时候。

  • *SWAP_BYTES

    如果默认为GL_FALSE(默认),那么用户内存中的字节顺序保持原样;否则字节将进行反转。字节反转的操作会应用到每一个数据元素上,但是它仅对多字节的元素有意义。

    假设OpenGL实现中定义了GLubyte为8位、GLushort为16位、GLuint为32位,可以看到对于单字节的数据没有作用。
    在这里插入图片描述

  • *LSB_FIRST

    只对1位的图像数据(也就是每个像素只有1位的数据)的绘制和读取起作用。如果设置为GL_FALSE(默认),那么系统将会从最高位开始读取数据;否则系统将会沿着相反的方向进行处理。

    举例来说,如果为GL_FLASE,而给定的字节数据是0x31,那么位数据的读取顺序是{0,0,1,1,0,0,0,1}。如果为GL_TRUE,那么顺序为{1,0,0,0,1,1,0,0}。

  • *ROW_LENGTH、*SKIP_ROWS、*SKIP_PIXELS

    有时候用户希望从内存中的图像数据选取一个子矩形,然后绘制或者读取其中的子数据。如果内存数据的实际矩形比给定的子矩形要大,那么我就需要使用*ROW_LENGTH来设置较大矩形的实际长度(以像素为单位)。

    如果*ROW_LENGTH为0(默认),那么我们认为内存数据的每行长度和glTextureSubImage2D()设置的宽度值是相等的。

    还需要指定内存数据起始的多行或者多个像素是否需要被忽略,然后开始拷贝子矩形的数据。这通过SKIP_ROWSSKIP_PIXELS来完成。默认值为0,数据从左下角开始读取。
    在这里插入图片描述

  • *ALIGNMENT

    参数设置为1,那么每个字节之间是紧密排列的。

    设置为2,在每一行的末尾都会自动空出2个字节,因此每一行在内存中的地址都是2的倍数。

    对于位图(或者1位图像)来说,每个像素都存储为一位,这种字节对齐的方式依然有效,但是需要自己对独立的为数据进行计数。

    例如:

    如果每个像素只有一位,而每行的长度为75,字节对齐参数设置为4,那么每一行就是需要75/8个字节;而大于75/8的数字,同时又是4的倍数的最小数值为12,那么每一行需要12字节的内存空间。

    若对齐参数为1,那么每一行需要10字节,75/8取整数为10。

    ALIGNMENT的默认值是4。因此一个常见的编程错误就是把图像数据当作时紧密排列的,并且字节紧密对齐(也就是自认为ALIGNMENT是1)。

  • IMAGE_HEIGHTSKIP_IMAGES

    用于影响三维纹理和二维纹理数组的定义和查询。这两个参数设置的是像素存储,可以实现glTextureSubImage3D()glGetTextureImage()函数访问纹理数组的子切片时的空间区域设置。

    IMAGE_HEIGHT是一个以像素为单位的参数,用来设置三维纹理图像中单层切片的高度(行数)。如果设置为0(该值不能为负,默认参数也是0),那么每个二维矩形图像的行数,就是三维纹理的高度值——glTextureSubImage3D()中的传递的参数。否则,单层高度为IMAGE_HEIGHT
    在这里插入图片描述
    SKIP_IMAGES定义了可用的子区域数据之前还需要跳过的图像层数。如果为正整数(例如n),那么纹理图像数据中的指针首先递增n层(n每层的纹素数据大小)。得到的结果子区将从第n层切片开始,并延续一定的层数。这个层数是通过glTextureSubImages3D()中传递的深度来决定的。如果为0(默认),那么系统将从纹素数组的第一层开始读取纹素数据。
    在这里插入图片描述

纹理格式

函数glTextureStorage1D()glTextureStorage2D()glTextureStorage3D()都需要设置一个internalformat参数,它负责判断OpenGL存储内部纹理数据的格式。

内部格式

纹理的内部格式(internal format)也就是OpenGL内部用来存储用户提供的纹理数据的格式。OpenGL支持一系列的内部格式用来存储图像数据,每个格式都有尺寸、性能和画面质量上的权衡比重。下表给出了所有OpenGL支持的内部格式,以及它们对于每个分量设置的位尺寸。

含有尺寸信息的内部格式定义
内部格式(含尺寸)内部格式(基础)R位G位B位A位共享位
GL_R8GL_RED8
GL_R8_SNORMGL_REDs8
GL_R16GL_RED16
GL_R16_SNORMGL_REDs16
GL_RG8GL_RG88
GL_RG8_SNORMGL_RGs8s8
GL_RG16GL_RG1616
GL_RG16_SNORMGL_RGs16s16
GL_R3_G3_B2GL_RGB332
GL_RGB4GL_RGB444
GL_RGB5GL_RGB555
GL_RGB565GL_RGB565
GL_RGB8GL_RGB888
GL_RGB8_SNORMGL_RGBs8s8s8
GL_RGB10GL_RGB101010
GL_RGB12GL_RGB121212
GL_RGB16GL_RGB161616
GL_RGB16_SNORMGL_RGBs16s16s16
GL_RGBA2GL_RGBA2222
GL_RGBA4GL_RGBA4444
GL_RGB5_A1GL_RGBA5551
GL_RGBA8GL_RGBA8888
GL_RGBA8_SNORMGL_RGBAs8s8s8s8
GL_RGB10_A2GL_RGBA1010102
GL_RGB10_A2UIGL_RGBAui10ui10ui10ui2
GL_RGBA12GL_RGBA12121212
GL_RGBA16GL_RGBA16161616
GL_RGBA16_SNORMGL_RGBAs16s16s16s16
GL_SRGB8GL_RGB888
GL_SRGB8_ALPHA8GL_RGBA8888
GL_R16FGL_REDf16
GL_RG16FGL_RGf16f16
GL_RGB16FGL_RGBf16f16f16
GL_RGBA16FGL_RGBAf16f16f16f16
GL_R32FGL_REDf32
GL_RG32FGL_RGf32f32
GL_RGB32FGL_RGBf32f32f32
GL_RGBA32FGL_RGBAf32f32f32f32
GL_R11F_G11F_B10FGL_RGBf11f11f10
GL_RGB9_E5GL_RGB9995
GL_R8IGL_REDi8
GL_R8UIGL_REDui8
GL_R16IGL_REDi16
GL_R16UIGL_REDui16
GL_R32IGL_REDi32
GL_R32UIGL_REDui32
GL_RG8IGL_RGi8i8
GL_RG8UIGL_RGui8ui8
GL_RG16IGL_RGi16i16
GL_RG16UIGL_RGui16ui16
GL_RG32IGL_RGi32i32
GL_RG32UIGL_RGui32ui32
GL_RGB8IGL_RGBi8i8i8
GL_RGB8UIGL_RGBui8ui8ui8
GL_RGB16IGL_RGBi16i16i16
GL_RGB16UIGL_RGBui16ui16ui16
GL_RGB32IGL_RGBi32i32i32
GL_RGB32UIGL_RGBui32ui32ui32
GL_RGBA8IGL_RGBAi8i8i8i8
GL_RGBA8UIGL_RGBAui8ui8ui8ui8
GL_RGBA16IGL_RGBAi16i16i16i16
GL_RGBA16UIGL_RGBAui16ui16ui16ui16
GL_RGBA32IGL_RGBAi32i32i32i32
GL_RGBA32UIGL_RGBAui32ui32ui32ui32

大多数情况下,我们只需要一个尺寸参数即可。这种时候所有的像素分量都是按照同样的位尺寸来存储的。默认情况下,OpenGL会将纹理数据存储为无符号归一化(unsigned normalized)的格式。

  • 无符号归一化

    这种情况下,纹素保存在内存中的值是一个整数,而它被读取到着色器之后将会除以对应整数类型的最大值,并转换为浮点数形式。

    因此着色器中的数据结果将被限制在0.0~1.0的范围之内(即归一化)。

  • 有符号归一化

    如果类型中有**_SNORM**的后缀(例如GL_RGB8_SNORM),那么数据就是有符号归一化(signed normalized)的形式。

    这种情况下,内存中的数据将被视为一个有符号的整数,当我们在着色器读取它的时候,它会除以有符号整型类型的最大值,然后转换为浮点数,因此着色器中得到的浮点数结果被限制在-1.0~1.0的范围内。

  • 类型标识符

    内部格式的名称中包含的类型标识符,有IUIF,分别表示有符号整数、无符号整数、以及浮点数。

    有符号和无符号的整数类型,内部格式分别对应于着色器中的有符号和无符号采样器(例如isampler2D和usampler2D)。

    浮点数类型的内部格式,则会直接在内存中保存真正的浮点数据,并且在着色器中返回的数值也是全精度的浮点数(根据具体OpenGL实现的支持)。在这种情况下,纹素对应的浮点数据范围不一定在-1.0~1.0的范围内。

  • 不同通道使用不同尺寸的标识符

    有时OpenGL会使用不同位大小的内存来存储不同的通道。例如:

    GL_RGB10_A2类型的纹理的每个纹素都有32位的大小,其中红色、绿色和蓝色通道分别有10位的存储空间,而alpha通道只有2位的存储空间。这种格式的纹理对于表达高动态范围图像是非常有用的,其中不需要过多的不透明度级别(或者使用alpha通道来存储其他属性的数据,而不是传统的不透明度)。

    GL_R11F_G11F_B10F类型使用11位的空间来存储红色和绿色通道,10位的空间存储蓝色通道,每个通道中存储的是特殊类型的低精度浮点数。这种11位的分量并没有符号位,而是由5位的指数位(exponent)和6位的尾数位(mantissa)组成。

    GL_RGB9_E5格式非常特殊,它采用一种共享指数(shared exponent)的格式进行存储。每个分量都单独存储了9位的尾数信息,但是5位的指数数据是所有分量一起存储的。这样纹理就可以保存为相对高动态范围的格式,但是每个纹素的存储空间还需要使用16位。

    GL_SRGB8GL_SRGB8_ALPHA8格式都是建立在sRGB颜色空间的RGB纹理,前者不带有alpha通道,而后者带有alpha通道。在GL_SRGB8_ALPHA8格式中的alpha通道是单独存储的,因为它并不属于sRGB颜色空间,因此也不应当受到其他分量的gamma运算的影响。

外部格式

所谓的外部格式(external format)指的是用户向OpenGL API提供数据时所用的格式,它是通过glTextureSubImage2D()这样的函数中的format和type参数来设置的。

format描述了每个像素数据的分量组成方式,也可以使用可选的INTEGER后缀;此外,我们可以使用一种打包整数(packed integer)的格式来表达打包之前的纹理数据,然后使用内部格式将它们保存到纹理中。

外部纹理格式
格式分量的表达
GL_RED红色
GL_GREEN绿色
GL_BLUE蓝色
GL_RG红色、绿色
GL_RGB红色、绿色、蓝色
GL_RGBA红色、绿色、蓝色、alpha
GL_BGR蓝色、绿色、红色
GL_BGRA蓝色、绿色、红色、alpha
GL_RED_INTEGER红色(整数)
GL_GREEN_INTEGER绿色(整数)
GL_BLUE_INTEGER蓝色(整数)
GL_RG_INTEGER红色、绿色(整数)
GL_RGB_INTEGER红色、绿色、蓝色(整数)
GL_RGBA_INTEGER红色、绿色、蓝色、alpha(整数)
GL_BGR_INTEGER蓝色、绿色、红色(整数)
GL_BGRA_INTEGER蓝色、绿色、红色、alpha(整数)

如果使用**_INTEGER**后缀,数据传递到OpenGL的时候,会被看待为一个没有归一化的整数数据,但如果内部格式是浮点数格式,这个数据会直接转换为浮点数的形式,输入的数据类型在这里并不重要。

若想在着色器中直接获取整数,可使用整型采样器类型,以及整型的内部格式(如GL_RGBA32UI),同时还有整型的外部数据格式和类型(例如GL_RGBA_INTEGER和GL_UNSIGNED_INT)。

type通常如下:

外部数据类型
标识符含义对应数据类型
GL_BYTE有符号byteGLbyte
GL_UNSIGNED_BYTE无符号byteGLubyte
GL_SHORT有符号shortGLshort
GL_UNSIGNED_SHORT无符号shortGLushort
GL_INT有符号intGLint
GL_UNSIGNED_INT无符号intGLuint
GL_HALF_FLOAT半精度浮点数GLhalf
GL_FLOAT全精度浮点数GLfloat
GL_DOUBLE双精度浮点数GLdouble

除了这些传统类型的标识符,还有一些特殊的标识符来表达打包格式或者混合类型的格式。这类标识符名称通常是由标准类型标记(例如GL_UNSIGNED_INT)和表达数据在内存中的排列方式的后缀共同组成。

格式标识符分量布局
GL_UNSIGNED_BYTE_3_3_2
R
G
B
GL_UNSIGNED_BYTE_2_3_2_REV
B
G
R
GL_UNSIGNED_SHORT_5_6_5
R
G
B
GL_UNSIGNED_SHORT_5_6_5_REV
B
G
R
GL_UNSIGNED_SHORT_4_4_4_4
R
G
B
A
GL_UNSIGNED_SHORT_4_4_4_4_REV
A
B
G
R
GL_UNSIGNED_SHORT_5_5_5_1
R
B
G
A
GL_UNSIGNED_SHORT_1_5_5_5_REV
A
B
G
R
GL_UNSIGNED_INT_10_10_10_2
R
B
G
A
GL_UNSIGNED_INT_2_10_10_10_REV
A
B
G
R
GL_UNSIGNED_INT_10F_11F_11F_REV
B
G
R
GL_UNSIGNED_INT_5_9_9_9_REV
A
B
G
R

压缩纹理

压缩是一种降低存储或者信息传输所需的数据总量的方法。由于纹理数据会消耗大量的内存(包括内存带宽),OpenGL支持压缩形式的纹理存储方法来降低纹理的尺寸。

OpenGL中有两种可以传输压缩纹理数据的方法:

  1. 由OpenGL负责压缩数据,我们可以直接提供未压缩数据,设置一个压缩格式的内部格式。OpenGL的实现平台会使用未压缩的、原始的纹理数据并尝试进行压缩。由于这个过程是实时进行的,因此压缩器通常只会实现一个非常简单的算法,以便快速地压缩数据,而得到的压缩纹理结果质量并不会很高。
  2. 采用离线的方式(在程序运行之前)压缩纹理数据,然后将结果直接传递给OpenGL。

无论哪种方法,第一步都要做选择一种压缩形式的内部格式。纹理压缩算法和格式的种类繁多,而不同的硬件和OpenGL实现也会支持不同类型的格式,大部分都是采用扩展功能的形式实现,如果想知道你所使用的OpenGL平台能够支持什么格式,可以直接查阅它的扩展功能列表。

有两种最常用且肯定被OpenGL本身支持的压缩格式,它们就是RGTC(Red-Green 纹理压缩算法)和BPTC(Block Partitioned 纹理压缩算法)。这两种格式是基于区块的,可按照4x4的纹素来保存数据,也就是说,每幅图像都是按照4x4的纹素区块形式来记录的,而每个区块会独立进行压缩。

如果我们需要为压缩格式的纹理数据分配永久存储空间,还是需要使用之前提到过的glTextureStorage1D()glTextureStorage2D()glTextureStorage3D()函数。

当设置压缩数据的时候,数据的绝对尺寸是由压缩格式决定的。因此所有的图像压缩数据函数都需要一个参数来设置个尺寸(字节为单位)。用户程序要负责确保这个尺寸是正确的,以及传递给OpenGL的数据必须是合法且匹配函数参数的压缩格式。

void glCompressedTextureSubImage1D(GLuint texture,GLint level,GLint xoffset,GLsizei width,GLenum format,GLsizei imageSize,const void* data);
void glCompressedTextureSubImage2D(GLuint texture,GLint level,GLint xoffset,GLint yoffset,GLsizei width,GLsizei height,GLenum format,GLsizei imageSize,const void* data);
void glCompressedTextureSubImage3D(GLuint texture,GLint level,GLint xoffset,GLint yoffset,GLint zoffset,GLsizei width,GLsizei height,GLsizei depth,GLenum format,GLsizei imageSize,const void* data);

更新压缩过的纹理数据,纹理名称为texture,层级为level。

xoffset和width:负责设置x方向的偏移值和纹理数据的宽度(以纹素为单位)。

yoffset和height:对于二维和三维纹理,用于设置y方向的偏移值和纹理数据的高度;对于一维数据纹理,用于设置数组中的起始切片和要更新的切片数量。

zoffset和depth:对于三维纹理,设置了z方向的偏移值和纹理数据的深度;对于二维数组纹理来说,用于设置数组中的起始切片和要更新的切片数量。

format:设置了压缩图像数据的格式,它必须和纹理的内部格式匹配。

imageSize和data:设置了准备更新的纹理数据的尺寸以及数据的地址。

采样器对象

我们可以通过着色器中带有纹理单元信息的采样器变量来读取纹理,使用GLSL内置的函数从纹理图像中读取纹素。纹素读取的方式依赖于另一个对象中的参数,名为采样器对象(sampler object)。采样器对象会绑定到采样器单元,默认情况下,每一个纹理对象中都包含了一个内置的采样器对象,如果没有把采样器对象绑定到专门的采样器单元,这个对象可以用来从纹理中读取数据。

void glCreateSamplers(GLsizei n,GLuint *samplers);

返回n个保存在数组samplers中的采样器对象的名称。samplers中返回的对象名称表示新初始化的一系列采样器对象,使用默认的状态。

0是一个保留字,不会返回这个数值。

创建好名称之后,就可以绑定到对应的采样器绑定点了。

void glBindSampler(GLuint uint,GLuint sampler);
void glBindSamplers(GLuint first,GLsizei count,const GLuint *samplers);

glBindSampler()将一个采样器对象sampler绑定到uint所设置的采样器单元。

glBindSamplers()则是将多个采样器对象绑定到连续的采样器单元。这里的第一个单元通过参数first给出,采样器单元的数量通过count给出,而samplers指向采样器对象名称的数组。

如果sampler或者samplers中某个元素为0,那么对应的目标单元中已经绑定的采样器对象将被解除绑定,当前单元位置恢复为未绑定的状态。

GLboolean glIsSampler(GLuint id);

如果Id是一个已知的采样器对象,返回GL_TRUE,否则返回GL_FLASE。

注意glBindSampler()和glBindSamplers()并没有target参数,这一点与glBindTextureUnit()函数不同。这是因为采样器没有目标的概念,它们总是与纹理相关联。采样器对象并没有内在的维度概念,因此也就没有必要在多种采样器对象类型中进行区分了。

采样器参数

每个采样器对象都对应了一系列用于控制纹理对象中纹素读取过程的参数。

void glSamplerParameter{fi}(GLuint sampler,GLenum pname,Type param);
void glSamplerParameter{fi}v(GLuint sampler,GLenum pname,const Type* param);
void glSamplerParameterI{i ui}v(GLuint sampler,GLenum pname,const Type* param);

对于名为sampler的采样器对象,设置参数pname的数值为param给定的值。

glSamplerParameteri()的param是一个整数值;

glSamplerParameterf()的param是一个浮点数值;

glSamplerParameteriv()中的param是一个整数值数组的地址;

glSamplerParameterfv()中的param是一个浮点数数组的地址。

每个纹理对象都会默认包含一个默认的采样器对象,可以在不需要绑定到相应采样器单元的前提下,直接从纹理中读取数据。

void glTextureParameter{fi}(GLuint texture,GLenum pname,Type param);
void glTextureParameter{fi}v(GLuint texture,GLenum pname,const Type *param);
void glTextureParameterI{i ui}v(GLuint texture,GLenum pname,const Type *param);

设置纹理对象texture的参数pname的数值为param给定的值。

glTextureParameteri()中的param是一个整数值;

glTextureParameterf()中的param是一个浮点数值;

glTextureParameteriv()和glTextureParameterIiv()中的param是一个整数值数组的地址;

glTextureParameterfv()中的param是一个浮点数数组的地址;

glTextureParameterIuiv()中的param是一个无符号整数值数组的地址。

如果pname设置的是采样器对象的参数,那么函数会访问这个纹理的内置默认采样器对象。

void glDelteSamplers(GLsizei count,const GLuint *samplers);

删除count个采样器,它们的名称保存在数组samplers中。删除之后,samplers中的名称都会恢复到未使用的状态,可以经由glCreateSamplers()的调用再次返回。

纹理的使用

当创建和初始化一个纹理对象,并且将图像数据传递进去后,就可以在程序中通过着色器来读取它了。

着色器中的纹理是通过不同维度和类型的采样器变量来表达的。

每个采样器变量都是由纹理对象传入的图像数据,以及采样器对象(或者纹理内置的采样器对象)传入的采样参数来共同定义的。

纹理需要绑定到纹理单元上,而采样器需要绑定到对应的采样器单元上,它们共同作用实现纹理图像的读取。

这一过程被称作采样(sampling)。

gvec4 texture(gsampler1D tex,float P[,float bias]);
gvec4 texture(gsampler2D tex,vec2 P[,float bias]);
gvec4 texture(gsampler3D tex,vec3 P[,float bias]);
gvec4 texture(gsamplerCube tex,vec3 P[,float bias]);
gvec4 texture(gsampler1DArray tex,vec2 P[,float bias]);
gvec4 texture(gsampler2DArray tex,vec3 P[,float bias]);
gvec4 texture(gsampler2DRect tex,vec2 P);
gvec4 texture(gsamplerCubeArray tex,vec4 P[,float bias]);

从名为tex的采样器中采样一个纹素,对应的纹理坐标为P。

如果对象支持mipmap,并且设置了bias,那么这个参数将被用于mipmap细节层次(level-of-detail)的偏移量计算,来判断采样应当在哪一层进行。

函数的返回值是一个包含了采样后的纹理数据的向量。

对于很多GLSL函数的原型,可以看到gvec4(或者其他维度的向量)的定义,它实际上是一个"占位符"类型,标识任何类型的一个向量,它可以用来表达vec4、ivec4或uvec4。

gsampler2D也是一个这样的占位符,它可以表达sampler2D、isampler2D或者usampler2D类型。

方括号([和]),说明这个参数是可选的,可以忽略不计。

简单使用示例:

//顶点着色器
#version 330 core

layout (location = 0) in vec4 in_position;
layout (location = 1) in vec2 in_tex_coord;

out vec2 vs_tex_coord;

void main(void)
{
    gl_Position = in_position;
    vs_tex_coord = in_tex_coord;
}

//片元着色器
#version 330 core

uniform sampler2D tex;

in vec2 vs_tex_coord;

layout (location = 0) out vec4 color;

void main(void)
{
    color = texture(tex,vs_tex_coord);
}

  • 顶点着色器

    两个输入变量是顶点的位置和纹理坐标,它们直接被传递到着色器的输出变量中。分别是内置输出gl_Position,以及用户定义的输出vs_tex_coord,并且传递到着色器中,其中包含了名称相同的输入变量。

  • 片元着色器

    该阶段从纹理中读取数据。纹理可以在任何着色器阶段使用,但是只有片元着色器中最适合表现纹理所产生的效果。

    着色器中,声明了一个二维uniform采样器tex,输入变量vs_tex_coord用作纹理坐标,输出一个颜色值color。

纹理坐标

纹理坐标也就是纹理中的坐标,用于对图像进行采样。通常是按照逐顶点的方式来设置的,然后对结果几何体区域进行插值来获得逐片元的坐标值。这个坐标值是在片元着色器中使用的,以便读取纹理数据并返回纹理中的颜色值作为结果片元。

以下为简单设置纹理坐标的用户程序代码。

// prog是已经链接的着色器对象,其中包含示例的顶点和片元着色器
glUseProgram(prog);

//tex是纹理对象的名称,已经经过了初始化并设置了纹理数据
glBindTexture(GL_TEXTURE_2D,tex);

//一个简单的四边形,由纹理坐标设置
static const GLfloat quad_data[] = 
{
    //顶点位置
    -1.0f,-1.0f,0.0f,1.0f,
    1.0f,-1.0f,0.0f,1.0f,
    1.0f,1.0f,0.0f,1.0f,
    -1.0f,1.0f,0.0f,1.0f,
    //纹理坐标
    0.0f,0.0f,
    1.0f,0.0f,
    1.0f,0.0f,
    0.0f,0.0f
};

//创建并初始化一个缓存对象
GLuint buf;

glGenBuffers(1,&buf);
glBindBuffer(GL_ARRAY_BUFFER,buf);
glBufferData(GL_ARRAY_BUFFER,quad_data,sizeof(quad_data),GL_STATIC_DRAW);

//设置顶点属性
GLuint vao;

glGenVertexArrays(1,&vao);
glBindVertexArray(vao);
glVertexAttribPointer(0,4,GL_FLOAT,GL_FALSE,0,(GLvoid*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1,2,GL_FLOAT,GL_FALSE,0,(GLvoid*)(16*sizeof(float)));
glEnableVertexAttribArray(1);

//现在可以绘制了
glDrawArrays(GL_TRIANGLE_FAN,0,4);

此例中,prog是着色器程序对象的名称,已经编译和链接了之前的着色器代码,而tex是一个纹理对象,已经加载了纹理的数据。

GLSL中的每个纹理查找函数都需要一系列的坐标来进行纹素采样。将纹理视为一片区域,它的覆盖范围会沿着每个坐标轴从0.0扩展到1.0。通常来说,它们会以顶点输入的形式被传递到顶点着色器中,然后由OpenGL负责沿着每个多边形的每个面进行数据的插值,然后传递到片元着色器中。纹理坐标范围是0.0~1.0,因此所有的插值坐标结果也都会落在这个范围之内。

若传递给纹理查找函数的纹理坐标超出了0.0~1.0的取值范围,那么它们必须经过重新修改以使正确落在范围之内。

OpenGL通过几个采样器参数来进行控制,超出0.0~1.0取值范围之后的处理方式,分别为GL_TEXTURE_WRAP_SGL_TEXTURE_WRAP_TGL_TEXTURE_WRAP_R,它们分别对应于纹理范围的S、T和R轴向。

纹理坐标的轴通常被命名为s、t、r和q,以便与空间坐标轴(x、y、z、w)和颜色坐标轴(r、g、b、a)进行区分。

每个轴向可以设置如下截断方式:

  • GL_CLAMP_EDGE

    如果纹理坐标超出0.0~1.0的范围,那么纹理边缘的纹素将作为结果值直接返回给着色器。

  • GL_CLAMP_TO_BORDER

    读取到纹理范围之外的时候,将返回一个常数边界颜色值,作为函数的最终结果。默认情况下是黑色透明的(即颜色的每个分量都是0.0),可以通过设置采样器参数GL_TEXTURE_BORDER_COLOR来设置新的数值。

    GLuint sampler;			//采样器名称的变量
    GLuint texture;			//纹理名称的变量
    const GLfloat red[] = {1.0f,0.0f,0.0f,1.0f};		//红色
    
    //设置采样器对象的GL_TEXTURE_BORDER_COLOR参数
    glSamplerParameterfv(sampler,GL_TEXTURE_BORDER_COLOR,red);
    
    //另一种方法是设置纹理对象的边界颜色
    //如果纹理已经绑定到纹理单元,但是没有设置采样器对象的话,需要采用这种方式
    glTextureParameterfv(texture,GL_TEXTURE_BORDER_COLOR,red);
    
    
  • GL_REPEAT

    纹理将被视为是无限重复的形式,然后直接截断。从本质上来说,这相当于只使用纹理坐标的小数部分来实现纹素的查找,而整数部分将被直接丢弃。

  • GL_MIRRORED_REPEAT

    纹理将被视为特殊的镜像方式,然后作为无限重复的形式进行截取。纹理坐标的整数部分为偶数的时候,只取小数部分进行数据读取。纹理坐标的整数部分为奇数的时候(例如1.3、3.8等),将使用1.0减去小数部分,从而得到最终的坐标值。这种模式可以有效避免简单重复纹理带来的贴图边界瑕疵。

排列纹理数据

在某种情况下,纹理数据可能是采用特殊的分量顺序来存储的,并不是常见的红色、绿色、蓝色和alpha(RGBA)。举例来说,常见的ABGR格式(也就是以little endian顺序存储的RGBA字节),以及ARGB,甚至是RGBx(即RGB数据打包到32位的词中,有一个字节没有使用)。OpenGL很擅长处理这类型的数据,并且将它迅速转换到标准的RGBA数据并传递给着色器,需要使用纹理乱序(texture swizzle)机制,也就是对当时实时纹理数据的分量顺序重新进行排序,然后交给图形硬件去读取。

纹理乱序:是一种纹理参数集合,需要对纹理的每个通道使用glTextureParameteri()函数单独进行设置,并将乱序参数名和对应的参数值传递给函数。

外部正常顺序参数名包括:

  • GL_TEXTURE_SWIZZLE_R,对应正常顺序的红色
  • GL_TEXTURE_SWIZZLE_G,对应正常顺序的绿色
  • GL_TEXTURE_SWIZZLE_B,对应正常顺序的蓝色
  • GL_TEXTURE_SWIZZLE_A,对应正常顺序的alpha
  • GL_TEXTURE_SWIZZLE_RGBA,一次性对应正常顺序的红、绿、蓝、alpha四个通道

乱序参数名:

  • GL_RED
  • GL_GREEN
  • GL_BLUE
  • GL_ALPHA
  • GL_ONE
  • GL_ZERO

分别对应乱序纹理数据中的红色、绿色、蓝色和alpha通道,或者将正常参数设置为常量1或0。

GLuint tex
//创建纹理
glCreateTextures(GL_TEXTURE_2D, 1, &tex);
//绑定纹理
glBindTexture(GL_TEXTURE_2D, tex);
//分配纹理数据的存储空间
glTextureStorage2D(tex, 4, GL_RGBA8, 8, 8);
//纹理数据
static const GLubyte texture_data[] =
{
	0xF9, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00,
	0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF,
	0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00,
	0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF,
	0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00,
	0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF,
	0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00,
	0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF
};
//将数据传递到纹理中
glTextureSubImage2D(tex,
	0,
	0, 0,
	8, 8,
	GL_RGB, GL_UNSIGNED_BYTE_3_3_2,
	texture_data);
//排列纹理数据
static const GLint swizzles[] = { GL_BLUE,GL_ZERO,GL_ZERO,GL_ONE };
glTextureParameteriv(tex, GL_TEXTURE_SWIZZLE_RGBA, swizzles);
glTextureParameteri(tex, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTextureParameteri(tex, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTextureParameteri(tex, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTextureParameteri(tex, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

glGenerateMipmap(GL_TEXTURE_2D);

设置效果如下:
在这里插入图片描述

使用多重纹理

OpenGL可以同时支持很多个纹理,一个着色器阶段可以最少支持16个纹理,如果乘以OpenGL支持的着色器阶段的数量就是80个纹理。实际上,OpenGL确实有80个纹理,对应的标识符分别是GL_TEXTURE0~GL_TEXTURE79

在着色器中使用多重纹理有两种方式:

  • 对于opengl 4.2及以前的版本

    在着色器中,需要定义多个uniform类型的采样器变量。每个变量对应着一个不同的纹理单元。在应用程序中,采样器uniform,可以使用glGetActiveUnifrom函数来进行枚举,也可以使用glUniformli()函数来进行设置数值。设置给采样器uniform的整数数值也就是它所关联的纹理单元的索引值。

    从技术上来说,这个采样器变量也可以关联到相同的纹理单元上,如果有两个或者更多采样器都关联到同一个纹理单元,它们将对同一个纹理进行数据的采样工作。

    片元着色器代码:

    #version 330 core
    
    in vec2 tex_coord0;
    in vec2 tex_coord1;
    
    layout (location = 0) out vec4 color;
    
    uniform sampler2D tex1;
    uniform sampler2D tex2;
    
    void main(void)
    {
        color = texture(tex1,tex_coord0) + texture(tex2,tex_coord1);
    }
    
    

    应用程序代码:

    //数据初始化
    //设置纹理索引值
    glUniform1i(glGetUniformLocation(base_prog, "tex1"), 0);
    glUniform1i(glGetUniformLocation(base_prog, "tex2"), 1);
    
    //加载纹理数据
    vglImageData image;
    tex1 = vglLoadTexture("media/test.dds", 0, &image);
    glTexParameteri(image.target, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    vglUnloadImage(&image);
    tex2 = vglLoadTexture("media/test3.dds", 0, &image);
    vglUnloadImage(&image);
    
    //图形绘制
    //分别激活GL_TEXTURE0,GL_TEXTURE1,并绑定纹理
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, tex1);
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, tex2);
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
    
    
  • 对于opengl 4.2以后的版本

    在着色器中,仍需要定义多个uniform类型的采样器变量,但是可以binding布局限定符,直接将采样器关联到相应索引的纹理单元。在应用程序中,使用glBindTextureUnit()函数将纹理绑定到纹理单元上。

    片元着色器代码:

    #version 440 core
    
    in vec2 tex_coord0;
    in vec2 tex_coord1;
    
    layout (location = 0) out vec4 color;
    
    layout (binding = 0) uniform sampler2D tex1;
    layout (binding = 1) uniform sampler2D tex2;
    
    void main(void)
    {
        color = texture(tex1,tex_coord0) + texture(tex2,tex_coord1);
    }
    
    

    应用程序代码:

    //加载纹理数据
    vglImageData image;
    tex1 = vglLoadTexture("./test.dds", 0, &image);
    glTextureParameteri(tex1, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    vglUnloadImage(&image);
    tex2 = vglLoadTexture("./test3.dds", 0, &image);
    vglUnloadImage(&image);
    
    //图形绘制
    glBindTextureUnit(0, tex1);
    glBindTextureUnit(1, tex2);
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
    
    

复杂纹理类型

纹理类型有,3D纹理、纹理数组、立方体映射、阴影、深度-模板,以及缓存纹理。

3D纹理

3D纹理可以被认为是3D网格形式排列的一种纹素体。创建一个3D纹理:

  1. 生成一个纹理对象,并绑定到GL_TEXTURE_3D目标
  2. 使用glTextureStorage3D()来创建纹理对象的存储空间。

3D纹理不只有宽度和高度,还有深度值。3D纹理的最大宽度和高度与2D纹理相同,可以通过GL_MAX_TEXTURE_SIZE的值来获取。OpenGL实现所支持的3D纹理的最大深度值可以通过GL_MAX_3D_TEXTURE_SIZE来获取,它可能与纹理宽度和高度的最大值不同。

3D纹理的一个典型用户就是体渲染,用在医疗影像和流体模拟的领域,在这种类型的程序当中,纹理的内容通常是一个密度图像,每个体素(voxel)都表达介质密度中的一个采样值。渲染体积的一个简单方法是,沿着体积像渲染有纹理的四边形一样渲染切平面,而3D纹理坐标取自顶点属性。

纹理数组

纹理数组可以将多幅一维或者二维纹理合并到一个集合中,它们的尺寸和格式必须是相同的,然后保存到更高一个维度的纹理中(例如,一组二维纹理需要使用类似三维纹理形式的集合去保存)。

但是如果直接使用一个三维纹理区保存一组二维纹理,就会遇到一个不方便的地方:用来索引的纹理坐标r在这种情况下是被归一化到[0,1]区间的,如有一组7张纹理,需要访问第3张的时候就需要设置坐标0.35714(约数),但是更应该直接设置为索引为2。

此外,纹理数组还允许按索引进行纹理之间的mipmap滤波,与之相比,三维纹理对纹理"切片"之间的滤波可能就无法返回我们所期望的结果了。

gvec4 texture(gsampler2D tex,vec2 P[,float bias]);
gvec4 texture(gsampler2DArray tex,vec3 P[,float bias]);

我们可以比较二维纹理和二维纹理数组的texture函数原型。第二个函数使用的采样器类型为sampler2DArray,并且纹理坐标P有一个额外的维度。P的第三个分量也就是数组的索引值,或者切片值。

立方体映射纹理

立方体映射纹理是一种特殊形式的纹理,特别适用于环境贴图(environment mapping),它会将一系列图像作为一个立方体的6个面来处理。这六个面分别对应于6幅子图像,它们必须是正方形且大小相等的。

当对立方体映射进行采样的时候,所使用的纹理坐标是三维形式,并且被视为从原点触发的一个方向向量,这个方向向量本质上就是立方体表面读取纹理数据的一个点。

立方体映射很适合表达周遭的环境信息、光照,以及反射的效果,它还可以用于复杂物体的纹理截取操作。

如果需要分配立方体映射纹理的存储空间,可以调用glCreateTexture(),并设置target为GL_TEXTURE_CUBE_MAP;然后为新纹理调用glTextureStorage2D(),这一次调用会直接创建立方体映射6个面的存储空间。

注意,即使调用的是二维纹理的存储空间函数,立方体映射还是会生成6层,就像一个二维数组纹理一样。每个立方体面都有自己的mipmap集合。纹理使用glTextureSubImage3D()来设置立方体每个面的数据(这里是三维,因为这个纹理与数组纹理很类似)。

如下为创建和初始化立方体映射纹理的过程。

GLuint tex;		//要创建的纹理

extern const GLvoid* texture_data[6];		//各个面的数据

//生成、绑定和初始化纹理对象,使用GL_TEXTURE_CUBE_MAP目标
glCreateTextures(1,GLTEXTURE_CUBE_MAP,&tex);
glTextureStorage2D(tex,10,GL_RGBA8,1024,1024);

//已经分配了纹理对象的存储空间,可以设置纹素数组中的纹理数据
for(int face = 0;face < 6;face++)
{
    glTextureSubImage3D(texture,			//面
    					0,					//层次
                       	0,0,				//X和Y偏移
                        face,				//Z偏移(面索引)
                        1024,1024,			//面的尺寸
                        1,					//每次一个面(深度)
                        GL_RGBA,			//格式
                        GL_UNSIGNED_BYTE,	//数据类型
                        texture_data[face]	//数据
                       );
}
//若有需要,还可以设置每个面的低层mipmap的数据

立方体映射纹理可以组合成数组,使用GL_TEXTURE_CUBE_MAP_ARRAY纹理目标来创建和修改立方体映射数组纹理。立方体映射数组中的每个立方体映射是底层数组纹理的6个连续切片。因此,5个立方体映射纹理数组共有30个切片。

如下所示,创建一个纹理中有5个立方体映射的数组。

GLuint tex;		//要创建的纹理

extern const GLvoid* texture_data[6][5];		//各个面的数据

//生成、绑定和初始化纹理对象,使用GL_TEXTURE_CUBE_MAP_ARRAY目标
glCreateTexture(1,&tex);
glBindTexture(GL_TEXTURE_CUBE_MAP_ARRAY,tex);
glTextureStorage3D(tex,10,GL_RGBA,1024,1024,5);

//已经分配了纹理对象的存储空间,可以设置纹素数组中的纹理数据
for(int cube_index = 0;cube_index < 5;cube_index++)
{
    for(int face = 0;face < 6;face++)
    {
        GLenum target = GL_TEXTURE_CUBE_MAP_POSITIVE_X + face;
        glTexSubImage3D(target,							//面
                       	0,								//层数
                        0,0,							//偏移
                        cube_index,						//立方体面索引
                        1024,1024,						//宽度、高度
                        1,								//面数
                        GL_RGBA,						//格式
                        GL_UNSIGNED_BYTE,				//数据类型
                        texture_data[face][cube_index]	//数据
                       );
    }
}

应用示例天空盒

天空盒是纹理的一种应用方式,它可以将整个场景高效地封装到一个立方体的大盒子里,同时确保观察着位于盒子的正中央。在场景渲染的时候,场景内任何没有被遮挡的物体都会出现在盒子的内部。通过选择合适的纹理内容,可以让整个立方体从观察者的角度看起来就是环境本身。

为了渲染一个天空盒,需要渲染一个单位立方体,它位于场景原点,并且采用物体局部空间位置作为纹理坐标,以便对立方体映射进行采样。

//顶点着色器
#version 330 core
layout(location = 0) in vec3 in_position;
out vec3 tex_coord;
uniform mat4 tc_rotate;
void main(void)
{
    tex_coord = in_position;
    gl_Position = tc_rotate * vec4(in_position,1.0);
}

//片元着色器
#version 330 core
in vec3 tex_coord;
layout (location = 0) out vec4 color;
uniform samplerCube tex;
void main(void)
{
    color = texture(tex,tex_coord);
}

使用立方体映射实现环境映射

现已经创建了环境并将它放置在场景中,可以新建物体并让它称为环境的一部分。这就是环境映射的定义,也是立方体映射纹理的另一种常见用法。这种时候,立方体映射被当做一种环境贴图(environment map)来使用,可以在场景中投射物体。

为了实现环境映射,需要重新计算纹理坐标,方法是在准备贴图的物体表面点上,围绕表面法线对入射的视角向量进行反射,然后通过反射向量对立方体映射进行采样。

如下例子中,顶点着色器负责将物体空间坐标变换到人眼中,方法是乘以模型-视点-投影矩阵(model-view-projection matrix)。它还可以将表面法线变换到人眼空间,方法是乘以模型-视点矩阵(model-view matrix)。

片元着色器,获得顶点着色器中计算的人眼空间的表面法线和位置数据,然后使用reflect函数,将每个片元的人眼空间的位置值沿着表面法线进行反射。这样高效的实现了视点向量沿物体表面的反射,并与立方体映射相交。将这个反射向量作为纹理坐标并对立方体映射进行采样,用得到的结果纹素对表面进行着色,这样做就得到了,环境信息被反射到物体的表面上。

//顶点着色器
#version 330 core

//输入位置和法线
layout(location = 0) in vec4 in_position;
layout(location = 1) in vec3 in_normal;

//输出表面法线和人眼空间的位置
out vec3 vs_fs_normal;
out vec3 vs_fs_position;

//模型-视点-投影矩阵和模型-视点矩阵
uniform mat4 mat_mvp;
uniform mat4 mat_mv;

void main(void)
{
    //剪切空间的位置坐标
    gl_Position = mat_mvp * in_position;
    //人眼空间的法线和位置
    vs_fs_normal = mat3(mat_mv) * in_normal;
    va_fs_position = (mat_mv * in_position).xyz;
}

//片元着色器
#version 330 core

//输入表面法线和人眼空间的位置
in vec3 vs_fs_normal;
in vec3 vs_fs_position;

//最后的片元颜色
layout (location = 0) out vec4 color;

//立方体映射纹理
uniform samplerCube tex;

void main(void)
{
    //沿着表面法线对人眼空间的位置坐标进行反射,从而计算纹理坐标
    vec3 tc = reflect(-vs_fs_position,normalize(vs_fs_normal));
    //对纹理进行采样,并且让结果的片元值呈现出金黄色的颜色
    color = vec4(0.3,0.2,0.1,1.0) + vec4(0.97,0.83,0.79,0.0) * texture(tex,tc);
}

无缝的立方体映射采样

立方体映射是6个独立的立方体面的集合,也可以扩展成立方体的数组,面的总数是6的倍数。当OpenGL对一个立方体映射进行采样的时候,第一步会使用三维纹理坐标的特征分量去判断哪个立方体将要被采样。当这个面被判断出来之后,系统就可以很高效地将它视为一个二维纹理并且查找纹素数值。

默认情况下,纹理的边界部分依然采用默认的纹理坐标截取方式,粗看起来效果不错,因为生成的二维纹理坐标应道落在立方体面之内,很难看出有瑕疵的情形。

若纹理的滤波方式设置为线性,那么沿着立方体各个面的边界部分,那些互相连接的表面的纹素就不是理想的最终结果值了。它会造成滤波后的纹理产生一条明显的接缝。

更严重的是,如果纹理坐标的截取方式被设置为重复方式或者镜像方式,还可能会用到立方体面的另一边的纹素数据,从而产生完全不正确的结果。

对于相邻面接缝的问题,可以使用glEnable()开启无缝立方体映射滤波GL_TEXTURE_CUBE_MAP_SEAMLESS,开启后,会使用相邻的立方体映射面上的纹素来获取滤波后的纹素结果。

阴影采样器

GLSL中有一类特殊的采样器叫做阴影采样器(shadow sampler)。阴影采样器需要纹理坐标中增加一个额外的分量,用来与返回的纹素数据进行比较。纹理函数返回的值是0.0~1.0的一个浮点数,它表示准备进行比较的纹素数据的小数部分。

若访问纹理时只采样一个纹素(滤波方式为GL_NEAREST,没有mipmap,并且每个纹素一个采样点),那么比较之后返回的值可能是0.0也可能是1.0(这取决于纹素是否通过了比较)。

如果着色器中返回的数据是通过不止一个纹素构建而成的(例如线性的滤波方式,或者使用了多重采样),那么返回值应当在0.0~1.0之间,这取决于有多少个纹素通过了比较测试。

float texture(gsampler1DShadow tex,vec3 P[,float bias]);
float texture(gsampler2DShadow tex,vec3 P[,float bias]);
float texture(gsamplerCubeShadow tex,vec4 P[,float bias]);
float texture(gsampler1DArrayShadow tex,vec3 P[,float bias]);
float texture(gsampler2DArrayShadow tex,vec4 P[,float bias]);
float texture(gsampler2DRectShadow tex,vec3 P);
float texture(gsamplerCubeArrayShadow tex,vecP P,float compare);

对绑定到tex所对应的纹理单元的阴影纹理进行采样,纹理坐标通过P来设置。返回值是一个浮点数,用来衡量阴影比较操作的结果。即获取的纹素数据通过比较测试的数据所占的分数。

如果要开启采样器的比较功能,需要调用glSamplerParameteri()(或glTextureParameteri(),如果没有使用采样器对象),并且设置pname为GL_TEXTURE_COMPARE_MODE,param为GL_COMPARE_REF_TO_TEXTURE。如果要关闭的话,则param设置为GL_NONE

若纹理比较模式已经设置为GL_COMPARE_REF_TO_TEXTURE,那么对应的采样器还要设置比较的方式。需要调用glSamplerParameteri()并设置pname为GL_TEXTURE_COMPARE_FUNC,param为以下这些比较函数:

GL_LEQUALGL_GEQUALGL_LESSGL_GREATERGL_EQUALGL_NOTEQUALGL_ALWAYSGL_NEVER

深度-模版纹理

纹理还可以存储深度和模板数据,也是采用逐纹素的方式,对应的纹理格式为GL_DEPTH_STENCIL。使用深度-模板纹理进行映射的时候,着色器默认读取深度数据。对于4.3版本的着色器,同样可以读取模板数据,应用程序中需设置GL_DEPTH_STENCIL_TEXTURE_MODEGL_STENCIL_COMPONENTS,着色器必须使用整数类型的采样器。

缓存纹理

缓存纹理是一种特殊形式的纹理,它允许从着色器中直接访问缓存对象的内容,将它当做一个巨大的一维纹理使用。可以把它当做一般的纹理对象创建,绑定到纹理单元,并使用glTextureParameteri()控制它们的参数。但是,纹理数据的存储是由一个缓存对象(它必须有名称)来管理和控制。

缓存纹理和一维纹理不同之处:

  • 一维纹理的尺寸受限于GL_MAX_TEXTURE_SIZE对应的最大值,缓存纹理的尺寸受限于GL_MAX_TEXTURE_BUFFER_SIZE的值,通常能达到2GB甚至更多。
  • 一维纹理支持滤波、mipmap、纹理坐标的截取,以及其他一些采样器参数,但是缓存纹理没有内置的采样器,并且采样器对象也不会对缓存纹理产生效果。
  • 一维纹理的纹理坐标是归一化的浮点数值,但是缓存纹理使用的是没有归一化的整数纹理坐标。

如果要创建缓存纹理,需调用glCreateTextures()来创建纹理对象,并将GL_TEXTURE_BUFFER作为输入的目标参数,然后使用glTextureBuffer()函数将纹理与一个缓存对象关联起来。

void glTextureBuffer(GLuint texture,GLenum internalformat,GLuint buffer);

将缓存对象buffer的存储格式与缓存纹理texture进行关联。buffer的存储数据将被视为一组数据格式为internalformat的元素进行解析,注意数据格式必须是有尺寸后缀的。如果buffer为0,那么缓存纹理texture中当前已经存在的关联信息将被断开,缓存数据将无法再读取。

如下所示,创建和初始化缓存纹理过程

//作为数据存储的缓存对象
GLuint buf;
//作为缓存纹理使用的纹理对象
GLuint tex;
//数据应当被保存到程序内存中了
extern const GLvoid* data;

//生成、绑定和初始化缓存对象,设置绑定点为GL_TEXTURE_BUFFER。假设这里用到的数据是1MB
glCreateBuffers(1,&buf);
glBindBuffer(GL_TEXTURE_BUFFER,buf);
glBufferData(GL_TEXTURE_BUFFER,1024*1024,data,GL_STATIC_DRAW);

//现在创建缓存纹理对象并将它与缓存对象关联
glCreateTextures(1,GL_TEXTURE_BUFFER,&tex);
glTextureBuffer(tex,GL_R32F,buf);

如果只需要关联部分缓存对象到缓存纹理中,使用如下函数:

void glTextureBufferRange(GLuint texture,GLenum internalformat,GLuint buffer,GLintptr offset,GLsizeiptr size);

将缓存对象buffer中从offset开始,总共size字节的一部分存储区域,关联给缓存纹理texture。

buffer的存储数据将被视为一组数据格式为inernalformat的元素进行解析,注意数据格式必须是有尺寸后缀的。

如果buffer为0,那么缓存纹理texture中当前已经存在的关联信息将断开,缓存数据将无法再读取。

offset必须是一个整数值,并且是系统平台所定义的常量GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT的倍数。

着色器中访问缓存纹理,需要使用uniform变量samplerBuffer(对于有符号或者无符号整型的变量,还可以使用isamplerBuffer和usamplerBuffer),然后用texelFetch函数来读取单独的纹素采样数据并使用。

vec4 texelFetch(samplerBuffer s,int coord);
ivec4 texelFetch(isamplerBuffer s,int coord);
uvec4 texelFetch(usamplerBuffer s,int coord);

对一个单独的纹素进行查找,纹理当前绑定到s,而纹理坐标设置为coord。

//从缓存纹理中查找纹素
#version 450 core
layout (binding = 0) uniform samplerBuffer buf;
in int tex_coord;
layout(location = 0) out vec4 color;
void main(void)
{
    color = texelFetch(buf,tex_coord);
}

纹理视图

OpenGL允许用户在多个纹理之间共享存储数据,每个纹理可以有自己的格式和维度设置。

  1. 创建一个纹理并使用相关函数初始化数据存储空间(例如glTextureStorage2D())
  2. 创建这个"父"纹理的纹理视图,它会对第一个纹理的底层存储空间做引用计数加1的操作,将视图引用到存储空间上
void glTextureView(GLuint texture,GLenum target,GLuint origTexture,GLenum internalFormat,GLuint minLevel,GLuint numLevels,GLuint minLayer,GLuint numLayers);

针对纹理origTexture创建一个新的纹理视图,原始纹理的名称必须有效,并且已经初始化数据空间。

texture会被关联到origTexture的存储空间,并且成为一个目标为target的永久纹理。

texture的内部格式通过internalFormat来设置,它必须和origTexture的内部格式是兼容的。

minLevel和numLevels分别设置了新纹理的第一个mipmap层以及mipmap的层次数量。与之类似,minLayer和numLayers设置了新纹理的第一层切片和总切片数,如果它是纹理数组的话。

创建视图时,新纹理目标必须和已有纹理的目标兼容。

纹理视图兼容的内部格式
原始纹理目标 (GL_TEXTURE_*)兼容目标 (GL_TEXTURE_*)
1D1D, 1D_ARRAY
2D2D, 2D_ARRAY
3D3D
CUBE_MAPCUBE_MAP, 2D, 2D_ARRAY, CUBE_MAP_ARRAY
RECTANGLERECTANGLE
BUFFER
1D_ARRAY1D, 1D_ARRAY
2D_ARRAY2D, 2D_ARRAY
CUBE_MAP_ARRAYCUBE_MAP, 2D, 2D_ARRAY, CUBE_MAP_ARRAY
2D_MULTISAMPLE2D_MULTISAMPLE, 2D_MULTISAMPLE_ARRAY
2D_MULTISAMPLE_ARRAY2D_MULTISAMPLE, 2D_MULTISAMPLE_ARRAY

新视图的内部格式与原始父纹理也必须是相同的格式类别。

纹理视图兼容的内部格式
原始目标兼容的目标
128-bitGL_RGB32F、GL_RGBA32UI、GL_RGBA32I
96-bitGL_RGB32F、GL_RGB32UI、GL_RGB32I
64-bitGL_RGBA16F、GL_RG32F、GL_RGBA16UI、GL_RG32UI、GL_RGBA16I、GL_RG32I、GL_RGBA16、GL_RGBA16、GL_RGBA16_SNORM
48-bitGL_RGB16、GL_RGB16_SNORM、GL_RGB16F、GL_RGB16UI、GL_RGB16I
32-bitGL_RG16F、GL_R11F_G11F_B10F、GL_R32F、GL_RGB10_A2UI、GL_RGBA8UI、GL_RG16UI、GL_R32UI、GL_RGBA8I、GL_RG16I、GL_R32I、GL_RGB10_A2、GL_RGBA8、GL_RG16、GL_RGBA8_SNORM、GL_RG16_SNORM、GL_SRGB8_ALPHA8、GL_RGB9_E5
24-bitGL_RGB8、GL_RGB8_SNORM、GL_SRGB8、GL_RGB8UI、GL_RGB8I
16-bitGL_R16F、GL_RG8UI、GL_R16UI、GL_RG8I、GL_R16I、GL_RG8、GL_R16、GL_RG8_SNORM、GL_R16_SNORM
8-bitGL_R8UI、GL_R8I、GL_R8、GL_R8_SNORM
GL_RGTC1_REDGL_COMPRESSED_RED_RGTC1、GL_COMPRESSED_SIGNED_RED_RGTC1
GL_RGTC2_RGGL_COMPRESSED_RG_RGTC2、GL_COMPRESSED_SIGNED_RG_RGTC2
GL_BPTC_UNORMGL_COMPRESSED_RGBA_BPTC_UNORM、GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM
GL_BPTC_FLOATGL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT、GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT

根据兼容性,可以同时使用多种方式来解析纹理中的数据。

  1. 举例来说,可以为一个RGB8纹理创建两个视图:一个使用无符号归一化格式(在着色器中返回浮点数据);另一个使用无符号整数纹理格式(在着色器中返回整数数据)。

    //创建两个纹理名称:一个父纹理;另一个就用作纹理视图
    GLuint tex[2];
    glCreateTextures(2,&tex);
    
    //绑定第一个纹理并初始化数据
    //这里设置的存储空间是1024*1024的二维纹理,带有mipmap并且格式为
    //GL_RGB8--每个分量为8位,无符号归一化类型
    glBindTexture(GL_TEXTURE_2D,tex[0]);
    glTextureStorage2D(tex[0],10,GL_RGB8,1024,1024);
    
    //现在创建纹理的视图,这一次使用GL_RGB8UI格式,即直接获取纹理的原始数据
    glTextureView(tex[1],				//纹理视图
                  GL_TEXTURE_2D,		//新视图的目标
                  tex[0],				//源纹理
                  GL_RGB8UI,			//新的格式
                  0,10,					//所有的mipmap层次
                 0,1);					//只有一个切片
    
    
  2. 若一个更大的二维数组纹理,需要从数组中获取单独的一个切片并作为独立的二维纹理使用。要实现这个需求,可以使用GL_TEXTURE_2D创建一个视图,而原始纹理为GL_TEXTURE_2D_ARRAY

    //创建两个纹理名称:一个是父纹理;另一个就用作纹理视图
    GLuint tex[2];
    glCreateTextures(1,GL_TEXTURE_2D_ARRAY,&tex[0]);
    glCreateTextures(2,GL_TEXTURE_2D,&tex[1]);
    
    //绑定第一个纹理并初始化数据
    //这一次我们会创建一个二维数组纹理,它的每一层都是256*256大小的纹素数据,共有100层
    glTextureStorage3D(tex[0],8,GL_RGBA32F,256,256,100);
    
    //现在创建一个GL_TEXTURE_2D的纹理视图,将之前数组中间部分的一个切片抽取出来使用
    glTextureView(tex[1],			//新的纹理视图
    			  GL_TEXTURE_2D,	//新视图的目标
    			  tex[0],			//源纹理
                  GL_RGBA32F,		//格式设置与源纹理相同
                  0,8,				//所有的mipmap层次
                  50,1);			//只用到其中一层切片
    
    

创建的纹理视图,可以在任何有关纹理的场合使用,例如加载图像并存储,或者作为帧缓存的附件。可以为纹理绘图创建新的视图(以及进一步再创建视图),而这里的每个视图都引用了原始的数据空间并进行计数。可以直接删除原始的父纹理,只要还有一个视图在引用数据本身,数据就不会被删除。

滤波方式

当纹理贴图被映射到多边形或者物体表面,再变换到屏幕坐标系之后,纹理上的独立纹素几乎不可能直接和屏幕上的最终画面像素直接对应起来。根据所用的变换方法和纹理贴图的不同,屏幕上的一个像素可能对应了一个纹素的一部分(放大),或者大量的纹素集合(缩小)。无论哪种情况,我们都无法精确知道应该使用哪些纹素值,以及如何对它们求平均或者插值。

因此,OpenGL允许用户从多种不同的滤波选项中进行选择,来完成上述计算需求。

线性滤波

线性滤波技术的含义是:使用坐标值从一组离散的采样信号中选择相邻的采样点,然后将信号曲线拟合成线性近似的形式。

在这里插入图片描述
实线所对应的信号被离散采样为图中的圆点。但是我们无法通过直接在两点之间连接来重新构建原始信号。在信号的某些区域中,线性的重建结果与原始信号是可以较好地匹配的。但是在其他一些区域中,重建结果与原始信号的差异较大,重采样的结果会出现明显的尖锐转折。

对于图像数据来说,也可以使用相同的技术。因为纹理的采样率(分辨率)相比图像数据(细节)中的尖锐转折说是足够高的,所以图像进行线性的重构理论上可以得到较高的质量。

为了完成这个过程,OpenGL会将用户传递的纹理坐标视为浮点数值,然后找到两个离它最近的采样点。坐标到这两个采样点的距离也就是两个采样点参数计算的权重,从而得到加权平均后的最终结果。因为线性重采样是可分解的,OpenGL可以现在一个维度上应用这个过程,然后在第二个维度上应用,从而重构二维图像,当然也可以再应用第三个维度来得到三维纹理。

可分解的操作(spearable operation)也就是可以解构成两次或者更多操作的意思,每次数据的操作过程通常都是类似的。

线性滤波不仅可以用在采样点到一维、二维和三维纹理的平滑变换过程中,还可以用于纹理相邻mipmap层之间的采样过程中的纹素混合操作。

OpenGL会根据所选择的采样点来计算mipmap的层次,计算结果是一个带有小数部分的浮点数值,这被视为一个带有小数位的纹理坐标,直接在相邻的纹素之间进行采样。这里会选择两个相邻的mipmap层来构建一对采样值,并且把细节层次计算得到的小数位作为两个采样值的权重,参数到平均值的计算中。

滤波选项通过OpenGL采样器对象中的滤波纹理(texture filter)模式来控制

  • GL_TEXTURE_MAG_FILTER

    参数控制纹理放大的参数,当渲染需要的细节分辨率已经超过了最高分辨率的mipmap层(默认是第0层),mipmap计算的层级结果是一个小于等于0的数值。由于放大的情形下只有最高层级的mipmap会被使用,因此GL_TEXTURE_MAG_FILTER只有两个可以选择的参数。

    • GL_NEAREST

      该参数会禁止滤波并直接返回与采样位置距离最近的纹素。

    • GL_LINEAR

      该参数会开启滤波。

  • GL_TEXTURE_MIN_FILTER

    纹理缩小的设置中mipmap层次会起到作用,后文中详细介绍。

使用和生成mipmap

与其他场景中的对象一样,纹理对象也可以从不同的视点距离进行观察。在一个动态的场景中,当贴了纹理的物体远离视点运动时,屏幕像素与纹理像素之间的比率会变得非常低,因此纹理的采样频率也会变得非常低。这样会产生渲染图像上的瑕疵,因为有纹理数据下的采样(undersampling)的缘故。

举例来说,如果要渲染一面砖墙,可能会用到一张很大的纹理图像(比如1024X1024个纹素),在观察者距离墙很近的时候这样是没问题的。但是如果这面墙正在远离观察者运动,直到它在屏幕上变成了一个像素点,那么纹理采样的结果可能会在某个过渡点上发生突然的变化。

为了降低这个效果的影响,可以对纹理贴图进行提前滤波,并且将滤波后的图像存储为连续的低分辨率版本(原始图像为全分辨率)。这就叫做mipmap。mip来自于拉丁语multum in parvo,也就是"在一个小区域的很多东西"的意思。

OpenGL在使用mipmap的时候会依据被映射的物体的尺寸(像素为单位),来决定当前应当使用纹理贴图的哪个分辨率层级。这样,可以根据纹理贴图的细节层次(level of detail),找到当前绘制到屏幕的最合适的图像,物体的图像变得更小,那么纹理贴图层次的尺寸也会更小。

mipmap需要一些额外的计算和纹理存储的区域,不过如果不使用mipmap的话,纹理被映射到较小的运动物体上之后,可能会产生闪烁的问题。

  • GL_TEXTURE_MIN_FILTER

    负责控制mipmap层次大于0的时候,纹素构建的方式。有6个可以设置的参数值。

    • GL_NEARESTGL_LINEAR

      OpenGL会关闭mimap并且仅仅使用纹理的基本层(0层)。

    • GL_NEAREST_MIPMAP_NEARESTGL_NEAREST_MIPMAP_LINEARGL_LINEAR_MIPMAP_NEARESTGL_LINEAR_MIPMAP_LINEAR

      每个模式参数都是由两部分组成的,名称的结构总是**GL_{A}_MIPMAP_{B}**的形式,这里的{A}和{B}都可以是NEAREST或者LINEAR中的一个。

      第一部分{A}负责控制每个mipmap层次的纹素构成方式,它与GL_TEXTURE_MAG_FILTER中的设置是按照同样的方式工作的。

      第二部分{B}负责控制采样点在两个mipmap层次之间的融合方式。若设置为NEAREST,那么计算只用到最近的mipmap层。如果设置为LINEAR,那么两个最近的mipmap层数据之间会进行线性的插值计算。

如果要使用mipmap,用户需要给出纹理从最大尺寸到1x1之间的所有尺寸,按照2的幂进行划分。如果不希望mipmap总是会递减到1x1纹理,也可以设置GL_TEXTURE_MAX_LEVEL的值,决定程序所支持的最大层次,OpenGL在计算纹理mipmap的时候,将不会再考虑任何进一步的层次数据。

如果纹理最高分辨率层次的数据不是正方形的形状,那么某一个维度的分辨率会比另一个维度更快到达1。这种情况下,我们需要继续构建新的层次,让另一个维度的数据继续见效,直到整个层数的大小为1x1为止。

举例来说,如果用户的最高分辨率贴图是64x16,那么我们还需要提供32x8、16x4、8x2、4x1、2x1和1x1尺寸的贴图。更小的贴图都是经过了滤波,它们本质上都是原始贴图下的采样版本,其中每个纹素都是经过更高级别的纹理的4个对应纹素加权平均得到的。

由于OpenGL不需要任何特殊的方法来计算低分辨率的贴图,因此这些不同尺寸的纹理之间可以是完全不相关的。但是,不相关的纹理会导致mipmap层次之间的过渡十分明显。

  • 手动生成

    要设置mipmap纹理的话,需要使用glTextureStorage2D()分配纹理空间,然后调用glTextureSubImage2D(),对纹理贴图的每个分辨率都要执行一次,但是level、width、height和image参数的设置都不相同。从第0层开始,level设置的是金字塔中每层纹理的索引;若最高分辨率是64x64,那么它相当于level = 0,那么32x32的纹理就是level = 1,以此类推。

  • 自动生成

    OpenGL提供了一个函数,可快速生成一个纹理的所有mipmap,函数是glGenerateTextureMipmap(),OpenGL的具体实现会提供对应的机制来采样高分辨率图像,产生低分辨率的mipmap图。这一过程通常是内部通过着色器或者纹理滤波的硬件设备来完成。使用这个工具的时候,通常考虑更高的性能,而不是质量,不同的OpenGL实现效果也各有不同。

    void glGenerateTextureMipmap(GLuint texture);
    
    

    为纹理图像texture生成一个完整的mipmap层次结构,纹理类型必须是下面的类型:

    GL_TEXTURE_1DGL_TEXTURE_2DGL_TEXTURE_3DGL_TEXTURE_1D_ARRAYGL_TEXTURE_2D_ARRAYGL_TEXTURE_CUBE_MAP

    mipmap的层次结构是通过GL_TEXTURE_BASE_LEVELGL_TEXTURE_MAX_LEVEL来控制的。如果这些参数值依然使用默认值,那么整个mipmap都会被构建起来,直到单个纹素的最后一层纹理。创建连续的每层纹理的滤波方式是由OpenGL的具体硬件平台自己完成的。

计算mipmap层次

有关某个像素点对应的纹理mipmap层级的计算,是取决于当前纹理图像和贴纹理的多边形的尺寸(以像素为单位)之间的缩放比例的。将这个缩放比例设为ρ,定义第二个数值λ,
λ = log ⁡ 2 ( ρ ) + l o d b i a s \lambda = \log_{2}(\rho)+lod_{bias} λ=log2(ρ)+lodbias
对于多个维度,ρ是所有的维度中最大的缩放比例值。

lodbias是采样器的细节层次偏移值,它是一个常数值,通过glSamplerParameteri()来设置,pname参数为GL_TEXTURE_LOD_BIAS,它的作用是调节λ的值,默认值为0.0,也就是没有效果。

若λ≤0.0,那么纹素会比像素更小,因此会调用纹理放大滤波器,若λ>0.0,会调用纹理缩小滤波器。如果纹理缩小的时候用到了mipmap,那么λ还给出了mipmap的层次。(从纹理缩小滤波到放大滤波的切换节点通常就是λ=0.0的时候,但并不总是这样,mipmap滤波的选项有可能会导致这个切换的节点发生移动)。

例如:

如果纹理图像是64x64纹素大小,多边形的尺寸为32x32像素,ρ=2.0,由此得到λ=1.0。如果纹理图像变成64x32纹素,多边形尺寸8x16像素,ρ=8.0(x缩放为8.0,y缩放为2.0,取最大值),得到λ=3.0。

计算λ和ρ的公式如下:
λ b a s e ( x , y ) = log ⁡ 2 [ ρ ( x , y ) ] λ ′ ( x , y ) = λ b a s e + c l a m p ( b i a s t e x o b j + b i a s s h a d e r ) \lambda_{base}(x,y) = \log_{2}[\rho(x,y)] \\ \lambda'(x,y) = \lambda_{base} + clamp(bias_{texobj} + bias_{shader}) λbase(x,y)=log2[ρ(x,y)]λ(x,y)=λbase+clamp(biastexobj+biasshader)
mipmap层次的计算可以通过一些采样器参数控制:

  • GL_TEXTURE_LOD_BIAS

    可以用来偏移λ的值,lodbias

  • GL_TEXTURE_MIN_LODGL_TEXTURE_MAX_LOD

    用于设置λ的限制范围值,默认为-1000.0和1000.0,可以通过glSamplerParameterf()(如果没有单独设置采样器对象,则使用glTextureParameterf()),这样基本上任何数值都可以落在这个范围之内。

    以下公式,lodmin和lodmax表示
    λ = { l o d m a x , λ ′ > l o d m a x λ ′ , l o d m i n ≤ λ ′ ≤ l o d m a x l o d m i n , λ ′ < l o d m i n 未定义 , l o d m i n > l o d m a x \lambda = \begin{cases} lod_{max},&\lambda' > lod_{max}\\ \lambda',&lod_{min} \le \lambda' \le lod_{max}\\ lod_{min},&\lambda' < lod_{min} \\ 未定义,&lod_{min} > lod_{max} \end{cases} λ= lodmax,λ,lodmin,未定义,λ>lodmaxlodminλlodmaxλ<lodminlodmin>lodmax

GL_TEXTURE_MAG_FILTERGL_TEXTURE_MIN_FILTER的默认参数分别是GL_LINEARGL_LINEAR_MIPMAP_LINEAR。注意默认的缩小滤波器会启用mipmap。因此我们使用glTextureStorage2D()分配了纹理之后,总会带有完整的mipmap层次,但是对应的纹理数据在刚创建的时候并不存在。

一些人经常会犯的错误,就是忘记设置滤波模式参数,或者忘记给新创建的纹理设置mipmap数据,导致纹理代码无法工作。

mipmap细节层次的控制

对于mipmap金字塔的层次选择机制亦可进行控制:

  • GL_TEXTURE_BASE_LEVEL

    设置了采样所用的最低层次的mipmap纹理(也就是最高的分辨率),并且无视λ的影响

  • GL_TEXTURE_MAX_LEVEL

    设置了采样的最高mipmap层次(也就是最低分辨率)。

这样就可以将采样的过程限制到整个mipmap金字塔的一个子集当中。

GL_TEXTURE_BASE_LEVEL的一个潜在用法是纹理的流式加载(texture streaming)。使用这个机制的时候,整个纹理对象的存储是通过glTextureStorage2D()来分配的,但是一开始并不会加载数据,当用户程序运行并且新的对象进入视野的时候,它们的纹理数据将会按照低分辨率mipmap到高分辨率mipmap的顺序被加载进来。

为了确保用户总能看到一些有意义的画面(此时整个纹理可能并没有被加载进来),需要设置GL_TEXTURE_BASE_LEVEL的值为当前已经加载过的最大分辨率的层次。这样,随着越来越多的纹理数据被加载进来,整个物体在屏幕上的外观也就变得越来越生动可信了。

高级纹理查询函数

显示的细节层次控制

通常来说,使用mipmap的时候,OpenGL会负责计算细节层次并得到mipmap层级的结果,再将采样结果返回给用户。也可以自己取代这个计算过程,通过纹理获取函数的参数来显示设置细节层次。

函数textureLod提供了一个lod参数,而不是texture函数通常提供的(可选的)bias参数。

gvec4 textureLod(gsampler1D tex,float P,float lod);
gvec4 textureLod(gsampler2D tex,vec2 P,float lod);
gvec4 textureLod(gsampler3D tex,vec3 P,float lod);
gvec4 textureLod(gsamplerCube tex,vec3 P,float lod);
gvec4 textureLod(gsampler1DArray tex,vec2 P,float lod);
gvec4 textureLod(gsampler2DArray tex,vec3 P,float lod);
gvec4 textureLod(gsampler2DRect tex,vec2 P,float lod);
gvec4 textureLod(gsamplerCubeArray tex,vec4 P,float lod);

从给定的采样器tex采样一个纹素,纹理坐标为P,显式的细节层次设置为lod。

显式的梯度设置

梯度纹理函数,会将纹理坐标的偏导数作为参数。

gvec4 textureGrad(gsampler1D tex,float P,float dPdx,float dPdy);
gvec4 textureGrad(gsampler2D tex,vec2 P,vec2 dPdx,vec2 dPdy);
gvec4 textureGrad(gsampler3D tex,vec3 P,vec3 dPdx,vec3 dPdy);
gvec4 textureGrad(gsamplerCube tex,vec3 P,vec3 dPdx,vec3 dPdy);
gvec4 textureGrad(gsampler1DArray tex,vec2 P,float dPdx,float dPdy);
gvec4 textureGrad(gsampler2DArray tex,vec3 P,vec2 dPdx,vec2 dPdy);
gvec4 textureGrad(gsamplerCubeArray tex,vec4 P,vec3 dPdx,vec3 dPdy);

从给定的采样器tex读取纹素,纹理坐标为P,此外P在x和y方向的偏导数分别由dPdx和dPdy给定。

在textureGrad函数中,上面提到的变量ρ会通过dPdx和dPdy的形式传入。如果可以获得纹理坐标的导数的解析公式,或者纹理坐标的不可导的时候,这个函数就有价值了。

带有偏移参数的纹理获取函数

有的时候需要获取某个区域中的多个纹素数据,或者需要再采样的时候稍微对纹理坐标进行偏移。GLSL中提供了这种功能的函数,并且会比直接在着色器中手动修改纹理坐标的值更为高效。

gvec4 textureOffset(gsampler1D tex,float P,int offset,[float bias]);
gvec4 textureOffset(gsampler2D tex,vec2 P,ivec2 offset,[float bias]);
gvec4 textureOffset(gsampler3D tex,vec3 P,ivec3 offset,[float bias]);
gvec4 textureOffset(gsampler1DArray tex,vec2 P,int offset,[float bias]);
gvec4 textureOffset(gsampler2DArray tex,vec3 P,ivec2 offset,[float bias]);
gvec4 textureOffset(gsampler2DRect tex,vec2 P,ivec2 offset,[float bias]);

从采样器tex中读取一个纹素,纹理坐标为P。当浮点数类型的纹理坐标被缩放和转换到合适的绝对纹理坐标范围之后,还可以用offset参数对纹理坐标进行偏移,然后再进行读取工作。

textureOffset函数中的offset参数是一个整数值。实际上,它必须是一个常数表达式,并且有一定的限定范围。这个限定范围可以通过内置的GLSL常量是gl_MinProgramTexelOffsetgl_MaxProgramTexelOffset来获取。

投影纹理

投影纹理(projective texture)也就是使用一个透视变换矩阵对纹理坐标进行变换。变换的输入参数是一组齐次坐标,输出的结果是变换后的向量,但是最后一个分量不一定是1。

textureProj函数就是用来处理最后一个分量的,它会把结果的纹理坐标投影到纹理的坐标空间当中。对于某些场合,例如将花纹投影到平面表面上(或者手电在墙上照出一个光圈),以及阴影图映射。

gvec4 textureProj(gsampler1D tex,vec2 P[,float bias]);
gvec4 textureProj(gsampler1D tex,vec4 P[,float bias]);
gvec4 textureProj(gsampler2D tex,vec3 P[,float bias]);
gvec4 textureProj(gsampler2D tex,vec4 P[,float bias]);
gvec4 textureProj(gsampler3D tex,vec4 P[,float bias]);
gvec4 textureProj(gsamplerRect tex,vec3 P);
gvec4 textureProj(gsamplerRect tex,vec4 P);

执行带有投影信息的纹理查找。纹理坐标为P,但是会除以它的最后一个分量并且通过结果值来执行纹理查找,后半部分与texture函数并无区别。

在着色器中执行纹理查询

下面的GLSL函数并不会真的从纹理中读取数据,而是会返回纹理数据处理的相关信息。

第一个函数是textureQueryLod,它会获取硬件固定流水线的mipmap计算结果并返回。

vec2 textureQueryLod(gsampler1D tex,float P);
vec2 textureQueryLod(gsampler2D tex,vec2 P);
vec2 textureQueryLod(gsampler3D tex,vec3 P);
vec2 textureQueryLod(gsamplerCube tex,vec3 P);
vec2 textureQueryLod(gsampler1DArray tex,float P);
vec2 textureQueryLod(gsampler2DArray tex,vec2 P);
vec2 textureQueryLod(gsamplerCubeArray tex,vec3 P);
vec2 textureQueryLod(sampler1DShadow tex,float P);
vec2 textureQueryLod(sampler2DShadow tex,vec2 P);
vec2 textureQueryLod(samplerCubeShadow tex,vec3 P);
vec2 textureQueryLod(sampler1DArrayShadow tex,float P);
vec2 textureQueryLod(sampler2DArrayShadow tex,vec2 P);
vec2 textureQueryLod(samplerCubeArrayShadow tex,vec3 P);

返回mipmap的处理信息,其中x分量是当前访问的mipmap数组,y分量是当前计算得到的细节层(相对于纹理base层次的差)。

上面的每个textureQueryLod()函数来说,都有一个对应的查询函数textureQueryLevels(),它会返回当前的mipmap层次数。

int textureQueryLevels(gsampler1D tex);
int textureQueryLevels(gsampler2D tex);
int textureQueryLevels(gsampler3D tex);
int textureQueryLevels(gsamplerCube tex);
int textureQueryLevels(gsampler1DArray tex);
int textureQueryLevels(gsampler2DArray tex);
int textureQueryLevels(gsamplerCubeArray tex);
int textureQueryLevels(sampler1DShadow tex);
int textureQueryLevels(sampler2DShadow tex);
int textureQueryLevels(samplerCubeShadow tex);
int textureQueryLevels(sampler1DArrayShadow tex);
int textureQueryLevels(sampler2DArrayShadow tex);
int textureQueryLevels(samplerCubeArrayShadow tex);

返回给定的采样器包含的mipmap层次。

textureSize函数会返回纹理的某个层次的尺寸。它的原型如下:

int textureSize(gsampler1D tex,int lod);
ivec2 textureSize(gsampler2D tex,int lod);
ivec3 textureSize(gsampler3D tex,int lod);
ivec2 textureSize(gsamplerCube tex,int lod);
ivec2 textureSize(gsamplerRect tex,int lod);
ivec3 textureSize(gsamplerCubeRect tex);
ivec2 textureSize(gsampler1DArray tex,int lod);
ivec3 textureSize(gsampler2DArray tex,int lod);
int textureSize(gsamplerBuffer tex);

返回当前绑定到采样器的纹理tex在某个细节层次lod(如果存在的话)上的尺寸。返回值的分量按照纹理宽度、高度、深度的顺序进行填充。对于数组形式的纹理来说,返回值的最后一个分量表示数组中的切片数量。

在着色器中获取一个多重采样纹理中,每个纹素的采样点数:

int textureSamplers(gsampler2DMS tex);
int textureSamplers(gsampler2DMSArray tex);

返回tex中每个纹素对应的采样点数量。

纹素收集

textureGather函数是一个特殊的函数,可以在着色器中直接读取四个采样值,从二维纹理(或者立方体映射、长方体映射、以及对应的纹理数组)中创建双线性滤波的纹素结果。

通常这里会读取单通道的纹理数据,可选的参数comp设置了要读取的分量通道的索引(而不是默认的x或者r分量)。

和直接多次采样同一个纹理并且读取它的某个通道的做法相比,这个函数会体现出显著的性能优势,这是因为它依赖于特殊的接口机制,可以把纹理查找的次数降到预期的四分之一。

gvec4 textureGather(gsampler2D tex,vec2 P[,int comp]);
gvec4 textureGather(gsampler2DArray tex,vec3 P[,int comp]);
gvec4 textureGather(gsamplerCube tex,vec3 P,[,int comp]);
gvec4 textureGather(gsamplerCubeArray tex,vec4 P[,int comp]);
gvec4 textureGather(gsamplerRect tex,vec2 P[,int comp]);

直接从长方形、二维(数组)或者立方体映射(数组)类型的纹理tex中读取和收集4个纹素,以便创建一个双线性滤波的纹素结果,这4个纹素的选定通道数据会被返回,并分别存储在返回结果的4个通道里。

如果有必要的话,可以使用comp参数来指定要获取的通道索引,0、1、2、3分别表示x、y、z、w分量。如果没有指定comp,那么默认返回x分量。

组合功能的特殊函数

还有更多集合了多种特性的纹理函数,举例来说,如果我们需要的是投影纹理,然后又需要显式设置它细节层次或者梯度,那么可以使用合并的函数textureProjLod或者textureProjGrad。

以下是采样器参数为二维的组合函数:

gvec4 textureProjLod(gsampler2D tex,vec2 P,float lod);
gvec4 textureProjGrad(gsampler2D tex,vec3 P,vec2 dPdx,vec2 dPdy);
gvec4 textureProjOffset(gsampler2D tex,vec3 P,ivec2 offset[,float bias]);
gvec4 textureGradOffset(gsampler2D tex,vec2 P,ivec2 dPdx,vec2 dPdy,ivec2 offset);
gvec4 textureProjLodOffset(gsampler2D tex,vec3 P,float lod,ivec2 offset);
gvec4 textureProjGradOffset(gsampler2D tex,vec3 P,vec2 dPdx,vec2 dPdy,ivec2 offset);

高级纹理查找函数可以进行合并,从而在一次函数调用中执行多个特殊的功能。

textureProjLod会为绑定的纹理tex执行投影变换(类似textureProj),同时显式地设置它的细节层次lod(类似textureLod)。

依此类推,textureProjGrad会执行textureProj形式的投影变换,同时传递dPdx和dPdy参数来显示地设置梯度数据(类似textureGrad)。

textureProjOffset会执行纹理坐标的投影变换,同时对变换后的纹理坐标进行偏移。

textureProjLodOffset,会进行投影纹理变换,使用显式的细节层次设置和纹素偏移(类似textureOffset)。

textureProjGradOffset,会进行投影纹理变换,同时显式设置梯度和纹素偏移值。

无绑定纹理

当目前为止,当需要再着色器中使用纹理的时候,都需要将它绑定到纹理单元,然后把纹理单元和着色器中的采样器关联起来,再使用内置函数读取纹理数据。

OpenGL支持一定数量的全局纹理单元,而一个着色器可以使用的最大纹理数量也是有限的。如果用户程序使用了大量的纹理,就需要再绘制每个场景物体的过程中反复绑定和重新绑定纹理。在对象绑定到环境的过程中,应用程序会消耗掉相当可观的时间的。

无绑定纹理(bindless texture),它不需要将纹理和采样器关联起来,而是直接将纹理对象本身表达成一个64位的数字。不需要使用采样器uniform变脸,而是使用采样器句柄(handle)。句柄的值由OpenGL提供的,也不需要关心这些数值是怎么被着色器解析的,只要还是保持原样就可以。

例如:

可以把这个64位数值设置给一个uniform块或者顶点属性,甚至是从纹理中获取。当在着色器中得到了这个64位的纹理句柄之后,就可以创建一个采样器。这个采样器也可以通过一对32位的数值来创建。当采样器被创建之后,可以和其他类型的采样器一起在着色器中工作,从纹理中读取纹素。

OpenGL对于无绑定纹理必须通过GL_ARB_bindless_texture扩展来实现。

纹理句柄

GLuint64 GetTextureHandleARB(GLuint texture);
GLuint64 GetTextureSamplerHandleARB(GLuint texture,GLuint sampler);

GetTextureHandleARB()会返回一个对应于纹理texture的64位句柄。

GetTextureSamplerHandleARB()会返回一个对应于纹理texture的64位句柄,同时使用采样器对象sampler的参数进行替代。

这些函数的结果句柄,可以传递给着色器并且用来采样纹理数据,使用纹理自己的采样参数(GetTextureHandleARB()的句柄)或者使用sampler的采样参数(GetTextureSamplerHandleARB()的句柄)。

获取了纹理的句柄之后,纹理的参数(以及采样器参数)就会被"烘焙"到句柄中。也就是说,就算用户改变了纹理或者采样器的参数,句柄也依然是指向这个纹理或者采样器之前的参数,而不会发生变化。

纹理驻留

在开始在着色器中使用纹理句柄之前,要确保纹理本身是常驻的(resident)。

通常这是OpenGL负责的工作:

将纹理绑定到环境的时候,OpenGL会有效地维护一个着色器可用的全部纹理的列表,因为着色器只能够从已经绑定的纹理集合中采样数据。在运行着色器代码之前,OpenGL会确认已经绑定的纹理中,全部的数据都已经正确驻留在内存中,可以随时读取。

对于无绑定的纹理来说,要访问纹理句柄可能来自任何地方。可以把句柄放在内存里然后通过uniform缓存去读取,也可以从纹理中采样读取。因此OpenGL无法判断当前采用了何种数据集合。这个检查工作需要由用户程序完成,并告诉OpenGL是否可以在着色器中访问某个句柄,或者不可以访问它。

void MakeTextureHandleResidentARB(GLuint64 handle);
void MakeTextureHandleNonResidentARB(GLuint64 handle);

MakeTextureHandleResidentARB()和MakeTextureHandleNonResidentARB()会向当前的驻留纹理列表中,添加或者删除一个纹理句柄。纹理句柄是通过之前的GetTextureHandleARB()和GetTextureSamplerHandleARB()返回的。在着色器中访问一个没有驻留的纹理,可能带来无法预知的问题,包括应用程序崩溃。

GLboolean IsTextureHandleResidentARB(GLuint64 handle);

如果纹理句柄handle当前已经驻留,那么返回true,如果没有驻留或者handle不是一个纹理句柄,那么返回false。

经过MakeTextureHandleResidentARB()MakeTextureHandleNonResidentARB()处理的句柄会持续有效,直到纹理被删除为止。当纹理被删除之后,它所产生的任何句柄都会无效化,并且不能再次被使用。句柄本身不需要特地执行删除操作。

采样无绑定纹理

将纹理的句柄传递到着色器当中之后,就可以创建一个采样器句柄,并且和常规采样器一样使用它了。

也可以直接将一个采样器变量放置到uniform块中直接使用,这种情况下,块中的采样器需要采取等价于GLuint64的主机端内存排列方式。因此可以映射一块缓存,然后将GLuint64形式的句柄(由glGetTextureHandleARB()产生)写入到缓存中;这样会比直接调用glBindTextureUnit()要快得多。

#version 450 core
#extension GL_ARB_bindless_texture : require
in FS_INPUTS
{
	vec2 i_texcoord;
	flat int i_texindex;
};
layout (location = 0) out vec4 o_color;
layout (binding = 0) uniform ALL_TEXTURES
{
	sampler2D my_textures[200];
};
void main(void)
{
    o_color = texture(m_textures[i],i_texcoord);
}

uniform块ALL_TEXTURES中包含了200个纹理句柄,它比通常情况下着色器中支持的纹理数量要更多,并且从一个纹理换到另一个纹理的速度要大大加快。只是在uniform绑定点上绑定了一个新的缓存,然后就可以做到在着色器中同时使用了200个不同的纹理对象了。

稀疏纹理

纹理可能是最为耗费的一种资源数据形式(主要指内存耗费)。在实际工程中,很多程序需要加载大量的纹理数据,但是并不需要在一帧内生成所有的纹理数据,此时可以利用稀疏纹理(sparse texture)的特性,这种纹理在逻辑上是完整的,但是每次只需要用到它的一部分数据。

如果OpenGL实现中有GL_ARB_sparse_texture扩展字符串,那么它可以支持稀疏纹理。

要创建稀疏纹理:

  1. 调用glCreateTextures()创建一个纹理对象。
  2. 对纹理调用glTextureParameteri()并设置GL_TEXTURE_SPARSE_ARB属性
  3. 对纹理调用glTextureStorage*D(),OpenGL将为纹理分配虚拟空间(virtual space),但是不会真的分配对应的物理空间。

由于纹理的分配是虚拟出来的,可以创建比实际内存更大纹理。例如2048x2048x2048二维数组纹理,内部格式为GL_RGBA8,它应当消耗的内存空间是32GB。现代GPU可以很轻易地将它映射到虚拟的地址空间。

GLuint tex;
//首先创建一个纹理对象
glCreateTextures(GL_TEXTURE_2D_ARRAY,1,&tex);
//开启它的稀疏属性
glTextureParameteri(tex,GL_TEXTURE_SPARSE_ARB,GL_TRUE);
//现在分配纹理的虚拟存储空间
glTextureStorage3D(tex,11,GL_RGBA8,2048,2048,2048);

上述代码,tex称为一个纹理对象名称,没有实际存储内容,逻辑上占用32GB的虚拟内存,由2048x2048x2048个纹素。

稀疏纹理的数据提交

对于稀疏纹理,可以将它绑定到纹理单元,然后再着色器中访问,或者使用无绑定纹理的形式。但在着色器中对纹理进行采样,得到的数据只有0。如果使用glTextureSubImage2D()去传递纹理数据,传入的数据也会被直接抛弃。

可以使用glTexturePageCommitmentEXT(),从物理上给稀疏纹理传递数据,它负责控制稀疏纹理的单独页面的提交操作。

void TexturePageCommitmentEXT(GLuint texture,GLint level,GLint xoffset,GLint yoffset,GLint zoffset,GLsizei width,GLsizei height,GLsizei depth,GLboolean commit);

设置稀疏纹理某个页面的提交操作。要执行提交修改的纹理对象为texture。

纹理对象的GL_TEXTURE_SPARSE_ARB属性必须设置为GL_TURE

level设置的是指定页面对应的纹理层次,它必须是0到纹理总层数减1之间的数值。

xoffset、yoffset和zoffset参数设置了页面的纹素偏移值,分别对应于X、Y和Z方向。而width、height和depth参数设置了页面的总纹素大小。

所有xoffset、yoffset、zoffset、width、height和depth这些参数都必须是texture页面大小的整数倍,除非这个区域正好扩展到纹理层次的边界。

稀疏纹理的页面

一个页面是一个尺寸单位为纹素的区域,这个区域的大小,通常在存储空间中是固定的,因此它的纹素大小也取决于纹理的内部格式。

使用glGetInternalformativ()函数,可以获得某个特定格式对应的页面大小,可以传入参数为GL_VIRTUAL_PAGE_SIZE_XGL_VIRTUAL_PAGE_SIZE_YGL_VIRTUAL_PAGE_SIZE_Z

某个纹理格式可有多种不同的页面大小的可能,要判断一个给定的内部格式有多少种支持的页面大小,可调用glGetInternalformativ()函数,并传入参数GL_NUM_VIRTUAL_PAGE_SIZES。当查询时,需要传入一个足够大的数组,以便包含所有可用尺寸的整数数值。若返回0,则这种格式中不支持稀疏纹理。

若要选择纹理使用的尺寸和排列方式,需要使用glTextureParameteri()并传入GL_VIRTUAL_PAGE_SIZE_INDEX_ARB参数。这个参数的值,也就是页面大小列表中的索引号。参数值默认为0,OpenGL通常会把最佳的排列方式放在第一位,因此,没什么特殊需求,只使用默认值即可。

调用glGetTextureParameteriv()来查询纹理使用的排列方式的索引,给定任意纹理,可用该函数判断它采用的布局方式的索引,以及页面大小对应的格式属性。

点精灵

点精灵(point sprite)是使用片元着色器来渲染OpenGL的点,并且使用点内的片元坐标来完成计算的过程。

点内的坐标是通过一个二维向量gl_PointCoord来计算的,它常见的两种用法是作为纹理坐标使用(这也是点精灵这个名词的经典起源),或者用来解析计算颜色和覆盖度。

纹理点精灵

在片元着色器中,使用gl_PointCoord从纹理中直接查找纹素,就可以生成一个简单的点精灵。每个点精灵将纹理渲染为一个正方形。

//顶点着色器
uniform mat4 model_matrix;
uniform mat4 projection_matrix;

layout (location = 0) in vec4 position;

void main(void)
{
    vec4 pos = projection_matrix * (model_matrix * position);
    gl_PointSize = (1.0 - pos.z / pos.w) * 64.0;
    gl_Position = pos;
}

//片元着色器
uniform sampler2D sprite_texture;

out vec4 color;

void main(void)
{
    color = texture(sprite_texture,gl_PointCoord);
}

顶点着色器中,gl_PointSize为了控制点精灵的大小,缩放比例取决于它们到近平面的距离。

片元着色器中,使用gl_PointCoord作为纹理坐标完成纹理的查找操作。

解析颜色和形状

纹理的分辨率是有限的,但是gl_PointCoord的精度是足够高的。

下面的着色器代码中,片元着色器使用解析的方法来判断结果的覆盖度,先把gl_PointCoord设置到原点附近,计算片元到点精灵中心点的平方距离。若它大于0.25(点精灵宽度的一半的平方根,或者就是一个容许圆圈的半径),则直接使用discard关键字丢弃片元;小于等于0.25,则在两个颜色之间进行插值来产生最终的结果。

//解析形状的片元着色器
out vec4 color;

void main(void)
{
    const vec4 color1 = vec4(0.6,0.0,0.0,1.0);
    const vec4 color2 = vec4(0.9,0.7,1.0,0.0);
    
    vec2 temp = gl_PointCoord - vec2(0.5);
    float f = dot(temp,temp);
    
    if(f > 0.25)
        discard;
    
    color = mix(color1,color2,smoothstep(0.1,0.25,f));
}

这样可以得到一个非常完美的圆。

控制点的显示

void glPointParameteri(GLenum pname,GLint param);
void glPointParameterf(GLenum pname,GLfloat param);
void glPointParameteriv(GLenum pname,const GLint *param);
void glPointParameterfv(GLenum pname,const GLfloat *param);

设置点的参数pname的数值为param。

pname必须是GL_POINT_SPRITE_COORD_ORIGIN或者GL_POINT_FADE_THRESHOLD_SIZE

如果pname是GL_POINT_SPRITE_COORD_ORIGIN,那么param必须是GL_LOWER_LEFT或者GL_UPPER_LEFT(或者是一个包含了这两个值之一的变量地址)。

如果pname是GL_POINT_FADE_THRESHOLD_SIZE,param必须是一个大于等于0的浮点数,或者是一个包含了数据值的变量地址。

  • GL_POINT_SPRITE_COORD_ORIGIN:表示gl_PointCoord的原点

    点精灵坐标的原点设置了gl_PointCoord.y在片元着色器中增加的方向(从上到下还是从下到上)。

    GL_UPPER_LEFT,是从上到下的方向,但是需要注意,这与窗口坐标的增量方向是相反的,后者的原点位于左下角。

    GL_LOWER_LEFT,那么gl_PointCoord.y增加的方向与gl_FragCoord.y就是相同的,后者表示了片元的实际窗口坐标。

  • GL_POINT_FADE_THRESHOLD_SIZE:表示点消隐的阈值。

    负责控制点(以及点精灵)反走样的方式。如果点的大小比这个阈值更低的话,OpenGL就不再对这个点执行真正的反走样,而是直接将点的颜色与背景进行融混。

    默认值为1.0,也就是说如果点经过光栅化之后,尺寸小于1.0,OpenGL将不再对每个片元做一次光照的采样计算,而是对采样点的所有片元统一进行光照计算,然后通过点的消隐因数来衰减alpha分量,计算公式如下:
    f a d e = { 1 , 若 d e r i v e d   s i z e ≥ t h r e s h o l d ( d e r i v e d   s i z e t h r e s h o l d ) 2 , 其他 fade = \begin{cases} 1,&若 derived\:size \ge threshold \\ (\frac{derived\:size}{threshold})^2,&其他 \end{cases} fade={1,(thresholdderivedsize)2,derivedsizethreshold其他

帧缓存对象

目前为止,所有关于缓存的讨论,都集中在窗口系统的缓存。这类缓存可应用各种各样的技术,但是仍然很多操作需要在不同的缓存之间大量地迁移数据,这就是帧缓存对象存在的意义。

通过帧缓存对象,可创建自己的帧缓存,并且将他们绑定到渲染缓存(renderbuffer)上,将数据拷贝的消耗最小化,同时对性能进行优化。

窗口系统提供的帧缓存是唯一可以被图形服务器的显示系统所识别的帧缓存,也就是说,在屏幕上看到的只能是这个缓存。应用程序中创建的帧缓存无法被显示器所显示,它们只能用于离屏渲染的场合。

在窗口创建之时,窗口系统所管理的帧缓存有自己的缓存对象(颜色、深度和模板)。若自己创建了一个应用程序管理的帧缓存对象,还需要创建额外的渲染缓存并与帧缓存对象相关联。窗口系统管理的缓存是无法与应用程序创建的帧缓存关联的,反之亦然。

void glCreateFramebuffers(GLsizei n,GLuint* framebuffers);

分配n个未使用的帧缓存对象名称,将它们存储到地址位于framebuffers的数组中。每个返回的名称都代表一个新的帧缓存对象,并且赋予了默认的帧缓存状态。

如果n为负数,那么将产生一个GL_INVALID_VALUE错误。

void glBindFramebuffer(GLenum target,GLuint framebuffer);

设置一个可读或者可写的帧缓存。

如果target为GL_DRAW_FRAMEBUFFER,那么framebuffer设置的是绘制时的目标帧缓存。

设置为GL_READ_FRAMEBUFFER,那么framebuffer就是读取操作的数据源。

设置为GL_FRAMEBUFFER,那么framebuffer所设置的帧缓存是既可读也可写的。

framebuffer设置为0的话,表示绑定目标到默认的窗口系统帧缓存,或者也可以设置为一个glGenFramebuffers()所生成的帧缓存对象。

若framebuffer不是0,也不是一个可用的帧缓存对象(可用的对象是通过glGenFramebuffers()生成的,并且没有被glDeleteFramebuffers()所释放),那么将产生一个GL_INVALID_OPERATION错误。

void glDeleteFramebuffers(GLsizei n,const GLuint* ids);

将n个帧缓存对象释放,对象的名称保存在ids中。

如果某个帧缓存对象当前已经被绑定(例如它的名字在最近一次调用glBindFramebuffer()时被使用),那么删除它意味着帧缓存的目标被立即重置为0(也就是窗口系统的帧缓存),同时帧缓存对象本身被释放。

如果n为负数,那么该函数将产生一个GL_INVALID_VALUE错误。如果传入的名称是未分配的,或者传入0,那么函数不会产生错误,而是直接忽略这些值。

GLboolean glIsFramebuffer(GLuint framebuffer);

如果framebuffer是某个glCreateFramebuffers()所生成的帧缓存的名称,那么返回GL_TRUE。如果framebuffer为0(窗口系统的默认帧缓存),或者值是未分配的,或者已经被glDeleteFramebuffers()删除,那么将返回GL_FALSE。

void glNamedFramebufferParameteri(GLuint framebuffer,GLenum pname,GLint param);

设置帧缓存对象的参数,前提是帧缓存对象还没有进行关联,否则这些参数的值将通过帧缓存附件来设置。

framebuffer必须是一个通过glCreateFrameBuffers()创建并返回的帧缓存对象的名称,并且它还没有被glDeleteFrameBuffers()删除。

pname设置的是帧缓存对象的参数,它必须是一下枚举量:GL_FRAMEBUFFER_DEFAULT_WIDTHGL_FRAMEBUFFER_DEFAULT_HEIGHTGL_FRAMEBUFFER_DEFAULT_LAYERSGL_FRAMEBUFFER_DEFAULT_SAMPLES,或者GL_FRAMEBUFFER_DEFAULT_FIXED_SAMPLE_LOCATIONS

渲染到纹理贴图

帧缓存对象的一个最常见的用途就是直接渲染到纹理贴图了。可以将纹理贴图的一个层次,关联到帧缓存的附件中。完成渲染后,可以从帧缓存对象中解除对纹理贴图的关联,继续执行下一步渲染。

纹理被绑定成缓存附件并且正在进行写入操作,依然可以自由地读取纹理的内容。这种情况被称作帧缓存渲染的循环,因为两个同时进行的操作都可能产生不确定的结果。

void glNamedFramebufferTexture(GLuint framebuffer,GLenum attachment,GLuint texture,GLint level);
void glNamedFramebufferTextureLayer(GLuint framebuffer,GLenum attachment,GLuint texture,GLint level,GLint layer);

glNamedFramebufferTexture*这类函数可以将为贴图的一个层次关联到帧缓存附件中,对应的帧缓存对象名为framebuffer。

glNamedFramebufferTexture()会关联纹理对象texture(假设它的值不是0)的层次level到附件attachment。

glNamedFramebufferTextureLayer()将一个数组纹理的某一层关联到帧缓存中,这个时候texture必须是一个纹理数组类型,layer是这个纹理的层索引,用于关联到帧缓存附件。

attachment必须是下面列出的某个帧缓存附件类型:GL_COLOR_ATTACHMENTiGL_DEPTH_ATTACHMENTGL_STENCIL_ATTACHMENTGL_DEPTH_STENCIL_ATTACHMENT(它对应的纹理内部格式必须是GL_DEPTH_STENCIL)。

如果texture是0,就表示任何绑定到attachment的纹理都会被释放掉,并且attachment上不会再有别的绑定物。此时level和layer(如果有的话)参数都会被忽略。

如果texture非0,它必须是一个已经存在的纹理对象(通过glCreateTextures()创建)。

level表示被关联为渲染目标的纹理图像的mipmap层次。对于三维纹理或者二维纹理数组来说,layer表示纹理的层索引。

//将纹理的一层关联到帧缓存附件
GLsizei texWidth,texHeight;
GLuint framebuffer,texture;

void init()
{
    //创建一个空的纹理
    glCreateTextures(GL_TEXTURE_2D,1,&texture);
    glTextureStorage2D(texture,1,GL_RGBA8,TexWidth,TexHeight);
    //将纹理关联到帧缓存
    glCreateFramebuffers(1,&framebuffer);
    glNamedFramebufferTexture(framebuffer,GL_COLOR_ATTACHMENT0,texture,0);
}

void display()
{
    //渲染到渲染缓存当中
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER,framebuffer);
    glViewport(0,0,TexWidth,TexHeight);
    glClearColor(1.0,0.0,1.0,1.0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    //生成纹理的mipmap
    glGenerateTextureMipmap(texture);
    
    //解除绑定(它可以贴到别的物体上了),然后绑定到窗口系统的帧缓存
    glBindFramebuffer(GL_FRAMEBUFFER,0);
    glViewport(0,0,windowWidth,windowHeight);
    glClearColor(0.0,0.0,1.0,1.0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    //使用刚才的纹理进行渲染
    glEnable(GL_TEXTURE_2D);
    ...
}

抛弃渲染数据

我们总是需要再渲染新的一帧之前清除帧缓存数据,这是一个经验法则。当我们清除帧缓存的时候,OpenGL实现就会知道它需要抛弃目前帧缓存中所有渲染数据,并且尽可能返回到一个干净的,压缩过的状态。

如果自己知道怎么处理目前的帧缓存并且进行后继处理,这种时候再清除一次数据也许就是浪费了,因为用户本来就打算完全覆盖整个区域去进行绘制。

void glInvalidateNamedFramebufferData(GLuint framebuffer,GLsizei numAttachments,const GLenum *attachments);
void glInvalidateNamedFramebufferSubData(GLuint framebuffer,GLsizei numAttachments,const GLenum *attachments,GLint x,GLint y,GLsizei width,GLsizei height);

通知OpenGL给定的帧缓存附件的内容将会被抛弃,数据范围通过x、y、width和height来设置。glInvalidateFramebuffer()可以抛弃指定帧缓存附件中所有的内容。

附件的数量是通过numAttachments来设置的,attachments是一个数组的地址,其中包含了附件相关的标识量。

对于非默认的帧缓存而言,这里的附件标识量必须来自GL_DEPTH_ATTACHMENTGL_STENCIL_ATTACHMENTGL_DEPTH_STENCIL_ATTACHMENTGL_COLOR_ATTACHMENTi(i表示颜色附件的索引)。

如果并不想抛弃帧缓存对象附件的数据,或者打算直接抛弃纹理内容、

void glInvalidateTexImage(GLuint texture,GLint level);
void glInvalidateTexSubImage(GLuint texture,GLint level,GLint xoffset,GLint yoffset,GLint zoffset,GLint width,GLint height,GLint depth);

通知OpenGL给定的纹理texture中某一层level的内容将会被抛弃。glInvalidateTexImage()会抛弃纹理对象的整个图像层次,而glInvatelidateTexSubImage()只会抛弃给定范围内的数据,这个范围的大小通过width、height、depth设置,原点通过xoffset、yoffset、zoffset来设置。

渲染缓存

渲染缓存(renderbuffer)是OpenGL所管理的一处高效的内存区域,它可以存储格式化的图像数据。渲染缓存中的数据只有关联到一个帧缓存对象之后才有意义,并且需要保证图像缓存的格式必须与OpenGL要求的渲染格式相符。

void glCreateRenderbuffers(GLsizei n,GLuint *renderbuffers);

分配n个未使用的渲染缓存对象名称,并且将它们保存到renderbuffers中。

void glDeleteRenderbuffers(GLsizei n,const GLuint* ids);

释放n个渲染缓存对象,它们的名称由ids参数提供。如果某个渲染缓存当前已经被绑定,然后又调用了glDeleteRenderbuffers(),那么当前的帧缓存附件点(framebuffer attachment point)会被重新绑定到0,再将这处渲染缓存释放。

glDeleteRenderbuffers()不会产生任何错误。如果名称是不可用的或者是0,它们将被直接忽略。

void glIsRenderbuffer(GLuint renderbuffer);

如果renderbuffer是glCreateRenderbuffers()所产生的一个渲染缓存,则返回GL_TRUE。如果当前帧缓存为0(窗口系统的默认帧缓存),或者给定的值是未分配的,或者已经被glDeleteRenderbuffers()所删除,那么返回GL_FALSE。

void glBindRenderbuffer(GLenum target,GLuint renderbuffer);

创建并绑定一个名字为renderbuffer的渲染缓存。target必须是GL_RENDERBUFFER。而renderbuffer可以是0,即移除当前的绑定,也可以是glCreateRenderbuffers()所生成的一个名字;否则,系统将产生一个GL_INVALID_OPERATION错误。

创建渲染缓存的存储空间

void glNamedRenderbufferStorage(GLuint renderbuffer,GLenum internalformat,GLsizei width,GLsizei height);
void glNamedRenderbufferStorageMultisample(GLuint renderbuffer,GLsizei samples,GLenum internalformat,GLsizei width,GLsizei height);

为渲染缓存renderbuffer分配图像数据的空间。

对于一个可以绘制颜色信息的缓存来说,internalformat必须是下面各个枚举量中的一个:

GL_REDGL_R8GL_R16
GL_RGGL_RG8GL_RG16
GL_RGBGL_R3_G3_B2GL_RGB4
GL_RGB5GL_RGB8GL_RGB10
GL_RGB12GL_RGB16GL_RGBA
GL_RGBA2GL_RGBA4GL_RGB5_A1
GL_RGBA8GL_RGB10_A2GL_RGBA12
GL_RGBA16GL_SRGBGL_SRGB8
GL_SRGB_ALPHAGL_SRGB8_ALPHA8GL_R16F
GL_R32FGL_RG16FGL_RG32F
GL_RGBA32FGL_R11F_G11F_B10FGL_RGB9_E5
GL_R8IGL_R8UIGL_R16I
GL_R16UIGL_R32IGL_R32UI
GL_RGB8IGL_RGB8UIGL_RGB16I
GL_RGB16UIGL_RGB32IGL_RGB32UI
GL_RGBA8IGL_RGBA8UIGL_RGBA16I
GL_RGBA16UIGL_RGBA32IGL_R8_SNORM
GL_R16_SNORMGL_RG8_SNORMGL_RG16_SNORM
GL_RGB8_SNORMGL_RGB16_SNORMGL_RGBA8_SNORM
GL_RGBA16_SNORM

如果渲染缓存是作为深度缓存使用的,那么它必须可以写入深度信息,internalformat须设为:

GL_DEPTH_COMPONENTGL_DEPTH_COMPONENT16GL_DEPTH_COMPONENT32GL_DEPTH_COMPONENT32F

如果渲染缓存作为模板缓存使用,internalformat须设为:

GL_STENCIL_INDEXGL_STENCIL_INDEX1GL_STENCIL_INDEX4GL_STENCIL_INDEX8GL_STENCIL_INDEX16

对于压缩的深度模板格式,internalformat须设为GL_DEPTH_STENCIL,这样就允许渲染缓存绑定到深度缓存或者模板缓存,甚至是合并的深度模板附件点了。

width和height用来设置渲染缓存的像素大小,而samplers可以设置逐像素多重采样的样本个数。对于glRenderbufferStorageMultisample()来说,设置samplers为0时,与glRenderbufferStorage()等价。

如果width和height超出了GL_MAX_RENDERBUFFER_SIZE所定义的数值范围,或者samples超出了GL_MAX_SAMPLES所定义的范围,那么将产生一个GL_INVALID_VALUE错误。

如果internalformat是有符号或者无符号的整数类型(例如名称中带有I或者UI字样的格式枚举量),并且samples非零,而且硬件实现无法支持多重采样的整数缓存的话,那么系统将产生一个GL_INVALID_OPERATION错误。

最后,如果渲染缓存的大小和格式合起来超出了可分配的内存范围,系统将产生一个GL_OUT_OF_MEMORY错误。

//创建一个256x256的RGBA颜色渲染缓存
glCreateRenderbuffers(1,&color);
glNamedRenderbufferStorage(color,GL_RGBA,256,256);

当我们创建了渲染缓存的存储空间之后,需要将它真正关联到帧缓存对象上,然后渲染到这处缓存中。

帧缓存附件

开始渲染时,可以将渲染结果保存到以下几个地方:

  • 创建图像到颜色缓存,甚至是多个颜色缓存,前提是使用了多重渲染目标。
  • 将遮挡信息保存到深度缓存。
  • 将逐像素的渲染掩码保存到模板缓存。

这些缓存类型每个都表示了一种帧缓存的附件。

帧缓存附件
附件名称描述
GL_COLOR_ATTACHMENTi第i个颜色缓存。i的范围从0(默认颜色缓存)到GL_MAX_COLOR_ATTACHMENTS-1。
GL_DEPTH_ATTACHMENT深度缓存
GL_STENCIL_ATTACHMENT模板缓存
GL_DEPTH_STENCIL_ATTACHMENT这是一种特殊的附件类型,用于保存压缩后的深度—模板缓存(此时需要渲染缓存的像素格式被设置为GL_DEPTH_STENCIL)

渲染缓存关联到帧缓存对象:

void glNamedFramebufferRenderbuffer(GLuint framebuffer,GLenum attachment,GLenum renderbuffertarget,GLuint renderbuffer);

将渲染缓存renderbuffer关联到当前绑定的帧缓存对象framebuffer的附件attachment上。

attachment必须是GL_COLOR_ATTACHMENTiGL_DEPTH_ATTACHMENTGL_STENCIL_ATTACHMENT或者GL_DEPTH_STENCIL_ATTACHMENT中的一个。

renderbuffertarget必须设置为GL_RENDERBUFFER,而renderbuffer必须是0(表示将附件所关联的渲染缓存移除)或者从glCreateRenderbuffers()生成的名称,否则将会产生GL_INVALID_OPERATION错误。

例如,关联渲染缓存并用作渲染示例:

enum {Color,Depth,NumRenderbuffers}
GLuint framebuffer,renderbuffer[NumRenderbuffers]

void init()
{
    glCreateRenderbuffers(NumRenderbuffers,renderbuffer);
    glNamedRenderbufferStorage(renderbuffer[Color],GL_RGBA,256,256);
    glNamedRenderbufferStorage(renderbuffer[Depth],GL_DEPTH_COMPONENT24,256,256);
    
    glGenFramebuffers(1,&framebuffer);
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER,framebuffer);
    
    glNamedFramebufferRenderbuffer(framebuffer,GL_COLOR_ATTACHMENT0,
                                   GL_RENDERBUFFER,renderbuffer[Color]);
    glNamedFramebufferRenderbuffer(framebuffer,GL_DEPTH_ATTACHMENT,
                                   GL_RENDERBUFFER,renderbuffer[Depth]);
    
    glEnable(GL_DEPTH_TEST);
}
void display()
{
    //准备渲染到渲染缓存当中
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER,framebuffer);
    glViewport(0,0,256,256);
    //渲染到渲染缓存
    glClearColor(1.0,0.0,0.0,1.0);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    ...
    //设置从渲染缓存读取,然后绘制到窗口系统的帧缓存中
    glBindFramebuffer(GL_READ_FRAMEBUFFER,framebuffer);
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER,0);
    
    glViewport(0,0,windowWidth,windowHeigt);
    glClearColor(0.0,0.0,1.0,1.0);
    
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
    //执行拷贝操作
    glBlitFramebuffer(0,0,255,255,0,0,255,255,
                     GL_COLOR_BUFFER_BIT,GL_NEAREST);
    glfwSwapBuffers(window);
}

帧缓存的完整性

纹理和缓存的格式组合是多种多样的,再加上不同的帧缓存附件的设置,可能会遇到很多不同的问题。因此当修改了帧缓存对象的附件信息之后,最好对帧缓存的当前状态进行一次检查。

GLenum glCheckFramebufferStatus(GLenum target);

返回帧缓存完整性状态检查的结果,返回值见下表。

target:GL_READ_FRAMEBUFFERGL_DRAW_FRAMEBUFFER或者GL_FRAMEBUFFER(等价于GL_DRAW_FRAMEBUFFER)。

如果glCheckFramebufferStatus()产生了一个错误,那么返回值为0。

返回的错误信息
帧缓存完整性状态枚举量描述
GL_FRAMEBUFFER_COMPLETE帧缓存和它的附件完全符合渲染或者数据读取的需求
GL_FRAMEBUFFER_UNDEFINED绑定的帧缓存可能是默认的帧缓存(例如glBindFramebuffer()中设置帧缓存的参数为0),而默认的帧缓存是不存在的。
GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT绑定的帧缓存没有设置必需的附件信息
GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT帧缓存没有关联任何的图像(例如纹理层或者渲染缓存)
GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER每个绘制缓存(例如glDrawBuffers()所指定的GL_DRAW_BUFFERi)都必须有一个附件
GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER每个用glReadBuffer()设置的缓存必须有一个附件
GL_FRAMEBUFFER_UNSUPPORTED关联到帧缓存对象的图像数据与OpenGL设备实现的需求不兼容
GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE帧缓存各个附件所关联的所有图像的采样值数量互相不匹配

在这些错误中,GL_FRAMEBUFFER_UNSUPPORTED可能是最难调试的一类错误,并且它与具体的平台实现也是密切相关的。

帧缓存的无效化

OpenGL的某些实现可能是基于一个内存有限的环境使用的(包括移动平台或者嵌入式设备的OpenGL ES)。而帧缓存有可能占用相当大的内存资源(尤其是多个帧缓存、多重采样的颜色附件以及纹理的情形)。

因此OpenGL提供了一种机制,可以将帧缓存的一块区域或者整体声明为不再使用的,可以立即释放。

void glInvalidateFramebuffer(GLenum target,GLsizei numAttachments,const GLenum* attachments);
void glInvalidateSubFramebuffer(GLenum target,GLsizei numAttachments,const GLenum* attchments,GLint x,GLint y,GLsizei width,GLsizei height);

设置绑定的帧缓存对象的一部分或者整体将不再保留。对于这两个函数而言,

target参数必须是GL_DRAW_FRAMEBUFFERGL_READ_FRAMEBUFFER或者GL_FRAMEBUFFER(同时指向绘制和读取目标)。

attachments中保存了一系列附件的标识符,包括:GL_COLOR_ATTACHMENTiGL_DEPTH_ATTACHMENT,以及GL_STENCIL_ATTACHMENT

numAttachments设置了这个附件列表中元素的个数。

对于glInvalidateSubFramebuffer()来说,可以通过左下角的坐标(x,y)以及宽度width和高度height(从(x,y)出发)来设置它的作用区域,而这个区域对于所有attachments中的附件而言,都将不再使用。

这两个函数会触发以下错误:

  • 如果标识符不是来自于上面的列表,那么返回GL_INVALID_ENUM
  • 如果某个附件的索引(例如GL_COLOR_ATTACHMENTi的i值)大于等于颜色附件的最大索引值,那么返回GL_INVALID_OPERATION
  • 如果numAttachments、width或者height为负数,那么返回GL_INVALID_VALUE

多重渲染缓存的同步写入

帧缓存对象的多渲染缓存(或者多纹理)特性,也就是在一个片元着色器中同时写入到多个缓存的能力,通常也叫MRT(多重渲染目标,multiple-render target)渲染。

使用这项技术,需要设置一个帧缓存对象并附加多组颜色值(可能还有深度和模板值)附件,再对片元着色器进行一定的修改。片元着色器通过out变量来输出数据,若设置out变量与帧缓存附件之间的对应关系,只需要使用layout限定符直接将变量设置到正确的位置即可。

//设置MRT渲染的layout限定符
layout(location = 0) out vec4 color;
layout(location = 1) out vec4 normal;

如果在MRT渲染的时候,使用了双源融混的机制,那么你只能设置layout限定符当中的location和index选项。

如果没有用layout指定关联,OpenGL会在着色器链接的阶段完成这项工作,可以使用glBindFragDataLocation()函数来引导链接程序完成正确的关联工作,也可以使用glBindFragDataLocationIndexed()来设置片元的index索引值。

如果在着色器代码中已经设置了片元着色器的关联关系,那么它会继续沿用,这两个函数所设置的位置值不会产生作用。

void glBindFragDataLocation(GLuint program,GLuint colorNumber,const GLchar *name);
void glBindFragDataLocationIndexed(GLuint program,GLuint colorNumber,GLuint index,const GLchar* name);

使用colorNumber的值来对应片元着色器的变量name,从而与着色器程序program的输出位置进行关联。

对于有颜色索引的情形,index可以用来同时设置输出的索引位置。

如果program不是一个着色器程序,或者index大于1,或colorNumber大于等于最大的颜色附件索引值,都将生成GL_INVALID_VALUE错误。

GLint glGetFragDataLocation(GLuint program,const GLchar* name);
GLint glGetFragDataIndex(GLuint program,const GLchar* name);

如果片元着色器变量name已经与链接后的着色器程序相关联,返回它的位置或者索引。

如果name不是一个可用的程序变量,或者program已经链接但是没有设置片元着色器,或者program没有完成链接的话,返回值均为-1。对于program链接失败的情形,同时产生一个GL_INVALID_OPERATION错误。

选择颜色缓存进行读写操作

绘制或者读取操作的结果通常与以下几种颜色缓存的内容关联:

  • 默认帧缓存中的前、后、左前、左后、右前、右后缓存;
  • 或者用于自定义帧缓存对象中的前缓存,或者任意渲染缓存附件。

可以选择一个独立的缓存来完成目标的绘制或者读取。对于绘制操作,可以同时向多个缓存中写入目标。

void glDrawBuffer(GLenum mode);
void glDrawBuffers(GLsizei n,const GLenum* buffers);

设置可以进行写入或者清除操作的颜色缓存,同时将禁止之前一次glDrawBuffer()或者glDrawBuffers()所设置的缓存。可以一次性启用多个缓存。mode的值必须是下面的枚举量:

GL_FRONTGL_FRONT_LEFTGL_NONEGL_BACKGL_FRONT_RIGHTGL_FRONT_AND_BACKGL_LEFTGL_BACK_LEFTGL_COLOR_ATTACHMENTiGL_RIGHTGL_BACK_RIGHT

如果mode或者buffers中的对象不属于上述任何一种,那么产生一个GL_INVALID_ENUM错误。

若当前绑定一个帧缓存对象,并且不是默认帧缓存,那么只能使用GL_NONE或者GL_COLOR_ATTACHMENTi模式,否则也会产生GL_INVALID_ENUM错误。

名称中忽略了LEFTRIGHT的枚举量,可以同时用于立体缓存中的左缓存和右缓存;以此类推,名称中不包含FRONT或者BACK的枚举量,也可以同时用于前缓存和后缓存。默认条件下,mode设置为GL_BACK,以用于双重缓冲的情形。

glDrawBuffers()函数可以设置多个颜色缓存来接收多组颜色值。buffers是一个缓存枚举量的数组,它只能接受GL_NONEGL_FRONT_LEFTGL_FRONT_RIGHTGL_BACK_LEFTGL_BACK_RIGHT

void glReadBuffer(GLenum mode);

设置可以用作像素读取的缓存,像素读取的相关函数包括glReadPixels()glCopyTexImage*()glCopyTexSubImage*()等。这个函数会禁止上一次使用glReadBuffer()所设置的缓存。

mode的值必须是下面几种枚举量中的一种:

GL_FRONTGL_FRONT_LEFTGL_NONEGL_BACKGL_FRONT_RIGHTGL_FRONT_AND_BACKGL_LEFTGL_BACK_LEFTGL_COLOR_ATTACHMENTiGL_RIGHTGL_BACK_RIGHT

如果mode的值不属于上述任何一种,那么将产生GL_INVALID_ENUM错误。

如果帧缓存对象有多个附件的话,可以自由控制作为附件的渲染缓存的方方面面,例如控制剪切盒的大小,或者执行融混。可以使用glEnablei()glDisablei()指令,对每个附件分别进行精确的控制。

void glEnablei(GLenum capability,GLuint index);
void glDisablei(GLenum capability,GLuint index);

开启或者关闭缓存index的某项功能。

如果index大于等于GL_MAX_DRAW_BUFFERS,那么将产生一个GL_INVALID_VALUE错误。

GLboolean glIsEnabledi(GLenum capability,GLuint index);

判断缓存index是否已经开启了某项功能。

如果index超出了许可的范围,将产生一个GL_INVALID_VALUE错误。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值