【OpenGL ES】纹理

来源:互联网 发布:电脑淘宝如何收藏宝贝 编辑:程序博客网 时间:2024/05/21 11:13

1、2D纹理

2D纹理是OpenGL ES中最基本和最常用的纹理形式,它是一个图像数据的二维数组,纹理坐标(s, t)或(u, v)用作纹理图像中的索引,代表用于查找一个纹理贴图的规范化坐标。纹理图像的左下角由st坐标(0.0, 0.0)指定,右上角由坐标(1.0, 1.0)指定,在[0.0, 1.0]区间之外的坐标是允许的,在该区间外的纹理读取行为由纹理包装模式定义。一个纹理的单独数据元素称作纹素Texel,即纹理元素Texture Pixels,由基本格式和数据类型而定,可以用许多不同的基本格式表现,如GL_RGB。

2、立方图纹理

立方图是一个由6个单独的2D纹理面组成的纹理,形状为立方体,最常用的是环境贴图特效。环境贴图特效,就是环境在物体上的倒影通过一个表示环境的立方图进行渲染,这个立方图通过在场景中央放置一个摄像机,从6个轴的方向(+X, -X, +Y, -Y, +Z, -Z)捕捉场景图像并将结果保存在立方体的每个面来生成。立方图纹素的读取通过一个3D向量(s, t, r)作为纹理坐标,在立方图中查找。用于纹理坐标的3D向量与2D纹理的不同,通常不直接逐顶点地保存在网格上,相反,立方图通常使用法向量作为计算立方图纹理坐标的基础来读取。一般来说,法向量和一个来自眼睛的向量一起使用,计算出一个反射向量,然后用这个向量在立方图中查找。

3、3D纹理

3D纹理或称为体纹理,可以看作2D纹理多个切片的一个数组,用坐标(s, t, r)访问,r坐标选择3D纹理中需要采样的切片,(s, t)坐标读取每个切片中的2D贴图。3D纹理中的每个mip贴图级别包含上一个级别的纹理中的半数切片。

4、2D纹理数组

2D纹理数组与3D纹理很相似,使用纹理坐标(s, t, r),但是也有差别,用途不同,例如,2D纹理数组常常用于存储2D图像的一个动画,数组的每个切片表示纹理动画的一帧。对于过滤,3D纹理过滤发生在切片之间,而从2D纹理数组中读取只从一个单独的切片采样。对于mip贴图,2D纹理数组的每个2D切片完全独立于其它切片,每个mip贴图级别包含与上一级别相同的切片数量,而3D纹理的每个mip贴图级别却是上一个级别切片数量的一半。

5、纹理加载

void glGenTextures(GLsizei n,    GLuint * textures);void glDeleteTextures(GLsizei n,    const GLuint * textures);void glBindTexture(GLenum target,    GLuint texture);void glTexImage2D(GLenum target,    GLint level,    GLint internalFormat,    GLsizei width,    GLsizei height,    GLint border,    GLenum format,    GLenum type,    const GLvoid * data);void glPixelStorei(GLenum pname,    GLint param);void glTexParameteri(GLenum target,    GLenum pname,    GLint param);

纹理对象加载之前,首先要使用glGenTextures创建纹理对象,纹理对象是一个容器对象,保存渲染所需的纹理数据,例如图像数据、过滤模式和包装模式。纹理对象不再使用时,使用glDeleteTextures进行删除。在使用纹理对象之前,还要通过glBindTexture进行绑定,绑定之后,就可以对纹理对象进行操作。用于加载2D和立方图纹理的基本函数是glTexImage2D,参数data包含图像的实际像素数据,数据必须包含一定的像素个数,每个像素根据格式和类型规范有相应的字节数,像素行必须对齐到用glPixelStorei设置的GL_UNPACK_ALIGNMENT。为了得到最佳的性能,建议使用不可变纹理。下面是一个简单的例子,完整代码请参考https://github.com/geminy/aidear/tree/master/graphics/mu/examples/opengles3/Simple_Texture2D。

   // Texture object handle   GLuint textureId;   // 2x2 Image, 3 bytes per pixel (R, G, B)   GLubyte pixels[4 * 3] =   {      255,   0,   0, // Red        0, 255,   0, // Green        0,   0, 255, // Blue      255, 255,   0  // Yellow   };   // Use tightly packed data   glPixelStorei ( GL_UNPACK_ALIGNMENT, 1 );   // Generate a texture object   glGenTextures ( 1, &textureId );   // Bind the texture object   glBindTexture ( GL_TEXTURE_2D, textureId );   // Load the texture   glTexImage2D ( GL_TEXTURE_2D, 0, GL_RGB, 2, 2, 0, GL_RGB, GL_UNSIGNED_BYTE, pixels );   // Set the filtering mode   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );

