最近为别的项目组写了一个基于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了。下面重点介绍下这一块。其实这一块的内容详细来说的话单独写一篇都可能不够,所以在这里我就不说这么详细了,诸位如果感兴趣向大家推荐罗大的教程,更系统(http://blog.csdn.net/kesalin/article/details/8223649)。这里只是给出一个过程图,咱们详细的还是按照代码来。
这里面,我们最需要关注的就是灰色的两个部分,即顶点和片元shader的编写,不同的shader会实现不同的滤镜效果,可以理解为一对shader对应一个滤镜效果(当然不绝对,有的效果为了模块化会拆分通过几个基础滤镜shader叠加形成)。下面,给出一对最简单的白板shader的代码:
顶点shader:
attribute vec4 position;
attribute vec4 inputTextureCoordinate;
varying vec2 textureCoordinate;
void main()
{
gl_Position = position;
textureCoordinate = inputTextureCoordinate.xy;
}
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是用来对光栅化后的片元点进行处理的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;
}
+(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;
}
好了,至此绘制的过程也大致讲通了,方法就是上面说的利用shader对图像按照纹理来进行处理。另外,对于上面的shader编程,其实是一个可以研究的地方,这里只是粗略的极少下。最后提醒一点,shader中很多变量的个数都是有限制的,而且不同的gpu支持个数是不一样的,所以当你使用变量比较多又出现了比较奇怪的bug的时候,建议可以查询下是不是自己使用的变量超标了。
最后总结下,滤镜架构其实不复杂,因为毕竟就只是一个二维的绘制过程。难点在于写出好的shader,而这一块又和数学和设计都紧密相关,当初我做的时候为了出好效果也是反复试验了很多次参数,有时候看着PS好的效果都来来回回琢磨好久,真的希望PS能把它的算法开源出来(这里有点痴人说梦了TT)。另外,有些效果会使用一些附加的外部图片,主要是用来调色或者蒙影用的,这样就需要使用多个shader来进行绘制,这块多shader的架构这里就不在多啰嗦了,思路一样只不过需要添加些东西。