android开发基础知识:OpenGL纹理本质

摘要 几个月前调试3D纹理时发现一个有趣的问题:同样的3D HW lib库在android2.2系统上可以正常工作,但在2.3系统上却不能工作,显示的图像白屏,调试了几天才将问题定位并解决,解决方法很简单: 1、修改GLExtensions.h头文件对于变量mHaveDirectTexture赋值使其

几个月前调试3D纹理时发现一个有趣的问题:同样的3D HW lib库在android2.2系统上可以正常工作,但在2.3系统上却不能工作,显示的图像白屏,调试了几天才将问题定位并解决,解决方法很简单:

1、修改GLExtensions.h头文件对于变量mHaveDirectTexture赋值使其值为TRUE

2、使用mFailoverTexture作为当前纹理绘图

下面详细介绍OpenGL纹理知识及如何确认问题所在:

一、OpenGL基本原理

OpenGL是将用数学语言和色彩等信息描述的三维空间物体通过计算转换成二维图像并显示出来的程序库。
三维空间中的对象被描述成一系列的顶点(用来定义几何对象)或像素(用来定义图像)。OpenGL对数据进行几个步骤的处理将其转换成像素,这些像素存放帧缓冲区中形成最终需要的图形。---- 本质所在

整个数据处理流程:

1、OpenGL  纹理介绍

比如绘制一面砖墙,就可以用一幅真实的砖墙图像或照片作为纹理贴到一个矩形上,这样,一面逼真的砖墙就画好了。如果不用纹理映射的方法,则墙上的每一块砖都必须作为一个独立的多边形来画。另外,纹理映射能够保证在变换多边形时,多边形上的纹理图案也随之变化。例如,以透视投影方式观察墙面时,离视点远的砖块的尺寸就会缩小,而离视点较近的就会大些。此外,纹理映射也常常运用在其他一些领域,如飞行仿真中常把一大片植被的图像映射到一些大多边形上用以表示地面,或用大理石、木材、布匹等自然物质的图像作为纹理映射到多边形上表示相应的物体。

最基本的执行纹理映射所需的步骤:
1)定义纹理  
2)控制滤波
3)说明映射方式
4)绘制场景,给出顶点的纹理坐标和几何坐标。
注意:纹理映射只能在RGBA方式下执行,不能运用于颜色表方式。
纹理不仅可以是二维的,也可以是一维或其它维的。

纹理定义:
void glTexImage2D(GLenum target,GLint level,GLint components, GLsizei width, glsizei height,GLint border, GLenum format, GLenum type, const GLvoid *pixels); 定义一个二维纹理映射
参数target是常数GL_TEXTURE_2D。
参数level表示多级分辨率的纹理图像的级数,若只有一种分辨率,则level设为0。
参数components是一个从1到4的整数,指出选择了R、G、B、A中的哪些分量用于调整和混合,1表示选择了R分量,2表示选择了R和A两个分量,3表示选择了R、G、B三个分量,4表示选择了R、G、B、A四个分量。
参数width和height给出了纹理图像的长度和宽度
参数border为纹理边界宽度,它通常为0,width和height必须是2m+2b,这里m是整数,长和宽可以有不同的值,b是border的值。纹理映射的最大尺寸依赖于OpenGL,但它至少必须是使用64x64(若带边界为66x66),若width和height设置为0,则纹理映射关闭。

参数format和type描述了纹理映射的格式和数据类型,它们在这里的意义与在函数glDrawPixels()中的意义相同,事实上,纹理数据与glDrawPixels()所用的数据有同样的格式。数format可以是GL_COLOR_INDEX、GL_RGB、GL_RGBA、GL_RED、GL_GREEN、GL_BLUE、GL_ALPHA、GL_LUMINANCE或GL_LUMINANCE_ALPHA(注意:不能用GL_STENCIL_INDEX和GL_DEPTH_COMPONENT)。
类似地,参数type是GL_BYPE、GL_UNSIGNED_BYTE、GL_SHORT、 GL_UNSIGNED_SHORT、GL_INT、GL_UNSIGNED_INT、GL_FLOAT或GL_BITMAP。
参数pixels包含了纹理图像数据,这个数据描述了纹理图像本身和它的边界。

