OpenGL ES 綜合案例14:大長腿

OpenGL + OpenGL ES +Metal 系列文章匯總

本案例的目的在于理解大長腿效果的實(shí)現(xiàn)以及圖片的保存

操作流程如下


大長腿操作流程

最終的效果圖如下


效果圖

準(zhǔn)備工作

準(zhǔn)備工作主要有3部分

  • 主控制器UI界面邏輯:主要是一些控件的操作
  • 自定義的GLKView(LongLegView):用于顯示 & 更新紋理圖片
  • 兩個(gè)封裝的工具類
    • LongLegVertexAttribArrayBuffer:緩存區(qū)初始化&更新、準(zhǔn)備繪制及繪制的封裝
    • LongLegHelper:著色器編譯及連接的封裝

這部分內(nèi)容,將不作說明,如有疑問,請參考文末完整代碼

大長腿實(shí)現(xiàn) & 圖片保存

實(shí)現(xiàn)的功能模塊主要分為三部分

  • 第一次加載圖片
  • 拉伸圖片
  • 圖片保存

下圖為三部分的具體實(shí)現(xiàn)流程


大長腿整體實(shí)現(xiàn)流程

第一次加載圖片

第一次圖片的加載是使用GLKit加載,利用自定義的GLKView視圖,通過計(jì)算圖片的頂點(diǎn)數(shù)據(jù),繪制圖片并顯示到屏幕上,整體的流程如圖所示


第一次加載流程

主要分為兩部分

  • 初始化視圖:為紋理的加載做準(zhǔn)備工作
  • 加載圖片:view上加載未拉伸的原圖

初始化視圖
這部分內(nèi)容主要是初始化頂點(diǎn)數(shù)組、上下文以及頂點(diǎn)數(shù)組緩存區(qū),需要在加載圖片之前做好準(zhǔn)備

view的初始化流程

加載圖片
主要是使用GLKit加載原圖,有以下幾步

viewDidLoad函數(shù)流程

  • 設(shè)置上下滑桿按鈕
  • 設(shè)置zidingView的代理
  • 加載圖片
  • 設(shè)置初始化的拉伸區(qū)域

其中,加載圖片的流程如圖所示


updateImage函數(shù)流程

在圖片繪制之前,還需要通過原圖的size以及設(shè)定的view的大小,計(jì)算紋理的頂點(diǎn)數(shù)據(jù),其計(jì)算流程如下


頂點(diǎn)數(shù)據(jù)計(jì)算流程

這里需要介紹下以下計(jì)算

目前圖中數(shù)據(jù)是已經(jīng)拉伸后的數(shù)據(jù),但在第一次加載時(shí),newHeight、startY、endY都為0

  • 計(jì)算圖片的寬高比:根據(jù)已知的圖片大小和view的大小計(jì)算


    寬高比計(jì)算原理
  • 計(jì)算拉伸量 = (newHeight - (endY-startY)) * 紋理高度,即換算成紋理的拉伸量


    拉伸量計(jì)算原理
  • 計(jì)算紋理坐標(biāo):根據(jù)傳入的開始位置和結(jié)束位置的紋理坐標(biāo)計(jì)算


    紋理坐標(biāo)計(jì)算圖示
  • 計(jì)算頂點(diǎn)坐標(biāo):需要先將傳入的開始和結(jié)束的紋理坐標(biāo)換換算為頂點(diǎn)坐標(biāo)
    在計(jì)算頂點(diǎn)坐標(biāo)之前,需要計(jì)算出開始位置和結(jié)束位置的坐標(biāo)


    拉伸區(qū)域開始結(jié)束位置頂點(diǎn)計(jì)算原理

然后根據(jù)開始坐標(biāo)和結(jié)束位置坐標(biāo)計(jì)算8個(gè)點(diǎn)的頂點(diǎn)


頂點(diǎn)計(jì)算圖示
  • 繪制
    調(diào)用GLKView的display方法,系統(tǒng)會自動回調(diào)GLKViewDelagate的代碼方法glkView: drawInRect,具體的繪制流程如下
    繪制流程

拉伸圖片

大長腿的拉伸,主要是通過兩部分控制的,首先需要通過上下滑塊選定需要拉伸的范圍,其次需要滑動slider來決定拉伸范圍是拉長還是縮短

