/*
* Copyright (c) 2010 Stephen A. Pratt
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.critterai.nmgen;
import org.critterai.nmgen.SolidHeightfield.SolidHeightFieldIterator;
/**
* A class used to build solid heightfields from source geometry using a
* given configuration. The solid heightfield represents the space obstructed
* by the source geometry.
* <p>
* Each triangle in the source geometry is voxelized using conservative
* voxelization and added to the field. Conservative voxelization is an
* algorithm that ensures that polygon surfaces are completely encompassed by
* the the generated voxels.
* </p>
* <p>
* At the end of the process, spans with the {@link SpanFlags#WALKABLE} flag
* have survived the following tests:
* </p>
* <ul>
* <li>The top of the span is at least a minimum distance from the bottom of the
* span above it. (The tallest agent can "stand" on the span without colliding
* with an obstruction above.)</li>
* <li>The top voxel of the span represents geometry with a slope below a
* maximum allowed value. (The slope is low enough to be traversable by agents.)
* </li>
* <li>If ledge culling is enabled, the top of the span does not represent a
* ledge. (Agents can legally "step down" from the span to any of its
* neighbors.)</li>
* </ul>
*
* @see <a href="http://www.critterai.org/nmgen_voxel" target="_parent">The
* Voxelization Process</a>
* @see <a href="http://www.critterai.org/nmgen_hfintro"
* target="_parent">Introduction to Heightfields</a>
* @see <a href="http://www.ecn.purdue.edu/purpl/level2/papers/consvox.pdf"
* target="_blank">Conservative Voxelization (PDF)</a>
*/
public final class SolidHeightfieldBuilder {
/*
* Design notes:
*
* Recast reference:
* rcCreateHeightfield in Recast.cpp
* rcMarkWalkableTriangles in Recast.cpp
* rcRasterizeTriangles in RecastRasterization.cpp
* rcFilterLedgeSpans in RecastFilter.cpp
* rcFilterWalkableLowHeightSpans in RecastFilter.cpp
*
* Not adding configuration getters until they are needed.
* Never add setters. Configuration should remain immutable to keep
* the class thread friendly.
*
* TODO: EVAL: It may be better to implement post-processing as external
* algorithms similar to what is done with the open heightfield and
* contour classes.
*/
// Configuration settings.
private final boolean mClipLedges;
private final int mMinTraversableHeight;
private final int mMaxTraversableStep;
/**
* A derived value which represent the minimum y-normal permitted for
* a polygon to be considered traversable.
* <p>
* See the constructor for details.
* </p>
*/
private final float mMinNormalY;
/**
* The cell size to use for all new fields.
* <p>
* IMPORTANT: Only use this value for heightfield object initialization.
* After that, use the value in the heightfield object. (Since the
* heightfield instance may place limitations on the value.)
* </p>
*/
private final float mCellSize;
/**
* The cell height to use for all new fields.
* <p>
* IMPORTANT: Only use this value for heightfield object initialization.
* After that, use the value in the heightfield object. (Since the height
* field object may place limitations on the value.)
* </p>
*/
private final float mCellHeight;
/**
* Constructor
*
* @param cellSize
* The size of the cells. (The grid that forms the base
* of the field.)
* <p>
* This value represents the x and z-axis sampling resolution to use
* when generating voxels.
* </p>
*
* @param cellHeight
* The height increment of the field.
* <p>
* This value represents the y-axis sampling resolution to use when
* generating voxels.
* </p>
*
* @param minTraversableHeight
* Represents the minimum floor to ceiling
* height that will still allow a floor area to be considered
* traversable.
* <p>
* Permits detection of overhangs in the geometry which make the
* geometry below become unwalkable.
* </p>
* <p>
* Constraints: > 0
* </p>
*
* @param maxTraversableStep
* Represents the maximum ledge height that
* is considered to still be traversable.
* <p>
* Prevents minor deviations in height from improperly showing as
* obstructions. Permits detection of stair-like structures, curbs,
* etc.
* </p>
* <p>
* Constraints: >= 0
* </p>
*
* @param maxTraversableSlope
* The maximum slope that is considered
* traversable. (Degrees)
* <p>
* Spans that are at or above this slope have the
* {@link SpanFlags#WALKABLE} flag removed.
* </p>
* <p>
* Constraints: 0 <= value <= 85
* </p>
*
* @param clipLedges
* Indicates whether ledges should be marked as
* unwalkable. I.e. The {@link SpanFlags#WALKABLE} flag will be
* removed.
* <p>
* A ledge is a normally traversable span that has one or more
* accessible neighbors with a an un-steppable drop from span top to
* span top.
* </p>
* <p>
* E.g. If an agent using the navmesh were to travel down from the
* ledge span to its neighbor span, it would result in the maximum
* traversable step distance being violated. The agent cannot legally
* "step down" from a ledge to its neighbor.
* </p>
*/
public SolidHeightfieldBuilder(final float cellSize, final float cellHeight,
final int minTraversableHeight, final int maxTraversableStep,
float maxTraversableSlope, final boolean clipLedges) {
this.mMinTraversableHeight = Math.max(1, minTraversableHeight);
this.mMaxTraversableStep = Math.max(0, maxTraversableStep);
maxTraversableSlope = Math.min(85, Math.max(0, maxTraversableSlope));
this.mClipLedges = clipLedges;
this.mCellSize = cellSize;
this.mCellHeight = cellHeight;
/*
* Derive the minimum y-normal.
*
* Base Reference: http://mathworld.wolfram.com/DihedralAngle.html
*
* By ensuring n1 and n2 are both normalized before the calculation, the
* denominator in the reference equations evaluate to 1 and
* can be discarded. So the reference equation is simplified to...
*
* cos theta = n1 dot n2
*
* Using:
*
* n1 = (0, 1, 0) (Represents a flat surface on the (x,z) plane.)
* n2 = (x, y, z) Normalized. (A surface on an arbitrary plane.)
*
* Simplify and solve for y:
*
* cos theta = 0x + 1y + 0z
* y = cos theta
*
* We know theta. It is the value of maxTraversableSlope after
* conversion to radians. So we know what y-normal is at the walk
* slope angle. If a polygon's y-normal is LESS THAN our calculated
* y-normal, then we know we have exceeded the walk angle.
*/
this.mMinNormalY = (float) Math.cos((Math.abs(maxTraversableSlope) / 180) * Math.PI);
}
/**
* Generates a solid heightfield from the provided source geometry. The
* solid heightfield will represent the space obstructed by the source
* geometry.
* <p>
* The {@link SpanFlags#WALKABLE} will be applied to spans whose top surface
* is considered traversable. See the class description for details.
* </p>
*
* @param vertices
* Source geometry vertices in the form (x, y, z).
* @param indices
* Source geometry indices in the form (VertA, VertB, VertC).
* Wrapped: Clockwise.
* @return The generated solid heightfield, or null if the generation fails.
*/
public SolidHeightfield build(final float[] vertices, final int[] indices) {
// Perform basic checks.
if ((vertices == null) ||
(indices == null) ||
((vertices.length % 3) != 0) ||
((indices.length % 3) != 0)) {
return null;
}
// Initialize heightfield.
final SolidHeightfield result = new SolidHeightfield(this.mCellSize, this.mCellHeight);
// Pre-calculate values to save on the cost of division later.
final float inverseCellSize = 1 / result.cellSize();
final float inverseCellHeight = 1 / result.cellHeight();
// Detect and set the bounds of the source geometry.
// Default to the first vertex.
float xmin = vertices[0];
float ymin = vertices[1];
float zmin = vertices[2];
float xmax = vertices[0];
float ymax = vertices[1];
float zmax = vertices[2];
// Loop through all vertices, expanding the bounds
// as appropriate.
for (int i = 3; i < vertices.length; i += 3) {
xmax = Math.max(vertices[i], xmax);
ymax = Math.max(vertices[i + 1], ymax);
zmax = Math.max(vertices[i + 2], zmax);
xmin = Math.min(vertices[i], xmin);
ymin = Math.min(vertices[i + 1], ymin);
zmin = Math.min(vertices[i + 2], zmin);
}
// Set the bounds.
result.setBounds(xmin, ymin, zmin, xmax, ymax, zmax);
// Detect which polygons in the source mesh have a slope
// that low enough to be considered traversable. (Agent can walk up
// or down the slope.)
final int[] polyFlags = markInputMeshWalkableFlags(vertices, indices);
// For each polygon in the source mesh: Voxelize it and add the
// resulting spans to the solid field.
final int polyCount = indices.length / 3;
for (int iPoly = 0; iPoly < polyCount; iPoly++) {
voxelizeTriangle(iPoly, vertices, indices, polyFlags[iPoly], inverseCellSize,
inverseCellHeight, result);
}
// Remove the walkable flag from any span that has another span too
// close above it.
markLowHeightSpans(result);
if (this.mClipLedges) {
// Remove the walkable flag from any span that is determined to
// be a ledge.
markLedgeSpans(result);
}
return result;
}
/**
* Checks the slope of each polygon against the maximum allowed. Any
* polygon whose slope is below the maximum permitted gets the
* {@link SpanFlags#WALKABLE} flag.
*
* @param vertices
* The source geometry vertices in the form (x, y, z).
* @param indices
* The source geometry indices in the form
* (vertA, vertB, vertC), clockwise wrapped.
* @return An array of flags in the form
* (polyFlag0, polyFlag1, ..., polyFlagN), stride = 1.
*/
private int[] markInputMeshWalkableFlags(final float[] vertices, final int[] indices) {
// See mMinNormalY in constructor for more information on how this
// works.
final int[] flags = new int[indices.length / 3];
// Working variables. Content changes for every loop
// and has no meaning outside the loop.
final float[] diffAB = new float[3];
final float[] diffAC = new float[3];
final float[] crossDiff = new float[3];
// Loop through all polygons.
final int polyCount = indices.length / 3;
for (int iPoly = 0; iPoly < polyCount; iPoly++) {
// Get pointers to each polygon vertex.
final int pVertA = indices[iPoly * 3] * 3;
final int pVertB = indices[(iPoly * 3) + 1] * 3;
final int pVertC = indices[(iPoly * 3) + 2] * 3;
// Determine the y-normal for the polygon.
final float normalY =
getNormalY(cross(subtract(pVertB, pVertA, vertices, diffAB),
subtract(pVertC, pVertA, vertices, diffAC), crossDiff));
if (normalY > this.mMinNormalY) {
// The slope of this polygon is acceptable. Mark it as
// walkable.
flags[iPoly] = SpanFlags.WALKABLE;
}
}
return flags;
}
/**
* Removes the traversable flag for any spans that represent a ledge.
* A ledge occurs when stepping from the top of one span down to any of its
* neighbor spans exceeds the allowed walk climb distance. (i.e. Can't
* legally "step down" to a neighbor span.)
*
* @param field
* The field to operation on.
*/
private void markLedgeSpans(final SolidHeightfield field) {
/*
* Note: While this is a solid field representing obstructions, much
* of this algorithm deals with the space between obstructions.
* (The gaps.) So you will need to twist your thinking to gaps rather
* than obstructions.
*
* For visualization, see the @see in the class' javadoc.
*/
// Loop through all spans.
final SolidHeightFieldIterator iter = field.dataIterator();
while (iter.hasNext()) {
final HeightSpan span = iter.next();
if ((span.flags() & SpanFlags.WALKABLE) == 0) {
// Span is already known to be un-waklable.
// Skip it.
continue;
}
final int widthIndex = iter.widthIndex();
final int depthIndex = iter.depthIndex();
// These values represent the gap (floor to ceiling) above the
// current span.
final int currFloor = span.max();
final int currCeiling = (span.next() != null) ? span.next().min() : Integer.MAX_VALUE;
/*
* Represents the minimum distance from the current span's floor
* to a neighbor span's floor. A positive value indicates a step
* up. A negative distance represents a step down. If this
* distance is too far down, then the current span is a ledge
* and isn't traversable.
*
* This algorithm only cares about drops.
* Default to a maximum step up.
*/
int minDistanceToNeighbor = Integer.MAX_VALUE;
/*
* The lowest possible floor is at -mMaxTraversableStep. No span
* can exist below the zero height index. So stepping from a span
* whose floor is at zero to "empty space" will result in a drop
* to -mMaxTraversableStep.
*/
/*
* Loop through all neighbor grid cells.
*/
for (int dir = 0; dir < 4; dir++) {
final int nWidthIndex = widthIndex + BoundedField.getDirOffsetWidth(dir);
final int nDepthIndex = depthIndex + BoundedField.getDirOffsetDepth(dir);
// Get the lowest span in this neighbor column.
HeightSpan nSpan = field.getData(nWidthIndex, nDepthIndex);
if (nSpan == null) {
// No neighbor on this side. Treat as the maximum drop.
// (Which is always considered a ledge.)
// TODO: EVAL: Should this be a break rather than a
// continue? (Detected too close to release to risk code
// changes.)
minDistanceToNeighbor =
Math.min(minDistanceToNeighbor, -this.mMaxTraversableStep - currFloor);
continue;
}
/*
* First need to take into account the area below the lowest
* span in this neighbor column.
*
* In this special case, the floor of this gap is the lowest
* possible value. The ceiling of this gap is the bottom of
* neighbor span.
*/
// Default to an excessive drop.
int nFloor = -this.mMaxTraversableStep;
int nCeiling = nSpan.min(); // The bottom of this first span.
/*
* This check filters out the following:
*
* The distance from the current span's floor this neighbor's
* ceiling is not large enough to permit transit in that
* direction? (Agent will "bump its head" if it moves in this
* direction?)
*
* The neighbor gap is entirely below the floor of the
* current span.
*
* In such cases travel is not allowed to the neghbor gap, so
* it isn't taken into account.
*/
if ((Math.min(currCeiling, nCeiling) - currFloor) > this.mMinTraversableHeight) {
// Travel is permitted in this direction. So take this
// neighbor gap into account.
minDistanceToNeighbor = Math.min(minDistanceToNeighbor, (nFloor - currFloor));
}
/*
* Now process the rest of the gaps in this neighbor column
* normally. E.g. The top of the span is the floor. The
* bottom of the next span is the ceiling.
*/
for (nSpan = field.getData(nWidthIndex, nDepthIndex); nSpan != null; nSpan =
nSpan.next()) {
nFloor = nSpan.max();
nCeiling = (nSpan.next() != null) ? nSpan.next().min() : Integer.MAX_VALUE;
/*
* This next check filters out the following:
*
* The distance from the current span's floor this
* neighbor's ceiling is not large enough to permit
* transit in that direction? (Agent will "bump its head"
* if it moves in this direction?)
*
* The neighbor gap is entirely below the floor of the
* current span.
*
* The neighbor gap is entirely above the ceiling of the
* current gap.
*
* In such cases travel is not allowed to the neghbor gap,
* so it isn't taken into account.
*/
if ((Math.min(currCeiling, nCeiling) - Math.max(currFloor, nFloor)) > this.mMinTraversableHeight) {
// Potential travel to this neighbor span.
minDistanceToNeighbor =
Math.min(minDistanceToNeighbor, (nFloor - currFloor));
}
}
}
// Remember: A negative distance indicates a drop.
if (minDistanceToNeighbor < -this.mMaxTraversableStep) {
// Can only drop by mMaxTraversableStep, but a neighbor has a
// drop that exceeds this allowed drop. Remove the walkable
// flag.
span.setFlags(span.flags() & ~SpanFlags.WALKABLE);
}
}
}
/**
* Remove the traversable flag from spans that have another span too
* close above them.
*
* @param field
* The heightfield to operate on.
*/
private void markLowHeightSpans(final SolidHeightfield field) {
// TODO: EVAL: Consider merging this operation with markLedgeSpans.
// For visualization, see the @see in the class' javadoc.
// Iterate through all spans in the field.
final SolidHeightFieldIterator iter = field.dataIterator();
while (iter.hasNext()) {
final HeightSpan span = iter.next();
if ((span.flags() & SpanFlags.WALKABLE) == 0) {
// Span is already known to be un-waklable.
// Skip it.
continue;
}
// Find the gap between the current span and the next higher span.
// This represents the open space (floor to ceiling) above the
// current span.
final int spanFloor = span.max();
final int spanCeiling = (span.next() != null) ? span.next().min() : Integer.MAX_VALUE;
if ((spanCeiling - spanFloor) <= this.mMinTraversableHeight) {
// Can't stand on this span. Ceiling is too low.
// Remove its walkable flag.
span.setFlags(span.flags() & ~SpanFlags.WALKABLE);
}
}
}
/**
* Clamps the value to the specified range.
*
* @param value
* The value to clamp.
* @param minimum
* The minimum allowed value. (Inclusive.)
* @param maximum
* The maximum allowed value. (Inclusive.)
* @return If minimum <= value <= maximum, will return value.
* If value < minimum, will return minimum.
* If value > maximum, will return maximum.
*/
private static int clamp(final int value, final int minimum, final int maximum) {
return (value < minimum ? minimum : (value > maximum ? maximum : value));
}
private static int clipPoly(final float[] in, final int inputVertCount, final float[] out,
final float pnx, final float pnz, final float pd) {
// TODO: DOC: Figure out what is going on here. Not familiar with
// algorithm. pnx and pnz are normals.
final float d[] = new float[inputVertCount];
for (int vertIndex = 0; vertIndex < inputVertCount; ++vertIndex) {
d[vertIndex] = (pnx * in[vertIndex * 3]) + (pnz * in[(vertIndex * 3) + 2]) + pd;
}
int m = 0;
for (int current = 0, previous = d.length - 1; current < d.length; previous = current, ++current) {
final boolean ina = d[previous] >= 0;
final boolean inb = d[current] >= 0;
if (ina != inb) {
final float s = d[previous] / (d[previous] - d[current]);
out[(m * 3) + 0] =
in[(previous * 3) + 0] +
((in[(current * 3) + 0] - in[(previous * 3) + 0]) * s);
out[(m * 3) + 1] =
in[(previous * 3) + 1] +
((in[(current * 3) + 1] - in[(previous * 3) + 1]) * s);
out[(m * 3) + 2] =
in[(previous * 3) + 2] +
((in[(current * 3) + 2] - in[(previous * 3) + 2]) * s);
m++;
}
if (inb) {
out[(m * 3) + 0] = in[(current * 3) + 0];
out[(m * 3) + 1] = in[(current * 3) + 1];
out[(m * 3) + 2] = in[(current * 3) + 2];
m++;
}
}
return m;
}
/**
* Performs a cross product on the vectors u and v. (u x v)
*
* @param u
* The first vector in the form (x, y, z)
* @param v
* The second vector in the form (x, y, z)
* @param out
* The array to be loaded with the result.
* @return A reference to the out array loaded with the cross product.
*/
private static float[] cross(final float[] u, final float[] v, final float[] out) {
// Reference: http://mathworld.wolfram.com/CrossProduct.html
// Reference: http://en.wikipedia.org/wiki/Cross_product
// #Computing_the_cross_product
out[0] = (u[1] * v[2]) - (u[2] * v[1]);
out[1] = (-u[0] * v[2]) + (u[2] * v[0]);
out[2] = (u[0] * v[1]) - (u[1] * v[0]);
return out;
}
/**
* Normalizes the provided vector and returns the y-value.
*
* @param v
* The vector to normalize in the form: (x, y, z)
* @return The y-value of the normalized vector.
*/
private static float getNormalY(final float[] v) {
// This is just the standard normalization algorithm with
// unneeded x and z calculations removed.
final float epsilon = 0.0001f;
float length = (float) Math.sqrt((v[0] * v[0]) + (v[1] * v[1]) + (v[2] * v[2]));
if (length <= epsilon) {
length = 1;
}
float y = v[1] / length;
if (Math.abs(y) < epsilon) {
y = 0;
}
return y;
}
/**
* Performs vector subtraction on the vertices. (VertexA - VertexB)
*
* @param pVertA
* The pointer to a valid vertex in the source vertices array.
* @param pVertB
* The pointer to a valid vertex in the source vertices array.
* @param out
* An array of size 3. The array is loaded with the result of
* the subtraction and returned.
* @return A reference to the out array loaded with the result of the
* subtraction.
*/
private static float[] subtract(final int pVertA, final int pVertB, final float[] vertices,
final float[] out) {
out[0] = vertices[pVertA] - vertices[pVertB];
out[1] = vertices[pVertA + 1] - vertices[pVertB + 1];
out[2] = vertices[pVertA + 2] - vertices[pVertB + 2];
return out;
}
/**
* Voxelizes the chosen polygon and adds the resulting spans to the
* heightfield.
* <p>
* The inverse arguments are included for optimization. (No need to
* recalculate the values for every call.)
* </p>
* <p>
* The heightfield will make the final decision on whether to apply the
* flags provided in the arguments. See the
* {@link SolidHeightfield#addData(int, int, int, int, int) heightfield
* add operation} for details
* </p>
*
* @param polyIndex
* The polygon to voxelize.
* @param vertices
* The vertices of the source geometry in the form
* (x, y, z).
* @param indices
* The indices of the source geometry in the form
* (vertA, vertB, vertC), wrapped clockwise.
* @param polyFlags
* The flags to apply to all new spans within the
* heightfield.
* @param inverseCellSize
* Inverse cell size. (1/cellSize)
* @param inverseCellHeight
* Inverse cell height. (1/cellheight)
* @param inoutField
* The heightfield to add new spans to.
*/
private static void voxelizeTriangle(final int polyIndex, final float[] vertices,
final int[] indices, final int polyFlags, final float inverseCellSize,
final float inverseCellHeight, final SolidHeightfield inoutField) {
/*
* Design notes:
*
* There is significant processing going on here that is not
* required since this is a private operation and the input is tightly
* controlled. For example: We know that the heightfield is sized to
* hold the source geometry, so bounds checks aren't really needed.
*
* But, with the possible exception of object creation, the extra
* cost is not big. So I'm leaving the algorithm as it is just in
* case it is converted to a public operation at a later date.
*/
// Pointer to the polygon.
final int pPoly = polyIndex * 3;
// Polygon vertices.
final float[] triVerts =
{
vertices[indices[pPoly] * 3] // VertA
,
vertices[(indices[pPoly] * 3) + 1],
vertices[(indices[pPoly] * 3) + 2],
vertices[indices[pPoly + 1] * 3] // VertB
, vertices[(indices[pPoly + 1] * 3) + 1],
vertices[(indices[pPoly + 1] * 3) + 2],
vertices[indices[pPoly + 2] * 3] // VertC
, vertices[(indices[pPoly + 2] * 3) + 1],
vertices[(indices[pPoly + 2] * 3) + 2] };
// Determine the bounding box of the polygon.
// Initialize bounds to the first triangle vertex.
final float[] triBoundsMin = { triVerts[0], triVerts[1], triVerts[2] };
final float[] triBoundsMax = { triVerts[0], triVerts[1], triVerts[2] };
// Loop through all vertices to determine the actual bounding box.
for (int vertPointer = 3; vertPointer < 9; vertPointer += 3) {
triBoundsMin[0] = Math.min(triBoundsMin[0], triVerts[vertPointer]);
triBoundsMin[1] = Math.min(triBoundsMin[1], triVerts[vertPointer + 1]);
triBoundsMin[2] = Math.min(triBoundsMin[2], triVerts[vertPointer + 2]);
triBoundsMax[0] = Math.max(triBoundsMax[0], triVerts[vertPointer]);
triBoundsMax[1] = Math.max(triBoundsMax[1], triVerts[vertPointer + 1]);
triBoundsMax[2] = Math.max(triBoundsMax[2], triVerts[vertPointer + 2]);
}
// If the triangle does not overlap the heightfield, then skip it.
if (!inoutField.overlaps(triBoundsMin, triBoundsMax)) {
return;
}
/*
* Determine footprint of triangle bounding box on the heightfield's
* grid.
*
* Notes:
*
* The heightfield is an integer based grid with its origin at the
* heightfield's minimum bounds. I.e. Grid coordinate
* (0, 0) => (heightField.minbounds.x, heightField.minbounds.z)
*
* The heightfield width/depth values map to the (x, z) plane of the
* triangle, not the (x, y) plane.
*/
// First, convert the triangle bounds to field grid coordinates.
int triWidthMin = (int) ((triBoundsMin[0] - inoutField.boundsMin()[0]) * inverseCellSize);
int triDepthMin = (int) ((triBoundsMin[2] - inoutField.boundsMin()[2]) * inverseCellSize);
int triWidthMax = (int) ((triBoundsMax[0] - inoutField.boundsMin()[0]) * inverseCellSize);
int triDepthMax = (int) ((triBoundsMax[2] - inoutField.boundsMin()[2]) * inverseCellSize);
// Snap the grid coordinates to the grid bounds.
triWidthMin = clamp(triWidthMin, 0, inoutField.width() - 1);
triDepthMin = clamp(triDepthMin, 0, inoutField.depth() - 1);
triWidthMax = clamp(triWidthMax, 0, inoutField.width() - 1);
triDepthMax = clamp(triDepthMax, 0, inoutField.depth() - 1);
/*
* "in" will contain the final data.
* "out" and "inrow" are used for intermediate data.
* "in" is initially seeded with the triangle vertices.
* The arrays are sized to be 3 * 7. This allows for the storage of
* the maximum vertex count for a triangle clipped into a square (6)
* with an extra triple.
* (Don't know the purpose of the extra triple.)
*/
final float in[] = new float[21];
final float out[] = new float[21];
final float inrow[] = new float[21];
// The height of the heightfield.
final float fieldHeight = inoutField.boundsMax()[1] - inoutField.boundsMin()[1];
/*
* Loop through all grid locations overlapped by the polygon.
* (xz-plane only).
*
* Clip the triangle into all grid cells it touches.
*
* Any early exit from either of the loops means that the triangle
* does not overlap the grid column, or is outside the height
* bounds of the field.
*
* All detailed clip data is discarded in the end. The only information
* preserved is the height information.
*
* Dev Note: I don't understand the mathematical algorithm used here.
* So I've marked it as magic. But by tracing the process on paper
* I've determined that the algorithm is finding all intersection
* points between the grid column and the triangle face.
*
* For visualization, see the @see in the class' javadoc.
*/
for (int depthIndex = triDepthMin; depthIndex <= triDepthMax; ++depthIndex) {
// Seed with the triangle vertices.
System.arraycopy(triVerts, 0, in, 0, triVerts.length);
// Do some magic.
// Count of cell intersection vertices found.
int intermediateVertCount = 3;
final float rowWorldZ =
inoutField.boundsMin()[2] + (depthIndex * inoutField.cellSize());
intermediateVertCount = clipPoly(in, intermediateVertCount, out, 0, 1, -rowWorldZ);
if (intermediateVertCount < 3) {
continue;
}
intermediateVertCount =
clipPoly(out, intermediateVertCount, inrow, 0, -1,
rowWorldZ + inoutField.cellSize());
if (intermediateVertCount < 3) {
continue;
}
for (int widthIndex = triWidthMin; widthIndex <= triWidthMax; ++widthIndex) {
// Do some more magic.
int vertCount = intermediateVertCount;
final float colWorldX =
inoutField.boundsMin()[0] + (widthIndex * inoutField.cellSize());
vertCount = clipPoly(inrow, vertCount, out, 1, 0, -colWorldX);
if (vertCount < 3) {
continue;
}
vertCount = clipPoly(out, vertCount, in, -1, 0, colWorldX + inoutField.cellSize());
if (vertCount < 3) {
continue;
}
// If got here, then "in" contains the definition for a poly
// representing the portion
// of the input triangle that overlaps the grid location.
// Find the height (y-axis) range for this grid location.
float heightMin = in[1];
float heightMax = in[1];
for (int i = 1; i < vertCount; ++i) {
heightMin = Math.min(heightMin, in[(i * 3) + 1]);
heightMax = Math.max(heightMax, in[(i * 3) + 1]);
}
// Convert to height above the "floor" of the heightfield.
heightMin -= inoutField.boundsMin()[1];
heightMax -= inoutField.boundsMin()[1];
if ((heightMax < 0.0f) || (heightMin > fieldHeight)) {
// The height of the potential span is entirely outside
// the bounds of the heightfield.
continue;
}
// Clamp to the heightfield bounding box.
if (heightMin < 0.0f) {
heightMin = inoutField.boundsMin()[1];
}
if (heightMax > fieldHeight) {
heightMax = inoutField.boundsMax()[1];
}
// Convert the min/max to height grid index.
final int heightIndexMin =
clamp((int) Math.floor(heightMin * inverseCellHeight), 0, Short.MAX_VALUE);
final int heightIndexMax =
clamp((int) Math.ceil(heightMax * inverseCellHeight), 0, Short.MAX_VALUE);
// Add the span to the heightfield.
inoutField.addData(widthIndex, depthIndex, heightIndexMin, heightIndexMax,
polyFlags);
}
}
}
}