Representing a CFrame with the minimum amount of numbers possible

A couple things I’m interested in:

  • Why can’t you represent 3d rotations with 2 components?
  • Why can you with 3? (euler angles)
  • [SOLVED} Are [the rotational matrix part of] CFrames orthogonal matrices (specifically, are the row vectors orthogonal)? (how?)
  • What’s the best (define best/the options for it please) way to get the 3 components? (would be awesome if you compared different methods) I know toEulerAnglesXYZ() works

My intuition tells me that the answers to the first two is that because by definition, a 3d rotation space is formed by 3 independent basis vectors, so 3 is the minimum amount (and Euler angles satisfy it since the axes are all independent) Although is this really the proper way to think about it? (Since the vectors span more than up to 180 degrees (which is the rotation limit sine it “wraps” back around))

Thanks

The questions you are asking are pretty meta. You’re basically asking why logic is logic. It’s just how it is, but we can try, as you will see in this reply.

If you wanted, you could probably represent a 3D rotation in two numbers (or in a single number) if you used it as a code instead of as an actual value (meaning for it to be actually useful, it’ll need to be interpreted into 3 values). The number would get really huge, though, and if you wanted to be able to use decimals it would get wayyyyy bigger.

For example of how you could fit two values into one number (whole numbers only):

You have a number (N) and a resulting pair of numbers (X, Y).
There are two rules – the first is that if the number is smaller than or equal to your maximum number (in this case, 360), then the value of X is equal to N and the value of Y is equal to 0 (X = N, Y = 0).

The second rule is more complex – if the number is bigger than 360, you can find the value of X by dividing N-360 by 360 and then flooring it. You can find the value of Y by doing a modulo operation (get the remainder, essentially) on N: N % 360. You can represent this as a final equation, like so: X = math.floor((N - 360) / 360), Y = N % 360. My equations probably need testing to make sure they are right.

It’s an ugly solution and has a lot of flaws (pun intended), but can work maybe. For whole numbers, the highest value of N will be 361^2 (+1 to 360 for 0 and +1 to 360 for the first rule).

Long story short, there isn’t a good away AFAIK. I kinda wasted a good 20 minutes figuring my idea out lol, so hopefully it helps :wink: I guess I learned some stuff myself so /shrug

1 Like
  1. You can’t represent any rotation with just two components. If you have a starting vector, you can rotate it to any other vector with two Euler angle components (Eular angles being a rotation about a predefined Eular axis), however when a body has a definition of ‘up’ as well as ‘forward’ then you require three. Two components makes it possible to obtain the desired ‘forward’ right side up as defined by the original orientation, or upside down.

  2. To roll about the final rotation, or even to rotate into a position where the desired orientation can be reached requires a third rotation. Think about it, by rotating a point on a sphere (lat and lon) there are only four ways to reach another desired point. Unfortunately we can only get their upside down or right side up. Once we ad a third possible rotation, we can spin the point where ever we land and the reaching not just the desired location on a sphere, but orientation becomes possible.

  3. CFrames also contain a positional offset relative to world space. Literally their components (the 12 numbers in their matrix) are the vector position, and the 3 object space axes. CFrames, in order to be a proper rotational matrix must have their three axes defining the orientation linear. I’m not sure what you want to know when you ask ‘how’, but a third orthogonal vector to two original vectors can be obtained by taking their cross product. If these vectors are not orthogonal then they also define a skew. If I was to take all the corners of a part and multiply their positions by a rotation matrix whose axes were not orthogonal, then the box the points would form would be skewed. More than that, if these vectors are not units then they can also scale the resulting object space.

Here are some examples. I placed a part at the origin of the workspace with no rotation (1 stud high, 2 studs on the z axis, and 4 studs on the x axis) and put this script inside of it:

local part = script.Parent

local newP = Vector3.new(0, 5, 0)
local newX = Vector3.new(1, 0, 0)
local newY = Vector3.new(0, 1, 0)
local newZ = Vector3.new(0, 0, 1)
local skewCF = CFrame.new(
	newP.X, newP.Y, newP.Z,
	newX.X, newY.X, newZ.X,
	newX.Y, newY.Y, newZ.Y,
	newX.Z, newY.Z, newZ.Z
)

