# How to get the angles of a surface?

I’m working on a gravity controller and I’m trying to find the angles of the surface the player is standing on, I’m not sure how to achieve this though.

The reason I need this is to make sure the gravity controller is also compatible with parts that have irregular surfaces (such as wedges, balls or cylinders)

Here are 3 images to make it more clear (sorry for having a huge and dark screen):

4 Likes

I’ve managed to get something working, but I haven’t tested it a whole lot.

``````--Convert normal X and Z to degrees
local X = math.deg(math.asin(Hit.Normal.X))
local Z = math.deg(math.asin(Hit.Normal.Z))

local FloorAngle = math.floor( --Round to get rid of floating-point weirdness
math.max( --math.max to not get negative numbers
X == 0 and Z or X,   --Needs some revision
X == 0 and -Z or -X
)
)
``````

(“Hit” is the raycast)

I think you misunderstood my question, I don’t want just a single angle like in your video, but a vector3 that represents the angles of the surface below the player’s character just like in the 3 images of my first message.

Cant you just create a raycast that finds the part the player is standing on and get the part’s angle components from the cframe?

1 Like

It won’t work with parts that have irregular surfaces (wedges, spheres, cylinders, etc.)

Specifically perform a raycast to the part from the character position and take the “Normal” returned by the raycast. That normal is the vector your looking for.

1 Like

If you want the angle specifically. You can get the difference of two vectors. This would be the difference between the up vector `Vector3.new(0,1,0)` and the Normal vector returned by the raycast.

``````math.deg(math.acos(math.clamp(v1:Dot(v2), -1,1)) / (v1.Magnitude * v2.Magnitude))
``````

Ohhh, well then you can just do

``````local X = math.deg(math.asin(Hit.Normal.X))
local Y = math.deg(math.asin(Hit.Normal.Y))
local Z = math.deg(math.asin(Hit.Normal.Z))

local FloorAngles = Vector3.new(math.floor(X), math.floor(Y), math.floor(Z))
``````

Rather than trying to convert this up vector into an angle which is also complicated it’s better to leave it I would rather use @EgoMoose advanced CFrame technique to leave it in terms of vectors.

We can obtain a CFrame rotation to align the current persons “UpVector” to the new surface upvector. Here is an example script and video.

The advantage of this method is that it will allow you to rotate along the new upVector without having to specify a right vector as a CFrame.fromMatrix solution.

Even unity games use this method, They have the built in method Quaternion.FromToRotation which is @EgoMoose advanced CFrame trick

Also notice the similarities with my code piece:

``````local rotateToFloorCFrame = getRotationBetween(wedge.CFrame.UpVector,rayResult.Normal,randomAxis)
local goalCF = rotateToFloorCFrame*wedge.CFrame

--TL;DR
local fromToQuaternion = getRotationBetween(wedge.CFrame.UpVector,rayResult.Normal,randomAxis)
wedge.CFrame = fromToQuaternion * wedge.CFrame
``````

Edit: Yeah this is the correct and smoother version

``````local wedge = script.Parent

local randomAxis = Vector3.new(1,0,0) -- Read this in the EgoMoose article

local DOWN = -Vector3.new(0,500,0)

local function getRotationBetween(u, v, axis)
local dot, uxv = u:Dot(v), u:Cross(v)
if (dot < -0.99999) then return CFrame.fromAxisAngle(axis, math.pi) end
return CFrame.new(0, 0, 0, uxv.x, uxv.y, uxv.z, 1 + dot)
end

while true do
local dt = wait()

local rayResult = workspace:Raycast(wedge.Position,-1000*wedge.CFrame.UpVector)
if rayResult then
local rotateToFloorCFrame = getRotationBetween(wedge.CFrame.UpVector,rayResult.Normal,randomAxis)
local goalCF = rotateToFloorCFrame*wedge.CFrame
wedge.CFrame = wedge.CFrame:Lerp(goalCF,5*dt).Rotation +wedge.CFrame.Position
end
end
``````
17 Likes

There’s still one (minor) problem, though: I edited your script to test certain things, and I discovered that the rotation isn’t very sharp at high speeds:

I would like to make it as sharp as possible, like this:

Here is my current script (it’s a local script inside StarterCharacterScripts)

``````local char = script.Parent
local hrp = char:WaitForChild("HumanoidRootPart")
local wedge = workspace:WaitForChild("PartToMove") --wedge is the part I want to rotate around
local randomAxis = Vector3.new(1, 0 ,0)
local rs = game:GetService("RunService")

local function getRotationBetween(u, v, axis)
local dot, uxv = u:Dot(v), u:Cross(v)
if (dot < -0.99999) then return CFrame.fromAxisAngle(axis, math.pi) end
return CFrame.new(0, 0, 0, uxv.x, uxv.y, uxv.z, 1 + dot)
end

rs.Heartbeat:Connect(function()
local params = RaycastParams.new()
params.FilterDescendantsInstances = {char}
params.FilterType = Enum.RaycastFilterType.Blacklist

local hit = workspace:Raycast(wedge.Position, -1000 * hrp.CFrame.UpVector, params)

if hit then
local rotateToFloorCFrame = getRotationBetween(hrp.CFrame.UpVector, hit.Normal, randomAxis)
local goalCF = rotateToFloorCFrame * hrp.CFrame
wedge.CFrame = wedge.CFrame:Lerp(goalCF, 1).Rotation + hrp.CFrame.Position
else
wedge.Position = hrp.Position
end
end)
``````

EDIT: Since the cause of the sharpness issue is external and not caused by your code, I’m marking your message as the solution

EDIT 2: I figured out by myself how to fix the issue.
Here is the final code:

``````local plr = game.Players.LocalPlayer
local char = plr.Character or plr.CharacterAdded:Wait()
local hrp = char:WaitForChild("HumanoidRootPart")
local wedge = workspace:WaitForChild("PartToMove") --wedge is the part I want to rotate around
local randomAxis = Vector3.new(1, 0 ,0)
local rs = game:GetService("RunService")
local uis = game:GetService("UserInputService")
local fast = false
--Bless EgoMoose for his advanced CFrame tutorial on the DevForum! :D https://devforum.roblox.com/t/a-couple-of-advanced-cframe-tricks/337682
local function getRotationBetween(u, v, axis)
local dot, uxv = u:Dot(v), u:Cross(v)
if (dot < -0.99999) then return CFrame.fromAxisAngle(axis, math.pi) end
return CFrame.new(0, 0, 0, uxv.x, uxv.y, uxv.z, 1 + dot)
end

rs.RenderStepped:Connect(function()
local camCF = workspace.CurrentCamera.CFrame
--Detect if player is in first person mode or has shift lock enabled.
if (camCF.Position - head.Position).Magnitude < 0.8 or uis.MouseBehavior == Enum.MouseBehavior.LockCenter then
fast = true
else
fast = false
end

local params = RaycastParams.new()
params.FilterDescendantsInstances = {char}
params.FilterType = Enum.RaycastFilterType.Blacklist

local hit = workspace:Raycast(wedge.Position, -1000 * hrp.CFrame.UpVector, params)

local lv = camCF.LookVector
local uv = Vector3.new(0, 1, 0)
local rv = lv:Cross(uv)

local orientation = CFrame.fromMatrix(hrp.Position, rv, uv, -lv)

if hit then
local rotateToFloorCFrame = getRotationBetween(hrp.CFrame.UpVector, hit.Normal, randomAxis)
local goalCF = CFrame.new()

if fast == true then
goalCF = rotateToFloorCFrame * orientation
wedge.CFrame = wedge.CFrame:Lerp(goalCF, 1).Rotation + hrp.CFrame.Position
else
goalCF = rotateToFloorCFrame * hrp.CFrame
wedge.CFrame = wedge.CFrame:Lerp(goalCF, 1).Rotation + hrp.CFrame.Position
end
else
wedge.Position = hrp.Position
end
end)
``````
2 Likes

Perhaps use render stepped or stepped, that’s because shiftlock follows camera movement I believe hence it needs to move before the camera.

Also make sure to big EgoMoose a big like, credits to that article which made everything possible.

1 Like