分享

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

 mediatv 2019-10-12

(自定义Android-EGL)

 

1、确定需求

这次的项目需求总结下来是这样的:一个摄像头预览界面,一个按钮触发屏幕录制,录制视频带上水印效果。

1. 摄像头预览
2. 屏幕录制
3. 录制视频在指定位置附带上水印

确定需求后,我们逐一分析模块组成并完成它。So,Talk is cheap,Let me show codes!

 

2、EGL+Surface=EGLSurface

要想预览的时候增加水印(滤镜)效果,必须有EGL环境+shader的滤镜特效。所以我们先从简单开始,第一步的需求就是创建EGL,并能正常显示手机摄像头。首先创建我们这次的测试Activity->ContinuousRecordActivity并读取布局界面的SurfaceView,使用SurfaceView是方便直接获取Surface渲染表面。

  1. public class ContinuousRecordActivity extends Activity implements SurfaceHolder.Callback {
  2. public static final String TAG = "ContinuousRecord";
  3. SurfaceView sv;
  4. @Override
  5. protected void onCreate(Bundle savedInstanceState) {
  6. super.onCreate(savedInstanceState);
  7. setContentView(R.layout.continuous_record);
  8. sv = (SurfaceView) findViewById(R.id.continuousCapture_surfaceView);
  9. SurfaceHolder sh = sv.getHolder();
  10. sh.addCallback(this);
  11. }
  12. @Override
  13. public void surfaceCreated(SurfaceHolder surfaceHolder) {
  14. Log.d(TAG, "surfaceCreated holder=" + surfaceHolder);
  15. //首先我们描述一下在这里即将发生的:
  16.         // surface创建回调给开发者我们,然后我们创建一个EGL上下文,组成一个我们需要的EGLSurface
  17.         // EglCore = new EglCore();
  18.         // 把Egl和native的surface组合成=EglSurface,并保存下来。
  19. // EglSurface = new EglSurface(EglCore,surface);
  20. }
  21. @Override
  22. public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
  23. }
  24. @Override
  25. public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
  26. }
  27. }

接下来我们就要思考,要在surface的三大回调中做点什么?回想一下前一篇文章分析到的GLSurfaceView,该是要创建我们自己的EGL,并把native的surface和EGL组合成EGLSurface保存下来。以上注释已经给出了伪代码的执行过程。在开始之前建议打开我前一篇文章,跟着最底下的红色字体部分流程来理解。下面我们就带大家撸出这个EglCore和EglSurface吧。

  1. public class EglCore {
  2. private static String TAG = "EglCore";
  3. public static final int FLAG_TRY_GLES2 = 0x02;
  4. public static final int FLAG_TRY_GLES3 = 0x04;
  5. private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY;
  6. private EGLContext mEGLContext = EGL14.EGL_NO_CONTEXT;
  7. private EGLConfig mEGLConfig = null;
  8. private int mGlVersion = -1;
  9. public int getGlVersion() {
  10. return mGlVersion;
  11. }
  12. public EglCore() {
  13. this(null, FLAG_TRY_GLES2);
  14. }
  15. public EglCore(EGLContext sharedContext, int flags) {
  16. ... ...
  17. }
  18. }

首先我们看看EglCore的组成,我预先定义两个版本号2和3,还有当前版本值mGlVersion。然后其次就是EGLDisplay EGLContext EGLConfig 等相关EGL环境所绑定的变量。我们创建一个无参默认构造函数。和一个附带参数的构造函数。为什么我们要传一个EGLCotext进来呢?因为EGLContext是可以多个的!没错,你没看错,可以多个EGLContext,但是实际使用只能是当前唯一,哈哈!就是说EGLDisplay+EGLContext+EGLSurface只能是唯一对应,如果要替换别的EGLContext,那OPENGL的一系列设置(glEnable接口)都跟着上下文一并替换了。但正常情况我们也就一个EGLContext了,所以默认传null就好。   好了,废话了一段,我们继续~

  1. public EglCore(EGLContext sharedContext, int flags) {
  2. if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
  3. throw new RuntimeException("EGL already set up");
  4. }
  5. if (sharedContext == null) {
  6. sharedContext = EGL14.EGL_NO_CONTEXT;
  7. }
  8.         // 1、获取EGLDisplay对象
  9. mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
  10. if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
  11. throw new RuntimeException("unable to get EGL14 display");
  12. }
  13. // 2、初始化与EGLDisplay之间的关联。
  14. int[] version = new int[2];
  15. if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {
  16. mEGLDisplay = null;
  17. throw new RuntimeException("unable to initialize EGL14");
  18. }
  19. ... ...
  20. }

