1use std::fmt;
10
11use glam::{Quat, Vec3A};
12use thiserror::Error;
13
14use crate::{
15 AppendTransformInit, BoneIndex, BoneInit, BoneMorphOffset, GroupMorphOffset, IkAngleLimit,
16 IkLinkInit, IkSolverInit, LocalAxis, ModelArena, MorphIndex, MorphInit,
17};
18
19pub const RUNTIME_MODEL_DESCRIPTOR_VERSION_V1: u32 = 1;
21
22#[derive(Clone, Debug, PartialEq)]
24pub struct RuntimeModelDescriptorV1 {
25 pub descriptor_version: u32,
27 pub bones: Vec<RuntimeBoneDescriptorV1>,
28 pub ik_solvers: Vec<RuntimeIkSolverDescriptorV1>,
29 pub append_transforms: Vec<RuntimeAppendTransformDescriptorV1>,
30 pub morphs: RuntimeMorphDescriptorV1,
31}
32
33impl Default for RuntimeModelDescriptorV1 {
34 fn default() -> Self {
35 Self {
36 descriptor_version: RUNTIME_MODEL_DESCRIPTOR_VERSION_V1,
37 bones: Vec::new(),
38 ik_solvers: Vec::new(),
39 append_transforms: Vec::new(),
40 morphs: RuntimeMorphDescriptorV1::default(),
41 }
42 }
43}
44
45impl RuntimeModelDescriptorV1 {
46 pub fn new(bones: Vec<RuntimeBoneDescriptorV1>) -> Self {
47 Self {
48 bones,
49 ..Self::default()
50 }
51 }
52}
53
54#[derive(Clone, Debug, PartialEq)]
56pub struct RuntimeBoneDescriptorV1 {
57 pub parent: Option<BoneIndex>,
58 pub rest_position: Vec3A,
59 pub transform_order: i32,
60 pub transform_after_physics: bool,
61 pub fixed_axis: Option<Vec3A>,
62 pub local_axis: Option<LocalAxis>,
63}
64
65impl RuntimeBoneDescriptorV1 {
66 pub fn new(parent: Option<BoneIndex>, rest_position: Vec3A) -> Self {
67 Self {
68 parent,
69 rest_position,
70 transform_order: 0,
71 transform_after_physics: false,
72 fixed_axis: None,
73 local_axis: None,
74 }
75 }
76}
77
78#[derive(Clone, Debug, PartialEq)]
80pub struct RuntimeIkLinkDescriptorV1 {
81 pub bone: BoneIndex,
82 pub angle_limit: Option<IkAngleLimit>,
83}
84
85impl RuntimeIkLinkDescriptorV1 {
86 pub fn new(bone: BoneIndex) -> Self {
87 Self {
88 bone,
89 angle_limit: None,
90 }
91 }
92}
93
94#[derive(Clone, Debug, PartialEq)]
96pub struct RuntimeIkSolverDescriptorV1 {
97 pub ik_bone: BoneIndex,
98 pub target_bone: BoneIndex,
99 pub links: Vec<RuntimeIkLinkDescriptorV1>,
100 pub iteration_count: u32,
101 pub limit_angle: f32,
102}
103
104impl RuntimeIkSolverDescriptorV1 {
105 pub fn new(
106 ik_bone: BoneIndex,
107 target_bone: BoneIndex,
108 links: Vec<RuntimeIkLinkDescriptorV1>,
109 ) -> Self {
110 Self {
111 ik_bone,
112 target_bone,
113 links,
114 iteration_count: 1,
115 limit_angle: 0.0,
116 }
117 }
118}
119
120#[derive(Clone, Debug, PartialEq)]
123pub struct RuntimeAppendTransformDescriptorV1 {
124 pub target_bone: BoneIndex,
125 pub source_bone: BoneIndex,
126 pub ratio: f32,
127 pub affect_rotation: bool,
128 pub affect_translation: bool,
129 pub local: bool,
130}
131
132impl RuntimeAppendTransformDescriptorV1 {
133 pub fn new(target_bone: BoneIndex, source_bone: BoneIndex, ratio: f32) -> Self {
134 Self {
135 target_bone,
136 source_bone,
137 ratio,
138 affect_rotation: false,
139 affect_translation: false,
140 local: false,
141 }
142 }
143}
144
145#[derive(Clone, Debug, Default, PartialEq)]
148pub struct RuntimeMorphDescriptorV1 {
149 pub morph_count: u32,
150 pub bone_offsets: Vec<RuntimeBoneMorphOffsetDescriptorV1>,
151 pub group_offsets: Vec<RuntimeGroupMorphOffsetDescriptorV1>,
152}
153
154#[derive(Clone, Debug, PartialEq)]
155pub struct RuntimeBoneMorphOffsetDescriptorV1 {
156 pub morph_index: MorphIndex,
157 pub target_bone: BoneIndex,
158 pub position_offset: Vec3A,
159 pub rotation_offset: Quat,
160}
161
162#[derive(Clone, Debug, PartialEq)]
163pub struct RuntimeGroupMorphOffsetDescriptorV1 {
164 pub morph_index: MorphIndex,
165 pub child_morph: MorphIndex,
166 pub ratio: f32,
167}
168
169#[derive(Clone, Debug, Error, PartialEq, Eq)]
172#[error("{path}: {kind}")]
173pub struct RuntimeModelDescriptorError {
174 pub path: String,
175 pub kind: RuntimeModelDescriptorErrorKind,
176}
177
178#[derive(Clone, Debug, Error, PartialEq, Eq)]
179pub enum RuntimeModelDescriptorErrorKind {
180 #[error("descriptor version must be {expected}, got {actual}")]
181 UnsupportedVersion { expected: u32, actual: u32 },
182 #[error("model must contain at least one bone")]
183 EmptyBones,
184 #[error("index {value} is out of range for length {length}")]
185 IndexOutOfRange { value: u32, length: usize },
186 #[error("parent cannot reference itself")]
187 SelfParent,
188 #[error("parent hierarchy contains a cycle")]
189 ParentCycle,
190 #[error("value is not finite")]
191 NonFinite,
192 #[error("axis is zero-length or otherwise degenerate")]
193 DegenerateAxis,
194 #[error("quaternion is zero-length or otherwise degenerate")]
195 DegenerateQuaternion,
196 #[error("minimum must not exceed maximum")]
197 InvalidRange,
198 #[error("iteration count must be greater than zero")]
199 InvalidIterationCount,
200 #[error("limit angle must be finite and non-negative")]
201 InvalidLimitAngle,
202 #[error("append ratio must be finite")]
203 InvalidAppendRatio,
204 #[error("append target is already used by another append transform")]
205 DuplicateAppendTarget,
206 #[error("append requires rotation and/or translation")]
207 InvalidAppendFlags,
208 #[error("morph count is zero but morph offsets are present")]
209 EmptyMorphSet,
210 #[error("morph group graph contains a cycle")]
211 GroupMorphCycle,
212 #[error("model arena rejected normalized payload: {0}")]
213 ModelBuild(String),
214 #[error("descriptor storage allocation failed")]
215 AllocationFailed,
216}
217
218impl RuntimeModelDescriptorError {
219 fn new(path: impl Into<String>, kind: RuntimeModelDescriptorErrorKind) -> Self {
220 Self {
221 path: path.into(),
222 kind,
223 }
224 }
225}
226
227pub fn compile_runtime_model_descriptor_v1(
229 descriptor: &RuntimeModelDescriptorV1,
230) -> Result<ModelArena, RuntimeModelDescriptorError> {
231 if descriptor.descriptor_version != RUNTIME_MODEL_DESCRIPTOR_VERSION_V1 {
232 return Err(RuntimeModelDescriptorError::new(
233 "descriptor_version",
234 RuntimeModelDescriptorErrorKind::UnsupportedVersion {
235 expected: RUNTIME_MODEL_DESCRIPTOR_VERSION_V1,
236 actual: descriptor.descriptor_version,
237 },
238 ));
239 }
240 if descriptor.bones.is_empty() {
241 return Err(RuntimeModelDescriptorError::new(
242 "bones",
243 RuntimeModelDescriptorErrorKind::EmptyBones,
244 ));
245 }
246
247 let bone_count = descriptor.bones.len();
248 validate_bones(&descriptor.bones)?;
249 validate_ik_solvers(&descriptor.ik_solvers, bone_count)?;
250 validate_append_transforms(&descriptor.append_transforms, bone_count)?;
251
252 let mut bones = Vec::with_capacity(bone_count);
253 let mut local_axes = Vec::with_capacity(bone_count);
254 for (bone_index, descriptor_bone) in descriptor.bones.iter().enumerate() {
255 let parent = descriptor_bone.parent;
256 let absolute_position = descriptor_bone.rest_position;
257 let rest_position = parent
258 .map(|index| absolute_position - descriptor.bones[index.as_usize()].rest_position)
259 .unwrap_or(absolute_position);
260 validate_vec3(format!("bones[{bone_index}].rest_position"), rest_position)?;
261 bones.push(BoneInit {
262 parent,
263 rest_position,
264 inverse_bind_matrix: glam::Mat4::from_translation((-absolute_position).into()),
265 transform_order: descriptor_bone.transform_order,
266 transform_after_physics: descriptor_bone.transform_after_physics,
267 fixed_axis: descriptor_bone.fixed_axis,
268 enforce_fixed_axis: false,
271 });
272 local_axes.push(descriptor_bone.local_axis);
273 }
274
275 let ik_solvers = descriptor
276 .ik_solvers
277 .iter()
278 .map(|solver| IkSolverInit {
279 ik_bone: solver.ik_bone,
280 target_bone: solver.target_bone,
281 links: solver
282 .links
283 .iter()
284 .map(|link| IkLinkInit {
285 bone: link.bone,
286 angle_limit: link.angle_limit,
287 })
288 .collect(),
289 iteration_count: solver.iteration_count,
290 limit_angle: solver.limit_angle,
291 })
292 .collect();
293
294 let append_transforms = descriptor
295 .append_transforms
296 .iter()
297 .map(|append| AppendTransformInit {
298 target_bone: append.target_bone,
299 source_bone: append.source_bone,
300 ratio: append.ratio,
301 affect_rotation: append.affect_rotation,
302 affect_translation: append.affect_translation,
303 local: append.local,
304 })
305 .collect();
306
307 let morph = compile_morphs(&descriptor.morphs, bone_count)?;
308 let model = ModelArena::new_with_morphs(bones, ik_solvers, append_transforms, morph).map_err(
309 |error| match error {
310 crate::ModelBuildError::ParentCycle { bone } => RuntimeModelDescriptorError::new(
311 format!("bones[{bone}].parent"),
312 RuntimeModelDescriptorErrorKind::ParentCycle,
313 ),
314 crate::ModelBuildError::GroupMorphCycle { morph } => RuntimeModelDescriptorError::new(
315 format!("morphs.group_offsets[{morph}].child_morph"),
316 RuntimeModelDescriptorErrorKind::GroupMorphCycle,
317 ),
318 other => RuntimeModelDescriptorError::new(
319 "model",
320 RuntimeModelDescriptorErrorKind::ModelBuild(other.to_string()),
321 ),
322 },
323 )?;
324 Ok(model.with_local_axes(local_axes))
325}
326
327fn validate_bones(bones: &[RuntimeBoneDescriptorV1]) -> Result<(), RuntimeModelDescriptorError> {
328 for (bone_index, bone) in bones.iter().enumerate() {
329 if let Some(parent) = bone.parent {
330 if parent.as_usize() >= bones.len() {
331 return Err(RuntimeModelDescriptorError::new(
332 format!("bones[{bone_index}].parent"),
333 RuntimeModelDescriptorErrorKind::IndexOutOfRange {
334 value: parent.0,
335 length: bones.len(),
336 },
337 ));
338 }
339 if parent.as_usize() == bone_index {
340 return Err(RuntimeModelDescriptorError::new(
341 format!("bones[{bone_index}].parent"),
342 RuntimeModelDescriptorErrorKind::SelfParent,
343 ));
344 }
345 }
346 validate_vec3(
347 format!("bones[{bone_index}].rest_position"),
348 bone.rest_position,
349 )?;
350 if let Some(axis) = bone.fixed_axis {
351 validate_axis(format!("bones[{bone_index}].fixed_axis"), axis)?;
352 }
353 if let Some(axis) = bone.local_axis {
354 validate_axis(format!("bones[{bone_index}].local_axis.x"), axis.x)?;
355 validate_axis(format!("bones[{bone_index}].local_axis.z"), axis.z)?;
356 if axis.basis_quat().is_none() {
357 return Err(RuntimeModelDescriptorError::new(
358 format!("bones[{bone_index}].local_axis"),
359 RuntimeModelDescriptorErrorKind::DegenerateAxis,
360 ));
361 }
362 }
363 }
364
365 Ok(())
366}
367
368fn validate_ik_solvers(
369 solvers: &[RuntimeIkSolverDescriptorV1],
370 bone_count: usize,
371) -> Result<(), RuntimeModelDescriptorError> {
372 for (solver_index, solver) in solvers.iter().enumerate() {
373 validate_bone_index(
374 format!("ik_solvers[{solver_index}].ik_bone"),
375 solver.ik_bone,
376 bone_count,
377 )?;
378 validate_bone_index(
379 format!("ik_solvers[{solver_index}].target_bone"),
380 solver.target_bone,
381 bone_count,
382 )?;
383 if solver.iteration_count == 0 {
384 return Err(RuntimeModelDescriptorError::new(
385 format!("ik_solvers[{solver_index}].iteration_count"),
386 RuntimeModelDescriptorErrorKind::InvalidIterationCount,
387 ));
388 }
389 if !solver.limit_angle.is_finite() || solver.limit_angle < 0.0 {
390 return Err(RuntimeModelDescriptorError::new(
391 format!("ik_solvers[{solver_index}].limit_angle"),
392 RuntimeModelDescriptorErrorKind::InvalidLimitAngle,
393 ));
394 }
395 for (link_index, link) in solver.links.iter().enumerate() {
396 validate_bone_index(
397 format!("ik_solvers[{solver_index}].links[{link_index}].bone"),
398 link.bone,
399 bone_count,
400 )?;
401 if let Some(limit) = link.angle_limit {
402 let min_path =
403 format!("ik_solvers[{solver_index}].links[{link_index}].angle_limit.min");
404 let max_path =
405 format!("ik_solvers[{solver_index}].links[{link_index}].angle_limit.max");
406 validate_vec3(min_path, limit.min)?;
407 validate_vec3(max_path, limit.max)?;
408 if (limit.min.cmple(limit.max)).bitmask() != 0b111 {
409 return Err(RuntimeModelDescriptorError::new(
410 format!("ik_solvers[{solver_index}].links[{link_index}].angle_limit"),
411 RuntimeModelDescriptorErrorKind::InvalidRange,
412 ));
413 }
414 }
415 }
416 }
417 Ok(())
418}
419
420fn validate_append_transforms(
421 appends: &[RuntimeAppendTransformDescriptorV1],
422 bone_count: usize,
423) -> Result<(), RuntimeModelDescriptorError> {
424 let mut targets = std::collections::HashSet::with_capacity(appends.len());
425 for (append_index, append) in appends.iter().enumerate() {
426 validate_bone_index(
427 format!("append_transforms[{append_index}].target_bone"),
428 append.target_bone,
429 bone_count,
430 )?;
431 validate_bone_index(
432 format!("append_transforms[{append_index}].source_bone"),
433 append.source_bone,
434 bone_count,
435 )?;
436 if !append.ratio.is_finite() {
437 return Err(RuntimeModelDescriptorError::new(
438 format!("append_transforms[{append_index}].ratio"),
439 RuntimeModelDescriptorErrorKind::InvalidAppendRatio,
440 ));
441 }
442 if !append.affect_rotation && !append.affect_translation {
443 return Err(RuntimeModelDescriptorError::new(
444 format!("append_transforms[{append_index}]"),
445 RuntimeModelDescriptorErrorKind::InvalidAppendFlags,
446 ));
447 }
448 if !targets.insert(append.target_bone) {
449 return Err(RuntimeModelDescriptorError::new(
450 format!("append_transforms[{append_index}].target_bone"),
451 RuntimeModelDescriptorErrorKind::DuplicateAppendTarget,
452 ));
453 }
454 }
455 Ok(())
456}
457
458fn compile_morphs(
459 descriptor: &RuntimeMorphDescriptorV1,
460 bone_count: usize,
461) -> Result<MorphInit, RuntimeModelDescriptorError> {
462 let morph_count = descriptor.morph_count as usize;
463 if morph_count == 0
464 && (!descriptor.bone_offsets.is_empty() || !descriptor.group_offsets.is_empty())
465 {
466 return Err(RuntimeModelDescriptorError::new(
467 "morphs.morph_count",
468 RuntimeModelDescriptorErrorKind::EmptyMorphSet,
469 ));
470 }
471
472 let mut bone_offsets = Vec::new();
473 bone_offsets
474 .try_reserve_exact(descriptor.bone_offsets.len())
475 .map_err(|_| allocation_error("morphs.bone_offsets"))?;
476 for (offset_index, offset) in descriptor.bone_offsets.iter().enumerate() {
477 validate_morph_index(
478 format!("morphs.bone_offsets[{offset_index}].morph_index"),
479 offset.morph_index,
480 morph_count,
481 )?;
482 validate_bone_index(
483 format!("morphs.bone_offsets[{offset_index}].target_bone"),
484 offset.target_bone,
485 bone_count,
486 )?;
487 validate_vec3(
488 format!("morphs.bone_offsets[{offset_index}].position_offset"),
489 offset.position_offset,
490 )?;
491 bone_offsets.push((
492 offset_index,
493 offset.morph_index,
494 BoneMorphOffset {
495 target_bone: offset.target_bone,
496 position_offset: offset.position_offset,
497 rotation_offset: validate_quaternion(
498 format!("morphs.bone_offsets[{offset_index}].rotation_offset"),
499 offset.rotation_offset,
500 )?,
501 },
502 ));
503 }
504
505 let mut group_offsets = Vec::new();
506 group_offsets
507 .try_reserve_exact(descriptor.group_offsets.len())
508 .map_err(|_| allocation_error("morphs.group_offsets"))?;
509 for (offset_index, offset) in descriptor.group_offsets.iter().enumerate() {
510 validate_morph_index(
511 format!("morphs.group_offsets[{offset_index}].morph_index"),
512 offset.morph_index,
513 morph_count,
514 )?;
515 validate_morph_index(
516 format!("morphs.group_offsets[{offset_index}].child_morph"),
517 offset.child_morph,
518 morph_count,
519 )?;
520 if !offset.ratio.is_finite() {
521 return Err(RuntimeModelDescriptorError::new(
522 format!("morphs.group_offsets[{offset_index}].ratio"),
523 RuntimeModelDescriptorErrorKind::NonFinite,
524 ));
525 }
526 group_offsets.push((
527 offset_index,
528 offset.morph_index,
529 GroupMorphOffset {
530 child_morph: offset.child_morph,
531 ratio: offset.ratio,
532 },
533 ));
534 }
535
536 crate::model::build_morph_init_from_indexed_offsets(
537 descriptor.morph_count,
538 bone_offsets,
539 group_offsets,
540 )
541 .map_err(|error| match error {
542 crate::ModelBuildError::GroupMorphCycleAt { offset, .. } => {
543 RuntimeModelDescriptorError::new(
544 format!("morphs.group_offsets[{offset}].child_morph"),
545 RuntimeModelDescriptorErrorKind::GroupMorphCycle,
546 )
547 }
548 crate::ModelBuildError::MorphCountZeroWithData => RuntimeModelDescriptorError::new(
549 "morphs.morph_count",
550 RuntimeModelDescriptorErrorKind::EmptyMorphSet,
551 ),
552 crate::ModelBuildError::MorphStorageAllocation => allocation_error("morphs"),
553 other => RuntimeModelDescriptorError::new(
554 "morphs",
555 RuntimeModelDescriptorErrorKind::ModelBuild(other.to_string()),
556 ),
557 })
558}
559
560fn allocation_error(path: impl Into<String>) -> RuntimeModelDescriptorError {
561 RuntimeModelDescriptorError::new(path, RuntimeModelDescriptorErrorKind::AllocationFailed)
562}
563
564fn validate_bone_index(
565 path: String,
566 value: BoneIndex,
567 length: usize,
568) -> Result<(), RuntimeModelDescriptorError> {
569 if value.as_usize() >= length {
570 return Err(RuntimeModelDescriptorError::new(
571 path,
572 RuntimeModelDescriptorErrorKind::IndexOutOfRange {
573 value: value.0,
574 length,
575 },
576 ));
577 }
578 Ok(())
579}
580
581fn validate_morph_index(
582 path: String,
583 value: MorphIndex,
584 length: usize,
585) -> Result<(), RuntimeModelDescriptorError> {
586 if value.as_usize() >= length {
587 return Err(RuntimeModelDescriptorError::new(
588 path,
589 RuntimeModelDescriptorErrorKind::IndexOutOfRange {
590 value: value.0,
591 length,
592 },
593 ));
594 }
595 Ok(())
596}
597
598fn validate_vec3(path: String, value: Vec3A) -> Result<(), RuntimeModelDescriptorError> {
599 if !value.is_finite() {
600 return Err(RuntimeModelDescriptorError::new(
601 path,
602 RuntimeModelDescriptorErrorKind::NonFinite,
603 ));
604 }
605 Ok(())
606}
607
608fn validate_axis(path: String, value: Vec3A) -> Result<(), RuntimeModelDescriptorError> {
609 validate_vec3(path.clone(), value)?;
610 let length_squared = value.length_squared();
611 if !length_squared.is_finite() || length_squared <= f32::EPSILON {
612 return Err(RuntimeModelDescriptorError::new(
613 path.clone(),
614 RuntimeModelDescriptorErrorKind::DegenerateAxis,
615 ));
616 }
617 let normalized = value.normalize();
618 if !normalized.is_finite()
619 || !normalized.length_squared().is_finite()
620 || normalized.length_squared() <= f32::EPSILON
621 {
622 return Err(RuntimeModelDescriptorError::new(
623 path,
624 RuntimeModelDescriptorErrorKind::DegenerateAxis,
625 ));
626 }
627 Ok(())
628}
629
630fn validate_quaternion(path: String, value: Quat) -> Result<Quat, RuntimeModelDescriptorError> {
631 if !value.is_finite() {
632 return Err(RuntimeModelDescriptorError::new(
633 path,
634 RuntimeModelDescriptorErrorKind::NonFinite,
635 ));
636 }
637 let length_squared = value.length_squared();
638 if !length_squared.is_finite() || length_squared <= f32::EPSILON {
639 return Err(RuntimeModelDescriptorError::new(
640 path,
641 RuntimeModelDescriptorErrorKind::DegenerateQuaternion,
642 ));
643 }
644 let normalized = value.normalize();
645 if !normalized.is_finite() || normalized.length_squared() <= f32::EPSILON {
646 return Err(RuntimeModelDescriptorError::new(
647 path,
648 RuntimeModelDescriptorErrorKind::DegenerateQuaternion,
649 ));
650 }
651 Ok(normalized)
652}
653
654impl fmt::Display for RuntimeModelDescriptorV1 {
655 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
656 f.debug_struct("RuntimeModelDescriptorV1")
657 .field("descriptor_version", &self.descriptor_version)
658 .field("bones", &self.bones.len())
659 .field("ik_solvers", &self.ik_solvers.len())
660 .field("append_transforms", &self.append_transforms.len())
661 .field("morph_count", &self.morphs.morph_count)
662 .finish()
663 }
664}