mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-12 21:51:30 +00:00
1687bfd6ed
This PR adds support for BVH animation files, with a custom animation player and retargeting solution for our Blender Rigify-based skeletal meshes. --------- Co-authored-by: Brooks Palin <brookspalin710@gmail.com>
509 lines
18 KiB
Rust
509 lines
18 KiB
Rust
//! Contains options for `bvh` file formatting.
|
|
|
|
use crate::{frames::Frames, Bvh, Joint, Joints};
|
|
use smallvec::SmallVec;
|
|
use std::{
|
|
fmt,
|
|
io::{self, Write},
|
|
iter, mem,
|
|
num::NonZeroUsize,
|
|
};
|
|
|
|
/// Specify formatting options for writing a `Bvh`.
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
|
#[non_exhaustive]
|
|
pub struct WriteOptions {
|
|
/// Which indentation style to use for nested bones.
|
|
pub indent: IndentStyle,
|
|
/// Which style new line terminator to use when writing the `bvh`.
|
|
pub line_terminator: LineTerminator,
|
|
/// Number of significant figures to use when writing `OFFSET` values.
|
|
///
|
|
/// If this is `None`, then the minimum precision required will be used
|
|
pub offset_significant_figures: Option<usize>,
|
|
/// Number of significant figures to use when writing the `Frame Time` value.
|
|
///
|
|
/// If this is `None`, then the minimum precision required will be used
|
|
pub frame_time_significant_figures: Option<usize>,
|
|
/// Number of significant figures to use when writing `MOTION` values.
|
|
///
|
|
/// If this is `None`, then the minimum precision required will be used
|
|
pub motion_values_significant_figures: Option<usize>,
|
|
}
|
|
|
|
impl Default for WriteOptions {
|
|
#[inline]
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl WriteOptions {
|
|
/// Create a new `WriteOptions` with default values.
|
|
#[inline]
|
|
pub const fn new() -> Self {
|
|
WriteOptions {
|
|
indent: IndentStyle::Tabs,
|
|
line_terminator: LineTerminator::native(),
|
|
offset_significant_figures: None,
|
|
frame_time_significant_figures: None,
|
|
motion_values_significant_figures: None,
|
|
}
|
|
}
|
|
|
|
/// Output the `Bvh` file to the `writer` with the given options.
|
|
pub fn write<W: Write>(&self, bvh: &Bvh, writer: &mut W) -> io::Result<()> {
|
|
let mut curr_chunk = vec![];
|
|
let mut curr_bytes_written = 0usize;
|
|
let mut curr_string_len = 0usize;
|
|
let mut iter_state = WriteOptionsIterState::new();
|
|
|
|
while self.next_chunk(bvh, &mut curr_chunk, &mut iter_state) {
|
|
let bytes: &[u8] = curr_chunk.as_ref();
|
|
curr_string_len += bytes.len();
|
|
curr_bytes_written += writer.write(bytes)?;
|
|
|
|
if curr_bytes_written != curr_string_len {
|
|
return Err(io::Error::new(
|
|
io::ErrorKind::Other,
|
|
"Data has been dropped while writing to file",
|
|
));
|
|
}
|
|
}
|
|
writer.flush()
|
|
}
|
|
|
|
/// Output the `Bvh` file to the `string` with the given options.
|
|
pub fn write_to_string(&self, bvh: &Bvh) -> Vec<u8> {
|
|
let mut curr_chunk = vec![];
|
|
let mut out_string = vec![];
|
|
let mut iter_state = WriteOptionsIterState::new();
|
|
|
|
while self.next_chunk(bvh, &mut curr_chunk, &mut iter_state) {
|
|
out_string.append(&mut curr_chunk);
|
|
}
|
|
|
|
out_string
|
|
}
|
|
|
|
/// Sets `indent` on `self` to the new `IndentStyle`.
|
|
#[inline]
|
|
pub const fn with_indent(self, indent: IndentStyle) -> Self {
|
|
Self { indent, ..self }
|
|
}
|
|
|
|
/// Sets `line_terminator` on `self` to the new `LineTerminator`.
|
|
#[inline]
|
|
pub const fn with_line_terminator(self, line_terminator: LineTerminator) -> Self {
|
|
Self {
|
|
line_terminator,
|
|
..self
|
|
}
|
|
}
|
|
|
|
/// Sets `offset_significant_figures` on `self` to the new `offset_significant_figures`.
|
|
#[inline]
|
|
pub fn with_offset_significant_figures<N>(self, offset_significant_figures: N) -> Self
|
|
where
|
|
N: Into<Option<usize>>,
|
|
{
|
|
Self {
|
|
offset_significant_figures: offset_significant_figures.into(),
|
|
..self
|
|
}
|
|
}
|
|
|
|
/// Sets `motion_values_significant_figures` on `self` to the new `motion_values_significant_figures`.
|
|
#[inline]
|
|
pub fn with_frame_time_significant_figures<N>(self, frame_time_significant_figures: N) -> Self
|
|
where
|
|
N: Into<Option<usize>>,
|
|
{
|
|
Self {
|
|
frame_time_significant_figures: frame_time_significant_figures.into(),
|
|
..self
|
|
}
|
|
}
|
|
|
|
/// Sets `motion_values_significant_figures` on `self` to the new `motion_values_significant_figures`.
|
|
#[inline]
|
|
pub fn with_motion_values_significant_figures<N>(
|
|
self,
|
|
motion_values_significant_figures: N,
|
|
) -> Self
|
|
where
|
|
N: Into<Option<usize>>,
|
|
{
|
|
Self {
|
|
motion_values_significant_figures: motion_values_significant_figures.into(),
|
|
..self
|
|
}
|
|
}
|
|
|
|
// @TODO: Refactor all of this
|
|
/// Get the next text chunk of the written bvh file. This function is
|
|
/// structured so that the `chunk` string can be continually
|
|
/// re-used without allocating and de-allocating memory.
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// Returns `true` when there are still more lines available,
|
|
/// `false` when all lines have been extracted.
|
|
fn next_chunk<'a, 'b: 'a>(
|
|
&self,
|
|
bvh: &'b Bvh,
|
|
chunk: &mut Vec<u8>,
|
|
iter_state: &'a mut WriteOptionsIterState<'b>,
|
|
) -> bool {
|
|
chunk.clear();
|
|
|
|
let terminator = self.line_terminator.as_bytes();
|
|
|
|
match *iter_state {
|
|
WriteOptionsIterState::Hierarchy { ref mut written } => {
|
|
if !*written {
|
|
*chunk = b"HIERARCHY".to_vec();
|
|
chunk.extend_from_slice(self.line_terminator.as_bytes());
|
|
*written = true;
|
|
} else {
|
|
let mut joints = bvh.joints();
|
|
*iter_state = WriteOptionsIterState::Joints {
|
|
current_joint: joints.next(),
|
|
joints,
|
|
wrote_name: false,
|
|
wrote_offset: false,
|
|
wrote_channels: false,
|
|
};
|
|
}
|
|
}
|
|
WriteOptionsIterState::Joints {
|
|
current_joint: None,
|
|
..
|
|
} => {
|
|
*iter_state = WriteOptionsIterState::Motion { written: false };
|
|
}
|
|
WriteOptionsIterState::Joints {
|
|
ref mut joints,
|
|
ref mut current_joint,
|
|
ref mut wrote_name,
|
|
ref mut wrote_offset,
|
|
ref mut wrote_channels,
|
|
} => {
|
|
if let Some(ref joint) = current_joint {
|
|
let joint_data = joint.data();
|
|
let mut depth = joint_data.depth();
|
|
if *wrote_name {
|
|
depth += 1
|
|
}
|
|
|
|
match (&mut *wrote_name, &mut *wrote_offset, &mut *wrote_channels) {
|
|
(&mut false, _, _) => {
|
|
chunk.extend(self.indent.prefix_chars(depth));
|
|
if joint_data.is_root() {
|
|
chunk.extend_from_slice(b"ROOT ");
|
|
} else {
|
|
chunk.extend_from_slice(b"JOINT ");
|
|
}
|
|
chunk.extend_from_slice(joint_data.name().as_ref());
|
|
chunk.extend_from_slice(terminator);
|
|
chunk.extend(self.indent.prefix_chars(depth));
|
|
chunk.push(b'{');
|
|
chunk.extend_from_slice(terminator);
|
|
|
|
*wrote_name = true;
|
|
}
|
|
(&mut true, &mut false, _) => {
|
|
chunk.extend(self.indent.prefix_chars(depth));
|
|
|
|
let [x, y, z] = joint_data.offset();
|
|
let offset_str = match self.offset_significant_figures {
|
|
Some(sf) => {
|
|
format!("OFFSET {:.*} {:.*} {:.*}", sf, x, sf, y, sf, z,)
|
|
}
|
|
None => format!("OFFSET {:.} {:.} {:.}", x, y, z),
|
|
};
|
|
chunk.extend_from_slice(offset_str.as_bytes());
|
|
chunk.extend_from_slice(terminator);
|
|
*wrote_offset = true;
|
|
}
|
|
(&mut true, &mut true, &mut false) => {
|
|
chunk.extend(self.indent.prefix_chars(depth));
|
|
|
|
let channels = joint_data.channels();
|
|
let channels_str = channels
|
|
.iter()
|
|
.map(|ch| ch.channel_type().as_str())
|
|
.collect::<SmallVec<[_; 6]>>()
|
|
.join(" ");
|
|
|
|
let channels_str =
|
|
format!("CHANNELS {} {}", channels.len(), channels_str);
|
|
chunk.extend_from_slice(channels_str.as_bytes());
|
|
chunk.extend_from_slice(terminator);
|
|
*wrote_channels = true;
|
|
}
|
|
(&mut true, &mut true, &mut true) => {
|
|
if let Some([x, y, z]) = joint_data.end_site() {
|
|
chunk.extend(self.indent.prefix_chars(depth));
|
|
chunk.extend_from_slice(b"End Site");
|
|
chunk.extend_from_slice(terminator);
|
|
|
|
chunk.extend(self.indent.prefix_chars(depth));
|
|
chunk.push(b'{');
|
|
chunk.extend_from_slice(terminator);
|
|
|
|
chunk.extend(self.indent.prefix_chars(depth + 1));
|
|
let offset_str = match self.offset_significant_figures {
|
|
Some(sf) => {
|
|
format!("OFFSET {:.*} {:.*} {:.*}", sf, x, sf, y, sf, z)
|
|
}
|
|
None => format!("OFFSET {:.} {:.} {:.}", x, y, z),
|
|
};
|
|
chunk.extend_from_slice(offset_str.as_bytes());
|
|
chunk.extend_from_slice(terminator);
|
|
|
|
chunk.extend(self.indent.prefix_chars(depth));
|
|
chunk.push(b'}');
|
|
chunk.extend_from_slice(terminator);
|
|
|
|
let next_joint = joints.next();
|
|
let prev_joint = mem::replace(current_joint, next_joint).unwrap();
|
|
|
|
let (curr_depth, mut depth_difference) =
|
|
if let Some(ref curr_j) = *current_joint {
|
|
let curr_depth = curr_j.data().depth();
|
|
(curr_depth, Some(prev_joint.data().depth() - curr_depth))
|
|
} else {
|
|
(0, Some(prev_joint.data().depth()))
|
|
};
|
|
|
|
while let Some(d) = depth_difference {
|
|
chunk.extend(self.indent.prefix_chars(curr_depth + d));
|
|
chunk.push(b'}');
|
|
chunk.extend_from_slice(terminator);
|
|
depth_difference =
|
|
depth_difference.and_then(|d| d.checked_sub(1));
|
|
}
|
|
} else {
|
|
*current_joint = joints.next();
|
|
}
|
|
*wrote_name = false;
|
|
*wrote_offset = false;
|
|
*wrote_channels = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
WriteOptionsIterState::Motion { ref mut written } => {
|
|
if !*written {
|
|
*chunk = b"MOTION".to_vec();
|
|
chunk.extend_from_slice(terminator);
|
|
*written = true;
|
|
} else {
|
|
*iter_state = WriteOptionsIterState::NumFrames { written: false };
|
|
}
|
|
}
|
|
WriteOptionsIterState::NumFrames { ref mut written } => {
|
|
if !*written {
|
|
*chunk = format!("Frames: {}", bvh.frames().len()).into_bytes();
|
|
chunk.extend_from_slice(terminator);
|
|
*written = true;
|
|
} else {
|
|
*iter_state = WriteOptionsIterState::FrameTime { written: false };
|
|
}
|
|
}
|
|
WriteOptionsIterState::FrameTime { ref mut written } => {
|
|
if !*written {
|
|
*chunk = match self.frame_time_significant_figures {
|
|
Some(sf) => {
|
|
format!("Frame Time: {:.*}", sf, bvh.frame_time().as_secs_f64())
|
|
.into_bytes()
|
|
}
|
|
None => {
|
|
format!("Frame Time: {:.}", bvh.frame_time().as_secs_f64()).into_bytes()
|
|
}
|
|
};
|
|
chunk.extend_from_slice(terminator);
|
|
*written = true;
|
|
} else {
|
|
let frames = bvh.frames();
|
|
*iter_state = WriteOptionsIterState::Frames { frames };
|
|
}
|
|
}
|
|
WriteOptionsIterState::Frames { ref mut frames } => {
|
|
return frames
|
|
.next()
|
|
.map(|frame| {
|
|
let motion_values = frame
|
|
.as_slice()
|
|
.iter()
|
|
.map(|motion| match self.motion_values_significant_figures {
|
|
Some(sf) => format!("{:.*}", sf, motion),
|
|
None => format!("{:.}", motion),
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join(" ");
|
|
*chunk = motion_values.into_bytes();
|
|
chunk.extend_from_slice(terminator);
|
|
true
|
|
})
|
|
.unwrap_or_default();
|
|
}
|
|
}
|
|
|
|
true
|
|
}
|
|
}
|
|
|
|
enum WriteOptionsIterState<'a> {
|
|
Hierarchy {
|
|
written: bool,
|
|
},
|
|
Joints {
|
|
joints: Joints<'a>,
|
|
current_joint: Option<Joint<'a>>,
|
|
wrote_name: bool,
|
|
wrote_offset: bool,
|
|
wrote_channels: bool,
|
|
},
|
|
Motion {
|
|
written: bool,
|
|
},
|
|
NumFrames {
|
|
written: bool,
|
|
},
|
|
FrameTime {
|
|
written: bool,
|
|
},
|
|
Frames {
|
|
frames: Frames<'a>,
|
|
},
|
|
}
|
|
|
|
impl WriteOptionsIterState<'_> {
|
|
#[inline]
|
|
const fn new() -> Self {
|
|
WriteOptionsIterState::Hierarchy { written: false }
|
|
}
|
|
}
|
|
|
|
impl Default for WriteOptionsIterState<'_> {
|
|
#[inline]
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
/// Specify indentation style to use when writing the `Bvh` joints.
|
|
///
|
|
/// By default, this value is set to 1 tab.
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
|
pub enum IndentStyle {
|
|
/// Do not indent nested joints.
|
|
NoIndentation,
|
|
/// Use a single tab (`'\t'`) for indentation.
|
|
Tabs,
|
|
/// Use `n` spaces for indentation.
|
|
Spaces(NonZeroUsize),
|
|
}
|
|
|
|
impl IndentStyle {
|
|
/// Create a new `IndentStyle` with `n` preceeding spaces.
|
|
///
|
|
/// If `n` is `0`, then `IndentStyle::NoIndentation` is returned.
|
|
#[inline]
|
|
pub fn with_spaces(n: usize) -> Self {
|
|
NonZeroUsize::new(n)
|
|
.map(IndentStyle::Spaces)
|
|
.unwrap_or(IndentStyle::NoIndentation)
|
|
}
|
|
|
|
/// Return an `Iterator` which yields bytes corresponding to the ascii
|
|
/// chars which form the `String` this indentation style would take.
|
|
#[inline]
|
|
fn prefix_chars(&self, depth: usize) -> impl Iterator<Item = u8> {
|
|
match *self {
|
|
IndentStyle::NoIndentation => iter::repeat(b'\0').take(0),
|
|
IndentStyle::Tabs => iter::repeat(b'\t').take(depth),
|
|
IndentStyle::Spaces(n) => iter::repeat(b' ').take(n.get() * depth),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Create a new `IndentStyle` using a single tab.
|
|
impl Default for IndentStyle {
|
|
#[inline]
|
|
fn default() -> Self {
|
|
IndentStyle::Tabs
|
|
}
|
|
}
|
|
|
|
/// Represents which line terminator style to use when writing a `Bvh` file.
|
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
|
pub enum LineTerminator {
|
|
/// Use Unix-style line endings (`'\n'`).
|
|
Unix,
|
|
/// Use Windows-style line endings (`'\r\n'`).
|
|
Windows,
|
|
}
|
|
|
|
impl LineTerminator {
|
|
/// Get the line terminator style native to the current OS:
|
|
///
|
|
/// * On Windows, this returns `LineTerminator::Windows`.
|
|
/// * Otherwise, this returns `LineTerminator::Unix`.
|
|
#[inline]
|
|
pub const fn native() -> Self {
|
|
if cfg!(windows) {
|
|
LineTerminator::Windows
|
|
} else {
|
|
LineTerminator::Unix
|
|
}
|
|
}
|
|
|
|
/// Return the characters of the `LineTerminator` as a `&str`.
|
|
#[inline]
|
|
pub const fn as_str(&self) -> &str {
|
|
match *self {
|
|
LineTerminator::Unix => "\n",
|
|
LineTerminator::Windows => "\r\n",
|
|
}
|
|
}
|
|
|
|
/// Returns the escaped characters of the `LineTerminator` as a `&str`.
|
|
#[inline]
|
|
pub const fn as_escaped_str(&self) -> &str {
|
|
match *self {
|
|
LineTerminator::Unix => r"\n",
|
|
LineTerminator::Windows => r"\r\n",
|
|
}
|
|
}
|
|
|
|
/// Return the characters of the `LineTerminator` as a `&[u8]`.
|
|
#[inline]
|
|
pub const fn as_bytes(&self) -> &[u8] {
|
|
self.as_str().as_bytes()
|
|
}
|
|
|
|
/// Returns the escaped characters of the `LineTerminator` as a `&[u8]`.
|
|
#[inline]
|
|
pub const fn as_escaped_bytes(&self) -> &[u8] {
|
|
self.as_escaped_str().as_bytes()
|
|
}
|
|
}
|
|
|
|
/// Returns the native line terminator for the current OS.
|
|
impl Default for LineTerminator {
|
|
#[inline]
|
|
fn default() -> Self {
|
|
LineTerminator::native()
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for LineTerminator {
|
|
#[inline]
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
f.write_str(self.as_str())
|
|
}
|
|
}
|