OpenGLES(八)GPUImage滤镜链与将滤镜修改后的图片保存到相册

1.滤镜链

在一个复合滤镜中,多种滤镜效果处理时,通常都是图片 -> 设置顶点/纹理坐标 -> 滤镜效果处理 -> 帧缓冲区 -> 新的纹理 -> 滤镜效果处理 -> 帧缓冲区 -> 新的纹理。
此过程就是滤镜链。
思考:那如何将滤镜处理完之后存到帧缓冲区,并生成纹理的呢?

2.滤镜处理 -> 帧缓冲区 -> 新的纹理 -> 相册

2.1 步骤

  1. 根据屏幕上的显示,重新获取顶点坐标和纹理坐标,并生成新的纹理(将纹理加载到帧缓冲区中)

    - 根据屏幕的显示,设置顶点坐标与纹理坐标:在滤镜处理时不管使用什么方式;
    (GLKit、GLSL、GPUImage等)都可以获取当前的顶点坐标与纹理坐标。
    - 生成帧缓存区,并绑定;
    - 生成纹理ID,绑定纹理;
    - 将纹理加载到帧缓冲区中;
    - 设置、获取着色器,将顶点着色器和片元着色器编译、链接;
    - 传值,将顶点着色器和片元着色器需要用到的参数出入进去;
    - 准备绘制,绘制。
    
  2. 在帧缓冲区中获取图片(UIImage)

    - 绑定帧缓冲区;
    - 将帧缓存区内的图片纹理绘制到图片上;使用到了glReadPixels和CGDataProviderRef方法,
    glReadPixels:将已经绘制好的像素,从显存中读取到内存中;
    CGDataProviderCreateWithData : 生成新的数据类型, 方便访问二进制数据;
    
    - 将帧缓存区里像素点绘制到一张图片上;
    - 此时的 imageRef 是上下颠倒的,调用 CG 的方法重新绘制一遍,刚好翻转过来。
    
  3. 将图片保存到相册

    使用PHPhotoLibrary将图片保存到系统相册。
    

2.2代码的实现

2.2.1生成纹理部分

顶点着色器:

//顶点坐标
attribute vec4 Position;
//纹理坐标
attribute vec2 TextureCoords;
//需要传入片元着色器的纹理坐标
varying vec2 TextureCoordsVarying;

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

片元着色器:

//声明高精度float
precision highp float;
//纹理
uniform sampler2D Texture;
//纹理坐标
varying vec2 TextureCoordsVarying;

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

获取顶点坐标与纹理坐标:

/**
着色器的编译、链接
*/
//加载shader、链接
-(GLuint)loadShaders:(NSString *)vert Withfrag:(NSString *)frag
{
    //1.定义2个零时着色器对象
    GLuint verShader, fragShader;
    //创建program
    GLint program = glCreateProgram();
    
    //2.编译顶点着色程序、片元着色器程序
    //参数1:编译完存储的底层地址
    //参数2:编译的类型,GL_VERTEX_SHADER(顶点)、GL_FRAGMENT_SHADER(片元)
    //参数3:文件路径
    [self compileShader:&verShader type:GL_VERTEX_SHADER file:vert];
    [self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:frag];
    
    //3.创建最终的程序
    glAttachShader(program, verShader);
    glAttachShader(program, fragShader);
    
    //4.释放不需要的shader
    glDeleteShader(verShader);
    glDeleteShader(fragShader);
    
     //3. 链接 program
    glLinkProgram(program);
    
    //4. 检查链接是否成功
    GLint linkSuccess;
    glGetProgramiv(program, GL_LINK_STATUS, &linkSuccess);
    if (linkSuccess == GL_FALSE) {
        GLchar messages[256];
        glGetProgramInfoLog(program, sizeof(messages), 0, &messages[0]);
        NSString *messageString = [NSString stringWithUTF8String:messages];
        NSAssert(NO, @"program链接失败:%@", messageString);
    }
    return program;
   
}

