mmd-anim-format 0.4.1

MMD format parser and exporter for mmd-anim
Documentation
use glam::{Mat4, Quat, Vec3, Vec3A};
use mmd_anim_runtime::{
    BoneIndex, BoneInit, DensePoseSequenceView, ModelArena, MorphIndex, ReducedPoseSequence,
    ReductionTarget, ReductionTolerances, RuntimeInstance, SkeletonSnapshot,
    reduce_dense_pose_sequence,
};
use std::sync::Arc;

use super::*;
use crate::vmd::{
    build_clip_from_import, export_vmd_animation, import_vmd_motion, parse_vmd_animation,
};

fn reduced(start_frame: f32, frame_step: f32, morphs: &[f32]) -> ReducedPoseSequence {
    let world = [
        Mat4::from_translation(Vec3::new(1.0, 0.0, 0.0)),
        Mat4::from_translation(Vec3::new(2.0, 0.5, 0.0)),
        Mat4::from_translation(Vec3::new(3.0, 0.0, 0.0)),
    ];
    let input = DensePoseSequenceView::new(
        &world,
        morphs,
        3,
        1,
        usize::from(!morphs.is_empty()),
        start_frame,
        frame_step,
    )
    .unwrap();
    let snapshot = SkeletonSnapshot::new(
        vec![-1],
        vec![Vec3A::ZERO],
        vec![Quat::IDENTITY],
        usize::from(!morphs.is_empty()),
        99,
    )
    .unwrap();
    reduce_dense_pose_sequence(
        input,
        snapshot,
        ReductionTolerances {
            local_position: 0.01,
            world_position: 0.01,
            ..Default::default()
        },
        ReductionTarget::VmdBezier,
    )
    .unwrap()
}

fn bindings(morph_kind: VmdExportMorphKind) -> VmdPoseExportBindings {
    VmdPoseExportBindings {
        model_identity: 99,
        model_name: VmdExportName::new("model", b"model".to_vec()),
        bone_names: vec![VmdExportName::new("bone", b"bone".to_vec())],
        morph_names: vec![VmdExportName::new("morph", b"morph".to_vec())],
        ik_names: vec![VmdExportName::new("ik", b"ik".to_vec())],
        ik_solver_count: 1,
        append_affected_bones: vec![false],
        morph_kinds: vec![morph_kind],
    }
}

#[test]
fn static_track_rotation_comparison_is_exact_and_hemisphere_invariant() {
    let rotation =
        Quat::from_xyzw(0.182_574_18, -0.365_148_37, 0.547_722_6, 0.730_296_73).normalize();
    assert_eq!(rotation_error(rotation, rotation), 0.0);
    assert_eq!(rotation_error(rotation, -rotation), 0.0);
}

#[test]
fn exports_quantized_64_byte_curves_ik_off_and_roundtrips() {
    let sequence = reduced(0.0, 1.0, &[0.0, 0.5, 1.0]);
    let exported =
        export_reduced_pose_to_vmd(&sequence, &bindings(VmdExportMorphKind::Vertex)).unwrap();
    assert!(exported.report.physics_must_be_disabled_by_host);
    assert!(exported.report.ik_disabled_in_vmd);
    assert!(!exported.animation.bone_frames.is_empty());
    for frame in &exported.animation.bone_frames {
        assert_eq!(frame.interpolation.len(), 64);
        assert!(frame.interpolation.iter().all(|value| *value <= 127));
    }
    assert_eq!(exported.animation.property_frames.len(), 1);
    assert!(
        exported.animation.property_frames[0]
            .ik_states
            .iter()
            .all(|state| !state.enabled)
    );
    let bytes = export_vmd_animation(&exported.animation);
    let reparsed = parse_vmd_animation(&bytes).unwrap();
    assert_eq!(
        reparsed.bone_frames.len(),
        exported.animation.bone_frames.len()
    );
    assert_eq!(
        reparsed.morph_frames.len(),
        exported.animation.morph_frames.len()
    );
    assert!(!reparsed.property_frames[0].ik_states[0].enabled);
    let mut unique = reparsed
        .bone_frames
        .iter()
        .map(|frame| frame.frame)
        .collect::<Vec<_>>();
    unique.sort_unstable();
    unique.dedup();
    assert_eq!(unique.len(), reparsed.bone_frames.len());
}

