OpenGL 数据处理(下)

1 纹理基础

纹理是一种结构化的存储形式(Textures are a structured form of storage),着色器可以从其中读取数据,也能将数据写入其中。通常用于储存图像数据,并有着多种形式。最常见的纹理为2维,但是也存在1维、3维或者数组(多个纹理叠加形成单一的逻辑对象)、立方体等形式的纹理。

为了创建一个纹理,首先需要调用函数(glGenTextures())让OpenGL保存一个名字以备使用。该名字表示了一个已经被创建的纹理对象,只有在将其绑定至纹理目标(texture target)后才开启它作为纹理的生命周期。这和将缓存对象绑定到缓存绑定点类似。然而不同的是,一旦将一个纹理绑定到某个目标后,它的类型在它被回收前都不能改变。

纹理和图片指的都指经过编码的图像像素数据。不同的是常见的图片文件格式,比如PNG,JPG,BMP等,是图像为了存储信息而使用的对信息的特殊编码方式。它存储在磁盘中,或者内存中,但是并不能被GPU所识别。这些文件格式当被读入后,还是需要经过CPU解压成bitmap,再传送到GPU端进行使用。

而纹理格式是能被GPU所识别的像素格式,能被快速寻址并采样。压缩纹理,是一种GPU能直接读取并显示的格式,使得图像无需解压即可进行渲染,节约大量的内存。其中未压缩纹理的格式如下。

RGBA8888:每个像素4字节,RGBA通道各占用8位, OpenGL内GL_RGBA默认类型。
RGBA4444: 每个像素2字节,RGBA通道各占用4位。
RGB888: 每个像素3字节,RGB通道各占用8位,无透明通道。
RGB565:每个像素2字节,RGB通道各占用5/6/5位,无透明通道。
RGBA5551: 每个像素2字节,RGB通道各占用5位,透明通道1位,所以要么完全透明要么不透明。

正如图片的JPEG和PNG等压缩格式一样,纹理也有其各种压缩格式,如(DXT纹理压缩格式,.dds)(ETC纹理压缩格式,.ktx多纹理和.pkm单纹理)(PVRTC纹理压缩格式,.pvr)。具体的压缩格式不深入讨论,但是需要知道不同GPU芯片制造商采用了不同的GPU架构,因此它们支持的纹理压缩格式不同,其中iPhone系列使用的是Imagination Technologies的PVRTC架构GPU,其集成于iPhone A系列处理核心之内。不同芯片制造商的GPU支持纹理可参考文章1文章2。下面图片来源于文章2。

1.1 原始图像数据

计算机图形最初的时候只能用位图(bitmap)表示,最早的计算机图形对于某个点只能用0和1表示黑色或者白色,后来可以用从0-256的值来表示其灰度。

1.1.1 像素包装

图像数据在内存中很少以紧密包装的形式存在,在很多硬件平台上,出于性能考虑,图像的每一行应该都从某个特定对齐地址开始。在默认情况下,OpenGL采用4个字节的对齐方式。例如,有一张RGB图像,包含3个分量,每个分量占1个字节,如果图像宽度为199像素,那么每一行需要的字节数为597字节。但是如果硬件的体系结构是以4字节排列,那么图像的每一行末尾都会填充额外的3个空字节,从而使得每一行的内存地址偏移量为4的整数倍。尽管这样表面上看起来是浪费了内存空间,但是这种排列能够让大多数CPU更高效的获取数据块。另外许多未经压缩的图形格式也遵循这种惯例,如Windows中的.BMP文件。Targa(.TGA)是以1字节排列的。

在向OpenGL提交图像数据或从OpenGL中获取图像数据时,OpenGL需要知道对数据以何种方式包装和解包装操作。通过函数void glPixelStorei (GLenum pname, GLint param)glPixelStoref (GLenum pname, GLfloat param)可以改变或者恢复像素的存储方式。例如需要改成紧密包装方式(即像素行的对齐方式,可选1.2.4.8)调用函数glPixelStorei(GL_UNPACK_ALIGNMENT, 1)。其参数都是以GL_UNPACK_和GL_PACK_为前缀方式成对出现,前着表示从内存中读取数据(对函数glReadPixels产生影响),后者表示存储数据至内存中(对调用函数glTexImage2D等产生影响)。该函数的详细介绍参考官网或者原著。

1.1.2 像素图

现在的计算机图形都以像素图(pixmap)的方式存储,它有两种不同的方式存储图形,其一为亮度信息(luminance)加颜色分量(Cb, Cr),其二为RGB(A)颜色分量。

OpenGL中无法直接读取像素图。只能通过第三方库获取像素图中的像素数据。此外,在OpenGL中允许将GPU中的颜色缓存读取到内存中。对应函数如下。其中pixels所指的内存空间需要足够存储图形。如果指定的窗口坐标超过了允许的范围,实际只能获得OpenGL缓存区内部数据。Format表示了pixel指向的内存空间颜色布局方式。

void  glReadPixels (GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid* pixels)

此外需要注意,glReadPixels从图形硬件中复制数据,通常会通过总线传输到系统内存。在这种情况下,应用程序必将被阻塞,直至内存传输完成。此外,如果指定了一个与图形硬件本地排列不同的像素布局,那么在数据进行重定格式时将产生额外的性能开销。另外需要注意的是该函数将数据写入内存中,它的行为受到上文的函数glPixelStorei设置部分参数的影响。

另外该函数对于使用双缓存机制的OpenGL环境,默认的是读取后台缓存区数据,可以通过调用如下函数来使其读取前台缓存区,其参数为GL_FRONT、GL_BACK等。

void glReadBuffer(GLenum mode);
1.1.3 包装的像素格式

上一节中函数glReadPixels的参数type,通常使用的是GL_FLOAT等类型,但是也提供例如GL_UNSIGNED_BYTE_3_2_2等包装的RGB以及一些包装的RGBA值类型,为的是允许图形数据以更多的压缩形式存储,以便与更广泛的颜色图形硬件匹配。

GL_UNSIGNED_BYTE_3_2_2包装类型指用1个字节即8位存储三个颜色分量,其存储顺从高位到低位分别为第1至第3个分量。GL_UNSIGNED_BYTE_3_2_2_REV以相反顺序存储。

1.1.4 保存像素

从OpenGL的缓存区读取出来的颜色数据可以被存储到本地图片中。原书中使用的是GLTools三方库中的API,此处不具体介绍。对于mac OS环境下,可以使用CoreGraphics框架中的相关API使用这些像素数据绘制NSImage图片并存储至磁盘中。代码如下。

- (void)saveImage {
    CGSize imageSize = CGSizeZero;
    unsigned int imageByteSize = imageSize.width * imageSize.height * 4;
    GLubyte *rawImagePixels = (GLubyte *)malloc(imageByteSize);
    glReadPixels(0, 0, imageSize.width, imageSize.height, GL_RGBA, GL_FLOAT, rawImagePixels);
    
    CGColorSpaceRef genericRGBColorspace = CGColorSpaceCreateDeviceRGB();
    CGContextRef imageContext = CGBitmapContextCreate(rawImagePixels, imageSize.width, imageSize.height, 8, imageSize.width * 4, genericRGBColorspace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedLast);
    CGImageRef cgImage = CGBitmapContextCreateImage(imageContext);
    NSImage *image = [[NSImage alloc] initWithCGImage:cgImage size:imageSize];
    CGContextRelease(imageContext);
    CGColorSpaceRelease(genericRGBColorspace);
    CGImageRelease(cgImage);
    free(rawImagePixels);
    
    [image lockFocus];
    //先设置 下面一个实例
    NSBitmapImageRep *bits = [[NSBitmapImageRep alloc]initWithFocusedViewRect:NSMakeRect(0, 0, imageSize.width, imageSize.height)];
    [image unlockFocus];
    //再设置后面要用到得 props属性
    NSDictionary *imageProps = [NSDictionary dictionaryWithObject:[NSNumber numberWithFloat:0.9] forKey:NSImageCompressionFactor];
    //之后 转化为NSData 以便存到文件中
    NSData *imageData = [bits representationUsingType:NSJPEGFileType properties:imageProps];
    //设定好文件路径后进行存储就ok了
    NSString *filePath = @"";
    [imageData writeToFile:filePath atomically:YES];
}
1.1.5 读取像素

