Surface normal returned by findPartOnRay is not perpendicular to part surface

  1. What do you want to achieve? Keep it simple and clear!
    I’m doing some tests with ray casting and trying to calculate proper angles for rotating and positioning parts dynamically.

  2. What is the issue? Include screenshots / videos if possible!
    The surface normal is not being returned as a perpendicular vector from the part/terrain surface.

  3. What solutions have you tried so far?
    I have been tracing out the cast ray (Grey) the up direction vector of the black part (Blue) and the surfaceNormal vector (Green)

My understanding is that the surface normal (green line) should be pointed directly upward (like the blue line). What is happening here?


--Create a CFrame to pull the direction vector from
local directionFrame = myPart.CFrame * CFrame.Angles(math.rad(20), math.rad(180), 0)

--Cast a ray from the position of myPart going in direction of the front face of the direction cframe
--(This is backface of myPart down toward the terrain at a 20deg angle)
local ray =, directionFrame.LookVector * 50)
local hitPart, hitPos, surfaceNormal = game.Workspace:FindPartOnRayWithWhitelist(ray, {game.Workspace.Terrain, game.Workspace.FakeTerrain}, false)

--Calculate Rotation angle
local normal = part.CFrame.RightVector --(Surface facing upward...)
local rotAngle = math.acos(normal:Dot(surfaceNormal) / (normal.Magnitude*normal.Magnitude))
print("Rotation Z: "..rotAngle)

--Trace the Ray and vectors
local rayDistance = (myPart.Position - hitPos).Magnitude
local rayTracer ="Part")
rayTracer.Name = "Tracer"
rayTracer.Size =, 0.2, rayDistance)
rayTracer.Anchored = true
rayTracer.CanCollide = false
rayTracer.CFrame =, hitPos) *,0,(-rayDistance/2))
rayTracer.Parent = game.Workspace

local normalTracer = rayTracer:Clone()
normalTracer.Name = "NormTracer"
normalTracer.BrickColor = BrickColor.Blue()
normalTracer.CFrame =, normal*10) *,0,(-rayDistance/2))
normalTracer.Parent = game.Workspace

local surfTracer = rayTracer:Clone()
surfTracer.Name = "SurfaceTracer"
surfTracer.BrickColor = BrickColor.Green()
surfTracer.CFrame =, surfaceNormal*10) *,0,(-rayDistance/2))
surfTracer.Parent = game.Workspace

CFrame.lookAt(pos, lookAt) takes two global positions in space, whereas surface normals from rays are created relative to the point of contact.

The surface normal’s tracer should be fixed if you use CFrame.lookAt(hitPos, hitPos + surfaceNormal) instead of CFrame.lookAt(hitPos, surfaceNormal*10), similarly for the generic up vector’s normal.