使用 iOS OpenGL ES 實現(xiàn)長腿功能

本文介紹了如何使用 OpenGL ES 來實現(xiàn)長腿功能皂贩。學(xué)習(xí)這個例子可以加深我們對紋理渲染流程的理解。另外道批,還會著重介紹一下「渲染到紋理」這個新知識點错洁。

警告: 本文屬于進階教程,閱讀前請確保已經(jīng)熟悉 OpenGL ES 紋理渲染的相關(guān)概念疙剑,否則強行閱讀可能導(dǎo)致走火入魔氯迂。傳送門

注: 下文中的 OpenGL ES 均指代 OpenGL ES 2.0。

一言缤、效果展示

首先來看一下最終的效果嚼蚀,這個功能簡單來說,就是實現(xiàn)了圖片的局部拉伸管挟,從邏輯上來說并不復(fù)雜轿曙。

二、思路

1僻孝、怎么實現(xiàn)拉伸

我們來回憶一下导帝,我們要渲染一張圖片,需要將圖片拆分成兩個三角形穿铆,如下所示:

如果我們想對圖片進行拉伸您单,很簡單,只需要修改一下 4 個頂點坐標的 Y 值即可悴务。

那么睹限,如果我們只想對圖片中間的部分進行拉伸譬猫,應(yīng)該怎么做呢讯檐?

其實答案也很容易想到羡疗,我們只需要修改一下圖片的拆分方式。如下所示别洪,我們把圖片拆分成了 6 個三角形叨恨,也可以說是 3 個小矩形。這樣挖垛,我們只需要對中間的小矩形做拉伸處理就可以了痒钝。

2、怎么實現(xiàn)重復(fù)調(diào)整

我們觀察上面的動態(tài)效果圖痢毒,可以看到第二次的壓縮操作送矩,是基于第一次的拉伸操作的結(jié)果來進行的。因此哪替,在每一步我們都需要拿到上一步的結(jié)果栋荸,作為原始圖,進行再次調(diào)整凭舶。

這里的「原始圖」就是一個紋理晌块。換句話說,我們需要將每一次的調(diào)整結(jié)果帅霜,都重新生成一個紋理匆背,供下次調(diào)整的時候使用。

這一步是本文的重點身冀,我們會通過「渲染到紋理」的方式來實現(xiàn)钝尸,具體的步驟我們在后面會詳細介紹。

三搂根、為什么要使用 OpenGL ES

可能有人會說:你這個功能平平無奇蝶怔,就算不懂 OpenGL ES,我用其它方式也能實現(xiàn)呀兄墅。

確實踢星,在 iOS 中,我們繪圖一般是使用 CoreGraphics隙咸。假設(shè)我們使用 CoreGraphics沐悦,也按照上面的實現(xiàn)思路,對原圖進行拆分繪制五督,重復(fù)調(diào)整的時候進行重新拼接藏否,目測也是能實現(xiàn)相同的功能。

但是充包,由于 CoreGraphics 繪圖依賴于 CPU副签,當(dāng)我們在調(diào)節(jié)拉伸區(qū)域的時候遥椿,需要不斷地進行重繪,此時 CPU 的占用必然會暴漲淆储,從而引起卡頓冠场。而使用 OpenGL ES 則不存在這樣的問題。

四本砰、實現(xiàn)拉伸邏輯

從上面我們知道碴裙,渲染圖片我們需要 8 個頂點,而拉伸邏輯的關(guān)鍵就是頂點坐標的計算点额,在拿到計算結(jié)果后再重新渲染舔株。

計算頂點的關(guān)鍵步驟如下:

/**
 根據(jù)當(dāng)前控件的尺寸和紋理的尺寸,計算初始紋理坐標

 @param size 原始紋理尺寸
 @param startY 中間區(qū)域的開始縱坐標位置 0~1
 @param endY 中間區(qū)域的結(jié)束縱坐標位置 0~1
 @param newHeight 新的中間區(qū)域的高度
 */
