OpenGL ES 选择局部区域拉伸、保存

本文介绍如何使用 OpenGL ES 来实现大长腿拉伸的功能。先看下拉伸前后的效果对比图:


拉伸效果对比

我们首先来分析一下该图片应该如何拉伸。
结合我们前面所学的知识,绘制该纹理,只需要将该纹理分成两个三角形即可。但是,我们观察图片能够看出来,该图的拉伸只是对腿部做了拉伸,如果只分成两个三角形,肯定无法实现该效果 。所以们应该做如下划分:


区域划分图

将该图片划分成六个三角形,中间两个三角形就是我们要拉伸的部位,该区域是拖动的,可以改变的。

我们设置 初始纹理 的高度占视图区域(就是区域划分图中的蓝色背景区域)比例的最大值为:

// 初始纹理高度占控件高度的比例
static CGFloat const kDefaultOriginTextureHeight = 0.7f;

我们将整个视图区域的面积视为单位1,也就是在 xy 轴方向上的长度都为 1。所以纹理的宽和高在视图中的大小为:

textureHeight = self.currentImageSize.height / self.bounds.size.height
textureWidth = self.currentImageSize.width / self.bounds.size.width

所以纹理高和宽的比例为:

CGFloat ratio = textureHeight / textureWidth =

(self.currentImageSize.height / self.bounds.size.height) / (self.currentImageSize.width / self.bounds.size.width) =

(self.currentImageSize.height / self.currentImageSize.width) * (self.bounds.size.width / self.bounds.size.height)

根据该比例ratio计算出合理的宽度:

    // 高度所占最大比例为kDefaultOriginTextureHeight
    CGFloat textureHeight = MIN(ratio, kDefaultOriginTextureHeight);
    // 计算出图片合理的宽度;
    self.currentTextureWidth = textureHeight / ratio;

上面是我们对纹理的区域划分方式,以及纹理的宽高在视图中的占比。下面我们拉进行纹理的绘制。

  • 初始化顶点缓存区

- (id)initWithAttribStride:(GLsizei)stride
          numberOfVertices:(GLsizei)count
                      data:(const GLvoid *)data
                     usage:(GLenum)usage {
    self = [super init];
    if (self) {
        _stride = stride;
        _bufferSizeBytes = stride * count;
        glGenBuffers(1, &_glName);
        //将_glName 绑定到对应的缓存区;
        glBindBuffer(GL_ARRAY_BUFFER, _glName);
        //创建并初始化缓存区对象的数据存储;
        glBufferData(GL_ARRAY_BUFFER, _bufferSizeBytes, data, usage);
    }
    return self;
}
  • _stride:顶点坐标的步长。

  • count:顶点个数。

  • data:顶点数据。

  • usage:绘制方式。

  • _bufferSizeBytes:根据步长计算出的缓存区的大小。

  • _glName:生成的缓存区对象的名称ID。

  • 加载图片

    //1.GLKTextureInfo 设置纹理参数
    NSDictionary *options = @{GLKTextureLoaderOriginBottomLeft : @(YES)};
    GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithCGImage:[image CGImage]
                                                               options:options
                                                                 error:NULL];
    //2.创建GLKBaseEffect 方法.
    self.baseEffect = [[GLKBaseEffect alloc] init];
    self.baseEffect.texture2d0.name = textureInfo.name;
    
    //3.记录当前图片的size = 图片本身的size;
    self.currentImageSize = image.size;
    
    //4.计算出图片的高宽比例
    CGFloat ratio = (self.currentImageSize.height / self.currentImageSize.width) *
    (self.bounds.size.width / self.bounds.size.height);
    
    //5. 获取纹理的高度;
    CGFloat textureHeight = MIN(ratio, kDefaultOriginTextureHeight);
    //6. 根据纹理的高度以及宽度, 计算出图片合理的宽度;
    self.currentTextureWidth = textureHeight / ratio;

为了减少代码量这里我们使用了 GLKit 中的工具来获取并展示图片。

  • 初始纹理坐标

下面我们根据当前控件的尺寸和纹理的尺寸,计算初始纹理坐标。

