BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / preview.rs

preview.rs

228e617a History
preview.rs 23.2 KiB Download Raw
1//! CPU skeleton sampling and orbit camera for the live glTF preview.
2//!
3//! This is not the paperdoll export. The export camera is orthographic and
4//! still out of scope. The interactive camera can snap to the same three
5//! pitches and twelve yaws. The character forward axis of the test bodies is -Z,
6//! so yaw 0 places the camera on -Z, looking at the body.
7
8use std::path::Path;
9
10pub const PAPERDOLL_PITCHES_DEG: [f32; 3] = [-30.0, 0.0, 30.0];
11pub const PAPERDOLL_YAW_COUNT: usize = 12;
12pub const MAX_JOINTS: usize = 128;
13
14pub type Mat4 = [f32; 16];
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum PreviewSex {
18 Male,
19 Female,
20}
21
22#[derive(Debug, Clone)]
23pub struct PreviewControls {
24 pub sex: PreviewSex,
25 pub clip_name: String,
26 pub time: f32,
27 pub playing: bool,
28 pub yaw_deg: f32,
29 pub pitch_deg: f32,
30 pub distance: f32,
31}
32
33impl Default for PreviewControls {
34 fn default() -> Self {
35 Self {
36 sex: PreviewSex::Male,
37 clip_name: "walk".to_string(),
38 time: 0.0,
39 playing: true,
40 yaw_deg: 0.0,
41 pitch_deg: 0.0,
42 distance: 3.4,
43 }
44 }
45}
46
47impl PreviewControls {
48 pub fn drag(&mut self, dx_px: f32, dy_px: f32) {
49 self.yaw_deg = wrap_deg(self.yaw_deg - dx_px * 0.45);
50 self.pitch_deg = (self.pitch_deg + dy_px * 0.35).clamp(-80.0, 80.0);
51 }
52
53 pub fn zoom(&mut self, scroll_y: f32) {
54 let factor = (-scroll_y * 0.0015).exp();
55 self.distance = (self.distance * factor).clamp(0.8, 12.0);
56 }
57
58 pub fn snap_yaw_index(&mut self, index: i32) {
59 let wrapped = index.rem_euclid(PAPERDOLL_YAW_COUNT as i32) as f32;
60 self.yaw_deg = wrapped * 30.0;
61 }
62
63 pub fn snap_pitch_row(&mut self, row: i32) {
64 let row = row.clamp(0, 2) as usize;
65 self.pitch_deg = PAPERDOLL_PITCHES_DEG[row];
66 }
67
68 pub fn advance(&mut self, dt: f32, duration: f32) {
69 if self.playing && duration > 0.0 {
70 self.time = wrap_time(self.time + dt, duration);
71 }
72 }
73
74 pub fn select_clip(&mut self, name: &str) {
75 if self.clip_name != name {
76 self.clip_name = name.to_string();
77 self.time = 0.0;
78 }
79 }
80}
81
82#[derive(Debug, Clone)]
83pub struct PreviewCharacter {
84 pub nodes: Vec<SceneNode>,
85 pub joint_nodes: Vec<usize>,
86 pub joint_names: Vec<String>,
87 pub inverse_binds: Vec<Mat4>,
88 pub positions: Vec<[f32; 3]>,
89 pub normals: Vec<[f32; 3]>,
90 pub joints: Vec<[u16; 4]>,
91 pub weights: Vec<[f32; 4]>,
92 pub indices: Vec<u32>,
93 pub base_color: [f32; 4],
94 pub clips: Vec<PreviewClip>,
95}
96
97#[derive(Debug, Clone)]
98pub struct SceneNode {
99 pub name: String,
100 pub parent: Option<usize>,
101 pub translation: [f32; 3],
102 pub rotation: [f32; 4],
103 pub scale: [f32; 3],
104}
105
106#[derive(Debug, Clone)]
107pub struct PreviewClip {
108 pub name: String,
109 pub duration: f32,
110 pub channels: Vec<AnimChannel>,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum ChannelProperty {
115 Translation,
116 Rotation,
117 Scale,
118}
119
120#[derive(Debug, Clone)]
121pub struct AnimChannel {
122 pub node: usize,
123 pub property: ChannelProperty,
124 pub times: Vec<f32>,
125 pub values: Vec<f32>,
126}
127
128pub fn load_gltf_file(path: &Path) -> Result<PreviewCharacter, String> {
129 let (document, buffers, _images) = gltf::import(path).map_err(|err| err.to_string())?;
130 let mut nodes = Vec::new();
131 let mut parents = vec![None; document.nodes().len()];
132 for node in document.nodes() {
133 for child in node.children() {
134 parents[child.index()] = Some(node.index());
135 }
136 }
137 for node in document.nodes() {
138 let (translation, rotation, scale) = node.transform().decomposed();
139 nodes.push(SceneNode {
140 name: node.name().unwrap_or("").to_string(),
141 parent: parents[node.index()],
142 translation,
143 rotation,
144 scale,
145 });
146 }
147 let skin = document
148 .skins()
149 .next()
150 .ok_or_else(|| "het glTF-bestand heeft geen skeleton".to_string())?;
151 if skin.joints().len() > MAX_JOINTS {
152 return Err(format!(
153 "het skeleton heeft {} joints; de preview stopt bij {MAX_JOINTS}",
154 skin.joints().len()
155 ));
156 }
157 let joint_nodes: Vec<usize> = skin.joints().map(|joint| joint.index()).collect();
158 let joint_names: Vec<String> = joint_nodes
159 .iter()
160 .map(|index| nodes[*index].name.clone())
161 .collect();
162 let reader = skin.reader(|buffer| Some(buffers[buffer.index()].as_ref()));
163 let inverse_binds = reader
164 .read_inverse_bind_matrices()
165 .map(|iter| iter.map(columns_to_mat).collect())
166 .unwrap_or_else(|| vec![identity(); joint_nodes.len()]);
167 if inverse_binds.len() != joint_nodes.len() {
168 return Err("inverse-bindmatrices en joints verschillen".to_string());
169 }
170 let mut positions = Vec::new();
171 let mut normals = Vec::new();
172 let mut joints = Vec::new();
173 let mut weights = Vec::new();
174 let mut indices = Vec::new();
175 let mut base_color = [0.7, 0.7, 0.75, 1.0];
176 if let Some(mesh) = document.meshes().next() {
177 for primitive in mesh.primitives() {
178 let reader = primitive.reader(|buffer| Some(buffers[buffer.index()].as_ref()));
179 let prim_positions: Vec<[f32; 3]> = reader
180 .read_positions()
181 .map(|iter| iter.collect())
182 .unwrap_or_default();
183 let prim_normals: Vec<[f32; 3]> = reader
184 .read_normals()
185 .map(|iter| iter.collect())
186 .unwrap_or_else(|| vec![[0.0, 1.0, 0.0]; prim_positions.len()]);
187 let prim_joints: Vec<[u16; 4]> = reader
188 .read_joints(0)
189 .map(|iter| iter.into_u16().collect())
190 .unwrap_or_else(|| vec![[0, 0, 0, 0]; prim_positions.len()]);
191 let prim_weights: Vec<[f32; 4]> = reader
192 .read_weights(0)
193 .map(|iter| iter.into_f32().collect())
194 .unwrap_or_else(|| vec![[1.0, 0.0, 0.0, 0.0]; prim_positions.len()]);
195 let base = positions.len() as u32;
196 if let Some(read_indices) = reader.read_indices() {
197 indices.extend(read_indices.into_u32().map(|index| index + base));
198 } else {
199 indices.extend(base..base + prim_positions.len() as u32);
200 }
201 positions.extend(prim_positions);
202 normals.extend(prim_normals);
203 joints.extend(prim_joints);
204 weights.extend(prim_weights);
205 base_color = primitive
206 .material()
207 .pbr_metallic_roughness()
208 .base_color_factor();
209 }
210 }
211 if positions.is_empty() || indices.len() < 3 {
212 return Err("het glTF-bestand heeft geen tekenbare driehoeken".to_string());
213 }
214 let mut clips = Vec::new();
215 for animation in document.animations() {
216 let mut channels = Vec::new();
217 let mut duration = 0.0_f32;
218 for channel in animation.channels() {
219 let reader = channel.reader(|buffer| Some(buffers[buffer.index()].as_ref()));
220 let times: Vec<f32> = reader
221 .read_inputs()
222 .map(|iter| iter.collect())
223 .unwrap_or_default();
224 if let Some(last) = times.last() {
225 duration = duration.max(*last);
226 }
227 let Some(outputs) = reader.read_outputs() else {
228 continue;
229 };
230 let (property, values) = match outputs {
231 gltf::animation::util::ReadOutputs::Translations(values) => {
232 (ChannelProperty::Translation, values.flatten().collect())
233 }
234 gltf::animation::util::ReadOutputs::Scales(values) => {
235 (ChannelProperty::Scale, values.flatten().collect())
236 }
237 gltf::animation::util::ReadOutputs::Rotations(values) => (
238 ChannelProperty::Rotation,
239 values.into_f32().flatten().collect(),
240 ),
241 gltf::animation::util::ReadOutputs::MorphTargetWeights(_) => continue,
242 };
243 channels.push(AnimChannel {
244 node: channel.target().node().index(),
245 property,
246 times,
247 values,
248 });
249 }
250 clips.push(PreviewClip {
251 name: animation.name().unwrap_or("naamloos").to_string(),
252 duration,
253 channels,
254 });
255 }
256 Ok(PreviewCharacter {
257 nodes,
258 joint_nodes,
259 joint_names,
260 inverse_binds,
261 positions,
262 normals,
263 joints,
264 weights,
265 indices,
266 base_color,
267 clips,
268 })
269}
270
271pub fn sample_skin_matrices(character: &PreviewCharacter, clip_name: &str, time: f32) -> Vec<Mat4> {
272 let locals = sample_local_matrices(character, clip_name, time);
273 let worlds = world_matrices(&character.nodes, &locals);
274 character
275 .joint_nodes
276 .iter()
277 .enumerate()
278 .map(|(joint, node)| mul(worlds[*node], character.inverse_binds[joint]))
279 .collect()
280}
281
282pub fn joint_world_translation(
283 character: &PreviewCharacter,
284 clip_name: &str,
285 time: f32,
286 joint_name: &str,
287) -> Option<[f32; 3]> {
288 let joint = character
289 .joint_names
290 .iter()
291 .position(|name| name == joint_name)?;
292 let node = character.joint_nodes[joint];
293 let locals = sample_local_matrices(character, clip_name, time);
294 let worlds = world_matrices(&character.nodes, &locals);
295 Some([worlds[node][12], worlds[node][13], worlds[node][14]])
296}
297
298pub fn clip_duration(character: &PreviewCharacter, clip_name: &str) -> f32 {
299 character
300 .clips
301 .iter()
302 .find(|clip| {
303 clip.name == clip_name || clip.name.to_lowercase().contains(&clip_name.to_lowercase())
304 })
305 .map(|clip| clip.duration)
306 .unwrap_or(0.0)
307}
308
309pub fn eye_offset(yaw_deg: f32, pitch_deg: f32, distance: f32) -> [f32; 3] {
310 let yaw = yaw_deg.to_radians();
311 let pitch = pitch_deg.to_radians();
312 [
313 -distance * pitch.cos() * yaw.sin(),
314 distance * pitch.sin(),
315 -distance * pitch.cos() * yaw.cos(),
316 ]
317}
318
319pub fn paperdoll_yaws_deg() -> [f32; PAPERDOLL_YAW_COUNT] {
320 let mut yaws = [0.0; PAPERDOLL_YAW_COUNT];
321 for (index, yaw) in yaws.iter_mut().enumerate() {
322 *yaw = index as f32 * 30.0;
323 }
324 yaws
325}
326
327fn sample_local_matrices(character: &PreviewCharacter, clip_name: &str, time: f32) -> Vec<Mat4> {
328 let mut translation: Vec<[f32; 3]> = character
329 .nodes
330 .iter()
331 .map(|node| node.translation)
332 .collect();
333 let mut rotation: Vec<[f32; 4]> = character.nodes.iter().map(|node| node.rotation).collect();
334 let mut scale: Vec<[f32; 3]> = character.nodes.iter().map(|node| node.scale).collect();
335 if let Some(clip) = character.clips.iter().find(|clip| {
336 clip.name == clip_name || clip.name.to_lowercase().contains(&clip_name.to_lowercase())
337 }) {
338 let time = wrap_time(time, clip.duration);
339 for channel in &clip.channels {
340 if channel.node >= character.nodes.len() || channel.times.is_empty() {
341 continue;
342 }
343 let (left, right, blend) = key_blend(&channel.times, time);
344 match channel.property {
345 ChannelProperty::Translation => {
346 translation[channel.node] = lerp3(
347 key3(&channel.values, left),
348 key3(&channel.values, right),
349 blend,
350 );
351 }
352 ChannelProperty::Scale => {
353 scale[channel.node] = lerp3(
354 key3(&channel.values, left),
355 key3(&channel.values, right),
356 blend,
357 );
358 }
359 ChannelProperty::Rotation => {
360 rotation[channel.node] = slerp(
361 key4(&channel.values, left),
362 key4(&channel.values, right),
363 blend,
364 );
365 }
366 }
367 }
368 }
369 translation
370 .into_iter()
371 .zip(rotation)
372 .zip(scale)
373 .map(|((translation, rotation), scale)| trs(translation, rotation, scale))
374 .collect()
375}
376
377fn world_matrices(nodes: &[SceneNode], locals: &[Mat4]) -> Vec<Mat4> {
378 fn world(
379 index: usize,
380 nodes: &[SceneNode],
381 locals: &[Mat4],
382 worlds: &mut [Option<Mat4>],
383 depth: usize,
384 ) -> Mat4 {
385 if let Some(matrix) = worlds[index] {
386 return matrix;
387 }
388 if depth > nodes.len() {
389 return locals[index];
390 }
391 let matrix = match nodes[index].parent {
392 Some(parent) => mul(
393 world(parent, nodes, locals, worlds, depth + 1),
394 locals[index],
395 ),
396 None => locals[index],
397 };
398 worlds[index] = Some(matrix);
399 matrix
400 }
401 let mut cached = vec![None; nodes.len()];
402 (0..nodes.len())
403 .map(|index| world(index, nodes, locals, &mut cached, 0))
404 .collect()
405}
406
407fn key_blend(times: &[f32], time: f32) -> (usize, usize, f32) {
408 if times.len() == 1 || time <= times[0] {
409 return (0, 0, 0.0);
410 }
411 if time >= times[times.len() - 1] {
412 let last = times.len() - 1;
413 return (last, last, 0.0);
414 }
415 let mut index = 0;
416 while index + 1 < times.len() && times[index + 1] < time {
417 index += 1;
418 }
419 let span = times[index + 1] - times[index];
420 let blend = if span.abs() < 1e-6 {
421 0.0
422 } else {
423 (time - times[index]) / span
424 };
425 (index, index + 1, blend)
426}
427
428fn key3(values: &[f32], index: usize) -> [f32; 3] {
429 let start = index * 3;
430 [
431 values.get(start).copied().unwrap_or(0.0),
432 values.get(start + 1).copied().unwrap_or(0.0),
433 values.get(start + 2).copied().unwrap_or(0.0),
434 ]
435}
436
437fn key4(values: &[f32], index: usize) -> [f32; 4] {
438 let start = index * 4;
439 [
440 values.get(start).copied().unwrap_or(0.0),
441 values.get(start + 1).copied().unwrap_or(0.0),
442 values.get(start + 2).copied().unwrap_or(0.0),
443 values.get(start + 3).copied().unwrap_or(1.0),
444 ]
445}
446
447fn lerp3(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
448 [
449 a[0] + (b[0] - a[0]) * t,
450 a[1] + (b[1] - a[1]) * t,
451 a[2] + (b[2] - a[2]) * t,
452 ]
453}
454
455fn slerp(a: [f32; 4], mut b: [f32; 4], t: f32) -> [f32; 4] {
456 let mut dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
457 if dot < 0.0 {
458 dot = -dot;
459 b = [-b[0], -b[1], -b[2], -b[3]];
460 }
461 if dot > 0.9995 {
462 return normalize4([
463 a[0] + (b[0] - a[0]) * t,
464 a[1] + (b[1] - a[1]) * t,
465 a[2] + (b[2] - a[2]) * t,
466 a[3] + (b[3] - a[3]) * t,
467 ]);
468 }
469 let theta = dot.clamp(-1.0, 1.0).acos();
470 let sin_theta = theta.sin();
471 let w1 = ((1.0 - t) * theta).sin() / sin_theta;
472 let w2 = (t * theta).sin() / sin_theta;
473 [
474 a[0] * w1 + b[0] * w2,
475 a[1] * w1 + b[1] * w2,
476 a[2] * w1 + b[2] * w2,
477 a[3] * w1 + b[3] * w2,
478 ]
479}
480
481fn normalize4(value: [f32; 4]) -> [f32; 4] {
482 let length =
483 (value[0] * value[0] + value[1] * value[1] + value[2] * value[2] + value[3] * value[3])
484 .sqrt();
485 if length < 1e-8 {
486 [0.0, 0.0, 0.0, 1.0]
487 } else {
488 [
489 value[0] / length,
490 value[1] / length,
491 value[2] / length,
492 value[3] / length,
493 ]
494 }
495}
496
497fn trs(translation: [f32; 3], rotation: [f32; 4], scale: [f32; 3]) -> Mat4 {
498 let mut matrix = quat_to_mat(normalize4(rotation));
499 matrix[0] *= scale[0];
500 matrix[1] *= scale[0];
501 matrix[2] *= scale[0];
502 matrix[4] *= scale[1];
503 matrix[5] *= scale[1];
504 matrix[6] *= scale[1];
505 matrix[8] *= scale[2];
506 matrix[9] *= scale[2];
507 matrix[10] *= scale[2];
508 matrix[12] = translation[0];
509 matrix[13] = translation[1];
510 matrix[14] = translation[2];
511 matrix
512}
513
514fn quat_to_mat(q: [f32; 4]) -> Mat4 {
515 let (x, y, z, w) = (q[0], q[1], q[2], q[3]);
516 let xx = x * x;
517 let yy = y * y;
518 let zz = z * z;
519 let xy = x * y;
520 let xz = x * z;
521 let yz = y * z;
522 let wx = w * x;
523 let wy = w * y;
524 let wz = w * z;
525 [
526 1.0 - 2.0 * (yy + zz),
527 2.0 * (xy + wz),
528 2.0 * (xz - wy),
529 0.0,
530 2.0 * (xy - wz),
531 1.0 - 2.0 * (xx + zz),
532 2.0 * (yz + wx),
533 0.0,
534 2.0 * (xz + wy),
535 2.0 * (yz - wx),
536 1.0 - 2.0 * (xx + yy),
537 0.0,
538 0.0,
539 0.0,
540 0.0,
541 1.0,
542 ]
543}
544
545pub fn identity() -> Mat4 {
546 [
547 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,
548 ]
549}
550
551pub fn mul(left: Mat4, right: Mat4) -> Mat4 {
552 let mut out = [0.0; 16];
553 for column in 0..4 {
554 for row in 0..4 {
555 out[column * 4 + row] = left[row] * right[column * 4]
556 + left[4 + row] * right[column * 4 + 1]
557 + left[8 + row] * right[column * 4 + 2]
558 + left[12 + row] * right[column * 4 + 3];
559 }
560 }
561 out
562}
563
564fn columns_to_mat(columns: [[f32; 4]; 4]) -> Mat4 {
565 let mut matrix = [0.0; 16];
566 for column in 0..4 {
567 for row in 0..4 {
568 matrix[column * 4 + row] = columns[column][row];
569 }
570 }
571 matrix
572}
573
574pub fn wrap_time(time: f32, duration: f32) -> f32 {
575 if duration <= 1e-5 {
576 return 0.0;
577 }
578 let mut wrapped = time % duration;
579 if wrapped < 0.0 {
580 wrapped += duration;
581 }
582 wrapped
583}
584
585fn wrap_deg(mut degrees: f32) -> f32 {
586 degrees %= 360.0;
587 if degrees < 0.0 {
588 degrees += 360.0;
589 }
590 degrees
591}
592
593pub fn view_matrix(eye: [f32; 3], target: [f32; 3]) -> Mat4 {
594 let forward = normalize3([target[0] - eye[0], target[1] - eye[1], target[2] - eye[2]]);
595 let up = [0.0, 1.0, 0.0];
596 let mut side = normalize3(cross(forward, up));
597 if side[0].abs() + side[1].abs() + side[2].abs() < 1e-4 {
598 side = [1.0, 0.0, 0.0];
599 }
600 let up = cross(side, forward);
601 [
602 side[0],
603 up[0],
604 -forward[0],
605 0.0,
606 side[1],
607 up[1],
608 -forward[1],
609 0.0,
610 side[2],
611 up[2],
612 -forward[2],
613 0.0,
614 -dot(side, eye),
615 -dot(up, eye),
616 dot(forward, eye),
617 1.0,
618 ]
619}
620
621pub fn perspective(fov_y_deg: f32, aspect: f32, near: f32, far: f32) -> Mat4 {
622 let f = 1.0 / (fov_y_deg.to_radians() * 0.5).tan();
623 let aspect = if aspect.abs() < 1e-4 { 1.0 } else { aspect };
624 let nf = 1.0 / (near - far);
625 [
626 f / aspect,
627 0.0,
628 0.0,
629 0.0,
630 0.0,
631 f,
632 0.0,
633 0.0,
634 0.0,
635 0.0,
636 (far + near) * nf,
637 -1.0,
638 0.0,
639 0.0,
640 2.0 * far * near * nf,
641 0.0,
642 ]
643}
644
645fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
646 [
647 a[1] * b[2] - a[2] * b[1],
648 a[2] * b[0] - a[0] * b[2],
649 a[0] * b[1] - a[1] * b[0],
650 ]
651}
652
653fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
654 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
655}
656
657fn normalize3(value: [f32; 3]) -> [f32; 3] {
658 let length = dot(value, value).sqrt();
659 if length < 1e-8 {
660 [0.0, 0.0, 1.0]
661 } else {
662 [value[0] / length, value[1] / length, value[2] / length]
663 }
664}
665
666#[cfg(test)]
667mod tests {
668 use super::*;
669
670 fn male() -> PreviewCharacter {
671 load_gltf_file(
672 &Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/bodies/test-body-male.glb"),
673 )
674 .unwrap()
675 }
676
677 fn distance(a: [f32; 3], b: [f32; 3]) -> f32 {
678 let d = [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
679 dot(d, d).sqrt()
680 }
681
682 #[test]
683 fn walk_and_attack_move_the_matching_bones() {
684 let character = male();
685 assert!(character.clips.iter().any(|clip| clip.name == "walk"));
686 assert!(character.clips.iter().any(|clip| clip.name == "attack"));
687 assert_eq!(character.joint_names.len(), 19);
688 assert!(character.indices.len() >= 3);
689 assert!(character.positions.len() >= 19 * 8);
690 let leg_a = joint_world_translation(&character, "walk", 0.0, "LeftLowerLeg").unwrap();
691 let leg_b = joint_world_translation(&character, "walk", 0.25, "LeftLowerLeg").unwrap();
692 assert!(
693 distance(leg_a, leg_b) > 0.05,
694 "walk should move the lower leg, delta {}",
695 distance(leg_a, leg_b)
696 );
697 let arm_a = joint_world_translation(&character, "attack", 0.0, "RightHand").unwrap();
698 let arm_b = joint_world_translation(&character, "attack", 0.35, "RightHand").unwrap();
699 assert!(
700 distance(arm_a, arm_b) > 0.05,
701 "attack should move the right hand, delta {}",
702 distance(arm_a, arm_b)
703 );
704 let matrices = sample_skin_matrices(&character, "walk", 0.2);
705 assert_eq!(matrices.len(), 19);
706 assert!(matrices.iter().flatten().all(|value| value.is_finite()));
707 }
708
709 #[test]
710 fn female_fixture_has_the_same_clip_names() {
711 let character = load_gltf_file(
712 &Path::new(env!("CARGO_MANIFEST_DIR"))
713 .join("tests/fixtures/bodies/test-body-female.glb"),
714 )
715 .unwrap();
716 assert!(character.clips.iter().any(|clip| clip.name == "walk"));
717 assert!(character.clips.iter().any(|clip| clip.name == "attack"));
718 let a = joint_world_translation(&character, "walk", 0.0, "RightLowerLeg").unwrap();
719 let b = joint_world_translation(&character, "walk", 0.5, "RightLowerLeg").unwrap();
720 assert!(distance(a, b) > 0.05);
721 }
722
723 #[test]
724 fn yaw_zero_is_in_front_and_the_twelve_yaws_differ() {
725 let front = eye_offset(0.0, 0.0, 4.0);
726 assert!(front[2] < -3.5, "yaw 0 camera belongs on -Z, got {front:?}");
727 assert!(front[0].abs() < 0.05);
728 let mut seen = Vec::new();
729 for yaw in paperdoll_yaws_deg() {
730 let eye = eye_offset(yaw, 0.0, 4.0);
731 assert!(seen
732 .iter()
733 .all(|previous: &[f32; 3]| distance(*previous, eye) > 0.5));
734 seen.push(eye);
735 }
736 assert_eq!(seen.len(), 12);
737 let low = eye_offset(0.0, -30.0, 4.0);
738 let high = eye_offset(0.0, 30.0, 4.0);
739 assert!(low[1] < 0.0 && high[1] > 0.0);
740 assert_eq!(PAPERDOLL_PITCHES_DEG.len(), 3);
741 }
742
743 #[test]
744 fn controls_snap_wrap_and_keep_a_clip_change() {
745 let mut controls = PreviewControls::default();
746 controls.drag(-100.0, 40.0);
747 assert!(controls.yaw_deg > 40.0);
748 assert!(controls.pitch_deg > 10.0);
749 controls.zoom(400.0);
750 assert!(controls.distance < 3.4);
751 controls.snap_yaw_index(-1);
752 assert!((controls.yaw_deg - 330.0).abs() < 0.01);
753 controls.snap_pitch_row(99);
754 assert!((controls.pitch_deg - 30.0).abs() < 0.01);
755 controls.snap_pitch_row(-4);
756 assert!((controls.pitch_deg + 30.0).abs() < 0.01);
757 controls.time = 0.4;
758 controls.select_clip("attack");
759 assert_eq!(controls.clip_name, "attack");
760 assert_eq!(controls.time, 0.0);
761 controls.time = 1.2;
762 controls.advance(0.0, 1.0);
763 controls.playing = true;
764 controls.advance(0.3, 1.0);
765 assert!((controls.time - 0.5).abs() < 0.02 || (wrap_time(1.5, 1.0) - 0.5).abs() < 0.02);
766 }
767}