Orienting a part to the ground normal?

Hi everyone. I’ve been trying to make this work for a few days now to no avail.

Basically I’m trying to orient a part CFrame to the normal of the ground beneath it. The end result is a custom Go kart framework. Like shown in this video.

In the video the guy basically did four raycasts downward and then averaged the normals of them. That’s not a big deal, and I think I’ve got that part down ok.

local downNormal = Vector3.new(0, -1, 0)
	local _, _, leftFrontCast = findPartOnRay (workspace, newRay(Vector3.new(KartOccupied.Position.X - 2, KartOccupied.Position.Y - 1, KartOccupied.Position.Z + 3),  downNormal * 500))
	local _, _, rightFrontCast = findPartOnRay (workspace, newRay(Vector3.new(KartOccupied.Position.X + 2, KartOccupied.Position.Y - 1, KartOccupied.Position.Z + 3),  downNormal * 500))
	local _, _, rightBackCast = findPartOnRay (workspace, newRay(Vector3.new(KartOccupied.Position.X + 2, KartOccupied.Position.Y - 1, KartOccupied.Position.Z - 3),  downNormal * 500))
	local _, _, leftBackCast = findPartOnRay (workspace, newRay(Vector3.new(KartOccupied.Position.X - 2, KartOccupied.Position.Y - 1, KartOccupied.Position.Z - 3),  downNormal * 500))

My problem is trying to actually set the upVector of the kart block so that it orients correctly with the ground.

I’m not sure how to actually do that because CFrames are a bit different (and seemingly a lot more complex ://) than the Transform property that Unity uses.

Could anyone help me out? Here’s what I’ve tried, but it doesn’t work like I’d like it to.

local lookNormal = KartOccupied.CFrame.LookVector
	local rightNormal = (lookNormal.unit):Cross((leftFrontCast + rightFrontCast + rightBackCast + leftBackCast)).unit
	local upNormal = (rightNormal:Cross(lookNormal)).unit
	
	print (lookNormal)
	print (rightNormal)
	print (upNormal)
	
	KartOccupied.CFrame = CFrame.fromMatrix(KartOccupied.Position, lookNormal, upNormal, rightNormal)
4 Likes

So, when confronted with a problem like this, it’s useful to break it down into its simplest parts and start from there.

The way I see it, this is an exercise in vector math and a bit of trig.

In the following example, the goal is to alter the part’s CFrame such that its upVector’s alignment is equal to the direction of the surface normal.

Consider a part whose upVector you want to align with the normal of a part’s surface below it.

First, I define some unit vectors.

	local Xunit = Vector3.new(1, 0, 0)
	local Yunit = Vector3.new(0, 1, 0)
	local Zunit = Vector3.new(0, 0, 1)

Now would be a good time to cast a ray down from the part in question.

	local newRay = Ray.new(script.Parent.Position, Vector3.new(0, -10, 0)) --I used a magnitude of 10 for the raycast. This can be considered your distance at which the surface normal should influence your kart's alignment.
	local part, position, normal = workspace:FindPartOnRay(newRay) --FindPartOnRay returns the part that the ray intersected, the position of intersection, and the normal vector of the surface it hit.
	print(normal)
	local checkVector = script.Parent.CFrame.UpVector --Your part's upVector

Next, I simply get the angle between A: the UpVector of the part in question and the X, Y, and Z axes, and B: the surface normal vector and the X, Y, and Z axes.

Then, I calculate the difference between these angles to get the “component angles,” or rather, the angle between the UpVector and the surface normal about the X axis, Y axis, and Z axis.

By the way, you can get the angle between two vectors A and B using the following identity:
dot(A * B) = ||A|| ||B|| cos(θ).

Rearranging algebraically, you get:
θ = cos^-1(dot(A * B) / (||A|| ||B||)

Observe:

	local checkAngleX, normalAngleX = math.acos(checkVector:Dot(Xunit) / (checkVector.Magnitude * Xunit.Magnitude)), math.acos(normal:Dot(Xunit) / (normal.Magnitude * Xunit.Magnitude)) --Angle between the part's upVector and the X axis, angle between the surface normal and the X axis
	
	local diffX = checkAngleX - normalAngleX --The difference between those angles is the angle between the checkVector and the surface normal for X.
	
	local checkAngleZ, normalAngleZ = math.acos(checkVector:Dot(Zunit) / (checkVector.Magnitude * Zunit.Magnitude)), math.acos(normal:Dot(Zunit) / (normal.Magnitude * Zunit.Magnitude))
	local diffZ = checkAngleZ - normalAngleZ

--I simply do the same as before, but with the Z axis.

Consider how rotations work for a moment: When you rotate an object in 3D space, you rotate it about an axis. Here is a visualization:

Understanding this, I realize that I can simply rotate the part about the X axis by the angle between the vectors in the Z direction, and rotate about the Z axis by the negative of the angle between the parts in the X direction.

script.Parent.CFrame = script.Parent.CFrame * CFrame.Angles(diffZ, 0, -diffX)

The result:

However, you will notice that if you rotate the object about its Y axis, things get weird:

Here’s the reason:

The dot product doesn’t care about direction.
This means that regardless of whether or not vector A is oriented to the left or right of vector B, the dot product will still return the same value.

The solution to this? That would be the cross product. The cross product between vector A and B,
A x B, returns a vector orthogonal to your input vectors. This means that it is perpendicular to BOTH vectors A and B.

Let’s say that A and B are the unit vectors X and Z (Vector3.new(1, 0, 0) and Vector3.new(0, 0, 1), respectively).

The cross product between A and B would be the unit vector Y, or Vector3.new(0, 1, 0).

If we were taking the cross product between A and -B, or X and -Z, aka Vector3.new(1, 0, 0,):Cross(Vector3.new(0, 0, -1), we would get Vector3.new(0, -1, 0).

The idea to gleam off of this is that the direction of the vector returned by the cross product is dependent on the direction of the vectors you are crossing.

Based on the direction, you can multiply your angle by -1 or +1. I will leave this as an exercise for the reader.

If you have more questions, or get completely stuck and need me to provide a code example for the cross product to correct the direction issue, let me know and I will be glad to help, but I would rather you try and work it out yourself first to strengthen your understanding.

By the way, there is more than one way to actually do this; I think of these things as a trig/vector math problem. I don’t get into how quaternions work here, I simply do the vector math and apply the results.

EDIT: Or you can probably just do this lol, thanks to EchoReaper for his response to a similar question in 2018:

CFrame from normal - #3 by EchoReaper

I haven’t confirmed this works, but it’s EchoReaper, so it probably does. This is for aligning a frontVector with a surface normal, but you can modify this to work with an upVector pretty easily.

33 Likes