从0打造一个GPUImage(3)

306 阅读7分钟
原文链接: zhuanlan.zhihu.com

从0打造一个GPUImage(3)

一个纹理其实就是一幅图像。我们可以把这幅图像的整体或部分贴到我们先前用顶点勾画出的物体上去——比如对一个立方体、圆等贴上纹理图。我们也可以对纹理图像的整体或某个部分重复使用,贴到我们的目标物体上。

矩形

绘制一个矩形看起来非常简单,因为一个矩形就是由两个三角形组成的。
所以如果我们需要绘制一个满屏的正方形。


只需要传入六个顶点数据。
acd3个顶点数据用来绘制红色的三角形,abd3个顶点数据用来绘制黄色的三角形。那么,两个组合起来就是一个矩形了。

那么按照OpenGL ES的坐标系,把他们用一维数组的形式表示就是

const GLfloat vertices[] = {
        -1, 1,  0,   //a
        -1, -1, 0,   //c
        1,  -1, 0,   //d
        
        -1, 1,  0,   //a
        1, 1,     0,   //b
        1,  -1, 0,   //d };

我们把顶点数据替换到之前的- (void)drawTrangle函数中。

- (void)drawTrangle {
    const GLfloat vertices[] = {
        -1, 1,  0,   //a
        -1, -1, 0,   //c
        1,  -1, 0,   //d
        
        -1, 1,  0,   //a
        1,  1,   0,   //b
        1,  -1, 0,   //d
        
    };
    
    glEnableVertexAttribArray(_positionSlot);
    glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices);
    
    glDrawArrays(GL_TRIANGLES, 0, 6);
    [_eaglContext presentRenderbuffer:GL_RENDERBUFFER];
}

运行一下,是这样的。



是个矩形没错,但是这个矩形全部都是红色的。
我们想绘制的矩形是左下半边红,右上半边是黄色。

大家思考一下我们的fragment shader应该怎么写,可以达到这种效果。

纹理

在没有深入的理解OpenGL之前,你们可以简单的认为纹理就是一张图片。
刚才画了个矩形的原因很简单。因为我们需要在矩形上显示纹理。
因为普遍情况是,无论是什么平台,控件一般都是矩形,当然,我们可以用一个三角形显示我们的纹理。
不过这里还是以常规的矩形为主。

在介绍纹理之前,我们需要先了解一下纹理的坐标系。

因为在OpenGL ES中,纹理的作用主要是用作贴图的,打个比方就是,假如一个游戏中有一面大理石墙,那么为了做到仿真的效果,那么就需要我们拿一张真实世界的大理石图片,贴到OpenGL ES中的一个矩形区域上。这样就形成了墙面的效果。

纹理的坐标系如下。



所以,我们把这张图片贴到上一章的矩形上的操作就相当于。


如何向GPU传递纹理信息

简单来说,就是如何把iOS中的UIImage转换为OpenGL ES中的texture。

直接上代码

- (GLuint)getTextureFromImage:(UIImage *)image {
 // CoreGraphics部分
    CGImageRef imageRef = [image CGImage];
    size_t width = CGImageGetWidth(imageRef);
    size_t height = CGImageGetHeight(imageRef);
    GLubyte* textureData = (GLubyte *)malloc(width * height * 4);
    
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    NSUInteger bytesPerPixel = 4;
    NSUInteger bytesPerRow = bytesPerPixel * width;
    NSUInteger bitsPerComponent = 8;
    CGContextRef context = CGBitmapContextCreate(textureData, width, height,
                                                 bitsPerComponent, bytesPerRow, colorSpace,
                                                 kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
    CGContextTranslateCTM(context, 0, height);
    CGContextScaleCTM(context, 1.0f, -1.0f);
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
    
    // 纹理部分
    glEnable(GL_TEXTURE_2D);
    GLuint texName;
    glGenTextures(1, &texName);
    glBindTexture(GL_TEXTURE_2D, texName);
    
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)width, (GLsizei)height, 0, GL_RGBA, GL_UNSIGNED_BYTE, textureData);
    
    CGContextRelease(context);
    CGColorSpaceRelease(colorSpace);
    free(textureData);
    return texName;
}

这部分代码主要分为两部分。

  1. CoreGraphics部分
    主要是把Image里面的RGBA信息拿出来,需要注意的是,由于纹理坐标是左下角为原点坐标,所以在这里我们还进行了坐标系转换。
  2. 纹理部分

纹理的处理分为这么几个步骤。


当然,这里只是阐述了函数调用的过程。下面介绍一下为什么要这么用。

  • glGenTextures,用来生成纹理ID。使用纹理之前,必须执行这句命令为你的texture分配一个ID,这里我们使用texName来储存生成的纹理ID。glGenTextures(1, &texName);
  • glBindTexture, bind在OpenGL中是个特别常用的操作。它改变了OpenGL的一个状态,它告诉OpenGL下面对纹理的任何操作都是对它所绑定的纹理对象的,例如glBindTexture(GL_TEXTURE_2D, texName);告诉OpenGL下面代码中对2D纹理的任何设置都是针对索引为texName的纹理的。
  • 下面四句就是设置2D纹理的属性,由于GL_TEXTURE_2D已经被绑定到texName这个纹理。所以下面也就是设置texName这个纹理的属性。

glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

glTexParameterf有3个参数,第一个参数用来指明需要设置什么东西的属性。GL_TEXTURE_2D表明这里我们设置的是2D纹理的属性。
第二个参数指的是属性的名字。属性主要有4类。
GL_TEXTURE_MIN_FILTER,
GL_TEXTURE_MAG_FILTER, 
GL_TEXTURE_WRAP_S,
GL_TEXTURE_WRAP_T

