从这篇文章开始新的系列内容(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系列文章应运而生。
在网上传播的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;
}
// 设置显示时间戳 pts
void 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就直接结束了,请大家注意。
首先说明一下,java层代码非常简单,只需要获取SurfaceView的生命周期状态即可。真的真的很简单。
@Override
protected 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 JNICALL
Java_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 JNICALL
Java_org_zzrblog_nativecpp_NativeEGL_onSurfaceChange(JNIEnv *env, jobject instance,
jint width, jint height)
{
glThread->onSurfaceChange(width, height);
}
extern "C"
JNIEXPORT void JNICALL
Java_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/80724557
class 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)