Handling the Edge Cases of CFrame.fromMatrix()

Return

Hey y’all!

It’s International Sushi Day! :sushi: :earth_americas:
I personally don’t eat it, but I know many of you do and I’ve heard most people love it! Enjoy your sushis to help you get through quarantine!


Anyways, apparently this is turning into a series. CFrame.fromMatrix() series – is that a good name? It’s good enough for now.

As you may know, CFrame.new(pos, lookAt) has sadly been deprecated in the favor of the more evil cousin CFrame.fromMatrix(). In the last tutorial, I tried to help you understand the inner workings of it and help you get comfortable with it. I know it’s much more difficult than the deprecated version, but hey, until something happens, we need to get used to this new one.

[color=#e02626]As per today’s tutorial, it’s an addition on top of the last one, meaning, you’ll need to know the concepts from the last tutorial. So, click the link right above to refresh your memory (if you want).[/color]

So, you may be asking, why’s there so much to learn about this new replacement? Well, that new replacement isn’t exactly perfect. It works 99% of the time but has that 1% being tricky edge cases where the CFrame just breaks. For example, the position or orientation of a part may turn into huge numbers:

image

It’s our mission to correct them mistakes!


Credits

Before I start, I’d like to say that this entire tutorial is based on and due to @EgoMoose’s post that brings up the edge cases in the first place. You know, it’s not easy to stumble across them, but thanks to him we have what we need. All we have to do it understand it!

Thanks to @Halalaluyafail3 for correcting some of the code.


Refresh Your Memory

In the last tutorial, we created this function to simulate CFrame.new(pos, lookAt) since we were all used to it.

local function getCFrame(position, lookAt)

    local lookVector = (position - lookAt).Unit 
    local modelUpVector = Vector3.new(0, 1, 0)
    local rightVector = lookVector:Cross(modelUpVector)
    local upVector = rightVector:Cross(lookVector)

    return CFrame.fromMatrix(position, rightVector, upVector)

end

taylor.CFrame = getCFrame(taylor.Position, swift.Position)

As simple as it looks, it’s not the best at handling said edge cases.


Edge Cases

Here’s when the CFrame glitches out:

  1. If the part is at the position and is looking at the same position, then it’ll glitch. It’s physically not possible as it’d be facing inside itself.

  2. If the part is facing straight up or straight down, it breaks maybe do to it getting confused about the front face being the top/bottom face IDK?


Breaking Down the Code

So, keeping those two edge cases in mind, our function needs to be designed in a way to catch those instances.

--function lookAt 2.0!
local function lookAt2(eye, target) --be at eye, face target

    local lookVector = (target - eye)--a vector going from eye to target, aka look vector

end

We’ll change this to a unit vector shortly after!

Let’s take care of the first edge case where the part is facing the position it is in. So, there are two ways of doing this:

--Method 1
if eye == target then

end

--Method 2
if lookVector:Dot(lookVector) <= 1e-5 then 

end

So, why not magnitude instead of the dot product? Well, simply put, magnitude requires using square roots, which in Lua, are quite expensive (thanks to @EgoMoose for explaining that).

Let’s understand the dot product for a second. Unlike the cross product, the dot product IS commutative and returns a scalar (AKA a number) instead of another vector. Basically, it describes the relationship between the directions of two vectors. If they’re facing in opposite directions, then the dot product turns out to be -1, if they’re pependicular, it’s simply 0.

But here, we’re getting the dot product of a vector with itself, so what would that be? Another thing about the result of this is a vector dot product with itself is always its magnitude squared. Therefore, if we’re checking that a vector’s magnitude is 0, we’ll need to see if the dot product equals 0 as zero squared would be zero.

I do think that this will leave some of you confused, so I’d love to make a dot product tutorial soon. Plus, it’s actually quiet a cool topic. What do you guys think, should I do it?

Also, why 1e-5. Thanks to @Halalaluyafail3 for reminding me. When you just do == 0 then it will too strict and won’t catch any close values, which may also be “invalid” (inside the part), so it’s great to have it be a little more than 0.

Now that we have that sorted out, let’s add in the code inside of the conditional:

if lookVector:Dot(lookVector) == 0 then 

    return CFrame.new(eye) --just turn the position to be at into a CFrame

end

This CFrame constructor is not deprecated, it’s only the one with the pos and lookAt arguments!

One last thing for the first edge case. We need to make the look vector unit vector if it passes the if-statement:

if lookVector:Dot(lookVector) == 0 then 

    return CFrame.new(eye) --just turn the position to be at into a CFrame

--added this
else

    lookVector = lookVector.Unit

end

Alright, onto the second edge case where the look vector is facing straight up or straight down. Before we start on that, we need to define some basic vectors as we’ll use them later on!

local vecX = Vector3.new(1, 0, 0) --going right
local vecY = Vector3.new(0, 1, 0) --top
local vecZ = Vector3.new(0, 0, 1) --???

Where does vecZ go? If you’ve stumbled across this before, the front face of a part is not pointing in the positive Z direction, it’s actually the negative direction. Meaning, vecZ goes to the “Back” direction. I just wanted to clear that up because unlike the other two vectors, the look vector does not point in the positive direction, it’s the reverse for Z direction for some reason. Poor Z

Anyways, we know that a 3D vector is composed of 3 components:

vector A = <x, y, z>

The maximum value for all three components is 1 due to us dealing with unit vectors.

So, if a vector is not diagonal, then only one component is greater/less than 0. For example, to face straight to the right, only the X position needs to be above 0 (left would be below zero).

vector A = <1, 0, 0> --stright to the right!
vector B = <-1, 0, 0> --to the left

Similarily, a vector pointing straight up or down would be <0, 1, 0> or <0, -1, 0>, respectively. If you notice, both of them have an absolute value ot 1, meaning their “distance” from 0 on the ruler is 1.

So, that means we can just do this, right?

if math.abs(lookVector.Y) == 1 then

end

Not quite.

There’s something called the floating point error where floats (or simply numbers) like these get shifted to nearby decimals. For example, it may occasionally turn 1 to 0.999. This means that it will make the vector go ever so slightly diagonal, which is not good! Over a large distance, it’s faulty direction will lead any object to stray far from the intended path.

Thank you, again, to @EgoMoose for the explanation.

This means that we need to tweak it up a little bit:

if math.abs(lookVector.Y) >= 0.9999 then

end

You can add more decimal places, but it can leave out more and more floating point errors. Similarily, putting it too low like 0.99 can result in a false positive where we want the vector to be slightly diagonal, but it’ll incorrectly get…corrected. It’s best to leave it at four decimals because, if you’ve noticed, setting properties via a float maxes out at 3 decimal places. If we intended to skew the vector a bit, then we can only put 0.999 without it going to 1, so it’s good to put the floating point error detector to 0.9999.

Now, onto adding actual meat into this code block. Remember how the look, up, and right vectors usually look? The right vector is to the right of you if you face the look vector.


Absolute directions are shown (-z, +z, +y, etc.). It’ll be useful later on.

Say that our part is facing straight up, therefore, our look vector needs to face up as well. Imagine that you rotate those vectors above by pulling the up vector to the +z direction and the look vector to the +y direction:

Relative to each other, the three vectors didn’t change. Speaking absolute, however, the up vector is now pointing in the Z direction while the look vector is going in the Y direction. That is how our part should face.

So, this should work:

return CFrame.fromMatrix(eye, vecX, vecZ)
--vecX and vecZ both go in the positive direction, which is what we want

The cool thing is that since the vectors don’t change relatively speaking, you can rotate the vectors to look like this and should still make the part face up:

However, let’s set a standard. The “correct” rotation shall be the shortest one that will move from the default orientation to facing up.

In this case, it’s where the right vector stays unchanged (you can’t see it in this view, but it’s technically behind the part) and the up vector goes to +z; AKA the original design.

We got the top situation handled, but what if the part faces straight down? Well, turn the default vectors in a way that results into this:

If we focus on the up vector for a second, it’s facing in the opposite direction of where it used to face when the look vector was facing up. So, depending on if the look vetcor’s Y component is positive or not, the up vector will be faciong +z or -z, respectively.

The method of doing that is by using math.sign(x). Simply put, it returns -1 if x < 0, 1 if x > 0, and just 0 if x = 0.

You may know that vectors, if multiplied by a negative number, flip their direction. Multiplying by a positive number will keep the direction as it is. Since the magnitude of the look vector being 0 has already been taken care of, math.sign will only return -1 or 1 in this instance.

return CFrame.fromMatrix(eye, vecX, math.sign(lookVector.Y) * vecZ)
--it'll be 1 * vecZ if facing up and -1 * vecZ if facing down
--lookVector.Y is positive when facing up, so vecZ will face in the +z direction (what we need!)

We are almost done. All we have to do is take care of all other cases.

if math.abs(lookVector.Y) >= 0.9999 then

    return CFrame.fromMatrix(eye, vecX, math.sign(lookVector.Y) * vecZ)

else

    --added this
    local rightVec = lookVector:Cross(vecY).Unit --remember vecY is (0, 1, 0), aka modelUpVector from last time!
    local upVec = right:Cross(lookVector).Unit
	return CFrame.fromMatrix(eye, rightVec , upVec )

end

And that basically sums it up! I admit, that was much longer than I thought it was going to be…

Full script (uncommented)

Note: I moved the three variables (vecX, vecY, and vecZ) outside because they shouldn’t be defined every time the function runs, it should be a one-time thing.

local vecX = Vector3.new(1, 0, 0)
local vecY = Vector3.new(0, 1, 0)
local vecZ = Vector3.new(0, 0, 1)

local function lookAt2(eye, target)
	
	local lookVector = target - eye
	
	if lookVector:Dot(lookVector) <= 1e-5 then
		
		return CFrame.new(eye)

    else 

        lookVector = lookVector.Unit
		
	end
	
	if math.abs(lookVector.Y) >= 0.9999 then
		
		return CFrame.fromMatrix(eye, vecX, math.sign(lookVector.Y) * vecZ)
		
	else
		
		local rightVec = lookVector:Cross(vecY).Unit
		local upVec = rightVec:Cross(lookVector).Unit
		return CFrame.fromMatrix(eye, rightVec, upVec)
		
	end
	
end

Closing Remarks

Yes, this tutorial was kind of advanced, but kind of not (compared to the next parts of the series I have in mind :eyes:), so it’s intermediate. But still, I want to know how well I was able to convey the information.

This question is regarding the dot product tutorial from above!
Would you like to see a whole tutorial by me dedicated to the dot product?

  • Yes
  • No

0 voters

How strong is your understanding of the topic after reading this tutorial?
1 = “you were speaking another language” and 10 = “wow, a little trouble visualizing, but if I draw it out, I understand everything!”

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

0 voters

Did you learn anything new?

  • Yes
  • No

0 voters

One, more, come on I know you can do it!

Would you like to see more parts to this series?
I have them planned out!

  • Yes
  • No

0 voters


Phew, that was a lot!

Thank you for reading this and feedback,
And stay safe everyone! :mask:

20 Likes

EDIT: Nevermind, @Halalaluyafail3’s code works. I’d update the OP.

Hey, I’ve tried using this code as a replacement for CFrame.new(position, lookAt) for my projectiles but it seems to be behaving differently. Rather than the projectiles moving forward (as they did with CFrame.new) they go up. I’m not really sure how to fix it.

local spawnPos = (self.Barrel.CFrame * CFrame.new(1, 0, -8)).p;
self.Projectile.CFrame = GameFunctions.GetLookAt(spawnPos, self.HitP); --Matrix fn
self.Projectile.Velocity = self.Projectile.CFrame.LookVector * 125;
self.Projectile.BodyForce.Force = Vector3.new(0, self.Projectile:GetMass() * 196.2, 0);

I’m using the Matrix function exactly as you have it in your post, so I’m not entirely sure what to do.

1 Like

The problem is that the cross product results in a vector with magnitude 0, so you end up with a bad rotation matrix.

This code doesn’t work, lookVector is never normalized so it could be 50 studs away but only 1 stud higher and it will force the CFrame to look up. Because lookVector:Dot(lookVector) is being compared to 0, it will often not pick up when the position is just about the same. Instead of == 0 it should be <= 1e-5 or something similar.

This should cover the edge cases correctly.

local vertical = Vector3.fromAxis(Enum.Axis.Y)
local function lookAt(eye,target)
	local look = eye-target
	if look:Dot(look) <= 1e-5 then
		return CFrame.new(eye)
	else
		look = look.Unit
	end
	local right = vertical:Cross(look)
	if right:Dot(right) <= 1e-5 then
		return look.Y > 0 and CFrame.new(eye.X,eye.Y,eye.Z,0,1,0,0,0,1,1,0,0) or CFrame.new(eye.X,eye.Y,eye.Z,0,1,0,0,0,-1,-1,0,0)
	else
		right = right.Unit
	end
	return CFrame.fromMatrix(eye,right,look:Cross(right).Unit,look)
end
3 Likes

Isn’t that variable you have called look technically the back vector. Also can you use that check on the actual look vector, that is (target-eye).Unit

1 Like

It will still work fine since he did vertical:cross(back), I just wanted to know if you can do the same on the look vector

1 Like

If you consider that LookVector is the negative of the third column, I guess it could be considered the ‘back vector’? I’ll leave it as look because it’s what i’m putting in the third column. And @TheCarbyneUniverse it’s not exactly ‘backwards’, I just get the correct vector for the third column, which is implicitly calculated in your method when you leave it out and let fromMatrix do the cross product and normalization with the other two vectors.

2 Likes

So will it work if you do the edge case check on target-eye.

1 Like