本文介绍如何使用 OpenGL ES 来实现大长腿拉伸的功能。先看下拉伸前后的效果对比图:
拉伸效果对比
我们首先来分析一下该图片应该如何拉伸。
结合我们前面所学的知识,绘制该纹理,只需要将该纹理分成两个三角形即可。但是,我们观察图片能够看出来,该图的拉伸只是对腿部做了拉伸,如果只分成两个三角形,肯定无法实现该效果 。所以们应该做如下划分:
区域划分图
将该图片划分成六个三角形,中间两个三角形就是我们要拉伸的部位,该区域是拖动的,可以改变的。
我们设置 初始纹理 的高度占视图区域(就是区域划分图中的蓝色背景区域)比例的最大值为:
// 初始纹理高度占控件高度的比例
static CGFloat const kDefaultOriginTextureHeight = 0.7f;
我们将整个视图区域的面积视为单位1
,也就是在 x
和 y
轴方向上的长度都为 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
新的中间区域的高度。
我们现在计算的只是初始的纹理顶点坐标,所以这里的startY
、endY
、newHeight
可以暂时不考虑,看作是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.currentTop
和 self.currentBottom
的值是相对于纹理大小的值,我们设置为默认的0.25
和 0.75
,也就是默认拉伸区域设置在图片中间1/3
处。也可以理解为我们 区域划分图 中:
centerPointLT
、centerPointRT
两个顶点在y方向的1/3
处。
centerPointLB
、centerPointRB
两个顶点在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:
该方法又回到了计算 初始纹理坐标 的位置,上面将startY
、endY
、newHeight
看作是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.currentTextureStartY
、self.currentTextureEndY
、self.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_BYT
E 会把数据保存为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
。
size
:buffer
的数据大小。
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
。
bitmapInfo
:kCGBitmapByteOrderDefault
位图像素布局。
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
将图片保存到系统相册。
网友评论