- (void)calculateOriginTextureCoordWithTextureSize:(CGSize)size
                                            startY:(CGFloat)startY
                                              endY:(CGFloat)endY
                                         newHeight:(CGFloat)newHeight {
    CGFloat ratio = (size.height / size.width) *
                    (self.bounds.size.width / self.bounds.size.height);
    CGFloat textureWidth = self.currentTextureWidth;
    CGFloat textureHeight = textureWidth * ratio;
    
    // 拉伸量
    CGFloat delta = (newHeight - (endY -  startY)) * textureHeight;
    
    // 判斷是否超出最大值
    if (textureHeight + delta >= 1) {
        delta = 1 - textureHeight;
        newHeight = delta / textureHeight + (endY -  startY);
    }
    
    // 紋理的頂點
    GLKVector3 pointLT = {-textureWidth, textureHeight + delta, 0};  // 左上角
    GLKVector3 pointRT = {textureWidth, textureHeight + delta, 0};  // 右上角
    GLKVector3 pointLB = {-textureWidth, -textureHeight - delta, 0};  // 左下角
    GLKVector3 pointRB = {textureWidth, -textureHeight - delta, 0};  // 右下角
    
    // 中間矩形區(qū)域的頂點
    CGFloat startYCoord = MIN(-2 * textureHeight * startY + textureHeight, textureHeight);
    CGFloat endYCoord = MAX(-2 * textureHeight * endY + textureHeight, -textureHeight);
    GLKVector3 centerPointLT = {-textureWidth, startYCoord + delta, 0};  // 左上角
    GLKVector3 centerPointRT = {textureWidth, startYCoord + delta, 0};  // 右上角
    GLKVector3 centerPointLB = {-textureWidth, endYCoord - delta, 0};  // 左下角
    GLKVector3 centerPointRB = {textureWidth, endYCoord - delta, 0};  // 右下角
    
    // 紋理的上面兩個頂點
    self.vertices[0].positionCoord = pointLT;
    self.vertices[0].textureCoord = GLKVector2Make(0, 1);
    self.vertices[1].positionCoord = pointRT;
    self.vertices[1].textureCoord = GLKVector2Make(1, 1);
    // 中間區(qū)域的4個頂點
    self.vertices[2].positionCoord = centerPointLT;
    self.vertices[2].textureCoord = GLKVector2Make(0, 1 - startY);
    self.vertices[3].positionCoord = centerPointRT;
    self.vertices[3].textureCoord = GLKVector2Make(1, 1 - startY);
    self.vertices[4].positionCoord = centerPointLB;
    self.vertices[4].textureCoord = GLKVector2Make(0, 1 - endY);
    self.vertices[5].positionCoord = centerPointRB;
    self.vertices[5].textureCoord = GLKVector2Make(1, 1 - endY);
    // 紋理的下面兩個頂點
    self.vertices[6].positionCoord = pointLB;
    self.vertices[6].textureCoord = GLKVector2Make(0, 0);
    self.vertices[7].positionCoord = pointRB;
    self.vertices[7].textureCoord = GLKVector2Make(1, 0);
}

五还棱、渲染到紋理

上面提到:我們需要將每一次的調(diào)整結(jié)果载慈,都重新生成一個紋理,供下次調(diào)整的時候使用珍手。

出于對結(jié)果分辨率的考慮办铡,我們不會直接讀取當(dāng)前屏幕渲染結(jié)果對應(yīng)的幀緩存,而是采取「渲染到紋理」的方式珠十,重新生成一個寬度與原圖一致的紋理料扰。

這是為什么呢?

假設(shè)我們有一張 1000 X 1000 的圖片焙蹭,而屏幕上的控件大小是 100 X 100 晒杈,則紋理渲染到屏幕后,渲染結(jié)果對應(yīng)的渲染緩存的尺寸也是 100 X 100 (暫不考慮屏幕密度)孔厉。如果我們這時候直接讀取屏幕的渲染結(jié)果拯钻,最多也只能讀到 100 X 100 的分辨率。

這樣會導(dǎo)致圖片的分辨率下降撰豺,所以我們會使用能保持原有分辨率的方式粪般,即「渲染到紋理」。

在這之前污桦,我們都是將紋理直接渲染到屏幕上亩歹,關(guān)鍵步驟像這樣:

GLuint renderBuffer; // 渲染緩存
GLuint frameBuffer;  // 幀緩存
    
// 綁定渲染緩存要輸出的 layer
glGenRenderbuffers(1, &renderBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer);
[self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];
    
// 將渲染緩存綁定到幀緩存上
glGenFramebuffers(1, &frameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER,
                          GL_COLOR_ATTACHMENT0,
                          GL_RENDERBUFFER,
                          renderBuffer);

我們生成了一個渲染緩存,并把這個渲染緩存掛載到幀緩存的 GL_COLOR_ATTACHMENT0 顏色緩存上凡橱,并通過 context 為當(dāng)前的渲染緩存綁定了輸出的 layer 小作。

其實,如果我們不需要在屏幕上顯示我們的渲染結(jié)果稼钩,也可以直接將數(shù)據(jù)渲染到另一個紋理上顾稀。更有趣的是,這個渲染后的結(jié)果坝撑,還可以被當(dāng)成一個普通的紋理來使用静秆。這也是我們實現(xiàn)重復(fù)調(diào)整功能的基礎(chǔ)粮揉。

具體操作如下:

// 生成幀緩存,掛載渲染緩存
GLuint frameBuffer;
GLuint texture;
    
glGenFramebuffers(1, &frameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
    
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, newTextureWidth, newTextureHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
    
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);