- (void)calculateOriginTextureCoordWithTextureSize:(CGSize)size
                                            startY:(CGFloat)startY
                                              endY:(CGFloat)endY
                                         newHeight:(CGFloat)newHeight {
    NSLog(@"%f,%f",size.height,size.width);
    
    //1. 计算拉伸后的 高宽 比;
    CGFloat ratio = (size.height / size.width) *
    (self.bounds.size.width / self.bounds.size.height);
    //2. 宽度 = 纹理本身宽度;
    CGFloat textureWidth = self.currentTextureWidth;
    //3. 高度 = 纹理宽度 * radio(高宽比)
    CGFloat textureHeight = textureWidth * ratio;
    //4. 拉伸量 (newHeight - (endY-startY)) * 纹理高度;
    CGFloat delta = (newHeight - (endY -  startY)) * textureHeight;
    
    //5. 判断纹理高度+拉伸量是否超出最大值1
    if (textureHeight + delta >= 1) {
        delta = 1 - textureHeight;
        newHeight = delta / textureHeight + (endY -  startY);
    }
    
    //6. 纹理4个角的顶点
    // 左上角
    GLKVector3 pointLT = {-textureWidth, textureHeight + delta, 0};
    // 右上角
    GLKVector3 pointRT = {textureWidth, textureHeight + delta, 0};
    // 左下角
    GLKVector3 pointLB = {-textureWidth, -textureHeight - delta, 0};
    // 右下角
    GLKVector3 pointRB = {textureWidth, -textureHeight - delta, 0};
    
 // 中间矩形区域的顶点,这里的计算方式是用纹理相对于顶点坐标的比例值来计算的。顶点坐标的y值是[-1, 1]。
//而我们求出来的textureHeight是以视图坐标为基准的,是将整个视图区域的面积视为单位1来进行计算的。
    //0.7 - 2 * 0.7 * 0.25
    //拉伸区域的Y值
    CGFloat tempStartYCoord = textureHeight - 2 * textureHeight * startY;
    CGFloat tempEndYCoord = textureHeight - 2 * textureHeight * endY;
    
    CGFloat startYCoord = MIN(tempStartYCoord, textureHeight);
    CGFloat endYCoord = MAX(tempEndYCoord, -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};
    
    //--纹理的上面两个顶点
    //顶点V0的顶点坐标以及纹理坐标;
    self.vertices[0].positionCoord = pointRT;
    self.vertices[0].textureCoord = GLKVector2Make(1, 1);
    
    //顶点V1的顶点坐标以及纹理坐标;
    self.vertices[1].positionCoord = pointLT;
    self.vertices[1].textureCoord = GLKVector2Make(0, 1);
    
    //--中间区域的4个顶点
    //顶点V2的顶点坐标以及纹理坐标;
    self.vertices[2].positionCoord = centerPointRT;
    self.vertices[2].textureCoord = GLKVector2Make(1, 1 - startY);
    
    //顶点V3的顶点坐标以及纹理坐标;
    self.vertices[3].positionCoord = centerPointLT;
    self.vertices[3].textureCoord = GLKVector2Make(0, 1 - startY);
    
    //顶点V4的顶点坐标以及纹理坐标;
    self.vertices[4].positionCoord = centerPointRB;
    self.vertices[4].textureCoord = GLKVector2Make(1, 1 - endY);
    
    //顶点V5的顶点坐标以及纹理坐标;
    self.vertices[5].positionCoord = centerPointLB;
    self.vertices[5].textureCoord = GLKVector2Make(0, 1 - endY);
    
    // 纹理的下面两个顶点
    //顶点V6的顶点坐标以及纹理坐标;
    self.vertices[6].positionCoord = pointRB;
    self.vertices[6].textureCoord = GLKVector2Make(1, 0);
    
    //顶点V7的顶点坐标以及纹理坐标;
    self.vertices[7].positionCoord = pointLB;
    self.vertices[7].textureCoord = GLKVector2Make(0, 0);
    
    self.currentTextureStartY = startY;
    self.currentTextureEndY = endY;
    self.currentNewHeight = newHeight;
}
  • size:原始纹理尺寸。
  • startY 需要拉伸区域的开始纵坐标位置 [0, 1)
  • endY 需要拉伸区域的结束纵坐标位置 (0, 1]endY大于startY
  • newHeight 新的中间区域的高度。