例子中,pixels数组用简单的2x2纹理数据初始化,即width为2,height为2,每个pixel有3个字节,共4x4x3等于12个字节。当着色器从一个ubyte纹理分量读取数据时,该值从[0, 255]区间被映射到浮点区间(0.0, 1.0)。一般来说,应用程序不会以这种简单的方式创建纹理数据,而从一个图像文件中加载数据。glPixelStorei设置了像素存储模式GL_UNPACK_ALIGNMENT为1,即解包对齐方式设置为1,表示字节对齐。glPixelStorei设置的打包和解包选项是全局状态,不由纹理对象存储,也不与之关联。随后,使用glGenTextures创建了一个纹理对象,使用glBindTexture将这个纹理对象绑定到了GL_TEXTURE_2D目标,使用glTexImage2D将图像数据加载到纹理对象。最后,使用glTexParameteri将缩小和放大过滤模式设置为GL_NEAREST,这是必需的,因为还没有为纹理加载完整的mip贴图链,所以选择非mip贴图缩小过滤器。另外,glTexImage2D还可以用于加载立方体纹理,使用glTexImage3D加载3D纹理和2D纹理数组。例子的效果图如下。
这里写图片描述

6、mip贴图

当使用glPixelStorex设置缩小和放大过滤器为GL_NEAREST时,一个纹素将在提供的纹理坐标上读取,这称作点采样或者最近采样,最近采样可能产生严重的视觉伪像,这是因为三角形在屏幕空间中变得较小,在不同像素间的插值中,纹理坐标有很大的跳跃,导致从一个大得纹理贴图中取得少量样本,造成锯齿伪像,而且可能造成巨大的性能损失。解决这种伪像问题的方法是使用mip贴图,基本思想是构建一个贴图链,贴图链始于原来指定的图像,后续的每个图像在每个维度上是前一个图像的一半,一直持续到最后达到链底部的1x1纹理。mip贴图级别可以编程生成,一个mip级别中的每个像素通常根据上一级别中相同位置的4个像素的平均值计算,也就是盒式过滤。下面是一个使用盒式过滤技术生成mip贴图链的例子,完整代码请参考https://github.com/geminy/aidear/tree/master/graphics/mu/examples/opengles3/MipMap2D。

// Texture object handle   GLuint textureId;   int    width = 256,          height = 256;   int    level;   GLubyte *pixels;   GLubyte *prevImage;   GLubyte *newImage;   pixels = GenCheckImage ( width, height, 8 );   if ( pixels == NULL )   {      return 0;   }   // Generate a texture object   glGenTextures ( 1, &textureId );   // Bind the texture object   glBindTexture ( GL_TEXTURE_2D, textureId );   // Load mipmap level 0   glTexImage2D ( GL_TEXTURE_2D, 0, GL_RGB, width, height,                  0, GL_RGB, GL_UNSIGNED_BYTE, pixels );   level = 1;   prevImage = &pixels[0];   while ( width > 1 && height > 1 )   {      int newWidth,          newHeight;      // Generate the next mipmap level      GenMipMap2D ( prevImage, &newImage, width, height,                    &newWidth, &newHeight );      // Load the mipmap level      glTexImage2D ( GL_TEXTURE_2D, level, GL_RGB,                     newWidth, newHeight, 0, GL_RGB,                     GL_UNSIGNED_BYTE, newImage );      // Free the previous image      free ( prevImage );      // Set the previous image for the next iteration      prevImage = newImage;      level++;      // Half the width and height      width = newWidth;      height = newHeight;   }   free ( newImage );   // Set the filtering mode   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST_MIPMAP_NEAREST );   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );

