分享

OpenGL.ES在Android上的简单实践:19

 mediatv 2019-10-12

OpenGL.ES在Android上的简单实践:

19-水印录制(EGL+摄像头预览 GL_TEXTURE_EXTERNAL_OES)

 

 

0、补充EglSurfaceBase

在自己实际运用中,发现EglSurfaceBase还是缺了对原生的surface的管理,对整体的理解好像总缺了点啥。所以在EglSurfaceBase的基础上,派生出了WindowSurface。代码超级简单的,但从理解学习上就完全不同一个台阶了。

  1. public class WindowSurface extends EglSurfaceBase {
  2. private Surface mSurface;
  3. private boolean bReleaseSurface;
  4. //将native的surface 与 EGL关联起来
  5. public WindowSurface(EglCore eglCore, Surface surface, boolean isReleaseSurface) {
  6. super(eglCore);
  7. createWindowSurface(surface);
  8. mSurface = surface;
  9. bReleaseSurface = isReleaseSurface;
  10. }
  11. //将SurfaceTexture 与 EGL关联起来
  12. protected WindowSurface(EglCore eglCore, SurfaceTexture surfaceTexture) {
  13. super(eglCore);
  14. createWindowSurface(surfaceTexture);
  15. }
  16. //释放当前EGL上下文 关联 的 surface
  17. public void release() {
  18. releaseEglSurface();
  19. if (mSurface != null
  20. && bReleaseSurface) {
  21. mSurface.release();
  22. mSurface = null;
  23. }
  24. }
  25. // That's All.
  26. }

那么接下来,我们就要快速开始预览摄像头。从前篇的ContinuousRecordActivity开始代码:

  1. public class ContinuousRecordActivity extends Activity implements SurfaceHolder.Callback {
  2. public static final String TAG = "ContinuousRecord";
  3. // 因为Androi的摄像头默认是横着方向,所以width>height
  4. private static final int VIDEO_WIDTH = 1280;
  5.     private static final int VIDEO_HEIGHT = 720;   
  6.     private static final int DESIRED_PREVIEW_FPS = 15;
  7. private Camera mCamera;
  8. SurfaceView sv;
  9. @Override
  10. protected void onCreate(Bundle savedInstanceState) {
  11. super.onCreate(savedInstanceState);
  12. setContentView(R.layout.continuous_record);
  13. sv = (SurfaceView) findViewById(R.id.continuousRecord_surfaceView);
  14. SurfaceHolder sh = sv.getHolder();
  15. sh.addCallback(this);
  16. }
  17. @Override
  18.     protected void onResume() {
  19.         super.onResume();
  20.         openCamera(VIDEO_WIDTH, VIDEO_HEIGHT, DESIRED_PREVIEW_FPS);
  21.     }
  22. @Override
  23.     protected void onPause() {
  24.         super.onPause();
  25.         releaseCamera();
  26.     }
  27. private EglCore mEglCore;
  28. private WindowSurface mDisplaySurface;
  29. @Override
  30. public void surfaceCreated(SurfaceHolder surfaceHolder) {
  31. Log.d(TAG, "surfaceCreated holder=" + surfaceHolder);
  32. // 准备好EGL环境,创建渲染介质mDisplaySurface
  33. mEglCore = new EglCore(null, EglCore.FLAG_RECORDABLE);
  34. mDisplaySurface = new WindowSurface(mEglCore, surfaceHolder.getSurface(), false);
  35. mDisplaySurface.makeCurrent();
  36. //mTextureId = createTextureObject();
  37. //mCameraTexture = new SurfaceTexture(mTextureId);
  38. //mCameraTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
  39. // @Override
  40. // public void onFrameAvailable(SurfaceTexture surfaceTexture) {
  41. // Handler.sendEmptyMessage(MSG_FRAME_AVAILABLE);
  42. // }
  43. //});
  44. try {
  45. Log.d(TAG, "starting camera preview");
  46. //mCamera.setPreviewTexture(mCameraTexture);
  47. mCamera.startPreview();
  48. } catch (IOException ioe) {
  49. throw new RuntimeException(ioe);
  50. }
  51. }
  52. ... ... ...
  53. (省略 openCamera releaseCamera surfaceChanged 和 surfaceDestroy等不是重点的代码,节省篇幅。)
  54. (如有需要请follow github)
  55. }

