OpenGL ES--滤镜

2020-05-24  本文已影响0人  照顾一下

一、前言

OpenGL ES是OpenGL的一个子集,主要用于移动端图形渲染。苹果在2018年WWDC中宣布用metal代替OpenGL,据说渲染3D图形能提升10倍的性能。但是不管是OpenGL还是metal底层原理都是大同小异的。
这篇文章主要目的有两个:1.了解OpenGL ES加载纹理的流程,在之前的文章中已经实现了OpenGL加载纹理,这里用OpenGL ES;2.通过自定义顶点着色器、片元着色器对图片进行特殊处理,也就是我们常说的滤镜。

二、OpenGL ES加载纹理

整个流程分为五个步骤:

1.配置

iOS为我们提供了CAEAGLLayer、EAGLContext。CAEAGLLayer相当于一个画布,EAGLContext是上下文,这个很重要,因为OpenGL是状态机,设置当前工作区为上下文,OpenGL才能知道你到底想将图形显示在哪里。配置的代码异常简单,请看:

@property (nonatomic, strong) EAGLContext *context;
@property (nonatomic, strong) CAEAGLLayer *myLayer;
@property (nonatomic, assign) GLuint renderBuffer;
@property (nonatomic, assign) GLuint frameBuffer;

- (void)setupLayer{
    self.myLayer = [CAEAGLLayer layer];
    self.myLayer.frame = self.bounds;
    [self.myLayer setContentsScale:[UIScreen mainScreen].scale];
    [self.layer addSublayer:self.myLayer];
    
    self.myLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
                                       @false, kEAGLDrawablePropertyRetainedBacking,
                                       kEAGLDrawablePropertyColorFormat, kEAGLColorFormatRGBA8, nil];
    
}


- (void)setupContent{
    EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
    if (!context) {
        exit(1);
    }
    self.context = context;
    BOOL result = [EAGLContext setCurrentContext:self.context];
    if (!result) {
        exit(1);
    }
}
/// 清空渲染缓冲区和帧缓冲区
- (void)clearBuffer{
    glDeleteRenderbuffers(1, &_renderBuffer);
    _renderBuffer = 0;
    glDeleteFramebuffers(1, &_frameBuffer);
    _frameBuffer = 0;
}
/// 开辟缓冲区
- (void)setupBuffer{
    glGenRenderbuffers(1, &_renderBuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);
    
    glGenFramebuffers(1, &_frameBuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
    
    [self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.myLayer];
    
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _frameBuffer);
}

2.编译链接使用自定义着色器程序

可编程管线中只有顶点着色器和片元着色器可以自定义。自定义着色器需要用到着色器语言GL Shader Language,一般简称为GLSL,它是跨平台的。然而GLSL没有专门的编译器,所以只能在项目中手动去编译链接。下面一起来看看GLSL的编译链接流程。
a.编译