原书中使用GLTools三方库相关API读取targa图片文件中的像素数据。而在mac OS环境中,可以先通过NSImage读取文件,然后通过Core Graphics框架中的相关API将其转化为像素数据,其代码如下。

// 读取图片
NSImage *image = [NSImage imageNamed:@""];
NSData *imageNSData = [image TIFFRepresentation];
CGImageSourceRef imageSource = CGImageSourceCreateWithData((CFDataRef)imageNSData, NULL);
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
// 将图片转换为二进制数据
CGSize pixelSizeToUseForTexture = image.size;
GLubyte *imageData = calloc(1, (int)pixelSizeToUseForTexture.width * (int)pixelSizeToUseForTexture.height * 4);
CGColorSpaceRef genericRGBColorspace = CGColorSpaceCreateDeviceRGB();
CGContextRef imageContext = CGBitmapContextCreate(imageData, (size_t)pixelSizeToUseForTexture.width, (size_t)pixelSizeToUseForTexture.height, 8, (size_t)pixelSizeToUseForTexture.width * 4, genericRGBColorspace, kCGBitmapByteOrder32Little | kCGImageAlphaPremultipliedLast);
CGContextDrawImage(imageContext, CGRectMake(0, 0, pixelSizeToUseForTexture.width, pixelSizeToUseForTexture.height), imageRef);
CGContextRelease(imageContext);
CGColorSpaceRelease(genericRGBColorspace);
//此时已经获取的像素数据存储在imageData所表示的内存空间内,使用像素数据生成纹理的方法在后面载入纹理时候介绍
free(imageData);

1.2 载入纹理

一旦获取到像素数据后,可以调用函数载入纹理,纹理一旦被载入,这些纹理就会成为当前纹理状态的一部分,有三个如下OpenGL函数最常用来从缓存中载入纹理。

void glTexImage1D (GLenum target, GLint level, GLint internalformat, GLsizei width, GLint border, GLenum format, GLenum type, const GLvoid *pixels);
void glTexImage2D (GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid *pixels);
void glTexImage3D (GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLenum format, GLenum type, const GLvoid *pixels);

这里只接受了载入1维、2维和3维纹理,立方体纹理后面介绍。该函数从data指向的内存地址复制纹理信息,这种数据复制可能会有很大的开销,稍后将讨论几个减轻这个问题的方法。其target参数通常使用GL_TEXTURE_1(2/3)D,同时也可以使用GL_PROXY_TEXTURE_1(2/3)D指定代理纹理,代理纹理内容后面介绍。level参数指定了mip贴图层次,对于非mip贴图的纹理,该参数总为0。

internalformat参数指定了纹理单元颜色存储方式,以及是否进行压缩,常用GL_RGBA。参数width、height和depth指定加载纹理的宽度、高度和深度,此处需要注意的是,这些值最好为2的整数幂,对于OpenGL2.0以前的版本,不符合要求的值导致纹理贴图被隐式禁用,尽管之后的版本可以使用不符合要求的数据,但是会降低实现效率。

参数border指定了一个纹理边界宽度,这个值暂时设置为0,在介绍纹理过滤时,该参数扮演了重要的角色。参数format和type表示pixels指向的像素数据中的数据格式。

1.2.1 使用颜色缓存区

一维和二维纹理可以从GPU的颜色缓存区加载数据并将其作为一个新的纹理来使用。函数如下。源缓存区通过glReadBuffer设置。2D颜色缓存区是没有深度,因此不能读取3D纹理。

void glCopyTexImage1D (GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLint border);
void glCopyTexImage2D (GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLsizei height, GLint border);
1.2.2 更新纹理

重复加载纹理可能会成为性能的瓶颈,当某个已加载的纹理数据不再使用,可以替代掉其中的部分或者全部,替换纹理内容往往比使用函数glTextImage加载纹理更快。更新纹理函数如下。其中绝大部分参数与glTextImage函数中的参数准确对应。xOffset、yOffset、zOffset参数指定了在原纹理贴图中开始替换纹理数据的偏移量。

void glTexSubImage1D (GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLenum type, const GLvoid *pixels);
void glTexSubImage2D (GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const GLvoid *pixels);
void glTexSubImage3D (GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const GLvoid *pixels);

另外,OpenGL还可以从GPU颜色缓存区读取纹理,并插入或替换原来纹理的一部分,其函数如下。这里需要注意的是由于颜色缓存区是2D的,因此glCopyTexSubImage3D获取到的是一个颜色平面。

void glCopyTexSubImage1D (GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width);
void glCopyTexSubImage2D (GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint x, GLint y, GLsizei width, GLsizei height);
void glCopyTexSubImage3D (GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height);
1.2.3 纹理对象

纹理状态包含纹理图像本身和一组纹理参数,这些参数控制过滤和纹理坐标的行为。使用函数glTexParameter可以设置这些纹理状态参数。glTexImage和glTexSubImage函数调用时耗费内存较多,在纹理直接进行快速切换或者重新加载不同纹理是开销很大的操作。OpenGL可以通过纹理对象管理多重纹理并在他们之间进行快速切换。纹理状态由当前绑定纹理对象维护,纹理对象由一个无符号整数标识。可以通过下面函数生成一个纹理对象数组。

void glGenTextures (GLsizei n, GLuint *textures);

绑定其中的某个纹理可以通过函数glBindTexture完成。此后所有纹理加载和纹理参数设置只影响当前绑定的纹理对象。调用函数glDeleteTextures完成。多次调用函数在销毁大容量纹理时可能会有一定的延迟。判断某个句柄是否问纹理句柄可以调用函数glIsTexture

1.2.4 加载纹理示例

将读取到的像素数据加载到纹理中代码如下,注意此处imageData未像素数据,如果改段内存为malloc函数分配,在使用完后必须释放内存。

// 将二进制数据写入纹理中
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// 下面函数设置了纹理参数,用于描述当纹素坐标不为整数时,以及纹理坐标不在[0-1]范围内时,纹理颜色采样策略
// 具体请参考文章http://blog.csdn.net/wangdingqiaoit/article/details/51457675
// 这里使用最近纹素坐标采样方式,下文还会继续讲解设置纹理参数
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (int)pixelSizeToUseForTexture.width, (int)pixelSizeToUseForTexture.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);

2 纹理应用

2.1 创建和初始化纹理(Creating and Initializing Textures)

完整的纹理创建过程包括生成一个名字,将其绑定至某个纹理目标,然后告诉OpenGL需要在其中存储图像的尺寸。其使用实例的代码如下。

// The type used for names in OpenGL is GLuint
GLuint texture;

// Generate a name for the texture
glGenTextures(1, &texture);

// Now bind it to the context using the GL_TEXTURE_2D binding point
glBindTexture(GL_TEXTURE_2D, texture);

// Specify the amount of storage we want to use for the texture
// 注意该接口在OpenGL 4.3后才能使用,因此mac中无法调用该函数
glTexStorage2D(GL_TEXTURE_2D,    // 2D texture
               1,                // 1 mipmap level
               GL RGBA32F,       // 32-bit floating-point RGBA data
               256, 256);        // 256 x 256 texels

该函数的参数mipmapping表示表示纹理细分级数,该技术根据该参数生成一系列不同分辨率的贴图,在渲染时根据物体远近选取适当分辨率的贴图,从而加快渲染速度并缓解锯齿现象。internal format为纹理内部保存图像的数据格式,width和height为图像的宽和高。该函数调用后,OpenGL将会分配足够的内存空间用于存储纹理。接下来需要使用函数glTexSubImage2D()为缓存中输入数据。其示例如下。

// Define some data to upload into the texture
float * data = new float[256 * 256 * 4];

// generate_texture() is a function that fills memory with image data
generate_texture(data, 256, 256);

