How do I get a rotated bounding box between two randomly rotated parts?

  1. Explanation
    image
    The black box represents the typical bounding box. The red one is what I want to achieve. Essentially, I need the rotated bounding box of two randomly rotated parts that points from Part A to Part B (CFrame and Vector3 Size of the bounding box).
    I’m not really sure if “rotated bounding box” is the correct term for this.
  2. Issue
    I have no clue how you could do this. Many solutions are just about normal world space bounding boxes, while I need the rotated one.

I’m looking forward to someone assisting me; I’ve been trying to research this for some time with no luck.

2 Likes

Convert each 3D vertex to a 2D vertex in screen space, then use a 2D minimum bounding box algorithm like this.

You can look into Rotating calipers - Wikipedia.
image
They may be a way to find the oriented bounding box (also noticed they are in Emma’s post)
Found this as well, minimum-area-bounding-rectangle/min_bounding_rect.py at master · dbworth/minimum-area-bounding-rectangle · GitHub a python implementation

This looks terrifyingly more complicated than I anticipated. Perhaps it isn’t worth the trouble of attempting to implement this, especially given my lack of knowledge on the topic.

Maybe there’s an alternative, what do you need it for in the first place?

I’m using GetPartsInBoundsBox to create a hitbox detector.
Simply put, if you add a hitbox to a part with specific size and offset parameters, it will detect intersecting parts.

When the part moves from A to B, I want the hitbox to change size based on the distance travelled. As a result, as long as the part moves every frame, the hitbox determines its CFrame and Size.

Because the part can move at high speeds and in different orientations at any time, I figured the hitbox should simply copy the bounding box between positions A and B (A being the previous position of the part and B being the current position of the part).

The axis-aligned bounding box, on the other hand, is axis-aligned, and it does not work accurately. If the part moves at a 45-degree angle in any direction, the bounding box will be a whole square. So, I believe the solution is to rotate it (with an oriented minimum bounding box, as you suggested), which provides an accurate track of how the part travelled while taking up as little space as possible.

But it appears that this is not a simple problem. If there isn’t a way to accomplish what I’m looking for, it appears that I’ll have to simply form a hitbox around the part’s CFrame and Size and ignore how far the part travelled between two frames.

Interesting question. There’s a lot to unpack here.

First, let’s clarify the goal. We want to take two parts and find the minimum bounding box (not axis-align enforced) that contains both parts. More generally, find the minimum bounding box that contains an arbitrary point cloud.

There are two steps to answering this question:

  1. Write a function that will generate a bounding box given an arbitrary orientation
  2. Write a function that will pick the optimal orientation to minimize the bounding box.

I’m pretty confident I can answer #1 on the spot, but #2 is a bit more difficult.

Let’s talk about #1 for now:

When we do an axis-aligned bounding box (AABB) we simply get the max and min components of our point cloud in world space and then subtract.

local function getCorners(cframe: CFrame, size2: Vector3): {Vector3}
	local corners = {}
	for i = 0, 7 do
		corners[i + 1] = cframe * (size2 * Vector3.new(
			2 * (math.floor(i / 4) % 2) - 1,
			2 * (math.floor(i / 2) % 2) - 1,
			2 * (i % 2) - 1
		))
	end
	return corners
end

local function getPointCloud(parts: {BasePart}): {Vector3}
	local cloud = {}
	for _, part in parts do
		local corners = getCorners(part.CFrame, part.Size / 2)
		for _, corner in corners do
			table.insert(cloud, corner)
		end
	end
	return cloud
end

local function getAABB(parts: {BasePart}): (CFrame, Vector3)
	local cloud = getPointCloud(parts)
	local maxV, minV = cloud[1], cloud[1]

	for i = 2, #cloud do
		local point = cloud[i]
		maxV = maxV:Max(point)
		minV = minV:Min(point)
	end

	return CFrame.new((maxV + minV) / 2), maxV - minV
end

To extend this to accept an arbitrary rotation we approach it the same way except we treat the target orientation as the axis-aligned space. After we’ve figured out our min and max we can convert back to real world space to find the center for calculating the CFrame.

local function getOBB(oreintation: CFrame, parts: {BasePart}): (CFrame, Vector3)
	oreintation = oreintation.Rotation -- want X, Y, Z to be 0, 0, 0
	
	local cloud = getPointCloud(parts)
	for i = 1, #cloud do
		cloud[i] = oreintation:PointToObjectSpace(cloud[i])
	end
	
	local maxV, minV = cloud[1], cloud[1]

	for i = 2, #cloud do
		local point = cloud[i]
		maxV = maxV:Max(point)
		minV = minV:Min(point)
	end
	
	local wMaxV = oreintation:PointToWorldSpace(maxV)
	local wMinV = oreintation:PointToWorldSpace(minV)

	return CFrame.new((wMaxV + wMinV) / 2) * oreintation, maxV - minV
end


Using the green part to set an arbitrary orientation in the above video

All that leaves is #2 which I’d need to think about, but hopefully this can help get you started.

8 Likes

Didn’t find a nice easy solution in my short search (prob doesn’t exist), but you can take a look at this wikipedia page which references a paper that can compute the optimal orientation here:

2 Likes

I’m pretty sure that using your clever OBB function, you can just provide the lookAt CFrame between A and B for the function, and it’ll work fine! (@striker2783 discovered this solution initially)

while true do
	local DeltaTime: number = task.wait()
	local CFrame: CFrame, Size: Vector3 = getOBB(
		CFrame.lookAt(A.Position, B.Position),
		{
			A,
			B,
		}
	)
	Box.Size = Size
	Box.CFrame = CFrame
end


It works great!

1 Like

Hey, and many thanks for your assistance. I sent you a private message regarding your code because I had a few questions. Or should I simply ask them here?
I was mostly wondering why you used PointToObjectSpace instead of PointToWorldSpace in the first part of your code. According to my understanding, PointToObjectSpace will rotate the given corners by the opposite (or inverse) of the orientation variable. If this is correct, why not just rotate the corners by the regular orientation?

I wouldn’t think about those methods as rotations or inverses of rotations. It’s too general an understanding of the transformations and doesn’t really include the perspective of what the values spit out of these methods mean.

:PointToObjectSpace recontextualizes a world space vector3 in into space relative to the the provided CFrame.

In Roblox (and most game engines) we treat the identity CFrame as the origin of all things world space. When you read a part’s Position value it’s coordinates are relative to the world origin CFrame.identity.

Sometimes however, it’s useful to recontextualize those coordinates based on a completely different origin/rotation. What was once up, may now be considered down in this new coordinate space.

So, what I’m doing is first is transforming all the points in the cloud relative to my new orientation, finding the AABB in this new coordinate space, then converting back to world space where those results are actually useful to us.

Hope that helps a bit.

2 Likes

Your explanations seem reasonable to me. I’ve sketched out how the code works so I can comprehend the brilliance, and it helps me understand what you’re doing.

Do you think I’m on the right track? And, if we’re on the same page now, I’m curious whether it makes a difference whether you call:

:PointToObjectSpace

or

:PointToWorldSpace

on the provided corner cloud. Both ways, I think, achieve the same effect since the corners maintain the same distance between each other, allowing the same size of an AABB to be created. But that’s just my guess. I could just test it myself.

I also recognized, as you mentioned, that this would not create the most ideal orientation to minimize size, but that’s no huge deal. You’ve helped me a lot.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.