滑塊調(diào)整
這部分就是通過兩個(gè)下上滑桿,確定拉伸范圍,主要流程如下

滑塊調(diào)整流程

圖片拉伸過程
確定拉伸范圍后,需要通過操作slider來進(jìn)行圖片的拉伸,滑動slider會改變slder的值,繼而回調(diào)slider的值改變方法,其實(shí)現(xiàn)流程吐下

圖片拉伸流程

在拉伸時(shí),需要根據(jù)拉伸區(qū)域以及slder的值重新計(jì)算紋理的頂點(diǎn)數(shù)據(jù),并重新繪制顯示到屏幕上,這部分的計(jì)算與前面提及的計(jì)算原理是一致的,請參考前文計(jì)算原理

圖片保存

前兩兩部分將的都是紋理圖片的顯示,且都是通過GLKit框架顯示的,接下來我們需要說明的是如何存儲拉伸后的紋理圖片,這里就需要用到GLSL自定義的著色器來幫助我們完成圖片的存儲,這里的保存實(shí)際是利用context重繪來實(shí)現(xiàn)的,實(shí)現(xiàn)流程如下


圖片保存到相冊的整體流程

由上圖可知,圖片的保存主要分為四部分

  • 獲取處理后的圖片


    createResult函數(shù)流程
//從幀緩存區(qū)中獲取紋理圖片文件; 獲取當(dāng)前的渲染結(jié)果
- (UIImage *)createResult {
    
//    1、根據(jù)屏幕顯示的圖片,重新獲取頂點(diǎn) & 紋理坐標(biāo)
//    拉伸--顯示:baseEffect、圖片獲取--存儲:GLSL
//    :頂點(diǎn)&紋理坐標(biāo)--GLSL繪制圖片--幀緩存區(qū)--紋理(即新的圖片),當(dāng)次處理的結(jié)果作為下一次處理的初始圖片
    [self resetTextureWithOriginWidth:self.currentImageSize.width originHeight:self.currentImageSize.height topY:self.currentTextureStartY bottomY:self.currentTextureEndY newHeight:self.currentNewHeight];
    
//    2、綁定幀緩存區(qū)
    glBindBuffer(GL_FRAMEBUFFER, self.tmpFrameBuffer);
    
//    3、獲取新的圖片size
    CGSize imageSize = [self newImageSize];
    
//    4、從幀緩存區(qū)中獲取拉伸后的圖片
    UIImage *image = [self imageFromTextureWithWidth:imageSize.width height:imageSize.height];
    
//    5、將幀緩存區(qū)綁定0,清空
    glBindBuffer(GL_FRAMEBUFFER, 0);
    
//    6、返回拉伸后的圖片
    return image;
}
  • 根據(jù)屏幕上的顯示,重新計(jì)算頂點(diǎn)數(shù)據(jù),為重繪提供數(shù)據(jù)支持
    主要分為兩部分
    • 計(jì)算頂點(diǎn)數(shù)據(jù):這部分的計(jì)算邏輯與calculateOriginTextureCoordWithTextureSize函數(shù)中邏輯是一致的
    • 通過GLSL將頂點(diǎn)數(shù)據(jù)渲染成一張新的紋理圖片,并存儲到幀緩存區(qū)

其實(shí)渲染到紋理的過程就是所謂的濾鏡鏈,即當(dāng)次處理的結(jié)果作為下一次處理的初始圖片:
1、獲取頂點(diǎn)&紋理坐標(biāo)
2、通過GLSL利用1中的數(shù)據(jù)繪制圖片
3、將圖片存儲到幀緩存區(qū)
4、從幀緩存區(qū)獲取的新圖片作為紋理,即為下一次處理的初始圖片

resetTextureWithOriginWidth函數(shù)流程
/**
 根據(jù)當(dāng)前屏幕上的顯示,來重新創(chuàng)建紋理
 
 @param originWidth 紋理的原始實(shí)際寬度
 @param originHeight 紋理的原始實(shí)際高度
 @param topY 0 ~ 1,拉伸區(qū)域的頂邊的縱坐標(biāo)
 @param bottomY 0 ~ 1,拉伸區(qū)域的底邊的縱坐標(biāo)
 @param newHeight 0 ~ 1,拉伸區(qū)域的新高度
 */
