Detect if a block or an assembly can rotate?

I have a set of voxels (4x4x4 part instances)
I also keep a list of these in memory, and organize the list in ‘assemblies’ groups of connected voxels that should all move together.

Suppose I want to rotate the entire assembly (cframing) from the pivot point of a particular voxel in the assembly by 90 deg. What sort of insane dark magic math would I need to do, to make sure my voxels would not collide with any other voxels on the map that would be in the assembly’s rotation area?

Even if my assembly is only one voxel. It should not be able to rotate, if another (not in the same assembly) voxel is right beside it, because its corners would pass through the other voxel as it tried to rotate 90 degrees.

Thanks for any help

1 Like

To rotate an assembly of voxels without colliding with other voxels, you need to determine the pivot point within the assembly and calculate the new positions of the voxels after rotation. Then, you should check for collisions between the rotated assembly and other voxels, handle collisions if necessary, and finally apply the rotation to the assembly. This process involves using collision detection algorithms and spatial partitioning techniques to ensure the rotated assembly does not overlap with other voxels.

Do you have any articles or links on this sort of thing?
I don’t even know what I should be googling for.

1 Like

Here’s my best shot at an explanation for what I think would be the process for this, sorry if I misunderstood anything or if it is poorly explained.

Imagine we’re doing this to pixels instead of voxels, to make the problem easier.

Here are two connected pixels which you call assemblies.
image

In the game world, these pixels would fit onto a grid, where the centre of the grid is the origin of the world. You can imagine this grid as a 2d array.

So the array of our game world may look something like this

world = {
     {0 ,0 ,0}
     {0, 0, 1}
     {0, 0, 1}
}

Where the 1’s represent places where there are pixels and the 0s are places where there isnt any pixels.

