Incorrect Quaternion conversion in some rare cases.
Some quaternions do not convert correctly when decompressed and then recompressed.
Example
The vanilla, compressed binary value is: 0xBFFF 0xFFFF 0x7FFF
And has the following bit pattern: 1011 1111 1111 1111 1111 1111 1111 1111 0111 1111 1111 1111
The drop component is 0b11 = 3 = X
Which correctly decompresses to: 0.0, 0.0, 0.7071068, 0.7071068
When recompressed, the binary value is: 0x3FFF 0xBFFF 0x7FFF
And has the following bit pattern: 0011 1111 1111 1111 1011 1111 1111 1111 0111 1111 1111 1111
Note that the drop component is 0b01 = 1 = Z, not X
Cause
The DetermineDropComponent method detects duplicate component values and if any of the duplicate values are 0 (0x3FFF), the method assumes that the quaternion must be 0,0,0,1. As in the example above however, 0,0,0,1 is not the only quaternion that has components with duplicate values of 0.
There are worse examples than that.
Try: 0x4002 0x4002 0x044D Which decompresses to: 0.000129483014, 0.000129483014, -0.6595865, 0.7516287
Which will recompress into 0xC001 0x844D 0x8405 Which decompresses to: 0.3546579, 0.00008632201, -0.6595865, -0.6626941
I could explain why it does that and list everything that is wrong with the encoding and decoding of quaternions is libMBIN.
Instead i'm going to point out that the encoder Hello Games is using is bugged. Why do you think it likes to choose X as the drop component? It's becuase it thinks that is W. We know that is not W because a different drop component is chosen for the identity quaternion. So Hello Games is feeding x,y,z,w quaternions to an encoder that expects w,x,y,z.
There is really no reason to try and reimplement the algorithm it uses unless we can chooses the same drop component every time so we can compare the MBINs we create with the original. This is not possible due to data loss from rouding errors without doing some kind of correction when decoding.
Does anyone actually uses MBINCompiler or libMBIN to mod the animations? Because if no one is it seems like such a waste of time doing all that work.
Alternatives are to either replace DetermineDropComponent with something simple that works and fix all the other bugs and accept that it is never going to convert back exactly the same, or to drop the custom deserializer and store the rotations as a list of 16 bit integers in the EXML.
Considering how broken it is, it might be a good idea to add ".ANIM." to the default exclude list.
no-one would use MBINCompiler to mod anims, however NMSDK does support animations, and it's useful to know what the algorithm is so that when we add custom animations the game can read them.
NMSDK uses MBINCompiler, does it not? Does it take the rotations from the MBIN or the EXML? If it's the EXML then anyone using NMSDK to mod anims they are using MBINCompiler, even if only indirectly.
While knowing how to encode and decode the rotations is needed for custom animations, the specific algorithm for choosing the dop component is not.
Have you been able to import complex animations into blender and have them work right? If so, that says the decoder you are using is mostly correct.
In the example Fuzzy-Logik gives the re-encoded values decode to the same thing. Or at least it would if 0x7FFF did decode to 0.7071068. The current decoder decodes it to 7071499. It is 0x7FFE that it decodes to 0.7071068. That suggests that the scaling factor is slighlty off. It would be helpful to know exactly what the game does but I've not been able to find the code for this in the exe.
As far as I can tell this is how encoder they used chose the drop component:
All tests ignore the sign of the component.
- If one of the components is out of range, drop that.
- If the largest of the last three components does not have the same value as any of the other 3 components, drop that.
- Otherwise drop the first component.
The only one of those that is impotant is the first test. The out of range one. And libMBIN not doing that is why there was such a big error in the last component (W) in the example I gave. X was becuase it was dropped and then recalculated with the bad W. And Y was a one off rounding error, that could be easily fixed by rounding to nearest when doing the float to int conversion rather than just using the default round towards zero.
Also if the component to be dropped is negative you can negate the quaternion as that does not change the rotation it represents.