- (void)resetTextureWithOriginWidth:(CGFloat)originWidth
                       originHeight:(CGFloat)originHeight
                               topY:(CGFloat)topY
                            bottomY:(CGFloat)bottomY
                          newHeight:(CGFloat)newHeight {
   //1.新的紋理尺寸(新紋理圖片的寬高)
   GLsizei newTextureWidth = originWidth;
   GLsizei newTextureHeight = originHeight * (newHeight - (bottomY - topY)) + originHeight;
   
   //2.高度變化百分比
   CGFloat heightScale = newTextureHeight / originHeight;
   
   //3.在新的紋理坐標(biāo)下,重新計(jì)算topY、bottomY
   CGFloat newTopY = topY / heightScale;
   CGFloat newBottomY = (topY + newHeight) / heightScale;
   
   //4.創(chuàng)建頂點(diǎn)數(shù)組與紋理數(shù)組(邏輯與calculateOriginTextureCoordWithTextureSize 中關(guān)于紋理坐標(biāo)以及頂點(diǎn)坐標(biāo)邏輯是一模一樣的)
   SenceVertex *tmpVertices = malloc(sizeof(SenceVertex) * kVerticesCount);
   tmpVertices[0] = (SenceVertex){{-1, 1, 0}, {0, 1}};
   tmpVertices[1] = (SenceVertex){{1, 1, 0}, {1, 1}};
   tmpVertices[2] = (SenceVertex){{-1, -2 * newTopY + 1, 0}, {0, 1 - topY}};
   tmpVertices[3] = (SenceVertex){{1, -2 * newTopY + 1, 0}, {1, 1 - topY}};
   tmpVertices[4] = (SenceVertex){{-1, -2 * newBottomY + 1, 0}, {0, 1 - bottomY}};
   tmpVertices[5] = (SenceVertex){{1, -2 * newBottomY + 1, 0}, {1, 1 - bottomY}};
   tmpVertices[6] = (SenceVertex){{-1, -1, 0}, {0, 0}};
   tmpVertices[7] = (SenceVertex){{1, -1, 0}, {1, 0}};
   
   
   ///下面開始渲染到紋理的流程(將結(jié)果渲染成一張新的紋理圖片)
   
   //1. 生成幀緩存區(qū);
   GLuint frameBuffer;
   GLuint texture;
   //glGenFramebuffers 生成幀緩存區(qū)對象名稱;
   glGenFramebuffers(1, &frameBuffer);
   //glBindFramebuffer 綁定一個(gè)幀緩存區(qū)對象;
   glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
   
   //2. 生成紋理ID,綁定紋理;
   //glGenTextures 生成紋理ID
   glGenTextures(1, &texture);
   //glBindTexture 將一個(gè)紋理綁定到紋理目標(biāo)上;
   glBindTexture(GL_TEXTURE_2D, texture);
   //glTexImage2D 指定一個(gè)二維紋理圖像;
   glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, newTextureWidth, newTextureHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
   
   //3. 設(shè)置紋理相關(guān)參數(shù)
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
   glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
   
   //4. 將紋理圖像加載到幀緩存區(qū)對象上;
//    幀緩存區(qū) 可以附著 渲染緩存區(qū),還可以加載紋理對象
   /*
    glFramebufferTexture2D (GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level)
    target: 指定幀緩沖目標(biāo),符合常量必須是GL_FRAMEBUFFER;
    attachment: 指定附著紋理對象的附著點(diǎn)GL_COLOR_ATTACHMENT0
    textarget: 指定紋理目標(biāo), 符合常量:GL_TEXTURE_2D
    teture: 指定要附加圖像的紋理對象;
    level: 指定要附加的紋理圖像的mipmap級別,該級別必須為0。
    */
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
   
   //5. 設(shè)置視口尺寸
   glViewport(0, 0, newTextureWidth, newTextureHeight);
   
   //6. 獲取著色器程序
   GLuint program = [LongLegHelper programWithShaderName:@"spring"];
   glUseProgram(program);
   
   //7. 獲取傳遞數(shù)據(jù)的入口
   GLuint positionSlot = glGetAttribLocation(program, "Position");
   GLuint textureSlot = glGetUniformLocation(program, "Texture");
   GLuint textureCoordsSlot = glGetAttribLocation(program, "TextureCoords");
   
   //8. 傳值,即傳遞紋理ID
   glActiveTexture(GL_TEXTURE0);
   glBindTexture(GL_TEXTURE_2D, self.baseEffect.texture2d0.name);
   glUniform1i(textureSlot, 0);
   
   //9.初始化緩存區(qū),即創(chuàng)建VBO
   LongLegVertexAttribArrayBuffer *vbo = [[LongLegVertexAttribArrayBuffer alloc] initWithAttribStride:sizeof(SenceVertex) numberOfVertices:kVerticesCount data:tmpVertices usage:GL_STATIC_DRAW];
   
   //10.準(zhǔn)備繪制,將紋理/頂點(diǎn)坐標(biāo)傳遞進(jìn)去;
//    頂點(diǎn) & 紋理坐標(biāo) -- 準(zhǔn)備繪制
   [vbo prepareToDrawWithAttrib:positionSlot numberOfCoordinates:3 attribOffset:offsetof(SenceVertex, positionCoord) shouldEnable:YES];
   [vbo prepareToDrawWithAttrib:textureCoordsSlot numberOfCoordinates:2 attribOffset:offsetof(SenceVertex, textureCoord) shouldEnable:YES];
   
   //11. 繪制
   [vbo drawArrayWithMode:GL_TRIANGLE_STRIP startVertexIndex:0 numberOfVertices:kVerticesCount];
   
   //12.解綁緩存
   glBindFramebuffer(GL_FRAMEBUFFER, 0);
   //13.釋放頂點(diǎn)數(shù)組
   free(tmpVertices);
   
   //14.保存臨時(shí)的紋理對象/幀緩存區(qū)對象;
   self.tmpTexture = texture;
   self.tmpFrameBuffer = frameBuffer;
}
  • 從幀緩存區(qū)中獲取拉伸后的圖片
    圖片存儲的本質(zhì)其實(shí)是利用上下文將幀緩存區(qū)的圖片像素點(diǎn)進(jìn)行重繪,得到一張新的圖片


    imageFromTextureWithWidth函數(shù)流程

