Skip to main content

mmd_anim_format/vmd/
reduced.rs

1use mmd_anim_runtime::{
2    QuantizedBezier, ReducedBoneKey, ReducedPoseSequence, ReductionTarget, VmdBoneInterpolation,
3};
4use thiserror::Error;
5
6use super::{
7    VmdParsedAnimation, VmdParsedBoneFrame, VmdParsedCounts, VmdParsedIkState, VmdParsedMetadata,
8    VmdParsedMorphFrame, VmdParsedPropertyFrame,
9};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct VmdExportName {
13    pub text: String,
14    pub bytes: Vec<u8>,
15}
16
17impl VmdExportName {
18    pub fn new(text: impl Into<String>, bytes: Vec<u8>) -> Self {
19        Self {
20            text: text.into(),
21            bytes,
22        }
23    }
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum VmdExportMorphKind {
28    Vertex,
29    Uv,
30    Other,
31    Bone,
32    Group,
33    Material,
34}
35
36#[derive(Debug, Clone)]
37pub struct VmdPoseExportBindings {
38    pub model_identity: u64,
39    pub model_name: VmdExportName,
40    pub bone_names: Vec<VmdExportName>,
41    pub morph_names: Vec<VmdExportName>,
42    pub ik_names: Vec<VmdExportName>,
43    pub ik_solver_count: usize,
44    pub append_affected_bones: Vec<bool>,
45    pub morph_kinds: Vec<VmdExportMorphKind>,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct VmdPoseExportReport {
50    pub physics_must_be_disabled_by_host: bool,
51    pub ik_disabled_in_vmd: bool,
52    pub skipped_constant_bone_tracks: usize,
53    pub skipped_zero_morph_tracks: usize,
54}
55
56#[derive(Debug, Clone)]
57pub struct VmdPoseExport {
58    pub animation: VmdParsedAnimation,
59    pub report: VmdPoseExportReport,
60}
61
62#[derive(Debug, Error, Clone, PartialEq, Eq)]
63pub enum VmdPoseExportError {
64    #[error("reduced pose target must be VmdBezier")]
65    WrongTarget,
66    #[error("export binding model identity or counts do not match reduced pose")]
67    BindingMismatch,
68    #[error("sample {sample} has non-integer VMD frame {frame}")]
69    NonIntegerFrame { sample: usize, frame: String },
70    #[error("sample {sample} frame is outside the VMD u32 range")]
71    FrameOutOfRange { sample: usize },
72    #[error("{kind} name {index} exceeds the VMD byte limit {limit}")]
73    NameTooLong {
74        kind: &'static str,
75        index: usize,
76        limit: usize,
77    },
78    #[error("bone {bone} has baked motion but also participates in append transform")]
79    AppendTransformWouldDoubleApply { bone: usize },
80    #[error("morph {morph} of kind {kind:?} would double-apply baked deformation")]
81    MorphWouldDoubleApply {
82        morph: usize,
83        kind: VmdExportMorphKind,
84    },
85    #[error("morph {morph} of kind {kind:?} cannot be emitted by the VMD pose adapter")]
86    UnsupportedMorphKind {
87        morph: usize,
88        kind: VmdExportMorphKind,
89    },
90}
91
92pub fn export_reduced_pose_to_vmd(
93    sequence: &ReducedPoseSequence,
94    bindings: &VmdPoseExportBindings,
95) -> Result<VmdPoseExport, VmdPoseExportError> {
96    if sequence.target() != ReductionTarget::VmdBezier {
97        return Err(VmdPoseExportError::WrongTarget);
98    }
99    let snapshot = sequence.snapshot();
100    if !sequence.validate_model(
101        bindings.model_identity,
102        bindings.bone_names.len(),
103        bindings.morph_names.len(),
104    ) || bindings.append_affected_bones.len() != snapshot.bone_count()
105        || bindings.morph_kinds.len() != snapshot.morph_count()
106        || bindings.ik_names.len() != bindings.ik_solver_count
107    {
108        return Err(VmdPoseExportError::BindingMismatch);
109    }
110    validate_name(&bindings.model_name, "model", 0, 20)?;
111    for (index, name) in bindings.bone_names.iter().enumerate() {
112        validate_name(name, "bone", index, 15)?;
113    }
114    for (index, name) in bindings.morph_names.iter().enumerate() {
115        validate_name(name, "morph", index, 15)?;
116    }
117    for (index, name) in bindings.ik_names.iter().enumerate() {
118        validate_name(name, "ik", index, 20)?;
119    }
120
121    let sample_frames = vmd_sample_frames(sequence)?;
122    let mut bone_frames = Vec::new();
123    let mut skipped_constant_bone_tracks = 0;
124    for (bone, track) in sequence.bone_tracks().iter().enumerate() {
125        let rest_translation = snapshot.rest_local_translations()[bone];
126        let rest_rotation = snapshot.rest_local_rotations()[bone];
127        let active = track.keys().iter().any(|key| {
128            key.translation.distance(rest_translation) > 1.0e-6
129                || rotation_error(key.rotation, rest_rotation) > 1.0e-6
130        });
131        if !active {
132            skipped_constant_bone_tracks += 1;
133            continue;
134        }
135        if bindings.append_affected_bones[bone] {
136            return Err(VmdPoseExportError::AppendTransformWouldDoubleApply { bone });
137        }
138        for key in track.keys() {
139            bone_frames.push(vmd_bone_frame(
140                &bindings.bone_names[bone],
141                sample_frames[key.sample_index],
142                key,
143                rest_translation,
144                rest_rotation,
145            ));
146        }
147    }
148
149    let mut morph_frames = Vec::new();
150    let mut skipped_zero_morph_tracks = 0;
151    for (morph, track) in sequence.morph_tracks().iter().enumerate() {
152        let active = track.keys().iter().any(|key| key.weight.abs() > 1.0e-6);
153        if !active {
154            skipped_zero_morph_tracks += 1;
155            continue;
156        }
157        let kind = bindings.morph_kinds[morph];
158        if matches!(
159            kind,
160            VmdExportMorphKind::Bone | VmdExportMorphKind::Group | VmdExportMorphKind::Material
161        ) {
162            return Err(VmdPoseExportError::MorphWouldDoubleApply { morph, kind });
163        }
164        if kind != VmdExportMorphKind::Vertex {
165            return Err(VmdPoseExportError::UnsupportedMorphKind { morph, kind });
166        }
167        for key in track.keys() {
168            morph_frames.push(VmdParsedMorphFrame {
169                morph_name: bindings.morph_names[morph].text.clone(),
170                morph_name_bytes: bindings.morph_names[morph].bytes.clone(),
171                frame: sample_frames[key.sample_index],
172                weight: key.weight,
173            });
174        }
175    }
176    bone_frames.sort_by(|a, b| (a.frame, &a.bone_name_bytes).cmp(&(b.frame, &b.bone_name_bytes)));
177    morph_frames
178        .sort_by(|a, b| (a.frame, &a.morph_name_bytes).cmp(&(b.frame, &b.morph_name_bytes)));
179
180    let property_frames = vec![VmdParsedPropertyFrame {
181        frame: sample_frames[0],
182        visible: true,
183        ik_states: bindings
184            .ik_names
185            .iter()
186            .map(|name| VmdParsedIkState {
187                bone_name: name.text.clone(),
188                bone_name_bytes: name.bytes.clone(),
189                enabled: false,
190            })
191            .collect(),
192    }];
193    let max_frame = *sample_frames.last().expect("validated non-empty sequence");
194    let animation = VmdParsedAnimation {
195        kind: "vmd",
196        metadata: VmdParsedMetadata {
197            format: "vmd",
198            model_name: bindings.model_name.text.clone(),
199            model_name_bytes: bindings.model_name.bytes.clone(),
200            counts: VmdParsedCounts {
201                bones: bone_frames.len(),
202                morphs: morph_frames.len(),
203                cameras: 0,
204                lights: 0,
205                self_shadows: 0,
206                properties: property_frames.len(),
207            },
208            max_frame,
209        },
210        bone_frames,
211        morph_frames,
212        camera_frames: Vec::new(),
213        light_frames: Vec::new(),
214        self_shadow_frames: Vec::new(),
215        property_frames,
216    };
217    Ok(VmdPoseExport {
218        animation,
219        report: VmdPoseExportReport {
220            physics_must_be_disabled_by_host: true,
221            ik_disabled_in_vmd: true,
222            skipped_constant_bone_tracks,
223            skipped_zero_morph_tracks,
224        },
225    })
226}
227
228fn validate_name(
229    name: &VmdExportName,
230    kind: &'static str,
231    index: usize,
232    limit: usize,
233) -> Result<(), VmdPoseExportError> {
234    if name.bytes.len() > limit {
235        Err(VmdPoseExportError::NameTooLong { kind, index, limit })
236    } else {
237        Ok(())
238    }
239}
240
241fn vmd_sample_frames(sequence: &ReducedPoseSequence) -> Result<Vec<u32>, VmdPoseExportError> {
242    (0..sequence.frame_count())
243        .map(|sample| {
244            let frame = sequence.start_frame() + sample as f32 * sequence.frame_step();
245            if frame < 0.0 || frame as f64 > u32::MAX as f64 {
246                return Err(VmdPoseExportError::FrameOutOfRange { sample });
247            }
248            let rounded = frame.round();
249            if frame != rounded {
250                return Err(VmdPoseExportError::NonIntegerFrame {
251                    sample,
252                    frame: frame.to_string(),
253                });
254            }
255            Ok(rounded as u32)
256        })
257        .collect()
258}
259
260fn vmd_bone_frame(
261    name: &VmdExportName,
262    frame: u32,
263    key: &ReducedBoneKey,
264    rest_translation: glam::Vec3A,
265    rest_rotation: glam::Quat,
266) -> VmdParsedBoneFrame {
267    let translation = key.translation - rest_translation;
268    let rotation = (rest_rotation.inverse() * key.rotation).normalize();
269    VmdParsedBoneFrame {
270        bone_name: name.text.clone(),
271        bone_name_bytes: name.bytes.clone(),
272        frame,
273        translation: translation.to_array(),
274        rotation: rotation.to_array(),
275        interpolation: vmd_interpolation_block(key.vmd_interpolation).to_vec(),
276    }
277}
278
279fn vmd_interpolation_block(curves: VmdBoneInterpolation) -> [u8; 64] {
280    let channels = [
281        curves.translation[0],
282        curves.translation[1],
283        curves.translation[2],
284        curves.rotation,
285    ];
286    let mut first = [0u8; 16];
287    for (channel, curve) in channels.into_iter().enumerate() {
288        write_curve(&mut first, channel, curve);
289    }
290    let mut block = [0u8; 64];
291    for chunk in block.chunks_exact_mut(16) {
292        chunk.copy_from_slice(&first);
293    }
294    block
295}
296
297fn write_curve(block: &mut [u8; 16], channel: usize, curve: QuantizedBezier) {
298    block[channel] = curve.x1.min(127);
299    block[4 + channel] = curve.y1.min(127);
300    block[8 + channel] = curve.x2.min(127);
301    block[12 + channel] = curve.y2.min(127);
302}
303
304fn rotation_error(a: glam::Quat, b: glam::Quat) -> f32 {
305    let a = a.normalize();
306    let mut b = b.normalize();
307    if a.dot(b) < 0.0 {
308        b = -b;
309    }
310    4.0 * (((a - b).length() * 0.5).clamp(0.0, 1.0)).asin()
311}
312
313#[cfg(test)]
314mod tests;