//编译shader
- (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file{
    
    //1.读取文件路径字符串
    NSString* content = [NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil];
    const GLchar* source = (GLchar *)[content UTF8String];
    
    //2.创建一个shader(根据type类型)
    *shader = glCreateShader(type);
    
    //3.将着色器源码附加到着色器对象上。
    //参数1:shader,要编译的着色器对象 *shader
    //参数2:numOfStrings,传递的源码字符串数量 1个
    //参数3:strings,着色器程序的源码(真正的着色器程序源码)
    //参数4:lenOfStrings,长度,具有每个字符串长度的数组,或NULL,这意味着字符串是NULL终止的
    glShaderSource(*shader, 1, &source,NULL);
    
    //4.把着色器源代码编译成目标代码
    glCompileShader(*shader); 
    // 查询 shader 是否编译成功
    GLint compileSuccess;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compileSuccess);
    if (compileSuccess == GL_FALSE) {
        GLchar messages[256];
        glGetShaderInfoLog(shader, sizeof(messages), 0, &messages[0]);
        NSString *messageString = [NSString stringWithUTF8String:messages];
        NSAssert(NO, @"shader编译失败:%@", messageString);
    }
}

/**
 根据当前屏幕上的显示,来重新创建纹理
 
 @param originWidth 纹理的原始实际宽度
 @param originHeight 纹理的原始实际高度
 @param topY 0 ~ 1,拉伸区域的顶边的纵坐标
 @param bottomY 0 ~ 1,拉伸区域的底边的纵坐标
 @param newHeight 0 ~ 1,拉伸区域的新高度
 */
