Browse Source

finished threat indicator (needs backend)

Fabio Arnold 10 years ago
parent
commit
d86874d23e

BIN
assets/meshes/android.amh


+ 1 - 1
src/de/tudarmstadt/informatik/hostage/ui2/activity/MainActivity.java

@@ -55,7 +55,7 @@ public class MainActivity extends Activity {
 		setContentView(R.layout.activity_drawer_main);
 
 		ThreatIndicatorGLRenderer.assets = getAssets();
-		ThreatIndicatorGLRenderer.setThreatLevel(0);
+		ThreatIndicatorGLRenderer.setThreatLevel(ThreatIndicatorGLRenderer.kThreatLevelNotMonitoring);
 		// set background color
 		TypedArray arr = getTheme().obtainStyledAttributes(
 				new int[] { android.R.color.background_light });

+ 5 - 0
src/de/tudarmstadt/informatik/hostage/ui2/fragment/HomeFragment.java

@@ -17,6 +17,7 @@ import android.widget.TextView;
 
 import de.tudarmstadt.informatik.hostage.R;
 import de.tudarmstadt.informatik.hostage.commons.HelperUtils;
+import de.tudarmstadt.informatik.hostage.ui2.fragment.opengl.ThreatIndicatorGLRenderer;
 
 /**
  * @author Alexander Brakowski
@@ -81,6 +82,8 @@ public class HomeFragment extends Fragment {
 		mHomeTextAttacks.setTextColor(getResources().getColor(R.color.light_grey));
 		mHomeTextProfile.setTextColor(getResources().getColor(R.color.light_grey));
 		mHomeTextProfileHeader.setTextColor(getResources().getColor(R.color.light_grey));
+		
+	    ThreatIndicatorGLRenderer.setThreatLevel(ThreatIndicatorGLRenderer.kThreatLevelNotMonitoring);
 
 		mHomeSwitchConnection.setChecked(false);
 	}
@@ -97,6 +100,8 @@ public class HomeFragment extends Fragment {
 		mHomeTextName.setTextColor(defaultTextColor);
 		mHomeTextProfile.setTextColor(defaultTextColor);
 		mHomeTextProfileHeader.setTextColor(defaultTextColor);
+		
+	    ThreatIndicatorGLRenderer.setThreatLevel(ThreatIndicatorGLRenderer.kThreatLevelNoThreat);
 
 		mHomeSwitchConnection.setChecked(true);
 	}

+ 79 - 19
src/de/tudarmstadt/informatik/hostage/ui2/fragment/opengl/AnimatedMesh.java

@@ -14,6 +14,13 @@ import android.opengl.GLES20;
 import android.opengl.Matrix;
 import android.util.Log;
 
+/**
+ * @author Fabio Arnold
+ *
+ * Animated Mesh
+ * This class reads a mesh in the AMSH binary format and creates the necessary OpenGL objects for drawing
+ */
+
 public class AnimatedMesh {
 	private ByteBuffer data;
 	
@@ -31,6 +38,39 @@ public class AnimatedMesh {
 	private ArrayList<Action> actions;
 	private Action currentAction;
 	private int currentFrame;
+	private boolean loopAction = false;
+	private boolean reverseAction = false;
+	
+	public void startAction(String actionName, boolean loop, boolean reverse) {
+		if (!(currentAction != null && currentAction.name.equals(actionName) && reverse)) // keep the current frame
+			currentFrame = 0;
+		loopAction = loop;
+		reverseAction = reverse;
+		currentAction = null;
+		// find the action
+		for (Action action : actions) {
+			if (action.name.equals(actionName)) {
+				currentAction = action;
+				break;
+			}
+		}
+		if (currentAction != null && reverseAction)
+			if (!(currentAction != null && currentAction.name.equals(actionName) && reverse)) // keep the current frame
+				currentFrame = currentAction.numFrames - 1;
+	}
+	
+	public boolean isActionReversed() {
+		return reverseAction;
+	}
+	
+	public boolean isActionDone() {
+		if (currentAction == null)
+			return false;
+		if (reverseAction)
+			return currentFrame <= 0;
+		else
+			return currentFrame >= currentAction.numFrames - 1;
+	}
 	
 	private class Bone {
 		public String name; // 15 bytes
@@ -45,7 +85,7 @@ public class AnimatedMesh {
 				if ((c = (char)data.get()) == '\0') stop = true;
 				if (!stop) name += c;
 			}
-			Log.i("bone", name);
+			// Log.i("bone", name);
 			parentIndex = (int)data.get();
 			invBindPose = new float[16];
 			data.asFloatBuffer().get(invBindPose);
@@ -58,6 +98,8 @@ public class AnimatedMesh {
 		public int numFrames;
 		public ArrayList<Track> tracks;
 		
+		public static final int kHeaderSize = 28;
+		
 		Action(ByteBuffer data) {
 			name = "";
 			boolean stop = false;
@@ -72,7 +114,7 @@ public class AnimatedMesh {
 			int trackCount = data.getInt();
 			tracks = new ArrayList<Track>();
 			for (int i = 0; i < trackCount; i++) {
-				data.position(trackOffset + i * 12); // track header size == 12
+				data.position(trackOffset + i * Track.kHeaderSize);
 				tracks.add(new Track(data));
 			}
 		}
@@ -82,6 +124,8 @@ public class AnimatedMesh {
 		public int boneIndex;
 		public ArrayList<JointPose> poses;
 		
+		public static final int kHeaderSize = 12;
+		
 		Track(ByteBuffer data) {
 			boneIndex = data.getInt();
 			int jointPoseOffset = data.getInt();
@@ -89,7 +133,7 @@ public class AnimatedMesh {
 			poses = new ArrayList<JointPose>();
 			data.position(jointPoseOffset);
 			for (int i = 0; i < jointPoseCount; i++) {
-				//data.position(jointPoseOffset + i * 32); // joint pose size == 32
+				//data.position(jointPoseOffset + i * JointPose::kSize); // joint pose size == 32
 				poses.add(new JointPose(data));
 			}
 		}
@@ -100,6 +144,8 @@ public class AnimatedMesh {
 		public float[] translation;
 		float scale;
 		
+		public static final int kSize = 32;
+		
 		JointPose() { // empty pose == identity
 			rotation = new Quaternion();
 			translation = new float[3];
@@ -109,7 +155,7 @@ public class AnimatedMesh {
 		
 		JointPose(ByteBuffer data) {
 			FloatBuffer floatData = data.asFloatBuffer();
-			data.position(data.position() + 32);
+			data.position(data.position() + kSize);
  
 			// quat data is x y z w, because of glm
 			float x = floatData.get();
@@ -215,12 +261,13 @@ public class AnimatedMesh {
 		// actions
 		actions = new ArrayList<Action>();
 		for (int i = 0; i < actionCount; i++) {
-			data.position(actionOffset + i * 28); // action header size == 28
+			data.position(actionOffset + i * Action.kHeaderSize); // action header size == 28
 			actions.add(new Action(data));
 		}
 		
-		currentAction = actions.get(actions.size() - 1);
+		currentAction = null;
 		currentFrame = 0;
+		loopAction = false;
 	}
 	
 	public static float[] addVec3(final float[] v1, final float[] v2) {
@@ -230,22 +277,39 @@ public class AnimatedMesh {
 		v3[2] = v1[2] + v2[2];
 		return v3;
 	}
+	
+	private boolean halfFramerate = false;
 
 	public void tick() {
-		currentFrame++;
-		if (currentFrame >= currentAction.numFrames)
-			currentFrame = 0;
-		
 		// empty pose
 		ArrayList<JointPose> pose = new ArrayList<JointPose>();
 		for (int i = 0; i < bones.size(); i++)
 			pose.add(new JointPose());
 		
-		// fill pose with action
-		for (int i = 0; i < currentAction.tracks.size(); i++) {
-			// TODO: do lerp or something nice
-			pose.get(i).rotation = currentAction.tracks.get(i).poses.get(currentFrame).rotation;
-			pose.get(i).translation = currentAction.tracks.get(i).poses.get(currentFrame).translation;
+		if (currentAction != null) {
+			// fill pose with action
+			for (int i = 0; i < currentAction.tracks.size(); i++) {
+				// TODO: do lerp or something nice
+				pose.get(i).rotation = currentAction.tracks.get(i).poses.get(currentFrame).rotation;
+				pose.get(i).translation = currentAction.tracks.get(i).poses.get(currentFrame).translation;
+			}
+
+			// advance one frame
+			if (reverseAction) {
+				if (currentFrame > 0) {
+					if (halfFramerate = !halfFramerate)
+						currentFrame--;
+				} else if (loopAction) {
+					currentFrame = currentAction.numFrames - 1;
+				}
+			} else {
+				if (currentFrame < currentAction.numFrames - 1) {
+					if (halfFramerate = !halfFramerate)
+						currentFrame++;
+				} else if (loopAction) {
+					currentFrame = 0;
+				}
+			}
 		}
 		
 		// convert pose to skinning matrices
@@ -276,16 +340,12 @@ public class AnimatedMesh {
 		GLES20.glEnableVertexAttribArray(boneIndicesIndex);
 		GLES20.glEnableVertexAttribArray(boneWeightsIndex);
 		
-		//data.position(vertexOffset);
-		//GLES20.glVertexAttribPointer(positionIndex, 3, GLES20.GL_FLOAT, false, vertexSize, data.asFloatBuffer());
 		GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, vertexBuffer);
 		GLES20.glVertexAttribPointer(positionIndex, 3, GLES20.GL_FLOAT, false, vertexSize, 0);
 		GLES20.glVertexAttribPointer(normalIndex, 3, GLES20.GL_FLOAT, false, vertexSize, 12);
 		GLES20.glVertexAttribPointer(texCoordIndex, 2, GLES20.GL_FLOAT, false, vertexSize, 24);
 		GLES20.glVertexAttribPointer(boneIndicesIndex, 4, GLES20.GL_UNSIGNED_BYTE, false, vertexSize, 32);
 		GLES20.glVertexAttribPointer(boneWeightsIndex, 3, GLES20.GL_FLOAT, false, vertexSize, 36);
-		//data.position(triangleOffset);
-		//GLES20.glDrawElements(GLES20.GL_TRIANGLES, 3 * triangleCount, GLES20.GL_UNSIGNED_INT, data.asIntBuffer());
 		GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
 		GLES20.glDrawElements(GLES20.GL_TRIANGLES, 3 * triangleCount, GLES20.GL_UNSIGNED_SHORT, 0);
 		GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, 0);

+ 20 - 7
src/de/tudarmstadt/informatik/hostage/ui2/fragment/opengl/HomeGLSurfaceView.java

@@ -1,24 +1,37 @@
 package de.tudarmstadt.informatik.hostage.ui2.fragment.opengl;
 
 import android.content.Context;
-import android.graphics.PixelFormat;
 import android.opengl.GLSurfaceView;
 import android.util.AttributeSet;
 import android.util.Log;
+import android.view.MotionEvent;
 
 public class HomeGLSurfaceView extends GLSurfaceView {
 	public HomeGLSurfaceView(Context context) { // won't be called
 		super(context);
 		Log.e("gl", "called wrong constructor (w/o attributes)");
 	}
-
-	public HomeGLSurfaceView(Context context, AttributeSet attrs) { // will be called
+	
+	// this constructor will be called
+	public HomeGLSurfaceView(Context context, AttributeSet attrs) {
 		super(context, attrs);
 		setEGLContextClientVersion(2); // OpenGL ES 2.0
-		//setZOrderOnTop(true);
+		// setZOrderOnTop(true);
 		// transparency
-		//setEGLConfigChooser(8, 8, 8, 8, 16, 0);
-		//getHolder().setFormat(PixelFormat.RGBA_8888);
-        setRenderer(new ThreatIndicatorGLRenderer());
+		// setEGLConfigChooser(8, 8, 8, 8, 16, 0);
+		// getHolder().setFormat(PixelFormat.RGBA_8888);
+		setRenderer(new ThreatIndicatorGLRenderer());
+	}
+	
+	// TODO: just for testing -> remove this eventually
+	@Override
+	public boolean onTouchEvent(MotionEvent event) {
+		if (event.getAction() == MotionEvent.ACTION_DOWN) {
+			int threatLevel = 0;
+			if (event.getX() > 0.5f * getWidth()) threatLevel += 1;
+			if (event.getY() > 0.5f * getHeight()) threatLevel += 2;
+			ThreatIndicatorGLRenderer.setThreatLevel(threatLevel);
+		}
+		return false;
 	}
 }

+ 172 - 40
src/de/tudarmstadt/informatik/hostage/ui2/fragment/opengl/ThreatIndicatorGLRenderer.java

@@ -18,13 +18,34 @@ import android.opengl.GLUtils;
 import android.opengl.Matrix;
 import android.util.Log;
 
+/**
+ * @author Fabio Arnold
+ * 
+ * ThreatIndicatorGLRenderer
+ * This class is responsible for drawing an animation representing the current threat level.
+ * Use the method setThreatLevel to set the state (0 to 3).
+ */
+
 public class ThreatIndicatorGLRenderer implements Renderer {
-	private static int threatLevel = 0;
+	public static final int kThreatLevelNotMonitoring = 0;
+	public static final int kThreatLevelNoThreat = 1;
+	public static final int kThreatLevelPastThreat = 2;
+	public static final int kThreatLevelPersistentThreat = 3;
 	
+	/**
+	 * Set the threat level which should be indicated
+	 * @param level 0 to 3 but better use the constants
+	 */
 	public static void setThreatLevel(int level) {
-		threatLevel = level;
+		assert(level >= 0 && level <= 3);
+		nextThreatLevel = level;
+		Log.i("threat indicator", "next threat level = " + level);
 	}
 
+	/**
+	 * Match the background color of the view holding this renderer
+	 * @param color 32 bit integer encoding the color
+	 */
 	public static void setBackgroundColor(int color) {
 		backgroundColor[0] = (float)Color.red(color) / 255.0f;
 		backgroundColor[1] = (float)Color.green(color) / 255.0f;
@@ -32,12 +53,13 @@ public class ThreatIndicatorGLRenderer implements Renderer {
 	}
 	private static float[] backgroundColor = new float[3];
 	
-	public static AssetManager assets;
+	public static AssetManager assets = null; // needs to be set by the Activity using this class
 	
 	private int width;
 	private int height;
 	private float aspectRatio;
 	
+	// OpenGL data
 	private int program;
 	private float [] modelview;
 	private float [] projection;
@@ -47,13 +69,24 @@ public class ThreatIndicatorGLRenderer implements Renderer {
 	private AnimatedMesh beeMesh = null;
 	private int androidTexture;
 	private int beeTexture;
+	
+	// threat state
+	private static int nextThreatLevel = kThreatLevelNoThreat;
+	
+	private int currentThreatLevel = kThreatLevelNoThreat;
+	private int targetThreatLevel = kThreatLevelNoThreat;
+	private float threatLevelTransition = 1.0f; // 1.0 means transition is complete
 
-	public ThreatIndicatorGLRenderer() {}
+	private long startTimeMillis; // for animation
+	public ThreatIndicatorGLRenderer() {
+		startTimeMillis = System.currentTimeMillis();
+	}
 	
+	/**
+	 * Initialization will be called after GL context is created and is current
+	 */
 	public void onSurfaceCreated(GL10 arg0, EGLConfig arg1) {
-		//GLES20.glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
 		GLES20.glClearColor(backgroundColor[0], backgroundColor[1], backgroundColor[2], 1.0f);
-		// GLES20.glClearColor(0.3f, 0.3f, 0.3f, 1.0f); // dark background
 		GLES20.glEnable(GLES20.GL_DEPTH_TEST);
 		GLES20.glEnable(GLES20.GL_CULL_FACE);
 		GLES20.glEnable(GLES20.GL_TEXTURE_2D);
@@ -61,6 +94,7 @@ public class ThreatIndicatorGLRenderer implements Renderer {
 		try {
 			InputStream is = assets.open("meshes/android.amh");
 			androidMesh = new AnimatedMesh(is);
+			androidMesh.startAction("greet", false, false);
 		} catch (IOException e) {
 			Log.e("gl", "Couldn't open android mesh");
 		}
@@ -68,6 +102,7 @@ public class ThreatIndicatorGLRenderer implements Renderer {
 		try {
 			InputStream is = assets.open("meshes/bee.amh");
 			beeMesh = new AnimatedMesh(is);
+			beeMesh.startAction("bee_armatureAct", true, false);
 		} catch (IOException e) {
 			Log.e("gl", "Couldn't open bee mesh");
 		}
@@ -78,6 +113,7 @@ public class ThreatIndicatorGLRenderer implements Renderer {
 		projection = new float[16];
 		mvp = new float[16];
 		
+		// default shader
 		String vertexSource = "attribute vec3 position; void main() {gl_Position = vec4(position, 1.0);}";
 		String fragmentSource = "void main() {gl_FragColor = vec4(1.0, 0.0, 1.0, 1.0);}";
 		try {
@@ -91,63 +127,148 @@ public class ThreatIndicatorGLRenderer implements Renderer {
 		}
 		program = loadProgram(vertexSource, fragmentSource);
 	}
-
-	static float beeAnimation = 0.0f;
-	
-	private float[] mixColor(float alpha, float[] color1, float[] color2) {
-		float[] color3 = new float[4];
-		color3[0] = (1.0f - alpha) * color1[0] + alpha * color2[0];
-		color3[1] = (1.0f - alpha) * color1[1] + alpha * color2[1];
-		color3[2] = (1.0f - alpha) * color1[2] + alpha * color2[2];
-		color3[3] = (1.0f - alpha) * color1[3] + alpha * color2[3];
-		return color3;
-	}
 	
+	/**
+	 * Will be called every ~16 milliseconds -> 60 Hz
+	 */
 	public void onDrawFrame(GL10 arg0) {
+		long timeMillis = System.currentTimeMillis() - startTimeMillis;
+		double animTime = 0.001 * (double)timeMillis; // in seconds
+		
+		// threat level state machine
+		if (targetThreatLevel != currentThreatLevel) {
+			boolean blocked = false; // block until current action is completed
+			if (threatLevelTransition == 0.0f) {
+				if (androidMesh.isActionDone()) {
+					switch (targetThreatLevel) {
+					case kThreatLevelNotMonitoring:
+						androidMesh.startAction("sleep", false, false);
+						break;
+					case kThreatLevelNoThreat:
+						androidMesh.startAction("happy", true, false);
+						break;
+					case kThreatLevelPastThreat:
+						androidMesh.startAction("fear", true, false);
+						break;
+					case kThreatLevelPersistentThreat:
+						androidMesh.startAction("panic", true, false);
+						break;
+					}
+				} else blocked = true;
+			}
+			
+			if (!blocked) {
+				threatLevelTransition += 0.016f;
+				if (threatLevelTransition >= 1.0f) {
+					currentThreatLevel = targetThreatLevel;
+					threatLevelTransition = 1.0f;
+				}
+			}
+		} else {
+			if (nextThreatLevel != targetThreatLevel) {
+				targetThreatLevel = nextThreatLevel;
+				threatLevelTransition = 0.0f;
+				if (currentThreatLevel == kThreatLevelNotMonitoring)
+					androidMesh.startAction("sleep", false, true);
+			}
+		}
+		
+		androidMesh.tick(); // animate android
+		
+		// OpenGL drawing
 		GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
 		
 		GLES20.glUseProgram(program);
 		int colorUniformLoc = GLES20.glGetUniformLocation(program, "color");
 		int textureUniformLoc = GLES20.glGetUniformLocation(program, "texture");
 		int mvpUniformLoc = GLES20.glGetUniformLocation(program, "mvp");
-		
-		//final float[] androidColor = {165.0f / 255.0f, 202.0f / 255.0f, 57.0f / 255.0f, 1.0f}; // ok
+
 		final float[] whiteColor = {1.0f, 1.0f, 1.0f, 1.0f};
+		final float[] greyColor = {0.5f, 0.5f, 0.5f, 1.0f};
 		final float[] redColor = {2.0f, 0.4f, 0.2f, 1.0f};
-		final float[] yellowColor = {1.0f, 1.0f, 0.0f, 1.0f};
+		final float[] yellowColor = {1.2f * 255.0f / 166.0f, 1.2f * 255.0f / 200.0f, 0.0f, 1.0f};
 		
-		float[] color = mixColor(0.5f + 0.5f * (float)Math.sin(0.2f * beeAnimation), whiteColor, redColor);
-		GLES20.glUniform4fv(colorUniformLoc, 1, color, 0);
+		float[] currentColor = whiteColor;
+		float blink = 0.5f + 0.5f * (float)Math.sin(12.0 * animTime);
+		switch (currentThreatLevel) {
+		case kThreatLevelNotMonitoring:
+			currentColor = greyColor;
+			break;
+		case kThreatLevelPastThreat:
+			currentColor = mixColor(blink, whiteColor, yellowColor);
+			break;
+		case kThreatLevelPersistentThreat:
+			currentColor = mixColor(blink, whiteColor, redColor);
+			break;
+		}
+		if (targetThreatLevel != currentThreatLevel) {
+			float[] targetColor = whiteColor;
+			switch (targetThreatLevel) {
+			case kThreatLevelNotMonitoring:
+				targetColor = greyColor;
+				break;
+			case kThreatLevelPastThreat:
+				targetColor = mixColor(blink, whiteColor, yellowColor);
+				break;
+			case kThreatLevelPersistentThreat:
+				targetColor = mixColor(blink, whiteColor, redColor);
+				break;
+			}
+			currentColor = mixColor(threatLevelTransition, currentColor, targetColor);
+		}
+		GLES20.glUniform4fv(colorUniformLoc, 1, currentColor, 0);
 		
 		GLES20.glUniform1i(textureUniformLoc, 0);
 		
 		Matrix.setIdentityM(modelview, 0);
-		Matrix.translateM(modelview, 0, 0.0f, -0.8f, -2.0f);
-		Matrix.rotateM(modelview, 0, -80.0f, 1.0f, 0.0f, 0.0f);
+		if (currentThreatLevel == kThreatLevelPersistentThreat || targetThreatLevel == kThreatLevelPersistentThreat) {
+			float delta = 1.0f;
+			if (threatLevelTransition < 0.4f) {
+				delta = threatLevelTransition / 0.4f;
+				delta = -2.0f * delta * delta * delta + 3.0f * delta * delta; // ease in/out
+			}
+			if (targetThreatLevel != kThreatLevelPersistentThreat)
+				delta = 1.0f - delta;
+			Matrix.translateM(modelview, 0, 0.0f, -0.6f - 0.2f * delta, -1.6f - 0.4f * delta); // 0.0f, -0.8f, -2.0f
+			Matrix.rotateM(modelview, 0, -85.0f + 5.0f * delta, 1.0f, 0.0f, 0.0f); // -80.0f
+			
+		} else {
+			Matrix.translateM(modelview, 0, 0.0f, -0.6f, -1.6f);
+			Matrix.rotateM(modelview, 0, -85.0f, 1.0f, 0.0f, 0.0f);
+		}
 		Matrix.multiplyMM(mvp, 0, projection, 0, modelview, 0);
 		
 		GLES20.glUniformMatrix4fv(mvpUniformLoc, 1, false, mvp, 0);
 		
 		GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, androidTexture);
-		androidMesh.tick();
 		androidMesh.draw(program);
 		
 		GLES20.glUniform4fv(colorUniformLoc, 1, whiteColor, 0);
 
-		beeAnimation += 1.0f;
-		float beeSize = 0.2f;
-		Matrix.rotateM(modelview, 0, -4.0f * beeAnimation, 0.0f, 0.0f, 1.0f);
-		Matrix.translateM(modelview, 0, 0.6f, 0.0f, 0.7f + 0.1f * (float)Math.sin(0.2f * beeAnimation));
-		Matrix.rotateM(modelview, 0, 20.0f * (float)Math.cos(0.2f * beeAnimation), 1.0f, 0.0f, 0.0f);
-		Matrix.scaleM(modelview, 0, beeSize, beeSize, beeSize);
-		Matrix.multiplyMM(mvp, 0, projection, 0, modelview, 0);
-		GLES20.glUniformMatrix4fv(mvpUniformLoc, 1, false, mvp, 0);
-
-		GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, beeTexture);
-		beeMesh.tick();
-		beeMesh.draw(program);
+		if (currentThreatLevel == kThreatLevelPersistentThreat || targetThreatLevel == kThreatLevelPersistentThreat) {
+			// draw a bee rotating around the android
+			
+			float fadeIn = threatLevelTransition;
+			if (targetThreatLevel != kThreatLevelPersistentThreat) fadeIn = 1.0f - fadeIn; // fade out
+			float beePositionZ = 2.0f * (1.0f - fadeIn) * (1.0f - fadeIn); // animate the bee going in/out
+			
+			final float beeSize = 0.2f;
+			Matrix.rotateM(modelview, 0, (float)((-240.0 * animTime) % 360.0), 0.0f, 0.0f, 1.0f); // rotate around android
+			Matrix.translateM(modelview, 0, 0.6f, 0.0f, 0.7f + 0.1f * (float)Math.sin(12.0 * animTime) + beePositionZ); // go up and down
+			Matrix.rotateM(modelview, 0, 20.0f * (float)Math.cos(12.0 * animTime), 1.0f, 0.0f, 0.0f); // rock back and forth
+			Matrix.scaleM(modelview, 0, beeSize, beeSize, beeSize);
+			Matrix.multiplyMM(mvp, 0, projection, 0, modelview, 0);
+			GLES20.glUniformMatrix4fv(mvpUniformLoc, 1, false, mvp, 0);
+	
+			GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, beeTexture);
+			beeMesh.tick();
+			beeMesh.draw(program);
+		}
 	}
 
+	/**
+	 * Informs renderer of changed surface dimensions
+	 */
 	public void onSurfaceChanged(GL10 arg0, int w, int h) {
 		width = w;
 		height = h;
@@ -159,7 +280,18 @@ public class ThreatIndicatorGLRenderer implements Renderer {
 		GLES20.glViewport(0, 0, width, height);
 	}
 	
-	public int loadTexture(String filePath) {
+	// some private helper methods
+	
+	private float[] mixColor(float alpha, float[] color1, float[] color2) {
+		float[] color3 = new float[4];
+		color3[0] = (1.0f - alpha) * color1[0] + alpha * color2[0];
+		color3[1] = (1.0f - alpha) * color1[1] + alpha * color2[1];
+		color3[2] = (1.0f - alpha) * color1[2] + alpha * color2[2];
+		color3[3] = (1.0f - alpha) * color1[3] + alpha * color2[3];
+		return color3;
+	}
+	
+	private int loadTexture(String filePath) {
 		Bitmap bitmap = null;
 		try {
 			bitmap = BitmapFactory.decodeStream(assets.open(filePath));
@@ -192,7 +324,7 @@ public class ThreatIndicatorGLRenderer implements Renderer {
 	    return result;
 	}
 	
-	public static int loadShader(int type, String source) {
+	private static int loadShader(int type, String source) {
 		int shader = GLES20.glCreateShader(type);
 		GLES20.glShaderSource(shader, source);
 		GLES20.glCompileShader(shader);
@@ -200,7 +332,7 @@ public class ThreatIndicatorGLRenderer implements Renderer {
 		return shader;
 	}
 	
-	public static int loadProgram(String vertexSource, String fragmentSource) {
+	private static int loadProgram(String vertexSource, String fragmentSource) {
 		int program = GLES20.glCreateProgram();
 		GLES20.glAttachShader(program, loadShader(GLES20.GL_VERTEX_SHADER, vertexSource));
 		GLES20.glAttachShader(program, loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource));