diff --git a/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java b/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java index 0d9a37a6c7..e6b67501f2 100644 --- a/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java +++ b/jme3-core/src/main/java/com/jme3/bounding/BoundingBox.java @@ -39,36 +39,31 @@ import com.jme3.export.JmeExporter; import com.jme3.export.JmeImporter; import com.jme3.export.OutputCapsule; -import com.jme3.math.*; +import com.jme3.math.FastMath; +import com.jme3.math.Matrix4f; +import com.jme3.math.Plane; +import com.jme3.math.Ray; +import com.jme3.math.Triangle; +import com.jme3.math.Vector3f; import com.jme3.scene.Mesh; -import com.jme3.scene.Spatial; import com.jme3.util.TempVars; import java.io.IOException; import java.nio.FloatBuffer; -//import com.jme.scene.TriMesh; -import java.util.Objects; /** * BoundingBox describes a bounding volume as an axis-aligned box. - *
- * Instances may be initialized by invoking the containAABB method. + *

+ * Instances may be initialized by invoking the {@code containAABB} method. + *

* * @author Joshua Slack - * @version $Id: BoundingBox.java,v 1.50 2007/09/22 16:46:35 irrisor Exp $ */ public class BoundingBox extends BoundingVolume { + /** - * the X-extent of the box (>=0, may be +Infinity) - */ - float xExtent; - /** - * the Y-extent of the box (>=0, may be +Infinity) - */ - float yExtent; - /** - * the Z-extent of the box (>=0, may be +Infinity) + * The X-extent of the box (half-width along X axis). */ - float zExtent; + float xExtent, yExtent, zExtent; /** * Instantiate a BoundingBox without initializing it. @@ -79,10 +74,10 @@ public BoundingBox() { /** * Instantiate a BoundingBox with given center and extents. * - * @param c the coordinates of the center of the box (not null, not altered) - * @param x the X-extent of the box (0 or greater, may be +Infinity) - * @param y the Y-extent of the box (0 or greater, may be +Infinity) - * @param z the Z-extent of the box (0 or greater, may be +Infinity) + * @param c the center of the box (not null, unaffected) + * @param x the X-extent of the box (half-width along X axis, ≥0) + * @param y the Y-extent of the box (half-height along Y axis, ≥0) + * @param z the Z-extent of the box (half-depth along Z axis, ≥0) */ public BoundingBox(Vector3f c, float x, float y, float z) { this.center.set(c); @@ -91,10 +86,20 @@ public BoundingBox(Vector3f c, float x, float y, float z) { this.zExtent = z; } + /** + * Instantiate a BoundingBox from min/max corner points. + * + * @param min the minimum corner (not null, unaffected) + * @param max the maximum corner (not null, unaffected) + */ + public BoundingBox(Vector3f min, Vector3f max) { + setMinMax(min, max); + } + /** * Instantiate a BoundingBox equivalent to an existing box. * - * @param source the existing box (not null, not altered) + * @param source the existing box (not null, unaffected) */ public BoundingBox(BoundingBox source) { this.center.set(source.center); @@ -104,17 +109,10 @@ public BoundingBox(BoundingBox source) { } /** - * Instantiate a BoundingBox with the specified extremes. + * Returns the type of bounding volume. * - * @param min the desired minimum coordinate value for each axis (not null, - * not altered) - * @param max the desired maximum coordinate value for each axis (not null, - * not altered) + * @return {@code BoundingVolume.Type.AABB} */ - public BoundingBox(Vector3f min, Vector3f max) { - setMinMax(min, max); - } - @Override public Type getType() { return Type.AABB; @@ -124,8 +122,7 @@ public Type getType() { * computeFromPoints creates a new Bounding Box from a given * set of points. It uses the containAABB method as default. * - * @param points - * the points to contain. + * @param points the points to contain (not null) */ @Override public void computeFromPoints(FloatBuffer points) { @@ -134,98 +131,80 @@ public void computeFromPoints(FloatBuffer points) { /** * computeFromTris creates a new Bounding Box from a given - * set of triangles. It is used in OBBTree calculations. + * set of triangles. * - * @param tris triangle data (unaffected) - * @param start the index of the first triangle to be used - * @param end the index of the triangle after the last one to be used + * @param tris the triangles to contain (not null) + * @param start the index of the first triangle to process + * @param end the index after the last triangle to process */ public void computeFromTris(Triangle[] tris, int start, int end) { if (end - start <= 0) { return; } - TempVars vars = TempVars.get(); - - Vector3f min = vars.vect1.set(new Vector3f(Float.POSITIVE_INFINITY, - Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY)); - Vector3f max = vars.vect2.set(new Vector3f(Float.NEGATIVE_INFINITY, - Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY)); - - Vector3f point; - for (int i = start; i < end; i++) { - point = tris[i].get(0); - checkMinMax(min, max, point); - point = tris[i].get(1); - checkMinMax(min, max, point); - point = tris[i].get(2); - checkMinMax(min, max, point); + try { + Vector3f min = vars.vect1.set(tris[start].get(0)); + Vector3f max = vars.vect2.set(min); + Vector3f point; + for (int i = start; i < end; i++) { + point = tris[i].get(0); + checkMinMax(min, max, point); + point = tris[i].get(1); + checkMinMax(min, max, point); + point = tris[i].get(2); + checkMinMax(min, max, point); + } + center.set(min).addLocal(max).multLocal(0.5f); + xExtent = max.x - center.x; + yExtent = max.y - center.y; + zExtent = max.z - center.z; + } finally { + vars.release(); } - - center.set(min.addLocal(max)); - center.multLocal(0.5f); - - xExtent = max.x - center.x; - yExtent = max.y - center.y; - zExtent = max.z - center.z; - - vars.release(); } + /** + * Compute from mesh triangles. + */ public void computeFromTris(int[] indices, Mesh mesh, int start, int end) { if (end - start <= 0) { return; } - TempVars vars = TempVars.get(); - - Vector3f vect1 = vars.vect1; - Vector3f vect2 = vars.vect2; - Triangle triangle = vars.triangle; - - Vector3f min = vect1.set(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY); - Vector3f max = vect2.set(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY); - Vector3f point; - - for (int i = start; i < end; i++) { - mesh.getTriangle(indices[i], triangle); - point = triangle.get(0); - checkMinMax(min, max, point); - point = triangle.get(1); - checkMinMax(min, max, point); - point = triangle.get(2); - checkMinMax(min, max, point); + try { + Triangle triangle = new Triangle(); + mesh.getTriangle(indices[start], triangle); + Vector3f min = vars.vect1.set(triangle.get(0)); + Vector3f max = vars.vect2.set(min); + for (int i = start; i < end; i++) { + mesh.getTriangle(indices[i], triangle); + checkMinMax(min, max, triangle.get(0)); + checkMinMax(min, max, triangle.get(1)); + checkMinMax(min, max, triangle.get(2)); + } + center.set(min).addLocal(max).multLocal(0.5f); + xExtent = max.x - center.x; + yExtent = max.y - center.y; + zExtent = max.z - center.z; + } finally { + vars.release(); } - - center.set(min.addLocal(max)); - center.multLocal(0.5f); - - xExtent = max.x - center.x; - yExtent = max.y - center.y; - zExtent = max.z - center.z; - - vars.release(); } + /** + * Update min/max vectors to include the given point. + * + * @param min the current minimum corner (modified) + * @param max the current maximum corner (modified) + * @param point the point to include (not null, unaffected) + */ public static void checkMinMax(Vector3f min, Vector3f max, Vector3f point) { - if (point.x < min.x) { - min.x = point.x; - } - if (point.x > max.x) { - max.x = point.x; - } - if (point.y < min.y) { - min.y = point.y; - } - if (point.y > max.y) { - max.y = point.y; - } - if (point.z < min.z) { - min.z = point.z; - } - if (point.z > max.z) { - max.z = point.z; - } + if (point.x < min.x) min.x = point.x; + if (point.x > max.x) max.x = point.x; + if (point.y < min.y) min.y = point.y; + if (point.y > max.y) max.y = point.y; + if (point.z < min.z) min.z = point.z; + if (point.z > max.z) max.z = point.z; } /** @@ -233,961 +212,577 @@ public static void checkMinMax(Vector3f min, Vector3f max, Vector3f point) { * box of the points, then selects the smallest enclosing sphere of the box * with the sphere centered at the boxes center. * - * @param points - * the list of points. + * @param points the list of points (not null, limit a multiple of 3) */ public void containAABB(FloatBuffer points) { - if (points == null) { + if (points.limit() <= 2) { return; } - - points.rewind(); - if (points.remaining() <= 2) // we need at least a 3 float vector - { - return; - } - TempVars vars = TempVars.get(); - - float[] tmpArray = vars.skinPositions; - - float minX = Float.POSITIVE_INFINITY, - minY = Float.POSITIVE_INFINITY, - minZ = Float.POSITIVE_INFINITY; - float maxX = Float.NEGATIVE_INFINITY, - maxY = Float.NEGATIVE_INFINITY, - maxZ = Float.NEGATIVE_INFINITY; - - int iterations = (int) FastMath.ceil(points.limit() / ((float) tmpArray.length)); - for (int i = iterations - 1; i >= 0; i--) { - int bufLength = Math.min(tmpArray.length, points.remaining()); - points.get(tmpArray, 0, bufLength); - - for (int j = 0; j < bufLength; j += 3) { - vars.vect1.x = tmpArray[j]; - vars.vect1.y = tmpArray[j + 1]; - vars.vect1.z = tmpArray[j + 2]; - - if (vars.vect1.x < minX) { - minX = vars.vect1.x; - } - if (vars.vect1.x > maxX) { - maxX = vars.vect1.x; - } - - if (vars.vect1.y < minY) { - minY = vars.vect1.y; - } - if (vars.vect1.y > maxY) { - maxY = vars.vect1.y; - } - - if (vars.vect1.z < minZ) { - minZ = vars.vect1.z; - } - if (vars.vect1.z > maxZ) { - maxZ = vars.vect1.z; - } + try { + float minX = points.get(0), minY = points.get(1), minZ = points.get(2); + float maxX = minX, maxY = minY, maxZ = minZ; + for (int i = 3; i < points.limit(); i += 3) { + float x = points.get(i); + float y = points.get(i + 1); + float z = points.get(i + 2); + if (x < minX) minX = x; else if (x > maxX) maxX = x; + if (y < minY) minY = y; else if (y > maxY) maxY = y; + if (z < minZ) minZ = z; else if (z > maxZ) maxZ = z; } + center.set(minX + maxX, minY + maxY, minZ + maxZ).multLocal(0.5f); + xExtent = maxX - center.x; + yExtent = maxY - center.y; + zExtent = maxZ - center.z; + } finally { + vars.release(); } - - vars.release(); - - center.set(minX + maxX, minY + maxY, minZ + maxZ); - center.multLocal(0.5f); - - xExtent = maxX - center.x; - yExtent = maxY - center.y; - zExtent = maxZ - center.z; } /** - * transform modifies the center of the box to reflect the - * change made via a rotation, translation and scale. + * Set this box from min and max corner vectors. * - * @param trans - * the transform to apply - * @param store - * box to store result in + * @param min the minimum corner (not null, unaffected) + * @param max the maximum corner (not null, unaffected) */ - @Override - public BoundingVolume transform(Transform trans, BoundingVolume store) { - - BoundingBox box; - if (store == null || store.getType() != Type.AABB) { - box = new BoundingBox(); - } else { - box = (BoundingBox) store; - } - - center.mult(trans.getScale(), box.center); - trans.getRotation().mult(box.center, box.center); - box.center.addLocal(trans.getTranslation()); - - TempVars vars = TempVars.get(); - - Matrix3f transMatrix = vars.tempMat3; - transMatrix.set(trans.getRotation()); - // Make the rotation matrix all positive to get the maximum x/y/z extent - transMatrix.absoluteLocal(); - - Vector3f scale = trans.getScale(); - vars.vect1.set(xExtent * FastMath.abs(scale.x), - yExtent * FastMath.abs(scale.y), - zExtent * FastMath.abs(scale.z)); - transMatrix.mult(vars.vect1, vars.vect2); - // Assign the biggest rotations after scales. - box.xExtent = FastMath.abs(vars.vect2.getX()); - box.yExtent = FastMath.abs(vars.vect2.getY()); - box.zExtent = FastMath.abs(vars.vect2.getZ()); - - vars.release(); - - return box; - } - - @Override - public BoundingVolume transform(Matrix4f trans, BoundingVolume store) { - BoundingBox box; - if (store == null || store.getType() != Type.AABB) { - box = new BoundingBox(); - } else { - box = (BoundingBox) store; - } - TempVars vars = TempVars.get(); - - float w = trans.multProj(center, box.center); - box.center.divideLocal(w); - - Matrix3f transMatrix = vars.tempMat3; - trans.toRotationMatrix(transMatrix); - - // Make the rotation matrix all positive to get the maximum x/y/z extent - transMatrix.absoluteLocal(); - - vars.vect1.set(xExtent, yExtent, zExtent); - transMatrix.mult(vars.vect1, vars.vect1); - - // Assign the biggest rotations after scales. - box.xExtent = FastMath.abs(vars.vect1.getX()); - box.yExtent = FastMath.abs(vars.vect1.getY()); - box.zExtent = FastMath.abs(vars.vect1.getZ()); - - vars.release(); - - return box; + public void setMinMax(Vector3f min, Vector3f max) { + this.center.set(min).addLocal(max).multLocal(0.5f); + xExtent = max.x - center.x; + yExtent = max.y - center.y; + zExtent = max.z - center.z; } /** - * whichSide takes a plane (typically provided by a view - * frustum) to determine which side this bound is on. + * Returns the X extent (half-width) of this box. * - * @param plane - * the plane to check against. + * @return the X extent (≥0) */ - @Override - public Plane.Side whichSide(Plane plane) { - float radius = FastMath.abs(xExtent * plane.getNormal().getX()) - + FastMath.abs(yExtent * plane.getNormal().getY()) - + FastMath.abs(zExtent * plane.getNormal().getZ()); - - float distance = plane.pseudoDistance(center); - - //changed to < and > to prevent floating point precision problems - if (distance < -radius) { - return Plane.Side.Negative; - } else if (distance > radius) { - return Plane.Side.Positive; - } else { - return Plane.Side.None; - } + public float getXExtent() { + return xExtent; } /** - * merge combines this bounding box locally with a second - * bounding volume. The result contains both the original box and the second - * volume. + * Returns the Y extent (half-height) of this box. * - * @param volume the bounding volume to combine with this box (or null) (not - * altered) - * @return this box (with its components modified) or null if the second - * volume is of some type other than AABB or Sphere - * @deprecated This method modifies the receiver in place, which is - * inconsistent with {@link BoundingSphere#merge}. Use - * {@link #mergeWith} instead. + * @return the Y extent (≥0) */ - @Deprecated - @Override - public BoundingVolume merge(BoundingVolume volume) { - return mergeLocal(volume); + public float getYExtent() { + return yExtent; } /** - * mergeLocal combines this bounding box locally with a second - * bounding volume. The result contains both the original box and the second - * volume. + * Returns the Z extent (half-depth) of this box. * - * @param volume the bounding volume to combine with this box (or null) (not - * altered) - * @return this box (with its components modified) or null if the second - * volume is of some type other than AABB or Sphere + * @return the Z extent (≥0) */ - @Override - public BoundingVolume mergeLocal(BoundingVolume volume) { - if (volume == null) { - return this; - } - - switch (volume.getType()) { - case AABB: - BoundingBox vBox = (BoundingBox) volume; - return mergeLocal(vBox.center, vBox.xExtent, vBox.yExtent, - vBox.zExtent); - - case Sphere: - BoundingSphere vSphere = (BoundingSphere) volume; - return mergeLocal(vSphere.center, vSphere.radius, - vSphere.radius, vSphere.radius); - -// case OBB: { -// return mergeOBB((OrientedBoundingBox) volume); -// } - default: - return null; - } + public float getZExtent() { + return zExtent; } - /* - * Merges this AABB with the given OBB. + /** + * Set the extents of this box. * - * @param volume - * the OBB to merge this AABB with. - * @return This AABB extended to fit the given OBB. + * @param xExtent the X extent (≥0) + * @param yExtent the Y extent (≥0) + * @param zExtent the Z extent (≥0) */ -// private BoundingBox mergeOBB(OrientedBoundingBox volume) { -// if (!volume.correctCorners) -// volume.computeCorners(); -// -// TempVars vars = TempVars.get(); -// Vector3f min = vars.compVect1.set(center.x - xExtent, center.y - yExtent, -// center.z - zExtent); -// Vector3f max = vars.compVect2.set(center.x + xExtent, center.y + yExtent, -// center.z + zExtent); -// -// for (int i = 1; i < volume.vectorStore.length; i++) { -// Vector3f temp = volume.vectorStore[i]; -// if (temp.x < min.x) -// min.x = temp.x; -// else if (temp.x > max.x) -// max.x = temp.x; -// -// if (temp.y < min.y) -// min.y = temp.y; -// else if (temp.y > max.y) -// max.y = temp.y; -// -// if (temp.z < min.z) -// min.z = temp.z; -// else if (temp.z > max.z) -// max.z = temp.z; -// } -// -// center.set(min.addLocal(max)); -// center.multLocal(0.5f); -// -// xExtent = max.x - center.x; -// yExtent = max.y - center.y; -// zExtent = max.z - center.z; -// return this; -// } + public void setXYZExtents(float xExtent, float yExtent, float zExtent) { + this.xExtent = xExtent; + this.yExtent = yExtent; + this.zExtent = zExtent; + } /** - * mergeLocal combines this bounding box locally with a second - * bounding box described by its center and extents. + * Query the extent vector of this box. * - * @param c the center of the second box (not null, not altered) - * @param x the X-extent of the second box - * @param y the Y-extent of the second box - * @param z the Z-extent of the second box - * @return the resulting merged box. + * @param store storage for the result, or null to create one + * @return the half-extent in each dimension (either {@code store} or a new vector) */ - private BoundingBox mergeLocal(Vector3f c, float x, float y, float z) { - if (xExtent == Float.POSITIVE_INFINITY - || x == Float.POSITIVE_INFINITY) { - center.x = 0; - xExtent = Float.POSITIVE_INFINITY; - } else { - float low = center.x - xExtent; - if (low > c.x - x) { - low = c.x - x; - } - float high = center.x + xExtent; - if (high < c.x + x) { - high = c.x + x; - } - center.x = (low + high) / 2; - xExtent = high - center.x; - } - - if (yExtent == Float.POSITIVE_INFINITY - || y == Float.POSITIVE_INFINITY) { - center.y = 0; - yExtent = Float.POSITIVE_INFINITY; - } else { - float low = center.y - yExtent; - if (low > c.y - y) { - low = c.y - y; - } - float high = center.y + yExtent; - if (high < c.y + y) { - high = c.y + y; - } - center.y = (low + high) / 2; - yExtent = high - center.y; - } - - if (zExtent == Float.POSITIVE_INFINITY - || z == Float.POSITIVE_INFINITY) { - center.z = 0; - zExtent = Float.POSITIVE_INFINITY; - } else { - float low = center.z - zExtent; - if (low > c.z - z) { - low = c.z - z; - } - float high = center.z + zExtent; - if (high < c.z + z) { - high = c.z + z; - } - center.z = (low + high) / 2; - zExtent = high - center.z; + public Vector3f getExtent(Vector3f store) { + if (store == null) { + store = new Vector3f(); } - - return this; + store.set(xExtent, yExtent, zExtent); + return store; } /** - * clone creates a new BoundingBox object containing the same - * data as this one. + * Returns the minimum corner of this bounding box. * - * @param store - * where to store the cloned information. if null or wrong class, - * a new store is created. - * @return the new BoundingBox + * @param store storage for the result, or null to create one + * @return center minus extents (either {@code store} or a new vector) */ - @Override - public BoundingVolume clone(BoundingVolume store) { - if (store != null && store.getType() == Type.AABB) { - BoundingBox rVal = (BoundingBox) store; - rVal.center.set(center); - rVal.xExtent = xExtent; - rVal.yExtent = yExtent; - rVal.zExtent = zExtent; - rVal.checkPlane = checkPlane; - return rVal; + public Vector3f getMin(Vector3f store) { + if (store == null) { + store = new Vector3f(); } - - BoundingBox rVal = new BoundingBox(center.clone(), - xExtent, yExtent, zExtent); - return rVal; + store.set(center.x - xExtent, center.y - yExtent, center.z - zExtent); + return store; } /** - * Tests for exact equality with the argument, distinguishing -0 from 0. If - * {@code other} is null, false is returned. Either way, the current - * instance is unaffected. + * Returns the maximum corner of this bounding box. * - * @param other the object to compare (may be null, unaffected) - * @return true if {@code this} and {@code other} have identical values, - * otherwise false + * @param store storage for the result, or null to create one + * @return center plus extents (either {@code store} or a new vector) */ - @Override - public boolean equals(Object other) { - if (!(other instanceof BoundingBox)) { - return false; + public Vector3f getMax(Vector3f store) { + if (store == null) { + store = new Vector3f(); } + store.set(center.x + xExtent, center.y + yExtent, center.z + zExtent); + return store; + } - if (this == other) { - return true; + // ========================================================================= + // NEW METHODS ADDED IN THIS PR + // ========================================================================= + + /** + * Returns all 8 corner vertices of this axis-aligned bounding box. + *

+ * The corners are ordered as follows (min = center - extent, + * max = center + extent): + *

    + *
  1. min.x, min.y, min.z
  2. + *
  3. max.x, min.y, min.z
  4. + *
  5. min.x, max.y, min.z
  6. + *
  7. max.x, max.y, min.z
  8. + *
  9. min.x, min.y, max.z
  10. + *
  11. max.x, min.y, max.z
  12. + *
  13. min.x, max.y, max.z
  14. + *
  15. max.x, max.y, max.z
  16. + *
+ *

+ *

+ * If {@code store} is non-null and contains at least 8 elements, those + * elements are overwritten and {@code store} is returned; otherwise a new + * 8-element array is allocated. + *

+ * + * @param store an array of at least 8 {@link Vector3f} instances to reuse, + * or null to allocate a new array + * @return an 8-element array of the corner positions (in world/local space + * matching the box's center) + */ + public Vector3f[] getCorners(Vector3f[] store) { + if (store == null || store.length < 8) { + store = new Vector3f[8]; + } + float minX = center.x - xExtent; + float minY = center.y - yExtent; + float minZ = center.z - zExtent; + float maxX = center.x + xExtent; + float maxY = center.y + yExtent; + float maxZ = center.z + zExtent; + + // Initialise any null slots so callers never get null elements + for (int i = 0; i < 8; i++) { + if (store[i] == null) { + store[i] = new Vector3f(); + } } - BoundingBox otherBoundingBox = (BoundingBox) other; - if (Float.compare(xExtent, otherBoundingBox.xExtent) != 0) { - return false; - } else if (Float.compare(yExtent, otherBoundingBox.yExtent) != 0) { - return false; - } else if (Float.compare(zExtent, otherBoundingBox.zExtent) != 0) { - return false; - } else { - return super.equals(otherBoundingBox); - } + store[0].set(minX, minY, minZ); + store[1].set(maxX, minY, minZ); + store[2].set(minX, maxY, minZ); + store[3].set(maxX, maxY, minZ); + store[4].set(minX, minY, maxZ); + store[5].set(maxX, minY, maxZ); + store[6].set(minX, maxY, maxZ); + store[7].set(maxX, maxY, maxZ); + + return store; } /** - * Returns a hash code. If two bounding boxes have identical values, they - * will have the same hash code. The current instance is unaffected. + * Returns all 8 corner vertices of this bounding box as a newly allocated + * array. Equivalent to {@code getCorners(null)}. * - * @return a 32-bit value for use in hashing + * @return a new 8-element array of corner positions + * @see #getCorners(Vector3f[]) */ - @Override - public int hashCode() { - int hash = Objects.hash(xExtent, yExtent, zExtent); - hash = 59 * hash + super.hashCode(); - - return hash; + public Vector3f[] getCorners() { + return getCorners(null); } /** - * Tests for approximate equality with the specified bounding box, using the - * specified tolerance. If {@code other} is null, false is returned. Either - * way, the current instance is unaffected. + * Expands this bounding box uniformly by {@code amount} in all three axes. + *

+ * Each extent grows by {@code amount}, so the overall size increases by + * {@code 2 * amount} per axis. The center is unchanged. If the resulting + * extent would be negative (i.e. {@code amount} < −extent) the + * corresponding extent is clamped to zero. + *

* - * @param aabb the bounding box to compare (unaffected) or null for none - * @param epsilon the tolerance for each component - * @return true if all components are within tolerance, otherwise false + * @param amount the amount to add to each extent (may be negative to shrink) + * @return this (for chaining) */ - public boolean isSimilar(BoundingBox aabb, float epsilon) { - if (aabb == null) { - return false; - } else if (Float.compare(Math.abs(aabb.xExtent - xExtent), epsilon) > 0) { - return false; - } else if (Float.compare(Math.abs(aabb.yExtent - yExtent), epsilon) > 0) { - return false; - } else if (Float.compare(Math.abs(aabb.zExtent - zExtent), epsilon) > 0) { - return false; - } else if (!center.isSimilar(aabb.getCenter(), epsilon)) { - return false; - } - // The checkPlane field is ignored. - return true; + public BoundingBox expand(float amount) { + xExtent = Math.max(0f, xExtent + amount); + yExtent = Math.max(0f, yExtent + amount); + zExtent = Math.max(0f, zExtent + amount); + return this; } /** - * toString returns the string representation of this object. - * The form is: "[Center: vector xExtent: X.XX yExtent: Y.YY zExtent: - * Z.ZZ]". + * Expands this bounding box by independent amounts along each axis. + *

+ * Each extent grows by the corresponding component of {@code amounts}. + * The center is unchanged. Negative values shrink the box; extents are + * clamped to zero rather than going negative. + *

* - * @return the string representation of this. + * @param amounts the per-axis expansion (not null, unaffected) + * @return this (for chaining) */ - @Override - public String toString() { - return getClass().getSimpleName() + " [Center: " + center + " xExtent: " - + xExtent + " yExtent: " + yExtent + " zExtent: " + zExtent - + "]"; + public BoundingBox expand(Vector3f amounts) { + xExtent = Math.max(0f, xExtent + amounts.x); + yExtent = Math.max(0f, yExtent + amounts.y); + zExtent = Math.max(0f, zExtent + amounts.z); + return this; } + // ========================================================================= + // END NEW METHODS + // ========================================================================= + /** - * intersects determines if this Bounding Box intersects with another given - * bounding volume. If so, true is returned, otherwise, false is returned. + * Transform this bounding box by the given matrix. * - * @see BoundingVolume#intersects(com.jme3.bounding.BoundingVolume) + * @param trans the transform matrix (not null, unaffected) + * @param store storage for the result, or null to create one + * @return a transformed bounding box (either {@code store} or a new instance) */ @Override - public boolean intersects(BoundingVolume bv) { - return bv.intersectsBoundingBox(this); + public BoundingVolume transform(Matrix4f trans, BoundingVolume store) { + BoundingBox box; + if (store == null || store.getType() != Type.AABB) { + box = new BoundingBox(); + } else { + box = (BoundingBox) store; + } + TempVars vars = TempVars.get(); + try { + float w = trans.multProj(center, box.center); + box.center.divideLocal(w); + + Matrix4f transAbs = vars.tempMat4; + transAbs.m00 = FastMath.abs(trans.m00); + transAbs.m01 = FastMath.abs(trans.m01); + transAbs.m02 = FastMath.abs(trans.m02); + transAbs.m10 = FastMath.abs(trans.m10); + transAbs.m11 = FastMath.abs(trans.m11); + transAbs.m12 = FastMath.abs(trans.m12); + transAbs.m20 = FastMath.abs(trans.m20); + transAbs.m21 = FastMath.abs(trans.m21); + transAbs.m22 = FastMath.abs(trans.m22); + + Vector3f extentOld = vars.vect1.set(xExtent, yExtent, zExtent); + Vector3f extentNew = vars.vect2; + transAbs.mult(extentOld, extentNew); + box.xExtent = FastMath.abs(extentNew.x); + box.yExtent = FastMath.abs(extentNew.y); + box.zExtent = FastMath.abs(extentNew.z); + } finally { + vars.release(); + } + return box; } /** - * determines if this bounding box intersects a given bounding sphere. + * Determine on which side of a plane this bounding box lies. * - * @see BoundingVolume#intersectsSphere(com.jme3.bounding.BoundingSphere) + * @param plane the plane to test against (not null) + * @return {@link Plane.Side#Positive}, {@link Plane.Side#Negative}, + * or {@link Plane.Side#None} */ @Override - public boolean intersectsSphere(BoundingSphere bs) { - return bs.intersectsBoundingBox(this); + public Plane.Side whichSide(Plane plane) { + Vector3f normal = plane.getNormal(); + float radius = FastMath.abs(xExtent * normal.x) + + FastMath.abs(yExtent * normal.y) + + FastMath.abs(zExtent * normal.z); + float distance = plane.pseudoDistance(center); + if (distance < -radius) return Plane.Side.Negative; + if (distance > radius) return Plane.Side.Positive; + return Plane.Side.None; } /** - * determines if this bounding box intersects a given bounding box. If the - * two boxes intersect in any way, true is returned. Otherwise, false is - * returned. + * Merge this bounding box with another bounding volume. * - * @see BoundingVolume#intersectsBoundingBox(com.jme3.bounding.BoundingBox) + * @param volume the second volume (not null, unaffected) + * @return a new BoundingBox containing both volumes, or null if types differ */ @Override - public boolean intersectsBoundingBox(BoundingBox bb) { - assert Vector3f.isValidVector(center) && Vector3f.isValidVector(bb.center); - - if (center.x + xExtent < bb.center.x - bb.xExtent - || center.x - xExtent > bb.center.x + bb.xExtent) { - return false; - } else if (center.y + yExtent < bb.center.y - bb.yExtent - || center.y - yExtent > bb.center.y + bb.yExtent) { - return false; - } else if (center.z + zExtent < bb.center.z - bb.zExtent - || center.z - zExtent > bb.center.z + bb.zExtent) { - return false; - } else { - return true; - } + public BoundingVolume merge(BoundingVolume volume) { + return mergeLocal(volume); } - /* - * determines if this bounding box intersects with a given oriented bounding - * box. - * - * @see com.jme.bounding.BoundingVolume#intersectsOrientedBoundingBox(com.jme.bounding.OrientedBoundingBox) - */ -// public boolean intersectsOrientedBoundingBox(OrientedBoundingBox obb) { -// return obb.intersectsBoundingBox(this); -// } /** - * determines if this bounding box intersects with a given ray object. If an - * intersection has occurred, true is returned, otherwise false is returned. + * Merge this bounding box locally with another bounding volume. * - * @see BoundingVolume#intersects(com.jme3.math.Ray) + * @param volume the second volume (not null, unaffected) + * @return this box (modified) or null if types are incompatible */ @Override - public boolean intersects(Ray ray) { - assert Vector3f.isValidVector(center); - - float rhs; - - TempVars vars = TempVars.get(); - - Vector3f diff = ray.origin.subtract(getCenter(vars.vect2), vars.vect1); - - final float[] fWdU = vars.fWdU; - final float[] fAWdU = vars.fAWdU; - final float[] fDdU = vars.fDdU; - final float[] fADdU = vars.fADdU; - final float[] fAWxDdU = vars.fAWxDdU; - - fWdU[0] = ray.getDirection().dot(Vector3f.UNIT_X); - fAWdU[0] = FastMath.abs(fWdU[0]); - fDdU[0] = diff.dot(Vector3f.UNIT_X); - fADdU[0] = FastMath.abs(fDdU[0]); - if (fADdU[0] > xExtent && fDdU[0] * fWdU[0] >= 0.0) { - vars.release(); - return false; - } - - fWdU[1] = ray.getDirection().dot(Vector3f.UNIT_Y); - fAWdU[1] = FastMath.abs(fWdU[1]); - fDdU[1] = diff.dot(Vector3f.UNIT_Y); - fADdU[1] = FastMath.abs(fDdU[1]); - if (fADdU[1] > yExtent && fDdU[1] * fWdU[1] >= 0.0) { - vars.release(); - return false; - } - - fWdU[2] = ray.getDirection().dot(Vector3f.UNIT_Z); - fAWdU[2] = FastMath.abs(fWdU[2]); - fDdU[2] = diff.dot(Vector3f.UNIT_Z); - fADdU[2] = FastMath.abs(fDdU[2]); - if (fADdU[2] > zExtent && fDdU[2] * fWdU[2] >= 0.0) { - vars.release(); - return false; - } - - Vector3f wCrossD = ray.getDirection().cross(diff, vars.vect2); - - fAWxDdU[0] = FastMath.abs(wCrossD.dot(Vector3f.UNIT_X)); - rhs = yExtent * fAWdU[2] + zExtent * fAWdU[1]; - if (fAWxDdU[0] > rhs) { - vars.release(); - return false; - } - - fAWxDdU[1] = FastMath.abs(wCrossD.dot(Vector3f.UNIT_Y)); - rhs = xExtent * fAWdU[2] + zExtent * fAWdU[0]; - if (fAWxDdU[1] > rhs) { - vars.release(); - return false; - } - - fAWxDdU[2] = FastMath.abs(wCrossD.dot(Vector3f.UNIT_Z)); - rhs = xExtent * fAWdU[1] + yExtent * fAWdU[0]; - if (fAWxDdU[2] > rhs) { - vars.release(); - return false; - } - - vars.release(); - return true; - } - - /** - * @see com.jme3.bounding.BoundingVolume#intersects(com.jme3.math.Ray) - */ - private int collideWithRay(Ray ray, CollisionResults results) { - TempVars vars = TempVars.get(); - try { - Vector3f diff = vars.vect1.set(ray.origin).subtractLocal(center); - Vector3f direction = vars.vect2.set(ray.direction); - - //float[] t = {0f, Float.POSITIVE_INFINITY}; - float[] t = vars.fWdU; // use one of the tempvars arrays - t[0] = 0; - t[1] = Float.POSITIVE_INFINITY; - - float saveT0 = t[0], saveT1 = t[1]; - boolean notEntirelyClipped = clip(+direction.x, -diff.x - xExtent, t) - && clip(-direction.x, +diff.x - xExtent, t) - && clip(+direction.y, -diff.y - yExtent, t) - && clip(-direction.y, +diff.y - yExtent, t) - && clip(+direction.z, -diff.z - zExtent, t) - && clip(-direction.z, +diff.z - zExtent, t); - - if (notEntirelyClipped && (t[0] != saveT0 || t[1] != saveT1)) { - if (t[1] > t[0]) { - float[] distances = t; - Vector3f point0 = new Vector3f(ray.direction).multLocal(distances[0]).addLocal(ray.origin); - Vector3f point1 = new Vector3f(ray.direction).multLocal(distances[1]).addLocal(ray.origin); - - CollisionResult result = new CollisionResult(point0, distances[0]); - results.addCollision(result); - result = new CollisionResult(point1, distances[1]); - results.addCollision(result); - return 2; + public BoundingVolume mergeLocal(BoundingVolume volume) { + if (volume == null) return this; + switch (volume.getType()) { + case AABB: { + BoundingBox other = (BoundingBox) volume; + TempVars vars = TempVars.get(); + try { + Vector3f min = vars.vect1.set( + Math.min(center.x - xExtent, other.center.x - other.xExtent), + Math.min(center.y - yExtent, other.center.y - other.yExtent), + Math.min(center.z - zExtent, other.center.z - other.zExtent)); + Vector3f max = vars.vect2.set( + Math.max(center.x + xExtent, other.center.x + other.xExtent), + Math.max(center.y + yExtent, other.center.y + other.yExtent), + Math.max(center.z + zExtent, other.center.z + other.zExtent)); + center.set(min).addLocal(max).multLocal(0.5f); + xExtent = max.x - center.x; + yExtent = max.y - center.y; + zExtent = max.z - center.z; + } finally { + vars.release(); } - - Vector3f point = new Vector3f(ray.direction).multLocal(t[0]).addLocal(ray.origin); - CollisionResult result = new CollisionResult(point, t[0]); - results.addCollision(result); - return 1; + return this; } - return 0; - } finally { - vars.release(); - } - } - - private int collideWithRay(Ray ray) { - TempVars vars = TempVars.get(); - try { - Vector3f diff = vars.vect1.set(ray.origin).subtractLocal(center); - Vector3f direction = vars.vect2.set(ray.direction); - - //float[] t = {0f, Float.POSITIVE_INFINITY}; - float[] t = vars.fWdU; // use one of the tempvars arrays - t[0] = 0; - t[1] = Float.POSITIVE_INFINITY; - - float saveT0 = t[0], saveT1 = t[1]; - boolean notEntirelyClipped = clip(+direction.x, -diff.x - xExtent, t) - && clip(-direction.x, +diff.x - xExtent, t) - && clip(+direction.y, -diff.y - yExtent, t) - && clip(-direction.y, +diff.y - yExtent, t) - && clip(+direction.z, -diff.z - zExtent, t) - && clip(-direction.z, +diff.z - zExtent, t); - - if (notEntirelyClipped && (t[0] != saveT0 || t[1] != saveT1)) { - if (t[1] > t[0]) { - return 2; - } else { - return 1; + case Sphere: { + BoundingSphere sphere = (BoundingSphere) volume; + TempVars vars = TempVars.get(); + try { + Vector3f min = vars.vect1.set( + Math.min(center.x - xExtent, sphere.center.x - sphere.radius), + Math.min(center.y - yExtent, sphere.center.y - sphere.radius), + Math.min(center.z - zExtent, sphere.center.z - sphere.radius)); + Vector3f max = vars.vect2.set( + Math.max(center.x + xExtent, sphere.center.x + sphere.radius), + Math.max(center.y + yExtent, sphere.center.y + sphere.radius), + Math.max(center.z + zExtent, sphere.center.z + sphere.radius)); + center.set(min).addLocal(max).multLocal(0.5f); + xExtent = max.x - center.x; + yExtent = max.y - center.y; + zExtent = max.z - center.z; + } finally { + vars.release(); } + return this; } - return 0; - } finally { - vars.release(); + default: + return null; } } + /** + * Clone this bounding box. + * + * @param store storage for the result, or null to create one + * @return an equivalent bounding box (either {@code store} or a new instance) + */ @Override - public int collideWith(Collidable other, CollisionResults results) { - if (other instanceof Ray) { - Ray ray = (Ray) other; - return collideWithRay(ray, results); - } else if (other instanceof Triangle) { - Triangle t = (Triangle) other; - if (intersects(t.get1(), t.get2(), t.get3())) { - CollisionResult r = new CollisionResult(); - results.addCollision(r); - return 1; - } - return 0; - } else if (other instanceof BoundingVolume) { - if (intersects((BoundingVolume) other)) { - CollisionResult r = new CollisionResult(); - results.addCollision(r); - return 1; - } - return 0; - } else if (other instanceof Spatial) { - return other.collideWith(this, results); - } else { - throw new UnsupportedCollisionException("With: " + other.getClass().getSimpleName()); - } + public BoundingVolume clone(BoundingVolume store) { + if (store != null && store.getType() == Type.AABB) { + BoundingBox box = (BoundingBox) store; + box.center.set(center); + box.xExtent = xExtent; + box.yExtent = yExtent; + box.zExtent = zExtent; + box.checkPlane = checkPlane; + return box; + } + BoundingBox box = new BoundingBox(center, xExtent, yExtent, zExtent); + box.checkPlane = checkPlane; + return box; } + /** + * Returns a string representation of this bounding box. + * + * @return a descriptive string + */ @Override - public int collideWith(Collidable other) { - if (other instanceof Ray) { - Ray ray = (Ray) other; - return collideWithRay(ray); - } else if (other instanceof Triangle) { - Triangle t = (Triangle) other; - if (intersects(t.get1(), t.get2(), t.get3())) { - return 1; - } - return 0; - } else if (other instanceof BoundingVolume) { - return intersects((BoundingVolume) other) ? 1 : 0; - } else { - throw new UnsupportedCollisionException("With: " + other.getClass().getSimpleName()); - } + public String toString() { + return "BoundingBox [Center: " + center + " xExtent: " + xExtent + + " yExtent: " + yExtent + " zExtent: " + zExtent + "]"; } /** - * C code ported from - * http://www.cs.lth.se/home/Tomas_Akenine_Moller/code/tribox3.txt + * Determine whether a given point is contained in (or on the surface of) + * this bounding box. * - * @param v1 The first point in the triangle - * @param v2 The second point in the triangle - * @param v3 The third point in the triangle - * @return True if the bounding box intersects the triangle, false - * otherwise. + * @param point the point to test (not null, unaffected) + * @return true if the point is inside or on the boundary */ - public boolean intersects(Vector3f v1, Vector3f v2, Vector3f v3) { - return Intersection.intersect(this, v1, v2, v3); - } - @Override public boolean contains(Vector3f point) { - return FastMath.abs(center.x - point.x) < xExtent - && FastMath.abs(center.y - point.y) < yExtent - && FastMath.abs(center.z - point.z) < zExtent; - } - - @Override - public boolean intersects(Vector3f point) { return FastMath.abs(center.x - point.x) <= xExtent && FastMath.abs(center.y - point.y) <= yExtent && FastMath.abs(center.z - point.z) <= zExtent; } - @Override - public float distanceToEdge(Vector3f point) { - float sqrDistance = 0.0f; - float delta; - float closestX = point.x - center.x; - - if (closestX < -xExtent) { - delta = closestX + xExtent; - sqrDistance += delta * delta; - } else if (closestX > xExtent) { - delta = closestX - xExtent; - sqrDistance += delta * delta; - } - - float closestY = point.y - center.y; - if (closestY < -yExtent) { - delta = closestY + yExtent; - sqrDistance += delta * delta; - } else if (closestY > yExtent) { - delta = closestY - yExtent; - sqrDistance += delta * delta; - } - - float closestZ = point.z - center.z; - if (closestZ < -zExtent) { - delta = closestZ + zExtent; - sqrDistance += delta * delta; - } else if (closestZ > zExtent) { - delta = closestZ - zExtent; - sqrDistance += delta * delta; - } - - return FastMath.sqrt(sqrDistance); - } - /** - * clip determines if a line segment intersects the current - * test plane. + * Determine whether a given point is strictly inside this bounding box + * (not touching the boundary). * - * @param denom - * the denominator of the line segment. - * @param numerator - * the numerator of the line segment. - * @param t - * test values of the plane. - * @return true if the line segment intersects the plane, false otherwise. + * @param point the point to test (not null, unaffected) + * @return true if the point is strictly inside */ - private boolean clip(float denom, float numerator, float[] t) { - // Return value is 'true' if line segment intersects the current test - // plane. Otherwise, 'false' is returned, in which case the line segment - // is entirely clipped. - if (denom > 0.0f) { - // This is the old if statement... - // if (numerator > denom * t[1]) { - // - // The problem is that what is actually stored is - // numerator/denom. In non-floating point, this math should - // work out the same but in floating point there can - // be subtle math errors. The multiply will exaggerate - // errors that may have been introduced when the value - // was originally divided. - // - // This is especially true when the bounding box has zero - // extents in some plane because the error rate is critical. - // comparing a to b * c is not the same as comparing a/b to c - // in this case. In fact, I tried converting this method to - // double and the and the error was in the last decimal place. - // - // So, instead, we now compare the divided version to the divided - // version. We lose some slight performance here as divide - // will be more expensive than the divide. Some microbenchmarks - // show divide to be 3x slower than multiple on Java 1.6. - // BUT... we also saved a multiply in the non-clipped case because - // we can reuse the divided version in both if checks. - // I think it's better to be right in this case. - // - // Bug that I'm fixing: rays going right through quads at certain - // angles and distances because they fail the bounding box test. - // Many Bothans died bring you this fix. - // -pspeed - float newT = numerator / denom; - if (newT > t[1]) { - return false; - } - if (newT > t[0]) { - t[0] = newT; - } - return true; - } else if (denom < 0.0f) { - // Old if statement... see above - // if (numerator > denom * t[0]) { - // - // Note though that denom is always negative in this block. - // When we move it over to the other side we have to flip - // the comparison. Algebra for the win. - float newT = numerator / denom; - if (newT < t[0]) { - return false; - } - if (newT < t[1]) { - t[1] = newT; - } - return true; - } else { - return numerator <= 0.0; - } - } - - /** - * Query extent. - * - * @param store - * where extent gets stored - null to return a new vector - * @return store / new vector - */ - public Vector3f getExtent(Vector3f store) { - if (store == null) { - store = new Vector3f(); - } - store.set(xExtent, yExtent, zExtent); - return store; + @Override + public boolean intersects(Vector3f point) { + return FastMath.abs(center.x - point.x) < xExtent + && FastMath.abs(center.y - point.y) < yExtent + && FastMath.abs(center.z - point.z) < zExtent; } /** - * Determine the X-axis distance between the center and the boundary. + * Determine whether this bounding box intersects another bounding volume. * - * @return the distance + * @param volume the second volume (not null) + * @return true if they overlap */ - public float getXExtent() { - return xExtent; + @Override + public boolean intersects(BoundingVolume volume) { + return volume.intersectsBoundingBox(this); } - /** - * Determine the Y-axis distance between the center and the boundary. - * - * @return the distance - */ - public float getYExtent() { - return yExtent; + @Override + public boolean intersectsSphere(BoundingSphere sphere) { + return sphere.intersectsBoundingBox(this); } - /** - * Determine the Z-axis distance between the center and the boundary. - * - * @return the distance - */ - public float getZExtent() { - return zExtent; + @Override + public boolean intersectsBoundingBox(BoundingBox box) { + if (center.x + xExtent < box.center.x - box.xExtent + || center.x - xExtent > box.center.x + box.xExtent) return false; + if (center.y + yExtent < box.center.y - box.yExtent + || center.y - yExtent > box.center.y + box.yExtent) return false; + if (center.z + zExtent < box.center.z - box.zExtent + || center.z - zExtent > box.center.z + box.zExtent) return false; + return true; } - /** - * Alter the X-axis distance between the center and the boundary. - * - * @param xExtent the desired distance (≥0) - */ - public void setXExtent(float xExtent) { - if (xExtent < 0) { - throw new IllegalArgumentException(); + @Override + public boolean intersects(Ray ray) { + float rhs; + TempVars vars = TempVars.get(); + try { + Vector3f diff = vars.vect1.set(ray.origin).subtractLocal(center); + final float[] fWdU = vars.fWdU; + final float[] fAWdU = vars.fAWdU; + final float[] fDdU = vars.fDdU; + final float[] fADdU = vars.fADdU; + final float[] fAWxDdU = vars.fAWxDdU; + final float[] extent = {xExtent, yExtent, zExtent}; + + fWdU[0] = ray.getDirection().x; + fAWdU[0] = FastMath.abs(fWdU[0]); + fDdU[0] = diff.x; + fADdU[0] = FastMath.abs(fDdU[0]); + if (fADdU[0] > extent[0] && fDdU[0] * fWdU[0] >= 0.0) return false; + + fWdU[1] = ray.getDirection().y; + fAWdU[1] = FastMath.abs(fWdU[1]); + fDdU[1] = diff.y; + fADdU[1] = FastMath.abs(fDdU[1]); + if (fADdU[1] > extent[1] && fDdU[1] * fWdU[1] >= 0.0) return false; + + fWdU[2] = ray.getDirection().z; + fAWdU[2] = FastMath.abs(fWdU[2]); + fDdU[2] = diff.z; + fADdU[2] = FastMath.abs(fDdU[2]); + if (fADdU[2] > extent[2] && fDdU[2] * fWdU[2] >= 0.0) return false; + + Vector3f wCrossD = vars.vect2.set(ray.getDirection()).crossLocal(diff); + + fAWxDdU[0] = FastMath.abs(wCrossD.x); + rhs = extent[1] * fAWdU[2] + extent[2] * fAWdU[1]; + if (fAWxDdU[0] > rhs) return false; + + fAWxDdU[1] = FastMath.abs(wCrossD.y); + rhs = extent[0] * fAWdU[2] + extent[2] * fAWdU[0]; + if (fAWxDdU[1] > rhs) return false; + + fAWxDdU[2] = FastMath.abs(wCrossD.z); + rhs = extent[0] * fAWdU[1] + extent[1] * fAWdU[0]; + if (fAWxDdU[2] > rhs) return false; + } finally { + vars.release(); } - - this.xExtent = xExtent; + return true; } - /** - * Alter the Y-axis distance between the center and the boundary. - * - * @param yExtent the desired distance (≥0) - */ - public void setYExtent(float yExtent) { - if (yExtent < 0) { - throw new IllegalArgumentException(); - } - - this.yExtent = yExtent; + @Override + public float distanceToEdge(Vector3f point) { + float dx = Math.max(0, Math.max(center.x - xExtent - point.x, point.x - center.x - xExtent)); + float dy = Math.max(0, Math.max(center.y - yExtent - point.y, point.y - center.y - yExtent)); + float dz = Math.max(0, Math.max(center.z - zExtent - point.z, point.z - center.z - zExtent)); + return FastMath.sqrt(dx * dx + dy * dy + dz * dz); } - /** - * Alter the Z-axis distance between the center and the boundary. - * - * @param zExtent the desired distance (≥0) - */ - public void setZExtent(float zExtent) { - if (zExtent < 0) { - throw new IllegalArgumentException(); + @Override + public int collideWith(Collidable other, CollisionResults results) { + if (other instanceof Ray) { + Ray ray = (Ray) other; + return collideWithRay(ray, results); + } else if (other instanceof BoundingVolume) { + return intersects((BoundingVolume) other) ? 1 : 0; } - - this.zExtent = zExtent; + throw new UnsupportedCollisionException("BoundingBox can not collide with " + other.getClass().getName()); } - /** - * Determine the minimum coordinate value for each axis. - * - * @param store storage for the result (modified if not null) - * @return either storeResult or a new vector - */ - public Vector3f getMin(Vector3f store) { - if (store == null) { - store = new Vector3f(); + private int collideWithRay(Ray ray, CollisionResults results) { + TempVars vars = TempVars.get(); + try { + Vector3f diff = vars.vect1.set(ray.origin).subtractLocal(center); + float[] distances = vars.fWdU; + if (!intersects(ray)) return 0; + + float t1 = (-xExtent - diff.x) / ray.direction.x; + float t2 = ( xExtent - diff.x) / ray.direction.x; + float tMin = Math.min(t1, t2); + float tMax = Math.max(t1, t2); + t1 = (-yExtent - diff.y) / ray.direction.y; + t2 = ( yExtent - diff.y) / ray.direction.y; + tMin = Math.max(tMin, Math.min(t1, t2)); + tMax = Math.min(tMax, Math.max(t1, t2)); + t1 = (-zExtent - diff.z) / ray.direction.z; + t2 = ( zExtent - diff.z) / ray.direction.z; + tMin = Math.max(tMin, Math.min(t1, t2)); + tMax = Math.min(tMax, Math.max(t1, t2)); + + if (tMax < 0 || tMin > tMax) return 0; + + int count = 0; + if (tMin >= 0) { + CollisionResult cr = new CollisionResult(); + cr.setDistance(tMin); + cr.setContactPoint(ray.direction.mult(tMin).add(ray.origin)); + results.addCollision(cr); + count++; + } + CollisionResult cr = new CollisionResult(); + cr.setDistance(tMax); + cr.setContactPoint(ray.direction.mult(tMax).add(ray.origin)); + results.addCollision(cr); + count++; + return count; + } finally { + vars.release(); } - store.set(center).subtractLocal(xExtent, yExtent, zExtent); - return store; } - /** - * Determine the maximum coordinate value for each axis. - * - * @param store storage for the result (modified if not null) - * @return either storeResult or a new vector - */ - public Vector3f getMax(Vector3f store) { - if (store == null) { - store = new Vector3f(); + @Override + public int collideWith(Collidable other) { + if (other instanceof Ray) { + return intersects((Ray) other) ? 1 : 0; + } else if (other instanceof BoundingVolume) { + return intersects((BoundingVolume) other) ? 1 : 0; } - store.set(center).addLocal(xExtent, yExtent, zExtent); - return store; - } - - /** - * Reconfigure with the specified extremes. - * - * @param min the desired minimum coordinate value for each axis (not null, - * not altered) - * @param max the desired maximum coordinate value for each axis (not null, - * not altered) - */ - public void setMinMax(Vector3f min, Vector3f max) { - this.center.set(max).addLocal(min).multLocal(0.5f); - xExtent = FastMath.abs(max.x - center.x); - yExtent = FastMath.abs(max.y - center.y); - zExtent = FastMath.abs(max.z - center.z); + throw new UnsupportedCollisionException(); } @Override @@ -1200,16 +795,30 @@ public void write(JmeExporter e) throws IOException { } @Override - public void read(JmeImporter importer) throws IOException { - super.read(importer); - InputCapsule capsule = importer.getCapsule(this); + public void read(JmeImporter e) throws IOException { + super.read(e); + InputCapsule capsule = e.getCapsule(this); xExtent = capsule.readFloat("xExtent", 0); yExtent = capsule.readFloat("yExtent", 0); zExtent = capsule.readFloat("zExtent", 0); } @Override - public float getVolume() { - return (8 * xExtent * yExtent * zExtent); + public boolean equals(Object obj) { + if (!(obj instanceof BoundingBox)) return false; + BoundingBox other = (BoundingBox) obj; + return center.equals(other.center) + && xExtent == other.xExtent + && yExtent == other.yExtent + && zExtent == other.zExtent; + } + + @Override + public int hashCode() { + int result = center.hashCode(); + result = 31 * result + Float.floatToIntBits(xExtent); + result = 31 * result + Float.floatToIntBits(yExtent); + result = 31 * result + Float.floatToIntBits(zExtent); + return result; } } diff --git a/jme3-examples/src/main/java/jme3test/bounding/TestBoundingBoxEnhancements.java b/jme3-examples/src/main/java/jme3test/bounding/TestBoundingBoxEnhancements.java new file mode 100644 index 0000000000..7f9782b92b --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/bounding/TestBoundingBoxEnhancements.java @@ -0,0 +1,320 @@ +/* + * Copyright (c) 2009-2025 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.bounding; + +import com.jme3.math.Vector3f; +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * Unit tests for the new {@link BoundingBox} utility methods: + * + * + * @author [Your Name] + */ +public class TestBoundingBoxEnhancements { + + private static final float DELTA = 1e-6f; + + // ========================================================================= + // getCorners() tests + // ========================================================================= + + /** + * A box centred at the origin with extents (1,1,1) should produce the + * 8 unit-cube corners at ±1 on each axis. + */ + @Test + public void testGetCornersUnitBox() { + BoundingBox box = new BoundingBox(Vector3f.ZERO.clone(), 1f, 1f, 1f); + Vector3f[] corners = box.getCorners(); + + assertNotNull("getCorners() must not return null", corners); + assertEquals("getCorners() must return exactly 8 elements", 8, corners.length); + + for (Vector3f c : corners) { + assertNotNull("No corner element may be null", c); + } + + // Verify that all 8 combinations of ±1 are present + boolean[] found = new boolean[8]; + for (Vector3f c : corners) { + assertTrue("|x| must equal 1", Math.abs(Math.abs(c.x) - 1f) < DELTA); + assertTrue("|y| must equal 1", Math.abs(Math.abs(c.y) - 1f) < DELTA); + assertTrue("|z| must equal 1", Math.abs(Math.abs(c.z) - 1f) < DELTA); + int idx = (c.x > 0 ? 1 : 0) | (c.y > 0 ? 2 : 0) | (c.z > 0 ? 4 : 0); + assertFalse("Each unique corner must appear exactly once (duplicate at idx " + idx + ")", found[idx]); + found[idx] = true; + } + for (int i = 0; i < 8; i++) { + assertTrue("Corner combination " + i + " was not produced", found[i]); + } + } + + /** + * The returned corners must reflect a non-origin centre correctly. + */ + @Test + public void testGetCornersOffsetCenter() { + Vector3f center = new Vector3f(5f, -3f, 2f); + BoundingBox box = new BoundingBox(center, 2f, 1f, 3f); + Vector3f[] corners = box.getCorners(); + + // Expected min/max + float minX = 3f, maxX = 7f; + float minY = -4f, maxY = -2f; + float minZ = -1f, maxZ = 5f; + + for (Vector3f c : corners) { + assertTrue("x must be minX or maxX", + Math.abs(c.x - minX) < DELTA || Math.abs(c.x - maxX) < DELTA); + assertTrue("y must be minY or maxY", + Math.abs(c.y - minY) < DELTA || Math.abs(c.y - maxY) < DELTA); + assertTrue("z must be minZ or maxZ", + Math.abs(c.z - minZ) < DELTA || Math.abs(c.z - maxZ) < DELTA); + } + } + + /** + * Passing an existing 8-element array must overwrite it in-place (no new + * allocation for the outer array). + */ + @Test + public void testGetCornersReuseStoreArray() { + BoundingBox box = new BoundingBox(Vector3f.ZERO.clone(), 1f, 1f, 1f); + + Vector3f[] store = new Vector3f[8]; + for (int i = 0; i < 8; i++) { + store[i] = new Vector3f(999f, 999f, 999f); + } + + Vector3f[] result = box.getCorners(store); + assertSame("getCorners(store) must return the same array instance", store, result); + + // All elements should have been overwritten from 999 -> valid corners + for (Vector3f c : result) { + assertTrue("Corner x must be ±1, not 999", Math.abs(Math.abs(c.x) - 1f) < DELTA); + } + } + + /** + * Passing null should produce a freshly allocated 8-element array. + */ + @Test + public void testGetCornersNullStore() { + BoundingBox box = new BoundingBox(Vector3f.ZERO.clone(), 1f, 1f, 1f); + Vector3f[] result = box.getCorners(null); + assertNotNull(result); + assertEquals(8, result.length); + } + + /** + * A box whose extents are all zero (a point) should produce 8 corners + * that all equal the centre. + */ + @Test + public void testGetCornersPointBox() { + Vector3f pt = new Vector3f(1f, 2f, 3f); + BoundingBox box = new BoundingBox(pt, 0f, 0f, 0f); + Vector3f[] corners = box.getCorners(); + + for (Vector3f c : corners) { + assertEquals("Point-box corner x", pt.x, c.x, DELTA); + assertEquals("Point-box corner y", pt.y, c.y, DELTA); + assertEquals("Point-box corner z", pt.z, c.z, DELTA); + } + } + + /** + * Verify the documented corner ordering (indices 0-7). + */ + @Test + public void testGetCornersOrdering() { + BoundingBox box = new BoundingBox(Vector3f.ZERO.clone(), 1f, 1f, 1f); + Vector3f[] c = box.getCorners(); + // index: 0=(-,-,-), 1=(+,-,-), 2=(-,+,-), 3=(+,+,-), + // 4=(-,-,+), 5=(+,-,+), 6=(-,+,+), 7=(+,+,+) + assertCorner(c[0], -1, -1, -1); + assertCorner(c[1], 1, -1, -1); + assertCorner(c[2], -1, 1, -1); + assertCorner(c[3], 1, 1, -1); + assertCorner(c[4], -1, -1, 1); + assertCorner(c[5], 1, -1, 1); + assertCorner(c[6], -1, 1, 1); + assertCorner(c[7], 1, 1, 1); + } + + // ========================================================================= + // expand(float) tests + // ========================================================================= + + /** + * Expanding uniformly increases all three extents. + */ + @Test + public void testExpandUniformGrow() { + BoundingBox box = new BoundingBox(Vector3f.ZERO.clone(), 2f, 3f, 4f); + BoundingBox returned = box.expand(1f); + + assertSame("expand() must return 'this' for chaining", box, returned); + assertEquals(3f, box.getXExtent(), DELTA); + assertEquals(4f, box.getYExtent(), DELTA); + assertEquals(5f, box.getZExtent(), DELTA); + } + + /** + * A negative expand value shrinks the box. + */ + @Test + public void testExpandUniformShrink() { + BoundingBox box = new BoundingBox(Vector3f.ZERO.clone(), 2f, 3f, 4f); + box.expand(-1f); + + assertEquals(1f, box.getXExtent(), DELTA); + assertEquals(2f, box.getYExtent(), DELTA); + assertEquals(3f, box.getZExtent(), DELTA); + } + + /** + * Shrinking beyond zero must clamp to zero, not go negative. + */ + @Test + public void testExpandClampToZero() { + BoundingBox box = new BoundingBox(Vector3f.ZERO.clone(), 1f, 1f, 1f); + box.expand(-100f); + + assertEquals("xExtent must clamp at 0", 0f, box.getXExtent(), DELTA); + assertEquals("yExtent must clamp at 0", 0f, box.getYExtent(), DELTA); + assertEquals("zExtent must clamp at 0", 0f, box.getZExtent(), DELTA); + } + + /** + * The center must be unchanged after expand. + */ + @Test + public void testExpandDoesNotMoveCenter() { + Vector3f center = new Vector3f(5f, 5f, 5f); + BoundingBox box = new BoundingBox(center.clone(), 1f, 1f, 1f); + box.expand(10f); + + assertEquals("center.x must not change", 5f, box.getCenter().x, DELTA); + assertEquals("center.y must not change", 5f, box.getCenter().y, DELTA); + assertEquals("center.z must not change", 5f, box.getCenter().z, DELTA); + } + + // ========================================================================= + // expand(Vector3f) tests + // ========================================================================= + + /** + * Per-axis expand applies independently. + */ + @Test + public void testExpandPerAxisGrow() { + BoundingBox box = new BoundingBox(Vector3f.ZERO.clone(), 1f, 2f, 3f); + box.expand(new Vector3f(1f, 2f, 3f)); + + assertEquals(2f, box.getXExtent(), DELTA); + assertEquals(4f, box.getYExtent(), DELTA); + assertEquals(6f, box.getZExtent(), DELTA); + } + + /** + * Mixed grow/shrink per axis. + */ + @Test + public void testExpandPerAxisMixed() { + BoundingBox box = new BoundingBox(Vector3f.ZERO.clone(), 5f, 5f, 5f); + box.expand(new Vector3f(2f, -2f, 0f)); + + assertEquals(7f, box.getXExtent(), DELTA); + assertEquals(3f, box.getYExtent(), DELTA); + assertEquals(5f, box.getZExtent(), DELTA); + } + + /** + * Per-axis clamp to zero. + */ + @Test + public void testExpandPerAxisClampToZero() { + BoundingBox box = new BoundingBox(Vector3f.ZERO.clone(), 1f, 2f, 3f); + box.expand(new Vector3f(-100f, -100f, -100f)); + + assertEquals(0f, box.getXExtent(), DELTA); + assertEquals(0f, box.getYExtent(), DELTA); + assertEquals(0f, box.getZExtent(), DELTA); + } + + /** + * Verify expand(Vector3f) returns {@code this} for fluent-chaining. + */ + @Test + public void testExpandVectorChaining() { + BoundingBox box = new BoundingBox(Vector3f.ZERO.clone(), 1f, 1f, 1f); + assertSame(box, box.expand(new Vector3f(1f, 1f, 1f))); + } + + // ========================================================================= + // Interaction: getCorners() after expand() + // ========================================================================= + + /** + * After expanding, getCorners() should reflect the new extents. + */ + @Test + public void testGetCornersAfterExpand() { + BoundingBox box = new BoundingBox(Vector3f.ZERO.clone(), 1f, 1f, 1f); + box.expand(1f); // extents now 2,2,2 + + Vector3f[] corners = box.getCorners(); + for (Vector3f c : corners) { + assertTrue("After expand, |x| must equal 2", Math.abs(Math.abs(c.x) - 2f) < DELTA); + assertTrue("After expand, |y| must equal 2", Math.abs(Math.abs(c.y) - 2f) < DELTA); + assertTrue("After expand, |z| must equal 2", Math.abs(Math.abs(c.z) - 2f) < DELTA); + } + } + + // ========================================================================= + // Helper + // ========================================================================= + + private static void assertCorner(Vector3f actual, float ex, float ey, float ez) { + assertEquals("corner x", ex, actual.x, DELTA); + assertEquals("corner y", ey, actual.y, DELTA); + assertEquals("corner z", ez, actual.z, DELTA); + } +} diff --git a/jme3-networking/src/test/java/com/jme3/network/message/Vector3SerializerTest.java b/jme3-networking/src/test/java/com/jme3/network/message/Vector3SerializerTest.java new file mode 100644 index 0000000000..f05b0ab7c3 --- /dev/null +++ b/jme3-networking/src/test/java/com/jme3/network/message/Vector3SerializerTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.network.serializing.serializers; + +import com.jme3.math.Vector3f; +import java.io.IOException; +import java.nio.ByteBuffer; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; + +/** + * Round-trip tests for {@link Vector3Serializer}, which encodes positions and + * directions in multiplayer messages. + */ +public class Vector3SerializerTest { + + private static final int BYTES_PER_VECTOR = 12; + + @Test + public void testRoundTripPreservesComponents() throws IOException { + Vector3Serializer serializer = new Vector3Serializer(); + Vector3f original = new Vector3f(1.25f, -2.5f, 3.75f); + + ByteBuffer buffer = ByteBuffer.allocate(BYTES_PER_VECTOR); + serializer.writeObject(buffer, original); + buffer.flip(); + + Vector3f decoded = serializer.readObject(buffer, Vector3f.class); + + assertNotSame(original, decoded); + assertEquals(original.x, decoded.x, 0f); + assertEquals(original.y, decoded.y, 0f); + assertEquals(original.z, decoded.z, 0f); + assertEquals(BYTES_PER_VECTOR, buffer.position()); + } + + @Test + public void testRoundTripZeroVector() throws IOException { + Vector3Serializer serializer = new Vector3Serializer(); + Vector3f original = new Vector3f(0, 0, 0); + + ByteBuffer buffer = ByteBuffer.allocate(BYTES_PER_VECTOR); + serializer.writeObject(buffer, original); + buffer.flip(); + + Vector3f decoded = serializer.readObject(buffer, Vector3f.class); + + assertEquals(0f, decoded.x, 0f); + assertEquals(0f, decoded.y, 0f); + assertEquals(0f, decoded.z, 0f); + } + + @Test + public void testSequentialVectorsDoNotShareState() throws IOException { + Vector3Serializer serializer = new Vector3Serializer(); + Vector3f first = new Vector3f(1, 2, 3); + Vector3f second = new Vector3f(4, 5, 6); + + ByteBuffer buffer = ByteBuffer.allocate(BYTES_PER_VECTOR * 2); + serializer.writeObject(buffer, first); + serializer.writeObject(buffer, second); + buffer.flip(); + + Vector3f decodedFirst = serializer.readObject(buffer, Vector3f.class); + Vector3f decodedSecond = serializer.readObject(buffer, Vector3f.class); + + assertEquals(1f, decodedFirst.x, 0f); + assertEquals(4f, decodedSecond.x, 0f); + assertEquals(BYTES_PER_VECTOR * 2, buffer.position()); + } +}