Android实时滤镜实现原理

##原理##

在Android中,目前可以使用Opengl 来实时的预览相机界面,通过这相机的预览界面我们就可以实现滤镜的基本功能。

使用OpenGL 来实现相机的预览界面网上的代码很多,这里就不说了,主要记录一下使用OpenGL实现滤镜的原理。

在相机的预览界面中,我们通常都是相机的所采集到的数据直接渲染到了屏幕上,而我们渲染的屏幕实际上是系统默认提供的一个叫帧缓冲的地方,英文为Frame Buffer Object (FBO) , 既然系统提供了一个默认的帧缓冲对象,那么我们也可以创建一个我们自己的帧缓冲对象,将数据绘制到我们创建的帧缓冲对象上。关于FBO的相关概念,可以参见这篇博客。

通过我们自己创建的帧缓冲对象,可以附加一个或者多个纹理在这个帧缓冲区当中,最终渲染到这个Frame buffer中的数据实际上都是渲染到了纹理图像当中,既然有了这个纹理图像,那么就可以通过Fragmetn shader来实现各种滤镜的效果。

##实现及思考##

下面贴一下关键的代码

绘制相机预览界面的代码,在这段代码中,把相机的数据绘制到了自己创建的FBO对象当中。

if (fbo == null) {             
    fbo = FBO.createFBO(width, height);
}

GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fbo.framebufferID);

GLES20.glViewport(0, 0, width, height);
GLES20.glClearColor(0, 0, 0, 0);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

GLES20.glUseProgram(mProgram);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texture);

// get handle to vertex shader's vPosition member
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
beautyPosition = GLES20.glGetUniformLocation(mProgram, "beauty");

GLES20.glUniform1i(beautyPosition,  isBeauty ? 1 : 0);

// Enable a handle to the triangle vertices
GLES20.glEnableVertexAttribArray(mPositionHandle);

// Prepare the <insert shape here> coordinate data
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, vertexBuffer);

mTextureCoordHandle = GLES20.glGetAttribLocation(mProgram, "inputTextureCoordinate");
GLES20.glEnableVertexAttribArray(mTextureCoordHandle);


GLES20.glVertexAttribPointer(mTextureCoordHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, textureVerticesBuffer);
GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawListBuffer);

// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);
GLES20.glDisableVertexAttribArray(mTextureCoordHandle);

// Switch to the default frame buffer
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);`

创建FBO

public class FBO {
       public int textureID;
    public int renderbuffersID;
    public int framebufferID;

    public static FBO createFBO(int width, int height) {
        FBO  fbo = new FBO();
        int[] texture = new int[1];
        int[] renderbuffersID = new int[1];
        int[] framebufferID = new int[1];

        GLES20.glGenTextures(1, texture, 0);
        GLES20.glGenFramebuffers(1, framebufferID, 0);
        GLES20.glGenRenderbuffers(1, renderbuffersID, 0);

        // renderbuffer
        GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, renderbuffersID[0]);
        GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, GLES20.GL_DEPTH_COMPONENT16, width, height);

        // framebuffer
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebufferID[0]);
        GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT, GLES20.GL_RENDERBUFFER,
                framebufferID[0]);

        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture[0]);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
        GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0,
                GLES20.GL_RGBA, width, height, 0,
                GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE,
                null);

        GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER,
                GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D,
                texture[0], 0);
        GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);

        fbo.textureID = texture[0];
        fbo.framebufferID = framebufferID[0];
        fbo.renderbuffersID = framebufferID[0];

        return fbo;
    }
}

滤镜处理

在这一步就是将前面附加的纹理作为纹理参数传递到Fragment shader当中,然后可以实现滤镜的效果了。

Fragment shader代码

precision mediump float;
varying vec2 textureCoordinate;
uniform sampler2D inputImageTexture;

void main() {
    vec4 col = texture2D(inputImageTexture, textureCoordinate);
    float h = dot(col.rgb, vec3(0.3, 0.59, 0.21));
    gl_FragColor = vec4(h, h, h, col.a);
}

滤镜代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void draw(int texture) {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture);
GLES20.glUseProgram(mProgram);
// get handle to vertex shader's vPosition member
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
// Enable a handle to the triangle vertices
GLES20.glEnableVertexAttribArray(mPositionHandle);
// Prepare the <insert shape here> coordinate data
GLES20.glVertexAttribPointer(mPositionHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, vertexBuffer);
mTextureCoordHandle = GLES20.glGetAttribLocation(mProgram, "inputTextureCoordinate");
GLES20.glEnableVertexAttribArray(mTextureCoordHandle);
GLES20.glVertexAttribPointer(mTextureCoordHandle, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, textureVerticesBuffer);
GLES20.glDrawElements(GLES20.GL_TRIANGLES, drawOrder.length, GLES20.GL_UNSIGNED_SHORT, drawListBuffer);
// Disable vertex array
GLES20.glDisableVertexAttribArray(mPositionHandle);
GLES20.glDisableVertexAttribArray(mTextureCoordHandle);
}

思考

实际上在自己实现的过程中发现应该是可以直接避免到自己创建FBO这个对象来实现简单的滤镜的效果,但是这样的话各种滤镜选择的逻辑的可能都会存在一个类当中,而且在这个绘制对应的Fragment Shader 也会存在这种需要变量和属性,但是对应的属性和变量又可能只在一个对象中用到,这就造成了部分代码文件及其臃肿,以至于根本无法扩展,而通过FBO 的方式,这样相当于形成了一个流水线的方式,我们只要拿到上一个步骤最终绘制的纹理,就可以继续拼接下一步操作,这样在整个流程中插入想要的步骤就十分容易了。