Hello World for Android

From JPCT
Revision as of 23:20, 2 February 2013 by Admin (Talk | contribs)

Jump to: navigation, search

A very simple HelloWorld for jPCT-AE

This code should help to get you started. It displays a simple, lit cube that you can rotate by using the touch screen. It includes basic pause/resume handling. I'll post the complete source code and comment on some important parts later on the page.

The source cde

package com.threed.jpct.example;

import java.lang.reflect.Field;

import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.opengles.GL10;

import android.app.Activity;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.view.MotionEvent;

import com.threed.jpct.Camera;
import com.threed.jpct.FrameBuffer;
import com.threed.jpct.Light;
import com.threed.jpct.Logger;
import com.threed.jpct.Object3D;
import com.threed.jpct.Primitives;
import com.threed.jpct.RGBColor;
import com.threed.jpct.SimpleVector;
import com.threed.jpct.Texture;
import com.threed.jpct.TextureManager;
import com.threed.jpct.World;
import com.threed.jpct.util.BitmapHelper;
import com.threed.jpct.util.MemoryHelper;

/**
 * A simple demo. This shows more how to use jPCT-AE than it shows how to write
 * a proper application for Android. It includes basic activity management to
 * handle pause and resume...
 * 
 * @author EgonOlsen
 * 
 */
public class HelloWorld extends Activity {

	// Used to handle pause and resume...
	private static HelloWorld master = null;

	private GLSurfaceView mGLView;
	private MyRenderer renderer = null;
	private FrameBuffer fb = null;
	private World world = null;
	private RGBColor back = new RGBColor(50, 50, 100);

	private float touchTurn = 0;
	private float touchTurnUp = 0;

	private float xpos = -1;
	private float ypos = -1;

	private Object3D cube = null;
	private int fps = 0;

	private Light sun = null;

	protected void onCreate(Bundle savedInstanceState) {

		Logger.log("onCreate");

		if (master != null) {
			copy(master);
		}

		super.onCreate(savedInstanceState);
		mGLView = new GLSurfaceView(getApplication());

		mGLView.setEGLConfigChooser(new GLSurfaceView.EGLConfigChooser() {
			public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
				// Ensure that we get a 16bit framebuffer. Otherwise, we'll fall
				// back to Pixelflinger on some device (read: Samsung I7500)
				int[] attributes = new int[] { EGL10.EGL_DEPTH_SIZE, 16, EGL10.EGL_NONE };
				EGLConfig[] configs = new EGLConfig[1];
				int[] result = new int[1];
				egl.eglChooseConfig(display, attributes, configs, 1, result);
				return configs[0];
			}
		});

