本文介紹瞭如何使用 OpenGL ES 來實現長腿功能。學習這個例子可以加深我們對紋理渲染流程的理解。另外,還會著重介紹一下「渲染到紋理」這個新知識點。
警告: 本文屬於進階教程,閱讀前請確保已經熟悉 OpenGL ES 紋理渲染的相關概念,否則強行閱讀可能導致走火入魔。傳送門
注: 下文中的 OpenGL ES 均指代 OpenGL ES 2.0。
一、效果展示
首先來看一下最終的效果,這個功能簡單來說,就是實現了圖片的區域性拉伸,從邏輯上來說並不複雜。
二、思路
1、怎麼實現拉伸
我們來回憶一下,我們要渲染一張圖片,需要將圖片拆分成兩個三角形,如下所示:
如果我們想對圖片進行拉伸,很簡單,只需要修改一下 4 個頂點座標的 Y 值即可。
那麼,如果我們只想對圖片中間的部分進行拉伸,應該怎麼做呢?
其實答案也很容易想到,我們只需要修改一下圖片的拆分方式。如下所示,我們把圖片拆分成了 6 個三角形,也可以說是 3 個小矩形。這樣,我們只需要對中間的小矩形做拉伸處理就可以了。
2、怎麼實現重複調整
我們觀察上面的動態效果圖,可以看到第二次的壓縮操作,是基於第一次的拉伸操作的結果來進行的。因此,在每一步我們都需要拿到上一步的結果,作為原始圖,進行再次調整。
這裡的「原始圖」就是一個紋理。換句話說,我們需要將每一次的調整結果,都重新生成一個紋理,供下次調整的時候使用。
這一步是本文的重點,我們會通過「渲染到紋理」的方式來實現,具體的步驟我們在後面會詳細介紹。
三、為什麼要使用 OpenGL ES
可能有人會說:你這個功能平平無奇,就算不懂 OpenGL ES,我用其它方式也能實現呀。
確實,在 iOS 中,我們繪圖一般是使用 CoreGraphics。假設我們使用 CoreGraphics,也按照上面的實現思路,對原圖進行拆分繪製,重複調整的時候進行重新拼接,目測也是能實現相同的功能。
但是,由於 CoreGraphics 繪圖依賴於 CPU,當我們在調節拉伸區域的時候,需要不斷地進行重繪,此時 CPU 的佔用必然會暴漲,從而引起卡頓。而使用 OpenGL ES 則不存在這樣的問題。
四、實現拉伸邏輯
從上面我們知道,渲染圖片我們需要 8 個頂點,而拉伸邏輯的關鍵就是頂點座標的計算,在拿到計算結果後再重新渲染。
計算頂點的關鍵步驟如下:
/**
根據當前控制元件的尺寸和紋理的尺寸,計算初始紋理座標
@param size 原始紋理尺寸
@param startY 中間區域的開始縱座標位置 0~1
@param endY 中間區域的結束縱座標位置 0~1
@param newHeight 新的中間區域的高度
*/
- (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}; // 右下角
// 中間矩形區域的頂點
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 = pointRT;
self.vertices[0].textureCoord = GLKVector2Make(1, 1);
self.vertices[1].positionCoord = pointLT;
self.vertices[1].textureCoord = GLKVector2Make(0, 1);
// 中間區域的4個頂點
self.vertices[2].positionCoord = centerPointRT;
self.vertices[2].textureCoord = GLKVector2Make(1, 1 - startY);
self.vertices[3].positionCoord = centerPointLT;
self.vertices[3].textureCoord = GLKVector2Make(0, 1 - startY);
self.vertices[4].positionCoord = centerPointRB;
self.vertices[4].textureCoord = GLKVector2Make(1, 1 - endY);
self.vertices[5].positionCoord = centerPointLB;
self.vertices[5].textureCoord = GLKVector2Make(0, 1 - endY);
// 紋理的下面兩個頂點
self.vertices[6].positionCoord = pointRB;
self.vertices[6].textureCoord = GLKVector2Make(1, 0);
self.vertices[7].positionCoord = pointLB;
self.vertices[7].textureCoord = GLKVector2Make(0, 0);
}
複製程式碼
五、渲染到紋理
上面提到:我們需要將每一次的調整結果,都重新生成一個紋理,供下次調整的時候使用。
出於對結果解析度的考慮,我們不會直接讀取當前螢幕渲染結果對應的幀快取,而是採取「渲染到紋理」的方式,重新生成一個寬度與原圖一致的紋理。
這是為什麼呢?
假設我們有一張 1000 X 1000 的圖片,而螢幕上的控制元件大小是 100 X 100 ,則紋理渲染到螢幕後,渲染結果對應的渲染快取的尺寸也是 100 X 100 (暫不考慮螢幕密度)。如果我們這時候直接讀取螢幕的渲染結果,最多也只能讀到 100 X 100 的解析度。
這樣會導致圖片的解析度下降,所以我們會使用能保持原有解析度的方式,即「渲染到紋理」。
在這之前,我們都是將紋理直接渲染到螢幕上,關鍵步驟像這樣:
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
為當前的渲染快取繫結了輸出的 layer
。
其實,如果我們不需要在螢幕上顯示我們的渲染結果,也可以直接將資料渲染到另一個紋理上。更有趣的是,這個渲染後的結果,還可以被當成一個普通的紋理來使用。這也是我們實現重複調整功能的基礎。
具體操作如下:
// 生成幀快取,掛載渲染快取
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);
複製程式碼
通過對比我們可以發現,這裡我們用 Texture
來替換 Renderbuffer
,並且同樣是掛載到 GL_COLOR_ATTACHMENT0
上,不過這裡就不需要另外再繫結 layer
了。
另外,我們需要為新的紋理設定一個尺寸,這個尺寸不再受限於螢幕上控制元件的尺寸,這也是新紋理可以保持原有解析度的原因。
這時候,渲染的結果都會被儲存在 texture
中,而 texture
也可以被當成普通的紋理來使用。
六、儲存結果
當我們調整出滿意的圖片後,需要把它儲存下來。這裡分為兩步,第一步仍然是上面提到的重新生成紋理,第二步就是把紋理轉化為圖片。
第二步主要通過 glReadPixels
方法來實現,它可以從當前的幀快取中讀取出紋理資料。直接上程式碼:
// 返回某個紋理對應的 UIImage,呼叫前先繫結對應的幀快取
- (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 是上下顛倒的,呼叫 CG 的方法重新繪製一遍,剛好翻轉過來
UIGraphicsBeginImageContext(CGSizeMake(width, height));
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
free(buffer);
return image;
}
複製程式碼
至此,我們已經拿到了 UIImage
物件,可以把它儲存到相簿裡了。
原始碼
請到 GitHub 上檢視完整程式碼。
參考
獲取更佳的閱讀體驗,請訪問原文地址【Lyman's Blog】使用 iOS OpenGL ES 實現長腿功能