// Assume the texture is already bound to the GL_TEXTURE_2D target 
glTexSubImage2D(GL_TEXTURE_2D,     // 2D texture
                0,                 // Level 0
                0, 0,              // Offset 0, 0
                256, 256,          // 256 x 256 texels, replace entire image
                GL_RGBA,           // Four channel data
                GL_FLOAT,          // Floating-point data
                data);             // Pointer to data
// Free the memory we allocated before - OpenGL now has our data
delete [] data;

简单的纹理贴图实例如下,源代码

2.2 纹理目标和类型(Texture Targets and Types)

OpenGL中可用的纹理类型如下,每个关键字都省略了相同部分GL_TEXTURE_
1D:一维纹理。 2D:二维纹理。 3D:三维纹理。RECTANGLE:矩形纹理。1D_ARRAY:一维纹理数组。2D_ARRAY:二维纹理数组。CUBE_MAP:立方体纹理。CUBE_MAP_ARRAY:立方体纹理数组。BUFFER:缓存纹理。2D_MULTISAMPLE:二维多重样本纹理。2D_MULTISAMPLE _ARRAY:二维多重样本纹理数组。

GL_TEXTURE_2D类型的纹理是最常用类型,通常用于包裹模型表面。GL_TEXTURE_1D类型的纹理被认为高度为1的二维纹理。另外,3D纹理可以用于表示具有容量的纹理(can be used to represent a volume),具有三维纹理坐标。矩形纹理是二维纹理的一个特例,它们在通过着色器读取数据的方式以及支持的参数方面有细微的差别。由于并非所有的硬件都支持尺寸不为2的整数次幂的纹理,矩形纹理便被引入。现代的硬件通常都支持尺寸不为2的整数次幂的纹理,因此矩形纹理可以被看做是2维纹理的一个子集,通常不会使用。

GL_TEXTURE_1D_ARRAYGL_TEXTURE_2D_ARRAY表示组合在单个对象中的一组纹理图像。在本章后面会详细讨论。同样的,立方体纹理表示了六张方形图片集合,它们组合成一个立方体,该立方体通常可以被用于模拟光照环境。正如GL_TEXTURE_1D_ARRAYGL_TEXTURE _2D_ARRAYGL_TEXTURE_CUBE_MAP_ARRAY 表示了立方体纹理的一个数组。

GL_TEXTURE_BUFFER表示的纹理是类似于1D纹理的一个特殊类型,不同之处在于它的存储实际上位于缓存之中。除此之外,该类型纹理的容量比1D纹理大得多。OpenGL中最小的要求是65536纹元(The minimum requirement from the OpenGL specification is 65536 texels),但是实际上大多数实现允许创建更大的缓存---通常可达到几百百万字节。缓存纹理同样缺乏某些1D纹理支持的特性,如过滤(filtering)和mipmaps。

最后,GL_TEXTURE_2D_MULTISAMPLEGL_TEXTURE_2D_MULTISAMPLE_ARRAY用于多重采样抗锯齿(multi-sample antialiasing),该计数用于提高图形质量,特别是对于线条和多边形的边缘。

2.3 在着色器中从纹理中读取数据(Reading from Textures in Shaders)

创建纹理对象并将数据传入其中后,可以在着色器中读取数据,例如用于为片段着色。在着色器中,纹理被声明为采样变量(sampler variables),它们通过sampler类型的统一变量和外部关联。GLSL中有多个采样类型变量关键词用于声明不同类型的纹理对象。在GLSL中可以使用内置函数texelFetch以及坐标值获取纹理中对应坐标的颜色值。其用法如下。

#version 410 core 
uniform sampler2D s; 
out vec4 color;

void main(void) {
  color = texelFetch(s, ivec2(gl_FragCoord.xy), 0);
}

上述代码中gl_FragCoord为OpenGL的内置变量,表示的是正在被处理的片段在窗口坐标系中的坐标,坐标的单位为浮点型数据。然而函数texelFetch的参数必须是从(0,0)到(width, height)范围内的整数型数据。第三个参数是纹理的mipmap等级。因为此处纹理并未启用mipmap功能,因此使用0。

2.3.1 采样器类型(Sampler Types)

纹理类型和纹理声明关键字的对应如下。(GL_TEXTURE_1D,sampler1D)(GL_TEXTURE_2D,sampler2D)(GL_TEXTURE_3D,sampler3D)(GL_TEXTURE_RECTANGLE,sampler2DRect)(GL _TEXTURE_1D_ARRAY,sampler1DArray)(GL_TEXTURE_2D_ARRAY,sampler2DArray)(GL _TEXTURE _CUBE_MAP,samplerCube)(GL_TEXTURE_CUBE_MAP_ARRAY,samplerCubeArray)(GL _TEXTURE_BUFFER,samplerBuffer)(GL_TEXTURE_2D_MULTISAMPLE,sampler2DMS)(GL_ TEXTURE_2D_MULTISAMPLE_ARRAY,sampler2DMSArray)

GLSL中的各个sanpler类型纹理数据表示的未浮点型数据,它们也能在纹理中存储有符号和无符号的数据,并在着色器中读取这些数据。为了表示包含有符号整形数据的纹理数据变量,只需在对应关键词前面加前缀i,同样的,为了表示包含无符号整形数据的纹理数据变量,只需要在对应关键词之前加前缀u。例如isampler2Dusampler2D

从纹理中读取数据的函数有多个变形。他们的第一个参数为纹理数据,第二参数为采样作标,第三个参数为纹理mipmap级别。函数如下。函数的返回值一定是4维向量,如果纹理中的颜色通道小于4,那么缺少的通道将返回0。如果返回值中的某个成员永远不会被使用,那么着色器编译器将会优化掉冗余代码。

vec4 texelFetch(sampler1D s, int P, int lod); 
vec4 texelFetch(sampler2D s, ivec2 P, int lod); 
ivec4 texelFetch(isampler2D s, ivec2 P, int lod); 
uvec4 texelFetch(usampler3D s, ivec3 P, int lod);

2.4 从文件中读取纹理(Loading Textures from Files)

存储图片的格式很多,但是很少的格式能够很好的存储OpenGL支持的所有属性,或者能够描述一下例如mipmaps,cubemaps等高级特性。一个满足上述条件的纹理格式为.KTX或者Khronos TeXture format,它专用于OpenGL中纹理的存储,它作为容器能存储压缩后的纹理,也能储存此处使用的未压缩纹理。实际上,.ktx文件格式中包括了大多数需要传递到纹理函数中的参数,这些函数能直接从文件中加载纹理,如glTexStorage2D()glTexSubImage2D().KTX文件的头结构如下。

struct header {
  unsigned char      identifier[12];    用于验证是否为合法.ktx文件
  unsigned int       endianness;        表示文件中数据是以大端模式还是小端模式存储
  unsigned int       gltype;            GLenum类型数据
  unsigned int       gltypesize;        以gltype为单位的单个数据字节大小,当数据的大小端类型
                                        和系统环境不匹配时,在加载数据时需要转换
  unsigned int       glformat;          GLenum类型数据
  unsigned int       glinternalformat;        GLenum类型数据
  unsigned int       glbaseinternalformat;    GLenum类型数据
  unsigned int       pixelwidth;
  unsigned int       pixelheight;
  unsigned int       pixeldepth;
  unsigned int       arrayelements;
  unsigned int       faces;
  unsigned int       miplevels;
  unsigned int       keypairbytes;      在数据头末尾,纹理数据之前存储额外信息
};

超级圣经中提供了一个C++loader类用于加载纹理中的数据,但是遗憾的是其内部使用的是OpenGL4.3的接口,因此在mac上无法正常使用,因此不具体研究,需要了解可以查看官方源代码。这里只需要指定其加载原来还是通过c语言文件操作读取二进制流,根据KTX文件官方的格式规范解析其中的像素数据,随后通过函数glTexStorage2D申请纹理内存,通过函数glTexSubImage2D和像素数据载入。

2.4.1 纹理坐标(Texture Coordinates)

在前面的示例中,片段着色器中直接使用当前的标准设备坐标作为纹理坐标读取数据,另外,OpenGL也允许使用自定义的纹理坐标。通常对于在片段着色器中的多重纹理而言,推荐使用同于的纹理坐标。对应的顶点着色器代码如下。

