1.滤镜链
在一个复合滤镜中,多种滤镜效果处理时,通常都是图片 -> 设置顶点/纹理坐标 -> 滤镜效果处理 -> 帧缓冲区 -> 新的纹理 -> 滤镜效果处理 -> 帧缓冲区 -> 新的纹理。
此过程就是滤镜链。
思考:那如何将滤镜处理完之后存到帧缓冲区,并生成纹理的呢?
2.滤镜处理 -> 帧缓冲区 -> 新的纹理 -> 相册
2.1 步骤
-
根据屏幕上的显示,重新获取顶点坐标和纹理坐标,并生成新的纹理(将纹理加载到帧缓冲区中)
- 根据屏幕的显示,设置顶点坐标与纹理坐标:在滤镜处理时不管使用什么方式; (GLKit、GLSL、GPUImage等)都可以获取当前的顶点坐标与纹理坐标。 - 生成帧缓存区,并绑定; - 生成纹理ID,绑定纹理; - 将纹理加载到帧缓冲区中; - 设置、获取着色器,将顶点着色器和片元着色器编译、链接; - 传值,将顶点着色器和片元着色器需要用到的参数出入进去; - 准备绘制,绘制。
-
在帧缓冲区中获取图片(UIImage)
- 绑定帧缓冲区; - 将帧缓存区内的图片纹理绘制到图片上;使用到了glReadPixels和CGDataProviderRef方法, glReadPixels:将已经绘制好的像素,从显存中读取到内存中; CGDataProviderCreateWithData : 生成新的数据类型, 方便访问二进制数据;- 将帧缓存区里像素点绘制到一张图片上; - 此时的 imageRef 是上下颠倒的,调用 CG 的方法重新绘制一遍,刚好翻转过来。
-
将图片保存到相册
使用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;//创建programGLint 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.释放不需要的shaderglDeleteShader(verShader);glDeleteShader(fragShader);//3. 链接 programglLinkProgram(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)originWidthoriginHeight:(CGFloat)originHeighttopY:(CGFloat)topYbottomY:(CGFloat)bottomYnewHeight:(CGFloat)newHeight {
//1.新的纹理尺寸(新纹理图片的宽高)GLsizei newTextureWidth = originWidth;GLsizei newTextureHeight = originHeight * (newHeight - (bottomY - topY)) + originHeight;//2.高度变化百分比CGFloat heightScale = newTextureHeight / originHeight;//3.在新的纹理坐标下,重新计算topY、bottomYCGFloat 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 生成纹理IDglGenTextures(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_ATTACHMENT0textarget: 指定纹理目标, 符合常量:GL_TEXTURE_2Dteture: 指定要附加图像的纹理对象;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:指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0glVertexAttribPointer(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:指定一个指针,指向数组中的第一个顶点属性的第一个组件。默认为0glVertexAttribPointer(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 的方法重新绘制一遍,刚好翻转过来//创建一个图片contextUIGraphicsBeginImageContext(CGSizeMake(width, height));CGContextRef context = UIGraphicsGetCurrentContext();//将图片绘制上去CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);//从context中获取图片UIImage *image = UIGraphicsGetImageFromCurrentImageContext();//结束图片context处理UIGraphicsEndImageContext();//释放bufferfree(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);}];
}