use crate::ecs::primitives::EasingFunction;
use nalgebra_glm::{Quat, Vec3};
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct CutsceneShot {
pub eye: Vec3,
pub target: Vec3,
pub field_of_view_degrees: f32,
pub roll_degrees: f32,
}
impl CutsceneShot {
pub fn new(eye: Vec3, target: Vec3) -> Self {
Self {
eye,
target,
field_of_view_degrees: 45.0,
roll_degrees: 0.0,
}
}
pub fn with_field_of_view(mut self, field_of_view_degrees: f32) -> Self {
self.field_of_view_degrees = field_of_view_degrees;
self
}
pub fn with_roll(mut self, roll_degrees: f32) -> Self {
self.roll_degrees = roll_degrees;
self
}
pub fn interpolate(self, other: CutsceneShot, factor: f32) -> CutsceneShot {
CutsceneShot {
eye: self.eye + (other.eye - self.eye) * factor,
target: self.target + (other.target - self.target) * factor,
field_of_view_degrees: self.field_of_view_degrees
+ (other.field_of_view_degrees - self.field_of_view_degrees) * factor,
roll_degrees: self.roll_degrees + (other.roll_degrees - self.roll_degrees) * factor,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum CutsceneAction {
CameraMove {
from: CutsceneShot,
to: CutsceneShot,
},
CameraFollow {
actor: String,
eye_offset: Vec3,
look_height: f32,
field_of_view_degrees: f32,
},
CameraPath {
waypoints: Vec<CutsceneShot>,
},
CameraShake {
amplitude: f32,
frequency: f32,
},
Handheld {
position_amplitude: f32,
target_amplitude: f32,
frequency: f32,
},
ActorMove {
actor: String,
from: Vec3,
to: Vec3,
},
ActorTurn {
actor: String,
from_yaw_radians: f32,
to_yaw_radians: f32,
},
ActorVisible {
actor: String,
visible: bool,
},
ActorAnimation {
actor: String,
clip: String,
looping: bool,
speed: f32,
blend: f32,
},
CameraLookAt {
actor: String,
look_height: f32,
damping: f32,
},
Dialogue {
speaker: Option<String>,
line: String,
},
Title {
line: String,
},
Fade {
from: f32,
to: f32,
color: Vec3,
},
Letterbox {
from: f32,
to: f32,
},
FocusPull {
from_distance: f32,
to_distance: f32,
range: f32,
},
Grade {
exposure_ev: (f32, f32),
saturation: (f32, f32),
contrast: (f32, f32),
},
Trigger {
id: String,
},
Sound {
clip: String,
volume: f32,
},
Music {
track: String,
volume: f32,
},
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CutsceneEvent {
pub start: f32,
pub duration: f32,
pub easing: EasingFunction,
pub action: CutsceneAction,
}
impl CutsceneEvent {
pub fn end(&self) -> f32 {
self.start + self.duration
}
pub fn eased_progress(&self, time: f32) -> f32 {
if self.duration <= 0.0 {
return if time >= self.start { 1.0 } else { 0.0 };
}
let raw = ((time - self.start) / self.duration).clamp(0.0, 1.0);
self.easing.evaluate(raw)
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct Cutscene {
pub name: String,
pub events: Vec<CutsceneEvent>,
}
impl Cutscene {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
events: Vec::new(),
}
}
pub fn duration(&self) -> f32 {
self.events
.iter()
.map(CutsceneEvent::end)
.fold(0.0, f32::max)
}
fn push(
mut self,
start: f32,
duration: f32,
easing: EasingFunction,
action: CutsceneAction,
) -> Self {
self.events.push(CutsceneEvent {
start,
duration,
easing,
action,
});
self
}
pub fn camera(
self,
start: f32,
duration: f32,
easing: EasingFunction,
from: CutsceneShot,
to: CutsceneShot,
) -> Self {
self.push(
start,
duration,
easing,
CutsceneAction::CameraMove { from, to },
)
}
pub fn camera_follow(
self,
start: f32,
duration: f32,
actor: impl Into<String>,
eye_offset: Vec3,
look_height: f32,
field_of_view_degrees: f32,
) -> Self {
self.push(
start,
duration,
EasingFunction::Linear,
CutsceneAction::CameraFollow {
actor: actor.into(),
eye_offset,
look_height,
field_of_view_degrees,
},
)
}
pub fn camera_look_at(
self,
start: f32,
duration: f32,
actor: impl Into<String>,
look_height: f32,
damping: f32,
) -> Self {
self.push(
start,
duration,
EasingFunction::Linear,
CutsceneAction::CameraLookAt {
actor: actor.into(),
look_height,
damping,
},
)
}
pub fn camera_path(
self,
start: f32,
duration: f32,
easing: EasingFunction,
waypoints: Vec<CutsceneShot>,
) -> Self {
self.push(
start,
duration,
easing,
CutsceneAction::CameraPath { waypoints },
)
}
pub fn handheld(
self,
start: f32,
duration: f32,
position_amplitude: f32,
target_amplitude: f32,
frequency: f32,
) -> Self {
self.push(
start,
duration,
EasingFunction::Linear,
CutsceneAction::Handheld {
position_amplitude,
target_amplitude,
frequency,
},
)
}
pub fn camera_shake(self, start: f32, duration: f32, amplitude: f32, frequency: f32) -> Self {
self.push(
start,
duration,
EasingFunction::Linear,
CutsceneAction::CameraShake {
amplitude,
frequency,
},
)
}
pub fn camera_cut(self, start: f32, shot: CutsceneShot) -> Self {
self.push(
start,
0.0,
EasingFunction::Linear,
CutsceneAction::CameraMove {
from: shot,
to: shot,
},
)
}
pub fn actor_move(
self,
actor: impl Into<String>,
start: f32,
duration: f32,
easing: EasingFunction,
from: Vec3,
to: Vec3,
) -> Self {
self.push(
start,
duration,
easing,
CutsceneAction::ActorMove {
actor: actor.into(),
from,
to,
},
)
}
pub fn actor_turn(
self,
actor: impl Into<String>,
start: f32,
duration: f32,
easing: EasingFunction,
from_yaw_radians: f32,
to_yaw_radians: f32,
) -> Self {
self.push(
start,
duration,
easing,
CutsceneAction::ActorTurn {
actor: actor.into(),
from_yaw_radians,
to_yaw_radians,
},
)
}
pub fn actor_animation(
self,
actor: impl Into<String>,
start: f32,
clip: impl Into<String>,
looping: bool,
speed: f32,
) -> Self {
self.actor_animation_blended(actor, start, clip, looping, speed, 0.2)
}
pub fn actor_animation_blended(
self,
actor: impl Into<String>,
start: f32,
clip: impl Into<String>,
looping: bool,
speed: f32,
blend: f32,
) -> Self {
self.push(
start,
0.0,
EasingFunction::Linear,
CutsceneAction::ActorAnimation {
actor: actor.into(),
clip: clip.into(),
looping,
speed,
blend,
},
)
}
pub fn actor_place(self, actor: impl Into<String>, time: f32, position: Vec3) -> Self {
self.push(
time,
0.0,
EasingFunction::Linear,
CutsceneAction::ActorMove {
actor: actor.into(),
from: position,
to: position,
},
)
}
pub fn actor_face(self, actor: impl Into<String>, time: f32, yaw_radians: f32) -> Self {
self.push(
time,
0.0,
EasingFunction::Linear,
CutsceneAction::ActorTurn {
actor: actor.into(),
from_yaw_radians: yaw_radians,
to_yaw_radians: yaw_radians,
},
)
}
pub fn actor_visible(self, actor: impl Into<String>, time: f32, visible: bool) -> Self {
self.push(
time,
0.0,
EasingFunction::Linear,
CutsceneAction::ActorVisible {
actor: actor.into(),
visible,
},
)
}
pub fn dialogue(
self,
start: f32,
duration: f32,
speaker: Option<&str>,
line: impl Into<String>,
) -> Self {
self.push(
start,
duration,
EasingFunction::Linear,
CutsceneAction::Dialogue {
speaker: speaker.map(str::to_string),
line: line.into(),
},
)
}
pub fn title(self, start: f32, duration: f32, line: impl Into<String>) -> Self {
self.push(
start,
duration,
EasingFunction::Linear,
CutsceneAction::Title { line: line.into() },
)
}
pub fn fade(
self,
start: f32,
duration: f32,
easing: EasingFunction,
from: f32,
to: f32,
color: Vec3,
) -> Self {
self.push(
start,
duration,
easing,
CutsceneAction::Fade { from, to, color },
)
}
pub fn fade_in(self, start: f32, duration: f32) -> Self {
self.fade(
start,
duration,
EasingFunction::QuadOut,
1.0,
0.0,
Vec3::zeros(),
)
}
pub fn fade_out(self, start: f32, duration: f32) -> Self {
self.fade(
start,
duration,
EasingFunction::QuadIn,
0.0,
1.0,
Vec3::zeros(),
)
}
pub fn letterbox(
self,
start: f32,
duration: f32,
easing: EasingFunction,
from: f32,
to: f32,
) -> Self {
self.push(
start,
duration,
easing,
CutsceneAction::Letterbox { from, to },
)
}
pub fn letterbox_in(self, start: f32, duration: f32) -> Self {
self.letterbox(start, duration, EasingFunction::CubicOut, 0.0, 1.0)
}
pub fn letterbox_out(self, start: f32, duration: f32) -> Self {
self.letterbox(start, duration, EasingFunction::CubicIn, 1.0, 0.0)
}
pub fn focus_pull(
self,
start: f32,
duration: f32,
easing: EasingFunction,
from_distance: f32,
to_distance: f32,
range: f32,
) -> Self {
self.push(
start,
duration,
easing,
CutsceneAction::FocusPull {
from_distance,
to_distance,
range,
},
)
}
pub fn grade(
self,
start: f32,
duration: f32,
easing: EasingFunction,
exposure_ev: (f32, f32),
saturation: (f32, f32),
contrast: (f32, f32),
) -> Self {
self.push(
start,
duration,
easing,
CutsceneAction::Grade {
exposure_ev,
saturation,
contrast,
},
)
}
pub fn trigger(self, start: f32, id: impl Into<String>) -> Self {
self.push(
start,
0.0,
EasingFunction::Linear,
CutsceneAction::Trigger { id: id.into() },
)
}
pub fn sound(self, start: f32, clip: impl Into<String>, volume: f32) -> Self {
self.push(
start,
0.0,
EasingFunction::Linear,
CutsceneAction::Sound {
clip: clip.into(),
volume,
},
)
}
pub fn music(self, start: f32, track: impl Into<String>, volume: f32) -> Self {
self.push(
start,
0.0,
EasingFunction::Linear,
CutsceneAction::Music {
track: track.into(),
volume,
},
)
}
pub fn then(
self,
gap: f32,
duration: f32,
easing: EasingFunction,
action: CutsceneAction,
) -> Self {
let start = self.duration() + gap;
self.push(start, duration, easing, action)
}
pub fn then_dialogue(
self,
gap: f32,
duration: f32,
speaker: Option<&str>,
line: impl Into<String>,
) -> Self {
let start = self.duration() + gap;
self.dialogue(start, duration, speaker, line)
}
pub fn dialogue_sequence(
mut self,
start: f32,
gap: f32,
lines: &[(Option<&str>, &str, f32)],
) -> Self {
let mut cursor = start;
for (speaker, line, duration) in lines {
self = self.dialogue(cursor, *duration, *speaker, *line);
cursor += duration + gap;
}
self
}
}
fn catmull_rom(p0: f32, p1: f32, p2: f32, p3: f32, t: f32) -> f32 {
let t2 = t * t;
let t3 = t2 * t;
0.5 * ((2.0 * p1)
+ (-p0 + p2) * t
+ (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t2
+ (-p0 + 3.0 * p1 - 3.0 * p2 + p3) * t3)
}
fn catmull_rom_vec3(p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, t: f32) -> Vec3 {
Vec3::new(
catmull_rom(p0.x, p1.x, p2.x, p3.x, t),
catmull_rom(p0.y, p1.y, p2.y, p3.y, t),
catmull_rom(p0.z, p1.z, p2.z, p3.z, t),
)
}
pub fn sample_camera_path(waypoints: &[CutsceneShot], progress: f32) -> CutsceneShot {
match waypoints.len() {
0 => CutsceneShot::new(Vec3::zeros(), -Vec3::z()),
1 => waypoints[0],
_ => {
let segments = waypoints.len() - 1;
let scaled = (progress.clamp(0.0, 1.0) * segments as f32).min(segments as f32 - 1e-4);
let index = scaled.floor() as usize;
let local = scaled - index as f32;
let point = |offset: isize| -> CutsceneShot {
let clamped = (index as isize + offset).clamp(0, waypoints.len() as isize - 1);
waypoints[clamped as usize]
};
let p0 = point(-1);
let p1 = point(0);
let p2 = point(1);
let p3 = point(2);
CutsceneShot {
eye: catmull_rom_vec3(p0.eye, p1.eye, p2.eye, p3.eye, local),
target: catmull_rom_vec3(p0.target, p1.target, p2.target, p3.target, local),
field_of_view_degrees: catmull_rom(
p0.field_of_view_degrees,
p1.field_of_view_degrees,
p2.field_of_view_degrees,
p3.field_of_view_degrees,
local,
),
roll_degrees: catmull_rom(
p0.roll_degrees,
p1.roll_degrees,
p2.roll_degrees,
p3.roll_degrees,
local,
),
}
}
}
}
pub fn camera_look_rotation(eye: Vec3, target: Vec3, roll_degrees: f32) -> Quat {
let direction = target - eye;
if direction.magnitude_squared() < f32::EPSILON {
return Quat::identity();
}
let direction = direction.normalize();
let yaw = (-direction.x).atan2(-direction.z);
let pitch = (-direction.y).clamp(-1.0, 1.0).asin();
let yaw_rotation = nalgebra_glm::quat_angle_axis(yaw, &Vec3::y());
let pitch_rotation = nalgebra_glm::quat_angle_axis(-pitch, &Vec3::x());
let look = yaw_rotation * pitch_rotation;
if roll_degrees.abs() < f32::EPSILON {
return look;
}
let roll = nalgebra_glm::quat_angle_axis(roll_degrees.to_radians(), &direction);
roll * look
}