OpenGL.ES在Android上的简单实践:
19-水印录制(EGL+摄像头预览 GL_TEXTURE_EXTERNAL_OES)
0、补充EglSurfaceBase
在自己实际运用中,发现EglSurfaceBase还是缺了对原生的surface的管理,对整体的理解好像总缺了点啥。所以在EglSurfaceBase的基础上,派生出了WindowSurface。代码超级简单的,但从理解学习上就完全不同一个台阶了。
public class WindowSurface extends EglSurfaceBase { private Surface mSurface; private boolean bReleaseSurface; //将native的surface 与 EGL关联起来 public WindowSurface(EglCore eglCore, Surface surface, boolean isReleaseSurface) { createWindowSurface(surface); bReleaseSurface = isReleaseSurface; //将SurfaceTexture 与 EGL关联起来 protected WindowSurface(EglCore eglCore, SurfaceTexture surfaceTexture) { createWindowSurface(surfaceTexture); //释放当前EGL上下文 关联 的 surface
那么接下来,我们就要快速开始预览摄像头。从前篇的ContinuousRecordActivity开始代码:
public class ContinuousRecordActivity extends Activity implements SurfaceHolder.Callback { public static final String TAG = "ContinuousRecord"; // 因为Androi的摄像头默认是横着方向,所以width>height private static final int VIDEO_WIDTH = 1280; private static final int VIDEO_HEIGHT = 720; private static final int DESIRED_PREVIEW_FPS = 15; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.continuous_record); sv = (SurfaceView) findViewById(R.id.continuousRecord_surfaceView); SurfaceHolder sh = sv.getHolder(); protected void onResume() { openCamera(VIDEO_WIDTH, VIDEO_HEIGHT, DESIRED_PREVIEW_FPS); protected void onPause() { private EglCore mEglCore; private WindowSurface mDisplaySurface; public void surfaceCreated(SurfaceHolder surfaceHolder) { Log.d(TAG, "surfaceCreated holder=" + surfaceHolder); // 准备好EGL环境,创建渲染介质mDisplaySurface mEglCore = new EglCore(null, EglCore.FLAG_RECORDABLE); mDisplaySurface = new WindowSurface(mEglCore, surfaceHolder.getSurface(), false); mDisplaySurface.makeCurrent(); //mTextureId = createTextureObject(); //mCameraTexture = new SurfaceTexture(mTextureId); //mCameraTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() { // public void onFrameAvailable(SurfaceTexture surfaceTexture) { // Handler.sendEmptyMessage(MSG_FRAME_AVAILABLE); Log.d(TAG, "starting camera preview"); //mCamera.setPreviewTexture(mCameraTexture); } catch (IOException ioe) { throw new RuntimeException(ioe); (省略 openCamera releaseCamera surfaceChanged 和 surfaceDestroy等不是重点的代码,节省篇幅。)
我们先分析以上代码,首先我们运用前篇文章的EGL,在合适时间(surfaceCreated)创建了能自己管控的EGL,并把surface和EGL绑定成我们这个项目的OpenGL渲染界面EGLSurface。接下来我们看看注释部分的代码,要先理解好这部分,我们才能继续下去。
在我们创建好EGLSurface后,我们要创建一个纹理对象并保存其纹理ID->mTextureId;这个纹理有啥子用?它是用来创建SurfaceTexture对象的,而这个SurfaceTexture又是啥子用啊?这是用来承接摄像头mCamera的预览帧。
啥,风太大听不懂?额,逻辑道理确实就是这样,详情请参考这里。重点查阅以下这段内容:
首先,SurfaceTexture从图像流(来自Camera预览,视频解码,GL绘制场景等)中获得帧数据,当调用updateTexImage()时,根据内容流中最近的图像更新SurfaceTexture对应的GL纹理对象,接下来,就可以像操作普通GL纹理一样操作它了。SurfaceTexture.OnFrameAvailableListener用于让SurfaceTexture的使用者知道有新数据到来。
所以我们可以这样理解,就是我们通过这个SurfaceTexture从Camera接取预览帧的图像流,然后我们就可以通过其绑定的GL纹理对象,直接按照纹理使用绘制了。
但这个纹理比较特殊,我们来show下代码:
public static final String TAG = "ZZR-GL"; public static void checkGlError(String op) { int error = GLES20.glGetError(); if (error != GLES20.GL_NO_ERROR) { String msg = op + ": glError 0x" + Integer.toHexString(error); throw new RuntimeException(msg); public static int createExternalTextureObject() { int[] textures = new int[1]; GLES20.glGenTextures(1, textures, 0); GlUtil.checkGlError("glGenTextures"); GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId); GlUtil.checkGlError("glBindTexture " + texId); GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); GlUtil.checkGlError("glTexParameter");
我们之前创建的纹理类型都是GLES20.GL_TEXTURE_2D; 这里我们要使用GLES11Ext.GL_TEXTURE_EXTERNAL_OES; 这也是Android平台下特有的类型,意思是这纹理数据是额外存储到其他地方(内存or流),并不是在显存的环境上。 这个区别很重要,因为这个类型的纹理是不能 和 存储在显存的纹理在同一管线(shader)上渲染的,会出现问题。什么问题?最直接就是黑屏什么的,这个往后在讨论。
通过createExternalTextureObject我们已经得到了 创建SurfaceTexture的纹理对象,现在我们放开代码注释,并通过Handler通信机制看看OnFrameAvailable的回调是否能正常触发?
private static class MainHandler extends Handler { private WeakReference<ContinuousRecordActivity> mWeakActivity; public static final int MSG_FRAME_AVAILABLE = 1; MainHandler(ContinuousRecordActivity activity) { mWeakActivity = new WeakReference<ContinuousRecordActivity>(activity); public void handleMessage(Message msg) { ContinuousRecordActivity activity = mWeakActivity.get(); Log.d(TAG, "Got message for dead activity"); case MSG_FRAME_AVAILABLE: super.handleMessage(msg); public void surfaceCreated(SurfaceHolder surfaceHolder) { Log.d(TAG, "surfaceCreated holder=" + surfaceHolder); // 准备好EGL环境,创建渲染介质mDisplaySurface mEglCore = new EglCore(null, EglCore.FLAG_RECORDABLE); mDisplaySurface = new WindowSurface(mEglCore, surfaceHolder.getSurface(), false); mDisplaySurface.makeCurrent(); mTextureId = GlUtil.createExternalTextureObject(); mCameraTexture = new SurfaceTexture(mTextureId); mCameraTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() { public void onFrameAvailable(SurfaceTexture surfaceTexture) { mHandler.sendEmptyMessage(MainHandler.MSG_FRAME_AVAILABLE); Log.d(TAG, "starting camera preview"); mCamera.setPreviewTexture(mCameraTexture); } catch (IOException ioe) { throw new RuntimeException(ioe); private void drawFrame() { Log.d(TAG, "Skipping drawFrame after shutdown"); Log.d(TAG, " MSG_FRAME_AVAILABLE"); mDisplaySurface.makeCurrent(); GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT); GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f); mDisplaySurface.swapBuffers();
我们分析一下drawFrame方法,首先我们用EglSurface.makeCurrent锁定了渲染的介质,然后我们就可以做自己的GL.draw的操作了,这里我们就是最简单的draw指令,清空颜色缓冲区和深度缓冲区,并把界面画成0xFF00FF的颜色值。然后我们调用swapBuffers交换读写的渲染介质,让Android系统把画面渲染出来。 然后,,,你们看到0xFF00FF的屏幕了吗? 奸笑.jpg
05-21 18:29:47.215 28583-28583/org.zzrblog.blogapp D/ZZR-GL: Trying GLES 2 05-21 18:29:47.217 28583-28583/org.zzrblog.blogapp D/ZZR-GL: Got GLES 2 config 05-21 18:29:47.219 28583-28583/org.zzrblog.blogapp D/ContinuousRecord: starting camera preview 05-21 18:29:47.299 28583-28616/org.zzrblog.blogapp D/OpenGLRenderer: endAllActiveAnimators on 0x72bd2d9000 (RippleDrawable) with handle 0x72bcd54c80 05-21 18:29:47.599 28583-28583/org.zzrblog.blogapp D/ContinuousRecord: MSG_FRAME_AVAILABLE
此时我们看看日志输出,我们创建了GLES2的EGL环境,打开摄像头预览,就只有一次MSG_FRAME_AVAILABLE预览的信息到达?这个是Android的Camera.SurfaceTexture的机制了,系统既然通知(OnFrameAvailableListener)告诉你,预览帧图像已经给你了,你是不是已经也有个责任告诉系统你已经使用这帧图像呢?是的,我们需要加上一句updateTexImage我们已经使用你的帧图像了。修改drawFrame代码
private void drawFrame() { mDisplaySurface.makeCurrent(); GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT); GLES20.glClearColor(1.0f, 0.0f, 1.0f, 1.0f); mCameraTexture.updateTexImage(); //告诉Android.Camera已经使用帧图像了。 // 怀疑还是不动的同学,不妨通过记录来帧的次数 分别glClearColor不同的颜色 ? mDisplaySurface.swapBuffers();
1、画出预览帧
好了,基本框架已经出来了。我们现在就想想怎么画出一张矩形帧图?有了以前的教学,我想这个任务不难吧。两个步骤,ShaderProgram 和 对应的顶点模型。
我们先来看看这次的渲染管线程序FrameRectSProgram
public class FrameRectSProgram extends ShaderProgram { private static final String VERTEX_SHADER = "uniform mat4 uMVPMatrix;\n" + "attribute vec4 aPosition;\n" + "uniform mat4 uTexMatrix;\n" + "attribute vec4 aTextureCoord;\n" + "varying vec2 vTextureCoord;\n" + " gl_Position = uMVPMatrix * aPosition;\n" + " vTextureCoord = (uTexMatrix * aTextureCoord).xy;\n" + private static final String FRAGMENT_SHADER_EXT = "#extension GL_OES_EGL_image_external : require\n" + "precision mediump float;\n" + "varying vec2 vTextureCoord;\n" + "uniform samplerExternalOES sTexture;\n" + " gl_FragColor = texture2D(sTexture, vTextureCoord);\n" + public FrameRectSProgram() { super(VERTEX_SHADER, FRAGMENT_SHADER_EXT); uMVPMatrixLoc = GLES20.glGetUniformLocation(programId, "uMVPMatrix"); GlUtil.checkLocation(uMVPMatrixLoc, "uMVPMatrix"); aPositionLoc = GLES20.glGetAttribLocation(programId, "aPosition"); GlUtil.checkLocation(aPositionLoc, "aPosition"); uTexMatrixLoc = GLES20.glGetUniformLocation(programId, "uTexMatrix"); GlUtil.checkLocation(uTexMatrixLoc, "uTexMatrix"); aTextureCoordLoc = GLES20.glGetAttribLocation(programId, "aTextureCoord"); GlUtil.checkLocation(aTextureCoordLoc, "aTextureCoord");
我们还是沿用以前的基类模板代码ShaderProgram,派生出这次渲染预览帧的FrameRectSProgram;
先看顶点着色器,除了以前标准的顶点坐标aPosition,纹理坐标aTextureCoord,MVP三大矩阵的核矩阵uMVPMatrix之外,这下多了一个纹理矩阵uTexMatrix ?这个矩阵可以理解为是操作纹理变换的矩阵,之后我们就知道为啥要有这么一个矩阵。
再到片段着色器,多了一句#extension~ 还多了一个纹理类型samplerExternalOES ,这些都是与上面介绍的Android特有类型GLES11Ext.GL_TEXTURE_EXTERNAL_OES相关,要想使用这个类似的纹理,我们要先申明扩展“#extension~~”然后才能使用新的纹理类型samplerExternalOES。
然后我们再看对应的顶点模型
public static final int SIZE_OF_FLOAT = 4; * 当 模型/视图/投影矩阵是都为单位矩阵的时候,这将完全覆盖视口。 * (This seems to work out right with external textures from SurfaceTexture.) private static final float FULL_RECTANGLE_COORDS[] = { -1.0f, -1.0f, // 0 bottom left 1.0f, -1.0f, // 1 bottom right -1.0f, 1.0f, // 2 top left 1.0f, 1.0f, // 3 top right private static final float FULL_RECTANGLE_TEX_COORDS[] = { 0.0f, 0.0f, // 0 bottom left 1.0f, 0.0f, // 1 bottom right 0.0f, 1.0f, // 2 top left 1.0f, 1.0f // 3 top right // 定义mVertexArray mTexCoordArray mCoordsPerVertex mVertexCount mVertexStride mTexCoordStride mVertexArray = createFloatBuffer(FULL_RECTANGLE_COORDS); mTexCoordArray = createFloatBuffer(FULL_RECTANGLE_TEX_COORDS); mVertexCount = FULL_RECTANGLE_COORDS.length / mCoordsPerVertex; // 4 mTexCoordStride = 2 * SIZE_OF_FLOAT; mVertexStride = 2 * SIZE_OF_FLOAT; private FrameRectSProgram mProgram; public void setShaderProgram(FrameRectSProgram mProgram) { this.mProgram = mProgram;
没啥疑问的吧,但请注意顶点坐标那句英文注释,那是我从Camera.SurfaceTexture源码中找出来的,是Android系统知道自己的特性所以就没有反转y了吧。然后有同学疑问,为啥只有矩阵的4个点,不应该是两个三角形吗 or 用索引吗?额,并不是,接着下去吧。
FrameRect暂且是这样,我们回到测试页面ContinuousRecordActivity,我们在onCreate创建FrameRect,在EGL环境下创建渲染管线程序FrameRectSProgram 并设置到 FrameRect当中
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); frameRect = new FrameRect(); public void surfaceCreated(SurfaceHolder surfaceHolder) { mDisplaySurface.makeCurrent(); frameRect.setShaderProgram(new FrameRectSProgram()); //为啥要这样分开,还记得OpenGL的指令需要在EGL环境下执行的铁律了吗?
我们来个小总结,这次我们学会了:
1、自定义的EGL环境的使用。
2、Camera.SurfaceTexture对应的使用注意事项。
3、Android平台下特有的纹理类型GL_OES_EGL_image_external 和使用方法。
由于篇幅关系,我们先到这。下一篇我们把预览图像画出来,额外加上个性签名的水印效果。
|