// Copyright 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 com.google.android.stardroid.renderer;
import com.google.android.stardroid.renderer.util.GLBuffer;
import com.google.android.stardroid.renderer.util.SkyRegionMap;
import com.google.android.stardroid.renderer.util.TextureManager;
import com.google.android.stardroid.renderer.util.UpdateClosure;
import com.google.android.stardroid.units.GeocentricCoordinates;
import com.google.android.stardroid.units.Vector3;
import com.google.android.stardroid.util.Matrix4x4;
import com.google.android.stardroid.util.VectorUtil;
import android.content.res.Resources;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
import android.util.FloatMath;
import android.util.Log;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class SkyRenderer implements GLSurfaceView.Renderer {
private SkyBox mSkyBox = null;
private OverlayManager mOverlayManager = null;
private RenderState mRenderState = new RenderState();
private Matrix4x4 mProjectionMatrix;
private Matrix4x4 mViewMatrix;
// Indicates whether the transformation matrix has changed since the last
// time we started rendering
private boolean mMustUpdateView = true;
private boolean mMustUpdateProjection = true;
private Set<UpdateClosure> mUpdateClosures = new TreeSet<UpdateClosure>();
private RendererObjectManager.UpdateListener mUpdateListener =
new RendererObjectManager.UpdateListener() {
public void queueForReload(RendererObjectManager rom, boolean fullReload) {
mManagersToReload.add(new ManagerReloadData(rom, fullReload));
}
};
// All managers - we need to reload all of these when we recreate the surface.
private Set<RendererObjectManager> mAllManagers = new TreeSet<RendererObjectManager>();
protected final TextureManager mTextureManager;
private static class ManagerReloadData {
ManagerReloadData(RendererObjectManager manager, boolean fullReload) {
this.manager = manager;
this.fullReload = fullReload;
}
public RendererObjectManager manager;
public boolean fullReload;
}
// A list of managers which need to be reloaded before the next frame is rendered. This may
// be because they haven't ever been loaded yet, or because their objects have changed since
// the last frame.
private ArrayList<ManagerReloadData> mManagersToReload = new ArrayList<ManagerReloadData>();
// Maps an integer indicating render order to a list of objects at that level. The managers
// will be rendered in order, with the lowest number coming first.
private TreeMap<Integer, Set<RendererObjectManager>> mLayersToManagersMap = null;
public SkyRenderer(Resources res) {
mRenderState.setResources(res);
mLayersToManagersMap = new TreeMap<Integer, Set<RendererObjectManager>>();
mTextureManager = new TextureManager(res);
// The skybox should go behind everything.
mSkyBox = new SkyBox(Integer.MIN_VALUE, mTextureManager);
mSkyBox.enable(false);
addObjectManager(mSkyBox);
// The overlays go on top of everything.
mOverlayManager = new OverlayManager(Integer.MAX_VALUE, mTextureManager);
addObjectManager(mOverlayManager);
Log.d("SkyRenderer", "SkyRenderer::SkyRenderer()");
}
// Returns true if the buffers should be swapped, false otherwise.
public void onDrawFrame(GL10 gl) {
// Initialize any of the unloaded managers.
for (ManagerReloadData data : mManagersToReload) {
data.manager.reload(gl, data.fullReload);
}
mManagersToReload.clear();
maybeUpdateMatrices(gl);
// Determine which sky regions should be rendered.
mRenderState.setActiveSkyRegions(
SkyRegionMap.getActiveRegions(
mRenderState.getLookDir(),
mRenderState.getRadiusOfView(),
(float) mRenderState.getScreenWidth() / mRenderState.getScreenHeight()));
gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
for (int layer : mLayersToManagersMap.keySet()) {
Set<RendererObjectManager> managers = mLayersToManagersMap.get(layer);
for (RendererObjectManager rom : managers) {
rom.draw(gl);
}
}
checkForErrors(gl);
// Queue updates for the next frame.
for (UpdateClosure update : mUpdateClosures) {
update.run();
}
}
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
Log.d("SkyRenderer", "surfaceCreated");
gl.glEnable(GL10.GL_DITHER);
/*
* Some one-time OpenGL initialization can be made here
* probably based on features of this particular context
*/
gl.glHint(GL10.GL_PERSPECTIVE_CORRECTION_HINT,
GL10.GL_FASTEST);
gl.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
gl.glEnable(GL10.GL_CULL_FACE);
gl.glShadeModel(GL10.GL_SMOOTH);
gl.glDisable(GL10.GL_DEPTH_TEST);
// Release references to all of the old textures.
mTextureManager.reset();
String extensions = gl.glGetString(GL10.GL_EXTENSIONS);
Log.i("SkyRenderer", "GL extensions: " + extensions);
// Determine if the phone supports VBOs or not, and set this on the GLBuffer.
// TODO(jpowell): There are two extension strings which seem applicable.
// There is GL_OES_vertex_buffer_object and GL_ARB_vertex_buffer_object.
// I can't find any documentation which explains the difference between
// these two. Most phones which support one seem to support both,
// except for the Nexus One, which only supports ARB but doesn't seem
// to benefit from using VBOs anyway. I should figure out what the
// difference is and use ARB too, if I can.
boolean canUseVBO = false;
if (extensions.contains("GL_OES_vertex_buffer_object")) {
canUseVBO = true;
}
// VBO support on the Cliq and Behold is broken and say they can
// use them when they can't. Explicitly disable it for these devices.
final String[] badModels = {
"MB200",
"MB220",
"Behold",
};
for (String model : badModels) {
if (android.os.Build.MODEL.contains(model)) {
canUseVBO = false;
}
}
Log.i("SkyRenderer", "Model: " + android.os.Build.MODEL);
Log.i("SkyRenderer", canUseVBO ? "VBOs enabled" : "VBOs disabled");
GLBuffer.setCanUseVBO(canUseVBO);
// Reload all of the managers.
for (RendererObjectManager rom : mAllManagers) {
rom.reload(gl, true);
}
}
public void onSurfaceChanged(GL10 gl, int width, int height) {
Log.d("SkyRenderer", "Starting sizeChanged, size = (" + width + ", " + height + ")");
mRenderState.setScreenSize(width, height);
mOverlayManager.resize(gl, width, height);
// Need to set the matrices.
mMustUpdateView = true;
mMustUpdateProjection = true;
Log.d("SkyRenderer", "Changing viewport size");
gl.glViewport(0, 0, width, height);
Log.d("SkyRenderer", "Done with sizeChanged");
}
public void setRadiusOfView(float degrees) {
// Log.d("SkyRenderer", "setRadiusOfView(" + degrees + ")");
mRenderState.setRadiusOfView(degrees);
mMustUpdateProjection = true;
}
public void addUpdateClosure(UpdateClosure update) {
mUpdateClosures.add(update);
}
public void removeUpdateCallback(UpdateClosure update) {
mUpdateClosures.remove(update);
}
// Sets up from the perspective of the viewer.
// ie, the zenith in celestial coordinates.
public void setViewerUpDirection(GeocentricCoordinates up) {
mOverlayManager.setViewerUpDirection(up);
}
public void addObjectManager(RendererObjectManager m) {
m.setRenderState(mRenderState);
m.setUpdateListener(mUpdateListener);
mAllManagers.add(m);
// It needs to be reloaded before we try to draw it.
mManagersToReload.add(new ManagerReloadData(m, true));
// Add it to the appropriate layer.
Set<RendererObjectManager> managers = mLayersToManagersMap.get(m.getLayer());
if (managers == null) {
managers = new TreeSet<RendererObjectManager>();
mLayersToManagersMap.put(m.getLayer(), managers);
}
managers.add(m);
}
public void removeObjectManager(RendererObjectManager m) {
mAllManagers.remove(m);
Set<RendererObjectManager> managers = mLayersToManagersMap.get(m.getLayer());
// managers shouldn't ever be null, so don't bother checking. Let it crash if it is so we
// know there's a bug.
managers.remove(m);
}
public void enableSkyGradient(GeocentricCoordinates sunPosition) {
mSkyBox.setSunPosition(sunPosition);
mSkyBox.enable(true);
}
public void disableSkyGradient() {
mSkyBox.enable(false);
}
public void enableSearchOverlay(GeocentricCoordinates target, String targetName) {
mOverlayManager.enableSearchOverlay(target, targetName);
}
public void disableSearchOverlay() {
mOverlayManager.disableSearchOverlay();
}
public void setNightVisionMode(boolean enabled) {
mRenderState.setNightVisionMode(enabled);
}
// Used to set the orientation of the text. The angle parameter is the roll
// of the phone. This angle is rounded to the nearest multiple of 90 degrees
// to keep the text readable.
public void setTextAngle(float angleInRadians) {
final float TWO_OVER_PI = 2.0f / (float)Math.PI;
final float PI_OVER_TWO = (float)Math.PI / 2.0f;
float newAngle = Math.round(angleInRadians * TWO_OVER_PI) * PI_OVER_TWO;
mRenderState.setUpAngle(newAngle);
}
public void setViewOrientation(float dirX, float dirY, float dirZ,
float upX, float upY, float upZ) {
// Normalize the look direction
float dirLen = FloatMath.sqrt(dirX*dirX + dirY*dirY + dirZ*dirZ);
float oneOverDirLen = 1.0f / dirLen;
dirX *= oneOverDirLen;
dirY *= oneOverDirLen;
dirZ *= oneOverDirLen;
// We need up to be perpendicular to the look direction, so we subtract
// off the projection of the look direction onto the up vector
float lookDotUp = dirX * upX + dirY * upY + dirZ * upZ;
upX -= lookDotUp * dirX;
upY -= lookDotUp * dirY;
upZ -= lookDotUp * dirZ;
// Normalize the up vector
float upLen = FloatMath.sqrt(upX*upX + upY*upY + upZ*upZ);
float oneOverUpLen = 1.0f / upLen;
upX *= oneOverUpLen;
upY *= oneOverUpLen;
upZ *= oneOverUpLen;
mRenderState.setLookDir(new GeocentricCoordinates(dirX, dirY, dirZ));
mRenderState.setUpDir(new GeocentricCoordinates(upX, upY, upZ));
mMustUpdateView = true;
mOverlayManager.setViewOrientation(new GeocentricCoordinates(dirX, dirY, dirZ),
new GeocentricCoordinates(upX, upY, upZ));
}
protected int getWidth() { return mRenderState.getScreenWidth(); }
protected int getHeight() { return mRenderState.getScreenHeight(); }
static void checkForErrors(GL10 gl) {
checkForErrors(gl, false);
}
static void checkForErrors(GL10 gl, boolean printStackTrace) {
int error = gl.glGetError();
if (error != 0) {
Log.e("SkyRenderer", "GL error: " + error);
Log.e("SkyRenderer", GLU.gluErrorString(error));
if (printStackTrace) {
StringWriter writer = new StringWriter();
new Throwable().printStackTrace(new PrintWriter(writer));
Log.e("SkyRenderer", writer.toString());
}
}
}
private void updateView(GL10 gl) {
// Get a vector perpendicular to both, pointing to the right, by taking
// lookDir cross up.
Vector3 lookDir = mRenderState.getLookDir();
Vector3 upDir = mRenderState.getUpDir();
Vector3 right = VectorUtil.crossProduct(lookDir, upDir);
mViewMatrix = Matrix4x4.createView(lookDir, upDir, right);
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glLoadMatrixf(mViewMatrix.getFloatArray(), 0);
}
private void updatePerspective(GL10 gl) {
mProjectionMatrix = Matrix4x4.createPerspectiveProjection(
mRenderState.getScreenWidth(),
mRenderState.getScreenHeight(),
mRenderState.getRadiusOfView() * 3.141593f / 360.0f);
gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glLoadMatrixf(mProjectionMatrix.getFloatArray(), 0);
// Switch back to the model view matrix.
gl.glMatrixMode(GL10.GL_MODELVIEW);
}
private void maybeUpdateMatrices(GL10 gl) {
boolean updateTransform = mMustUpdateView || mMustUpdateProjection;
if (mMustUpdateView) {
updateView(gl);
mMustUpdateView = false;
}
if (mMustUpdateProjection) {
updatePerspective(gl);
mMustUpdateProjection = false;
}
if (updateTransform) {
// Device coordinates are a square from (-1, -1) to (1, 1). Screen
// coordinates are (0, 0) to (width, height). Both coordinates
// are useful in different circumstances, so we'll pre-compute
// matrices to do the transformations from world coordinates
// into each of these.
Matrix4x4 transformToDevice = Matrix4x4.multiplyMM(mProjectionMatrix, mViewMatrix);
Matrix4x4 translate = Matrix4x4.createTranslation(1, 1, 0);
Matrix4x4 scale = Matrix4x4.createScaling(mRenderState.getScreenWidth() * 0.5f,
mRenderState.getScreenHeight() * 0.5f, 1);
Matrix4x4 transformToScreen =
Matrix4x4.multiplyMM(Matrix4x4.multiplyMM(scale, translate),
transformToDevice);
mRenderState.setTransformationMatrices(transformToDevice, transformToScreen);
}
}
// WARNING! These factory methods are invoked from another thread and
// therefore cannot do any OpenGL operations or any nontrivial nontrivial
// initialization.
//
// TODO(jpowell): This would be much safer if the renderer controller
// schedules creation of the objects in the queue.
public PointObjectManager createPointManager(int layer) {
return new PointObjectManager(layer, mTextureManager);
}
public PolyLineObjectManager createPolyLineManager(int layer) {
return new PolyLineObjectManager(layer, mTextureManager);
}
public LabelObjectManager createLabelManager(int layer) {
return new LabelObjectManager(layer, mTextureManager);
}
public ImageObjectManager createImageManager(int layer) {
return new ImageObjectManager(layer, mTextureManager);
}
}
interface RenderStateInterface {
public GeocentricCoordinates getCameraPos();
public GeocentricCoordinates getLookDir();
public GeocentricCoordinates getUpDir();
public float getRadiusOfView();
public float getUpAngle();
public float getCosUpAngle();
public float getSinUpAngle();
public int getScreenWidth();
public int getScreenHeight();
public Matrix4x4 getTransformToDeviceMatrix();
public Matrix4x4 getTransformToScreenMatrix();
public Resources getResources();
public boolean getNightVisionMode();
public SkyRegionMap.ActiveRegionData getActiveSkyRegions();
}
// TODO(jpowell): RenderState is a bad name. This class is a grab-bag of
// general state which is set once per-frame, and which individual managers
// may need to render the frame. Come up with a better name for this.
class RenderState implements RenderStateInterface {
public GeocentricCoordinates getCameraPos() { return mCameraPos; }
public GeocentricCoordinates getLookDir() { return mLookDir; }
public GeocentricCoordinates getUpDir() { return mUpDir; }
public float getRadiusOfView() { return mRadiusOfView; }
public float getUpAngle() { return mUpAngle; }
public float getCosUpAngle() { return mCosUpAngle; }
public float getSinUpAngle() { return mSinUpAngle; }
public int getScreenWidth() { return mScreenWidth; }
public int getScreenHeight() { return mScreenHeight; }
public Matrix4x4 getTransformToDeviceMatrix() { return mTransformToDevice; }
public Matrix4x4 getTransformToScreenMatrix() { return mTransformToScreen; }
public Resources getResources() { return mRes; }
public boolean getNightVisionMode() { return mNightVisionMode; }
public SkyRegionMap.ActiveRegionData getActiveSkyRegions() { return mActiveSkyRegionSet; }
public void setCameraPos(GeocentricCoordinates pos) { mCameraPos = pos.copy(); }
public void setLookDir(GeocentricCoordinates dir) { mLookDir = dir.copy(); }
public void setUpDir(GeocentricCoordinates dir) { mUpDir = dir.copy(); }
public void setRadiusOfView(float radius) { mRadiusOfView = radius; }
public void setUpAngle(float angle) {
mUpAngle = angle;
mCosUpAngle = FloatMath.cos(angle);
mSinUpAngle = FloatMath.sin(angle);
}
public void setScreenSize(int width, int height) {
mScreenWidth = width;
mScreenHeight = height;
}
public void setTransformationMatrices(Matrix4x4 transformToDevice,
Matrix4x4 transformToScreen) {
mTransformToDevice = transformToDevice;
mTransformToScreen = transformToScreen;
}
public void setResources(Resources res) { mRes = res; }
public void setNightVisionMode(boolean enabled) { mNightVisionMode = enabled; }
public void setActiveSkyRegions(SkyRegionMap.ActiveRegionData set) {
mActiveSkyRegionSet = set;
}
private GeocentricCoordinates mCameraPos = new GeocentricCoordinates(0, 0, 0);
private GeocentricCoordinates mLookDir = new GeocentricCoordinates(1, 0, 0);
private GeocentricCoordinates mUpDir = new GeocentricCoordinates(0, 1, 0);
private float mRadiusOfView = 45; // in degrees
private float mUpAngle = 0;
private float mCosUpAngle = 1;
private float mSinUpAngle = 0;
private int mScreenWidth = 100;
private int mScreenHeight = 100;
private Matrix4x4 mTransformToDevice = Matrix4x4.createIdentity();
private Matrix4x4 mTransformToScreen = Matrix4x4.createIdentity();
private Resources mRes;
private boolean mNightVisionMode = false;
private SkyRegionMap.ActiveRegionData mActiveSkyRegionSet = null;
}