Position at random surface extent

I am trying to position parts at the extents of a given part, the parts must never be inside of the main part. A good use case would be positioning gems or ore onto a rock… Here is what I have right now - however this solution only guarantees random positioning on the Y axis, as I cannot figure out how to guarantee on all three without ending up on the inside.


( the ~=0 checks are to avoid 0 being selected as the random integer (to keep on the extents) )

Is there some way I could do this by manipulating the normals of the main part? I’m not very good with math so I am struggling ^^

Any help is appreciated!

2 Likes

Is the shape of the rock always a rectangular prism? Or is it a mesh/combination of parts, or do you want to position the gems on a sphere around the rock?

The shape of the rock, currently, is always a rectangular prism, as I figured it will be must easier to calculate this type of generation on a constant shape… Not sure how I’d handle odd mesh parts without multiple rectangular ‘nodes’ to spawn the gems onto.

At this time, I’d just like to position the gems on a rectangular/square part, at a random position on the extents of the part.

Then try this:

  1. set bounds = 1/2 the part size
  2. create a new vector3 with either its x, y, or z value = to +/- bounds x, y, or z respectively. Ex: create a vector with the y component the -y component of the bounds vector. The other two components can be any random numbers between their respective +/- bound components
  3. call rock.CFrame:positionToWorldSpace() on the new vector and you have your gem position!
2 Likes

Sorry, I’m having a tough time understanding exactly what you mean. Could you provide a quick example?

Put this together quickly for you. Note that a generated point is equally likely to be on each face, so it is not a uniform distribution over the surface area.

local RNG = Random.new()

local function RandomPointOnPart(Part)
	local c = Part.CFrame
	local s = Part.Size * 0.5
	local sx, sy, sz = s.x, s.y, s.z
	
	local x = RNG:NextNumber(-sx, sx)
	local y = RNG:NextNumber(-sy, sy)
	local z = RNG:NextNumber(-sz, sz)
	
	local f = RNG:NextInteger(1, 6)
	if f == 1 then -- clamp +x
		return c * Vector3.new(sx, y, z)
	elseif f == 2 then -- clamp -x
		return c * Vector3.new(-sx, y, z)
	elseif f == 3 then -- clamp +y
		return c * Vector3.new(x, sy, z)
	elseif f == 4 then -- clamp -y
		return c * Vector3.new(x, -sy, z)
	elseif f == 5 then -- clamp +z
		return c * Vector3.new(x, y, sz)
	elseif f == 6 then -- clamp -z
		return c * Vector3.new(x, y, -sz)
	end
end
1 Like

ah, I see. I guess I was not too far off, I just was avoiding a big if/elseif chain!

Thank you guys :slight_smile:

There’s almost certainly a much neater way to do this, but I’m glad it helped.

Here is what I was thinking of.

-- Beware! Untested code ahead!
local components = {'X', 'Y', 'Z'}
local signs = {1, -1}
local function getGemPos(rock)
	local bounds = rock.Size/2
	local vector = {}

	local component = components[math.random(1, 3)]
	local sign = signs[math.random(1, 2)]
	vector[component] = sign * bounds[component]
	for i, component in ipairs(components) do
		if not vector[component] then
			vector[component] = math.random() * bounds[component] * 2 - bounds[component]
		end
	end

	return rock.CFrame:positionToWorldSpace(Vector3.new(vector.X, vector.Y, vector.Z))
end
1 Like

lol and you hadn’t seen my code yet…

this worked great! If i passed a BasePart instance (the gem) as the second parameter, which line would I add the +/- size/2 to center the gem on the surface?

edit: never mind, I just realized how simple of an answer that is

While not very elegant, there is a solution if you wanted to support attaching to any kind of object. You could make a random vector, multiply it by the parts size plus a bit extra, add this vector to the parts position then raycast back to the part.

2 Likes

true, because I can use the intersecting point returned from the ray as the position.

I decided to clean it up a bit… I like this version a lot more. I took some of what @sircfenner did too. It is down to only 12 lines!

local components = { 'X', 'Y', 'Z' }
local signs = { 0.5, -0.5 }
local RNG = math.random

local function getPosOnPart(part)
	local vector = {}
	for _, component in pairs(components) do
		vector[component] = RNG() - 0.5
	end
	vector[components[RNG(1, 3)]] = signs[RNG(1, 2)]
	return part.CFrame * part.Size * Vector3.new(vector.X, vector.Y, vector.Z)
end
2 Likes

I ended up switching over to MeshParts, as I know I will end up doing that in the long run. I am using a raycasting method to get a random surface point of the mesh.

I am using a variation of @IdiomicLanguage’s orignal response (before edit) as a backup, in case a raycast ever fails (shouldn’t happen)

For anyone who may be curious, this is my end product:

which ends up with some pretty nice results:

Plenty of room for improvement but gets what I’m trying to do done!

8 Likes

You can actually simplify this a bit, by simply getting a random point on the surface of a sphere centered at the part’s position, and then raycasting inwards from that point.

2 Likes