代碼如下

// 返回某個(gè)紋理對應(yīng)的 UIImage,調(diào)用前先綁定對應(yīng)的幀緩存
- (UIImage *)imageFromTextureWithWidth:(int)width height:(int)height {
    
//    1、綁定幀緩存區(qū)
    glBindFramebuffer(GL_FRAMEBUFFER, self.tmpFrameBuffer);
    
//    2、將幀緩存區(qū)內(nèi)的圖片紋理繪制到圖片上
    //計(jì)算圖片的字節(jié)數(shù)
    int size = width * height * 4;
    GLubyte *buffer = malloc(size);
    /*
    
    glReadPixels (GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid* pixels);
    @功能: 讀取像素(理解為將已經(jīng)繪制好的像素,從顯存中讀取到內(nèi)存中;)
    @參數(shù)解讀:
    參數(shù)x,y,width,height: xy坐標(biāo)以及讀取的寬高;
    參數(shù)format: 顏色格式; GL_RGBA;
    參數(shù)type: 讀取到的內(nèi)容保存到內(nèi)存所用的格式;GL_UNSIGNED_BYTE 會把數(shù)據(jù)保存為GLubyte類型;
    參數(shù)pixels: 指針,像素?cái)?shù)據(jù)讀取后, 將會保存到該指針指向的地址內(nèi)存中;
    
    注意: pixels指針,必須保證該地址有足夠的可以使用的空間, 以容納讀取的像素?cái)?shù)據(jù); 例如一副256 * 256的圖像,如果讀取RGBA 數(shù)據(jù), 且每個(gè)數(shù)據(jù)保存在GLUbyte. 總大小就是 256 * 256 * 4 = 262144字節(jié), 即256M;
    int size = width * height * 4;
    GLubyte *buffer = malloc(size);
    */
    glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
    
    //使用data和size 數(shù)組來訪問buffer數(shù)據(jù);
    /*
     CGDataProviderRef CGDataProviderCreateWithData(void *info, const void *data, size_t size, CGDataProviderReleaseDataCallback releaseData);
     @功能: 新的數(shù)據(jù)類型, 方便訪問二進(jìn)制數(shù)據(jù);
     @參數(shù):
     參數(shù)info: 指向任何類型數(shù)據(jù)的指針, 或者為Null;
     參數(shù)data: 數(shù)據(jù)存儲的地址,buffer
     參數(shù)size: buffer的數(shù)據(jù)大小;
     參數(shù)releaseData: 釋放的回調(diào),默認(rèn)為空;
     
     */
    CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, buffer, size, NULL);
    //每個(gè)組件的位數(shù);
    int bitsPerComponent = 8;
    //像素占用的比特?cái)?shù)4 * 8 = 32;
    int bitsPerPixel = 32;
    //每一行的字節(jié)數(shù)
    int bytesPerRow = 4 * width;
    //顏色空間格式;
    CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
    //位圖圖形的組件信息 - 默認(rèn)的
    CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;
    //顏色映射
    CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;

    