纹理控制函数:

void glTexParameter{if}[v](GLenum target,GLenum pname,TYPE param); 具体控制方式:
纹理图像为正方形或长方形,但当它映射到一个多边形或曲面上并变换到屏幕坐标时,纹理的单个纹素很少对应于屏幕图像上的象素。根据所用变换和所用纹理映射,屏幕上单个象素可以对应于一个纹素的一小部分(即放大)或一大批纹素(即缩小)。
glTexParameter*(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
glTexParameter*(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
第一个参数可以是GL_TEXTURE_1D或GL_TEXTURE_2D,即表明所用的纹理是一维的还是二维的;第二个参数指定滤波方法,其中参数值GL_TEXTURE_MAG_FILTER指定为放大滤波方法GL_TEXTURE_MIN_FILTER指定为缩小滤波方法;第三个参数说明滤波方式,若选择GL_NEAREST则采用坐标最靠近象素中心的纹素,这有可能使图像走样;若选择GL_LINEAR则采用最靠近象素中心的四个象素的加权平均值。

void glTexParameter{if}[v](GLenum target,GLenum pname,TYPE param); 如提供了mipmap, 对放大和缩小映射的影响: 1) 对于放大映射, 只使用最小等级的纹理图像. 2) 对于缩小映射, 使用最合适的一种或两种的mipmap的滤波方法. 如滤波方法为GL_NEAREST或GL_LINEAR, 则只使用最小等级的mipmap. 3) 只同时使用一个mipmap, 选择滤波方法GL_NEAREST_MIPMAP_NEAREST或GL_LINEAR_MIPMAP_NEAREST(使用线性插值).   使用那种mipmap取决于缩小量.缺点为存在一个到使用下一个mipmap的切换点. 4) 使用滤波方法GL_NEAREST_MIPMAP_LINEAR或GL_LINEAR_MIPMAP_LINEAR, 根据两个最合适的mipmap中的纹素值进行线性插值. 警告: 如指定一种mipmap纹理滤波方法,但未提供一组完整的mipmap, 则OpenGL隐式禁用纹理映射.

void glTexParameter{if}[v](GLenum target,GLenum pname,TYPE param); 重复与约简
纹理坐标可以超出(0,1)范围,并且在纹理映射过程中可以重复映射或约简映射。在重复映射的情况下,纹理可以在s,t方向上重复,即
glTexParameterfv(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_REPEAT);
glTexParameterfv(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_REPEAT);
若将参数GL_REPEAT改为GL_CLAMP,则所有大于1的纹素值都置为1,所有小于0的值都置为0。

实际上,可以用纹理中的值来调整多边形(曲面)原来的颜色,或用纹理图像中的颜色与多边形(曲面)原来的颜色进行混合。因此,OpenGL提供了三种纹理映射的方式,这个函数是: void glTexEnv{if}[v](GLenum target,GLenum pname,TYPE param);
第一个参数为纹理环境,必为GL_TEXTURE_ENV。第二个参数为纹理环境参数的名称。第三个参数为单值符号常数或指向参数数组的指针。设置纹理映射方式。
若参数pname是GL_TEXTURE_ENV_MODE,则参数param可以是GL_DECAL、GL_MODULATE或GL_BLEND,以说明纹理值怎样与原来表面颜色的处理方式;
若参数pname是GL_TEXTURE_ENV_COLOR,则参数param是包含四个浮点数(分别是R、G、B、A分量)的数组,这些值只在采用GL_BLEND纹理函数时才有用。

