OpenGL + C++ + Java
这个组合有一点奇怪,因为要实现在 opengl 中播放视频,所以不得不有这样奇怪的组合。上层的 MediaPlayer 封装的基本上是针对 android UI 的框架,如果想在 opengl 中显示,估计难度很大。另外,很多开源的 opengl 的游戏基本上都是 C++ 编写,所以这个体系的作用还是很大的,之所以需要 java ,因为上层的很多例如触摸、重力感应,这样的东西, android 是以 java 实现的,底层的根本看不到,这样的组合起来,充分的利用每一个层次的优点,最大效率提高用户体验。
我们首先会简单的测试一下 opengl 立方体例子,然后我们实现在 native 层上面的绘制,如何使用 jni 来传递。最后我们简单的说一下使用自带的 android 封装的 opengl 接口 api 有什么样的限制。
Google 提供的 api 是 java 的 api ,对于 java 开发者来说,这是好消息,对于 C++ 开发者来说,就会有很大的麻烦。现在主流的游戏引擎、游戏算法基本上都是 C/C++ 实现。
我们纯粹利用 C/C++ 来写 OpenGL 的应用程序的时候,基本流程如下,初始化 EGL ,选择配置,然后选择 Display ,然后创建 Surface ,之后就是 DrawFram 和 Swap 了。 Google 很聪明的给这个流程进行了一次封装,封装成这样的几个类,大致的代码如下:
首先是 EGL 的相关操作:
public class EglHelper {
public EglHelper() {
}
/**
* Initialize EGL for a given configuration spec.
* @param configSpec
*/
public void start( int [] configSpec){
/*
* Get an EGL instance
*/
mEgl = (EGL10) EGLContext.getEGL ();
/*
* Get to the default display.
*/
mEglDisplay = mEgl .eglGetDisplay(EGL10. EGL_DEFAULT_DISPLAY );
/*
* We can now initialize EGL for that display
*/
int [] version = new int [2];
mEgl .eglInitialize( mEglDisplay , version);
EGLConfig[] configs = new EGLConfig[1];
int [] num_config = new int [1];
mEgl .eglChooseConfig( mEglDisplay , configSpec, configs, 1,
num_config);
mEglConfig = configs[0];
/*
* Create an OpenGL ES context. This must be done only once, an
* OpenGL context is a somewhat heavy object.
*/
mEglContext = mEgl .eglCreateContext( mEglDisplay , mEglConfig ,
EGL10. EGL_NO_CONTEXT , null );
mEglSurface = null ;
}
/*
* Create and return an OpenGL surface
*/
public GL createSurface(SurfaceHolder holder) {
/*
* The window size has changed, so we need to create a new
* surface.
*/
if ( mEglSurface != null ) {
/*
* Unbind and destroy the old EGL surface, if
* there is one.
*/
mEgl .eglMakeCurrent( mEglDisplay , EGL10. EGL_NO_SURFACE ,
EGL10. EGL_NO_SURFACE , EGL10. EGL_NO_CONTEXT );
mEgl .eglDestroySurface( mEglDisplay , mEglSurface );
}
/*
* Create an EGL surface we can render into.
*/
mEglSurface = mEgl .eglCreateWindowSurface( mEglDisplay ,
mEglConfig , holder, null );
/*
* Before we can issue GL commands, we need to make sure
* the context is current and bound to a surface.
*/
mEgl .eglMakeCurrent( mEglDisplay , mEglSurface , mEglSurface ,
mEglContext );
GL gl = mEglContext .getGL();
return gl;
}
/**
* Display the current render surface.
* @return false if the context has been lost.
*/
public boolean swap() {
mEgl .eglSwapBuffers( mEglDisplay , mEglSurface );
/*
* Always check for EGL_CONTEXT_LOST, which means the context
* and all associated data were lost (For instance because
* the device went to sleep). We need to sleep until we
* get a new surface.
*/
return mEgl .eglGetError() != EGL11. EGL_CONTEXT_LOST ;
}
public void finish() {
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 ;
}
}
public int getError () {
return mEgl .eglGetError();
}
// public void setGLWrapper(GLWrapper glWrapper) {
// mGLWrapper = glWrapper;
// }
// private GLWrapper mGLWrapper;
EGL10 mEgl ;
EGLDisplay mEglDisplay ;
EGLSurface mEglSurface ;
EGLConfig mEglConfig ;
EGLContext mEglContext ;
}
上面的代码我不多说。有兴趣的自己看,无非是对 EGL 进行一次简单的封装。
然后实现一个 SurfaceView 专门针对 opengl 的。这里有一个 GLSurfaceView 。
/*
* Copyright (C) 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package opengl.scenes;
import opengl.jni.Natives;
import android.content.Context;
import android.graphics.PixelFormat;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
/**
* An implementation of SurfaceView that uses the dedicated surface for
* displaying an OpenGL animation. This allows the animation to run in a
* separate thread, without requiring that it be driven by the update mechanism
* of the view hierarchy.
*
* The application-specific rendering code is delegated to a GLView.Renderer
* instance.
*/
public class GLSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
public GLSurfaceView(Context context) {
super(context);
init();
}
public GLSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
// Install a SurfaceHolder.Callback so we get notified when the
// underlying surface is created and destroyed
mHolder = getHolder();
mHolder.addCallback(this);
mHolder.setType(SurfaceHolder.SURFACE_TYPE_GPU);
// 得到我们的 holder 并且添加 callback
}
public SurfaceHolder getSurfaceHolder() {
return mHolder;
}
// 这个是我们添加的一个借口,只要调用了这个就会 render ,创建一个线程
public void setRenderer(Renderer renderer) {
mGLThread = new GLThread(renderer, mHolder);
mGLThread.start();
System.out.println("GLSurfaceView::setRenderer setting natives listener");
Natives.setListener(mGLThread);
}
public void surfaceCreated(SurfaceHolder holder) {
mGLThread.surfaceCreated();
}
public void surfaceDestroyed(SurfaceHolder holder) {
// Surface will be destroyed when we return
mGLThread.surfaceDestroyed();
}
public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
// Surface size or format has changed. This should not happen in this
// example.
mGLThread.onWindowResize(w, h);
}
/**
* Inform the view that the activity is paused.
*/
public void onPause() {
mGLThread.onPause();
}
/**
* Inform the view that the activity is resumed.
*/
public void onResume() {
mGLThread.onResume();
}
/**
* Inform the view that the window focus has changed.
*/
@Override public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
mGLThread.onWindowFocusChanged(hasFocus);
}
/**
* Queue an "event" to be run on the GL rendering thread.
* @param r the runnable to be run on the GL rendering thread.
*/
public void queueEvent(Runnable r) {
mGLThread.queueEvent(r);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mGLThread.requestExitAndWait();
}
private SurfaceHolder mHolder;
private GLThread mGLThread;
}
下面就是 GLThread 这个整个这几个内,这里略。通过上面的代码我们可以知道,实际上整个的 Opengl 上层 google 给的凤凰其实还是按照我们 C/C++ 的编写习惯来的,实际上还是通过 jni 调用下面的 EGL 接口。这样 java 可以使用,但是就会有一个问题,频繁的 jni 调用严重的影响效率。特别是连我们的绘制都是 jni 的调用,这样效率就会大大的折扣,所以这里我们开始把上面的过程的大部分转移到 native 层。分为如下几个步骤:
1、 初始化: OpenGLES 是一个单线程的东西。需要初始化一个 GLContext ,同时只有可能一个线程来访问这个东西。在 EGL 中,这一步分为了如下几步:
a、 得到一个 EGL 的 Context 。 EGLContex.getEgl();
b、 得到默认的 display 。
c、 初始化 display
d、 设置像素格式 深度大小等等
2、 主循环,一帧又一帧的绘制。
3、 真正的绘制 swap
4、 清空
上面所有的东西都可以通过纯粹的 java 来实现,这里感谢一下 google 。但是某些步骤同样也可以用 C/C++ 来实现的。哪些可以通过 java :
1、 初始化工作。获得 EGL 的实例初始化显示器和一些颜色和深度的设置。
2、 Java 的 main loop ,这个 loop 不做什么事情,直接调用 native 的方法进行绘制。
3、 Swap buffer 。
一个 C/C++ 开源的 opengl 代码最大的部分就是他的显示,至于如何 swap 如何初始化都是这些代码不关心的,所以可以用 java 来实现。
我们用这种思想在此实现我们的 OpenGL 程序:
Main Activity 主要的 activity :
public class NativeGLActivity extends Activity
{
private GLSurfaceView mGLSurfaceView ;
// 首先载入我们的 so
{
final String LIB_PATH = "/data/libgltest_jni.so" ;
System. out .println( "Loading JNI lib using abs path:" + LIB_PATH);
System.load (LIB_PATH);
}
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super .onCreate(savedInstanceState);
//setContentView(R.layout.main);
mGLSurfaceView = new GLSurfaceView( this );
try {
mGLSurfaceView .setRenderer( new CubeRenderer( true , true ));
setContentView( mGLSurfaceView );
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void onResume() {
// Ideally a game should implement onResume() and onPause()
// to take appropriate action when the activity looses focus
super .onResume();
mGLSurfaceView .onResume();
}
@Override
protected void onPause() {
// Ideally a game should implement onResume() and onPause()
// to take appropriate action when the activity looses focus
super .onPause();
mGLSurfaceView .onPause();
}
}
因为我们的 Render 有这样的一个功能,自动的选择是 java 的 DrawFrame 或者 C/C++ 的 draw Frame :
public class CubeRenderer implements Renderer
{
private boolean mNativeDraw = false ;
public CubeRenderer( boolean useTranslucentBackground, boolean nativeDraw) {
mTranslucentBackground = useTranslucentBackground;
mNativeDraw = nativeDraw;
mCube = new Cube();
}
public void drawFrame(GL10 gl ) {
if ( mNativeDraw )
doNativeDraw();
else
doJavaDraw(gl );
}
这里我们直接看看我们的 doNativeDraw 方法。
public void doNativeDraw() {
Natives.NativeRender ();
}
这样我们看看我们下面的主要的 C++ 代码:
JNIEXPORT jint JNICALL Java_opengl_jni_Natives_NativeRender
(JNIEnv *, jclass);
/*
* Class: opengl_jni_Natives
* Method: RenderTest
* Signature: ()V
*/
JNIEXPORT jint JNICALL Java_opengl_jni_Natives_NativeRender
(JNIEnv * env, jclass cls)
{
(*env)->GetJavaVM(env, &g_VM);
static int initialized = 0;
if ( ! initialized ) {
jni_printf("Native:RenderTest initscene");
init_scene();
initialized = 1;
}
drawFrame();
return 1;
}
static void drawFrame()
{
/*
* By default, OpenGL enables features that improve quality
* but reduce performance. One might want to tweak that
* especially on software renderer.
*/
glDisable(GL_DITHER);
glTexEnvx(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE,GL_MODULATE);
/*
* Usually, the first thing one might want to do is to clear
* the screen. The most efficient way of doing this is to use
* glClear().
*/
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
/*
* Now we're ready to draw some 3D objects
*/
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(0, 0, -3.0f);
glRotatef(mAngle, 0, 0, 1.0f);
glRotatef(mAngle*0.25f, 1, 0, 0);
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
Cube_draw();
glRotatef(mAngle*2.0f, 0, 1, 1);
glTranslatef(0.5f, 0.5f, 0.5f);
Cube_draw();
mAngle += 1.2f;
}
基本上把原来 java 使用的东西全部搬到了我们下面的。
显示效果基本一样,全部都是 58fps 左右。由于代码比较简单,好像 c++ 的速度优势还木有显示出来。
小结一下:这里有一些注意的问题,当今智能设备越来越强大, GPU 的能力也越来越大。毫无疑问,用 OpenGL 来写嵌入式设备上的应用程序是大势所趋。像 quake 这样的游戏,已经被移植到很多智能平台上面了这个游戏使用的是直接绘制一些特殊的几何图形,例如下面是绘制一个多边形。
glBegin(GL_POLYGON);
glTexcoord2();
…………
glEnd();
这种方式很显然只是桌面应用程序的,在 android 上面是不适用的,因为 OES 是没有多边形绘制的接口。移植这样的代码很困难。这是我们要考虑的第一点。
然后就是浮点数的问题。很多嵌入式设备都没有 FPU 的概念,浮点运算很弱。 Opengl ES 使用 16 位来表示整数部分,剩下的 16 位表示小数部分。所以浮点数可以通过另外一种方式:
glTranslatex ( 10<<16,0,0,2<<16 ) ; // glTranslatex(10.0f,0.0f,0.0f,2.0f)
另外一点就是这里木有 GLU 工具函数,可以自己去实现。