#version 410 core 
uniform mat4 mv_matrix;
uniform mat4 proj_matrix;
layout (location = 0) in vec4 position;
layout (location = 4) in vec2 tc; 
out VS_OUT {
  vec2 tc;
} vs_out;

void main(void) {
  //Calculate the position of each vertex
  vec4 pos_vs = mv_matrix * position;
  //Pass the texture coordinate through unmodified
  vs out.tc = tc;
  gl_Position = proj_matrix * pos_vs;
}

片段着色器代码如下。

#version 410 core
layout (binding = 0) uniform sampler2D tex_object;
// Input from vertex shader
in VS_OUT {
  vec2 tc;
} fs_in;
// Output to framebuffer
out vec4 color;

void main(void) {
  // Simply read from the texture at the (scaled) coordinates, and assign the result to the shader’s output.
  color = texture(tex_object, fs_in.tc * vec2(3.0, 1.0));
}

通过为顶点指定纹理坐标的方式,可以使用纹理贴图布满整个模型,得到如下效果,源代码。纹理坐标可以是程序生成,也可以是艺术家使用模型编辑软件将其包含在模型文件中。

2.4.2 纹理的使用

纹理的使用包含初始化纹理对象,程序和着色器之间数据通信,着色器内部读取纹理数据三个重要阶段。前面已经讲到,OpenGL中当前线程会存在一个OpenGLContext,纹理对象只有绑定到上下文中才能被着色器使用。一个着色器可以包含多个纹理对象,它们可以通过GL_TEXTURE0等枚举值关联。

默认情况下,调用函数glBindTexture(GL_TEXTURE_2D, textureID);会默认将当前纹理关联至GL_TEXTURE0,再次调用绑定函数会将最后一次绑定的纹理和GL_TEXTURE0关联,之前绑定的纹理和GL_TEXTURE1/2...关联。并且此时着色器内部的的所有纹理变量都会从GL_TEXTURE0关联的纹理对象中读取数据。

注意,通常需要在渲染代码中将纹理数据输入至着色器的纹理变量中,但是有时并不需要手动指定,OpenGL会自动关联数据,如上文的示例。但是有时对于复杂的逻辑必须手动关联这些数据。调用函数glUniform1i(_textureLoc, 0);,这里第二个参数的值和例如GL_TEXTURE0等枚举变量的最后一个数值一一对应。

对于需要启用多个纹理对象,完整的使用方式如下。

// 1. 在着色器中声明纹理变量
uniform sampler2D length_texture;
uniform sampler2D orientation_texture;
uniform sampler2D grasscolor_texture;
uniform sampler2D bend_texture;

// 2. 在调用函数glLinkProgram(program);后,获取纹理变量的绑定点,即位置
_colorTextureLoc = glGetUniformLocation(_program, "grasscolor_texture");
_bendTextureLoc = glGetUniformLocation(_program, "bend_texture");
_orientationTextureLoc = glGetUniformLocation(_program, "orientation_texture");
_lengthTextureLoc = glGetUniformLocation(_program, "length_texture");

// 3. 在初始化程序后,从文件或者内存中加载纹理数据
GLenum texturePoint[] = {GL_TEXTURE1, GL_TEXTURE2, GL_TEXTURE3, GL_TEXTURE4};
NSArray *textureFileNames = @[@"grass_length.ktx", @"grass_orientation.ktx", @"grass_color.ktx" ,@"grass_bend.ktx"];
GLuint textures[] = {_lengthTxtureID, _orientationTxtureID, _colorTxtureID, _bendTxtureID};
for (int i = 0; i < 4; i++) {
  glActiveTexture(texturePoint[i]);
  [[TextureManager shareManager] loadObjectWithFileName:textureFileNames[i] toTextureID:&textures[i]];
}

// 4. 在渲染代码内,将纹理对象和着色器中的纹理变量相关联
glUniform1i(_lengthTextureLoc, 1);
glUniform1i(_orientationTextureLoc, 2);
glUniform1i(_colorTextureLoc, 3);
glUniform1i(_bendTextureLoc, 4);

2.5 控制纹理数据的读取方式(Controlling How Texture Data Is Read)

纹理坐标是标准化坐标,其范围为0.0到1.0之间。OpenGL允许控制当坐标不在这个范围内时对应的处理策略,称为采样器的包装模式(wrapping mode)。同样,也可以设置纹理数据的计算方式,这被称为采样器的过滤模式(filtering mode)。控制它们的参数被存在采样器对象(sampler object)中。创建采样器的代码如下。

void glGenSamplers (GLsizei count, GLuint *samplers);

设置采样器参数的代码如下。

void glSamplerParameteri (GLuint sampler, GLenum pname, GLint param);
void glSamplerParameterf (GLuint sampler, GLenum pname, GLfloat param);

在设置完采样器参数后,需要将其绑定至某个纹理单元,其代码如下。

void glBindSampler(GLuint unit, GLuint sampler);

参数unit指需要绑定至采样器的纹理单元索引。这样纹理对象和采样器对象都绑定至同一纹理单元,此时,完整的元素数据和参数就能构建着色器所需要的纹素数据。将纹理采样器逻辑和纹理数据分离具有以下三个特点。

第一,它允许为不同的纹理对象使用相同的纹理参数,不用在每个纹理数据中都包含冗余数据。
第二,它允许在更新纹理对象时,不用再更新采样器参数。
第三,它允许对同一个纹理对象使用多套不同的采样器参数。

部分程序可能将纹理采样器参数集成到纹理数据之中。为了改变这种类型的采样器参数,首先需要将纹理对象绑定至纹理单元,然后通过纹理单元调用以下函数。

void glTexParameterf (GLenum target, GLenum pname, GLfloat param);
void glTexParameteri (GLenum target, GLenum pname, GLint param);
2.5.1 使用多重纹理

有时,在单个着色器中,需要使用多个纹理。OpenGL直冲多重纹理单元,此时程序中需要创建多个采样器统一变量,它们引用了不同的纹理单元,同时还需要被绑定至上下文中。获取最大支持纹理单元数量代码如下。

GLint units;
glGetIntegerv(GL MAX COMBINED TEXTURE IMAGE UNITS, &units);

将纹理绑定至某个特定的纹理单元之前,首先需要改变活跃的纹理单元。其函数如下。void glActiveTexture (GLenum texture);,其中参数texture为内部定义变量如GL_TEXTURE0形式。生成三个纹理单元的代码如下。

GLuint textures[3];
glGenTextures(3, &textures);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textures[0]);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, textures[1]);
glActiveTexture(GL_TEXTURE2); 
glBindTexture(GL_TEXTURE_2D, textures[2]);

当上下文中绑定多个纹理时,需要为着色器中的采样器统一变量指定至不同的纹理单元,此时有两种方法可供选择。第一,通过在程序中调用函数glGetUniformLocation()获得它们绑定的纹理单元。第二,可以直接在着色器声明变量时同时指定绑定的纹理单元。当指定完纹理单元后,在程序中调用函数glUniform1i()可以直接将纹理输入至着色器中。此处需要注意,采样器变量并不包含一个在着色器中能够被读取的整形变量,但是为了设置纹理对应的纹理单元数据,他被看做是一个整形的统一变量。在着色器中声明绑定至纹理单元0、1和2的采样器统一变量代码如下。

layout (binding = 0) uniform sampler2D foo; 
layout (binding = 1) uniform sampler2D bar; 
layout (binding = 2) uniform sampler2D baz;
2.5.2 纹理过滤(Texture Filtering)

纹理中的纹素坐标和屏幕上的像素坐标几乎不可能完全对应。因此,纹理作为几何图形表面贴图的时候,它们总会被拉伸或者压缩。因为几何图形具有方向性,一个确定的纹理在同一时间在模型的不同表面上会有不同的收缩表现。

在前文中,着色器中对纹理采样使用的函数为texelFetch(),其参数必须为整数,这里可以理解为纹素坐标,即纹理中的像素坐标。对于使用标准纹理坐标时,纹理采样需要调用函数texture()。对应变形如下。该函数的参数0和1分别对应纹理中的对应维度的最小和最大纹素,p的取值可以不再0到1范围内,此时OpenGL的处理策略将会在后面介绍。