纹理坐标
在绘制纹理映射场景时,不仅要给每个顶点定义几何坐标,而且也要定义纹理坐标。经过多种变换后,几何坐标决定顶点在屏幕上绘制的位置,而纹理坐标决定纹理图像中的哪一个纹素赋予该顶点。并且顶点之间的纹理坐标插值与平滑着色插值方法相同。
纹理图像是方形数组,纹理坐标通常可定义成一、二、三或四维形式,称为s,t,r和q坐标,以区别于物体坐标(x,y,z,w)和其他坐标。一维纹理常用s坐标表示,二维纹理常用(s,t)坐标表示,目前忽略r坐标,q坐标象w一样,一般值为1,主要用于建立齐次坐标。OpenGL坐标定义的函数:
void gltexCoord{1234}{sifd}[v](TYPE coords);
设置当前纹理坐标,此后调用glVertex*()所产生的顶点都赋予当前的纹理坐标。对于gltexCoord1*(),s坐标被设置成给定值,t和r设置为0,q设置为1;用gltexCoord2*()可以设置s和t坐标值,r设置为0,q设置为1;对于gltexCoord3*(),q设置为1,其它坐标按给定值设置;用gltexCoord4*()可以给定所有的坐标。

坐标自动生成 
在某些场合(环境映射等)下,为获得特殊效果需要自动产生纹理坐标,并不要求为用函数gltexCoord*()为每个物体顶点赋予纹理坐标值。OpenGL提供了自动产生纹理坐标的函数,其如下: void glTexGen{if}[v](GLenum coord,GLenum pname,TYPE param); 自动产生纹理坐标。
第一个参数必须是GL_S、GL_T、GL_R或GL_Q,它指出纹理坐标s,t,r,q中的哪一个要自动产生;
第二个参数值为GL_TEXTURE_GEN_MODE、GL_OBJECT_PLANE或 GL_EYE_PLANE;
第三个参数param是一个定义纹理产生参数的指针,其值取决于第二个参数pname的设置,当pname为GL_TEXTURE_GEN_MODE时,param是一个常量,即GL_OBJECT_LINEAR、GL_EYE_LINEAR或GL_SPHERE_MAP,它们决定用哪一个函数来产生纹理坐标。对于pname的其它可能值,param是一个指向参数数组的指针。

纹理对象
使用纹理对象来存储纹理数据的步骤: 1) 生成纹理对象名称 2) 将纹理对象绑定到纹理数据(包括图像数据数组和纹理属性), 即创建纹理对象. 3) 如果OpenGL实现高性能纹理工作集, 应检查是否有足够的空间来存储所有的纹理对象. 如没有足够空间, 应设置每个纹理对象的优先级, 以确保最常用的纹理留在工作集中 4) 绑定和重新绑定纹理对象, 以便可以将其中的纹理映射到物体上.

生成纹理对象名称
void glGenTextures(GLsizei n, GLint* textureNames); 功能: 通过数组textureNames返回n个未用的纹理对象名, 返回的名称不必是相邻的整数. GLboolean glIsTexture(GLint textureName); 功能: 如textureName是已被绑定的纹理对象名, 且没有被删除, 则返回GL_TRUE, 如textureName为0, 或非0, 但不是已有纹理对象的名称, 返回GL_FALSE. 注意: 如glGenTextures()返回, 但未使用glBindTextures()绑定, 仍返回GL_FALSE.

生成纹理对象名称
void glGenTextures(GLsizei n, GLint* textureNames); 功能: 通过数组textureNames返回n个未用的纹理对象名, 返回的名称不必是相邻的整数. GLboolean glIsTexture(GLint textureName); 功能: 如textureName是已被绑定的纹理对象名, 且没有被删除, 则返回GL_TRUE, 如textureName为0, 或非0, 但不是已有纹理对象的名称, 返回GL_FALSE. 注意: 如glGenTextures()返回, 但未使用glBindTextures()绑定, 仍返回GL_FALSE.

