说到基于GPU的图像处理和实时滤镜,大家肯定会想到鼎鼎大名的GPUImage,这个项目确实为后续开发提供了很多方便,基本的图像处理工具一应俱全。IOS版的项目比较活跃,Android的好像已经被遗弃好久了,一堆bug没人解决。但是学习借鉴GPUImage的项目结构,可以为我们提供不小的帮助。
GPUImage项目结构
GPUImage的项目结构其实很简单,Android版本就更是简陋,结构如下:
- 一堆滤镜(shader以及配套设置参数的代码)
- FilterGroup(利用FBO进行同一副图像的多次处理)
- EGL管理类(主要用来做离屏渲染)
虽然GPUImage的主要价值在那堆滤镜上,但是我们主要来分析后面两个,这是GPUImage的框架,而滤镜就像插件一样,想插就插:D,我们也可以依葫芦画瓢定制自己的滤镜。
为什么要离屏渲染
离屏渲染的主要目的是在后台处理数据,做过Camera应用的都知道,如果用SurfaceView进行预览,那么就不得不把相机数据显示出来,为了不显示,就要把SurfaceView缩到很小,麻烦又浪费资源。Android 3.0后有了SurfaceTexture和GLSurfaceView,之后又有了TextureView,可以自由处理相机数据不显示出来了,但是依然有一个显示和绘制的过程。换句话说,TextureView和GLSurfaceView还不够听话,不能完成我们的所有要求。
如果我们只是想要利用GPU处理一张图片,但是不把他显示出来呢?
举个栗子
我们来看一下Camera360 Lite版的界面:
这些图片都是打开以后选择滤镜就能看到的,不用联网也可以,他们是APK自带的吗?为什么都是同一个人呢?
然而找了一圈以后,我只能在APK中找到这些:
不同颜色的大姐姐去哪了?
这就说明,这些不同的滤镜效果,其实是APK在第一次运行时,在用户手机上生成的。(可以自行查看Camera360 的data文件夹)
这样有很多好处呀,例如说大大减小了APK体积,同一套代码还可以用来完成不同的功能等。
当然,这只是离屏渲染的一个优点。
之前使用GLSurfaceView时,GLSurfaceView帮我们完成了EGL的环境配置,现在不使用GLSurfaceView,我们就要自行管理了,看看GPUImage是怎么做的吧:
GPUImage参考了GLSurfaceView,自己进行了OpenGL的环境配置(好像什么都没说啊,逃…
后面我们在分析GLSurfaceView的代码时,会再来说离屏渲染应该怎么做(毕竟环境配置什么的都是套路)
滤镜组与帧缓存对象(FBO)
GPUImage的滤镜组可以说是对这些滤镜的最好复用。借助于FrameBufferObject(FBO,帧缓存),我们可以在一幅图像上使用不同的滤镜组合来得到想要的结果。
再举个栗子:
我写了一个灰度滤镜,可以把图片转成黑白效果,代码如下:
precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D sTexture;
void main() {
vec3 centralColor = texture2D(sTexture, vTextureCoord).rgb;
gl_FragColor = vec4(0.299*centralColor.r+0.587*centralColor.g+0.114*centralColor.b);
}
有一天我闲的没事干,又写了一个反色滤镜:
precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D sTexture;
void main() {
vec4 centralColor = texture2D(sTexture, vTextureCoord);
gl_FragColor = vec4((1.0 - centralColor.rgb), centralColor.w);
}
现在Boss要求我对于视频流先进行黑白处理,再进行反色。
这点小事怎么难得到我呢,然后我花了10分钟写出了下面的代码:
precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D sTexture;
void main() {
vec4 centralColor = texture2D(sTexture, vTextureCoord);
gl_FragColor =vec4(0.299*centralColor.r+0.587*centralColor.g+0.114*centralColor.b);
gl_FragColor = vec4((1.0 - gl_FragColor.rgb), gl_FragColor.w);
}
这两个滤镜比较简单(只有一行),如果每个滤镜都很复杂呢?如果组合很多呢?
我们将两个功能写到了同一个滤镜里面,这样每次都要修改shader,一点都不优雅,一点都没有体现大学老师辛辛苦苦灌输的OO理念。
在GPUImage中,帧缓存对象就是用来解决这个问题的,之前我们都是一次性处理完就绘制到屏幕上了,现在不,我们可以将结果保存在帧缓存当中,然后再拿绘制结果作为下一次的输入数据来进行处理,于是我的代码就变成了:
filterGroup.addFilter(new GrayScaleShaderFilter(context));
filterGroup.addFilter(new InvertColorFilter(context));
如果还要有第三步处理怎么办?
再new一个呀!是不是很方便?
FBO的创建与绘制流程
首先我们需要两个数组,用来保存FBO的ID和绘制结果的纹理ID。
protected int[] frameBuffers = null;
protected int[] frameBufferTextures = null;
没错,FBO也像纹理一样,用一个数字表示。
if (frameBuffers == null) {
frameBuffers = new int[size-1];
frameBufferTextures = new int[size-1];
for (int i = 0; i < size-1; i++) {
GLES20.glGenFramebuffers(1, frameBuffers, i);
GLES20.glGenTextures(1, frameBufferTextures, i);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, frameBufferTextures[i]);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA,
filters.get(i).surfaceWidth, filters.get(i).surfaceHeight, 0,
GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffers[i]);
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
GLES20.GL_TEXTURE_2D, frameBufferTextures[i], 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
}
}
这里的代码比较长,但是和我们之前生成纹理的代码很相似(没有OpenGL ES基础的同学可以看这个)
- GLES20.glGenFramebuffers用来生成帧缓存对象
- 下面的一大段其实就是生成一个纹理并且用我们当前要绘制的长和宽对其进行配置,并且指定边界的处理情况,放大缩小的策略等
- 关键来了:我们用GLES20.glFramebufferTexture2D来把一幅纹理图像关联到一个帧缓存对象,告诉OpenGL这个FBO是用来关联一个2D纹理的,frameBufferTextures[i]就是和这个FBO关联的纹理
- 为什么是size-1呢,因为我们最后一个纹理是直接绘制到屏幕上的呀~
绘制
生成了FBO以后,我们就可以这样改写我们的绘制代码
if (i < size - 1) {
GLES20.glViewport(0, 0, filter.surfaceWidth, filter.surfaceHeight);
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffers[i]);
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
filter.onDrawFrame(previousTexture);
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
previousTexture = frameBufferTextures[i];
}else{
GLES20.glViewport(0, 0 ,filter.surfaceWidth, filter.surfaceHeight);
filter.onDrawFrame(previousTexture);
}
- 每次绘制之前使用glBindFramebuffer绑定FBO,然后这次我们绘制的结果就不会显示在屏幕上,而是变成了一个刚才和FBO绑定的纹理对象,然后再用这个纹理给下一个滤镜作为输入
- 第一个滤镜的输入就是我们的相机或者播放器对应的纹理
- 最后一个滤镜不需要再输出到FBO了,因此直接绘制出来就好。
滤镜组完整代码
package com.martin.ads.omoshiroilib.filter.base;
import android.opengl.GLES20;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
/**
* Created by Ads on 2016/11/19.
*/
public class FilterGroup extends AbsFilter {
private static final String TAG = "FilterGroup";
protected int[] frameBuffers = null;
protected int[] frameBufferTextures = null;
protected List<AbsFilter> filters;
protected boolean isRunning;
public FilterGroup() {
super("FilterGroup");
filters=new ArrayList<AbsFilter>();
}
@Override
public void init() {
for (AbsFilter filter : filters) {
filter.init();
}
isRunning=true;
}
@Override
public void onPreDrawElements() {
}
@Override
public void destroy() {
destroyFrameBuffers();
for (AbsFilter filter : filters) {
filter.destroy();
}
isRunning=false;
}
@Override
public void onDrawFrame(int textureId) {
runPreDrawTasks();
if (frameBuffers == null || frameBufferTextures == null) {
return ;
}
int size = filters.size();
int previousTexture = textureId;
for (int i = 0; i < size; i++) {
AbsFilter filter = filters.get(i);
Log.d(TAG, "onDrawFrame: "+i+" / "+size +" "+filter.getClass().getSimpleName()+" "+
filter.surfaceWidth+" "+filter.surfaceHeight);
if (i < size - 1) {
GLES20.glViewport(0, 0, filter.surfaceWidth, filter.surfaceHeight);
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffers[i]);
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
filter.onDrawFrame(previousTexture);
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
previousTexture = frameBufferTextures[i];
}else{
GLES20.glViewport(0, 0 ,filter.surfaceWidth, filter.surfaceHeight);
filter.onDrawFrame(previousTexture);
}
}
}
@Override
public void onFilterChanged(int surfaceWidth, int surfaceHeight) {
super.onFilterChanged(surfaceWidth, surfaceHeight);
int size = filters.size();
for (int i = 0; i < size; i++) {
filters.get(i).onFilterChanged(surfaceWidth, surfaceHeight);
}
if(frameBuffers != null){
destroyFrameBuffers();
}
if (frameBuffers == null) {
frameBuffers = new int[size-1];
frameBufferTextures = new int[size-1];
for (int i = 0; i < size-1; i++) {
GLES20.glGenFramebuffers(1, frameBuffers, i);
GLES20.glGenTextures(1, frameBufferTextures, i);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, frameBufferTextures[i]);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA,
filters.get(i).surfaceWidth, filters.get(i).surfaceHeight, 0,
GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBuffers[i]);
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
GLES20.GL_TEXTURE_2D, frameBufferTextures[i], 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
}
}
}
private void destroyFrameBuffers() {
if (frameBufferTextures != null) {
GLES20.glDeleteTextures(frameBufferTextures.length, frameBufferTextures, 0);
frameBufferTextures = null;
}
if (frameBuffers != null) {
GLES20.glDeleteFramebuffers(frameBuffers.length, frameBuffers, 0);
frameBuffers = null;
}
}
public void addFilter(final AbsFilter filter){
if (filter==null) return;
//If one filter is added multiple times,
//it will execute the same times
//BTW: Pay attention to the order of execution
if (!isRunning){
filters.add(filter);
}
else
addPreDrawTask(new Runnable() {
@Override
public void run() {
filter.init();
filters.add(filter);
onFilterChanged(surfaceWidth,surfaceHeight);
}
});
}
public void addFilterList(final List<AbsFilter> filterList){
if (filterList==null) return;
//If one filter is added multiple times,
//it will execute the same times
//BTW: Pay attention to the order of execution
if (!isRunning){
for(AbsFilter filter:filterList){
filters.add(filter);
}
}
else
addPreDrawTask(new Runnable() {
@Override
public void run() {
for(AbsFilter filter:filterList){
filter.init();
filters.add(filter);
}
onFilterChanged(surfaceWidth,surfaceHeight);
}
});
}
}