vec4 texture(sampler1D s, float P); 
vec4 texture(sampler2D s, vec2 P); 
ivec4 texture(isampler2D s, vec2 P); 
uvec4 texture(usampler3D s, vec3 P);

压缩或者拉伸纹理的映射中,颜色的计算过程被称为纹理过滤。在设置采样器参数函数中,参数GL_TEXTURE_MAG_FILTERGL_TEXTURE_MIN_FILTER分别表示拉伸和收缩纹理时采用的颜色计算方式。两种基本的计算策略GL_NEARESTGL_LINEAR可以被采用。确保对于参数GL_TEXTURE_MIN_FILTER必须选择了上述两个策略之一,因为默认的纹理过滤设置再没有mipmap纹理(稍后介绍)时不会生效。

最近过滤策略是最简单也是最快的过滤方法。该策略下选择距离想要采样的纹素坐标最近的纹素,并将其颜色信息返回作为片段颜色。该策略的特定是,当纹理被极端放大后,图像中会出现大量不连续色块。设置采样器过滤策略为最近过滤代码如下。

glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 
glSamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

线性过滤的性能消耗更大,但是对于现代的图像硬件,这个影响可以忽略。线性过滤的策略是,对于需要采样的纹理坐标,取它周围纹素及自己的加权平均值作为片段的颜色值返回。当采样坐标位于单个纹素中心时,会直接返回该纹素的颜色值。线性过滤的特点是当纹理被极端拉伸时,图像模糊化。设置采样器过滤策略为线性过滤的代码如下。

glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glSamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

对于同一个纹理使用两种不同的过滤方法对比如下。

2.5.3 分级细化纹理(Mipmaps)

分级细化纹理可以提高渲染性能,同时提升场景视觉质量。它解决了使用标准纹理映射面临的两个问题。其一为闪烁(scintillation)(混叠效应(aliasing artifacts)),它出现在当使用大尺寸纹理渲染小尺寸模型时。在运动场景中,这种现象非常频繁。

第二个问题更多的是和性能相关,但其原因和第一个问题类似。纹理占用了大量的内存,相邻片段访问了不联系的纹理,导致纹理中很大一部分几乎不被使用。

分级细化纹理指的是一些列的图像,即在加载纹理时,同时加载了从最大尺寸到最小尺寸的纹理。OpenGL提供了新的一组过滤模式来针对不同的几何模型选取最适合的纹理。在牺牲内存的前提下,可以消除闪烁现象,同时缓解在不相邻内存直接的寻址性能压力,同时可以在需要的时候提供高分辨率的纹理。

分级细化纹理由一系列纹理组成,每个纹理在各轴上的长度都是前一个纹理的一半。分级细化纹理并不要求一定是正方形纹理,但是最后一张纹理尺寸像素一定为1*1。当其中1个维度到1后,只需要缩小另外一个维度即可。对于2D纹理,使用分级细化纹理将额外增加1/3的内存开销。

此时调用函数glTexImage2D加载分级细化纹理时,参数level传分级索引,0表示未经缩小的纹理。使用函数glTexStorage2D()分配内存时可以指定在参数levels中指定分级的索引数,另外,可以设置渲染时使用的最小最大纹理索引值。其代码如下。

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0); 
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 4);
2.5.4 分级细化纹理过滤(Mipmap Filtering)

分级细化纹理过滤方式较于一般纹理,其有4个额外的过滤方式。GL_NEAREST_MIPMA P_NEAREST:选择最近的纹理贴图,选择最近的纹素。GL_LINEAR_MIPMAP_LINEAR :在分级纹理直接执行线性插值确定最终的纹理图,然后在执行纹素之间的加权平均线性过滤操作。GL_NEAREST_MIPMAP_LINEARGL_LINEAR_MIPMAP_NEAREST。前一个参数表示纹素的过滤方式,后一个参数表示分级纹理的选择方式。

如果纹理的过滤模式被设置为GL_LINEAR或者GL_NEAREST,那么纹理分级不会被启用。必须使用上述4个分级细化纹理过滤方式中的一个。

纹理的过滤方式需要根据具体要求来选择,例如,GL_NEAREST_MIPMAP_NEAREST提供非常好的性能和低混染(闪烁)效果,但是最近过滤法总会造成视觉上的显示问题。GL_LINEAR_MIPMAP_NEAREST通常用于加速游戏,因为使用了更高质量的线性过滤,同时最近选择分级纹理策略也能迅速完成。需要注意的是,设置分级纹理过滤方式只能用于参数GL_TEXTURE_MIN_FILTERGL_TEXTURE_MAG_FILTER只能使用两个基本过滤法则。

使用最近选择分级纹理策略有很好的视觉体验。从模型侧面看,在不同的表面上能看见不同分级索引纹理之间的过度,从一个高分辨率表面转换到另外一个表面可能会看见扭曲线或者很剧烈的过度。GL_LINEAR_MIPMAP_LINEARGL_NEAREST_MIPMAP_ LINEAR在选择分级纹理时执行了额外的插值来消除这种剧烈的过度区域,但是会造成额外的性能开销。GL_LINEAR_MIPMAP_LINEAR通常被称为3次线性纹理分级细化(trilinear mipmapping),另外还有更先进的图像过滤技术,它们可以产生很好的结果。

2.5.5 生成分级细化纹理等级(Generating Mip Levels)

前文已经讲到,在加载纹理时加载所有级别的分级纹理会浪费额外的内存,并且其中部分纹理无论是程序员还是用户几乎都不会使用,因此在程序中通过相关API生成分级纹理是一个更高效更常用的方法。该功能对应函数如下,它会生成从一组完整的分级纹理。

void glGenerateMipmap(GLenum target);

纹理目标参数和前文生成纹理可选参数相同,此时需要注意,生成纹理的速度并不会比加载纹理的速度更快。对于性能要求高,以及视觉特效要求高的程序,仍然推荐加载预生成纹理。

2.5.6 使用分级细化纹理(Mipmaps in Action)

使用分级纹理的实例如下,源代码

2.5.7 纹理包装(Texture Wrap)

通常,纹理采样函数中的位置坐标范围是0到1之间,如果超出这个范围,OpenGL将会根据采样器的包装模式处理数据。通过调用函数glSamplerParameteri可以指定包装模式,其中参数GL_TEXTURE_WRAP_S(T/R)分别对应坐标的三个分量,其值可以设置的类型为GL_REPEATGL_MIRRORED_REPEATGL_CLAMP_TO_EDGEGL_CLAMP_TO_BORDER。其中repeat模式可以理解为整数值取1,小数值只取小数部分,用于将小尺寸贴图贴在大尺寸的几何表面。MIRRORED_REPEAT表示镜像重复。

当使用线性过滤时,纹理边缘的纹素需要取其周围纹素值计算,此时,如包装模式为GL_REPEAT,那么OpenGL将会取纹理另一侧的第一列或者第一行。这种模式在纹理将会包裹模型,并且每一边和对边拼接在一起时很常用(如球体中)。

夹具纹理包装模式又细分为两种。第一种to_border的颜色由预先通过函数glSamplerParameterfv和参数GL_TEXTURE_BORDER_COLOR设置而得。第二种to_edge强制令大于1的值等于1,小于0的值等于0。四种不同的纹理包装模式如下,从上至下,从左至右,分别为TO_BORDERMIRRORED_REPEATTO_EDGEREPEAT。不同类型包装模式如下图所示,源代码

2.6 数组纹理(Array Textures)

上文中多个着色器中引用多个纹理都是通过声明多个采样器变量实现的,另外OpenGL还提供了采样器数组变量来加载数组纹理。其实在分级纹理中单个采样器同样引用了多个纹理,此外cube类型的纹理中每个面都是单独的一个纹理对象。需要注意的是,OpenGL不支持创建3D的纹理数组。数组纹理的成员也能是分级纹理。为了区分单个纹理和纹理数组,通常数组元素被称为层(layers)。

