OpenGL 分屏滤镜

219 阅读6分钟

效果图

流程逻辑

程序执行流程

viewDidLoad函数(设置背景+创建切换滤镜效果工具栏) --> filterInit函数 (初始化上下文,准备顶点和纹理加载)-->着色器加载/编译 , program使用 --> startFilerAnimation

当切换底部FilterBar时 --> 切换分屏选项,使用对应的着色器加载/编译 , program使用 --> startFilerAnimation

内置属性

  • 定义顶点坐标和纹理坐标结构SenceVertex
typedef struct {
    GLKVector3 positionCoord; // (X, Y, Z)
    GLKVector2 textureCoord; // (U, V)
} SenceVertex;
  • 上下文EAGLContext --->EAGLContext
  • 定时器刷新CADisplayLink和开始时间NSTimeInterval
  • 着色器程序句柄 program--->GLuint
  • 顶点缓存 vertexBuffer--->GLuint
  • 纹理ID textureID--->GLuint

setupFilterBar 底部滤镜滚动条

创建底部分屏选项工具栏,通过FilterBarDelegate回调改变工具栏选项的action

@protocol FilterBarDelegate <NSObject>

- (void)filterBar:(FilterBar *)filterBar didScrollToIndex:(NSUInteger)index;

@end

filterInit 滤镜处理初始化

  • 初始化上下文并设置为当前上下文
self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
[EAGLContext setCurrentContext:self.context];
  • 开辟顶点数组内存空间,初始化顶点(0,1,2,3)的顶点坐标以及纹理坐标
self.vertices = malloc(sizeof(SenceVertex) * 4);
self.vertices[0] = (SenceVertex){{-1, 1, 0}, {0, 1}};
self.vertices[1] = (SenceVertex){{-1, -1, 0}, {0, 0}};
self.vertices[2] = (SenceVertex){{1, 1, 0}, {1, 1}};
self.vertices[3] = (SenceVertex){{1, -1, 0}, {1, 0}};
  • 绑定渲染缓存区 bindRenderLayer
    • 创建渲染缓存区,帧缓存区对象
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);
  • 获取纹理图片路径,读取图片UIImage
 NSString *imagePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"kunkun.jpg"];
//读取图片
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
  • 从图片中加载纹理,返回纹理ID:texttureID

    • 将 UIImage 转换为 CGImageRef

    • 读取图片的大小,宽和高,rect

    • 获取图片的颜色空间

    • 获取图片字节数 宽4(RGBA)开辟图片数据存储空间

    • 创建上下文

    • 将图片翻转过来(图片默认是倒置的)

    • 对图片进行重新绘制,得到一张新的解压缩后的位图

    • 获取纹理ID

    • 载入纹理2D数据

    • 设置纹理属性(过滤方式和环绕方式)

    • 绑定纹理

    • 释放context,imageData

  //从图片中加载纹理