local s = part.Size
part:Destroy()
for x = -s.X/2, s.X/2, s.X do
for y = -s.Y/2, s.Y/2, s.Y do
for z = -s.Z/2, s.Z/2, s.Z do
	local corner = Vector3.new(x, y, z)
	
	local original = Instance.new 'Part'
	original.Anchored = true
	original.Size = Vector3.new(0.2, 0.2, 0.2)
	original.Position = corner
	original.Parent = workspace
	
	corner = skewCF * corner
	
	local mutated = Instance.new 'Part'
	mutated.Anchored = true
	mutated.Size = Vector3.new(0.2, 0.2, 0.2)
	mutated.Position = corner
	mutated.Parent = workspace
end
end
end

When I run this code as seen above (which defines an identity rotation) I get the following result:

When I replace the axes with still orthogonal, but scaled vectors, it scales the resulting points. Below I have halved the size of the x vector (the longest side of the part). What I get is a new part with size 1x2x2.

local newX = Vector3.new(1, 0, 0) / 2
local newY = Vector3.new(0, 1, 0)
local newZ = Vector3.new(0, 0, 1)

Now, when a 90 degree rotation about the Y axis occurs, the new x Axis (starting on the left) moves to where the forward axis (z) was pointing. The z (forward) axis also moves clockwise to the right side (-x axis). So when I replace the x and z axes in a CFrame with what they become after a 90 degree rotation, then the resulting CFrames defines a space with the new rotation:

newX, newZ = newZ, -newX

or

local newX = Vector3.new(0, 0, 1)
local newY = Vector3.new(0, 1, 0)
local newZ = Vector3.new(-1, 0, 0)


But note that if I didn’t make the new Z axis negative, the parts in this example would still end up in the right place (although drawn in a different order, the other z side being drawn first). However, don’t be fooled! Even though the vectors are still orthogonal to each other, a normal right hand rule coordinate system:

could never reach this left hand rule configuration. Through a ‘regular’ rotation in the sense that we are used to. The z axis is inverted! The object space the CFrame defines has an inverted z axis. Now, this is cool! When you set a part’s CFrame to one which is inverted, the following effect occurs:

newX, newZ = newZ, newX
…
part.CFrame = skewCF

Don’t forget to remove the destroy part line:
part:Destroy()


Note that while none orthogonal rotation matrices (which we’ll get to in a minute) and scaled rotation matrices are filtered when used in a part’s CFrame. However it is difficult to detect when an axis is inverted, so we get this funny effect. (Which you may see in your future toying with CFrames, so remember it for when you encounter it!) I believe it used to be possible to set the Camera’s CFrame to a skewed CFrame, and you’d have this cool skewed vision. But I’ve tested it and it seems this is no longer possible.

Now, for a rotation matrix which is not orthogonal, it skews the resulting space. If we change the xAxis to be 1 unit long in the world x direction, and 1 unit long in the z direction, then for every unit the part is long in the x direction, it will grow that much longer in the z direction. Here is a snap shot of the result like I’ve been showing, and from the top down. Note that it is still as long in the x axis as before (there is still one unit being added in the x direction for every x unit), but is longer in the z direction:

local newX = Vector3.new(1, 0, 1)
local newY = Vector3.new(0, 1, 0)
local newZ = Vector3.new(0, 0, 1)

or

newX = newX + newZ


SkewTopDown

So to sum it up, we need 2 components to rotate a vector to another vector. We need 3 to rotate a orientation to another orientation. CFrames, made up of many more than 3 components, can define all sorts of cool effects. They have position as well as orientation, and define a space rather than just an orientation. They can be skewed, scaled, and inverted. While it is true that a CFrame could be defined with 3 coordinates for position, 2 rotations for the forward axis, and 2 rotations for the side or x axis, it wouldn’t work out as slick as a rotational matrix! Literally all you have to do to rotate a vector into a CFrame’s object space is to perform matrix multiplication! Super slick, and much faster than an implementation optimized for space. (haha, space…)

  1. You mean the 3 angles? Yeah, getting those out of a rotational matrix is difficult. There are formulas posted on the internet to get it directly from the components. Another option would be to multiply the forward unit vector (0, 0, 1) by the matrix, find the two angles that made it up. Then, take the y vector and multiply it by the matrix and find the angle between the resulting vector and the y vector to find out the final rotation. Really, the best way to find the Eular angles is to use the function built into the CFrames. It is fast since it is written in C++. If you can get away without needing the angles (through vector math/rotations) then you can easily access CFrame.LookVector, RightVector, and UpVector.

(Oh, if you want the place file I used to create all these snippets, here you are! Have fun, learn lots: CFrameTesting.rbxl (12.6 KB)
)

4 Likes