- (void)resetTextureWithOriginWidth:(CGFloat)originWidth
                       originHeight:(CGFloat)originHeight
                               topY:(CGFloat)topY
                            bottomY:(CGFloat)bottomY
                          newHeight:(CGFloat)newHeight {
//1.新的纹理尺寸(新纹理图片的宽高)
    GLsizei newTextureWidth = originWidth;
    GLsizei newTextureHeight = originHeight * (newHeight - (bottomY - topY)) + originHeight;
    
    //2.高度变化百分比
    CGFloat heightScale = newTextureHeight / originHeight;
    
    //3.在新的纹理坐标下,重新计算topY、bottomY
    CGFloat newTopY = topY / heightScale;
    CGFloat newBottomY = (topY + newHeight) / heightScale;
    
    //4.创建顶点数组与纹理数组(逻辑与calculateOriginTextureCoordWithTextureSize 中关于纹理坐标以及顶点坐标逻辑是一模一样的)
    SenceVertex *tmpVertices = malloc(sizeof(SenceVertex) * kVerticesCount);
    tmpVertices[0] = (SenceVertex){{-1, 1, 0}, {0, 1}};
    tmpVertices[1] = (SenceVertex){{1, 1, 0}, {1, 1}};
    tmpVertices[2] = (SenceVertex){{-1, -2 * newTopY + 1, 0}, {0, 1 - topY}};
    tmpVertices[3] = (SenceVertex){{1, -2 * newTopY + 1, 0}, {1, 1 - topY}};
    tmpVertices[4] = (SenceVertex){{-1, -2 * newBottomY + 1, 0}, {0, 1 - bottomY}};
    tmpVertices[5] = (SenceVertex){{1, -2 * newBottomY + 1, 0}, {1, 1 - bottomY}};
    tmpVertices[6] = (SenceVertex){{-1, -1, 0}, {0, 0}};
    tmpVertices[7] = (SenceVertex){{1, -1, 0}, {1, 0}};
    
    
    ///下面开始渲染到纹理的流程
    
    //1. 生成帧缓存区;
    GLuint frameBuffer;
    GLuint texture;
    //glGenFramebuffers 生成帧缓存区对象名称;
    glGenFramebuffers(1, &frameBuffer);
    //glBindFramebuffer 绑定一个帧缓存区对象;
    glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
    
    //2. 生成纹理ID,绑定纹理;
    //glGenTextures 生成纹理ID
    glGenTextures(1, &texture);
    //glBindTexture 将一个纹理绑定到纹理目标上;
    glBindTexture(GL_TEXTURE_2D, texture);
    //glTexImage2D 指定一个二维纹理图像;
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, newTextureWidth, newTextureHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
    
    //3. 设置纹理相关参数
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    
    //4. 将纹理图像加载到帧缓存区对象上;
    /*
     glFramebufferTexture2D (GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level)
     target: 指定帧缓冲目标,符合常量必须是GL_FRAMEBUFFER;
     attachment: 指定附着纹理对象的附着点GL_COLOR_ATTACHMENT0
     textarget: 指定纹理目标, 符合常量:GL_TEXTURE_2D
     teture: 指定要附加图像的纹理对象;
     level: 指定要附加的纹理图像的mipmap级别,该级别必须为0。
     */
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
    
    //5. 设置视口尺寸
    glViewport(0, 0, newTextureWidth, newTextureHeight);
    //6. 获取着色器程序
    GLuint program = [self loadShaders:"spring.vsh" Withfrag:"spring.fsh" ];
    glUseProgram(program);
    
    //7. 获取参数ID
    //(1)注意:第二参数字符串必须和shaderv.vsh中的输入变量:position保持一致
    GLuint position = glGetAttribLocation(self.myPrograme, "position");
    
    //(2).设置合适的格式从buffer里面读取数据
    glEnableVertexAttribArray(position);
    
    //(3).设置读取方式
    //参数1:index,顶点数据的索引
    //参数2:size,每个顶点属性的组件数量,1,2,3,或者4.默认初始值是4.
    //参数3:type,数据中的每个组件的类型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默认初始值为GL_FLOAT
    //参数4:normalized,固定点数据值是否应该归一化,或者直接转换为固定值。(GL_FALSE)
    //参数5:stride,连续顶点属性之间的偏移量,默认为0;
    //参数6:指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0
    glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, positionSlot, NULL+offsetof(SenceVertex, positionCoord));
    
    
    //9.----处理纹理数据-------
    //(1).glGetAttribLocation,用来获取vertex attribute的入口的.
    //注意:第二参数字符串必须和shaderv.vsh中的输入变量:textCoordinate保持一致
    GLuint textCoor = glGetAttribLocation(self.myPrograme, "textCoordinate");
    
    //(2).设置合适的格式从buffer里面读取数据
    glEnableVertexAttribArray(textCoor);
    
    //(3).设置读取方式
    //参数1:index,顶点数据的索引
    //参数2:size,每个顶点属性的组件数量,1,2,3,或者4.默认初始值是4.
    //参数3:type,数据中的每个组件的类型,常用的有GL_FLOAT,GL_BYTE,GL_SHORT。默认初始值为GL_FLOAT
    //参数4:normalized,固定点数据值是否应该归一化,或者直接转换为固定值。(GL_FALSE)
    //参数5:stride,连续顶点属性之间的偏移量,默认为0;
    //参数6:指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0
    glVertexAttribPointer(textCoor, 2, GL_FLOAT, GL_FALSE, textureCoordsSlot, (float *)NULL + offsetof(SenceVertex, textureCoord));
   
    //8. 传值
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, self.baseEffect.texture2d0.name);
    glUniform1i(textureSlot, 0);
    }
    //开始绘制
    glDrawArrays(mode, first, count);
    //12.解绑缓存
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    //13.释放顶点数组
    free(tmpVertices);
    
    //14.保存临时的纹理对象/帧缓存区对象;
    self.tmpTexture = texture;
    self.tmpFrameBuffer = frameBuffer;
    

2.2.2 在帧缓冲区中获取图片(UIImage)

