iOS --- OpenGLES之图片纹理

1,700 阅读6分钟

在之前的一系列博客中, 介绍了OpenGLES相关的着色器Shader, 顶点及图形绘制。

那么, 接下来将进入图片纹理Texture的部分. 这里, 我们首先使用OpenGLES的方式绘制一张图片到屏幕上.

Shader脚本

Vertex Shader如下:

attribute vec4 Position;
attribute vec2 TextureCoords;
varying vec2 TextureCoordsOut;

void main(void)
{
    gl_Position = Position;
    TextureCoordsOut = TextureCoords;
}

其中, Position依然是顶点坐标, 即将要绘制到OpenGLES坐标系上的位置. TextureCoords是纹理坐标, 即将要把图片纹理的哪一个像素点绘制出来.

Fragment Shader如下:

precision mediump float;

uniform sampler2D Texture;
varying vec2 TextureCoordsOut;

void main(void)
{
    vec4 mask = texture2D(Texture, TextureCoordsOut);
    gl_FragColor = vec4(mask.rgb, 1.0);
}

TextureCoordsOut是接收Vertex Shader中的TextureCoords变量的值. 参数Texture是图片纹理的来源, 使用texture2D(Texture, TextureCoordsOut);可获取对应纹理坐标点上的颜色. 将该颜色传递给gl_FragColor即可绘制出来图片在该纹理坐标点上的颜色.

关联Shader参数

_glProgram = [ShaderOperations compileShaders:@"DemoDrawImageTextureVertex" shaderFragment:@"DemoDrawImageTextureFragment"];
glUseProgram(_glProgram);

// 需要三个参数, 跟Shader中的一一对应。
// Position: 将颜色放置在CAEAGLLayer上的哪个位置
// Texture: 图像的纹理
// TextureCoords: 图像的纹理坐标,即图像纹理的哪一块颜色
_positionSlot = glGetAttribLocation(_glProgram, "Position");
_textureSlot = glGetUniformLocation(_glProgram, "Texture");
_textureCoordsSlot = glGetAttribLocation(_glProgram, "TextureCoords");

此处与之前的demo基本一致.

获取图片纹理

获取图片纹理的方式通常比较固定, 涉及到一系列的CoreGraphics方法调用, 封装如下:

/**
 *  加载image, 使用CoreGraphics将位图以RGBA格式存放. 将UIImage图像数据转化成OpenGL ES接受的数据.
 *  然后在GPU中将图像纹理传递给GL_TEXTURE_2D。
 *  @return 返回的是纹理对象,该纹理对象暂时未跟GL_TEXTURE_2D绑定(要调用bind)。
 *  即GL_TEXTURE_2D中的图像数据都可从纹理对象中取出。
 */
- (GLuint)setupTexture:(UIImage *)image {
    CGImageRef cgImageRef = [image CGImage];
    GLuint width = (GLuint)CGImageGetWidth(cgImageRef);
    GLuint height = (GLuint)CGImageGetHeight(cgImageRef);
    CGRect rect = CGRectMake(0, 0, width, height);

    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    void *imageData = malloc(width * height * 4);
    CGContextRef context = CGBitmapContextCreate(imageData, width, height, 8, width * 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
    CGContextTranslateCTM(context, 0, height);
    CGContextScaleCTM(context, 1.0f, -1.0f);
    CGColorSpaceRelease(colorSpace);
    CGContextClearRect(context, rect);
    CGContextDrawImage(context, rect, cgImageRef);

    glEnable(GL_TEXTURE_2D);

    /**
     *  GL_TEXTURE_2D表示操作2D纹理
     *  创建纹理对象,
     *  绑定纹理对象,
     */

    GLuint textureID;
    glGenTextures(1, &textureID);
    glBindTexture(GL_TEXTURE_2D, textureID);

    /**
     *  纹理过滤函数
     *  图象从纹理图象空间映射到帧缓冲图象空间(映射需要重新构造纹理图像,这样就会造成应用到多边形上的图像失真),
     *  这时就可用glTexParmeteri()函数来确定如何把纹理象素映射成像素.
     *  如何把图像从纹理图像空间映射到帧缓冲图像空间(即如何把纹理像素映射成像素)
     */
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // S方向上的贴图模式
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // T方向上的贴图模式
    // 线性过滤:使用距离当前渲染像素中心最近的4个纹理像素加权平均值
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    /**
     *  将图像数据传递给到GL_TEXTURE_2D中, 因其于textureID纹理对象已经绑定,所以即传递给了textureID纹理对象中。
     *  glTexImage2d会将图像数据从CPU内存通过PCIE上传到GPU内存。
     *  不使用PBO时它是一个阻塞CPU的函数,数据量大会卡。
     */
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);

    // 结束后要做清理
    glBindTexture(GL_TEXTURE_2D, 0); //解绑
    CGContextRelease(context);
    free(imageData);

    return textureID;
}