/// 如果这里看不明白,继续往下来,将两个流程结合起来
+ (void)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)filePath{
    
    // 读取文件中的字符串
    NSString *content = [NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
    const GLchar *source = (GLchar *)[content UTF8String];
    
    // 创建shader
    *shader = glCreateShader(type);
    glShaderSource(*shader, 1, &source, NULL);
    // 编译
    glCompileShader(*shader);
}

b.链接

/// verFile顶点着色器文件,fragFile片元着色器文件
+ (GLuint)loadShaderProgramFrom:(NSString *)verFile fragFile:(NSString *)fragFile{
    
    GLuint verShader, fragShader;
    /// 创建着色器程序
    GLuint program = glCreateProgram();
    
    [GLSLUtils compileShader:&verShader type:GL_VERTEX_SHADER file:verFile];
    [GLSLUtils compileShader:&fragShader type:GL_FRAGMENT_SHADER file:fragFile];
    
    // 链接
    glAttachShader(program, verShader);
    glAttachShader(program, fragShader);
    
    // 释放
    glDeleteShader(verShader);
    glDeleteShader(fragShader);
    
    return program;
}

3.解压缩图片并绑定到默认的纹理ID上

解压缩图片用的是CoreGraphics框架,请看:

+ (void)readTexture:(NSString *)imgName{
    
    // 判断是哪里的图片,目前先认为是asset中的
    
    // 1.将UIimage转成 CGImageRef
    CGImageRef spriteImage = [UIImage imageNamed:imgName].CGImage;
    if (!spriteImage) {
        NSLog(@"fail load image %@", imgName);
        exit(1);
    }
    
    size_t width = CGImageGetWidth(spriteImage);
    size_t height = CGImageGetHeight(spriteImage);
    
    // 获取图片字节数 宽 x 高 x 4 (RGBA)
    GLubyte *spriteData = (GLubyte *)calloc(width*height*4, sizeof(GLubyte));
    
    // 创建上下文
    /*
    参数1:data,指向要渲染的绘制图像的内存地址
    参数2:width,bitmap的宽度,单位为像素
    参数3:height,bitmap的高度,单位为像素
    参数4:bitPerComponent,内存中像素的每个组件的位数,比如32位RGBA,就设置为8
    参数5:bytesPerRow,bitmap的每一行的内存所占的比特数
    参数6:colorSpace,bitmap上使用的颜色空间  kCGImageAlphaPremultipliedLast:RGBA
    */
    CGContextRef spriteContext = CGBitmapContextCreate(spriteData, width, height, 8, width*4, CGImageGetColorSpace(spriteImage), kCGImageAlphaPremultipliedLast);
    
    // 在CGContextRef 上将图片绘制出来
    CGRect rect = CGRectMake(0, 0, width, height);
    CGContextDrawImage(spriteContext, rect, spriteImage);
    CGContextRelease(spriteContext);
    // 将纹理绑定到默认的纹理ID上
    glBindTexture(GL_TEXTURE_2D, 0);
    
    // 设置纹理属性
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    
    float fw = width, fh = height;
    
    // 载入纹理数据
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, fw, fh, 0, GL_RGBA, GL_UNSIGNED_BYTE, spriteData);
    
    // 释放
    free(spriteData);
}

4.着色器程序处理纹理

// 使用自定义着色器程序,position是顶点着色器中定义的变量
GLuint position = glGetAttribLocation(self.program, "position");
// 允许顶点着色器读取GPU(服务器端)数据
glEnableVertexAttribArray(position);
// 读取方式
/*
 参数1:用什么着色器,这里是顶点着色器 position 顶点数据ID
 参数2:每次读取字节数 数量
 参数3:数据类型
 参数4:是否希望数据被标准化(归一化),只表示方向不表示大小
 参数5:步长(Stride),指定在连续的顶点属性之间的间隔
 参数6:位置数据在缓冲区起始位置的偏移量
 */
glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, NULL);
    
// 片源着色器
GLuint vTextCoor = glGetAttribLocation(self.program, "vTextCoor");
glEnableVertexAttribArray(vTextCoor);
glVertexAttribPointer(vTextCoor, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (GLfloat *)NULL+3);

这段代码是对顶点着色器和片元着色器分别进行传值。直接看这段代码,可能看不明白,因为没有来龙去脉,不过没有关系,继续看。

5.渲染

这里将显示完整的渲染过程,有些过程是被封装了的。例如,着色器程序的编译、链接过程。