- (GLuint)createTextureWithImage:(UIImage *)image {
    
    //1、将 UIImage 转换为 CGImageRef
    CGImageRef cgImageRef = [image CGImage];
    //判断图片是否获取成功
    if (!cgImageRef) {
        NSLog(@"Failed to load image");
        exit(1);
    }
    //2、读取图片的大小,宽和高
    GLuint width = (GLuint)CGImageGetWidth(cgImageRef);
    GLuint height = (GLuint)CGImageGetHeight(cgImageRef);
    //获取图片的rect
    CGRect rect = CGRectMake(0, 0, width, height);
    
    //获取图片的颜色空间
    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
    //3.获取图片字节数 宽*高*4(RGBA)
    void *imageData = malloc(width * height * 4);
    //4.创建上下文
    /*
     参数1:data,指向要渲染的绘制图像的内存地址
     参数2:width,bitmap的宽度,单位为像素
     参数3:height,bitmap的高度,单位为像素
     参数4:bitPerComponent,内存中像素的每个组件的位数,比如32位RGBA,就设置为8
     参数5:bytesPerRow,bitmap的没一行的内存所占的比特数
     参数6:colorSpace,bitmap上使用的颜色空间  kCGImageAlphaPremultipliedLast:RGBA
     */
    CGContextRef context = CGBitmapContextCreate(imageData, width, height, 8, width * 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
    
    //将图片翻转过来(图片默认是倒置的)
    CGContextTranslateCTM(context, 0, height);
    CGContextScaleCTM(context, 1.0f, -1.0f);
    CGColorSpaceRelease(colorSpace);
    CGContextClearRect(context, rect);
    
    //对图片进行重新绘制,得到一张新的解压缩后的位图
    CGContextDrawImage(context, rect, cgImageRef);
    
    //设置图片纹理属性
    //5. 获取纹理ID
    GLuint textureID;
    glGenTextures(1, &textureID);
    glBindTexture(GL_TEXTURE_2D, textureID);
    
    //6.载入纹理2D数据
    /*
     参数1:纹理模式,GL_TEXTURE_1D、GL_TEXTURE_2D、GL_TEXTURE_3D
     参数2:加载的层次,一般设置为0
     参数3:纹理的颜色值GL_RGBA
     参数4:宽
     参数5:高
     参数6:border,边界宽度
     参数7:format
     参数8:type
     参数9:纹理数据
     */
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
    
    //7.设置纹理属性
    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_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    
    //8.绑定纹理
    /*
     参数1:纹理维度
     参数2:纹理ID,因为只有一个纹理,给0就可以了。
     */
    glBindTexture(GL_TEXTURE_2D, 0);
    
    //9.释放context,imageData
    CGContextRelease(context);
    free(imageData);
    
    //10.返回纹理ID
    return textureID;
    
    
}
  • 设置纹理ID
self.textureID = textureID
  • 设置视口
 glViewport(0, 0, self.drawableWidth, self.drawableHeight);
 
 //获取渲染缓存区的宽
- (GLint)drawableWidth {
    GLint backingWidth;
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &backingWidth);
    return backingWidth;
}
//获取渲染缓存区的高
- (GLint)drawableHeight {
    GLint backingHeight;
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &backingHeight);
    return backingHeight;
}
 
  • 设置顶点缓存区
	GLuint vertexBuffer;
    glGenBuffers(1, &vertexBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
    GLsizeiptr bufferSizeBytes = sizeof(SenceVertex) * 4;
    glBufferData(GL_ARRAY_BUFFER, bufferSizeBytes, self.vertices, GL_STATIC_DRAW);
  • 设置默认着色器 setupNormalShaderProgram
[self setupShaderProgramWithName:@"Normal"];

startFilerAnimation 滤镜动画

  • 暂停CADisplayLink 定时器
  • 设置displayLink 的方法
  • 将displayLink 添加到runloop 运行循环;
// 开始一个滤镜动画
- (void)startFilerAnimation {
    //1.判断displayLink 是否为空
    //CADisplayLink 定时器
    if (self.displayLink) {
        [self.displayLink invalidate];
        self.displayLink = nil;
    }
    //2. 设置displayLink 的方法
    self.startTimeInterval = 0;
    self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(timeAction)];
    
    //3.将displayLink 添加到runloop 运行循环
    [self.displayLink addToRunLoop:[NSRunLoop mainRunLoop]
                           forMode:NSRunLoopCommonModes];
}
  • 定时动画

    • 使用program

    • 绑定buffer

    • 传入时间

    • 清除画布

    • 重绘

    • 渲染到屏幕上

//2. 动画
- (void)timeAction {
    //DisplayLink 的当前时间撮
    if (self.startTimeInterval == 0) {
        self.startTimeInterval = self.displayLink.timestamp;
    }
    //使用program
    glUseProgram(self.program);
    //绑定buffer
    glBindBuffer(GL_ARRAY_BUFFER, self.vertexBuffer);
    
    // 传入时间
    CGFloat currentTime = self.displayLink.timestamp - self.startTimeInterval;
    GLuint time = glGetUniformLocation(self.program, "Time");
    glUniform1f(time, currentTime);
    
    // 清除画布
    glClear(GL_COLOR_BUFFER_BIT);
    glClearColor(1, 1, 1, 1);
    
    // 重绘
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    //渲染到屏幕上
    [self.context presentRenderbuffer:GL_RENDERBUFFER];
}

FilterBarDelegate

根据index选择灰度shader后重新开始滤镜动画

-(void)filterBar:(FilterBar *)filterBar didScrollToIndex:(NSUInteger)index;

  • index = 0 设置默认着色器

    [self setupShaderProgramWithName:@"Normal"];

  • index = 1 设置二分屏着色器

    [self setupShaderProgramWithName:@"SplitScreen_2"];

  • index = 2 设置3分屏着色器

    [self setupShaderProgramWithName:@"SplitScreen_3"];

  • index = 3 设置4分屏着色器

    [self setupShaderProgramWithName:@"SplitScreen_4"];

  • index = 4 设置6分屏着色器

    [self setupShaderProgramWithName:@"SplitScreen_6"];

  • index = 5 设置9分屏着色器

    [self setupShaderProgramWithName:@"SplitScreen_9"];

分屏滤镜:片元着色器着色器实现不同的算法

切换分屏 ---setupShaderProgramWithName加载不同的顶点着色器程序和片元着色器程序

二分屏

纹理坐标的x值不变的,y值变化:

当 y 在[0, 0.5]范围时,屏幕的(0,0)坐标需要对应图片的(0,0.25),所以y = y+0.25

