BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / preview.rs

preview.rs

preview.rs 28.1 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 /// A placement-only fallback must never masquerade as a production normal map.
91 pub source_normals_complete: bool,
92 pub joints: Vec<[u16; 4]>,
93 pub weights: Vec<[f32; 4]>,
94 pub indices: Vec<u32>,
95 pub base_color: [f32; 4],
96 pub colors: Vec<[f32; 4]>,
97 pub uvs: Vec<[f32; 2]>,
98 pub surfaces: Vec<BodySurface>,
99 pub clips: Vec<PreviewClip>,
100}
101
102#[derive(Debug, Clone)]
103pub struct BodySurface {
104 pub first: usize,
105 pub count: usize,
106 pub color: [f32; 4],
107 pub albedo: Option<std::sync::Arc<crate::equipment::Texture>>,
108 pub cutoff: f32,
109 pub flat_normals: bool,
110}
111
112#[derive(Debug, Clone)]
113pub struct SceneNode {
114 pub name: String,
115 pub parent: Option<usize>,
116 pub translation: [f32; 3],
117 pub rotation: [f32; 4],
118 pub scale: [f32; 3],
119}
120
121#[derive(Debug, Clone)]
122pub struct PreviewClip {
123 pub name: String,
124 pub duration: f32,
125 pub channels: Vec<AnimChannel>,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum ChannelProperty {
130 Translation,
131 Rotation,
132 Scale,
133}
134
135#[derive(Debug, Clone)]
136pub struct AnimChannel {
137 pub node: usize,
138 pub property: ChannelProperty,
139 pub times: Vec<f32>,
140 pub values: Vec<f32>,
141}
142
143pub fn load_gltf_file(path: &Path) -> Result<PreviewCharacter, String> {
144 let (document, buffers, images) = gltf::import(path).map_err(|err| err.to_string())?;
145 let mut nodes = Vec::new();
146 let mut parents = vec![None; document.nodes().len()];
147 for node in document.nodes() {
148 for child in node.children() {
149 parents[child.index()] = Some(node.index());
150 }
151 }
152 for node in document.nodes() {
153 let (translation, rotation, scale) = node.transform().decomposed();
154 nodes.push(SceneNode {
155 name: node.name().unwrap_or("").to_string(),
156 parent: parents[node.index()],
157 translation,
158 rotation,
159 scale,
160 });
161 }
162 let skin = document
163 .skins()
164 .next()
165 .ok_or_else(|| "het glTF-bestand heeft geen skeleton".to_string())?;
166 if skin.joints().len() > MAX_JOINTS {
167 return Err(format!(
168 "het skeleton heeft {} joints; de preview stopt bij {MAX_JOINTS}",
169 skin.joints().len()
170 ));
171 }
172 let joint_nodes: Vec<usize> = skin.joints().map(|joint| joint.index()).collect();
173 let joint_names: Vec<String> = joint_nodes
174 .iter()
175 .map(|index| nodes[*index].name.clone())
176 .collect();
177 let reader = skin.reader(|buffer| Some(buffers[buffer.index()].as_ref()));
178 let inverse_binds = reader
179 .read_inverse_bind_matrices()
180 .map(|iter| iter.map(columns_to_mat).collect())
181 .unwrap_or_else(|| vec![identity(); joint_nodes.len()]);
182 if inverse_binds.len() != joint_nodes.len() {
183 return Err("inverse-bindmatrices en joints verschillen".to_string());
184 }
185 let mut positions = Vec::new();
186 let mut normals = Vec::new();
187 let mut source_normals_complete = true;
188 let mut joints = Vec::new();
189 let mut weights = Vec::new();
190 let mut indices = Vec::new();
191 let mut base_color = [0.7, 0.7, 0.75, 1.0];
192 let mut colors = Vec::new();
193 let mut uvs = Vec::new();
194 let mut surfaces = Vec::new();
195 if let Some(mesh) = document.meshes().next() {
196 for primitive in mesh.primitives() {
197 if primitive.mode() != gltf::mesh::Mode::Triangles {
198 return Err("live/productie ondersteunt alleen driehoekprimitieven".into());
199 }
200 let reader = primitive.reader(|buffer| Some(buffers[buffer.index()].as_ref()));
201 let prim_positions: Vec<[f32; 3]> = reader
202 .read_positions()
203 .map(|iter| iter.collect())
204 .unwrap_or_default();
205 source_normals_complete &= reader.read_normals().is_some();
206 let prim_normals: Vec<[f32; 3]> = reader
207 .read_normals()
208 .map(|iter| iter.collect())
209 .unwrap_or_else(|| vec![[0.0, 1.0, 0.0]; prim_positions.len()]);
210 let prim_joints: Vec<[u16; 4]> = reader
211 .read_joints(0)
212 .map(|iter| iter.into_u16().collect())
213 .unwrap_or_else(|| vec![[0, 0, 0, 0]; prim_positions.len()]);
214 let prim_weights: Vec<[f32; 4]> = reader
215 .read_weights(0)
216 .map(|iter| iter.into_f32().collect())
217 .unwrap_or_else(|| vec![[1.0, 0.0, 0.0, 0.0]; prim_positions.len()]);
218 let material = primitive.material();
219 let pbr = material.pbr_metallic_roughness();
220 let texture = pbr.base_color_texture();
221 let uvset = texture.as_ref().map(|t| t.tex_coord()).unwrap_or(0);
222 colors.extend(
223 reader
224 .read_colors(0)
225 .map(|v| v.into_rgba_f32().collect::<Vec<_>>())
226 .unwrap_or_else(|| vec![[1.; 4]; prim_positions.len()]),
227 );
228 uvs.extend(
229 reader
230 .read_tex_coords(uvset)
231 .map(|v| v.into_f32().collect::<Vec<_>>())
232 .unwrap_or_else(|| vec![[0.; 2]; prim_positions.len()]),
233 );
234 let first = indices.len();
235 let base = positions.len() as u32;
236 if let Some(read_indices) = reader.read_indices() {
237 indices.extend(read_indices.into_u32().map(|index| index + base));
238 } else {
239 indices.extend(base..base + prim_positions.len() as u32);
240 }
241 positions.extend(prim_positions);
242 normals.extend(prim_normals);
243 joints.extend(prim_joints);
244 weights.extend(prim_weights);
245 base_color = primitive
246 .material()
247 .pbr_metallic_roughness()
248 .base_color_factor();
249 let albedo = texture
250 .map(|t| {
251 let image = images
252 .get(t.texture().source().index())
253 .ok_or("ontbrekende bodytexture")?;
254 use gltf::image::Format;
255 let channels = match image.format {
256 Format::R8 => 1,
257 Format::R8G8 => 2,
258 Format::R8G8B8 => 3,
259 Format::R8G8B8A8 => 4,
260 _ => return Err("bodytexture moet 8-bit zijn".to_string()),
261 };
262 if image.width > 2048 || image.height > 2048 {
263 return Err("bodytexture groter dan 2048×2048".into());
264 }
265 let mut rgba = Vec::new();
266 for p in image.pixels.chunks_exact(channels) {
267 rgba.extend_from_slice(&match channels {
268 1 => [p[0], p[0], p[0], 255],
269 2 => [p[0], p[0], p[0], p[1]],
270 3 => [p[0], p[1], p[2], 255],
271 _ => [p[0], p[1], p[2], p[3]],
272 });
273 }
274 use std::hash::{Hash, Hasher};
275 let mut hash = std::collections::hash_map::DefaultHasher::new();
276 rgba.hash(&mut hash);
277 Ok(std::sync::Arc::new(crate::equipment::Texture {
278 content_hash: hash.finish(),
279 width: image.width,
280 height: image.height,
281 rgba,
282 }))
283 })
284 .transpose()?;
285 surfaces.push(BodySurface {
286 flat_normals: reader.read_normals().is_none(),
287 first,
288 count: indices.len() - first,
289 color: base_color,
290 albedo,
291 cutoff: if material.alpha_mode() == gltf::material::AlphaMode::Mask {
292 material.alpha_cutoff().unwrap_or(0.5)
293 } else {
294 0.001
295 },
296 });
297 }
298 }
299 if positions.is_empty() || indices.len() < 3 {
300 return Err("het glTF-bestand heeft geen tekenbare driehoeken".to_string());
301 }
302 let mut clips = Vec::new();
303 for animation in document.animations() {
304 let mut channels = Vec::new();
305 let mut duration = 0.0_f32;
306 for channel in animation.channels() {
307 let reader = channel.reader(|buffer| Some(buffers[buffer.index()].as_ref()));
308 let times: Vec<f32> = reader
309 .read_inputs()
310 .map(|iter| iter.collect())
311 .unwrap_or_default();
312 if let Some(last) = times.last() {
313 duration = duration.max(*last);
314 }
315 let Some(outputs) = reader.read_outputs() else {
316 continue;
317 };
318 let (property, values) = match outputs {
319 gltf::animation::util::ReadOutputs::Translations(values) => {
320 (ChannelProperty::Translation, values.flatten().collect())
321 }
322 gltf::animation::util::ReadOutputs::Scales(values) => {
323 (ChannelProperty::Scale, values.flatten().collect())
324 }
325 gltf::animation::util::ReadOutputs::Rotations(values) => (
326 ChannelProperty::Rotation,
327 values.into_f32().flatten().collect(),
328 ),
329 gltf::animation::util::ReadOutputs::MorphTargetWeights(_) => continue,
330 };
331 channels.push(AnimChannel {
332 node: channel.target().node().index(),
333 property,
334 times,
335 values,
336 });
337 }
338 clips.push(PreviewClip {
339 name: animation.name().unwrap_or("naamloos").to_string(),
340 duration,
341 channels,
342 });
343 }
344 Ok(PreviewCharacter {
345 nodes,
346 joint_nodes,
347 joint_names,
348 inverse_binds,
349 positions,
350 normals,
351 source_normals_complete,
352 joints,
353 weights,
354 indices,
355 base_color,
356 colors,
357 uvs,
358 surfaces,
359 clips,
360 })
361}
362
363pub fn sample_skin_matrices(character: &PreviewCharacter, clip_name: &str, time: f32) -> Vec<Mat4> {
364 let locals = sample_local_matrices(character, clip_name, time);
365 let worlds = world_matrices(&character.nodes, &locals);
366 character
367 .joint_nodes
368 .iter()
369 .enumerate()
370 .map(|(joint, node)| mul(worlds[*node], character.inverse_binds[joint]))
371 .collect()
372}
373
374/// Stable hierarchy path includes node indices to disambiguate duplicate names.
375pub fn joint_paths(character: &PreviewCharacter) -> Vec<String> {
376 character
377 .joint_nodes
378 .iter()
379 .map(|&node| {
380 let mut names = Vec::new();
381 let mut cursor = Some(node);
382 while let Some(index) = cursor {
383 let item = &character.nodes[index];
384 names.push(format!("{}[{}]", item.name, index));
385 cursor = item.parent;
386 if names.len() > character.nodes.len() {
387 break;
388 }
389 }
390 names.reverse();
391 names.join("/")
392 })
393 .collect()
394}
395
396pub fn animated_joint_world(
397 character: &PreviewCharacter,
398 clip: &str,
399 time: f32,
400 joint: usize,
401) -> Option<Mat4> {
402 let locals = sample_local_matrices(character, clip, time);
403 let worlds = world_matrices(&character.nodes, &locals);
404 character
405 .joint_nodes
406 .get(joint)
407 .and_then(|node| worlds.get(*node))
408 .copied()
409}
410
411pub fn joint_world_translation(
412 character: &PreviewCharacter,
413 clip_name: &str,
414 time: f32,
415 joint_name: &str,
416) -> Option<[f32; 3]> {
417 let joint = character
418 .joint_names
419 .iter()
420 .position(|name| name == joint_name)?;
421 let node = character.joint_nodes[joint];
422 let locals = sample_local_matrices(character, clip_name, time);
423 let worlds = world_matrices(&character.nodes, &locals);
424 Some([worlds[node][12], worlds[node][13], worlds[node][14]])
425}
426
427pub fn clip_duration(character: &PreviewCharacter, clip_name: &str) -> f32 {
428 character
429 .clips
430 .iter()
431 .find(|clip| {
432 clip.name == clip_name || clip.name.to_lowercase().contains(&clip_name.to_lowercase())
433 })
434 .map(|clip| clip.duration)
435 .unwrap_or(0.0)
436}
437
438pub fn eye_offset(yaw_deg: f32, pitch_deg: f32, distance: f32) -> [f32; 3] {
439 let yaw = yaw_deg.to_radians();
440 let pitch = pitch_deg.to_radians();
441 [
442 -distance * pitch.cos() * yaw.sin(),
443 distance * pitch.sin(),
444 -distance * pitch.cos() * yaw.cos(),
445 ]
446}
447
448pub fn paperdoll_yaws_deg() -> [f32; PAPERDOLL_YAW_COUNT] {
449 let mut yaws = [0.0; PAPERDOLL_YAW_COUNT];
450 for (index, yaw) in yaws.iter_mut().enumerate() {
451 *yaw = index as f32 * 30.0;
452 }
453 yaws
454}
455
456fn sample_local_matrices(character: &PreviewCharacter, clip_name: &str, time: f32) -> Vec<Mat4> {
457 let mut translation: Vec<[f32; 3]> = character
458 .nodes
459 .iter()
460 .map(|node| node.translation)
461 .collect();
462 let mut rotation: Vec<[f32; 4]> = character.nodes.iter().map(|node| node.rotation).collect();
463 let mut scale: Vec<[f32; 3]> = character.nodes.iter().map(|node| node.scale).collect();
464 if let Some(clip) = character.clips.iter().find(|clip| {
465 clip.name == clip_name || clip.name.to_lowercase().contains(&clip_name.to_lowercase())
466 }) {
467 let time = wrap_time(time, clip.duration);
468 for channel in &clip.channels {
469 if channel.node >= character.nodes.len() || channel.times.is_empty() {
470 continue;
471 }
472 let (left, right, blend) = key_blend(&channel.times, time);
473 match channel.property {
474 ChannelProperty::Translation => {
475 translation[channel.node] = lerp3(
476 key3(&channel.values, left),
477 key3(&channel.values, right),
478 blend,
479 );
480 }
481 ChannelProperty::Scale => {
482 scale[channel.node] = lerp3(
483 key3(&channel.values, left),
484 key3(&channel.values, right),
485 blend,
486 );
487 }
488 ChannelProperty::Rotation => {
489 rotation[channel.node] = slerp(
490 key4(&channel.values, left),
491 key4(&channel.values, right),
492 blend,
493 );
494 }
495 }
496 }
497 }
498 translation
499 .into_iter()
500 .zip(rotation)
501 .zip(scale)
502 .map(|((translation, rotation), scale)| trs(translation, rotation, scale))
503 .collect()
504}
505
506fn world_matrices(nodes: &[SceneNode], locals: &[Mat4]) -> Vec<Mat4> {
507 fn world(
508 index: usize,
509 nodes: &[SceneNode],
510 locals: &[Mat4],
511 worlds: &mut [Option<Mat4>],
512 depth: usize,
513 ) -> Mat4 {
514 if let Some(matrix) = worlds[index] {
515 return matrix;
516 }
517 if depth > nodes.len() {
518 return locals[index];
519 }
520 let matrix = match nodes[index].parent {
521 Some(parent) => mul(
522 world(parent, nodes, locals, worlds, depth + 1),
523 locals[index],
524 ),
525 None => locals[index],
526 };
527 worlds[index] = Some(matrix);
528 matrix
529 }
530 let mut cached = vec![None; nodes.len()];
531 (0..nodes.len())
532 .map(|index| world(index, nodes, locals, &mut cached, 0))
533 .collect()
534}
535
536fn key_blend(times: &[f32], time: f32) -> (usize, usize, f32) {
537 if times.len() == 1 || time <= times[0] {
538 return (0, 0, 0.0);
539 }
540 if time >= times[times.len() - 1] {
541 let last = times.len() - 1;
542 return (last, last, 0.0);
543 }
544 let mut index = 0;
545 while index + 1 < times.len() && times[index + 1] < time {
546 index += 1;
547 }
548 let span = times[index + 1] - times[index];
549 let blend = if span.abs() < 1e-6 {
550 0.0
551 } else {
552 (time - times[index]) / span
553 };
554 (index, index + 1, blend)
555}
556
557fn key3(values: &[f32], index: usize) -> [f32; 3] {
558 let start = index * 3;
559 [
560 values.get(start).copied().unwrap_or(0.0),
561 values.get(start + 1).copied().unwrap_or(0.0),
562 values.get(start + 2).copied().unwrap_or(0.0),
563 ]
564}
565
566fn key4(values: &[f32], index: usize) -> [f32; 4] {
567 let start = index * 4;
568 [
569 values.get(start).copied().unwrap_or(0.0),
570 values.get(start + 1).copied().unwrap_or(0.0),
571 values.get(start + 2).copied().unwrap_or(0.0),
572 values.get(start + 3).copied().unwrap_or(1.0),
573 ]
574}
575
576fn lerp3(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
577 [
578 a[0] + (b[0] - a[0]) * t,
579 a[1] + (b[1] - a[1]) * t,
580 a[2] + (b[2] - a[2]) * t,
581 ]
582}
583
584fn slerp(a: [f32; 4], mut b: [f32; 4], t: f32) -> [f32; 4] {
585 let mut dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
586 if dot < 0.0 {
587 dot = -dot;
588 b = [-b[0], -b[1], -b[2], -b[3]];
589 }
590 if dot > 0.9995 {
591 return normalize4([
592 a[0] + (b[0] - a[0]) * t,
593 a[1] + (b[1] - a[1]) * t,
594 a[2] + (b[2] - a[2]) * t,
595 a[3] + (b[3] - a[3]) * t,
596 ]);
597 }
598 let theta = dot.clamp(-1.0, 1.0).acos();
599 let sin_theta = theta.sin();
600 let w1 = ((1.0 - t) * theta).sin() / sin_theta;
601 let w2 = (t * theta).sin() / sin_theta;
602 [
603 a[0] * w1 + b[0] * w2,
604 a[1] * w1 + b[1] * w2,
605 a[2] * w1 + b[2] * w2,
606 a[3] * w1 + b[3] * w2,
607 ]
608}
609
610fn normalize4(value: [f32; 4]) -> [f32; 4] {
611 let length =
612 (value[0] * value[0] + value[1] * value[1] + value[2] * value[2] + value[3] * value[3])
613 .sqrt();
614 if length < 1e-8 {
615 [0.0, 0.0, 0.0, 1.0]
616 } else {
617 [
618 value[0] / length,
619 value[1] / length,
620 value[2] / length,
621 value[3] / length,
622 ]
623 }
624}
625
626fn trs(translation: [f32; 3], rotation: [f32; 4], scale: [f32; 3]) -> Mat4 {
627 let mut matrix = quat_to_mat(normalize4(rotation));
628 matrix[0] *= scale[0];
629 matrix[1] *= scale[0];
630 matrix[2] *= scale[0];
631 matrix[4] *= scale[1];
632 matrix[5] *= scale[1];
633 matrix[6] *= scale[1];
634 matrix[8] *= scale[2];
635 matrix[9] *= scale[2];
636 matrix[10] *= scale[2];
637 matrix[12] = translation[0];
638 matrix[13] = translation[1];
639 matrix[14] = translation[2];
640 matrix
641}
642
643fn quat_to_mat(q: [f32; 4]) -> Mat4 {
644 let (x, y, z, w) = (q[0], q[1], q[2], q[3]);
645 let xx = x * x;
646 let yy = y * y;
647 let zz = z * z;
648 let xy = x * y;
649 let xz = x * z;
650 let yz = y * z;
651 let wx = w * x;
652 let wy = w * y;
653 let wz = w * z;
654 [
655 1.0 - 2.0 * (yy + zz),
656 2.0 * (xy + wz),
657 2.0 * (xz - wy),
658 0.0,
659 2.0 * (xy - wz),
660 1.0 - 2.0 * (xx + zz),
661 2.0 * (yz + wx),
662 0.0,
663 2.0 * (xz + wy),
664 2.0 * (yz - wx),
665 1.0 - 2.0 * (xx + yy),
666 0.0,
667 0.0,
668 0.0,
669 0.0,
670 1.0,
671 ]
672}
673
674pub fn identity() -> Mat4 {
675 [
676 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,
677 ]
678}
679
680pub fn mul(left: Mat4, right: Mat4) -> Mat4 {
681 let mut out = [0.0; 16];
682 for column in 0..4 {
683 for row in 0..4 {
684 out[column * 4 + row] = left[row] * right[column * 4]
685 + left[4 + row] * right[column * 4 + 1]
686 + left[8 + row] * right[column * 4 + 2]
687 + left[12 + row] * right[column * 4 + 3];
688 }
689 }
690 out
691}
692
693fn columns_to_mat(columns: [[f32; 4]; 4]) -> Mat4 {
694 let mut matrix = [0.0; 16];
695 for column in 0..4 {
696 for row in 0..4 {
697 matrix[column * 4 + row] = columns[column][row];
698 }
699 }
700 matrix
701}
702
703pub fn wrap_time(time: f32, duration: f32) -> f32 {
704 if duration <= 1e-5 {
705 return 0.0;
706 }
707 let mut wrapped = time % duration;
708 if wrapped < 0.0 {
709 wrapped += duration;
710 }
711 wrapped
712}
713
714fn wrap_deg(mut degrees: f32) -> f32 {
715 degrees %= 360.0;
716 if degrees < 0.0 {
717 degrees += 360.0;
718 }
719 degrees
720}
721
722pub fn view_matrix(eye: [f32; 3], target: [f32; 3]) -> Mat4 {
723 let forward = normalize3([target[0] - eye[0], target[1] - eye[1], target[2] - eye[2]]);
724 let up = [0.0, 1.0, 0.0];
725 let mut side = normalize3(cross(forward, up));
726 if side[0].abs() + side[1].abs() + side[2].abs() < 1e-4 {
727 side = [1.0, 0.0, 0.0];
728 }
729 let up = cross(side, forward);
730 [
731 side[0],
732 up[0],
733 -forward[0],
734 0.0,
735 side[1],
736 up[1],
737 -forward[1],
738 0.0,
739 side[2],
740 up[2],
741 -forward[2],
742 0.0,
743 -dot(side, eye),
744 -dot(up, eye),
745 dot(forward, eye),
746 1.0,
747 ]
748}
749
750pub fn perspective(fov_y_deg: f32, aspect: f32, near: f32, far: f32) -> Mat4 {
751 let f = 1.0 / (fov_y_deg.to_radians() * 0.5).tan();
752 let aspect = if aspect.abs() < 1e-4 { 1.0 } else { aspect };
753 let nf = 1.0 / (near - far);
754 [
755 f / aspect,
756 0.0,
757 0.0,
758 0.0,
759 0.0,
760 f,
761 0.0,
762 0.0,
763 0.0,
764 0.0,
765 (far + near) * nf,
766 -1.0,
767 0.0,
768 0.0,
769 2.0 * far * near * nf,
770 0.0,
771 ]
772}
773
774fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
775 [
776 a[1] * b[2] - a[2] * b[1],
777 a[2] * b[0] - a[0] * b[2],
778 a[0] * b[1] - a[1] * b[0],
779 ]
780}
781
782fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
783 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
784}
785
786fn normalize3(value: [f32; 3]) -> [f32; 3] {
787 let length = dot(value, value).sqrt();
788 if length < 1e-8 {
789 [0.0, 0.0, 1.0]
790 } else {
791 [value[0] / length, value[1] / length, value[2] / length]
792 }
793}
794
795#[cfg(test)]
796mod tests {
797 use super::*;
798
799 fn male() -> PreviewCharacter {
800 load_gltf_file(
801 &Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/bodies/test-body-male.glb"),
802 )
803 .unwrap()
804 }
805
806 fn distance(a: [f32; 3], b: [f32; 3]) -> f32 {
807 let d = [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
808 dot(d, d).sqrt()
809 }
810
811 #[test]
812 fn walk_and_attack_move_the_matching_bones() {
813 let character = male();
814 assert!(character.clips.iter().any(|clip| clip.name == "walk"));
815 assert!(character.clips.iter().any(|clip| clip.name == "attack"));
816 assert_eq!(character.joint_names.len(), 19);
817 assert!(character.indices.len() >= 3);
818 assert!(character.positions.len() >= 19 * 8);
819 let leg_a = joint_world_translation(&character, "walk", 0.0, "LeftLowerLeg").unwrap();
820 let leg_b = joint_world_translation(&character, "walk", 0.25, "LeftLowerLeg").unwrap();
821 assert!(
822 distance(leg_a, leg_b) > 0.05,
823 "walk should move the lower leg, delta {}",
824 distance(leg_a, leg_b)
825 );
826 let arm_a = joint_world_translation(&character, "attack", 0.0, "RightHand").unwrap();
827 let arm_b = joint_world_translation(&character, "attack", 0.35, "RightHand").unwrap();
828 assert!(
829 distance(arm_a, arm_b) > 0.05,
830 "attack should move the right hand, delta {}",
831 distance(arm_a, arm_b)
832 );
833 let matrices = sample_skin_matrices(&character, "walk", 0.2);
834 assert_eq!(matrices.len(), 19);
835 assert!(matrices.iter().flatten().all(|value| value.is_finite()));
836 }
837
838 #[test]
839 fn female_fixture_has_the_same_clip_names() {
840 let character = load_gltf_file(
841 &Path::new(env!("CARGO_MANIFEST_DIR"))
842 .join("tests/fixtures/bodies/test-body-female.glb"),
843 )
844 .unwrap();
845 assert!(character.clips.iter().any(|clip| clip.name == "walk"));
846 assert!(character.clips.iter().any(|clip| clip.name == "attack"));
847 let a = joint_world_translation(&character, "walk", 0.0, "RightLowerLeg").unwrap();
848 let b = joint_world_translation(&character, "walk", 0.5, "RightLowerLeg").unwrap();
849 assert!(distance(a, b) > 0.05);
850 }
851
852 #[test]
853 fn yaw_zero_is_in_front_and_the_twelve_yaws_differ() {
854 let front = eye_offset(0.0, 0.0, 4.0);
855 assert!(front[2] < -3.5, "yaw 0 camera belongs on -Z, got {front:?}");
856 assert!(front[0].abs() < 0.05);
857 let mut seen = Vec::new();
858 for yaw in paperdoll_yaws_deg() {
859 let eye = eye_offset(yaw, 0.0, 4.0);
860 assert!(seen
861 .iter()
862 .all(|previous: &[f32; 3]| distance(*previous, eye) > 0.5));
863 seen.push(eye);
864 }
865 assert_eq!(seen.len(), 12);
866 let low = eye_offset(0.0, -30.0, 4.0);
867 let high = eye_offset(0.0, 30.0, 4.0);
868 assert!(low[1] < 0.0 && high[1] > 0.0);
869 assert_eq!(PAPERDOLL_PITCHES_DEG.len(), 3);
870 }
871
872 #[test]
873 fn controls_snap_wrap_and_keep_a_clip_change() {
874 let mut controls = PreviewControls::default();
875 controls.drag(-100.0, 40.0);
876 assert!(controls.yaw_deg > 40.0);
877 assert!(controls.pitch_deg > 10.0);
878 controls.zoom(400.0);
879 assert!(controls.distance < 3.4);
880 controls.snap_yaw_index(-1);
881 assert!((controls.yaw_deg - 330.0).abs() < 0.01);
882 controls.snap_pitch_row(99);
883 assert!((controls.pitch_deg - 30.0).abs() < 0.01);
884 controls.snap_pitch_row(-4);
885 assert!((controls.pitch_deg + 30.0).abs() < 0.01);
886 controls.time = 0.4;
887 controls.select_clip("attack");
888 assert_eq!(controls.clip_name, "attack");
889 assert_eq!(controls.time, 0.0);
890 controls.time = 1.2;
891 controls.advance(0.0, 1.0);
892 controls.playing = true;
893 controls.advance(0.3, 1.0);
894 assert!((controls.time - 0.5).abs() < 0.02 || (wrap_time(1.5, 1.0) - 0.5).abs() < 0.02);
895 }
896}