OpenGLES 给图片加滤镜并导出

前言

  • 最终需求:多张图片合成视频,并且给指定的图片加滤镜并设置滤镜的时间段。类似于抖音等APP里面的特效视频。

  • 阶段需求:给图片加滤镜并导出图片,这是一个非常关键的一环。尽管GPUImage框架对图片处理已经支持的很好了,但文本并没有使用GPUImage来处理。而是用OpenGL ES一步一步来实现,通过这个过程可以了解缓冲区的原理。

  • 文章目的:这篇文章的主要目的是了解缓冲区的基本知识,以及缓冲区如何工作的,缓冲区与纹理单元直接的关系等。文本有许多内容来源于《OpenGL超级宝典 · 第5版》中的第8章,这本书对于OpenGL的学习非常有帮助,强烈推荐。

  • 画外音:如果对OpenGL没有一定的了解,建议先看作者的前几篇文章,先了解OpenGL基本知识,不然这篇文章将看的一头雾水。

一、缓冲区介绍

  • 什么是缓冲区
  • 缓冲区的使用
  • 帧缓冲区,摆脱窗口的限制
  • 纹理与片段着色器

1.什么是缓冲区

缓冲区对象是一个强大的概念,它允许应用程序快速方便地将数据从一个渲染管线移动到另一个渲染管线,以及从一个对象绑定到另一个对象。帧缓冲区对象使我们获得了对像素的真正控制。在OpenGL有缓冲区对象之前,应用程序只有有限的选择可以在GPU中存储数据。
摘自:《OpenGL超级宝典 · 第5版》

缓冲区存储在GPU中,它能够保存顶点数据、像素数据、纹理数据、着色器处理的输入,或者不同着色器阶段的输出。

2.缓冲区的使用

  • 与创建有关的API几乎都以glGen开头
  • 与绑定有关的API几乎都已glBind开头

a.创建缓冲区

Gluint pixBufferObj;
glGenBuffers(1, & pixBufferObj);

b.绑定缓冲区

这里有一个新的概念,绑定点,如图1.1
绑定点
/// GL_PIXEL_PACK_BUFFER表示绑定到某种类型的缓冲区上(个人理解)
glBindBuffer(GL_PIXEL_PACK_BUFFER, pixBufferObj);
/// 删除缓冲区
glDeleteBuffer(1, pixBufferObj);

c.填充缓冲区
有时候我们需要在创建完缓冲区后写入数据,或者仅仅只是为了开辟内存,都需要填充缓冲区。填充的数据根据缓冲区绑定点确定是否是必须要写入数据。例如,纹理单元就不用写入实际数据。

/// pixelDataSize,数据大小
/// pixelData填充的数据,有些绑定点可以传空,GL_TEXTURE_2D
/// GL_DAYNAMIC_COPY 缓冲区对象的使用方式,
glBufferData(GL_PIXEL_PACK_BUFFER, pixelDataSize, pixelData, GL_DAYNAMIC_COPY);

3.帧缓冲区,摆脱窗口的限制

帧缓冲区是本文的重点,这里进行详细讲解,后面全部用FBO表示帧缓冲区,全称是FrameBufferObject。

虽然帧缓冲区的名称中包含一个“缓冲区”字眼,但是其实他们根本不是缓冲区。实际上,并不存在与一个帧缓冲区对象相关联的真正内存存储空间。相反,帧缓冲区对象是一种容器,它可以保存其他确实有内存存储并且可以进行渲染的对象,例如纹理或渲染缓冲区。采用这种方式,帧缓冲区对象能够在保存OpenGL管线的输出时将需要的状态和表面绑定到一起。
摘自:《OpenGL超级宝典 · 第5版》

用oc中的数组来描述,就是数组存放的是指针,然后指针指向的那块内存才是真正存储数据的区域,而数组本身并没有存储数据。这样更容易理解帧缓冲区,所以帧缓冲区创建之后是一个空的,里面啥都没有。下面用代码来解释。

/// 创建帧缓冲区
glGenFramebuffers(1, &_frameBuffer);
/// 绑定帧缓冲区
glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
/// 创建纹理单元
glGenTextures(1, &_texture);
/// 绑定纹理单元
glBindTexture(GL_TEXTURE_2D, _texture);
//将纹理绑定到FBO,这里就相当于数组里面添加一个对象指针了,这个对象指针就是纹理单元_texture
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0);

