我们通过 OpenGLES 来实现图片拉伸的功能,例如:大长腿效果。
通过这个例子的学习,可以加深我们对纹理渲染流程的理解,以及「渲染到纹理」这个知识点。
进阶教程,阅读前请已具备 OpenGLES 纹理渲染的相关概念。
思路
整个实现流程分为三个部分:初次加载图片、拉伸图片、保存图片。
首次加载图片
第一次图片的加载通过使用 GLKit 完成,整个过程为:
- 自定义
GLKView的初始化配置 - 计算图片的顶点数据
- 绘制图片并渲染
初始化
- 设置当前
EAGLContext上下文 - 使用 VBO 加载纹理,需要申请顶点数组、顶点缓冲区以及顶点数据与顶点缓冲区的绑定
加载图片
通过
GLKBaseEffect加载纹理记录当前纹理的宽高、计算宽高比
获得纹理的高度
根据纹理高度,计算图片合理的宽度
计算纹理坐标与顶点坐标
更新顶点数据到顶点缓冲区
调用
display方法触发代理方法去渲染根据图片 size 计算纹理宽高比
计算拉伸量:拉伸量 = (newHeight - (endY - StartY)) * 纹理高度

计算纹理坐标

计算顶点坐标

拉伸图片
拉伸的关键是将图片切割为 6 个三方形,而中间区域的 4 个顶点坐标计算,是整个拉伸逻辑的核心。
拉伸流程:
- 触发
- (IBAction)sliderValueDidChanged:(UISlider *)sender,根据 slider 的 value 计算拉伸区域的高度 - 调用
- (void)stretchingFromStartY:(CGFloat)startY toEndY:(CGFloat)endY withNewHeight:(CGFloat)newHeight,将区域拉伸或压缩为某个高度 - 根据当前纹理的 size,重新计算纹理坐标,顶点坐标
- 更新顶点缓冲区的顶点数据
- 重新绘制
重复拉伸调整
每一次压缩操作,都是基于上一次的拉伸操作的结果,也就是说每一次操作时,都需要拿到上一步的结果,作为原始图,进行再次调整。
渲染到纹理
其实,如果我们不需要在屏幕上显示我们的渲染结果,也可以直接将数据渲染到另一个纹理上。更有趣的是,这个渲染后的结果,还可以被当成一个普通的纹理来使用。这也是我们实现重复调整功能的基础。
这里需要使用GLSL自定义着色器,完成渲染到纹理
纹理直接渲染到屏幕上
生成一个渲染缓冲,并把这个渲染缓冲挂载到帧缓冲的GL_COLOR_ATTACHMENT0颜色缓冲上,并通过 context 为当前的渲染缓冲绑定输出的 layer
GLuint renderBuffer; // 渲染缓存
GLuint frameBuffer; // 帧缓存
// 绑定渲染缓存要输出的 layer
glGenRenderbuffers(1, &renderBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer);
[self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];
// 将渲染缓存绑定到帧缓存上
glGenFramebuffers(1, &frameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_RENDERBUFFER,
renderBuffer);
纹理渲染到纹理
通过对比,采用 Texture 来替换 Renderbuffer,并且同样是挂载在GL_COLOR_ATTACHMENT0上,不过这里就不需要再绑定 layer
// 生成帧缓存,挂载渲染缓存
GLuint frameBuffer;
GLuint texture;
glGenFramebuffers(1, &frameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, newTextureWidth, newTextureHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
另外,我们需要为新的纹理设置一个尺寸,这个尺寸不再受限于屏幕上控件的尺寸,这也是新纹理可以保持原有分辨率的原因。
图片保存
流程:
- 通过帧缓冲重新生成纹理,即通过渲染到纹理的方式
- 将纹理转化为图片,
glReadPixels方法来实现,将当前帧缓冲中读取纹理数据
// 返回某个纹理对应的 UIImage,调用前先绑定对应的帧缓存
- (UIImage *)imageFromTextureWithWidth:(int)width height:(int)height {
int size = width * height * 4;
GLubyte *buffer = malloc(size);
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, buffer, size, NULL);
int bitsPerComponent = 8;
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 的方法重新绘制一遍,刚好翻转过来
UIGraphicsBeginImageContext(CGSizeMake(width, height));
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
free(buffer);
return image;
}
滤镜链:将当次处理的结果,作为下一次处理的原始图