// 返回某个纹理对应的 UIImage,调用前先绑定对应的帧缓存
- (UIImage *)imageFromTextureWithWidth:(int)width height:(int)height {
    
    //1.绑定帧缓存区;
    glBindFramebuffer(GL_FRAMEBUFFER, self.tmpFrameBuffer);
    
    //2.将帧缓存区内的图片纹理绘制到图片上;
    int size = width * height * 4;
    GLubyte *buffer = malloc(size);
    
    /*
     
     glReadPixels (GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid* pixels);
     @功能: 读取像素(理解为将已经绘制好的像素,从显存中读取到内存中;)
     @参数解读:
     参数x,y,width,height: xy坐标以及读取的宽高;
     参数format: 颜色格式; GL_RGBA;
     参数type: 读取到的内容保存到内存所用的格式;GL_UNSIGNED_BYTE 会把数据保存为GLubyte类型;
     参数pixels: 指针,像素数据读取后, 将会保存到该指针指向的地址内存中;
     
     注意: pixels指针,必须保证该地址有足够的可以使用的空间, 以容纳读取的像素数据; 例如一副256 * 256的图像,如果读取RGBA 数据, 且每个数据保存在GLUbyte. 总大小就是 256 * 256 * 4 = 262144字节, 即256M;
     int size = width * height * 4;
     GLubyte *buffer = malloc(size);
     */
    glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
    
    //使用data和size 数组来访问buffer数据;
    /*
     CGDataProviderRef CGDataProviderCreateWithData(void *info, const void *data, size_t size, CGDataProviderReleaseDataCallback releaseData);
     @功能: 新的数据类型, 方便访问二进制数据;
     @参数:
     参数info: 指向任何类型数据的指针, 或者为Null;
     参数data: 数据存储的地址,buffer
     参数size: buffer的数据大小;
     参数releaseData: 释放的回调,默认为空;
     
     */
    CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, buffer, size, NULL);
    //每个组件的位数;
    int bitsPerComponent = 8;
    //像素占用的比特数4 * 8 = 32;
    int bitsPerPixel = 32;
    //每一行的字节数
    int bytesPerRow = 4 * width;
    //颜色空间格式;
    CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
    //位图图形的组件信息 - 默认的
    CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;
    //颜色映射
    CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
    
    //3.将帧缓存区里像素点绘制到一张图片上;
    /*
     CGImageCreate(size_t width, size_t height,size_t bitsPerComponent, size_t bitsPerPixel, size_t bytesPerRow,CGColorSpaceRef space, CGBitmapInfo bitmapInfo, CGDataProviderRef provider,const CGFloat decode[], bool shouldInterpolate,CGColorRenderingIntent intent);
     @功能:根据你提供的数据创建一张位图;
     注意:size_t 定义的是一个可移植的单位,在64位机器上为8字节,在32位机器上是4字节;
     参数width: 图片的宽度像素;
     参数height: 图片的高度像素;
     参数bitsPerComponent: 每个颜色组件所占用的位数, 比如R占用8位;
     参数bitsPerPixel: 每个颜色的比特数, 如果是RGBA则是32位, 4 * 8 = 32位;
     参数bytesPerRow :每一行占用的字节数;
     参数space:颜色空间模式,CGColorSpaceCreateDeviceRGB
     参数bitmapInfo:kCGBitmapByteOrderDefault 位图像素布局;
     参数provider: 图片数据源提供者, 在CGDataProviderCreateWithData ,将buffer 转为 provider 对象;
     参数decode: 解码渲染数组, 默认NULL
     参数shouldInterpolate: 是否抗锯齿;
     参数intent: 图片相关参数;kCGRenderingIntentDefault
     
     */
    CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);
    
    //4. 此时的 imageRef 是上下颠倒的,调用 CG 的方法重新绘制一遍,刚好翻转过来
    //创建一个图片context
    UIGraphicsBeginImageContext(CGSizeMake(width, height));
    CGContextRef context = UIGraphicsGetCurrentContext();
    //将图片绘制上去
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
    //从context中获取图片
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    //结束图片context处理
    UIGraphicsEndImageContext();
    
    //释放buffer
    free(buffer);
    //返回图片
    return image;
}

2.2.3 将图片存到相册

// 保存图片到相册
- (void)saveImage:(UIImage *)image {
    //将图片通过PHPhotoLibrary保存到系统相册
    [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
        [PHAssetChangeRequest creationRequestForAssetFromImage:image];
    } completionHandler:^(BOOL success, NSError * _Nullable error) {
        NSLog(@"success = %d, error = %@ 图片已保存到相册", success, error);
    }];
}

你可能感兴趣的:(OpenGLES)