How to read the 64 interpolation bytes in VMD: x1 of Z move and rotation is not in the first row

Each VMD bone keyframe holds interpolation curves for four channels (X move, Y move, Z move, rotation) in 64 bytes. It is often said that you only need the first 16 bytes, but counting real files shows that two bytes of the first row are not the real values. Reading them changes the easing of rotations.

Last updated: 2026-10-12 · Counted all 64 bytes of every bone key (255,380 keys) in 30 real VMD files bundled with the kphp repository, and checked how three.js r171 MMDLoader reads them in its source. The size of the error was estimated per pair of adjacent keys as the difference in interpolation progress × the rotation angle between the keys

This site's player and writer have used this reading since 2026-10-07. Before that we read the first row like three.js does, and our own writer wrote x1 of Z move and rotation as 0.

The layout of the 64 bytes

Each curve is a cubic Bézier with two control points, (x1, y1) and (x2, y2), with values from 0 to 127 (127 = 1.0). Four channels give 16 values, laid out in the first row like this:

Position in row 1Content
[0]–[3]x1 (X move, Y move, Z move, rotation)
[4]–[7]y1 (same order)
[8]–[11]x2 (same order)
[12]–[15]y2 (same order)

Rows 2–4 are copies of row 1 shifted left by one byte each. Checking real files, the copies differ from row 1 only at the five places that copy row 1's [2] and [3] ([17], [18], [32], [33], [48]); everything else matched in every key.

Row 1's [2] and [3] are always 0

In 255,380 keys from 30 real files, row 1's [2] (x1 of Z move) and [3] (x1 of rotation) were 0 without exception. Meanwhile [17], the row-2 copy, was non-zero in 143,385 keys and [18] in 134,552.

Which one is real becomes clear from the "linear" curve MMD puts on a new key. The most common 64 bytes in the real files (68,138 keys) were:

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,  0

X and Y move are (20, 20)–(107, 107), a straight line on the diagonal. If you trust the zeros in row 1, only Z move and rotation become (0, 20)–(107, 107), which is not a straight line. Reading the 20 from the copies gives the same straight line for all four channels. We concluded that x1 of Z move and rotation should be read from the copies at [17] and [18] (what MMD uses the two bytes in row 1 for was not investigated here).

  • The other copies hold the same values ([32] = [17], [33] = [48] = [18])
  • 37,189 keys had all 64 bytes set to 0. A curve with control points (0, 0)–(0, 0) is linear, so the reading makes no difference for them

three.js MMDLoader reads the zeros

three.js (r171) MMDLoader reads x1 of channel i from interpolation[i]. That is correct for X and Y move, but Z move and rotation are always interpolated with x1 = 0.

We estimated the impact on the 30 real files. There were 250,950 pairs of adjacent keys of the same bone; excluding pairs only one frame apart, 124,560 pairs had a different rotation curve. Multiplying the largest difference in interpolation progress by the rotation angle between the two keys gives:

Rotation error (estimate)Value
Median1.29°
Top 10%10.0° or more
Top 1%44.2° or more
Maximum83.2°
Pairs above 5°25,544

Even MMD's default linear curve is off by up to 0.070 in progress (around 22% into the interval) when x1 is read as 0. Keys where the author shaped the rotation easing differ the most, so stops and holds in a dance appear at the wrong moment. The position error for Z move was almost always 0, with a maximum of 3.56 units.

How to fix it

  • When reading: take x1 of Z move and rotation from [17] and [18]. The other 14 values can stay as in row 1
  • When handing to three.js: copy the VMD bytes, write the values of [17] and [18] into [2] and [3] of every key, and pass that to MMDLoader. No change to three.js itself is needed
  • When writing: use MMD's 64 bytes above as is. If you build the copies by shifting row 1, the zeros of row 1 end up in the copies too, and when MMD reads the file back, Z move and rotation are no longer linear

Camera key interpolation (24 bytes) is a different, unshifted layout, so none of this applies to it (see the VMD file format).