Repository navigation
MMD Formats and Internals
This page covers the parts of PMX and VMD that shape how UMT behaves, and the runtime mechanics the public API leaves implicit. It is aimed at developers extending or debugging the pipeline. For the format itself, the authoritative reference is the official PMX 2.0/2.1 specification that ships with PmxEditor (PMX仕様.txt); it is not part of this package.
MMD and Unity are both left-handed, Y-up coordinate systems, so converting between them needs no handedness flip. What differs is the unit scale and the default facing direction: an MMD model faces the opposite way down Z from where Unity expects. UMT resolves both in one place. PMXReader converts every spatial value as it parses, so by the time a PMXModel exists it is already in Unity space and the rest of the pipeline copies values through untouched.
The conversion the reader applies, value by value:
-
Positions are rotated 180° about the Y axis (X and Z negated, Y kept), then multiplied by
MMDConstants.k_MMDUnitToUnityUnit(0.08). The rotation turns the model to face Unity's forward; the scale brings MMD's units down to metres. This covers vertex positions, bone positions, bone position offsets, rigid-body and joint positions, and vertex-morph offsets. - Normals get the same 180° Y rotation but no scale.
-
Rotation quaternions (bone-morph rotations and similar) are rebased through the same rotation, which works out to
(x, y, z, w)becoming(-x, y, -z, w). - IK per-link angle limits have their X and Z bounds negated, and because negating an axis swaps the sense of a min/max pair, the lower and upper bounds are exchanged on those axes.
-
UVs are read unchanged here. The V flip that Unity's texture origin needs (
vbecomes1 - v) is applied later, inPMXMeshBuilder, not in the reader.
The consequence is a rule the whole build pipeline depends on: nothing converts a spatial value a second time. Mesh vertices, morph offsets, bone positions, rigid-body and joint positions are all already in Unity space, so object building copies them straight through. If you add a build step that touches positions, normals or rotations, do not reapply the rotation or the scale.
PMXReader.Read parses the header, model info, vertices, faces, textures, materials, bones, morphs, display frames, rigid bodies and joints into a PMXModel. The header declares text encoding (UTF-16LE or UTF-8), the additional-UV count, and the byte width of each index kind (vertex, texture, material, bone, morph, rigid body), which vary per file; the reader decodes indices at the declared width. Text is decoded per the header encoding. UMT supports PMX 2.0 and reads 2.1 files where it can, without implementing 2.1-exclusive features.
PMX vertices carry one of several weight types. BDEF1, BDEF2 and BDEF4 map directly onto Unity bone weights. SDEF (Spherical Deform) is skinned faithfully by a per-frame full-mesh pass rather than approximated, so joints bend spherically the way they do in MMD. That pass has two interchangeable implementations that compute the same result — a GPU compute shader and a CPU Burst job — selected per model by a mode flag (GPU by default, CPU when no compute shader is available). The maths and the pipeline live on the dedicated SDEF Skinning page. QDEF is parsed as a weight type but treated as linear blending. Bone weights and bind poses are assigned before the mesh asset is created, which Unity requires for skinning to bind correctly.
Vertex morphs become Unity blend shapes. Group morphs are resolved down to the vertex morphs they reference. This creates a constraint on how meshes can be split: a Unity blend shape lives on a single mesh, so any two materials touched by the same vertex morph must end up in the same generated mesh, or that morph could not move both.
PMXMorphBuilder resolves this with a union-find pass over morph-affected vertex sets, computed before any mesh is built. Materials linked by a shared vertex morph are unioned into one group. PMXMeshBuilder then creates one Unity Mesh per group, with one submesh per PMX material inside the group, and adds blend shapes only for the vertices that actually appear in each generated mesh. This is why an imported model is usually several meshes rather than one, and why the split lines fall where they do.
PMXMaterialBuilder builds one Unity material per PMX material and assigns textures from the texture-index map by the material's textureIndex and sphereTextureIndex. Shader choice follows a preference order: lilToon (_lil/lilToonMulti) when it is installed, then URP Unlit, then built-in Unlit. Under lilToon, the PMX sphere texture maps onto the matcap slot, double-sided and edge/outline settings carry across, and the result is close to MMD's look. Under the Unlit fallbacks you keep the base texture and transparency but lose toon and sphere shading.
Transparency is decided by measuring actual alpha coverage rather than trusting a flag. The builder decodes each material's source texture from the original file bytes into raw Color32 pixels — not from the imported texture asset, which may be compressed or non-readable and would corrupt the sampling — then walks the material's UV footprint on the CPU and measures the alpha coverage over it. Materials whose coverage falls below the threshold are set up as transparent; so is any material with even a single fully-transparent texel in its footprint, or a diffuse alpha below one. A separate, higher threshold decides whether a transparent material still writes depth. The two thresholds are exposed on PMXImportOptions (alphaDetectionThreshold, alphaCoverageZWriteThreshold); the coverage pass is pure CPU work and needs no compute shader.
Bones carry a parent, a transform level, a set of flags, and optional IK, axis and constraint data. The flags drive solver behaviour: whether a bone is rotatable or translatable, whether it carries an append/inherit rotation or translation constraint, whether it has a fixed or local axis, and crucially whether it updates before or after physics. IK chains are CCD-based with an iteration count, an overall angle limit, and optional per-link lower/upper angle limits.
The avatar, when requested, is built with Unity's native AvatarBuilder.BuildHumanAvatar. PMXAvatarBuilder maps PMX bones to Unity human bones using a JSON config in UMTResources and constructs the HumanDescription directly. It succeeds only when every required human bone is present; otherwise the avatar is skipped and the rest of the import proceeds.
MMDTransformManager reproduces MMD's transform pipeline, which is not the same as Unity's. MMD applies bones in a specific order, by transform level, with append/inherit constraints and IK resolved in passes and a split around the physics step, and the solver mirrors that.
Each step it samples the current Unity transforms, resets the bones to their initial pose, solves constraints and IK for the pre-physics pass, optionally steps physics, then solves the after-physics pass and writes the solved transforms back to Unity. It runs late in the frame (execution order 10000) and can run in edit mode. The constraint and IK maths run in a Burst-compiled inner type over blittable per-bone data, with native caches for bone runtime data, IK controllers and links, and pass ordering. After you rebuild bones or change the bind pose, call MMDTransformBuilder.RefreshInitialTransforms so the cached initial pose and those native caches are rebuilt to match.
The transform manager also owns the SDEF skinning dispatch: after the bone solve it drives each MMDSDEFSkinner once per frame, either dispatching the GPU compute pass at camera-render time or scheduling the CPU Burst job from LateUpdate. See SDEF Skinning for the detail.
Physics runs on a native Bullet 2.75 plugin, matching the Bullet version MMD itself uses, rather than on Unity's physics. MMDPhysicsManager owns the native context and builds rigid bodies, joints and ground collision from the PMXModel; MMDBulletPhysics is the single P/Invoke wrapper around the plugin, and all native calls are centralised there. Rigid bodies come in sphere, box and capsule shapes and in three modes: kinetic (follows its bone), dynamic (simulated), and dynamic-but-bone-aligned. Only 6DOF joints are supported, both the spring-6DOF and generic-6DOF types; other joint types raise a clear exception rather than being silently approximated.
One scaling detail is worth knowing. Bullet's default convex collision margin (0.04) is sized for MMD-native-scale colliders, but this pipeline scales everything to Unity units by 0.08, so the unscaled margin would inflate the small colliders badly, enough that a thin capsule would gain a skin large enough to rest visibly above surfaces. The plugin compensates by setting the margin to 0.04 × 0.08 on box and capsule shapes; spheres use their radius as margin and need no override.
The managed structs marshalled into the plugin (RigidBodySimulationData on MMDRigidBody, and NativeJointData) must stay byte-for-byte compatible with the plugin's C structs; changing one side without the other corrupts the data crossing the boundary. If you change a marshalled layout, rebuild the plugin to match before entering play mode.
This package ships the prebuilt native binaries Plugins/Windows/x64/UMTNativePlugin.dll, Plugins/Android/arm64-v8a/libUMTNativePlugin.so and Plugins/Web/libUMTNativePlugin.a (a WebAssembly/Emscripten static library, statically linked into the WebGL player at build time). The C++ source lives in its own repository (UMTNativePlugin), which builds Bullet 2.75 and exposes the flat Cdecl ABI that MMDBulletPhysics calls into. On platforms without a binary you can still import models and convert non-physics motion, but live and baked physics need the plugin.
Windows, Android and WebAssembly are the platforms shipped as prebuilt binaries, but they are not the only platforms the plugin can run on. The source is a portable CMake C++17 project with no Unity dependency and no platform-specific code beyond the build toolchain, so Linux, macOS, iOS and other targets are supported if you compile the plugin yourself. Build it from the UMTNativePlugin repository against the target toolchain, then drop the resulting library into the matching Plugins/<platform>/<abi>/ folder (with a Unity .meta set for that platform) and the runtime picks it up through the same UMTNativePlugin entry point. Nothing in the managed side is platform-specific; the platform coverage is purely a matter of which binaries you have built.
VMDReader parses a .vmd into a VMDAnimation: a model name, bone frames, morph frames, and optional camera, light, self-shadow and show/IK sections. The optional sections are detected by checking what remains in the stream, since older files simply stop early. Names are decoded as CP932 (Shift-JIS), which is what the format uses, not UTF-8.
VMDAnimationClipConverter.Convert does not return a Unity AnimationClip; it returns a VMDModelClipData (and ConvertCamera returns a VMDCameraClipData) — flat sets of AnimationCurve channels grouped by target path. The reason is that AnimationClip.SetCurve only works at runtime for legacy clips, and Unity Timeline's AnimationTrack rejects legacy clips, so a runtime player has to evaluate the curves itself in a custom playable rather than lean on a clip. The editor path (VMDClipDataBuilder, called by the scripted importer and the converter window) rebuilds an actual AnimationClip from this curve data — that is where .anim sub-assets come from. Everything below about baking applies to the curves either way.
MMD attaches interpolation to the destination keyframe, not the source, and uses a cubic Bézier shaped by control points stored as bytes in each frame. UMT follows this exactly. A bone frame's 64-byte interpolation block stores its channels at fixed byte offsets, with X movement starting at 0, Y at 1, Z at 17 and rotation at 18, and the four control-point bytes for a channel are read at offset, offset + 4, offset + 8 and offset + 12. Camera frames use a 24-byte block instead: movement curves in bytes 0 to 11, rotation in 12 to 15, distance in 16 to 19, and view angle in 20 to 23.
This is the most important runtime behaviour to understand, and the reason the converter defaults to baking.
MMD interpolates a bone's rotation as a genuine rotation, a spherical path between orientations shaped by the Bézier handles the animator set. Unity's AnimationClip does not interpolate quaternion curves that way. It blends the four quaternion components independently, and it ignores tangent weighting entirely. The two diverge on any rotation with real curvature: a limb can swing along a different arc, ease at the wrong rate, or overshoot between keys. There is no clip setting that makes Unity interpolate a quaternion curve spherically with weighted tangents.
Baking removes the dependency on Unity's interpolation. The baked path samples the fully solved pose on every frame, after IK and after physics if physics baking is on, then writes a key per frame, so the curve already holds the correct value everywhere and there is nothing left for Unity to interpolate wrongly. The cost is denser curves and rotations that are no longer a few editable keys, which for converted MMD motion is the right trade.
The runtime-solved path (bakeIKToFK = false) instead writes sparse local position and Euler-rotation curves plus stepped IK on/off curves, and leaves IK to the runtime solver. Euler curves at least interpolate per channel rather than per quaternion component, and you can shape them by hand in the editor to approach MMD's path, but they remain an approximation: they will not reproduce the spherical interpolation exactly, and they carry their own gimbal and continuity issues. Use baking when faithful playback matters.
A VMD camera track stores its raw Euler angles and a distance, and these are not the values Unity's camera orientation needs directly. The converter maps the raw angles first, as pitch = -rawRx, yaw = rawRy + π and roll = rawRz. Skip that mapping and the pitch inverts (the camera ends up underground) and a 180° yaw is lost (it faces backwards).
From there the centre orientation is built from MMD's camera view basis (forward -(UnitZ · M), up UnitY · M, with M = RotationZ(roll) · RotationX(-pitch) · RotationY(-yaw)), rebased into Unity space by negating the X and Z axes, then turned into a quaternion with LookRotation(forward, up). The basis is never assembled into a matrix by hand: a wrong sign on the right vector flips the determinant and yields a garbage rotation. The Burst-compiled camera maths emit one keyframe per sampled frame (the native 30 fps timeline, optionally sub-sampled to 60 or 120 while keeping the real-time duration). The output drives a two-node rig: the root carries the centre position and orientation, and a child sits along local Z at -|distance| × 0.08, looking back toward the centre, and also carries the field-of-view curve.
Renaming and avatar building both read from a single UMTResources asset at Resources/UMTResources.asset, holding the rename lists, the humanoid bone mapping, the Kawazu/NMeCab dictionary binaries for Japanese romanisation, and the Pinyin dictionary for Chinese. Rebuild it with Tools ▸ UMT ▸ Create Default Resources. The romanisation dictionaries are data, not code. There are no hardcoded conversion tables in the source, so name conversion is driven entirely by these assets.