How to Generate a Random Rotation (and much more)

This is a list of some random generators I use quite frequently.

Some Definitions

Uniform distributions over a space or subspace implies that no point is any more likely to generate than any other point.
A probability distribution function describes how probable the generation of any value is relative to any other value. For example, these two probability distribution functions:
image
The rightward axis describes the output values. Both functions are limited to outputs between 0 and 1.
The upward axis tells us a number relating to the probability of generating said output value.
The blue function tells us that no value is any more likely to generate than any other value because the probabilities are uniform. This is a uniform distribution
The red function, for example, tells us that the value 0.5 is twice as likely to be generated as the value 0.25. This is a triangular distribution.

Uniform Random Variable [0, 1)

This generates a uniformly random variable r, 0 <= r < 1

local r = math.random()

Uniform Random Variable (0, 1]

This generates a uniformly random variable r, 0 < r <= 1
Usually inclusivity does not matter, but sometimes it will.

local r = 1 - math.random()

Uniform Random Variable [l0, l1)

This generates a uniformly random variable r, l0 <= r < l1

local r = (l1 - l0)*math.random() + l0

Uniform Random 2D Unit Vector

This generates a random point (x, y) on a circle

local a = 2*math.pi*math.random()
local x, y = math.cos(a), math.sin(a)

Triangular Random Variable [0, 1)

This generates a random variable r, whose probability distribution is triangular.
It will be much more likely to generate points near 1 than near 0.

local r = math.sqrt(math.random())
-- This is a fun way to accomplish the same thing:
local r = 1 - math.abs(math.random() - math.random())

Uniform Random 2D Vector on a Disk

This generates a random point (x, y) within a circle.
We accomplish this by generating a random 2D unit, then multiplying it by a triangular distribution. There are (proportionally to the radius) more points further out on the disk than near the center

local a = 2*math.pi*math.random()
local r = math.sqrt(math.random())
local x, y = r*math.cos(a), r*math.sin(a)

Uniform Random Unit 3D Vector

This generates a random point (x, y, z) on a sphere.
We recognize that the number of points of any slice (of some thickness) out of a sphere is dependent only upon the radius of the sphere an the thickness of the slice.


So we can pick a random slice along the x axis and a random angle to generate a point on the surface of a sphere.

local a = 2*math.pi*math.random()
local x = 2*math.random() - 1
local r = math.sqrt(1 - x*x)
local y, z = r*math.cos(a), r*math.sin(a)

Uniform Random Unit 3D Vector Within an Angle

We can extend the above concept to generate points within a slice defined between two angles.


Not to scale
For example, we could choose to generate points just within 0 to 40 degrees of the -z axis, or 70 to 80 degrees of the -z axis. You can choose any axis.

-- assuming axis and perp are unit orthogonal vectors
local function uniformRandomUnit3DVectorWithinAngularRange(minAngle, maxAngle, axis, perp)
    -- transform angle to length along axis
    local l0 = math.cos(maxAngle)
    local l1 = math.cos(minAngle)
    -- generate random length between given lengths
    local l = (l1 - l0)*math.random() + l0
    -- compute radius given length along axis
    local r = math.sqrt(1 - l*l)
    -- compute the axis-perpendicular point
    local a = 2*math.pi*math.random()
    local u, v = r*math.cos(a), r*math.sin(a)
    -- now transform into some world space
    return l*axis + u*perp + v*axis:Cross(perp)
end

Uniform Random 3D Vector Within Sphere

Taking the unit sphere solutions above and multiplying the result by a radius generated from the correct probability distribution, we get (x, y, z) is a uniform random point within a sphere.

local a = 2*math.pi*math.random()
local l = 2*math.random() - 1
local i = math.sqrt(1 - l*l)
-- we now compute a random radius, and multiply it into the result
local r = math.random()^(1/3)
local x = r*l
local y = r*i*math.cos(a)
local z = r*i*math.sin(a)

or alternatively:

local function uniformRandom3DVectorWithinSphereWithinAngularRange(minAngle, maxAngle, axis, perp)
    local r = math.random()^(1/3)
    return r*uniformRandomUnit3DVectorWithinAngularRange(minAngle, maxAngle, axis, perp)
end

Gaussian Distributed Random Variable(s) (-inf, inf), Centered at 0 with Standard Deviation 1

Gaussian distributed random variables are useful for two reasons:

  1. They are the result of adding a bunch of random variables together. For example: height is the result of nutrition, activity, and countless different genetic traits.
  2. Knowing the value of any combination of gaussian random values tells you nothing about any other orthogonal combination of the same gaussian random variables. An example of something that does not have this property: If I tell you I have a point within a unit circle, and the x coordinate is 0.8, you know that the y coordinate must be between -0.6 and 0.6. With a 2D gaussian point, knowing x tells you nothing about y.

It generates values (in 1D) according to this probability distribution:

It is not possible to algebraically generate a single gaussian random variable. However, we CAN algebraically generate the distance from the center of two gaussian random variables, and we CAN generate a random direction. This is called the Box-Muller transform.

