本案例的目的在于理解大長(zhǎng)腿效果的實(shí)現(xiàn)
以及圖片的保存
準(zhǔn)備工作
準(zhǔn)備工作主要有3部分
- 主控制器UI界面邏輯:主要是一些控件的操作
- 自定義的GLKView(LongLegView):用于顯示 & 更新紋理圖片
- 兩個(gè)封裝的工具類
- LongLegVertexAttribArrayBuffer:緩存區(qū)初始化&更新抄课、準(zhǔn)備繪制及繪制的封裝
- LongLegHelper:著色器編譯及連接的封裝
大長(zhǎng)腿實(shí)現(xiàn) & 圖片保存
實(shí)現(xiàn)的功能模塊主要分為三部分
- 第一次加載圖片
- 拉伸圖片
- 圖片保存
下圖為三部分的具體實(shí)現(xiàn)流程
第一次加載圖片
第一次圖片的加載是使用GLKit加載袖迎,利用自定義的GLKView視圖,通過(guò)計(jì)算圖片的頂點(diǎn)數(shù)據(jù),繪制圖片并顯示到屏幕上尚蝌,整體的流程如圖所示
主要分為兩部分
- 初始化視圖:為紋理的加載做準(zhǔn)備工作
- 加載圖片:view上加載未拉伸的原圖
初始化視圖
這部分內(nèi)容主要是初始化頂點(diǎn)數(shù)組、上下文以及頂點(diǎn)數(shù)組緩存區(qū)旺韭,需要在加載圖片之前做好準(zhǔn)備
加載圖片
主要是使用GLKit加載原圖胶坠,有以下幾步
- 設(shè)置上下滑桿按鈕
- 設(shè)置zidingView的代理
- 加載圖片
- 設(shè)置初始化的拉伸區(qū)域
其中,加載圖片的流程如圖所示
在圖片繪制之前叔壤,還需要通過(guò)原圖的size以及設(shè)定的view的大小瞎饲,計(jì)算紋理的頂點(diǎn)數(shù)據(jù),其計(jì)算流程如下
這里需要介紹下以下計(jì)算
目前圖中數(shù)據(jù)是已經(jīng)拉伸后的數(shù)據(jù)炼绘,但在第一次加載時(shí)嗅战,newHeight、startY、endY都為0
- 計(jì)算圖片的寬高比:根據(jù)已知的圖片大小和view的大小計(jì)算
- 計(jì)算拉伸量 = (newHeight - (endY-startY)) * 紋理高度该窗,即換算成紋理的拉伸量
- 計(jì)算紋理坐標(biāo):根據(jù)傳入的開始位置和結(jié)束位置的紋理坐標(biāo)計(jì)算
- 計(jì)算頂點(diǎn)坐標(biāo):需要先將傳入的開始和結(jié)束的紋理坐標(biāo)換換算為頂點(diǎn)坐標(biāo)在計(jì)算頂點(diǎn)坐標(biāo)之前赫舒,需要計(jì)算出開始位置和結(jié)束位置的坐標(biāo)
然后根據(jù)開始坐標(biāo)和結(jié)束位置坐標(biāo)計(jì)算8個(gè)點(diǎn)的頂點(diǎn)
- 繪制
調(diào)用GLKView的display方法,系統(tǒng)會(huì)自動(dòng)回調(diào)GLKViewDelagate的代碼方法glkView: drawInRect
,具體的繪制流程如下
拉伸圖片
大長(zhǎng)腿的拉伸厌漂,主要是通過(guò)兩部分控制的,首先需要通過(guò)上下滑塊選定需要拉伸的范圍斟珊,其次需要滑動(dòng)slider來(lái)決定拉伸范圍是拉長(zhǎng)還是縮短
滑塊調(diào)整
這部分就是通過(guò)兩個(gè)下上滑桿苇倡,確定拉伸范圍,主要流程如下
圖片拉伸過(guò)程
確定拉伸范圍后囤踩,需要通過(guò)操作slider來(lái)進(jìn)行圖片的拉伸旨椒,滑動(dòng)slider會(huì)改變slder的值,繼而回調(diào)slider的值改變方法堵漱,其實(shí)現(xiàn)流程吐下
在拉伸時(shí)综慎,需要根據(jù)拉伸區(qū)域以及slder的值重新計(jì)算紋理的頂點(diǎn)數(shù)據(jù),并重新繪制顯示到屏幕上勤庐,這部分的計(jì)算與前面提及的計(jì)算原理是一致的示惊,請(qǐng)參考前文計(jì)算原理
圖片保存
前兩兩部分將的都是紋理圖片的顯示,且都是通過(guò)GLKit框架顯示的愉镰,接下來(lái)我們需要說(shuō)明的是如何存儲(chǔ)拉伸后的紋理圖片米罚,這里就需要用到GLSL自定義的著色器來(lái)幫助我們完成圖片的存儲(chǔ),這里的保存實(shí)際是利用context重繪來(lái)實(shí)現(xiàn)的丈探,實(shí)現(xiàn)流程如下
由上圖可知录择,圖片的保存主要分為四部分
- 獲取處理后的圖片
//從幀緩存區(qū)中獲取紋理圖片文件; 獲取當(dāng)前的渲染結(jié)果
- (UIImage *)createResult {
// 1、根據(jù)屏幕顯示的圖片,重新獲取頂點(diǎn) & 紋理坐標(biāo)
// 拉伸--顯示:baseEffect隘竭、圖片獲取--存儲(chǔ):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ù)中邏輯是一致的 - 通過(guò)GLSL將頂點(diǎn)數(shù)據(jù)渲染成一張新的紋理圖片霹期,并存儲(chǔ)到幀緩存區(qū)
- 計(jì)算頂點(diǎn)數(shù)據(jù):這部分的計(jì)算邏輯與
其實(shí)渲染到紋理的過(guò)程就是所謂的
濾鏡鏈
,即當(dāng)次處理的結(jié)果作為下一次處理的初始圖片:
1拯田、獲取頂點(diǎn)&紋理坐標(biāo)
2历造、通過(guò)GLSL利用1中的數(shù)據(jù)繪制圖片
3、將圖片存儲(chǔ)到幀緩存區(qū)
4船庇、從幀緩存區(qū)獲取的新圖片作為紋理吭产,即為下一次處理的初始圖片
/**
根據(jù)當(dāng)前屏幕上的顯示,來(lái)重新創(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ū)對(duì)象名稱;
glGenFramebuffers(1, &frameBuffer);
//glBindFramebuffer 綁定一個(gè)幀緩存區(qū)對(duì)象;
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ū)對(duì)象上;
// 幀緩存區(qū) 可以附著 渲染緩存區(qū),還可以加載紋理對(duì)象
/*
glFramebufferTexture2D (GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level)
target: 指定幀緩沖目標(biāo),符合常量必須是GL_FRAMEBUFFER;
attachment: 指定附著紋理對(duì)象的附著點(diǎn)GL_COLOR_ATTACHMENT0
textarget: 指定紋理目標(biāo), 符合常量:GL_TEXTURE_2D
teture: 指定要附加圖像的紋理對(duì)象;
level: 指定要附加的紋理圖像的mipmap級(jí)別按厘,該級(jí)別必須為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í)的紋理對(duì)象/幀緩存區(qū)對(duì)象;
self.tmpTexture = texture;
self.tmpFrameBuffer = frameBuffer;
}
- 從幀緩存區(qū)中獲取拉伸后的圖片
圖片存儲(chǔ)的本質(zhì)其實(shí)是利用上下文將幀緩存區(qū)的圖片像素點(diǎn)進(jìn)行重繪卿堂,得到一張新的圖片
// 返回某個(gè)紋理對(duì)應(yīng)的 UIImage,調(diào)用前先綁定對(duì)應(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 會(huì)把數(shù)據(jù)保存為GLubyte類型;
參數(shù)pixels: 指針,像素?cái)?shù)據(jù)讀取后, 將會(huì)保存到該指針指向的地址內(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ù)組來(lái)訪問(wèn)buffer數(shù)據(jù);
/*
CGDataProviderRef CGDataProviderCreateWithData(void *info, const void *data, size_t size, CGDataProviderReleaseDataCallback releaseData);
@功能: 新的數(shù)據(jù)類型, 方便訪問(wèn)二進(jìn)制數(shù)據(jù);
@參數(shù):
參數(shù)info: 指向任何類型數(shù)據(jù)的指針, 或者為Null;
參數(shù)data: 數(shù)據(jù)存儲(chǔ)的地址,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 對(duì)象;
參數(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)過(guò)來(lái)
//創(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;
}
- 存儲(chǔ)到相冊(cè)
將獲取的拉伸后的圖片通過(guò)蘋果自帶的Photos框架享钞,將其存儲(chǔ)到系統(tǒng)相冊(cè)
// 保存圖片到相冊(cè)
- (void)saveImage:(UIImage *)image {
//將圖片通過(guò)PHPhotoLibrary保存到系統(tǒng)相冊(cè)
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
[PHAssetChangeRequest creationRequestForAssetFromImage:image];
} completionHandler:^(BOOL success, NSError * _Nullable error) {
NSLog(@"success = %d, error = %@ 圖片已保存到相冊(cè)", success, error);
}];
}