OpenGL ES -- 如何实现美图大长腿功能
2020-08-19 本文已影响0人
HardCabbage
本文主要分析大长腿功能的实现过程以及如何保存图片到相册

一、如何加载一张图片
-
实现流程图
截屏2020-08-19 10.48.08.png
- 第一步:初始化顶点数组,上下文以及顶点缓存区;
- (void)commonInit {
//1.初始化vertices,context
self.vertices = malloc(sizeof(SenceVertex) * kVerticesCount);
self.backgroundColor = [UIColor clearColor];
self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
self.delegate = self;
[EAGLContext setCurrentContext:self.context];
glClearColor(0, 0, 0, 0);
//2.初始化vertexAttribArrayBuffer
self.vertexAttribArrayBuffer = [[LongLegVertexAttribArrayBuffer alloc] initWithAttribStride:sizeof(SenceVertex) numberOfVertices:kVerticesCount data:self.vertices usage:GL_STATIC_DRAW];
}
- 第二步:加载图片,这里的加载图片我们使用的是GLBaseEffect实现的;
- (void)updateImage:(UIImage *)image {
//记录SpringView是否发生拉伸动作
self.hasChange = NO;
//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;
//7.根据当前控件的尺寸以及纹理的尺寸,计算纹理坐标以及顶点坐标;
[self calculateOriginTextureCoordWithTextureSize:self.currentImageSize
startY:0
endY:0
newHeight:0];
//8. 更新顶点数组缓存区;
[self.vertexAttribArrayBuffer updateDataWithAttribStride:sizeof(SenceVertex)
numberOfVertices:kVerticesCount
data:self.vertices
usage:GL_STATIC_DRAW];
//9. 显示(绘制)
[self display];
}
- 第三步:根据当前的控件尺寸对 纹理坐标进行初始化;
/**
根据当前控件的尺寸和纹理的尺寸,计算初始纹理坐标
@param size 原始纹理尺寸
@param startY 中间区域的开始纵坐标位置 0~1
@param endY 中间区域的结束纵坐标位置 0~1
@param newHeight 新的中间区域的高度
*/
- (void)calculateOriginTextureCoordWithTextureSize:(CGSize)size
startY:(CGFloat)startY
endY:(CGFloat)endY
newHeight:(CGFloat)newHeight;
- 第四步:绘制图片,显示未被拉伸的原始图片;
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
//1.准备绘制GLBaseEffect
[self.baseEffect prepareToDraw];
//2.清空缓存区;
glClear(GL_COLOR_BUFFER_BIT);
//3. 准备绘制数据-顶点数据
[self.vertexAttribArrayBuffer prepareToDrawWithAttrib:GLKVertexAttribPosition
numberOfCoordinates:3
attribOffset:offsetof(SenceVertex, positionCoord)
shouldEnable:YES];
//4. 准备绘制数据-纹理坐标数据
[self.vertexAttribArrayBuffer prepareToDrawWithAttrib:GLKVertexAttribTexCoord0
numberOfCoordinates:2
attribOffset:offsetof(SenceVertex, textureCoord)
shouldEnable:YES];
//5. 开始绘制;
[self.vertexAttribArrayBuffer drawArrayWithMode:GL_TRIANGLE_STRIP
startVertexIndex:0
numberOfVertices:kVerticesCount];
}
二、如何调整滑块
-
实现流程图
截屏2020-08-19 10.49.19.png
- 第一步:判断
springView
是否被拉伸,如果有拉伸,那么给SpringView
更新纹理并且拉伸完成需要后将滑块设置到起始位置; - 第二步:修改滑块约束以及滑块的位置;
- 第三步:计算移动后的
topLineTop
和topLineBottom
位置,进行拉伸调整 - 代码
//1.判断springView是否发生改变
if ([self.springView hasChange]) {
//2.给springView 更新纹理
[self.springView updateTexture];
//3.重置滑杆位置(因为此时相当于对一个张新图重新进行拉伸处理~)
self.slider.value = 0.5f;
}
//修改约束信息;
CGPoint translation = [pan translationInView:self.view];
//修改topLineSpace的预算条件;
self.topLineSpace.constant = MIN(self.topLineSpace.constant + translation.y,
self.bottomLineSpace.constant);
//纹理Top = springView的height * textureTopY
//606
CGFloat textureTop = self.springView.bounds.size.height * self.springView.textureTopY;
NSLog(@"%f,%f",self.springView.bounds.size.height,self.springView.textureTopY);
NSLog(@"%f",textureTop);
//设置topLineSpace的约束常量;
self.topLineSpace.constant = MAX(self.topLineSpace.constant, textureTop);
//将pan移动到view的Zero位置;
[pan setTranslation:CGPointZero inView:self.view];
//计算移动了滑块后的currentTop和currentBottom
self.currentTop = [self stretchAreaYWithLineSpace:self.topLineSpace.constant];
self.currentBottom = [self stretchAreaYWithLineSpace:self.bottomLineSpace.constant];
三、通过LongLegView进行Slider拉伸调整方法的调用过程