- (void)render{
    float scale = [UIScreen mainScreen].scale;
    glViewport(0, 0, self.width*scale, self.height*scale);
    
    glClearColor(0.5, 0.5, 0.5, 1);
    glClear(GL_COLOR_BUFFER_BIT);
    
    NSString *vFile = [[NSBundle mainBundle] pathForResource:@"normal" ofType:@"vsh"];
    NSString *fFile = [[NSBundle mainBundle] pathForResource:@"normal" ofType:@"fsh"];
    
    self.program = [GLSLUtils loadShaderProgramFrom:vFile fragFile:fFile];
    
    // 链接
    glLinkProgram(self.program);
    
    GLint linkStatus;
    glGetProgramiv(self.program, GL_LINK_STATUS, &linkStatus);
    if (linkStatus == GL_FALSE) {
        GLchar msg[512];
        glGetProgramInfoLog(self.program, sizeof(msg), 0, &msg[0]);
        NSString *info = [NSString stringWithUTF8String:msg];
        NSLog(@"%@", info);
        return;
    }
    NSLog(@"link success!");
    
    glUseProgram(self.program);
    
    float sub = 1.0;
    
    // 设置顶点、纹理坐标
    GLfloat points[] = {
        -sub, -sub, 0,    0, 0,
         sub, -sub, 0,    1, 0,
         sub,  sub, 0,    1, 1,
        -sub,  sub, 0,    0, 1,
    };
    
    // 开辟顶点缓冲区
    GLuint vexBuffer;
    glGenBuffers(1, &vexBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, vexBuffer);
    // 第一个参数target可以为GL_ARRAY_BUFFER或GL_ELEMENT_ARRAY。
    // 第二个参数size为待传递数据字节数量
    // 第三个参数为源数据数组指针,如data为NULL,则VBO仅仅预留给定数据大小的内存空间。
    // 最后一个参数usage标志位VBO的另一个性能提示,它提供缓存对象将如何使用:static、dynamic或stream、与read、copy或draw。
    glBufferData(GL_ARRAY_BUFFER, sizeof(points), points, GL_STATIC_DRAW);
    
    
    // 使用自定义着色器程序
    GLuint position = glGetAttribLocation(self.program, "position");
    // 允许顶点着色器读取GPU(服务器端)数据
    glEnableVertexAttribArray(position);
    // 读取方式
    /*
     参数1:用什么着色器,这里是顶点着色器 position 顶点数据ID
     参数2:每次读取字节数 数量
     参数3:数据类型
     参数4:是否希望数据被标准化(归一化),只表示方向不表示大小
     参数5:步长(Stride),指定在连续的顶点属性之间的间隔
     参数6:位置数据在缓冲区起始位置的偏移量
     */
    glVertexAttribPointer(position, 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, NULL);
    
    // 片源着色器
    GLuint vTextCoor = glGetAttribLocation(self.program, "vTextCoor");
    glEnableVertexAttribArray(vTextCoor);
    glVertexAttribPointer(vTextCoor, 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, (GLfloat *)NULL+3);
    
    // 加载纹理
    [GLSLUtils readTexture:@"girl"];
    // 设置纹理采样器
    glUniform1i(glGetUniformLocation(self.program, "colorMap"), 0);
    // 绘图
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
    
    [self.context presentRenderbuffer:GL_RENDERBUFFER];
}

看到这里,你会发现没有顶点和片元着色器的源码,别急,下面就是:

/// normal.vsh
attribute vec4 position;
attribute vec2 vTextCoor;
varying lowp vec2 varyTextCoord;

void main() {
    varyTextCoord = vTextCoor;
    gl_Position = position;
}
//---------------------------------------------------------------//
/// normal.fsh
varying lowp vec2 varyTextCoord;
// 取色器
uniform sampler2D colorMap;

void main() {
    // 取色器取色,varyTextCoord是从顶点着色器传进来的纹理坐标
    lowp vec2 coor = vec2(varyTextCoord.x, 1.0-varyTextCoord.y);
    gl_FragColor = texture2D(colorMap, coor);
}

使用自定义着色器程序加载纹理的流程大致如此,有些细节没有讲到,如果不理解可以在文章最后留言。如果到这里还不是很清楚,没关系,在文章末尾有完整的demo地址可供参考。

三、几种滤镜

GLSL是一种让初学者很难受的语言,它没有代码联想,也不会给你任何提示,即便你的语法有问题,同时它对精度和变量类型很严格。因为顶点着色器代码都一样,所以下面只提供片元着色器代码。

1.分屏滤镜

看效果图就知道怎么分屏了


分屏
varying lowp vec2 varyTextCoord;
// 取色器
uniform sampler2D colorMap;

void main() {
    // 取色器取色,varyTextCoord是从顶点着色器传进来的纹理坐标
    lowp vec2 coor = vec2(varyTextCoord.x, 1.0-varyTextCoord.y);
    
    if (coor.x < 0.5) {
        coor.x = coor.x * 2.0;
    }else{
        coor.x = coor.x * 2.0 - 1.0;
    }
    
    if (coor.y < 0.5) {
        coor.y = coor.y * 2.0;
    }else{
        coor.y = coor.y * 2.0 - 1.0;
    }
    gl_FragColor = texture2D(colorMap, coor);
}
// 分屏滤镜太简单了,不解释

2.旋涡滤镜

先看效果


旋涡

旋涡滤镜需要分两步:

// 定义该区域内所有的float的精度
precision mediump float;

uniform float yinzi;
varying vec2 varyTextCoord;
uniform sampler2D colorMap;
// 最大半径,在这个半径内都会发生旋转
const float maxRadius = 0.5;
// 偏移角度
const float angle = 90.0;