		renderer = new MyRenderer();
		mGLView.setRenderer(renderer);
		setContentView(mGLView);
	}

	@Override
	protected void onPause() {
		super.onPause();
		mGLView.onPause();
	}

	@Override
	protected void onResume() {
		super.onResume();
		mGLView.onResume();
	}

	@Override
	protected void onStop() {
		super.onStop();
	}

	private void copy(Object src) {
		try {
			Logger.log("Copying data from master Activity!");
			Field[] fs = src.getClass().getDeclaredFields();
			for (Field f : fs) {
				f.setAccessible(true);
				f.set(this, f.get(src));
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public boolean onTouchEvent(MotionEvent me) {

		if (me.getAction() == MotionEvent.ACTION_DOWN) {
			xpos = me.getX();
			ypos = me.getY();
			return true;
		}

		if (me.getAction() == MotionEvent.ACTION_UP) {
			xpos = -1;
			ypos = -1;
			touchTurn = 0;
			touchTurnUp = 0;
			return true;
		}

		if (me.getAction() == MotionEvent.ACTION_MOVE) {
			float xd = me.getX() - xpos;
			float yd = me.getY() - ypos;

			xpos = me.getX();
			ypos = me.getY();

			touchTurn = xd / -100f;
			touchTurnUp = yd / -100f;
			return true;
		}

		try {
			Thread.sleep(15);
		} catch (Exception e) {
			// No need for this...
		}

		return super.onTouchEvent(me);
	}

	protected boolean isFullscreenOpaque() {
		return true;
	}

	class MyRenderer implements GLSurfaceView.Renderer {

		private long time = System.currentTimeMillis();

		public MyRenderer() {
		}

		public void onSurfaceChanged(GL10 gl, int w, int h) {
			if (fb != null) {
				fb.dispose();
			}
			fb = new FrameBuffer(gl, w, h);

			if (master == null) {

				world = new World();
				world.setAmbientLight(20, 20, 20);

				sun = new Light(world);
				sun.setIntensity(250, 250, 250);

				// Create a texture out of the icon...:-)
				Texture texture = new Texture(BitmapHelper.rescale(BitmapHelper.convert(getResources().getDrawable(R.drawable.icon)), 64, 64));
				TextureManager.getInstance().addTexture("texture", texture);

				cube = Primitives.getCube(10);
				cube.calcTextureWrapSpherical();
				cube.setTexture("texture");
				cube.strip();
				cube.build();

				world.addObject(cube);

				Camera cam = world.getCamera();
				cam.moveCamera(Camera.CAMERA_MOVEOUT, 50);
				cam.lookAt(cube.getTransformedCenter());

				SimpleVector sv = new SimpleVector();
				sv.set(cube.getTransformedCenter());
				sv.y -= 100;
				sv.z -= 100;
				sun.setPosition(sv);
				MemoryHelper.compact();

				if (master == null) {
					Logger.log("Saving master Activity!");
					master = HelloWorld.this;
				}
			}
		}

		public void onSurfaceCreated(GL10 gl, EGLConfig config) {
		}

		public void onDrawFrame(GL10 gl) {
			if (touchTurn != 0) {
				cube.rotateY(touchTurn);
				touchTurn = 0;
			}

			if (touchTurnUp != 0) {
				cube.rotateX(touchTurnUp);
				touchTurnUp = 0;
			}

			fb.clear(back);
			world.renderScene(fb);
			world.draw(fb);
			fb.display();

			if (System.currentTimeMillis() - time >= 1000) {
				Logger.log(fps + "fps");
				fps = 0;
				time = System.currentTimeMillis();
			}
			fps++;
		}
	}
}

Comments on the source code

onCreate(Bundle savedInstanceState)

This is the basic setup for an Activity that uses OpenGL ES 1.x. It can be found in many Android examples except for two small parts:

  • The master/copy-lines: With these, i'm doing very basic pause/resume handling. I'm sure that there are better ways but this is the first thing that i came up with, i never had a problem with it and it works fine for me even in complex applications. Feel free to use anything you like instead.
  • The creation of a dedicated EGLConfigChooser: This serves one single purpose, which is to make 3D acceleration on my old Samsung Galaxy. I never came across any other device that required this and i most likely never will. But it doesn't hurt either, so i'm using it in all my OpenGL ES 1.x Activities. If you aren't targetting a real old device running on Android 1.5, you should be save to omit this part. If you are using OpenGL ES 2.0, it's not an issue anyway.
protected void onCreate(Bundle savedInstanceState) {

	Logger.log("onCreate");

	if (master != null) {
		copy(master);
	}

	super.onCreate(savedInstanceState);
	mGLView = new GLSurfaceView(getApplication());

	mGLView.setEGLConfigChooser(new GLSurfaceView.EGLConfigChooser() {
		public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
			// Ensure that we get a 16bit framebuffer. Otherwise, we'll fall
			// back to Pixelflinger on some device (read: Samsung I7500)
			int[] attributes = new int[] { EGL10.EGL_DEPTH_SIZE, 16, EGL10.EGL_NONE };
			EGLConfig[] configs = new EGLConfig[1];
			int[] result = new int[1];
			egl.eglChooseConfig(display, attributes, configs, 1, result);
			return configs[0];
		}
	});

	renderer = new MyRenderer();
	mGLView.setRenderer(renderer);
	setContentView(mGLView);
}
copy(Object src)

Again, this is my way to implement pause/resume/destroy/create of the Activity. It's pretty hacky. The basic idea is to keep a copy of the first instance of the Activity in a static field and copy all other fields to the new one. Again, i don't claim that this is a good solution. But it works for me and i never bothered to find another one.

private void copy(Object src) {
	try {
		Logger.log("Copying data from master Activity!");
		Field[] fs = src.getClass().getDeclaredFields();
		for (Field f : fs) {
			f.setAccessible(true);
			f.set(this, f.get(src));
		}
	} catch (Exception e) {
		throw new RuntimeException(e);
	}
}
onTouchEvent(MotionEvent me)

This is the basic touch event handling. There are two things to note here:

  • The events all set flags/variables to be evaluated in the render thread. They don't fiddle around with jPCT-AE objects directly. That's because that yould interfere with the rendering (which happens in another thread in parallel) and jPCT-AE isn't thread safe, so this wouldn't be a good idea.
  • There's a sleep in there. On older Android versions, the DalvikVM's garbage collection was really slow and the idea was to slow down the processing of touch events, so that less garbage would be created (but you might get choppy controls in return). However, looking at this code, it doesn't seem to make much sense, because in almost every case, the method will returned earlier anyway and it won't reach that sleep in most cases. I'm not sure, what i was thinking here...it might be best to ignore this sleep...
public boolean onTouchEvent(MotionEvent me) {

	if (me.getAction() == MotionEvent.ACTION_DOWN) {
		xpos = me.getX();
		ypos = me.getY();
		return true;
	}

	....

	try {
		Thread.sleep(15);
	} catch (Exception e) {
		// No need for this...
	}

	return super.onTouchEvent(me);
}