mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-18 07:35:19 +00:00
Animation Timeline Initial (#88)
This PR adds an initial animation timeline. --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Danny McGee <dannymcgee@gmail.com>
This commit is contained in:
@@ -1,208 +0,0 @@
|
||||
use bevy::prelude::*;
|
||||
use itertools::Itertools;
|
||||
use space_editor::space_prefab::editor_registry::EditorRegistryExt;
|
||||
|
||||
use crate::math::{InvLerp, Lerp};
|
||||
|
||||
use super::GlobalSeekTime;
|
||||
|
||||
pub struct CameraAnimPlugin;
|
||||
impl Plugin for CameraAnimPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.register_type::<CameraKeyframe>()
|
||||
.editor_registry::<CameraKeyframe>()
|
||||
.register_type::<CameraTargetKeyframe>()
|
||||
.editor_registry::<CameraTargetKeyframe>();
|
||||
|
||||
app.add_systems(Update, draw_camera_keyframes);
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
wasm::spawn_camera_keyframes,
|
||||
wasm::notify_keyframe_changes.map(bevy::utils::error),
|
||||
wasm::process_update_keyframe,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct CameraKeyframe(pub f32);
|
||||
|
||||
#[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct CameraTargetKeyframe(pub f32);
|
||||
|
||||
fn draw_camera_keyframes(
|
||||
mut gizmos: Gizmos,
|
||||
r_seek_time: Res<GlobalSeekTime>,
|
||||
q_cam_keyframe_xforms: Query<&GlobalTransform, With<CameraKeyframe>>,
|
||||
q_cam_target_keyframe_xforms: Query<&GlobalTransform, With<CameraTargetKeyframe>>,
|
||||
q_cam_keyframes: Query<(&CameraKeyframe, &GlobalTransform)>,
|
||||
) {
|
||||
if q_cam_keyframes.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
for world_xform in q_cam_keyframe_xforms
|
||||
.iter()
|
||||
.chain(q_cam_target_keyframe_xforms.iter())
|
||||
{
|
||||
let origin = world_xform.translation();
|
||||
let x_axis = world_xform.right() * 0.25;
|
||||
let y_axis = world_xform.up() * 0.25;
|
||||
let z_axis = world_xform.back() * 0.25;
|
||||
|
||||
gizmos.line(origin, origin + x_axis, Color::RED);
|
||||
gizmos.line(origin, origin + y_axis, Color::GREEN);
|
||||
gizmos.line(origin, origin + z_axis, Color::BLUE);
|
||||
}
|
||||
|
||||
let seek_time = **r_seek_time;
|
||||
let keyframes_sorted = q_cam_keyframes
|
||||
.iter()
|
||||
.sorted_by(|(a, _), (b, _)| a.partial_cmp(&b.0).unwrap())
|
||||
.collect_vec();
|
||||
|
||||
let (timestamp_first, xform_first) = keyframes_sorted.first().unwrap();
|
||||
let (timestamp_last, xform_last) = keyframes_sorted.last().unwrap();
|
||||
|
||||
if seek_time <= timestamp_first.0 {
|
||||
let (_, rot, pos) = xform_first.to_scale_rotation_translation();
|
||||
gizmos.sphere(pos, rot, 0.25, Color::WHITE);
|
||||
} else if seek_time >= timestamp_last.0 {
|
||||
let (_, rot, pos) = xform_last.to_scale_rotation_translation();
|
||||
gizmos.sphere(pos, rot, 0.25, Color::WHITE);
|
||||
} else if let Some(((timestamp_a, xform_a), (timestamp_b, xform_b))) = keyframes_sorted
|
||||
.iter()
|
||||
.tuple_windows()
|
||||
.find(|((a, _), (b, _))| a.0 <= seek_time && b.0 >= seek_time)
|
||||
{
|
||||
let t = seek_time.inv_lerp(timestamp_a.0, timestamp_b.0);
|
||||
let (_, rot_a, pos_a) = xform_a.to_scale_rotation_translation();
|
||||
let (_, rot_b, pos_b) = xform_b.to_scale_rotation_translation();
|
||||
let pos = t.lerp(pos_a, pos_b);
|
||||
let rot = Quat::slerp(rot_a, rot_b, t);
|
||||
|
||||
gizmos.sphere(pos, rot, 0.25, Color::WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
mod wasm {
|
||||
use std::sync::Arc;
|
||||
|
||||
use bevy::{prelude::*, window::PrimaryWindow};
|
||||
use space_editor::prelude::space_undo::{self, AddedEntity};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{
|
||||
camera_controller::CameraController,
|
||||
wasm::{self, FromStr, StaticBuffer, WasmMessageBuffer},
|
||||
};
|
||||
|
||||
use super::{CameraKeyframe, CameraTargetKeyframe};
|
||||
|
||||
static ADD_KEYFRAME: WasmMessageBuffer<f32> = WasmMessageBuffer::new();
|
||||
static UPDATE_KEYFRAME: WasmMessageBuffer<(Entity, f32)> = WasmMessageBuffer::new();
|
||||
|
||||
#[wasm_bindgen(js_name = addCameraKeyframe)]
|
||||
pub fn add_camera_keyframe(timestamp: f32) {
|
||||
ADD_KEYFRAME.write_message(timestamp);
|
||||
}
|
||||
|
||||
pub(super) fn spawn_camera_keyframes(
|
||||
mut cmd: Commands,
|
||||
mut ew_changes: EventWriter<space_undo::NewChange>,
|
||||
q_camera: Query<(&CameraController, &GlobalTransform)>,
|
||||
) {
|
||||
if let Some(timestamp) = ADD_KEYFRAME.take_message() {
|
||||
let Ok((cam_controller, world_xform)) = q_camera.get_single() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let entity = cmd
|
||||
.spawn((
|
||||
Name::new("Camera Target Keyframe"),
|
||||
TransformBundle::from_transform(Transform::from_translation(
|
||||
cam_controller.focus,
|
||||
)),
|
||||
CameraTargetKeyframe(timestamp),
|
||||
))
|
||||
.id();
|
||||
ew_changes.send(space_undo::NewChange {
|
||||
change: Arc::new(AddedEntity { entity }),
|
||||
});
|
||||
|
||||
let entity = cmd
|
||||
.spawn((
|
||||
Name::new("Camera Keyframe"),
|
||||
TransformBundle::from_transform(Transform::from(*world_xform)),
|
||||
CameraKeyframe(timestamp),
|
||||
))
|
||||
.id();
|
||||
ew_changes.send(space_undo::NewChange {
|
||||
change: Arc::new(AddedEntity { entity }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = updateKeyframe)]
|
||||
pub fn update_keyframe(entity: &str, timestamp: f32) {
|
||||
let entity = Entity::from_str(entity).unwrap();
|
||||
UPDATE_KEYFRAME.write_message((entity, timestamp));
|
||||
}
|
||||
|
||||
pub(super) fn process_update_keyframe(
|
||||
mut q_camera_keyframes: Query<&mut CameraKeyframe>,
|
||||
mut q_camera_target_keyframes: Query<&mut CameraTargetKeyframe>,
|
||||
) {
|
||||
if let Some((ent, timestamp)) = UPDATE_KEYFRAME.take_message() {
|
||||
if let Ok(mut keyframe) = q_camera_keyframes.get_mut(ent) {
|
||||
**keyframe = timestamp;
|
||||
} else if let Ok(mut keyframe) = q_camera_target_keyframes.get_mut(ent) {
|
||||
**keyframe = timestamp;
|
||||
} else {
|
||||
warn!("No matching keyframe found for entity: {ent:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(typescript_custom_section)]
|
||||
const CAMERA_KEYFRAMES_EVENT: &str = r#"
|
||||
export interface CameraKeyframesEvent extends CustomEvent {
|
||||
type: "camera-keyframes";
|
||||
detail: Record<string, number>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
export interface GlobalEventHandlersEventMap {
|
||||
"camera-keyframes": CameraKeyframesEvent;
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
pub(super) fn notify_keyframe_changes(
|
||||
mut l_notified: Local<serde_json::Map<String, serde_json::Value>>,
|
||||
q_keyframes: Query<(Entity, &CameraKeyframe)>,
|
||||
q_window: Query<&Window, With<PrimaryWindow>>,
|
||||
) -> Result<(), String> {
|
||||
if l_notified.len() == q_keyframes.iter().count() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Ok(window) = q_window.get_single() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
l_notified.clear();
|
||||
for (ent, timestamp) in q_keyframes.iter() {
|
||||
l_notified.insert(format!("{ent:?}"), timestamp.0.into());
|
||||
}
|
||||
|
||||
wasm::dispatch_to_js(window, "camera-keyframes", &*l_notified)
|
||||
}
|
||||
}
|
||||
+5
-33
@@ -9,8 +9,8 @@ use space_editor::space_prefab::editor_registry::EditorRegistryExt;
|
||||
use crate::{hierarchy, scene::SceneElement};
|
||||
pub use retargeting::*;
|
||||
|
||||
mod camera;
|
||||
mod retargeting;
|
||||
pub mod timeline_animation;
|
||||
|
||||
pub struct AnimPlugin;
|
||||
impl Plugin for AnimPlugin {
|
||||
@@ -29,19 +29,14 @@ impl Plugin for AnimPlugin {
|
||||
.editor_registry::<InvBindPose>();
|
||||
|
||||
app.add_systems(Update, process_skinned_meshes);
|
||||
app.insert_resource(GlobalSeekTime(0.));
|
||||
|
||||
app.add_plugins((retargeting::RetargetingPlugin, camera::CameraAnimPlugin));
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
app.add_systems(Update, wasm::update_global_seek_time);
|
||||
app.add_plugins((
|
||||
retargeting::RetargetingPlugin,
|
||||
timeline_animation::TimelinePlugin,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
|
||||
#[reflect(Resource)]
|
||||
pub struct GlobalSeekTime(pub f32);
|
||||
|
||||
#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct Skeleton;
|
||||
@@ -149,26 +144,3 @@ fn process_skinned_meshes(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
mod wasm {
|
||||
use bevy::prelude::*;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::wasm::{StaticBuffer, WasmMessageBuffer};
|
||||
|
||||
use super::GlobalSeekTime;
|
||||
|
||||
static SEEK_TIME: WasmMessageBuffer<f32> = WasmMessageBuffer::new();
|
||||
|
||||
#[wasm_bindgen(js_name = seekTime)]
|
||||
pub fn seek_time(timestamp: f32) {
|
||||
SEEK_TIME.write_message(timestamp);
|
||||
}
|
||||
|
||||
pub(super) fn update_global_seek_time(mut r_seek_time: ResMut<GlobalSeekTime>) {
|
||||
if let Some(timestamp) = SEEK_TIME.take_message() {
|
||||
**r_seek_time = timestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,836 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use bevy::{
|
||||
ecs::{
|
||||
entity::{EntityMapper, MapEntities},
|
||||
reflect::ReflectMapEntities,
|
||||
},
|
||||
prelude::*,
|
||||
utils::HashSet,
|
||||
};
|
||||
|
||||
use bevy_tweening::{
|
||||
lens::{TransformPositionLens, TransformRotationLens, TransformScaleLens},
|
||||
Animator, EaseMethod, Sequence, Targetable, Tracks, Tween, TweenCompleted,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use space_editor::space_prefab::editor_registry::EditorRegistryExt;
|
||||
|
||||
use crate::interaction::{MarkedForDeletion, Selectable};
|
||||
|
||||
pub struct TimelinePlugin;
|
||||
|
||||
impl Plugin for TimelinePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.register_type::<TimedAnimationClipAsset>()
|
||||
.editor_registry::<TimedAnimationClipAsset>()
|
||||
.register_type::<AnimationTarget>()
|
||||
.editor_registry::<AnimationTarget>()
|
||||
.register_type::<TransformKeyframe>()
|
||||
.editor_registry::<TransformKeyframe>()
|
||||
.register_type::<Keyframe>()
|
||||
.editor_registry::<Keyframe>()
|
||||
.register_type::<KeyframeOrder>()
|
||||
.editor_registry::<KeyframeOrder>()
|
||||
.add_event::<TimelineInteractionEvent>()
|
||||
.add_event::<KeyframeInteractionEvent>();
|
||||
|
||||
app.insert_resource(GlobalSeekTime {
|
||||
time: 0.,
|
||||
playing: false,
|
||||
delta: None,
|
||||
})
|
||||
.add_event::<TweenCompleted>();
|
||||
|
||||
app.add_systems(PreUpdate, (process_timeline_events, update_tick).chain());
|
||||
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
process_keyframe_events,
|
||||
timeline_bound_component_animator_system::<Transform>,
|
||||
),
|
||||
);
|
||||
|
||||
app.add_systems(PostUpdate, generate_transform_animation);
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
{
|
||||
app.add_systems(
|
||||
First,
|
||||
(
|
||||
wasm::update_global_seek_time,
|
||||
wasm::update_keyframe_interaction,
|
||||
),
|
||||
);
|
||||
app.add_systems(
|
||||
PostUpdate,
|
||||
(
|
||||
wasm::notify_seek_time_changes.map(bevy::utils::error),
|
||||
wasm::notify_keyframe_changes.map(bevy::utils::error),
|
||||
wasm::notify_updated_animation_targets.map(bevy::utils::error),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MIN_KEYFRAME_DISTANCE: f32 = 0.01;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Event, Clone, Copy, Debug)]
|
||||
pub enum TimelineInteractionEvent {
|
||||
Play,
|
||||
Pause,
|
||||
Seek(f32),
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Event, Clone, Copy, Debug)]
|
||||
pub enum KeyframeInteractionEvent {
|
||||
RecordKeyframe { target: Entity },
|
||||
MoveKeyframeTime { target: Entity, time: f32 },
|
||||
AddEntityTrack { target: Entity },
|
||||
}
|
||||
|
||||
#[derive(Resource, Clone, Copy, Debug, Default, Reflect, Serialize, Deserialize)]
|
||||
#[reflect(Resource)]
|
||||
pub struct GlobalSeekTime {
|
||||
pub time: f32,
|
||||
pub playing: bool,
|
||||
pub delta: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Copy, Default, Reflect, PartialEq, Serialize, Deserialize)]
|
||||
pub enum EaseType {
|
||||
#[default]
|
||||
Linear,
|
||||
CubicHermite,
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, Debug, Reflect, PartialEq)]
|
||||
#[reflect(Component)]
|
||||
pub struct TransformKeyframe {
|
||||
incoming_easing: EaseType,
|
||||
affect_translation: bool,
|
||||
affect_rotation: bool,
|
||||
affect_scale: bool,
|
||||
}
|
||||
|
||||
impl Default for TransformKeyframe {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
incoming_easing: Default::default(),
|
||||
affect_translation: true,
|
||||
affect_rotation: true,
|
||||
affect_scale: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, Debug, Reflect, PartialEq)]
|
||||
#[reflect(Component, MapEntities)]
|
||||
pub struct Keyframe {
|
||||
target: Entity,
|
||||
time: f32,
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, Debug, Default, Reflect, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[reflect(Component)]
|
||||
pub struct KeyframeOrder(u32);
|
||||
|
||||
impl PartialOrd for Keyframe {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
self.time.partial_cmp(&other.time)
|
||||
}
|
||||
}
|
||||
|
||||
impl MapEntities for Keyframe {
|
||||
fn map_entities(&mut self, entity_mapper: &mut EntityMapper) {
|
||||
self.target = entity_mapper.get_or_reserve(self.target);
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Keyframe {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
target: Entity::PLACEHOLDER,
|
||||
time: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, Debug, Reflect, PartialEq)]
|
||||
#[reflect(Component)]
|
||||
pub struct TimedAnimationClipAsset {
|
||||
clip: Handle<AnimationClip>,
|
||||
duration: f32,
|
||||
speed: f32,
|
||||
}
|
||||
|
||||
impl PartialOrd for TimedAnimationClipAsset {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
match self.duration.partial_cmp(&other.duration) {
|
||||
Some(core::cmp::Ordering::Equal) => {}
|
||||
ord => return ord,
|
||||
}
|
||||
match self.speed.partial_cmp(&other.speed) {
|
||||
Some(core::cmp::Ordering::Equal) => {}
|
||||
ord => return ord,
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TimedAnimationClipAsset {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
clip: Default::default(),
|
||||
duration: Default::default(),
|
||||
speed: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, Debug, Default, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct AnimationTarget;
|
||||
|
||||
fn process_timeline_events(
|
||||
mut e_timeline_event: EventReader<TimelineInteractionEvent>,
|
||||
mut r_global_seek_time: ResMut<GlobalSeekTime>,
|
||||
) {
|
||||
for event in e_timeline_event.read() {
|
||||
match event {
|
||||
TimelineInteractionEvent::Play => r_global_seek_time.playing = true,
|
||||
TimelineInteractionEvent::Pause => {
|
||||
r_global_seek_time.playing = false;
|
||||
}
|
||||
TimelineInteractionEvent::Seek(time) => {
|
||||
r_global_seek_time.time = time.max(0.);
|
||||
}
|
||||
}
|
||||
|
||||
r_global_seek_time.delta = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn update_tick(mut r_global_seek_time: ResMut<GlobalSeekTime>, time: Res<Time>) {
|
||||
if r_global_seek_time.playing {
|
||||
let delta = time.delta_seconds();
|
||||
r_global_seek_time.time += delta;
|
||||
r_global_seek_time.delta = Some(delta);
|
||||
}
|
||||
}
|
||||
|
||||
fn process_keyframe_events(
|
||||
mut commands: Commands,
|
||||
mut e_timeline_event: EventReader<KeyframeInteractionEvent>,
|
||||
r_global_seek_time: Res<GlobalSeekTime>,
|
||||
q_animation_targets: Query<(Entity, &Transform), With<AnimationTarget>>,
|
||||
q_potential_targets: Query<Entity, (With<Transform>, Without<AnimationTarget>)>,
|
||||
mut q_keyframes: Query<(Entity, &mut Keyframe)>,
|
||||
) {
|
||||
for event in e_timeline_event.read() {
|
||||
info!("Processing Keyframe Event {event:?}");
|
||||
match event {
|
||||
KeyframeInteractionEvent::RecordKeyframe { target } => {
|
||||
if let Ok((target, transform)) = q_animation_targets.get(*target) {
|
||||
let mut existing = None;
|
||||
for (entity, keyframe) in q_keyframes.iter_mut() {
|
||||
if keyframe.target == target
|
||||
&& (keyframe.time - r_global_seek_time.time).abs()
|
||||
< MIN_KEYFRAME_DISTANCE
|
||||
{
|
||||
existing = Some(entity);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(existing) = existing {
|
||||
commands.entity(existing).insert((
|
||||
*transform,
|
||||
TransformKeyframe::default(),
|
||||
Keyframe {
|
||||
target,
|
||||
time: r_global_seek_time.time,
|
||||
},
|
||||
));
|
||||
} else {
|
||||
commands.spawn((
|
||||
SpatialBundle {
|
||||
transform: *transform,
|
||||
..Default::default()
|
||||
},
|
||||
Selectable,
|
||||
Keyframe {
|
||||
target,
|
||||
time: r_global_seek_time.time,
|
||||
},
|
||||
TransformKeyframe::default(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyframeInteractionEvent::MoveKeyframeTime { target, time } => {
|
||||
if let Ok((_, mut keyframe)) = q_keyframes.get_mut(*target) {
|
||||
keyframe.time = *time;
|
||||
}
|
||||
}
|
||||
KeyframeInteractionEvent::AddEntityTrack { target } => {
|
||||
if let Ok(target) = q_potential_targets.get(*target) {
|
||||
info!("Adding Animation Target to Entity");
|
||||
commands.entity(target).insert(AnimationTarget);
|
||||
} else {
|
||||
warn!("Can't add animation target to entity");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_transform_animation(
|
||||
mut commands: Commands,
|
||||
changed: Query<
|
||||
&Keyframe,
|
||||
(
|
||||
With<TransformKeyframe>,
|
||||
Or<(Changed<Keyframe>, Changed<Transform>)>,
|
||||
),
|
||||
>,
|
||||
keyframes: Query<
|
||||
(&Keyframe, &Transform, &TransformKeyframe, Entity),
|
||||
Without<MarkedForDeletion>,
|
||||
>,
|
||||
removed: Query<&Keyframe, With<MarkedForDeletion>>,
|
||||
) {
|
||||
let changed_targets = changed
|
||||
.iter()
|
||||
.map(|keyframe| keyframe.target)
|
||||
.chain(removed.iter().map(|keyframe| {
|
||||
info!("Processing a removed keyframe {keyframe:?}");
|
||||
keyframe.target
|
||||
}))
|
||||
.collect::<HashSet<_>>();
|
||||
let keyframes = keyframes
|
||||
.iter()
|
||||
.filter(|(keyframe, _, _, _)| changed_targets.contains(&keyframe.target))
|
||||
.group_by(|(keyframe, _, _, _)| keyframe.target);
|
||||
|
||||
for (target, keyframes) in &keyframes {
|
||||
let keyframes = keyframes.sorted_by(|a, b| a.0.time.total_cmp(&b.0.time));
|
||||
|
||||
let (_, _, _, translation, rotation, scale) = keyframes.fold(
|
||||
(
|
||||
Option::<(f32, Vec3)>::None,
|
||||
Option::<(f32, Quat)>::None,
|
||||
Option::<(f32, Vec3)>::None,
|
||||
Vec::<Tween<Transform>>::new(),
|
||||
Vec::<Tween<Transform>>::new(),
|
||||
Vec::<Tween<Transform>>::new(),
|
||||
),
|
||||
|(
|
||||
last_translation,
|
||||
last_rotation,
|
||||
last_scale,
|
||||
mut translation_list,
|
||||
mut rotation_list,
|
||||
mut scale_list,
|
||||
),
|
||||
(keyframe, transform, transform_keyframe, _)| {
|
||||
let time = keyframe.time;
|
||||
let translation = transform.translation;
|
||||
let rotation = transform.rotation;
|
||||
let scale = transform.scale;
|
||||
|
||||
info!("Processing Keyframe: {time} previous: {last_rotation:?}");
|
||||
|
||||
if transform_keyframe.affect_translation {
|
||||
let (last_time, start) = match last_translation {
|
||||
Some(v) => v,
|
||||
None => (0., translation),
|
||||
};
|
||||
info!("Should apply translation");
|
||||
if (time - last_time).abs() > 0.00001 {
|
||||
translation_list.push(Tween::new(
|
||||
EaseMethod::Linear,
|
||||
Duration::from_secs_f32((time - last_time).max(0.00001)),
|
||||
TransformPositionLens {
|
||||
start,
|
||||
end: translation,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if transform_keyframe.affect_rotation {
|
||||
let (last_time, start) = match last_rotation {
|
||||
Some(v) => v,
|
||||
None => (0., rotation),
|
||||
};
|
||||
|
||||
info!("Should apply rotation");
|
||||
|
||||
if (time - last_time).abs() > 0.00001 {
|
||||
rotation_list.push(Tween::new(
|
||||
EaseMethod::Linear,
|
||||
Duration::from_secs_f32((time - last_time).max(0.00001)),
|
||||
TransformRotationLens {
|
||||
start,
|
||||
end: rotation,
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
if transform_keyframe.affect_scale {
|
||||
let (last_time, start) = match last_scale {
|
||||
Some(v) => v,
|
||||
None => (0., scale),
|
||||
};
|
||||
info!("Should apply scale");
|
||||
if (time - last_time).abs() > 0.00001 {
|
||||
scale_list.push(Tween::new(
|
||||
EaseMethod::Linear,
|
||||
Duration::from_secs_f32((time - last_time).max(0.00001)),
|
||||
TransformScaleLens { start, end: scale },
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let next_translation = if transform_keyframe.affect_translation {
|
||||
Some((time, translation))
|
||||
} else {
|
||||
last_translation
|
||||
};
|
||||
let next_rotation = if transform_keyframe.affect_rotation {
|
||||
Some((time, rotation))
|
||||
} else {
|
||||
last_rotation
|
||||
};
|
||||
let next_scale = if transform_keyframe.affect_scale {
|
||||
Some((time, scale))
|
||||
} else {
|
||||
last_scale
|
||||
};
|
||||
|
||||
(
|
||||
next_translation,
|
||||
next_rotation,
|
||||
next_scale,
|
||||
translation_list,
|
||||
rotation_list,
|
||||
scale_list,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
let mut tracks = Vec::with_capacity(3);
|
||||
|
||||
if !translation.is_empty() {
|
||||
tracks.push(Sequence::new(translation));
|
||||
} else {
|
||||
info!("No translation");
|
||||
}
|
||||
if !rotation.is_empty() {
|
||||
tracks.push(Sequence::new(rotation));
|
||||
} else {
|
||||
info!("No rotation");
|
||||
}
|
||||
if !scale.is_empty() {
|
||||
tracks.push(Sequence::new(scale));
|
||||
} else {
|
||||
info!("No scale");
|
||||
}
|
||||
|
||||
if tracks.is_empty() {
|
||||
commands.entity(target).remove::<Animator<Transform>>();
|
||||
} else {
|
||||
let tracks = Tracks::new(tracks);
|
||||
|
||||
commands.entity(target).insert(Animator::new(tracks));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ComponentTarget<'a, T: Component>(&'a mut T);
|
||||
|
||||
impl<'a, T: Component> ComponentTarget<'a, T> {
|
||||
fn new(target: Mut<'a, T>) -> Self {
|
||||
Self(target.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Component> Targetable<T> for ComponentTarget<'a, T> {
|
||||
fn target_mut(&mut self) -> &mut T {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn timeline_bound_component_animator_system<T: Component>(
|
||||
time: Res<GlobalSeekTime>,
|
||||
mut query: Query<(Entity, &mut T, &mut Animator<T>)>,
|
||||
events: ResMut<Events<TweenCompleted>>,
|
||||
) {
|
||||
if time.is_changed() && time.time >= 0.0 {
|
||||
info!("Animating with time: {time:?}");
|
||||
let mut events: Mut<Events<TweenCompleted>> = events.into();
|
||||
for (entity, target, mut animator) in query.iter_mut() {
|
||||
if animator.tweenable().duration() <= Duration::from_secs_f32(0.0001) {
|
||||
continue;
|
||||
}
|
||||
let mut target = ComponentTarget::new(target);
|
||||
if let Some(delta) = time.delta {
|
||||
info!("Playing animation with delta {delta}");
|
||||
animator.tweenable_mut().tick(
|
||||
Duration::from_secs_f32(delta),
|
||||
&mut target,
|
||||
entity,
|
||||
&mut events,
|
||||
);
|
||||
} else {
|
||||
info!("setting tween time to {}", time.time);
|
||||
let tween = animator.tweenable_mut();
|
||||
tween.set_elapsed(Duration::from_secs_f32(time.time.max(0.)));
|
||||
info!("Tween time set successfully");
|
||||
tween.tick(Duration::ZERO, &mut target, entity, &mut events);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
mod wasm {
|
||||
use bevy::{prelude::*, utils::hashbrown::HashMap, window::PrimaryWindow};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use crate::{
|
||||
interaction::MarkedForDeletion,
|
||||
scene::SceneElement,
|
||||
wasm::{FromStr, StaticBuffer, WasmMessageBuffer},
|
||||
};
|
||||
|
||||
use super::{
|
||||
AnimationTarget, EaseType, GlobalSeekTime, Keyframe, KeyframeInteractionEvent,
|
||||
TimedAnimationClipAsset, TimelineInteractionEvent, TransformKeyframe,
|
||||
};
|
||||
|
||||
static SEEK_TIME: WasmMessageBuffer<TimelineInteractionEvent> = WasmMessageBuffer::new();
|
||||
static KEYFRAME_EVENT: WasmMessageBuffer<Vec<KeyframeInteractionEvent>> =
|
||||
WasmMessageBuffer::new();
|
||||
|
||||
#[wasm_bindgen(js_name = seekTime)]
|
||||
pub fn seek_time(timestamp: f32) {
|
||||
SEEK_TIME.write_message(TimelineInteractionEvent::Seek(timestamp));
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = play)]
|
||||
pub fn play() {
|
||||
SEEK_TIME.write_message(TimelineInteractionEvent::Play);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = pause)]
|
||||
pub fn pause() {
|
||||
SEEK_TIME.write_message(TimelineInteractionEvent::Pause);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = recordKeyframe)]
|
||||
pub fn record_keyframe(entities: Vec<String>) {
|
||||
KEYFRAME_EVENT.write_message(
|
||||
entities
|
||||
.iter()
|
||||
.filter_map(|entity| Entity::from_str(entity).ok())
|
||||
.map(|target| KeyframeInteractionEvent::RecordKeyframe { target })
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = moveKeyframeTime)]
|
||||
pub fn move_keyframe_time(entity: String, time: f32) {
|
||||
let Ok(target) = Entity::from_str(&entity) else {
|
||||
return;
|
||||
};
|
||||
KEYFRAME_EVENT.write_message(vec![KeyframeInteractionEvent::MoveKeyframeTime {
|
||||
target,
|
||||
time,
|
||||
}]);
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = addEntityTrack)]
|
||||
pub fn add_entity_track(entity: String) {
|
||||
let Ok(target) = Entity::from_str(&entity) else {
|
||||
return;
|
||||
};
|
||||
KEYFRAME_EVENT.write_message(vec![KeyframeInteractionEvent::AddEntityTrack { target }]);
|
||||
}
|
||||
|
||||
pub(super) fn update_global_seek_time(
|
||||
mut e_timeline_event: EventWriter<TimelineInteractionEvent>,
|
||||
) {
|
||||
if let Some(timestamp) = SEEK_TIME.take_message() {
|
||||
e_timeline_event.send(timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn update_keyframe_interaction(
|
||||
mut e_keyframe_event: EventWriter<KeyframeInteractionEvent>,
|
||||
) {
|
||||
if let Some(keyframes) = KEYFRAME_EVENT.take_message() {
|
||||
for keyframe in keyframes.into_iter() {
|
||||
e_keyframe_event.send(keyframe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(typescript_custom_section)]
|
||||
const TIMELINE_TYPES: &str = r#"
|
||||
export interface SeekTimeChanged extends CustomEvent {
|
||||
type: "seek-time-changed";
|
||||
detail: {
|
||||
time: number,
|
||||
playing: boolean
|
||||
};
|
||||
}
|
||||
|
||||
export interface UpdateAnimationTargets extends CustomEvent {
|
||||
type: "update-animation-targets",
|
||||
detail: {
|
||||
added: Record<string, { name?: string, target_type: SceneElement } >,
|
||||
removed: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface TransformKeyframeInfo {
|
||||
target: string,
|
||||
time: number,
|
||||
entity: string,
|
||||
incoming_easing: "linear" | "cubic-hermite",
|
||||
affect_translation: bool,
|
||||
affect_rotation: bool,
|
||||
affect_scale: bool
|
||||
}
|
||||
|
||||
export interface TimedAnimationClipInfo {
|
||||
target: string,
|
||||
time: number,
|
||||
entity: string,
|
||||
type: 'animation-clip',
|
||||
clip: string,
|
||||
duration: number,
|
||||
speed: number
|
||||
}
|
||||
|
||||
export type Keyframe = TransformKeyframeInfo | TimedAnimationClipInfo;
|
||||
|
||||
export interface UpdateKeyframes extends CustomEvent {
|
||||
type: "update-keyframes",
|
||||
detail: {
|
||||
added: Record<string, Keyframe>,
|
||||
removed: string[]
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
export interface GlobalEventHandlersEventMap {
|
||||
"seek-time-changed": CameraKeyframesEvent;
|
||||
"update-animation-targets": UpdateAnimationTargets;
|
||||
"update-keyframes": UpdateKeyframes;
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct AddedAnimationTargetRecordItem {
|
||||
name: Option<String>,
|
||||
target_type: SceneElement,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct UpdatedAnimationTargetsEvent {
|
||||
added: HashMap<String, AddedAnimationTargetRecordItem>,
|
||||
removed: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct TimedAnimationClipInfo {
|
||||
entity: String,
|
||||
target: String,
|
||||
time: f32,
|
||||
clip: String,
|
||||
duration: f32,
|
||||
speed: f32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TransformKeframeInfo {
|
||||
entity: String,
|
||||
target: String,
|
||||
time: f32,
|
||||
incoming_easing: EaseType,
|
||||
affect_translation: bool,
|
||||
affect_rotation: bool,
|
||||
affect_scale: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct UpdateKeyframes {
|
||||
added: HashMap<String, serde_json::Value>,
|
||||
removed: Vec<String>,
|
||||
}
|
||||
|
||||
pub(super) fn notify_updated_animation_targets(
|
||||
q_window: Query<&Window, With<PrimaryWindow>>,
|
||||
q_cameras: Query<(Entity, Option<&Name>), (Added<AnimationTarget>, With<Camera>)>,
|
||||
q_scene_elements: Query<
|
||||
(Entity, Option<&Name>, &SceneElement),
|
||||
(Added<AnimationTarget>, Without<Camera>),
|
||||
>,
|
||||
q_transforms: Query<
|
||||
(Entity, Option<&Name>),
|
||||
(
|
||||
Added<AnimationTarget>,
|
||||
With<Transform>,
|
||||
Without<Camera>,
|
||||
Without<SceneElement>,
|
||||
),
|
||||
>,
|
||||
mut removed: bevy::prelude::RemovedComponents<AnimationTarget>,
|
||||
) -> Result<(), String> {
|
||||
let Ok(window) = q_window.get_single() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let added = q_transforms
|
||||
.iter()
|
||||
.map(|(entity, name)| {
|
||||
(
|
||||
format!("{entity:?}"),
|
||||
AddedAnimationTargetRecordItem {
|
||||
name: name.map(|v| v.to_string()),
|
||||
target_type: SceneElement::Generic,
|
||||
},
|
||||
)
|
||||
})
|
||||
.chain(q_cameras.iter().map(|(entity, name)| {
|
||||
(
|
||||
format!("{entity:?}"),
|
||||
AddedAnimationTargetRecordItem {
|
||||
name: name.map(|v| v.to_string()),
|
||||
target_type: SceneElement::Camera,
|
||||
},
|
||||
)
|
||||
}))
|
||||
.chain(q_scene_elements.iter().map(|(entity, name, element)| {
|
||||
(
|
||||
format!("{entity:?}"),
|
||||
AddedAnimationTargetRecordItem {
|
||||
name: name.map(|v| v.to_string()),
|
||||
target_type: *element,
|
||||
},
|
||||
)
|
||||
}))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let removed = removed.read().map(|v| format!("{v:?}")).collect::<Vec<_>>();
|
||||
|
||||
if added.is_empty() && removed.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let event = UpdatedAnimationTargetsEvent { added, removed };
|
||||
|
||||
crate::wasm::dispatch_to_js(
|
||||
window,
|
||||
"update-animation-targets",
|
||||
&serde_json::to_value(&event).map_err(|e| format!("{e}"))?,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn notify_keyframe_changes(
|
||||
q_window: Query<&Window, With<PrimaryWindow>>,
|
||||
q_transform_keyframes: Query<
|
||||
(Entity, &Keyframe, &TransformKeyframe),
|
||||
Or<(Changed<Keyframe>, Changed<TransformKeyframe>)>,
|
||||
>,
|
||||
q_clips: Query<
|
||||
(Entity, &Keyframe, &TimedAnimationClipAsset),
|
||||
Or<(Changed<Keyframe>, Changed<TimedAnimationClipAsset>)>,
|
||||
>,
|
||||
removed: Query<Entity, With<MarkedForDeletion>>,
|
||||
) -> Result<(), String> {
|
||||
let Ok(window) = q_window.get_single() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let added = q_transform_keyframes
|
||||
.iter()
|
||||
.map(|(entity, keyframe, transform_keyframe)| {
|
||||
match serde_json::to_value(TransformKeframeInfo {
|
||||
entity: format!("{entity:?}"),
|
||||
target: format!("{:?}", keyframe.target),
|
||||
time: keyframe.time,
|
||||
incoming_easing: transform_keyframe.incoming_easing,
|
||||
affect_translation: transform_keyframe.affect_translation,
|
||||
affect_rotation: transform_keyframe.affect_rotation,
|
||||
affect_scale: transform_keyframe.affect_scale,
|
||||
})
|
||||
.map_err(|e| format!("{e}"))
|
||||
{
|
||||
Ok(value) => Ok((format!("{entity:?}"), value)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
})
|
||||
.chain(q_clips.iter().map(|(entity, keyframe, clip)| {
|
||||
match serde_json::to_value(TimedAnimationClipInfo {
|
||||
entity: format!("{entity:?}"),
|
||||
target: format!("{:?}", keyframe.target),
|
||||
time: keyframe.time,
|
||||
clip: clip.clip.id().to_string(),
|
||||
duration: clip.duration,
|
||||
speed: clip.speed,
|
||||
})
|
||||
.map_err(|e| format!("{e}"))
|
||||
{
|
||||
Ok(value) => Ok((format!("{entity:?}"), value)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}))
|
||||
.collect::<Result<HashMap<_, _>, String>>()?;
|
||||
|
||||
let removed = removed.iter().map(|v| format!("{v:?}")).collect::<Vec<_>>();
|
||||
|
||||
if added.is_empty() && removed.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let event = UpdateKeyframes { added, removed };
|
||||
|
||||
info!("Updating keyframes: {event:?}");
|
||||
|
||||
crate::wasm::dispatch_to_js(
|
||||
window,
|
||||
"update-keyframes",
|
||||
&serde_json::to_value(&event).map_err(|e| format!("{e}"))?,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn notify_seek_time_changes(
|
||||
r_global_seek_time: Res<GlobalSeekTime>,
|
||||
q_window: Query<&Window, With<PrimaryWindow>>,
|
||||
) -> Result<(), String> {
|
||||
if !r_global_seek_time.is_changed() {
|
||||
return Ok(());
|
||||
}
|
||||
let Ok(window) = q_window.get_single() else {
|
||||
return Ok(());
|
||||
};
|
||||
info!("Current Seek Time: {r_global_seek_time:?}");
|
||||
|
||||
crate::wasm::dispatch_to_js(
|
||||
window,
|
||||
"seek-time-changed",
|
||||
&serde_json::to_value(r_global_seek_time.as_ref()).map_err(|e| format!("{e}"))?,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ impl Plugin for SelectionPlugin {
|
||||
PostUpdate,
|
||||
draw_selection_outline.run_if(resource_changed_or_removed::<Selection>()),
|
||||
)
|
||||
.add_systems(First, finalize_delete)
|
||||
.add_systems(
|
||||
OnEnter(SceneState::Active),
|
||||
(apply_deferred, tag_scene_elements)
|
||||
@@ -158,6 +159,9 @@ fn update_selection(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct MarkedForDeletion;
|
||||
|
||||
fn process_delete_inputs(
|
||||
mut cmd: Commands,
|
||||
mut er_input: EventReader<InputAction>,
|
||||
@@ -172,7 +176,7 @@ fn process_delete_inputs(
|
||||
}) {
|
||||
let entity = **r_selection;
|
||||
|
||||
cmd.entity(entity).despawn_recursive();
|
||||
cmd.entity(entity).insert(MarkedForDeletion);
|
||||
cmd.remove_resource::<Selection>();
|
||||
ew_changes.send(NewChange {
|
||||
change: Arc::new(RemovedEntity { entity }),
|
||||
@@ -180,6 +184,12 @@ fn process_delete_inputs(
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_delete(mut cmd: Commands, q: Query<Entity, With<MarkedForDeletion>>) {
|
||||
for entity in &q {
|
||||
cmd.entity(entity).despawn_recursive();
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_selection_outline(
|
||||
mut cmd: Commands,
|
||||
q_selected: Query<&Children, With<Selected>>,
|
||||
|
||||
@@ -218,7 +218,7 @@ pub(crate) fn update_camera_motion(
|
||||
// main_cam_transform.look_at(Vec3::ZERO, Vec3::Y);
|
||||
}
|
||||
|
||||
fn zoom_camera(main_cam_transform: &mut Transform, time: &Time<()>, speed : f32) {
|
||||
fn zoom_camera(main_cam_transform: &mut Transform, time: &Time<()>, speed: f32) {
|
||||
let scaled_time = time.elapsed().as_secs_f32() * speed;
|
||||
let normalized_cos = (-scaled_time.cos() + 1.0) / 2.0;
|
||||
let start_pos = Vec3::new(0.366, 1.1, 2.5);
|
||||
@@ -228,7 +228,7 @@ fn zoom_camera(main_cam_transform: &mut Transform, time: &Time<()>, speed : f32)
|
||||
main_cam_transform.rotation = Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
fn pan_camera(main_cam_transform: &mut Transform, time: &Time<()>, speed : f32) {
|
||||
fn pan_camera(main_cam_transform: &mut Transform, time: &Time<()>, speed: f32) {
|
||||
let scaled_time = time.elapsed().as_secs_f32() * speed;
|
||||
let normalized_cos = (-scaled_time.cos() + 1.0) / 2.0;
|
||||
let start_pos = Vec3::new(0.676, 1.1, 0.119);
|
||||
@@ -238,7 +238,7 @@ fn pan_camera(main_cam_transform: &mut Transform, time: &Time<()>, speed : f32)
|
||||
main_cam_transform.rotation = Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
fn orbit_camera(main_cam_transform: &mut Mut<'_, Transform>, time: &Res<'_, Time>, speed : f32) {
|
||||
fn orbit_camera(main_cam_transform: &mut Mut<'_, Transform>, time: &Res<'_, Time>, speed: f32) {
|
||||
let scaled_time = time.elapsed().as_secs_f32() * speed;
|
||||
let max_angle = std::f32::consts::PI / 2.0;
|
||||
let normalized_cos = (scaled_time.cos() + 1.0) / 2.0;
|
||||
|
||||
@@ -23,6 +23,7 @@ use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use wgpu::{TextureDescriptor, TextureUsages};
|
||||
|
||||
use crate::{
|
||||
anim::timeline_animation::AnimationTarget,
|
||||
camera_controller::{CameraController, CameraControllerBundle},
|
||||
change_tracking::{ChangeTracker, ChangeTrackingPlugin},
|
||||
debug::DebugPoint,
|
||||
@@ -105,6 +106,7 @@ impl Plugin for ScenePlugin {
|
||||
fn minimal_scene_setup(mut cmd: Commands) {
|
||||
cmd.spawn((
|
||||
Name::new("Directional Light"),
|
||||
AnimationTarget,
|
||||
DirectionalLightBundle {
|
||||
directional_light: DirectionalLight {
|
||||
shadows_enabled: false,
|
||||
@@ -260,6 +262,7 @@ fn initialize(mut cmd: Commands) {
|
||||
cmd.spawn((
|
||||
Name::new("Main Camera"),
|
||||
MainCamera,
|
||||
AnimationTarget,
|
||||
ChangeTracker,
|
||||
Camera3dBundle {
|
||||
transform: camera_xform,
|
||||
@@ -316,6 +319,7 @@ fn initialiaze_headless(
|
||||
cmd.spawn((
|
||||
Name::new("Main Camera"),
|
||||
MainCamera,
|
||||
AnimationTarget,
|
||||
Camera3dBundle {
|
||||
transform: camera_xform,
|
||||
tonemapping: Tonemapping::BlenderFilmic,
|
||||
|
||||
@@ -144,9 +144,11 @@ fn update_screen_capture_on_extract_step(
|
||||
fn extract_screen_capture_components(
|
||||
mut commands: Commands,
|
||||
screen_capure_comps: Extract<Query<(Entity, &ScreenCapture), With<Camera>>>,
|
||||
scene_state : Extract<Res<State<SceneState>>>
|
||||
scene_state: Extract<Res<State<SceneState>>>,
|
||||
) {
|
||||
if scene_state.get() != &SceneState::Active { return };
|
||||
if scene_state.get() != &SceneState::Active {
|
||||
return;
|
||||
};
|
||||
for (entity, screen_capture) in screen_capure_comps.iter() {
|
||||
if screen_capture.should_capture() {
|
||||
info!(
|
||||
@@ -252,25 +254,24 @@ impl ViewNode for ScreenCaptureNode {
|
||||
//==============================================================================
|
||||
|
||||
fn save_buffer_to_file(
|
||||
mut frames_to_skip : Local<Option<u32>>,
|
||||
mut frames_to_skip: Local<Option<u32>>,
|
||||
buffers: Query<(&ScreenCapture, &ScreenCaptureBuffers)>,
|
||||
device: Res<RenderDevice>,
|
||||
) {
|
||||
for (screen_capture, buffers) in buffers.iter() {
|
||||
|
||||
// ALERT!! ALERT!! Filthy Hack in effect
|
||||
// Engine does not load scene in time for the first 3 frames
|
||||
// So we skip them. Oops :(
|
||||
// TODO: Fix the Race Condition and properly take screenshots.
|
||||
let frames_skipped = frames_to_skip.unwrap_or(3);
|
||||
|
||||
|
||||
if frames_skipped > 0 {
|
||||
println!("Skipping Frame");
|
||||
*frames_to_skip = Some(frames_skipped - 1);
|
||||
return;
|
||||
}
|
||||
// Filthy Hack Ends
|
||||
|
||||
|
||||
if buffers.main_bytes_per_row != buffers.main_padded_bytes_per_row {
|
||||
println!("These don't match!");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user