IBigerBiger的成长之路

移动端滤镜开发(二)初识OpenGl

上一篇文章对Android端的ColorMatrix进行了讲解,虽然说可以满足我们做滤镜的简单需求,但是对于视频和相机这些更多方面的滤镜需求就不能够满足需求了,所以为了滤镜可以在更多场景使用,选择用OpenGL来实现滤镜效果,由于OpenGL是通过GPU来计算的,所以处理起来速度会更快,也会避免由于图像计算较复杂带来的OOM问题。

但是对于广大的开发者来说,OpenGl是一个相对来说比较陌生的东西,可能大家都听但是不是特别了解,所以这篇文章就对OpenGL进行简单的介绍,并实现图片用OpenGL显示出来。

一.OpenGl基础知识

  • OpenGL 绘制的都是图形,包括形状和填充,基本形状是三角形。
  • 每个形状都有顶点,Vertix,顶点的序列就是一个图形。
  • Shader,着色器,用来描述如何绘制(渲染),GLSL 是 OpenGL 的编程语言,全称就叫 OpenGL Shader Language。OpenGL 渲染需要两种 shader,vertex 和 fragment。
  • Vertex shader(顶点着色器),控制顶点的绘制,指定坐标、变换等。
  • Fragment shader(片段着色器),控制形状内区域渲染,纹理填充内容。

OpenGl的坐标系与Android的坐标系不同,是三维坐标系,原点在中间,x 轴向右,y 轴向上,z 轴朝向我们,x y z 取值范围都是 [-1, 1]

如图所示


图1 OpenGl坐标系

OpenGL 纹理坐标系为二维坐标系,原点在左下角,s(x)轴向右,t(y)轴向上,x y 取值范围都是 [0, 1]:

如图所示


图2 纹理坐标系

其实通过介绍这些基础知识,大家应该可以对OpenGl基本的实现有点思路了。

注意:OpenGL ES 2.0需要Android2.2 (API Level 8) 及以上版本,所以请确保你的Android项目的运行目标的API等级不低于8或更高。

二.OpenGl简单构造

与我们创建界面时候添加的View一样,实现OpenGl则需要GLSurfaceView,这是让我们渲染的”画布”。

1.构造一个GLSurfaceView对象

事实上GLSurfaceView并没有提供很多功能,实际上绘制对象的任务都在GLSurfaceView.Renderer中进行。所以GLSurfaceView中代码也非常少,甚至可以直接使用GLSurfaceView。但最好别这样做,因为你需要扩展这个类来响应触摸事件。

1
2
3
4
5
6
7
8
9
10
public class MyGLSurfaceView extends GLSurfaceView{
public MyGLSurfaceView(Context context) {
this(context,null);
}
public MyGLSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
}
}

2.构造一个Renderer类

Renderer类负责控制在GLSurfaceView中绘制任务,并提供三个回调方法供Android系统调用,用来计算在GLSurfaceView中绘制什么以及如何绘制。

  • onSurfaceCreated():仅调用一次,用于设置view的OpenGL ES环境。
  • onDrawFrame():每次重绘view时调用。
  • onSurfaceChanged():当view的几何形状发生变化时调用,比如设备从竖屏变为横屏。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class MyRender implements GLSurfaceView.Renderer{
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
    // 设置背景色
    GLES20.glClearColor(0.5f, 0.5f, 0.5f, 1.0f);
    }
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
    GLES20.glViewport(0, 0, width, height);
    }
    @Override
    public void onDrawFrame(GL10 gl) {
    // 重绘背景色
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
    }
    }

这里只是创建了一个带颜色的显示,并没有做任何事情

3.GLSurfaceView与Renderer关联

设置渲染对象,用于控制在GLSurfaceView中的绘制工作

1
setRenderer(new MyRenderer());

指定使用的OpenGl版本

1
setEGLContextClientVersion(2);

指定刷新方式(刷新方式有两种, RENDERMODE_WHEN_DIRTY 和 RENDERMODE_CONTINUOUSLY ,前者是懒惰渲染,需要手动调用 glSurfaceView.requestRender() 才会进行更新,而后者则是不停渲染。)