Lets say we want to rotate the assembly at world[3][3] (the bottom right 1). 90 degrees counter-clockwise
The steps I would take:

  1. Given the pivot point world[3][3], find which pixels are connected to it, and add all of these to a list called assembly.
  2. Now, knowing the points of the assembly, convert these points in the 2d array world into coordinates, relative to the pivot point (so the pivot point is (0,0) and that say a pixel pixel to the right of it would be (0,1)
  3. Iterate throughout these points in the assembly and apply the appropriate matrix multiplication, if the “new point” of the point tries to overwrite a point in the world which is already 0, then we must be colliding with another pixel or assembly, so cancel the rotation. (One issue with this is that the 1 we are overwriting could be apart of the current assembly we are working on, you just need to check if this is the case)

Okay… so thats quite complex.

First we need the assembly - the group of connected pixels.
In the visualisation, these would just be the ones which are orthogonally adjacent. Meaning, what is the value of point which is “above” the pivot, as well as “right” “left” and “down”. So given the pivot point world[3][3], if we add 1 to the value of the second bracket, we would get the value which is “right” of the pivot point. -1 for the value to the left and apply these sums to the first bracket to find the “up” value and the “below” value.
We find all the points which are adjacent to the pivot, and check if they are 1, if they are 1, there is a pixel there, so it must be part of the assembly.
Here is a visualisation, where the pink pixel is the pivot:

We get the places you see there, and check if they are 1 or 0, the ones which are 1 will be added to an assembly list.

Cool, now we have a list storing the values which represent the grey part, the pink part and the other grey part. So the grey parts connected to the pink part. But you and I both know that that other grey part should belong to the assembly, as they’re connected in a chain along the other grey parts.
To do this, after finding the points around the pink part, we can imagine that the point world[2][4] becomes the pink part and we check all the values around it, then we do the same for the other grey part world[4][4], and we would find that world[4][3] is 1, so must be a part of this assembly. Once no more parts around the parts in the assembly are equal to 1, then we know we must have the full assembly.

With a list of all the points in the 2d array, we must now find there coordinates
If we imagine the top left to be the origin (world[1][1]) then we need to find the coordinates of the points in the assembly relative to it. HOWEVER, we’re getting these coordinates all in the hope that we can use matrix multiplication to rotate the entire assembly, but for that, if the origin was world[1][1], the entire assembly would rotate about the top left corner, not the pink pivot. So instead, we need to find the coordinates of the points in the assembly relative to the pink pivot.

This is quite simple (I imagine):
image
Given this part of the assembly, the point at world[2][4] has the first index which is 3-2 = +1. So it is 1 above the pink part, so the coordinate for the y component is 1. X would be 4-4 = 0, so the coordinate of world[2][4] relative to the pivot world[3][4] is (0,1), which, just by looking at it we know is true!
This same principle can be applied to all the points in the assembly, to work out how th point at world[4][3] is (-1,-1). Then we store all these coordinates in a list.

Phew…

Final step, apply the appropriate matrix multiplication to all the coordinates, then turn the new coordinate into a point in the 2d array, and check if that point is already occupied, if it is then we are intersecting some other pixel/assembly
So, iterate throughout the coordinates list. Apply the appropriate matrix multiplication, which in our case for “Lets say we want to rotate the assembly at world[3][3] (the bottom right 1). 90 degrees counter-clockwise” would be:


That one.

Then, given this new coordinate, turn that into a position in the world array by reversing the steps in step 2. And check if the world array already has a 1 at this point, if it does, we’re colliding with another assembly and the rotation should be cancelled, if not we can let that point be 1.
If we iterate through all the coordinates in the assembly without ever colliding, we can allow the assembly to rotate.

And we’re done.

So potential problems:

  • Firstly, that bit in italics, is like that because I dont think that would actually work, or at least it requires some extra step. Too tired and late to really look into right now.
  • Secondly, you need to actually visualise this in the roblox workspace, by using the 2d world array to generate those 4x4 parts you were on about, how you update this is up to you.
  • And quite possibly the biggest complication, this was all about 2d arrays and pixels, but we actually want to do this with voxels, so 3d space. Obviously this means turning the world array into a 3d array, but then as for the matrix multiplication, I’m not sure. I’m slightly sure that when working with 3d space, the matrix multiplication for the same rotation ( say 90 counter clockwise) will also change in some way, not sure though, not learnt that in school yet :man_shrugging:

So yeah, thats my best shot at an explanation, hopefully you found it helpful and that I actually understood what you’re trying to achieve. Any questions just ask.

Oh yeah, and that @F0ZYAN guy is using ChatGPT to generate responses, someone called him out in some other post.

1 Like

Another ChatGPT-generated post, don’t you just love it?

1 Like

I probably didn’t explain very well in my original post. Let me add some pictures.
I should have stated, I already have the voxels in assemblies, and I even put all instances of an assembly in a ‘model’ instance, so I can use the model’s cframe, for the actual physical rotation.
I just don’t know how to detect the following situation

Suppose I start with this…
image
One assembly is the gray blocks, along with the pink (the pink being its pivot)
the second assembly is the blue block all on its own

The end result of a successful clockwise rotation, would look like this…
image

However, I would like to detect that this rotation should fail, because of the following images…
image
image
image
image
image

I would need some way (not using roblox collision since I will cframe the parts) to determine, that there will be overlap by corners, or edges or whatever, and not allow the rotation.

Sorry I wasnt clear with my original request, I appreciate all the time you put into your reply.

1 Like

After writing that post I actually had a thought that that kind of thing is what you meant lol.
Anyway, here is something I’ve come up with.

Its a little hacky, and doesn’t use any dark magic math. One more note, you said that you can’t use roblox collision since you’re going to CFrame the parts, however, I am CFraming the parts in the video and yet I still used roblox collision. Perhaps I’m misunderstanding something though.

Okay, so the assembly you see in the video is made up of part instances like you stated, and all of those part instances are in a model instance.
image
The highlight is purely cosmetic.

The pivot point is anchored, whilst the voxels are not, and the voxels are welded to the pivot point in a chain.


Firstly, the pivot has a click detector and script which handles the script detector, this isn’t massively important, but here is the code anyway.

--== References ==--
local pivot = script.Parent
local model = pivot.Parent
local assembly = {} --> Array of all the part instances in this assembly

local Rotater = require(game:GetService("ServerScriptService").Rotater)

--Find Assembly (setup)
for _, instance in pairs(model:GetChildren()) do
	if instance:IsA("Part") then
		table.insert(assembly, instance)
	end
end

--== Functions ==--
script.Parent.ClickDetector.MouseClick:Connect(function()
	Rotater.rotateAssembly(model, assembly, -90)
end)

Okay, so how does the Rotater module and its “rotateAssembly” method actually work.

Say we’re given this problem: “Rotate this assembly 90 degrees clockwise”


What its going to do, is repeatedly rotate the assembly by some small increment, enough times so that the number of times rotated * increment is equal to the 90 degrees movement.
So, rotate it by 10 degrees 9 times, or 18 degrees 5 times.

Here is the first 18 degree rotation.


What the script will then do is (for each 18 degree rotation) loop through all the parts in the assembly, and use the BasePart:GetTouchingParts() method, which returns an array of all the touching parts. If this array has a length greater than 0, then the assembly would have collided with something. So we set some boolean called “canRotate” to false.

That way, after all of these small increments have been complete, if and only if the canRotate variable (which was originally true, but then toggled to false when a collision is detected) is true, then we can go ahead with the rotation, otherwise, set the assemblies primary part cframe back to its original cframe.

This also works with player characters in the workspace.

Here is the code for the module, which actually handles this rotation.

local module = {}

function showHighlight(highlight) -- Cosmetic function
	spawn(function()
		highlight.Enabled = true
		wait(0.2)
		highlight.Enabled = false
	end)
end

function module.rotateAssembly(assemblyModel, assembly, degrees)
	local rotationAllowed = true
	local startCFrame = assemblyModel.PrimaryPart.CFrame --> That way, if the rotation is a failiure, we can snap it back to the original cframe

	local fidelity = 5 --> I call this fidelity, not sure if its an appropriate name though, the higher the fidelity, the smaller the rotation amount and the more iterations
	-- this probably means more accurate collision detection, at the cost of more performance
	for i = 1, fidelity do
		local rotationAmount = degrees / fidelity --> Work out how much we have to rotate each step.

		assemblyModel.PrimaryPart.CFrame = assemblyModel.PrimaryPart.CFrame * CFrame.Angles(0, math.rad(rotationAmount),0) --> Slightly rotate the assembly

		for _, voxel in pairs(assembly) do --> For all of the voxels in the assembly
			local touchingParts = voxel:GetTouchingParts()

			if #touchingParts > 0 then --> If any are touching another voxel (or just part for that matter)
				rotationAllowed = false --> Then toggle this boolean
			end
		end
	end
	
	assemblyModel.PrimaryPart.CFrame = startCFrame --> After doing the small increments, reset the cframe
	if rotationAllowed then
		assemblyModel.PrimaryPart.CFrame = assemblyModel.PrimaryPart.CFrame * CFrame.Angles(0, math.rad(degrees),0) --> Important bit, rotate the assembly if we can
		
		-- Cosmetic
		assemblyModel.Highlight.FillColor = Color3.fromRGB(0, 255, 0)
		showHighlight(assemblyModel.Highlight)
	else
		-- Cosmetic
		assemblyModel.Highlight.FillColor = Color3.fromRGB(255, 0, 0)
		showHighlight(assemblyModel.Highlight)
	end
end

return module

And here is the file:
Assembly Rotation.rbxl (49.5 KB)

Sorry for any more misunderstandings or poor explanations, any more questions just ask.

I will give this a try, I didn’t know that :GetTouchingParts would detect when using cframes.
Thanks for the write up.

1 Like