我们先分析以上代码,首先我们运用前篇文章的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下代码:

  1. public class GlUtil {
  2. public static final String TAG = "ZZR-GL";
  3. public static void checkGlError(String op) {
  4. int error = GLES20.glGetError();
  5. if (error != GLES20.GL_NO_ERROR) {
  6. String msg = op + ": glError 0x" + Integer.toHexString(error);
  7. Log.e(TAG, msg);
  8. throw new RuntimeException(msg);
  9. }
  10. }
  11. public static int createExternalTextureObject() {
  12. int[] textures = new int[1];
  13. GLES20.glGenTextures(1, textures, 0);
  14. GlUtil.checkGlError("glGenTextures");
  15. int texId = textures[0];
  16. GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId);
  17. GlUtil.checkGlError("glBindTexture " + texId);
  18. GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER,
  19. GLES20.GL_NEAREST);
  20. GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER,
  21. GLES20.GL_LINEAR);
  22. GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S,
  23. GLES20.GL_CLAMP_TO_EDGE);
  24. GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T,
  25. GLES20.GL_CLAMP_TO_EDGE);
  26. GlUtil.checkGlError("glTexParameter");
  27. return texId;
  28. }
  29. }

我们之前创建的纹理类型都是GLES20.GL_TEXTURE_2D; 这里我们要使用GLES11Ext.GL_TEXTURE_EXTERNAL_OES; 这也是Android平台下特有的类型,意思是这纹理数据是额外存储到其他地方(内存or流),并不是在显存的环境上。 这个区别很重要,因为这个类型的纹理是不能 和 存储在显存的纹理在同一管线(shader)上渲染的,会出现问题。什么问题?最直接就是黑屏什么的,这个往后在讨论。

通过createExternalTextureObject我们已经得到了 创建SurfaceTexture的纹理对象,现在我们放开代码注释,并通过Handler通信机制看看OnFrameAvailable的回调是否能正常触发?

  1. private static class MainHandler extends Handler {
  2.         private WeakReference<ContinuousRecordActivity> mWeakActivity;
  3.         public static final int MSG_FRAME_AVAILABLE = 1;
  4.         MainHandler(ContinuousRecordActivity activity) {
  5.             mWeakActivity = new WeakReference<ContinuousRecordActivity>(activity);
  6.         }
  7.         @Override
  8.         public void handleMessage(Message msg) {
  9.             ContinuousRecordActivity activity = mWeakActivity.get();
  10.             if (activity == null) {
  11.                 Log.d(TAG, "Got message for dead activity");
  12.                 return;
  13.             }
  14.             switch (msg.what) {
  15.                 case MSG_FRAME_AVAILABLE:
  16.                     activity.drawFrame();
  17.                     break;
  18.                 default:
  19.                     super.handleMessage(msg);
  20.                     break;
  21.             }
  22.         }
  23.     }
  24. @Override
  25. public void surfaceCreated(SurfaceHolder surfaceHolder) {
  26. Log.d(TAG, "surfaceCreated holder=" + surfaceHolder);
  27. // 准备好EGL环境,创建渲染介质mDisplaySurface
  28. mEglCore = new EglCore(null, EglCore.FLAG_RECORDABLE);
  29. mDisplaySurface = new WindowSurface(mEglCore, surfaceHolder.getSurface(), false);
  30. mDisplaySurface.makeCurrent();
  31. mTextureId = GlUtil.createExternalTextureObject();
  32. mCameraTexture = new SurfaceTexture(mTextureId);
  33. mCameraTexture.setOnFrameAvailableListener(new SurfaceTexture.OnFrameAvailableListener() {
  34. @Override
  35. public void onFrameAvailable(SurfaceTexture surfaceTexture) {
  36. mHandler.sendEmptyMessage(MainHandler.MSG_FRAME_AVAILABLE);
  37. }
  38. });
  39. try {
  40. Log.d(TAG, "starting camera preview");
  41. mCamera.setPreviewTexture(mCameraTexture);
  42. mCamera.startPreview();
  43. } catch (IOException ioe) {
  44. throw new RuntimeException(ioe);
  45. }
  46. }
  47.     private void drawFrame() {
  48.         if (mEglCore == null) {
  49.             Log.d(TAG, "Skipping drawFrame after shutdown");
  50.             return;
  51.         }
  52.         Log.d(TAG, " MSG_FRAME_AVAILABLE");
  53.         mDisplaySurface.makeCurrent();
  54.         GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
  55.         GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f);
  56.         mDisplaySurface.swapBuffers();
  57.     }

