利用EGL在android上使用C/C++写OpenGL ES程序
很多教程都是在C/C++写的OpenGL的代码,其中有很多优秀的框架,除了前面提过的Assimp库外,还有很强大的库glm,从另外一个角度来看,在学习EGL的时候,很多的资料都是C语言的代码,我在android上写OpenGL ES的代码似乎从来没见过那些代码,不使用一下总觉得缺少点什么。
事实上,Android在native层构建OpenGL环境的步骤就如同前面博客OpenGL ES EGL介绍中讲过的这样,在Java层,与EGL相关的操作我们也看不到,其实都是由GLSurfaceView封装好了。
因此这篇博客就研究一下怎么使用Native代码写OpenGL ES代码和如何利用EGL自己创建OpenGL的环境。最终包含:
1.使用Native代码+GLSurfaceView写的六边形,这里会介绍到glm库。
2.Java代码自己写一个功能类似GLSurfaceView的类,主要是在java层使用EGL自己创建OpenGL的环境,思想上参考了GLSurfaceView的源码。
3.全部使用native代码写的六边形。Java层仅用了SurfaceView,余下的功能全部在native层实现。
先贴上效果图,这三幅图分别是上面描述的三个程序执行得到的结果。
第一幅图是第一篇写OpenGL入门的时候使用的Demo,我用本博客第二部分写的MySurfaceView替换了系统自带的GLSurfaceView执行的效果。
第一幅图和三幅图也是画一个六边形,只是没有旋转,大小并且都是用AndroidStudio创建的项目。
使用Native代码+GLSurfaceView
这种方式其实就是将GLSurfaceView.Renderer的几个回调函数在native层实现,算是体验C/C++写OpenGL代码的第一步吧。Render类大概就是下面的样子
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
String vetexShaderStr = LoadShaderStr(mContext, R.raw.vshader);
String fragmentShaderStr = LoadShaderStr(mContext, R.raw.fshader);
// native
nativeInit(vetexShaderStr, fragmentShaderStr);
}
@Override
public void onDrawFrame(GL10 gl) {
// native
nativeDraw(mAngleX, mAngleY);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
// native
nativeSurfaceChanged(width, height);
}
public static native void nativeInit(String vertexShaderCode, String fragmentShaderCode);
private static native void nativeDraw(float angleX, float angleY);
private static native void nativeSurfaceChanged(int width, int height);
}
在native层要做的事情其实和前面用java语言写的代码是类似的,无外乎就是写一个六边形的类,函数名也是一致的,因此写起来很简单。
使用Java代码的时候在android.opengl包中有个很方便的类Matrix,里面封装了矩阵的相关操作,设置投影、摄像机矩阵、矩阵相乘等操作都被封装好了,在C/C++中可没有这个类,不过也不是难事。Android Native Development Kit Cookbook一书的作者就封装了相关操作。
#ifndef MATRIX_H
#define MATRIX_H
#include <math.h>
#define MYPI 3.14159265358979323846
void translate_matrix(float tx, float ty, float tz, float *M);
void scale_matrix(float xs, float ys, float zs, float *M);
void rotate_matrix(float angle, float x, float y, float z, float *M);
void perspective_matrix(float fovy, float aspect, float znear, float zfar, float *M);
void multiply_matrix(float *A, float *B, float *M);
#endif
#include "matrix.h"
//all matrix are in column-major order
//the 4x4 matrix are stored in an array as shown below
//0-15 indicates the array index
//0 4 8 12
//1 5 9 13
//2 6 10 14
//3 7 11 15
void load_identity(float *M) {
for (int i = 0; i < 16; ++i) {
M[i] = 0.0f;
}
M[0] = 1.0f;
M[5] = 1.0f;
M[10] = 1.0f;
M[15] = 1.0f;
}
//return translation matrix
// 1 0 0 tx
// 0 1 0 ty
// 0 0 1 tz
// 0 0 0 1
void translate_matrix(float tx, float ty, float tz, float *M) {
load_identity(M);
M[12] = tx;
M[13] = ty;
M[14] = tz;
}
//return scaling matrix
// sx 0 0 0
// 0 sy 0 0
// 0 0 sz 0
// 0 0 0 1
void scale_matrix(float sx, float sy, float sz, float *M) {
load_identity(M);
M[0] *= sx;
M[5] *= sy;
M[10] *= sz;
}
//return rotation matrix
//R = Rx*Ry*Rz
// 1 0 0 0
// Rx = 0 cosx -sinx 0
// 0 sinx cosx 0
// 0 0 0 1
// cosy 0 siny 0
// 0 1 0 0
// Ry =-siny 0 cosy 0
// 0 0 0 1
//
// cosz -sinz 0 0
// Rz= sinz cosz 0 0
// 0 0 1 0
// 0 0 0 1
//refer to http://lignumcad.sourceforge.net/doc/en/HTML/SOHTML/TechnicalReference.html
//for detailed info
void rotate_matrix(float angle, float x, float y, float z, float *M) {
double radians, c, s, c1, u[3], length;
int i, j;
radians = (angle * MYPI) / 180.0;
c = cos(radians);
s = sin(radians);
c1 = 1.0 - cos(radians);
length = sqrt(x * x + y * y + z * z);
u[0] = x / length;
u[1] = y / length;
u[2] = z / length;
for (i = 0; i < 16; i++) {
M[i] = 0.0;
}
M[15] = 1.0;
for (i = 0; i < 3; i++) {
M[i * 4 + (i + 1) % 3] = u[(i + 2) % 3] * s;
M[i * 4 + (i + 2) % 3] = -u[(i + 1) % 3] * s;
}
for (i = 0; i < 3; i++) {
for (j = 0; j < 3; j++) {
M[i * 4 + j] += c1 * u[i] * u[j] + (i == j ? c : 0.0);
}
}
}
/* simulate gluPerspectiveMatrix
* //return perspective projection matrix
* cot(fovy/2) / aspect 0 0 0
* 0 cot(fovy/2) 0 0
* 0 0 (znear+zfar)/(znear-zfar) 2*znear*zfa/(znear-zfar)
* 0 0 -1 0
*/
void perspective_matrix(float fovy, float aspect, float znear, float zfar, float *M) {
int i;
double f;
load_identity(M);
f = 1.0/tan(fovy * 0.5);
M[0] = f / aspect;
M[5] = f;
M[10] = (znear + zfar) / (znear - zfar);
M[11] = -1.0;
M[14] = (2.0 * znear * zfar) / (znear - zfar);
M[15] = 0.0;
}
//Multiplies A by B and output to C, a tmp buffer is used to support in-place multiplication
void multiply_matrix(float *A, float *B, float *C) {
int i, j, k;
float tmpM[16];
for (i = 0; i < 4; i++) {
for (j = 0; j < 4; j++) {
tmpM[j * 4 + i] = 0.0;
for (k = 0; k < 4; k++) {
tmpM[j * 4 + i] += A[k * 4 + i] * B[j * 4 + k];
}
}
}
for (i = 0; i < 16; i++) {
C[i] = tmpM[i];
}
}
不过我觉得这都是不必要的工作,很浪费时间,事实上有一个非常强大的数学库glm(OpenGL Mathematics
)主要还体现在它是为OpenGL开发人员量身定做的,和使用着色器语言一样,使用很方便。我在这个Demo中主要用了它的设置摄像机矩阵、投影矩阵。
#include "glm/mat4x4.hpp"
#include "glm/ext.hpp"
glm::mat4 projection;
glm::mat4 view;
//glm::mat4 module;
projection = glm::ortho(-1.0f, 1.0f, -(float) height / width, (float) height / width, 5.0f,
7.0f);
view = glm::lookAt(glm::vec3(0.0f, 0.0f, 6.0f),
glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3(0.0f, 1.0f, 0.0f));
// 矩阵相乘太方便
glm::mat4 mvpMatrix = projection * view/* * module*/;
// 装换成数组形式
float *mvp = (float *) glm::value_ptr(mvpMatrix);
mShape.draw(mvp);
代码在这里:https://github.com/qhyuan1992/OpenGL-ES/tree/master/NativeGLESView
在Java层使用EGL
其实在利用OpenGL ES进行Android手游录屏研究这篇博客中已经接触到了Java层的EGL使用了,GLSurfaceView继承自SurfaceView,我们用它来写OpenGL代码很方便,就是因为它已经封装好了创建EGL环境的部分了,如果看GLSurfaceView的代码必然可以看到前面讲EGL部分的时候创建EGL环境的相关函数调用。
不过由于GLSurfaceView代码比较多,下面是我自己写的一个类似GLSurfaceView的类,写的很简略,用其来替代系统的GLSurfaceView即可。显然这样做用来了解Java层EGL的使用是足够的。
里面有部分代码我是参考GLSurfaceView的源码的。
package com.example.opengles_circle;
import java.lang.ref.WeakReference;
import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.egl.EGLSurface;
import android.content.Context;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class MyGLSurfaceView extends SurfaceView implements SurfaceHolder.Callback{
public SurfaceHolder mHolder;
public Renderer mRenderer;
private final WeakReference<MyGLSurfaceView> mThisWeakRef = new WeakReference<MyGLSurfaceView>(this);
Object mLock = new Object();
private GLThread mThread;
public MyGLSurfaceView(Context context) {
super(context);
mHolder = getHolder();
mHolder.addCallback(this);
}
public void setRenderer(Renderer renderer) {
mRenderer = renderer;
mThread = new GLThread(mThisWeakRef);
mThread.start();
}
public void surfaceCreated(SurfaceHolder holder) {
mThread.surfaceCreated();
}
public void surfaceDestroyed(SurfaceHolder holder) {
mThread.surfaceDestroyed();
}
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
mThread.surfaceChanged(w, h);
}
public void stopRender() {
// 设置停止标示,notify
}
public interface Renderer {
void onSurfaceCreated();
void onSurfaceChanged(int width, int height);
void onDrawFrame();
}
}
class GLThread extends Thread {
private Object lock = new Object();
private EGLHelper mEglHelper;
private boolean mHasSurface = false;
public int mWidth;
public int mHeight;
private WeakReference<MyGLSurfaceView> mGLSurfaceViewWeakRef;
GLThread(WeakReference<MyGLSurfaceView> glSurfaceViewWeakRef) {
super();
mWidth = 0;
mHeight = 0;
mGLSurfaceViewWeakRef = glSurfaceViewWeakRef;
}
@Override
public void run() {
try {
doThread();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void doThread() throws InterruptedException {
mEglHelper = new EGLHelper(mGLSurfaceViewWeakRef);
MyGLSurfaceView view = mGLSurfaceViewWeakRef.get();
// 确保有了Surface
synchronized (lock) {
if (!mHasSurface) {
lock.wait();
}
}
mEglHelper.start();
/*----实际上是在一个循环里面针对不同的事件(onSurfaceCreated/onSurfaceChanged/onDrawFrame/terminal)执行不同的回调----*/
/*-----所有情况都执行完后,wait,当某个事件发生后标志置位,然后调用notify----*/
view.mRenderer.onSurfaceCreated();
// 也要确保在surfaceChanged之后执行
view.mRenderer.onSurfaceChanged(mWidth, mHeight);
view.mRenderer.onDrawFrame();
mEglHelper.swap();
}
public void surfaceCreated() {
}
public void surfaceChanged(int w, int h) {
mWidth = w;
mHeight = h;
synchronized (lock) {
mHasSurface = true;
lock.notifyAll();
}
}
public void surfaceDestroyed() {
synchronized (lock) {
mHasSurface = false;
lock.notifyAll();
}
}
}
class EGLHelper{
public WeakReference<MyGLSurfaceView> mGLSurfaceViewWeakRef;
private EGL10 mEgl;
private EGLDisplay mEglDisplay;
private EGLConfig mEglConfig;
private EGLContext mEglContext;
private EGLSurface mEglSurface;
public EGLHelper(WeakReference<MyGLSurfaceView> glSurfaceViewWeakRef) {
mGLSurfaceViewWeakRef = glSurfaceViewWeakRef;
}
// start函数中就是创建EGL环境的步骤
public void start(){
int[] num_config = new int[1];
EGLConfig[] configs = new EGLConfig[1];
int[] configSpec = { EGL10.EGL_RED_SIZE, 8,
EGL10.EGL_GREEN_SIZE, 8,
EGL10.EGL_BLUE_SIZE, 8,
EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT, EGL10.EGL_NONE };
this.mEgl = (EGL10) EGLContext.getEGL();
mEglDisplay = this.mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
this.mEgl.eglInitialize(mEglDisplay, null);
this.mEgl.eglChooseConfig(mEglDisplay, configSpec, configs, 1, num_config);
mEglConfig = configs[0];
mEglSurface = this.mEgl.eglCreateWindowSurface(mEglDisplay, mEglConfig,
mGLSurfaceViewWeakRef.get().mHolder, null);
int[] attrs = {0x3098, 2, EGL10.EGL_NONE}; // EGL_CONTEXT_CLIENT_VERSION
mEglContext = this.mEgl.eglCreateContext(mEglDisplay, mEglConfig,
EGL10.EGL_NO_CONTEXT, attrs);
this.mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext);
}
public void swap() {
mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
}
public void destroySurface() {
if (mEglSurface != null) {
mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
mEglSurface = null;
}
if (mEglContext != null) {
mEgl.eglDestroyContext(mEglDisplay, mEglContext);
mEglContext = null;
}
if (mEglDisplay != null) {
mEgl.eglTerminate(mEglDisplay);
mEglDisplay = null;
}
}
}
可以看到当调用setRender时就开启了一个子线程GLThread,不过这个线程没有立即执行而是wait直到surface创建成功。
EGLHelper就是一个工具类,里面的start函数就包含了创建EGL环境的步骤,等待EGL上下文创建好了,在执行 view.mRenderer.onDrawFrame();回调中其实就是我们要实现的部分,在里面进行OpenGL绘图,然后在执行EGLHelper的swap函数,将后台缓冲中的内容显示到native_window也就是屏幕上去。
一般来说在这个GLThread线程中是一个无限循环,根据收到的不同状态值,执行对应的操作,每执行一轮就会调用wait直到其他的状态被设置后notify唤醒,在循环执行。
在这里注意一下eglCreateWindowSurface函数中的native_window参数。它是一个Object对象,它的实现在com.google.android.gles_jni.EGLImpl.java(EGLImpl.java),先记住native_window可以传入的参数是SurfaceView、SurfaceHolder或者Surface,最终都转换成了Surface,Surface在Java层代表这一个窗口。
EGLSurface eglCreateWindowSurface(EGLDisplay display, EGLConfig config, Object native_window, int[] attrib_list) {
Surface sur = null;
if (native_window instanceof SurfaceView) {
SurfaceView surfaceView = (SurfaceView)native_window;
sur = surfaceView.getHolder().getSurface();
} else if (native_window instanceof SurfaceHolder) {
SurfaceHolder holder = (SurfaceHolder)native_window;
sur = holder.getSurface();
} else if (native_window instanceof Surface) {
// .......
}
在native层使用EGL
在native层使用EGL创建EGL环境并且使用C/C++写OpenGL代码,思路就是结合前面两个Demo,在native层创建EGL环境和在Java层是类似的思路,只是有些API可能不一样,并且还用到了很多NDK的知识,
代码在这里https://github.com/qhyuan1992/OpenGL-ES/tree/master/NativeGLESViewWithEGL
核心部分的代码
void Renderer::requestInitEGL(ANativeWindow * pWindow) {
LOGI(1, "-------requestInitEGL");
pthread_mutex_lock(&mMutex);
mWindow = pWindow;
// mEnumRenderEvent表示事件
mEnumRenderEvent = RE_SURFACE_CHANGED;
LOGI(1, "-------mEnumRenderEvent=%d", mEnumRenderEvent);
pthread_mutex_unlock(&mMutex);
// 唤醒处于wait状态的线程,继续函数onRenderThreadRun的执行,onRenderThreadRun是线程的入口函数
pthread_cond_signal(&mCondVar);
}
// 每次需要绘制的时候调用,实际上会调用到
void Renderer::requestRenderFrame() {
pthread_mutex_lock(&mMutex);
mEnumRenderEvent = RE_DRAW_FRAME;
pthread_mutex_unlock(&mMutex);
pthread_cond_signal(&mCondVar);
}
void Renderer::requestDestroy() {
pthread_mutex_lock(&mMutex);
mEnumRenderEvent = RE_EXIT;
pthread_mutex_unlock(&mMutex);
pthread_cond_signal(&mCondVar);
}
void Renderer::onRenderThreadRun() {
mISRenderering = true;
while(mISRenderering) {
pthread_mutex_lock(&mMutex);
// 每完成一个事件就wait在这里直到有其他事件唤醒
pthread_cond_wait(&mCondVar, &mMutex);
LOGI(1, "-------this mEnumRenderEvent is %d", mEnumRenderEvent);
switch (mEnumRenderEvent) {
case RE_SURFACE_CHANGED:
LOGI(1, "-------case RE_SURFACE_CHANGED");
mEnumRenderEvent = RE_NONE;
pthread_mutex_unlock(&mMutex);
initEGL();
nativeSurfaceCreated();
nativeSurfaceChanged(mWidth, mHeight);
break;
case RE_DRAW_FRAME:
mEnumRenderEvent = RE_NONE;
pthread_mutex_unlock(&mMutex);
// draw
// 这个函数留给真实的GLES绘制操作。
nativeDraw();
eglSwapBuffers(mDisplay, mSurface);
break;
case RE_EXIT:
mEnumRenderEvent = RE_NONE;
pthread_mutex_unlock(&mMutex);
terminateDisplay();
mISRenderering = false;
break;
default:
mEnumRenderEvent = RE_NONE;
pthread_mutex_unlock(&mMutex);
}
}
}
下面我也把native层创建EGL环境的部分列出来,显然和Java层是类似的。
void Renderer::initEGL() {
const EGLint attribs[] = {
EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
EGL_BLUE_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_RED_SIZE, 8,
EGL_NONE
};
EGLint width, height, format;
EGLint numConfigs;
EGLConfig config;
EGLSurface surface;
EGLContext context;
EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(display, 0, 0);
eglChooseConfig(display, attribs, &config, 1, &numConfigs);
surface = eglCreateWindowSurface(display, config, mWindow, NULL);
EGLint attrs[]= {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
context = eglCreateContext(display, config, NULL, attrs);
if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE) {
LOGI(1, "------EGL-FALSE");
return ;
}
eglQuerySurface(display, surface, EGL_WIDTH, &width);
eglQuerySurface(display, surface, EGL_HEIGHT, &height);
mDisplay = display;
mSurface = surface;
mContext = context;
mWidth = width;
mHeight = height;
LOGI(1, "width:%d, height:%d", mWidth, mHeight);
}
在Java层继承自一个SurfaceView
public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback {
public MySurfaceView(Context context) {
super(context);
getHolder().addCallback(this);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
nativeRequestRender();
return super.onTouchEvent(event);
}
@Override
public void surfaceCreated(SurfaceHolder surfaceHolder) {
setRender();
}
@Override
public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
nativeSurfaceChanged(surfaceHolder.getSurface());
}
@Override
public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
nativeSurfaceDestroyed();
}
private void setRender() {
nativeStartRender();
}
private static native void nativeSurfaceChanged(Surface surface);
private static native void nativeSurfaceDestroyed();
private static native void nativeStartRender();
private static native void nativeRequestRender();
static {
System.loadLibrary("NativeWithEGL");
}
}
转而在native层执行SurfaceHolder.Callback的回调,注意和前面的GLSurfaceView.Renderer的回调。
接下来在合适的时刻调用Renderer的request***函数,发出某种消息,比如执行requestRenderFrame函数,也就是发出了要绘制一帧的消息,那么在绘制线程中,就去执行OpenGL ES绘制操作代码。
ANativeWindow * mWindow;
Renderer * mRenderer;
extern "C" {
JNIEXPORT void JNICALL
Java_com_example_weiersyuan_nativeglesviewwithegl_MySurfaceView_nativeStartRender(JNIEnv *env, jclass type) {
mRenderer = new Renderer();
mRenderer->start();
}
JNIEXPORT void JNICALL
Java_com_example_weiersyuan_nativeglesviewwithegl_MySurfaceView_nativeSurfaceChanged(JNIEnv *env, jclass type, jobject surface) {
mWindow = ANativeWindow_fromSurface(env, surface);
// surfacechange时 发送SurfaceChanged消息,此时创建egl环境的消息
mRenderer->requestInitEGL(mWindow);
}
JNIEXPORT void JNICALL
Java_com_example_weiersyuan_nativeglesviewwithegl_MySurfaceView_nativeSurfaceDestroyed(JNIEnv *env, jclass type) {
mRenderer->requestDestroy();
ANativeWindow_release(mWindow);
delete mRenderer;
}
JNIEXPORT void JNICALL
Java_com_example_weiersyuan_nativeglesviewwithegl_MySurfaceView_nativeRequestRender(JNIEnv *env, jclass type){
mRenderer->requestRenderFrame();
}
}
在Native层ANativeWindow对象代表一个窗口,看起来好像和Java层的Surface是对应着的,native层的ANativeWindow的指针是Java层的Surface对象的一个成员变量,这也是持久保存native层数据的常用方式,在NDK中有很多类似于ANativeWindow_fromSurface的函数,将Java层的独享转换为native层的对象,都是类似的方式。
Java代码自己写一个功能类似GLSurfaceView的类