This is answering the literal title of your post.
IT’S MATH TIME!
Part of what is returned by a raycasting operation is a normal vector, which is a directional unit vector that faces straight out of whatever face was hit by the raycast.
If you simply want to find the side of the part we hit, we can compare the normal vector we hit to the vectors that make up the face normals of the part and return the face for the given side.
This leaves us a question: how would we compare two vectors to see how similar they are? Well, a property of Vector Dot products will help us out. Given the dot product of two unit vectors, the result will be 1 if the vectors are parallel. So, we can see if the dot product of the raycast result’s normal vector to each face’s normal vector on the part is almost equal to 1. (We check that they are “almost equal” instead of “equal” due to floating point precision errors, you can read more about that here.)
Okay, how do we get the normal vector for each face? Simple, we just do some transformation math. Roblox provides us a constructor for getting the normal from a face here. However, if you used this alone, you would see that these would only work with parts with no rotation. This is because these vectors are oriented in ‘local space,’ or relative to the unrotated part. In order to convert the normal vector to world space, we can use a handy function on the CFrame called “VectorToWorldSpace.” This function will apply the part’s orientation to the vector using complex linear algebra, and get us the normal vector relative to the part’s actual rotation in the world.
In aggregate, here’s how we would do such a thing…
-- Untested!
--[[**
This function returns the face that we hit on the given part based on
an input normal. If the normal vector is not within a certain tolerance of
any face normal on the part, we return nil.
@param normalVector (Vector3) The normal vector we are comparing to the normals of the faces of the given part.
@param part (BasePart) The part in question.
@return (Enum.NormalId) The face we hit.
**--]]
function NormalToFace(normalVector, part)
local TOLERANCE_VALUE = 1 - 0.001
local allFaceNormalIds = {
Enum.NormalId.Front,
Enum.NormalId.Back,
Enum.NormalId.Bottom,
Enum.NormalId.Top,
Enum.NormalId.Left,
Enum.NormalId.Right
}
for _, normalId in pairs( allFaceNormalIds ) do
-- If the two vectors are almost parallel,
if GetNormalFromFace(part, normalId):Dot(normalVector) > TOLERANCE_VALUE then
return normalId -- We found it!
end
end
return nil -- None found within tolerance.
end
--[[**
This function returns a vector representing the normal for the given
face of the given part.
@param part (BasePart) The part for which to find the normal of the given face.
@param normalId (Enum.NormalId) The face to find the normal of.
@returns (Vector3) The normal for the given face.
**--]]
function GetNormalFromFace(part, normalId)
return part.CFrame:VectorToWorldSpace(Vector3.FromNormalId(normalId))
end
EDIT:
Added missing documentation to first function in code sample.