Merge branch 'main' into update-version

This commit is contained in:
rewin
2024-03-27 22:56:50 +03:00
90 changed files with 7820 additions and 977 deletions
-208
View File
@@ -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 = pos_a.lerp(pos_b, t);
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
View File
@@ -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;
}
}
}
+993
View File
@@ -0,0 +1,993 @@
use std::{str::FromStr, time::Duration};
use bevy::{prelude::*, utils::hashbrown::HashMap};
use bevy_tweening::{
lens::{TransformPositionLens, TransformRotationLens, TransformScaleLens},
Animator, EaseMethod, Sequence, Targetable, Tracks, Tween, TweenCompleted,
};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use space_editor::{prelude::PrefabMarker, space_prefab::editor_registry::EditorRegistryExt};
use crate::{
headless::HeadlessSettings,
interaction::{MarkedForDeletion, Selectable},
scene::SceneElement,
};
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>()
.register_type::<EaseType>()
.register_type::<uuid::Uuid>()
.add_event::<TimelineInteractionEvent>()
.add_event::<KeyframeInteractionEvent>()
.add_state::<KeyingMode>();
app.insert_resource(GlobalSeekTime {
time: 0.,
playing: false,
delta: None,
})
.add_event::<TweenCompleted>();
app.add_systems(First, generate_transform_animation);
app.add_systems(PreUpdate, (process_timeline_events, update_tick).chain());
app.add_systems(
Update,
(
process_auto_key.run_if(in_state(KeyingMode::Auto)),
process_keyframe_events,
timeline_bound_component_animator_system::<Transform>,
),
);
// app.add_systems(PostUpdate, generate_transform_animation);
#[cfg(feature = "wasm")]
{
app.add_systems(
Last,
(
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),
wasm::notify_keying_mode_changed.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),
ToggleAutoKey,
}
#[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(States, Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum KeyingMode {
#[default]
Manual,
Auto,
}
#[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)]
pub struct Keyframe {
target: AnimationTarget,
pub 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 Default for Keyframe {
fn default() -> Self {
Self {
time: Default::default(),
target: AnimationTarget::Unknown,
}
}
}
#[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, PartialEq, Eq, Hash, Copy, Serialize, Deserialize,
)]
#[reflect(Component)]
#[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>,
mut r_global_seek_time: ResMut<GlobalSeekTime>,
key_mode: Res<State<KeyingMode>>,
mut next_keying_mode: ResMut<NextState<KeyingMode>>,
) {
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.);
}
TimelineInteractionEvent::ToggleAutoKey => {
next_keying_mode.set(match key_mode.get() {
KeyingMode::Manual => KeyingMode::Auto,
KeyingMode::Auto => KeyingMode::Manual,
});
}
}
r_global_seek_time.delta = None;
}
}
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 = 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);
}
}
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, &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, animation_target)) = q_animation_targets.get(*target)
{
let mut existing = None;
for (entity, keyframe) in q_keyframes.iter_mut() {
if keyframe.target == *animation_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 {
time: r_global_seek_time.time,
target: *animation_target,
},
));
} else {
commands.spawn((
PrefabMarker,
SpatialBundle {
transform: *transform,
..Default::default()
},
Selectable,
Keyframe {
time: r_global_seek_time.time,
target: *animation_target,
},
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::new());
} else {
warn!("Can't add animation target to entity");
}
}
}
}
}
fn process_auto_key(
changed: Query<Entity, (With<AnimationTarget>, Changed<Transform>)>,
r_global_seek_time: Res<GlobalSeekTime>,
mut event: EventWriter<KeyframeInteractionEvent>,
) {
if r_global_seek_time.playing || r_global_seek_time.is_changed() {
info!("Skipping Auto Key");
return;
}
for entity in &changed {
info!("Setting Auto Key");
event.send(KeyframeInteractionEvent::RecordKeyframe { target: entity })
}
}
fn generate_transform_animation(
mut commands: Commands,
keyframes: Query<
(&Keyframe, &Transform, &TransformKeyframe, Entity),
Without<MarkedForDeletion>,
>,
valid_targets: Query<(
Entity,
&AnimationTarget,
Option<&Name>,
Option<&SceneElement>,
)>,
mut e_timeline_event: EventReader<TimelineInteractionEvent>,
) {
let mut playing = false;
for e in e_timeline_event.read() {
if matches!(
e,
TimelineInteractionEvent::Play | TimelineInteractionEvent::Seek(_)
) {
playing = true;
continue;
}
}
if !playing {
return;
}
info!("Generating Animation From Keyframes");
let available_targets = valid_targets
.iter()
.map(|(e, a, name, element)| (*a, (e, name, element)))
.collect::<HashMap<_, _>>();
let keyframes = keyframes
.iter()
.into_group_map_by(|(keyframe, _, _, _)| keyframe.target);
for (target, keyframes) in keyframes.into_iter() {
info!("Keyframes For Target {target:?}");
let Some(target) = available_targets.get(&target) else {
info!("No valid target.");
continue;
};
let (target, target_name, target_scene_element) = *target;
let keyframes = keyframes.iter();
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;
if transform_keyframe.affect_translation {
let (last_time, start) = match last_translation {
Some(v) => v,
None => (0., 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),
};
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),
};
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() {
info!("Empty Animation Track for {target_name:?} {target_scene_element:?} {target:?}");
commands.entity(target).remove::<Animator<Transform>>();
} else {
info!("Adding Animation Track for {target_name:?} {target_scene_element:?} {target:?}");
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 {
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 {
animator.tweenable_mut().tick(
Duration::from_secs_f32(delta),
&mut target,
entity,
&mut events,
);
} else {
let tween = animator.tweenable_mut();
tween.set_elapsed(Duration::from_secs_f32(time.time.max(0.)));
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, KeyingMode,
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 = toggleAutoKey)]
pub fn toggle_auto_key() {
SEEK_TIME.write_message(TimelineInteractionEvent::ToggleAutoKey);
}
#[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() {
info!("Updating Seek Time {timestamp:?}");
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, animation_target: AnimationTarget } >,
removed: string[]
}
}
export interface TransformKeyframeInfo {
target: AnimationTarget,
time: number,
entity: string,
incoming_easing: "linear" | "cubic-hermite",
affect_translation: bool,
affect_rotation: bool,
affect_scale: bool
}
export interface TimedAnimationClipInfo {
target: AnimationTarget,
time: number,
entity: string,
type: 'animation-clip',
clip: string,
duration: number,
speed: number
}
export type Keyframe = TransformKeyframeInfo | TimedAnimationClipInfo;
export type AnimationTarget = string;
export interface UpdateKeyframes extends CustomEvent {
type: "update-keyframes",
detail: {
added: Record<string, Keyframe>,
removed: string[]
}
}
export interface UpdateKeyingMode extends CustomEvent {
type: "keying-mode-changed",
detail: {
automatic: boolean
}
}
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,
animation_target: AnimationTarget,
}
#[derive(Serialize, Deserialize)]
struct UpdatedAnimationTargetsEvent {
added: HashMap<String, AddedAnimationTargetRecordItem>,
removed: Vec<String>,
}
#[derive(Serialize, Deserialize)]
struct UpdateKeyingMode {
automatic: bool,
}
#[derive(Serialize, Deserialize)]
struct TimedAnimationClipInfo {
entity: String,
target: AnimationTarget,
time: f32,
clip: String,
duration: f32,
speed: f32,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct TransformKeframeInfo {
entity: String,
target: AnimationTarget,
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>, &AnimationTarget),
(Added<AnimationTarget>, With<Camera>),
>,
q_scene_elements: Query<
(Entity, Option<&Name>, &SceneElement, &AnimationTarget),
(Added<AnimationTarget>, Without<Camera>),
>,
q_transforms: Query<
(Entity, Option<&Name>, &AnimationTarget),
(
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, 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, 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, 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<_>>();
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: 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: 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}"))?,
)
}
pub(super) fn notify_keying_mode_changed(
r_keying_mode: Res<State<KeyingMode>>,
q_window: Query<&Window, With<PrimaryWindow>>,
) -> Result<(), String> {
if !r_keying_mode.is_changed() {
return Ok(());
}
let Ok(window) = q_window.get_single() else {
return Ok(());
};
crate::wasm::dispatch_to_js(
window,
"keying-mode-changed",
&serde_json::to_value(UpdateKeyingMode {
automatic: match r_keying_mode.get() {
KeyingMode::Manual => false,
KeyingMode::Auto => true,
},
})
.map_err(|e| format!("{e}"))?,
)
}
}
+11 -6
View File
@@ -6,8 +6,8 @@ use bevy::{
};
use crate::{
input::{Action, CameraMotion, InputAction, Lifecycle, Selection},
interaction::Selected,
input::{self, Action, CameraMotion, InputAction, Lifecycle},
interaction::Selection,
math::{
Easing, InterpPosition, InterpVelocity, Interpolation, InterpolationSystem, NearlyEq,
NearlyZero, Velocity,
@@ -155,20 +155,25 @@ fn process_physical_inputs(
fn process_focus_inputs(
mut cmd: Commands,
r_selection: Option<Res<Selection>>,
mut input: EventReader<InputAction>,
mut q_camera: Query<(Entity, &CameraController, &mut Velocity)>,
q_interp_vel: Query<&InterpVelocity>,
mut q_interp_pos: Query<&mut InterpPosition>,
q_selected_xform: Query<&GlobalTransform, With<Selected>>,
q_global_xforms: Query<&GlobalTransform>,
) {
let Some(selection) = r_selection else {
return;
};
if input.read().any(|action| {
matches!(
action,
InputAction(Lifecycle::Stop, Action::Selection(Selection::Focus))
InputAction(Lifecycle::Stop, Action::Selection(input::Selection::Focus))
)
}) {
let Ok(target) = q_selected_xform
.get_single()
let Ok(target) = q_global_xforms
.get(**selection)
.map(|xform| xform.translation())
else {
return;
+48 -2
View File
@@ -5,15 +5,17 @@ use bevy::{
core_pipeline::Skybox,
prelude::*,
};
use bevy_web_asset::WebAssetPlugin;
use space_editor::prelude::{EditorEvent, EditorPrefabPath};
use wgpu::Extent3d;
use crate::{
anim::timeline_animation::{Keyframe, TimelineInteractionEvent},
bvh::{BvhAnimCompleteEvent, TestBvh},
scene::{SceneState, SkyboxHolder},
screenshot::{ScreenCapture, ScreenShotPlugin},
BvhPlugin, CommonPlugins, LoadLibraryObjectEvent, LoadSceneEvent, MainCamera,
MixamoImportEvent, MixamoPlugin, SkyboxId, StudioMode, WorldGridPlugin,
MixamoImportEvent, MixamoPlugin, RemoteAssetPlugin, SkyboxId, StudioMode, WorldGridPlugin,
};
#[derive(Debug, Resource, Clone)]
@@ -24,6 +26,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,10 +49,14 @@ 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((
ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64(1.0 / fps as f64)),
WebAssetPlugin,
RemoteAssetPlugin,
DefaultPlugins.set(WindowPlugin {
primary_window: None,
exit_condition: bevy::window::ExitCondition::DontExit,
@@ -84,7 +98,8 @@ pub fn start_headless(
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);
@@ -124,6 +139,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 {
@@ -136,6 +153,8 @@ impl Default for HeadlessSettings {
},
output_dir: "./screen_shots/".to_string(),
max_frames: u32::MAX,
fps: 24.0,
frame_delta: 1.0 / 24.0,
}
}
}
@@ -176,3 +195,30 @@ 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))
.max(3.0);
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 -3
View File
@@ -35,9 +35,7 @@ pub(crate) fn nearest_common_ancestor(
}
// Find the lowest depth of all the entities
let Some(lowest_depth) = depths.values().copied().reduce(usize::min) else {
return None;
};
let lowest_depth = depths.values().copied().reduce(usize::min)?;
// Traverse up the tree from each entity until we find each one's ancestor
// at the level of lowest_depth
+67 -6
View File
@@ -39,27 +39,88 @@ impl Plugin for StudioInputPlugin {
.after(bevy::input::InputSystem)
.after(read_mouse_delta),
);
app.add_systems(
PostUpdate,
(apply_deferred, log_input_actions).after(StudioInputSystem),
);
}
}
#[derive(Event, Clone, Copy)]
/// Logs all `InputAction`s emitted, along with the input state at the time they
/// were fired, with `debug` verbosity.
fn log_input_actions(
ri_keyboard: Res<Input<KeyCode>>,
ri_mouse_buttons: Res<Input<MouseButton>>,
mut er_input_actions: EventReader<InputAction>,
) {
if er_input_actions.is_empty() {
return;
}
let lmb = if ri_mouse_buttons.pressed(MouseButton::Left) {
'X'
} else {
'-'
};
let mmb = if ri_mouse_buttons.pressed(MouseButton::Middle) {
'X'
} else {
'-'
};
let rmb = if ri_mouse_buttons.pressed(MouseButton::Right) {
'X'
} else {
'-'
};
use KeyCode::*;
let mod_keys = [AltLeft, AltRight, ShiftLeft, ControlLeft, ControlRight]
.into_iter()
.collect::<HashSet<_>>();
let action_keys = [W, A, S, D, E, Q, Escape, Grave, Delete, F]
.into_iter()
.collect::<HashSet<_>>();
let keys_pressed = ri_keyboard.get_pressed().copied().collect::<HashSet<_>>();
let keys_display = keys_pressed
.intersection(&mod_keys)
.map(|key| format!("{key:?}"))
.chain(
keys_pressed
.intersection(&action_keys)
.map(|key| format!("{key:?}")),
)
.collect::<Vec<_>>()
.join("+");
for action in er_input_actions.read() {
debug!(
"Mouse: [{lmb}{mmb}{rmb}] \
Keyboard: [{keys_display}] \
Action: [{action:.2?}]"
);
}
}
#[derive(Event, Clone, Copy, Debug)]
pub struct InputAction(pub Lifecycle, pub Action);
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Debug)]
pub enum Lifecycle {
Start,
Ongoing,
Stop,
}
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Debug)]
pub enum Action {
Camera(CameraMotion),
Selection(Selection),
ToggleDebug,
}
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Debug)]
pub enum CameraMotion {
Translate(Vec3),
Pan(Vec2),
@@ -73,7 +134,7 @@ impl From<CameraMotion> for Action {
}
}
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Debug)]
pub enum Selection {
/// Clear the current selection
Clear,
@@ -212,7 +273,7 @@ fn translate_dcc_camera_events(
CameraMotion::Rotate(Vec2::ZERO).into(),
));
}
// Alt Camera Rotation (designed for laptop)
// Alternative Camera Rotation (designed for laptop)
else if keyboard.pressed(KeyCode::ShiftLeft) {
let lifecycle = if keyboard.just_pressed(KeyCode::ShiftLeft) {
Lifecycle::Start
+100 -72
View File
@@ -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)
@@ -48,60 +49,73 @@ impl Plugin for SelectionPlugin {
.run_if(resource_changed_or_removed::<Selection>()),
);
app.add_systems(Update, sync_editor_selected);
app.add_systems(PostUpdate, propagate_space_editor_to_studio);
}
}
#[derive(Component, Clone, Copy)]
#[derive(Component, Clone, Copy, Reflect, Default)]
#[reflect(Component, Default)]
pub struct Selectable;
#[derive(Component, Clone, Copy)]
pub struct Selected;
#[derive(Resource, Deref, DerefMut, Debug)]
pub struct Selection(Entity);
fn sync_editor_selected(
fn propagate_space_editor_to_studio(
mut cmd: Commands,
q_new_selected: Query<Entity, (Added<Selected>, Without<space_editor::prelude::Selected>)>,
q_editor_selected: Query<Entity, With<space_editor::prelude::Selected>>,
q_selected: Query<Entity, With<Selected>>,
q_added_editor_selected: Query<
Entity,
(Without<Selected>, Added<space_editor::prelude::Selected>),
>,
r_selection: Option<Res<Selection>>,
q_new_selected: Query<Entity, Added<space_editor::prelude::Selected>>,
mut er_deselected: RemovedComponents<space_editor::prelude::Selected>,
q_parents: Query<&Parent>,
q_selectable: Query<&Selectable>,
) {
for ent in q_new_selected.iter() {
for ed_ent in q_editor_selected.iter() {
cmd.entity(ed_ent)
.remove::<space_editor::prelude::Selected>();
let studio_selection_deselected_in_space_editor = r_selection
.as_ref()
.map(|selection| {
er_deselected
.read()
.any(|deselected| deselected == ***selection)
})
.unwrap_or(false);
if q_new_selected.is_empty() && studio_selection_deselected_in_space_editor {
cmd.remove_resource::<Selection>();
} else {
let mut new_selected_iter = q_new_selected.iter();
let Some(selected) = new_selected_iter.next().and_then(|se_selected| {
nearest_selectable_parent(se_selected, &q_parents, &q_selectable)
}) else {
return;
};
if r_selection.is_none() || **r_selection.unwrap() != selected {
cmd.insert_resource(Selection(selected));
}
cmd.entity(ent).insert(space_editor::prelude::Selected);
}
for ent in q_added_editor_selected.iter() {
cmd.entity(ent).insert(Selected);
cmd.insert_resource(Selection(ent));
for ent in q_selected.iter() {
cmd.entity(ent).remove::<Selected>();
if new_selected_iter.next().is_some() {
warn!(
"Multiple new entities were selected in Space Editor, but \
Storyteller Studio doesn't currently support multiple selections"
);
}
}
}
fn tag_scene_elements(
mut cmd: Commands,
q_scene_elements: Query<(Entity, &Children), (With<SceneElement>, Without<Selectable>)>,
q_scene_elements: Query<(Entity, Option<&Children>), (With<SceneElement>, Without<Selectable>)>,
q_primitives: Query<&Handle<Mesh>>,
) {
for (ent, children) in q_scene_elements.iter() {
cmd.entity(ent).insert(Selectable);
cmd.entity(ent)
.insert((Selectable, PickableBundle::default()));
for &ent_child in children
.iter()
.filter(|child| q_primitives.contains(**child))
{
cmd.entity(ent_child).insert(PickableBundle::default());
if let Some(children) = children {
for &ent_child in children
.iter()
.filter(|child| q_primitives.contains(**child))
{
cmd.entity(ent_child).insert(PickableBundle::default());
}
}
}
}
@@ -116,31 +130,11 @@ fn update_selection(
mut ev_input: EventReader<InputAction>,
q_parents: Query<&Parent>,
q_selectable: Query<&Selectable>,
q_selected: Query<Entity, With<Selected>>,
) {
if let Some(event) = ev_select.read().last() {
// Clear any existing `Selected` components
for ent in q_selected.iter() {
cmd.entity(ent).remove::<Selected>();
}
// Find the nearest Selectable parent of the target entity
let mut ent = event.target;
let ent_selected = loop {
if q_selectable.get(ent).is_ok() {
break Some(ent);
}
if let Ok(parent) = q_parents.get(ent) {
ent = parent.get();
} else {
break None;
}
};
if let Some(ent) = ent_selected {
if let Some(ent) = nearest_selectable_parent(event.target, &q_parents, &q_selectable) {
cmd.insert_resource(Selection(ent));
cmd.entity(ent).insert(Selected);
} else {
cmd.remove_resource::<Selection>();
}
@@ -151,13 +145,30 @@ fn update_selection(
)
}) {
cmd.remove_resource::<Selection>();
}
}
for ent in q_selected.iter() {
cmd.entity(ent).remove::<Selected>();
fn nearest_selectable_parent(
mut target: Entity,
q_parents: &Query<&Parent>,
q_selectable: &Query<&Selectable>,
) -> Option<Entity> {
loop {
if q_selectable.get(target).is_ok() {
break Some(target);
}
if let Ok(parent) = q_parents.get(target) {
target = parent.get();
} else {
break None;
}
}
}
#[derive(Component)]
pub struct MarkedForDeletion;
fn process_delete_inputs(
mut cmd: Commands,
mut er_input: EventReader<InputAction>,
@@ -172,7 +183,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,9 +191,16 @@ 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>>,
r_selection: Option<Res<Selection>>,
q_children: Query<&Children>,
q_meshes: Query<
Entity,
(
@@ -199,19 +217,30 @@ fn draw_selection_outline(
cmd.entity(ent).remove::<OutlineBundle>();
}
for children in q_selected.iter() {
for &child in children.iter() {
if q_meshes.contains(child) {
cmd.entity(child).insert(OutlineBundle {
outline: OutlineVolume {
visible: true,
width: 4.,
colour: Color::YELLOW,
},
..default()
});
}
}
let Some(selection) = r_selection else {
return;
};
let target = if q_meshes.contains(**selection) {
Some(**selection)
} else {
q_children.get(**selection).ok().and_then(|children| {
children
.iter()
.copied()
.find(|&child| q_meshes.contains(child))
})
};
if let Some(target) = target {
cmd.entity(target).insert(OutlineBundle {
outline: OutlineVolume {
visible: true,
width: 4.,
colour: Color::YELLOW,
},
..default()
});
}
}
@@ -223,7 +252,7 @@ mod wasm {
};
use wasm_bindgen::prelude::*;
use super::{Selected, Selection};
use super::Selection;
use crate::wasm::{self, FromStr, StaticBuffer, WasmMessageBuffer};
#[wasm_bindgen(typescript_custom_section)]
@@ -263,7 +292,6 @@ declare global {
pub(super) fn update_selection_from_frontend(mut cmd: Commands) {
if let Some(entity) = SELECTION_REQUEST.take_message() {
cmd.insert_resource(Selection(entity));
cmd.entity(entity).insert(Selected);
}
}
}
+1 -1
View File
@@ -29,7 +29,7 @@ mod mixamo;
mod path;
mod persistance;
mod remote;
mod scene;
pub mod scene;
pub mod screenshot;
mod skybox;
mod ui;
+88 -32
View File
@@ -1,6 +1,9 @@
use bevy::{prelude::*, render::render_resource::Extent3d, transform::components::Transform};
use clap::{Args, Parser, Subcommand};
use studio::headless::{start_headless, HeadlessMode};
use studio::{
headless::{start_headless, CameraMotion, HeadlessMode},
scene::SceneState,
};
#[derive(Parser)]
#[command(name = "StorytellerStudioHeadless")]
@@ -20,11 +23,9 @@ enum Subcommands {
Scene {
scene_dir: String,
#[arg(long)]
orbit: bool,
#[arg(long, value_parser = clap::value_parser!(f32))]
orbit_radius: Option<f32>,
#[arg(long, value_parser = clap::value_parser!(f32))]
orbit_speed: Option<f32>,
camera_motion: Option<String>,
#[arg(long)]
camera_motion_speed: Option<f32>,
#[arg(long, short)]
output_dir: Option<String>,
#[arg(long)]
@@ -73,9 +74,8 @@ fn main() {
let mut app = match args.subcommand {
Subcommands::Scene {
scene_dir,
orbit,
orbit_speed,
orbit_radius,
camera_motion,
camera_motion_speed,
skybox,
output_dir,
output_resolution,
@@ -98,15 +98,29 @@ fn main() {
frames,
output_res,
);
let orbit_speed = orbit_speed.unwrap_or(1.0);
let orbit_radius = orbit_radius.unwrap_or(8.0);
if orbit {
app.insert_resource(OrbitSettings {
radius: orbit_radius,
speed: orbit_speed,
})
.add_systems(Update, orbit_scene);
if let Some(camera_motion) = camera_motion {
let camera_motion_speed = camera_motion_speed.unwrap_or(1.0);
match camera_motion.as_str() {
"orbit" => {
app.insert_resource(CameraMotion::Orbit(camera_motion_speed));
}
"pan" => {
app.insert_resource(CameraMotion::Pan(camera_motion_speed));
}
"zoom" => {
app.insert_resource(CameraMotion::Zoom(camera_motion_speed));
}
"static" => {
app.insert_resource(CameraMotion::Static);
}
_ => panic!("Unsupported camera motion type."),
}
app.add_systems(
Update,
update_camera_motion.run_if(in_state(SceneState::Active)),
);
}
app
@@ -166,27 +180,69 @@ fn main() {
app.run();
}
#[derive(Resource)]
struct OrbitSettings {
radius: f32,
speed: f32,
}
fn orbit_scene(
mut camera: Query<&mut Transform, With<Camera>>,
orbit_settings: Res<OrbitSettings>,
pub(crate) fn update_camera_motion(
mut camera: Query<&mut Transform, (With<Camera>, Without<Parent>)>,
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;
};
main_cam_transform.translation = Vec3::new(
orbit_settings.radius * (time.elapsed_seconds() * orbit_settings.speed).cos(),
6.0,
orbit_settings.radius * (time.elapsed_seconds() * orbit_settings.speed).sin(),
);
main_cam_transform.look_at(Vec3::ZERO, Vec3::Y);
println!("Updating camera motion");
match *camera_motion {
CameraMotion::Orbit(speed) => {
orbit_camera(&mut main_cam_transform, &time, speed);
}
CameraMotion::Pan(speed) => {
pan_camera(&mut main_cam_transform, &time, speed);
}
CameraMotion::Zoom(speed) => {
zoom_camera(&mut main_cam_transform, &time, speed);
}
CameraMotion::Static => {
main_cam_transform.translation = Vec3::new(0.340, 1.1, -0.5);
main_cam_transform.rotation = Quat::from_euler(EulerRot::XYZ, 0.0, 0.0, 0.0);
}
}
// main_cam_transform.look_at(Vec3::ZERO, Vec3::Y);
}
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);
let end_pos = Vec3::new(0.366, 1.1, -0.5);
main_cam_transform.translation = start_pos.lerp(end_pos, normalized_cos);
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) {
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);
let end_pos = Vec3::new(0.067, 1.1, 0.121);
main_cam_transform.translation = start_pos.lerp(end_pos, normalized_cos);
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) {
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;
let angle = max_angle * normalized_cos;
// let t = (time.elapsed().as_secs_f32() / 5.0) % 1.0;
main_cam_transform.translation = Vec3::new(-2.2 * angle.cos(), 1.0, 2.5 * angle.sin());
main_cam_transform.look_at(Vec3::new(0.347, 0.556, -1.0), Vec3::Y);
}
//==============================================================================
+3 -3
View File
@@ -3,7 +3,7 @@ use bevy::{
window::{close_on_esc, Window},
};
use clap::Parser;
use studio::{screenshot::ScreenShotPlugin, InitAsset, InitParams, StudioMode};
use studio::{InitAsset, InitParams, StudioMode};
#[derive(Parser)]
#[command(name = "StorytellerStudio")]
@@ -19,7 +19,7 @@ struct StudioArgs {
fn main() {
let args = StudioArgs::parse();
let object_id = args.model.unwrap_or("Mannequin.gltf".to_string());
let object_id = args.model.unwrap_or("sample-room.gltf".to_string());
let skybox_id = args.skybox.unwrap_or("gum_trees_4k".to_string());
// Edit the arguments here to change the default startup scene or skybox
@@ -37,7 +37,7 @@ fn main() {
// Add any additional systems, resources, etc. for local debugging
app.add_systems(Update, close_on_esc);
app.add_plugins(ScreenShotPlugin);
// app.add_plugins(ScreenShotPlugin);
app.run();
}
+73 -61
View File
@@ -1,5 +1,6 @@
use std::{io::ErrorKind, path::Path, sync::Arc};
use async_compat::Compat;
use bevy::{
asset::io::{AssetReader, AssetReaderError, PathStream, Reader, VecReader},
prelude::*,
@@ -15,6 +16,68 @@ pub struct RemoteAssetReader;
impl RemoteAssetReader {
const API_BASE_URL: &'static str = "https://api.fakeyou.com/v1/media_files/file";
const BUCKET_BASE_URL: &'static str = "https://storage.googleapis.com/vocodes-public";
async fn load<'a>(&'a self, path: &'a Path) -> Result<Box<Reader<'a>>, AssetReaderError> {
let token = path
.ext_file_prefix()
.ok_or_else(|| {
AssetReaderError::Io(std::io::Error::new(
ErrorKind::Other,
format!("Failed to parse token from '{path:?}'"),
))
})?
.to_str()
.unwrap();
info!("Loading asset from media token: '{token}'");
let response = reqwest::get(format!("{}/{token}", &Self::API_BASE_URL))
.await
.map_err(IntoError::<AssetReaderError>::into)?;
let response = match response.status() {
StatusCode::OK => response
.json::<GetMediaFileSuccessResponse>()
.await
.map_err(IntoError::<AssetReaderError>::into),
other => Err(AssetReaderError::Io(std::io::Error::new(
ErrorKind::Other,
format!("Status Code from endpoint: {other}"),
))),
}?;
let url = format!(
"{}{}",
&Self::BUCKET_BASE_URL,
&response.media_file.public_bucket_path
);
let response = reqwest::get(url)
.await
.map_err(IntoError::<AssetReaderError>::into)?;
let bytes = match response.status() {
// TODO: This is not thread-safe, but some variation of this
// would be better than the current solution once Bevy
// WASM becomes multi-threaded:
// StatusCode::OK => Ok(response.bytes_stream().map(|result| {
// result.map_err(|err| std::io::Error::new(ErrorKind::Other, format!("{err}")))
// })),
StatusCode::OK => Ok(response
.bytes()
.await
.map_err(IntoError::<AssetReaderError>::into)?
.into_iter()
.collect::<Vec<_>>()),
other => Err(AssetReaderError::Io(std::io::Error::new(
ErrorKind::Other,
format!("Status Code from bucket URL: {other}"),
))),
}?;
let reader: Box<Reader> = Box::new(VecReader::new(bytes));
Ok(reader)
}
}
impl AssetReader for RemoteAssetReader {
@@ -22,67 +85,16 @@ impl AssetReader for RemoteAssetReader {
&'a self,
path: &'a Path,
) -> BoxedFuture<'a, Result<Box<Reader<'a>>, AssetReaderError>> {
Box::pin(async move {
let token = path
.ext_file_prefix()
.ok_or_else(|| {
AssetReaderError::Io(Arc::new(std::io::Error::new(
ErrorKind::Other,
format!("Failed to parse token from '{path:?}'"),
)))
})?
.to_str()
.unwrap();
info!("Loading asset from media token: '{token}'");
let response = reqwest::get(format!("{}/{token}", &Self::API_BASE_URL))
.await
.map_err(IntoError::<AssetReaderError>::into)?;
let response = match response.status() {
StatusCode::OK => response
.json::<GetMediaFileSuccessResponse>()
.await
.map_err(IntoError::<AssetReaderError>::into),
other => Err(AssetReaderError::Io(Arc::new(std::io::Error::new(
ErrorKind::Other,
format!("Status Code from endpoint: {other}"),
)))),
}?;
let url = format!(
"{}{}",
&Self::BUCKET_BASE_URL,
&response.media_file.public_bucket_path
);
let response = reqwest::get(url)
.await
.map_err(IntoError::<AssetReaderError>::into)?;
let bytes = match response.status() {
// TODO: This is not thread-safe, but some variation of this
// would be better than the current solution once Bevy
// WASM becomes multi-threaded:
// StatusCode::OK => Ok(response.bytes_stream().map(|result| {
// result.map_err(|err| std::io::Error::new(ErrorKind::Other, format!("{err}")))
// })),
StatusCode::OK => Ok(response
.bytes()
.await
.map_err(IntoError::<AssetReaderError>::into)?
.into_iter()
.collect::<Vec<_>>()),
other => Err(AssetReaderError::Io(Arc::new(std::io::Error::new(
ErrorKind::Other,
format!("Status Code from bucket URL: {other}"),
)))),
}?;
let reader: Box<Reader> = Box::new(VecReader::new(bytes));
Ok(reader)
})
let unboxed = async move { self.load(path).await };
if cfg!(not(feature = "wasm")) {
info!("wasm mode not detected");
// NB: Headless mode (server rendering) is running without a Tokio runtime.
// The reqwest HTTP client requires async-compat to function at runtime.
Box::pin(Compat::new(unboxed))
} else {
info!("wasm mode");
Box::pin(unboxed)
}
}
fn read_meta<'a>(
+105 -15
View File
@@ -12,22 +12,28 @@ use bevy::{
prelude::*,
render::{camera::RenderTarget, primitives::Aabb},
scene::InstanceId,
utils::{EntityHashSet, HashSet},
};
#[cfg(feature = "wasm")]
use serde_repr::{Deserialize_repr, Serialize_repr};
use space_editor::{
prelude::{EditorEvent, EditorPrefabPath, PrefabMarker},
space_prefab::component::SceneAutoChild,
space_prefab::{
component::{GltfPrefab, SceneAutoChild},
editor_registry::EditorRegistryExt,
},
};
#[cfg(feature = "wasm")]
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,
headless::{is_headless, is_not_headless, HeadlessSettings},
is_editor,
math::{Combinable, Extrema},
screenshot::ScreenCapture,
MainCamera,
@@ -57,21 +63,28 @@ 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),
);
app.add_systems(Update, tag_scene_elements);
app.add_systems(
Update,
(
tag_scene_elements,
autoplay_animations.run_if(not(is_editor)),
),
);
app.init_resource::<SkyboxHolder>();
app.add_systems(Update, attach_skybox);
@@ -103,6 +116,8 @@ impl Plugin for ScenePlugin {
wasm::notify_loading_queue_changes.map(bevy::utils::error),
),
);
app.editor_registry::<SceneElement>();
}
}
@@ -126,6 +141,7 @@ fn attach_skybox(
fn minimal_scene_setup(mut cmd: Commands) {
cmd.spawn((
Name::new("Directional Light"),
AnimationTarget::new(),
DirectionalLightBundle {
directional_light: DirectionalLight {
shadows_enabled: false,
@@ -164,8 +180,19 @@ pub struct LoadingQueue {
fn check_loading(
mut loading_queue: ResMut<LoadingQueue>,
mut st_next: ResMut<NextState<SceneState>>,
r_scene_state: Res<State<SceneState>>,
asset_server: Res<AssetServer>,
) {
// Avoid transitioning from `SceneState::None` directly to `SceneState::Active`.
// We will always be initializing the scene with _something_, so if the
// `LoadingQueue` is empty when we're in `SceneState::None`, it just means
// our startup routine hasn't yet gotten to the point of processing any
// `Handle<T>`s.
if loading_queue.queue.is_empty() && **r_scene_state == SceneState::Loading {
st_next.set(SceneState::Active);
return;
}
if !loading_queue.queue.is_empty() {
//filter queue to only include notloaded assets
@@ -200,8 +227,6 @@ fn check_loading(
} else {
st_next.set(SceneState::Loading);
}
} else {
st_next.set(SceneState::Active);
}
}
@@ -256,9 +281,11 @@ pub struct ImportSceneEvent(pub String);
wasm_bindgen,
derive(Serialize_repr, Deserialize_repr)
)]
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq, Reflect, Default)]
#[reflect(Component, Default)]
#[allow(dead_code)] // Some variants only used in `wasm`, some planned for later use
pub enum SceneElement {
#[default]
Generic,
Mesh,
Skeleton,
@@ -283,6 +310,7 @@ fn initialize(mut cmd: Commands) {
cmd.spawn((
Name::new("Main Camera"),
MainCamera,
AnimationTarget::MainCamera,
ChangeTracker,
Camera3dBundle {
transform: camera_xform,
@@ -339,6 +367,7 @@ fn initialiaze_headless(
cmd.spawn((
Name::new("Main Camera"),
MainCamera,
AnimationTarget::MainCamera,
Camera3dBundle {
transform: camera_xform,
tonemapping: Tonemapping::BlenderFilmic,
@@ -350,8 +379,6 @@ fn initialiaze_headless(
..default()
},
CameraControllerBundle::new(focus, camera_xform.translation),
ScreenCapture::from_out(&headless_settings.output_dir)
.with_max_frames(headless_settings.max_frames),
));
}
@@ -371,7 +398,11 @@ fn init_object_loading(mut cmd: Commands, mut er: EventReader<LoadLibraryObjectE
}
}
fn init_scene_import(mut cmd: Commands, mut er: EventReader<ImportSceneEvent>) {
fn init_scene_import(
mut cmd: Commands,
r_assets: Res<AssetServer>,
mut er: EventReader<ImportSceneEvent>,
) {
if let Some(path) = er.read().last() {
info!("Importing asset: {}", path.0);
cmd.spawn((
@@ -381,6 +412,7 @@ fn init_scene_import(mut cmd: Commands, mut er: EventReader<ImportSceneEvent>) {
path: path.0.clone(),
scene: "Scene0".to_string(),
},
r_assets.load::<Gltf>(&path.0),
PrefabMarker,
SceneRoot,
));
@@ -426,6 +458,64 @@ fn tag_scene_elements(
}
}
fn autoplay_animations(
mut cmd: Commands,
mut l_done: Local<HashSet<String>>,
mut l_animating: Local<EntityHashSet<Entity>>,
ra_gltfs: Res<Assets<Gltf>>,
ra_anims: Res<Assets<AnimationClip>>,
q_prefabs: Query<(&GltfPrefab, &Handle<Gltf>)>,
q_named_ents: Query<(Entity, &Name)>,
mut q_players: Query<&mut AnimationPlayer>,
) {
for (prefab, handle) in q_prefabs.iter() {
if l_done.contains(&prefab.path) {
continue;
}
let Some(gltf) = ra_gltfs.get(handle) else {
continue;
};
let mut playing_anims = 0_usize;
for (anim_name, anim_handle) in gltf.named_animations.iter() {
let Some(anim) = ra_anims.get(anim_handle.clone()) else {
continue;
};
let Some((target, name)) = q_named_ents
.iter()
.find(|(_, name)| anim.compatible_with(name))
else {
continue;
};
if !l_animating.contains(&target) {
info!(
"Playing animation '{}' on entity {:?} '{}'",
anim_name, target, name,
);
if let Ok(mut player) = q_players.get_mut(target) {
if !player.is_playing_clip(anim_handle) {
player.play(anim_handle.clone()).repeat();
}
} else {
let mut player = AnimationPlayer::default();
player.play(anim_handle.clone()).repeat();
cmd.entity(target).insert(player);
}
l_animating.insert(target);
}
playing_anims += 1;
}
if playing_anims == gltf.named_animations.len() {
l_done.insert(prefab.path.clone());
}
}
}
fn frame_scene_in_view(
mut cmd: Commands,
q_aabbs: Query<(&Aabb, &GlobalTransform), With<SceneAutoChild>>,
@@ -529,7 +619,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();
}
+18
View File
@@ -147,7 +147,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>>>,
) {
if scene_state.get() != &SceneState::Active {
return;
};
for (entity, screen_capture) in screen_capure_comps.iter() {
if screen_capture.should_capture() {
info!(
@@ -249,10 +253,24 @@ impl ViewNode for ScreenCaptureNode {
//==============================================================================
fn save_buffer_to_file(
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!");
}
+2 -9
View File
@@ -1,7 +1,6 @@
use std::sync::Arc;
use bevy::prelude::*;
use bevy_mod_picking::PickableBundle;
use space_editor::prelude::{
space_undo::{AddedEntity, NewChange},
*,
@@ -9,7 +8,6 @@ use space_editor::prelude::{
use wasm_bindgen::prelude::*;
use crate::{
interaction::Selectable,
scene::SceneElement,
wasm::{StaticBuffer, WasmMessageBuffer},
};
@@ -27,13 +25,8 @@ fn read_command_system(mut commands: Commands, mut change_event: EventWriter<New
return;
};
let mut new_entity = commands.spawn((
SpatialBundle::default(),
PrefabMarker,
Selectable,
SceneElement::Mesh,
PickableBundle::default(),
));
let mut new_entity =
commands.spawn((SpatialBundle::default(), PrefabMarker, SceneElement::Mesh));
match cmd {
BlockingCommand::Cube => {
+356 -105
View File
@@ -8,18 +8,20 @@ use bevy::{
window::PrimaryWindow,
};
use serde::{Deserialize, Serialize};
use serde_json::*;
use serde_json::{self, json, Map as JsonMap, Value as JsonValue};
use space_editor::{
space_editor_ui::{
editor_tab::{EditorTab, EditorTabName},
ui_plugin::EditorUiAppExt,
EditorUiAppExt,
},
space_prefab::{
component::MeshPrimitivePrefab, editor_registry::EditorRegistryExt, save::SaveState,
},
space_prefab::component::MeshPrimitive3dPrefab,
};
use wasm_bindgen::prelude::*;
use crate::{
interaction::Selected,
interaction::Selection,
wasm::{self, StaticBuffer, WasmMessageBuffer},
};
@@ -48,7 +50,11 @@ impl Plugin for InspectorPlugin {
app.add_systems(
Update,
update_field_by_ui.in_set(InspectorSystem::WriteChanges),
(
update_field_by_ui.map(bevy::utils::error),
update_local_rotation_by_ui,
)
.in_set(InspectorSystem::WriteChanges),
);
app.editor_tab_by_trait(
@@ -60,16 +66,17 @@ impl Plugin for InspectorPlugin {
app.register_inspect::<Transform>();
app.register_inspect::<Name>();
app.register_inspect::<MeshPrimitive3dPrefab>();
app.register_inspect::<MeshPrimitivePrefab>();
//register standart material
{
app.register_type::<ShortMaterial>()
app.editor_registry::<ShortMaterial>()
.add_event::<UpdateInspect<Handle<StandardMaterial>>>()
.add_systems(
Update,
(
update_inspect_by_select_change::<Handle<StandardMaterial>>,
update_inspect_by_select_change::<Handle<StandardMaterial>>
.run_if(resource_changed_or_removed::<Selection>()),
update_inspect_by_remove::<Handle<StandardMaterial>>,
update_inspect_by_change::<Handle<StandardMaterial>>,
notify_asset_change_system::<StandardMaterial>,
@@ -81,32 +88,90 @@ impl Plugin for InspectorPlugin {
serialize_standart_materal.in_set(InspectorSystem::PerComponentCollect),
);
app.add_systems(
OnEnter(SaveState::Save),
(create_short_materials, apply_deferred)
.chain()
.before(space_editor::space_prefab::sub_scene::prepare_auto_scene),
);
app.add_systems(PostUpdate, apply_short_materials);
app.world.resource_mut::<InspectGetters>().getters.insert(
StandardMaterial::get_type_registration()
.type_info()
.type_path()
.to_string(),
Arc::new(|entity, path, world, f| unsafe {
let handle = entity.get::<Handle<StandardMaterial>>().unwrap();
let handle = entity.get::<Handle<StandardMaterial>>().ok_or_else(|| {
format!(
"Failed to get StandardMaterial for entity {:?}",
entity.id()
)
})?;
let mut assets = world
.get_resource_mut::<Assets<StandardMaterial>>()
.unwrap();
let material = assets.get_mut(handle).unwrap();
.ok_or_else(|| {
"Failed to get `Assets<StandadrMaterial>` resource".to_string()
})?;
let reflect_field = material.reflect_path_mut(path).unwrap();
(f)(reflect_field);
let material = assets.get_mut(handle).ok_or_else(|| {
format!("Failed to retrieve material from handle {handle:?}")
})?;
let reflect_field = material
.reflect_path_mut(path)
.map_err(|err| format!("{err}"))?;
(f)(reflect_field)
}),
);
}
}
}
fn create_short_materials(
mut commands: Commands,
q_mats: Query<(Entity, &Handle<StandardMaterial>)>,
materials: Res<Assets<StandardMaterial>>,
) {
for (entity, handle) in q_mats.iter() {
if let Some(material) = materials.get(handle) {
commands.entity(entity).insert(ShortMaterial::new(material));
}
}
}
fn apply_short_materials(
mut commands: Commands,
q_mats: Query<(Entity, &Handle<StandardMaterial>, &ShortMaterial)>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
for (entity, handle, short) in q_mats.iter() {
let Some(material) = materials.get_mut(handle) else {
continue;
};
material.base_color = short.base_color;
material.perceptual_roughness = short.perceptual_roughness;
material.metallic = short.metallic;
material.reflectance = short.reflectance;
commands.entity(entity).remove::<ShortMaterial>();
}
}
#[derive(Resource, Default)]
struct InspectGetters {
getters: HashMap<
String,
Arc<
dyn Fn(&mut EntityWorldMut, &str, &mut UnsafeWorldCell, &dyn Fn(&mut dyn Reflect))
dyn Fn(
&mut EntityWorldMut,
&str,
&mut UnsafeWorldCell,
&dyn Fn(&mut dyn Reflect) -> Result<(), String>,
) -> Result<(), String>
+ Send
+ Sync,
>,
@@ -125,7 +190,8 @@ impl AppInspectable for App {
.add_systems(
Update,
(
update_inspect_by_select_change::<T>,
update_inspect_by_select_change::<T>
.run_if(resource_changed_or_removed::<Selection>()),
update_inspect_by_remove::<T>,
update_inspect_by_change::<T>,
)
@@ -146,7 +212,7 @@ impl AppInspectable for App {
Arc::new(|entity, path, _, f| {
let mut component = entity.get_mut::<T>().unwrap();
let reflect_field = component.reflect_path_mut(path).unwrap();
(f)(reflect_field);
(f)(reflect_field)
}),
);
}
@@ -158,7 +224,8 @@ impl AppInspectable for App {
.add_systems(
Update,
(
update_inspect_by_select_change::<Handle<T>>,
update_inspect_by_select_change::<Handle<T>>
.run_if(resource_changed_or_removed::<Selection>()),
update_inspect_by_remove::<Handle<T>>,
update_inspect_by_change::<Handle<T>>,
notify_asset_change_system::<T>,
@@ -186,7 +253,7 @@ impl EditorTab for TestInspectorTab {
CHANGE_FROM_UI.write_message(UpdateField {
component: "bevy_transform::components::transform::Transform".to_string(),
reflect_path: ".translation.x".to_string(),
value: serde_json::Value::from(0.0),
value: JsonValue::from(0.0),
});
}
}
@@ -220,116 +287,181 @@ impl<T> Default for UpdateInspect<T> {
}
}
fn update_field_by_ui(world: &mut World) {
#[derive(Serialize, Deserialize)]
struct Rgba {
red: f32,
green: f32,
blue: f32,
alpha: f32,
}
fn update_field_by_ui(world: &mut World) -> Result<(), String> {
unsafe {
let mut unsafe_world = world.as_unsafe_world_cell();
let Some(msg) = CHANGE_FROM_UI.take_message() else {
return;
return Ok(());
};
info!("Processing update: {msg:?}");
let mut selected_query = unsafe_world
.world_mut()
.query_filtered::<Entity, With<Selected>>();
let Ok(entity) = selected_query.get_single(unsafe_world.world_mut()) else {
return;
let Some(selection) = unsafe_world.world_mut().get_resource::<Selection>() else {
return Ok(());
};
let entity = **selection;
let getters = unsafe_world.get_resource_mut::<InspectGetters>().unwrap();
let getter = getters
.getters
.get(&msg.component)
.ok_or_else(|| format!("Failed to find getter for component: {}", msg.component))?;
let mut entity_mut = unsafe_world.world_mut().entity_mut(entity);
// TODO: This is pretty ugly
let target = 'find_target: {
if &msg.component[..] == "bevy_pbr::pbr_material::StandardMaterial" {
let mut q_handles = unsafe_world
.world_mut()
.query::<&Handle<StandardMaterial>>();
let Some(getter) = getters.getters.get(&msg.component) else {
error!("Failed to find getter for component: {}", msg.component);
return;
if q_handles.get(unsafe_world.world(), entity).is_ok() {
break 'find_target Some(entity);
}
let mut q_children = unsafe_world.world_mut().query::<&Children>();
let Ok(ent_children) = q_children.get(unsafe_world.world(), entity) else {
break 'find_target None;
};
break 'find_target ent_children
.iter()
.copied()
.find(|&child| q_handles.get(unsafe_world.world(), child).is_ok());
} else {
break 'find_target Some(entity);
}
};
let Some(target) = target else {
return Err(format!(
"Failed to find target for component {}",
&msg.component
));
};
info!("Applying to entity: {target:?}");
let mut entity_mut = unsafe_world.world_mut().entity_mut(target);
getter.as_ref()(
&mut entity_mut,
&msg.reflect_path,
&mut unsafe_world,
&|reflect_field| {
let fmt_err = |err: serde_json::Error| {
format!(
"Failed to parse value {} for path {}: {err}",
msg.value, &msg.reflect_path,
)
};
if let Some(val) = reflect_field.downcast_mut::<f32>() {
let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else {
error!("Failed to parse value: {}", msg.value);
return;
};
let msg_value = serde_json::from_value(msg.value.clone()).map_err(fmt_err)?;
*val = msg_value;
Ok(())
} else if let Some(val) = reflect_field.downcast_mut::<f64>() {
let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else {
error!("Failed to parse value: {}", msg.value);
return;
};
let msg_value = serde_json::from_value(msg.value.clone()).map_err(fmt_err)?;
*val = msg_value;
Ok(())
} else if let Some(val) = reflect_field.downcast_mut::<u32>() {
let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else {
error!("Failed to parse value: {}", msg.value);
return;
};
let msg_value = serde_json::from_value(msg.value.clone()).map_err(fmt_err)?;
*val = msg_value;
Ok(())
} else if let Some(val) = reflect_field.downcast_mut::<u64>() {
let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else {
error!("Failed to parse value: {}", msg.value);
return;
};
let msg_value = serde_json::from_value(msg.value.clone()).map_err(fmt_err)?;
*val = msg_value;
Ok(())
} else if let Some(val) = reflect_field.downcast_mut::<i32>() {
let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else {
error!("Failed to parse value: {}", msg.value);
return;
};
let msg_value = serde_json::from_value(msg.value.clone()).map_err(fmt_err)?;
*val = msg_value;
Ok(())
} else if let Some(val) = reflect_field.downcast_mut::<i64>() {
let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else {
error!("Failed to parse value: {}", msg.value);
return;
};
let msg_value = serde_json::from_value(msg.value.clone()).map_err(fmt_err)?;
*val = msg_value;
Ok(())
} else if let Some(val) = reflect_field.downcast_mut::<bool>() {
let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else {
error!("Failed to parse value: {}", msg.value);
return;
};
let msg_value = serde_json::from_value(msg.value.clone()).map_err(fmt_err)?;
*val = msg_value;
Ok(())
} else if let Some(val) = reflect_field.downcast_mut::<String>() {
*val = msg.value.as_str().unwrap().to_string();
let msg_value = msg
.value
.as_str()
.ok_or_else(|| format!("Failed to parse value as string: {}", msg.value))?;
*val = msg_value.to_string();
Ok(())
} else if let Some(val) = reflect_field.downcast_mut::<Vec3>() {
let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else {
error!("Failed to parse value: {}", msg.value);
return;
};
let msg_value = serde_json::from_value(msg.value.clone()).map_err(fmt_err)?;
*val = msg_value;
Ok(())
} else if let Some(val) = reflect_field.downcast_mut::<Quat>() {
let Ok(msg_value) = serde_json::from_value(msg.value.clone()) else {
error!("Failed to parse value: {}", msg.value);
return;
};
*val = msg_value;
#[derive(Deserialize)]
struct JsonQuat {
w: f32,
x: f32,
y: f32,
z: f32,
}
let JsonQuat { w, x, y, z } =
serde_json::from_value(msg.value.clone()).map_err(fmt_err)?;
*val = Quat::from_xyzw(x, y, z, w);
Ok(())
} else if let Some(val) = reflect_field.downcast_mut::<Color>() {
let Rgba {
red,
green,
blue,
alpha,
} = serde_json::from_value(msg.value.clone()).map_err(fmt_err)?;
match *val {
Color::RgbaLinear { .. } => {
*val = Color::RgbaLinear {
red,
green,
blue,
alpha,
};
}
Color::Rgba { .. } => {
*val = Color::Rgba {
red,
green,
blue,
alpha,
};
}
other => {
return Err(format!("Unsupported color format: {other:?}"));
}
}
Ok(())
} else {
error!("Unsupported type {}!", &msg.reflect_path);
return;
Err(format!("Unsupported type {}!", &msg.reflect_path))
}
},
);
)
}
}
#[derive(Resource, Serialize, Deserialize, Default, Clone)]
pub struct EntityDescription {
pub components: HashMap<String, Value>,
pub components: HashMap<String, JsonValue>,
}
fn update_inspect_by_select_change<T: Component + Reflect + GetTypeRegistration>(
mut ev_inspect: EventWriter<UpdateInspect<T>>,
q_added: Query<(), Added<Selected>>,
q_removed: RemovedComponents<Selected>,
) {
if !q_added.is_empty() {
ev_inspect.send(UpdateInspect::<T>::default());
}
if !q_removed.is_empty() {
ev_inspect.send(UpdateInspect::<T>::default());
}
ev_inspect.send(UpdateInspect::<T>::default());
}
fn update_inspect_by_remove<T: Component + Reflect + GetTypeRegistration>(
@@ -342,16 +474,20 @@ fn update_inspect_by_remove<T: Component + Reflect + GetTypeRegistration>(
}
fn update_inspect_by_change<T: Component + Reflect + GetTypeRegistration>(
r_selection: Option<Res<Selection>>,
mut ev_inspect: EventWriter<UpdateInspect<T>>,
q_t_changed: Query<&T, (With<Selected>, Changed<T>)>,
q_t_changed: Query<&T, Changed<T>>,
) {
if q_t_changed.iter().next().is_some() {
ev_inspect.send(UpdateInspect::<T>::default());
if let Some(selection) = r_selection {
if q_t_changed.contains(**selection) {
ev_inspect.send(UpdateInspect::<T>::default());
}
}
}
fn auto_serialize_system<T: Component + Reflect + GetTypeRegistration>(
q_t: Query<&T, With<Selected>>,
r_selection: Option<Res<Selection>>,
q_t: Query<&T>,
mut description: ResMut<EntityDescription>,
registry: Res<AppTypeRegistry>,
mut ev_inspect: EventReader<UpdateInspect<T>>,
@@ -363,7 +499,7 @@ fn auto_serialize_system<T: Component + Reflect + GetTypeRegistration>(
ev_inspect.clear();
if let Some(t) = q_t.iter().next() {
if let Some(t) = r_selection.and_then(|selection| q_t.get(**selection).ok()) {
let registry = registry.read();
let json = serde_json::to_value(ReflectSerializer::new(t, &registry)).unwrap();
info!("json: {:?}", json.to_string());
@@ -384,12 +520,20 @@ fn auto_serialize_system<T: Component + Reflect + GetTypeRegistration>(
}
fn notify_asset_change_system<T: Asset>(
r_selection: Option<Res<Selection>>,
mut ev_inspect: EventWriter<UpdateInspect<Handle<T>>>,
mut asset_changing: EventReader<AssetEvent<T>>,
mut selected: Query<&Handle<T>, With<Selected>>,
q_handles: Query<&Handle<T>>,
q_children: Query<&Children>,
) {
for ev in asset_changing.read() {
for handle in selected.iter_mut() {
if let Some(handle) = r_selection
.as_ref()
.and_then(|selection| {
find_handle_in_self_or_children(***selection, &q_children, &q_handles)
})
.and_then(|target| q_handles.get(target).ok())
{
if ev.is_modified(handle.id()) {
ev_inspect.send(UpdateInspect::<Handle<T>>::default());
}
@@ -398,7 +542,9 @@ fn notify_asset_change_system<T: Asset>(
}
fn auto_serialize_asset<T: Asset + Reflect + GetTypeRegistration>(
q_t: Query<&Handle<T>, With<Selected>>,
r_selection: Option<Res<Selection>>,
q_t: Query<&Handle<T>>,
q_children: Query<&Children>,
mut description: ResMut<EntityDescription>,
registry: Res<AppTypeRegistry>,
mut ev_inspect: EventReader<UpdateInspect<Handle<T>>>,
@@ -411,7 +557,11 @@ fn auto_serialize_asset<T: Asset + Reflect + GetTypeRegistration>(
ev_inspect.clear();
if let Some(handle) = q_t.iter().next() {
if let Some(handle) = r_selection
.as_ref()
.and_then(|selection| find_handle_in_self_or_children(***selection, &q_children, &q_t))
.and_then(|target| q_t.get(target).ok())
{
if let Some(asset) = assets.get(handle) {
let registry = registry.read();
info!(
@@ -441,22 +591,30 @@ fn auto_serialize_asset<T: Asset + Reflect + GetTypeRegistration>(
ev_update_description.send(UpdateEntityDescription);
}
#[derive(Reflect, Default)]
#[reflect(Default)]
#[derive(Reflect, Default, Clone, Component)]
#[reflect(Default, Component)]
struct ShortMaterial {
pub base_color: Color,
pub perceptual_roughness: f32,
pub metallic: f32,
pub reflectance: f32,
}
impl ShortMaterial {
pub fn new(material: &StandardMaterial) -> Self {
Self {
base_color: material.base_color,
perceptual_roughness: material.perceptual_roughness,
metallic: material.metallic,
reflectance: material.reflectance,
}
}
}
fn serialize_standart_materal(
r_selection: Option<Res<Selection>>,
q_m: Query<&Handle<StandardMaterial>>,
q_children: Query<&Children>,
mut description: ResMut<EntityDescription>,
registry: Res<AppTypeRegistry>,
mut ev_inspect: EventReader<UpdateInspect<Handle<StandardMaterial>>>,
@@ -469,8 +627,12 @@ fn serialize_standart_materal(
ev_inspect.clear();
if let Some(handle) = q_m.iter().next() {
if let Some(material) = assets.get(handle) {
if let Some((target, handle)) = r_selection
.as_ref()
.and_then(|selection| find_handle_in_self_or_children(***selection, &q_children, &q_m))
.and_then(|target| q_m.get(target).ok().map(|h| (target, h.clone())))
{
if let Some(material) = assets.get(handle.clone()) {
let registry = registry.read();
let short_material = ShortMaterial::new(material);
let json =
@@ -522,11 +684,11 @@ fn update_description(
.components
.iter()
.map(|(name, v)| {
let mut value = Value::Object(Map::new());
let mut value = JsonValue::Object(JsonMap::new());
value
.as_object_mut()
.unwrap()
.insert("name".to_string(), Value::String(name.to_string()));
.insert("name".to_string(), JsonValue::String(name.to_string()));
value
.as_object_mut()
.unwrap()
@@ -543,25 +705,54 @@ fn update_description(
}
}
#[derive(Deserialize, Serialize)]
/// Find a `Handle<T>` component on this entity or one of its direct children
fn find_handle_in_self_or_children<T: Asset>(
target: Entity,
q_children: &Query<&Children>,
q_handles: &Query<&Handle<T>>,
) -> Option<Entity> {
if q_handles.contains(target) {
return Some(target);
}
let Ok(children) = q_children.get(target) else {
return None;
};
children
.iter()
.copied()
.find(|&child| q_handles.contains(child))
}
#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct UpdateField {
pub component: String,
pub reflect_path: String,
pub value: serde_json::Value,
pub value: JsonValue,
}
#[wasm_bindgen(typescript_custom_section)]
const EVENT_DECLARATION: &str = r#"
export interface InspectorChanged extends CustomEvent {
export interface InspectorChangedEvent extends CustomEvent {
detail: string | null;
}
declare global {
export interface HTMLElementEventMap {
"inspector-changed": InspectorChanged;
export interface HTMLElementEventMap {
"inspector-changed": InspectorChangedEvent;
}
}
export interface UpdateField<T> {
component: string;
reflectPath: string;
value: T;
}
export function updateComponentField<T>(params: UpdateField<T>): void;
"#;
pub(super) fn notify_changed(q_window: &Window, msg: String) {
@@ -573,11 +764,71 @@ pub(super) fn notify_changed(q_window: &Window, msg: String) {
static CHANGE_FROM_UI: WasmMessageBuffer<UpdateField> = WasmMessageBuffer::new();
#[wasm_bindgen(js_name = changeValue)]
pub fn change_value(msg: JsValue) {
let Ok(msg) = serde_wasm_bindgen::from_value(msg) else {
error!("Failed to parse message");
#[wasm_bindgen(js_name = updateComponentField, skip_typescript)]
pub fn update_component_field(msg: JsValue) -> Result<(), JsValue> {
let msg = serde_wasm_bindgen::from_value(msg)?;
info!("Received update: {msg:?}");
CHANGE_FROM_UI.write_message(msg);
Ok(())
}
/// Handle transform rotation updates from the UI as a special case.
///
/// It's impossible to reliably compute a new quaternion for a desired Euler-
/// axes or axis-angle rotation from the frontend, because the calculation is
/// dependent on the parent's orientation. To deal with this, we provide a
/// separate `rotate_local_transform` method for this operation and handle it
/// with a separate Bevy system.
static ROTATE_LOCAL_XFORM: WasmMessageBuffer<(RotationAxis, f32)> = WasmMessageBuffer::new();
#[wasm_bindgen]
pub enum RotationAxis {
X,
Y,
Z,
}
#[wasm_bindgen(js_name = rotateLocalTransform)]
pub fn rotate_local_transform(axis: RotationAxis, theta: f32) {
ROTATE_LOCAL_XFORM.write_message((axis, theta));
}
fn update_local_rotation_by_ui(
r_selection: Option<Res<Selection>>,
q_parents: Query<&Parent>,
q_global_xforms: Query<&GlobalTransform>,
mut q_xforms: Query<&mut Transform>,
) {
let Some((axis, theta)) = ROTATE_LOCAL_XFORM.take_message() else {
return;
};
CHANGE_FROM_UI.write_message(msg);
let Some(selection) = r_selection.map(|selection| **selection) else {
return;
};
let Ok(target_global_xform) = q_global_xforms.get(selection).map(|xform| xform.affine()) else {
return;
};
let parent_global_xform = q_parents
.get(selection)
.and_then(|parent| q_global_xforms.get(**parent))
.unwrap_or(&GlobalTransform::IDENTITY)
.affine();
let axis_ws = match axis {
RotationAxis::X => target_global_xform.matrix3.x_axis,
RotationAxis::Y => target_global_xform.matrix3.y_axis,
RotationAxis::Z => target_global_xform.matrix3.z_axis,
};
let axis_parent_space = parent_global_xform
.inverse()
.transform_vector3a(axis_ws)
.normalize();
if let Ok(mut xform) = q_xforms.get_mut(selection) {
xform.rotate_axis(axis_parent_space.into(), theta);
}
}