/*
* Copyright (C) 2011 United States Government as represented by the Administrator of the
* National Aeronautics and Space Administration.
* All Rights Reserved.
*/
package gov.nasa.worldwind.geom;
/**
* Represents a view frustum composed of six planes: left, right, bottom, top, near far.
* <p/>
* Frustum instances are immutable.
*
* @author Tom Gaskins
* @version $Id$
*/
public class Frustum
{
protected final Plane left;
protected final Plane right;
protected final Plane bottom;
protected final Plane top;
protected final Plane near;
protected final Plane far;
/** Holds all six frustum planes in an array in the order left, right, bottom, top, near, far. */
protected final Plane[] allPlanes;
/** Constructs a frustum two meters wide centered at the origin. Primarily used for testing. */
public Frustum()
{
this(
new Plane(1, 0, 0, 1), // Left
new Plane(-1, 0, 0, 1), // Right
new Plane(0, 1, 0, 1), // Bottom
new Plane(0, -1, 0, 1), // Top
new Plane(0, 0, -1, 1), // Near
new Plane(0, 0, 1, 1)); // Far
}
/**
* Create a frustum from six {@link gov.nasa.worldwind.geom.Plane}s defining the frustum boundaries.
* <p/>
* None of the arguments may be null.
*
* @param near the near plane
* @param far the far plane
* @param left the left plane
* @param right the right plane
* @param top the top plane
* @param bottom the bottom plane
*
* @throws IllegalArgumentException if any argument is null.
*/
public Frustum(Plane left, Plane right, Plane bottom, Plane top, Plane near, Plane far)
{
if (left == null || right == null || bottom == null || top == null || near == null || far == null)
{
throw new IllegalArgumentException("Plane Is Null");
}
this.left = left;
this.right = right;
this.bottom = bottom;
this.top = top;
this.near = near;
this.far = far;
this.allPlanes = new Plane[] {this.left, this.right, this.bottom, this.top, this.near, this.far};
}
/**
* Returns the left plane.
*
* @return the left plane.
*/
public final Plane getLeft()
{
return this.left;
}
/**
* Returns the right plane.
*
* @return the right plane.
*/
public final Plane getRight()
{
return this.right;
}
/**
* Returns the bottom plane.
*
* @return the bottom plane.
*/
public final Plane getBottom()
{
return this.bottom;
}
/**
* Returns the top plane.
*
* @return the top plane.
*/
public final Plane getTop()
{
return this.top;
}
/**
* Returns the near plane.
*
* @return the left plane.
*/
public final Plane getNear()
{
return this.near;
}
/**
* Returns the far plane.
*
* @return the left plane.
*/
public final Plane getFar()
{
return this.far;
}
/**
* Returns all the planes.
*
* @return an array of the frustum planes, in the order left, right, bottom, top, near, far.
*/
public Plane[] getAllPlanes()
{
return this.allPlanes;
}
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null || getClass() != obj.getClass())
return false;
Frustum that = (Frustum) obj;
return this.left.equals(that.left)
&& this.right.equals(that.right)
&& this.bottom.equals(that.bottom)
&& this.top.equals(that.top)
&& this.near.equals(that.near)
&& this.far.equals(that.far);
}
public int hashCode()
{
int result;
result = this.left.hashCode();
result = 31 * result + this.right.hashCode();
result = 19 * result + this.bottom.hashCode();
result = 23 * result + this.top.hashCode();
result = 17 * result + this.near.hashCode();
result = 19 * result + this.far.hashCode();
return result;
}
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("(");
sb.append("left=").append(this.left);
sb.append(", right=").append(this.right);
sb.append(", bottom=").append(this.bottom);
sb.append(", top=").append(this.top);
sb.append(", near=").append(this.near);
sb.append(", far=").append(this.far);
sb.append(")");
return sb.toString();
}
// ============== Factory Functions ======================= //
// ============== Factory Functions ======================= //
// ============== Factory Functions ======================= //
/**
* Creates a frustum by extracting the six frustum planes from a projection matrix.
*
* @param projectionMatrix the projection matrix to extract the frustum planes from.
*
* @return a frustum defined by the extracted planes.
*
* @throws IllegalArgumentException if the projection matrix is null.
*/
public static Frustum fromProjectionMatrix(Matrix projectionMatrix)
{
//noinspection UnnecessaryLocalVariable
Matrix m = projectionMatrix;
if (m == null)
{
throw new IllegalArgumentException("Matrix Is Null");
}
// Extract the six clipping planes from the projection-matrix.
Plane leftPlane = new Plane(m.m41 + m.m11, m.m42 + m.m12, m.m43 + m.m13, m.m44 + m.m14).normalize();
Plane rightPlane = new Plane(m.m41 - m.m11, m.m42 - m.m12, m.m43 - m.m13, m.m44 - m.m14).normalize();
Plane bottomPlane = new Plane(m.m41 + m.m21, m.m42 + m.m22, m.m43 + m.m23, m.m44 + m.m24).normalize();
Plane topPlane = new Plane(m.m41 - m.m21, m.m42 - m.m22, m.m43 - m.m23, m.m44 - m.m24).normalize();
Plane nearPlane = new Plane(m.m41 + m.m31, m.m42 + m.m32, m.m43 + m.m33, m.m44 + m.m34).normalize();
Plane farPlane = new Plane(m.m41 - m.m31, m.m42 - m.m32, m.m43 - m.m33, m.m44 - m.m34).normalize();
return new Frustum(leftPlane, rightPlane, bottomPlane, topPlane, nearPlane, farPlane);
}
/**
* Creates a <code>Frustum</code> from a horizontal field-of-view, viewport aspect ratio and distance to near and
* far depth clipping planes. The near plane must be closer than the far plane, and both near and far values must be
* positive.
*
* @param horizontalFieldOfView horizontal field-of-view angle in the range (0, 180)
* @param viewportWidth the width of the viewport in screen pixels
* @param viewportHeight the height of the viewport in screen pixels
* @param near distance to the near depth clipping plane
* @param far distance to far depth clipping plane
*
* @return Frustum configured from the specified perspective parameters.
*
* @throws IllegalArgumentException if fov is not in the range (0, 180), if either near or far are negative, or near
* is greater than or equal to far
*/
public static Frustum fromPerspective(Angle horizontalFieldOfView, int viewportWidth, int viewportHeight,
double near, double far)
{
if (horizontalFieldOfView == null)
{
throw new IllegalArgumentException("Field Of View Is Null");
}
double fov = horizontalFieldOfView.getDegrees();
double farMinusNear = far - near;
if (fov <= 0 || fov > 180)
throw new IllegalArgumentException("Field Of View Out Of Range");
if (near <= 0 || farMinusNear <= 0)
throw new IllegalArgumentException("Clipping Distance Out Of Range");
double focalLength = 1d / horizontalFieldOfView.tanHalfAngle();
double aspect = viewportHeight / (double) viewportWidth;
double lrLen = Math.sqrt(focalLength * focalLength + 1);
double btLen = Math.sqrt(focalLength * focalLength + aspect * aspect);
Plane leftPlane = new Plane(focalLength / lrLen, 0d, 0d - 1d / lrLen, 0);
Plane rightPlane = new Plane(0d - focalLength / lrLen, 0d, 0d - 1d / lrLen, 0d);
Plane bottomPlane = new Plane(0d, focalLength / btLen, 0d - aspect / btLen, 0d);
Plane topPlane = new Plane(0d, 0d - focalLength / btLen, 0d - aspect / btLen, 0d);
Plane nearPlane = new Plane(0d, 0d, 0d - 1d, 0d - near);
Plane farPlane = new Plane(0d, 0d, 1d, far);
return new Frustum(leftPlane, rightPlane, bottomPlane, topPlane, nearPlane, farPlane);
}
/**
* Creates a <code>Frustum</code> from three sets of parallel clipping planes (a parallel projectionMatrix). In this
* case, the near and far depth clipping planes may be negative.
*
* @param near distance to the near depth clipping plane
* @param far distance to far depth clipping plane
* @param width horizontal dimension of the near clipping plane
* @param height vertical dimension of the near clipping plane
*
* @return a Frustum configured with the specified perspective parameters.
*
* @throws IllegalArgumentException if the difference of any plane set (lright - left, top - bottom, far - near) is
* less than or equal to zero.
*/
public static Frustum fromPerspective(double width, double height, double near, double far)
{
double farMinusNear = far - near;
if (farMinusNear <= 0.0 || width <= 0.0 || height <= 0.0)
{
throw new IllegalArgumentException("Clipping Distance Out Of Range");
}
double width_over_2 = width / 2.0;
double height_over_2 = height / 2.0;
Plane leftPlane = new Plane(1.0, 0.0, 0.0, width_over_2);
Plane rightPlane = new Plane(-1.0, 0.0, 0.0, width_over_2);
Plane bottomPlane = new Plane(0.0, 1.0, 0.0, height_over_2);
Plane topPlane = new Plane(0.0, -1.0, 0.0, height_over_2);
Plane nearPlane = new Plane(0.0, 0.0, -1.0, (near < 0.0) ? near : -near);
Plane farPlane = new Plane(0.0, 0.0, 1.0, (far < 0.0) ? -far : far);
return new Frustum(leftPlane, rightPlane, bottomPlane, topPlane, nearPlane, farPlane);
}
/**
* Creates a <code>Frustum</code> from four edge vectors, viewport aspect ratio and distance to near and far planes.
* The edge vectors connect the near corners of the frustum to the far corners. The near plane must be closer than
* the far plane, and both planes must be positive.
*
* @param vTL vector defining the top-left of the frustum
* @param vTR vector defining the top-right of the frustum
* @param vBL vector defining the bottom-left of the frustum
* @param vBR vector defining the bottom-right of the frustum
* @param near distance to the near plane
* @param far distance to far plane
*
* @return Frustum that was created
*
* @throws IllegalArgumentException if any of the vectors are null, if either near or far are negative, or near is
* greater than or equal to far
*/
public static Frustum fromPerspectiveVecs(Vec4 vTL, Vec4 vTR, Vec4 vBL, Vec4 vBR,
double near, double far)
{
if (vTL == null || vTR == null || vBL == null || vBR == null)
{
throw new IllegalArgumentException("Edge Vector Is Null");
}
double farMinusNear = far - near;
if (near <= 0 || farMinusNear <= 0)
{
throw new IllegalArgumentException("Clipping Distance Out Of Range");
}
Vec4 lpn = vBL.cross3(vTL).normalize3();
Plane leftPlane = new Plane(lpn.x, lpn.y, lpn.z, 0);
Vec4 rpn = vTR.cross3(vBR).normalize3();
Plane rightPlane = new Plane(rpn.x, rpn.y, rpn.z, 0);
Vec4 bpn = vBR.cross3(vBL).normalize3();
Plane bottomPlane = new Plane(bpn.x, bpn.y, bpn.z, 0);
Vec4 tpn = vTL.cross3(vTR).normalize3();
Plane topPlane = new Plane(tpn.x, tpn.y, tpn.z, 0);
Plane nearPlane = new Plane(0d, 0d, 0d - 1d, 0d - near);
Plane farPlane = new Plane(0d, 0d, 1d, far);
return new Frustum(leftPlane, rightPlane, bottomPlane, topPlane, nearPlane, farPlane);
}
// ============== Intersection Functions ======================= //
// ============== Intersection Functions ======================= //
// ============== Intersection Functions ======================= //
/**
* Determines whether a line segment intersects this frustum.
*
* @param pa one end of the segment.
* @param pb the other end of the segment.
*
* @return true if the segment intersects or is contained in the frustum, otherwise false.
*
* @throws IllegalArgumentException if either point is null.
*/
public boolean intersectsSegment(Vec4 pa, Vec4 pb)
{
if (pa == null || pb == null)
{
throw new IllegalArgumentException("Point Is Null");
}
// First do a trivial accept test.
if (this.contains(pa) || this.contains(pb))
return true;
if (pa.equals(pb))
return false;
for (Plane p : this.getAllPlanes())
{
// See if both points are behind the plane and therefore not in the frustum.
if (p.onSameSide(pa, pb) < 0)
return false;
// See if the segment intersects the plane.
if (p.clip(pa, pb) != null)
return true;
}
return false; // segment does not intersect frustum
}
/**
* Indicates whether a specified point is within this frustum.
*
* @param point the point to test.
*
* @return true if the point is within the frustum, otherwise false.
*
* @throws IllegalArgumentException if the point is null.
*/
public final boolean contains(Vec4 point)
{
if (point == null)
{
throw new IllegalArgumentException("Point Is Null");
}
// See if the point is entirely within the frustum. The dot product of the point with each plane's vector
// provides a distance to each plane. If this distance is less than 0, the point is clipped by that plane and
// neither intersects nor is contained by the space enclosed by this Frustum.
if (this.far.dot(point) <= 0)
return false;
if (this.left.dot(point) <= 0)
return false;
if (this.right.dot(point) <= 0)
return false;
if (this.top.dot(point) <= 0)
return false;
if (this.bottom.dot(point) <= 0)
return false;
//noinspection RedundantIfStatement
if (this.near.dot(point) <= 0)
return false;
return true;
}
// ============== Geometric Functions ======================= //
// ============== Geometric Functions ======================= //
// ============== Geometric Functions ======================= //
/**
* Returns a copy of this frustum transformed by a specified {@link Matrix}.
*
* @param matrix the Matrix to apply to this frustum.
*
* @return a new frustum transformed by the specified matrix.
*
* @throws IllegalArgumentException if the matrix is null.
*/
public Frustum transformBy(Matrix matrix)
{
if (matrix == null)
{
throw new IllegalArgumentException("Matrix Is Null");
}
Plane left = new Plane(this.left.getVector().transformBy4(matrix));
Plane right = new Plane(this.right.getVector().transformBy4(matrix));
Plane bottom = new Plane(this.bottom.getVector().transformBy4(matrix));
Plane top = new Plane(this.top.getVector().transformBy4(matrix));
Plane near = new Plane(this.near.getVector().transformBy4(matrix));
Plane far = new Plane(this.far.getVector().transformBy4(matrix));
return new Frustum(left, right, bottom, top, near, far);
}
/** Holds the eight corner points of a frustum. */
public static class Corners
{
public Vec4 nbl, nbr, ntl, ntr, fbl, fbr, ftl, ftr;
}
/**
* Returns the eight corners of this frustum.
*
* @return the eight frustum corners.
*/
public Corners getCorners()
{
Corners corners = new Corners();
corners.nbl = Plane.intersect(this.near, this.bottom, this.left);
corners.nbr = Plane.intersect(this.near, this.bottom, this.right);
corners.ntl = Plane.intersect(this.near, this.top, this.left);
corners.ntr = Plane.intersect(this.near, this.top, this.right);
corners.fbl = Plane.intersect(this.far, this.bottom, this.left);
corners.fbr = Plane.intersect(this.far, this.bottom, this.right);
corners.ftl = Plane.intersect(this.far, this.top, this.left);
corners.ftr = Plane.intersect(this.far, this.top, this.right);
return corners;
}
}