Blade and Soul MOD > Animation Data in BNS UPKTo modify the animations in the game, AnimSequence is the one class of objects I am particularly interested in. Moreover, AnimSet also needs to be looked as it is always the parent object of a AnimSequence object. It includes shared information about all the AnimSequence objects it holds. It is difficult to find the standard definition about these two classes objects. Different versions of Unreal development kit tends to have slightly different definitions and it is likely a game developer company will add something of them own into them. Luckily, the serialized object provided a lot of information by including the meta information, such as names and types in plain text, so that the packed UPK provides valuable information. Combining with pieces of code I found here and there, I am able to depict the format of these two classes. AnimSetAn AnimSet object basically does two things: it collects a set of AnimSequence in its Sequence property and store various information shared by those AnimSequence objects. For the Blade and Soul package, the shared information includes: - TrackBoneNames: it is a ArrayProperty of indices in name table, each refer a bone name. The order of these bones is also used in AnimSequence for the ordering of motion data.
- Sequence: an array of object index, each pointing to a AnimSequence object.
- UseTranslationBoneNames/UseMovementBoneNames: another list of bone names in the form of indices in name table. It is suppose to tell game engine if the translation or movement information in the track should be used. Notice that UseTranslationBoneNames and UseMovementBoneNames do not coexist in a AnimSet. From my observation, UseTranslationBoneNames appears in body motion AnimSet and UseMovementBoneNames appears in face motion AnimSet.
- PreviewSkelMeshName: it contains which mesh should be used to preview this animation set. It is only useful in an editor.
- AdjustAniScaleBoneNames: it is an ArrayProperty of 8 bytes data type, not sure what it is so far.
- bServiced: a BoolProperty, which is always true for the package I saw.
As I see, the first three properties are important for understanding how the animation data is organized. AnimSequence AnimSet object usually holds a few AnimSequence objects, which contains the motion data. Each AnimSequence object has two parts: at the beginning, there is a serialized object, after which are animation raw data. It contains the following properties in BNS package: - SequenceName: the name of AnimSequence in export table is in a meaningless AnimSequence + ordinal number style, so this NameProperty contains the actual name of sequence. The game engine use this name to reference a certain animation sequence.
- SequenceLength: a FloatProperty, length of the animation sequence in time, unit in seconds.
- NumFrames: a IntProperty, number of frames in this sequence.
- RotationCompressionFormat: a ByteProperty, points to a string that looks like ACF_XXXX, which represents one of the format enum. It can be one of the following according to the Unreal Engine documentationhere: ACF_None, ACF_Float96NoW, ACF_Fixed48NoW, ACF_IntervalFixed32NoW, ACF_Fixed32NoW, ACF_Float32NoW, ACF_Identity, ACF_MAX. General description of them are in the the link above. The BNS introduced one of their own format called ACF_ZOnlyRLE, which is seen in other games. I have figured out this format and detailed description is in the ACF_ZOnlyRLE compression section.
- CompressedTrackOffsets: it is a int array that contains track data information. Motion of each bone in AnimSet::TrackBoneNames is stored in a track. Integers in this array are grouped in four, each four group corresponds to a track (a bone in AnimSet) in order. If there is N tracks (bones) in the animation, there are 4*N integer in this array. The four integers in a group are: TranlationOffset, TranlationNFrame, RotationOffset, RotationNFrame, respectively. The offsets are relative to end of serialized object data and both NFrames represents how many frames is actually in the track data, which may be difference than the sequence NumFrame property,
- bServiced: a BoolProperty, which is always true for the package I saw.
In BNS, track translation data is just stored as X, Y, Z in float for each frame. So the translation data for a track contains 4x3xTranlationNFrame bytes of data. The rotation data of a track is stored as quaternion, which consists of 4 numbers, named w, x, y, z. If you want to know more about quaternion, wiki should give you the answer. The rotation data is compressed as RotationCompressionFormat suggested. In quaternion, w^2+x^2+y^2+z^2 = 1, so a natural way to compress data is to leave one number out and generate them during loading process. This is the ACF_Float96NoW format. Each rotation frame has three float numbers (totally 96 bits) for x, y, z and w is left out. Beside this NoW compression, each number can be compressed using shorter bit length float or fixed point number, algorithm to decode them (except the ACF_ZOnlyRLE compression) can be found in the Unreal document. Besides ACF_Float96NoW, another format the BNS uses is ACF_Fixed48NoW. If the number of frame is greater than 1, the first 24 bytes of rotation data stores 6 floats, which are min values and ranges of x,y,z in this track. These values helps fixed point compression to recover the original value, but are also included in float tracks. If the number of frame is 1, no matter what RotationCompressionFormat say, the rotation data is always stored as three float numbers, in ACF_Float96NoW format. ACF_ZOnlyRLE Compression
This compression format is special for game of BNS. It is composite of two compression methods: ZOnly and RLE. I will explain them one by one. ZOnly is actually natural for many human joints that only has one degree of freedom (only rotate alone a single axis): such as the elbow. For these joints, one number, such as the rotation angle from the reference pose is enough to describe the pose. Using this representation will dramatically saves space without compromise motion data quality. RLE stands for run length encoding, which is good for the cases when a joint remains steady during part of the animation. Instead of specify data explicitly for each frame, RLE allows specify data for one frame, and until which frame this data lasts. On the data storage side, ACF_ZOnlyRLE has the same 24 bytes header at the beginning of each rotation track if the number of frame is greater than 1. Two more int32 are added after that. The first one is mode, and then nRLEPair. The mode can be value of 0, 1, 2 or 3. If mode is 0, ACF_ZOnlyRLE have same data format as ACF_Float96NoW. Mode 1, 2, 3 corresponds rotation along x, y, z axis, respectively. If mode is 1, 2 or 3, each frame is only one float number a ( half of the angle of rotataion in radian). The following set of equations convert a into quaternion: y = axis.y * sin(a) w = cos(a)
The (axis.x, axis.y, axis.z) is a unit vector of the axis, it is (1, 0, 0), (0, 1, 0), and (0, 0, 1) for mode 1, 2, 3. A catch here is that w in quaternion in Unreal engine should be greater than 0 always. So if cos( a) is smaller than 0, negate x, y, z, w to make sure w is greater than 0. Doing this do not change the rotation. Now, go back to the track data header. After the nRLEPair comes the RLE table, which contains nRLEPair uint16 pairs (totally 2 x 2 x nRLEPair bytes). Each pair specifies the start frame and end frame of a RLE segment. For example, if there is one pair [100, 120], it means the data for frame 100 (no matter the mode) is valid until frame 119. The next data in the track corresponds to frame 120. Notice that nRLEPair can be 0, in which case there will be no RLE table. The actual rotation track data comes after the RLE table. Moreover, for the reason of difference in frame definition, if UPK animation data is written into PSA file, Y in translation, y and w in rotation quaternion have to be negated. PSA File Format Since I have to export the animation data into PSA format for viewing and debugging, I took a look at the PSA file format as well. It turns out PSA format is quite simple after you understand it. There is official information about this format, so I do not have to really put many words here. This section serves as a stub for people to find stuff as I found the official page does not pop very easily in search engine. When you read the official description, it is better to look at the C Structure header file, which is also linked in previous official page, at the same time. It is also recommended to open a hex editor and look at the file as you go through the spec. A few things worth noting: - The data major order is different in AnimSequence of UPK and in PSA file. In UPK, order of animation frame data is like tracks of translation and rotation frames: entire data is first divided into many tracks, then each track data contains a series of translation frame data followed by a series of rotation frame data. In PSA, entire data is first divided into frames, each frame contains a number of 'keys', one key per joint (or bone, or track). Each key contains both translation and rotation information, see VQuatAnimKey structure in the header file above.
- The official document is unclear/ambiguous/wrong about a few data fields, at least from my point of view. In AnimInfoBinary structure, the KeyQuotum has a vague description. It is actually equal to number of frames times number of joints (or bones, or tracks). The TrackTime field in the same structure, I have to set it to float type of total number of frames to make it display right in SkelEdit of Gildor.
- Different coordinate system is used in PSA and Unreal game engine, which require some care when converting data back and forth. Please read the end of previous section if you are confused about this matter.
|
|