2D纹理数组可以被认为是一个特殊的3D纹理(同样的1D纹理数组能被看做是2D纹理),它们之间的主要区别是数组纹理的各层之间不会有过滤应用,另外纹理数组能支持的成员大小可能比3D纹理尺寸更大。

2.6.1 加载2D纹理数组(Loading a 2D Array Texture)

创建2D纹理数组的步骤为,创建纹理对象,绑定至GL_TEXTURE_2D_ARRAY目标,分配内存,填充数据。这里使用3D纹理模式分配内存和填充数据,第3维被理解为数组元素,或者层。另外.KTX格式文件支持数组纹理,可以从文件中直接读取纹理数组。读取纹理代码如下。

GLuint tex;
glGenTextures(1,_&tex); 
glBindTexture(GL TEXTURE 2D ARRAY, tex);
glTexStorage3D(GL_TEXTURE_2D_ARRAY, 8, GL_RGBA8, 256, 256, 100);
for (int i = 0; i < 100; i++) {
  glTexSubImage3D(GL_TEXTURE_2D_ARRAY, 0, 0, 0, i, 256, 256, 1, GL_RGBA, GL_UNSIGNED_BYTE, image_data[i]);
}

此处提供64个外星人头像纹理,在屏幕中绘制264个头像,将每个头像的旋转角度和偏离值传入顶点着色器,并在其中计算模型出现的随机位置、片段的纹理坐标和纹理索引。顶点着色器代码如下。

#version 410 core
layout (location = 0) in int alien_index;
out VS_OUT {
  flat int alien;
  vec2 tc; 
} vs_out;
struct droplet_t {
  loat x_offset; 
  float y_offset; 
  float orientation; 
  float unused;
};
layout (std140) uniform droplets {
  droplet_t droplet[256];
};

void main(void) {
  const vec2[4] position = vec2[4](vec2(-0.5, -0.5), vec2(0.5, -0.5), vec2(-0.5, 0.5), vec2(0.5, 0.5));
  vs_out.tc = position[gl VertexID].xy + vec2(0.5); 
  vs_out.alien = alien_index % 64;
  float co = cos(droplet[alien_index].orientation); 
  float so = sin(droplet[alien_index].orientation); 
  mat2 rot = mat2(vec2(co, so), vec2(-so, co)); 
  vec2 pos = 0.25 * rot * position[gl VertexID];
  gl_Position = vec4(pos.x + droplet[alien_index].x_offset, pos.y + droplet[alien_index].y_offset,0.5, 1.0);
}

在片段着色器中,使用从顶点着色器中获得的数据以及从程序中获得的纹理数据直接着色。代码如下。其中采样函数中的第三个参数为使用纹理的层索引,即具体使用哪个外星头像。

#version 410 core
layout (location = 0) out vec4 color;
in VS_OUT {
  flat int alien;
  vec2 tc; 
} fs_in;
layout (binding = 0) uniform sampler2DArray tex_aliens; 

void main(void) { 
  color = texture(tex_aliens, vec3(fs_in.tc, float(fs_in.alien)));
 }

渲染模型部分代码如下。

void render(double currentTime) {
  static const GLfloat black[] = { 0.0f, 0.0f, 0.0f, 0.0f }; 
  float t = (float)currentTime;
  glViewport(0, 0, info.windowWidth, info.windowHeight);     
  glClearBufferfv(GL_COLOR, 0, black);
  glUseProgram(render_prog);
  glBindBufferBase(GL_UNIFORM_BUFFER, 0, rain_buffer); 
  //GL_MAP_INVALIDATE_BUFFER_BIT 表示对映射的内存除缓存以外区域不会更新其值
  vmath::vec4 * droplet = (vmath::vec4 *)glMapBufferRange(GL_UNIFORM_BUFFER, 0, 256 * sizeof(vmath::vec4), GL_MAP_WRITE_BIT | GL_MAP_INVALIDATE_BUFFER_BIT);
  // droplet_x_offset等数组值已经提前预设
  for (int i = 0; i < 256; i++) {
    droplet[i][0] = droplet_x_offset[i]; 
    droplet[i][1] = 2.0f - fmodf((t + float(i)) * droplet_fall_speed[i], 4.31f);
    droplet[i][2] = t * droplet_rot_speed[i];
    droplet[i][3] = 0.0f; 
  }
  glUnmapBuffer(GL_UNIFORM_BUFFER);
  int alien_index;
  for (alien_index = 0; alien_index < 256; alien_index++) {
    glVertexAttribI1i(0, alien_index);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
  }
}

最后绘制结果如下,完整源码

2.7 在着色器中向纹理中写入数据(Writing to Textures in Shaders)

需要注意的是,下文内容在OpenGL4.2及以上版本才能使用,在4.2之前只能通过离屏渲染的技术将整个图像渲染至整个纹理中(该技术后续文章涉及)。OpenGL不仅能够在应用中读取纹理数据到纹理对象中,还允许在着色器中直接读取和写入纹理数据。正如使用采样器变量可以表示一个纹理对象,使用"image"变量可以表示纹理内的一张图片。和sampler变量一样,image变量有image1D等很多变形,最常用的是image2D。着色器中纹理的声明方式为uniform image2D my_image;

着色器中读取纹理数据和写入纹理数据的函数如下。注意,对于不同的纹理类型,该函数有各种变形。这里需要注意,坐标参数p为整形值,其表示纹素坐标,此处过滤模式没有任何意义。

vec4 imageLoad(readonly image2D image, ivec2 P); 
void imageStore(image2D image, ivec2 P, vec4 data);

另外,图像变量也能存储整形数据,其对应函数如下。

ivec4 imageLoad(readonly iimage2D image, ivec2 P); 
void imageStore(iimage2D image, ivec2 P, ivec4 data); 
uvec4 imageLoad(readonly uimage2D image, ivec2 P); 
void imageStore(uimage2D image, ivec2 P, uvec4 data);

和纹理绑定需要激活当前纹理不同,使用函数glBindImageTexture()可以直接将图像绑定至图像单元。其参数unit为从0增长的绑定点;参数level表示分级数量的索引;在使用单个纹理的时候参数layered设置为GL_FALSE,此时参数layer被忽略,当使用纹理数组时该参数设置为GL_TRUE,此时参数layer为对应索引;参数可选GL_READ_ONLY、GL_WRITE_ONLY或者GL_READ_WRITE。

参数format指图片内部数据,必须和分配内存时一致,可选GL_RGBA32F等(占用内存大小一致即可,例如GL_RGBA32F和GL_RGBA32I以及GL_RGBA32UI之间可以任意匹配)。另外,在着色器中写入数据时系统自动处理,读取数据时必须使用对应类型的声明标识符号(如rgba32f)。需要指出的是格式GL_R11F_G11F_B10F只和自己兼容,而GL_RGB10_A2UI和GL_RGB10_A2只互相兼容。

使用加载和存储图片数据的着色器示例代码如下。

#version 410 core
// Uniform image variables:
// Input image - note use of format qualifier because of loads 
layout (binding = 0, rgba32ui) readonly uniform uimage2D image_in; 
// Output image
layout (binding = 1) uniform writeonly uimage2D image_out;
void main(void) {
  // Use fragment coordinate as image coordinate
  ivec2 P = ivec2(gl_FragCoord.xy); 
  // Read from input image 
  uvec4 data = imageLoad(image_in, P);
  // Write inverted data to output image
  imageStore(image_out, P, ~data);
}

尽管上文代码很复杂,但这使得片段着色器更加强大。这意味着片段着色器不再是只能向固定位置写入数据,它能向一个图片的任意位置写入数据,同时通过多个图像变量还能向多个图像中写入数据。同时上面的技术使得任意阶段的着色器都能向图片中写入数据。但是需要注意的是,如果多个着色器都对同一个内存位置的图像更改,必须使用原子操作。(GPU高并发特性导致同一时刻多个同一阶段着色器函数同时运行)。

2.7.1 图像中的原子操作(Atomic Operations on Images)

