//----------------------------------------------------------------------------- // BoundingOrientedBox.cs // // Microsoft XNA Community Game Platform // Copyright (C) Microsoft Corporation. All rights reserved. //----------------------------------------------------------------------------- using System; using System.Collections.Generic; using Microsoft.Xna.Framework; namespace CollisionSample { /// /// Bounding volume using an oriented bounding box. /// public struct BoundingOrientedBox : IEquatable { #region Constants public const int CornerCount = 8; // Epsilon value used in ray tests, where a ray might hit the box almost edge-on. const float RAY_EPSILON = 1e-20F; #endregion #region Fields public Vector3 Center; public Vector3 HalfExtent; public Quaternion Orientation; #endregion #region Constructors /// /// Create an oriented box with the given center, half-extents, and orientation. /// public BoundingOrientedBox(Vector3 center, Vector3 halfExtents, Quaternion orientation) { Center = center; HalfExtent = halfExtents; Orientation = orientation; } /// /// Create an oriented box from an axis-aligned box. /// public static BoundingOrientedBox CreateFromBoundingBox(BoundingBox box) { Vector3 mid = (box.Min + box.Max) * 0.5f; Vector3 halfExtent = (box.Max - box.Min) * 0.5f; return new BoundingOrientedBox(mid, halfExtent, Quaternion.Identity); } /// /// Transform the given bounding box by a rotation around the origin followed by a translation /// /// /// /// A new bounding box, transformed relative to this one public BoundingOrientedBox Transform(Quaternion rotation, Vector3 translation) { return new BoundingOrientedBox(Vector3.Transform(Center, rotation) + translation, HalfExtent, Orientation * rotation); } /// /// Transform the given bounding box by a uniform scale and rotation around the origin followed /// by a translation /// /// A new bounding box, transformed relative to this one public BoundingOrientedBox Transform(float scale, Quaternion rotation, Vector3 translation) { return new BoundingOrientedBox(Vector3.Transform(Center * scale, rotation) + translation, HalfExtent * scale, Orientation * rotation); } #endregion #region IEquatable implementation public bool Equals(BoundingOrientedBox other) { return (Center == other.Center && HalfExtent == other.HalfExtent && Orientation == other.Orientation); } public override bool Equals(Object obj) { if (obj != null && obj is BoundingOrientedBox) { BoundingOrientedBox other = (BoundingOrientedBox)obj; return (Center == other.Center && HalfExtent == other.HalfExtent && Orientation == other.Orientation); } else { return false; } } public override int GetHashCode() { return Center.GetHashCode() ^ HalfExtent.GetHashCode() ^ Orientation.GetHashCode(); } public static bool operator==(BoundingOrientedBox a, BoundingOrientedBox b) { return Equals(a, b); } public static bool operator!=(BoundingOrientedBox a, BoundingOrientedBox b) { return !Equals(a, b); } public override string ToString() { return "{Center:" + Center.ToString() + " Extents:" + HalfExtent.ToString() + " Orientation:" + Orientation.ToString() + "}"; } #endregion #region Test vs. BoundingBox /// /// Determine if box A intersects box B. /// public bool Intersects(ref BoundingBox box) { Vector3 boxCenter = (box.Max + box.Min) * 0.5f; Vector3 boxHalfExtent = (box.Max - box.Min) * 0.5f; Matrix mb = Matrix.CreateFromQuaternion(Orientation); mb.Translation = Center - boxCenter; return ContainsRelativeBox(ref boxHalfExtent, ref HalfExtent, ref mb) != ContainmentType.Disjoint; } /// /// Determine if this box contains, intersects, or is disjoint from the given BoundingBox. /// public ContainmentType Contains(ref BoundingBox box) { Vector3 boxCenter = (box.Max + box.Min) * 0.5f; Vector3 boxHalfExtent = (box.Max - box.Min) * 0.5f; // Build the 3x3 rotation matrix that defines the orientation of 'other' relative to this box Quaternion relOrient; Quaternion.Conjugate(ref Orientation, out relOrient); Matrix relTransform = Matrix.CreateFromQuaternion(relOrient); relTransform.Translation = Vector3.TransformNormal(boxCenter - Center, relTransform); return ContainsRelativeBox(ref HalfExtent, ref boxHalfExtent, ref relTransform); } /// /// Determine if box A contains, intersects, or is disjoint from box B. /// public static ContainmentType Contains(ref BoundingBox boxA, ref BoundingOrientedBox oboxB) { Vector3 boxA_halfExtent = (boxA.Max - boxA.Min) * 0.5f; Vector3 boxA_center = (boxA.Max + boxA.Min) * 0.5f; Matrix mb = Matrix.CreateFromQuaternion(oboxB.Orientation); mb.Translation = oboxB.Center - boxA_center; return BoundingOrientedBox.ContainsRelativeBox(ref boxA_halfExtent, ref oboxB.HalfExtent, ref mb); } #endregion #region Test vs. BoundingOrientedBox /// /// Returns true if this box intersects the given other box. /// public bool Intersects(ref BoundingOrientedBox other) { return Contains(ref other) != ContainmentType.Disjoint; } /// /// Determine whether this box contains, intersects, or is disjoint from /// the given other box. /// public ContainmentType Contains(ref BoundingOrientedBox other) { // Build the 3x3 rotation matrix that defines the orientation of 'other' relative to this box Quaternion invOrient; Quaternion.Conjugate(ref Orientation, out invOrient); Quaternion relOrient; Quaternion.Multiply(ref invOrient, ref other.Orientation, out relOrient); Matrix relTransform = Matrix.CreateFromQuaternion(relOrient); relTransform.Translation = Vector3.Transform(other.Center - Center, invOrient); return ContainsRelativeBox(ref HalfExtent, ref other.HalfExtent, ref relTransform); } #endregion #region Test vs. BoundingFrustum /// /// Determine whether this box contains, intersects, or is disjoint from /// the given frustum. /// public ContainmentType Contains(BoundingFrustum frustum) { // Convert this bounding box to an equivalent BoundingFrustum, so we can rely on BoundingFrustum's // implementation. Note that this is very slow, since BoundingFrustum builds various data structures // for this test that it caches internally. To speed it up, you could convert the box to a frustum // just once and re-use that frustum for repeated tests. BoundingFrustum temp = ConvertToFrustum(); return temp.Contains(frustum); } /// /// Returns true if this box intersects the given frustum. /// public bool Intersects(BoundingFrustum frustum) { return (Contains(frustum) != ContainmentType.Disjoint); } /// /// Determine whether the given frustum contains, intersects, or is disjoint from /// the given oriented box. /// public static ContainmentType Contains(BoundingFrustum frustum, ref BoundingOrientedBox obox) { return frustum.Contains(obox.ConvertToFrustum()); } #endregion #region Test vs. BoundingSphere /// /// Test whether this box contains, intersects, or is disjoint from the given sphere /// public ContainmentType Contains(ref BoundingSphere sphere) { // Transform the sphere into local box space Quaternion iq = Quaternion.Conjugate(Orientation); Vector3 localCenter = Vector3.Transform(sphere.Center - Center, iq); // (dx,dy,dz) = signed distance of center of sphere from edge of box float dx = Math.Abs(localCenter.X) - HalfExtent.X; float dy = Math.Abs(localCenter.Y) - HalfExtent.Y; float dz = Math.Abs(localCenter.Z) - HalfExtent.Z; // Check for sphere completely inside box float r = sphere.Radius; if (dx <= -r && dy <= -r && dz <= -r) return ContainmentType.Contains; // Compute how far away the sphere is in each dimension dx = Math.Max(dx, 0.0f); dy = Math.Max(dy, 0.0f); dz = Math.Max(dz, 0.0f); if(dx*dx + dy*dy + dz*dz >= r*r) return ContainmentType.Disjoint; return ContainmentType.Intersects; } /// /// Test whether this box intersects the given sphere /// public bool Intersects(ref BoundingSphere sphere) { // Transform the sphere into local box space Quaternion iq = Quaternion.Conjugate(Orientation); Vector3 localCenter = Vector3.Transform(sphere.Center - Center, iq); // (dx,dy,dz) = signed distance of center of sphere from edge of box float dx = Math.Abs(localCenter.X) - HalfExtent.X; float dy = Math.Abs(localCenter.Y) - HalfExtent.Y; float dz = Math.Abs(localCenter.Z) - HalfExtent.Z; // Compute how far away the sphere is in each dimension dx = Math.Max(dx, 0.0f); dy = Math.Max(dy, 0.0f); dz = Math.Max(dz, 0.0f); float r = sphere.Radius; return dx * dx + dy * dy + dz * dz < r * r; } /// /// Test whether a BoundingSphere contains, intersects, or is disjoint from a BoundingOrientedBox /// public static ContainmentType Contains(ref BoundingSphere sphere, ref BoundingOrientedBox box) { // Transform the sphere into local box space Quaternion iq = Quaternion.Conjugate(box.Orientation); Vector3 localCenter = Vector3.Transform(sphere.Center - box.Center, iq); localCenter.X = Math.Abs(localCenter.X); localCenter.Y = Math.Abs(localCenter.Y); localCenter.Z = Math.Abs(localCenter.Z); // Check for box completely inside sphere float rSquared = sphere.Radius * sphere.Radius; if ((localCenter + box.HalfExtent).LengthSquared() <= rSquared) return ContainmentType.Contains; // (dx,dy,dz) = signed distance of center of sphere from edge of box Vector3 d = localCenter - box.HalfExtent; // Compute how far away the sphere is in each dimension d.X = Math.Max(d.X, 0.0f); d.Y = Math.Max(d.Y, 0.0f); d.Z = Math.Max(d.Z, 0.0f); if (d.LengthSquared() >= rSquared) return ContainmentType.Disjoint; return ContainmentType.Intersects; } #endregion #region Test vs. 0/1/2d primitives /// /// Returns true if this box contains the given point. /// public bool Contains(ref Vector3 point) { // Transform the point into box-local space and check against // our extents. Quaternion qinv = Quaternion.Conjugate(Orientation); Vector3 plocal = Vector3.Transform(point - Center, qinv); return Math.Abs(plocal.X) <= HalfExtent.X && Math.Abs(plocal.Y) <= HalfExtent.Y && Math.Abs(plocal.Z) <= HalfExtent.Z; } /// /// Determine whether the given ray intersects this box. If so, returns /// the parametric value of the point of first intersection; otherwise /// returns null. /// public float? Intersects(ref Ray ray) { Matrix R = Matrix.CreateFromQuaternion(Orientation); Vector3 TOrigin = Center - ray.Position; float t_min = -float.MaxValue; float t_max = float.MaxValue; // X-case float axisDotOrigin = Vector3.Dot(R.Right, TOrigin); float axisDotDir = Vector3.Dot(R.Right, ray.Direction); if (axisDotDir >= -RAY_EPSILON && axisDotDir <= RAY_EPSILON) { if ((-axisDotOrigin - HalfExtent.X) > 0.0 || (-axisDotOrigin + HalfExtent.X) > 0.0f) return null; } else { float t1 = (axisDotOrigin - HalfExtent.X) / axisDotDir; float t2 = (axisDotOrigin + HalfExtent.X) / axisDotDir; if (t1 > t2) { float temp = t1; t1 = t2; t2 = temp; } if (t1 > t_min) t_min = t1; if (t2 < t_max) t_max = t2; if (t_max < 0.0f || t_min > t_max) return null; } // Y-case axisDotOrigin = Vector3.Dot(R.Up, TOrigin); axisDotDir = Vector3.Dot(R.Up, ray.Direction); if (axisDotDir >= -RAY_EPSILON && axisDotDir <= RAY_EPSILON) { if ((-axisDotOrigin - HalfExtent.Y) > 0.0 || (-axisDotOrigin + HalfExtent.Y) > 0.0f) return null; } else { float t1 = (axisDotOrigin - HalfExtent.Y) / axisDotDir; float t2 = (axisDotOrigin + HalfExtent.Y) / axisDotDir; if (t1 > t2) { float temp = t1; t1 = t2; t2 = temp; } if (t1 > t_min) t_min = t1; if (t2 < t_max) t_max = t2; if (t_max < 0.0f || t_min > t_max) return null; } // Z-case axisDotOrigin = Vector3.Dot(R.Forward, TOrigin); axisDotDir = Vector3.Dot(R.Forward, ray.Direction); if (axisDotDir >= -RAY_EPSILON && axisDotDir <= RAY_EPSILON) { if ((-axisDotOrigin - HalfExtent.Z) > 0.0 || (-axisDotOrigin + HalfExtent.Z) > 0.0f) return null; } else { float t1 = (axisDotOrigin - HalfExtent.Z) / axisDotDir; float t2 = (axisDotOrigin + HalfExtent.Z) / axisDotDir; if (t1 > t2) { float temp = t1; t1 = t2; t2 = temp; } if (t1 > t_min) t_min = t1; if (t2 < t_max) t_max = t2; if (t_max < 0.0f || t_min > t_max) return null; } return t_min; } /// /// Classify this bounding box as entirely in front of, in back of, or /// intersecting the given plane. /// public PlaneIntersectionType Intersects(ref Plane plane) { float dist = plane.DotCoordinate(Center); // Transform the plane's normal into this box's space Vector3 localNormal = Vector3.Transform(plane.Normal, Quaternion.Conjugate(Orientation)); // Project the axes of the box onto the normal of the plane. Half the // length of the projection (sometime called the "radius") is equal to // h(u) * abs(n dot b(u))) + h(v) * abs(n dot b(v)) + h(w) * abs(n dot b(w)) // where h(i) are extents of the box, n is the plane normal, and b(i) are the // axes of the box. float r = Math.Abs(HalfExtent.X*localNormal.X) + Math.Abs(HalfExtent.Y*localNormal.Y) + Math.Abs(HalfExtent.Z*localNormal.Z); if(dist > r) { return PlaneIntersectionType.Front; } else if(dist < -r) { return PlaneIntersectionType.Back; } else { return PlaneIntersectionType.Intersecting; } } #endregion #region Helper methods /// /// Return the 8 corner positions of this bounding box. /// /// ZMax ZMin /// 0----1 4----5 /// | | | | /// | | | | /// 3----2 7----6 /// /// The ordering of indices is a little strange to match what BoundingBox.GetCorners() does. /// public Vector3[] GetCorners() { Vector3[] corners = new Vector3[CornerCount]; GetCorners(corners, 0); return corners; } /// /// Return the 8 corner positions of this bounding box. /// /// ZMax ZMin /// 0----1 4----5 /// | | | | /// | | | | /// 3----2 7----6 /// /// The ordering of indices is a little strange to match what BoundingBox.GetCorners() does. /// /// Array to fill with the eight corner positions /// Index within corners array to start writing positions public void GetCorners(Vector3[] corners, int startIndex) { Matrix m = Matrix.CreateFromQuaternion(Orientation); Vector3 hX = m.Left * HalfExtent.X; Vector3 hY = m.Up * HalfExtent.Y; Vector3 hZ = m.Backward * HalfExtent.Z; int i = startIndex; corners[i++] = Center - hX + hY + hZ; corners[i++] = Center + hX + hY + hZ; corners[i++] = Center + hX - hY + hZ; corners[i++] = Center - hX - hY + hZ; corners[i++] = Center - hX + hY - hZ; corners[i++] = Center + hX + hY - hZ; corners[i++] = Center + hX - hY - hZ; corners[i++] = Center - hX - hY - hZ; } /// /// Determine whether the box described by half-extents hA, axis-aligned and centered at the origin, contains /// the box described by half-extents hB, whose position and orientation are given by the transform matrix mB. /// The matrix is assumed to contain only rigid motion; if it contains scaling or perpsective the result of /// this method will be incorrect. /// /// Half-extents of first box /// Half-extents of second box /// Position and orientation of second box relative to first box /// ContainmentType enum indicating whether the boxes are disjoin, intersecting, or /// whether box A contains box B. public static ContainmentType ContainsRelativeBox(ref Vector3 hA, ref Vector3 hB, ref Matrix mB) { Vector3 mB_T = mB.Translation; Vector3 mB_TA = new Vector3(Math.Abs(mB_T.X), Math.Abs(mB_T.Y), Math.Abs(mB_T.Z)); // Transform the extents of B // TODO: check which coords Right/Up/Back refer to and access them directly. This looks dumb. Vector3 bX = mB.Right; // x-axis of box B Vector3 bY = mB.Up; // y-axis of box B Vector3 bZ = mB.Backward; // z-axis of box B Vector3 hx_B = bX * hB.X; // x extent of box B Vector3 hy_B = bY * hB.Y; // y extent of box B Vector3 hz_B = bZ * hB.Z; // z extent of box B // Check for containment first. float projx_B = Math.Abs(hx_B.X) + Math.Abs(hy_B.X) + Math.Abs(hz_B.X); float projy_B = Math.Abs(hx_B.Y) + Math.Abs(hy_B.Y) + Math.Abs(hz_B.Y); float projz_B = Math.Abs(hx_B.Z) + Math.Abs(hy_B.Z) + Math.Abs(hz_B.Z); if (mB_TA.X + projx_B <= hA.X && mB_TA.Y + projy_B <= hA.Y && mB_TA.Z + projz_B <= hA.Z) return ContainmentType.Contains; // Check for separation along the faces of the other box, // by projecting each local axis onto the other boxes' axes // http://www.cs.unc.edu/~geom/theses/gottschalk/main.pdf // // The general test form, given a choice of separating axis, is: // sizeA = abs(dot(A.e1,axis)) + abs(dot(A.e2,axis)) + abs(dot(A.e3,axis)) // sizeB = abs(dot(B.e1,axis)) + abs(dot(B.e2,axis)) + abs(dot(B.e3,axis)) // distance = abs(dot(B.center - A.center),axis)) // if distance >= sizeA+sizeB, the boxes are disjoint // // We need to do this test on 15 axes: // x, y, z axis of box A // x, y, z axis of box B // (v1 cross v2) for each v1 in A's axes, for each v2 in B's axes // // Since we're working in a space where A is axis-aligned and A.center=0, many // of the tests and products simplify away. // Check for separation along the axes of box A if (mB_TA.X >= hA.X + Math.Abs(hx_B.X) + Math.Abs(hy_B.X) + Math.Abs(hz_B.X)) return ContainmentType.Disjoint; if (mB_TA.Y >= hA.Y + Math.Abs(hx_B.Y) + Math.Abs(hy_B.Y) + Math.Abs(hz_B.Y)) return ContainmentType.Disjoint; if (mB_TA.Z >= hA.Z + Math.Abs(hx_B.Z) + Math.Abs(hy_B.Z) + Math.Abs(hz_B.Z)) return ContainmentType.Disjoint; // Check for separation along the axes box B, hx_B/hy_B/hz_B if (Math.Abs(Vector3.Dot(mB_T, bX)) >= Math.Abs(hA.X * bX.X) + Math.Abs(hA.Y * bX.Y) + Math.Abs(hA.Z * bX.Z) + hB.X) return ContainmentType.Disjoint; if (Math.Abs(Vector3.Dot(mB_T, bY)) >= Math.Abs(hA.X * bY.X) + Math.Abs(hA.Y * bY.Y) + Math.Abs(hA.Z * bY.Z) + hB.Y) return ContainmentType.Disjoint; if (Math.Abs(Vector3.Dot(mB_T, bZ)) >= Math.Abs(hA.X * bZ.X) + Math.Abs(hA.Y * bZ.Y) + Math.Abs(hA.Z * bZ.Z) + hB.Z) return ContainmentType.Disjoint; // Check for separation in plane containing an axis of box A and and axis of box B // // We need to compute all 9 cross products to find them, but a lot of terms drop out // since we're working in A's local space. Also, since each such plane is parallel // to the defining axis in each box, we know those dot products will be 0 and can // omit them. Vector3 axis; // a.X ^ b.X = (1,0,0) ^ bX axis = new Vector3(0, -bX.Z, bX.Y); if (Math.Abs(Vector3.Dot(mB_T, axis)) >= Math.Abs(hA.Y * axis.Y) + Math.Abs(hA.Z * axis.Z) + Math.Abs(Vector3.Dot(axis, hy_B)) + Math.Abs(Vector3.Dot(axis, hz_B))) return ContainmentType.Disjoint; // a.X ^ b.Y = (1,0,0) ^ bY axis = new Vector3(0, -bY.Z, bY.Y); if (Math.Abs(Vector3.Dot(mB_T, axis)) >= Math.Abs(hA.Y * axis.Y) + Math.Abs(hA.Z * axis.Z) + Math.Abs(Vector3.Dot(axis, hz_B)) + Math.Abs(Vector3.Dot(axis, hx_B))) return ContainmentType.Disjoint; // a.X ^ b.Z = (1,0,0) ^ bZ axis = new Vector3(0, -bZ.Z, bZ.Y); if (Math.Abs(Vector3.Dot(mB_T, axis)) >= Math.Abs(hA.Y * axis.Y) + Math.Abs(hA.Z * axis.Z) + Math.Abs(Vector3.Dot(axis, hx_B)) + Math.Abs(Vector3.Dot(axis, hy_B))) return ContainmentType.Disjoint; // a.Y ^ b.X = (0,1,0) ^ bX axis = new Vector3(bX.Z, 0, -bX.X); if (Math.Abs(Vector3.Dot(mB_T, axis)) >= Math.Abs(hA.Z * axis.Z) + Math.Abs(hA.X * axis.X) + Math.Abs(Vector3.Dot(axis, hy_B)) + Math.Abs(Vector3.Dot(axis, hz_B))) return ContainmentType.Disjoint; // a.Y ^ b.Y = (0,1,0) ^ bY axis = new Vector3(bY.Z, 0, -bY.X); if (Math.Abs(Vector3.Dot(mB_T, axis)) >= Math.Abs(hA.Z * axis.Z) + Math.Abs(hA.X * axis.X) + Math.Abs(Vector3.Dot(axis, hz_B)) + Math.Abs(Vector3.Dot(axis, hx_B))) return ContainmentType.Disjoint; // a.Y ^ b.Z = (0,1,0) ^ bZ axis = new Vector3(bZ.Z, 0, -bZ.X); if (Math.Abs(Vector3.Dot(mB_T, axis)) >= Math.Abs(hA.Z * axis.Z) + Math.Abs(hA.X * axis.X) + Math.Abs(Vector3.Dot(axis, hx_B)) + Math.Abs(Vector3.Dot(axis, hy_B))) return ContainmentType.Disjoint; // a.Z ^ b.X = (0,0,1) ^ bX axis = new Vector3(-bX.Y, bX.X, 0); if (Math.Abs(Vector3.Dot(mB_T, axis)) >= Math.Abs(hA.X * axis.X) + Math.Abs(hA.Y * axis.Y) + Math.Abs(Vector3.Dot(axis, hy_B)) + Math.Abs(Vector3.Dot(axis, hz_B))) return ContainmentType.Disjoint; // a.Z ^ b.Y = (0,0,1) ^ bY axis = new Vector3(-bY.Y, bY.X, 0); if (Math.Abs(Vector3.Dot(mB_T, axis)) >= Math.Abs(hA.X * axis.X) + Math.Abs(hA.Y * axis.Y) + Math.Abs(Vector3.Dot(axis, hz_B)) + Math.Abs(Vector3.Dot(axis, hx_B))) return ContainmentType.Disjoint; // a.Z ^ b.Z = (0,0,1) ^ bZ axis = new Vector3(-bZ.Y, bZ.X, 0); if (Math.Abs(Vector3.Dot(mB_T, axis)) >= Math.Abs(hA.X * axis.X) + Math.Abs(hA.Y * axis.Y) + Math.Abs(Vector3.Dot(axis, hx_B)) + Math.Abs(Vector3.Dot(axis, hy_B))) return ContainmentType.Disjoint; return ContainmentType.Intersects; } /// /// Convert this BoundingOrientedBox to a BoundingFrustum describing the same volume. /// /// A BoundingFrustum is defined by the matrix that carries its volume to the /// box from (-1,-1,0) to (1,1,1), so we just need a matrix that carries our box there. /// public BoundingFrustum ConvertToFrustum() { Quaternion invOrientation; Quaternion.Conjugate(ref Orientation, out invOrientation); float sx = 1.0f / HalfExtent.X; float sy = 1.0f / HalfExtent.Y; float sz = .5f / HalfExtent.Z; Matrix temp; Matrix.CreateFromQuaternion(ref invOrientation, out temp); temp.M11 *= sx; temp.M21 *= sx; temp.M31 *= sx; temp.M12 *= sy; temp.M22 *= sy; temp.M32 *= sy; temp.M13 *= sz; temp.M23 *= sz; temp.M33 *= sz; temp.Translation = Vector3.UnitZ*0.5f + Vector3.TransformNormal(-Center, temp); return new BoundingFrustum(temp); } #endregion } }