创建和使用纹理对象
void glBindTexture(GLenum target, GLuint textureName); 功能: 完成下面几项工作. 1) 如textureName为非零无符号整数, 首次被使用, 则创建一新的纹理对象, 并将其名称设置为参数textureName的值. 2) 绑定一个已创建的纹理对象时, 该纹理对象将进入活动状态. 3) 如textureName为0, OpenGL将停止使用纹理对象, 返回到未命名的默认纹理. 4) 首次被创建时, target指定了维数, 其取值为GL_TEXTURE_1D, GL_TEXTURE_2D, GL_TEXTURE_3D 或 GL_TEXTURE_CUBE_MAP. 5) 首次被创建时, 诸如缩小滤波方法, 放大滤波方法, 环绕模式, 边框颜色, 纹理优先级等纹理属性被设置为默认值.

纹理映射整个过程:

三维图形中Texture Mapping应用的最广,将位图粘贴到模型表面使物体更具有真实感。模型做变换时映射的纹理也会随之变化。
auxDIBImageLoad(Filename); // 载入位图并返回指针
glGenTextures(1, &texture[0]); // 创建纹理
glBindTexture(GL_TEXTURE_2D, texture[0]); //选择纹理
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data); //使用来自位图数据生成纹理
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR); //显示图像时采用的滤波方式,线形滤波需要CPU和显卡做更多的运算 glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST); // 非线形滤波,低质量贴图,图片斑驳
glBindTexture(GL_TEXTURE_2D, texture[0]); // 选择纹理
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f); glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f); glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f); // 纹理和四边形的左上

二、解决问题的方法

从图片显示白屏原因可能是生成的图片或者贴图纹理上显示有问题,因此先从图片生成源查起,在androdi2.3中有个函数:(layer.cpp文件)

void Layer::reloadTexture(const Region& dirty)
{
    sp<GraphicBuffer> buffer(mBufferManager.getActiveBuffer());
    if (buffer == NULL) {
        // this situation can happen if we ran out of memory for instance.
        // not much we can do. continue to use whatever texture was bound
        // to this context.
        return;
    }
    if (mGLExtensions.haveDirectTexture()) {
        EGLDisplay dpy(mFlinger->graphicPlane(0).getEGLDisplay());
        if (mBufferManager.initEglImage(dpy, buffer) != NO_ERROR) {
            // not sure what we can do here...
            goto slowpath;
        }
    } else {
slowpath:
        GGLSurface t;
        if (buffer->usage & GRALLOC_USAGE_SW_READ_MASK) {
            status_t res = buffer->lock(&t, GRALLOC_USAGE_SW_READ_OFTEN);  // 1、得到生成图片的缓冲区存储在GGLSurface类型变量中
            LOGE_IF(res, "error %d (%s) locking buffer %p",
                    res, strerror(res), buffer.get());
            if (res == NO_ERROR) {
                mBufferManager.loadTexture(dirty, t); // 2、加载到3D纹理中生成纹理贴图
                buffer->unlock();
            }
        } else {
            // we can't do anything
        }
    }
}

因此可以在以上两个点加入代码,将生成的图片内容生成位图文件,从而分析出其具体的问题。

其中生成bmp图片的代码如下,代码很清晰明了就不再分析了,利用一个普通的dummy.bmp文件生成图,而利用glReadPixels读取纹理图内容再处理保存成一帧帧的位图文件

