Turns out the maths was off for this. I went back and sorted out the details for it. I’ve made a place with a fixed version of the maths and I’ll explain the fixed way of doing it below.
Bullet Spread CFrame.rbxl (51.1 KB)
My approach uses the fact that for a matrix R_ij:
| R_11, R_12, R_13 |
R = | R_21, R_22, R_23 |
| R_31, R_32, R_33 |
we know that:
R * (1,0,0) == (R_11, R_21, R_31)
R * (0,1,0) == (R_12, R_22, R_32)
R * (0,0,1) == (R_13, R_23, R_33)
so we can build an appropriate CFrame by thinking about how we want the three axis to change after being transformed by the cframe. The fact that we just want to rotate the vector means we actually only need to think about two axis as the third can be determined by the cross product of the other two (a rotation matix is orthogonal).
We know that (0, 0, 1)
goes to (Sin(θ)Cos(φ), Sin(θ)Sin(φ), Cos(θ))
so we just need to think about one other axis.
It is useful to think of the overall transformation as two rotations. The first around the z axis by the angle φ. The second around the new y axis by angle θ. The interesting thing about these transformations is that the second one won’t alter the new y axis, so we only need to know how the first transformation affects the original y axis.
This means that (0, 1, 0)
→ (-Sin(φ), Cos(φ), 0)
. We then know two columns of the matrix:
| R_11, -Sin(φ), Sin(θ)Cos(φ) |
R = | R_21, Cos(φ), Sin(θ)Sin(φ) |
| R_31, 0 , Cos(θ) |
If you work out the cross product you can fill out the final column which happens to be:
| Cos(θ)Cos(φ), -Sin(φ), Sin(θ)Cos(φ) |
R = | Cos(θ)Sin(φ), Cos(φ), Sin(θ)Sin(φ) |
| -Sin(θ) , 0 , Cos(θ) |
The code then looks something like this:
local function getSpreadCframe(theta, phi)
local cTheta = math.cos(theta)
local sTheta = math.sin(theta)
local cPhi = math.cos(phi)
local sPhi = math.sin(phi)
local vY = Vector3.new(-sPhi, cPhi, 0)
local vZ = Vector3.new(sTheta * cPhi, sTheta * sPhi, cTheta)
local vX = vY:Cross(vZ)
return CFrame.fromMatrix(
Vector3.new(),
vX, vY, vZ
)
end