iOS OpenGL的滤镜架构搭建实战

最近为别的项目组写了一个基于OpenGL的实时滤镜架构,感觉还是挺系统的一个东西,值得记录下来。滤镜,个人理解可以说是一个对图像进行实时绘制和处理的机制,基于采集到的图像进行颜色、线条、轮廓等等的处理。在OpenGL中,思路是将图像先绘制为一个纹理,然后利用OpenGL的方法对该纹理进行基于GPU的处理,最后再将处理后的纹理回读到内存里成为图像。因为通过这个机制将各种处理放在了GPU上,发挥了GPU的优势所以效率相对使用CPU直接处理高出很多,还可以将CPU资源节省出来给其他任务。
因为使用到的只是OpenGL中纹理绘制和贴图的部分,所以就直接说这一块。其实在实时滤镜上这一块的原理和画画很像,因为所有操作都是2D的。采集到的图像就是画画的底板,就是远处让你参考的风景;绘制的目标buffer就是支架上的画板;绘制的过程就是你基于风景进行各种加工和绘制的过程。接下来就分块说一下。

1、生成纹理:

glActiveTexture(GL_TEXTURE0);  
  
glGenTextures(1, &drawTexture);  
glBindTexture(GL_TEXTURE_2D, drawTexture);  
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);  
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);  
// This is necessary for non-power-of-two textures  
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);  
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);  
  
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);  

代码中变量声明如下:

GLuint drawTexture;

首先,激活一个纹理贴片,这里用的是0号,之后创建一个纹理句柄,并将刚才的二维纹理绑定在句柄上。之后是对二维纹理参数的设置,具体含义可以自己查一下介绍的文字很多。最后就是使用采集到的图像image来生成纹理了。

2、创建画板:

OpenGL中的画板其实就是renderbuffer,但OpenGL机制中renderbuffer要使用framebuffer进行管理,所以要首先生成framebuffer和renderbuffer然后将两者绑定在一起,当然renderbuffer作为画板要有空间,所以还要为renderbuffer绑定一个空间用来绘制。代码如下:

- (void)prepareOffScrnFrameBuffer:(int)width imgHeight:(int)height  
{  
    glGenRenderbuffers(1, &offScrnRenderBuffer);  
    glBindRenderbuffer(GL_RENDERBUFFER, offScrnRenderBuffer);  
    glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8_OES, width, height);  
      
    glGenFramebuffers(1, &offScrnFramBuffer);  
    glBindFramebuffer(GL_FRAMEBUFFER, offScrnFramBuffer);  
      
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, offScrnRenderBuffer);  
      
}  

代码中变量声明大致如下:

GLuint offScrnFramBuffer;  
GLuint offScrnRenderBuffer;  

framebuffer和renderbuffer的句柄从名字上就可以看出来,绑定的话有个附加点的概念,就是代码中的GL_COLOR_ATTACHMENT0,一个framebuffer可以绑定几个renderbuffer,当然还有其他如深度等,这一点不同的机器是不一样的,有一个命令可以查询该参数。但至少一个renderbuffer和深度buffer是没问题的。

当然,在显示的时候,这个画板往往小和一个layer绑定在一起才能显示在界面上。一般使用opengl es支持的CAEAGLLayer来增强原有layer属性以支持显示

- (void)setupLayer  
{  
    _eaglLayer = (CAEAGLLayer*) self.layer;  
      
    // CALayer 默认是透明的,必须将它设为不透明才能让其可见  
    _eaglLayer.opaque = YES;  
      
    // 设置描绘属性,在这里设置不维持渲染内容以及颜色格式为 RGBA8  
    _eaglLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES],  
                                     kEAGLDrawablePropertyRetainedBacking, kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat, nil];  
}  

3、绘制过程:

- (BOOL)RenderFrameAndPreview:(GLubyte *)imageBuffer imgWidth:(int)width imgHight:(int)height  
{  
    //setup draw context and prepare FBOs  
    [self prepareContext];  
    [self prepareOffScrnFrameBuffer:width imgHeight:height];  
    //create texture  
    [self createTexture:imageBuffer imgWidth:width imgHeight:height];  
    //create extra textures  
    [self createExtTextures];  
      
    //off screen draw  
    glBindFramebuffer(GL_FRAMEBUFFER, offScrnFramBuffer);  
    glBindRenderbuffer(GL_RENDERBUFFER, offScrnRenderBuffer);  
    mProgram = [GLESUtils initCertainShaders:FILTERNUM];  
    glUseProgram(mProgram);  
      
    glClearColor(0.0f, 1.0f, 0.0f, 1);  
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  
    glViewport(0, 0, width, height);  
      
    [self setTexPoints];  
    [self setProgrammeSlots:FILTERNUM];  
      
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);  
    //[self readImageFromFrameBuffer];  
    [self saveFilterImageToBuffer:imageBuffer imgWidth:width imgHeight:height];  
  
    //destroy FBOs  
    [self destroyDataFBO];  
      
    return true;  
}  

代码中变量声明如下:

GLuint mProgram;

里面用到了很多其他函数,现在先大致说一下函数功能,来屡一下绘制的过程。OpenGL中的绘制首先需要设定绘制的上下文环境,也就是代码中的

prepareContext

这里上下文环境主要是指明使用的opengl API版本:

- (void)prepareContext{  
    mContext = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];  
    [EAGLContext setCurrentContext:mContext];  
}  

声明:

EAGLContext* mContext; 

接下来,就是上面的准备画板和生成纹理。然后到了绘制的时候,将之前准备好的framebuffer和renderbuffer句柄安装在状态机上,设定使用预定的绘制脚本,再清除颜色和深度,设定视点。然后将shader中的参数传递进去就可以draw了。
至此,流程已经比较清楚了,只剩下一块:如何对原始纹理进行修改和绘制? 这一块就是上面步骤里的shader和program了。下面重点介绍下这一块。这里只是给出一个过程图,咱们详细的还是按照代码来。


image.png

这里面,我们最需要关注的就是灰色的两个部分,即顶点和片元shader的编写,不同的shader会实现不同的滤镜效果,可以理解为一对shader对应一个滤镜效果(当然不绝对,有的效果为了模块化会拆分通过几个基础滤镜shader叠加形成)。下面,给出一对最简单的白板shader的代码:

顶点shader:

attribute vec4 position;  
attribute vec4 inputTextureCoordinate;  
  
varying vec2 textureCoordinate;  
  
void main()  
{  
    gl_Position = position;  
    textureCoordinate = inputTextureCoordinate.xy;  
}  

片元shader:

varying highp vec2 textureCoordinate;  
  
uniform sampler2D inputImageTexture;  
  
void main()  
{  
    lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);  
      
    gl_FragColor = vec4((1.0 - textureColor.rgb), textureColor.w);  
}  

顶点shader中attribute变量为顶点shader特有的属性变量,由外部传入,定义纹理是如何贴到画板上去的,比如是倒着贴还是正着贴等等,通过坐标组的对应来实现。varying变量为顶点shader光栅化后传递给片元shader的变量,所以可以看到片元shader中也有一个对应的varying变量。之后是一个类似C的main函数,这其中就是你用来实现自己想法的地方了,第一个语句gl_Position为顶点shader必须输出的一个变量,这里直接将position传递给它。第二个语句是讲传入的纹理顶点坐标信息赋给varying变量textureCoordinate,注意这里经过光栅化后,传递给片元的坐标值就不只是你在顶点shader中传入的四个顶点坐标对了,而是变为了M*N个片元点坐标。在片元shader中,使用此坐标来从输入的纹理中去的相应点的像素值,进行接下来操作,从而实现了利用shaer来对传入图片进行滤镜处理的目的。
片元shader是用来对光栅化后的片元点进行处理的shader。个人理解顶点shader在这个过程中依据顶点等其他状态会光栅化成很多个片元点,片元shader将在这些点上使用。代码中的varying变量在上一段中已经介绍了下了,是有顶点shader传递过来的。uniform变量是外部传入的,并且这里变量类型为sampler2D类型,也就是一个二维的纹理,程序中这里一般用来传入之前生成的纹理。接下来同样也是main函数,第一句是获取textureCoordinate点的像素值,第二句这里没做任何变换就直接输出了。gl_FragColor是片元shader必须输出的变量。

在渲染中,使用shader是需要先将一对shader编译成一个program使用的,也就是上面renderandpreview函数中的mprogramm变量。连接生成program的函数如下:

+ (GLuint)initProgramme:(NSString*) verShaderName fragment:(NSString*) fragShaderName  
{  
    GLuint shaderProgramme = glCreateProgram();  
    if (shaderProgramme == 0)  
    {  
        NSLog(@"create programme failed!");  
        return 0;  
    }  
      
    NSString * vertexShaderPath = [[NSBundle mainBundle] pathForResource:verShaderName  
                                                                  ofType:@"glsl"];  
    NSString * fragmentShaderPath = [[NSBundle mainBundle] pathForResource:fragShaderName  
                                                                    ofType:@"glsl"];  
      
    GLuint vertexShader = [self loadShader:GL_VERTEX_SHADER withFilepath:vertexShaderPath];  
    GLuint fragmentShader = [self loadShader:GL_FRAGMENT_SHADER withFilepath:fragmentShaderPath];  
      
    glAttachShader(shaderProgramme, vertexShader);  
    glAttachShader(shaderProgramme, fragmentShader);  
      
    glLinkProgram(shaderProgramme);  
    // Check the link status  
    GLint linked;  
    glGetProgramiv(shaderProgramme, GL_LINK_STATUS, &linked);  
      
    if (!linked) {  
        GLint infoLen = 0;  
        glGetProgramiv(shaderProgramme, GL_INFO_LOG_LENGTH, &infoLen);  
          
        if (infoLen > 1){  
            char * infoLog = malloc(sizeof(char) * infoLen);  
            glGetProgramInfoLog(shaderProgramme, infoLen, NULL, infoLog);  
              
            NSLog(@"Error linking program:\n%s\n", infoLog);  
              
            free(infoLog);  
        }  
          
        glDeleteProgram(shaderProgramme );  
        return 0;  
    }  
    // Free up no longer needed shader resources  
    glDeleteShader(vertexShader);  
    glDeleteShader(fragmentShader);  
      
    return shaderProgramme;  
}  

过程是load->attach->link的过程。其中load是个人封装的函数,如下:

+(GLuint)loadShader:(GLenum)type withString:(NSString *)shaderString  
{     
    // Create the shader object  
    GLuint shader = glCreateShader(type);  
    if (shader == 0) {  
        NSLog(@"Error: failed to create shader.");  
        return 0;  
    }  
      
    // Load the shader source  
    const char * shaderStringUTF8 = [shaderString UTF8String];  
    glShaderSource(shader, 1, &shaderStringUTF8, NULL);  
      
    // Compile the shader  
    glCompileShader(shader);  
      
    // Check the compile status  
    GLint compiled = 0;  
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compiled);  
      
    if (!compiled) {  
        GLint infoLen = 0;  
        glGetShaderiv ( shader, GL_INFO_LOG_LENGTH, &infoLen );  
          
        if (infoLen > 1) {  
            char * infoLog = malloc(sizeof(char) * infoLen);  
            glGetShaderInfoLog (shader, infoLen, NULL, infoLog);  
            NSLog(@"Error compiling shader:\n%s\n", infoLog );              
              
            free(infoLog);  
        }  
          
        glDeleteShader(shader);  
        return 0;  
    }  
  
    return shader;  
}  

主要就是一个compile的过程。
好了,至此绘制的过程也大致讲通了,方法就是上面说的利用shader对图像按照纹理来进行处理。另外,对于上面的shader编程,其实是一个可以研究的地方,这里只是粗略的极少下。最后提醒一点,shader中很多变量的个数都是有限制的,而且不同的gpu支持个数是不一样的,所以当你使用变量比较多又出现了比较奇怪的bug的时候,建议可以查询下是不是自己使用的变量超标了。

最后总结下,滤镜架构其实不复杂,因为毕竟就只是一个二维的绘制过程。难点在于写出好的shader,而这一块又和数学和设计都紧密相关,当初我做的时候为了出好效果也是反复试验了很多次参数,有时候看着PS好的效果都来来回回琢磨好久,真的希望PS能把它的算法开源出来(这里有点痴人说梦了)。另外,有些效果会使用一些附加的外部图片,主要是用来调色或者蒙影用的,这样就需要使用多个shader来进行绘制,这块多shader的架构这里就不在多啰嗦了,思路一样只不过需要添加些东西。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值