Rotate CFrame by "relative" yaw instead of "absolute" yaw

I’m currently scripting the beginnings of a kart-racing game. Currently I can make the carts turn accurately when they are on level ground. I can’t, however, figure out how to accurately make them turn on surfaces such as walls or hills.
Intended behavior on level surface: https://streamable.com/c5h1ju
Turning behavior on slanted surface: https://streamable.com/oxt159
To be clear, the cart should look the same as it does on level surfaces as it does on slanted surfaces.

I’ve tried a couple different things. Most things either produce an identical half-solution similar to the one below or don’t work at all. I’ve looked up a couple different resources and I either didn’t understand it at all or it didn’t work.
Current solution which works on level surface but not slanted surfaces:

self.CFrame *= CFrame.Angles(0, self.InputVector.X*step*self.TurningRadius, 0)

but I’ve also tried:

local upv, theta = self.CFrame:ToAxisAngle()
self.CFrame = CFrame.fromAxisAngle(upv, theta + self.InputVector.X*step*self.TurningRadius)

which didn’t work at all https://streamable.com/ogczig
I want the vehicle to turn on the green axis in this picture:
Screenshot_14
and not like the green axis in this picture (which is currently how it is rotating)
Screenshot_15

What you need to do is called a change of basis. In Roblox, a basis is the up, right, and look vectors of a CFrame. When you do CFrame.Angles(0, 1, 0), you’re rotating about the up vector of a basis. Similarly, CFrame.Angles(1, 0, 0) rotates about the right vector and CFrame.Angles(0, 0, 1) rotates about the look vector. So if you want to rotate a CFrame in a basis that isn’t its own, we need to perform a change of basis. This is done using a transformation matrix.


Let’s say we have a part with some arbitrary CFrame A that we want to rotate in the basis of a CFrame B. For our purposes, the position of A is at the origin. We want to first transform A into B. This is done in a 2-step process. The first step is to transform A into the identity CFrame (CFrame.new()). We do this by multiplying A by its inverse:

A * A:Inverse() = CFrame.new()

Mathematically, we instead say

A * A-1 = I

where I is the identity CFrame. You can think of this like doing 1 + (-1) = 0; we sum 1 and the inverse of 1 to get the additive identity 0. What makes the identity CFrame unique is that multiplying the identity by another CFrame is just that other CFrame. So we can say that I * B = B. Knowing this, we can multiply what we have above by B to get B.

A * A-1 * B = I * B = B

What we have now is a transformation matrix T = A-1 * B. It’s called the transformation matrix because multiplying A by T transforms A into B. Now that we are in the B basis, we can apply whatever rotations we want. Let’s say we rotate by some CFrame R, so we have

A * T * R.

Now that we have applied the rotation, we have to transform back to the basis of A. This is done by multiplying by the inverse of the transformation matrix.

A * T * R * T-1.

Voila, we have applied a rotation on CFrame A in the basis of CFrame B.


In code, this might look like

local A = CFrame.new(#, #, #)
local B = CFrame.new(#, #, #) 
local T = A:Inverse() * B
local R = CFrame.Angles(0, math.pi / 2, 0) -- rotate 90 degrees counterclockwise about the up vector of B
local NewCFrame = A * T * R * T:Inverse()

One thing I didn’t mention is that we were dealing with rotations but CFrames also have positions. So the full solution is

local A = CFrame.new(#, #, #) 
local B = CFrame.new(#, #, # )
local T = (A - A.Position):Inverse() * B
local R = CFrame.Angles(0, math.pi / 2, 0) -- rotate 90 degrees counterclockwise about the up vector of B
local NewCFrame = A * T * R * T:Inverse()

Let me know if I can clarify anything.

5 Likes

So if you want to rotate a CFrame using it’s own rotation as the basis, would A=B?

That’s right. And if you collapse all the math, then T becomes the identity CFrame I, so you’re left with just

local NewCFrame = A * R