0
\$\begingroup\$

I have researched this a lot and so far all the examples, tutorials etc always use the following order: Scale * Rotate * Translate but for some reason this order must always be backwards in my Scene Graph. I am not sure if this is intended due to transforming local matrix to world matrix or it is an inherent bug in my application.

Here is my simple Scene Graph implementation. I am using Vulkan and GLM math library.

void SceneNode::setTransform(glm::mat4 transform) {
  transformLocal = transform;
}

void SceneNode::update() {
  // if root node
  if (parentNode == nullptr) {
    transformWorld = transformLocal;
  } else {
    transformWorld = parent->transformWorld * transformLocal;
  }

  for (auto &node : children) {
    node->update();
  }
}

And my shader logic:

// initialization code hidden for simplicity

void main() {
  // worldTransform = transform matrix calculated during scene node update
  gl_Position = UniformCameraData.projectionView * ModelPushConstants.worldTransform * vec4(vPosition, 1.0f);

  outColor = vColor;
}

As a demo, I have created a super simple scene with spheres:

Root
  Sphere Center -> Rotate around its own axis
    Sphere 1 -> Position somewhere near the parent and Rotate around its own axis
    Sphere 2 -> Position somewhere near the parent andRotate around its own axis
    Sphere 2 -> Position somewhere near the parent andRotate around its own axis

This scene graph gives me one sphere in the middle that rotates and three spheres that rotate due to being children of this sphere; plus, the spheres themselves rotate around their own axis:

Demo - Intended

However, the implementation of it is quiet weird to me. The order of transformations is Translate * Rotate * Scale instead of the other way around:

child1->setTransform(
          glm::translate(glm::mat4{1.0}, glm::vec3{2.0, 0.0, 0.0}) *
          glm::rotate(glm::mat4{1.0f}, glm::radians(frame * 0.9f),
                      glm::vec3{0.0, 0.0, -1.0}) *
          glm::scale(glm::mat4{1.0}, glm::vec3{0.4, 0.4, 0.4}));

// ...other children transformations

scene->update();

When I change the order of transformations, the rotation happens around the central sphere:

child1->setTransform(
          glm::rotate(glm::mat4{1.0f}, glm::radians(frame * 0.9f),
                      glm::vec3{0.0, 0.0, -1.0}) *
          glm::translate(glm::mat4{1.0}, glm::vec3{2.0, 0.0, 0.0}) *
          glm::scale(glm::mat4{1.0}, glm::vec3{0.4, 0.4, 0.4}));

// ...other children transformations

scene->update();

Demo - Wrong transformation

As you can see, the cyan color sphere rotates around the parent instead of its own center.

I was confused by this so, I added translation to the root node (e.g (2.0, 1.0)) and got the same result. If I rotate the sphere, then translate, it will rotate against (0, 0), then translate to (2.0, 1.0) at the same time, which will mean that it is at (2.0, 1.0) and rotating against (0.0, 0.0). On the other hand, if I translate first, then rotate, it will rotate against (2.0, 1.0) and translate to (2.0, 1.0) at the same time, which will mean that its rotating against the newly translated center.

Logically this makes sense to me because these multiplications are happening at the same time and the result is written to node but I still do not understand why the typically suggested transformation order is Scale * Rotate * Translate. Can someone explain to me what I am missing here that I have to use multiply transformations is in reverse order?

\$\endgroup\$
1
\$\begingroup\$

For theoretical understanding you'd have to dig a little deeper in how matrix transformations work in 3D space and how they affect the 3D coordinate basis. The simple answer is the following:

Matrix multiplication is not commutative. Changing the order of multiplication changes the result and the assumption that multiplications are happening at the same time means they should yield the same result is not correct.

More illustrative answer:

When you want to apply a rotation to your object, the end result you are expecting is for it to change the direction "it is looking at". This works if you use a rotation transform such as $$R=\begin{pmatrix} \cos\theta & -\sin\theta & 0 \\ \sin\theta & \cos\theta & 0 \\ 0 & 0 & 1 \end{pmatrix}.$$ This transformation will rotate the object by \$θ\$ degrees around the \$z\$ axis and this is what's important here. Which \$z\$ axis are we talking about exactly? In this specific case we're talking about the axis of the rotated object's coordinate basis i.e. the normal vector \$\vec{z} = (0,0,1)\$.

Translating the object also moves it based on its coordinate basis. The same for a combination of those transformations. But why does changing the order change the result? Because matrix multiplication is not commutative as mentioned in the beginning. Changing the order of multiplication changes the result.

Let's assume you are rotating a sphere as in your example.

If you do Rotate->Translate:

  1. The object is rotated around the \$\vec{z}\$ basis vector. The rotation "pivot" of the sphere in this case is its center i.e. the point \$(0,0,0)\$ which means it rotates like the earth around its own axis.
  2. Translation moves the sphere to a designated position in 3D space. Let's say at \$(3,2,0)\$

If you do Translate->Rotate:

  1. The sphere moves to a designated position in 3D space. Its center point is now \$(3,2,0)\$
  2. The object is rotated around the \$\vec{z}=(0,0,1)\$ basis vector of that same coordinate basis. The pivot of the rotation remains the same as previously at \$(0,0,0)\$ but the sphere center is now at \$(3,2,0)\$. It now goes all the way around like the earth rotates around the sun.

Crude illustration of the above

It is important to understand that a transformation doesn't change the "pivot" point. Everything is expressed based on the object's coordinate basis. If you want to change that you have to actually do a change of basis operation. This is what the localToWorld matrix operations actually do.

\$\endgroup\$
3
  • \$\begingroup\$ What you explained made sense but I still do not understand why in the examples that I provided, the transformations provide wrong results when rotating, then translating but provide correct results when translating, then rotating; because currently I am doing Translate -> Rotate to get Rotate -> Translate that you mentioned. \$\endgroup\$
    – Gasim
    Jun 18 at 9:12
  • 1
    \$\begingroup\$ @Gasim no, you are not. The transformations we apply in games — where we conventionally interpret the vector as a column matrix being multiplied on the right side of the transformation matrix — read right to left, with the "first"/"innermost" transformation being the one closest to the input vector: ie. furthest to the right. So translation * rotation * scale means "do scale first, then rotation, then translation last" \$\endgroup\$
    – DMGregory
    Jun 18 at 10:41
  • \$\begingroup\$ Thank you for the help! I need to revise my matrix math and 3d transformations knowledge :) \$\endgroup\$
    – Gasim
    Jun 18 at 12:15

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.