实现图片拉伸功能

我们通过 OpenGLES 来实现图片拉伸的功能,例如:大长腿效果。

通过这个例子的学习,可以加深我们对纹理渲染流程的理解,以及「渲染到纹理」这个知识点。

进阶教程,阅读前请已具备 OpenGLES 纹理渲染的相关概念。

思路

整个实现流程分为三个部分:初次加载图片、拉伸图片、保存图片。

首次加载图片

第一次图片的加载通过使用 GLKit 完成,整个过程为:

  • 自定义 GLKView 的初始化配置
  • 计算图片的顶点数据
  • 绘制图片并渲染

初始化

  1. 设置当前 EAGLContext 上下文
  2. 使用 VBO 加载纹理,需要申请顶点数组、顶点缓冲区以及顶点数据与顶点缓冲区的绑定

加载图片

  1. 通过 GLKBaseEffect 加载纹理

  2. 记录当前纹理的宽高、计算宽高比

  3. 获得纹理的高度

  4. 根据纹理高度,计算图片合理的宽度

  5. 计算纹理坐标与顶点坐标

  6. 更新顶点数据到顶点缓冲区

  7. 调用 display 方法触发代理方法去渲染

    • 根据图片 size 计算纹理宽高比

    • 计算拉伸量:拉伸量 = (newHeight - (endY - StartY)) * 纹理高度

      image-20200820154707866

    • 计算纹理坐标

      image-20200820154456588

    • 计算顶点坐标

      image-20200820154534254

拉伸图片

拉伸的关键是将图片切割为 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);

另外,我们需要为新的纹理设置一个尺寸,这个尺寸不再受限于屏幕上控件的尺寸,这也是新纹理可以保持原有分辨率的原因。

图片保存

流程:

  1. 通过帧缓冲重新生成纹理,即通过渲染到纹理的方式
  2. 将纹理转化为图片,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;
}

滤镜链:将当次处理的结果,作为下一次处理的原始图

代码获取

参考

使用 iOS OpenGL ES 实现长腿功能