我们现在计算的只是初始的纹理顶点坐标,所以这里的startYendYnewHeight可以暂时不考虑,看作是0即可,后面拉伸时再重点考虑,现在重点关注顶点的值。

  • 更新顶点数组缓存区

- (void)updateDataWithAttribStride:(GLsizei)stride
                  numberOfVertices:(GLsizei)count
                              data:(const GLvoid *)data
                             usage:(GLenum)usage {
    self.stride = stride;
    self.bufferSizeBytes = stride * count;
    //重新绑定缓存区空间
    glBindBuffer(GL_ARRAY_BUFFER, self.glName);
    //绑定缓存区的数据空间;
    glBufferData(GL_ARRAY_BUFFER, self.bufferSizeBytes, data, usage);
}
  • 绘制

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
    [self.baseEffect prepareToDraw];
    glClear(GL_COLOR_BUFFER_BIT);
    
    //准备绘制数据-顶点数据
    [self.vertexAttribArrayBuffer prepareToDrawWithAttrib:GLKVertexAttribPosition
                                      numberOfCoordinates:3
                                             attribOffset:offsetof(SenceVertex, positionCoord)
                                             shouldEnable:YES];
    //准备绘制数据-纹理坐标数据
    [self.vertexAttribArrayBuffer prepareToDrawWithAttrib:GLKVertexAttribTexCoord0
                                      numberOfCoordinates:2
                                             attribOffset:offsetof(SenceVertex, textureCoord)
                                             shouldEnable:YES];
    // 开始绘制;
    [self.vertexAttribArrayBuffer drawArrayWithMode:GL_TRIANGLE_STRIP
                                   startVertexIndex:0
                                   numberOfVertices:kVerticesCount];
}

看下准备绘制代码:

- (void)prepareToDrawWithAttrib:(GLuint)index
            numberOfCoordinates:(GLint)count
                   attribOffset:(GLsizeiptr)offset
                   shouldEnable:(BOOL)shouldEnable {
    
    //将_glName 绑定到对应的缓存区;
    glBindBuffer(GL_ARRAY_BUFFER, self.glName);
    //默认顶点属性是关闭的,所以使用前要手动打开;
    if (shouldEnable) {
        glEnableVertexAttribArray(index);
    }
    //定义顶点属性传递的方式;
    glVertexAttribPointer(index, count, GL_FLOAT, GL_FALSE, self.stride, NULL + offset);
}
  • index:顶点数据的索引。
  • count:每个顶点属性的组件数量。
  • offset:顶点的偏移量。
  • GL_FLOAT:顶点中的数据类型。
  • GL_FALSE:固定点数据值是否应该归一化,或者直接转换为固定值。GL_FALSE直接转换为固定值。

绘制方式:

- (void)drawArrayWithMode:(GLenum)mode
         startVertexIndex:(GLint)first
         numberOfVertices:(GLsizei)count {
    glDrawArrays(mode, first, count);
}
  • mode:图元装配方式。
  • first:开始绘制的顶点索引。
  • count:顶点个数。

到这一步我们已经绘制好了正常的图片,没有拉伸的图片。下面我们看下图片的拉伸。

  • 拉伸图片

- (IBAction)sliderValueDidChanged:(UISlider *)sender { 
    //获取图片的中间拉伸区域高度
    CGFloat newHeight = (self.currentBottom - self.currentTop) * ((sender.value) + 0.5); 
    //将currentTop和currentBottom以及新图片的高度传给springView,进行拉伸操作;
    [self.springView stretchingFromStartY:self.currentTop
                                   toEndY:self.currentBottom
                            withNewHeight:newHeight];
}

self.currentTopself.currentBottom 的值是相对于纹理大小的值,我们设置为默认的0.250.75,也就是默认拉伸区域设置在图片中间1/3处。也可以理解为我们 区域划分图 中:
centerPointLTcenterPointRT 两个顶点在y方向的1/3处。
centerPointLBcenterPointRB 两个顶点在y方向的2/3处。

这里sender.value的值为[0, 1],我们设置sender.value的默认值为0.5,加上0.5默认就是1,默认图片是不拉伸的。sender.value的值小于0.5时就是缩小,大于0.5时就是放大。

看下拉伸代码:

