mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-12 23:48:57 +00:00
a1779fb1ec
This PR adds `cargo fmt -- --check`, `nx lint ...` and `nx test ...` as GitHub Actions. This will cause future PRs to automatically fail when formatting issues, Clippy warnings, or test failures are present in the changed code.
618 lines
17 KiB
Rust
618 lines
17 KiB
Rust
use crate::{
|
|
joint::{JointData, Offset},
|
|
Bvh, Channel, ChannelType,
|
|
};
|
|
|
|
#[doc(hidden)]
|
|
#[macro_export]
|
|
macro_rules! match_channels {
|
|
($builder:ident; ) => {
|
|
};
|
|
($builder:ident; Xposition $($rest:ident)*) => {
|
|
$builder.push_channel($crate::ChannelType::PositionX);
|
|
$crate::match_channels!($builder; $($rest)*);
|
|
};
|
|
($builder:ident; Yposition $($rest:ident)*) => {
|
|
$builder.push_channel($crate::ChannelType::PositionY);
|
|
$crate::match_channels!($builder; $($rest)*);
|
|
};
|
|
($builder:ident; Zposition $($rest:ident)*) => {
|
|
$builder.push_channel($crate::ChannelType::PositionZ);
|
|
$crate::match_channels!($builder; $($rest)*);
|
|
};
|
|
($builder:ident; Xrotation $($rest:ident)*) => {
|
|
$builder.push_channel($crate::ChannelType::RotationX);
|
|
$crate::match_channels!($builder; $($rest)*);
|
|
};
|
|
($builder:ident; Yrotation $($rest:ident)*) => {
|
|
$builder.push_channel($crate::ChannelType::RotationY);
|
|
$crate::match_channels!($builder; $($rest)*);
|
|
};
|
|
($builder:ident; Zrotation $($rest:ident)*) => {
|
|
$builder.push_channel($crate::ChannelType::RotationZ);
|
|
$crate::match_channels!($builder; $($rest)*);
|
|
};
|
|
($builder:expr; $($other:tt)*) => {
|
|
compile_error!("Unknown tokens");
|
|
};
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
#[macro_export]
|
|
macro_rules! parse_offset {
|
|
($builder:ident ($ofst_x:literal $ofst_y:literal $ofst_z:literal)) => {
|
|
let offset = [f32::from($ofst_x), f32::from($ofst_y), f32::from($ofst_z)];
|
|
$builder.push_joint_offset(offset.into(), false);
|
|
};
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
#[macro_export]
|
|
macro_rules! parse_joints_internal {
|
|
($builder:ident (
|
|
$(
|
|
JOINT $joint_nm:ident
|
|
{
|
|
$( $children:tt )*
|
|
}
|
|
)*
|
|
)) => {
|
|
$(
|
|
$builder.push_joint(stringify!($joint_nm));
|
|
|
|
$builder.current_depth += 1;
|
|
$crate::parse_joints_internal!($builder ( $( $children )* ));
|
|
$builder.current_depth -= 1;
|
|
)*
|
|
};
|
|
|
|
($builder:ident (
|
|
OFFSET $ofst_x:literal $ofst_y:literal $ofst_z:literal
|
|
CHANNELS 0
|
|
$($rest:tt)*
|
|
)) => {
|
|
$crate::parse_offset!($builder ($ofst_x $ofst_y $ofst_z));
|
|
$crate::parse_joints_internal!($builder ( $($rest)* ));
|
|
};
|
|
|
|
($builder:ident (
|
|
OFFSET $ofst_x:literal $ofst_y:literal $ofst_z:literal
|
|
CHANNELS 1 $ch0:ident
|
|
$($rest:tt)*
|
|
)) => {
|
|
$crate::parse_offset!($builder ($ofst_x $ofst_y $ofst_z));
|
|
$crate::match_channels!($builder ; $ch0);
|
|
$crate::parse_joints_internal!($builder ( $($rest)* ));
|
|
};
|
|
|
|
($builder:ident (
|
|
OFFSET $ofst_x:literal $ofst_y:literal $ofst_z:literal
|
|
CHANNELS 2 $ch0:ident $ch1:ident
|
|
$($rest:tt)*
|
|
)) => {
|
|
$crate::parse_offset!($builder ($ofst_x $ofst_y $ofst_z));
|
|
$crate::match_channels!($builder ; $ch0 $ch1);
|
|
$crate::parse_joints_internal!($builder ( $($rest)* ));
|
|
};
|
|
|
|
($builder:ident (
|
|
OFFSET $ofst_x:literal $ofst_y:literal $ofst_z:literal
|
|
CHANNELS 3 $ch0:ident $ch1:ident $ch2:ident
|
|
$($rest:tt)*
|
|
)) => {
|
|
$crate::parse_offset!($builder ($ofst_x $ofst_y $ofst_z));
|
|
$crate::match_channels!($builder ; $ch0 $ch1 $ch2);
|
|
$crate::parse_joints_internal!($builder ( $($rest)* ));
|
|
};
|
|
|
|
($builder:ident (
|
|
OFFSET $ofst_x:literal $ofst_y:literal $ofst_z:literal
|
|
CHANNELS 4 $ch0:ident $ch1:ident $ch2:ident $ch3:ident
|
|
$($rest:tt)*
|
|
)) => {
|
|
bvh_anim$crate::parse_offset!($builder ($ofst_x $ofst_y $ofst_z));
|
|
bvh_anim$crate::match_channels!($builder ; $ch0 $ch1 $ch2 $ch3);
|
|
bvh_anim$crate::parse_joints_internal!($builder ( $($rest)* ));
|
|
};
|
|
|
|
($builder:ident (
|
|
OFFSET $ofst_x:literal $ofst_y:literal $ofst_z:literal
|
|
CHANNELS 5
|
|
$ch0:ident
|
|
$ch1:ident
|
|
$ch2:ident
|
|
$ch3:ident
|
|
$ch4:ident
|
|
$($rest:tt)*
|
|
)) => {
|
|
bvh_anim::parse_offset!($builder ($ofst_x $ofst_y $ofst_z));
|
|
bvh_anim::match_channels!($builder ; $ch0 $ch1 $ch2 $ch3 $ch4);
|
|
bvh_anim::parse_joints_internal!($builder ( $($rest)* ));
|
|
};
|
|
|
|
($builder:ident (
|
|
OFFSET $ofst_x:literal $ofst_y:literal $ofst_z:literal
|
|
CHANNELS 6
|
|
$ch0:ident
|
|
$ch1:ident
|
|
$ch2:ident
|
|
$ch3:ident
|
|
$ch4:ident
|
|
$ch5:ident
|
|
$($rest:tt)*
|
|
)) => {
|
|
$crate::parse_offset!($builder ($ofst_x $ofst_y $ofst_z));
|
|
$crate::match_channels!($builder ;
|
|
$ch0 $ch1 $ch2 $ch3 $ch4 $ch5);
|
|
$crate::parse_joints_internal!($builder ( $($rest)* ));
|
|
};
|
|
|
|
($builder:ident (
|
|
CHANNELS 7
|
|
$ch0:ident
|
|
$ch1:ident
|
|
$ch2:ident
|
|
$ch3:ident
|
|
$ch4:ident
|
|
$ch5:ident
|
|
$ch6:ident
|
|
$($rest:tt)*
|
|
)) => {
|
|
$crate::parse_offset!($builder ($ofst_x $ofst_y $ofst_z));
|
|
$crate::match_channels!($builder ;
|
|
$ch0 $ch1 $ch2 $ch3 $ch4 $ch5 $ch6);
|
|
$crate::parse_joints_internal!($builder ( $($rest)* ));
|
|
};
|
|
|
|
($builder:ident (
|
|
OFFSET $ofst_x:literal $ofst_y:literal $ofst_z:literal
|
|
CHANNELS 8
|
|
$ch0:ident
|
|
$ch1:ident
|
|
$ch2:ident
|
|
$ch3:ident
|
|
$ch4:ident
|
|
$ch5:ident
|
|
$ch6:ident
|
|
$ch7:ident
|
|
$($rest:tt)*
|
|
)) => {
|
|
$crate::parse_offset!($builder ($ofst_x $ofst_y $ofst_z));
|
|
$crate::match_channels!($builder ;
|
|
$ch0 $ch1 $ch2 $ch3 $ch4 $ch5 $ch6 $ch7);
|
|
$crate::parse_joints_internal!($builder ( $($rest)* ));
|
|
};
|
|
|
|
($builder:ident (
|
|
OFFSET $ofst_x:literal $ofst_y:literal $ofst_z:literal
|
|
CHANNELS 9
|
|
$ch0:ident
|
|
$ch1:ident
|
|
$ch2:ident
|
|
$ch3:ident
|
|
$ch4:ident
|
|
$ch5:ident
|
|
$ch6:ident
|
|
$ch7:ident
|
|
$ch8:ident
|
|
$($rest:tt)*
|
|
)) => {
|
|
$crate::parse_offset!($builder ($ofst_x $ofst_y $ofst_z));
|
|
$crate::match_channels!($builder ;
|
|
$ch0 $ch1 $ch2 $ch3 $ch4 $ch5 $ch6 $ch7 $ch8);
|
|
$crate::parse_joints_internal!($builder ( $($rest)* ));
|
|
};
|
|
|
|
($builder:ident (
|
|
OFFSET $ofst_x:literal $ofst_y:literal $ofst_z:literal
|
|
CHANNELS $unsupported:literal $($rest:tt)*
|
|
)) => {
|
|
compile_error!("No more than 9 channels supported in CHANNELS statement");
|
|
};
|
|
|
|
($builder:ident (
|
|
End Site
|
|
{
|
|
OFFSET $end_x:literal $end_y:literal $end_z:literal
|
|
}
|
|
)) => {
|
|
let offset = [
|
|
f32::from($end_x),
|
|
f32::from($end_y),
|
|
f32::from($end_z),
|
|
];
|
|
|
|
$builder.push_joint_offset(offset.into() , true);
|
|
};
|
|
}
|
|
|
|
/// Create a new [`Bvh`][`Bvh`] object using a macro literal. Useful for
|
|
/// testing.
|
|
///
|
|
/// # Notes
|
|
///
|
|
/// If you have a very complex `Bvh` file with a large number of joints and frames, then
|
|
/// this macro will scale badly to it, and compilation time will suffer.
|
|
///
|
|
/// # Example
|
|
///
|
|
/// ```
|
|
/// # use studio_bvh::bvh;
|
|
/// let simple_skeleton = bvh! {
|
|
/// HIERARCHY
|
|
/// ROOT Base
|
|
/// {
|
|
/// OFFSET 0.0 0.0 0.0
|
|
/// CHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation
|
|
/// JOINT Middle1
|
|
/// {
|
|
/// OFFSET 0.0 0.0 15.0
|
|
/// CHANNELS 3 Zrotation Xrotation Yrotation
|
|
/// JOINT Tip1
|
|
/// {
|
|
/// OFFSET 0.0 0.0 30.0
|
|
/// CHANNELS 3 Zrotation Xrotation Yrotation
|
|
/// End Site
|
|
/// {
|
|
/// OFFSET 0.0 0.0 45.0
|
|
/// }
|
|
/// }
|
|
/// }
|
|
/// JOINT Middle2
|
|
/// {
|
|
/// OFFSET 0.0 15.0 0.0
|
|
/// CHANNELS 3 Zrotation Xrotation Yrotation
|
|
/// JOINT Tip2
|
|
/// {
|
|
/// OFFSET 0.0 30.0 0.0
|
|
/// CHANNELS 3 Zrotation Xrotation Yrotation
|
|
/// End Site
|
|
/// {
|
|
/// OFFSET 0.0 45.0 0.0
|
|
/// }
|
|
/// }
|
|
/// }
|
|
/// }
|
|
///
|
|
/// MOTION
|
|
/// Frames: 3
|
|
/// // Time in seconds.
|
|
/// Frame Time: 0.033333333333
|
|
/// 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
|
|
/// 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0
|
|
/// 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0 2.0
|
|
/// };
|
|
/// ```
|
|
///
|
|
/// You can use the `bvh` macro to create empty `Bvh` instances:
|
|
///
|
|
/// ```
|
|
/// # use studio_bvh::bvh;
|
|
/// let empty = bvh!{};
|
|
///
|
|
/// let empty = bvh! {
|
|
/// HIERARCHY
|
|
/// MOTION
|
|
/// };
|
|
///
|
|
/// let empty = bvh! {
|
|
/// HIERARCHY
|
|
/// MOTION
|
|
/// Frames: 0
|
|
/// };
|
|
/// ```
|
|
///
|
|
/// [`bvh`]: struct.Bvh.html
|
|
#[macro_export]
|
|
macro_rules! bvh {
|
|
() => {
|
|
$crate::Bvh::default()
|
|
};
|
|
|
|
(
|
|
HIERARCHY
|
|
MOTION
|
|
) => {
|
|
$crate::Bvh::default()
|
|
};
|
|
|
|
(
|
|
HIERARCHY
|
|
MOTION
|
|
Frames: 0
|
|
) => {
|
|
$crate::Bvh::default()
|
|
};
|
|
|
|
(
|
|
HIERARCHY
|
|
MOTION
|
|
Frames: 0
|
|
Frame Time: $frame_time:literal
|
|
) => {
|
|
{
|
|
use std::time::Duration;
|
|
let mut bvh = $crate::Bvh::default();
|
|
bvh.set_frame_time(Duration::from_secs_f64(f64::from($frame_time)));
|
|
bvh
|
|
}
|
|
};
|
|
|
|
(
|
|
HIERARCHY
|
|
ROOT $root_name:ident
|
|
{
|
|
$( $joints:tt )*
|
|
}
|
|
MOTION
|
|
Frames: 0
|
|
Frame Time: $frame_time:literal
|
|
) => {
|
|
{
|
|
use std::time::Duration;
|
|
use $crate::parse_joints_internal;
|
|
|
|
let mut builder = $crate::BvhLiteralBuilder::default();
|
|
builder.push_root(stringify!($root_name));
|
|
|
|
builder.current_depth += 1;
|
|
parse_joints_internal!(builder ($($joints)*));
|
|
builder.current_depth -= 1;
|
|
|
|
builder.set_num_channels();
|
|
builder.bvh.set_frame_time(Duration::from_secs_f64(f64::from($frame_time)));
|
|
|
|
builder.bvh
|
|
}
|
|
};
|
|
|
|
(
|
|
HIERARCHY
|
|
ROOT $root_name:ident
|
|
{
|
|
$( $joints:tt )*
|
|
}
|
|
MOTION
|
|
Frames: $num_frames:literal
|
|
Frame Time: $frame_time:literal
|
|
$(
|
|
$motion:literal
|
|
)+
|
|
) => {
|
|
{
|
|
use $crate::bvh;
|
|
|
|
let mut new_bvh = bvh! {
|
|
HIERARCHY
|
|
ROOT $root_name
|
|
{
|
|
$( $joints )*
|
|
}
|
|
MOTION
|
|
Frames: 0
|
|
Frame Time: $frame_time
|
|
};
|
|
|
|
let num_channels = new_bvh.num_channels();
|
|
|
|
new_bvh
|
|
.frame_cursor()
|
|
.try_insert_frames([ $( f32::from($motion) ),+ ].chunks(num_channels))
|
|
.expect("Could not create bvh type from macro literal");
|
|
|
|
assert_eq!(new_bvh.frames().len(), $num_frames as usize);
|
|
|
|
new_bvh
|
|
}
|
|
};
|
|
}
|
|
|
|
// @TODO: refactor this into a general `Builder` so that we have an
|
|
// easy interface to get other animation/skeleton data into a `bvh`
|
|
// file.
|
|
/// Helper struct to build a `Bvh` from the macro without exposing
|
|
/// too many internals.
|
|
#[doc(hidden)]
|
|
#[derive(Default)]
|
|
pub struct BvhLiteralBuilder {
|
|
pub bvh: Bvh,
|
|
pub current_channel_index: usize,
|
|
pub current_depth: usize,
|
|
pub current_index: usize,
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
impl BvhLiteralBuilder {
|
|
pub fn push_root(&mut self, name: &str) {
|
|
let mut root = JointData::empty_root();
|
|
root.set_name(name);
|
|
self.bvh.joints.push(root);
|
|
self.current_index += 1;
|
|
}
|
|
|
|
pub fn push_joint(&mut self, name: &str) {
|
|
// @TODO: make this shared
|
|
#[inline]
|
|
fn get_parent_index(joints: &[JointData], for_depth: usize) -> usize {
|
|
joints
|
|
.iter()
|
|
.rev()
|
|
.find(|jd| jd.depth() == for_depth.saturating_sub(1))
|
|
.and_then(|jd| jd.private_data().map(|p| p.self_index))
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
let idx = self.current_index;
|
|
let dpth = self.current_depth;
|
|
let parent = get_parent_index(&self.bvh.joints[..], dpth);
|
|
|
|
let mut joint = JointData::empty_child();
|
|
joint.set_name(name);
|
|
if let Some(ref mut private) = joint.private_data_mut() {
|
|
private.self_index = idx;
|
|
private.depth = dpth;
|
|
private.parent_index = parent;
|
|
};
|
|
|
|
self.bvh.joints.push(joint);
|
|
|
|
self.current_index += 1;
|
|
}
|
|
|
|
pub fn push_channel(&mut self, channel: ChannelType) {
|
|
let channel = Channel::new(channel, self.current_channel_index);
|
|
if let Some(joint) = self.last_joint() {
|
|
match *joint {
|
|
JointData::Root {
|
|
ref mut channels, ..
|
|
} => {
|
|
channels.push(channel);
|
|
}
|
|
JointData::Child {
|
|
ref mut channels, ..
|
|
} => {
|
|
channels.push(channel);
|
|
}
|
|
}
|
|
}
|
|
self.current_channel_index += 1;
|
|
}
|
|
|
|
pub fn push_joint_offset(&mut self, offset: Offset, is_end_site: bool) {
|
|
if let Some(joint) = self.last_joint() {
|
|
joint.set_offset(offset, is_end_site);
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
pub fn set_num_channels(&mut self) {
|
|
self.bvh.num_channels = self.current_channel_index;
|
|
}
|
|
|
|
#[inline]
|
|
fn last_joint(&mut self) -> Option<&mut JointData> {
|
|
self.bvh.joints.last_mut()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
// Needed for macros
|
|
use std::time::Duration;
|
|
|
|
// #[test] // FIXME
|
|
fn macro_create() {
|
|
let bvh = bvh! {
|
|
HIERARCHY
|
|
ROOT Base
|
|
{
|
|
OFFSET 0.0 0.0 0.0
|
|
CHANNELS 6 Xposition Yposition Zposition Zrotation Xrotation Yrotation
|
|
JOINT End
|
|
{
|
|
OFFSET 0.0 0.0 15.0
|
|
CHANNELS 3 Zrotation Xrotation Yrotation
|
|
End Site
|
|
{
|
|
OFFSET 0.0 0.0 30.0
|
|
}
|
|
}
|
|
}
|
|
|
|
MOTION
|
|
Frames: 1
|
|
Frame Time: 0.033333333333
|
|
0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
|
|
};
|
|
|
|
{
|
|
use super::{ChannelType, JointData, Offset};
|
|
|
|
fn check_joint<O: Into<Option<Offset>>>(
|
|
joint: &JointData,
|
|
expected_name: &str,
|
|
expected_offset: Offset,
|
|
channels: &[ChannelType],
|
|
end_site: O,
|
|
) {
|
|
let name = match joint.name_str() {
|
|
Ok(name) => name,
|
|
Err(err) => panic!("{err}"),
|
|
};
|
|
assert_eq!(name, expected_name);
|
|
assert_eq!(*joint.offset(), expected_offset);
|
|
for (chan, expected_chan) in joint
|
|
.channels()
|
|
.iter()
|
|
.map(|c| c.channel_type())
|
|
.zip(channels.iter())
|
|
{
|
|
assert_eq!(chan, *expected_chan);
|
|
}
|
|
let end_site = end_site.into().map(Into::into);
|
|
assert_eq!(joint.end_site(), end_site.as_ref());
|
|
}
|
|
|
|
let mut joints = bvh.joints();
|
|
|
|
check_joint(
|
|
joints.next().unwrap().data(),
|
|
"Base",
|
|
[0.0, 0.0, 0.0],
|
|
&[
|
|
ChannelType::PositionX,
|
|
ChannelType::PositionY,
|
|
ChannelType::PositionZ,
|
|
ChannelType::RotationZ,
|
|
ChannelType::RotationX,
|
|
ChannelType::RotationY,
|
|
],
|
|
None,
|
|
);
|
|
|
|
check_joint(
|
|
joints.next().unwrap().data(),
|
|
"End",
|
|
[0.0, 0.0, 15.0],
|
|
&[
|
|
ChannelType::RotationZ,
|
|
ChannelType::RotationX,
|
|
ChannelType::RotationY,
|
|
],
|
|
[0.0, 0.0, 30.0],
|
|
);
|
|
}
|
|
|
|
assert_eq!(*bvh.frame_time(), Duration::from_nanos(33333333));
|
|
|
|
let frames = bvh.frames();
|
|
assert_eq!(frames.len(), 1);
|
|
for frame in frames {
|
|
assert_eq!(frame.len(), 9);
|
|
for channel in frame.as_slice().iter() {
|
|
assert_eq!(*channel, 0.0);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_empty_create() {
|
|
macro_rules! assert_empty {
|
|
($bvh:expr) => {
|
|
assert!($bvh.joints().next().is_none());
|
|
assert_eq!(*$bvh.frame_time(), Duration::default());
|
|
assert!($bvh.frames().next().is_none());
|
|
};
|
|
}
|
|
|
|
let empty = bvh! {};
|
|
assert_empty!(empty);
|
|
|
|
let empty_2 = bvh! {
|
|
HIERARCHY
|
|
MOTION
|
|
};
|
|
assert_empty!(empty_2);
|
|
}
|
|
}
|