-- we must use 1 - math.random(), otherwise we will sometimes get log(0) and generate nan values
local radius = math.sqrt(-2*math.log(1 - math.random()))
local angle = 2*math.pi*math.random()
local x, y = radius*math.cos(angle), radius*math.sin(angle)
-- the standard deviation of this 2D point is sqrt(2)

x and y are each gaussian random variables. You can throw away one if you only need one. For example, we could write some succinct code like:

local function gaussianRandom()
    return math.sqrt(-2*math.log(1 - math.random()))*math.cos(2*math.pi*math.random())
end

Uniform Random Unit in Arbitrary Dimensions

Using property no. 2 of gaussian random variables, we can generate a random unit vector in any dimension.

local function randomUnit2D()
    local x = gaussianRandom()
    local y = gaussianRandom()
    local r = math.sqrt(x*x + y*y)
    return x/r, y/r
end

local function randomUnit3D()
    local x = gaussianRandom()
    local y = gaussianRandom()
    local z = gaussianRandom()
    local r = math.sqrt(x*x + y*y + z*z)
    return x/r, y/r, z/r
end

-- The pattern continues...

Uniform Random Rotation CFrame

Quaternions map uniformly to rotation matrices, and quaternions can be interpreted as lying on the surface a 4D hyper sphere, so we can generate a 4D unit vector, and derive a random rotation CFrame from this. Roblox’s Quaternion CFrame constructor does not require a unitized quaternion, so we can be lazy about unitization.

local function randomRotationCFrame()
    local w = gaussianRandom()
    local x = gaussianRandom()
    local y = gaussianRandom()
    local z = gaussianRandom()
    return CFrame.new(0, 0, 0, x, y, z, w)
end

Uniform Random 4D Unit (Quaternion)

If we want to be more efficient about it, we can make some optimizations to our naïve code:

local function randomUnit4D()
    local log0 = math.log(1 - math.random())
    local log1 = math.log(1 - math.random())
    local ang0 = 2*math.pi*math.random()
    local ang1 = 2*math.pi*math.random()
    local sqrt0 = math.sqrt(log0/(log0 + log1))
    local sqrt1 = math.sqrt(log1/(log0 + log1))
    local w, x = sqrt0*math.cos(ang0), sqrt0*math.sin(ang0)
    local y, z = sqrt1*math.cos(ang1), sqrt1*math.sin(ang1)
    return w, x, y, z
end

Uniform Random Vector within N-Sphere

We can take any of our uniform random unit functions and multiply it by a correctly chosen radius relating to the dimension.

local function randomWithinSphere3D()
    local x0 = gaussianRandom()
    local x1 = gaussianRandom()
    local x2 = gaussianRandom()
    local r = math.random()^(1/3)/math.sqrt(x0*x0 + x1*x1 + x2*x2)
    return r*x0, r*x1, r*x2
end

local function randomWithinSphere5D()
    local x0 = gaussianRandom()
    local x1 = gaussianRandom()
    local x2 = gaussianRandom()
    local x3 = gaussianRandom()
    local x4 = gaussianRandom()
    -- the power of our radius multiplier is 1/dimension
    local r = math.random()^(1/5)/math.sqrt(x0*x0 + x1*x1 + x2*x2 + x3*x3 + x4*x4)
    return r*x0, r*x1, r*x2, r*x3, r*x4
end

Picking an Object from a List of Objects and Given Probabilities

If we have a list of objects and the relative frequency with which we want each one to be chosen, we can pick a random number and map this to an object.

local objects = {
    {
        thing = "I am the most common";
        frequency = 5;
    }, {
        thing = "I am somewhat common";
        frequency = 3;
    }, {
        thing = "I am least common";
        frequency = 1;
    }
}

-- We can imagine we have a bag, put a number of each item in the bag, and
-- randomly choose one.
-- We can do this more efficiently in code by assigning numbers to objects, then
-- picking a random number, and taking the associated object.
-- The more numbers we assign to a single object, the more likely it will be to
-- choose that one.
local totalFrequency = 0
for i = 1, #objects do
    totalFrequency = totalFrequency + objects[i].frequency
end
-- choose a number between 1 and totalFrequency
local randomChoice = totalFrequency*math.random()
-- and figure out which object is associated with that number
local count = 0
for i = 1, #objects do
    count = count + objects[i].frequency
    if randomChoice <= count then
        return objects[i].thing
    end
end

So How Was this Derived? AKA, How to Create a Random Generator given a PDF (probability distribution function)

r = integrate(PDF(x), x, -inf, x)/integrate(PDF(x), x, -inf, inf), solve for x
This will map a uniform random unit, r, generated from math.random(), into a random number, x, distributed by PDF(x). It can be quite fickle, and often times is not solvable algebraically, but sometimes, like in all the cases above, it is!
For an intuitive explanation, watch this video:

35 Likes

This is a really valuable article, it would be cool if there is a module out there for all sorts of cool randomness functions :smiley:

3 Likes

I thought that this was going to be some low quality trashy script like this:

local r1 = math.random(1, 360)
local r2 = math.random(1, 360)
local r3 = math.random(1, 360)

But it’s a lot more in depth than that, great work!

5 Likes

Thank you for posting this, the random points on a sphere within angle range was so helpful because I kept finding a lot of math and though Im good at it, its the middle of summer and I’m not in math mode.