正如着色器储存闭包统一变量中的原子操作一样,图像变量中也提供了相应的原子操作。OpenGL中提供的相关内置函数如下。除了imageAtomicCompSwap,各个函数参数都由图形变量,修改点坐标,数据组成。

imageAtomicAdd:读-加-存,返回原始值。
imageAtomicAnd:读-逻辑与-存,返回原始值。
imageAtomicOr:读-逻辑或-存,返回原始值。
imageAtomicXor:读-逻辑异或-存,返回原始值。
imageAtomicMin:读-取小-存,返回原始值。
imageAtomicMax:读-取大-存,返回原始值。
imageAtomicExchange:读-存,返回原始值。
imageAtomicCompSwap:读-和comp比较-如果相等存,返回原始值。

上述各个函数都存在以下几个重载函数,其代码如下。

uint imageAtomicAdd(uimage1D image, int P, uint data); 
uint imageAtomicAdd(uimage2D image, ivec2 P, uint data); 
uint imageAtomicCompSwap(uimage3D image, ivec3 P, uint comp, uint data);

使用原子计算变量,image变量,着色器储存闭包变量可以为每个像素的所有片段建立一个链表。其中原子变量用于表示各个片段在数据数组中的索引,着色器存储闭包用于表示数据数组,image变量用于存储每个像素点的链表头索引(这里需要注意OepnGL在光栅化过程中,通常每个像素包含多个片段)。一个示例代码如下。

#version 430 core
// Atomic counter for filled size
layout (binding = 0, offset = 0) uniform atomic_uint fill_counter; 
// 2D image to store head pointers
layout (binding = 0) uniform uimage2D head_pointer;
// Shader storage buffer containing appended fragments
struct list_item {
  vec4 color; 
  float depth; 
  int facing; 
  uint next;
};
layout (binding = 0, std430) buffer list_item_block {
  list_item   item[];
};
// Input from vertex shader
in VS_OUT {
  vec4 in; 
} fs_in;
void main(void) {
  ivec2 P = ivec2(gl_FragCoord.xy);
  uint index = atomicCounterIncrement(fill_counter);
  uint old_head = imageAtomicExchange(head_pointer, P, index);
  item[index].color = fs_in.color;
  item[index].depth = gl_FragCoord.z;
  // gl_FrontFacing 为back-face culling stage阶段生成的变量,无论精选是否被禁用,该变量都会生成。
  item[index].facing = gl_FrontFacing ? 1 : 0;
  // 需要注意的是在OpenGL的光栅化过程中,每个像素会含多个片段,
  // 而此处内置变量gl_FragCoord的x和y取值为0到当前在屏幕中渲染视图的宽高
  item[index].next = old_head;
}

在另外一个片段着色器中,通过追溯链表的方式将每个像素的所有片段的深度值叠加在一起,并将其作为输出颜色用于渲染图形。其代码如下。注:这里书中例子分了三个program来处理图像,第一个program用于预处理数据,其中调用函数imageStore(head_ pointer, P, uvec4(0xFFFFFFFF));将image变量中所有变量赋值为最大整数,用于标识链表末尾。而此处上下两个片段着色器分别为第二和第三个阶段中的片段着色器代码。

#version 430 core
// 2D image to store head pointers,关键字coherent表示内存连续
layout (binding = 0, r32ui) coherent uniform uimage2D head_pointer;
// Shader storage buffer containing appended fragments
struct list_item {
  vec4 color; 
  float depth; 
  int facing; 
  uint next;
};
layout (binding = 0, std430) buffer list_item_block {
  list_item  item[];
};
layout (location = 0) out vec4 color; 
const uint max_fragments = 10;
void main(void) {
  uint frag_count = 0;
  float depth_accum = 0.0;
  ivec2 P = ivec2(gl_FragCoord.xy);
  uint index = imageLoad(head_pointer, P).x;
  while (index != 0xFFFFFFFF && frag_count < max_fragments) {
    list_item this_item = item[index];
    if (this_item.facing != 0) {
      depth_accum -= this_item.depth;
    } else { 
      depth_accum += this_item.depth;
    }
    index = this_item.next;
    frag_count++;
  }
  depth_accum *= 3000.0;
  color = vec4(depth_accum, depth_accum, depth_accum, 1.0); 
}

2.8 同步访问图形(Synchronizing Access to Images)

正如前文说明着色器存储闭包统一变量时同步操作一样,对于图形数据的写入也有内存屏障来实现同步访问。其函数为glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BIT);。即当某些着色器必须使用前一个着色器写入的颜色变量时,需要在两个操作之间插入内存屏障以保证第一个着色器中的图像写入操作被完整执行后,后续使用到该数据的代码才会执行。同样的,该函数也有对应的着色器语法版本,为memoryBarrierImage()。该函数包装了再某个函数中图形操作完整执行后函数才会被执行返回操作。

此处原著中有一个示例程序,使用了前文的两个片段着色器,但是该部分逻辑必须使用OpenGL4.2以上特性,mac只能支持到4.1。示例程序运行结果如下。

2.9 纹理压缩(Texture Compression)

渲染图形时将纹理所有数据全部载入GPU对于高质量的图像渲染十分重要,对于大容量的纹理数据通常使用纹理压缩的方式保证GPU处理纹理的高效。纹理的压缩并不像类似于JPEG这种图像压缩有很高的压缩率,但是仍能节约大量的空间。另外由于GPU在处理压缩纹理时所需要的数据更少,因此处理时占用的内存带宽(memory bandwidth)也更小。OpengGL中支持的压缩格式如下。其中各个类型省略相同关键字GL_COMPRESSED_

这里暂未提到通用于PC端(由Nvidia和Intel提供的GPU芯片)的DXT(.dds)格式,通用于iOS设备的PVRTC纹理格式(A8处理器后,支持ASTC格式,其压缩比更高,苹果设备的GPU为PowerVR),以及高通GPU支持的ATC格式。其中ETC2为所有支持OpenGL ES3.0的GPU通用纹理压缩格式。

Generic类型
RED、RG、RGB、RGBA、SRGB、DRGB_ALPHA、
RGTC类型
RED_RGTC1、SIGNED_RED_RGTC1、RG_RGTC2、SIGNED_RG_RGTC2
BPTC类型
RGBA_BPTC_UNORM、SRGB_ALPHA_BPTC_UNORM、RGB_BPTC_SIGNED_FLOAT、RGB_BPTC_UNSIGNED_FLOAT
ETC2类型
RGB8_ETC2、SRGB8_ETC2、RGB8_PUNCHTHROUGH_ALPHA1_ETC2、SRGB8_PUNCHTHROUGH_ALPHA1_ETC2、RGBA8_ETC2_EAC、SRGB8_ALPHA8_ETC2_EAC
EAC类型
R11_EAC、SIGNED_R11_EAC、RG11_EAC、SIGNED_RG11_EAC

Generic类型的纹理压缩格式允许OpenGL自行以最优方式决定压缩机制。

RGTC类型压缩格式将纹理分解为4乘4纹素的块,各个块内部进行压缩。该格式只对使用单或者双通道的有符号和无符号数据格式的纹理生效。该格式压缩率大约为50%。

BPTC类型压缩格式同样将纹理分解为4乘4纹素的块,每个块的大学为16字节。每个块内部使用了更复杂的压缩方法,它对一组端点和他们之间连线的位置描述进行压缩。这样就能通过端点来生成每个像素的颜色信息。该格式用于单个通道为1字节的未标准化数据和单个通道为4字节的浮点型数据格式的纹理。该格式对于RGBA浮点型格式的纹理压缩率为25%,对于RGB未标准化格式纹理压缩率为33%。

ETC2(爱立信纹理压缩)格式和EAC(Ericsson Alpha Compression)格式都能被OpenGL ES3.0以上版本支持。它用于单个像素内存占用极小的应用程序,这些应用程序位于移动设备中,他们的内存带宽和电脑中CPU的带宽有很大的差距。

处理上述提到的格式,还有类似于S3TC格式(DXT的早期版本)和ETC1格式,在使用他们之前都需要检查当前GPU是否支持该格式的纹理。最好的方式是检查是否支持相关的扩展,例如当检查是否支持S3TC格式时,使用字符串GL_EXT_texture_compression_ s3tc