- (void)stretchingFromStartY:(CGFloat)startY
                      toEndY:(CGFloat)endY
               withNewHeight:(CGFloat)newHeight {
    self.hasChange = YES;
    
    // 根据当前控件的尺寸和纹理的尺寸,计算初始纹理坐标
    [self calculateOriginTextureCoordWithTextureSize:self.currentImageSize
                                              startY:startY
                                                endY:endY
                                           newHeight:newHeight];
    // 更新顶点数组缓存区的数据
    [self.vertexAttribArrayBuffer updateDataWithAttribStride:sizeof(SenceVertex)
                                            numberOfVertices:kVerticesCount
                                                        data:self.vertices
                                                       usage:GL_STATIC_DRAW];
    // 显示
    [self display];
    
    // Change改变完毕之后, 通知ViewController 的 SpringView拉伸区域修改
    if (self.springDelegate &&
        [self.springDelegate respondsToSelector:@selector(springViewStretchAreaDidChanged:)]) {
        [self.springDelegate springViewStretchAreaDidChanged:self];
    }
}
  • calculateOriginTextureCoordWithTextureSize:startY:endY:newHeight:该方法又回到了计算 初始纹理坐标 的位置,上面将startYendYnewHeight看作是0的地方,我们再回去仔细看看,是如何计算的。
    纹理在顶点坐标中的高度差值:
    CGFloat delta = (newHeight - (endY - startY)) * textureHeight;
    newHeight - (endY - startY)计算出来的是纹理中的高度差,所以我们需要乘以纹理在顶点坐标中的高度textureHeight,这样计算出来就是纹理在顶点坐标中的高度差值。
    拉伸区域的Y值:
    CGFloat tempStartYCoord = textureHeight - 2 * textureHeight * startY;
    CGFloat tempEndYCoord = textureHeight - 2 * textureHeight * endY;
    这时我们计算出来的顶点坐标值就是拉伸后的顶点坐标。如果理解不了可以参考一下这张图:

  • updateDataWithAttribStride:numberOfVertices:data:usage:该方法右回到了 更新顶点数组缓存区 模块中。

  • display重新回到 绘制 模块。

再看下代理的回调的方法,拉伸区域修改:

- (void)springViewStretchAreaDidChanged:(LongLegView *)springView {
    CGFloat topY = self.springView.bounds.size.height * self.springView.stretchAreaTopY;
    CGFloat bottomY = self.springView.bounds.size.height * self.springView.stretchAreaBottomY;
    self.topLineSpace.constant = topY;
    self.bottomLineSpace.constant = bottomY;
}

拉伸结束后,更新topY, bottomY, topLineSpace, bottomLineSpace 的位置。

- (CGFloat)stretchAreaTopY {
    CGFloat stretchAreaTopYValue = (1 - self.vertices[2].positionCoord.y) / 2;
    return stretchAreaTopYValue;
}
- (CGFloat)stretchAreaBottomY {
    CGFloat stretchAreaBottomYValue = (1 - self.vertices[5].positionCoord.y) / 2;
    return stretchAreaBottomYValue;
}
  • self.vertices[2].positionCoord就是修改后的 centerPointRT 点, y值在 (-1, 1]范围内。所以stretchAreaTopY的值在[0, 1) 范围内。
  • self.vertices[5].positionCoord就是修改后的 centerPointLB 点,y值在 [-1, 1)范围内。所以stretchAreaBottomY的值在(0, 1] 范围内。

这里之所以使用 1 减去 y 值再除以 2,是因为顶点坐标在[-1, 1]之间,而我们的视图坐标是[0, 1]。将顶点坐标转换到视图坐标中。

到这里我们绘制拉伸的图片也就完成了。下面看下拉伸后的图片如何保存到相册。

  • 拉伸后的图片保存

从帧缓存区中获取纹理图片文件,获取当前的渲染结果。

