use bevy::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Asset, Reflect, Clone, Debug, Serialize, Deserialize)]
pub struct SequenceAsset {
pub name: String,
pub shots: Vec<Shot>,
#[serde(default)]
pub markers: Vec<Marker>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub texts: Vec<TextBlock>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub actors: Vec<ActorTrack>,
#[serde(default)]
pub blend_out: Option<Blend>,
}
impl SequenceAsset {
pub fn empty(name: impl Into<String>) -> Self {
Self {
name: name.into(),
shots: Vec::new(),
markers: Vec::new(),
texts: Vec::new(),
actors: Vec::new(),
blend_out: None,
}
}
pub fn duration(&self) -> f32 {
self.shots
.iter()
.map(|s| s.start + s.duration)
.fold(0.0, f32::max)
}
}
#[derive(Reflect, Clone, Debug, Serialize, Deserialize)]
pub struct Shot {
pub start: f32,
pub duration: f32,
#[serde(default)]
pub blend_in: Option<Blend>,
pub rig: Rig,
#[serde(default)]
pub look: Look,
#[serde(default)]
pub lens: Lens,
#[serde(default)]
pub shake: Option<Shake>,
}
#[derive(Reflect, Clone, Copy, Debug, Serialize, Deserialize)]
pub struct Shake {
pub amplitude_deg: f32,
pub frequency_hz: f32,
#[serde(default)]
pub seed: u32,
#[serde(default)]
pub pos_amplitude: f32,
}
#[derive(Reflect, Clone, Debug, Serialize, Deserialize)]
pub enum Rig {
Keys {
keys: Vec<Key>,
#[serde(default)]
interp: KeyInterp,
},
Rail {
points: Vec<Vec3>,
#[serde(default)]
kind: RailKind,
#[serde(default = "yes")]
constant_speed: bool,
#[serde(default)]
progress: ScalarTrack,
},
Orbit {
center: TargetRef,
radius: ScalarTrack,
#[serde(default)]
yaw_deg: ScalarTrack,
#[serde(default)]
pitch_deg: ScalarTrack,
},
}
fn yes() -> bool {
true
}
#[derive(Reflect, Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum KeyInterp {
#[default]
Eased,
CatmullRom,
}
#[derive(Reflect, Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum RailKind {
#[default]
CatmullRom,
BSpline,
}
#[derive(Reflect, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum TargetRef {
Point(Vec3),
Entity(String),
}
#[derive(Reflect, Clone, Copy, Debug, Serialize, Deserialize)]
pub struct Key {
pub time: f32,
pub pos: Vec3,
#[serde(default)]
pub rot: Option<Quat>,
#[serde(default = "linear")]
pub ease: EaseFunction,
}
fn linear() -> EaseFunction {
EaseFunction::Linear
}
#[derive(Reflect, Clone, Debug, Default, Serialize, Deserialize)]
pub enum Look {
#[default]
Free,
At {
target: TargetRef,
#[serde(default)]
damping: Option<f32>,
},
Velocity,
}
#[derive(Reflect, Clone, Debug, Default, Serialize, Deserialize)]
pub struct ScalarTrack {
pub keys: Vec<ScalarKey>,
}
#[derive(Reflect, Clone, Copy, Debug, Serialize, Deserialize)]
pub struct ScalarKey {
pub time: f32,
pub value: f32,
#[serde(default = "linear")]
pub ease: EaseFunction,
}
impl ScalarTrack {
pub fn constant(value: f32) -> Self {
Self {
keys: vec![ScalarKey {
time: 0.0,
value,
ease: EaseFunction::Linear,
}],
}
}
pub fn sample(&self, t: f32) -> f32 {
let keys = &self.keys;
let Some(first) = keys.first() else {
return 0.0;
};
if t <= first.time {
return first.value;
}
for pair in keys.windows(2) {
let (a, b) = (&pair[0], &pair[1]);
if t <= b.time {
let span = (b.time - a.time).max(f32::EPSILON);
let w = a.ease.sample_clamped((t - a.time) / span);
return a.value + (b.value - a.value) * w;
}
}
keys.last().map_or(0.0, |k| k.value)
}
}
#[derive(Reflect, Clone, Debug, Default, Serialize, Deserialize)]
pub struct Lens {
#[serde(default)]
pub fov: FovSpec,
#[serde(default)]
pub focus: Option<FocusTrack>,
#[serde(default)]
pub aperture_f_stops: Option<ScalarTrack>,
#[serde(default)]
pub exposure_ev100: Option<ScalarTrack>,
#[serde(default)]
pub dof_mode: DofMode,
}
#[derive(Reflect, Clone, Debug, Serialize, Deserialize)]
pub enum FovSpec {
VerticalFovDeg(ScalarTrack),
FocalLengthMm {
track: ScalarTrack,
#[serde(default)]
filmback: Filmback,
},
}
impl Default for FovSpec {
fn default() -> Self {
FovSpec::VerticalFovDeg(ScalarTrack::constant(45.0))
}
}
#[derive(Reflect, Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum Filmback {
#[default]
Super35,
FullFrame,
Super16,
Custom {
sensor_height_mm: f32,
},
}
impl Filmback {
pub fn sensor_height_mm(&self) -> f32 {
match self {
Filmback::Super35 => 18.66,
Filmback::FullFrame => 24.0,
Filmback::Super16 => 7.41,
Filmback::Custom { sensor_height_mm } => *sensor_height_mm,
}
}
}
#[derive(Reflect, Clone, Debug, Serialize, Deserialize)]
pub enum FocusTrack {
Distance(ScalarTrack),
Target {
target: TargetRef,
#[serde(default)]
offset: f32,
},
}
#[derive(Reflect, Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum DofMode {
#[default]
Gaussian,
Bokeh,
}
#[derive(Reflect, Clone, Copy, Debug, Serialize, Deserialize)]
pub struct Blend {
pub secs: f32,
pub ease: EaseFunction,
}
#[derive(Reflect, Clone, Debug, Serialize, Deserialize)]
pub struct Marker {
pub time: f32,
pub name: String,
}
#[derive(Reflect, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TextBlock {
pub start: f32,
pub duration: f32,
pub text: String,
#[serde(default)]
pub anchor: TextAnchor,
#[serde(default)]
pub fade_in: f32,
#[serde(default)]
pub fade_out: f32,
#[serde(default)]
pub style: TextBlockStyle,
}
#[derive(Reflect, Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum TextAnchor {
Center,
#[default]
LowerThird,
TopCenter,
}
#[derive(Reflect, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TextBlockStyle {
#[serde(default = "default_title_size")]
pub font_size: f32,
#[serde(default = "default_title_color")]
pub color: Color,
#[serde(default = "yes")]
pub shadow: bool,
#[serde(default)]
pub background: Option<Color>,
}
fn default_title_size() -> f32 {
28.0
}
fn default_title_color() -> Color {
Color::WHITE
}
impl Default for TextBlockStyle {
fn default() -> Self {
Self {
font_size: default_title_size(),
color: default_title_color(),
shadow: true,
background: None,
}
}
}
impl TextBlock {
pub fn end(&self) -> f32 {
self.start + self.duration
}
pub fn active_at(&self, t: f32) -> bool {
self.duration > 0.0 && t >= self.start && t < self.end()
}
pub fn alpha_at(&self, t: f32) -> f32 {
if !self.active_at(t) {
return 0.0;
}
let ramp_in = if self.fade_in > 0.0 {
(t - self.start) / self.fade_in
} else {
1.0
};
let ramp_out = if self.fade_out > 0.0 {
(self.end() - t) / self.fade_out
} else {
1.0
};
ramp_in.min(ramp_out).clamp(0.0, 1.0)
}
}
#[derive(Reflect, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ActorTrack {
pub target: TargetRef,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cues: Vec<ActorCue>,
}
#[derive(Reflect, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ActorCue {
pub start: f32,
pub duration: f32,
pub anim: String,
#[serde(default)]
pub fade_in: f32,
#[serde(default)]
pub fade_out: f32,
#[serde(default = "one")]
pub speed: f32,
}
fn one() -> f32 {
1.0
}
impl ActorCue {
pub fn end(&self) -> f32 {
self.start + self.duration
}
pub fn active_at(&self, t: f32) -> bool {
self.duration > 0.0 && t >= self.start && t < self.end()
}
pub fn alpha_at(&self, t: f32) -> f32 {
if !self.active_at(t) {
return 0.0;
}
let ramp_in = if self.fade_in > 0.0 {
(t - self.start) / self.fade_in
} else {
1.0
};
let ramp_out = if self.fade_out > 0.0 {
(self.end() - t) / self.fade_out
} else {
1.0
};
ramp_in.min(ramp_out).clamp(0.0, 1.0)
}
}
impl Key {
pub fn at(time: f32) -> Self {
Self {
time,
pos: Vec3::ZERO,
rot: None,
ease: EaseFunction::Linear,
}
}
pub fn pos(mut self, pos: Vec3) -> Self {
self.pos = pos;
self
}
pub fn rot(mut self, rot: Quat) -> Self {
self.rot = Some(rot);
self
}
pub fn looking_at(mut self, target: Vec3) -> Self {
self.rot = Some(
Transform::from_translation(self.pos)
.looking_at(target, Vec3::Y)
.rotation,
);
self
}
pub fn ease(mut self, ease: EaseFunction) -> Self {
self.ease = ease;
self
}
}
impl Shot {
pub fn keys(start: f32, duration: f32, keys: impl Into<Vec<Key>>) -> Self {
Self {
start,
duration,
blend_in: None,
rig: Rig::Keys {
keys: keys.into(),
interp: KeyInterp::Eased,
},
look: Look::default(),
lens: Lens::default(),
shake: None,
}
}
pub fn blend_in(mut self, secs: f32, ease: EaseFunction) -> Self {
self.blend_in = Some(Blend { secs, ease });
self
}
pub fn look(mut self, look: Look) -> Self {
self.look = look;
self
}
pub fn lens(mut self, lens: Lens) -> Self {
self.lens = lens;
self
}
pub fn shake(mut self, shake: Shake) -> Self {
self.shake = Some(shake);
self
}
}
impl TextBlock {
pub fn at(start: f32, duration: f32, text: impl Into<String>) -> Self {
Self {
start,
duration,
text: text.into(),
anchor: TextAnchor::default(),
fade_in: 0.0,
fade_out: 0.0,
style: TextBlockStyle::default(),
}
}
pub fn anchor(mut self, anchor: TextAnchor) -> Self {
self.anchor = anchor;
self
}
pub fn fades(mut self, fade_in: f32, fade_out: f32) -> Self {
self.fade_in = fade_in;
self.fade_out = fade_out;
self
}
pub fn style(mut self, style: TextBlockStyle) -> Self {
self.style = style;
self
}
}
impl ActorTrack {
pub fn entity(name: impl Into<String>) -> Self {
Self {
target: TargetRef::Entity(name.into()),
cues: Vec::new(),
}
}
pub fn cue(mut self, cue: ActorCue) -> Self {
self.cues.push(cue);
self
}
}
impl ActorCue {
pub fn at(start: f32, duration: f32, anim: impl Into<String>) -> Self {
Self {
start,
duration,
anim: anim.into(),
fade_in: 0.0,
fade_out: 0.0,
speed: 1.0,
}
}
pub fn fades(mut self, fade_in: f32, fade_out: f32) -> Self {
self.fade_in = fade_in;
self.fade_out = fade_out;
self
}
pub fn speed(mut self, speed: f32) -> Self {
self.speed = speed;
self
}
}
impl SequenceAsset {
pub fn single_shot(name: impl Into<String>, shot: Shot) -> Self {
Self {
name: name.into(),
shots: vec![shot],
markers: Vec::new(),
texts: Vec::new(),
actors: Vec::new(),
blend_out: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_block_alpha_ramps_and_boundaries() {
let block = TextBlock::at(2.0, 4.0, "caption").fades(1.0, 2.0);
assert_eq!(block.alpha_at(1.999), 0.0);
assert_eq!(block.alpha_at(6.0), 0.0);
assert_eq!(block.alpha_at(2.0), 0.0);
assert!((block.alpha_at(2.5) - 0.5).abs() < 1e-6);
assert_eq!(block.alpha_at(3.5), 1.0);
assert!((block.alpha_at(4.5) - 0.75).abs() < 1e-6);
}
#[test]
fn text_block_zero_fades_are_instant() {
let block = TextBlock::at(1.0, 2.0, "cut in");
assert_eq!(block.alpha_at(1.0), 1.0);
assert_eq!(block.alpha_at(2.999), 1.0);
assert_eq!(block.alpha_at(3.0), 0.0);
}
#[test]
fn text_block_overlong_fades_peak_below_one() {
let block = TextBlock::at(0.0, 1.0, "flash").fades(1.0, 1.0);
assert!((block.alpha_at(0.5) - 0.5).abs() < 1e-6);
assert!(block.alpha_at(0.25) < 0.5);
assert!(block.alpha_at(0.75) < 0.5);
}
#[test]
fn text_block_zero_duration_is_never_active() {
let block = TextBlock::at(1.0, 0.0, "nothing");
assert!(!block.active_at(1.0));
assert_eq!(block.alpha_at(1.0), 0.0);
}
#[test]
fn actor_cue_alpha_ramps_and_boundaries() {
let cue = ActorCue::at(2.0, 4.0, "cutscene.sleeping").fades(1.0, 2.0);
assert_eq!(cue.alpha_at(1.999), 0.0);
assert_eq!(cue.alpha_at(6.0), 0.0);
assert_eq!(cue.alpha_at(2.0), 0.0);
assert!((cue.alpha_at(2.5) - 0.5).abs() < 1e-6);
assert_eq!(cue.alpha_at(3.5), 1.0);
assert!((cue.alpha_at(4.5) - 0.75).abs() < 1e-6);
}
#[test]
fn actor_cue_zero_duration_is_never_active() {
let cue = ActorCue::at(1.0, 0.0, "nothing");
assert!(!cue.active_at(1.0));
assert_eq!(cue.alpha_at(1.0), 0.0);
}
#[test]
fn actor_cue_default_speed_is_one() {
let cue = ActorCue::at(0.0, 1.0, "walk");
assert_eq!(cue.speed, 1.0);
assert_eq!(cue.speed(1.5).speed, 1.5);
}
}