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

I’m using C++ and trying to do a sphere trace after an arbitrarily-sized projectile hits its target, in order to find the projectile’s entry point. For the sphere trace, I must know the radius of the projectile, which is dynamic and can be a rock, an arrow, etc.

I’ve read about bounding boxes, and used GetActorBounds and other ways to get the bounds of the projectile, including getting an FBox struct from the mesh’s “Bounds” attribute, but bounds seem to be axis-aligned in all cases (AABB), which means that they don’t represent the true size of the object, and change with its orientation.

Is there a way to obtain an “object-oriented” bounding box that returns the true size of the object, disregarding orientation? An [FOrientedBox][1] struct exists, but I’ve found no way to get one from the actor or mesh, so it might not be possible.

Additionally, if the above is impossible, is there a way to get, at runtime, the approximate size of a mesh, as reported in the mesh editor?

51547-approx.png

How has this not been answered yet, this was asked in 2015. Getting OBB is very important. Anyone know?

Seconded - I’d like to know this as well.

I’m still interested too if anyone know !

To get mesh size in C++ code:

auto MeshComponent = Cast<UStaticMeshComponent>(Actor->GetRootComponent());
auto StaticMesh = MeshComponent->GetStaticMesh();
auto Bounds = StaticMesh->GetBounds();

It will return FBoxSphereBounds struct which has SphereRadius and BoxExtent and of course Origin

Then you can use code like this:

FRotator Rotator = Actor->GetActorRotation();
FVector RotatedVector = Rotator.RotateVector(Bounds.BoxExtent);

But remember, the RotatedVector will be rotated BoxExtent and if you will use it in axis aligned coordinates it wont give you results that you want. E.g. if you will try to use it in DrawDebugBox. At least, it should make your life easier.

You can calculate your own Oriented Bounding Box (OBB) with a center and three extent axes using the following code:

// Get the AABB in Local space (aka Object space: such as in the Blueprint viewer). You might want to cache this result as this may be costly. 
const FBox Box = Actor->CalculateComponentsBoundingBoxInLocalSpace();
const FTransform Transform = Actor->GetTransform();

// Get World space Location.
const FVector Center = Transform.TransformPosition(Box.GetCenter());

// And World space extent
const FVector Extent = Box.GetExtent();
const FVector Forward = Transform.TransformVector(FVector::ForwardVector * Extent.X);
const FVector Right = Transform.TransformVector(FVector::RightVector * Extent.Y);
const FVector Up = Transform.TransformVector(FVector::UpVector * Extent.Z);

Source: How to calculate an Actor Oriented Bounding Box (OBB) With Unreal Engine – Flo GameDev blog

1 Like

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
					);
				}
			}
		}
	}
};