// Copyright 2009 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 android.graphics.Paint;
import android.graphics.Typeface;
import com.google.android.stardroid.renderer.util.GLBuffer;
import com.google.android.stardroid.renderer.util.LabelMaker;
import com.google.android.stardroid.renderer.util.SkyRegionMap;
import com.google.android.stardroid.renderer.util.TextureManager;
import com.google.android.stardroid.renderer.util.TextureReference;
import com.google.android.stardroid.source.TextSource;
import com.google.android.stardroid.units.GeocentricCoordinates;
import com.google.android.stardroid.units.Vector3;
import com.google.android.stardroid.util.FixedPoint;
import com.google.android.stardroid.util.MathUtil;
import com.google.android.stardroid.util.Matrix4x4;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import javax.microedition.khronos.opengles.GL10;
import javax.microedition.khronos.opengles.GL11;
/**
* Manages rendering of text labels.
*
* @author James Powell
*
*/
public class LabelObjectManager extends RendererObjectManager {
// Should we compute the regions for the labels?
// If false, we just put them in the catchall region.
private static final boolean COMPUTE_REGIONS = true;
private Paint mLabelPaint = null;
private LabelMaker mLabelMaker = null;
private Label[] mLabels = new Label[0];
private SkyRegionMap<ArrayList<Label>> mSkyRegions = new SkyRegionMap<ArrayList<Label>>();
private IntBuffer mQuadBuffer;
// These are intermediate variables set in beginDrawing() and used in
// draw() to make the transformations more efficient
private Vector3 mLabelOffset = new Vector3(0, 0, 0);
private float mDotProductThreshold;
private TextureReference mTexture = null;
public LabelObjectManager(int layer, TextureManager textureManager) {
super(layer, textureManager);
mLabelPaint = new Paint();
mLabelPaint.setAntiAlias(true);
mLabelPaint.setTypeface(Typeface.create("Verdana", Typeface.NORMAL));
ByteBuffer quadBuffer = ByteBuffer.allocateDirect(4*2*4);
quadBuffer.order(ByteOrder.nativeOrder());
mQuadBuffer = quadBuffer.asIntBuffer();
mQuadBuffer.position(0);
// A quad with size 1 on each size, so we just need to multiply
// by the label's width and height to get it to the right size for each
// label.
float[] vertices = {
-0.5f, -0.5f, // lower left
-0.5f, 0.5f, // upper left
0.5f, -0.5f, // lower right
0.5f, 0.5f}; // upper right
for (float f : vertices) {
mQuadBuffer.put(FixedPoint.floatToFixedPoint(f));
}
mQuadBuffer.position(0);
// We want to initialize the labels of a sky region to an empty list.
mSkyRegions.setRegionDataFactory(
new SkyRegionMap.RegionDataFactory<ArrayList<Label>>() {
public ArrayList<Label> construct() { return new ArrayList<Label>(); }
});
}
@Override
public void reload(GL10 gl, boolean fullReload) {
// We need to regenerate the texture. If we're re-creating the surface
// (fullReload=true), all resources were automatically released by OpenGL,
// so we don't want to try to release it again. Otherwise, we need to
// release it to avoid a resource leak (mLabelMaker.shutdown takes
// care of freeing the texture).
//
// TODO(jpowell): This whole reload interface is horrendous, and I should
// make a better way of scheduling reloads.
//
// TODO(jpowell): LabelMaker and textures have gone through some changes
// since they were originally created, and I feel like it might not make
// sense for it to own the texture anymore. I should see if I can just
// let it create but not own it.
if (!fullReload && mLabelMaker != null) {
mLabelMaker.shutdown(gl);
}
mLabelMaker = new LabelMaker(true);
mTexture = mLabelMaker.initialize(gl, mLabelPaint, mLabels,
getRenderState().getResources(),
textureManager());
}
public void updateObjects(List<TextSource> labels, EnumSet<UpdateType> updateType) {
if (updateType.contains(UpdateType.Reset)) {
mLabels = new Label[labels.size()];
for (int i = 0; i < labels.size(); i++) {
mLabels[i] = new Label(labels.get(i));
}
queueForReload(false);
} else if (updateType.contains(UpdateType.UpdatePositions)) {
if (labels.size() != mLabels.length) {
logUpdateMismatch("LabelObjectManager", mLabels.length, labels.size(), updateType);
return;
}
// Since we don't store the positions in any GPU memory, and do the
// transformations manually, we can just update the positions stored
// on the label objects.
for (int i = 0; i < mLabels.length; i++) {
GeocentricCoordinates pos = labels.get(i).getLocation();
mLabels[i].x = pos.x;
mLabels[i].y = pos.y;
mLabels[i].z = pos.z;
}
}
// Put all of the labels in their sky regions.
// TODO(jpowell): Get this from the label source itself once it supports
// this.
mSkyRegions.clear();
for (Label l : mLabels) {
int region;
if (COMPUTE_REGIONS) {
region = SkyRegionMap.getObjectRegion(new GeocentricCoordinates(l.x, l.y, l.z));
} else {
region = SkyRegionMap.CATCHALL_REGION_ID;
}
mSkyRegions.getRegionData(region).add(l);
}
}
@Override
protected void drawInternal(GL10 gl) {
gl.glTexEnvx(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE,
GL10.GL_MODULATE);
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glActiveTexture(GL10.GL_TEXTURE0);
mTexture.bind(gl);
gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S,
GL10.GL_REPEAT);
gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,
GL10.GL_REPEAT);
beginDrawing(gl);
// Draw the labels for the active sky regions.
SkyRegionMap.ActiveRegionData activeRegions = getRenderState().getActiveSkyRegions();
ArrayList<ArrayList<Label>> allActiveLabels =
mSkyRegions.getDataForActiveRegions(activeRegions);
for (ArrayList<Label> labelsInRegion : allActiveLabels) {
for (Label l : labelsInRegion) {
drawLabel(gl, l);
}
}
endDrawing(gl);
}
/**
* Begin drawing labels. Sets the OpenGL state for rapid drawing.
*
* @param gl
*/
public void beginDrawing(GL10 gl) {
mTexture.bind(gl);
gl.glShadeModel(GL10.GL_FLAT);
gl.glEnable(GL10.GL_ALPHA_TEST);
gl.glAlphaFunc(GL10.GL_GREATER, 0.5f);
gl.glEnable(GL10.GL_TEXTURE_2D);
// We're going to do the transformation on the CPU, so set the matrices
// to the identity
gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glPushMatrix();
gl.glLoadIdentity();
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glPushMatrix();
gl.glLoadIdentity();
gl.glOrthof(0, getRenderState().getScreenWidth(),
0, getRenderState().getScreenHeight(),
-1, 1);
GLBuffer.unbind((GL11) gl);
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
RenderStateInterface rs = super.getRenderState();
float viewWidth = rs.getScreenWidth();
float viewHeight = rs.getScreenHeight();
Matrix4x4 rotation = Matrix4x4.createRotation(rs.getUpAngle(), rs.getLookDir());
mLabelOffset = Matrix4x4.multiplyMV(rotation, rs.getUpDir());
// If a label isn't within the field of view angle from the target vector, it can't
// be on the screen. Compute the cosine of this angle so we can quickly identify these.
// TODO(jpowell): I know I can make this tighter - do so.
final float DEGREES_TO_RADIANS = MathUtil.PI / 180.0f;
mDotProductThreshold = MathUtil.cos(rs.getRadiusOfView() * DEGREES_TO_RADIANS *
(1 + viewWidth / viewHeight) * 0.5f);
}
/**
* Ends the drawing and restores the OpenGL state.
*
* @param gl
*/
public void endDrawing(GL10 gl) {
gl.glDisable(GL10.GL_ALPHA_TEST);
gl.glMatrixMode(GL10.GL_PROJECTION);
gl.glPopMatrix();
gl.glMatrixMode(GL10.GL_MODELVIEW);
gl.glPopMatrix();
gl.glDisable(GL10.GL_TEXTURE_2D);
gl.glColor4x(FixedPoint.ONE, FixedPoint.ONE, FixedPoint.ONE, FixedPoint.ONE);
}
/**
* A private class which extends the LabelMaker's label data with an xyz position and rgba color values.
* For the red-eye mode, it's easier to set the color in the texture to white and set the color when we render
* the label than to have two textures, one with red labels and one without.
*/
private static class Label extends LabelMaker.LabelData {
public Label(TextSource ts) {
super(ts.getText(), 0xffffffff, ts.getFontSize());
if (ts.getText() == null || ts.getText().equals("")) {
throw new RuntimeException("Bad Label: " + ts.getClass());
}
x = ts.getLocation().x;
y = ts.getLocation().y;
z = ts.getLocation().z;
offset = ts.getOffset();
int rgb = ts.getColor();
int a = 0xff;
int r = (rgb >> 16) & 0xff;
int g = (rgb >> 8) & 0xff;
int b = rgb & 0xff;
fixedA = FixedPoint.floatToFixedPoint(a / 255.0f);
fixedB = FixedPoint.floatToFixedPoint(b / 255.0f);
fixedG = FixedPoint.floatToFixedPoint(g / 255.0f);
fixedR = FixedPoint.floatToFixedPoint(r / 255.0f);
}
public float x;
public float y;
public float z;
// The distance this should be rendered underneath the specified position, in world coordinates.
public float offset;
// Fixed point color values
public int fixedR;
public int fixedG;
public int fixedB;
public int fixedA;
}
private void drawLabel(GL10 gl, Label label) {
Vector3 lookDir = getRenderState().getLookDir();
if (lookDir.x * label.x + lookDir.y * label.y + lookDir.z * label.z < mDotProductThreshold) {
return;
}
// Offset the label to be underneath the given position (so a label will
// always appear underneath a star no matter how the phone is rotated)
Vector3 v = new Vector3(
label.x - mLabelOffset.x * label.offset,
label.y - mLabelOffset.y * label.offset,
label.z - mLabelOffset.z * label.offset);
Vector3 screenPos = Matrix4x4.transformVector(
getRenderState().getTransformToScreenMatrix(),
v);
// We want this to align consistently with the pixels on the screen, so we
// snap to the nearest x/y coordinate, and add a magic offset of less than
// half a pixel. Without this, rounding error can cause the bottom and
// top of a label to be one pixel off, which results in a noticeable
// distortion in the text.
final float MAGIC_OFFSET = 0.25f;
screenPos.x = (int)screenPos.x + MAGIC_OFFSET;
screenPos.y = (int)screenPos.y + MAGIC_OFFSET;
gl.glPushMatrix();
gl.glTranslatef(screenPos.x, screenPos.y, 0);
gl.glRotatef(MathUtil.RADIANS_TO_DEGREES * getRenderState().getUpAngle(), 0, 0, -1);
gl.glScalef(label.getWidthInPixels(), label.getHeightInPixels(), 1);
gl.glVertexPointer(2, GL10.GL_FIXED, 0, mQuadBuffer);
gl.glTexCoordPointer(2, GL10.GL_FIXED, 0, label.getTexCoords());
if (getRenderState().getNightVisionMode()) {
gl.glColor4x(FixedPoint.ONE, 0, 0, label.fixedA);
} else {
gl.glColor4x(label.fixedR, label.fixedG, label.fixedB, label.fixedA);
}
gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, 4);
gl.glPopMatrix();
}
}