我们按照前篇文章结尾部分的 创建EGL过程的流程步骤 开始:1、获取EGLDisplay对象;2、初始化与EGLDisplay之间的关联。

接下来我们就要执行3、获取EGLConfig对象;4、创建EGLContext 实例,见如下代码

  1. public EglCore(EGLContext sharedContext, int flags) {
  2. ... ...
  3. if ((flags & FLAG_TRY_GLES3) != 0) {
  4. EGLConfig config = getConfig(flags, 3);
  5. if (config != null) {
  6. int[] attrib3_list = {
  7. EGL14.EGL_CONTEXT_CLIENT_VERSION, 3,
  8. EGL14.EGL_NONE
  9. };
  10. EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext, attrib3_list, 0);
  11. if (EGL14.eglGetError() == EGL14.EGL_SUCCESS) {
  12. Log.d(TAG, "Got GLES 3 config");
  13. mEGLConfig = config;
  14. mEGLContext = context;
  15. mGlVersion = 3;
  16. }
  17. }
  18. }
  19. if (mEGLContext == EGL14.EGL_NO_CONTEXT) { //如果只要求GLES版本2 又或者GLES3失败了。
  20. Log.d(TAG, "Trying GLES 2");
  21. EGLConfig config = getConfig(flags, 2);
  22. if (config != null) {
  23. int[] attrib2_list = {
  24. EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
  25. EGL14.EGL_NONE
  26. };
  27. EGLContext context = EGL14.eglCreateContext(mEGLDisplay, config, sharedContext, attrib2_list, 0);
  28. if (EGL14.eglGetError() == EGL14.EGL_SUCCESS) {
  29. Log.d(TAG, "Got GLES 2 config");
  30. mEGLConfig = config;
  31. mEGLContext = context;
  32. mGlVersion = 2;
  33. }
  34. }
  35. }
  36. }

我们来看看模板代码,我们通过getConfig获取相应的EGLConfig对象,然后在通过EGL14.eglCreateContext,并传入EGLConfig和属性列表,创建出EGLContext;EGL相关的接口很多这种属性配置列表,都是一个int数组,然后按照key-value这样排列下去,这里创建EGLContext,我们只需要传入版本号的信息,然后以单独一个EGL_NONE为结束符标志。

接下来,我们看看getConfig是如何构建EGLConfig:

  1. public static final int FLAG_RECORDABLE = 0x01;
  2.     public static final int FLAG_TRY_GLES2 = 0x02;
  3.     public static final int FLAG_TRY_GLES3 = 0x04;
  4. ... ...
  5. /**
  6. * 从本地设备中寻找合适的 EGLConfig.
  7. */
  8. private EGLConfig getConfig(int flags, int version) {
  9. int renderableType = EGL14.EGL_OPENGL_ES2_BIT;
  10. if (version >= 3) {
  11. renderableType |= EGLExt.EGL_OPENGL_ES3_BIT_KHR;
  12. }
  13. int[] attribList = {
  14. EGL14.EGL_RED_SIZE, 8,
  15. EGL14.EGL_GREEN_SIZE, 8,
  16. EGL14.EGL_BLUE_SIZE, 8,
  17. EGL14.EGL_ALPHA_SIZE, 8,
  18. //EGL14.EGL_DEPTH_SIZE, 16,
  19. //EGL14.EGL_STENCIL_SIZE, 8,
  20. EGL14.EGL_RENDERABLE_TYPE, renderableType,
  21. EGL14.EGL_NONE, 0, // placeholder for recordable [@-3]
  22. EGL14.EGL_NONE
  23. };
  24. if ((flags & FLAG_RECORDABLE) != 0) {
  25. attribList[attribList.length - 3] = EGL_RECORDABLE_ANDROID;
  26. // EGLExt.EGL_RECORDABLE_ANDROID;0x3142(required 26)
  27. // 如果说希望保留自己的最低版本SDK,我们可以自己定义一个EGL_RECORDABLE_ANDROID=0x3142;
  28. attribList[attribList.length - 2] = 1;
  29. }
  30. EGLConfig[] configs = new EGLConfig[1];
  31. int[] numConfigs = new int[1];
  32. if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length, numConfigs, 0)) {
  33. Log.w(TAG, "unable to find RGBA8888 / " + version + " EGLConfig");
  34. return null;
  35. }
  36. return configs[0];
  37. }

首先我们根据传入的版本号确定渲染模式renderableType ,然后我们开始构建EGLConfig的属性列表了。(这里又出现属性列表了)我们解读这个属性列表:我们请求RGBA四通道,每个通道都是8个字节。然后注释的两个字段,一个是深度测试,一个是模板缓冲测试的,以后我们再来讨论这部分,现在我们用不着这些所以就注释掉吧;紧接着就是渲染模式renderableType ;

好,来干货了。这里出现一个占位的概念,我们顺着代码继续,判断flags是否附带FLAG_RECORDABLE,如果是我们把占位填充成EGLExt.EGL_RECORDABLE_ANDROID = 1;声明当前EGL是可录屏的。这个是Android平台特意有的标志位。需要版本26的SDK,如果说希望保留自己的最低版本SDK,我们可以自己定义一个EGL_RECORDABLE_ANDROID=0x3142。我们回溯代码,这个标志位flags(FLAG_RECORDABLE=0x01)是EglCore初始化传进来的,是我们自己定义的专门用以判断是否开启录制。如果希望EGL+Surface带录制属性则EglCore(null,FLAG_RECORDABLE);否则EglCore(null,0)即可。到此我们就创建好EGLConfig 和 EGLContext。

接下来就是第五步5、创建EGLSurface实例,直接上代码

  1. /**
  2. * 创建一个 EGL+Surface
  3. * @param surface
  4. * @return
  5. */
  6. public EGLSurface createWindowSurface(Object surface) {
  7. if (!(surface instanceof Surface) && !(surface instanceof SurfaceTexture)) {
  8. throw new RuntimeException("invalid surface: " + surface);
  9. }
  10. // 创建EGLSurface, 绑定传入进来的surface
  11. int[] surfaceAttribs = {
  12. EGL14.EGL_NONE
  13. };
  14. EGLSurface eglSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, mEGLConfig, surface,
  15. surfaceAttribs, 0);
  16. GlUtil.checkGlError("eglCreateWindowSurface");
  17. if (eglSurface == null) {
  18. throw new RuntimeException("surface was null");
  19. }
  20. return eglSurface;
  21. }

我们模仿Android源代码的写法,因为系统的surface和SurfaceTexture都可以充当EGLSurface的载体,所以我们把参数定义为Object,在函数开始就先判断是否合法。然后就是调用eglCreateWindowSurface根据我们之前的mEGLDisplay, mEGLConfig,创建出EGLSurface,然后就大功告成了 ... ... 吗?