void main() {
    
    vec2 xy = vec2(varyTextCoord.x-0.5, varyTextCoord.y-0.5);
    
    float r = length(xy);
    
    // 将平面坐标系转成极坐标系
    // x = r*cos(a)  y = r*sin(a)
    // tana = y / x
    // a = atan(y/x) = atan(y, x) 定义域(-00, +00),可取
    // a = acos(x/r) = asin(y/x) 定义域(-1, 1),不可取
    // float a = atan(xy.y , xy.x) + radians(angle);
    // float a = asin(xy.x/r)
    // float a = acos(xy.y/r)
    
    if (r <= maxRadius) {
        float sub = 1.0 - yinzi * r * r;
        float a = atan(xy.y , xy.x) + radians(angle) * 2.0 * sub;
        xy = vec2(0.5, 0.5) + vec2(r*cos(a), r*sin(a));
    }else{
        xy = varyTextCoord;
    }

    gl_FragColor = texture2D(colorMap, xy);

}

3.马赛克滤镜

看效果之前,简单说下马赛克的原理,用一句话解释:将某个区域类的颜色值用同一个颜色值替代。看下面的效果图。


马赛克

这里的马赛克效果比较简单不做过多解释。

precision mediump float;

varying vec2 varyTextCoord;
uniform sampler2D colorMap;
// 几行
uniform int rowCount;
// 几列
//uniform int clownCount;

void main() {
    
    int clownCount = rowCount;
    
    vec2 coor = vec2(varyTextCoord.x, 1.0-varyTextCoord.y);
    
    float row = 1.0/float(clownCount);
    float clown = 1.0/float(rowCount);
    
    float x = coor.x;
    float y = coor.y;
    
    float currentX = row * float(int(x/row));
    float currentY = clown * float(int(y/clown));
    
    gl_FragColor = texture2D(colorMap, vec2(currentX, currentY));
}

4.仿抖音灵魂出窍滤镜

一句话解释原理:将纹理做缩放效果并和原纹理进行颜色混合计算出最终的颜色值。看效果图。


灵魂出窍
precision mediump float;
// 放大因子,从外面传入,通过定时器控制
uniform float scale;

uniform sampler2D colorMap;

varying vec2 varyTextCoord;

void main() {
    
    vec2 coor = vec2(varyTextCoord.x, 1.0-varyTextCoord.y);
    
    // 原图
    vec4 originColor = texture2D(colorMap, coor);
    
    // 放大的图
    float x = coor.x - 0.5;
    float y = coor.y - 0.5;
    coor = vec2(coor.x - x * scale, coor.y - y * scale);
    
    float mask = 0.3;
    
    vec4 scaleColor = texture2D(colorMap, coor);
    
//    vec4 endColor = vec4(originColor.r * (1.0-mask) + (scaleColor.r+0.5) * mask,
//                         originColor.g * (1.0-mask) + scaleColor.g * mask,
//                         originColor.b * (1.0-mask) + scaleColor.b * mask,
//                         originColor.a * (1.0-mask) + scaleColor.a * mask);
    
    vec4 endColor = originColor * (1.0-mask) + scaleColor * mask;
    
    gl_FragColor = endColor;
    
}

5.灰度滤镜

so easy,啥也不想讲


灰度
precision mediump float;
varying vec2 varyTextCoord;

uniform sampler2D colorMap;

void main() {
    /*
     任何颜色都有红、绿、蓝三原色组成,假如原来某点的颜色为RGB(R,G,B),那么,我们可以通过下面几种方法,将其转换为灰度:
     1.浮点算法:Gray=R*0.3+G*0.59+B*0.11
     2.整数方法:Gray=(R*30+G*59+B*11)/100
     3.平均值法:Gray=(R+G+B)/3;
     4.仅取绿色:Gray=G;
     */
    vec2 coor = vec2(varyTextCoord.x, 1.0-varyTextCoord.y);
    vec4 source = texture2D(colorMap, coor);
    
    float r = (source.r + source.g + source.b)/3.0;
    
    r = source.r*0.3 + source.g*0.59 + source.b*0.11;
    
    r = dot(source, vec4(0.3, 0.59, 0.11, 0.0));
    
    vec4 color = vec4(r, r, r, source.a);
    
    gl_FragColor = color;
    
}

四、总结

这篇文章主要讲了OpenGL ES加载纹理的流程,还有几种简单滤镜。实际上直接看这篇文章应该还是不能理解,所以建议先看看前几篇,对OpenGL有了一些认识之后再看。
最后附上demo地址:https://github.com/zhaoguyixia/OpenGL.git
祝生活愉快!!

上一篇 下一篇

猜你喜欢

热点阅读