渲染缓冲区与FBO的绑定这里不作说明,之前的文章中已经有了,而渲染缓冲区因为绑定了可视化图层layer,所以它可以直接将像素数据输出到屏幕上。而单纯的用纹理缓冲区TBO只能写入数据,输出还得用到glReadPixels,后面将提到。

4.纹理与片段着色器

为什么要将纹理与片段着色器关联起来,是因为在片段着色器中有个取色器与纹理单元有关。在OpenGL ES中纹理单元有32个,GL_TEXTURE0-GL_TEXTURE21,片段着色器中取色器sampler2D是uniform属性,它可以从外部传值进去,针对当前所激活的纹理单元,需要使用对应的sampler2D值。例如,当前激活了GL_TEXTURE1,则需要对uniform sampler2D colorMap中的colorMap设置成1,这样片段着色器才会对该纹理进行渲染。下面看代码:

/// 先激活GL_TEXTURE1
glActiveTexture(GL_TEXTURE1);
glGenTextures(1, &_texture);
glBindTexture(GL_TEXTURE_2D, _texture);
/// 加载纹理到_texture,spriteData是图片数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
/// 设置sampler2D的值
glUniform1i(glGetUniformLocation(self.program, "colorMap"), 1);

看到这里,相信不理解的小伙伴仍然是一头雾水,不过没关系,下面开始真正的对图片加滤镜并输出的操作。

二、图片加滤镜并从缓冲区读取像素数据

我们先看下大概的流程,该小节主要以代码和注释的方式进行说明。

  • 创建上下文
  • 创建帧缓冲区
  • 初始化着色器程序
  • 创建纹理缓冲区并绑定到帧缓冲区
  • 加载纹理
  • 渲染(就是加滤镜)
  • 输出并生成图片

a.创建上下文

- (void)initContext{
    self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
    if (self.context) {
        [EAGLContext setCurrentContext:self.context];
    }
}

b.创建帧缓冲区

