Timeline Saving/Loading, Cameras, and Headless Playback (#98)

This commit is contained in:
Lee-Orr
2024-03-21 20:35:25 -04:00
committed by GitHub
parent 7f12634780
commit 67a12e42e2
11 changed files with 748 additions and 81 deletions
+2
View File
@@ -41,3 +41,5 @@ Thumbs.db
/target /target
/screen_shots /screen_shots
/frames
+1
View File
@@ -501,6 +501,7 @@ mod tests {
use std::time::Duration; use std::time::Duration;
// #[test] // FIXME // #[test] // FIXME
#[allow(dead_code)]
fn macro_create() { fn macro_create() {
let bvh = bvh! { let bvh = bvh! {
HIERARCHY HIERARCHY
@@ -47,9 +47,9 @@ implements TreeSelectionProvider {
@state() playHead = 0; @state() playHead = 0;
@state() playing = false; @state() playing = false;
@state() tracks : Record<string, {name?: string, target_type: studio.SceneElement }> = {}; @state() tracks : Record<string, {name?: string, target_type: studio.SceneElement, animation_target: studio.AnimationTarget }> = {};
@state() track_ordered : string[] = []; @state() track_ordered : [string, studio.AnimationTarget][] = [];
@state() keyframes : Record<string, Keyframe> = {}; @state() keyframes : Record<string, Keyframe> = {};
@@ -112,7 +112,7 @@ implements TreeSelectionProvider {
delete tracks[track_entity]; delete tracks[track_entity];
} }
this.track_ordered = Object.keys(tracks); this.track_ordered = Object.keys(tracks).map(entity => [entity,tracks[entity].animation_target]);
if (typeof this.selectedTrack === "object") { if (typeof this.selectedTrack === "object") {
if (!isOutsideEntity(this.selectedTrack) && !(this.selectedTrack.track in tracks)) { if (!isOutsideEntity(this.selectedTrack) && !(this.selectedTrack.track in tracks)) {
@@ -262,9 +262,11 @@ implements TreeSelectionProvider {
protected override render(): TemplateResult { protected override render(): TemplateResult {
const track_order_hashed : Record<string, number> = {}; const track_order_hashed : Record<string, number> = {};
const track_animation_target_order_hashed : Record<studio.AnimationTarget, number> = {};
for (const [index, track] of this.track_ordered.entries()) { for (const [index, [track, target]] of this.track_ordered.entries()) {
track_order_hashed[track] = index; track_order_hashed[track] = index;
track_animation_target_order_hashed[target] = index;
} }
const selected = !isOutsideEntity(this.selectedTrack) && this.selectedTrack?.track || false; const selected = !isOutsideEntity(this.selectedTrack) && this.selectedTrack?.track || false;
@@ -282,7 +284,7 @@ implements TreeSelectionProvider {
'--clip-start': `${clip_start * 100}%` '--clip-start': `${clip_start * 100}%`
}; };
const tracklist = this.track_ordered.map((entity) => { const tracklist = this.track_ordered.map(([entity,]) => {
let track = this.tracks[entity]; let track = this.tracks[entity];
let name = track.name ?? track.target_type; let name = track.name ?? track.target_type;
let icon = this.sceneData?.entityMap.get(entity)?.icon ?? "minus"; let icon = this.sceneData?.entityMap.get(entity)?.icon ?? "minus";
@@ -381,6 +383,7 @@ implements TreeSelectionProvider {
.playHead=${bind(this, "playHead")} .playHead=${bind(this, "playHead")}
.keyframes=${bind(this, "keyframes")} .keyframes=${bind(this, "keyframes")}
.track_ordered=${track_order_hashed} .track_ordered=${track_order_hashed}
.track_animation_target_ordered=${track_animation_target_order_hashed}
></sts-anim-timeline> ></sts-anim-timeline>
` `
}; };
@@ -69,7 +69,8 @@ export class AnimTimelineElement extends LitElement {
@property({ type: Number }) playHead = 0; @property({ type: Number }) playHead = 0;
/** Keyframe positions (in seconds) */ /** Keyframe positions (in seconds) */
@property({ attribute: false }) keyframes: Record<string, studio.Keyframe> = {}; @property({ attribute: false }) keyframes: Record<string, studio.Keyframe> = {};
@property({ attribute: false }) track_ordered: Record<string, number> = {} @property({ attribute: false }) track_ordered: Record<string, number> = {};
@property({ attribute: false }) track_animation_target_ordered: Record<string, number> = {};
@state() _selectedKeyframe: string | null = null; @state() _selectedKeyframe: string | null = null;
@@ -344,7 +345,7 @@ export class AnimTimelineElement extends LitElement {
Object.entries(this.keyframes), Object.entries(this.keyframes),
([id]) => id, ([id]) => id,
([id, keyframe]) => { ([id, keyframe]) => {
let row = this.track_ordered[keyframe.target]; let row = this.track_animation_target_ordered[keyframe.target];
return html` return html`
<div id=${id} <div id=${id}
class=${classMap({ class=${classMap({
+1
View File
@@ -51,6 +51,7 @@ anyhow.workspace = true
serde_repr = "0.1.18" serde_repr = "0.1.18"
bevy_tweening = "0.9" bevy_tweening = "0.9"
async-compat = "0.2" async-compat = "0.2"
uuid = { version = "1", features = ["v4", "serde"]}
[dependencies.reqwest] [dependencies.reqwest]
version = "0.11.24" version = "0.11.24"
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+128 -50
View File
@@ -1,12 +1,8 @@
use std::time::Duration; use std::{str::FromStr, time::Duration};
use bevy::{ use bevy::{
ecs::{
entity::{EntityMapper, MapEntities},
reflect::ReflectMapEntities,
},
prelude::*, prelude::*,
utils::HashSet, utils::{hashbrown::HashMap, HashSet},
}; };
use bevy_tweening::{ use bevy_tweening::{
@@ -17,7 +13,10 @@ use itertools::Itertools;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use space_editor::{prelude::PrefabMarker, space_prefab::editor_registry::EditorRegistryExt}; use space_editor::{prelude::PrefabMarker, space_prefab::editor_registry::EditorRegistryExt};
use crate::interaction::{MarkedForDeletion, Selectable}; use crate::{
headless::HeadlessSettings,
interaction::{MarkedForDeletion, Selectable},
};
pub struct TimelinePlugin; pub struct TimelinePlugin;
@@ -34,6 +33,7 @@ impl Plugin for TimelinePlugin {
.register_type::<KeyframeOrder>() .register_type::<KeyframeOrder>()
.editor_registry::<KeyframeOrder>() .editor_registry::<KeyframeOrder>()
.register_type::<EaseType>() .register_type::<EaseType>()
.register_type::<uuid::Uuid>()
.add_event::<TimelineInteractionEvent>() .add_event::<TimelineInteractionEvent>()
.add_event::<KeyframeInteractionEvent>(); .add_event::<KeyframeInteractionEvent>();
@@ -131,10 +131,10 @@ impl Default for TransformKeyframe {
} }
#[derive(Component, Clone, Debug, Reflect, PartialEq)] #[derive(Component, Clone, Debug, Reflect, PartialEq)]
#[reflect(Component, MapEntities)] #[reflect(Component)]
pub struct Keyframe { pub struct Keyframe {
target: Entity, target: AnimationTarget,
time: f32, pub time: f32,
} }
#[derive(Component, Clone, Debug, Default, Reflect, PartialEq, Eq, PartialOrd, Ord)] #[derive(Component, Clone, Debug, Default, Reflect, PartialEq, Eq, PartialOrd, Ord)]
@@ -147,17 +147,11 @@ impl PartialOrd for Keyframe {
} }
} }
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 { impl Default for Keyframe {
fn default() -> Self { fn default() -> Self {
Self { Self {
target: Entity::PLACEHOLDER,
time: Default::default(), time: Default::default(),
target: AnimationTarget::Unknown,
} }
} }
} }
@@ -194,9 +188,61 @@ impl Default for TimedAnimationClipAsset {
} }
} }
#[derive(Component, Clone, Debug, Default, Reflect)] #[derive(
Component, Clone, Debug, Default, Reflect, PartialEq, Eq, Hash, Copy, Serialize, Deserialize,
)]
#[reflect(Component)] #[reflect(Component)]
pub struct AnimationTarget; #[serde(try_from = "&str", into = "String")]
pub enum AnimationTarget {
#[default]
Unknown,
MainCamera,
Uuid(uuid::Uuid),
}
impl AnimationTarget {
pub fn new() -> Self {
Self::Uuid(uuid::Uuid::new_v4())
}
}
impl ToString for AnimationTarget {
fn to_string(&self) -> String {
match self {
Self::Unknown => "unknown".to_string(),
Self::MainCamera => "MainCamera".to_string(),
Self::Uuid(uuid) => uuid.to_string(),
}
}
}
impl From<AnimationTarget> for String {
fn from(val: AnimationTarget) -> Self {
val.to_string()
}
}
impl TryFrom<&str> for AnimationTarget {
type Error = uuid::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
impl FromStr for AnimationTarget {
type Err = uuid::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "unknown" {
Ok(Self::Unknown)
} else if s == "MainCamera" {
Ok(Self::MainCamera)
} else {
Ok(Self::Uuid(s.parse()?))
}
}
}
fn process_timeline_events( fn process_timeline_events(
mut e_timeline_event: EventReader<TimelineInteractionEvent>, mut e_timeline_event: EventReader<TimelineInteractionEvent>,
@@ -217,9 +263,16 @@ fn process_timeline_events(
} }
} }
fn update_tick(mut r_global_seek_time: ResMut<GlobalSeekTime>, time: Res<Time>) { fn update_tick(
mut r_global_seek_time: ResMut<GlobalSeekTime>,
time: Res<Time>,
headless_settings: Option<Res<HeadlessSettings>>,
) {
if r_global_seek_time.playing { if r_global_seek_time.playing {
let delta = time.delta_seconds(); let delta = match headless_settings {
Some(headless_settings) => headless_settings.frame_delta,
None => time.delta_seconds(),
};
r_global_seek_time.time += delta; r_global_seek_time.time += delta;
r_global_seek_time.delta = Some(delta); r_global_seek_time.delta = Some(delta);
} }
@@ -229,7 +282,7 @@ fn process_keyframe_events(
mut commands: Commands, mut commands: Commands,
mut e_timeline_event: EventReader<KeyframeInteractionEvent>, mut e_timeline_event: EventReader<KeyframeInteractionEvent>,
r_global_seek_time: Res<GlobalSeekTime>, r_global_seek_time: Res<GlobalSeekTime>,
q_animation_targets: Query<(Entity, &Transform), With<AnimationTarget>>, q_animation_targets: Query<(Entity, &Transform, &AnimationTarget)>,
q_potential_targets: Query<Entity, (With<Transform>, Without<AnimationTarget>)>, q_potential_targets: Query<Entity, (With<Transform>, Without<AnimationTarget>)>,
mut q_keyframes: Query<(Entity, &mut Keyframe)>, mut q_keyframes: Query<(Entity, &mut Keyframe)>,
) { ) {
@@ -237,10 +290,11 @@ fn process_keyframe_events(
info!("Processing Keyframe Event {event:?}"); info!("Processing Keyframe Event {event:?}");
match event { match event {
KeyframeInteractionEvent::RecordKeyframe { target } => { KeyframeInteractionEvent::RecordKeyframe { target } => {
if let Ok((target, transform)) = q_animation_targets.get(*target) { if let Ok((_target, transform, animation_target)) = q_animation_targets.get(*target)
{
let mut existing = None; let mut existing = None;
for (entity, keyframe) in q_keyframes.iter_mut() { for (entity, keyframe) in q_keyframes.iter_mut() {
if keyframe.target == target if keyframe.target == *animation_target
&& (keyframe.time - r_global_seek_time.time).abs() && (keyframe.time - r_global_seek_time.time).abs()
< MIN_KEYFRAME_DISTANCE < MIN_KEYFRAME_DISTANCE
{ {
@@ -253,8 +307,8 @@ fn process_keyframe_events(
*transform, *transform,
TransformKeyframe::default(), TransformKeyframe::default(),
Keyframe { Keyframe {
target,
time: r_global_seek_time.time, time: r_global_seek_time.time,
target: *animation_target,
}, },
)); ));
} else { } else {
@@ -266,8 +320,8 @@ fn process_keyframe_events(
}, },
Selectable, Selectable,
Keyframe { Keyframe {
target,
time: r_global_seek_time.time, time: r_global_seek_time.time,
target: *animation_target,
}, },
TransformKeyframe::default(), TransformKeyframe::default(),
)); ));
@@ -282,7 +336,7 @@ fn process_keyframe_events(
KeyframeInteractionEvent::AddEntityTrack { target } => { KeyframeInteractionEvent::AddEntityTrack { target } => {
if let Ok(target) = q_potential_targets.get(*target) { if let Ok(target) = q_potential_targets.get(*target) {
info!("Adding Animation Target to Entity"); info!("Adding Animation Target to Entity");
commands.entity(target).insert(AnimationTarget); commands.entity(target).insert(AnimationTarget::new());
} else { } else {
warn!("Can't add animation target to entity"); warn!("Can't add animation target to entity");
} }
@@ -304,8 +358,14 @@ fn generate_transform_animation(
(&Keyframe, &Transform, &TransformKeyframe, Entity), (&Keyframe, &Transform, &TransformKeyframe, Entity),
Without<MarkedForDeletion>, Without<MarkedForDeletion>,
>, >,
added_targets: Query<(Entity, &AnimationTarget), Added<AnimationTarget>>,
valid_targets: Query<(Entity, &AnimationTarget)>,
removed: Query<&Keyframe, With<MarkedForDeletion>>, removed: Query<&Keyframe, With<MarkedForDeletion>>,
) { ) {
let available_targets = valid_targets
.iter()
.map(|(e, a)| (*a, e))
.collect::<HashMap<_, _>>();
let changed_targets = changed let changed_targets = changed
.iter() .iter()
.map(|keyframe| keyframe.target) .map(|keyframe| keyframe.target)
@@ -313,6 +373,7 @@ fn generate_transform_animation(
info!("Processing a removed keyframe {keyframe:?}"); info!("Processing a removed keyframe {keyframe:?}");
keyframe.target keyframe.target
})) }))
.chain(added_targets.iter().map(|(_, a)| *a))
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
let keyframes = keyframes let keyframes = keyframes
.iter() .iter()
@@ -320,6 +381,12 @@ fn generate_transform_animation(
.group_by(|(keyframe, _, _, _)| keyframe.target); .group_by(|(keyframe, _, _, _)| keyframe.target);
for (target, keyframes) in &keyframes { for (target, keyframes) in &keyframes {
let Some(target) = available_targets.get(&target) else {
info!("No valid target.");
continue;
};
let target = *target;
let keyframes = keyframes.sorted_by(|a, b| a.0.time.total_cmp(&b.0.time)); let keyframes = keyframes.sorted_by(|a, b| a.0.time.total_cmp(&b.0.time));
let (_, _, _, translation, rotation, scale) = keyframes.fold( let (_, _, _, translation, rotation, scale) = keyframes.fold(
@@ -474,7 +541,6 @@ pub fn timeline_bound_component_animator_system<T: Component>(
events: ResMut<Events<TweenCompleted>>, events: ResMut<Events<TweenCompleted>>,
) { ) {
if time.is_changed() && time.time >= 0.0 { if time.is_changed() && time.time >= 0.0 {
info!("Animating with time: {time:?}");
let mut events: Mut<Events<TweenCompleted>> = events.into(); let mut events: Mut<Events<TweenCompleted>> = events.into();
for (entity, target, mut animator) in query.iter_mut() { for (entity, target, mut animator) in query.iter_mut() {
if animator.tweenable().duration() <= Duration::from_secs_f32(0.0001) { if animator.tweenable().duration() <= Duration::from_secs_f32(0.0001) {
@@ -482,7 +548,6 @@ pub fn timeline_bound_component_animator_system<T: Component>(
} }
let mut target = ComponentTarget::new(target); let mut target = ComponentTarget::new(target);
if let Some(delta) = time.delta { if let Some(delta) = time.delta {
info!("Playing animation with delta {delta}");
animator.tweenable_mut().tick( animator.tweenable_mut().tick(
Duration::from_secs_f32(delta), Duration::from_secs_f32(delta),
&mut target, &mut target,
@@ -597,13 +662,13 @@ mod wasm {
export interface UpdateAnimationTargets extends CustomEvent { export interface UpdateAnimationTargets extends CustomEvent {
type: "update-animation-targets", type: "update-animation-targets",
detail: { detail: {
added: Record<string, { name?: string, target_type: SceneElement } >, added: Record<string, { name?: string, target_type: SceneElement, animation_target: AnimationTarget } >,
removed: string[] removed: string[]
} }
} }
export interface TransformKeyframeInfo { export interface TransformKeyframeInfo {
target: string, target: AnimationTarget,
time: number, time: number,
entity: string, entity: string,
incoming_easing: "linear" | "cubic-hermite", incoming_easing: "linear" | "cubic-hermite",
@@ -613,7 +678,7 @@ mod wasm {
} }
export interface TimedAnimationClipInfo { export interface TimedAnimationClipInfo {
target: string, target: AnimationTarget,
time: number, time: number,
entity: string, entity: string,
type: 'animation-clip', type: 'animation-clip',
@@ -624,6 +689,8 @@ mod wasm {
export type Keyframe = TransformKeyframeInfo | TimedAnimationClipInfo; export type Keyframe = TransformKeyframeInfo | TimedAnimationClipInfo;
export type AnimationTarget = string;
export interface UpdateKeyframes extends CustomEvent { export interface UpdateKeyframes extends CustomEvent {
type: "update-keyframes", type: "update-keyframes",
detail: { detail: {
@@ -645,6 +712,7 @@ mod wasm {
struct AddedAnimationTargetRecordItem { struct AddedAnimationTargetRecordItem {
name: Option<String>, name: Option<String>,
target_type: SceneElement, target_type: SceneElement,
animation_target: AnimationTarget,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@@ -656,7 +724,7 @@ mod wasm {
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
struct TimedAnimationClipInfo { struct TimedAnimationClipInfo {
entity: String, entity: String,
target: String, target: AnimationTarget,
time: f32, time: f32,
clip: String, clip: String,
duration: f32, duration: f32,
@@ -667,7 +735,7 @@ mod wasm {
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct TransformKeframeInfo { struct TransformKeframeInfo {
entity: String, entity: String,
target: String, target: AnimationTarget,
time: f32, time: f32,
incoming_easing: EaseType, incoming_easing: EaseType,
affect_translation: bool, affect_translation: bool,
@@ -683,13 +751,16 @@ mod wasm {
pub(super) fn notify_updated_animation_targets( pub(super) fn notify_updated_animation_targets(
q_window: Query<&Window, With<PrimaryWindow>>, q_window: Query<&Window, With<PrimaryWindow>>,
q_cameras: Query<(Entity, Option<&Name>), (Added<AnimationTarget>, With<Camera>)>, q_cameras: Query<
(Entity, Option<&Name>, &AnimationTarget),
(Added<AnimationTarget>, With<Camera>),
>,
q_scene_elements: Query< q_scene_elements: Query<
(Entity, Option<&Name>, &SceneElement), (Entity, Option<&Name>, &SceneElement, &AnimationTarget),
(Added<AnimationTarget>, Without<Camera>), (Added<AnimationTarget>, Without<Camera>),
>, >,
q_transforms: Query< q_transforms: Query<
(Entity, Option<&Name>), (Entity, Option<&Name>, &AnimationTarget),
( (
Added<AnimationTarget>, Added<AnimationTarget>,
With<Transform>, With<Transform>,
@@ -705,33 +776,40 @@ mod wasm {
let added = q_transforms let added = q_transforms
.iter() .iter()
.map(|(entity, name)| { .map(|(entity, name, target)| {
( (
format!("{entity:?}"), format!("{entity:?}"),
AddedAnimationTargetRecordItem { AddedAnimationTargetRecordItem {
name: name.map(|v| v.to_string()), name: name.map(|v| v.to_string()),
target_type: SceneElement::Generic, target_type: SceneElement::Generic,
animation_target: *target,
}, },
) )
}) })
.chain(q_cameras.iter().map(|(entity, name)| { .chain(q_cameras.iter().map(|(entity, name, target)| {
( (
format!("{entity:?}"), format!("{entity:?}"),
AddedAnimationTargetRecordItem { AddedAnimationTargetRecordItem {
name: name.map(|v| v.to_string()), name: name.map(|v| v.to_string()),
target_type: SceneElement::Camera, target_type: SceneElement::Camera,
animation_target: *target,
}, },
) )
})) }))
.chain(q_scene_elements.iter().map(|(entity, name, element)| { .chain(
( q_scene_elements
format!("{entity:?}"), .iter()
AddedAnimationTargetRecordItem { .map(|(entity, name, element, target)| {
name: name.map(|v| v.to_string()), (
target_type: *element, format!("{entity:?}"),
}, AddedAnimationTargetRecordItem {
) name: name.map(|v| v.to_string()),
})) target_type: *element,
animation_target: *target,
},
)
}),
)
.collect::<HashMap<_, _>>(); .collect::<HashMap<_, _>>();
let removed = removed.read().map(|v| format!("{v:?}")).collect::<Vec<_>>(); let removed = removed.read().map(|v| format!("{v:?}")).collect::<Vec<_>>();
@@ -770,7 +848,7 @@ mod wasm {
.map(|(entity, keyframe, transform_keyframe)| { .map(|(entity, keyframe, transform_keyframe)| {
match serde_json::to_value(TransformKeframeInfo { match serde_json::to_value(TransformKeframeInfo {
entity: format!("{entity:?}"), entity: format!("{entity:?}"),
target: format!("{:?}", keyframe.target), target: keyframe.target,
time: keyframe.time, time: keyframe.time,
incoming_easing: transform_keyframe.incoming_easing, incoming_easing: transform_keyframe.incoming_easing,
affect_translation: transform_keyframe.affect_translation, affect_translation: transform_keyframe.affect_translation,
@@ -786,7 +864,7 @@ mod wasm {
.chain(q_clips.iter().map(|(entity, keyframe, clip)| { .chain(q_clips.iter().map(|(entity, keyframe, clip)| {
match serde_json::to_value(TimedAnimationClipInfo { match serde_json::to_value(TimedAnimationClipInfo {
entity: format!("{entity:?}"), entity: format!("{entity:?}"),
target: format!("{:?}", keyframe.target), target: keyframe.target,
time: keyframe.time, time: keyframe.time,
clip: clip.clip.id().to_string(), clip: clip.clip.id().to_string(),
duration: clip.duration, duration: clip.duration,
+42 -1
View File
@@ -9,6 +9,7 @@ use space_editor::prelude::{EditorEvent, EditorPrefabPath};
use wgpu::Extent3d; use wgpu::Extent3d;
use crate::{ use crate::{
anim::timeline_animation::{Keyframe, TimelineInteractionEvent},
bvh::{BvhAnimCompleteEvent, TestBvh}, bvh::{BvhAnimCompleteEvent, TestBvh},
scene::SceneState, scene::SceneState,
screenshot::{ScreenCapture, ScreenShotPlugin}, screenshot::{ScreenCapture, ScreenShotPlugin},
@@ -24,6 +25,14 @@ pub enum HeadlessMode {
None, None,
} }
#[derive(Resource)]
pub enum CameraMotion {
Orbit(f32),
Pan(f32),
Zoom(f32),
Static,
}
pub fn start_headless( pub fn start_headless(
mode: HeadlessMode, mode: HeadlessMode,
fps: u32, fps: u32,
@@ -39,6 +48,8 @@ pub fn start_headless(
max_frames: frames_to_capture, max_frames: frames_to_capture,
output_dims: image_res, output_dims: image_res,
output_dir: outout_dir.to_string(), output_dir: outout_dir.to_string(),
fps: fps as f32,
frame_delta: 1.0 / (fps as f32),
}); });
app.add_plugins(( app.add_plugins((
@@ -80,7 +91,8 @@ pub fn start_headless(
cmd.insert_resource(SkyboxId(skybox.clone())); cmd.insert_resource(SkyboxId(skybox.clone()));
ew.send(EditorEvent::Load(EditorPrefabPath::File(scene.clone()))) ew.send(EditorEvent::Load(EditorPrefabPath::File(scene.clone())))
}, },
); )
.add_systems(OnEnter(SceneState::Active), start_timeline_animation);
} }
HeadlessMode::Mixamo(mixamo_path, skybox) => { HeadlessMode::Mixamo(mixamo_path, skybox) => {
app.add_plugins(MixamoPlugin); app.add_plugins(MixamoPlugin);
@@ -120,6 +132,8 @@ pub struct HeadlessSettings {
pub output_dims: Extent3d, pub output_dims: Extent3d,
pub output_dir: String, pub output_dir: String,
pub max_frames: u32, pub max_frames: u32,
pub fps: f32,
pub frame_delta: f32,
} }
impl Default for HeadlessSettings { impl Default for HeadlessSettings {
@@ -132,6 +146,8 @@ impl Default for HeadlessSettings {
}, },
output_dir: "./screen_shots/".to_string(), output_dir: "./screen_shots/".to_string(),
max_frames: u32::MAX, max_frames: u32::MAX,
fps: 24.0,
frame_delta: 1.0 / 24.0,
} }
} }
} }
@@ -172,3 +188,28 @@ fn check_for_mixamo_capture_complete(
} }
} }
} }
fn start_timeline_animation(
mut headless_settings: ResMut<HeadlessSettings>,
keyframes: Query<&Keyframe>,
camera: Query<Entity, With<MainCamera>>,
mut commands: Commands,
mut ev: EventWriter<TimelineInteractionEvent>,
) {
if keyframes.is_empty() {
return;
}
commands.remove_resource::<CameraMotion>();
let total_time = keyframes
.iter()
.fold(0f32, |previous, keyframe| previous.max(keyframe.time));
let frames = (total_time * headless_settings.fps).ceil() as u32;
headless_settings.max_frames = frames;
ev.send(TimelineInteractionEvent::Play);
let camera = camera.single();
commands.entity(camera).insert(
ScreenCapture::from_out(&headless_settings.output_dir)
//Part of quick hack, see screenshot.rs#261
.with_max_frames(headless_settings.max_frames + 3),
);
}
+5 -10
View File
@@ -1,7 +1,7 @@
use bevy::{prelude::*, render::render_resource::Extent3d, transform::components::Transform}; use bevy::{prelude::*, render::render_resource::Extent3d, transform::components::Transform};
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
use studio::{ use studio::{
headless::{start_headless, HeadlessMode}, headless::{start_headless, CameraMotion, HeadlessMode},
scene::SceneState, scene::SceneState,
}; };
@@ -180,19 +180,14 @@ fn main() {
app.run(); app.run();
} }
#[derive(Resource)]
enum CameraMotion {
Orbit(f32),
Pan(f32),
Zoom(f32),
Static,
}
pub(crate) fn update_camera_motion( pub(crate) fn update_camera_motion(
mut camera: Query<&mut Transform, (With<Camera>, Without<Parent>)>, mut camera: Query<&mut Transform, (With<Camera>, Without<Parent>)>,
camera_motion: Res<CameraMotion>, camera_motion: Option<Res<CameraMotion>>,
time: Res<Time>, time: Res<Time>,
) { ) {
let Some(camera_motion) = camera_motion else {
return;
};
println!("{}", camera.iter().count()); println!("{}", camera.iter().count());
let Ok(mut main_cam_transform) = camera.get_single_mut() else { let Ok(mut main_cam_transform) = camera.get_single_mut() else {
return; return;
+11 -13
View File
@@ -57,16 +57,17 @@ impl Plugin for ScenePlugin {
) )
.add_systems( .add_systems(
Update, Update,
(init_object_loading, init_scene_import, init_scene_loading) (
init_object_loading,
init_scene_import,
init_scene_loading,
start_capture.run_if(is_headless),
)
.in_set(SceneSpawnerSystem), .in_set(SceneSpawnerSystem),
) )
.add_systems( .add_systems(
OnEnter(SceneState::Active), OnEnter(SceneState::Active),
( (tag_scene_elements, frame_scene_in_view)
tag_scene_elements,
frame_scene_in_view,
start_capture.run_if(is_headless),
)
.chain() .chain()
.in_set(SceneSpawnerSystem), .in_set(SceneSpawnerSystem),
); );
@@ -106,7 +107,7 @@ impl Plugin for ScenePlugin {
fn minimal_scene_setup(mut cmd: Commands) { fn minimal_scene_setup(mut cmd: Commands) {
cmd.spawn(( cmd.spawn((
Name::new("Directional Light"), Name::new("Directional Light"),
AnimationTarget, AnimationTarget::new(),
DirectionalLightBundle { DirectionalLightBundle {
directional_light: DirectionalLight { directional_light: DirectionalLight {
shadows_enabled: false, shadows_enabled: false,
@@ -271,7 +272,7 @@ fn initialize(mut cmd: Commands) {
cmd.spawn(( cmd.spawn((
Name::new("Main Camera"), Name::new("Main Camera"),
MainCamera, MainCamera,
AnimationTarget, AnimationTarget::MainCamera,
ChangeTracker, ChangeTracker,
Camera3dBundle { Camera3dBundle {
transform: camera_xform, transform: camera_xform,
@@ -328,7 +329,7 @@ fn initialiaze_headless(
cmd.spawn(( cmd.spawn((
Name::new("Main Camera"), Name::new("Main Camera"),
MainCamera, MainCamera,
AnimationTarget, AnimationTarget::MainCamera,
Camera3dBundle { Camera3dBundle {
transform: camera_xform, transform: camera_xform,
tonemapping: Tonemapping::BlenderFilmic, tonemapping: Tonemapping::BlenderFilmic,
@@ -340,9 +341,6 @@ fn initialiaze_headless(
..default() ..default()
}, },
CameraControllerBundle::new(focus, camera_xform.translation), CameraControllerBundle::new(focus, camera_xform.translation),
ScreenCapture::from_out(&headless_settings.output_dir)
//Part of quick hack, see screenshot.rs#261
.with_max_frames(headless_settings.max_frames + 3),
)); ));
} }
@@ -520,7 +518,7 @@ fn frame_scene_in_view(
cam_xform.translation = cam.focus + cam_xform.back() * radius; cam_xform.translation = cam.focus + cam_xform.back() * radius;
} }
fn start_capture(mut captures: Query<&mut ScreenCapture>) { fn start_capture(mut captures: Query<&mut ScreenCapture, Added<ScreenCapture>>) {
for mut capture in captures.iter_mut() { for mut capture in captures.iter_mut() {
capture.start_capturing(); capture.start_capturing();
} }