OpenGL.Shader:1-重新认识Android上OpenGL(纯Cpp)
OpenGL.Shader:1-重新认识Android上的OpenGL(Cpp)
0、前言
从这篇文章开始新的系列内容(NativeOpenGL),主要收录的是Android上C++开发的OpenGL.ES,OpenGL.Shader,ARCore(NDK),以及新的图像渲染接口Vulkan,往后会继续扩展Unity,Unreal 等游戏引擎的学习。 在开展新系列的内容之前,假设你已经具备了一定的OpenGL基础知识。如果没有,可以到这里学习 OpenGL.ES在Android上的简单实践 。除了这点之外,希望还会一丢丢的Android C++基础 :)
开始内容前,扯下无聊。从标题上说,怎么看就怎么都觉得是个标题党。毕竟Android已经自带有GLSurfaceView的控件了,为什么还需要什么重新认识Android上的OpenGL?额。。。作为一个有qiang追po求zheng的程序员来说,编码不仅仅是为了完成需求就万事大吉。代码的风格,性能的考验,各种小因素都恰恰的影响着在看文章的你。而且,那一丢丢的基础知识就满足了吗?起码我自己是不满足的,so,NativeOpenGL系列文章应运而生。
1、还是EglCore
在网上传播的Android NativeOpenGL大多都是参考GoogleSamples/android-ndk上的 hello-gl2,gles3jni,两个项目没多大区别,都是借助GLSurfaceView的三大回调接口,然后通过jni实现逻辑。EGL环境生命周期都没能掌控,这还不算是真正的Native。之前的 OpenGL.ES在Android上的简单实践 系列文章中的水印录制,借助参考Google团队的Grafika项目,确实实现了由开发者完全掌控的EGL环境搭建,但却是Java版本。 所以现在要做的第一件事,就是自己实现Cpp版本的 EglCore EglSurfaceBase 和 WindowSurface三个主要类。
#ifndef NATIVECPPAPP_EGLCORE_H#define NATIVECPPAPP_EGLCORE_H#include #include "../common/constructormagic.h"#define FLAG_RECORDABLE 0x01#define FLAG_TRY_GLES2 0x02#define FLAG_TRY_GLES3 0x04// 参考android-26/android/opengl/EGLxt.java中的定义#define EGL_OPENGL_ES3_BIT_KHR 0x0040#define EGL_RECORDABLE_ANDROID 0x3142// 参考android-26/android/opengl/EGLxt.java中的定义// egl.h没有 eglPresentationTimeANDROID 的接口,// 所以只能自己定义函数指针,并通过eglGetProcAddress动态获取其函数地址了// 使用前记得判断是否为空typedef EGLBoolean (* EGL_PRESENTATION_TIME_ANDROID_PROC)(EGLDisplay display, EGLSurface surface, khronos_stime_nanoseconds_t time);class EglCore {public: EGLDisplay mEglDisplay; EGLContext mEglContext; int mEglVersion = -1; EGLConfig mEglConfig;public: EglCore(); ~EglCore(); EglCore(EGLContext sharedContext, int flags); int initEgl(EGLContext sharedContext, int flags); void release(); // 创建EGLSurface EGLSurface createWindowSurface(ANativeWindow * surface); // 创建离屏Surface EGLSurface createOffscreenSurface(int width, int height); // 查询当前surface的状态值。 int querySurface(EGLSurface eglSurface, int what); // 切换到当前上下文 void makeCurrent(EGLSurface eglSurface); // 切换到某个上下文 void makeCurrent(EGLSurface drawSurface, EGLSurface readSurface); // Makes no context current. void makeNothingCurrent(); // 交换缓冲区显示 bool swapBuffers(EGLSurface eglSurface); //判断当前的EGLContext 和 EGLSurface是否同一个EGL bool isCurrent(EGLSurface eglSurface); // Destroys the specified surface. // Note the EGLSurface won't actually be destroyed if it's still current in a context. void releaseSurface(EGLSurface eglSurface); // 设置pts void setPresentationTime(EGLSurface eglSurface, long nsecs); static void logCurrentEglState(); // 动态 设置pts方法 EGL_PRESENTATION_TIME_ANDROID_PROC eglPresentationTimeANDROID = NULL;private: EGLConfig getConfig(int flags, int version); void checkEglError(const char *msg);private: DISALLOW_EVIL_CONSTRUCTORS(EglCore);};#endif //NATIVECPPAPP_EGLCORE_H
其中选取EglCore为例讲解,因为这个最核心,也确实有点小问题需要注意,而且还有个C++的代码艺术介绍给看文章的你,我们从头文件开始,首先我们要自己定义一些标志位,EGL_OPENGL_ES3_BIT_KHR 和 EGL_RECORDABLE_ANDROID 这两个标志位我没在已知的系统头文件找到,所以只能参考Android-SDK android-26/android/opengl/EGLxt.java中自行定义。
第二个同样的问题,android-26/android/opengl/EGLxt.java中的eglPresentationTimeANDROID方法,也是找不到头文件定义,但发现了eglGetProcAddress这一个方法,这下子就妙啦。借助eglGetProcAddress可以动态查找想要的函数地址,通过C++的函数指针,我们也能实现其方法内容,不过eglGetProcAddress也不一定能确定查找得到eglPresentationTimeANDROID,所以使用前需要判断是否为空。
第三个就是我使用多年的 DISALLOW_EVIL_CONSTRUCTORS,这个其实是没必要的内容,但是理解这个内容后必定对C++又有更高程度的认识。 这宏定义做了些什么能让你得道飞升?请看这里的代码艺术
EglCore::EglCore() { mEglDisplay = EGL_NO_DISPLAY; mEglContext = EGL_NO_CONTEXT; mEglConfig = NULL; initEgl(NULL, 0);}EglCore::~EglCore() { if (mEglDisplay != EGL_NO_DISPLAY) { // 类析构函数 检测回收资源 LOGW(TAG, "WARNING: EglCore was not explicitly released -- state may be leaked"); release(); }}EglCore::EglCore(EGLContext sharedContext, int flags) { mEglDisplay = EGL_NO_DISPLAY; mEglContext = EGL_NO_CONTEXT; mEglConfig = NULL; initEgl(sharedContext, flags);}int EglCore::initEgl(EGLContext sharedContext, int flags){ if (mEglDisplay != EGL_NO_DISPLAY) { LOGE("EGL already set up"); return -1; } if (sharedContext == NULL) { sharedContext = EGL_NO_CONTEXT; } mEglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY); if(mEglDisplay == EGL_NO_DISPLAY) { LOGE("eglGetDisplay wrong"); return -1; } EGLint *version = new EGLint[2]; if (!eglInitialize(mEglDisplay, &version[0], &version[1]) ) { mEglDisplay = NULL; LOGE("unable to initialize"); return -1; } if ((flags & FLAG_TRY_GLES3) != 0) { EGLConfig config = getConfig(flags, 3); if (config != NULL) { const EGLint attrib3_list[] = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE }; EGLContext context = eglCreateContext(mEglDisplay, config, EGL_NO_CONTEXT, attrib3_list); if (eglGetError() == EGL_SUCCESS) { LOGD("Got GLES 3 config"); mEglConfig = config; mEglContext = context; mEglVersion = 3; } } } //如果只要求GLES版本2 又或者GLES3失败了。 if (mEglContext == EGL_NO_CONTEXT) { EGLConfig config = getConfig(flags, 2); if (config != NULL) { int attrib2_list[] = { EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE }; EGLContext context = eglCreateContext(mEglDisplay, config, sharedContext, attrib2_list); if (eglGetError() == EGL_SUCCESS) { LOGD("Got GLES 2 config"); mEglConfig = config; mEglContext = context; mEglVersion = 2; } } } // 动态获取eglPresentationTimeANDROID方法的地址 eglPresentationTimeANDROID = (EGL_PRESENTATION_TIME_ANDROID_PROC) eglGetProcAddress("eglPresentationTimeANDROID"); if (!eglPresentationTimeANDROID) { LOGE("eglPresentationTimeANDROID is not available!"); } return 0;}// 设置显示时间戳 ptsvoid EglCore::setPresentationTime(EGLSurface eglSurface, long nsecs) { if(eglPresentationTimeANDROID) eglPresentationTimeANDROID(mEglDisplay, eglSurface, nsecs); else LOGE("eglPresentationTimeANDROID is not available!");}
以上是部分的实现代码,我就不全部贴上来了,和之前Java版本的没两样,需要的同学到 https://github.com/MrZhaozhirong/NativeCppApp 这里follow。 需要注意的是,我们利用eglGetProcAddress获取eglPresentationTimeANDROID的函数地址,指向自定义的函数指针,再强调一遍,eglPresentationTimeANDROID不一定能找到!所以使用前记得判空。其次就是构造函数的mEglDisplay mEglContext mEglConfig三个变量一定要给一个默认空值,因为cpp默认不像java自动给空值,如果不给默认空值导致出现了野指针,initEgl的第一步mEglDisplay != EGL_NO_DISPLAY就直接结束了,请大家注意。
2、自己实现GLThread / GLRenderer
首先说明一下,java层代码非常简单,只需要获取SurfaceView的生命周期状态即可。真的真的很简单。
@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_native_gl); SurfaceView surfaceview = findViewById(R.id.easy_surface_view); surfaceview.getHolder().setFormat(PixelFormat.RGBA_8888); final NativeEGL nativeEGL = new NativeEGL(); surfaceview.getHolder().addCallback(new SurfaceHolder.Callback() { @Override public void surfaceCreated(SurfaceHolder holder) { nativeEGL.onSurfaceCreate(holder.getSurface()); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { nativeEGL.onSurfaceChange(width, height); } @Override public void surfaceDestroyed(SurfaceHolder holder) { nativeEGL.onSurfaceDestroy(); } });}
通过这三个方法,我们就能掌握到Android系统的渲染窗体Surface的生命周期。我们把获取surface并通过生命周期的方法传入到NativeEGL当中。NativeEGL代码也是贼简单。(因为优秀的我已经完美的封装实现好了)
GLThread *glThread = NULL;NativeGLRender* testRender = NULL;extern "C"JNIEXPORT void JNICALLJava_org_zzrblog_nativecpp_NativeEGL_onSurfaceCreate(JNIEnv *env, jobject instance, jobject surface){ ANativeWindow* nativeWindow = ANativeWindow_fromSurface(env, surface); glThread = new GLThread(); testRender = new NativeGLRender(); glThread->setGLRender(testRender); glThread->onSurfaceCreate(nativeWindow);}extern "C"JNIEXPORT void JNICALLJava_org_zzrblog_nativecpp_NativeEGL_onSurfaceChange(JNIEnv *env, jobject instance, jint width, jint height){ glThread->onSurfaceChange(width, height);}extern "C"JNIEXPORT void JNICALLJava_org_zzrblog_nativecpp_NativeEGL_onSurfaceDestroy(JNIEnv *env, jobject instance){ glThread->setGLRender(NULL); glThread->onSurfaceDestroy(); glThread->release(); delete testRender; delete glThread;}
显然GLThread是关键类,它跟随着Surface生命回调作相应的处理,现在就来看看GLThread的实现。
GLThread::GLThread() { isExit = false; isCreate = false; isChange = false; isStart = false;}GLThread::~GLThread() { }void *glThreadImpl(void *context){ GLThread *glThread = static_cast(context); glThread->isExit = false; while(true) { if (glThread->isExit) { LOGI("GLThread onDestroy."); if(glThread->mRender!=NULL) { glThread->mRender->surfaceDestroyed(); } break; } usleep(16000); // 1s/0.016s = 62.5 fps //onCreate if (glThread->isCreate) { glThread->isCreate = false; LOGI("GLThread onCreate."); if(glThread->mRender!=NULL) { glThread->mRender->surfaceCreated(glThread->window); } } //onChange if(glThread->isChange) { glThread->isChange = false; glThread->isStart = true; LOGI("GLThread onChange."); if(glThread->mRender!=NULL) { glThread->mRender->surfaceChanged(glThread->width,glThread->height); } } //onDraw if(glThread->isStart) { //LOGI("GLThread onDraw."); glThread->mRender->renderOnDraw(); } } return 0;}void GLThread::onSurfaceCreate(ANativeWindow* window){ this->window = window; this->isCreate = true; pthread_create(&mThreadImpl, NULL, glThreadImpl, this);}void GLThread::onSurfaceChange(int w, int h){ this->width = w; this->height = h; this->isChange = true;}void GLThread::onSurfaceDestroy() { this->isExit = true;}void GLThread::setGLRender(GLRender * render) { this->mRender = render;}
逻辑也是很简单,就一个线程,不断的根据状态回调render渲染器对象对应的方法,回想我们之前 GLSurfaceview源码分析的执行过程,也就是这样一个逻辑流程而已。
紧接着就是GLRender的接口类和对应的实现类NativeGLRender,代码如下:
#ifndef NATIVECPPAPP_GLRENDER_H#define NATIVECPPAPP_GLRENDER_H#include // C++接口类的注意事项// https://blog.csdn.net/netyeaxi/article/details/80724557class GLRender {public: virtual ~GLRender() {}; virtual void surfaceCreated(ANativeWindow *window)=0; virtual void surfaceChanged(int width, int height)=0; virtual void renderOnDraw()=0; virtual void surfaceDestroyed(void)=0;};#endif //NATIVECPPAPP_GLRENDER_H
其中Cpp定义接口类的语法,是Cpp面试多肽的基础考点,析构函数需要带virtual关键字,其中的知识点在注释的连接有所提及,我就不重复说明了。只有记住如果使用Cpp多肽这种特性的时候,纯虚基类必须是定义virtual的析构函数,并最好带上默认的实现。
#ifndef NATIVECPPAPP_NATIVEGLRENDER_H#define NATIVECPPAPP_NATIVEGLRENDER_H#include "../egl/GLRender.hpp"#include "../common/constructormagic.h"#include "../egl/EglCore.h"#include "../egl/WindowSurface.h"#include "../objects/CubeIndex.h"class NativeGLRender : public GLRender{public: NativeGLRender(); ~NativeGLRender(); void surfaceCreated(ANativeWindow *window) override; void surfaceChanged(int width, int height) override; void renderOnDraw() override; void surfaceDestroyed(void) override;private: int r_count; EglCore * mEglCore; WindowSurface * mWindowSurface; DISALLOW_EVIL_CONSTRUCTORS(NativeGLRender);};#endif //NATIVECPPAPP_NATIVEGLRENDER_H
#include #include #include "NativeGLRender.h"#include "../common/zzr_common.h"NativeGLRender::NativeGLRender() { mEglCore = NULL; mWindowSurface = NULL;}NativeGLRender::~NativeGLRender() { if (mWindowSurface) { mWindowSurface->release(); delete mWindowSurface; mWindowSurface = NULL; } if (mEglCore) { mEglCore->release(); delete mEglCore; mEglCore = NULL; }}void NativeGLRender::surfaceCreated(ANativeWindow *window){ if (mEglCore == NULL) { mEglCore = new EglCore(NULL, FLAG_RECORDABLE); } mWindowSurface = new WindowSurface(mEglCore, window, true); assert(mWindowSurface != NULL && mEglCore != NULL); LOGD("render surface create ... ");}void NativeGLRender::surfaceChanged(int width, int height){ mWindowSurface->makeCurrent(); LOGD("render surface change ... update MVP!"); mWindowSurface->swapBuffers();}void NativeGLRender::renderOnDraw(){ if (mEglCore == NULL) { LOGW("Skipping drawFrame after shutdown"); return; } mWindowSurface->makeCurrent(); glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT); r_count++; if(r_count > 255) { r_count = 0; } glClearColor(static_cast(r_count / 100.0), 0.6, static_cast(1.0 - r_count / 100.0), 1.0); mWindowSurface->swapBuffers();}void NativeGLRender::surfaceDestroyed(void){ // 清空自定义模型,纹理,各种BufferObject}
最后就是我们根据业务逻辑作出调整的GLRender实现类,使用上和GLSurfaceView.java基本上是一模一样的。到此我们就完全的在NDK内部,创建和实现了由开发者完全可控的EGL+渲染窗体+draw回调,类似于一个游戏引擎开发器一样,接管了上层的渲染窗体,就安安心心的实现业务逻辑了。而且性能上,我敢保证,是大大的优于Android.opengl.GLSurfaceView.java!
本项目代码 https://github.com/MrZhaozhirong/NativeCppApp(看清楚,之前的内容是BlogApp)
更多相关文章
- C语言函数的递归(上)
- Android(Java):对应用进行单元测试
- Android中对MIME类型的理解
- android Aspectj实践问题
- android 一个很漂亮的控件ObservableScrollView(含片段代码和源码
- android:layout_margin真实含义 及 自定义复合控件 layout()执行
- Android的回调机制
- Android(安卓)利用addView 动态给Activity添加View组件
- 第一行代码读书笔记 Kotlin Android