“大长腿”原理与流程
一、第一次加载图片
第一次图片的加载是使用GLKit加载,利用自定义的GLKView视图,通过计算图片的顶点数据,绘制图片并显示到屏幕上,整体的流程如图所示:
① View 初始化
初始化顶点数组、上下文以及顶点数组缓存区,完成加载图片准备的工作;
- LongLegView的- (instancetype)initWithFrame:(CGRect)frame初始化;
- LongLegView的- (void)commonInit初始化顶点数组、上下文context;
// 顶点
@property (nonatomic, assign) SenceVertex *vertices;
// 顶点数组缓存区;
@property (nonatomic, strong) LongLegVertexAttribArrayBuffer *vertexAttribArrayBuffer;
// 初始化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);
// 初始化vertexAttribArrayBuffer
self.vertexAttribArrayBuffer = [[LongLegVertexAttribArrayBuffer alloc] initWithAttribStride:sizeof(SenceVertex) numberOfVertices:kVerticesCount data:self.vertices usage:GL_STATIC_DRAW];
- LongLegVertexAttribArrayBuffer的- (id)initWithAttribStride:(GLsizei)stride numberOfVertices:(GLsizei)count data:(const GLvoid *)data usage:(GLenum)usage的初始化顶点缓存区;
- (id)initWithAttribStride:(GLsizei)stride
numberOfVertices:(GLsizei)count
data:(const GLvoid *)data
usage:(GLenum)usage {
self = [super init];
if (self) {
_stride = stride;
// 根据步长计算出缓存区的大小 stride * count
_bufferSizeBytes = stride * count;
// 生成缓存区对象的名称;
glGenBuffers(1, &_glName);
// 将_glName 绑定到对应的缓存区;
glBindBuffer(GL_ARRAY_BUFFER, _glName);
// 创建并初始化缓存区对象的数据存储;
glBufferData(GL_ARRAY_BUFFER, _bufferSizeBytes, data, usage);
}
return self;
}
② 加载图片
- GLKTextureInfo设置纹理参数:
// GLKTextureInfo 设置纹理参数
NSDictionary *options = @{GLKTextureLoaderOriginBottomLeft : @(YES)};
GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithCGImage:[image CGImage]
options:options
error:NULL];
- 创建GLKBaseEffect方法
self.baseEffect = [[GLKBaseEffect alloc] init];
self.baseEffect.texture2d0.name = textureInfo.name;
- 记录当前图片的size = 图片本身的size, 计算出图片的宽高比例
self.currentImageSize = image.size;
CGFloat ratio = (self.currentImageSize.height / self.currentImageSize.width) *
(self.bounds.size.width / self.bounds.size.height);
- 获取纹理的高度, 并根据纹理的高度以及宽度, 计算出图片合理的宽度;
CGFloat textureHeight = MIN(ratio, kDefaultOriginTextureHeight);
self.currentTextureWidth = textureHeight / ratio;
- 根据当前控件的尺寸以及纹理的尺寸, 计算纹理坐标以及顶点坐标(下文③详细说明)
- 更新顶点数组缓存区
- (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);
}
- 显示(绘制), 出发GLKViewDelegate协议
[self display];
#pragma mark - GLKViewDelegate
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
// 准备绘制GLBaseEffect
[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)calculateOriginTextureCoordWithTextureSize:(CGSize)size startY:(CGFloat)startY endY:(CGFloat)endY newHeight:(CGFloat)newHeight
- 计算拉伸后的宽高比:图片只进行高度拉伸,变化的只是self.bounds.size.height,因此就可以根据self.bounds.size.height得到宽高比ratio
CGFloat ratio = (size.height / size.width) *
(self.bounds.size.width / self.bounds.size.height);
- 根据纹理本身的宽度self.currentTextureWidth就可以计算出纹理的高度textureHeight, 计算拉伸量 = (newHeight - (endY-startY)) * 纹理高度,即换算成纹理的拉伸量delta
CGFloat textureWidth = self.currentTextureWidth;
CGFloat textureHeight = textureWidth * ratio;
CGFloat delta = (newHeight - (endY - startY)) * textureHeight;
- 计算纹理坐标:根据传入的开始位置和结束位置的纹理坐标计算
// 左上角
GLKVector3 pointLT = {-textureWidth, textureHeight + delta, 0};
// 右上角
GLKVector3 pointRT = {textureWidth, textureHeight + delta, 0};
// 左下角
GLKVector3 pointLB = {-textureWidth, -textureHeight - delta, 0};
// 右下角
GLKVector3 pointRB = {textureWidth, -textureHeight - delta, 0};
- 计算顶点坐标:需要先将传入的开始和结束的纹理坐标换换算为顶点坐标; 在计算顶点坐标之前,需要计算出开始位置和结束位置的坐标
// 中间矩形区域的顶点
// 0.7 - 2 * 0.7 * 0.25
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};
- 根据开始坐标和结束位置坐标计算8个点的顶点
// 纹理的上面两个顶点
// 顶点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);
二、“大长腿”拉伸调整过程
- (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];
}
}
- (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);
}
三、保存图片
- 从帧缓存区中获取纹理图片文件createResult
// 从帧缓存区中获取纹理图片文件, 获取当前的渲染结果
- (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;
}
- 根据当前屏幕上的显示,来重新获取纹理、顶点坐标resetTextureWithOriginWidth: originHeight: topY: bottomY: newHeight:
// 根据当前的拉伸结果来重新生成纹理
- (void)updateTexture {
[self resetTextureWithOriginWidth:self.currentImageSize.width
originHeight:self.currentImageSize.height
topY:self.currentTextureStartY
bottomY:self.currentTextureEndY
newHeight:self.currentNewHeight];
// 设置新的纹理
if (self.baseEffect.texture2d0.name != 0) {
// 获取原始的纹理ID
GLuint textureName = self.baseEffect.texture2d0.name;
// 删除纹理
glDeleteTextures(1, &textureName);
}
// 重新设置新的纹理ID
self.baseEffect.texture2d0.name = self.tmpTexture;
// 重置图片的尺寸
self.currentImageSize = [self newImageSize];
self.hasChange = NO;
// 更新纹理的顶点/纹理坐标信息
[self calculateOriginTextureCoordWithTextureSize:self.currentImageSize
startY:0
endY:0
newHeight:0];
// 更新顶点数组缓存里的顶点/纹理坐标数据
[self.vertexAttribArrayBuffer updateDataWithAttribStride:sizeof(SenceVertex)
numberOfVertices:kVerticesCount
data:self.vertices
usage:GL_STATIC_DRAW];
// 显示;
[self display];
}
- (void)resetTextureWithOriginWidth:(CGFloat)originWidth
originHeight:(CGFloat)originHeight
topY:(CGFloat)topY
bottomY:(CGFloat)bottomY
newHeight:(CGFloat)newHeight {
// 新的纹理尺寸(新纹理图片的宽高)
GLsizei newTextureWidth = originWidth;
GLsizei newTextureHeight = originHeight * (newHeight - (bottomY - topY)) + originHeight;
// 高度变化百分比
CGFloat heightScale = newTextureHeight / originHeight;
// 在新的纹理坐标下,重新计算topY、bottomY
CGFloat newTopY = topY / heightScale;
CGFloat newBottomY = (topY + newHeight) / heightScale;
// 创建顶点数组与纹理数组(逻辑与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}};
///下面开始渲染到纹理的流程
// 生成帧缓存区
GLuint frameBuffer;
GLuint texture;
// glGenFramebuffers 生成帧缓存区对象名称;
glGenFramebuffers(1, &frameBuffer);
// glBindFramebuffer 绑定一个帧缓存区对象;
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
// 生成纹理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);
// 设置纹理相关参数
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);
// 将纹理图像加载到帧缓存区对象上
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
// 设置视口尺寸
glViewport(0, 0, newTextureWidth, newTextureHeight);
// 获取着色器程序
GLuint program = [LongLegHelper programWithShaderName:@"spring"];
glUseProgram(program);
// 获取参数ID
GLuint positionSlot = glGetAttribLocation(program, "Position");
GLuint textureSlot = glGetUniformLocation(program, "Texture");
GLuint textureCoordsSlot = glGetAttribLocation(program, "TextureCoords");
// 传值
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, self.baseEffect.texture2d0.name);
glUniform1i(textureSlot, 0);
// 初始化缓存区
LongLegVertexAttribArrayBuffer *vbo = [[LongLegVertexAttribArrayBuffer alloc] initWithAttribStride:sizeof(SenceVertex) numberOfVertices:kVerticesCount data:tmpVertices usage:GL_STATIC_DRAW];
// 准备绘制,将纹理/顶点坐标传递进去;
[vbo prepareToDrawWithAttrib:positionSlot numberOfCoordinates:3 attribOffset:offsetof(SenceVertex, positionCoord) shouldEnable:YES];
[vbo prepareToDrawWithAttrib:textureCoordsSlot numberOfCoordinates:2 attribOffset:offsetof(SenceVertex, textureCoord) shouldEnable:YES];
// 绘制
[vbo drawArrayWithMode:GL_TRIANGLE_STRIP startVertexIndex:0 numberOfVertices:kVerticesCount];
// 解绑缓存
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 释放顶点数组
free(tmpVertices);
// 保存临时的纹理对象/帧缓存区对象;
self.tmpTexture = texture;
self.tmpFrameBuffer = frameBuffer;
}
- 从某个纹理对象获取UIImage对象
// 返回某个纹理对应的 UIImage,调用前先绑定对应的帧缓存
- (UIImage *)imageFromTextureWithWidth:(int)width height:(int)height {
// 绑定帧缓存区;
glBindFramebuffer(GL_FRAMEBUFFER, self.tmpFrameBuffer);
// 将帧缓存区内的图片纹理绘制到图片上;
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;
// 将帧缓存区里像素点绘制到一张图片上;
CGImageRef imageRef = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent);
// 此时的 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;
}
- 将图片保存到系统相册中saveImage
// 保存图片到相册
- (void)saveImage:(UIImage *)image {
// 将图片通过PHPhotoLibrary保存到系统相册
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
[PHAssetChangeRequest creationRequestForAssetFromImage:image];
} completionHandler:^(BOOL success, NSError * _Nullable error) {
NSLog(@"success = %d, error = %@ 图片已保存到相册", success, error);
}];
}
效果展示
完整示例
GLSL之仿抖音“大长腿”美颜滤镜效果