Object-oriented bounding box (from either AActor or mesh)

In case anyone is interested, I’ve created a struct called FOBBox (Orient Bound Box). I’m currently using it to group hierarchical instanced static mesh instances together based on the OBB IsInsideOrOn or Intersect as you can see in the image.

You don’t need to use the += operator, it works fine on a single instance or static mesh. :grin:

Example Usage:

FTransform OtherTransform;
OtherComponent->GetInstanceTransform(j, OtherTransform, true);
GothGirlOBBHelper::FOBBox OtherObb(OtherComponent->GetStaticMesh()->GetBounds(), OtherTransform);

if (CombinedObb.IsInsideOrOn(OtherObb) || CombinedObb.Intersect(OtherObb))
{
	CombinedObb += OtherObb;
}

Then once that’s complete calling ToTransform to return a transform of the OBB.

FTransform BoundsTransform = CombinedObb.ToTransform();

Code Below with Debug Draw helper. Example Usage:

GothGirlOBBHelper::GothGirlDrawOBB(HISMComponent->GetWorld(), CombinedObb, FColor::Red, 10.0f);

The Struct:

namespace GothGirlOBBHelper
{
	struct FOBBox
	{
		// Center of the OBB in world space
		FVector Center{ FVector::ZAxisVector };

		// Half-size extents along each of the OBB's local axes
		FVector Extents{ FVector::ZAxisVector };

		// Orientation of the OBB as a quaternion
		FQuat Orientation{ FQuat::Identity };

		// Choose to keep the current rotation or handle rotation interpolation
		bool SlerpRotation{ false };

		// Default constructor
		FOBBox() {}

		// Constructor that calculates the OBB from local bounds and instance transform
		FOBBox(const FBoxSphereBounds& LocalBounds, const FTransform& InstanceTransform, const bool& InSlerpRotation = false)
		{
			// Calculate the OBB center in world space
			Center = InstanceTransform.TransformPosition(LocalBounds.Origin);

			// Extract rotation and absolute scale from the instance transform
			Orientation = InstanceTransform.GetRotation();
			FVector Scale3D = InstanceTransform.GetScale3D().GetAbs();

			// Scale the extents by the absolute instance scale to handle negative scaling
			Extents = LocalBounds.BoxExtent * Scale3D;

			SlerpRotation = InSlerpRotation;
		}

		// Method to get the OBB's axes in world space
		void GetAxes(FVector& AxisX, FVector& AxisY, FVector& AxisZ) const
		{
			AxisX = Orientation.GetAxisX();
			AxisY = Orientation.GetAxisY();
			AxisZ = Orientation.GetAxisZ();
		}

		// Method to get the eight corners of the OBB
		void GetCorners(TArray<FVector>& OutCorners) const
		{
			FVector AxisX, AxisY, AxisZ;
			GetAxes(AxisX, AxisY, AxisZ);

			// Half-size vectors along each axis
			FVector HalfSizeX = AxisX * Extents.X;
			FVector HalfSizeY = AxisY * Extents.Y;
			FVector HalfSizeZ = AxisZ * Extents.Z;

			// Initialize the corners array
			OutCorners.SetNumUninitialized(8);

			// Calculate the eight corners of the OBB
			OutCorners[0] = Center + HalfSizeX + HalfSizeY + HalfSizeZ;
			OutCorners[1] = Center + HalfSizeX + HalfSizeY - HalfSizeZ;
			OutCorners[2] = Center + HalfSizeX - HalfSizeY + HalfSizeZ;
			OutCorners[3] = Center + HalfSizeX - HalfSizeY - HalfSizeZ;
			OutCorners[4] = Center - HalfSizeX + HalfSizeY + HalfSizeZ;
			OutCorners[5] = Center - HalfSizeX + HalfSizeY - HalfSizeZ;
			OutCorners[6] = Center - HalfSizeX - HalfSizeY + HalfSizeZ;
			OutCorners[7] = Center - HalfSizeX - HalfSizeY - HalfSizeZ;
		}

