math.randomUnitVector()

I often need to generate random unit vectors in 3-space.

It’s non-trivial.

It would be cool if this were a library function.

25 Likes

I haven’t fully read the linked article, but why can’t you just generate random numbers in range of 0…2pi as yaw and pitch, and convert those into a direction unit vector? This is something done pretty frequently for FPS cameras in other game engines, so the math for this should be accessible.

It would be nice to have a helper function for this, but I’m not sure I understand why this isn’t trivial. Note I haven’t tried to do this myself, but conceptually it seems simple enough provided you just want a unit vector.

E.g. maybe this is helpful

3 Likes

That results in a bias towards the extremes of the pitch value. Because though 0 azimuth and 180 azimuth should be pretty much opposite, they have almost no difference as elevation approaches 90. It is fine in most cases I’m sure, but it is not a perfect system to get a random vector.

4 Likes

Ah yeah I see now, coordinates in this kind of system are biased towards the poles, which you can kind of see if you look at a polar coordinate system.

1 Like

I implemented the function in the article you linked. It uses the Box-Muller transform to convert two uniformly distributed points into two normally distributed points. These are used for the x and y coordinates. Then, it uses the Box-Muller transform again, but since we only need one more axis, it only generates one normally distributed point.

local function randomUnitVector()
	local sqrt = math.sqrt(-2 * math.log(math.random()))
	local angle = 2 * math.pi * math.random()

	return Vector3.new(
		sqrt * math.cos(angle),
		sqrt * math.sin(angle),
		math.sqrt(-2 * math.log(math.random())) * math.cos(2 * math.pi * math.random())
	).Unit
end

Here’s the result of generating 5000 points.


And for completeness, I implemented the yaw/pitch suggestion that @PeZsmistic linked. This does indeed cluster points at the poles.

local function biasedRandomUnitVector()
	local yaw = math.random() * 2 * math.pi
	local pitch = math.random() * 2 * math.pi - math.pi
	
	return Vector3.new(
		math.cos(yaw) * math.cos(pitch),
		math.sin(pitch),
		math.sin(yaw) * math.cos(pitch)
	)
end


40 Likes

Will definitely be borrowing this, thanks.

I still think it’s generically useful enough to be added a library function, either to math or as a constructor for Vector3.

7 Likes

Indeed, it should be in the API somewhere. Put in a proposal to add it as Random::NextUnitVector() so that it can be used in procedural generation tools too.

24 Likes

Is there a reason that
Vector3.new(math.random()-0.5, math.random()-0.5, math.random()-0.5).Unit
is not good enough?

You’re mapping a random cube space to the surface of a sphere by doing .Unit. The rounded corners that you’re cutting off of that shape will cause a biased distribution there on the resulting sphere.

Also Vector3.new(0,0,0) has no solution for .Unit if someone hits the jackpot and gets all of those random values as 0.5. It’d end up as NaN and that could lead to strange issues if you use that vector in follow-up calc. Though less of a concern than what I mentioned above.

3 Likes