例子中,原始像素数据由GenCheckImage函数生成。生成mip贴图链的代码由GenMipMap2D函数提供,这个函数以一个RGB8图像作为输入,在前面的图像上执行盒式过滤,生成下一个mip贴图级别。mip贴图链用glTexImage2D加载。加载mip贴图链之后,便可以使用过滤模式,以使用mip贴图,结果是实现了屏幕像素和纹理像素间的更好比率,从而减少了锯齿伪像,图像的锯齿也减少了,这是因为mip贴图链中的每个图像连续进行过滤,使得高频元素随着贴图链的下移而越来越少。例子中的贴图链是手动创建的,OpenGL ES 3.0提供了自动生成贴图链的函数glGenerateMipMap。例子的效果图入如下,左边为GL_NEARST,右边为GL_LIEAR_MAPMAP_LINEAR即三线性过滤,产生所有模式中最佳的质量。
这里写图片描述
渲染发生时,发生两种过滤,缩小和放大。缩小发生在屏幕上投影的多边形小于纹理尺寸的时候,放大发生在屏幕上投影的多边形大于纹理尺寸的时候。过滤器类型的确定由硬件自动处理,但是API提供了对每种情况下使用的过滤类型的控制。对于放大,mip贴图不起作用,因为我们总是从最大的可用级别进行采样。对于缩小,可以使用不同的采样模式,所用模式的选择基于需要实现的显示质量水平以及为了纹理过滤愿意损失缩少性能。在纹理缩小模式中,只有GL_NEARST和GL_LIEAR不需要为纹理指定完整的贴图链,其它所有模式都要求纹理存在完整的贴图链。在实际的使用中,需要平衡不同的过滤模式带来的质量效果以及由此而带来的性能代价。

在OpenGL ES 2.0中,当线性过滤核心落到立方图边缘时,过滤将只发生在立方图的一个面上,这将在立方图各面之间的边上造成伪像。在OpenGL ES 3.0中,支持无缝立方图过滤,默认支持,也就是说,过滤核心跨越立方图不只一个面时,核心将会从其覆盖的每个面中获得样本,从而在立方图各面的边缘形成了更平滑的过滤。

7、纹理包装

纹理包装模式用于指定纹理坐标超出[0.0, 1.0]范围时所发生的行为,用glTexParameterx设置,可以为s、t、r坐标单独设置,分别对应于GL_TEXTURE_WRAP_S、GL_TEXTURE_WRAP_T、GL_TEXTURE_WRAP_R,模式包括GL_REPEAT重复纹理、GL_CLAMP_TO_EDGE限定读取纹理的边缘、GL_MIRRORED_REPEAT重复纹理和镜像,其中r坐标包装仅用于3D纹理和2D纹理数组。纹理包装模式影响过滤行为。下面的例子使用了纹理包装,完整示例代码请参照https://github.com/geminy/aidear/tree/master/graphics/mu/examples/opengles3/TextureWrap。

   // Draw quad with repeat wrap mode   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT );   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT );   glUniform1f ( userData->offsetLoc, -0.7f );   glDrawElements ( GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indices );   // Draw quad with clamp to edge wrap mode   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );   glUniform1f ( userData->offsetLoc, 0.0f );   glDrawElements ( GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indices );   // Draw quad with mirrored repeat   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT );   glTexParameteri ( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT );   glUniform1f ( userData->offsetLoc, 0.7f );   glDrawElements ( GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, indices );