回溯到我们一开始给出的伪代码,类EglCore用以表示EGL环境,还有一个类用以表示EglSurface。为啥要这样划分?因为很多的时候,我们都是要直接或间接操作EglSurface的。我们创建EglSurfaceBase,用以表示我们自己的EglSurface

  1. public class EglSurfaceBase {
  2. private static final String TAG = GlUtil.TAG;
  3. protected EglCore mEglCore;
  4. private EGLSurface mEGLSurface = EGL14.EGL_NO_SURFACE;
  5. private int mWidth = -1;
  6. private int mHeight = -1;
  7. protected EglSurfaceBase(EglCore eglCore) {
  8. mEglCore = eglCore;
  9. }
  10. /**
  11. * 创建要使用的渲染表面EGLSurface
  12. * @param Surface or SurfaceTexture.
  13. */
  14. public void createWindowSurface(Object surface) {
  15. if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
  16. throw new IllegalStateException("surface already created");
  17. }
  18. mEGLSurface = mEglCore.createWindowSurface(surface);
  19. // 不用急着在这里创建width/height, 因为surface的大小,不同情况下都会改变。
  20. //mWidth = mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH);
  21. //mHeight = mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT);
  22. }
  23. /**
  24.      * 返回surface的width长度, 单位是pixels.
  25.      */
  26.     public int getWidth() {
  27.         if (mWidth < 0) {
  28.             return mEglCore.querySurface(mEGLSurface, EGL14.EGL_WIDTH);
  29.         } else {
  30.             return mWidth;
  31.         }
  32.     }
  33.     public int getHeight() {
  34.         if (mHeight < 0) {
  35.             return mEglCore.querySurface(mEGLSurface, EGL14.EGL_HEIGHT);
  36.         } else {
  37.             return mHeight;
  38.         }
  39.     }
  40. ... ... ...
  41. }
  42. public class EglCore {
  43. ... ... ...
  44. // 查询当前surface的状态值。
  45.     public int querySurface(EGLSurface eglSurface, int what) {
  46.         int[] value = new int[1];
  47.         EGL14.eglQuerySurface(mEGLDisplay, eglSurface, what, value, 0);
  48.         return value[0];
  49.     }
  50.     ... ... ...
  51. }

看看以上代码,我们这个EglSurfaceBase扩展功能,拥有了长宽值,这长宽值是直接通过查询当前EGLSurface的状态值返回的,并没有赋值到变量width/height,保证其准确性。那我们什么时候才赋值呢?当我们是创建离屏渲染的EGLSurface的时候。这个以后才讨论。到此我才算初步的完成第5、创建EGLSurface实例。

紧接着就是第6、连接EGLContext和EGLSurface。那是怎么连接呢?还记得源码分析的makeCurrent吗?是的就是它。让我们来继续封装它。

  1. (开发的时候多数是通过EglSurfce,间接操作的EglCore)
  2. public class EglSurfaceBase {
  3.     ... ... ...
  4. // 连接 EGL context 和当前 eglsurface
  5. public void makeCurrent() {
  6. mEglCore.makeCurrent(mEGLSurface);
  7. }
  8. public void makeCurrentReadFrom(EglSurfaceBase readSurface) {
  9.         mEglCore.makeCurrent(mEGLSurface, readSurface.mEGLSurface);
  10.     }
  11. }
  12. public class EglCore{
  13. ... ... ...
  14. public void makeCurrent(EGLSurface eglSurface) {
  15.         if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
  16.             Log.d(TAG, "NOTE: makeCurrent w/r display");
  17.         }
  18.         if (!EGL14.eglMakeCurrent(mEGLDisplay, eglSurface, eglSurface, mEGLContext)) {
  19.             throw new RuntimeException("eglMakeCurrent failed");
  20.         }
  21.     }
  22. public void makeCurrent(EGLSurface drawSurface, EGLSurface readSurface) {
  23.         if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) {
  24.             Log.d(TAG, "NOTE: makeCurrent w/o display");
  25.         }
  26.         if (!EGL14.eglMakeCurrent(mEGLDisplay, drawSurface, readSurface, mEGLContext)) {
  27.             throw new RuntimeException("eglMakeCurrent(draw,read) failed");
  28.         }
  29.     }
  30. }

我暴露了两种参数方法,看参数名字应该都能搞清楚了。不知道大家还记不记得,OpenGL是使用双缓冲机制的渲染面的,所以我们makeCurrent之后,还需要每帧的交替(swapBuffers)读写的surface。我们赶紧也把此接口封装。

  1. (开发的时候多数是通过EglSurfce,间接操作的EglCore)
  2. public class EglSurfaceBase {
  3. public boolean swapBuffers() {
  4. boolean result = mEglCore.swapBuffers(mEGLSurface);
  5. if (!result) {
  6. Log.d(TAG, "WARNING: swapBuffers() failed");
  7. }
  8. return result;
  9. }
  10. }
  11. public class EglCore{
  12. public boolean swapBuffers(EGLSurface eglSurface) {
  13.         return EGL14.eglSwapBuffers(mEGLDisplay, eglSurface);
  14.     }
  15. }