#[test]
fn rejects_fractional_frames_without_rounding() {
    let sequence = reduced(0.5, 1.0, &[]);
    let mut binding = bindings(VmdExportMorphKind::Vertex);
    binding.morph_names.clear();
    binding.morph_kinds.clear();
    binding.ik_names.clear();
    binding.ik_solver_count = 0;
    assert!(matches!(
        export_reduced_pose_to_vmd(&sequence, &binding),
        Err(VmdPoseExportError::NonIntegerFrame { .. })
    ));
}

#[test]
fn rejects_append_and_baked_deformation_morph_double_application() {
    let no_morph = reduced(0.0, 1.0, &[]);
    let mut append_binding = bindings(VmdExportMorphKind::Vertex);
    append_binding.morph_names.clear();
    append_binding.morph_kinds.clear();
    append_binding.append_affected_bones[0] = true;
    assert_eq!(
        export_reduced_pose_to_vmd(&no_morph, &append_binding).unwrap_err(),
        VmdPoseExportError::AppendTransformWouldDoubleApply { bone: 0 }
    );

    for kind in [
        VmdExportMorphKind::Bone,
        VmdExportMorphKind::Group,
        VmdExportMorphKind::Material,
    ] {
        let sequence = reduced(0.0, 1.0, &[0.0, 0.5, 1.0]);
        assert_eq!(
            export_reduced_pose_to_vmd(&sequence, &bindings(kind)).unwrap_err(),
            VmdPoseExportError::MorphWouldDoubleApply { morph: 0, kind }
        );
    }

    for kind in [VmdExportMorphKind::Uv, VmdExportMorphKind::Other] {
        let sequence = reduced(0.0, 1.0, &[0.0, 0.5, 1.0]);
        assert_eq!(
            export_reduced_pose_to_vmd(&sequence, &bindings(kind)).unwrap_err(),
            VmdPoseExportError::UnsupportedMorphKind { morph: 0, kind }
        );
    }
}

#[test]
fn rejects_incomplete_ik_bindings() {
    let sequence = reduced(0.0, 1.0, &[]);
    let mut binding = bindings(VmdExportMorphKind::Vertex);
    binding.morph_names.clear();
    binding.morph_kinds.clear();
    binding.ik_names.clear();

    assert_eq!(
        export_reduced_pose_to_vmd(&sequence, &binding).unwrap_err(),
        VmdPoseExportError::BindingMismatch
    );
}

#[test]
fn reparsed_vmd_runtime_pose_matches_reducer_sampler() {
    let sequence = reduced(0.0, 1.0, &[]);
    let mut binding = bindings(VmdExportMorphKind::Vertex);
    binding.morph_names.clear();
    binding.morph_kinds.clear();
    binding.ik_names.clear();
    binding.ik_solver_count = 0;
    let exported = export_reduced_pose_to_vmd(&sequence, &binding).unwrap();
    let bytes = export_vmd_animation(&exported.animation);
    let imported = import_vmd_motion(&bytes).unwrap();
    let clip = build_clip_from_import(
        imported,
        &|name| (name == b"bone").then_some(BoneIndex(0)),
        &|_name| None::<MorphIndex>,
    );
    let model = Arc::new(ModelArena::new(vec![BoneInit::new(None, Vec3A::ZERO)]).unwrap());
    let mut runtime = RuntimeInstance::new(model);
    for frame in 0..3 {
        runtime.evaluate_clip_frame(&clip, frame as f32);
        let expected = sequence.sample(frame as f32).unwrap();
        for (actual, expected) in runtime.world_matrices()[0]
            .to_cols_array()
            .iter()
            .zip(expected.world_matrices[0].to_cols_array())
        {
            assert!((actual - expected).abs() <= 0.01);
        }
    }
}