通過對比我們可以發(fā)現(xiàn)抚笔,這里我們用 Texture 來替換 Renderbuffer 扶认,并且同樣是掛載到 GL_COLOR_ATTACHMENT0 上,不過這里就不需要另外再綁定 layer 了塔沃。

另外蝠引,我們需要為新的紋理設(shè)置一個尺寸阳谍,這個尺寸不再受限于屏幕上控件的尺寸蛀柴,這也是新紋理可以保持原有分辨率的原因。

這時候矫夯,渲染的結(jié)果都會被保存在 texture 中鸽疾,而 texture 也可以被當(dāng)成普通的紋理來使用。

六训貌、保存結(jié)果

當(dāng)我們調(diào)整出滿意的圖片后制肮,需要把它保存下來。這里分為兩步递沪,第一步仍然是上面提到的重新生成紋理豺鼻,第二步就是把紋理轉(zhuǎn)化為圖片

第二步主要通過 glReadPixels 方法來實現(xiàn)款慨,它可以從當(dāng)前的幀緩存中讀取出紋理數(shù)據(jù)儒飒。直接上代碼:

// 返回某個紋理對應(yīng)的 UIImage,調(diào)用前先綁定對應(yīng)的幀緩存
- (UIImage *)imageFromTextureWithWidth:(int)width height:(int)height {
    int size = width * height * 4;
    GLubyte *buffer = malloc(size);
    glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
    CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, buffer, size, NULL);
    int bitsPerComponent = 8;
    int bitsPerPixel = 32;
    int bytesPerRow = 4 * width;
    CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
    CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;
    CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
    CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);
    
    // 此時的 imageRef 是上下顛倒的檩奠,調(diào)用 CG 的方法重新繪制一遍桩了,剛好翻轉(zhuǎn)過來
    UIGraphicsBeginImageContext(CGSizeMake(width, height));
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    
    free(buffer);
    return image;
}

至此,我們已經(jīng)拿到了 UIImage 對象埠戳,可以把它保存到相冊里了井誉。

源碼

請到 GitHub 上查看完整代碼。

參考

獲取更佳的閱讀體驗整胃,請訪問原文地址【Lyman's Blog】使用 iOS OpenGL ES 實現(xiàn)長腿功能

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末颗圣,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子屁使,更是在濱河造成了極大的恐慌在岂,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,542評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件屋灌,死亡現(xiàn)場離奇詭異洁段,居然都是意外死亡,警方通過查閱死者的電腦和手機共郭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評論 3 385
  • 文/潘曉璐 我一進店門祠丝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來疾呻,“玉大人,你說我怎么就攤上這事写半“段希” “怎么了?”我有些...
    開封第一講書人閱讀 158,021評論 0 348
  • 文/不壞的土叔 我叫張陵叠蝇,是天一觀的道長璃岳。 經(jīng)常有香客問我,道長悔捶,這世上最難降的妖魔是什么铃慷? 我笑而不...
    開封第一講書人閱讀 56,682評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮蜕该,結(jié)果婚禮上犁柜,老公的妹妹穿的比我還像新娘。我一直安慰自己堂淡,他們只是感情好馋缅,可當(dāng)我...
    茶點故事閱讀 65,792評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著绢淀,像睡著了一般萤悴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上皆的,一...
    開封第一講書人閱讀 49,985評論 1 291
  • 那天覆履,我揣著相機與錄音,去河邊找鬼祭务。 笑死内狗,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的义锥。 我是一名探鬼主播柳沙,決...
    沈念sama閱讀 39,107評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼拌倍!你這毒婦竟也來了赂鲤?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,845評論 0 268
  • 序言:老撾萬榮一對情侶失蹤柱恤,失蹤者是張志新(化名)和其女友劉穎数初,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體梗顺,經(jīng)...
    沈念sama閱讀 44,299評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡泡孩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,612評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了寺谤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片仑鸥。...
    茶點故事閱讀 38,747評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡吮播,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出眼俊,到底是詐尸還是另有隱情意狠,我是刑警寧澤,帶...
    沈念sama閱讀 34,441評論 4 333
  • 正文 年R本政府宣布疮胖,位于F島的核電站环戈,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏澎灸。R本人自食惡果不足惜院塞,卻給世界環(huán)境...
    茶點故事閱讀 40,072評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望击孩。 院中可真熱鬧迫悠,春花似錦鹏漆、人聲如沸巩梢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽括蝠。三九已至,卻和暖如春饭聚,著一層夾襖步出監(jiān)牢的瞬間忌警,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評論 1 267
  • 我被黑心中介騙來泰國打工秒梳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留法绵,地道東北人。 一個月前我還...
    沈念sama閱讀 46,545評論 2 362
  • 正文 我出身青樓酪碘,卻偏偏與公主長得像朋譬,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子兴垦,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,658評論 2 350