用比较简单的话解释就是。GL_TEXTURE_MIN_FILTER表示当图片纹理需要压缩的时候采取哪些策略。比如你的图片1024 * 768, 放在了500 * 500的矩形上,怎么处理。
GL_TEXTURE_MAG_FILTER表示图小但是容器大的情况怎么处理。
GL_TEXTURE_WRAP_S和GL_TEXTURE_WRAP_T使用场景比较特殊,大家知道,上面我们提到了纹理的区间无论是X轴还是Y轴,都是0 - 1,但是如果区间超过这个范围。应该采取什么措施。这里,有一篇更好的文章来介绍他们。http://blog.csdn.net/wangdingqiaoit/article/details/51457675

  • glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)width, (GLsizei)height, 0, GL_RGBA, GL_UNSIGNED_BYTE, textureData);这句话就是把CoreGraphics生成的储存rgb信息的data赋值给纹理了。

如何使用纹理信息

使用纹理信息需要两个必要条件。第一个是,纹理的坐标。第二个是纹理。

转化为fragment shader里的内容就是。

precision mediump float;
uniform sampler2D u_Texture;
varying vec2 v_TexCoordOut;

void main(void) {
    vec4 color = texture2D(u_Texture, v_TexCoordOut);
    gl_FragColor = color;
    
}

uniform sampler2D u_Texture就是我们的纹理。
varying vec2 v_TexCoordOut是我们的纹理坐标。
由于fragment shader中不能直接传递坐标信息。所以我们使用了varying关键字,意思是这个变量的值是经由vertex shader处理之后传递过来的。那么相应的,vertex shader中也要变一下。

attribute vec4 a_Position;

attribute vec2 a_TexCoordIn;
varying vec2 v_TexCoordOut;

void main(void) {
    gl_Position = a_Position;
    v_TexCoordOut = a_TexCoordIn;
}

利用一个attribute变量a_TexCoordIn接受纹理坐标信息,然后传递给我们的varying变量v_TexCoordOut。这样,fragment shader就拥有了纹理坐标信息。

这里fragment shader中有一句vec4 color = texture2D(u_Texture, v_TexCoordOut);,这句话是什么意思呢?texture2D函数其实就是通过纹理的某个坐标,取出这个坐标的RGBA信息。然后,我们又把这个color赋给了gl_FragColor,这样,我们的矩形中的每个像素又和纹理有一一映射的关系,所以,我们的整个矩形显示的就是整张图片了。

代码中的应用

绑定了纹理之后,还有几个步骤需要处理。

- (void)activeTexture {
    GLuint texName = [self getTextureFromImage:[UIImage imageNamed:@"smalldaniel.jpg"]];
    
    glActiveTexture(GL_TEXTURE5);
    glBindTexture(GL_TEXTURE_2D, texName);
    glUniform1i(_textureSlot, 5);
}

首先,需要激活某个纹理,为什么是某个纹理呢?



看这张图,iOS中为我们提供了一共31个纹理单元,也就是说,你同时可以使用31个纹理,但是我们这里通过
glActiveTexture表示,我们现在需要使用第5个纹理单元。

至于又使用了一次glBindTexture是因为我们之前绑定了GL_TEXTURE_2D和texName之后,利用glBindTexture(GL_TEXTURE_2D, 0);解除了绑定。所以这里我们需要重新绑定一下。告诉OpenGL 接下来的任何对GL_TEXTURE_2D的操作都是在对texName的设定。
最后glUniform1i(_textureSlot, 5);意思就是把第五个纹理单元的值传递到fragment shader里的u_Texture。因为_textureSlot是我们通过上一章内容介绍的_textureSlot = [shaderCompiler uniformIndex:@"u_Texture"]; 方法获得的location。

这样,我们的shader里就获得了纹理信息。

最后看一下我们的draw方法发生了哪些改变。

- (void)drawTrangle {
    [self activeTexture];
    UIImage *image = [UIImage imageNamed:@"smalldaniel.jpg"];
    CGRect realRect = AVMakeRectWithAspectRatioInsideRect(image.size, self.view.bounds);
    CGFloat widthRatio = realRect.size.width/self.view.bounds.size.width;
    CGFloat heightRatio = realRect.size.height/self.view.bounds.size.height;
    
    const GLfloat vertices[] = {
        -1, -1, 0,   //左下
        1,  -1, 0,   //右下
        -1, 1,  0,   //左上
        1,  1,  0 }; //右上
    glEnableVertexAttribArray(_positionSlot);
    glVertexAttribPointer(_positionSlot, 3, GL_FLOAT, GL_FALSE, 0, vertices);
    
    // normal
    static const GLfloat coords[] = {
        0, 0,
        1, 0,
        0, 1,
        1, 1
    };

    glEnableVertexAttribArray(_textureCoordSlot);
    glVertexAttribPointer(_textureCoordSlot, 2, GL_FLOAT, GL_FALSE, 0, coords);
    
    static const GLfloat colors[] = {
        1, 0, 0, 1,
        1, 0, 0, 1,
        1, 0, 0, 1,
        1, 0, 0, 1
    };
    
    glEnableVertexAttribArray(_colorSlot);
    glVertexAttribPointer(_colorSlot, 4, GL_FLOAT, GL_FALSE, 0, colors);
    
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    [_eaglContext presentRenderbuffer:GL_RENDERBUFFER];
}

这里主要是多了赋值纹理坐标的操作。就不赘述了。需要注意的是,纹理坐标在数组中的顺序要和顶点的顺序匹配。不然会发生意想不到的情况。当然有兴趣的朋友可以试试。

最后运行一下代码。


demo地址是:https://github.com/zangqilong198812/OpenGLESTutorial