VMD file format explained byte by byte (header, keyframes, interpolation curves)
VMD is a simple binary: a 30-byte header, a 20-byte model name and six blocks. The pitfalls are the interpolation layout and the camera, and getting them wrong shows up as "it parses, but the playback does not match". This is what we learned writing our own parser and writer and round-tripping real files.
To just check the kind, length and bone names of a VMD, drop it into Inspect PMX/PMD/VMD. To edit it (mirror, trim, merge, change speed), pick a tool from the VMD toolbox. Neither needs upload or login (the tools have a Japanese UI).
Basics
- All numbers are little-endian. Integers are
uint32, reals arefloat32(IEEE 754 single precision). - Strings are fixed-length Shift_JIS. There is no length field; the rest is zero-padded. They are not UTF-8.
- Time is a frame number at a fixed 30 fps. Frame ÷ 30 is seconds. The fps is not stored in the file.
- The coordinate system is MMD's (left-handed, Y up). Converting to a right-handed system such as three.js needs a transform (see below).
Overall layout
The blocks appear in this order. Each block is "count (uint32) + that many records", and the record length is fixed per kind (only IK/visibility is variable).
| Offset | Content | Record length |
|---|---|---|
| 0 | Header Vocaloid Motion Data 0002 + zero padding | 30 bytes |
| 30 | Model name (Shift_JIS) | 20 bytes |
| 50 | Bone keyframe count (uint32) + records | 111 bytes |
| … | Morph (facial) count + records | 23 bytes |
| … | Camera count + records | 61 bytes |
| … | Light count + records | 28 bytes |
| … | Self-shadow count + records | 9 bytes |
| … | IK/visibility count + records | Variable (9 + 21 × IK count) |
The later blocks may be missing. Of the 31 files measured, none had even one light or self-shadow record, and some files do not even have the count field. Stop reading when fewer than 4 bytes remain, or your parser will crash.
Older files use the header string Vocaloid Motion Data file (version 0001). Checking the prefix (does it start with Vocaloid Motion Data) is the safe test.
Bone keyframe (111 bytes)
| Offset | Length | Type | Content |
|---|---|---|---|
| 0 | 15 | Shift_JIS | Bone name (zero-padded, fixed length) |
| 15 | 4 | uint32 | Frame number |
| 19 | 12 | float32 × 3 | Position (x, y, z). Offset from the rest pose |
| 31 | 16 | float32 × 4 | Rotation (quaternion x, y, z, w) |
| 47 | 64 | uint8 × 64 | Interpolation curves (see below) |
A bone name that does not fit in 15 bytes cannot be written. Japanese characters take 2 bytes in Shift_JIS, so an all-Japanese name can have at most 7 characters. Standard bones such as 左親指0 fit, but long model-specific helper bone names may not. A writer must check that the name encodes to 15 bytes or less.
The position is not absolute; it is the offset from the model's rest pose. For most bones other than the center and IK bones it stays at 0 and only the rotation is set.
The 64-byte interpolation layout
A bone keyframe has two cubic Bézier control points (x1, y1, x2, y2) for each of four channels: X move, Y move, Z move and rotation. Values range from 0 to 127, where 127 means 1.0. VMD does not store these 16 values plainly; it writes the same 16-byte row four times, shifting it by one byte each time.
// Default MMD puts on a new key (all linear). 64 bytes from offset 47
20, 20, 0, 0, 20, 20, 20, 20, 107,107,107,107,107,107,107,107,
20, 20, 20, 20, 20, 20, 20,107, 107,107,107,107,107,107,107, 0,
20, 20, 20, 20, 20, 20,107,107, 107,107,107,107,107,107, 0, 0,
20, 20, 20, 20, 20,107,107,107, 107,107,107,107,107, 0, 0, 0In the first row, [i] is x1, [i+4] is y1, [i+8] is x2 and [i+12] is y2 (i = 0–3 for X move, Y move, Z move, rotation). However, [2] and [3] of the first row (x1 of Z move and rotation) are always 0 in real files; the real values are in the shifted copies at [17] and [18]. If you read only the first 16 bytes, the easing of rotations changes (how to read the 64 bytes).
When the control points lie on the diagonal, (20, 20) and (107, 107), the curve is linear (constant speed). That is also the default MMD writes for a new keyframe. Of the 255,408 bone keyframes in the 31 measured files, 71,122 (27.8%) still had this default. The other 70% were edited by the author, so connecting keys linearly and ignoring the curves loses the motion's easing.
Use the interpolation curve of the later keyframe. It describes how the motion arrives at that keyframe. Taking the earlier keyframe's curve swaps acceleration and deceleration.
Interpolation only works between keyframes of the same bone
This is the biggest trap when building a motion by hand. If you raise an arm only at frame 30 and that bone has no other keyframe, the arm stays raised from frame 0 to the end. Time during which a bone does not move can only be expressed by writing that it does not move.
So when you build a VMD from a list of poses, take the union of bone names across all keyframes and write the rest pose (no rotation, no offset) wherever a bone is missing. This site's motion builder does exactly that.
Morph (facial) keyframe (23 bytes)
| Offset | Length | Type | Content |
|---|---|---|---|
| 0 | 15 | Shift_JIS | Morph name (zero-padded, fixed length) |
| 15 | 4 | uint32 | Frame number |
| 19 | 4 | float32 | Weight (0.0–1.0) |
There is no interpolation curve. Morphs are interpolated linearly between keyframes by design.
Camera keyframe (61 bytes)
| Offset | Length | Type | Content |
|---|---|---|---|
| 0 | 4 | uint32 | Frame number |
| 4 | 4 | float32 | Distance from the target. A negative value pulls back |
| 8 | 12 | float32 × 3 | Target (x, y, z) |
| 20 | 12 | float32 × 3 | Rotation (Euler angles in radians, not a quaternion) |
| 32 | 24 | uint8 × 24 | Interpolation curves (6 channels × 4 bytes) |
| 56 | 4 | uint32 | Field of view (degrees) |
| 60 | 1 | uint8 | Perspective flag (0 = perspective on) |
The 24 camera interpolation bytes are a plain layout of 6 channels × (x1, x2, y1, y2) with no shifting. The channels are 0–2 = target X/Y/Z, 3 = rotation, 4 = distance, 5 = field of view. Rotation is one curve shared by X, Y and Z.
Mind the camera position formula. The correct one is position = target + R · (0, 0, -distance). Building it as R · (target + (0, 0, -distance)) drifts further the farther the target is from the origin. three.js r171 MMDLoader.buildCameraAnimation() does the latter. On a real camera VMD (129 keyframes), an author distance of 16.40 became an effective 15.29 and 24.60 became 21.95; the position error was 3.4 on average and 19.0 at most (the model is about 20 tall). The camera still looks at the target, so the only symptom is that close-ups look like wide shots.
Do not interpolate between keyframes that are only one frame apart. Camera cuts are written that way. The measured camera.vmd had 32 such places among its 129 keyframes (counted after sorting by frame number; counting in file order gives 30). Interpolating them makes the camera slide at every cut.
Light keyframe (28 bytes)
| Offset | Length | Type | Content |
|---|---|---|---|
| 0 | 4 | uint32 | Frame number |
| 4 | 12 | float32 × 3 | Color (r, g, b), 0.0–1.0 |
| 16 | 12 | float32 × 3 | Position (x, y, z), actually the light direction vector |
There is no interpolation curve; linear interpolation is correct. The "position" is a direction, not a coordinate, and its length has no meaning. Use it as the direction of a directional light and decide where to put the light yourself (normalizing it to a fixed distance is safest, since a light inside the model cuts off its shadows).
The mmdparser that three.js uses internally does not read the light track. If you need light VMDs, you have to parse that block yourself.
Self-shadow (9 bytes) and IK/visibility (variable)
| Block | Offset | Content |
|---|---|---|
| Self-shadow | 0 / 4 / 5 | Frame number (uint32) / mode (uint8) / distance (float32) |
| IK/visibility | 0 / 4 / 5 | Frame number (uint32) / visible (uint8) / IK count (uint32) |
| 9 onward | [IK bone name 20 bytes + enabled flag 1 byte] × IK count |
Only the IK/visibility record has a variable length (9 + 21 × IK count), so you cannot skip the block from the count alone; read each record's IK count and advance. Note that the bone name here is 20 bytes, not 15.
Telling motion, morph, camera and light files apart
A VMD file does not say "I am a camera". You decide from how many records each block has. This site uses the following order (the same thresholds as mmdbox.net).
| Kind | Condition |
|---|---|
| Motion | More than 10 bone keyframes |
| Morph | More than 10 morph keyframes and 10 or fewer bone keyframes |
| Camera | More than 10 camera keyframes and 10 or fewer bone keyframes |
| Light | At least one light keyframe, with 10 or fewer bone and camera keyframes |
| Unknown | None of the above |
Light has a threshold of 0 because many files change the light color for only a few frames; a threshold of 10 would put them in "unknown". The 31 measured files came out as 24 motions, 1 camera, 1 morph and 5 unknown (the unknown ones were tiny fragments).
From MMD coordinates to a right-handed system (three.js)
- Positions: negate Z.
- Quaternions: negate X and Y (
(x, y, z, w)→(-x, -y, z, w)). - Camera Euler angles: negate Z only and compose in XYZ order. Negate Z of the target as well.
- Mirroring left/right is different. Negate X of positions, and map rotations
(x, y, z, w)→(x, -y, -z, w)(a rotation axis is a pseudovector, so only the components parallel to the mirror plane flip). Bone names with 左 (left) and 右 (right) must be swapped too.
Things to watch when writing
- Keep records as raw bytes. Expanding data you do not need to touch, like the 64 interpolation bytes, into structs and writing it back loses information in the round trip. Trimming and merging only need "which records to keep" and "rewrite the frame number". The frame number is at byte 15 for bone and morph records and byte 0 for the others.
- When joining two VMDs, shift the second one first. Both start at frame 0, so concatenating them as is duplicates (bone, frame) pairs and breaks the file.
- Speeding up makes rounded (bone, frame) pairs collide. Show the user how many were dropped (silently discarding them leaves people wondering why the motion got shorter).
- The 20-byte model name may be cut in the middle of a multibyte character. Decoding it as is leaves a replacement character (U+FFFD) at the end; drop it before displaying.