/* * This file is part of SpongeAPI, licensed under the MIT License (MIT). * * Copyright (c) SpongePowered * Copyright (c) contributors * * 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 de.bluecolored.bluemap.core.util; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import java.util.Optional; import com.flowpowered.math.vector.Vector3d; import com.flowpowered.math.vector.Vector3i; /** * An axis aligned bounding box. That is, an un-rotated cuboid. * It is represented by its minimum and maximum corners. * *

The box will never be degenerate: the corners are always not equal and * respect the minimum and maximum properties.

* *

This class is immutable, all objects returned are either new instances or * itself.

*/ public class AABB { private final Vector3d min; private final Vector3d max; private Vector3d size = null; private Vector3d center = null; /** * Constructs a new bounding box from two opposite corners. * Fails the resulting box would be degenerate (a dimension is 0). * * @param firstCorner The first corner * @param secondCorner The second corner */ public AABB(Vector3i firstCorner, Vector3i secondCorner) { this(checkNotNull(firstCorner, "firstCorner").toDouble(), checkNotNull(secondCorner, "secondCorner").toDouble()); } /** * Constructs a new bounding box from two opposite corners. * Fails the resulting box would be degenerate (a dimension is 0). * * @param x1 The first corner x coordinate * @param y1 The first corner y coordinate * @param z1 The first corner z coordinate * @param x2 The second corner x coordinate * @param y2 The second corner y coordinate * @param z2 The second corner z coordinate */ public AABB(double x1, double y1, double z1, double x2, double y2, double z2) { this(new Vector3d(x1, y1, z1), new Vector3d(x2, y2, z2)); } /** * Constructs a new bounding box from two opposite corners. * Fails the resulting box would be degenerate (a dimension is 0). * * @param firstCorner The first corner * @param secondCorner The second corner */ public AABB(Vector3d firstCorner, Vector3d secondCorner) { checkNotNull(firstCorner, "firstCorner"); checkNotNull(secondCorner, "secondCorner"); this.min = firstCorner.min(secondCorner); this.max = firstCorner.max(secondCorner); checkArgument(this.min.getX() != this.max.getX(), "The box is degenerate on x"); checkArgument(this.min.getY() != this.max.getY(), "The box is degenerate on y"); checkArgument(this.min.getZ() != this.max.getZ(), "The box is degenerate on z"); } /** * The minimum corner of the box. * * @return The minimum corner */ public Vector3d getMin() { return this.min; } /** * The maximum corner of the box. * * @return The maximum corner */ public Vector3d getMax() { return this.max; } /** * Returns the center of the box, halfway between each corner. * * @return The center */ public Vector3d getCenter() { if (this.center == null) { this.center = this.min.add(getSize().div(2)); } return this.center; } /** * Gets the size of the box. * * @return The size */ public Vector3d getSize() { if (this.size == null) { this.size = this.max.sub(this.min); } return this.size; } /** * Checks if the bounding box contains a point. * * @param point The point to check * @return Whether or not the box contains the point */ public boolean contains(Vector3i point) { checkNotNull(point, "point"); return contains(point.getX(), point.getY(), point.getZ()); } /** * Checks if the bounding box contains a point. * * @param point The point to check * @return Whether or not the box contains the point */ public boolean contains(Vector3d point) { checkNotNull(point, "point"); return contains(point.getX(), point.getY(), point.getZ()); } /** * Checks if the bounding box contains a point. * * @param x The x coordinate of the point * @param y The y coordinate of the point * @param z The z coordinate of the point * @return Whether or not the box contains the point */ public boolean contains(double x, double y, double z) { return this.min.getX() <= x && this.max.getX() >= x && this.min.getY() <= y && this.max.getY() >= y && this.min.getZ() <= z && this.max.getZ() >= z; } /** * Checks if the bounding box intersects another. * * @param other The other bounding box to check * @return Whether this bounding box intersects with the other */ public boolean intersects(AABB other) { checkNotNull(other, "other"); return this.max.getX() >= other.getMin().getX() && other.getMax().getX() >= this.min.getX() && this.max.getY() >= other.getMin().getY() && other.getMax().getY() >= this.min.getY() && this.max.getZ() >= other.getMin().getZ() && other.getMax().getZ() >= this.min.getZ(); } /** * Tests for intersection between the box and a ray defined by a starting * point and a direction. * * @param start The starting point of the ray * @param direction The direction of the ray * @return An intersection point, if any */ public Optional intersects(Vector3d start, Vector3d direction) { checkNotNull(start, "start"); checkNotNull(direction, "direction"); // Adapted from: https://github.com/flow/react/blob/develop/src/main/java/com/flowpowered/react/collision/RayCaster.java#L156 // The box is interpreted as 6 infinite perpendicular places, one for each face (being expanded infinitely) // "t" variables are multipliers: start + direction * t gives the intersection point // Find the intersections on the -x and +x planes, oriented by direction final double txMin; final double txMax; final Vector3d xNormal; if (Math.copySign(1, direction.getX()) > 0) { txMin = (this.min.getX() - start.getX()) / direction.getX(); txMax = (this.max.getX() - start.getX()) / direction.getX(); xNormal = Vector3d.UNIT_X; } else { txMin = (this.max.getX() - start.getX()) / direction.getX(); txMax = (this.min.getX() - start.getX()) / direction.getX(); xNormal = Vector3d.UNIT_X.negate(); } // Find the intersections on the -y and +y planes, oriented by direction final double tyMin; final double tyMax; final Vector3d yNormal; if (Math.copySign(1, direction.getY()) > 0) { tyMin = (this.min.getY() - start.getY()) / direction.getY(); tyMax = (this.max.getY() - start.getY()) / direction.getY(); yNormal = Vector3d.UNIT_Y; } else { tyMin = (this.max.getY() - start.getY()) / direction.getY(); tyMax = (this.min.getY() - start.getY()) / direction.getY(); yNormal = Vector3d.UNIT_Y.negate(); } // The ray should intersect the -x plane before the +y plane and intersect // the -y plane before the +x plane, else it is outside the box if (txMin > tyMax || txMax < tyMin) { return Optional.empty(); } // Keep track of the intersection normal which also helps with floating point errors Vector3d normalMax; Vector3d normalMin; // The ray intersects only the furthest min plane on the box and only the closest // max plane on the box double tMin; if (tyMin == txMin) { tMin = tyMin; normalMin = xNormal.negate().sub(yNormal); } else if (tyMin > txMin) { tMin = tyMin; normalMin = yNormal.negate(); } else { tMin = txMin; normalMin = xNormal.negate(); } double tMax; if (tyMax == txMax) { tMax = tyMax; normalMax = xNormal.add(yNormal); } else if (tyMax < txMax) { tMax = tyMax; normalMax = yNormal; } else { tMax = txMax; normalMax = xNormal; } // Find the intersections on the -z and +z planes, oriented by direction final double tzMin; final double tzMax; final Vector3d zNormal; if (Math.copySign(1, direction.getZ()) > 0) { tzMin = (this.min.getZ() - start.getZ()) / direction.getZ(); tzMax = (this.max.getZ() - start.getZ()) / direction.getZ(); zNormal = Vector3d.UNIT_Z; } else { tzMin = (this.max.getZ() - start.getZ()) / direction.getZ(); tzMax = (this.min.getZ() - start.getZ()) / direction.getZ(); zNormal = Vector3d.UNIT_Z.negate(); } // The ray intersects only the furthest min plane on the box and only the closest // max plane on the box if (tMin > tzMax || tMax < tzMin) { return Optional.empty(); } // The ray should intersect the closest plane outside first and the furthest // plane outside last if (tzMin == tMin) { normalMin = normalMin.sub(zNormal); } else if (tzMin > tMin) { tMin = tzMin; normalMin = zNormal.negate(); } if (tzMax == tMax) { normalMax = normalMax.add(zNormal); } else if (tzMax < tMax) { tMax = tzMax; normalMax = zNormal; } // Both intersection points are behind the start, there are no intersections if (tMax < 0) { return Optional.empty(); } // Find the final intersection multiplier and normal final double t; Vector3d normal; if (tMin < 0) { // Only the furthest intersection is after the start, so use it t = tMax; normal = normalMax; } else { // Both are after the start, use the closest one t = tMin; normal = normalMin; } normal = normal.normalize(); // To avoid rounding point errors leaving the intersection point just off the plane // we check the normal to use the actual plane value from the box coordinates final double x; final double y; final double z; if (normal.getX() > 0) { x = this.max.getX(); } else if (normal.getX() < 0) { x = this.min.getX(); } else { x = direction.getX() * t + start.getX(); } if (normal.getY() > 0) { y = this.max.getY(); } else if (normal.getY() < 0) { y = this.min.getY(); } else { y = direction.getY() * t + start.getY(); } if (normal.getZ() > 0) { z = this.max.getZ(); } else if (normal.getZ() < 0) { z = this.min.getZ(); } else { z = direction.getZ() * t + start.getZ(); } return Optional.of(new IntersectionPoint(new Vector3d(x, y, z), normal)); } /** * Offsets this bounding box by a given amount and returns a new box. * * @param offset The offset to apply * @return The new offset box */ public AABB offset(Vector3i offset) { checkNotNull(offset, "offset"); return offset(offset.getX(), offset.getY(), offset.getZ()); } /** * Offsets this bounding box by a given amount and returns a new box. * * @param offset The offset to apply * @return The new offset box */ public AABB offset(Vector3d offset) { checkNotNull(offset, "offset"); return offset(offset.getX(), offset.getY(), offset.getZ()); } /** * Offsets this bounding box by a given amount and returns a new box. * * @param x The amount of offset for the x coordinate * @param y The amount of offset for the y coordinate * @param z The amount of offset for the z coordinate * @return The new offset box */ public AABB offset(double x, double y, double z) { return new AABB(this.min.add(x, y, z), this.max.add(x, y, z)); } /** * Expands this bounding box by a given amount in both directions and * returns a new box. The expansion is applied half and half to the * minimum and maximum corners. * * @param amount The amount of expansion to apply * @return The new expanded box */ public AABB expand(Vector3i amount) { checkNotNull(amount, "amount"); return expand(amount.getX(), amount.getY(), amount.getZ()); } /** * Expands this bounding box by a given amount in both directions and * returns a new box. The expansion is applied half and half to the * minimum and maximum corners. * * @param amount The amount of expansion to apply * @return The new expanded box */ public AABB expand(Vector3d amount) { checkNotNull(amount, "amount"); return expand(amount.getX(), amount.getY(), amount.getZ()); } /** * Expands this bounding box by a given amount in both directions and * returns a new box. The expansion is applied half and half to the * minimum and maximum corners. * * @param x The amount of expansion for the x coordinate * @param y The amount of expansion for the y coordinate * @param z The amount of expansion for the z coordinate * @return The new expanded box */ public AABB expand(double x, double y, double z) { x /= 2; y /= 2; z /= 2; return new AABB(this.min.sub(x, y, z), this.max.add(x, y, z)); } @Override public boolean equals(Object other) { if (this == other) { return true; } if (!(other instanceof AABB)) { return false; } final AABB aabb = (AABB) other; return this.min.equals(aabb.min) && this.max.equals(aabb.max); } @Override public int hashCode() { int result = this.min.hashCode(); result = 31 * result + this.max.hashCode(); return result; } @Override public String toString() { return "AABB(" + this.min + " to " + this.max + ")"; } }