		// Overload the += operator to combine two OBBs
		FOBBox& operator+=(const FOBBox& Other)
		{
			// Step 1: Transform the corners of the other OBB into this OBB's local space
			TArray<FVector> OtherCorners;
			Other.GetCorners(OtherCorners);

			// Inverse rotation to transform into local space
			FQuat InverseOrientation = Orientation.Inverse();

			// Store min and max extents in local space
			FVector MinExtents = -Extents;
			FVector MaxExtents = Extents;

			for (const FVector& Corner : OtherCorners)
			{
				// Vector from this OBB's center to the corner
				FVector LocalVec = InverseOrientation.RotateVector(Corner - Center);

				// Update min and max extents
				MinExtents = MinExtents.ComponentMin(LocalVec);
				MaxExtents = MaxExtents.ComponentMax(LocalVec);
			}

			// Step 2: Update extents and center
			// New extents are half the size of the new bounds
			FVector NewExtents = (MaxExtents - MinExtents) * 0.5f;

			// The center offset in local space
			FVector LocalCenterOffset = (MaxExtents + MinExtents) * 0.5f;

			// Update the center in world space
			Center = Center + Orientation.RotateVector(LocalCenterOffset);

			// Update extents
			Extents = NewExtents;

			// Keep the current rotation or handle rotation interpolation if needed
			if (SlerpRotation)
			{
				FQuat CombinedQuat = FQuat::Slerp(this->Orientation, Other.Orientation, 0.5f);
				Orientation = CombinedQuat;
			}

			return *this;
		}

		// Method to check if a point is inside or on the OBB
		bool IsInsideOrOn(const FVector& Point) const
		{
			// Transform the point to the OBB's local space
			FVector LocalPoint = Orientation.UnrotateVector(Point - Center);

			// Check if the point lies within the extents
			return FMath::Abs(LocalPoint.X) <= Extents.X &&
				FMath::Abs(LocalPoint.Y) <= Extents.Y &&
				FMath::Abs(LocalPoint.Z) <= Extents.Z;
		}

		// Method to check if this OBB is entirely inside or on another OBB
		bool IsInsideOrOn(const FOBBox& Other) const
		{
			// Step 1: Get the corners of this OBB
			TArray<FVector> Corners;
			Other.GetCorners(Corners);

			// Step 2: Check if each corner is inside or on the other OBB
			for (const FVector& Corner : Corners)
			{
				if (this->IsInsideOrOn(Corner))
				{
					// At least one corner is outside the other OBB
					return true;
				}
			}

			// All corners are inside or on the other OBB
			return false;
		}