搞定。之后就是opengl业务开发的相关draw接口的绘制工作了。(呼~先休息休息     你看我不到你看我不到o(*////▽////*)q )

 

跳过第7、使用GL指令绘制图形 的过程之后,我们就要开始回收拾EGL了。即就是执行8~9~10~11的操作了。

我们先来 8、断开并释放与EGLSurface关联的EGLContext对象; 9、删除EGLSurface对象

  1. (开发的时候多数是通过EglSurfce,间接操作的EglCore)
  2. public class EglSurfaceBase {  
  3. // 释放 EGL surface.
  4. public void releaseEglSurface() {
  5. mEglCore.makeNothingCurrent();
  6. mEglCore.releaseSurface(mEGLSurface);
  7. mEGLSurface = EGL14.EGL_NO_SURFACE;
  8. mWidth = mHeight = -1;
  9. }
  10. ... ... ...
  11. }
  12. public class EglCore{
  13. ... ...
  14. public void makeNothingCurrent() {
  15.         if (!EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
  16.                 EGL14.EGL_NO_CONTEXT)) {
  17.             throw new RuntimeException("eglMakeCurrent To EGL_NO_SURFACE failed");
  18.         }
  19.     }
  20. public void releaseSurface(EGLSurface eglSurface) {
  21.         EGL14.eglDestroySurface(mEGLDisplay, eglSurface);
  22.     }
  23. }

然后就是最后的 10、删除EGLContext对象;11、终止与EGLDisplay之间的连接。这部分和EGLSurface没关系了,就是EglCore的部分。

  1. public class EglCore {
  2. ... ... ...
  3. // 释放EGL资源
  4. public void release() {
  5. if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) {
  6. // Android 使用一个引用计数EGLDisplay。
  7. // 因此,对于每个eglInitialize,我们需要一个eglTerminate。
  8. EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
  9. EGL14.EGL_NO_CONTEXT); // 确保EglSurface和EGLContext已经分离
  10. EGL14.eglDestroyContext(mEGLDisplay, mEGLContext);
  11. EGL14.eglReleaseThread();
  12. EGL14.eglTerminate(mEGLDisplay);
  13. }
  14. mEGLDisplay = EGL14.EGL_NO_DISPLAY;
  15. mEGLContext = EGL14.EGL_NO_CONTEXT;
  16. mEGLConfig = null;
  17. }
  18. }

到此,我们就完整的自定义了一套EGL环境操作接口,并封装自己了属于的EglSurface。拿着这套代码,我们就可以嵌入Android任何的渲染载体(native的Surface/SurfaceTexture)开展我们的OpenGL之旅了!下一篇文章,我们就拿着这套代码去实际运用,完成项目需求。


 

后台有同学私信对那个离屏surface敢兴趣,其实就老版本的PBuffer,这里给出相关代码,详情请follow github:

  1. //EglSurfaceBase 创建离屏的EGLSurface,不过FBO比它好用多了
  2. @Deprecated
  3. public void createOffScreenSurface(int width, int height) {
  4. if (mEGLSurface != EGL14.EGL_NO_SURFACE) {
  5. throw new IllegalStateException("surface already created");
  6. }
  7. mEGLSurface = mEglCore.createOffscreenSurface(width, height);
  8. mWidth = width;
  9. mHeight = height;
  10. }
  11. //EglCore 用旧版的Pbuffer,创建离屏的EGLSurface
  12.     public EGLSurface createOffscreenSurface(int width, int height) {
  13.         int[] surfaceAttribs = {
  14.                 EGL14.EGL_WIDTH, width,
  15.                 EGL14.EGL_HEIGHT, height,
  16.                 EGL14.EGL_NONE
  17.         };
  18.         EGLSurface eglSurface = EGL14.eglCreatePbufferSurface(mEGLDisplay, mEGLConfig,
  19.                 surfaceAttribs, 0);
  20.         GlUtil.checkGlError("eglCreatePbufferSurface");
  21.         if (eglSurface == null) {
  22.             throw new RuntimeException("surface was null");
  23.         }
  24.         return eglSurface;
  25.     }

The  End .

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

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多