该方法接收一个UIImage对象, 将其图像数据传递至一个纹理对象中, 最后返回该纹理对象. 则以后取出该图像数据时, 直接根据纹理对象取出即可. 其中的glTexImage2D方法用于将图像数据传递至GL_TEXTURE_2D中, 而如果已经提前将GL_TEXTURE_2D与一个纹理对象绑定好了, 则该方法可将图像数据传递至纹理对象中(这里是textureID).

混合模式

关于混合模式, 是一个非常大的话题. 这里只介绍glBlendFunc的使用. glBlendFunc的参数1作用于源数据, 参数2作用于目标数据. 混合作用的结果颜色是 源颜色源因子 + 目标颜色目标因子. 例如, 我们这里的demo是直接将一张图片贴在OpenGL画布上, 因此图片取GL_ONE, 而目标数据即OpenGL画布取GL_ZERO.

glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ZERO);

而另外一种非常常见的混合模式是:

glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

混合后的结果颜色是 source * source_alpha + destination * (1 - source_alpha). 这种混合模式就是将两种颜色混合叠加的效果. 只有RGBA模式下才使用blend模式. 其他常见的混合模式有:

glBlendFunc(GL_ONE, GL_ZERO) 完全使用源颜色, 完全不使用目标颜色. 和不使用blend的时候一致.
glBlendFunc(GL_ZERO, GL_ONE) 完全不使用源颜色, 完全使用目标颜色. 即原图没有变化.
glBlendFunc(GL_ONE, GL_ONE) 两种颜色的直接相加.

纹理的渲染

有了纹理对象且存储了图像数据后, 就可以拿来进行纹理渲染了. 使用时, 直接再次绑定GL_TEXTURE_2D即可, 表示接下来读取GL_TEXTURE_2D时要使用的是_textureID纹理对象中的数据:

// 第一行和第三行不是严格必须的,默认使用GL_TEXTURE0作为当前激活的纹理单元
glActiveTexture(GL_TEXTURE5); // 指定纹理单元GL_TEXTURE5
glBindTexture(GL_TEXTURE_2D, _textureID); // 绑定,即可从_textureID中取出图像数据。
glUniform1i(_textureSlot, 5); // 与纹理单元的序号对应

纹理数据可以跟纹理坐标一一对应, 在OpenGL坐标中呈现出来.

GLfloat texCoords[] = {
    0, 0,//左下
    1, 0,//右下
    0, 1,//左上
    1, 1,//右上
};
glVertexAttribPointer(_textureCoordsSlot, 2, GL_FLOAT, GL_FALSE, 0, texCoords);
glEnableVertexAttribArray(_textureCoordsSlot);

纹理坐标也同样存储于一个数组中, 使用glVertexAttribPointer方法将其传递给对应的插槽_textureCoordsSlot, 继而传递给Shader中的TextureCoords变量. 注意, 纹理坐标的坐标系与OpenGL不同, 左下角是原点.

有了纹理数据和纹理坐标, 接下来只需要我们已经很熟悉的顶点绘制方式将OpenGL画布绘制出来即可. 这里, 顶点坐标与纹理坐标的位置要一一对应, 即将图像的某个点绘制到OpenGL画布的对应点. 如果我们只是普通的渲染图片, 则将左下, 右下, 左上, 右上这四个点一一对应起来就好了.

GLfloat vertices[] = {
    -1, -1, 0,   //左下
    1,  -1, 0,   //右下
    -1, 1,  0,   //左上
    1,  1,  0 }; //右上
glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices);
glEnableVertexAttribArray(_positionSlot);

// 一旦纹理数据准备好,两个坐标系的顶点位置一一对应好。
// 就直接绘制顶点即可, 具体的绘制方式就与纹理坐标和纹理数据没有关系了。
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

glBindTexture(GL_TEXTURE_2D, 0); // 使用完之后解绑GL_TEXTURE_2D
[_eaglContext presentRenderbuffer:GL_RENDERBUFFER];

这样, 使用OpenGLES来渲染一张图片的过程就结束了.

实际上, 准备好了纹理数据, 并且将纹理坐标传递给了Shader之后, 就不再需要对其进行操作了. 因此, 至于要不要使用VBO来优化顶点绘制的效率, 就是另外一回事了.

使用索引和VBO来绘制

那么, 既然如此, 再来回顾一下索引数组和VBO的使用吧.

const GLfloat vertices[] = {
    -1, -1, 0,   //左下
    1,  -1, 0,   //右下
    -1, 1,  0,   //左上
    1,  1,  0 }; //右上

GLuint vertexBuffer;
glGenBuffers(1, &vertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, 0);
glEnableVertexAttribArray(_positionSlot);


const GLubyte indices[] = {
    0,1,2,
    1,2,3
};
GLuint indexBuffer;
glGenBuffers(1, &indexBuffer);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

glDrawElements(GL_TRIANGLE_STRIP, sizeof(indices)/sizeof(indices[0]), GL_UNSIGNED_BYTE, 0);

Demo

请参考: Demo