前言
最终需求:多张图片合成视频,并且给指定的图片加滤镜并设置滤镜的时间段。类似于抖音等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.绑定缓冲区
/// 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地址。
祝生活愉快!!