use crate::{Clip, Component, Entity, Geometry, JointId, JsonError, RigInstance, Skeleton, World};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeSet;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ControllerParameterKind {
Float,
Int,
Bool,
Trigger,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ControllerParameter {
pub name: String,
pub kind: ControllerParameterKind,
pub default: f64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum LayerBlending {
Override,
Additive,
Other(i64),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum Motion {
Clip(Entity),
Pending(Entity),
Blend1D {
parameter: usize,
children: Vec<(f32, Motion)>,
},
Unsupported(String),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ControllerState {
pub name: String,
pub motion: Motion,
pub speed: f64,
pub speed_parameter: Option<usize>,
pub cycle_offset: f32,
pub cycle_offset_parameter: Option<usize>,
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct MaskedJoint {
pub skeleton: Entity,
#[serde(deserialize_with = "joint_id")]
pub joint: JointId,
pub weight: f32,
}
fn joint_id<'de, D: serde::Deserializer<'de>>(d: D) -> Result<JointId, D::Error> {
u32::deserialize(d).map(JointId)
}
fn stated<'de, D: serde::Deserializer<'de>, T: Deserialize<'de>>(d: D) -> Result<Option<T>, D::Error> {
Option::<T>::deserialize(d)
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum LayerMask {
Joints(Vec<MaskedJoint>),
Unsupported(String),
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ControllerLayer {
pub name: String,
pub states: Vec<ControllerState>,
pub default_state: Option<usize>,
pub default_weight: f32,
#[serde(deserialize_with = "stated")]
pub weight_parameter: Option<usize>,
pub blending: LayerBlending,
#[serde(deserialize_with = "stated")]
pub mask: Option<LayerMask>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Controller {
pub name: String,
pub parameters: Vec<ControllerParameter>,
pub layers: Vec<ControllerLayer>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ControllerBinding {
pub controller: Entity,
pub rig: Entity,
}
impl Controller {
fn numeric_parameter(&self, index: usize) -> Result<(), String> {
let parameter = self
.parameters
.get(index)
.ok_or_else(|| format!("parameter index {index} is out of range"))?;
if !matches!(
parameter.kind,
ControllerParameterKind::Float | ControllerParameterKind::Int
) {
return Err(format!(
"parameter {} is {:?}, not numeric",
parameter.name, parameter.kind
));
}
Ok(())
}
}
impl Component for Controller {
const NAME: &'static str = "core.controller";
const GEOMETRY: Geometry = Geometry::None;
fn to_json(&self) -> Result<Option<Value>, JsonError> {
crate::json::to_value(self).map(Some)
}
fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
crate::world::Linearity::Invariant
}
fn references(&self, out: &mut Vec<Entity>) {
let mut pending: Vec<_> = self
.layers
.iter()
.flat_map(|layer| layer.states.iter().map(|state| &state.motion))
.collect();
while let Some(motion) = pending.pop() {
match motion {
Motion::Clip(clip) | Motion::Pending(clip) => out.push(*clip),
Motion::Blend1D { children, .. } => pending.extend(children.iter().map(|(_, motion)| motion)),
Motion::Unsupported(_) => {}
}
}
for layer in &self.layers {
if let Some(LayerMask::Joints(joints)) = &layer.mask {
out.extend(joints.iter().map(|j| j.skeleton));
}
}
}
fn verify(&self, _e: Entity, w: &World) -> Result<(), String> {
if self.name.trim().is_empty() {
return Err("empty controller name".into());
}
let mut names = BTreeSet::new();
for parameter in &self.parameters {
if parameter.name.trim().is_empty() || !names.insert(¶meter.name) {
return Err(format!(
"empty or duplicate controller parameter {:?}",
parameter.name
));
}
if !parameter.default.is_finite() {
return Err(format!("{}: non-finite default", parameter.name));
}
match parameter.kind {
ControllerParameterKind::Int if parameter.default.fract() != 0. => {
return Err(format!("{}: fractional Int default", parameter.name));
}
ControllerParameterKind::Bool | ControllerParameterKind::Trigger
if parameter.default != 0. && parameter.default != 1. =>
{
return Err(format!("{}: Bool/Trigger default is not 0 or 1", parameter.name));
}
_ => {}
}
}
for (layer_index, layer) in self.layers.iter().enumerate() {
if layer.name.trim().is_empty() {
return Err(format!("layer {layer_index}: empty name"));
}
if !layer.default_weight.is_finite() {
return Err(format!("layer {layer_index}: non-finite weight"));
}
if let Some(parameter) = layer.weight_parameter {
self.numeric_parameter(parameter)
.map_err(|why| format!("layer {layer_index}: weight {why}"))?;
}
match &layer.mask {
None => {}
Some(LayerMask::Unsupported(why)) if why.trim().is_empty() => {
return Err(format!("layer {layer_index}: unsupported mask has no reason"));
}
Some(LayerMask::Unsupported(_)) => {}
Some(LayerMask::Joints(joints)) => {
let mut seen = BTreeSet::new();
for masked in joints {
if !masked.weight.is_finite() || !(0.0..=1.0).contains(&masked.weight) {
return Err(format!(
"layer {layer_index}: mask weight {} is not in 0 to 1",
masked.weight
));
}
if !seen.insert((masked.skeleton, masked.joint)) {
return Err(format!(
"layer {layer_index}: joint {:?} is masked twice",
masked.joint
));
}
let skeleton = w.get::<Skeleton>(masked.skeleton).ok_or_else(|| {
format!(
"layer {layer_index}: mask skeleton {:?} holds no core.skeleton",
masked.skeleton
)
})?;
if skeleton.index_of(masked.joint).is_none() {
return Err(format!(
"layer {layer_index}: masked joint {:?} is not in skeleton {:?}",
masked.joint, masked.skeleton
));
}
}
}
}
match layer.default_state {
Some(default) if default >= layer.states.len() => {
return Err(format!("layer {layer_index}: default state is out of range"));
}
None if !layer.states.is_empty() => {
return Err(format!("layer {layer_index}: has states but no default state"));
}
_ => {}
}
for (state_index, state) in layer.states.iter().enumerate() {
if state.name.trim().is_empty() {
return Err(format!("layer {layer_index} state {state_index}: empty name"));
}
if !state.speed.is_finite() || !state.cycle_offset.is_finite() {
return Err(format!(
"layer {layer_index} state {state_index}: non-finite speed/offset"
));
}
for parameter in [state.speed_parameter, state.cycle_offset_parameter]
.into_iter()
.flatten()
{
self.numeric_parameter(parameter)?;
}
let mut pending = vec![&state.motion];
while let Some(motion) = pending.pop() {
match motion {
Motion::Clip(clip) => {
if !w.has::<Clip>(*clip) {
return Err(format!("motion {clip:?} holds no core.clip"));
}
}
Motion::Blend1D { parameter, children } => {
self.numeric_parameter(*parameter)?;
if children.is_empty() {
return Err("1D blend has no children".into());
}
if !children.iter().all(|(threshold, _)| threshold.is_finite())
|| !children.windows(2).all(|pair| pair[0].0 < pair[1].0)
{
return Err(
"1D blend thresholds must be finite and strictly increasing".into()
);
}
pending.extend(children.iter().map(|(_, motion)| motion));
}
Motion::Pending(_) => {}
Motion::Unsupported(why) => {
if why.trim().is_empty() {
return Err("unsupported motion has no reason".into());
}
}
}
}
}
}
Ok(())
}
}
impl Component for ControllerBinding {
const NAME: &'static str = "core.controller_binding";
const GEOMETRY: Geometry = Geometry::None;
fn to_json(&self) -> Result<Option<Value>, JsonError> {
crate::json::to_value(self).map(Some)
}
fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
crate::world::Linearity::Invariant
}
fn references(&self, out: &mut Vec<Entity>) {
out.extend([self.controller, self.rig]);
}
fn verify(&self, _e: Entity, w: &World) -> Result<(), String> {
if !w.has::<Controller>(self.controller) {
return Err(format!("{:?} holds no core.controller", self.controller));
}
if !w.has::<RigInstance>(self.rig) {
return Err(format!("{:?} holds no core.rig_instance", self.rig));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Extrapolation, Transform};
fn fixture() -> (World, Entity, Controller) {
let mut w = World::new();
let clip = w.spawn();
w.insert(
clip,
Clip {
name: "motion".into(),
duration: 1.,
extrapolation: Extrapolation::Hold,
channels: vec![],
root_motion: None,
additive: None,
},
);
let controller = w.spawn();
let value = Controller {
name: "character".into(),
parameters: vec![
ControllerParameter {
name: "speed".into(),
kind: ControllerParameterKind::Float,
default: 0.,
},
ControllerParameter {
name: "index".into(),
kind: ControllerParameterKind::Int,
default: 1.,
},
ControllerParameter {
name: "flag".into(),
kind: ControllerParameterKind::Bool,
default: 0.,
},
ControllerParameter {
name: "fire".into(),
kind: ControllerParameterKind::Trigger,
default: 1.,
},
],
layers: vec![ControllerLayer {
name: "base".into(),
default_state: Some(0),
default_weight: 0.75,
weight_parameter: None,
blending: LayerBlending::Override,
mask: None,
states: vec![ControllerState {
name: "idle".into(),
speed: -1.,
speed_parameter: Some(0),
cycle_offset: 1.5,
cycle_offset_parameter: Some(1),
motion: Motion::Blend1D {
parameter: 0,
children: vec![
(-1., Motion::Clip(clip)),
(
1.,
Motion::Unsupported("2D tree remains in source dialect".into()),
),
],
},
}],
}],
};
(w, controller, value)
}
#[test]
fn validates_parameter_defaults_without_rounding_or_clamping() {
let (w, e, c) = fixture();
assert!(c.verify(e, &w).is_ok());
for (index, value) in [(0, f64::NAN), (0, f64::INFINITY), (1, 1.5), (2, 2.), (3, -1.)] {
let mut bad = c.clone();
bad.parameters[index].default = value;
assert!(bad.verify(e, &w).is_err());
}
let mut bad = c.clone();
bad.parameters[1].name = "speed".into();
assert!(bad.verify(e, &w).is_err());
let mut bad = c.clone();
bad.parameters[1].name.clear();
assert!(bad.verify(e, &w).is_err());
}
#[test]
fn numeric_refs_blends_and_default_states_are_checked() {
let (w, e, c) = fixture();
for index in [2, 3, 4] {
let mut bad = c.clone();
bad.layers[0].states[0].speed_parameter = Some(index);
assert!(bad.verify(e, &w).is_err());
let mut bad = c.clone();
bad.layers[0].states[0].cycle_offset_parameter = Some(index);
assert!(bad.verify(e, &w).is_err());
let mut bad = c.clone();
bad.layers[0].states[0].motion = Motion::Blend1D {
parameter: index,
children: vec![(0., Motion::Unsupported("not lowered".into()))],
};
assert!(bad.verify(e, &w).is_err());
}
for thresholds in [vec![], vec![1., 1.], vec![1., 0.], vec![f32::NAN]] {
let mut bad = c.clone();
bad.layers[0].states[0].motion = Motion::Blend1D {
parameter: 0,
children: thresholds
.into_iter()
.map(|t| (t, Motion::Unsupported("not lowered".into())))
.collect(),
};
assert!(bad.verify(e, &w).is_err());
}
let mut bad = c.clone();
bad.layers[0].default_state = Some(1);
assert!(bad.verify(e, &w).is_err());
let mut bad = c.clone();
bad.layers[0].default_state = None;
assert!(
bad.verify(e, &w)
.unwrap_err()
.contains("has states but no default state")
);
let mut bad = c.clone();
bad.layers[0].default_weight = f32::NAN;
assert!(bad.verify(e, &w).is_err());
let mut bad = c.clone();
bad.layers[0].states[0].speed = f64::INFINITY;
assert!(bad.verify(e, &w).is_err());
let mut bad = c;
bad.layers[0].states[0].cycle_offset = f32::INFINITY;
assert!(bad.verify(e, &w).is_err());
}
#[test]
fn recursive_motion_references_require_clips_and_keep_named_refusals() {
let (mut w, e, mut c) = fixture();
let absent = w.spawn();
c.layers[0].states[0].motion = Motion::Blend1D {
parameter: 0,
children: vec![(
0.,
Motion::Blend1D {
parameter: 1,
children: vec![(0., Motion::Clip(absent))],
},
)],
};
let mut refs = Vec::new();
c.references(&mut refs);
assert_eq!(refs, vec![absent]);
assert!(c.verify(e, &w).is_err());
c.layers[0].states[0].motion = Motion::Unsupported(String::new());
assert!(c.verify(e, &w).is_err());
c.layers[0].states[0].motion = Motion::Unsupported("Direct blend is not lowered".into());
assert!(c.verify(e, &w).is_ok());
}
#[test]
fn an_empty_layer_has_no_default_state() {
let (w, e, mut c) = fixture();
let mut empty = c.layers[0].clone();
empty.name = "empty".into();
empty.states.clear();
empty.default_state = None;
c.layers.push(empty);
assert!(c.verify(e, &w).is_ok());
let json = c.to_json().unwrap().unwrap();
assert_eq!(json["layers"][0]["default_state"], 0);
assert!(json["layers"][1]["default_state"].is_null());
assert_eq!(serde_json::from_value::<Controller>(json).unwrap(), c);
c.layers[1].default_state = Some(0);
assert!(
c.verify(e, &w)
.unwrap_err()
.contains("default state is out of range")
);
}
fn skeleton_of(w: &mut World, joints: u32) -> Entity {
let e = w.spawn();
w.insert(
e,
Skeleton {
joints: (0..joints)
.map(|i| crate::Joint {
id: JointId(i),
name: format!("j{i}"),
parent: i.checked_sub(1).map(JointId),
rest: crate::Rest::Trs {
translation: [0.; 3],
rotation: [0., 0., 0., 1.],
scale: [1.; 3],
},
})
.collect(),
},
);
e
}
fn masked(skeleton: Entity, joint: u32, weight: f32) -> MaskedJoint {
MaskedJoint {
skeleton,
joint: JointId(joint),
weight,
}
}
#[test]
fn a_layer_mask_is_checked_against_the_skeletons_it_names() {
let (mut w, e, mut c) = fixture();
let a = skeleton_of(&mut w, 3);
let b = skeleton_of(&mut w, 2);
c.layers[0].mask = Some(LayerMask::Joints(vec![
masked(a, 0, 1.),
masked(a, 2, 0.25),
masked(b, 0, 0.),
]));
c.layers[0].weight_parameter = Some(0);
assert!(c.verify(e, &w).is_ok());
let mut refs = Vec::new();
c.references(&mut refs);
assert!(refs.contains(&a) && refs.contains(&b));
for (mask, why) in [
(vec![masked(a, 0, 1.), masked(a, 0, 0.5)], "masked twice"),
(vec![masked(a, 0, 1.0001)], "not in 0 to 1"),
(vec![masked(a, 0, -0.5)], "not in 0 to 1"),
(vec![masked(a, 0, f32::NAN)], "not in 0 to 1"),
(vec![masked(a, 3, 1.)], "is not in skeleton"),
(vec![masked(b, 2, 1.)], "is not in skeleton"),
(vec![masked(e, 0, 1.)], "holds no core.skeleton"),
] {
let mut bad = c.clone();
bad.layers[0].mask = Some(LayerMask::Joints(mask));
let err = bad.verify(e, &w).unwrap_err();
assert!(err.contains(why), "{err}");
}
let mut bad = c.clone();
bad.layers[0].mask = Some(LayerMask::Unsupported(" ".into()));
assert!(bad.verify(e, &w).unwrap_err().contains("no reason"));
bad.layers[0].mask = Some(LayerMask::Unsupported("a humanoid body mask".into()));
assert!(bad.verify(e, &w).is_ok());
for index in [2, 3, 4] {
let mut bad = c.clone();
bad.layers[0].weight_parameter = Some(index);
assert!(bad.verify(e, &w).unwrap_err().contains("weight"));
}
}
#[test]
fn a_layer_mask_round_trips_and_its_absence_is_not_unmasked() {
let (mut w, _, mut c) = fixture();
let a = skeleton_of(&mut w, 2);
c.layers[0].weight_parameter = Some(0);
c.layers[0].mask = Some(LayerMask::Joints(vec![masked(a, 1, 0.5)]));
let json = c.to_json().unwrap().unwrap();
assert_eq!(serde_json::from_value::<Controller>(json.clone()).unwrap(), c);
c.layers[0].mask = Some(LayerMask::Unsupported("why".into()));
let json = c.to_json().unwrap().unwrap();
assert_eq!(
json["layers"][0]["mask"],
serde_json::json!({"Unsupported": "why"})
);
assert_eq!(serde_json::from_value::<Controller>(json).unwrap(), c);
c.layers[0].mask = None;
c.layers[0].weight_parameter = None;
let json = c.to_json().unwrap().unwrap();
assert!(json["layers"][0]["mask"].is_null() && json["layers"][0]["weight_parameter"].is_null());
assert_eq!(serde_json::from_value::<Controller>(json.clone()).unwrap(), c);
for field in ["mask", "weight_parameter"] {
let mut stripped = json.clone();
stripped["layers"][0].as_object_mut().unwrap().remove(field);
assert!(serde_json::from_value::<Controller>(stripped).is_err(), "{field}");
}
}
#[test]
fn controller_and_binding_json_round_trip_keep_entity_ids_and_layer_modes() {
let (mut w, e, mut c) = fixture();
for blending in [
LayerBlending::Override,
LayerBlending::Additive,
LayerBlending::Other(17),
] {
c.layers[0].blending = blending;
assert_eq!(
serde_json::from_value::<Controller>(c.to_json().unwrap().unwrap()).unwrap(),
c
);
}
w.insert(e, c.clone());
let rig = w.spawn();
let binding = ControllerBinding { controller: e, rig };
assert!(binding.verify(e, &w).is_err());
let skeleton = w.spawn();
w.insert(rig, RigInstance { skeleton });
w.insert(rig, Transform::IDENTITY);
assert!(binding.verify(e, &w).is_ok());
let mut refs = Vec::new();
binding.references(&mut refs);
assert_eq!(refs, vec![e, rig]);
assert_eq!(
serde_json::from_value::<ControllerBinding>(binding.to_json().unwrap().unwrap()).unwrap(),
binding
);
assert!(c.remap_primitives(&[0]).is_none());
assert!(binding.remap_primitives(&[0]).is_none());
let mut json = c.to_json().unwrap().unwrap();
json["forgotten_source_field"] = serde_json::json!(true);
assert!(serde_json::from_value::<Controller>(json).is_err());
let mut invalid = binding;
invalid.controller = skeleton;
assert!(invalid.verify(e, &w).is_err());
}
}