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.