Aligning Object to Surface using CFrame:fromMatrix()

In this tutorial, you will learn how to make an object align to a surface while also pointing in some direction like this:

  • An understanding of vectors and CFrame is required.

The scenario:
In this example, a raycast is projected downwards to get the surface normal of the ground to align onto. We also have the lookvector of the HumanoidRootPart, or whatever the object should face towards.

Here is that info visualized:

Before we figure out how to align the object, we must first know some concepts.

What is CFrame.fromMatrix()?
CFrame.fromMatrix() creates a CFrame given the position, lookvector, right vector, and upvector as shown:

CFrame.fromMatrix(Position, LookVector, RightVector, UpVector)

We will use CFrame.fromMatrix() because we need to align the object with a certain UpVector (the ground UpVector) while also pointing the object in a direction (the LookVector of the HumanoidRootPart)

What are Cross Products?
Vector3:Cross() returns a vector that is perpendicular to the two vectors inputted. For example:

All three of these vectors are perpendicular (90 degrees apart) from each other.

The Problem
If we look back at our scenario, you may notice that the two vectors we have are NOT perpendicular to each other

How can we make all the vectors perpendicular? We also have the RightVector of the HumanoidRootPart!

The RightVector of the HumanoidRootPart is already perpendicular to the Surface Normal! This means that we can take the cross product of it to get the new LookVector!

With these 3 perpendicular vectors, we can now use CFrame.fromMatrix()!!

