1use glam::{Mat3, Mat4, Quat, Vec3, Vec3A};
2use thiserror::Error;
3
4#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
5pub struct BoneIndex(pub u32);
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
8pub struct MorphIndex(pub u32);
9
10impl MorphIndex {
11 #[inline]
12 pub fn as_usize(self) -> usize {
13 self.0 as usize
14 }
15}
16
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct BoneMorphOffset {
19 pub target_bone: BoneIndex,
20 pub position_offset: Vec3A,
21 pub rotation_offset: Quat,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq)]
25pub struct VertexMorphOffset {
26 pub vertex_index: u32,
27 pub position_offset: Vec3A,
28}
29
30#[derive(Clone, Copy, Debug, PartialEq)]
31pub struct GroupMorphOffset {
32 pub child_morph: MorphIndex,
33 pub ratio: f32,
34}
35
36#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
37pub struct MorphOffsetSpan {
38 pub start: u32,
39 pub count: u32,
40}
41
42#[derive(Clone, Debug, Default)]
43pub struct MorphInit {
44 pub morph_count: u32,
45 pub vertex_offsets: Vec<VertexMorphOffset>,
46 pub vertex_spans: Vec<MorphOffsetSpan>,
47 pub bone_offsets: Vec<BoneMorphOffset>,
48 pub bone_spans: Vec<MorphOffsetSpan>,
49 pub group_offsets: Vec<GroupMorphOffset>,
50 pub group_spans: Vec<MorphOffsetSpan>,
51}
52
53impl BoneIndex {
54 #[inline]
55 pub fn as_usize(self) -> usize {
56 self.0 as usize
57 }
58}
59
60#[derive(Clone, Copy, Debug, PartialEq)]
66pub struct LocalAxis {
67 pub x: Vec3A,
68 pub z: Vec3A,
69}
70
71impl LocalAxis {
72 pub fn new(x: Vec3A, z: Vec3A) -> Self {
73 Self { x, z }
74 }
75
76 pub fn basis_quat(self) -> Option<Quat> {
82 build_local_axis_basis_quat(self.x, self.z)
83 }
84}
85
86#[derive(Clone, Debug)]
87pub struct BoneInit {
88 pub parent: Option<BoneIndex>,
89 pub rest_position: Vec3A,
90 pub inverse_bind_matrix: Mat4,
91 pub transform_order: i32,
92 pub transform_after_physics: bool,
93 pub fixed_axis: Option<Vec3A>,
94 pub enforce_fixed_axis: bool,
99}
100
101impl BoneInit {
102 pub fn new(parent: Option<BoneIndex>, rest_position: Vec3A) -> Self {
103 Self {
104 parent,
105 rest_position,
106 inverse_bind_matrix: Mat4::IDENTITY,
107 transform_order: 0,
108 transform_after_physics: false,
109 fixed_axis: None,
110 enforce_fixed_axis: false,
111 }
112 }
113
114 pub fn with_fixed_axis(mut self, axis: Vec3A) -> Self {
115 self.fixed_axis = Some(axis);
116 self.enforce_fixed_axis = true;
117 self
118 }
119}
120
121fn build_local_axis_basis_quat(x: Vec3A, z: Vec3A) -> Option<Quat> {
126 if !x.is_finite() || !z.is_finite() {
127 return None;
128 }
129 let x_len_sq = x.length_squared();
130 if x_len_sq <= f32::EPSILON {
131 return None;
132 }
133 let x_n = x * x_len_sq.sqrt().recip();
134 let y = z.cross(x_n);
135 let y_len_sq = y.length_squared();
136 if y_len_sq <= f32::EPSILON {
137 return None;
138 }
139 let y_n = y * y_len_sq.sqrt().recip();
140 let z_n = x_n.cross(y_n);
141 let z_len_sq = z_n.length_squared();
142 if z_len_sq <= f32::EPSILON {
143 return None;
144 }
145 let z_n = z_n * z_len_sq.sqrt().recip();
146 if !x_n.is_finite() || !y_n.is_finite() || !z_n.is_finite() {
147 return None;
148 }
149 let mat = Mat3::from_cols(Vec3::from(x_n), Vec3::from(y_n), Vec3::from(z_n));
150 let quat = Quat::from_mat3(&mat);
151 if !quat.is_finite() || quat.length_squared() <= f32::EPSILON {
152 return None;
153 }
154 Some(quat.normalize())
155}
156
157#[derive(Clone, Copy, Debug, PartialEq)]
158pub struct IkAngleLimit {
159 pub min: Vec3A,
160 pub max: Vec3A,
161}
162
163impl IkAngleLimit {
164 pub fn new(min: Vec3A, max: Vec3A) -> Self {
165 Self { min, max }
166 }
167}
168
169#[derive(Clone, Debug, PartialEq)]
170pub struct IkLinkInit {
171 pub bone: BoneIndex,
172 pub angle_limit: Option<IkAngleLimit>,
173}
174
175impl IkLinkInit {
176 pub fn new(bone: BoneIndex) -> Self {
177 Self {
178 bone,
179 angle_limit: None,
180 }
181 }
182
183 pub fn with_angle_limit(mut self, angle_limit: IkAngleLimit) -> Self {
184 self.angle_limit = Some(angle_limit);
185 self
186 }
187}
188
189#[derive(Clone, Debug, PartialEq)]
190pub struct IkSolverInit {
191 pub ik_bone: BoneIndex,
192 pub target_bone: BoneIndex,
193 pub links: Vec<IkLinkInit>,
194 pub iteration_count: u32,
195 pub limit_angle: f32,
196}
197
198#[derive(Clone, Debug, PartialEq)]
199pub struct AppendTransformInit {
200 pub target_bone: BoneIndex,
201 pub source_bone: BoneIndex,
202 pub ratio: f32,
203 pub affect_rotation: bool,
204 pub affect_translation: bool,
205 pub local: bool,
206}
207
208impl AppendTransformInit {
209 pub fn new(target_bone: BoneIndex, source_bone: BoneIndex, ratio: f32) -> Self {
210 Self {
211 target_bone,
212 source_bone,
213 ratio,
214 affect_rotation: false,
215 affect_translation: false,
216 local: false,
217 }
218 }
219
220 pub fn with_rotation(mut self) -> Self {
221 self.affect_rotation = true;
222 self
223 }
224
225 pub fn with_translation(mut self) -> Self {
226 self.affect_translation = true;
227 self
228 }
229
230 pub fn with_local(mut self) -> Self {
231 self.local = true;
232 self
233 }
234}
235
236impl IkSolverInit {
237 pub fn new(ik_bone: BoneIndex, target_bone: BoneIndex, links: Vec<IkLinkInit>) -> Self {
238 Self {
239 ik_bone,
240 target_bone,
241 links,
242 iteration_count: 1,
243 limit_angle: 0.0,
244 }
245 }
246}
247
248#[derive(Debug, Error, PartialEq, Eq)]
249pub enum ModelBuildError {
250 #[error("model must contain at least one bone")]
251 EmptyModel,
252 #[error("bone {bone} references invalid parent {parent}")]
253 InvalidParent { bone: usize, parent: u32 },
254 #[error("bone hierarchy contains a cycle involving bone {bone}")]
255 ParentCycle { bone: usize },
256 #[error("ik solver {solver} references invalid {role} bone {bone}")]
257 InvalidIkBone {
258 solver: usize,
259 role: &'static str,
260 bone: u32,
261 },
262 #[error("append transform {append} references invalid {role} bone {bone}")]
263 InvalidAppendBone {
264 append: usize,
265 role: &'static str,
266 bone: u32,
267 },
268 #[error("bone {bone} has more than one append transform")]
269 DuplicateAppendTransform { bone: u32 },
270 #[error("bone morph offset {offset} references invalid target bone {bone}")]
271 InvalidBoneMorphBone { offset: usize, bone: u32 },
272 #[error("morph span list has length {actual}, expected {expected}")]
273 InvalidMorphSpanCount { actual: usize, expected: usize },
274 #[error("morph {morph} has invalid {kind} offset span")]
275 InvalidMorphSpan { morph: usize, kind: &'static str },
276 #[error("group morph {morph} references invalid child morph {child}")]
277 InvalidGroupMorphChild { morph: usize, child: u32 },
278 #[error("group morph cycle detected at morph {morph}")]
279 GroupMorphCycle { morph: usize },
280}
281
282#[derive(Debug)]
283pub struct ModelArena {
284 parent_indices: Box<[i32]>,
285 rest_positions: Box<[Vec3A]>,
286 inverse_bind_matrices: Box<[Mat4]>,
287 transform_orders: Box<[i32]>,
288 fixed_axis_flags: Box<[u8]>,
289 fixed_axis_constraint_flags: Box<[u8]>,
290 fixed_axes: Box<[Vec3A]>,
291 local_axis_flags: Box<[u8]>,
292 local_axis_x: Box<[Vec3A]>,
293 local_axis_z: Box<[Vec3A]>,
294 local_axis_basis: Box<[Quat]>,
295 transform_after_physics_flags: Box<[u8]>,
296 ik_link_flags: Box<[u8]>,
297 eval_order: Box<[BoneIndex]>,
298 eval_order_before_physics: Box<[BoneIndex]>,
299 eval_order_after_physics: Box<[BoneIndex]>,
300 eval_order_positions: Box<[usize]>,
301 ik_bone_solver_spans: Box<[MorphOffsetSpan]>,
302 ik_bone_solver_indices: Box<[u32]>,
303 ik_solvers: Box<[IkSolver]>,
304 append_transforms: Box<[AppendTransform]>,
305 append_transform_indices: Box<[i32]>,
306 morph_count: u32,
307 vertex_morph_offsets: Box<[VertexMorphOffset]>,
308 vertex_morph_spans: Box<[MorphOffsetSpan]>,
309 bone_morph_offsets: Box<[BoneMorphOffset]>,
310 bone_morph_spans: Box<[MorphOffsetSpan]>,
311 group_morph_offsets: Box<[GroupMorphOffset]>,
312 group_morph_spans: Box<[MorphOffsetSpan]>,
313}
314
315impl ModelArena {
316 pub fn new(bones: Vec<BoneInit>) -> Result<Self, ModelBuildError> {
317 Self::new_full(bones, Vec::new(), Vec::new())
318 }
319
320 pub fn new_with_ik(
321 bones: Vec<BoneInit>,
322 ik_solvers: Vec<IkSolverInit>,
323 ) -> Result<Self, ModelBuildError> {
324 Self::new_full(bones, ik_solvers, Vec::new())
325 }
326
327 pub fn new_full(
328 bones: Vec<BoneInit>,
329 ik_solvers: Vec<IkSolverInit>,
330 append_transforms: Vec<AppendTransformInit>,
331 ) -> Result<Self, ModelBuildError> {
332 Self::new_with_morphs(bones, ik_solvers, append_transforms, MorphInit::default())
333 }
334
335 pub fn new_with_morphs(
336 bones: Vec<BoneInit>,
337 ik_solvers: Vec<IkSolverInit>,
338 append_transforms: Vec<AppendTransformInit>,
339 morph: MorphInit,
340 ) -> Result<Self, ModelBuildError> {
341 if bones.is_empty() {
342 return Err(ModelBuildError::EmptyModel);
343 }
344
345 let bone_count = bones.len();
346 let mut parent_indices = Vec::with_capacity(bone_count);
347 let mut rest_positions = Vec::with_capacity(bone_count);
348 let mut inverse_bind_matrices = Vec::with_capacity(bone_count);
349 let mut transform_orders = Vec::with_capacity(bone_count);
350 let mut transform_after_physics_flags = Vec::with_capacity(bone_count);
351 let mut fixed_axis_flags = Vec::with_capacity(bone_count);
352 let mut fixed_axis_constraint_flags = Vec::with_capacity(bone_count);
353 let mut fixed_axes = Vec::with_capacity(bone_count);
354 let mut local_axis_flags = Vec::with_capacity(bone_count);
355 let mut local_axis_x = Vec::with_capacity(bone_count);
356 let mut local_axis_z = Vec::with_capacity(bone_count);
357 let mut local_axis_basis = Vec::with_capacity(bone_count);
358
359 for (bone_index, bone) in bones.iter().enumerate() {
360 let parent = match bone.parent {
361 Some(parent) if parent.as_usize() < bone_count => parent.0 as i32,
362 Some(parent) => {
363 return Err(ModelBuildError::InvalidParent {
364 bone: bone_index,
365 parent: parent.0,
366 });
367 }
368 None => -1,
369 };
370
371 parent_indices.push(parent);
372 rest_positions.push(bone.rest_position);
373 inverse_bind_matrices.push(bone.inverse_bind_matrix);
374 transform_orders.push(bone.transform_order);
375 transform_after_physics_flags.push(u8::from(bone.transform_after_physics));
376 match bone.fixed_axis {
377 Some(axis) if axis.length_squared() > f32::EPSILON && axis.is_finite() => {
378 fixed_axis_flags.push(1);
379 fixed_axis_constraint_flags.push(u8::from(bone.enforce_fixed_axis));
380 fixed_axes.push(axis.normalize());
381 }
382 _ => {
383 fixed_axis_flags.push(0);
384 fixed_axis_constraint_flags.push(0);
385 fixed_axes.push(Vec3A::X);
386 }
387 }
388 local_axis_flags.push(0);
391 local_axis_x.push(Vec3A::X);
392 local_axis_z.push(Vec3A::Z);
393 local_axis_basis.push(Quat::IDENTITY);
394 }
395
396 let eval_order = build_eval_order(&parent_indices, &transform_orders)?;
397 let eval_order_before_physics =
398 build_eval_order_for_phase(&eval_order, &transform_after_physics_flags, false);
399 let eval_order_after_physics =
400 build_eval_order_for_phase(&eval_order, &transform_after_physics_flags, true);
401 let eval_order_positions = build_eval_order_positions(&eval_order, bone_count);
402 let (ik_solvers, ik_link_flags) = build_ik_solvers(ik_solvers, bone_count)?;
403 let (ik_bone_solver_spans, ik_bone_solver_indices) =
404 build_ik_bone_solver_lookup(&ik_solvers, bone_count);
405 let (append_transforms, append_transform_indices) =
406 build_append_transforms(append_transforms, bone_count)?;
407 validate_morph_init(&morph, bone_count)?;
408
409 Ok(Self {
410 parent_indices: parent_indices.into_boxed_slice(),
411 rest_positions: rest_positions.into_boxed_slice(),
412 inverse_bind_matrices: inverse_bind_matrices.into_boxed_slice(),
413 transform_orders: transform_orders.into_boxed_slice(),
414 fixed_axis_flags: fixed_axis_flags.into_boxed_slice(),
415 fixed_axis_constraint_flags: fixed_axis_constraint_flags.into_boxed_slice(),
416 fixed_axes: fixed_axes.into_boxed_slice(),
417 local_axis_flags: local_axis_flags.into_boxed_slice(),
418 local_axis_x: local_axis_x.into_boxed_slice(),
419 local_axis_z: local_axis_z.into_boxed_slice(),
420 local_axis_basis: local_axis_basis.into_boxed_slice(),
421 transform_after_physics_flags: transform_after_physics_flags.into_boxed_slice(),
422 ik_link_flags,
423 eval_order,
424 eval_order_before_physics,
425 eval_order_after_physics,
426 eval_order_positions,
427 ik_bone_solver_spans,
428 ik_bone_solver_indices,
429 ik_solvers,
430 append_transforms,
431 append_transform_indices,
432 morph_count: morph.morph_count,
433 vertex_morph_offsets: morph.vertex_offsets.into_boxed_slice(),
434 vertex_morph_spans: morph.vertex_spans.into_boxed_slice(),
435 bone_morph_offsets: morph.bone_offsets.into_boxed_slice(),
436 bone_morph_spans: morph.bone_spans.into_boxed_slice(),
437 group_morph_offsets: morph.group_offsets.into_boxed_slice(),
438 group_morph_spans: morph.group_spans.into_boxed_slice(),
439 })
440 }
441
442 #[inline]
443 pub fn bone_count(&self) -> usize {
444 self.parent_indices.len()
445 }
446
447 #[inline]
448 pub fn parent_index(&self, bone: BoneIndex) -> Option<BoneIndex> {
449 let parent = self.parent_indices[bone.as_usize()];
450 if parent < 0 {
451 None
452 } else {
453 Some(BoneIndex(parent as u32))
454 }
455 }
456
457 #[inline]
458 pub fn rest_position(&self, bone: BoneIndex) -> Vec3A {
459 self.rest_positions[bone.as_usize()]
460 }
461
462 #[inline]
463 pub fn inverse_bind_matrix(&self, bone: BoneIndex) -> Mat4 {
464 self.inverse_bind_matrices[bone.as_usize()]
465 }
466
467 #[inline]
468 pub fn transform_order(&self, bone: BoneIndex) -> i32 {
469 self.transform_orders[bone.as_usize()]
470 }
471
472 #[inline]
473 pub fn fixed_axis(&self, bone: BoneIndex) -> Option<Vec3A> {
474 if self.fixed_axis_flags[bone.as_usize()] != 0 {
475 Some(self.fixed_axes[bone.as_usize()])
476 } else {
477 None
478 }
479 }
480
481 #[inline]
482 pub(crate) fn fixed_axis_constraint(&self, bone: BoneIndex) -> Option<Vec3A> {
483 if self.fixed_axis_constraint_flags[bone.as_usize()] != 0 {
484 Some(self.fixed_axes[bone.as_usize()])
485 } else {
486 None
487 }
488 }
489
490 #[inline]
491 pub fn fixed_axis_count(&self) -> usize {
492 self.fixed_axis_flags
493 .iter()
494 .filter(|&&flag| flag != 0)
495 .count()
496 }
497
498 pub fn with_local_axes(
502 mut self,
503 local_axes: impl IntoIterator<Item = Option<LocalAxis>>,
504 ) -> Self {
505 for (bone_index, axis) in local_axes.into_iter().enumerate() {
506 if bone_index >= self.bone_count() {
507 break;
508 }
509 match axis.and_then(|axis| {
510 let basis = axis.basis_quat()?;
511 Some((axis.x, axis.z, basis))
512 }) {
513 Some((x, z, basis)) => {
514 self.local_axis_flags[bone_index] = 1;
515 self.local_axis_x[bone_index] = x;
516 self.local_axis_z[bone_index] = z;
517 self.local_axis_basis[bone_index] = basis;
518 }
519 None => {
520 self.local_axis_flags[bone_index] = 0;
521 self.local_axis_x[bone_index] = Vec3A::X;
522 self.local_axis_z[bone_index] = Vec3A::Z;
523 self.local_axis_basis[bone_index] = Quat::IDENTITY;
524 }
525 }
526 }
527 self
528 }
529
530 #[inline]
531 pub fn local_axis(&self, bone: BoneIndex) -> Option<LocalAxis> {
532 if self.local_axis_flags[bone.as_usize()] != 0 {
533 Some(LocalAxis {
534 x: self.local_axis_x[bone.as_usize()],
535 z: self.local_axis_z[bone.as_usize()],
536 })
537 } else {
538 None
539 }
540 }
541
542 #[inline]
545 pub fn local_axis_basis(&self, bone: BoneIndex) -> Option<Quat> {
546 if self.local_axis_flags[bone.as_usize()] != 0 {
547 Some(self.local_axis_basis[bone.as_usize()])
548 } else {
549 None
550 }
551 }
552
553 #[inline]
554 pub fn local_axis_count(&self) -> usize {
555 self.local_axis_flags
556 .iter()
557 .filter(|&&flag| flag != 0)
558 .count()
559 }
560
561 #[inline]
562 pub fn transform_after_physics(&self, bone: BoneIndex) -> bool {
563 self.transform_after_physics_flags[bone.as_usize()] != 0
564 }
565
566 #[inline]
567 pub(crate) fn is_ik_link_bone(&self, bone: BoneIndex) -> bool {
568 self.ik_link_flags[bone.as_usize()] != 0
569 }
570
571 #[inline]
572 pub fn eval_order(&self) -> &[BoneIndex] {
573 &self.eval_order
574 }
575
576 #[inline]
577 pub(crate) fn eval_order_for_phase(&self, after_physics: bool) -> &[BoneIndex] {
578 if after_physics {
579 &self.eval_order_after_physics
580 } else {
581 &self.eval_order_before_physics
582 }
583 }
584
585 #[inline]
586 pub(crate) fn eval_order_position(&self, bone: BoneIndex) -> usize {
587 self.eval_order_positions[bone.as_usize()]
588 }
589
590 #[inline]
591 pub fn ik_count(&self) -> usize {
592 self.ik_solvers.len()
593 }
594
595 #[inline]
596 pub fn ik_solvers(&self) -> &[IkSolver] {
597 &self.ik_solvers
598 }
599
600 #[inline]
601 pub(crate) fn ik_solver_count_for_bone(&self, bone: BoneIndex) -> usize {
602 self.ik_bone_solver_spans[bone.as_usize()].count as usize
603 }
604
605 #[inline]
606 pub(crate) fn ik_solver_index_for_bone(&self, bone: BoneIndex, local_index: usize) -> usize {
607 let span = &self.ik_bone_solver_spans[bone.as_usize()];
608 self.ik_bone_solver_indices[span.start as usize + local_index] as usize
609 }
610
611 #[inline]
612 pub fn append_transform_index(&self, bone: BoneIndex) -> Option<usize> {
613 let index = self.append_transform_indices[bone.as_usize()];
614 if index < 0 {
615 None
616 } else {
617 Some(index as usize)
618 }
619 }
620
621 #[inline]
622 pub fn append_transform(&self, append_index: usize) -> &AppendTransform {
623 &self.append_transforms[append_index]
624 }
625
626 #[inline]
627 pub fn append_transforms(&self) -> &[AppendTransform] {
628 &self.append_transforms
629 }
630
631 #[inline]
632 pub fn morph_count(&self) -> u32 {
633 self.morph_count
634 }
635
636 #[inline]
637 pub fn vertex_morph_offsets(&self) -> &[VertexMorphOffset] {
638 &self.vertex_morph_offsets
639 }
640
641 #[inline]
642 pub fn vertex_morph_spans(&self) -> &[MorphOffsetSpan] {
643 &self.vertex_morph_spans
644 }
645
646 #[inline]
647 pub fn bone_morph_offsets(&self) -> &[BoneMorphOffset] {
648 &self.bone_morph_offsets
649 }
650
651 #[inline]
652 pub fn bone_morph_spans(&self) -> &[MorphOffsetSpan] {
653 &self.bone_morph_spans
654 }
655
656 #[inline]
657 pub fn group_morph_offsets(&self) -> &[GroupMorphOffset] {
658 &self.group_morph_offsets
659 }
660
661 #[inline]
662 pub fn group_morph_spans(&self) -> &[MorphOffsetSpan] {
663 &self.group_morph_spans
664 }
665}
666
667#[derive(Clone, Debug, PartialEq)]
668pub struct IkSolver {
669 pub ik_bone: BoneIndex,
670 pub target_bone: BoneIndex,
671 pub links: Box<[IkLink]>,
672 pub iteration_count: u32,
673 pub limit_angle: f32,
674}
675
676#[derive(Clone, Debug, PartialEq)]
677pub struct IkLink {
678 pub bone: BoneIndex,
679 pub angle_limit: Option<IkAngleLimit>,
680}
681
682#[derive(Clone, Copy, Debug, PartialEq)]
683pub struct AppendTransform {
684 pub target_bone: BoneIndex,
685 pub source_bone: BoneIndex,
686 pub ratio: f32,
687 pub affect_rotation: bool,
688 pub affect_translation: bool,
689 pub local: bool,
690}
691
692type AppendTransformBuildOutput = (Box<[AppendTransform]>, Box<[i32]>);
693type IkSolverBuildOutput = (Box<[IkSolver]>, Box<[u8]>);
694
695fn build_ik_solvers(
696 ik_solvers: Vec<IkSolverInit>,
697 bone_count: usize,
698) -> Result<IkSolverBuildOutput, ModelBuildError> {
699 let mut solvers = Vec::with_capacity(ik_solvers.len());
700 let mut ik_link_flags = vec![0; bone_count];
701
702 for (solver_index, solver) in ik_solvers.into_iter().enumerate() {
703 validate_ik_bone(solver_index, "ik", solver.ik_bone, bone_count)?;
704 validate_ik_bone(solver_index, "target", solver.target_bone, bone_count)?;
705
706 let mut links = Vec::with_capacity(solver.links.len());
707 for link in solver.links {
708 validate_ik_bone(solver_index, "link", link.bone, bone_count)?;
709 ik_link_flags[link.bone.as_usize()] = 1;
710 links.push(IkLink {
711 bone: link.bone,
712 angle_limit: link.angle_limit,
713 });
714 }
715
716 solvers.push(IkSolver {
717 ik_bone: solver.ik_bone,
718 target_bone: solver.target_bone,
719 links: links.into_boxed_slice(),
720 iteration_count: solver.iteration_count,
721 limit_angle: solver.limit_angle,
722 });
723 }
724
725 Ok((solvers.into_boxed_slice(), ik_link_flags.into_boxed_slice()))
726}
727
728type IkBoneSolverLookup = (Box<[MorphOffsetSpan]>, Box<[u32]>);
729
730fn build_ik_bone_solver_lookup(ik_solvers: &[IkSolver], bone_count: usize) -> IkBoneSolverLookup {
731 let mut counts = vec![0u32; bone_count];
732 for solver in ik_solvers {
733 counts[solver.ik_bone.as_usize()] += 1;
734 }
735
736 let mut spans = Vec::with_capacity(bone_count);
737 let mut next_offset = 0u32;
738 for count in counts {
739 spans.push(MorphOffsetSpan {
740 start: next_offset,
741 count,
742 });
743 next_offset += count;
744 }
745
746 let mut indices = vec![0u32; ik_solvers.len()];
747 let mut write_positions: Vec<u32> = spans.iter().map(|span| span.start).collect();
748 for (solver_index, solver) in ik_solvers.iter().enumerate() {
749 let bone = solver.ik_bone.as_usize();
750 let write_index = write_positions[bone] as usize;
751 indices[write_index] = solver_index as u32;
752 write_positions[bone] += 1;
753 }
754
755 (spans.into_boxed_slice(), indices.into_boxed_slice())
756}
757
758fn validate_ik_bone(
759 solver: usize,
760 role: &'static str,
761 bone: BoneIndex,
762 bone_count: usize,
763) -> Result<(), ModelBuildError> {
764 if bone.as_usize() < bone_count {
765 Ok(())
766 } else {
767 Err(ModelBuildError::InvalidIkBone {
768 solver,
769 role,
770 bone: bone.0,
771 })
772 }
773}
774
775fn build_append_transforms(
776 append_transforms: Vec<AppendTransformInit>,
777 bone_count: usize,
778) -> Result<AppendTransformBuildOutput, ModelBuildError> {
779 let mut transforms = Vec::with_capacity(append_transforms.len());
780 let mut indices = vec![-1; bone_count];
781
782 for (append_index, append) in append_transforms.into_iter().enumerate() {
783 validate_append_bone(append_index, "target", append.target_bone, bone_count)?;
784 validate_append_bone(append_index, "source", append.source_bone, bone_count)?;
785
786 let target = append.target_bone.as_usize();
787 if indices[target] >= 0 {
788 return Err(ModelBuildError::DuplicateAppendTransform {
789 bone: append.target_bone.0,
790 });
791 }
792 indices[target] = append_index as i32;
793 transforms.push(AppendTransform {
794 target_bone: append.target_bone,
795 source_bone: append.source_bone,
796 ratio: append.ratio,
797 affect_rotation: append.affect_rotation,
798 affect_translation: append.affect_translation,
799 local: append.local,
800 });
801 }
802
803 Ok((transforms.into_boxed_slice(), indices.into_boxed_slice()))
804}
805
806fn validate_append_bone(
807 append: usize,
808 role: &'static str,
809 bone: BoneIndex,
810 bone_count: usize,
811) -> Result<(), ModelBuildError> {
812 if bone.as_usize() < bone_count {
813 Ok(())
814 } else {
815 Err(ModelBuildError::InvalidAppendBone {
816 append,
817 role,
818 bone: bone.0,
819 })
820 }
821}
822
823fn build_eval_order(
824 parent_indices: &[i32],
825 transform_orders: &[i32],
826) -> Result<Box<[BoneIndex]>, ModelBuildError> {
827 let mut state = vec![VisitState::Unvisited; parent_indices.len()];
828 let mut order = Vec::with_capacity(parent_indices.len());
829 let mut start_order = Vec::with_capacity(parent_indices.len());
830 for bone in 0..parent_indices.len() {
831 start_order.push(bone);
832 }
833 start_order.sort_by_key(|bone| (transform_orders[*bone], *bone));
834
835 for bone in start_order {
836 visit_bone(bone, parent_indices, &mut state, &mut order)?;
837 }
838
839 Ok(order.into_boxed_slice())
840}
841
842fn build_eval_order_for_phase(
843 eval_order: &[BoneIndex],
844 transform_after_physics_flags: &[u8],
845 after_physics: bool,
846) -> Box<[BoneIndex]> {
847 eval_order
848 .iter()
849 .copied()
850 .filter(|bone| {
851 let bone_after_physics = transform_after_physics_flags[bone.as_usize()] != 0;
852 bone_after_physics == after_physics
853 })
854 .collect()
855}
856
857fn build_eval_order_positions(eval_order: &[BoneIndex], bone_count: usize) -> Box<[usize]> {
858 let mut positions = vec![0; bone_count];
859 for (position, bone) in eval_order.iter().enumerate() {
860 positions[bone.as_usize()] = position;
861 }
862 positions.into_boxed_slice()
863}
864
865fn visit_bone(
866 bone: usize,
867 parent_indices: &[i32],
868 state: &mut [VisitState],
869 order: &mut Vec<BoneIndex>,
870) -> Result<(), ModelBuildError> {
871 match state[bone] {
872 VisitState::Visited => return Ok(()),
873 VisitState::Visiting => return Err(ModelBuildError::ParentCycle { bone }),
874 VisitState::Unvisited => {}
875 }
876
877 state[bone] = VisitState::Visiting;
878
879 let parent = parent_indices[bone];
880 if parent >= 0 {
881 visit_bone(parent as usize, parent_indices, state, order)?;
882 }
883
884 state[bone] = VisitState::Visited;
885 order.push(BoneIndex(bone as u32));
886 Ok(())
887}
888
889#[derive(Clone, Copy, Debug, PartialEq, Eq)]
890enum VisitState {
891 Unvisited,
892 Visiting,
893 Visited,
894}
895
896fn validate_morph_init(morph: &MorphInit, bone_count: usize) -> Result<(), ModelBuildError> {
897 let morph_count = morph.morph_count as usize;
898 if !morph.vertex_spans.is_empty() || !morph.vertex_offsets.is_empty() {
899 validate_morph_spans(
900 "vertex",
901 &morph.vertex_spans,
902 morph_count,
903 morph.vertex_offsets.len(),
904 )?;
905 }
906 validate_morph_spans(
907 "bone",
908 &morph.bone_spans,
909 morph_count,
910 morph.bone_offsets.len(),
911 )?;
912 validate_morph_spans(
913 "group",
914 &morph.group_spans,
915 morph_count,
916 morph.group_offsets.len(),
917 )?;
918
919 for (offset_index, offset) in morph.bone_offsets.iter().enumerate() {
920 if offset.target_bone.as_usize() >= bone_count {
921 return Err(ModelBuildError::InvalidBoneMorphBone {
922 offset: offset_index,
923 bone: offset.target_bone.0,
924 });
925 }
926 }
927
928 for (morph_index, span) in morph.group_spans.iter().enumerate() {
929 for offset_index in span.start..span.start + span.count {
930 let child = morph.group_offsets[offset_index as usize].child_morph;
931 if child.as_usize() >= morph_count {
932 return Err(ModelBuildError::InvalidGroupMorphChild {
933 morph: morph_index,
934 child: child.0,
935 });
936 }
937 }
938 }
939 validate_group_morph_cycles(morph)?;
940
941 Ok(())
942}
943
944fn validate_group_morph_cycles(morph: &MorphInit) -> Result<(), ModelBuildError> {
945 let mut state = vec![VisitState::Unvisited; morph.morph_count as usize];
946 for morph_index in 0..morph.morph_count as usize {
947 visit_group_morph(morph_index, morph, &mut state)?;
948 }
949 Ok(())
950}
951
952fn visit_group_morph(
953 morph_index: usize,
954 morph: &MorphInit,
955 state: &mut [VisitState],
956) -> Result<(), ModelBuildError> {
957 match state[morph_index] {
958 VisitState::Visited => return Ok(()),
959 VisitState::Visiting => {
960 return Err(ModelBuildError::GroupMorphCycle { morph: morph_index });
961 }
962 VisitState::Unvisited => {}
963 }
964
965 state[morph_index] = VisitState::Visiting;
966 let span = morph.group_spans[morph_index];
967 for offset_index in span.start..span.start + span.count {
968 let child = morph.group_offsets[offset_index as usize]
969 .child_morph
970 .as_usize();
971 if morph.group_spans[child].count > 0 {
972 visit_group_morph(child, morph, state)?;
973 }
974 }
975 state[morph_index] = VisitState::Visited;
976 Ok(())
977}
978
979fn validate_morph_spans(
980 kind: &'static str,
981 spans: &[MorphOffsetSpan],
982 morph_count: usize,
983 offset_count: usize,
984) -> Result<(), ModelBuildError> {
985 if spans.len() != morph_count {
986 return Err(ModelBuildError::InvalidMorphSpanCount {
987 actual: spans.len(),
988 expected: morph_count,
989 });
990 }
991
992 for (morph_index, span) in spans.iter().enumerate() {
993 let start = span.start as usize;
994 let count = span.count as usize;
995 if start
996 .checked_add(count)
997 .is_none_or(|end| end > offset_count)
998 {
999 return Err(ModelBuildError::InvalidMorphSpan {
1000 morph: morph_index,
1001 kind,
1002 });
1003 }
1004 }
1005
1006 Ok(())
1007}
1008
1009#[cfg(test)]
1010mod tests {
1011 use super::*;
1012
1013 #[test]
1014 fn rejects_invalid_parent() {
1015 let error =
1016 ModelArena::new(vec![BoneInit::new(Some(BoneIndex(10)), Vec3A::ZERO)]).unwrap_err();
1017
1018 assert_eq!(
1019 error,
1020 ModelBuildError::InvalidParent {
1021 bone: 0,
1022 parent: 10
1023 }
1024 );
1025 }
1026
1027 #[test]
1028 fn eval_order_for_phase_preserves_relative_order_within_each_phase() {
1029 let mut root = BoneInit::new(None, Vec3A::ZERO);
1030 root.transform_order = 0;
1031 let mut pre_child = BoneInit::new(Some(BoneIndex(0)), Vec3A::ZERO);
1032 pre_child.transform_order = 2;
1033 let mut after_parent = BoneInit::new(None, Vec3A::ZERO);
1034 after_parent.transform_order = 1;
1035 after_parent.transform_after_physics = true;
1036 let mut after_child = BoneInit::new(Some(BoneIndex(2)), Vec3A::ZERO);
1037 after_child.transform_order = 3;
1038 after_child.transform_after_physics = true;
1039 let mut pre_sibling = BoneInit::new(Some(BoneIndex(0)), Vec3A::ZERO);
1040 pre_sibling.transform_order = 4;
1041
1042 let model = ModelArena::new(vec![
1043 root,
1044 pre_child,
1045 after_parent,
1046 after_child,
1047 pre_sibling,
1048 ])
1049 .unwrap();
1050
1051 let expected_before_physics: Vec<_> = model
1052 .eval_order()
1053 .iter()
1054 .copied()
1055 .filter(|bone| !model.transform_after_physics(*bone))
1056 .collect();
1057 let expected_after_physics: Vec<_> = model
1058 .eval_order()
1059 .iter()
1060 .copied()
1061 .filter(|bone| model.transform_after_physics(*bone))
1062 .collect();
1063
1064 assert_eq!(
1065 model.eval_order_for_phase(false),
1066 expected_before_physics.as_slice()
1067 );
1068 assert_eq!(
1069 model.eval_order_for_phase(true),
1070 expected_after_physics.as_slice()
1071 );
1072 assert_eq!(
1073 model.eval_order_for_phase(false),
1074 &[BoneIndex(0), BoneIndex(1), BoneIndex(4)]
1075 );
1076 assert_eq!(
1077 model.eval_order_for_phase(true),
1078 &[BoneIndex(2), BoneIndex(3)]
1079 );
1080 }
1081
1082 #[test]
1083 fn parent_is_ordered_before_child_even_if_input_order_is_not_transform_order() {
1084 let mut root = BoneInit::new(None, Vec3A::ZERO);
1085 root.transform_order = 10;
1086 let child = BoneInit::new(Some(BoneIndex(0)), Vec3A::ZERO);
1087
1088 let model = ModelArena::new(vec![root, child]).unwrap();
1089
1090 assert_eq!(model.eval_order(), &[BoneIndex(0), BoneIndex(1)]);
1091 }
1092
1093 #[test]
1094 fn stores_ik_solver_descriptors() {
1095 let solver = IkSolverInit {
1096 ik_bone: BoneIndex(2),
1097 target_bone: BoneIndex(1),
1098 links: vec![
1099 IkLinkInit::new(BoneIndex(0))
1100 .with_angle_limit(IkAngleLimit::new(Vec3A::splat(-1.0), Vec3A::splat(1.0))),
1101 ],
1102 iteration_count: 4,
1103 limit_angle: 0.5,
1104 };
1105
1106 let model = ModelArena::new_with_ik(
1107 vec![
1108 BoneInit::new(None, Vec3A::ZERO),
1109 BoneInit::new(Some(BoneIndex(0)), Vec3A::ZERO),
1110 BoneInit::new(Some(BoneIndex(1)), Vec3A::ZERO),
1111 ],
1112 vec![solver],
1113 )
1114 .unwrap();
1115
1116 assert_eq!(model.ik_count(), 1);
1117 assert_eq!(model.ik_solvers()[0].ik_bone, BoneIndex(2));
1118 assert_eq!(model.ik_solvers()[0].target_bone, BoneIndex(1));
1119 assert_eq!(model.ik_solvers()[0].links[0].bone, BoneIndex(0));
1120 assert_eq!(model.ik_solvers()[0].iteration_count, 4);
1121 assert_eq!(model.ik_solvers()[0].limit_angle, 0.5);
1122 }
1123
1124 #[test]
1125 fn ik_bone_solver_lookup_maps_multiple_bones_and_preserves_registration_order() {
1126 let model = ModelArena::new_with_ik(
1127 vec![
1128 BoneInit::new(None, Vec3A::ZERO),
1129 BoneInit::new(Some(BoneIndex(0)), Vec3A::ZERO),
1130 BoneInit::new(Some(BoneIndex(1)), Vec3A::ZERO),
1131 BoneInit::new(None, Vec3A::ZERO),
1132 BoneInit::new(None, Vec3A::ZERO),
1133 ],
1134 vec![
1135 IkSolverInit::new(
1136 BoneIndex(2),
1137 BoneIndex(1),
1138 vec![IkLinkInit::new(BoneIndex(0))],
1139 ),
1140 IkSolverInit::new(
1141 BoneIndex(4),
1142 BoneIndex(3),
1143 vec![IkLinkInit::new(BoneIndex(0))],
1144 ),
1145 IkSolverInit::new(
1146 BoneIndex(2),
1147 BoneIndex(1),
1148 vec![IkLinkInit::new(BoneIndex(0))],
1149 ),
1150 ],
1151 )
1152 .unwrap();
1153
1154 assert_eq!(model.ik_solver_count_for_bone(BoneIndex(0)), 0);
1155 assert_eq!(model.ik_solver_count_for_bone(BoneIndex(1)), 0);
1156 assert_eq!(model.ik_solver_count_for_bone(BoneIndex(2)), 2);
1157 assert_eq!(model.ik_solver_count_for_bone(BoneIndex(3)), 0);
1158 assert_eq!(model.ik_solver_count_for_bone(BoneIndex(4)), 1);
1159 assert_eq!(model.ik_solver_index_for_bone(BoneIndex(2), 0), 0);
1160 assert_eq!(model.ik_solver_index_for_bone(BoneIndex(2), 1), 2);
1161 assert_eq!(model.ik_solver_index_for_bone(BoneIndex(4), 0), 1);
1162 }
1163
1164 #[test]
1165 fn stores_normalized_fixed_axis_descriptors() {
1166 let model = ModelArena::new(vec![
1167 BoneInit::new(None, Vec3A::ZERO).with_fixed_axis(Vec3A::new(0.0, 2.0, 0.0)),
1168 BoneInit::new(Some(BoneIndex(0)), Vec3A::ZERO),
1169 ])
1170 .unwrap();
1171
1172 assert_eq!(model.fixed_axis(BoneIndex(0)), Some(Vec3A::Y));
1173 assert_eq!(model.fixed_axis(BoneIndex(1)), None);
1174 }
1175
1176 #[test]
1177 fn stores_local_axis_descriptors_and_basis() {
1178 let model = ModelArena::new(vec![
1179 BoneInit::new(None, Vec3A::ZERO),
1180 BoneInit::new(Some(BoneIndex(0)), Vec3A::ZERO),
1181 BoneInit::new(Some(BoneIndex(0)), Vec3A::ZERO),
1182 ])
1183 .unwrap()
1184 .with_local_axes([
1185 Some(LocalAxis::new(
1186 Vec3A::new(0.0, 1.0, 0.0),
1187 Vec3A::new(0.0, 0.0, 1.0),
1188 )),
1189 Some(LocalAxis::new(Vec3A::ZERO, Vec3A::Z)),
1190 None,
1191 ]);
1192
1193 let axis = model.local_axis(BoneIndex(0)).expect("local axis retained");
1194 assert_eq!(axis.x, Vec3A::new(0.0, 1.0, 0.0));
1195 assert_eq!(axis.z, Vec3A::new(0.0, 0.0, 1.0));
1196 let basis = model
1197 .local_axis_basis(BoneIndex(0))
1198 .expect("local axis basis");
1199 let x_dir = basis * Vec3A::X;
1201 let y_dir = basis * Vec3A::Y;
1202 let z_dir = basis * Vec3A::Z;
1203 assert!((x_dir - Vec3A::Y).length() < 1.0e-5);
1204 assert!((y_dir - (-Vec3A::X)).length() < 1.0e-5);
1205 assert!((z_dir - Vec3A::Z).length() < 1.0e-5);
1206 assert!(model.local_axis(BoneIndex(1)).is_none());
1207 assert!(model.local_axis_basis(BoneIndex(1)).is_none());
1208 assert!(model.local_axis(BoneIndex(2)).is_none());
1209 assert_eq!(model.local_axis_count(), 1);
1210 }
1211
1212 #[test]
1213 fn existing_bone_init_constructors_have_no_local_axes() {
1214 let via_new = ModelArena::new(vec![BoneInit::new(None, Vec3A::ZERO)]).unwrap();
1217 assert_eq!(via_new.local_axis_count(), 0);
1218 assert!(via_new.local_axis(BoneIndex(0)).is_none());
1219
1220 let via_literal = ModelArena::new(vec![BoneInit {
1221 parent: None,
1222 rest_position: Vec3A::ZERO,
1223 inverse_bind_matrix: Mat4::IDENTITY,
1224 transform_order: 0,
1225 transform_after_physics: false,
1226 fixed_axis: None,
1227 enforce_fixed_axis: false,
1228 }])
1229 .unwrap();
1230 assert_eq!(via_literal.local_axis_count(), 0);
1231 }
1232
1233 #[test]
1234 fn rejects_invalid_ik_link_bone() {
1235 let error = ModelArena::new_with_ik(
1236 vec![BoneInit::new(None, Vec3A::ZERO)],
1237 vec![IkSolverInit::new(
1238 BoneIndex(0),
1239 BoneIndex(0),
1240 vec![IkLinkInit::new(BoneIndex(10))],
1241 )],
1242 )
1243 .unwrap_err();
1244
1245 assert_eq!(
1246 error,
1247 ModelBuildError::InvalidIkBone {
1248 solver: 0,
1249 role: "link",
1250 bone: 10,
1251 }
1252 );
1253 }
1254
1255 #[test]
1256 fn stores_append_transform_descriptors() {
1257 let model = ModelArena::new_full(
1258 vec![
1259 BoneInit::new(None, Vec3A::ZERO),
1260 BoneInit::new(None, Vec3A::ZERO),
1261 ],
1262 Vec::new(),
1263 vec![
1264 AppendTransformInit::new(BoneIndex(1), BoneIndex(0), 0.5)
1265 .with_rotation()
1266 .with_translation(),
1267 ],
1268 )
1269 .unwrap();
1270
1271 let append_index = model.append_transform_index(BoneIndex(1)).unwrap();
1272 let append = model.append_transform(append_index);
1273 assert_eq!(append.source_bone, BoneIndex(0));
1274 assert_eq!(append.ratio, 0.5);
1275 assert!(append.affect_rotation);
1276 assert!(append.affect_translation);
1277 }
1278
1279 #[test]
1280 fn rejects_duplicate_append_transform_target() {
1281 let error = ModelArena::new_full(
1282 vec![
1283 BoneInit::new(None, Vec3A::ZERO),
1284 BoneInit::new(None, Vec3A::ZERO),
1285 ],
1286 Vec::new(),
1287 vec![
1288 AppendTransformInit::new(BoneIndex(1), BoneIndex(0), 1.0),
1289 AppendTransformInit::new(BoneIndex(1), BoneIndex(0), 1.0),
1290 ],
1291 )
1292 .unwrap_err();
1293
1294 assert_eq!(error, ModelBuildError::DuplicateAppendTransform { bone: 1 });
1295 }
1296
1297 #[test]
1298 fn rejects_invalid_bone_morph_target_bone() {
1299 let error = ModelArena::new_with_morphs(
1300 vec![BoneInit::new(None, Vec3A::ZERO)],
1301 Vec::new(),
1302 Vec::new(),
1303 MorphInit {
1304 morph_count: 1,
1305 bone_offsets: vec![BoneMorphOffset {
1306 target_bone: BoneIndex(10),
1307 position_offset: Vec3A::ZERO,
1308 rotation_offset: Quat::IDENTITY,
1309 }],
1310 bone_spans: vec![MorphOffsetSpan { start: 0, count: 1 }],
1311 group_offsets: Vec::new(),
1312 group_spans: vec![MorphOffsetSpan::default()],
1313 ..MorphInit::default()
1314 },
1315 )
1316 .unwrap_err();
1317
1318 assert_eq!(
1319 error,
1320 ModelBuildError::InvalidBoneMorphBone {
1321 offset: 0,
1322 bone: 10
1323 }
1324 );
1325 }
1326
1327 #[test]
1328 fn accepts_group_morph_child_that_is_later() {
1329 let model = ModelArena::new_with_morphs(
1330 vec![BoneInit::new(None, Vec3A::ZERO)],
1331 Vec::new(),
1332 Vec::new(),
1333 MorphInit {
1334 morph_count: 2,
1335 bone_offsets: Vec::new(),
1336 bone_spans: vec![MorphOffsetSpan::default(), MorphOffsetSpan::default()],
1337 group_offsets: vec![GroupMorphOffset {
1338 child_morph: MorphIndex(1),
1339 ratio: 1.0,
1340 }],
1341 group_spans: vec![
1342 MorphOffsetSpan { start: 0, count: 1 },
1343 MorphOffsetSpan::default(),
1344 ],
1345 ..MorphInit::default()
1346 },
1347 )
1348 .unwrap();
1349
1350 assert_eq!(model.morph_count(), 2);
1351 }
1352
1353 #[test]
1354 fn rejects_group_morph_child_out_of_range() {
1355 let error = ModelArena::new_with_morphs(
1356 vec![BoneInit::new(None, Vec3A::ZERO)],
1357 Vec::new(),
1358 Vec::new(),
1359 MorphInit {
1360 morph_count: 2,
1361 bone_offsets: Vec::new(),
1362 bone_spans: vec![MorphOffsetSpan::default(), MorphOffsetSpan::default()],
1363 group_offsets: vec![GroupMorphOffset {
1364 child_morph: MorphIndex(2),
1365 ratio: 1.0,
1366 }],
1367 group_spans: vec![
1368 MorphOffsetSpan { start: 0, count: 1 },
1369 MorphOffsetSpan::default(),
1370 ],
1371 ..MorphInit::default()
1372 },
1373 )
1374 .unwrap_err();
1375
1376 assert_eq!(
1377 error,
1378 ModelBuildError::InvalidGroupMorphChild { morph: 0, child: 2 }
1379 );
1380 }
1381
1382 #[test]
1383 fn rejects_group_morph_cycle() {
1384 let error = ModelArena::new_with_morphs(
1385 vec![BoneInit::new(None, Vec3A::ZERO)],
1386 Vec::new(),
1387 Vec::new(),
1388 MorphInit {
1389 morph_count: 2,
1390 bone_offsets: Vec::new(),
1391 bone_spans: vec![MorphOffsetSpan::default(), MorphOffsetSpan::default()],
1392 group_offsets: vec![
1393 GroupMorphOffset {
1394 child_morph: MorphIndex(1),
1395 ratio: 1.0,
1396 },
1397 GroupMorphOffset {
1398 child_morph: MorphIndex(0),
1399 ratio: 1.0,
1400 },
1401 ],
1402 group_spans: vec![
1403 MorphOffsetSpan { start: 0, count: 1 },
1404 MorphOffsetSpan { start: 1, count: 1 },
1405 ],
1406 ..MorphInit::default()
1407 },
1408 )
1409 .unwrap_err();
1410
1411 assert_eq!(error, ModelBuildError::GroupMorphCycle { morph: 0 });
1412 }
1413
1414 #[test]
1415 fn stores_vertex_morph_offsets() {
1416 let model = ModelArena::new_with_morphs(
1417 vec![BoneInit::new(None, Vec3A::ZERO)],
1418 Vec::new(),
1419 Vec::new(),
1420 MorphInit {
1421 morph_count: 1,
1422 vertex_offsets: vec![VertexMorphOffset {
1423 vertex_index: 7,
1424 position_offset: Vec3A::new(1.0, 2.0, 3.0),
1425 }],
1426 vertex_spans: vec![MorphOffsetSpan { start: 0, count: 1 }],
1427 bone_spans: vec![MorphOffsetSpan::default()],
1428 group_spans: vec![MorphOffsetSpan::default()],
1429 ..MorphInit::default()
1430 },
1431 )
1432 .unwrap();
1433
1434 assert_eq!(
1435 model.vertex_morph_offsets(),
1436 &[VertexMorphOffset {
1437 vertex_index: 7,
1438 position_offset: Vec3A::new(1.0, 2.0, 3.0),
1439 }]
1440 );
1441 assert_eq!(
1442 model.vertex_morph_spans(),
1443 &[MorphOffsetSpan { start: 0, count: 1 }]
1444 );
1445 }
1446}