If you think of the collection of possible rotations as a geometric space, one can assign a dimension to this space. The space of 3D rotations is 3-dimensional, so we need 3 coordinates to make a “map” of this space, just as we need 2 coordinates for a sphere.

Note: the only time where N dimensional space has an N-dimensional group of rotations is in three dimensions. In 2D we need one coordinate, and in four dimensions we need six. In general, the space of rotations in N-dimensional space has dimension N(N-1)/2.

I’m going to assume you meant rotations, and not CFrames (which are a special class of affine matrices). Hint: take the defining equation A^t * A = A * A^t = I, and expand it out. It implies that the set of rows is pairwise orthogonal, and the same for the set of columns. Also, you can show that A^t * A = I implies A * A^t = I, and vice versa.

There are 12 different “Euler angle” combinations (more accurately Tait-Bryan angles), based on what order you do the rotations. However there are also other formalisms, which you can find here.

2 Likes

i imagine there are some sort of proofs about this stuff and people throughout history haven’t just been guessing and it has all been true so far


I’m not really interested in compressing two (or 3) independent numbers into one number because that is impossible if each number can take on infinite possibilities, im interested in finding minimum amount of numbers and what they are such that accuracy is not sacrificed for representing the orientation of a cframe

1 Like

I’m not convinced as to why that’s true

do you mean by applying negative or positive rotations restricted to mod 180? aren’t there infinite ones since x+180 == x %180
also I can only imagine getting to a point with 2 components with 0 roll (not 180 roll like you said here:)

I’m trying to get a better understanding though than visual speculation

yea my bad

also i’m aware that the column vectors are orthogonal unit vectors (rightVector, upVector, and backVector)
but I’m interested in the row vectors
aka
does r00^2+r01^2+r02^2 always = 1

so basically if right:Cross(up) == look then thats left handed and aka it would be an inverted part? (which would be a way to check if the axis is inverted?)

about a month ago i accidentally did it and it was annoying lol

i don’t necessarily care about the euler angles, i just want 3 scalars that define the orientation of a cframe

would the yaw of the backVector be the same as the yaw of the upVector?

What he’s saying is essentially true. Let u,v,w be a linearly independent basis for 3d Euclidean space, and let u’,v’,w’ be another such basis. The equation R[u v w] = [u’ v’ w’] has a unique solution, equivalent to solving the systems Ru=u’, Rv=v’, and Rw=w’ simultaneously. This is a system of 9 linear equations and 9 unknowns (the entries of R), hence the solution is unique.

To put this in context, consider u,v,w to be the axes of a rigid body at time t0, and u’,v’,w’ to be the axes of that body at a later point in time t1. Then there is exactly one rotation which moves that rigid body from t0 to t1, which would be R.

1 Like

and i guess it is a wrong way of thinking about it? since:

but that’s not really helpful for me because I want to understand why

ya lol my bad

dang I was trying to use cross product and column magnitude xd thanks a lot

can you say let A be B^T?

Doesn’t this show that 3x3 orthogonal rotation matrices can transform each other? So wouldn’t you need a bijection between rotation matrices and 3d euler angles?

I’ve tried a couple times to look at the proof for Euler’s Rotation Theorem but I don’t understand both the Matrix proof and Euler’s proof

(Which is also (along with lack of linear algebra) impeding me from comprehending your wikipedia link)

The most accessible way to understand “why” is to study the system of polynomial equations given by the condition A^t * A = I. This gives rise to 6 distinct equations in 9 variables. Roughly speaking, each equation takes away a degree of freedom, which means that you are left with 3 degrees of freedom, subtracting 6 equations from 9 variables.

Likewise, in four dimensions, you have 10 distinct equations in 16 variables, which leaves 6 degrees of freedom.

There are certainly some more technicalities, but I think this is a good vantage point for understanding this sort of thing.

Yup, that’s one way to do it.

I don’t understand. Yes, orthogonal matrices can act on each other, but what does that have to do with Euler angles?
Also, the Euler angles parametrization is not a bijection. That’s why phenomena like gimbal lock occur.

2 Likes

Actually, substituting A=B^t doesn’t prove that B itself is orthogonal. We need more to show why this is true.

The proper way to do it is by multiplying on both sides: A * (A^t * A) * A^-1 = A * I * A^-1, and then simplify.

1 Like

are the equations:

r00^2+r01^2+r02^2=1
r10^2+r11^2+r12^2=1
r20^2+r21^2+r22^2=1
r00*r10+r01*r11+r02*r12=0
r00*r20+r01*r21+r02*r22=0
r10*r20+r11*r21+r12*r22=0

