Skip to the content.

How it works

build.py opens the untouched character file in Blender, runs six stages and saves a new scene. Each stage reads the merged config (defaults.toml < project.toml < project.local.toml).

motion  ->  camera  ->  rig  ->  physics  ->  face  ->  stage  ->  (render, encode)
retarget    MMD cam     chains,   simulate,    morphs    set, lights,
the VMD     + shots     weights,  bake         -> tiles  render settings
                        repairs                or keys

Caches

Everything slow is cached in paths.cache. Each file name carries a hash of its inputs:

file contents key
proxy_<key>.npz the VMD evaluated on the proxy skeleton (world rotations and positions per frame) VMD content, scale, arm angle, frame range, proxy layout
body_track_<key>.npz world matrices of the 24 tracked body bones per frame the above plus the rest pose
hands_<key>.npz the generated hand meshes with part labels hand bones and [hands]
sim_<key>.npz particle positions of every lattice per frame body track, lattice, [physics], colliders
lattice.npz the chain layout from the rig stage (rewritten every build)

When you edit the config, the stages that depend on the change recompute on the next build and the rest are reused. --resim and --resample force a recompute. The face atlases in paths.textures record their key in face_atlas.json the same way.

Motion

MMD motions rotate bones relative to MMD’s own rest pose: an A-pose with the arms about 37° below horizontal (motion.arm_angle). The motion stage builds a proxy skeleton with MMD bone names at your rig’s own joint positions, with the arms posed in that A-pose. mmd_tools then imports the VMD onto the proxy, keeping its Bézier interpolation, IK targets and centre/groove bones.

Every frame, the proxy’s world-space rotation deltas are copied onto your bones, relative to the same A-pose:

motion.scale converts MMD units to metres. Auto mode takes your leg length over 9.42, a standard MMD leg in MMD units. When the motion asks a leg to reach further than its length, the reach is clamped; the build log counts the frames where that happened. Hops in the source then lift the foot a little.

Rig: chains and weights

Physics

solver.py is a position-based (PBD) solver in numpy. The particles are the chain joints, and each lattice’s root row is pinned to its body bone. Each frame runs physics.substeps substeps, with body bones interpolated in between. Each substep runs:

physics.preroll frames on the first pose let everything settle before the dance starts. The result is baked as rotation and location keys on the chain bones: each bone’s frame comes from its lattice neighbours. Check it with python -m mmdance qa project.toml [from to]. It reports how much the particles move relative to the body, and lists high-jerk frames to step through: fast spins show up there as well as real pops. Given a spin’s frame range, it also measures how far the hem trails the hips.

Face

Stage and render

stage.builder = "default" gives a floor disc under the world colour. A project builder is a Python file defining build(collection, cfg), and optionally colliders(cfg) to return furniture boxes for the physics; see projects/claude_chika/room.py.

The render is EEVEE with ray tracing, a fog-glow compositor pass and PNG output. use_overwrite = False and placeholders are on, so several Blender processes can share the frame range and an interrupted render resumes. encode muxes the frames with the song. If the song is longer, it holds the last frame, then fades picture and sound out together.