OpenGLES 給圖片加濾鏡并導出

前言

  • 最終需求:多張圖片合成視頻讶舰,并且給指定的圖片加濾鏡并設(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ū)

這里有一個新的概念贡耽,綁定點衷模,如圖1.1
綁定點
/// 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妥瘛!
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末负溪,一起剝皮案震驚了整個濱河市透揣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌川抡,老刑警劉巖辐真,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異崖堤,居然都是意外死亡侍咱,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門密幔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來楔脯,“玉大人,你說我怎么就攤上這事老玛∮倌辏” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵蜡豹,是天一觀的道長。 經(jīng)常有香客問我溉苛,道長镜廉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任愚战,我火速辦了婚禮娇唯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘寂玲。我一直安慰自己塔插,他們只是感情好,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布拓哟。 她就那樣靜靜地躺著想许,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上流纹,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天糜烹,我揣著相機與錄音,去河邊找鬼漱凝。 笑死疮蹦,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的茸炒。 我是一名探鬼主播愕乎,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼壁公!你這毒婦竟也來了感论?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤贮尖,失蹤者是張志新(化名)和其女友劉穎笛粘,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體湿硝,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡薪前,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了关斜。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片示括。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖痢畜,靈堂內(nèi)的尸體忽然破棺而出垛膝,到底是詐尸還是另有隱情,我是刑警寧澤丁稀,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布吼拥,位于F島的核電站,受9級特大地震影響线衫,放射性物質(zhì)發(fā)生泄漏凿可。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一授账、第九天 我趴在偏房一處隱蔽的房頂上張望枯跑。 院中可真熱鬧,春花似錦白热、人聲如沸敛助。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽纳击。三九已至续扔,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間评疗,已是汗流浹背测砂。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留百匆,地道東北人砌些。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像加匈,于是被迫代替她去往敵國和親存璃。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353