例子中,用3种不同的包装模式绘制正方形,纹理坐标范围为[-1.0, 2.0],左边的为GL_REPEAT,纹理只是在[0, 1]区间之外重复,造成倾斜的图案,中间的为GL_CLAMP_TO_EDGE,当纹理左边超出[0, 1]的范围时,纹理坐标限定于来自纹理边缘的样本,右边的为GL_MIRRORED_REPEAT,当纹理左边超出[0, 1]的范围时,图像被镜像并重复,效果图如下。
这里写图片描述

8、纹理调配

纹理调配Swizzle控制输入的R、RG、RGB或RGBA纹理中的颜色分量在着色器中读取时如何映射到分量。例如,应用程序可能希望一个GL_RED纹理映射为(0, 0, 0, R)或者(R, R, R, 1)而不是默认的R, 0, 0, 1)。纹理调配通过glTexParameterx设置,对应的分量为GL_TEXTURE_SWIZZLE_R、GL_TEXTURE_SWIZZLE_G、GL_TEXTURE_SWIZZLE_B、GL_TEXTURE_SWIZZLE_A,纹理值来源可能分别是从R、G、B、A分量读取的GL_RED、GL_GREEN、GL_BLUE、GL_ALPHA,或者设置为常数GL_ZERO、GL_ONE。

9、纹理细节级别

在某些应用中,在所有纹理mip贴图级别可用之前就能够开始显示场景是很有用的。例如,通过数据连接下载纹理图像的GPS应用可以从最低级别的mip贴图开始,在更高级别可用时再显示它们。在OpenGL ES 3.0中,这可以通过使用glTexParameterx的多个参数来实现。GL_TEXTURE_BASE_LEVEL设置用于纹理的最大mip贴图级别,默认情况下,该值为0,但是如果mip贴图级别还不可用,则它可以设置为更高的值。同样,GL_TEXTURE_MAX_LEVEL设置使用的最小mip贴图级别,默认情况下,它的值为1000,超过了任何纹理可能具备的最大级别,但是可以将其设置为较小的值,以控制用以纹理的最小mip级别。为了选择要用于渲染的mip贴图级别,OpenGL ES自动计算一个细节级别LOD值,以确定从哪一个mip贴图级别过滤,在三线性过滤中,控制每个mip贴图使用的多少。应用程序还可以使用GL_TEXTURE_MIN_LOD和GL_TEXTURE_MAX_LOD控制最小和最大的LOD值。可以从基本和最大mip贴图级别单独控制LOD限制的一个原因是在新的mip贴图级别可用时提供平滑过渡,仅仅设置纹理的基本和最大级别可能在新mip贴图级别可用时造成间歇伪像,而插入LOG可以使这一过渡看起来更平滑。

10、深度纹理对比

glTexParameterx的纹理参数GL_TEXTURE_COMPARE_FUNC和GL_TEXTURE_COMPARE_MODE,提供了百分比渐进过滤PCF功能。在执行被称作阴影贴图的阴影技术时,片段着色器需要比较一个片段的当前深度值和深度纹理中的深度值,以确定阴影在片段之内还是之外。为了实现平滑的阴影边缘效果,对深度纹理进行双线性过滤是很有用的。但是,在过滤深度值时,我们希望过滤在采样深度值与当前深度或者参考值比较之后发生。如果过滤在比较之前发生,我们将平均计算深度纹理中的值,这不能提供正确的结果。PCF提供了正确的过滤,将采样的深度值与每个参考值比较,然后将这些比较的结果0或者1一起进行平均。GL_TEXTURE_COMPARE_MODE默认为GL_NONE,但是当它被设置为GL_COMPARE_REF_TO_TEXTURE时,(s, t, r)纹理坐标中的r坐标将于深度纹理的值进行比较,然后,比较的结果将成为阴影纹理读取的结果,可能是0或者1,如果启用纹理过滤,则为这些值的平均。GL_TEXTURE_COMPARE_FUNC设置比较函数,可以设置为GL_LEQUAL、GL_GEQUAL、GL_LESS、GL_GREATER、GL_EQUAL、GL_NOTEQUAL、GL_ALWAYS、GL_NEVER。

