use crate::assets::{NamedMesh, Unresolved};
use crate::mesh::{Animation, Clip, MeshData, Part, Rig};
pub(crate) struct NamedModel {
mesh: NamedMesh,
rig: Rig,
animations: Vec<NamedAnimation>,
}
impl NamedModel {
pub(crate) fn new(mesh: NamedMesh, rig: Rig, animations: Vec<NamedAnimation>) -> Self {
Self {
mesh,
rig,
animations,
}
}
pub(crate) fn resolved<P: Part, C: Clip>(
&self,
asset: &str,
) -> Result<MeshData<P, C>, Vec<Unresolved>> {
match (self.mesh.parts::<P>(asset), self.clips::<C>(asset)) {
(Ok(slots), Ok(clips)) => Ok(MeshData::resolved(
self.mesh.vertices().to_vec(),
self.mesh.indices().to_vec(),
slots,
)
.posed(self.rig.clone(), clips)),
(parts, clips) => Err(parts
.err()
.into_iter()
.chain(clips.err())
.flatten()
.collect()),
}
}
fn clips<C: Clip>(&self, asset: &str) -> Result<Vec<Animation>, Vec<Unresolved>> {
let (moved, wrong): (Vec<_>, Vec<_>) = C::all()
.into_iter()
.map(|clip| self.resolve_clip(asset, clip))
.partition(Result::is_ok);
match wrong.is_empty() {
true => Ok(moved.into_iter().filter_map(Result::ok).collect()),
false => Err(wrong.into_iter().filter_map(Result::err).collect()),
}
}
fn resolve_clip<C: Clip>(&self, asset: &str, clip: C) -> Result<Animation, Unresolved> {
let named =
|animation: &&NamedAnimation| C::from_name(&animation.name).as_ref() == Some(&clip);
let matching: Vec<&NamedAnimation> = self.animations.iter().filter(named).collect();
let asset = asset.to_owned();
let clip = format!("{clip:?}");
match matching[..] {
[one] => match &one.moves {
Readable::Tracks(animation) if animation.tracks().is_empty() => {
Err(Unresolved::ClipStill {
asset,
clip,
animation: one.name.clone(),
})
}
Readable::Tracks(animation) => Ok(animation.clone()),
Readable::Repeated { node } => Err(Unresolved::ClipRepeated {
asset,
clip,
animation: one.name.clone(),
node: node.clone(),
}),
},
[] => Err(Unresolved::ClipUnnamed { asset, clip }),
[first, second, ..] => Err(Unresolved::ClipTwice {
asset,
clip,
first: first.name.clone(),
second: second.name.clone(),
}),
}
}
}
pub(crate) struct NamedAnimation {
pub(crate) name: String,
pub(crate) moves: Readable,
}
pub(crate) enum Readable {
Tracks(Animation),
Repeated { node: String },
}
#[cfg(test)]
mod tests {
use super::*;
use crate::assets::{
Assets, BEACON, CHEST, CORGI, MERGED, PROP, RIG, ROBOT, SCALED, TRACKED, file,
};
use crate::math::{Mat4, Quat, Vec3};
use crate::mesh::{Geometry, NoClips, NoParts, Placed};
macro_rules! clips {
($name:ident { $($variant:ident => $spelling:pat),+ $(,)? }) => {
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
enum $name { $($variant),+ }
impl Clip for $name {
fn from_name(name: &str) -> Option<Self> {
match name {
$($spelling => Some(Self::$variant),)+
_ => None,
}
}
fn all() -> Vec<Self> {
vec![$(Self::$variant),+]
}
fn index(&self) -> u32 {
Self::all()
.iter()
.position(|clip| clip == self)
.unwrap_or_default() as u32
}
}
};
}
clips! { Paces { Idle => "idle", Walk => "walk" } }
clips! { Moods { Idle => "idle", Attack => "attack", Dead => "dead", Walk => "walk" } }
clips! { Absent { Attack => "attack" } }
clips! { Walks { Walk => "walk" } }
clips! { Opens { Open => "open" } }
clips! { Spins { Spin => "prop_spin" } }
clips! { Dances { Dance => "Dance" } }
clips! { Either { Spin => "spin" | "spin.001" } }
clips! { Other { Spin => "spin.001" } }
fn loaded(stem: &str, source: &[u8]) -> Assets {
Assets::load([file(&format!("{stem}.glb"), source)]).expect("the fixture decodes")
}
fn model<C: Clip>(assets: &Assets, name: &str) -> Geometry {
assets
.model::<NoParts, C>(name)
.erased()
.expect("built whole")
}
fn error(assets: &Assets) -> String {
let recorded = assets
.unresolved()
.expect("something did not resolve")
.to_string();
recorded
.strip_prefix("the game's assets did not resolve: ")
.expect("every startup error opens with that")
.to_owned()
}
#[test]
fn a_model_pulls_by_the_name_of_a_root_node_and_by_no_other() {
let assets = loaded("rig", RIG);
let rig = model::<Paces>(&assets, "Rig");
assert_eq!(rig.rig().joints().len(), 3, "the three bones of the rig");
assert_eq!(rig.clips().len(), 2, "and the two clips it is posed by");
assert!(assets.unresolved().is_none());
assert!(
model::<NoClips>(&assets, "Body").rig().joints().is_empty(),
"the mesh under the root node is no model of its own"
);
assert_eq!(error(&assets), "no asset is named `Body`");
}
#[test]
fn a_root_node_and_a_mesh_of_one_name_each_read_as_their_own_kind() {
let assets = loaded("hello", BEACON);
assert_eq!(assets.mesh::<NoParts>("beacon").slots().len(), 2);
assert_eq!(
model::<NoClips>(&assets, "beacon").part_count(),
2,
"the node of that name holds the mesh of that name"
);
assert!(
assets.unresolved().is_none(),
"and neither shadows the other"
);
}
#[test]
fn a_models_joints_are_its_skin_joints_and_every_node_a_clip_moves() {
let assets = loaded("corgi", CORGI);
let corgi = model::<Moods>(&assets, "RootNode");
let joints = corgi.rig().joints();
assert_eq!(joints.len(), 16, "the sixteen bones the skin names");
assert!(
matches!(joints[0].placed, Placed::Within(_)),
"the root bone hangs under no joint"
);
assert_eq!(
joints[1].placed,
Placed::Under(0),
"the hips under the root"
);
for (at, leg) in joints.iter().enumerate().skip(2).take(5) {
assert_eq!(leg.placed, Placed::Under(1), "the legs and the spine: {at}");
}
for (at, above) in joints.iter().enumerate().skip(7) {
assert_eq!(
above.placed,
Placed::Under(6),
"everything over the spine: {at}"
);
}
assert_eq!(corgi.clips().len(), 4, "and four clips resolve by name");
for clip in corgi.clips() {
assert_eq!(clip.tracks().len(), 48, "each moving every joint");
}
assert!(assets.unresolved().is_none());
}
#[test]
fn a_skinned_node_is_posed_by_its_skin_and_never_by_its_own_transform() {
let assets = loaded("corgi", CORGI);
let corgi = model::<NoClips>(&assets, "RootNode");
let mesh = assets.mesh::<NoParts>("Corgi");
assert_eq!(
corgi.vertices(),
mesh.vertices(),
"the turn the skinned node carries reaches no vertex"
);
let above = match corgi.rig().joints()[0].placed {
Placed::Within(above) => above,
Placed::Under(_) => panic!("the root bone hangs under no joint"),
};
assert!(
above.abs_diff_eq(Mat4::IDENTITY, 1e-5),
"nor any joint: {above} stands above them"
);
}
#[test]
fn a_root_node_that_is_scaled_leaves_the_joints_at_rest_and_stands_above_them() {
let plain = model::<Paces>(&loaded("rig", RIG), "Rig");
let scaled = model::<Paces>(&loaded("scaled", SCALED), "Rig");
let placement = Mat4::from_scale_rotation_translation(
Vec3::splat(0.5),
Quat::from_rotation_y(core::f32::consts::FRAC_PI_6),
Vec3::new(1.0, 0.0, -2.0),
);
for (at, (turned, rest)) in scaled
.rig()
.joints()
.iter()
.zip(plain.rig().joints())
.enumerate()
{
assert!(
turned.rest.matrix().abs_diff_eq(rest.rest.matrix(), 1e-5),
"joint {at} rests where the same joint of the unscaled rig does"
);
}
let Placed::Within(above) = scaled.rig().joints()[0].placed else {
panic!("the root bone hangs under no joint");
};
assert!(
above.abs_diff_eq(placement, 1e-6),
"{above} is not the scale, the turn and the position of the root node"
);
assert!(
(scaled.rig().joints()[0]
.bind
.transform_vector3(Vec3::X)
.length()
- 2.0)
.abs()
< 1e-4,
"and the binds carry what the source states, the scale among it"
);
}
#[test]
fn a_part_no_skin_covers_takes_its_own_joint_whole_at_the_rest_of_its_node() {
let assets = loaded("prop", PROP);
let prop = model::<Spins>(&assets, "Crate");
let joints = prop.rig().joints();
assert_eq!(joints.len(), 2, "the cube and the child under it");
assert_eq!(joints[1].placed, Placed::Under(0));
assert!(
prop.rig()
.weights()
.iter()
.all(|vertex| vertex.weights == [1.0, 0.0, 0.0, 0.0]),
"every vertex takes one joint whole"
);
let lid: Vec<&Vec3> = prop
.vertices()
.iter()
.zip(prop.rig().weights())
.filter(|(_, taken)| taken.joints[0] == 1)
.map(|(vertex, _)| &vertex.position)
.collect();
assert!(!lid.is_empty(), "the child's own vertices take its joint");
assert!(
lid.iter().all(|position| position.y > 0.5),
"and lie where its node rests, a way up from the cube"
);
}
#[test]
fn two_meshes_that_share_one_skin_are_posed_by_the_same_joints() {
let assets = loaded("chest", CHEST);
let chest = model::<Opens>(&assets, "Chest");
assert_eq!(chest.part_count(), 2, "the two meshes of the node");
assert_eq!(
chest.rig().joints().len(),
2,
"over the two joints of one skin"
);
assert!(
chest
.rig()
.weights()
.iter()
.all(|vertex| vertex.joints.iter().all(|&joint| joint < 2)),
"which the vertices of both meshes take"
);
assert!(assets.unresolved().is_none());
}
#[test]
fn weights_read_back_summing_to_one_over_the_joints_two_skins_share() {
let assets = loaded("robot", ROBOT);
let robot = model::<Dances>(&assets, "RootNode");
let joints = robot.rig().joints().len();
assert_eq!(
joints, 55,
"one joint per node the skins name or a clip moves, never one per skin"
);
for taken in robot.rig().weights() {
let sum: f32 = taken.weights.iter().sum();
assert!(
(sum - 1.0).abs() < 1e-5,
"{sum} is what {taken:?} adds up to"
);
assert!(taken.joints.iter().all(|&joint| (joint as usize) < joints));
}
assert!(
assets.unresolved().is_none(),
"and the shapes it holds are read past"
);
}
#[test]
fn one_action_on_two_tracks_is_read_as_one_track_per_path() {
let assets = loaded("tracked", TRACKED);
let tracked = model::<Walks>(&assets, "Rig");
assert_eq!(
tracked.clips()[0].tracks().len(),
6,
"a path of a joint is one track, however many tracks state it"
);
assert!(assets.unresolved().is_none());
}
#[test]
fn a_clip_no_animation_of_the_source_resolves_stops_startup() {
let assets = loaded("rig", RIG);
assert_eq!(
assets.model::<NoParts, Absent>("Rig").slots().len(),
0,
"nothing of a model whose clips do not resolve is drawn"
);
assert_eq!(
error(&assets),
"the asset `Rig` has no animation that resolves to the clip Attack"
);
}
#[test]
fn a_clip_two_animations_resolve_stops_startup() {
let assets = loaded("merged", MERGED);
assert_eq!(assets.model::<NoParts, Either>("Alpha").slots().len(), 0);
assert_eq!(
error(&assets),
"the asset `Alpha` has two animations that resolve to the clip Spin: `spin` and \
`spin.001`"
);
}
#[test]
fn a_clip_whose_animation_moves_no_joint_of_the_model_stops_startup() {
let assets = loaded("merged", MERGED);
assert_eq!(assets.model::<NoParts, Other>("Alpha").slots().len(), 0);
assert_eq!(
error(&assets),
"the asset `Alpha` has the animation `spin.001` for the clip Spin, which moves no \
joint of it"
);
assert!(
model::<Other>(&loaded("merged", MERGED), "Beta")
.clips()
.len()
== 1,
"while the model that animation does move reads it"
);
}
#[test]
fn a_mesh_of_a_source_that_holds_skins_loads_as_a_mesh_of_any_other_does() {
let assets = loaded("corgi", CORGI);
let corgi = assets
.mesh::<NoParts>("Corgi")
.erased()
.expect("built whole");
assert_eq!(corgi.part_count(), 1);
assert!(!corgi.vertices().is_empty());
assert!(
corgi.rig().joints().is_empty() && corgi.clips().is_empty(),
"a mesh is posed by nothing"
);
assert!(assets.unresolved().is_none());
}
}