BlueGit

golden-age paperdoll-renderer

Files

paperdoll-renderer / src / preview.rs

preview.rs

preview.rs 28.8 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
396/// Rest-world joint transform, never selected by a possibly empty clip name.
397/// Bounded parent walk rejects malformed cycles/indices instead of recursing.
398pub fn rest_joint_world(character: &PreviewCharacter, joint: usize) -> Option<Mat4> {
399 let mut node = Some(*character.joint_nodes.get(joint)?);
400 let mut chain = Vec::new();
401 while let Some(i) = node {
402 if chain.len() >= character.nodes.len() {
403 return None;
404 }
405 let n = character.nodes.get(i)?;
406 chain.push(i);
407 node = n.parent;
408 }
409 let mut world = identity();
410 for i in chain.into_iter().rev() {
411 let n = &character.nodes[i];
412 world = mul(world, trs(n.translation, n.rotation, n.scale));
413 }
414 world.iter().all(|n| n.is_finite()).then_some(world)
415}
416
417pub fn animated_joint_world(
418 character: &PreviewCharacter,
419 clip: &str,
420 time: f32,
421 joint: usize,
422) -> Option<Mat4> {
423 let locals = sample_local_matrices(character, clip, time);
424 let worlds = world_matrices(&character.nodes, &locals);
425 character
426 .joint_nodes
427 .get(joint)
428 .and_then(|node| worlds.get(*node))
429 .copied()
430}
431
432pub fn joint_world_translation(
433 character: &PreviewCharacter,
434 clip_name: &str,
435 time: f32,
436 joint_name: &str,
437) -> Option<[f32; 3]> {
438 let joint = character
439 .joint_names
440 .iter()
441 .position(|name| name == joint_name)?;
442 let node = character.joint_nodes[joint];
443 let locals = sample_local_matrices(character, clip_name, time);
444 let worlds = world_matrices(&character.nodes, &locals);
445 Some([worlds[node][12], worlds[node][13], worlds[node][14]])
446}
447
448pub fn clip_duration(character: &PreviewCharacter, clip_name: &str) -> f32 {
449 character
450 .clips
451 .iter()
452 .find(|clip| {
453 clip.name == clip_name || clip.name.to_lowercase().contains(&clip_name.to_lowercase())
454 })
455 .map(|clip| clip.duration)
456 .unwrap_or(0.0)
457}
458
459pub fn eye_offset(yaw_deg: f32, pitch_deg: f32, distance: f32) -> [f32; 3] {
460 let yaw = yaw_deg.to_radians();
461 let pitch = pitch_deg.to_radians();
462 [
463 -distance * pitch.cos() * yaw.sin(),
464 distance * pitch.sin(),
465 -distance * pitch.cos() * yaw.cos(),
466 ]
467}
468
469pub fn paperdoll_yaws_deg() -> [f32; PAPERDOLL_YAW_COUNT] {
470 let mut yaws = [0.0; PAPERDOLL_YAW_COUNT];
471 for (index, yaw) in yaws.iter_mut().enumerate() {
472 *yaw = index as f32 * 30.0;
473 }
474 yaws
475}
476
477fn sample_local_matrices(character: &PreviewCharacter, clip_name: &str, time: f32) -> Vec<Mat4> {
478 let mut translation: Vec<[f32; 3]> = character
479 .nodes
480 .iter()
481 .map(|node| node.translation)
482 .collect();
483 let mut rotation: Vec<[f32; 4]> = character.nodes.iter().map(|node| node.rotation).collect();
484 let mut scale: Vec<[f32; 3]> = character.nodes.iter().map(|node| node.scale).collect();
485 if let Some(clip) = character.clips.iter().find(|clip| {
486 clip.name == clip_name || clip.name.to_lowercase().contains(&clip_name.to_lowercase())
487 }) {
488 let time = wrap_time(time, clip.duration);
489 for channel in &clip.channels {
490 if channel.node >= character.nodes.len() || channel.times.is_empty() {
491 continue;
492 }
493 let (left, right, blend) = key_blend(&channel.times, time);
494 match channel.property {
495 ChannelProperty::Translation => {
496 translation[channel.node] = lerp3(
497 key3(&channel.values, left),
498 key3(&channel.values, right),
499 blend,
500 );
501 }
502 ChannelProperty::Scale => {
503 scale[channel.node] = lerp3(
504 key3(&channel.values, left),
505 key3(&channel.values, right),
506 blend,
507 );
508 }
509 ChannelProperty::Rotation => {
510 rotation[channel.node] = slerp(
511 key4(&channel.values, left),
512 key4(&channel.values, right),
513 blend,
514 );
515 }
516 }
517 }
518 }
519 translation
520 .into_iter()
521 .zip(rotation)
522 .zip(scale)
523 .map(|((translation, rotation), scale)| trs(translation, rotation, scale))
524 .collect()
525}
526
527fn world_matrices(nodes: &[SceneNode], locals: &[Mat4]) -> Vec<Mat4> {
528 fn world(
529 index: usize,
530 nodes: &[SceneNode],
531 locals: &[Mat4],
532 worlds: &mut [Option<Mat4>],
533 depth: usize,
534 ) -> Mat4 {
535 if let Some(matrix) = worlds[index] {
536 return matrix;
537 }
538 if depth > nodes.len() {
539 return locals[index];
540 }
541 let matrix = match nodes[index].parent {
542 Some(parent) => mul(
543 world(parent, nodes, locals, worlds, depth + 1),
544 locals[index],
545 ),
546 None => locals[index],
547 };
548 worlds[index] = Some(matrix);
549 matrix
550 }
551 let mut cached = vec![None; nodes.len()];
552 (0..nodes.len())
553 .map(|index| world(index, nodes, locals, &mut cached, 0))
554 .collect()
555}
556
557fn key_blend(times: &[f32], time: f32) -> (usize, usize, f32) {
558 if times.len() == 1 || time <= times[0] {
559 return (0, 0, 0.0);
560 }
561 if time >= times[times.len() - 1] {
562 let last = times.len() - 1;
563 return (last, last, 0.0);
564 }
565 let mut index = 0;
566 while index + 1 < times.len() && times[index + 1] < time {
567 index += 1;
568 }
569 let span = times[index + 1] - times[index];
570 let blend = if span.abs() < 1e-6 {
571 0.0
572 } else {
573 (time - times[index]) / span
574 };
575 (index, index + 1, blend)
576}
577
578fn key3(values: &[f32], index: usize) -> [f32; 3] {
579 let start = index * 3;
580 [
581 values.get(start).copied().unwrap_or(0.0),
582 values.get(start + 1).copied().unwrap_or(0.0),
583 values.get(start + 2).copied().unwrap_or(0.0),
584 ]
585}
586
587fn key4(values: &[f32], index: usize) -> [f32; 4] {
588 let start = index * 4;
589 [
590 values.get(start).copied().unwrap_or(0.0),
591 values.get(start + 1).copied().unwrap_or(0.0),
592 values.get(start + 2).copied().unwrap_or(0.0),
593 values.get(start + 3).copied().unwrap_or(1.0),
594 ]
595}
596
597fn lerp3(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
598 [
599 a[0] + (b[0] - a[0]) * t,
600 a[1] + (b[1] - a[1]) * t,
601 a[2] + (b[2] - a[2]) * t,
602 ]
603}
604
605fn slerp(a: [f32; 4], mut b: [f32; 4], t: f32) -> [f32; 4] {
606 let mut dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
607 if dot < 0.0 {
608 dot = -dot;
609 b = [-b[0], -b[1], -b[2], -b[3]];
610 }
611 if dot > 0.9995 {
612 return normalize4([
613 a[0] + (b[0] - a[0]) * t,
614 a[1] + (b[1] - a[1]) * t,
615 a[2] + (b[2] - a[2]) * t,
616 a[3] + (b[3] - a[3]) * t,
617 ]);
618 }
619 let theta = dot.clamp(-1.0, 1.0).acos();
620 let sin_theta = theta.sin();
621 let w1 = ((1.0 - t) * theta).sin() / sin_theta;
622 let w2 = (t * theta).sin() / sin_theta;
623 [
624 a[0] * w1 + b[0] * w2,
625 a[1] * w1 + b[1] * w2,
626 a[2] * w1 + b[2] * w2,
627 a[3] * w1 + b[3] * w2,
628 ]
629}
630
631fn normalize4(value: [f32; 4]) -> [f32; 4] {
632 let length =
633 (value[0] * value[0] + value[1] * value[1] + value[2] * value[2] + value[3] * value[3])
634 .sqrt();
635 if length < 1e-8 {
636 [0.0, 0.0, 0.0, 1.0]
637 } else {
638 [
639 value[0] / length,
640 value[1] / length,
641 value[2] / length,
642 value[3] / length,
643 ]
644 }
645}
646
647fn trs(translation: [f32; 3], rotation: [f32; 4], scale: [f32; 3]) -> Mat4 {
648 let mut matrix = quat_to_mat(normalize4(rotation));
649 matrix[0] *= scale[0];
650 matrix[1] *= scale[0];
651 matrix[2] *= scale[0];
652 matrix[4] *= scale[1];
653 matrix[5] *= scale[1];
654 matrix[6] *= scale[1];
655 matrix[8] *= scale[2];
656 matrix[9] *= scale[2];
657 matrix[10] *= scale[2];
658 matrix[12] = translation[0];
659 matrix[13] = translation[1];
660 matrix[14] = translation[2];
661 matrix
662}
663
664fn quat_to_mat(q: [f32; 4]) -> Mat4 {
665 let (x, y, z, w) = (q[0], q[1], q[2], q[3]);
666 let xx = x * x;
667 let yy = y * y;
668 let zz = z * z;
669 let xy = x * y;
670 let xz = x * z;
671 let yz = y * z;
672 let wx = w * x;
673 let wy = w * y;
674 let wz = w * z;
675 [
676 1.0 - 2.0 * (yy + zz),
677 2.0 * (xy + wz),
678 2.0 * (xz - wy),
679 0.0,
680 2.0 * (xy - wz),
681 1.0 - 2.0 * (xx + zz),
682 2.0 * (yz + wx),
683 0.0,
684 2.0 * (xz + wy),
685 2.0 * (yz - wx),
686 1.0 - 2.0 * (xx + yy),
687 0.0,
688 0.0,
689 0.0,
690 0.0,
691 1.0,
692 ]
693}
694
695pub fn identity() -> Mat4 {
696 [
697 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,
698 ]
699}
700
701pub fn mul(left: Mat4, right: Mat4) -> Mat4 {
702 let mut out = [0.0; 16];
703 for column in 0..4 {
704 for row in 0..4 {
705 out[column * 4 + row] = left[row] * right[column * 4]
706 + left[4 + row] * right[column * 4 + 1]
707 + left[8 + row] * right[column * 4 + 2]
708 + left[12 + row] * right[column * 4 + 3];
709 }
710 }
711 out
712}
713
714fn columns_to_mat(columns: [[f32; 4]; 4]) -> Mat4 {
715 let mut matrix = [0.0; 16];
716 for column in 0..4 {
717 for row in 0..4 {
718 matrix[column * 4 + row] = columns[column][row];
719 }
720 }
721 matrix
722}
723
724pub fn wrap_time(time: f32, duration: f32) -> f32 {
725 if duration <= 1e-5 {
726 return 0.0;
727 }
728 let mut wrapped = time % duration;
729 if wrapped < 0.0 {
730 wrapped += duration;
731 }
732 wrapped
733}
734
735fn wrap_deg(mut degrees: f32) -> f32 {
736 degrees %= 360.0;
737 if degrees < 0.0 {
738 degrees += 360.0;
739 }
740 degrees
741}
742
743pub fn view_matrix(eye: [f32; 3], target: [f32; 3]) -> Mat4 {
744 let forward = normalize3([target[0] - eye[0], target[1] - eye[1], target[2] - eye[2]]);
745 let up = [0.0, 1.0, 0.0];
746 let mut side = normalize3(cross(forward, up));
747 if side[0].abs() + side[1].abs() + side[2].abs() < 1e-4 {
748 side = [1.0, 0.0, 0.0];
749 }
750 let up = cross(side, forward);
751 [
752 side[0],
753 up[0],
754 -forward[0],
755 0.0,
756 side[1],
757 up[1],
758 -forward[1],
759 0.0,
760 side[2],
761 up[2],
762 -forward[2],
763 0.0,
764 -dot(side, eye),
765 -dot(up, eye),
766 dot(forward, eye),
767 1.0,
768 ]
769}
770
771pub fn perspective(fov_y_deg: f32, aspect: f32, near: f32, far: f32) -> Mat4 {
772 let f = 1.0 / (fov_y_deg.to_radians() * 0.5).tan();
773 let aspect = if aspect.abs() < 1e-4 { 1.0 } else { aspect };
774 let nf = 1.0 / (near - far);
775 [
776 f / aspect,
777 0.0,
778 0.0,
779 0.0,
780 0.0,
781 f,
782 0.0,
783 0.0,
784 0.0,
785 0.0,
786 (far + near) * nf,
787 -1.0,
788 0.0,
789 0.0,
790 2.0 * far * near * nf,
791 0.0,
792 ]
793}
794
795fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
796 [
797 a[1] * b[2] - a[2] * b[1],
798 a[2] * b[0] - a[0] * b[2],
799 a[0] * b[1] - a[1] * b[0],
800 ]
801}
802
803fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
804 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
805}
806
807fn normalize3(value: [f32; 3]) -> [f32; 3] {
808 let length = dot(value, value).sqrt();
809 if length < 1e-8 {
810 [0.0, 0.0, 1.0]
811 } else {
812 [value[0] / length, value[1] / length, value[2] / length]
813 }
814}
815
816#[cfg(test)]
817mod tests {
818 use super::*;
819
820 fn male() -> PreviewCharacter {
821 load_gltf_file(
822 &Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/bodies/test-body-male.glb"),
823 )
824 .unwrap()
825 }
826
827 fn distance(a: [f32; 3], b: [f32; 3]) -> f32 {
828 let d = [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
829 dot(d, d).sqrt()
830 }
831
832 #[test]
833 fn walk_and_attack_move_the_matching_bones() {
834 let character = male();
835 assert!(character.clips.iter().any(|clip| clip.name == "walk"));
836 assert!(character.clips.iter().any(|clip| clip.name == "attack"));
837 assert_eq!(character.joint_names.len(), 19);
838 assert!(character.indices.len() >= 3);
839 assert!(character.positions.len() >= 19 * 8);
840 let leg_a = joint_world_translation(&character, "walk", 0.0, "LeftLowerLeg").unwrap();
841 let leg_b = joint_world_translation(&character, "walk", 0.25, "LeftLowerLeg").unwrap();
842 assert!(
843 distance(leg_a, leg_b) > 0.05,
844 "walk should move the lower leg, delta {}",
845 distance(leg_a, leg_b)
846 );
847 let arm_a = joint_world_translation(&character, "attack", 0.0, "RightHand").unwrap();
848 let arm_b = joint_world_translation(&character, "attack", 0.35, "RightHand").unwrap();
849 assert!(
850 distance(arm_a, arm_b) > 0.05,
851 "attack should move the right hand, delta {}",
852 distance(arm_a, arm_b)
853 );
854 let matrices = sample_skin_matrices(&character, "walk", 0.2);
855 assert_eq!(matrices.len(), 19);
856 assert!(matrices.iter().flatten().all(|value| value.is_finite()));
857 }
858
859 #[test]
860 fn female_fixture_has_the_same_clip_names() {
861 let character = load_gltf_file(
862 &Path::new(env!("CARGO_MANIFEST_DIR"))
863 .join("tests/fixtures/bodies/test-body-female.glb"),
864 )
865 .unwrap();
866 assert!(character.clips.iter().any(|clip| clip.name == "walk"));
867 assert!(character.clips.iter().any(|clip| clip.name == "attack"));
868 let a = joint_world_translation(&character, "walk", 0.0, "RightLowerLeg").unwrap();
869 let b = joint_world_translation(&character, "walk", 0.5, "RightLowerLeg").unwrap();
870 assert!(distance(a, b) > 0.05);
871 }
872
873 #[test]
874 fn yaw_zero_is_in_front_and_the_twelve_yaws_differ() {
875 let front = eye_offset(0.0, 0.0, 4.0);
876 assert!(front[2] < -3.5, "yaw 0 camera belongs on -Z, got {front:?}");
877 assert!(front[0].abs() < 0.05);
878 let mut seen = Vec::new();
879 for yaw in paperdoll_yaws_deg() {
880 let eye = eye_offset(yaw, 0.0, 4.0);
881 assert!(seen
882 .iter()
883 .all(|previous: &[f32; 3]| distance(*previous, eye) > 0.5));
884 seen.push(eye);
885 }
886 assert_eq!(seen.len(), 12);
887 let low = eye_offset(0.0, -30.0, 4.0);
888 let high = eye_offset(0.0, 30.0, 4.0);
889 assert!(low[1] < 0.0 && high[1] > 0.0);
890 assert_eq!(PAPERDOLL_PITCHES_DEG.len(), 3);
891 }
892
893 #[test]
894 fn controls_snap_wrap_and_keep_a_clip_change() {
895 let mut controls = PreviewControls::default();
896 controls.drag(-100.0, 40.0);
897 assert!(controls.yaw_deg > 40.0);
898 assert!(controls.pitch_deg > 10.0);
899 controls.zoom(400.0);
900 assert!(controls.distance < 3.4);
901 controls.snap_yaw_index(-1);
902 assert!((controls.yaw_deg - 330.0).abs() < 0.01);
903 controls.snap_pitch_row(99);
904 assert!((controls.pitch_deg - 30.0).abs() < 0.01);
905 controls.snap_pitch_row(-4);
906 assert!((controls.pitch_deg + 30.0).abs() < 0.01);
907 controls.time = 0.4;
908 controls.select_clip("attack");
909 assert_eq!(controls.clip_name, "attack");
910 assert_eq!(controls.time, 0.0);
911 controls.time = 1.2;
912 controls.advance(0.0, 1.0);
913 controls.playing = true;
914 controls.advance(0.3, 1.0);
915 assert!((controls.time - 0.5).abs() < 0.02 || (wrap_time(1.5, 1.0) - 0.5).abs() < 0.02);
916 }
917}