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

纹理综述

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

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

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

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

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

基本纹理类型

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

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

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

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

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

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

使用概述

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

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

纹理目标和对应的采样器类型
目标(GL_TEXTURE_*) 采样器类型 维度
1D sampler1D 一维
1D_ARRAY sampler1DArray 一维数组
2D sampler2D 二维
2D_ARRAY sampler2DArray 二维数组
2D_MULTISAMPLE sampler2DMS 二维多重采样
2D_MULTISAMPLE_ARRAY sampler2DMSArray 二维多重采样数组
3D sampler3D 三维
CUBE samplerCube 立方体映射纹理
ARRAY samplerCubeArray 立方体映射纹理数组
RECTANGLE samplerRect 二维长方形
BUFFER samplerBuffer 一维缓存
  • 长方形纹理目标(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_*)
1D 1D
1D_ARRAY 1D_ARRAY
2D 2D
2D_ARRAY 2D_ARRAY
2D_MULTISAMPLE 2D_MULTISAMPLE_ARRAY
3D 3D
CUBE CUBE
CUBE_ARRAY CUBE_ARRAY
RECTANGLE RECTANGLE
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_R8 GL_RED 8
GL_R8_SNORM GL_RED s8
GL_R16 GL_RED 16
GL_R16_SNORM GL_RED s16
GL_RG8 GL_RG 8 8
GL_RG8_SNORM GL_RG s8 s8
GL_RG16 GL_RG 16 16
GL_RG16_SNORM GL_RG s16 s16
GL_R3_G3_B2 GL_RGB 3 3 2
GL_RGB4 GL_RGB 4 4 4
GL_RGB5 GL_RGB 5 5 5
GL_RGB565 GL_RGB 5 6 5
GL_RGB8 GL_RGB 8 8 8
GL_RGB8_SNORM GL_RGB s8 s8 s8
GL_RGB10 GL_RGB 10 10 10
GL_RGB12 GL_RGB 12 12 12
GL_RGB16 GL_RGB 16 16 16
GL_RGB16_SNORM GL_RGB s16 s16 s16
GL_RGBA2 GL_RGBA 2 2 2 2
GL_RGBA4 GL_RGBA 4 4 4 4
GL_RGB5_A1 GL_RGBA 5 5 5 1
GL_RGBA8 GL_RGBA 8 8 8 8
GL_RGBA8_SNORM GL_RGBA s8 s8 s8 s8
GL_RGB10_A2 GL_RGBA 10 10 10 2
GL_RGB10_A2UI GL_RGBA ui10 ui10 ui10 ui2
GL_RGBA12 GL_RGBA 12 12 12 12
GL_RGBA16 GL_RGBA 16 16 16 16
GL_RGBA16_SNORM GL_RGBA s16 s16 s16 s16
GL_SRGB8 GL_RGB 8 8 8
GL_SRGB8_ALPHA8 GL_RGBA 8 8 8 8
GL_R16F GL_RED f16
GL_RG16F GL_RG f16 f16
GL_RGB16F GL_RGB f16 f16 f16
GL_RGBA16F GL_RGBA f16 f16 f16 f16
GL_R32F GL_RED f32
GL_RG32F GL_RG f32 f32
GL_RGB32F GL_RGB f32 f32 f32
GL_RGBA32F GL_RGBA f32 f32 f32 f32
GL_R11F_G11F_B10F GL_RGB f11 f11 f10
GL_RGB9_E5 GL_RGB 9 9 9 5
GL_R8I GL_RED i8
GL_R8UI GL_RED ui8
GL_R16I GL_RED i16
GL_R16UI GL_RED ui16
GL_R32I GL_RED i32
GL_R32UI GL_RED ui32
GL_RG8I GL_RG i8 i8
GL_RG8UI GL_RG ui8 ui8
GL_RG16I GL_RG i16 i16
GL_RG16UI GL_RG ui16 ui16
GL_RG32I GL_RG i32 i32
GL_RG32UI GL_RG ui32 ui32
GL_RGB8I GL_RGB i8 i8 i8
GL_RGB8UI GL_RGB ui8 ui8 ui8
GL_RGB16I GL_RGB i16 i16 i16
GL_RGB16UI GL_RGB ui16 ui16 ui16
GL_RGB32I GL_RGB i32 i32 i32
GL_RGB32UI GL_RGB ui32 ui32 ui32
GL_RGBA8I GL_RGBA i8 i8 i8 i8
GL_RGBA8UI GL_RGBA ui8 ui8 ui8 ui8
GL_RGBA16I GL_RGBA i16 i16 i16 i16
GL_RGBA16UI GL_RGBA ui16 ui16 ui16 ui16
GL_RGBA32I GL_RGBA i32 i32 i32 i32
GL_RGBA32UI GL_RGBA ui32 ui32 ui32 ui32

大多数情况下,我们只需要一个尺寸参数即可。这种时候所有的像素分量都是按照同样的位尺寸来存储的。默认情况下,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)的格式来表达打包之前的纹理数据,然后使用内部格式将它们保存到纹理中。

</
外部纹理格式
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值