.XMD - nbreeze/closers-rev GitHub Wiki
.XMD files store animations played by models in the game. Yes, Closers uses the .X file format for model meshes and vertices, but the majority of animations are stored separately in an .XMD file.
The VARINT
format used is a variant of the usual VARINT
format. The sign bit is still used to signal the end of a VARINT
, except instead of a value of 0 signalling end, a value of 1 signals the end instead, and vice versa. Fortunately, the bytes are still normally stored in little-endian order.
For more info about variable-length quantities or you have no idea what I'm talking about, look here first: https://en.wikipedia.org/wiki/Variable-length_quantity.
The fields are ordered as how they are encountered in the .XMD file itself, first field being the first one, etc.
A UTF-16 wide string.
Type | Name | Description |
---|---|---|
VARINT |
nChars | The number of characters in the string. |
WCHAR[nChars >> 1] |
chars |
Represents a standard 3D vector with X, Y, and Z axes.
Type | Name | Description |
---|---|---|
float |
x | |
float |
y | |
float |
z |
Represents a standard quaternion with W, X, Y, and Z components.
Type | Name | Description |
---|---|---|
float |
x | |
float |
y | |
float |
z | |
float |
w |
The local position and rotation of a bone in an animation frame.
Type | Name | Description |
---|---|---|
Quaternion |
rotation | |
Vector |
position |
Like Transform
, but with an added scaling field.
Type | Name | Description |
---|---|---|
Quaternion |
rotation | |
Vector |
scale | Unknown if this is the scale of the bone's size or the actual 3D transformation. |
Vector |
position |
A keyframe that contains the required 3D transformations of every bone.
Type | Name | Description |
---|---|---|
float |
time | The start time of this keyframe. |
VARINT |
nScaleTransforms | |
ScaleTransform[nScaleTransforms] |
scaleTransforms | |
VARINT |
nTransforms | |
Transform[nTransforms] |
transforms |
A single animation with keyframes. A single .XMD can contain multiple animations.
Type | Name | Description |
---|---|---|
PWSTR |
name | |
PWSTR |
name02 | A secondary name. Unknown why two names are stored, but it's there. |
float |
duration | How long the animation lasts in seconds. It should be noted that this is usually a multiple of 1/15, implying that animations run in 15 frames per second. |
char[2] |
unk01 | |
VARINT |
nKeys | |
AnimationKey[nKeys] |
keys |
Type | Name | Description |
---|---|---|
char[18] |
header |
‘CLOSERS_XMD_001 or \x91\x43\x4C\x4F\x53\x45\x52\x53\x5F\x58\x4D\x44\x5F\x30\x30\x30\x31\x00 in hex. |
DWORD |
nBones | The number of bones that each animation affects. |
DWORD |
unk02 | |
VARINT |
nAnimations | |
Animation[nAnimations] |
animations |
The transformations written in the .XMD are based on Closers's coordinate system, not in DirectX. The rotation and position axes are not aligned with each other in this coordinate system. Attempts to compose a transformation matrix from the Transform
that matched with the corresponding bone's FrameTransformMatrix in the .X file via axes inversion/swapping alone was not possible (bone indexing is discussed further in Bone Mapping). In my experience, inverting the Z axis of the position vector and transposing the rotation matrix led to a match with the corresponding .X FrameTransformMatrix, so in order to properly use the animations outside of Closers you may need to do this as well.
The .XMD file makes no named references to any bones. Seeing a total bone count given at the header and its correspondence to the number of bones in the appropriate .X model, it's safe to assume that bones are enumerated by zero-based index. The game has to save space and time, and retrieving by random access is much faster than string searching or having to do a O(n) search.
At least for the playable characters in the game (i.e. Caster, Striker, etc.), the bone indexing is based on the alphabetical sorting of bone names in the MOTION_<name>.X
model file associated with the animation. The MOTION_<name>.X
file must have the same amount of bones as the bone count given in the animation file.
Retrieving the bones of the character Soma (MOTION_ASTRA.X
) yields the following list:
BIP01 = 0
BIP01_FOOTSTEPS = 1
BIP01_HEAD = 2
BIP01_LCALFTWIST = 3
BIP01_LTHIGHTWIST = 4
BIP01_LUPARMTWIST = 5
BIP01_L_CALF = 6
BIP01_L_CLAVICLE = 7
BIP01_L_FINGER0 = 8
BIP01_L_FINGER01 = 9
BIP01_L_FINGER1 = 10
BIP01_L_FINGER11 = 11
BIP01_L_FINGER2 = 12
BIP01_L_FINGER21 = 13
BIP01_L_FINGER3 = 14
BIP01_L_FINGER31 = 15
BIP01_L_FINGER4 = 16
BIP01_L_FINGER41 = 17
BIP01_L_FOOT = 18
BIP01_L_FOREARM = 19
BIP01_L_FORETWIST = 20
BIP01_L_HAND = 21
BIP01_L_THIGH = 22
BIP01_L_TOE0 = 23
BIP01_L_UPPERARM = 24
BIP01_NECK = 25
BIP01_PELVIS = 26
BIP01_PROP1 = 27
BIP01_PROP1_BONE = 28
BIP01_PROP2 = 29
BIP01_PROP2_BONE = 30
BIP01_RCALFTWIST = 31
BIP01_RTHIGHTWIST = 32
BIP01_RUPARMTWIST = 33
BIP01_R_CALF = 34
BIP01_R_CLAVICLE = 35
BIP01_R_FINGER0 = 36
BIP01_R_FINGER01 = 37
BIP01_R_FINGER1 = 38
BIP01_R_FINGER11 = 39
BIP01_R_FINGER2 = 40
BIP01_R_FINGER21 = 41
BIP01_R_FINGER3 = 42
BIP01_R_FINGER31 = 43
BIP01_R_FINGER4 = 44
BIP01_R_FINGER41 = 45
BIP01_R_FOOT = 46
BIP01_R_FOREARM = 47
BIP01_R_FORETWIST = 48
BIP01_R_HAND = 49
BIP01_R_THIGH = 50
BIP01_R_TOE0 = 51
BIP01_R_UPPERARM = 52
BIP01_SPINE = 53
BIP01_SPINE1 = 54
BIP01_SPINE2 = 55
...
Fully enumerated, there are 158 bones in total for the model. This can be easily done by traversing through each Frame object stored in the .X file, and then sorting by name.
Opening up MOTION_ASTRA.XMD
in a hex editor, we're greeted with this:
91 43 4C 4F 53 45 52 53 5F 58 4D 44 5F 30 30 30
31 00 9E 00 00 00 02 00 00 00 8C 8C 41 00 53 00
The header is as expected for the first 18 bytes. The next field is nBones. This is stored in little endian, and 0x0000009E
in decimal is 158.
There's 158 bones in total for Soma's model, and the animation file reflects that, so both of these files are compatible. You can confirm this by doing this same process with other MOTION_<name>
files and you'll see that the bone counts do match up, enough to not be a coincidence.
To confirm that bone indexing is determined alphabetically, use the binding pose stored in the .XMD file. Interestingly, the bind pose of the character is always stored in every .XMD file prefixed with the name _DEFAULT
. It is usually the last animation stored in the .XMD file and has only 1 frame. Using an application like AssimpViewer on the .X file will show that the .X file is in a binding pose as well.
With the first alphabetically sorted bone in the list, take the position of the Transform
of the first AnimationKey
from the .XMD file and compare it with the local position of that same bone in the .X file. In the .X file, a child FrameTransformMatrix is used to indicate local position and rotation for the Frame object, and extracting the position (or more accurately, translation) from a transformation matrix is very easy. Taking into account differences between coordinate systems, the local positions between the .XMD and the .X files should yield the same thing, if not very, very similar. Even the float values match up which is almost uncanny!