		// Method to check if this OBB intersects with another OBB
		bool Intersect(const FOBBox& Other) const
		{
			// Use the Separating Axis Theorem (SAT)
			// Step 1: Get the axes of both OBBs
			FVector AxesA[3];
			this->GetAxes(AxesA[0], AxesA[1], AxesA[2]);

			FVector AxesB[3];
			Other.GetAxes(AxesB[0], AxesB[1], AxesB[2]);

			// Step 2: Compute the rotation matrix expressing Other in A's coordinate frame
			float R[3][3];
			float AbsR[3][3];

			for (int i = 0; i < 3; ++i)
			{
				for (int j = 0; j < 3; ++j)
				{
					R[i][j] = FVector::DotProduct(AxesA[i], AxesB[j]);
					AbsR[i][j] = FMath::Abs(R[i][j]) + KINDA_SMALL_NUMBER; // Add epsilon to avoid division by zero
				}
			}

			// Step 3: Compute the translation vector
			FVector t = Other.Center - this->Center;
			// Bring translation into A's coordinate frame
			FVector tA(FVector::DotProduct(t, AxesA[0]),
				FVector::DotProduct(t, AxesA[1]),
				FVector::DotProduct(t, AxesA[2]));

			// Step 4: Test axes
			float ra, rb;

			// Test axes L = A0, A1, A2
			for (int i = 0; i < 3; ++i)
			{
				ra = this->Extents[i];
				rb = Other.Extents.X * AbsR[i][0] + Other.Extents.Y * AbsR[i][1] + Other.Extents.Z * AbsR[i][2];
				if (FMath::Abs(tA[i]) > ra + rb)
					return false;
			}

			// Test axes L = B0, B1, B2
			for (int i = 0; i < 3; ++i)
			{
				ra = this->Extents.X * AbsR[0][i] + this->Extents.Y * AbsR[1][i] + this->Extents.Z * AbsR[2][i];
				rb = Other.Extents[i];
				if (FMath::Abs(tA[0] * R[0][i] + tA[1] * R[1][i] + tA[2] * R[2][i]) > ra + rb)
					return false;
			}

			// Test axis L = A0 x B0
			ra = this->Extents.Y * AbsR[2][0] + this->Extents.Z * AbsR[1][0];
			rb = Other.Extents.Y * AbsR[0][2] + Other.Extents.Z * AbsR[0][1];
			if (FMath::Abs(tA[2] * R[1][0] - tA[1] * R[2][0]) > ra + rb)
				return false;

			// Continue testing cross products of axes...
			// There are 9 cross-product axes to test in total

			// For brevity, we'll implement the rest in a loop

			static const int TestCases[9][4] = {
				{0, 1, 2, 0}, // L = A0 x B0
				{0, 1, 2, 1}, // L = A0 x B1
				{0, 1, 2, 2}, // L = A0 x B2
				{1, 2, 0, 0}, // L = A1 x B0
				{1, 2, 0, 1}, // L = A1 x B1
				{1, 2, 0, 2}, // L = A1 x B2
				{2, 0, 1, 0}, // L = A2 x B0
				{2, 0, 1, 1}, // L = A2 x B1
				{2, 0, 1, 2}  // L = A2 x B2
			};

			for (int k = 0; k < 9; ++k)
			{
				int i = TestCases[k][0];
				int j = TestCases[k][1];
				int m = TestCases[k][2];
				int n = TestCases[k][3];

				ra = this->Extents[m] * AbsR[(i + 1) % 3][n] + this->Extents[(i + 2) % 3] * AbsR[(i + 2) % 3][n];
				rb = Other.Extents[(n + 1) % 3] * AbsR[i][(n + 2) % 3] + Other.Extents[(n + 2) % 3] * AbsR[i][(n + 1) % 3];
				float tVal = FMath::Abs(tA[(i + 2) % 3] * R[(i + 1) % 3][n] - tA[(i + 1) % 3] * R[(i + 2) % 3][n]);

				if (tVal > ra + rb)
					return false;
			}

			// No separating axis found, the OBBs intersect
			return true;
		}

		// Method to convert the OBB to an FTransform
		FTransform ToTransform() const
		{
			// The position is the center of the OBB
			FVector Position = Center;

			// The rotation is the orientation of the OBB
			FQuat Rotation = Orientation;

			// The scale is twice the extents (since extents are half-sizes)
			FVector Scale = Extents * 2.0f;

			// Construct and return the FTransform
			return FTransform(Rotation, Position, Scale);
		}
	};

	void GothGirlDrawOBB(UWorld* World, FOBBox InOBB, FColor InColor, float Thickness)
	{
		TArray<FVector> Corners;
		InOBB.GetCorners(Corners);

		// Optionally, visualize the OBB using debug drawing
		for (int32 i = 0; i < 8; ++i)
		{
			for (int32 j = i + 1; j < 8; ++j)
			{
				if (FMath::CountBits(i ^ j) == 1) // Edges differ by one bit
				{
					DrawDebugLine(
						World,
						Corners[i],
						Corners[j],
						InColor,
						false,
						120.0f,
						0,
						Thickness
					);
				}
			}
		}
	}
};

4 Likes