- (UIImage *)createResult {
    // 根据屏幕上显示结果, 重新获取顶点/纹理坐标
    [self resetTextureWithOriginWidth:self.currentImageSize.width
                         originHeight:self.currentImageSize.height
                                 topY:self.currentTextureStartY
                              bottomY:self.currentTextureEndY
                            newHeight:self.currentNewHeight];
    
    //绑定帧缓存区;
    glBindFramebuffer(GL_FRAMEBUFFER, self.tmpFrameBuffer);
    //获取新的图片Size
    CGSize imageSize = [self newImageSize];
    //从帧缓存中获取拉伸后的图片;
    UIImage *image = [self imageFromTextureWithWidth:imageSize.width height:imageSize.height];
    // 将帧缓存绑定0,清空;
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    return image;
}

这里的self.currentTextureStartYself.currentTextureEndYself.currentNewHeight 的值是在 初始纹理坐标 的最后记录下来的。如果没有印象可以滑上去看一眼。

首先我们看下如何 重新获取顶点、纹理坐标:

- (void)resetTextureWithOriginWidth:(CGFloat)originWidth
                       originHeight:(CGFloat)originHeight
                               topY:(CGFloat)topY
                            bottomY:(CGFloat)bottomY
                          newHeight:(CGFloat)newHeight {
    //1.新的纹理尺寸(新纹理图片的宽高)
    GLsizei newTextureWidth = originWidth;
/*
newHeight:拉伸后的区域的新的高度比例值。
bottomY - topY:上一次拉伸区域的高度比例值。
这两个量相减后 乘以originHeight 就是拉伸后实际增加的图片的尺寸。
再加上 originHeight 就是拉伸后实际图片的尺寸。
*/
    GLsizei newTextureHeight = originHeight * (newHeight - (bottomY - topY)) + originHeight;
    
    //2.高度变化百分比
    CGFloat heightScale = newTextureHeight / originHeight;
    
    //3.在新的纹理坐标下,重新计算topY、bottomY
    CGFloat newTopY = topY / heightScale;
    CGFloat newBottomY = (topY + newHeight) / heightScale;
    
    //4.创建顶点数组与纹理数组(逻辑与calculateOriginTextureCoordWithTextureSize 中关于纹理坐标以及顶点坐标逻辑是一模一样的)
    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}};
    
    
    ///下面开始渲染到纹理的流程
    
    //1. 生成帧缓存区;
    GLuint frameBuffer;
    GLuint texture;
    //glGenFramebuffers 生成帧缓存区对象名称;
    glGenFramebuffers(1, &frameBuffer);
    //glBindFramebuffer 绑定一个帧缓存区对象;
    glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
    
    //2. 生成纹理ID,绑定纹理;
    //glGenTextures 生成纹理ID
    glGenTextures(1, &texture);
    //glBindTexture 将一个纹理绑定到纹理目标上;
    glBindTexture(GL_TEXTURE_2D, texture);
    //glTexImage2D 指定一个二维纹理图像;
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, newTextureWidth, newTextureHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
    
    //3. 设置纹理相关参数
    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. 将纹理图像加载到帧缓存区对象上;
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
    
    //5. 设置视口尺寸
    glViewport(0, 0, newTextureWidth, newTextureHeight);
    
    //6. 获取着色器程序
    GLuint program = [LongLegHelper programWithShaderName:@"spring"];
    glUseProgram(program);
    
    //7. 获取参数ID
    GLuint positionSlot = glGetAttribLocation(program, "Position");
    GLuint textureSlot = glGetUniformLocation(program, "Texture");
    GLuint textureCoordsSlot = glGetAttribLocation(program, "TextureCoords");
    
    //8. 传值
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, self.baseEffect.texture2d0.name);
    glUniform1i(textureSlot, 0);
    
    //9.初始化缓存区
    LongLegVertexAttribArrayBuffer *vbo = [[LongLegVertexAttribArrayBuffer alloc] initWithAttribStride:sizeof(SenceVertex) numberOfVertices:kVerticesCount data:tmpVertices usage:GL_STATIC_DRAW];
    
    //10.准备绘制,将纹理/顶点坐标传递进去;
    [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.释放顶点数组
    free(tmpVertices);
    
    //14.保存临时的纹理对象/帧缓存区对象;
    self.tmpTexture = texture;
    self.tmpFrameBuffer = frameBuffer;
}

