It’s been so long since I implemented surface normal snapping that to be honest, I don’t remember how I did it. My current system relies on adding and subtracting rotational velocity in the necessary axes in order to correct the current kart normal to the surface normal (feedback control system), and then doing some CFrame magik to calculate the new CFrame of the kart in the current frame from the last CFrame, the velocity, rotvelocity and delta time.
For your application, here’s a solution I’ve just come up with:
To calculate the new roomba CFrame, you will need the following information:
*The lookvector of the last CFrame
*The normal (upvector) of the current surface.
Now, the first thing we want to do is take the last lookvector, and then apply some transformation to make that lookvector be 90 degrees to the new normal.
Before I explain that, I’m going to introduce a function that I use religiously when handling vectors:
*Flatten(vector, axis)
To flatten means to take a vector, and make that vector completely orthogonal to the input axis. In other words, imagine taking a vector, and subtracting all components that produce any movement in the provided axis. So, if we had vec(0.5, 3, 7) and an axis of vec(0, 1, 0), the output would be vec(0.5, 0, 7). This method works for more than just the cardinal axes, which is why it is so useful.
Implementing Flatten:
local new = axis
if axis.Magnitude ~= 0 then
new = axis.Unit
end
return new:Cross(v1):Cross(new)
If you just visualize an axis and a vector in 3D space that aren’t orthogonal to each other initially, and then imagine the output of each cross product one step at a time, it should be obvious why this works. I haven’t heard of anyone else using this, and maybe there is a “proper” way to do this, but cross products should be super fast to compute and it is super simple to write.
Note for the function, I put in an extra check to handle cases where the axis is not a unit vector and may have a magnitude of 0.
Anyways, here’s how I would use Flatten to accomplish your goal:
Take the last lookvector, and flatten it along the normal of the current surface. You may have to normalize the output of flatten? not sure. Anyways, the output is your new lookvector. Use this new lookvector, the current normal (upvector) and take a cross product to find the new rightvector, and then use all 3 to calculate the new CFrame using fromMatrix or the CFrame.new() with 12 arguments. Warning, the devhub page on the two cframe constructors are wrong and the ordering of vectors is flipped; you may have to mess around a bit to get the correct cframe from the 3 vectors.
This way of doing it is super simple; one limitation is that if the new normal is collinear to the last lookvector, the output of flatten will be a zero vector. You can check for this condition if the dot product of the new normal and last lookvector is zero.
One ugly but effective way to counter this problem would be to check if you are hitting the edge case; if so, calculate the new lookvector twice: First, do the calculation with the target normal, plus some minor shifting on a different axis. This is to counter the problem of the zero vector. Then, using the new lookvector you just calculated with the shifted normal, calculate the proper lookvector using the actual normal. It’s a two-step solution, can be done super quickly in the same frame; I would call it “heuristic” in the nature of the solution, but it should in theory produce the precise result always.