当 y 在[0.5, 1]范围时,屏幕的(0,0.5)坐标需要对应图片的(0,0.25),所以y = y-0.25

  • 片元着色器程序
precision highp float;
uniform sampler2D Texture;
varying highp vec2 TextureCoordsVarying;

void main() {
    vec2 uv = TextureCoordsVarying.xy;
    float y;
    if (uv.y >= 0.0 && uv.y <= 0.5) {
        y = uv.y + 0.25;
    } else {
        y = uv.y - 0.25;
    }
    gl_FragColor = texture2D(Texture, vec2(uv.x, y));
}

三分屏

纹理坐标的x值不变,y值变化:

当 y 在[0, 1/3]范围时,屏幕的(0,0)坐标需要对应图片的(0,1/3),所以y = y+1/3

当 y 在[1/3, 2/3]范围时,屏幕的(0,1/3)坐标需要对应图片的(0,1/3),所以y 不变

当 y 在[2/3, 1]范围时,屏幕的(0,2/3)坐标需要对应图片的(0,1/3),所以y = y-1/3

  • 片元着色器程序
precision highp float;
uniform sampler2D Texture;
varying highp vec2 TextureCoordsVarying;

void main() {
    vec2 uv = TextureCoordsVarying.xy;
    if (uv.y < 1.0/3.0) {
        uv.y = uv.y + 1.0/3.0;
    } else if (uv.y > 2.0/3.0){
        uv.y = uv.y - 1.0/3.0;
    }
    gl_FragColor = texture2D(Texture, uv);
}

四分屏

屏幕四等分,分别显示缩小的纹理图片:

当 x 在[0, 0.5]范围时,x = x*2

当 x在[0.5, 1]范围时,x = (x-0.5)*2

当 y 在[0, 0.5]范围时,y = y*2

当 y 在[0.5, 1]范围时,y = (y-0.5)*2

  • 片元着色器程序
precision highp float;
uniform sampler2D Texture;
varying highp vec2 TextureCoordsVarying;

void main() {
    vec2 uv = TextureCoordsVarying.xy;
    if(uv.x <= 0.5){
        uv.x = uv.x * 2.0;
    }else{
        uv.x = (uv.x - 0.5) * 2.0;
    }
    
    if (uv.y<= 0.5) {
        uv.y = uv.y * 2.0;
    }else{
        uv.y = (uv.y - 0.5) * 2.0;
    }
    
    gl_FragColor = texture2D(Texture, uv);
}

六分屏

纹理坐标x、y变化规则:

当 x 在[0, 1/3]范围时,x = x+1/3

当 x 在[1/3, 2/3]范围时,x 不变

当 x 在[2/3, 1]范围时,x = x-1/3

当 y 在[0, 0.5]范围时,y = y+0.25

当 y 在[0.5, 1]范围时,y = y-0.24

  • 片元着色器程序
precision highp float;
uniform sampler2D Texture;
varying highp vec2 TextureCoordsVarying;

void main() {
    vec2 uv = TextureCoordsVarying.xy;
   
    if(uv.x <= 1.0 / 3.0){
        uv.x = uv.x + 1.0/3.0;
    }else if(uv.x >= 2.0/3.0){
        uv.x = uv.x - 1.0/3.0;
    }
    
    if(uv.y <= 0.5){
        uv.y = uv.y + 0.25;
    }else {
        uv.y = uv.y - 0.25;
    }
    
    gl_FragColor = texture2D(Texture, uv);
}

九分屏

纹理坐标x、y变化规则: 当 x 在[0, 1/3]范围时,x = x*3

当 x 在[1/3, 2/3]范围时,x = (x-1/3)*3

当 x 在[2/3, 1]范围时,x = (x-2/3)*3

当 y 在[0, 1/3]范围时,y= y*3

当 y 在[1/3, 2/3]范围时,y = (y-1/3)*3

当 y在[2/3, 1]范围时,y = (y-2/3)*3

  • 片元着色器程序
precision highp float;
uniform sampler2D Texture;
varying highp vec2 TextureCoordsVarying;

void main() {
    vec2 uv = TextureCoordsVarying.xy;
    if (uv.x < 1.0 / 3.0) {
        uv.x = uv.x * 3.0;
    } else if (uv.x < 2.0 / 3.0) {
        uv.x = (uv.x - 1.0 / 3.0) * 3.0;
    } else {
        uv.x = (uv.x - 2.0 / 3.0) * 3.0;
    }
    if (uv.y <= 1.0 / 3.0) {
        uv.y = uv.y * 3.0;
    } else if (uv.y < 2.0 / 3.0) {
        uv.y = (uv.y - 1.0 / 3.0) * 3.0;
    } else {
        uv.y = (uv.y - 2.0 / 3.0) * 3.0;
    }
    gl_FragColor = texture2D(Texture, uv);
}