// Copyright 2010 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.util;
import android.util.Log;
import com.google.android.stardroid.units.GeocentricCoordinates;
import com.google.android.stardroid.util.MathUtil;
import com.google.android.stardroid.util.VectorUtil;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.TreeMap;
/**
* This is a utility class which divides the sky into a fixed set of regions
* and maps each of the regions into a generic data object which contains the
* data for rendering that region of the sky. For a given frame, this class
* will determine which regions are on-screen and which are totally
* off-screen, and will return only the on-screen ones (so we can avoid paying
* the cost of rendering the ones that aren't on-screen). There should
* typically be one of these objects per type of object being rendered: for
* example, points and labels will each have their own SkyRegionMap.
*
* Each region consists of a center (a point on the unit sphere) and an angle,
* and should contain every object on the unit sphere within the specified
* angle from the region's center.
*
* This also allows for a special "catchall" region which is always rendered
* and may contain objects from anywhere on the unit sphere. This is useful
* because, for small layers, it is cheaper to just render the
* whole layer than to break it up into smaller pieces.
*
* The center of all regions is fixed for computational reasons. This allows
* us to find the distance between each region and the current look direction
* once per frame and share that between all SkyRegionMaps. For most types
* of objects, they can also use regions with the same radius, which means
* that they are the same exact part of the unit sphere. For these we can
* compute the regions which are on screen ("active regions") once per frame,
* and share that between all SkyRegionMaps. These are called "standard
* regions", as opposed to "non-standard regions", where the region's angle
* may be greater than that of the standard region. Non-standard regions
* are necessary for some types of objects, such as lines, which may not be
* fully contained within any standard region. For lines, we can find the
* region center which is closest to fully containing the line, and simply
* increase the angle until it does fully contain it.
*
* @param <RegionRenderingData> A object which contains the data needed to
* render a sky region.
* @author James Powell
*/
public class SkyRegionMap<RegionRenderingData> {
public static final int CATCHALL_REGION_ID = -1;
/**
* Interface for a factory that constructs a rendering data.
*/
public interface RegionDataFactory<RegionRenderingData> {
public RegionRenderingData construct();
}
/**
* This stores data that we only want to compute once per frame about
* which regions are on the screen. We don't want to compute these
* regions for every manager separately, since we can share them
* between managers.
*/
public static class ActiveRegionData {
private ActiveRegionData(float[] regionCenterDotProducts,
float screenAngle,
ArrayList<Integer> activeScreenRegions) {
if (regionCenterDotProducts.length != REGION_CENTERS.length) {
Log.e("SkyRegionMap", "Bad regionCenterDotProducts length: " +
regionCenterDotProducts.length + " vs " + REGION_CENTERS.length);
}
this.regionCenterDotProducts = regionCenterDotProducts;
this.screenAngle = screenAngle;
this.activeStandardRegions = activeScreenRegions;
}
// Dot product of look direction with each region's center.
// We need this for non-standard regions. For standard regions,
// we can compute the visible regions when we compute the
// ActiveRegionData, so we don't need to cache this.
private final float[] regionCenterDotProducts;
// Angle between the look direction and the corners of the screen.
private final float screenAngle;
// The list of standard regions which are active given the current
// look direction and screen angle.
private final ArrayList<Integer> activeStandardRegions;
/**
* Returns true if a non-standard region is active.
* @param region The ID of the region to check
* @param coverageAngle the coverage angle of the region.
* @return true if the region is active, false if not.
*/
private boolean regionIsActive(int region, float coverageAngle) {
// A region cannot be active if the angle between screen's center
// and the region's center is greater than the sum of the region angle
// and screen angle. I make a few definitions:
// S = screen direction (look direction)
// s = screen angle
// R = region center
// r = region angle
// If the region is active, then
// (angle between S and R) < s + r
// These angles are between 0 and Pi, and cos is decreasing here, so
// cos(angle between S and R) > cos(s + r)
// S and R are unit vectors, so S dot R = cos(angle between S and R)
// S dot R > cos(s + r)
// So the regions where this holds true are the visible regions.
return regionCenterDotProducts[region] > MathUtil.cos(coverageAngle + screenAngle);
}
}
/**
* Data representing an individual object's position in a region.
* We care about the region itself for obvious reasons, but we care about
* the dot product with the center because it is a measure of how
* close it is to the center of a region.
*/
public static class ObjectRegionData {
public int region = SkyRegionMap.CATCHALL_REGION_ID;
public float regionCenterDotProduct = -1;
}
// We want to use a set of points that minimizes the maximum distance from
// any point on the sphere to one of these points. This is called
// "covering" a sphere, and is a common and well-studied problem.
// There are many links to papers on this problem at
// http://www.ogre.nu/sphere.htm and solutions for various numbers of
// points may be found at http://www2.research.att.com/~njas/coverings.
// The points and cover angle used here are taken from the latter site.
// This vim command will convert their file of points into the array below:
// :%s/\(.*\)\n\(.*\)\n\(.*\)\n/new GeocentricCoordinates(\1f, \2f, \3f),\r
// If the angle between the center of a region and a point on the sphere is
// not within this angle, it cannot be in that region.
// TODO(jpowell): We have to cite the source of these numbers in order to
// use this. Not sure where to do that. Figure that out before releasing
// a version that uses it.
/* TODO(jpowell): I'm not sure how many regions we want. The more we have,
* the more setup work we need to do to figure out which ones are on screen,
* and the more rendering calls we have (there is one per region).
* On the other hand, each rendering call is smaller, so we do less work.
* There's a balance here, and I need to experiment with to find the optimal
* number of regions.
// 16 points to cover the sphere. Each region is about 33 degrees.
public static final float REGION_COVERAGE_ANGLE_IN_RADIANS = 0.574193f;
public static final GeocentricCoordinates[] REGION_CENTERS = {
new GeocentricCoordinates(0.35286933463f, 0.74990446679f, -0.55957709252f),
new GeocentricCoordinates(-0.39102044981f, -0.50929142069f, -0.76663241256f),
new GeocentricCoordinates(0.62339142523f, 0.05646475870f, 0.77986848913f),
new GeocentricCoordinates(-0.87305064313f, 0.15072732698f, 0.46374976728f),
new GeocentricCoordinates(0.24643376097f, -0.81052424572f, 0.53133873082f),
new GeocentricCoordinates(-0.28740443382f, -0.27010080590f, 0.91893647518f),
new GeocentricCoordinates(-0.49803568050f, 0.86228929273f, -0.09174766950f),
new GeocentricCoordinates(-0.92485864274f, 0.03113202471f, -0.37903467845f),
new GeocentricCoordinates(0.52418663166f, -0.17254281044f, -0.83394085615f),
new GeocentricCoordinates(0.93851553648f, 0.32907632924f, -0.10439040418f),
new GeocentricCoordinates(0.19462575390f, -0.93011955358f, -0.31144571024f),
new GeocentricCoordinates(-0.64954237639f, -0.74621124099f, 0.14582003653f),
new GeocentricCoordinates(0.40467733983f, 0.86949976626f, 0.28320735506f),
new GeocentricCoordinates(0.85939374845f, -0.51093567814f, 0.01967528462f),
new GeocentricCoordinates(-0.20828264579f, 0.56991119217f, 0.79487078916f),
new GeocentricCoordinates(-0.31189865805f, 0.33072059787f, -0.89069810416f)
};
*/
// 32 points to cover the sphere. Each region is about 22.7 degrees.
public static final float REGION_COVERAGE_ANGLE_IN_RADIANS = 0.396023592f;
public static final GeocentricCoordinates[] REGION_CENTERS = {
new GeocentricCoordinates(-0.850649066269f, 0.525733930059f, -0.000001851469f),
new GeocentricCoordinates(-0.934170971625f, 0.000004098751f, -0.356825719588f),
new GeocentricCoordinates(0.577349931933f, 0.577346773818f, 0.577354100533f),
new GeocentricCoordinates(0.577350600623f, -0.577350601554f, -0.577349603176f),
new GeocentricCoordinates(-0.577354427427f, -0.577349954285f, 0.577346424572f),
new GeocentricCoordinates(-0.577346098609f, 0.577353779227f, -0.577350928448f),
new GeocentricCoordinates(-0.577349943109f, -0.577346729115f, -0.577354134060f),
new GeocentricCoordinates(-0.577350598760f, 0.577350586653f, 0.577349620871f),
new GeocentricCoordinates(0.577354458161f, 0.577349932864f, -0.577346415259f),
new GeocentricCoordinates(0.577346091159f, -0.577353793196f, 0.577350921929f),
new GeocentricCoordinates(-0.850652559660f, -0.525728277862f, -0.000004770234f),
new GeocentricCoordinates(-0.934173742309f, 0.000002107583f, 0.356818466447f),
new GeocentricCoordinates(0.525734450668f, 0.000000594184f, -0.850648744032f),
new GeocentricCoordinates(0.000002468936f, -0.356819496490f, -0.934173349291f),
new GeocentricCoordinates(0.525727798231f, -0.000004087575f, 0.850652855821f),
new GeocentricCoordinates(-0.000002444722f, 0.356819517910f, 0.934173340909f),
new GeocentricCoordinates(-0.525727787986f, 0.000004113652f, -0.850652862340f),
new GeocentricCoordinates(0.000004847534f, 0.356824675575f, -0.934171371162f),
new GeocentricCoordinates(-0.000004885718f, -0.850652267225f, 0.525728750974f),
new GeocentricCoordinates(-0.356825215742f, -0.934171164408f, -0.000003995374f),
new GeocentricCoordinates(0.000000767410f, 0.850649364293f, 0.525733447634f),
new GeocentricCoordinates(0.356825180352f, 0.934171177447f, 0.000003952533f),
new GeocentricCoordinates(-0.000000790693f, -0.850649344735f, -0.525733478367f),
new GeocentricCoordinates(0.356818960048f, -0.934173554182f, -0.000001195818f),
new GeocentricCoordinates(0.850652555004f, 0.525728284381f, 0.000004773028f),
new GeocentricCoordinates(0.934170960449f, -0.000004090369f, 0.356825748459f),
new GeocentricCoordinates(-0.525734410621f, -0.000000609085f, 0.850648769177f),
new GeocentricCoordinates(-0.000004815869f, -0.356824668124f, 0.934171373956f),
new GeocentricCoordinates(0.000004877336f, 0.850652255118f, -0.525728769600f),
new GeocentricCoordinates(-0.356819001026f, 0.934173538350f, 0.000001183711f),
new GeocentricCoordinates(0.850649050437f, -0.525733955204f, 0.000001879409f),
new GeocentricCoordinates(0.934173759073f, -0.000002136454f, -0.356818422675f),
};
// This is the coverage angle of each region. For most sky region
// maps, this will be null, which means that the coverage is specified by
// REGION_COVERAGE_ANGLE_IN_RADIANS. If some regions have a coverage
// angle bigger than that, this must be non-NULL and should specify
// the coverage angles for all of the regions.
// Rather than only setting this if some regions have special angles,
// we could just set it for everything. The reason we don't is that
// we can just use the standard visible regions if we don't set any
// special coverage angles, which is a significant performance win.
public float[] mRegionCoverageAngles = null;
// Maps the region ID to the rendering data for the region.
private Map<Integer, RegionRenderingData> mRegionData =
new TreeMap<Integer, RegionRenderingData>();
// Used to construct a new region the first time we access it.
private RegionDataFactory<RegionRenderingData> mRegionDataFactory = null;
/**
* Computes the data necessary to determine which regions on the screen
* are active. This should be produced once per frame and passed to
* the getDataForActiveRegions method of all SkyRegionMap objects to
* get the active regions for each map.
*
* @param lookDir The direction the user is currently facing.
* @param fovyInDegrees The field of view (in degrees).
* @param aspect The aspect ratio of the screen.
* @return A data object containing data for quickly determining the
* active regions.
*/
public static ActiveRegionData getActiveRegions(
GeocentricCoordinates lookDir,
float fovyInDegrees,
float aspect) {
// We effectively compute a screen "region" here. The center of this
// region is the look direction, and the radius is the angle between
// the center and one of the corners. If any region intersects the
// screen region, we consider that region to be active.
//
// First, we compute the screen angle. The angle between the vectors
// to the top of the screen and the center of the screen is defined to
// be fovy/2.
// The distance between the top and center of the view plane, then, is
// sin(fovy / 2). The difference between the right and center must be.
// (width / height) * sin(fovy / 2) = aspect * sin(fovy / 2)
// This gives us a right triangle to find the distance between the center
// and the corner of the screen. This distance is:
// d = sin(fovy / 2) * sqrt(1 + aspect^2).
// The angle for the screen region is the arcsin of this value.
float halfFovy = (fovyInDegrees * MathUtil.DEGREES_TO_RADIANS) / 2;
float screenAngle = MathUtil.asin(
MathUtil.sin(halfFovy) * MathUtil.sqrt(1 + aspect * aspect));
// Next, determine whether or not the region is active. See the
// regionIsActive method for an explanation of the math here.
// We don't use that method because if we did, we would repeatedly
// compute the same cosine in that function.
float angleThreshold = screenAngle + REGION_COVERAGE_ANGLE_IN_RADIANS;
float dotProductThreshold = MathUtil.cos(angleThreshold);
float[] regionCenterDotProducts = new float[REGION_CENTERS.length];
ArrayList<Integer> activeStandardRegions = new ArrayList<Integer>();
for (int i = 0; i < REGION_CENTERS.length; i++) {
float dotProduct = VectorUtil.dotProduct(lookDir, REGION_CENTERS[i]);
regionCenterDotProducts[i] = dotProduct;
if (dotProduct > dotProductThreshold) {
activeStandardRegions.add(i);
}
}
// Log.d("SkyRegionMap", "ScreenAngle: " + screenAngle);
// Log.d("SkyRegionMap", "Angle Threshold: " + angleThreshold);
// Log.d("SkyRegionMap", "DP Threshold: " + dotProductThreshold);
return new ActiveRegionData(regionCenterDotProducts, screenAngle,
activeStandardRegions);
}
/**
* Returns the region that a point belongs in.
*
* @param position
* @return The region the point belongs in.
*/
public static int getObjectRegion(GeocentricCoordinates position) {
return getObjectRegionData(position).region;
}
/**
* Returns the region a point belongs in, as well as the dot product of the
* region center and the position. The latter is a measure of how close it
* is to the center of the region (1 being a perfect match).
*
* TODO(jpowell): I think this is useful for putting lines into regions, but
* if I don't end up using this when I implement that, I should delete this.
* @param position
* @return The closest region and dot product with center of that region.
*/
public static ObjectRegionData getObjectRegionData(GeocentricCoordinates position) {
// The closest region will minimize the angle between the vectors, which
// will maximize the dot product, so we just return the region which
// does that.
ObjectRegionData data = new ObjectRegionData();
for (int i = 0; i < REGION_CENTERS.length; i++) {
float dotProduct = VectorUtil.dotProduct(REGION_CENTERS[i], position);
if (dotProduct > data.regionCenterDotProduct) {
data.regionCenterDotProduct = dotProduct;
data.region = i;
}
}
// For debugging only: make sure we're within the maximum region coverage angle.
if (data.regionCenterDotProduct < MathUtil.cos(REGION_COVERAGE_ANGLE_IN_RADIANS)) {
Log.e("ActiveSkyRegionData",
"Object put in region, but outside of coverage angle." +
"Angle was " + MathUtil.acos(data.regionCenterDotProduct) + " vs " +
REGION_COVERAGE_ANGLE_IN_RADIANS + ". Region was " + data.region);
}
return data;
}
// Clear the region map and coverage angles.
public void clear() {
mRegionData.clear();
mRegionCoverageAngles = null;
}
/**
* Sets a function for constructing an empty rendering data object
* for a sky region. This is used to create an object if getRegionData()
* is called and none already exists.
*/
public void setRegionDataFactory(
RegionDataFactory<RegionRenderingData> factory) {
mRegionDataFactory = factory;
}
public void setRegionData(int id, RegionRenderingData data) {
mRegionData.put(id, data);
}
public float getRegionCoverageAngle(int id) {
return mRegionCoverageAngles == null ? REGION_COVERAGE_ANGLE_IN_RADIANS
: mRegionCoverageAngles[id];
}
/**
* Sets the coverage angle for a sky region. Needed for non-point
* objects (see the javadoc for this class).
* @param id
* @param angleInRadians
*/
public void setRegionCoverageAngle(int id, float angleInRadians) {
if (mRegionCoverageAngles == null) {
mRegionCoverageAngles = new float[REGION_CENTERS.length];
for (int i = 0; i < REGION_CENTERS.length; ++i) {
mRegionCoverageAngles[i] = REGION_COVERAGE_ANGLE_IN_RADIANS;
}
}
if (angleInRadians < mRegionCoverageAngles[id]) {
Log.e("SkyRegionMap", "Reducing coverage angle of region " + id +
" from " + mRegionCoverageAngles[id] + " to " + angleInRadians);
}
mRegionCoverageAngles[id] = angleInRadians;
}
/**
* Lookup the region data corresponding to a region ID. If none exists,
* and a region data constructor has been set (see setRegionDataConstructor),
* that will be used to create a new region - otherwise, this will return
* null. This can be useful while building or updating a region, but to get
* the region data when rendering a frame, use getDataForActiveRegions().
* @param id
* @return The data for the specified region.
*/
public RegionRenderingData getRegionData(int id) {
RegionRenderingData data = mRegionData.get(id);
if (data == null && mRegionDataFactory != null) {
// If we have a factory, construct a new object.
data = mRegionDataFactory.construct();
mRegionData.put(id, data);
}
return data;
}
/**
* Returns the rendering data for the active regions. When using a
* SkyRegionMap for rendering, this is the function will return the
* data for the regions you need to render.
*
* TODO(jpowell): I've done a little bit to verify that the regions I'm
* computing here doesn't include regions that are obviously off screen, but
* I should do some more work to verify that.
*
* @param regions
* @return ArrayList of rendering data corresponding to the on-screen
* regions.
*/
public ArrayList<RegionRenderingData> getDataForActiveRegions(ActiveRegionData regions) {
ArrayList<RegionRenderingData> data = new ArrayList<RegionRenderingData>();
// Always add the catchall region if non-NULL.
RegionRenderingData catchallData = mRegionData.get(CATCHALL_REGION_ID);
if (catchallData != null) {
data.add(catchallData);
}
if (mRegionCoverageAngles == null) {
// Just return the data for the standard visible regions.
for (int region : regions.activeStandardRegions) {
RegionRenderingData regionData = mRegionData.get(region);
if (regionData != null) {
data.add(regionData);
}
}
return data;
} else {
for (int i = 0; i < REGION_CENTERS.length; i++) {
// Need to specially compute the visible regions.
if (regions.regionIsActive(i, mRegionCoverageAngles[i])) {
RegionRenderingData regionData = mRegionData.get(i);
if (regionData != null) {
data.add(regionData);
}
}
}
return data;
}
}
public Collection<RegionRenderingData> getDataForAllRegions() {
return mRegionData.values();
}
}