How to make this function less arbitrary and ugly?

For a project of mine, I recently wrote a function that allows me to specify a CFrame, Size, NumPoints, and Minimum Distance(for distribution). It’s supposed to create NumPoints points evenly distributed within that volume.

It “works” , but my main two issue of concern are:

  • I’m worried its performance is almost entirely based on the RNG
  • Having to specify a minimum distance manually that works with the target size is really tedious and I feel like it could be done automatically
  • Everything feels arbitrary

Here’s the function

function SharedUtils:GetRandomDistributedPointsWithinVolume(cframe,size,numPoints,distanceThreshold)
    local position = cframe.p;

    --[[
    This is just a simple grid based chunk thingy I made,
    it's comparable to using an octree here and is just made to speed up iteration
    by only checking things around the candidate point. 3,5 is 3 dimensions, 5x5x5 studs
    also really ugly and arbitrary in this case
    --]]

    local chunkSystem = ChunkSystem.new(3,5); 


    --

    local Points = {};
    local attempts = 0;
    local maxAttempts = 50; -- lol

    while #Points<numPoints do

        if attempts > maxAttempts then
            warn("Max attempts reached! Try setting a smaller threshold!");
            break;
        end

        local rx = RNG:NextNumber(position.x-size.x/2,position.x+size.x/2);
        local ry = RNG:NextNumber(position.y-size.y/2,position.y+size.y/2);
        local rz = RNG:NextNumber(position.z-size.z/2,position.z+size.z/2);

        local randomPoint = Vector3.new(rx,ry,rz);

        local candidateChunk = chunkSystem:GetChunk(randomPoint,true);
        local objectsFound = candidateChunk:GetObjects("Surrounding");
        
        local tooClose;
        for i,objectFound in ipairs(objectsFound) do
            local dist = (objectFound.Position - randomPoint).magnitude;
            if dist <= distanceThreshold then
                tooClose=true;
                break;
            end
        end
        
        if not tooClose then  -- If the candidate is not too close to another previously placed point, add it to the chunk insert and repeat
            candidateChunk:AddObject({Position=randomPoint});

            local relative = CFrame.new(randomPoint) * (CFrame.new(position)):Inverse(); 

            randomPoint = (cf * relative).p; -- Moves the point to conform to the cframe/orientation

            table.insert(Points,randomPoint);
            attempts=0;
        else
            attempts = attempts + 1;
        end
    end
    return Points;
end

Preferably I’d like something that is more reliable and consistent in performance, and preferably doesn’t need a “minimum distance” variable to be manually found for the target size. It should still be fast and work for just about any size I can throw at it.

I’m a bit stumped on this.

Thanks for your time, interested in seeing what ideas you guys have :slight_smile:

To make it less “Ugly”, I used the Minifier plugin by Stravant which allows me to make a script shorter or better

function SharedUtils:GetRandomDistributedPointsWithinVolume(cframe, size, numPoints, distanceThreshold)
	local position = cframe.p;
	local chunkSystem = ChunkSystem.new(3, 5); 

	local Points = {};
	local attempts = 0;
	local maxAttempts = 5000000000000000000000000000000; -- lol to you too

	while #Points < numPoints do

		if attempts > maxAttempts then
			warn("Max attempts reached! Try setting a smaller threshold!");
			break;
		end

		local rx = RNG:NextNumber(position.x - size.x / 2, position.x + size.x / 2);
		local ry = RNG:NextNumber(position.y - size.y / 2, position.y + size.y / 2);
		local rz = RNG:NextNumber(position.z - size.z / 2, position.z + size.z / 2);

		local randomPoint = Vector3.new(rx, ry, rz);

		local candidateChunk = chunkSystem:GetChunk(randomPoint, true);
		local objectsFound = candidateChunk:GetObjects("Surrounding");
        
		local tooClose;
		for i, objectFound in ipairs(objectsFound) do
			local dist = (objectFound.Position - randomPoint).magnitude;
			if dist <= distanceThreshold then
				tooClose = true;
				break;
			end
		end
        
		if not tooClose then  -- If the candidate is not too close to another previously placed point, add it to the chunk insert and repeat
			candidateChunk:AddObject({
				Position = randomPoint
			});

			local relative = CFrame.new(randomPoint) * (CFrame.new(position)):Inverse(); 

			randomPoint = (cf * relative).p; -- Moves the point to conform to the cframe/orientation

			table.insert(Points, randomPoint);
			attempts = 0;
		else
			attempts = attempts + 1;
		end
	end
	return Points;
end

Sorry if I’ve misunderstood something.

The main thing that the above beautified code does better is have consistent spacing. For instance, in your code when you write a = b right now you sometimes put spaces around the equals and other times just write it all as one blob with no spaces. Same problem with some other operators like + and commas in argument lists.

Adopting a consistent style for this across all of your code for those cases will help you out in the long run.

1 Like

While I appreciate that by ugly I meant more of how it works rather than how it looks

1 Like

Here’s an alternative algorithm you could apply which doesn’t take a potentially arbitrarily large number of iterations to get a good result:

  1. Randomly generate the points, evenly distributed over the space
  2. Calculate the Delaunay triangulation (or slightly less efficiently use your chunk system) to find each pair of nearby points
  3. For each pair of points which is closer than the threshold, add a “pusher” (a spring but it can only push the points further apart) between them
  4. For some number of iterations, “push” each pair of points which were too close further apart along the axis separating them such that they’re no longer too close (The reason you need more than one iteration is that if you have a big clump of points the central ones will get pushed back and forth a bit while the outer ones “spread out” over the course of a few iterations)
  5. To avoid having to choose the threshold manually, you can use a formula like this to figure out what the value “should be” for the number of points passed and the size of the area.
3 Likes

Quick note: you can use numbers such as 9e18 instead of spamming 5’s.

1 Like

You didn’t have to tag me since it was a small joke, but thanks anyways!