#define BMP_Header_Length 54
static void dump_surface_info(GGLSurface *sur)
{  
    LOGI("===== dump_surface_info ========== \\n");
    LOGI("width = %d" ,sur->width);
    LOGI("height = %d",sur->height);
    LOGI("stride = %d",sur->stride);
    LOGI("format = %d",sur->format);
    LOGI("data = 0x%x" ,sur->data);
}
static int bmp_convert_format(int format,GLenum *fmt,GLenum *type)
{
    switch(format){
        case HAL_PIXEL_FORMAT_RGBA_8888:
        case HAL_PIXEL_FORMAT_RGBX_8888:
            *fmt = GL_RGBA;
            *type= GL_UNSIGNED_BYTE;
            break;
        case HAL_PIXEL_FORMAT_RGB_888:
            *fmt = GL_RGB;
            *type= GL_UNSIGNED_BYTE;
            break;
        case HAL_PIXEL_FORMAT_RGB_565:
            *fmt = GL_RGB;
            *type= GL_UNSIGNED_SHORT_5_6_5;
            break;
        case HAL_PIXEL_FORMAT_RGBA_5551:
            *fmt = GL_RGBA;
            *type= GL_UNSIGNED_SHORT_5_5_5_1;
            break;
        case HAL_PIXEL_FORMAT_RGBA_4444:
            *fmt = GL_RGBA;
            *type= GL_UNSIGNED_SHORT_4_4_4_4;
            break;
    }
    return 0;
}
/* 函数bmp_capture_grab
 * 抓取窗口中的像素,这里强制按照RGB888进行读取,存储为24位颜色图片
*/
int bmp_capture_grab(GGLSurface *sur,char *fname,int no)
{
    FILE* pDummyFile = NULL;
    FILE* pWritingFile = NULL;
    GLubyte* pPixelData = NULL;
    GLubyte BMP_Header\[BMP_Header_Length\];
    GLint i, j;
    GLint PixelDataLength;
    GLint WindowWidth,WindowHeight;
    char  filename\[256\];
    GLenum mFormat=GL_RGB,mType=GL_UNSIGNED_BYTE;  
    LOGI("===== bmp_capture_grab info ========== \\n");
    dump_surface_info(sur);
    //bmp_convert_format(sur->format,&mFormat,&mType);
    LOGI("bmp capture start format=0x%x,type=0x%x....",mFormat,mType);
    WindowWidth  = sur->width;
    WindowHeight = sur->height;
    // 计算像素数据的实际长度
    i = WindowWidth * 3; // 得到每一行的像素数据长度
    while (i % 4 != 0) // 补充数据,直到i是的倍数
        ++i; // 本来还有更快的算法, 但这里仅追求直观,对速度没有太高要求
    PixelDataLength = i * WindowHeight;
    // 分配内存和打开文件
    pPixelData = (GLubyte*) malloc(PixelDataLength);
    if (pPixelData == NULL){
        LOGI("malloc buffer out of memory failed");
        return -1;
    }
    pDummyFile = fopen("/system/etc/dummy.bmp", "rb");
    if (pDummyFile == NULL){
        LOGE("open dummy file failed");
        return -1;
    }
    sprintf(filename,"/system/etc/%s_%04d.bmp",fname,no);
    pWritingFile = fopen(filename, "wb");
    if (pWritingFile == NULL){
        LOGE("open grab file failed(%s)",filename);
        return -1;
    }
    // 读取像素
    glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
    glReadPixels(0, 0, WindowWidth, WindowHeight, GL_RGB, GL_UNSIGNED_BYTE,pPixelData);
    // 把dummy.bmp的文件头复制为新文件的文件头
    fread(BMP_Header, sizeof(BMP_Header), 1, pDummyFile);
    fwrite(BMP_Header, sizeof(BMP_Header), 1, pWritingFile);
    fseek(pWritingFile, 0x0012, SEEK_SET);
    i = WindowWidth;
    j = WindowHeight;
    fwrite(&i, sizeof(i), 1, pWritingFile);
    fwrite(&j, sizeof(j), 1, pWritingFile);
    // 写入像素数据
    fseek(pWritingFile, 0, SEEK_END);
    fwrite(pPixelData, PixelDataLength, 1, pWritingFile);
    // 释放内存和关闭文件
    fclose(pDummyFile);
    fclose(pWritingFile);
    free(pPixelData);
    pPixelData = NULL;
    LOGI("bmp capture end succcess");
    return 0;  
}