重点介绍下glFramebufferTexture2D (GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level)函数,将纹理图像加载到帧缓存区对象上。

  • target: 指定帧缓冲目标,符合常量必须是GL_FRAMEBUFFER;
  • attachment: 指定附着纹理对象的附着点GL_COLOR_ATTACHMENT0
  • textarget: 指定纹理目标, 符合常量:GL_TEXTURE_2D
  • teture:指定要附加图像的纹理对象;
  • level:指定要附加的纹理图像的mipmap级别,该级别必须为0。

获取纹理对应的 UIImage,调用前先绑定对应的帧缓存。

- (UIImage *)imageFromTextureWithWidth:(int)width height:(int)height {
    
    //1.绑定帧缓存区;
    glBindFramebuffer(GL_FRAMEBUFFER, self.tmpFrameBuffer);
    
    //2.将帧缓存区内的图片纹理绘制到图片上;
    int size = width * height * 4;
    GLubyte *buffer = malloc(size);
    glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
    
    //使用data和size 数组来访问buffer数据;
    CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, buffer, size, NULL);
    //每个组件的位数;
    int bitsPerComponent = 8;
    //像素占用的比特数4 * 8 = 32;
    int bitsPerPixel = 32;
    //每一行的字节数
    int bytesPerRow = 4 * width;
    //颜色空间格式;
    CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB();
    //位图图形的组件信息 - 默认的
    CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault;
    //颜色映射
    CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;
    
    //3.将帧缓存区里像素点绘制到一张图片上;
  CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);
    
    //4. 此时的 imageRef 是上下颠倒的,调用 CG 的方法重新绘制一遍,刚好翻转过来
    //创建一个图片context
    UIGraphicsBeginImageContext(CGSizeMake(width, height));
    CGContextRef context = UIGraphicsGetCurrentContext();
    //将图片绘制上去
    CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
    //从context中获取图片
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    //结束图片context处理
    UIGraphicsEndImageContext();
    
    //释放buffer
    free(buffer);
    //返回图片
    return image;
}
  • glReadPixels (GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, GLvoid* pixels);
    功能: 读取像素(理解为将已经绘制好的像素,从显存中读取到内存中)
    x、y、width、height:xy坐标以及读取的宽高。
    format:颜色格式GL_RGBA
    type:读取到的内容保存到内存所用的格式,GL_UNSIGNED_BYTE 会把数据保存为GLubyte类型。
    pixels:指针,像素数据读取后,将会保存到该指针指向的地址内存中。

注意pixels指针必须保证该地址有足够的可以使用的空间,以容纳读取的像素数据。例如一副256 * 256的图像,如果读取RGBA 数据,且每个数据保存为GLubyte类型。总大小就是 256 * 256 * 4 = 262144 字节, 即256M

  • CGDataProviderRef CGDataProviderCreateWithData(void *info, const void *data, size_t size, CGDataProviderReleaseDataCallback releaseData);
    功能:返回新的数据类型,方便访问二进制数据。
    info:指向任何类型数据的指针,或者为Null
    data:数据存储的地址buffer
    sizebuffer的数据大小。
    releaseData:释放的回调,默认为空。

  • 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);
    功能:根据提供的数据创建一张位图。
    注意size_t定义的是一个可移植的单位,在64位机器上为8字节,在32位机器上是4字节;
    width:图片的宽度像素。
    height:图片的高度像素。
    bitsPerComponent:每个颜色组件所占用的位数,比如R占用8位。
    bitsPerPixel:每个颜色的比特数,如果是RGBA则是32位,4 * 8 = 32位。
    bytesPerRow:每一行占用的字节数。
    space:颜色空间模式CGColorSpaceCreateDeviceRGB
    bitmapInfokCGBitmapByteOrderDefault位图像素布局。
    provider:图片数据源提供者,在CGDataProviderCreateWithData,将buffer 转为 provider 对象。
    decode:解码渲染数组,默认NULL
    shouldInterpolate:是否抗锯齿。
    intent:图片相关参数kCGRenderingIntentDefault

最后将图片保存到相册:

- (void)saveImage:(UIImage *)image {
    [[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
        [PHAssetChangeRequest creationRequestForAssetFromImage:image];
    } completionHandler:^(BOOL success, NSError * _Nullable error) {
        NSLog(@"success = %d, error = %@ 图片已保存到相册", success, error);
    }];
}

通过PHPhotoLibrary将图片保存到系统相册。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354