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):



6 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
23 Likes

Your solution is really good!
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 head = char:WaitForChild("Head")
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