//    3、將幀緩存區(qū)里的像素點(diǎn)繪制到一張圖片上:讀取的數(shù)據(jù) -- 圖片
    /*
    CGImageCreate(size_t width, size_t height,size_t bitsPerComponent, size_t bitsPerPixel, size_t bytesPerRow,CGColorSpaceRef space, CGBitmapInfo bitmapInfo, CGDataProviderRef provider,const CGFloat decode[], bool shouldInterpolate,CGColorRenderingIntent intent);
    @功能:根據(jù)你提供的數(shù)據(jù)創(chuàng)建一張位圖;
    注意:size_t 定義的是一個(gè)可移植的單位,在64位機(jī)器上為8字節(jié),在32位機(jī)器上是4字節(jié);
    參數(shù)width: 圖片的寬度像素;
    參數(shù)height: 圖片的高度像素;
    參數(shù)bitsPerComponent: 每個(gè)顏色組件所占用的位數(shù), 比如R占用8位;
    參數(shù)bitsPerPixel: 每個(gè)顏色的比特?cái)?shù), 如果是RGBA則是32位, 4 * 8 = 32位;
    參數(shù)bytesPerRow :每一行占用的字節(jié)數(shù);
    參數(shù)space:顏色空間模式,CGColorSpaceCreateDeviceRGB
    參數(shù)bitmapInfo:kCGBitmapByteOrderDefault 位圖像素布局;
    參數(shù)provider: 圖片數(shù)據(jù)源提供者, 在CGDataProviderCreateWithData ,將buffer 轉(zhuǎn)為 provider 對象;
    參數(shù)decode: 解碼渲染數(shù)組, 默認(rèn)NULL
    參數(shù)shouldInterpolate: 是否抗鋸齒;
    參數(shù)intent: 圖片相關(guān)參數(shù);kCGRenderingIntentDefault
    
    */
    CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);
    
//    4、此時(shí)的 imageRef 是上下顛倒的,調(diào)用 CG 的方法重新繪制一遍,剛好翻轉(zhuǎn)過來
    //創(chuàng)建一個(gè)圖片context
    UIGraphicsBeginImageContext(CGSizeMake(width, height));
    CGContextRef context = UIGraphicsGetCurrentContext();
    //將圖片繪制上去
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
    //從context中獲取圖片
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    //結(jié)束圖片context處理
    UIGraphicsEndImageContext();
    
    //釋放buffer
    free(buffer);
    //返回圖片
    return image;
}
  • 存儲到相冊
    將獲取的拉伸后的圖片通過蘋果自帶的Photos框架,將其存儲到系統(tǒng)相冊
// 保存圖片到相冊
- (void)saveImage:(UIImage *)image {
    //將圖片通過PHPhotoLibrary保存到系統(tǒng)相冊
    [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
        [PHAssetChangeRequest creationRequestForAssetFromImage:image];
    } completionHandler:^(BOOL success, NSError * _Nullable error) {
        NSLog(@"success = %d, error = %@ 圖片已保存到相冊", success, error);
    }];
}

完整的代碼見Github - 16_大長腿_OC

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。

友情鏈接更多精彩內(nèi)容