前言
最終需求:多張圖片合成視頻讶舰,并且給指定的圖片加濾鏡并設(shè)置濾鏡的時間段。類似于抖音等APP里面的特效視頻。
階段需求:給圖片加濾鏡并導出圖片腊瑟,這是一個非常關(guān)鍵的一環(huán)聚假。盡管GPUImage框架對圖片處理已經(jīng)支持的很好了,但文本并沒有使用GPUImage來處理闰非。而是用OpenGL ES一步一步來實現(xiàn)膘格,通過這個過程可以了解緩沖區(qū)的原理。
文章目的:這篇文章的主要目的是了解緩沖區(qū)的基本知識财松,以及緩沖區(qū)如何工作的瘪贱,緩沖區(qū)與紋理單元直接的關(guān)系等。文本有許多內(nèi)容來源于《OpenGL超級寶典 · 第5版》中的第8章辆毡,這本書對于OpenGL的學習非常有幫助菜秦,強烈推薦。
畫外音:如果對OpenGL沒有一定的了解舶掖,建議先看作者的前幾篇文章球昨,先了解OpenGL基本知識,不然這篇文章將看的一頭霧水眨攘。
一主慰、緩沖區(qū)介紹
- 什么是緩沖區(qū)
- 緩沖區(qū)的使用
- 幀緩沖區(qū),擺脫窗口的限制
- 紋理與片段著色器
1.什么是緩沖區(qū)
緩沖區(qū)對象是一個強大的概念鲫售,它允許應(yīng)用程序快速方便地將數(shù)據(jù)從一個渲染管線移動到另一個渲染管線共螺,以及從一個對象綁定到另一個對象。幀緩沖區(qū)對象使我們獲得了對像素的真正控制情竹。在OpenGL有緩沖區(qū)對象之前藐不,應(yīng)用程序只有有限的選擇可以在GPU中存儲數(shù)據(jù)。
摘自:《OpenGL超級寶典 · 第5版》
緩沖區(qū)存儲在GPU中秦效,它能夠保存頂點數(shù)據(jù)雏蛮、像素數(shù)據(jù)、紋理數(shù)據(jù)棉安、著色器處理的輸入底扳,或者不同著色器階段的輸出。
2.緩沖區(qū)的使用
- 與創(chuàng)建有關(guān)的API幾乎都以glGen開頭
- 與綁定有關(guān)的API幾乎都已glBind開頭
a.創(chuàng)建緩沖區(qū)
Gluint pixBufferObj;
glGenBuffers(1, & pixBufferObj);
b.綁定緩沖區(qū)
/// GL_PIXEL_PACK_BUFFER表示綁定到某種類型的緩沖區(qū)上(個人理解)
glBindBuffer(GL_PIXEL_PACK_BUFFER, pixBufferObj);
/// 刪除緩沖區(qū)
glDeleteBuffer(1, pixBufferObj);
c.填充緩沖區(qū)
有時候我們需要在創(chuàng)建完緩沖區(qū)后寫入數(shù)據(jù),或者僅僅只是為了開辟內(nèi)存蒲赂,都需要填充緩沖區(qū)阱冶。填充的數(shù)據(jù)根據(jù)緩沖區(qū)綁定點確定是否是必須要寫入數(shù)據(jù)。例如滥嘴,紋理單元就不用寫入實際數(shù)據(jù)木蹬。
/// pixelDataSize,數(shù)據(jù)大小
/// pixelData填充的數(shù)據(jù),有些綁定點可以傳空若皱,GL_TEXTURE_2D
/// GL_DAYNAMIC_COPY 緩沖區(qū)對象的使用方式镊叁,
glBufferData(GL_PIXEL_PACK_BUFFER, pixelDataSize, pixelData, GL_DAYNAMIC_COPY);
3.幀緩沖區(qū)尘颓,擺脫窗口的限制
幀緩沖區(qū)是本文的重點,這里進行詳細講解晦譬,后面全部用FBO表示幀緩沖區(qū)疤苹,全稱是FrameBufferObject。
雖然幀緩沖區(qū)的名稱中包含一個“緩沖區(qū)”字眼敛腌,但是其實他們根本不是緩沖區(qū)卧土。實際上,并不存在與一個幀緩沖區(qū)對象相關(guān)聯(lián)的真正內(nèi)存存儲空間像樊。相反尤莺,幀緩沖區(qū)對象是一種容器,它可以保存其他確實有內(nèi)存存儲并且可以進行渲染的對象生棍,例如紋理或渲染緩沖區(qū)颤霎。采用這種方式,幀緩沖區(qū)對象能夠在保存OpenGL管線的輸出時將需要的狀態(tài)和表面綁定到一起足绅。
摘自:《OpenGL超級寶典 · 第5版》
用oc中的數(shù)組來描述捷绑,就是數(shù)組存放的是指針,然后指針指向的那塊內(nèi)存才是真正存儲數(shù)據(jù)的區(qū)域氢妈,而數(shù)組本身并沒有存儲數(shù)據(jù)。這樣更容易理解幀緩沖區(qū)段多,所以幀緩沖區(qū)創(chuàng)建之后是一個空的首量,里面啥都沒有。下面用代碼來解釋进苍。
/// 創(chuàng)建幀緩沖區(qū)
glGenFramebuffers(1, &_frameBuffer);
/// 綁定幀緩沖區(qū)
glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
/// 創(chuàng)建紋理單元
glGenTextures(1, &_texture);
/// 綁定紋理單元
glBindTexture(GL_TEXTURE_2D, _texture);
//將紋理綁定到FBO加缘,這里就相當于數(shù)組里面添加一個對象指針了,這個對象指針就是紋理單元_texture
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0);
渲染緩沖區(qū)與FBO的綁定這里不作說明觉啊,之前的文章中已經(jīng)有了拣宏,而渲染緩沖區(qū)因為綁定了可視化圖層layer,所以它可以直接將像素數(shù)據(jù)輸出到屏幕上杠人。而單純的用紋理緩沖區(qū)TBO只能寫入數(shù)據(jù)勋乾,輸出還得用到glReadPixels,后面將提到嗡善。
4.紋理與片段著色器
為什么要將紋理與片段著色器關(guān)聯(lián)起來辑莫,是因為在片段著色器中有個取色器與紋理單元有關(guān)。在OpenGL ES中紋理單元有32個罩引,GL_TEXTURE0-GL_TEXTURE21各吨,片段著色器中取色器sampler2D是uniform屬性,它可以從外部傳值進去袁铐,針對當前所激活的紋理單元揭蜒,需要使用對應(yīng)的sampler2D值横浑。例如,當前激活了GL_TEXTURE1屉更,則需要對uniform sampler2D colorMap中的colorMap設(shè)置成1伪嫁,這樣片段著色器才會對該紋理進行渲染。下面看代碼:
/// 先激活GL_TEXTURE1
glActiveTexture(GL_TEXTURE1);
glGenTextures(1, &_texture);
glBindTexture(GL_TEXTURE_2D, _texture);
/// 加載紋理到_texture偶垮,spriteData是圖片數(shù)據(jù)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
/// 設(shè)置sampler2D的值
glUniform1i(glGetUniformLocation(self.program, "colorMap"), 1);
看到這里张咳,相信不理解的小伙伴仍然是一頭霧水,不過沒關(guān)系似舵,下面開始真正的對圖片加濾鏡并輸出的操作脚猾。
二、圖片加濾鏡并從緩沖區(qū)讀取像素數(shù)據(jù)
我們先看下大概的流程砚哗,該小節(jié)主要以代碼和注釋的方式進行說明龙助。
- 創(chuàng)建上下文
- 創(chuàng)建幀緩沖區(qū)
- 初始化著色器程序
- 創(chuàng)建紋理緩沖區(qū)并綁定到幀緩沖區(qū)
- 加載紋理
- 渲染(就是加濾鏡)
- 輸出并生成圖片
a.創(chuàng)建上下文
- (void)initContext{
self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
if (self.context) {
[EAGLContext setCurrentContext:self.context];
}
}
b.創(chuàng)建幀緩沖區(qū)
- (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.創(chuàng)建紋理緩沖區(qū)并綁定到幀緩沖區(qū)
- (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);
// 載入紋理數(shù)據(jù)
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讀取不到數(shù)據(jù)
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
e.加載紋理
- (void)readTextureForImage:(UIImage *)image{
// 1.將UIimage轉(zhuǎn)成 CGImageRef
CGImageRef spriteImage = image.CGImage;
if (!spriteImage) {
NSLog(@"fail load image %@", image);
exit(1);
}
size_t width = CGImageGetWidth(spriteImage);
size_t height = CGImageGetHeight(spriteImage);
// 獲取圖片字節(jié)數(shù) 寬 x 高 x 4 (RGBA)
GLubyte *spriteData = (GLubyte *)calloc(width*height*4, sizeof(GLubyte));
// 創(chuàng)建上下文
/*
參數(shù)1:data,指向要渲染的繪制圖像的內(nèi)存地址
參數(shù)2:width,bitmap的寬度仅淑,單位為像素
參數(shù)3:height,bitmap的高度称勋,單位為像素
參數(shù)4:bitPerComponent,內(nèi)存中像素的每個組件的位數(shù),比如32位RGBA涯竟,就設(shè)置為8
參數(shù)5:bytesPerRow,bitmap的每一行的內(nèi)存所占的比特數(shù)
參數(shù)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);
// 設(shè)置紋理屬性
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;
// 載入紋理數(shù)據(jù)
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緩沖區(qū)
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.外部調(diào)用
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試試筐钟。
總結(jié)
每次到總結(jié)部分揩瞪,我都會感到非常愉快,因為文章終于要結(jié)束了篓冲,意味著又向前了一步李破。在完成渲染并且輸出像素數(shù)據(jù)的過程中有幾個點需要關(guān)心的。
- 紋理單元與片段著色器纹因,激活的是哪個紋理單元喷屋,片段著色器中的sampler2D就需要設(shè)置成幾,這樣它才會修改對應(yīng)的紋理緩沖區(qū)中的像素數(shù)據(jù)了瞭恰。
- 紋理綁定到FBO之后屯曹,需要將幀緩沖區(qū)綁定到默認的幀緩沖區(qū)上,也就是glBindFramebuffer(GL_FRAMEBUFFER, 0)。至于理由恶耽,暫時不是很清楚密任,只知道不加會出問題。
- 通過這種方式生成的圖片是正向的偷俭,并沒有上下顛倒浪讳,所以不需要對頂點進行特殊處理。
最后附上demo地址涌萤。
祝生活愉快Q妥瘛!