mmd-anim-runtime 0.1.3

Renderer-independent MMD animation runtime core
Documentation
use std::sync::Arc;

use glam::{Quat, Vec3A};
use mmd_anim_runtime::{
    AnimationClip, BoneAnimationBinding, BoneIndex, BoneInit, ModelArena, MorphAnimationBinding,
    MorphIndex, MorphKeyframe, MorphTrack, MovableBoneKeyframe, MovableBoneTrack,
    PropertyAnimationBinding, PropertyKeyframe, RuntimeInstance,
};

fn assert_matrix_near(actual: &[f32; 16], expected: &[f32; 16]) {
    for (index, (actual, expected)) in actual.iter().zip(expected.iter()).enumerate() {
        let delta = (actual - expected).abs();
        assert!(
            delta < 1.0e-5,
            "matrix[{index}] actual={actual} expected={expected} delta={delta}"
        );
    }
}

#[test]
fn evaluates_synthetic_clip_basic_outputs() {
    let model = Arc::new(ModelArena::new(vec![BoneInit::new(None, Vec3A::ZERO)]).unwrap());
    let clip = AnimationClip::new_full(
        vec![BoneAnimationBinding {
            bone: BoneIndex(0),
            track: MovableBoneTrack::from_keyframes(vec![
                MovableBoneKeyframe::new(0, Vec3A::ZERO, Quat::IDENTITY),
                MovableBoneKeyframe::new(60, Vec3A::new(2.0, 0.0, 0.0), Quat::IDENTITY),
            ]),
        }],
        vec![MorphAnimationBinding {
            morph: MorphIndex(0),
            track: MorphTrack::from_keyframes(vec![
                MorphKeyframe::new(0, 0.0),
                MorphKeyframe::new(60, 1.0),
            ]),
        }],
        Some(PropertyAnimationBinding::from_keyframes(vec![
            PropertyKeyframe::new(0, vec![true]),
            PropertyKeyframe::new(30, vec![false]),
        ])),
    );
    let mut runtime = RuntimeInstance::new_with_counts(model, 1, 1);

    let expected = [
        (
            0,
            [
                1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
            ],
            0.0,
            true,
        ),
        (
            30,
            [
                1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0,
            ],
            0.5,
            false,
        ),
        (
            60,
            [
                1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 2.0, 0.0, 0.0, 1.0,
            ],
            1.0,
            false,
        ),
    ];

    for (frame, world_matrix, morph_weight, ik_enabled) in expected {
        runtime.evaluate_clip_frame(&clip, frame as f32);
        assert_matrix_near(&runtime.world_matrices()[0].to_cols_array(), &world_matrix);
        assert!((runtime.morph_weights()[0] - morph_weight).abs() < 1.0e-5);
        assert_eq!(runtime.ik_enabled()[0], u8::from(ik_enabled));
    }
}