- (void)initFrameBuffer{
    glDeleteFramebuffers(1, &_frameBuffer);
    _frameBuffer = 0;
    glGenFramebuffers(1, &_frameBuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
}

c.初始化着色器程序

- (void)initProgram{
    NSString *vFile = [[NSBundle mainBundle] pathForResource:@"gray" ofType:@"vsh"];
    NSString *fFile = [[NSBundle mainBundle] pathForResource:@"gray" ofType:@"fsh"];
    /// 着色器程序被封装了一下,文章末尾会有demo地址
    self.mfProgram = [[MFGLProgram alloc] initWithVerFile:vFile fragFile:fFile];
    [self.mfProgram linkUseProgram];
}

d.创建纹理缓冲区并绑定到帧缓冲区

- (void)setTextureSize:(CGSize)size{
    /// 图片存放在 Assets中,读出来的图片宽高是实际图片宽高的1/2,所以这里需要放大2倍
    _size = CGSizeMake(size.width*2, size.height*2);
    [self generateTexture];
}
- (void)generateTexture{
    
    glActiveTexture(GL_TEXTURE1);
    glGenTextures(1, &_texture);
    glBindTexture(GL_TEXTURE_2D, _texture);
    
    // 载入纹理数据
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (int)_size.width, (int)_size.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

    //将纹理绑定到FBO
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0);
    
    GLenum err = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if (err != GL_FRAMEBUFFER_COMPLETE) {
        NSLog(@"frame buffer error %u", err);
    }else{
        NSLog(@"frame buffer success");
    }
    // 不加,则glReadPixels读取不到数据
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

e.加载纹理

- (void)readTextureForImage:(UIImage *)image{
    
    // 1.将UIimage转成 CGImageRef
    CGImageRef spriteImage = image.CGImage;
    if (!spriteImage) {
        NSLog(@"fail load image %@", image);
        exit(1);
    }
    
    size_t width = CGImageGetWidth(spriteImage);
    size_t height = CGImageGetHeight(spriteImage);
    
    // 获取图片字节数 宽 x 高 x 4 (RGBA)
    GLubyte *spriteData = (GLubyte *)calloc(width*height*4, sizeof(GLubyte));
    
    // 创建上下文
    /*
    参数1:data,指向要渲染的绘制图像的内存地址
    参数2:width,bitmap的宽度,单位为像素
    参数3:height,bitmap的高度,单位为像素
    参数4:bitPerComponent,内存中像素的每个组件的位数,比如32位RGBA,就设置为8
    参数5:bytesPerRow,bitmap的每一行的内存所占的比特数
    参数6:colorSpace,bitmap上使用的颜色空间  kCGImageAlphaPremultipliedLast:RGBA
    */
    CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
    
    // 在CGContextRef 上将图片绘制出来
    CGRect rect = CGRectMake(0, 0, width, height);
    CGContextDrawImage(spriteContext, rect, spriteImage);
    CGContextRelease(spriteContext);
    // 将纹理绑定到指定的纹理ID上
    glBindTexture(GL_TEXTURE_2D, _texture);
    
    // 设置纹理属性
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    
    float fw = width, fh = height;
    
    // 载入纹理数据
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
    
    GLenum err = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if (err != GL_FRAMEBUFFER_COMPLETE) {
        NSLog(@"frame buffer error %u", err);
    }else{
        NSLog(@"frame buffer success");
    }
    
    // 释放
    free(spriteData);
}

f.渲染(就是加滤镜)

- (void)render{
    float width = _size.width;
    float height = _size.height;
    
    glViewport(0, 0, (int)width, (int)height);
    
    glClearColor(0.5, 0.5, 0.5, 1);
    glClear(GL_COLOR_BUFFER_BIT);
    
    float sub = 1.0;
    
    GLfloat points[] = {
        -sub,  sub, 0,
        -sub, -sub, 0,
         sub, -sub, 0,
         sub,  sub, 0,
    };
    
    GLfloat textCoors[] = {
        0, 0,
        0, 1,
        1, 1,
        1, 0,
    };
    
    // 激活_frameBuffer缓冲区
    glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
    glBindTexture(GL_TEXTURE_2D, _texture);
    /// 因为在generateTexture方法中激活了GL_TEXTURE1,所以这里需要传1,如果激活的是GL_TEXTURE2,这里传2
    [self.mfProgram letSample:"colorMap" useTexture:1];
    
    [self.mfProgram useLocationAttribute:"position" perReadCount:3 points:points];
    
    [self.mfProgram useLocationAttribute:"vTextCoor" perReadCount:2 points:textCoors];
    
    // 绘图
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
    
}

g.输出并生成图片

- (UIImage *)getProcessImage{
    
    __block CGImageRef cgImageFromBytes;
    
    NSUInteger totalBytesForImage = (int)_size.width * (int)_size.height * 4;
    
    GLubyte *rawImagePixels;
    CGDataProviderRef dataProvider = NULL;
    
    rawImagePixels = (GLubyte *)malloc(totalBytesForImage);
    
    glReadPixels(0, 0, (int)_size.width, (int)_size.height, GL_RGBA, GL_UNSIGNED_BYTE, rawImagePixels);
    dataProvider = CGDataProviderCreateWithData(NULL, rawImagePixels, totalBytesForImage, NULL);
    
    CGColorSpaceRef defaultRGBColorSpace = CGColorSpaceCreateDeviceRGB();
    
    cgImageFromBytes = CGImageCreate((int)_size.width, (int)_size.height, 8, 32, 4 * (int)_size.width, defaultRGBColorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaLast, dataProvider, NULL, NO, kCGRenderingIntentDefault);
    
    CGDataProviderRelease(dataProvider);
    CGColorSpaceRelease(defaultRGBColorSpace);
    
    return [UIImage imageWithCGImage:cgImageFromBytes];
}

h.外部调用

imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 100, width, width)];
[self.view addSubview:imageView];
    
UIImage *inputImage = [UIImage imageNamed:@"video_demo_8"];
        
OutputFilterImageManager *filterManager = [OutputFilterImageManager new];
[filterManager setTextureSize:inputImage.size];
[filterManager setImage:inputImage];
[filterManager render];
    
UIImage *processImage = [filterManager getProcessImage];
imageView.image = processImage;

以上就是完整的流程了,如果还没有理解的,可以直接运行文章末尾的demo试试。

总结

每次到总结部分,我都会感到非常愉快,因为文章终于要结束了,意味着又向前了一步。在完成渲染并且输出像素数据的过程中有几个点需要关心的。

  • 纹理单元与片段着色器,激活的是哪个纹理单元,片段着色器中的sampler2D就需要设置成几,这样它才会修改对应的纹理缓冲区中的像素数据了。
  • 纹理绑定到FBO之后,需要将帧缓冲区绑定到默认的帧缓冲区上,也就是glBindFramebuffer(GL_FRAMEBUFFER, 0)。至于理由,暂时不是很清楚,只知道不加会出问题。
  • 通过这种方式生成的图片是正向的,并没有上下颠倒,所以不需要对顶点进行特殊处理。
    最后附上demo地址
    祝生活愉快!!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272