11、纹理格式

纹理格式包括规范化纹理、浮点纹理、整数纹理、共享指数纹理、sRGB纹理和深度纹理,有些格式支持渲染,有些格式支持过滤。

规范化纹理——
规范化指的是从片段着色器中读取纹理时,结果将映射到[0.0, 1,0]或[-1.0, 1.0]范围内。规范化纹理都可以过滤,有符号时不能渲染。

浮点纹理——
大部分浮点格式由16位半浮点数据或者32位浮点数据支持。OpenGL ES 3.0还引入了11/11/10 GL_R11F_G11F_B10F浮点格式,只能用于代表正值,目的是提供更高精度的三通道纹理,同时保持每个纹素的存储量为32位,这种格式的使用可能达到比16/16/16 GL_RGB16F或32/32/32 GL_RGB32F更高的性能。浮点纹理不强制用作渲染目标,只强制16位半浮点数据可以过滤。

整数纹理——
整数纹理中的值在片段着色器读取时仍为整数。整数纹理不可过滤,但是R、RG和RGBA变种可以用作帧缓冲区对象中渲染的颜色附着。使用整数纹理作为颜色附着的时候,整数渲染目标不可能进行混合,忽略Alhpa混合状态。用于从整数纹理读取并输出到整数渲染目标的片段着色器应该使用对应该格式的有符号或者无符号整数类型。

共享指数纹理——
共享指数纹理为不需要浮点纹理使用的那么多深度位数的大范围RGB纹理提供了一种存储方式,通常用于高动态范围HDR图像,不需要半浮点或者全浮点数据。OpenGL ES 3.0中的共享指数纹理格式是GL_RGB9_E5,3个RGB分量共享一个5位的指数,5位的指数隐含地由数值15调整。

sRGB纹理——
sRGB是一个非线性颜色空间,大约遵循一个幂函数,大部分图像实际上都存储为sRGB颜色空间,这种非线性解释了人类能够在不同的亮度级别上更好地区分颜色这一事实。如果用于纹理的图像是以sRGB颜色空间创作的,但是没有使用sRGB纹理读取,那么所有发生在着色器中的照明计算都会在非线性颜色空间中进行。为了正确地处理sRGB图像,应用程序应该使用一个sRGB纹理格式,这种格式在着色器中读取时将从sRGB转换为线性颜色空间,然后,着色器中的所有计算都将在线性颜色空间中完成。最后,通过渲染到一个sRGB渲染目标时,图像将会自动地转换回sRGB。可以使用着色器命令pow(value, 2.2)进行近似的sRGB到线性的转换,然后用pow(value, 1/2.2)进行近似的线性到sRGB的转换。然后,尽可能使用sRGB纹理是最好的做法,因为这样减少了着色器指令数量,并且提供更准确的sRGB转换。

深度纹理——
深度纹理允许应用程序从帧缓冲区对象的深度附着中读取深度值和可选的模板值,这在各种高级渲染算法中很有用,包括阴影贴图。

12、着色器与纹理

下面是在着色器中使用纹理的一个例子。

// vertex#version 300 eslayout(location = 0) in vec4 a_position;layout(location = 1) in vec2 a_texCoord;out vec2 v_texCoord;void main(){    gl_Position = a_position;    v_texCoord = a_texCoord;}// fragment#version 300 esprecision mediump float;in vec2 v_texCoord;layout(location = 0) out vec4 outColor;uniform sampler2D s_texture;void main(){    outColor = texture( s_texture, v_texCoord );}

例子中,顶点着色器以一个二分量vec2纹理坐标a_texCoord作为顶点输入,并将其作为输出v_texCoord传递给片段着色器,片段着色器消费该纹理坐标,并将其用于纹理读取。片段着色器声明一个类型为sampler2D的统一变量s_texture,采样器是用于从纹理贴图中读取的特殊统一变量,将加载一个指定纹理绑定的纹理单元的数值。例如,通过glUniformi指定采样器s_texture为数值0,表示用glActiveTexture激活纹理时参数为GL_TEXTURE0,从0开始,最大为31。glActiveTexture设置当前纹理单元,以便后续的glBindTexture将纹理绑定到当前活动单元。texture内建函数返回一个代表从纹理贴图中指定位置读取颜色的vec4。