1
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

4.Activity中显示

1
2
3
4
5
6
7
8
9
10
11
12
public class OpenGLActivity extends Activity {
private GLSurfaceView mGLSurfaceView;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mGLSurfaceView = new MyGLSurfaceView(this);
setContentView(mGLSurfaceView);
}
}

这里就是创建一个GLSurfaceView对象,并将其设置为当前Activity的ContentView

运行如下


图3 仅带颜色的GLSurfaceView

三.OpenGl实现Hello World

OpenGl的Hello World当然不是我们认知的Hello World,而是实现一个最基础的图形显示

通过上面的简单构造我们可以知道,渲染这部分其实是在Renderer里面去实现的,所以其实我们需要改动的地方就是Renderer这部分

接下来我们一步一步来实现显示一个三角形图像

1.变量

前面最开始提到的两个着色器并没有在上个部分有体现,而当我们实现真正的图像和图形,时候则需要用顶点着色器对图像定位,片段着色器对图像进行渲染

所以首先我们声明两个着色器程序

着色器程序则是用着色器语言来写的,至于着色器语言的知识,这里我就不介绍了,大家可以自行搜索,简单的了解下就可以继续后面的流程了

顶点着色器程序

1
2
3
4
private static final String VERTEX_SHADER = "attribute vec4 vPosition;\n"
+ "void main() {\n"
+ " gl_Position = vPosition;\n"
+ "}";

这里做的事情是将传过来的坐标作为顶点的位置

片段着色器程序

1
2
3
4
private static final String FRAGMENT_SHADER = "precision mediump float;\n"
+ "void main() {\n"
+ " gl_FragColor = vec4(0,0,1,1);\n"
+ "}";

这里做的事情是将纹理的颜色设置为蓝色

顶点坐标数组

1
2
3
4
5
private static final float[] VERTEX = {
0.0f, 1.0f, 0.0f, // top
-1.0f, -1.0f, 0.0f, // bottom left
1.0ff, -1.0f, 0.0f, // bottom right
};

FloatBuffer存储顶点坐标数组/片段坐标数组

1
private final FloatBuffer mVertexBuffer;

这里我们只需要把顶点坐标传给着色器,所以只声明了顶点所用的FloatBuffer,由于不能直接把上面的顶点坐标数组直接传给着色器,所以需要FloatBuffer来进行传递

接下来初始化FloatBuffer,将数组传递给FloatBuffer

1
2
3
4
5
mVertexBuffer = ByteBuffer.allocateDirect(VERTEX.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(VERTEX);
mVertexBuffer.position(0);

定义需要传递的变量

前面的顶点着色器需要将外部的顶点坐标传入,所以我们要先定义一个变量,并将这个变量与着色器语言中需要传入的变量关联起来

1
private int mPositionHandle;

定义GLSL程序

1
private int mProgram;
2.构建GLSL程序

构建步骤如下

  • 创建 GLSL 程序:
  • 加载 shader 代码
  • attatch shader 代码
  • 链接 shader 代码
  • 获取 shader 代码中的变量索引

和普通的 view 利用 canvas 来绘制不一样,OpenGL 需要加载 GLSL 程序,让 GPU 进行绘制。所以我们需要定义 shader 代码,并在 onSurfaceChanged回调中加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
mProgram = GLES20.glCreateProgram();
int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, VERTEX_SHADER);
int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, FRAGMENT_SHADER);
GLES20.glAttachShader(mProgram, vertexShader);
GLES20.glAttachShader(mProgram, fragmentShader);
GLES20.glLinkProgram(mProgram);
mPositionHandle = GLES20.glGetAttribLocation(mProgram, "vPosition");
}
//加载Shader代码
static int loadShader(int type, String shaderCode) {
int shader = GLES20.glCreateShader(type);
GLES20.glShaderSource(shader, shaderCode);
GLES20.glCompileShader(shader);
return shader;
}

就不对代码进行分析了,按照上面的步骤基本是对应的

