本项目基于OpenGL ES完成短视频的录制,添加美颜、贴纸,放大眼睛,实现快速、慢速播放,随意添加滤镜效果等功能。
完整项目代码可前往OpenGL短视频
OpenGL ES知识点
首先,创建C/C++ Surpport的项目,并在清单文件中添加ES20支持:
<uses-feature android:glEsVersion="0x00020000" android:required="true"/>
复制代码
初步了解一下几个需要用到的类、中间件:
-
GLSurfaceView
继承至SurfaceView,它内嵌的surface专门负责OpenGL渲染。
管理Surface与EGL
允许自定义渲染器(render)。
让渲染器在独立的线程里运作,和UI线程分离。
支持按需渲染(on-demand)和连续渲染(continuous)。
-
Render(渲染器接口中的三个方法需要实现)
-
GLThread(GLSurfaceView必须在这个线程里操作,不同于Android其它的View的绘制)
-
EGL:(Embeded Graphics Library)中间层连接OpenGL ES和本地窗口系统的接口,GLSurfaceView已搭建好这个中间件
创建自己的GLSurfaceview
定义一个DouyinView继承 GLSurfaceview,并且自定义一个DouyinRender继承自 Render,把DouyinRender集成到 DouyinView
public class DouyinView extends GLSurfaceView {
DouyinRender mRender;
public DouyinView(Context context) {
this(context, null);
}
public DouyinView(Context context, AttributeSet attrs) {
super(context, attrs);
setEGLContextClientVersion(2);
mRender = new DouyinRender(this);
setRenderer(mRender);
//设置按需渲染,当我们调用requestRender(); 请求GLThread回调一次onDrawFrame
setRenderMode(RENDERMODE_WHEN_DIRTY);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
super.surfaceDestroyed(holder);
mRender.onSurfaceDestroyed();
}
}
复制代码
public class DouyinRender implements GLSurfaceView.Renderer{
CameraHelper mCameraHelper;
SurfaceTexture mSurfaceTexture;
DouyinView mDouyinView;
int[] mTextures;
public DouyinRender(DouyinView douyinView){
this.mDouyinView = douyinView;
}
/**
* 创建好渲染器
*
* @param gl
* @param config
*/
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//初始化操作
mCameraHelper = new CameraHelper(Camera.CameraInfo.CAMERA_FACING_BACK);
//准备好画布
mTextures = new int[1];
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
//开启预览
mCameraHelper.startPreview(mSurfaceTexture);
mCameraFiliter.onReady(width, height);
mScreenFiliter.onReady(width, height);
}
public void onSurfaceDestroyed() {
mCameraHelper.stopPreview();
}
}
复制代码
着色器
OpenGL 是通过着色器来绘制图片,把物体分割成无数个三角形,把各个定点的坐标交给Vertex Shader(顶点着色器)确定图形形状,然后经过光栅化划分成无数个Fragments,然后把纹理交给Fragment Shader(片元着色器)进行上色,最终绘制到Window上去。
通过GLSL语言编写Vertex shader、Fragment shader,实现它们才是画画的重点。着色器(shader)它们都是运行在GPU 上的小程序,GPU并行处理的能力非常好。
- 顶点着色器(vertex shader)如何处理顶点、法线等数据的小程序
- 片元着色器(fragment shader)如何处理光、阴影、遮挡、环境等等对物体表面的影响,最终生成一副图像的小程序
如何编写Shader?
-
AS安装插件:GLSL Surpport,高亮显示
-
GLSL:OpenGL 着色语言(OpenGL shading Language)
-
数据类型
float
vec2 2个float类型
vec4 4个float类型
sampler2D 2D纹理采样器
-
修饰符
- attribute 属性变量。只能用于顶点着色器中。 一般用该变量来表示一些顶点数据,如:顶点坐标、纹理坐标、颜色等
- uniforms 一致变量。在着色器执行期间一致变量的值是不变的。与const常量不同的是,这个值在编译时期是未知的是由着色器外部初始化的。
- varying 易变变量。是从顶点着色器传递到片元着色器的数据变量。
编写shader,在资源文件raw下面创建 *.vert, *.frag 文件,后缀名无所谓,主要是给AS中GLSL插件高亮用的
如下的vertex shader文件base_vertex.vert
// 把顶点坐标给这个变量, 确定要画画的形状
attribute vec4 vPosition;
//接收纹理坐标,接收采样器采样图片的坐标
//不用和矩阵相乘了,接收一个点只要2个float就可以了,所以写成了vec2,而不是上节课的vec4
attribute vec2 vCoord;
//传给片元着色器 像素点
varying vec2 aCoord;
void main(){
//内置变量 gl_Position ,我们把顶点数据赋值给这个变量 opengl就知道它要画什么形状了
gl_Position = vPosition;
// 进过测试 和设备有关(有些设备直接就采集不到图像,有些呢则会镜像)
aCoord = vCoord;
}
复制代码
Fragment shader文件 base_frag.frag
//SurfaceTexture比较特殊
//float数据是什么精度的
precision mediump float;
//采样点的坐标
varying vec2 aCoord;
//采样器 不是从android的surfaceTexure中的纹理 采数据了,所以不再需要android的扩展纹理采样器了
//使用正常的 sampler2D
uniform sampler2D vTexture;
void main(){
//变量 接收像素值
// texture2D:采样器 采集 aCoord的像素
//赋值给 gl_FragColor 就可以了
gl_FragColor = texture2D(vTexture,aCoord);
}
复制代码
说明:varying变量将aCoord的值从Vertex shader传给 Fragment Shader; gl_Position, gl_FragColor 是内置变量。
编写Render
渲染需要首页用到Camera预览摄像,然后进行render,通过之前的项目了解到预览的三种方式:
- camera + surfaceview
- Camera + surfaceTexture + NativeWindow
- Camera + surfaceTexture + Opengl
同样,首先创建CameraHelper,在Render的onSurfaceCreate中初始化,在onSurfaceChange中开启渲染。
这里用上面的第三种方式,通过OpenGL 创建纹理 Texture,这里是传入数组。
/**
* 创建好渲染器
*/
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//初始化操作
mCameraHelper = new CameraHelper(Camera.CameraInfo.CAMERA_FACING_BACK);
//准备好画布
mTextures = new int[1];
//这里创建了纹理,直接应用了,没有配置。
GLES20.glGenTextures(mTextures.length, mTextures, 0);
mSurfaceTexture = new SurfaceTexture(mTextures[0]);
mSurfaceTexture.setOnFrameAvailableListener(this);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
//开启预览
mCameraHelper.startPreview(mSurfaceTexture);
}
复制代码
这里总体的渲染流程结构如下图,这里不能用ANative_Window, 因为图像被直接显示出来了
设置的接口 mSurfaceTexture.setOnFrameAvailableListener(this); 接口回调拿到有效的Frame的时候通过mDouyinview.requestRender()实现渲染,这样就刚好构成一个循环闭环,按需渲染,节省资源。
//有一个新的有效的图片的时候调用,让它调用onDrawFrame方法,通过GLSurfaceview的 requestRender()
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
//有数据时调用,省资源省电,然后调用 onDrawFrame,构成循环。
mDouyinView.requestRender();
}
复制代码
渲染核心
这里在Render的onDrawFrame中实现,首先告诉opengl按照RGBA清理屏幕,运行后,就会把你给的颜色绘到屏幕上
@Override
public void onDrawFrame(GL10 gl) {
//清理屏幕, 告诉opengl需要把屏幕清理成什么颜色
GLES20.glClearColor(0, 0, 0, 0);
//执行上一个:glClearColor配置的屏幕颜色
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
......
}
复制代码
在这里我们需要把Camera预览的data到这里渲染出来,Camera现在的数据现在都在SurfaceTexture中,我们需要更新纹理,然后从它当中获得数据进行渲染:
@Override
public void onDrawFrame(GL10 gl) {
......
//把摄像头的数据显示出来
//更新纹理,然后我们才能使用opengl从surfaceTexture当中获得数据进行渲染
mSurfaceTexture.updateTexImage();
//SurfaceTexture比较特殊, 采用的是sampleExtension(而不是用的Sample2)
//获得变换矩阵, 变换矩阵是一个 4 * 4 的矩阵
mSurfaceTexture.getTransformMatrix(mtx);
//进行画画
mCameraFiliter.setMatrix(mtx);
}
复制代码
这里为什么要给SurfaceTexture设置一个变换矩阵呢?先看一下OpenGL的坐标系。
OpenGL坐标系(二维)
- 世界坐标 定点坐标是按照 世界坐标来确定的。相当于我们画布的坐标,vertex shader绘制的坐标确定形状依赖世界坐标确定的。比如给一个三角形,需要给坐标的三个点。而项目中给的是Camera的预览,是个矩形,所以给的是以下四个点的坐标,构成两个三角形。
- 纹理坐标
- android屏幕坐标, 这个坐标我们很清楚。现在我们需要画矩形 , 由两个三角形来确定的,画好定点后,把Android屏幕坐标贴图到 世界坐标,需要矩阵变换。
矩阵变换
介绍完这些坐标,SurfaceTexture确定坐标需要一个变换举证,一般的我们采用Sample2D的采样器,而它需要用到的是SamplerExternalOES,如何运用这个变换矩阵呢?我们先设置一个顶点着色器的GLSL, 通过Java把 顶点的vPosition传入4个点的坐标这里对应的是世界坐标{ (-1,-1), (1, -1), (-1, 1), (1, 1) },然后给到 内置变量gl_Position,这样就 opengl就知道它要画什么形状了。
// 把顶点坐标给这个变量, 确定要画画的形状
attribute vec4 vPosition;
void main(){
//内置变量 gl_Position ,我们把顶点数据赋值给这个变量 opengl就知道它要画什么形状了
gl_Position = vPosition;
}
复制代码
再设置一个变量接受采样器采样图片的坐标,正常预览是android 屏幕坐标的4个点(01,11,00,10)。这几个值在顶点着色器是没有用的,通过定义的aCoord传给 Fragment Shader,通过 aCoord = (vMatrix*vCoord).xy;取到aCoord的值,它是片元着色器,只有两个坐标,对应的是像素点。顶点着色器通过光栅化会产生无数个点,每个点(x, y)就对应Fragment Shader的坐标。
//把顶点坐标给这个变量, 确定要画画的形状
attribute vec4 vPosition;
//接收纹理坐标,接收采样器采样图片的坐标
attribute vec4 vCoord;
//变换矩阵,需要将原本的vCoord(01,11,00,10)与矩阵相乘才能够得到surfacetexture
uniform mat4 vMatrix;
//传给片元着色器 像素点
varying vec2 aCoord;
void main(){
//内置变量 gl_Position ,我们把顶点数据赋值给这个变量 opengl就知道它要画什么形状了
gl_Position = vPosition;
// 进过测试 和设备有关(有些设备直接就采集不到图像,有些呢则会镜像)
aCoord = (vMatrix*vCoord).xy;
}
复制代码
然后来写对应的 Fragment Shader, 参照以下代码中的注释了解片元的写法。
#extension GL_OES_EGL_image_external:require
//SurfaceTexture比较特殊
//float数据是什么精度的
precision mediump float;
//采样点的坐标
varying vec2 aCoord;
//采样器 android的surfaceTexure中的纹理 采数据需要android的扩展纹理采样器了
uniform samplerExternalOES vTexture;
void main(){
//变量 接收像素值
// texture2D:采样器 采集 aCoord的像素
//赋值给 gl_FragColor 就可以了
gl_FragColor = texture2D(vTexture,aCoord);
}
复制代码
写了Vertex, Fragment Shader之后,OpenGL其实就可以实现将我们采集的摄像头的图像Render到我们的屏幕上去了,现在唯一要实现的是给 Vertex, Fragment Shader中的变量赋值,唯独除了varying vec2 aCoord,它负责从Vertex到Fragment内部传值的。
这里先创建渲染器 ScreenFiliter,首先需要读取Shader文件,通过工具包OpenGLUtils读取着色器的代码,然后渲染到屏幕:
//OpenGLUtils中代码
//从 shader文件读出 字符串
public static String readRawTextFile(Context context, int rawId) {
InputStream is = context.getResources().openRawResource(rawId);
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line;
StringBuilder sb = new StringBuilder();
try {
while ((line = br.readLine()) != null) {
sb.append(line);
sb.append("\n");
}
} catch (Exception e) {
e.printStackTrace();
}
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
复制代码
使用两个文件的字符串,创建顶点着色器,片元着色器、着色器程序,这里其实给的是对应的ID。
//顶点着色
protected int mVertexShaderId;
//片段着色
protected int mFragmentShaderId;
//着色器程序
protected int mGLProgramId;
//读取GLSL文件
protected void initilize(Context context) {
String vertexSharder = OpenGLUtils.readRawTextFile(context, mVertexShaderId);
String framentShader = OpenGLUtils.readRawTextFile(context, mFragmentShaderId);
mGLProgramId = OpenGLUtils.loadProgram(vertexSharder, framentShader);
........
// 获得着色器中的 attribute 变量 position 的索引值
vPosition = GLES20.glGetAttribLocation(mGLProgramId, "vPosition");
vCoord = GLES20.glGetAttribLocation(mGLProgramId,"vCoord");
vMatrix = GLES20.glGetUniformLocation(mGLProgramId,"vMatrix");
// 获得Uniform变量的索引值
vTexture = GLES20.glGetUniformLocation(mGLProgramId,"vTexture");
}
复制代码
OpenGLUtils.loadProgram:把创建的vShader, fShader给到着色器程序 program,它是运行在GPU上的。
public static int loadProgram(String vSource,String fSource){
//顶点着色器
int vShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
//加载着色器代码
GLES20.glShaderSource(vShader,vSource);
//编译(配置)
GLES20.glCompileShader(vShader);
//查看配置 是否成功
int[] status = new int[1];
GLES20.glGetShaderiv(vShader,GLES20.GL_COMPILE_STATUS,status,0);
if(status[0] != GLES20.GL_TRUE){//失败
throw new IllegalStateException("load vertex shader:"+ GLES20.glGetShaderInfoLog(vShader));
}
//片元着色器 流程和上面一样
int fShader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
//加载着色器代码
GLES20.glShaderSource(fShader,fSource);
//编译(配置)
GLES20.glCompileShader(fShader);
//查看配置 是否成功
GLES20.glGetShaderiv(fShader,GLES20.GL_COMPILE_STATUS,status,0);
if(status[0] != GLES20.GL_TRUE){//失败
throw new IllegalStateException("load fragment shader:"+GLES20.glGetShaderInfoLog(vShader));
}
//创建着色器程序
int program = GLES20.glCreateProgram();
//绑定顶点和片元
GLES20.glAttachShader(program,vShader);
GLES20.glAttachShader(program,fShader);
//链接着色器程序
GLES20.glLinkProgram(program);
//获得状态
GLES20.glGetProgramiv(program,GLES20.GL_LINK_STATUS,status,0);
if(status[0] != GLES20.GL_TRUE){
throw new IllegalStateException("link program:"+GLES20.glGetProgramInfoLog(program));
}
GLES20.glDeleteShader(vShader);
GLES20.glDeleteShader(fShader);
return program;
}
复制代码
然后通过 program进行画画,只需将mGLProgramId给到opengl,将Vertex, Fragment需要的变量值传进去就OK了, 这里先拿到 里面变量的索引,这个之前在init方法里实现了。
/**
* 顶点着色器
* attribute vec4 position;
* 赋值给gl_Position(顶点)
*/
protected int vPosition;
/**
* varying vec2 textureCoordinate;
*/
protected int vCoord;
/**
* uniform mat4 vMatrix;
*/
protected int vMatrix;
/**
* 片元着色器
* Samlpe2D 扩展 samplerExternalOES
*/
protected int vTexture;
//读取GLSL文件
protected void initilize(Context context) {
........
// 获得着色器中的 attribute 变量 position 的索引值
vPosition = GLES20.glGetAttribLocation(mGLProgramId, "vPosition");
vCoord = GLES20.glGetAttribLocation(mGLProgramId,"vCoord");
vMatrix = GLES20.glGetUniformLocation(mGLProgramId,"vMatrix");
// 获得Uniform变量的索引值
vTexture = GLES20.glGetUniformLocation(mGLProgramId,"vTexture");
}
复制代码
这里需要用到NIO的Buffer来存储数据,通过Buffer来传给对应的着色器
protected FloatBuffer mGLVertexBuffer;
protected FloatBuffer mGLTextureBuffer;
public ScreenFiliter(Context context, int vertexShaderId, int fragmentShaderId) {
this.mVertexShaderId = vertexShaderId;
this.mFragmentShaderId = fragmentShaderId;
// 4个点 x,y = 4*2 float 4字节 所以 4*2*4
mGLVertexBuffer = ByteBuffer.allocateDirect(4 * 2 * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
mGLVertexBuffer.clear();
float[] VERTEX = {
-1.0f, -1.0f,
1.0f, -1.0f,
-1.0f, 1.0f,
1.0f, 1.0f
};
mGLVertexBuffer.put(VERTEX);
mGLTextureBuffer = ByteBuffer.allocateDirect(4 * 2 * 4)
.order(ByteOrder.nativeOrder())
.asFloatBuffer();
mGLTextureBuffer.clear();
float[] TEXTURE = {
0.0f, 1.0f,
1.0f, 1.0f,
0.0f, 0.0f,
1.0f, 0.0f
};
mGLTextureBuffer.put(TEXTURE);
initilize(context);
initCoordinate();
}
复制代码
将以上的 Vertex,Fragment中对应需要接受值传递的 上面的索引,以及给了值Buffer应用到OpenGL
-
[ ] ```Java public int onDrawFrame(int textureId) { //设置显示窗口,这里传入的是全屏 GLES20.glViewport(0, 0, mOutputWidth, mOutputHeight); //使用着色器 GLES20.glUseProgram(mGLProgramId); //传递坐标 mGLVertexBuffer.position(0); GLES20.glVertexAttribPointer(vPosition,2,GLES20.GL_FLOAT, false,0,mGLVertexBuffer); GLES20.glEnableVertexAttribArray(vPosition);
//2、将纹理坐标传入,采样坐标 mGLTextureBuffer.position(0); GLES20.glVertexAttribPointer(vCoord,2,GLES20.GL_FLOAT,false, 0, mGLTextureBuffer); //传入数据后,激活 GLES20.glEnableVertexAttribArray(vCoord); //片元 vTexture 绑定图像数据到采样器 //激活图层, 第0层。它本身可以放多层数据 GLES20.glActiveTexture(GLES20.GL_TEXTURE0); //图像数据,将 //正常传GL_TEXTURE_2D, 这里需要穿GL_TEXTURE_external_oes GLES20.glBindTexture(GLES20.GL_TEXTURE_EXTERNAL_OES, textureId); //传递参数, 对应上面的第0层, 跟上面的对应 GLES20.glUniform1i(vTexture, 0); //参数传完了 通知opengl 画画 从第0点开始 共4个点 GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); GLES20.glBindTexture(GLES20.GL_TEXTURE_EXTERNAL_OES, 0); return textureId; } ``` 复制代码
上面的这些步骤都是在给 着色器传值,着色器已经给到 Program,最后通知OpenGL进行画画。GLES20.glBindTexture(GLES20.GL_TEXTURE_EXTERNAL_OES, 0);
这样就完成把Camera扫描的数据通过OpenGL绘制到屏幕上去了。