mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-23 08:16:05 +00:00
Media Library: Importing and spawning assets (#109)
This PR updates the Media Library feature to implement asset imports
from the local file system, and spawning assets from the library into
the scene. Additionally, some placeholder functionality has been
scaffolded for adding animations onto existing characters in the scene.
### Additional Changes
* Dialogs can be set to `nonBlocking` to allow interaction with
background elements (note: this is fraught for accessibility and likely
temporary, to facilitate testing while the UI/UX redesign is in
progress)
* Dialogs can be moved around the screen by dragging their title-bars
* The `scene` module has been moved into a folder with a separate
`wasm.rs` file for its submodule. (`scene/mod.rs` remains one of our
largest single Rust files and could probably stand to be partitioned
further.)
### To-Do
* Support for adding animations is not yet fully implemented, though
some prerequisite functionality has been scaffolded. Namely, the "Add to
Scene" button will be disabled if:
- There is a mix of animations and non-animations selected in the Media
Library
- There is more than one animation selected in the Media Library
- There is one animation selected in the Media Library, but no
animatable character selected in the scene
* Adding full Storyteller-format scenes to the existing scene is not a
use case that has been tested, and may not be a use case we wish to
support in the long-term, as some of the data stored in Storyteller
scenes are intended to be "singleton" resources that may conflict with
existing data in the scene.
**Attempting to spawn a Storyteller scene through the Media Library has
not been explicitly blocked for now, but may cause unexpected
behavior.** This needs additional UX consideration and grooming.
This commit is contained in:
@@ -0,0 +1,616 @@
|
||||
use bevy::{
|
||||
asset::{DependencyLoadState, LoadState, RecursiveDependencyLoadState},
|
||||
core_pipeline::{
|
||||
bloom::{BloomCompositeMode, BloomPrefilterSettings, BloomSettings},
|
||||
fxaa::Fxaa,
|
||||
tonemapping::Tonemapping,
|
||||
},
|
||||
gltf::Gltf,
|
||||
math::vec2,
|
||||
pbr::ShadowFilteringMethod,
|
||||
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::{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,
|
||||
};
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
mod wasm;
|
||||
|
||||
pub struct ScenePlugin;
|
||||
|
||||
#[derive(SystemSet, Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct SceneSpawnerSystem;
|
||||
|
||||
impl Plugin for ScenePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_plugins(ChangeTrackingPlugin);
|
||||
|
||||
app.insert_resource(ClearColor(Color::hsl(0.0, 0.0, 0.25)))
|
||||
.insert_resource(Msaa::Off)
|
||||
.add_state::<SceneState>()
|
||||
.add_event::<LoadLibraryObjectEvent>()
|
||||
.add_event::<LoadSceneEvent>()
|
||||
.add_event::<ImportSceneEvent>()
|
||||
.add_systems(
|
||||
Startup,
|
||||
(
|
||||
initialize.run_if(is_not_headless),
|
||||
initialiaze_headless.run_if(is_headless),
|
||||
),
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
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)
|
||||
.chain()
|
||||
.in_set(SceneSpawnerSystem),
|
||||
);
|
||||
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
tag_scene_elements,
|
||||
autoplay_animations.run_if(not(is_editor)),
|
||||
),
|
||||
);
|
||||
|
||||
//Auto loading screen
|
||||
//Register handles on which we will look to show loading screen
|
||||
app.init_resource::<LoadingQueue>();
|
||||
app.add_systems(
|
||||
PostUpdate,
|
||||
(
|
||||
check_loading,
|
||||
auto_add_handle::<Scene>,
|
||||
auto_add_handle::<Mesh>,
|
||||
auto_add_handle::<StandardMaterial>,
|
||||
),
|
||||
);
|
||||
|
||||
//setup default light
|
||||
app.add_systems(Startup, minimal_scene_setup);
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
wasm::handle_spawn_asset_messages,
|
||||
wasm::notify_state_changes
|
||||
.map(bevy::utils::error)
|
||||
.run_if(resource_changed::<State<SceneState>>()),
|
||||
wasm::notify_loading_queue_changes.map(bevy::utils::error),
|
||||
),
|
||||
);
|
||||
|
||||
app.editor_registry::<SceneElement>();
|
||||
}
|
||||
}
|
||||
|
||||
fn minimal_scene_setup(mut cmd: Commands) {
|
||||
cmd.spawn((
|
||||
Name::new("Directional Light"),
|
||||
AnimationTarget::new(),
|
||||
DirectionalLightBundle {
|
||||
directional_light: DirectionalLight {
|
||||
shadows_enabled: false,
|
||||
illuminance: 20000.0,
|
||||
..default()
|
||||
},
|
||||
transform: Transform::default().looking_at(Vec3::NEG_ONE, Vec3::Y),
|
||||
..default()
|
||||
},
|
||||
PrefabMarker,
|
||||
SceneElement::Light,
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", wasm_bindgen)]
|
||||
#[derive(States, Clone, Copy, Default, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum SceneState {
|
||||
#[default]
|
||||
None,
|
||||
Loading,
|
||||
Active,
|
||||
}
|
||||
|
||||
#[derive(Resource, Deref)]
|
||||
pub struct ActiveScene {
|
||||
#[deref]
|
||||
pub instance: InstanceId,
|
||||
pub source: Handle<Gltf>,
|
||||
}
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
pub struct LoadingQueue {
|
||||
pub queue: Vec<UntypedHandle>,
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
let mut not_loaded_count = 0;
|
||||
|
||||
loading_queue.queue.retain(|h| {
|
||||
if let Some((load_state, dep_load_state, rec_load_state)) =
|
||||
asset_server.get_load_states(h.id())
|
||||
{
|
||||
let is_loaded = dep_load_state == DependencyLoadState::Loaded;
|
||||
|
||||
let is_failed = dep_load_state == DependencyLoadState::Failed
|
||||
|| rec_load_state == RecursiveDependencyLoadState::Failed
|
||||
|| load_state == LoadState::Failed;
|
||||
|
||||
if load_state == LoadState::NotLoaded {
|
||||
not_loaded_count += 1;
|
||||
}
|
||||
|
||||
!(is_failed || is_loaded)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
info!("Loading {} assets", loading_queue.queue.len());
|
||||
|
||||
if loading_queue.queue.len() - not_loaded_count == 0 {
|
||||
//not change state
|
||||
} else {
|
||||
st_next.set(SceneState::Loading);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn auto_add_handle<T: Asset>(
|
||||
mut queue: ResMut<LoadingQueue>,
|
||||
query: Query<&Handle<T>, Added<Handle<T>>>,
|
||||
) {
|
||||
for h in query.iter() {
|
||||
queue.queue.push(h.clone().untyped());
|
||||
}
|
||||
}
|
||||
|
||||
/// Request the application to load a `gltf`/`glb` scene from the "bundled"
|
||||
/// asset library in `assets/gltf/`. Expects a filename including the extension.
|
||||
///
|
||||
/// ```ignore
|
||||
/// fn main() {
|
||||
/// App::new()
|
||||
/// .add_plugins((
|
||||
/// DefaultPlugins,
|
||||
/// CommonPlugins,
|
||||
/// ))
|
||||
/// .add_systems(Startup, load_mannequin)
|
||||
/// }
|
||||
///
|
||||
/// fn load_mannequin(mut ew_load_object: EventWriter<LoadLibraryObjectEvent>) {
|
||||
/// ew_load_object.send(LoadLibraryObjectEvent("Mannequin.gltf".into()));
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Event, Deref)]
|
||||
pub struct LoadLibraryObjectEvent(pub String);
|
||||
|
||||
/// Request the application to load a previously saved Storyteller Studio scene.
|
||||
///
|
||||
/// Expects a full, resolvable path to the file (e.g. `scenes/my-scene.scn.ron`
|
||||
/// for a local file saved under `studio/assets/scenes`, or
|
||||
/// `remote://<media_token>.scn.ron` for a scene saved to the Storyteller.ai
|
||||
/// backend).
|
||||
#[derive(Event, Deref)]
|
||||
pub struct LoadSceneEvent(pub String);
|
||||
|
||||
/// Request the application to import and process a glTF scene from an arbitrary
|
||||
/// asset source (e.g., an http path or a Storyteller.ai media token).
|
||||
#[derive(Event)]
|
||||
pub struct ImportSceneEvent {
|
||||
pub path: String,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
/// A tag that identifies its entity as a user-facing scene element like a prop,
|
||||
/// a light, or a camera.
|
||||
#[cfg_attr(
|
||||
feature = "wasm",
|
||||
repr(u8),
|
||||
wasm_bindgen,
|
||||
derive(Serialize_repr, Deserialize_repr)
|
||||
)]
|
||||
#[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,
|
||||
Bone,
|
||||
Light,
|
||||
Camera,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct SceneData {
|
||||
pub scene_path: String,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct SceneRoot;
|
||||
|
||||
fn initialize(mut cmd: Commands) {
|
||||
// Camera, graphics settings
|
||||
let focus = Vec3::ZERO;
|
||||
let camera_xform = Transform::from_xyz(-2.5, 4.5, 5.0).looking_at(focus, Vec3::Y);
|
||||
|
||||
cmd.spawn((
|
||||
Name::new("Main Camera"),
|
||||
MainCamera,
|
||||
AnimationTarget::MainCamera,
|
||||
ChangeTracker,
|
||||
Camera3dBundle {
|
||||
transform: camera_xform,
|
||||
tonemapping: Tonemapping::BlenderFilmic,
|
||||
camera: Camera {
|
||||
hdr: true,
|
||||
..default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
CameraControllerBundle::new(focus, camera_xform.translation),
|
||||
ShadowFilteringMethod::Hardware2x2,
|
||||
BloomSettings {
|
||||
composite_mode: BloomCompositeMode::EnergyConserving,
|
||||
intensity: 0.25,
|
||||
prefilter_settings: BloomPrefilterSettings {
|
||||
threshold: 0.8,
|
||||
threshold_softness: 0.5,
|
||||
},
|
||||
..default()
|
||||
},
|
||||
Fxaa::default(),
|
||||
));
|
||||
}
|
||||
|
||||
fn initialiaze_headless(
|
||||
mut cmd: Commands,
|
||||
mut image_assets: ResMut<Assets<Image>>,
|
||||
headless_settings: Res<HeadlessSettings>,
|
||||
) {
|
||||
// Camera, graphics settings
|
||||
let focus = Vec3::ZERO;
|
||||
let camera_xform = Transform::from_xyz(-2.5, 4.5, 5.0).looking_at(focus, Vec3::Y);
|
||||
|
||||
let mut test_image = Image {
|
||||
texture_descriptor: TextureDescriptor {
|
||||
label: Some("render_texture"),
|
||||
size: headless_settings.output_dims,
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::Rgba8UnormSrgb,
|
||||
usage: TextureUsages::TEXTURE_BINDING
|
||||
| TextureUsages::COPY_SRC
|
||||
| TextureUsages::COPY_DST
|
||||
| TextureUsages::RENDER_ATTACHMENT,
|
||||
view_formats: &[],
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
test_image.resize(headless_settings.output_dims);
|
||||
|
||||
cmd.spawn((
|
||||
Name::new("Main Camera"),
|
||||
MainCamera,
|
||||
AnimationTarget::MainCamera,
|
||||
Camera3dBundle {
|
||||
transform: camera_xform,
|
||||
tonemapping: Tonemapping::BlenderFilmic,
|
||||
camera: Camera {
|
||||
hdr: false,
|
||||
target: RenderTarget::Image(image_assets.add(test_image)),
|
||||
..Default::default()
|
||||
},
|
||||
..default()
|
||||
},
|
||||
CameraControllerBundle::new(focus, camera_xform.translation),
|
||||
));
|
||||
}
|
||||
|
||||
fn init_object_loading(mut cmd: Commands, mut er: EventReader<LoadLibraryObjectEvent>) {
|
||||
if let Some(id) = er.read().last() {
|
||||
info!("Loading library asset: {}", id.0);
|
||||
cmd.spawn((
|
||||
Name::new(id.0.clone()),
|
||||
SpatialBundle::default(),
|
||||
space_editor::prelude::GltfPrefab {
|
||||
path: format!("gltf/{}", id.0),
|
||||
scene: "Scene0".to_string(),
|
||||
},
|
||||
PrefabMarker,
|
||||
SceneRoot,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn init_scene_import(
|
||||
mut cmd: Commands,
|
||||
r_assets: Res<AssetServer>,
|
||||
mut er: EventReader<ImportSceneEvent>,
|
||||
) {
|
||||
for event in er.read() {
|
||||
info!("Importing asset: {}", event.path);
|
||||
|
||||
let name = event
|
||||
.name
|
||||
.as_ref()
|
||||
.map(|name| Name::new(name.clone()))
|
||||
.unwrap_or_else(|| Name::new("Scene"));
|
||||
|
||||
cmd.spawn((
|
||||
name,
|
||||
SpatialBundle::default(),
|
||||
space_editor::prelude::GltfPrefab {
|
||||
path: event.path.clone(),
|
||||
scene: "Scene0".to_string(),
|
||||
},
|
||||
r_assets.load::<Gltf>(&event.path),
|
||||
PrefabMarker,
|
||||
SceneRoot,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn init_scene_loading(
|
||||
mut er_load_scene: EventReader<LoadSceneEvent>,
|
||||
mut ew_editor: EventWriter<EditorEvent>,
|
||||
) {
|
||||
if let Some(scene_path) = er_load_scene.read().last() {
|
||||
info!("Loading scene: {}", scene_path.0);
|
||||
ew_editor.send(EditorEvent::Load(EditorPrefabPath::File(
|
||||
scene_path.0.clone(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
fn tag_scene_elements(
|
||||
mut cmd: Commands,
|
||||
// A "glTF mesh" is actually a container for one or more "glTF primitives",
|
||||
// which is basically a bundle of vertices paired with the material used to
|
||||
// shade them. It's those "sub-meshes" that carry the actual mesh handles,
|
||||
// but we don't necessarily want to expose those to users a la carte,
|
||||
// because they often consist of stuff like a single eyeball, or just the
|
||||
// leafy parts of a plant. Here we only want to tag the elements that a user
|
||||
// might want to individually address when arranging or animating a scene.
|
||||
q_named_non_primitives: Query<
|
||||
(Entity, &Children),
|
||||
(
|
||||
With<SceneAutoChild>,
|
||||
With<Name>,
|
||||
Without<Handle<Mesh>>,
|
||||
Without<SceneElement>,
|
||||
),
|
||||
>,
|
||||
mesh_entities: Query<Entity, With<Handle<Mesh>>>,
|
||||
) {
|
||||
for (e, children) in q_named_non_primitives.iter() {
|
||||
if children.iter().any(|child| mesh_entities.contains(*child)) {
|
||||
cmd.entity(e).insert(SceneElement::Mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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>>,
|
||||
mut q_camera: Query<(&mut CameraController, &mut Transform, &Projection)>,
|
||||
) {
|
||||
let Ok((mut cam, mut cam_xform, cam_proj)) = q_camera.get_single_mut() else {
|
||||
error!("Failed to get camera controller");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(aabb) = Aabb::combine(q_aabbs.iter()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let world2cam = cam_xform.compute_matrix().inverse();
|
||||
let plane_origin = Vec3::from(aabb.center);
|
||||
let plane_normal = cam_xform.forward();
|
||||
|
||||
let Some((_depth, bounding_rect)) = aabb
|
||||
.extrema()
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|mut p| {
|
||||
cmd.spawn(DebugPoint {
|
||||
point: p,
|
||||
color: Color::CYAN,
|
||||
});
|
||||
|
||||
// Inflate the combined AABB to give the model some breathing room
|
||||
p *= 1.5;
|
||||
|
||||
// Project each corner onto the camera plane
|
||||
let v = p - plane_origin;
|
||||
let proj = p - v.project_onto_normalized(plane_normal);
|
||||
|
||||
cmd.spawn(DebugPoint {
|
||||
point: proj,
|
||||
color: Color::FUCHSIA,
|
||||
});
|
||||
|
||||
// Transform into the camera's local space
|
||||
world2cam.transform_point(proj)
|
||||
})
|
||||
// Fold the camera-space plane-projected points into a bounding rect and view depth
|
||||
.fold(None, |accum: Option<(f32, Rect)>, p: Vec3| match accum {
|
||||
Some((depth, rect)) => Some((depth, rect.union_point(vec2(p.x, p.y)))),
|
||||
None => {
|
||||
let depth = p.z.abs();
|
||||
let p = vec2(p.x, p.y);
|
||||
Some((depth, Rect::from_corners(p, p)))
|
||||
}
|
||||
})
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let pproj = match cam_proj {
|
||||
Projection::Perspective(p) => p,
|
||||
_ => panic!("Expected a perspective projection"),
|
||||
};
|
||||
|
||||
// Formula for the target camera distance
|
||||
// (c/o https://stackoverflow.com/questions/2866350/move-camera-to-fit-3d-scene):
|
||||
//
|
||||
// d = (s/2) / tan(a/2)
|
||||
// where:
|
||||
// d = distance
|
||||
// s = size of the largest camera-aligned axis in world coords
|
||||
// a = field-of-view angle for the same axis
|
||||
let Vec2 {
|
||||
x: width,
|
||||
y: height,
|
||||
} = bounding_rect.size();
|
||||
|
||||
let s = f32::max(width, height);
|
||||
let a = if s == height {
|
||||
pproj.fov
|
||||
} else {
|
||||
f32::atan(pproj.aspect_ratio * f32::tan(pproj.fov / 2.))
|
||||
};
|
||||
|
||||
// TODO:
|
||||
// According to "I eat babies" at the Ars Technica forums
|
||||
// (https://arstechnica.com/civis/threads/if-you-have-aspect-ratio-and-vertical-fov-can-you-calc-horizontal-fov.37447/):
|
||||
//
|
||||
// tan(fov_v / 2) = y / z
|
||||
// tan(fov_h / 2) = x / z
|
||||
//
|
||||
// Theoretically, this would let us skip some costly tan/atan
|
||||
// computations, but I must be missing something, because just
|
||||
// substituting (s / depth) for the divisor below would make the
|
||||
// whole equation independent of the FOV, which is obviously
|
||||
// wrong.
|
||||
//
|
||||
// Unfortunately I'm embarrassingly bad at algebra, but there
|
||||
// must be some room for simplification here.
|
||||
let radius = (s / 2.) / f32::tan(a / 2.);
|
||||
|
||||
cam.focus = aabb.center.into();
|
||||
cam.radius = radius;
|
||||
cam_xform.translation = cam.focus + cam_xform.back() * radius;
|
||||
}
|
||||
|
||||
fn start_capture(mut captures: Query<&mut ScreenCapture, Added<ScreenCapture>>) {
|
||||
for mut capture in captures.iter_mut() {
|
||||
capture.start_capturing();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
use bevy::{prelude::*, window::PrimaryWindow};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use super::{ImportSceneEvent, LoadingQueue, SceneState};
|
||||
use crate::wasm::{self, StaticBuffer, WasmMessageBuffer};
|
||||
|
||||
static SPAWN_ASSETS: WasmMessageBuffer<Vec<AssetImport>> = WasmMessageBuffer::new();
|
||||
|
||||
#[wasm_bindgen(typescript_custom_section)]
|
||||
const ASSET_IMPORT_TS: &str = r#"
|
||||
export interface AssetImport {
|
||||
path: string;
|
||||
name?: string;
|
||||
}
|
||||
"#;
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct AssetImport {
|
||||
path: String,
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen(typescript_custom_section)]
|
||||
const SPAWN_ASSET_TS: &str = r#"
|
||||
export function spawnAsset(asset: AssetImport): void;
|
||||
"#;
|
||||
#[wasm_bindgen(js_name = spawnAsset, skip_typescript)]
|
||||
pub fn spawn_asset(asset: JsValue) -> Result<(), JsValue> {
|
||||
let asset: AssetImport = serde_wasm_bindgen::from_value(asset)?;
|
||||
SPAWN_ASSETS.write_message(vec![asset]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(typescript_custom_section)]
|
||||
const SPAWN_ASSETS_TS: &str = r#"
|
||||
export function spawnAssets(assets: AssetImport[]): void;
|
||||
"#;
|
||||
#[wasm_bindgen(js_name = spawnAssets, skip_typescript)]
|
||||
pub fn spawn_assets(assets: JsValue) -> Result<(), JsValue> {
|
||||
let assets: Vec<AssetImport> = serde_wasm_bindgen::from_value(assets)?;
|
||||
SPAWN_ASSETS.write_message(assets);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn handle_spawn_asset_messages(mut ew_import_scene: EventWriter<ImportSceneEvent>) {
|
||||
if let Some(assets) = SPAWN_ASSETS.take_message() {
|
||||
for AssetImport { path, name } in assets {
|
||||
ew_import_scene.send(ImportSceneEvent { path, name });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen(typescript_custom_section)]
|
||||
const SCENE_STATE_EVENT: &str = r#"
|
||||
export interface SceneStateEvent extends CustomEvent {
|
||||
type: "scene-state";
|
||||
detail: SceneState;
|
||||
}
|
||||
|
||||
declare global {
|
||||
export interface GlobalEventHandlersEventMap {
|
||||
"scene-state": SceneStateEvent;
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
pub(super) fn notify_state_changes(
|
||||
r_state: Res<State<SceneState>>,
|
||||
q_window: Query<&Window, With<PrimaryWindow>>,
|
||||
) -> Result<(), String> {
|
||||
if let Ok(window) = q_window.get_single() {
|
||||
wasm::dispatch_to_js(window, "scene-state", &(**r_state as u32))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen(typescript_custom_section)]
|
||||
const LOADING_QUEUE_EVENT: &str = r#"
|
||||
export interface LoadingQueueEvent extends CustomEvent {
|
||||
type: "loading-queue";
|
||||
detail: number;
|
||||
}
|
||||
|
||||
declare global {
|
||||
export interface GlobalEventHandlersEventMap {
|
||||
"loading-queue": LoadingQueueEvent;
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
pub(super) fn notify_loading_queue_changes(
|
||||
mut l_last_sent: Local<Option<usize>>,
|
||||
r_loading: Res<LoadingQueue>,
|
||||
q_window: Query<&Window, With<PrimaryWindow>>,
|
||||
) -> Result<(), String> {
|
||||
let queue_len = r_loading.queue.len();
|
||||
if let Some(last_sent) = *l_last_sent {
|
||||
if last_sent == queue_len {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let Ok(window) = q_window.get_single() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
wasm::dispatch_to_js(window, "loading-queue", &queue_len)?;
|
||||
*l_last_sent = Some(queue_len);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user