[csswg-drafts] [css-transforms-2] Algorithm for decomposing a 3D matrix, produces different results to 2D matrix

birtles has just created a new issue for https://github.com/w3c/csswg-drafts:

== [css-transforms-2] Algorithm for decomposing a 3D matrix, produces different results to 2D matrix ==
Over in [Mozilla bug 1499862](https://bugzilla.mozilla.org/show_bug.cgi?id=1499862) I'm trying to work out why Gecko rotates in a different direction to WebKit/Chromium when doing matrix interpolation for some content.

I've [re-implemented both the 2D and 3D code paths in JS](https://bug1499862.bmoattachments.org/attachment.cgi?id=9018947) and done a comparison between the spec, Chromium, WebKit, and Gecko.

The particular test case I'm debugging is when animating from `rotate(180deg)` and `translate(0px) rotate(300deg)` which should use matrix interpolation.

At 50% progress the results I get are:

* CSS Transforms 1 (2D code path): `matrix(-0.5, -0.866025, 0.866025, -0.5, 0, 0)`
* CSS Transforms 2 (3D code path): `matrix(0.5, -0.866025, 0.866025, 0.5, 0, 0)`
* Chromium / WebKit: `matrix(-0.5, -0.866025, 0.866025, -0.5, 0, 0)`
* Gecko: `matrix(0.5, 0.866025, -0.866025, 0.5, 0, 0)`

In WebKit there is a separate 2D code path, but for Chromium the same 3D code path appears to be used for all cases, at least when running on the main thread (but this code appears not to have changed since it was introduced to WebKit in 2009).

After debugging where Chromium, Gecko, and the spec differ, the first place they differ is the calculation of quaternions.

After calculating the initial value of the quaternion, the spec has the following steps:

```
if (row[2][1] > row[1][2])
    quaternion[0] = -quaternion[0]
if (row[0][2] > row[2][0])
    quaternion[1] = -quaternion[1]
if (row[1][0] > row[0][1])
    quaternion[2] = -quaternion[2]
```

The corresponding code in [WebKit](https://github.com/WebKit/webkit/blob/c8125d66e8dd7adc81e45f39f75a99c85b7876eb/Source/WebCore/platform/graphics/transforms/TransformationMatrix.cpp#L489-L525) and [Chromium](https://cs.chromium.org/chromium/src/third_party/blink/renderer/platform/transforms/transformation_matrix.cc?l=702-738&rcl=9ef9d7d626a0a44c4104e3dcb60251feaa1a5df6), however, does not have these steps causing the calculated quaternion to differ.

For the 'to' value in this case I get:

* CSS Transforms 2: `0, 0, -0.49999991257807147, 0.8660254542575067`
* Chromium: `0.000000, 0.000000, 0.500000, 0.866025`
* Gecko: `-0.0, -0.0, -0.5000000126183913, 0.8660253964992068`

So it would appear Gecko is doing the right thing here. The problem is doing the right thing seems to produce a different result to the 2D code path which is undesirable.

The subsequent difference between Gecko and the spec comes about because the recompose step where the spec has:

```
// Construct a composite rotation matrix from the quaternion values
// rotationMatrix is a identity 4x4 matrix initially
rotationMatrix[0][0] = 1 - 2 * (y * y + z * z)
rotationMatrix[0][1] = 2 * (x * y - z * w)
rotationMatrix[0][2] = 2 * (x * z + y * w)
rotationMatrix[1][0] = 2 * (x * y + z * w)
rotationMatrix[1][1] = 1 - 2 * (x * x + z * z)
rotationMatrix[1][2] = 2 * (y * z - x * w)
rotationMatrix[2][0] = 2 * (x * z - y * w)
rotationMatrix[2][1] = 2 * (y * z + x * w)
rotationMatrix[2][2] = 1 - 2 * (x * x + y * y)
``` 

This calculation appears to be based on the WebKit source but will produce the wrong result. The calculation should be:

```
rotationMatrix[0][0] = 1 - 2 * (y * y + z * z)
rotationMatrix[0][1] = 2 * (x * y + z * w)
rotationMatrix[0][2] = 2 * (x * z - y * w)
rotationMatrix[1][0] = 2 * (x * y - z * w)
rotationMatrix[1][1] = 1 - 2 * (x * x + z * z)
rotationMatrix[1][2] = 2 * (y * z - x * w)
rotationMatrix[2][0] = 2 * (x * z - y * w)
rotationMatrix[2][1] = 2 * (y * z + x * w)
rotationMatrix[2][2] = 1 - 2 * (x * x + y * y)
``` 

(Notice the difference in sign in the second, third, and fourth lines.)

However, interestingly this order appears to be correct if we **don't** do the quaternion fix up steps mentioned above.

I don't really understand the theory behind all this but the summary is:

* CSS Transforms Level 1 - Does not use quaternions but tries to produce a certain rotation effect
* CSS Transforms Level 2 - Produces completely the wrong results because it includes __both__ fixups to the quaternion and then recomposition steps that presumably assume such steps were not taken
* Gecko - Follows the spec but fixes the recomposition steps so that the output is sane, but differs from what CSS Transforms Level 1 would have us do
* Chromium - Deviates from the spec during decomposition but ends up producing the same result as CSS Transforms Level 1
* WebKit - Appears to follow separate 2D and 3D codepaths but otherwise is the same as Chromium

The simple solution would appear to be to drop the following steps:

```
if (row[2][1] > row[1][2])
    quaternion[0] = -quaternion[0]
if (row[0][2] > row[2][0])
    quaternion[1] = -quaternion[1]
if (row[1][0] > row[0][1])
    quaternion[2] = -quaternion[2]
```

from the spec, but I still need to work out what this is doing (since [Gecko takes a different approach altogether](https://searchfox.org/mozilla-central/rev/0ec9f5972ef3e4a479720630ad7285a03776cdfc/servo/components/style/properties/helpers/animated_properties.mako.rs#1750-1766) that produces the same result as these steps which makes me think that mathematically they are correct.)

Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/3230 using your GitHub account

Received on Monday, 22 October 2018 06:15:24 UTC