AcademySoftwareFoundation / MaterialX

MaterialX is an open standard for the exchange of rich material and look-development content across applications and renderers.
http://www.materialx.org/
Apache License 2.0
1.88k stars 355 forks source link

Invert direction of `rotate3d` in MaterialX v1.40 #2083

Open HardCoreCodin opened 1 month ago

HardCoreCodin commented 1 month ago

it seem to assume that the matrix constructor is expecting to receive components of basis vectors as: X.x, Y.x, Z.x, W.x, X.y, Y.y, Z.y, W.y, X.z, Y.z, Z.z, W.z, X.w, Y.w, Z.w, W.w This is derived by the negative sign of the sine in the 2nd argument to the constructor, and positive sign in the 5th argument (see screenshots). As far as I know, the correct expected ordering, in both OSL and GLSL, should actually be: X.x, X.y, X.z, X.w, Y.x, Y.y, Y.z, Y.w, Z.x, Z.y, Z.x, Z.w, W.x, W.y, W.x, W.w That is according to the OpenGL specification stating that the matrix representation holds the translation in the 13th, 14th and 15th components (out of 16). And translation in a 4x4 matrix is a shear in the 4th dimension so is represented by the W basis vector. And according to the OSL specification the matrix constructor is expecting components in row-major order (which I interpret to mean, providing contiguous basis vectors one after the other) This is further corroborated by the OSL overload transform(matrix, vector4) function bundled with the MaterialX library from the official OSL repository Where, the manual matrix/vector multiplication implementation contained, shows that the matrix indexing assumes contiguous basis vectors of X in M[0], Y in M[1] , Z in M[2] and W in M[3] (see screenshot attached) Using the MaterialX Graph Editor provided with the library, applying a -30 degrees rotation using the rotate3d node, actually shows a positive rotation. This may in first glance seem correct as the pattern seem to turn clockwise as negative rotation would suggest. Until one realizes that it is actually reversed, because the object itself is not rotating, it is the object-space position coordinates that are being rotated. So, with a positive +30 degrees rotation, the object should appear as though the space around it is rotating clockwise, as it's rotated position grab values from the rotated coordinate frame. This is illustrated in the attached image I made, and can be tested with the files attached.

So it would appear as though both OSL and GLSL implementation of rotate3d are incorrect (transposed in the matrix construction). Included is the same behavior in Blender, using the OSL code from MaterialX.

From discussion on chat, it has been suggested that the actual reason is a deliberate choice to rotate from the other side of the axis vector. To that I've responded with the following:

Imagine an actual physical 3D model of an orthonormal basis, basically 3 cylindrical arrows, colored red, green and blue, and connected at their base as the origin. When someone says to you, rotate this object by 30 degrees around the X axis, what do you do? I think everyone would agree that you grab the red arrow, looking at it where it's arrow-tip is pointing at you, and rotating it counterclockwise to form a positive angle. That is not a controversial convention, and is how 3D rotations work in any 3D math library that I know of. OSL has a built-in rotate() function that does exactly that - it takes an arbitrary axis vector and an angle. The only differences between it and the rotate3d node in MaterialX are:

  1. It assumes the axis vector is normalized so does not normalize it
  2. It works with radians, not degrees
  3. It operates on a point and returns a point instead of a vector

MaterialX does not have a point type and I'm not suggesting that it would, and working with degrees is totally fine. Normalizing in the MaterialX is also fine, it is safer that way. But they definitely should both be rotating in the same direction. In fact, I would go further and say that the OSL back-end implementation of MaterialX's rotate3d_vector3 node should just be: vector result = (vector)rotate((point)in, axis, radians(amount)) Currently, to make that work, the amount would have to be negated.

RotationAroundZaxis_MGE MaterialX_rotate3d_OSL_vs_GLSL OSL_transform_matrix_vector4 RowMajor_VectorMatrixMultiplication RotationAroundZaxis_BlenderOSL

Video: https://github.com/user-attachments/assets/b119d4a1-92c0-4e3a-a276-fd36eb1f7cc7

Additional test content: rotation3d_vector3.zip

jstone-lucasfilm commented 1 month ago

Thanks for this great overview, @HardCoreCodin!

My sense is that we should address this in MaterialX v1.40, inverting the rotation direction in rotate3d and adding a version upgrade path that maintains the visual interpretation of existing documents from v1.39 and earlier.

roelandschoukens commented 1 month ago

Yeah there is a subtle difference between OSL and GLSL matrix math:

Consider a basic affine transform:

$$ \begin{pmatrix} m{11} & m{12} & m{13} & t\mathrm{x}\ m{21} & m{22} & m{23} & t\mathrm{y}\ m{31} & m{32} & m{33} & t\mathrm{z}\ 0 & 0 & 0 & 1\ \end{pmatrix} \cdot \begin{pmatrix} p\mathrm{x}\ p\mathrm{y}\ p_\mathrm{z}\ 1\ \end{pmatrix} $$

In OSL you write

matrix M = {m11, m21, m31, 0,
            m12, m22, m32, 0,
            m13, m23, m33, 0,
            tx, ty, tz, 1};
result = transform(M, p);

In GLSL it is

mat44 M = mat44(vec4(m11, m21, m31, 0),
                vec4(m12, m22, m32, 0),
                vec4(m13, m23, m33, 0),
                vec4(tx, ty, tz, 1));
result = M * p;

That is so far quite similar. Same order of elements. However, OSL specifies that the matrix above is row-major, in other words, (m11, m21, m31, 0) is the first row. While GLSL specifies the given vec4 elements are columns. This difference becomes important if you have two matrices, because multiplication is not commutative: The mathematical expression $\mathbf{M}_2 \mathbf{M}_1 \mathbf{p}$ is written as transform(M1 * M2, p) in OSL while GLSL will follow the algebraic multiplication order M2 * M1 * p.

So algebraically, the OSL expression has to be thought of as:

$$ \begin{pmatrix} p\mathrm{x} & p\mathrm{y} & p\mathrm{z} & 1 \end{pmatrix} \cdot \begin{pmatrix} m{11} & m{21} & m{31} & 0\ m{12} & m{22} & m{32} & 0\ m{13} & m{23} & m{33} & 0\ t\mathrm{x} & t\mathrm{y} & t_\mathrm{z} & 1\ \end{pmatrix} $$

lgritz commented 1 month ago

That's right. OSL does it the same way that RenderMan did it, FYI.