is there any easy way to see that they are independent?

I didn’t (still don’t) understand what your explanation has to do with Euler angles, I thought you were talking about matrices

I guess bijection is not the right word, showing an injection from orthogonal rotation matrix to Euler angles would suffice since that means there exists an Euler angle for every orthogonal rotational matrix

I thought gimbal lock occurs because you can’t lerp the components independently, what does not being bijective have to do with gimbal lock?

That works too if you assume A is orthogonal (like I was assuming B is orthogonal)

As opposed to demonstrating a numerical proof, I will use a geometrical proof. They are just as valid and appeal to my visual mind. But first, lets go over some predetermined facts that I will use as the basis of my proof: An angle of rotation is defined as a clockwise rotation about an axis, the magnitude of which being described in various units (degrees, radians being two). Any possible rotation can be described by an infinite number magnitudes in both the positive (clockwise) or negative (counterclockwise) directions. Since each of these magnitudes result in the same rotation, we will only be considering the first positive magnitude that results in a given rotation in this proof. Also, any rotation of a vector results in a vector of the same magnitude. The last fact we must agree upon is that a rotation of a vector about an axis must result in a vector whose endpoint lies on a ‘circle of rotation’ whose normal is the axis of rotation, radius is the magnitude of the rejection of the original vector and the unit of the axis of rotation, and center is the projection of the endpoint of the vector onto the unit axis of rotation.

With these facts in mind, let us go on to the proof! I will go about it as I did in my previous post, but with more rigorous definitions and methods. We will first define all the possible ways to rotate a vector, and then observe that the none of them result in a desired rotation, thus proving that not all rotations can be obtained from two angle rotations about the Eular axes (hint hint). (Post proof note: I actually encountered results that were unexpected to me and disproved that any vector to vector rotation can be represented with 2 components, not just orientation to orientation rotations as I originally set out to disprove.)

Let us consider a vector which is rotated about one Eular axis. As described above, the endpoint after a rotation can only lie in a circle around the axis of rotation. Given a sphere centered at the origin with a radius equal to the vectors magnitude, then any rotation applied to the vector must result in the endpoint of the vector laying on the surface of the sphere. Also given a desired resulting vector from a rotation and its endpoint on the sphere (note that the magnitude must remain the same) then the vector could only be rotated to obtain that point if the endpoint of the original vector and endpoint of the desired vector both laid on the circle of rotation. An addition, for an angular rotation to be useful we must know which axis to rotate about. To avoid adding any more information that must be conveyed to define a rotation other than an angular rotation magnitude, we must predetermine which axis each angle specified will be around. This means that a single component can only result in a limited number of rotations. But that isn’t what we are interested in, and isn’t much of an extrapolation of our facts.

This setup gets interesting is when you consider the resulting vector of one rotation to be the input to a second rotation about a different axes. Note that this axis may also be predefined, or could be defined intrinsically as the cross product of the vector after the first rotation and the axis of rotation. Because all of the endpoints must lay on the circle of rotation about the first axis, the center of the second circle of rotation must lay on the second axis of rotation, with a distance from the origin at most equal to the radius of the first circle of rotation (Since the axis of rotations are guaranteed to be orthogonal to each other; this would take a long time to fully explain, but I hope you can see why). Now this yields a result that is surprising to me, and in fact we could stop our proof right here! Unfortunately, because the center of the second circle may not go all the way to either end of the axis, the second rotation may not be able to result in a vector whose endpoint lays anywhere on a sphere. This is counter intuitive to me since after all can’t an position on earth be described as a latitude and longitude? However in that case, the original vector is special. It is orthogonal to the first axis of rotation, resulting in circle of rotation centered at the origin with a radius of the rejection (since they are orthogonal, always the magnitude of the original vector, same as the radius of the sphere.) If the original vector is not orthogonal to the first axis of rotation, then the resulting vector’s endpoint can only lay on a band around the sphere of rotation. The height of the band is the rejection twice the magnitude of the rejection of the original vector on the first axis of rotation, the radius of the first circle of rotation, or the magnitude of the projection of the original vector on the second axis of rotation, since all those things are equivalent.

