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;