2.9.1 使用压缩纹理(Using Compression)

压缩纹理可以通过两种方式,推荐使用第一种先将纹理压缩后存储在文件中,OpenGL支持读取压缩纹理。另外OpenGL也支持在程序中压缩纹理,通过指定某个internalformat为某个压缩格式,可以在加载纹理的同时对纹理进行压缩。

在使用压缩纹理和未压缩纹理时并没有任何区别。在采样函数内部OpenGL会自动进行转换。KTX格式文件可以直接存储压缩纹理。检查纹理是否被压缩可以使用函数glGetTex LevelParameteriv (GLenum target, GLint level, GLenum pname, GLint *params);。可以通过参数GL_TEXTURE_INTERNAL_FORMAT检查纹理格式是否为压缩纹理。压缩的纹理格式可以指定也可以通过函数glGetInternalFormativ()以及参数GL_TEX TURE_COMPRES SED获得。另外可以直接通过参数GL_TEXTURE_ COMPRESSED直接检查纹理是否被压缩。

当加载完压缩纹理后,可以调用函数glGetCompressedTexImage()直接获取压缩的纹理数据。其代码如下。

 Glint imageSize = 0;
glGetTexParameteriv(GL_TEXTURE_2D,
                    GL_TEXTURE_COMPRESSED_IMAGE_SIZE,
                    &imageSize); 
void *data = malloc(imageSize);
glGetCompressedTexImage(GL_TEXTURE_2D, 0, data);

加载压缩纹理可以调用函数glCompressedTexImage1D()等及它们的对应更新纹理函数glCompressedTexSubImage3D。在使用上述函数时需注意x和y轴上的偏移量,此外大多数压缩纹理格式都是通过压缩4*4的纹素块来实现纹理压缩。(在原著示例代码loder类中,并未见压缩纹理的特殊处理,但书中提到可以使用其loader类可以直接加载压缩纹理,可能是使用普通纹理加载函数仍能加载压缩纹理,此处需继续研究)。

2.9.2 共享指数(Shared Exponents)

尽管在真实场景下,共享指数并不是一个真正的压缩纹理格式,但是它仍能在节约存储空间的情况下允许使用浮点型纹理数据。该格式下将每个通道的小数部分和他们共同的指数部分以整数方式存储(The fractional and exponential parts of each value are stored as integers),在采样时计算出颜色值。例如格式GL_RGB9_E5即是9bits用于存储每个通道的小数部分,5bit用于存储他们的共同指数部分,这样对于RGB格式可以达到67%的压缩率。

2.10 纹理视图(Texture Views)

前文中所有案例中,程序中加载的纹理类型和着色器中使用的类型是相同的。但是有时会遇到加载的纹理类型和着色器中需要的纹理类型不匹配的情况。此时可以使用纹理视图(Texture Views)在新的纹理对象中重用原纹理对象的数据。该技术的使用方式主要有以下两种,当然它们可以同时使用。

一:纹理视图可以将某种类型纹理包装为一个不同的类型,例如,可以通过纹理视图将一个2D纹理作为一个单层2D数组纹理使用。

二:纹理视图能够将纹理中的数据包装成为其真实数据类型以外的数据。例如,加载一个内部格式为GL_RGBA32F(即4个32bit的float类型数据)纹理,创建一个GL_RGBA32UI的纹理视图。(so that you can get at the individual bits of the texels)

2.10.1 纹理视图(Texture Views)

大多数纹理类型都能创建至少一个以上的纹理视图,但是缓存纹理(buffer textures)不能,因为它已经是缓存对象的视图,对于该类型纹理,只能将同一个缓存对象绑定至另外一个缓存纹理来使用其中的数据。

void glTextureView(GLuint texture, 
                   GLenum target,
                   GLuint origtexture,
                   GLenum internalformat,
                   GLuint minlevel,
                   GLuint numlevels,
                   GLuint minlayer,
                   GLuint numlayers);

相关函数如上所示,参数texture为视图内部纹理的标识名,target为新创建纹理类型,origtexture表示原纹理标识名,target必须和origtexture兼容。internalformat为新纹理的数据类型,它必须和原始纹理的数据类型兼容。

纹理类型兼容表如下。

Original Texture               New Texture
1D                             1D or 1D_ARRAY
2D                             2D or 2D_ARRAY
3D                             3D
CUBE_MAP                       CUBE_MAP, 2D, 2D_ARRAY, or CUBE_MAP_ARRAY
RECTANGLE                      RECTANGLE
BUFFER                         none
1D_ARRAY                       1D or 1D_ARRAY
2D_ARRAY                       2D or 2D_ARRAY
CUBE_MAP_ARRAY                 CUBE_MAP, 2D, 2D_ARRAY, or CUBE_MAP_ARRAY
2D_MULTISAMPLE                 2D_MULTISAMPLE or 2D_MULTISAMPLE_ARRAY
2D_MULTISAMPLE_ARRAY           2D_MULTISAMPLE or 2D_MULTISAMPLE_ARRAY

兼容的格式必须在同一类格式中,格式分类如下。

Format Class           Members of the Class
128-bit                GL_RGBA32F, GL_RGBA32UI, GL_RGBA32I
96-bit                 GL_RGB32F, GL_RGB32UI, GL_RGB32I
64-bit                 GL_RGBA16F, GL_RG32F, GL_RGBA16UI, GL_RG32UI, GL_RGBA16I, GL_RG32I, GL_RGBA16, GL_RGBA16_SNORM
48-bit                 GL_RGB16, GL_RGB16_SNORM, GL_RGB16F, GL_RGB16UI, GL_RGB16I
32-bit                 GL_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-bit                 GL_RGB8, GL_RGB8_SNORM, GL_SRGB8, GL_RGB8UI, GL_RGB8I
16-bit                 GL_R16F, GL_RG8UI, GL_R16UI, GL_RG8I, GL_R16I, GL_RG8, GL_R16, GL_RG8_SNORM, GL_R16_SNORM
8-bit                  GL_R8UI, GL_R8I, GL_R8, GL_R8_SNORM
RGTC1_RED              GL_COMPRESSED_RED_RGTC1, GL_COMPRESSED_SIGNED_RED_RGTC1
RGTC2_RG               GL_COMPRESSED_RG_RGTC2, GL_COMPRESSED_SIGNED_RG_RGTC2
BPTC_UNORM             GL_COMPRESSED_RGBA_BPTC_UNORM, GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM
BPTC_FLOAT             GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT, GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT

minlevel和numlevels分别表示新纹理中第一个及分级纹理和分级数量,这两个参数允许从旧的分级纹理中提取部分层,例如当只创建旧的分级纹理第一级纹理时该组参数分别为0和1。

同样的参数minlayer和numlayers用于提取数组纹理类型中的部分层。例如当需要抽取一个20层旧纹理的中间4层是,该组参数可以分别设置为8和4。

在使用该方式生成新纹理后,新纹理中的数据更改会影响旧纹理中的数据。例如,当使用2D类型纹理通过纹理视图引用2D数组纹理中的某一层时,使用函数glTexSubImage2D更新新纹理中的数据后,旧纹理中对应层的数据也随之改变。新的纹理在着色器中使用的方式和纹理的基本用法相同,上述新纹理在着色器中用关键字sampler2D表示。

2.11 OpenGL数据操作总结

该章主要介绍OpenGL中大量数据的传递方法。在管道的起点,通过缓存对象和顶点属性为顶点着色器自动提供顶点相关数据。另外还可以通过常量为着色器赋值,如统一变量,统一变量的使用可以使通过缓存或者默认统一变量闭包的方式。此外着色器存储闭包可以用于着色器直接的数据交流。统一变量可以用于存储纹理、图像和缓存数据,在着色器中可以直接对纹理数据、图像数据和缓存进行读写操作。纹理数据可以抽取部分并将其包装称为另外一种不同格式和类型的纹理数据镜像使用。另外原子操作可以包装现代高并发GPU安全访问数据。

推荐阅读更多精彩内容