13、纹理压缩

我们可以加载压缩过的纹理图像数据,通过glCompressedTexXxx完成。纹理压缩,可以减少纹理在设备上的内存占用,节约着色器中读取纹理时消耗的内存带宽,减少应用程序加载纹理的大小。在OpenGL ES 2.0中,核心规范不定义任何压缩的纹理图像格式,因此,许多供应商提供了许多特定于硬件的纹理压缩扩展,它们并不兼容。OpenGL ES 3.0引入了所有供应商必须支持的标准纹理压缩格式,ETC2和EAC,不支持3D纹理,但是glCompressedTexImage3D可以用于加载供应商专用的3D纹理压缩格式。获取实现支持的纹理压缩格式,可以用glGetIntergerv查询GL_NUM_COMPRESSED_TEXTURE_FORMATX、GL_COMPRESSED_TEXTURE_FORMATS。

14、纹理更新

用glTexImage2D加载纹理图象之后,可以更新图像的各个部分,更新图像的一个子区域使用glTexSubImage2D,还支持其它的3D纹理、压缩纹理的子区域更新。

15、从颜色缓冲区复制纹理数据

纹理数据的来源可以从颜色缓冲区复制,使用渲染的结果作为纹理中的图像。从颜色缓冲区复制数据到纹理,对应的函数为glReadBuffer、glCopyTexImage2D等,glReadBuffer指定读取的颜色缓冲区,可能为GL_BACK、GL_COLOR_ATTACHMENTi、GL_NONE。

16、采样器对象

前面介绍了使用glTexParameteri设置纹理的过滤模式、坐标包装以及LOD,缺点是可能造成大量的不必要的API开销,为了缓解这一问题,OpenGL ES 3.0引入了采样器对象,将采样器状态与纹理状态分离。所有可用glTexParameteri进行的设置,都可以对采样器对象进行,可以在一次函数调用中与纹理单元绑定使用。采样器对象可以用于许多纹理,从而降低API开销。创建采样器对象使用glGenSamplers,删除采样器对象使用glDeleteSamplers,绑定纹理与采样器对象使用glBindSampler,设置采样器对象属性使用glSamplerParameterx。

缓冲区对象可以在服务器端或者GPU内存中存储数据,而不是在客户端或者主机内存,这样减少了从CPU到GPU的数据传送,因而能够改进性能,降低内存占用率。OpenGL ES 3.0还引入了像素解包缓冲区对象,它与GL_PIXEL_UNPACK_BUFFER目标绑定指定。

17、不可变纹理

OpenGL ES 3.0引入了另一种有助于改进应用程序性能的的功能是不可变纹理。应用程序glTexImage2D和glTexImage3D等函数独立地指定纹理的每个mip贴图级别,这对OpenGL ES驱动程序造成的问题是驱动程序在绘图之前无法确定纹理是否已经完全指定,它必须检查每个mip贴图级别或者子图像的格式是否相符、每个级别的大小是否正确以及是否有足够的内存,这种绘图时检查可能代价很高,而使用不可变纹理可以避免这种情况。不可变纹理,应用程序在加载数据之前指定纹理的格式和大小,之后,纹理格式变成不可变的,OpenGL ES驱动程序可以预先进行所有一致性和内存检查。纹理不可变之后,应用程序只能通过glTexSubImage2D、glTexSubImage3D、glGenerateMipMap或者渲染到纹理加载图像数据。创建不可变纹理之前,先用glBindTexture绑定纹理,然后用glTexStorage2D或glTexStorage3D分配不可变存储,之后可以使用glGetTexParameterx查询GL_TEXTURE_IMMUTABLE_FORMAT为GL_TRUE。

原创粉丝点击