mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-09 00:09:55 +00:00
Timeline Saving/Loading, Cameras, and Headless Playback (#98)
This commit is contained in:
@@ -41,3 +41,5 @@ Thumbs.db
|
||||
/target
|
||||
|
||||
/screen_shots
|
||||
|
||||
/frames
|
||||
@@ -501,6 +501,7 @@ mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
// #[test] // FIXME
|
||||
#[allow(dead_code)]
|
||||
fn macro_create() {
|
||||
let bvh = bvh! {
|
||||
HIERARCHY
|
||||
|
||||
@@ -47,9 +47,9 @@ implements TreeSelectionProvider {
|
||||
@state() playHead = 0;
|
||||
@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> = {};
|
||||
|
||||
@@ -112,7 +112,7 @@ implements TreeSelectionProvider {
|
||||
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 (!isOutsideEntity(this.selectedTrack) && !(this.selectedTrack.track in tracks)) {
|
||||
@@ -262,9 +262,11 @@ implements TreeSelectionProvider {
|
||||
protected override render(): TemplateResult {
|
||||
|
||||
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_animation_target_order_hashed[target] = index;
|
||||
}
|
||||
|
||||
const selected = !isOutsideEntity(this.selectedTrack) && this.selectedTrack?.track || false;
|
||||
@@ -282,7 +284,7 @@ implements TreeSelectionProvider {
|
||||
'--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 name = track.name ?? track.target_type;
|
||||
let icon = this.sceneData?.entityMap.get(entity)?.icon ?? "minus";
|
||||
@@ -381,6 +383,7 @@ implements TreeSelectionProvider {
|
||||
.playHead=${bind(this, "playHead")}
|
||||
.keyframes=${bind(this, "keyframes")}
|
||||
.track_ordered=${track_order_hashed}
|
||||
.track_animation_target_ordered=${track_animation_target_order_hashed}
|
||||
></sts-anim-timeline>
|
||||
`
|
||||
};
|
||||
|
||||
@@ -69,7 +69,8 @@ export class AnimTimelineElement extends LitElement {
|
||||
@property({ type: Number }) playHead = 0;
|
||||
/** Keyframe positions (in seconds) */
|
||||
@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;
|
||||
|
||||
@@ -344,7 +345,7 @@ export class AnimTimelineElement extends LitElement {
|
||||
Object.entries(this.keyframes),
|
||||
([id]) => id,
|
||||
([id, keyframe]) => {
|
||||
let row = this.track_ordered[keyframe.target];
|
||||
let row = this.track_animation_target_ordered[keyframe.target];
|
||||
return html`
|
||||
<div id=${id}
|
||||
class=${classMap({
|
||||
|
||||
@@ -51,6 +51,7 @@ anyhow.workspace = true
|
||||
serde_repr = "0.1.18"
|
||||
bevy_tweening = "0.9"
|
||||
async-compat = "0.2"
|
||||
uuid = { version = "1", features = ["v4", "serde"]}
|
||||
|
||||
[dependencies.reqwest]
|
||||
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
@@ -1,12 +1,8 @@
|
||||
use std::time::Duration;
|
||||
use std::{str::FromStr, time::Duration};
|
||||
|
||||
use bevy::{
|
||||
ecs::{
|
||||
entity::{EntityMapper, MapEntities},
|
||||
reflect::ReflectMapEntities,
|
||||
},
|
||||
prelude::*,
|
||||
utils::HashSet,
|
||||
utils::{hashbrown::HashMap, HashSet},
|
||||
};
|
||||
|
||||
use bevy_tweening::{
|
||||
@@ -17,7 +13,10 @@ use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
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;
|
||||
|
||||
@@ -34,6 +33,7 @@ impl Plugin for TimelinePlugin {
|
||||
.register_type::<KeyframeOrder>()
|
||||
.editor_registry::<KeyframeOrder>()
|
||||
.register_type::<EaseType>()
|
||||
.register_type::<uuid::Uuid>()
|
||||
.add_event::<TimelineInteractionEvent>()
|
||||
.add_event::<KeyframeInteractionEvent>();
|
||||
|
||||
@@ -131,10 +131,10 @@ impl Default for TransformKeyframe {
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, Debug, Reflect, PartialEq)]
|
||||
#[reflect(Component, MapEntities)]
|
||||
#[reflect(Component)]
|
||||
pub struct Keyframe {
|
||||
target: Entity,
|
||||
time: f32,
|
||||
target: AnimationTarget,
|
||||
pub time: f32,
|
||||
}
|
||||
|
||||
#[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 {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
target: Entity::PLACEHOLDER,
|
||||
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)]
|
||||
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(
|
||||
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 {
|
||||
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.delta = Some(delta);
|
||||
}
|
||||
@@ -229,7 +282,7 @@ 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_animation_targets: Query<(Entity, &Transform, &AnimationTarget)>,
|
||||
q_potential_targets: Query<Entity, (With<Transform>, Without<AnimationTarget>)>,
|
||||
mut q_keyframes: Query<(Entity, &mut Keyframe)>,
|
||||
) {
|
||||
@@ -237,10 +290,11 @@ fn process_keyframe_events(
|
||||
info!("Processing Keyframe Event {event:?}");
|
||||
match event {
|
||||
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;
|
||||
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()
|
||||
< MIN_KEYFRAME_DISTANCE
|
||||
{
|
||||
@@ -253,8 +307,8 @@ fn process_keyframe_events(
|
||||
*transform,
|
||||
TransformKeyframe::default(),
|
||||
Keyframe {
|
||||
target,
|
||||
time: r_global_seek_time.time,
|
||||
target: *animation_target,
|
||||
},
|
||||
));
|
||||
} else {
|
||||
@@ -266,8 +320,8 @@ fn process_keyframe_events(
|
||||
},
|
||||
Selectable,
|
||||
Keyframe {
|
||||
target,
|
||||
time: r_global_seek_time.time,
|
||||
target: *animation_target,
|
||||
},
|
||||
TransformKeyframe::default(),
|
||||
));
|
||||
@@ -282,7 +336,7 @@ fn process_keyframe_events(
|
||||
KeyframeInteractionEvent::AddEntityTrack { target } => {
|
||||
if let Ok(target) = q_potential_targets.get(*target) {
|
||||
info!("Adding Animation Target to Entity");
|
||||
commands.entity(target).insert(AnimationTarget);
|
||||
commands.entity(target).insert(AnimationTarget::new());
|
||||
} else {
|
||||
warn!("Can't add animation target to entity");
|
||||
}
|
||||
@@ -304,8 +358,14 @@ fn generate_transform_animation(
|
||||
(&Keyframe, &Transform, &TransformKeyframe, Entity),
|
||||
Without<MarkedForDeletion>,
|
||||
>,
|
||||
added_targets: Query<(Entity, &AnimationTarget), Added<AnimationTarget>>,
|
||||
valid_targets: Query<(Entity, &AnimationTarget)>,
|
||||
removed: Query<&Keyframe, With<MarkedForDeletion>>,
|
||||
) {
|
||||
let available_targets = valid_targets
|
||||
.iter()
|
||||
.map(|(e, a)| (*a, e))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let changed_targets = changed
|
||||
.iter()
|
||||
.map(|keyframe| keyframe.target)
|
||||
@@ -313,6 +373,7 @@ fn generate_transform_animation(
|
||||
info!("Processing a removed keyframe {keyframe:?}");
|
||||
keyframe.target
|
||||
}))
|
||||
.chain(added_targets.iter().map(|(_, a)| *a))
|
||||
.collect::<HashSet<_>>();
|
||||
let keyframes = keyframes
|
||||
.iter()
|
||||
@@ -320,6 +381,12 @@ fn generate_transform_animation(
|
||||
.group_by(|(keyframe, _, _, _)| keyframe.target);
|
||||
|
||||
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 (_, _, _, translation, rotation, scale) = keyframes.fold(
|
||||
@@ -474,7 +541,6 @@ pub fn timeline_bound_component_animator_system<T: Component>(
|
||||
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) {
|
||||
@@ -482,7 +548,6 @@ pub fn timeline_bound_component_animator_system<T: Component>(
|
||||
}
|
||||
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,
|
||||
@@ -597,13 +662,13 @@ mod wasm {
|
||||
export interface UpdateAnimationTargets extends CustomEvent {
|
||||
type: "update-animation-targets",
|
||||
detail: {
|
||||
added: Record<string, { name?: string, target_type: SceneElement } >,
|
||||
added: Record<string, { name?: string, target_type: SceneElement, animation_target: AnimationTarget } >,
|
||||
removed: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface TransformKeyframeInfo {
|
||||
target: string,
|
||||
target: AnimationTarget,
|
||||
time: number,
|
||||
entity: string,
|
||||
incoming_easing: "linear" | "cubic-hermite",
|
||||
@@ -613,7 +678,7 @@ mod wasm {
|
||||
}
|
||||
|
||||
export interface TimedAnimationClipInfo {
|
||||
target: string,
|
||||
target: AnimationTarget,
|
||||
time: number,
|
||||
entity: string,
|
||||
type: 'animation-clip',
|
||||
@@ -624,6 +689,8 @@ mod wasm {
|
||||
|
||||
export type Keyframe = TransformKeyframeInfo | TimedAnimationClipInfo;
|
||||
|
||||
export type AnimationTarget = string;
|
||||
|
||||
export interface UpdateKeyframes extends CustomEvent {
|
||||
type: "update-keyframes",
|
||||
detail: {
|
||||
@@ -645,6 +712,7 @@ mod wasm {
|
||||
struct AddedAnimationTargetRecordItem {
|
||||
name: Option<String>,
|
||||
target_type: SceneElement,
|
||||
animation_target: AnimationTarget,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -656,7 +724,7 @@ mod wasm {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct TimedAnimationClipInfo {
|
||||
entity: String,
|
||||
target: String,
|
||||
target: AnimationTarget,
|
||||
time: f32,
|
||||
clip: String,
|
||||
duration: f32,
|
||||
@@ -667,7 +735,7 @@ mod wasm {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct TransformKeframeInfo {
|
||||
entity: String,
|
||||
target: String,
|
||||
target: AnimationTarget,
|
||||
time: f32,
|
||||
incoming_easing: EaseType,
|
||||
affect_translation: bool,
|
||||
@@ -683,13 +751,16 @@ mod wasm {
|
||||
|
||||
pub(super) fn notify_updated_animation_targets(
|
||||
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<
|
||||
(Entity, Option<&Name>, &SceneElement),
|
||||
(Entity, Option<&Name>, &SceneElement, &AnimationTarget),
|
||||
(Added<AnimationTarget>, Without<Camera>),
|
||||
>,
|
||||
q_transforms: Query<
|
||||
(Entity, Option<&Name>),
|
||||
(Entity, Option<&Name>, &AnimationTarget),
|
||||
(
|
||||
Added<AnimationTarget>,
|
||||
With<Transform>,
|
||||
@@ -705,33 +776,40 @@ mod wasm {
|
||||
|
||||
let added = q_transforms
|
||||
.iter()
|
||||
.map(|(entity, name)| {
|
||||
.map(|(entity, name, target)| {
|
||||
(
|
||||
format!("{entity:?}"),
|
||||
AddedAnimationTargetRecordItem {
|
||||
name: name.map(|v| v.to_string()),
|
||||
target_type: SceneElement::Generic,
|
||||
animation_target: *target,
|
||||
},
|
||||
)
|
||||
})
|
||||
.chain(q_cameras.iter().map(|(entity, name)| {
|
||||
.chain(q_cameras.iter().map(|(entity, name, target)| {
|
||||
(
|
||||
format!("{entity:?}"),
|
||||
AddedAnimationTargetRecordItem {
|
||||
name: name.map(|v| v.to_string()),
|
||||
target_type: SceneElement::Camera,
|
||||
animation_target: *target,
|
||||
},
|
||||
)
|
||||
}))
|
||||
.chain(q_scene_elements.iter().map(|(entity, name, element)| {
|
||||
(
|
||||
format!("{entity:?}"),
|
||||
AddedAnimationTargetRecordItem {
|
||||
name: name.map(|v| v.to_string()),
|
||||
target_type: *element,
|
||||
},
|
||||
)
|
||||
}))
|
||||
.chain(
|
||||
q_scene_elements
|
||||
.iter()
|
||||
.map(|(entity, name, element, target)| {
|
||||
(
|
||||
format!("{entity:?}"),
|
||||
AddedAnimationTargetRecordItem {
|
||||
name: name.map(|v| v.to_string()),
|
||||
target_type: *element,
|
||||
animation_target: *target,
|
||||
},
|
||||
)
|
||||
}),
|
||||
)
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let removed = removed.read().map(|v| format!("{v:?}")).collect::<Vec<_>>();
|
||||
@@ -770,7 +848,7 @@ mod wasm {
|
||||
.map(|(entity, keyframe, transform_keyframe)| {
|
||||
match serde_json::to_value(TransformKeframeInfo {
|
||||
entity: format!("{entity:?}"),
|
||||
target: format!("{:?}", keyframe.target),
|
||||
target: keyframe.target,
|
||||
time: keyframe.time,
|
||||
incoming_easing: transform_keyframe.incoming_easing,
|
||||
affect_translation: transform_keyframe.affect_translation,
|
||||
@@ -786,7 +864,7 @@ mod wasm {
|
||||
.chain(q_clips.iter().map(|(entity, keyframe, clip)| {
|
||||
match serde_json::to_value(TimedAnimationClipInfo {
|
||||
entity: format!("{entity:?}"),
|
||||
target: format!("{:?}", keyframe.target),
|
||||
target: keyframe.target,
|
||||
time: keyframe.time,
|
||||
clip: clip.clip.id().to_string(),
|
||||
duration: clip.duration,
|
||||
|
||||
+42
-1
@@ -9,6 +9,7 @@ use space_editor::prelude::{EditorEvent, EditorPrefabPath};
|
||||
use wgpu::Extent3d;
|
||||
|
||||
use crate::{
|
||||
anim::timeline_animation::{Keyframe, TimelineInteractionEvent},
|
||||
bvh::{BvhAnimCompleteEvent, TestBvh},
|
||||
scene::SceneState,
|
||||
screenshot::{ScreenCapture, ScreenShotPlugin},
|
||||
@@ -24,6 +25,14 @@ pub enum HeadlessMode {
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub enum CameraMotion {
|
||||
Orbit(f32),
|
||||
Pan(f32),
|
||||
Zoom(f32),
|
||||
Static,
|
||||
}
|
||||
|
||||
pub fn start_headless(
|
||||
mode: HeadlessMode,
|
||||
fps: u32,
|
||||
@@ -39,6 +48,8 @@ pub fn start_headless(
|
||||
max_frames: frames_to_capture,
|
||||
output_dims: image_res,
|
||||
output_dir: outout_dir.to_string(),
|
||||
fps: fps as f32,
|
||||
frame_delta: 1.0 / (fps as f32),
|
||||
});
|
||||
|
||||
app.add_plugins((
|
||||
@@ -80,7 +91,8 @@ pub fn start_headless(
|
||||
cmd.insert_resource(SkyboxId(skybox.clone()));
|
||||
ew.send(EditorEvent::Load(EditorPrefabPath::File(scene.clone())))
|
||||
},
|
||||
);
|
||||
)
|
||||
.add_systems(OnEnter(SceneState::Active), start_timeline_animation);
|
||||
}
|
||||
HeadlessMode::Mixamo(mixamo_path, skybox) => {
|
||||
app.add_plugins(MixamoPlugin);
|
||||
@@ -120,6 +132,8 @@ pub struct HeadlessSettings {
|
||||
pub output_dims: Extent3d,
|
||||
pub output_dir: String,
|
||||
pub max_frames: u32,
|
||||
pub fps: f32,
|
||||
pub frame_delta: f32,
|
||||
}
|
||||
|
||||
impl Default for HeadlessSettings {
|
||||
@@ -132,6 +146,8 @@ impl Default for HeadlessSettings {
|
||||
},
|
||||
output_dir: "./screen_shots/".to_string(),
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use bevy::{prelude::*, render::render_resource::Extent3d, transform::components::Transform};
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use studio::{
|
||||
headless::{start_headless, HeadlessMode},
|
||||
headless::{start_headless, CameraMotion, HeadlessMode},
|
||||
scene::SceneState,
|
||||
};
|
||||
|
||||
@@ -180,19 +180,14 @@ fn main() {
|
||||
app.run();
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
enum CameraMotion {
|
||||
Orbit(f32),
|
||||
Pan(f32),
|
||||
Zoom(f32),
|
||||
Static,
|
||||
}
|
||||
|
||||
pub(crate) fn update_camera_motion(
|
||||
mut camera: Query<&mut Transform, (With<Camera>, Without<Parent>)>,
|
||||
camera_motion: Res<CameraMotion>,
|
||||
camera_motion: Option<Res<CameraMotion>>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
let Some(camera_motion) = camera_motion else {
|
||||
return;
|
||||
};
|
||||
println!("{}", camera.iter().count());
|
||||
let Ok(mut main_cam_transform) = camera.get_single_mut() else {
|
||||
return;
|
||||
|
||||
+11
-13
@@ -57,16 +57,17 @@ impl Plugin for ScenePlugin {
|
||||
)
|
||||
.add_systems(
|
||||
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),
|
||||
)
|
||||
.add_systems(
|
||||
OnEnter(SceneState::Active),
|
||||
(
|
||||
tag_scene_elements,
|
||||
frame_scene_in_view,
|
||||
start_capture.run_if(is_headless),
|
||||
)
|
||||
(tag_scene_elements, frame_scene_in_view)
|
||||
.chain()
|
||||
.in_set(SceneSpawnerSystem),
|
||||
);
|
||||
@@ -106,7 +107,7 @@ impl Plugin for ScenePlugin {
|
||||
fn minimal_scene_setup(mut cmd: Commands) {
|
||||
cmd.spawn((
|
||||
Name::new("Directional Light"),
|
||||
AnimationTarget,
|
||||
AnimationTarget::new(),
|
||||
DirectionalLightBundle {
|
||||
directional_light: DirectionalLight {
|
||||
shadows_enabled: false,
|
||||
@@ -271,7 +272,7 @@ fn initialize(mut cmd: Commands) {
|
||||
cmd.spawn((
|
||||
Name::new("Main Camera"),
|
||||
MainCamera,
|
||||
AnimationTarget,
|
||||
AnimationTarget::MainCamera,
|
||||
ChangeTracker,
|
||||
Camera3dBundle {
|
||||
transform: camera_xform,
|
||||
@@ -328,7 +329,7 @@ fn initialiaze_headless(
|
||||
cmd.spawn((
|
||||
Name::new("Main Camera"),
|
||||
MainCamera,
|
||||
AnimationTarget,
|
||||
AnimationTarget::MainCamera,
|
||||
Camera3dBundle {
|
||||
transform: camera_xform,
|
||||
tonemapping: Tonemapping::BlenderFilmic,
|
||||
@@ -340,9 +341,6 @@ fn initialiaze_headless(
|
||||
..default()
|
||||
},
|
||||
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;
|
||||
}
|
||||
|
||||
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() {
|
||||
capture.start_capturing();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user