OpenGL 13 - 案例:纹理图片拉伸与保存
此案例用来处理纹理的拉伸,并对拉伸后图片进行保存。
拉伸效果:
一、拉伸案例 - 主流程
1、加载原图
2、拉伸区域的滑块处理 -- sliderView
3、图片拉伸绘制
4、保存图片到本地相册
二、拉伸,顶点/纹理坐标处理过程
1、手动指定拉伸区域、选取合适的图元装配方式
8个顶点,通过方式 GL_LINE_STRIP 连接绘制。--> V2 ~ V5,拉伸区域 --> 拉伸区域高度 = V5.y - V3.y
2、设置纹理宽高比 得到拉伸量
根据图片实际size计算出纹理高,宽一直不变。
设置初识纹理高度占控件 LongLegView 高度 的 0.8
radio = 当前纹理图片的高宽比 * 控件的宽高比 = img.height/img.width * (view.width/view.height);
纹理高度 textureHeight = textureWidth * radio;
拉伸量 delta = (newHeight - (endY - startY)) * textureHeight; --> newHeight: 拉伸后的纹理高度; startY和endY: 当前的拉伸区域上下的纹理值
3、根据传入的开始结束纹理坐标,计算拉伸后的顶点坐标 [-1 ~ 1]
textureWidth = 0.8455;
4、换算拉伸后对应的纹理坐标 [0 ~ 1]
5、记录拉伸后的当前值,绘制
GLKitView 的 display 方法执行,触发delegate:glkView:drawInRect
1、准备绘制
2、清空缓冲区
3、顶点数据、纹理数据的绑定传递 bind()、glVertexAttribPointer()
4、开始绘制 draw
三、主要代码
1、拉伸后纹理、顶点坐标的计算
// 获取图片的中间拉伸区域高度: (currentBottom - currentTop)*sliderValue + 0.5; CGFloat newHeight = (self.currentBottom - self.currentTop) * ((sender.value) + 0.5);
1 /** 2 根据当前控件的尺寸和纹理的尺寸,计算初始纹理、顶点坐标 3 4 @param size 原始纹理尺寸 5 @param startY 中间区域的开始纵坐标位置 0~1 6 @param endY 中间区域的结束纵坐标位置 0~1 7 @param newHeight 新的中间区域的高度 8 */ 9 - (void)calculateOriginTextureCoordWithTextureSize:(CGSize)size 10 startY:(CGFloat)startY 11 endY:(CGFloat)endY 12 newHeight:(CGFloat)newHeight { 13 NSLog(@"%f,%f",size.height,size.width); 14 15 // 1. 计算拉伸后的宽高比; 16 CGFloat ratio = (size.height / size.width) * 17 (self.bounds.size.width / self.bounds.size.height); 18 CGFloat rr = self.bounds.size.width / self.bounds.size.height; 19 // 2. 宽度=纹理本身宽度; 20 CGFloat textureWidth = self.currentTextureWidth; 21 // 3. 高度=纹理高度*radio(宽高比) 22 CGFloat textureHeight = textureWidth * ratio; 23 24 NSLog(@"%f,%f,%f,%f",newHeight,endY,startY,textureHeight); 25 // 4. 拉伸量 (newHeight - (endY-startY)) * 纹理高度; 26 CGFloat delta = (newHeight - (endY - startY)) * textureHeight; 27 28 // 5. 判断纹理高度+拉伸量是否超出最大值1 29 if (textureHeight + delta >= 1) { 30 delta = 1 - textureHeight; 31 newHeight = delta / textureHeight + (endY - startY); 32 } 33 34 // 6. 纹理4个角的顶点 35 // 左上角 36 GLKVector3 pointLT = {-textureWidth, textureHeight + delta, 0}; 37 // 右上角 38 GLKVector3 pointRT = {textureWidth, textureHeight + delta, 0}; 39 // 左下角 40 GLKVector3 pointLB = {-textureWidth, -textureHeight - delta, 0}; 41 // 右下角 42 GLKVector3 pointRB = {textureWidth, -textureHeight - delta, 0}; 43 44 // 中间矩形区域的顶点 45 CGFloat tempStartYCoord = textureHeight - 2 * textureHeight * startY; 46 CGFloat tempEndYCoord = textureHeight - 2 * textureHeight * endY; 47 48 CGFloat startYCoord = MIN(tempStartYCoord, textureHeight); 49 CGFloat endYCoord = MAX(tempEndYCoord, -textureHeight); 50 51 // 中间部分左上角 52 GLKVector3 centerPointLT = {-textureWidth, startYCoord + delta, 0}; 53 // 中间部分右上角 54 GLKVector3 centerPointRT = {textureWidth, startYCoord + delta, 0}; 55 // 中间部分左下角 56 GLKVector3 centerPointLB = {-textureWidth, endYCoord - delta, 0}; 57 // 中间部分右下角 58 GLKVector3 centerPointRB = {textureWidth, endYCoord - delta, 0}; 59 60 // --纹理的上面两个顶点 61 // 顶点V0的顶点坐标以及纹理坐标; 62 self.vertices[0].positionCoord = pointRT; 63 self.vertices[0].textureCoord = GLKVector2Make(1, 1); 64 65 // 顶点V1的顶点坐标以及纹理坐标; 66 self.vertices[1].positionCoord = pointLT; 67 self.vertices[1].textureCoord = GLKVector2Make(0, 1); 68 69 // -- 中间区域的4个顶点 70 //顶点V2的顶点坐标以及纹理坐标; 71 self.vertices[2].positionCoord = centerPointRT; 72 self.vertices[2].textureCoord = GLKVector2Make(1, 1 - startY); 73 74 // 顶点V3的顶点坐标以及纹理坐标; 75 self.vertices[3].positionCoord = centerPointLT; 76 self.vertices[3].textureCoord = GLKVector2Make(0, 1 - startY); 77 78 // 顶点V4的顶点坐标以及纹理坐标; 79 self.vertices[4].positionCoord = centerPointRB; 80 self.vertices[4].textureCoord = GLKVector2Make(1, 1 - endY); 81 82 // 顶点V5的顶点坐标以及纹理坐标; 83 self.vertices[5].positionCoord = centerPointLB; 84 self.vertices[5].textureCoord = GLKVector2Make(0, 1 - endY); 85 86 // 纹理的下面两个顶点 87 // 顶点V6的顶点坐标以及纹理坐标; 88 self.vertices[6].positionCoord = pointRB; 89 self.vertices[6].textureCoord = GLKVector2Make(1, 0); 90 91 // 顶点V7的顶点坐标以及纹理坐标; 92 self.vertices[7].positionCoord = pointLB; 93 self.vertices[7].textureCoord = GLKVector2Make(0, 0); 94 95 // 保存临时值 96 self.currentTextureStartY = startY; 97 self.currentTextureEndY = endY; 98 self.currentNewHeight = newHeight; 99 }
2、图片保存至相册
1 // 从帧缓存区中获取纹理图片文件,获取当前的渲染结果 2 - (UIImage *)createResult { 3 4 // 1. 根据屏幕上显示结果, 重新获取顶点/纹理坐标 5 [self resetTextureWithOriginWidth:self.currentImageSize.width 6 originHeight:self.currentImageSize.height 7 topY:self.currentTextureStartY 8 bottomY:self.currentTextureEndY 9 newHeight:self.currentNewHeight]; 10 11 // 2.绑定帧缓存区; 12 glBindFramebuffer(GL_FRAMEBUFFER, self.tmpFrameBuffer); 13 // 3.获取新的图片Size 14 CGSize imageSize = [self newImageSize]; 15 // 4.从帧缓存中获取拉伸后的图片; 16 UIImage *image = [self imageFromTextureWithWidth:imageSize.width height:imageSize.height]; 17 // 5. 将帧缓存绑定0,清空; 18 glBindFramebuffer(GL_FRAMEBUFFER, 0); 19 20 // 6. 返回拉伸后的图片 21 return image; 22 }
1 /** 2 根据当前屏幕上的显示,重新创建纹理 3 4 @param originWidth 纹理的原始实际宽度 5 @param originHeight 纹理的原始实际高度 6 @param topY 0 ~ 1,拉伸区域的顶边的纵坐标 7 @param bottomY 0 ~ 1,拉伸区域的底边的纵坐标 8 @param newHeight 0 ~ 1,拉伸区域的新高度 9 */ 10 - (void)resetTextureWithOriginWidth:(CGFloat)originWidth 11 originHeight:(CGFloat)originHeight 12 topY:(CGFloat)topY 13 bottomY:(CGFloat)bottomY 14 newHeight:(CGFloat)newHeight { 15 16 // 1.新的纹理尺寸(新纹理图片的宽高) 17 GLsizei newTextureWidth = originWidth; 18 GLsizei newTextureHeight = originHeight * (newHeight - (bottomY - topY)) + originHeight; 19 20 // 2.高度变化百分比 21 CGFloat heightScale = newTextureHeight / originHeight; 22 23 // 3.在新的纹理坐标下,重新计算 topY、bottomY 24 CGFloat newTopY = topY / heightScale; 25 CGFloat newBottomY = (topY + newHeight) / heightScale; 26 27 // 4.创建顶点数组与纹理数组(逻辑与calculateOriginTextureCoordWithTextureSize 中关于纹理坐标以及顶点坐标逻辑是一样的) 28 SenceVertex *tmpVertices = malloc(sizeof(SenceVertex) * kVerticesCount); 29 tmpVertices[0] = (SenceVertex){{-1, 1, 0}, {0, 1}}; 30 tmpVertices[1] = (SenceVertex){{1, 1, 0}, {1, 1}}; 31 tmpVertices[2] = (SenceVertex){{-1, -2 * newTopY + 1, 0}, {0, 1 - topY}}; 32 tmpVertices[3] = (SenceVertex){{1, -2 * newTopY + 1, 0}, {1, 1 - topY}}; 33 tmpVertices[4] = (SenceVertex){{-1, -2 * newBottomY + 1, 0}, {0, 1 - bottomY}}; 34 tmpVertices[5] = (SenceVertex){{1, -2 * newBottomY + 1, 0}, {1, 1 - bottomY}}; 35 tmpVertices[6] = (SenceVertex){{-1, -1, 0}, {0, 0}}; 36 tmpVertices[7] = (SenceVertex){{1, -1, 0}, {1, 0}}; 37 38 39 /// 下面开始渲染到纹理的流程 40 41 // 1. 生成帧缓存区; 42 GLuint frameBuffer; 43 GLuint texture; 44 // glGenFramebuffers 生成帧缓存区对象名称; 45 glGenFramebuffers(1, &frameBuffer); 46 // glBindFramebuffer 绑定一个帧缓存区对象; 47 glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer); 48 49 // 2. 生成纹理ID,绑定纹理; 50 // glGenTextures 生成纹理ID 51 glGenTextures(1, &texture); 52 // glBindTexture 将一个纹理绑定到纹理目标上; 53 glBindTexture(GL_TEXTURE_2D, texture); 54 // glTexImage2D 指定一个二维纹理图像; 55 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, newTextureWidth, newTextureHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); 56 57 // 3. 设置纹理相关参数 58 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 59 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 60 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 61 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 62 63 // 4. 将纹理图像加载到帧缓存区对象上; 64 /* 65 glFramebufferTexture2D (GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level) 66 target: 指定帧缓冲目标,符合常量必须是GL_FRAMEBUFFER; 67 attachment: 指定附着纹理对象的附着点GL_COLOR_ATTACHMENT0 68 textarget: 指定纹理目标, 符合常量:GL_TEXTURE_2D 69 teture: 指定要附加图像的纹理对象; 70 level: 指定要附加的纹理图像的mipmap级别,该级别必须为0。 71 */ 72 glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); 73 74 // 5. 设置视口尺寸 75 glViewport(0, 0, newTextureWidth, newTextureHeight); 76 77 // 6. 获取着色器程序 78 GLuint program = [LongLegHelper programWithShaderName:@"spring"]; 79 glUseProgram(program); 80 81 // 7. 获取参数ID 82 GLuint positionSlot = glGetAttribLocation(program, "Position"); 83 GLuint textureSlot = glGetUniformLocation(program, "Texture"); 84 GLuint textureCoordsSlot = glGetAttribLocation(program, "TextureCoords"); 85 86 // 8. 传值 87 glActiveTexture(GL_TEXTURE0); 88 glBindTexture(GL_TEXTURE_2D, self.baseEffect.texture2d0.name); 89 glUniform1i(textureSlot, 0); 90 91 // 9.初始化缓存区 92 LongLegVertexAttribArrayBuffer *vbo = [[LongLegVertexAttribArrayBuffer alloc] initWithAttribStride:sizeof(SenceVertex) numberOfVertices:kVerticesCount data:tmpVertices usage:GL_STATIC_DRAW]; 93 94 // 10.准备绘制,将纹理/顶点坐标传递进去; 95 [vbo prepareToDrawWithAttrib:positionSlot numberOfCoordinates:3 attribOffset:offsetof(SenceVertex, positionCoord) shouldEnable:YES]; 96 [vbo prepareToDrawWithAttrib:textureCoordsSlot numberOfCoordinates:2 attribOffset:offsetof(SenceVertex, textureCoord) shouldEnable:YES]; 97 98 // 11. 绘制 99 [vbo drawArrayWithMode:GL_TRIANGLE_STRIP startVertexIndex:0 numberOfVertices:kVerticesCount]; 100 101 // 12.解绑缓存 102 glBindFramebuffer(GL_FRAMEBUFFER, 0); 103 // 13.释放顶点数组 104 free(tmpVertices); 105 106 // 14.保存临时的纹理对象/帧缓存区对象; 107 self.tmpTexture = texture; 108 self.tmpFrameBuffer = frameBuffer; 109 }
1 // 返回某个纹理对应的 UIImage,调用前先绑定对应的帧缓存 2 - (UIImage *)imageFromTextureWithWidth:(int)width height:(int)height { 3 4 // 1.绑定帧缓存区; 5 glBindFramebuffer(GL_FRAMEBUFFER, self.tmpFrameBuffer); 6 7 // 2.将帧缓存区内的图片纹理绘制到图片上; 8 int size = width * height * 4; 9 GLubyte *buffer = malloc(size); 10 11 /* 12 13 glReadPixels (GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid* pixels); 14 @功能: 读取像素(理解为将已经绘制好的像素,从显存中读取到内存中;) 15 @参数解读: 16 参数x,y,width,height: xy坐标以及读取的宽高; 17 参数format: 颜色格式; GL_RGBA; 18 参数type: 读取到的内容保存到内存所用的格式;GL_UNSIGNED_BYTE 会把数据保存为GLubyte类型; 19 参数pixels: 指针,像素数据读取后, 将会保存到该指针指向的地址内存中; 20 21 注意: pixels指针,必须保证该地址有足够的可以使用的空间, 以容纳读取的像素数据; 例如一副256 * 256的图像,如果读取RGBA 数据, 且每个数据保存在GLUbyte. 总大小就是 256 * 256 * 4 = 262144字节, 即256M; 22 int size = width * height * 4; 23 GLubyte *buffer = malloc(size); 24 */ 25 glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, buffer); 26 27 // 使用data和size 数组来访问buffer数据; 28 /* 29 CGDataProviderRef CGDataProviderCreateWithData(void *info, const void *data, size_t size, CGDataProviderReleaseDataCallback releaseData); 30 @功能: 新的数据类型, 方便访问二进制数据; 31 @参数: 32 参数info: 指向任何类型数据的指针, 或者为Null; 33 参数data: 数据存储的地址,buffer 34 参数size: buffer的数据大小; 35 参数releaseData: 释放的回调,默认为空; 36 37 */ 38 CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, buffer, size, NULL); 39 // 每个组件的位数; 40 int bitsPerComponent = 8; 41 // 像素占用的比特数4 * 8 = 32; 42 int bitsPerPixel = 32; 43 // 每一行的字节数 44 int bytesPerRow = 4 * width; 45 // 颜色空间格式; 46 CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB(); 47 // 位图图形的组件信息 - 默认的 48 CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault; 49 // 颜色映射 50 CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault; 51 52 // 3.将帧缓存区里像素点绘制到一张图片上; 53 /* 54 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); 55 @功能:根据你提供的数据创建一张位图; 56 注意:size_t 定义的是一个可移植的单位,在64位机器上为8字节,在32位机器上是4字节; 57 参数width: 图片的宽度像素; 58 参数height: 图片的高度像素; 59 参数bitsPerComponent: 每个颜色组件所占用的位数, 比如R占用8位; 60 参数bitsPerPixel: 每个颜色的比特数, 如果是RGBA则是32位, 4 * 8 = 32位; 61 参数bytesPerRow :每一行占用的字节数; 62 参数space:颜色空间模式,CGColorSpaceCreateDeviceRGB 63 参数bitmapInfo:kCGBitmapByteOrderDefault 位图像素布局; 64 参数provider: 图片数据源提供者, 在CGDataProviderCreateWithData ,将buffer 转为 provider 对象; 65 参数decode: 解码渲染数组, 默认NULL 66 参数shouldInterpolate: 是否抗锯齿; 67 参数intent: 图片相关参数;kCGRenderingIntentDefault 68 69 */ 70 CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent); 71 72 // 4. 此时的 imageRef 是上下颠倒的,调用 CG 的方法重新绘制一遍,翻转过来 73 // 创建一个图片context 74 UIGraphicsBeginImageContext(CGSizeMake(width, height)); 75 CGContextRef context = UIGraphicsGetCurrentContext(); 76 // 将图片绘制上去 77 CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); 78 // 从context中获取图片 79 UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); 80 // 结束图片context处理 81 UIGraphicsEndImageContext(); 82 83 // 释放buffer 84 free(buffer); 85 // 返回图片 86 return image; 87 }