Now, we have proven that some goal vectors cannot be obtained by two rotations around orthogonal predefined axes. If the predefined axes used to define the rotations were not orthogonal then we still observe that a band of possible endpoints still results, but will always be smaller than if the axes of rotation were orthogonal. I will forgo proving this fact, but if it is an issue I can provide an explanation. If we don’t use a predefined axis then we would be using more than two components. There are currently proofs out there that any desired rotation can be obtained from an axis-angle representation (4 total components). We may also need to consider not just applying rotations one after the other, but simultaneously and intrinsically. These are a bit more difficult to work with, so I will forgo proving them unless you REALLY want to put me through that suffering… just trust me, it isn’t any more powerful unless you can alter the rate at which the rotations are applied or only perform a fraction of the rotation. Both methods require another component.

Since not every vector that can be obtained from rotations (any vector with the same magnitude) then I assert that not every rotation can be obtained by two angles without additional information.

(Note: this post lends itself to easy visualization. I’ve become lazy and haven’t included images. If someone mentions that the post was difficult to follow, then I’ll edit this post to include images.

2 Likes

If you don’t mind, is it okay if I ask questions along the way as I’m reading this and to continue reading on after clarification?

I don’t really understand this
When you say “radius is the magnitude of the rejection of the original vector and the unit of the axis of rotation” do you mean it is sqrt(||original vector||^2 - original vector:Dot(axis)^2)

I’m not familiar enough with matrices to answer this question. But essentially what you are asking is what is the relation of the rows to the columns of any matrix. I can’t think of a counter example off the top of my head either, when the column vectors are orthogonal but the row vectors are not. :frowning:

Yeah, you are right… I didn’t think about that statement too hard. :stuck_out_tongue: I wonder why they allow a left handed rotational matrix in a right handed system.

(I saw that you posted a question on my last reponse, so now I’ll go on to answer that before I continue.)

1 Like

It’s ok, @suremark answered it

GODLY inverted parts XD

1 Like

Yes, that would be the magnitude of the rejection and radius of the circle of rotation. Here is a picture from the Wikipedia page on vector projection to clarify:


a1 is the projection of a onto the unit vector in the direction of b, and a2 is the rejection of a on the unit vector in the direction of b.

1 Like

I don’t understand what you mean by “the center of the second circle may not go all the way to either end of the axis”

edit: this is probably what it means xd
well it deleted my msg but what it said before was something like "is it because it is limited by the radius of the first axis’ circle "

you could just assume that the original vector is collinear to one of the 2 basis vectors and create a matrix that transforms the right hand side by that rotation (so if you were to do a direction transformation with no matrix multiplication i think it comes out to be sort of what you mentioned with crossing original vector with 1st axis but instead it would be the 2nd axis is the unit of the original vector and the 1st axis is the cross of the original vector with upVector / some other vector (with a hard coded case where if the original vector == upVector then it just = lookVector or something idk (similar to how Roblox handles CFrame.new(vector3,vector3))

and if you wanted to do roll +yaw/pitch instead of yaw & pitch you would make 1st axis be what i said was 2nd axis and vice versa

so then I guess its obvious that there is a tradeoff between which of the three (roll,yaw,pitch) to exclude if you have 2 and with 3 basis you could just first roll it then apply yaw & pitch

Edit if we treat the original vector as original cframe instead and define roll to = acos(original.upVector:Dot(Vector3.new(0,1,0))) then I think you can only change roll “intentionally” (what I mean is that the transformation that upVector and lookVector undergo isn’t the same) if the axis of rotation is collinear with upVector (in which case only lookVector is changed) or collinear with lookVector (in which case only upVector is changed)

Sorry I took so long to respond, I was making a model to represent what I was talking about. Below you see the first circle of rotation and the sphere of rotation as I defined them:

If the original vector’s endpoint was on the edge of the red circle and the axis of the rotation was the green vector, then the circle of rotation would be the red circle. Note that the radius of this circle doesn’t extend all the way out to the full length of the radius of the sphere. In the example above I chose the red vector to be the axis of rotation (which one doesn’t matter). What you can observe is that the projection of any point on the edge of the circle of rotation will not have a magnitude greater than the radius of the circle of rotation. If we integrate the circles of rotation defined by the second axis of rotation along the length of the projections of the first circle of rotation on the axis of rotation, then we get the transparent area seen in the image above. This leaves the opaque caps not covered as possible results of the second angular rotation, no matter what the first angular rotation was.

Hopefully that helps a bit… It makes sense in my head :stuck_out_tongue:

1 Like

Using spherical coordinates with no radius, x = cosθsinφ, y = sinθsinφ, z = cosφ converts 2 angles into a direction. What’s missing is a third angle to clarify the perpendicular direction.

A CFrame just needs 3 more numbers for position. It requires 6 in total.

1 Like