3.绘制

绘制则是在onDrawFrame回调中加载

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
28
29
30
@Override
public void onDrawFrame(GL10 unused) {
//清除指定的buffer到预设值。可清除以下四类buffer:
//1)GL_COLOR_BUFFER_BIT
//2)GL_DEPTH_BUFFER_BIT
//3)GL_ACCUM_BUFFER_BIT
//4)GL_STENCIL_BUFFER_BIT
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
//安装一个program object,并把它作为当前rendering state的一部分。
GLES20.glUseProgram(mProgram);
//Enable由索引index指定的通用顶点属性数组。
GLES20.glEnableVertexAttribArray(mPositionHandle);
//定义一个通用顶点属性数组。当渲染时,它指定了通用顶点属性数组从索引index处开始的位置和数据格式
GLES20.glVertexAttribPointer(mPositionHandle, 3, GLES20.GL_FLOAT, false,
12, mVertexBuffer);
//三个成员变量mode,first,count
//1) mode:指明render原语,如:GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES, GL_QUAD_STRIP, GL_QUADS, 和 GL_POLYGON。
//2) first: 指明Enable数组中起始索引。
//3) count: 指明被render的原语个数。
//可以预先使用单独的数据定义vertex、normal和color,然后通过一个简单的glDrawArrays构造一系列原语。当调用 glDrawArrays时,它使用每个enable的数组中的count个连续的元素,来构造一系列几何原语,从第first个元素开始
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);
//Disable由索引index指定的通用顶点属性数组。
GLES20.glDisableVertexAttribArray(mPositionHandle);
}

注释已经写的很详细了,就不详细分析了

接下来运行如图


图4 蓝色的三角形

四.OpenGl实现图像显示

经过上面的讲解大家应该对OpenGl简单使用有一定的了解了吧,OK,接下来回到我们这个系统最初的目的,首先我们来实现用OpenGl展示图片

其实将上面的代码进行简单的修改就可以实现需要的效果

接下来一步一步实现

1.绘制矩形


这一步其实很简单,前面的三角形是绘制了三个点,那么这里绘制矩形的话,我们绘制两个三角形,六个点,自然就实现了

代码如下

1
2
3
4
5
6
7
8
9
10
private static final float[] VERTEX = {
-1.0f, -1.0f, 0.0f, // bottom left
1.0f, -1.0f, 0.0f, // bottom right
1.0f, 1.0f, 0.0f, // top right
-1.0f, -1.0f, 0.0f,
1.0f, 1.0f, 0.0f,
-1f, 1.0f, 0.0f,
};
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6);

当然除了这样以为,我们注意一下glDrawArrays这个第一个参数,我们是使用的GLES20.GL_TRIANGLES,当然除了这个参数,还有另外两个与TRIANGLES的参数,我们来看下区分

  • GL_TRIANGLES:每三个顶之间绘制三角形,之间不连接
  • GL_TRIANGLE_FAN:以V0V1V2,V0V2V3,V0V3V4,……的形式绘制三角形
  • GL_TRIANGLE_STRIP:顺序在每三个顶点之间均绘制三角形。这个方法可以保证从相同的方向上所有三角形均被绘制。以V0V1V2,V1V2V3,V2V3V4……的形式绘制三角形

所以其实我们并不需要6个点那么多,四个点就足够了

如下

1
2
3
4
5
6
7
8
private static final float[] VERTEX = {
-1.0f, -1.0f, 0.0f,
1.0f, -1.0f, 0.0f,
-1.0f, 1.0f, 0.0f,
1.0f, 1.0f, 0.0f,
};
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

运行如下


图5 矩形
2.着色器程序修改

因为我们要显示图片,自然之前的着色器程序是不能满足需求的,那么我们简单分析一下,怎么才能把图片渲染上去呢?首先图片纹理要传进去,其次还得知道什么地方绘制

