AxisAngle has written a great tutorial about choosing random points on and in circles and spheres.
The code in the tutorial for choosing a random point on a unit sphere is this:
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)
With a simple change to this, we get code that generates a random point on a hemisphere (half a sphere) of a unit sphere (sphere with radius 1). r is the radius of the cross section circle on which the chosen point is.
local a = 2*math.pi*math.random()
local x = math.random()
local r = math.sqrt(1 - x*x)
local y, z = r*math.cos(a), r*math.sin(a)
Sorry, I didn’t read your whole original post before writing my earlier reply.
CFrame.fromAxisAngle gives a CFrame that is got by rotating the identity CFrame the given amount of radians around the given axis. By changing the axis direction to opposite (which can be done by swapping the vectors in the cross product or by negating the cross product), the rotation direction will be opposite as well. I don’t know which of the two possible axis directions gives correct results so you may need to swap the vectors to get the code working.
The cosine of the angle between two vector a and b is a:Dot(b)/(a.Magnitude*b.Magnitude). In the case of unit vectors (Magnitude 1), this can be simplified to a:Dot(b). When b is the unit up vector (y axis vector) Vector3.new(0, 1, 0), the dot product can be simplified to a.Y. Thus, the angle is acos(a.Y)
Here’s the code.
local function getRandomPointOnUnitHemisphereWhichIsOnSurface(surfaceNormal)
local cframeRotationAxis = surfaceNormal:Cross(Vector3.yAxis).Unit
local cframeRotationAngle = math.acos(surfaceNormal.Y)
local centerCFrame = CFrame.fromAxisAngle(cframeRotationAxis, cframeRotationAngle)
local y = math.random() -- distance from the sphere center in the direction of the normal
local circleRadius = math.sqrt(1 - y*y) -- radius of the chosen cross section circle whose distance from sphere center is y.
local angleOnCircle = 2*math.pi*math.random() -- angle on the aforementioned circle
local x, z = circleRadius*math.cos(angleOnCircle), circleRadius*math.sin(angleOnCircle)
return centerCFrame * Vector3.new(x, y, z)
end
I initially forgot to change a to angleOnCircle in math.cos and math.sin. That should be fixed now. And yes, center should be the point where the ray hit the surface. And I don’t know of any easier way to choose the point. Actually, I realized I also forgot to add the position to centerCFrame and I hadn’t made the rotation axis vector a unit vector (the documentation on CFrame.fromAxisAngle implies it should be a unit vector). I’ve fixed these this now.
Edit: I just realized that I was a bit dumb. Since you need a direction and not a world space position on the hemisphere, the sphere center position is unnecessary. I’ve changed the code again.
A really elegant way to generate a random point on a hemisphere is to start by generating a random point, d on a sphere, then taking the direction through the center of the hemisphere, n, and flipping d if d:Dot(n) < 0
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)
local d = Vector3.new(x, y, z)
if d:Dot(n) < 0 then
d = -d
end
Is there some kind of a problem with the way I did it in my code (like not uniform distribution or some other problem)?
Edit: I understand now. The benefit of your way is that there’s no need for the CFrame stuff. Thanks for mentioning this alternative way. It’s definitely better. But shouldn’t x be 2*math.random()-1 in that code?
@axisangle’s code already works for any normal (n in the code is the normal). You can shorten the code by using the function Random:NextUnitVector() mentioned by @Soliform , though. I wasn’t aware of such a function existing.
The following code combines @AxisAngle’s solution and Random:NextUnitVector().
local randomVectorGenerator = Random.new()
local function getRandomUnitVectorOnHemisphere(surfaceNormal)
local randomUnitVectorOnSphere = randomVectorGenerator:NextUnitVector()
local randomUnitVectorOnHemisphere = if randomUnitVectorOnSphere:Dot(surfaceNormal) >= 0 then randomUnitVectorOnSphere else -randomUnitVectorOnSphere
return randomUnitVectorOnHemisphere
end
If you didn’t understand the logic behind using dot product to check whether the vector should be flipped, this explanation may help:
The angle between two vectors is in the range [0 degrees, 180 degrees]. 90 degree angle means that they are perpendicular and 180 degree angle means that they have opposite directions.
The dot product is positive when the angle between the normal and the random vector is less than 90 degrees, zero if it’s exactly 90 degrees and negative if it’s more than 90 degrees. The angle being more than 90 degrees means that the random vector points into the surface which is why in this case it’s flipped so that it points out of the surface.