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.

27 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

4 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


43 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.

25 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

While we’re at it, it would be useful to be able to specify a look vector and angular range in which to generate the unit vectors. This is extremely common for bullet spread, and an existing solution I have seen requires 3 CFrame constructions.

-- returns a uniformly-distributed random unit vector no more than maxAngle radians away from v
local function RandomVectorOffset(v, maxAngle)
	return (
		CFrame.lookAt(Vector3.new(), v)
		* CFrame.Angles(0, 0, rng:NextNumber(0, 2 * math.pi))
		* CFrame.Angles(math.acos(rng:NextNumber(math.cos(maxAngle), 1)), 0, 0)
	).LookVector
end 
2 Likes

That approach is almost never what you want for bullet spread, because bullet spread is far from a uniform distribution. You actually want the higher density at the center of the distribution that the two angle approach generates. The two angle solution also has the very nice property that you can wrap the random spread angle away from the center in an arbitrary modifier function F([0, 1]) -> [0, 1] to modify the angular distribution depending on the weapon.

6 Likes