- 第一步:检测slider的滑动大小来获取图片的中间拉伸区域高度,并且通过;
CGFloat newHeight = (self.currentBottom - self.currentTop) * ((sender.value) + 0.5);
- 第二步:将currentTop和currentBottom以及新图片的高度传给springView,进行拉伸操作;
- (void)stretchingFromStartY:(CGFloat)startY
toEndY:(CGFloat)endY
withNewHeight:(CGFloat)newHeight;
- 第三步:根据当前控件的尺寸和纹理的尺寸,计算初始纹理坐标;
/**
根据当前控件的尺寸和纹理的尺寸,计算初始纹理坐标
@param size 原始纹理尺寸
@param startY 中间区域的开始纵坐标位置 0~1
@param endY 中间区域的结束纵坐标位置 0~1
@param newHeight 新的中间区域的高度
*/
- (void)calculateOriginTextureCoordWithTextureSize:(CGSize)size
startY:(CGFloat)startY
endY:(CGFloat)endY
newHeight:(CGFloat)newHeight;
- 第四步:更新顶点数组缓存区的数据;
- (void)updateDataWithAttribStride:(GLsizei)stride
numberOfVertices:(GLsizei)count
data:(const GLvoid *)data
usage:(GLenum)usage;
- 第五步:更新显示,通过
- (void)display
方法调用- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
进行重绘
四、将拉伸后的图片保存到系统相册

- 第一步:帧缓存区中获取纹理图片文件; 获取当前的渲染结果
- (UIImage *)createResult {
//1. 根据屏幕上显示结果, 重新获取顶点/纹理坐标
[self resetTextureWithOriginWidth:self.currentImageSize.width
originHeight:self.currentImageSize.height
topY:self.currentTextureStartY
bottomY:self.currentTextureEndY
newHeight:self.currentNewHeight];
//2.绑定帧缓存区;
glBindFramebuffer(GL_FRAMEBUFFER, self.tmpFrameBuffer);
//3.获取新的图片Size
CGSize imageSize = [self newImageSize];
//4.从帧缓存中获取拉伸后的图片;
UIImage *image = [self imageFromTextureWithWidth:imageSize.width height:imageSize.height];
//5. 将帧缓存绑定0,清空;
glBindFramebuffer(GL_FRAMEBUFFER, 0);
//6. 返回拉伸后的图片
return image;
}
第二步:根据当前屏幕上的显示结果,重新获取纹理和顶点坐标
- (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.在新的纹理坐标下,重新计算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 (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。
*/
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;
}
第三步:返回某个纹理对应的 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 (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;
int size = width * height * 4;
GLubyte *buffer = malloc(size);
*/
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
//使用data和size 数组来访问buffer数据;
/*
CGDataProviderRef CGDataProviderCreateWithData(void *info, const void *data, size_t size, CGDataProviderReleaseDataCallback releaseData);
@功能: 新的数据类型, 方便访问二进制数据;
@参数:
参数info: 指向任何类型数据的指针, 或者为Null;
参数data: 数据存储的地址,buffer
参数size: buffer的数据大小;
参数releaseData: 释放的回调,默认为空;
*/
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.将帧缓存区里像素点绘制到一张图片上;
/*
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
*/
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;
}
- 第四步:保存到相册
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
[PHAssetChangeRequest creationRequestForAssetFromImage:image];
} completionHandler:^(BOOL success, NSError * _Nullable error) {
NSLog(@"success = %d, error = %@ 图片已保存到相册", success, error);
}];