/*******************************************************************************
* Copyright 2012 Geoscience Australia
*
* 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 au.gov.ga.earthsci.worldwind.common.layers.volume.btt;
import gov.nasa.worldwind.geom.Position;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.media.opengl.GL2;
import au.gov.ga.earthsci.worldwind.common.render.fastshape.FastShape;
import au.gov.ga.earthsci.worldwind.common.util.Util;
import au.gov.ga.earthsci.worldwind.common.util.Validate;
/**
* A mesh generation helper which uses a grid of positions to generate a mesh.
* Uses the Binary Triangle Tree mesh simplification algorithm.
*
* @author Michael de Hoog (michael.dehoog@ga.gov.au)
*/
public class BinaryTriangleTree
{
private final List<Position> positions;
private final int width;
private final int height;
private boolean generateTextureCoordinates = false;
private boolean forceGLTriangles = false;
/**
* Create a new {@link BinaryTriangleTree} object.
*
* @param positions
* Grid of positions in the mesh. Should be ordered in the
* x-axis, then the y-axis.
* @param width
* Number of positions in the x-axis
* @param height
* Number of positions in the y-axis
*/
public BinaryTriangleTree(List<Position> positions, int width, int height)
{
Validate.isTrue(positions.size() == width * height, "Positions list count doesn't match provided width/height");
this.positions = positions;
this.width = width;
this.height = height;
}
/**
* @return Should texture coordinates be generated for the mesh?
*/
public boolean isGenerateTextureCoordinates()
{
return generateTextureCoordinates;
}
/**
* Enable/disable texture coordinate generation during mesh creation.
*
* @param generateTextureCoordinates
*/
public void setGenerateTextureCoordinates(boolean generateTextureCoordinates)
{
this.generateTextureCoordinates = generateTextureCoordinates;
}
/**
* @return Should the mesh be forced to use the {@link GL#GL_TRIANGLES}
* mode? Otherwise a {@link GL#GL_TRIANGLE_STRIP} mesh could
* possibly be generated (if maximum variance is 0).
*/
public boolean isForceGLTriangles()
{
return forceGLTriangles;
}
/**
* Force the mesh generated (if max variance is 0) to use
* {@link GL#GL_TRIANGLES} instead of {@link GL#GL_TRIANGLE_STRIP}.
*
* @param forceGLTriangles
*/
public void setForceGLTriangles(boolean forceGLTriangles)
{
this.forceGLTriangles = forceGLTriangles;
}
/**
* Build a mesh from the position grid.
*
* @param maxVariance
* Variances between triangle vertices less than maxVariance will
* be simplified. Use 0 to use every vertex in the final mesh.
* @return A {@link FastShape} containing the mesh.
*/
public FastShape buildMesh(float maxVariance)
{
return buildMesh(maxVariance, new Rectangle(0, 0, width, height));
}
/**
* Build a mesh from the position grid. The simplification algorithm will
* start from the center instead of the top-left.
*
* @param maxVariance
* Variances between triangle vertices less than maxVariance will
* be simplified. Use 0 to use every vertex in the final mesh.
* @return A {@link FastShape} containing the mesh.
*/
public FastShape buildMeshFromCenter(float maxVariance)
{
return buildMeshFromCenter(maxVariance, new Rectangle(0, 0, width, height));
}
/**
* Build a mesh from the position grid using positions within the given
* rectangle.
*
* @param maxVariance
* Variances between triangle vertices less than maxVariance will
* be simplified. Use 0 to use every vertex in the final mesh.
* @param rectangle
* Sub-rectangle of positions to use in the mesh.
* @return A {@link FastShape} containing the mesh.
*/
public FastShape buildMesh(float maxVariance, Rectangle rectangle)
{
if (maxVariance <= 0)
{
return buildFullMesh(rectangle);
}
List<BTTTriangle> triangles = new ArrayList<BTTTriangle>();
buildMesh(maxVariance, rectangle.x, rectangle.y, rectangle.width, rectangle.height, false, false, triangles);
return buildFastShape(triangles);
}
/**
* Build a mesh from the position grid using positions within the given
* rectangle. The simplification algorithm will start from the center
* instead of the top-left.
*
* @param maxVariance
* Variances between triangle vertices less than maxVariance will
* be simplified. Use 0 to use every vertex in the final mesh.
* @param rectangle
* Sub-rectangle of positions to use in the mesh.
* @return A {@link FastShape} containing the mesh.
*/
public FastShape buildMeshFromCenter(float maxVariance, Rectangle rectangle)
{
if (maxVariance <= 0)
{
return buildFullMesh(rectangle);
}
List<BTTTriangle> triangles = new ArrayList<BTTTriangle>();
int centerWidth = Util.nextLowestPowerOf2Plus1(rectangle.width);
int centerHeight = Util.nextLowestPowerOf2Plus1(rectangle.height);
int centerXOffset = (rectangle.width - centerWidth) / 2;
int centerYOffset = (rectangle.height - centerHeight) / 2;
int remainingWidth = rectangle.width - centerWidth - centerXOffset;
int remainingHeight = rectangle.height - centerHeight - centerYOffset;
buildMesh(maxVariance, rectangle.x + centerXOffset, rectangle.y + centerYOffset, centerWidth, centerHeight,
false, false, triangles);
buildMesh(maxVariance, rectangle.x, rectangle.y, centerWidth + centerXOffset, centerYOffset + 1, true, true,
triangles);
buildMesh(maxVariance, rectangle.x, rectangle.y + centerYOffset, centerXOffset + 1, rectangle.height
- centerYOffset, true, false, triangles);
buildMesh(maxVariance, rectangle.x + centerWidth + centerXOffset - 1, rectangle.y, remainingWidth + 1,
centerHeight + centerYOffset, false, true, triangles);
buildMesh(maxVariance, rectangle.x + centerXOffset, rectangle.y + centerHeight + centerYOffset - 1,
rectangle.width - centerXOffset, remainingHeight + 1, false, false, triangles);
return buildFastShape(triangles);
}
/**
* Build a mesh from the position grid using positions within the given
* rectangle, using no simplification.
*
* @param rect
* Sub-rectangle of positions to use in the mesh.
* @return A {@link FastShape} containing the mesh.
*/
public FastShape buildFullMesh(Rectangle rect)
{
List<Position> positions;
if (rect.x == 0 && rect.y == 0 && rect.width == width && rect.height == height)
{
//if using the entire area, then use the original positions list
positions = this.positions;
}
else
{
//otherwise, create a new sub-list
positions = new ArrayList<Position>(rect.width * rect.height);
for (int y = rect.y; y < rect.height + rect.y; y++)
{
for (int x = rect.x; x < rect.width + rect.x; x++)
{
positions.add(this.positions.get(x + y * width));
}
}
}
int[] indices;
int i = 0;
if (forceGLTriangles)
{
int indexCount = 6 * (rect.width - 1) * (rect.height - 1);
indices = new int[indexCount];
int k = 0;
for (int y = 0; y < rect.height - 1; y++, k++)
{
for (int x = 0; x < rect.width - 1; x++, k++)
{
indices[i++] = k;
indices[i++] = k + 1;
indices[i++] = k + rect.width;
indices[i++] = k + 1;
indices[i++] = k + rect.width + 1;
indices[i++] = k + rect.width;
}
}
}
else
{
int indexCount = 2 * rect.width * (rect.height - 1);
indices = new int[indexCount];
int k = 0;
for (int y = 0; y < rect.height - 1; y++)
{
if (y % 2 == 0) //even
{
for (int x = 0; x < rect.width; x++, k++)
{
indices[i++] = k;
indices[i++] = k + rect.width;
}
}
else
{
k += rect.width - 1;
for (int x = 0; x < rect.width; x++, k--)
{
indices[i++] = k + rect.width;
indices[i++] = k;
}
k += rect.width + 1;
}
}
}
FastShape shape = new FastShape(positions, indices, forceGLTriangles ? GL2.GL_TRIANGLES : GL2.GL_TRIANGLE_STRIP);
if (generateTextureCoordinates)
{
i = 0;
float[] textureCoordinateBuffer = new float[positions.size() * 2];
for (int y = 0; y < rect.height; y++)
{
for (int x = 0; x < rect.width; x++)
{
textureCoordinateBuffer[i++] = (x + rect.x) / (float) (width - 1);
textureCoordinateBuffer[i++] = (y + rect.y) / (float) (height - 1);
}
}
shape.setTextureCoordinateBuffer(textureCoordinateBuffer);
}
return shape;
}
/**
* Build a mesh within the given rectangle, adding triangles to the triangle
* list. Because the BTT algorithm only supports power-of-2-plus-1 squares,
* this function divides the rectangle area into squares and then builds the
* tree from the sub-squares.
*
* @param maxVariance
* BTT algorithm variance
* @param x
* Rectangle x coordinate
* @param y
* Rectangle y coordinate
* @param width
* Rectangle width
* @param height
* Rectangle height
* @param reverseX
* Begin the mesh building from the right instead of left?
* @param reverseY
* Begin the mesh building from the bottom instead of top?
* @param triangles
* Triangle list to add generated triangles to
*/
protected void buildMesh(float maxVariance, int x, int y, int width, int height, boolean reverseX,
boolean reverseY, List<BTTTriangle> triangles)
{
//cannot build a mesh between less that 2 rows/columns
if (width < 2 || height < 2)
return;
int yStart = y;
int remainingHeight = height;
while (remainingHeight > 1)
{
int xStart = x;
int remainingWidth = width;
int currentHeight = Util.nextLowestPowerOf2Plus1(Math.min(remainingWidth, remainingHeight));
while (remainingWidth > 1)
{
int currentWidth = Util.nextLowestPowerOf2Plus1(Math.min(remainingWidth, remainingHeight));
for (int yOffset = 0; yOffset < currentHeight - 1; yOffset += currentWidth - 1)
{
int tx = reverseX ? width - xStart - currentWidth + x * 2 : xStart;
int ty = reverseY ? height - yStart - yOffset - currentWidth + y * 2 : yStart + yOffset;
buildTree(maxVariance, tx, ty, currentWidth, triangles);
}
remainingWidth -= currentWidth - 1;
xStart += currentWidth - 1;
}
remainingHeight -= currentHeight - 1;
yStart += currentHeight - 1;
}
}
/**
* Build a BinaryTriangleTree starting at the x,y coordinate.
*
* @param maxVariance
* BTT algorithm variance
* @param x
* x coordinate from which to start
* @param y
* y coordinate from which to start
* @param size
* Size of the square (must be a power of 2 plus 1)
* @param triangles
* Triangle list to add generated triangles to
*/
protected void buildTree(float maxVariance, int x, int y, int size, List<BTTTriangle> triangles)
{
/*
* left
* +---+
* |\ |
* | \ |
* | \|
* +---+
* apex right
*/
int apex1 = x + y * width, left1 = x + (y + size - 1) * width, right1 = (x + size - 1) + y * width;
BTTTriangle t1 = new BTTTriangle(apex1, left1, right1);
int apex2 = (x + size - 1) + (y + size - 1) * width, left2 = (x + size - 1) + y * width, right2 =
x + (y + size - 1) * width;
BTTTriangle t2 = new BTTTriangle(apex2, left2, right2);
t1.bottomNeighbour = t2;
t2.bottomNeighbour = t1;
buildFace(maxVariance, t1, x, y, size);
buildFace(maxVariance, t2, x, y, size);
addLeavesToTriangleList(t1, triangles);
addLeavesToTriangleList(t2, triangles);
}
/**
* Recursively sub-divide triangles within t if the triangle variance is
* greater than the maxVariance.
*
* @param maxVariance
* BTT algorithm variance
* @param t
* Triangle to sub-divide
* @param x
* x coordinate from which the area started
* @param y
* y coordinate from which the area started
* @param size
* Size of the square (must be a power of 2 plus 1)
*/
protected void buildFace(float maxVariance, BTTTriangle t, int x, int y, int size)
{
if (t.leftChild != null)
{
buildFace(maxVariance, t.leftChild, x, y, size);
buildFace(maxVariance, t.rightChild, x, y, size);
}
else
{
boolean atLowestLevel =
Math.abs(t.apexIndex - t.leftIndex) == 1 || Math.abs(t.apexIndex - t.rightIndex) == 1;
if (!atLowestLevel)
{
if (isAnyIndexOnEdge(t.apexIndex, t.leftIndex, t.rightIndex, x, y, size)
|| calculateVariance(t.apexIndex, t.leftIndex, t.rightIndex) >= maxVariance)
{
trySplitFace(t);
buildFace(maxVariance, t.leftChild, x, y, size);
buildFace(maxVariance, t.rightChild, x, y, size);
}
}
}
}
/**
* Test if any of the provided indices are on the edge of the BTT
* calculation square.
*
* @param apexIndex
* Index of the triangle apex position
* @param leftIndex
* Index of the triangle left position
* @param rightIndex
* Index of the triangle right position
* @param x
* x coordinate from which the area started
* @param y
* y coordinate from which the area started
* @param size
* Size of the square (must be a power of 2 plus 1)
* @return True if the provided indices are on the edge of the area
*/
protected boolean isAnyIndexOnEdge(int apexIndex, int leftIndex, int rightIndex, int x, int y, int size)
{
return isIndexOnEdge(apexIndex, x, y, size) || isIndexOnEdge(leftIndex, x, y, size)
|| isIndexOnEdge(rightIndex, x, y, size);
}
/**
* Test if the provided index is on the edge of the BTT calculation square.
*
* @param index
* Position index
* @param x
* x coordinate from which the area started
* @param y
* y coordinate from which the area started
* @param size
* Size of the square (must be a power of 2 plus 1)
* @return True if the provided index is on the edge of the area
*/
protected boolean isIndexOnEdge(int index, int x, int y, int size)
{
int ix = index % width;
int iy = index / width;
return ix == x || iy == y || ix == x + size - 1 || iy == y + size - 1;
}
/**
* Try splitting the given triangle. If the triangle's bottom neighbour
* isn't split, it also gets split to ensure there's no gaps in the mesh.
*
* @param t
* Triangle to split.
*/
protected void trySplitFace(BTTTriangle t)
{
if (t.bottomNeighbour != null)
{
if (t.bottomNeighbour.bottomNeighbour != t)
{
trySplitFace(t.bottomNeighbour);
}
splitFace(t);
splitFace(t.bottomNeighbour);
t.leftChild.rightNeighbour = t.bottomNeighbour.rightChild;
t.rightChild.leftNeighbour = t.bottomNeighbour.leftChild;
t.bottomNeighbour.leftChild.rightNeighbour = t.rightChild;
t.bottomNeighbour.rightChild.leftNeighbour = t.leftChild;
}
else
{
splitFace(t);
}
}
/**
* Actually split the given triangle.
*
* @param t
* Triangle to split.
*/
protected void splitFace(BTTTriangle t)
{
int midpointIndex = hypotenuseMidpointIndex(t.leftIndex, t.rightIndex);
t.rightChild = new BTTTriangle(midpointIndex, t.rightIndex, t.apexIndex);
t.leftChild = new BTTTriangle(midpointIndex, t.apexIndex, t.leftIndex);
t.leftChild.leftNeighbour = t.rightChild;
t.rightChild.rightNeighbour = t.leftChild;
t.leftChild.bottomNeighbour = t.leftNeighbour;
if (t.leftNeighbour != null)
{
if (t.leftNeighbour.bottomNeighbour == t)
{
t.leftNeighbour.bottomNeighbour = t.leftChild;
}
else if (t.leftNeighbour.leftNeighbour == t)
{
t.leftNeighbour.leftNeighbour = t.leftChild;
}
else
{
t.leftNeighbour.rightNeighbour = t.leftChild;
}
}
t.rightChild.bottomNeighbour = t.rightNeighbour;
if (t.rightNeighbour != null)
{
if (t.rightNeighbour.bottomNeighbour == t)
{
t.rightNeighbour.bottomNeighbour = t.rightChild;
}
else if (t.rightNeighbour.rightNeighbour == t)
{
t.rightNeighbour.rightNeighbour = t.rightChild;
}
else
{
t.rightNeighbour.leftNeighbour = t.rightChild;
}
}
}
/**
* Calculate the variance of the provided triangle.
*
* @param apexIndex
* Index of the triangle apex position.
* @param leftIndex
* Index of the triangle left position.
* @param rightIndex
* Index of the triangle right position.
* @return Variance of the triangle.
*/
protected float calculateVariance(int apexIndex, int leftIndex, int rightIndex)
{
if (Math.abs(apexIndex - leftIndex) == 1 || Math.abs(apexIndex - rightIndex) == 1)
return 0;
int midpointIndex = hypotenuseMidpointIndex(leftIndex, rightIndex);
double midpointElevation = positions.get(midpointIndex).elevation;
double interpolatedElevation = (positions.get(leftIndex).elevation + positions.get(rightIndex).elevation) / 2;
float delta = (float) Math.abs(midpointElevation - interpolatedElevation);
delta = Math.max(delta, calculateVariance(midpointIndex, rightIndex, apexIndex));
delta = Math.max(delta, calculateVariance(midpointIndex, apexIndex, leftIndex));
return delta;
}
/**
* Index of the midpoint position between the two provided indices.
*
* @param leftIndex
* Left position index.
* @param rightIndex
* Right position index.
* @return Index of the midpoint between leftIndex and rightIndex.
*/
protected int hypotenuseMidpointIndex(int leftIndex, int rightIndex)
{
int leftX = leftIndex % width;
int leftY = leftIndex / width;
int rightX = rightIndex % width;
int rightY = rightIndex / width;
return (leftX + rightX) / 2 + ((leftY + rightY) / 2) * width;
}
/**
* Recursively add all the leaves of the binary triangle tree to the
* provided triangle list, beginning at the provided triangle.
*
* @param t
* Parent triangle to add leaves from
* @param triangles
* Triangle list to add leaves to
*/
protected void addLeavesToTriangleList(BTTTriangle t, List<BTTTriangle> triangles)
{
if (t.leftChild == null || t.rightChild == null)
{
triangles.add(t);
}
else
{
//recurse through children
addLeavesToTriangleList(t.leftChild, triangles);
addLeavesToTriangleList(t.rightChild, triangles);
}
}
/**
* Build a {@link FastShape} object from the list of binary triangle tree
* leaves.
*
* @param triangles
* Triangle list.
* @return FastShape containing triangles from the provided triangle list.
*/
protected FastShape buildFastShape(List<BTTTriangle> triangles)
{
List<Position> positions = new ArrayList<Position>();
List<Integer> originalIndices = new ArrayList<Integer>();
Map<Position, Integer> positionIndexMap = new HashMap<Position, Integer>();
int[] indices = new int[triangles.size() * 3];
int i = 0;
for (BTTTriangle triangle : triangles)
{
Position apexPosition = this.positions.get(triangle.apexIndex);
Position leftPosition = this.positions.get(triangle.leftIndex);
Position rightPosition = this.positions.get(triangle.rightIndex);
Integer apexIndex = positionIndexMap.get(apexPosition);
Integer leftIndex = positionIndexMap.get(leftPosition);
Integer rightIndex = positionIndexMap.get(rightPosition);
if (apexIndex == null)
{
apexIndex = positions.size();
positionIndexMap.put(apexPosition, apexIndex);
positions.add(apexPosition);
originalIndices.add(triangle.apexIndex);
}
if (leftIndex == null)
{
leftIndex = positions.size();
positionIndexMap.put(leftPosition, leftIndex);
positions.add(leftPosition);
originalIndices.add(triangle.leftIndex);
}
if (rightIndex == null)
{
rightIndex = positions.size();
positionIndexMap.put(rightPosition, rightIndex);
positions.add(rightPosition);
originalIndices.add(triangle.rightIndex);
}
indices[i++] = leftIndex;
indices[i++] = apexIndex;
indices[i++] = rightIndex;
}
FastShape shape = new FastShape(positions, indices, GL2.GL_TRIANGLES);
if (generateTextureCoordinates)
{
float[] textureCoordinateBuffer = new float[positions.size() * 2];
i = 0;
for (Integer index : originalIndices)
{
int x = index % width;
int y = index / width;
textureCoordinateBuffer[i++] = x / (float) (width - 1);
textureCoordinateBuffer[i++] = y / (float) (height - 1);
}
shape.setTextureCoordinateBuffer(textureCoordinateBuffer);
}
return shape;
}
/**
* Helper class that stores binary triangle tree information.
*/
protected class BTTTriangle
{
public final int apexIndex;
public final int leftIndex;
public final int rightIndex;
public BTTTriangle leftChild;
public BTTTriangle rightChild;
public BTTTriangle leftNeighbour;
public BTTTriangle rightNeighbour;
public BTTTriangle bottomNeighbour;
public BTTTriangle(int apexIndex, int leftIndex, int rightIndex)
{
this.apexIndex = apexIndex;
this.leftIndex = leftIndex;
this.rightIndex = rightIndex;
}
@Override
public String toString()
{
return "Left: " + (leftIndex % BinaryTriangleTree.this.width) + ","
+ (leftIndex / BinaryTriangleTree.this.width) + ", Apex: "
+ (apexIndex % BinaryTriangleTree.this.width) + "," + (apexIndex / BinaryTriangleTree.this.width)
+ ", Right: " + (rightIndex % BinaryTriangleTree.this.width) + ","
+ (rightIndex / BinaryTriangleTree.this.width);
}
}
}