Useful Random Generators

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

I was going to post this into Algorithms Mega-Thread, but it is clear to me that this is a little too long and in-depth, especially as I continue to expand this in the future. This is my first post, so let me know if anything can be improved. If you find any bugs in the code, let me know. Thanks for letting me into the community :slight_smile:.

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:

15 Likes

Uniform Random Point in Viewport

This follows from a similar process to the disk but up one dimension where the surface area of a sphere scales with the square of the radius. Care must be taken to correct for Camera:ViewportPointToRay() clustering points near the corners as a result of taking points on the near plane and projecting them to a sphere.

local viewportSize = workspace.CurrentCamera.ViewportSize
local aspectRatio = viewportSize.X / viewportSize.Y
local verticalFOV = math.rad(workspace.CurrentCamera.FieldOfView)
local horizontalFOV = math.atan(aspectRatio * math.tan(verticalFOV/2)) * 2
local verticalHeight = math.tan(verticalFOV/2)
local horizontalHeight = math.tan(horizontalFOV/2)

local function viewportPointToRay(x, y, depth)
	return workspace.CurrentCamera:ViewportPointToRay(
		(math.tan(horizontalFOV*(0.5 - x))/2/horizontalHeight + 0.5)*viewportSize.X,
		(math.tan(verticalFOV*(0.5 - y))/2/verticalHeight + 0.5)*viewportSize.Y,
		depth
	).Origin
end

local function randomPointInViewport()
	local maxDepth = 50
	local depth = (math.random() ^ (1/3)) * maxDepth
	return viewportPointToRay(math.random(), math.random(), depth)
end

Here is a faster version that may generate points off-screen near the corners.

local viewportSize = workspace.CurrentCamera.ViewportSize
local aspectRatio = viewportSize.X / viewportSize.Y
local verticalFOV = math.rad(workspace.CurrentCamera.FieldOfView)
local horizontalFOV = math.atan(aspectRatio * math.tan(verticalFOV/2)) * 2

local function viewportPointToDirection(x, y)
	return CFrame.Angles(verticalFOV*(y - 0.5), horizontalFOV*(x - 0.5), 0)
		* workspace.CurrentCamera.CFrame.LookVector
end

local function randomPointInViewport()
	local maxDepth = 50
	local depth = (math.random() ^ (1/3)) * maxDepth
	return workspace.CurrentCamera.CFrame.Position
		+ viewportPointToDirection(math.random(), math.random()) * depth
end