我们分析一下drawFrame方法,首先我们用EglSurface.makeCurrent锁定了渲染的介质,然后我们就可以做自己的GL.draw的操作了,这里我们就是最简单的draw指令,清空颜色缓冲区和深度缓冲区,并把界面画成0xFF00FF的颜色值。然后我们调用swapBuffers交换读写的渲染介质,让Android系统把画面渲染出来。  然后,,,你们看到0xFF00FF的屏幕了吗? 奸笑.jpg

  1. 05-21 18:29:47.215 28583-28583/org.zzrblog.blogapp D/ZZR-GL: Trying GLES 2
  2. 05-21 18:29:47.217 28583-28583/org.zzrblog.blogapp D/ZZR-GL: Got GLES 2 config
  3. 05-21 18:29:47.219 28583-28583/org.zzrblog.blogapp D/ContinuousRecord: starting camera preview
  4. 05-21 18:29:47.299 28583-28616/org.zzrblog.blogapp D/OpenGLRenderer: endAllActiveAnimators on 0x72bd2d9000 (RippleDrawable) with handle 0x72bcd54c80
  5. 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代码

  1. private void drawFrame() {
  2.         ... ...
  3. mDisplaySurface.makeCurrent();
  4. GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
  5. GLES20.glClearColor(1.0f, 0.0f, 1.0f, 1.0f);
  6. mCameraTexture.updateTexImage(); //告诉Android.Camera已经使用帧图像了。
  7. // 怀疑还是不动的同学,不妨通过记录来帧的次数 分别glClearColor不同的颜色 ?
  8. mDisplaySurface.swapBuffers();
  9. }

 

1、画出预览帧

好了,基本框架已经出来了。我们现在就想想怎么画出一张矩形帧图?有了以前的教学,我想这个任务不难吧。两个步骤,ShaderProgram 和 对应的顶点模型。

我们先来看看这次的渲染管线程序FrameRectSProgram

  1. public class FrameRectSProgram extends ShaderProgram {
  2. private static final String VERTEX_SHADER =
  3. "uniform mat4 uMVPMatrix;\n" +
  4. "attribute vec4 aPosition;\n" +
  5. "uniform mat4 uTexMatrix;\n" +
  6. "attribute vec4 aTextureCoord;\n" +
  7. "varying vec2 vTextureCoord;\n" +
  8. "void main() {\n" +
  9. " gl_Position = uMVPMatrix * aPosition;\n" +
  10. " vTextureCoord = (uTexMatrix * aTextureCoord).xy;\n" +
  11. "}\n";
  12. private static final String FRAGMENT_SHADER_EXT =
  13. "#extension GL_OES_EGL_image_external : require\n" +
  14. "precision mediump float;\n" +
  15. "varying vec2 vTextureCoord;\n" +
  16. "uniform samplerExternalOES sTexture;\n" +
  17. "void main() {\n" +
  18. " gl_FragColor = texture2D(sTexture, vTextureCoord);\n" +
  19. "}\n";
  20. public FrameRectSProgram() {
  21. super(VERTEX_SHADER, FRAGMENT_SHADER_EXT);
  22. uMVPMatrixLoc = GLES20.glGetUniformLocation(programId, "uMVPMatrix");
  23. GlUtil.checkLocation(uMVPMatrixLoc, "uMVPMatrix");
  24. aPositionLoc = GLES20.glGetAttribLocation(programId, "aPosition");
  25. GlUtil.checkLocation(aPositionLoc, "aPosition");
  26. uTexMatrixLoc = GLES20.glGetUniformLocation(programId, "uTexMatrix");
  27. GlUtil.checkLocation(uTexMatrixLoc, "uTexMatrix");
  28. aTextureCoordLoc = GLES20.glGetAttribLocation(programId, "aTextureCoord");
  29. GlUtil.checkLocation(aTextureCoordLoc, "aTextureCoord");
  30. }
  31. int uMVPMatrixLoc;
  32. int aPositionLoc;
  33. int uTexMatrixLoc;
  34. int aTextureCoordLoc;
  35. }

