Hey y’all!
It’s International Sushi Day!
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:
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:
-
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.
-
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 ), 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!