Getting all positions inside a shape

What do you want to achieve? : I want to get a table list of all Vector3s with integer components (x, y, z must be integer) inside an area.


( Goal is to get all blue parts’ positions inside the red circle )


( same goal with the above, but a box instead of circle )

RECALL:

I am NOT trying to get 2D positions of these parts. The studs and shapes are simplified into 2D for easier understanding.

I am trying to get all integer Vector3s inside a Sphere or a 3D Box.

What is the issue? : I can’t find a way to optimize this. Looping through all the studs will crash the entire game, only going through a portion can still significantly impact the game’s performance.

Are you looking for something like this?

1 Like

Imagine if you already had each of these integer vector3’s in an ordered list for example the cube. It would be a list of 1000 Vector3s if the cube had sidelength 10.

You can imagine a function h(n, Sidelength) that would index that hypothetical array at n for this cube with the sidelength SideLength. However instead of indexing this nonexistent array, it would calculate what the value at that index would be using math. The same concept can apply to the sphere, although the math will be a lot trickier.

Of course I have to ask how you intend on using this information (the Vector3s), as you will run into the same lag issues even using this approach if you try to brute force calculate all values.
So instead holding all possible values, just let this mathematical function act as your array and calculate the ones you need on the spot. You could even implement a cache if you’re going to frequently reuse certain values.

Use Roblox’s API.
workspace:GetPartBoundsInRadius(Vector3, number, OverlapParams)
workspace:GetPartBoundsInBox(CFrame, Vector3, OverlapParams)

If you are searching for best practices, these methods are already pretty performant.
Depending on the size of the request, it can lag. But if it gets to that point, you should have an alternative.

You could cache all the positions of the parts and update them regularly.
Create a function to iterate through and compare distance via a magnitude check. This can act just as well as the GetPartBounds methods.

I would just use a workspace:GetPartBoundsInBox operation, then loop through the table and log each parts coordinates. For a flat circle I think I would use a workspace:GetPartBoundsInRadius then loop through the table that the operation gives me and filter by a specified Y coordinate, maybe to optimize just alter the way the grid works, this may not be an option for what you’re doing but I would just increase the gap between for each part so less items can be found.

Here is my approach, this assumes that the block is not rotated. Not sure if it works, you should test it yourself:

-- Returns every possible integer position in an unrotated block/ball.
local function getIntPositionsInBounds(boundPart: Part)

	local intPositionArray: {Vector3} = {}

	if boundPart.Shape == Enum.PartType.Block then
		-- This assumes that the block is not rotated.

		local lowerBounds = (boundPart.Position - boundPart.Size / 2):Ceil()
		local higherBounds = (boundPart.Position + boundPart.Size / 2):Floor()

		for x = lowerBounds.X, higherBounds.X do
			for y = lowerBounds.Y, higherBounds.Y do
				for z = lowerBounds.Z, higherBounds.Z do
					local intPosition = Vector3.new(x, y, z)
					table.insert(intPositionArray, intPosition)
				end
			end
		end

	elseif boundPart.Shape == Enum.PartType.Ball then
		-- It wouldn't matter if you rotated the ball.

		-- Act like the ball is a block, if the distance between the point and
		-- the position of the ball is bigger than the radius of the ball then
		-- ignore that position and continue to the next one.
		local lowerBounds = (boundPart.Position - boundPart.Size / 2):Ceil()
		local higherBounds = (boundPart.Position + boundPart.Size / 2):Floor()
		
		for x = lowerBounds.X, higherBounds.X do
			for y = lowerBounds.Y, higherBounds.Y do
				for z = lowerBounds.Z, higherBounds.Z do
					local intPosition = Vector3.new(x, y, z)
					if (intPosition - boundPart.Position).Magnitude > boundPart.Size.X / 2 then continue end
					table.insert(intPositionArray, intPosition)
				end
			end
		end
	else
		error("'boundPart' is not a block or a ball.")
	end

	return intPositionArray
end

print(getIntPositionsInBounds(-- Block/ball here.))

You could also add a check to see if the block is rotated (and perhaps error if it is), hope this helps!

Use raycasting algorithm, Roblox implemented raycast, tho it’s really raycast + vector, you need only raycast