我们还是沿用以前的基类模板代码ShaderProgram,派生出这次渲染预览帧的FrameRectSProgram;

先看顶点着色器,除了以前标准的顶点坐标aPosition,纹理坐标aTextureCoord,MVP三大矩阵的核矩阵uMVPMatrix之外,这下多了一个纹理矩阵uTexMatrix ?这个矩阵可以理解为是操作纹理变换的矩阵,之后我们就知道为啥要有这么一个矩阵。

再到片段着色器,多了一句#extension~ 还多了一个纹理类型samplerExternalOES ,这些都是与上面介绍的Android特有类型GLES11Ext.GL_TEXTURE_EXTERNAL_OES相关,要想使用这个类似的纹理,我们要先申明扩展“#extension~~”然后才能使用新的纹理类型samplerExternalOES。

然后我们再看对应的顶点模型

  1. public class FrameRect {
  2. public static final int SIZE_OF_FLOAT = 4;
  3. /**
  4. * 一个“完整”的正方形,从两维延伸到-1到1。
  5. * 当 模型/视图/投影矩阵是都为单位矩阵的时候,这将完全覆盖视口。
  6. * 纹理坐标相对于矩形是y反的。
  7. * (This seems to work out right with external textures from SurfaceTexture.)
  8. */
  9. private static final float FULL_RECTANGLE_COORDS[] = {
  10. -1.0f, -1.0f, // 0 bottom left
  11. 1.0f, -1.0f, // 1 bottom right
  12. -1.0f, 1.0f, // 2 top left
  13. 1.0f, 1.0f, // 3 top right
  14. };
  15. private static final float FULL_RECTANGLE_TEX_COORDS[] = {
  16. 0.0f, 0.0f, // 0 bottom left
  17. 1.0f, 0.0f, // 1 bottom right
  18. 0.0f, 1.0f, // 2 top left
  19. 1.0f, 1.0f // 3 top right
  20. };
  21. // 定义mVertexArray mTexCoordArray mCoordsPerVertex mVertexCount mVertexStride mTexCoordStride
  22. ... ... ...
  23. public FrameRect() {
  24. mVertexArray = createFloatBuffer(FULL_RECTANGLE_COORDS);
  25. mTexCoordArray = createFloatBuffer(FULL_RECTANGLE_TEX_COORDS);
  26. mCoordsPerVertex = 2;
  27. mVertexCount = FULL_RECTANGLE_COORDS.length / mCoordsPerVertex; // 4
  28. mTexCoordStride = 2 * SIZE_OF_FLOAT;
  29. mVertexStride = 2 * SIZE_OF_FLOAT;
  30. }
  31. //... ... ...以上变量的get方法
  32. private FrameRectSProgram mProgram;
  33. public void setShaderProgram(FrameRectSProgram mProgram) {
  34. this.mProgram = mProgram;
  35. }
  36. }

没啥疑问的吧,但请注意顶点坐标那句英文注释,那是我从Camera.SurfaceTexture源码中找出来的,是Android系统知道自己的特性所以就没有反转y了吧。然后有同学疑问,为啥只有矩阵的4个点,不应该是两个三角形吗 or 用索引吗?额,并不是,接着下去吧。

FrameRect暂且是这样,我们回到测试页面ContinuousRecordActivity,我们在onCreate创建FrameRect,在EGL环境下创建渲染管线程序FrameRectSProgram 并设置到 FrameRect当中

  1. @Override
  2. protected void onCreate(Bundle savedInstanceState) {
  3. super.onCreate(savedInstanceState);
  4. ... ...
  5. frameRect = new FrameRect();
  6. }
  7. @Override
  8.     public void surfaceCreated(SurfaceHolder surfaceHolder) {
  9. ... ...
  10. mDisplaySurface.makeCurrent();
  11. frameRect.setShaderProgram(new FrameRectSProgram());
  12. ... ...
  13. }
  14. //为啥要这样分开,还记得OpenGL的指令需要在EGL环境下执行的铁律了吗?

 

 

我们来个小总结,这次我们学会了:

1、自定义的EGL环境的使用。

2、Camera.SurfaceTexture对应的使用注意事项。

3、Android平台下特有的纹理类型GL_OES_EGL_image_external 和使用方法。

由于篇幅关系,我们先到这。下一篇我们把预览图像画出来,额外加上个性签名的水印效果。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多