CFrame.fromMatrix(
   Position, -- POSITION OF THE OBJECT
   -newRay.Normal:Cross(HumanoidRootPart.CFrame.RightVector), -- LOOKVECTOR (negative because it was facing backwards)
   HumanoidRootPart.CFrame.RightVector, --RIGHTVECTOR
   newRay.Normal --UPVECTOR

Now if we set the CFrame of an object to this new CFrame.fromMatrix() every frame, we will get this!

The script I used:

local RunService = game:GetService("RunService")

local LocalPlayer = Players.LocalPlayer

local rParams = RaycastParams.new()

function newPet(character)

	local humanoidRootPart = character:WaitForChild("HumanoidRootPart")

	local testPet = Instance.new("Part") -- Creating the new pet
	testPet.Parent = workspace
	testPet.Anchored = true
	testPet.Size = Vector3.new(2,2,2)
	testPet.CanCollide = true
	testPet.Material = Enum.Material.Neon
	testPet.BrickColor = BrickColor.White()

	rParams.FilterDescendantsInstances = {character,testPet}

	local connection
	connection = RunService.Stepped:Connect(function()
		if character == nil then --Stops the connection and kills pet when character is gone
			connection:Disconnect()
			testPet:Destroy()
			return
		end
		
	
		local characterRightCF = humanoidRootPart.CFrame * CFrame.new(4,0,0) -- The origin for the raycast
		
		local newRay = workspace:Raycast(characterRightCF.Position, Vector3.new(0,-1,0) * 25, rParams) -- Raycast to get floor position and normal

		if newRay then
			testPet.CFrame = CFrame.fromMatrix(
				newRay.Position, -- POSITION OF THE OBJECT
				-newRay.Normal:Cross(humanoidRootPart.CFrame.RightVector), -- LOOKVECTOR
				humanoidRootPart.CFrame.RightVector, --RIGHTVECTOR
				newRay.Normal --UPVECTOR
			)
		end
		
	end)

end

LocalPlayer.CharacterAdded:Connect(newPet)

if LocalPlayer.Character then
	newPet(LocalPlayer.Character)
end

This is the first tutorial I have made, please comment feedback!

43 Likes

I think I needed to do that a while ago, and I abandoned because I couldn’t figure a solution. Does your method work with aligning a player character to a surface directly ?

I’d imagine that you could use this method to make that happen by using the humanoid movement direction as the reference direction while raycasting down from the player. I haven’t tried, but that sort of thing takes a lot of trial and error to look nice.

I was wondering if you needed the LookVector why not use the RootPart’s LookVector, so I tried and while it works it’s not as great as crossing between -normal and RightVector

I’m confused about one thing. What if the right vector isn’t perpendicular to the surface normal? The angle would be more or less than 90 degrees if the object is on a surface angling to the left or right.

Now I know how to use CFrame.fromMatrix. Thanks! Also, your script could be optimized using paralell lua and other efficient methods, but that’s not the point.
I also figured out a use case for Vector3:Cross here, which is good.

i realised the same thing, and testing it, i found that you would lose such a tilt.
so i went and created the “Correction factor”, which works as follows:
you perform vector3.dot() on the normal and RightVector.
you multiply this value by 90 - this gets you how much rotation you need to apply before tilting with FromMatrix. (this value is in degrees btw.)
You take your part’s cframe, rotate it on the y-axis by that value using cframe.angles() and get a rotated cframe.
you then use this rotated cframe’s vector properties to create a new CFrame.FromMatrix()
finally, you rotate that matrix cframe by the rotation angle multiplied by -1, to get rid of this ultimately unwanted rotation.
here’s my code:

part = script.Parent

params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = {part}

while true do
	task.wait()
	result = workspace:Raycast(part.Position, (part.CFrame.LookVector * 5) + (Vector3.yAxis * -200))
	if result then
		StartingCFrame = part.CFrame
		local FinalFrame:CFrame
		determiner = part.CFrame.RightVector:Dot(result.Normal)
		RotationAngle = 90 * math.abs(determiner) -- our correction angle
		StartingCFrame *= CFrame.Angles(0,math.rad(RotationAngle),0) -- rotated starting CFrame
		MidFrame = CFrame.fromMatrix(
			StartingCFrame.Position,
			StartingCFrame.RightVector,
			result.Normal,
			result.Normal:Cross(StartingCFrame.RightVector).Unit * -1
		) -- aligned CFrame, with incorrect rotation
		FinalFrame = MidFrame * CFrame.Angles(0,math.rad(RotationAngle * -1),0) -- restores our initial rotation in respect to the slope.

		part.CFrame = FinalFrame -- applies this final matrix to the part (rather than applying at each calculation)
	end
end

This should give accurate, albeit more approximate tilt, which works with most situations.

Hope this clears things up! this topic was of great help to my game.

3 Likes

Does this solution account for gimbal lock?
cc: @anumiri

i revisited the code a few days ago, and realized that there was a better way of doing this.
after casting a downward ray:

if result then
		local normal = result.Normal
		
		
		Humanoid = character.Humanoid::Humanoid
		
		Humanoid:SetStateEnabled(Enum.HumanoidStateType.Ragdoll, false)
		Humanoid:SetStateEnabled(Enum.HumanoidStateType.FallingDown, false)
        -- the humanoid lines stop the character from ragdolling and falling when sloping at 90 degrees
		
        -- the meat of this algorithm
		AngBLookNorm = math.acos(StartCFrame.LookVector:Dot(normal))
		--this gets us the angle between the normal and the lookvector
		AngleDiff = AngBLookNorm - math.pi/2
        -- math.pi/2 is 90* in radians, we find the change in angle we need to apply
        -- to get a lookvector perpendicular to the normal.
		print(AngleDiff)
		RotatorC = CFrame.Angles(AngleDiff, 0, 0)
        -- we creata a rotation about the xaxis(the rightvector)
		NewCFrameLV = (StartCFrame * RotatorC).LookVector
        -- this gets us a new lookvector, perpendicular to the normal.
		
		HRP.CFrame = CFrame.lookAt(StartCFrame.Position, StartCFrame.Position + NewCFrameLV, normal)
        -- here we create the final CFrame. where the lookvector is already perpendicular 
        --to the normal, this code will continue to work because having defined the 
        --upvector to be the normal, the rightvector is  automatically calculated.

and yes, this one accounts for gimbal lock.

to note, if you try to use this with the humanoid root part, you are gonna have to do a good bit of messing with the hip-height. testing this on the character had the player’s shins half stuck in the ground. you can still move, but it can be a problem visually.

your amazing bro i needed this so much, ty for the notes aswell

1 Like