Open HardCoreCodin opened 2 weeks 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.
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} $$
That's right. OSL does it the same way that RenderMan did it, FYI.
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 theW
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 overloadtransform(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 ofX
inM[0]
,Y
inM[1]
,Z
inM[2]
andW
inM[3]
(see screenshot attached) Using the MaterialX Graph Editor provided with the library, applying a -30 degrees rotation using therotate3d
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 therotate3d
node in MaterialX are:point
and returns apoint
instead of avector
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'srotate3d_vector3
node should just be:vector result = (vector)rotate((point)in, axis, radians(amount))
Currently, to make that work, theamount
would have to be negated.Video: https://github.com/user-attachments/assets/b119d4a1-92c0-4e3a-a276-fd36eb1f7cc7
Additional test content: rotation3d_vector3.zip