前言
在上一篇博客中,简单介绍了一下有关于OpenGL的基础内容,没看过的,可以看一下OpenGL ES基础,如果对里面有很多内容还是不懂的话,就百度一下吧,里面我都是简单说了一下大概内容,从这一篇开始,用仿抖音的项目来一步步具体介绍怎么在Android中使用OpenGL。
首先抖音其实就是录制前处理和录制后特效的处理,今天先来第一步使用OpenGL显示摄像头,为后面的工作做准备。
需求
使用OpenGL显示摄像头,分析一下需求,其实也就是两步,第一步采集摄像头数据,第二步将摄像头数据显示到屏幕上
采集摄像头数据
使用Android的Camera就可以实现采集
将摄像头数据显示到屏幕上
这里显示到屏幕上,其实有很多方法,比如ANative_window等等,但是这个是使用OpenGL实现,使用OpenGL怎么实现呢,在上一篇博客中说到了OpenGL的绘制流程,基本就是按照那个流程进行实现的。
下面是项目结构中一个不太规范的类图,下面根据这种图来实现具体的代码
简单点说,我们都知道SurfaceView实质是将底层显存Surface显示到界面上,而GLSurfaceView实际就是在这个基础之上增加了OpenGL环境,DouyinView实际就相当于一块画布,而ScreenFilter中是封装了如何使用画笔去画当前摄像头采集到的内容,而DouyinRender渲染器,就是将ScreenFilter中的画渲染到画布上。
创建工程
创建一个JNI工程,就是在一开始创建项目的时候,勾选上 Include C++ support,这样就创建好了
配置文件 AndroidManifest.xml
OpenGL的使用还需要有设备制造商提供支持,以下是设备的支持情况,项目里都是用来2.0
OpenGL ES 1.0 和 1.1 :Android 1.0和更高的版本支持这个API规范。
OpenGL ES 2.0 :Android 2.2(API 8)和更高的版本支持这个API规范。
OpenGL ES 3.0 :Android 4.3(API 18)和更高的版本支持这个API规范。
OpenGL ES 3.1 : Android 5.0(API 21)和更高的版本支持这个API规范。
DouyinView.java
DouyinRender.java
这里使用的Render,是实现了OpenGL配置好EGL的渲染器,后面会自己来配置,这里先使用这个,上面也有提到过,OpenGL的操作都是在GLThread线程中完成,所以这里的onSurfaceCreated,onSurfaceChanged,onDrawFrame都是在GLThread中实现的,下面单独看看每个方法的使用
CameraHelper是封装了对Camera的一系列操作
创建着色器
AS里面有个插件可以支持GLSL的高亮显示,在plugin里面搜索 GLSL Support
顶点着色器
片元着色器
这里我们需要画的是矩形,也就是两个三角形,给了4个点的坐标,这4个点,会执行4次顶点着色器,依次将顶点传递给gl_Position,当4个点都传递完成之后,会进行光栅化,将一个矩形变换成一个个的片元,然后再去使用gl_FragColor去着色
因为SurfaceTexture是Android中的,并不是OpenGL的,所以需要使用额外扩展的采样器samplerExternalOES,而不是普通的sample2D采样器,在使用samplerExternalOES时候,需要再添加一句:#extension GL_OES_EGL_image_external : require
ScreenFilter.java
public ScreenFilter(Context context) {
//把camera_vertext内容读出来
String vertexSource= OpenUtils.readRawTextFile(context, R.raw.camera_vertex);
String fragSource = OpenUtils.readRawTextFile(context, R.raw.camera_frag);
//通过字符串创建着色器程序
//使用opengl
//一.顶点着色器
//1.1创建顶点着色器
int vSharderId = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
//1.2绑定代码到着色器中
GLES20.glShaderSource(vSharderId,vertexSource);
//1.3编译着色器
GLES20.glCompileShader(vSharderId);
//1.4主动获取成功失败
int[] status=new int[1];
GLES20.glGetShaderiv(vSharderId,GLES20.GL_COMPILE_STATUS,status,0);
if (status[0] != GLES20.GL_TRUE){
throw new IllegalStateException("ScreenFitler 顶点着色器配置失败!");
}
//二.创建片元着色器
int fShaderId = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
//绑定代码到着色器中
GLES20.glShaderSource(fShaderId,fragSource);
//编译
GLES20.glCompileShader(fShaderId);
GLES20.glGetShaderiv(fShaderId,GLES20.GL_COMPILE_STATUS,status,0);
if (status[0] != GLES20.GL_TRUE){
throw new IllegalStateException("ScreenFilter 片元着色器配置失败");
}
//三、把着色器塞到程序里面去,GPU
mProgram = GLES20.glCreateProgram();
GLES20.glAttachShader(mProgram,vSharderId);
GLES20.glAttachShader(mProgram,fShaderId);
//连接着色器
GLES20.glLinkProgram(mProgram);
//获取程序是否配置成功
GLES20.glGetProgramiv(mProgram,GLES20.GL_LINK_STATUS,status,0);
if (status[0] != GLES20.GL_TRUE){
throw new IllegalStateException("Screen Filter 着色器程序连接失败");
}
//因为着色器已经塞到了GPU程序中,所以可以删除了
GLES20.glDeleteShader(vSharderId);
GLES20.glDeleteShader(fShaderId);
//获得着色器程序中的变量的索引,通过这个索引对其进行赋值
vPosition = GLES20.glGetAttribLocation(mProgram,"vPosition");
vCoord = GLES20.glGetAttribLocation(mProgram,"vCoord");
vMatrix = GLES20.glGetUniformLocation(mProgram,"vMatrix");
vTexture = GLES20.glGetUniformLocation(mProgram,"vTexture");
//创建一个数据缓冲区
//OpenGL的中顶点的位置坐标
mVertextBuffer = ByteBuffer.allocateDirect(4 * 2 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
mVertextBuffer.clear();
float[] v = {-1.0f,-1.0f,
1.0f,-1.0f,
-1.0f,1.0f,
1.0f,1.0f
};
mVertextBuffer.put(v);
//采样器采样图片的坐标
mTextureBuffer = ByteBuffer.allocateDirect(4 * 2 * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
mTextureBuffer.clear();
// float[] f={0.0f,1.0f,
// 1.0f,1.0f,
// 0.0f,0.0f,
// 1.0f,0.0f
// };
//
//顺时针旋转90度
// float[] f={1.0f,1.0f,
// 1,0,
// 0,1,
// 0,0
//
//
// };
// 镜像
float[] f={1.0f,0.0f,
1.0f,1.0f,
0.0f,0.0f,
0.0f,1.0f
};
mTextureBuffer.put(f);
}
/**
* 使用着色器程序开始画画
* @param texture
* @param mtx
*/
public void onDrawFrame(int texture,float[] mtx){
//1.设置窗口大小
GLES20.glViewport(0,0,mWidth,mHeight);
//2.使用着色器程序
GLES20.glUseProgram(mProgram);
/**
* 3. 画顶点
*/
//3.1传入顶点数据,确定形状
mVertextBuffer.position(0);
//size:2表示xy两个数据
GLES20.glVertexAttribPointer(vPosition,2,GLES20.GL_FLOAT,false,0,mVertextBuffer);
//3.2激活
GLES20.glEnableVertexAttribArray(vPosition);
//4,片元,纹理
mTextureBuffer.position(0);
GLES20.glVertexAttribPointer(vCoord,2,GLES20.GL_FLOAT,false,0,mTextureBuffer);
GLES20.glEnableVertexAttribArray(vCoord);
//5.变换矩阵
GLES20.glUniformMatrix4fv(vMatrix,1,false,mtx,0);
//片元
//激活图层
GLES20.glActiveTexture(GLES20.GL_TEXTURE);
//图像数据
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES,texture);
//传递参数
GLES20.glUniform1i(vTexture,0);
//参数传完了,通知OpenGL画画,从第0个点开始,共4个点
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0,4);
}
Tips
因为OpenGL中如果显示不正确,错误一般都比较难找,所以在代码里有在一些地方去获取是否配置正确,来方便找错
记录我遇到的两个bug,如果遇到问题可做参考,第一个是在获取索引的时候,类型弄错了。第二个是在顶点着色器中,获取像素点aCoord进行矩阵变换的时候,写成了纹理坐标 * 矩阵,这样出来的结果是错的,一定要是 矩阵 * 纹理坐标。