所以对上面的着色器程序进行修改

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
private static final String VERTEX_SHADER = "attribute vec4 vPosition;" +
"attribute vec2 a_texCoord;" +
"varying vec2 v_texCoord;" +
"void main() {" +
" gl_Position = vPosition;" +
" v_texCoord = a_texCoord;" +
"}";
private static final String FRAGMENT_SHADER = "precision mediump float;" +
"varying vec2 v_texCoord;" +
"uniform sampler2D s_texture;" +
"void main() {" +
" gl_FragColor = texture2D( s_texture, v_texCoord );" +
"}";

我们可以看到增加的东西是varying vec2 v_texCoord与uniform sampler2D s_texture,第一是代表纹理坐标点,第二个代表图片纹理,通过GLSL的内建函数texture2D来获取对应位置纹理的颜色RGBA值

这里出现了更多的关键字, uniform , attribute , varying ,包括上面的内建函数,这里并不做讲解,有兴趣的同学去搜索一下就可以了解了解

3.绘制图片纹理

我们已经对着色器程序修改,接下来自然就是去将需要的坐标和纹理传给着色器程序

纹理坐标系文章最开始有提到,所以我们先创建纹理坐标数组,并初始化

(1)建纹理坐标数组,并初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
private static final float[] UV_TEX_VERTEX = { // in clockwise order:
0.0f, 1.0f,
1.0f, 1.0f,
0.0f, 0.0f,
1.0f, 0.0f,
};
FloatBuffer mUvTexVertexBuffer;
mUvTexVertexBuffer = ByteBuffer.allocateDirect(UV_TEX_VERTEX.length * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer()
.put(UV_TEX_VERTEX);
mUvTexVertexBuffer.position(0);

接下来生成图片纹理

(2)生成图片纹理

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
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
...
int[] mTexNames = new int[1];
GLES20.glGenTextures(1, mTexNames, 0);
Bitmap bitmap = BitmapFactory.decodeResource(mResources, R.drawable.p_300px);
//选择当前活跃的纹理源
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
//将生成的纹理的名称绑定到指定的纹理上
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTexNames[0]);
//设置纹理被缩小(距离视点很远时被缩小)时候的滤波方式
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
GLES20.GL_LINEAR);
//设置纹理被放大(距离视点很近时被方法)时候的滤波方式
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER,
GLES20.GL_LINEAR);
//设置在横向、纵向上都是位于纹理边缘或者靠近纹理边缘的纹理单元将用于纹理计算,但不使用纹理边框上的纹理单元。
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,
GLES20.GL_REPEAT);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,
GLES20.GL_REPEAT);
// 加载位图生成纹理
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
bitmap.recycle();
...
}

大家要注意 GLES20.glActiveTexture(GLES20.GL_TEXTURE0)这个方法,这是生成纹理,在OpenGl里面有很多个这种GL_TEXTURE表示不同纹理

(3)获取 shader 代码中的新的变量索引

1
2
3
4
5
6
7
8
9
10
private int mTexCoordHandle;
private int mTexSamplerHandle;
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
...
mTexCoordHandle = GLES20.glGetAttribLocation(mProgram, "a_texCoord");
mTexSamplerHandle = GLES20.glGetUniformLocation(mProgram, "s_texture");
...
}

(4)绘制

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void onDrawFrame(GL10 gl) {
...
GLES20.glEnableVertexAttribArray(mTexCoordHandle);
GLES20.glVertexAttribPointer(mTexCoordHandle, 2, GLES20.GL_FLOAT, false, 0,
mUvTexVertexBuffer);
GLES20.glUniform1i(mTexSamplerHandle, 0);
GLES20.glDisableVertexAttribArray(mTexCoordHandle);
...
}

注意这里的glUniform1i方法,这里的第二个参数和前面提到的glActiveTexture方法的后缀要一致

到这里基本就结束了,运行如下


图6 图像显示

写在后面的话

通过这么一大串的内容,我相信大家应该都对OpenGl在安卓上面的实现有一定了解了吧,关于GLSL语言呢,大家可以自行去了解一下,这会对后面的文章有一定帮助,下一篇则是介绍如何用OpenGl实现摄像机预览与音频播放,peace~~