What raycast is, is simply projecting a infinite line onto a 2D shape, for 3D you must repeat 2D for each height point

Then you check if number of points is even - it’s outside or uneven - it’s inside

This algorithm might be very expensive and for 3D shapes, take very long time to generate

a lot of math later….

I went ahead and wrote a short module to illustrate my idea. As I mentioned, instead of storing the exponential amount of Vector3s it simply calculates the one it needs. I wrote the vast majority of this on my phone so I apologize if it errors.

--!strict
local Query = {}

Query.Sphere = {}
Query.Cube = {}

function newQuery(ScalarName: string, ScalarValue: number, CF: CFrame?, SampleFunction: (index: number, Scalar: number) -> Vector3?)
	local Query = {
		_Cache = {};
		[ScalarName] = ScalarValue;
		CFrame = CF or CFrame.new();
	}

	--[[
	Samples the query at index to retrieve a point.
	]]
	function Query:Sample(index: number): Vector3?
		if not self._Cache[index] then
			local Position = SampleFunction(index, self[ScalarName])

			if Position then
				self._Cache[index] = self.CFrame * Position
			end
		end

		return self._Cache[index]
	end

	return Query
end

--[[
Determines the position of the point corresponding to the nth (index) element in a hypothetical ordering of integer triples in a sphere of radius Scalar.
The ordering assumes an XZY coordinate system.
]]
function Query.Sphere.GetSphereCell(index: number, Scalar: number): Vector3?
	if index >= 1 then
		local Position = Vector3.zero
		local FoundZ = false

		for Z = -math.floor(Scalar), math.floor(Scalar) do
			local PlanarRadius = math.floor(math.sqrt(Scalar ^ 2 - Z ^ 2))
			local PlaneCount = 0

			for Y = -PlanarRadius, PlanarRadius do
				local LinearRadius = math.floor(math.sqrt(Scalar ^ 2 - Z ^ 2 - Y ^ 2))

				PlaneCount += 2 * LinearRadius + 1
			end

			if index > PlaneCount then
				index -= PlaneCount
			else
				Position += Z * Vector3.zAxis
				FoundZ = true
				break
			end
		end

		if FoundZ then
			local ZPlanarRadius = math.floor(math.sqrt(Scalar ^ 2 - Position.Z ^ 2))

			for Y = -ZPlanarRadius, ZPlanarRadius do
				local LinearRadius = math.floor(math.sqrt(Scalar ^ 2 - Position.Z ^ 2 - Y ^ 2))
				local RowCount = 2 * LinearRadius + 1

				if index > RowCount then
					index -= RowCount
				else
					Position += Y * Vector3.yAxis
					break
				end
			end

			local YLinearRadius = math.floor(math.sqrt(Scalar ^ 2 - Position.Z ^ 2 - Position.Y ^ 2))
			Position += (index - YLinearRadius - 1) * Vector3.xAxis

			return Position
		end
	end
	
	return nil
end

--[[
Determines the position of the point corresponding to the nth (index) element in a hypothetical ordering of integer triples in a cube of sidelength Scalar.
The ordering assumes an XZY coordinate system.
]]
function Query.Cube.GetCubeCell(index: number, Scalar: number): Vector3?
	if 1 <= index and index <= Scalar ^ 3 then
		return Vector3.new(
			index % Scalar,
			math.ceil(index % Scalar ^ 2 / Scalar),
			math.ceil(index / Scalar ^ 2)
		)
	end
	
	return nil
end

--[[
Creates and returns a sphere which can be sampled from.
]]
function Query.Sphere.new(Radius: number, CFrame: CFrame?)
	return newQuery("Radius", Radius, CFrame, Query.Sphere.GetSphereCell)
end

--[[
Creates and returns a cube which can be sampled from.
]]
function Query.Cube.new(Sidelength: number, CFrame: CFrame?)
	return newQuery("Sidelength", Sidelength, CFrame, Query.Cube.GetCubeCell)
end

return Query

An example use case would be like so:

local ShapeQuery = require(path_to_module)
local Sphere = ShapeQuery.Sphere.new(50, CFrame.new(300, 50, 300))

local Position = Sphere:Sample(325000) -- ONLY calculates and caches the 325000th position, as opposed to all 523000 in a radius 50 sphere