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