diff --git a/Cargo.toml b/Cargo.toml index 47cb8a55c30d4..88a732ad3929e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1358,7 +1358,7 @@ wasm = true # Tools [[example]] name = "scene_viewer" -path = "examples/tools/scene_viewer.rs" +path = "examples/tools/scene_viewer/main.rs" [package.metadata.example.scene_viewer] name = "Scene Viewer" diff --git a/examples/README.md b/examples/README.md index e354e6f283120..9b732cc7f9cc8 100644 --- a/examples/README.md +++ b/examples/README.md @@ -295,7 +295,7 @@ Example | Description Example | Description --- | --- [Gamepad Viewer](../examples/tools/gamepad_viewer.rs) | Shows a visualization of gamepad buttons, sticks, and triggers -[Scene Viewer](../examples/tools/scene_viewer.rs) | A simple way to view glTF models with Bevy. Just run `cargo run --release --example scene_viewer /path/to/model.gltf#Scene0`, replacing the path as appropriate. With no arguments it will load the FieldHelmet glTF model from the repository assets subdirectory +[Scene Viewer](../examples/tools/scene_viewer/main.rs) | A simple way to view glTF models with Bevy. Just run `cargo run --release --example scene_viewer /path/to/model.gltf#Scene0`, replacing the path as appropriate. With no arguments it will load the FieldHelmet glTF model from the repository assets subdirectory ## Transforms diff --git a/examples/tools/scene_viewer.rs b/examples/tools/scene_viewer.rs deleted file mode 100644 index abdb3f87a8494..0000000000000 --- a/examples/tools/scene_viewer.rs +++ /dev/null @@ -1,578 +0,0 @@ -//! A simple glTF scene viewer made with Bevy. -//! -//! Just run `cargo run --release --example scene_viewer /path/to/model.gltf`, -//! replacing the path as appropriate. -//! In case of multiple scenes, you can select which to display by adapting the file path: `/path/to/model.gltf#Scene1`. -//! With no arguments it will load the `FlightHelmet` glTF model from the repository assets subdirectory. - -use bevy::{ - asset::LoadState, - gltf::Gltf, - input::mouse::MouseMotion, - math::Vec3A, - prelude::*, - render::primitives::{Aabb, Sphere}, - scene::InstanceId, -}; - -use std::f32::consts::*; - -#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] -struct CameraControllerCheckSystem; - -fn main() { - println!( - " -Controls: - MOUSE - Move camera orientation - LClick/M - Enable mouse movement - WSAD - forward/back/strafe left/right - LShift - 'run' - E - up - Q - down - L - animate light direction - U - toggle shadows - C - cycle through the camera controller and any cameras loaded from the scene - 5/6 - decrease/increase shadow projection width - 7/8 - decrease/increase shadow projection height - 9/0 - decrease/increase shadow projection near/far - - Space - Play/Pause animation - Enter - Cycle through animations -" - ); - let mut app = App::new(); - app.insert_resource(AmbientLight { - color: Color::WHITE, - brightness: 1.0 / 5.0f32, - }) - .init_resource::() - .add_plugins( - DefaultPlugins - .set(WindowPlugin { - window: WindowDescriptor { - title: "bevy scene viewer".to_string(), - ..default() - }, - ..default() - }) - .set(AssetPlugin { - asset_folder: std::env::var("CARGO_MANIFEST_DIR") - .unwrap_or_else(|_| ".".to_string()), - watch_for_changes: true, - }), - ) - .add_startup_system(setup) - .add_system_to_stage(CoreStage::PreUpdate, scene_load_check) - .add_system_to_stage(CoreStage::PreUpdate, setup_scene_after_load) - .add_system(update_lights) - .add_system(camera_controller) - .add_system(camera_tracker); - - #[cfg(feature = "animation")] - app.add_system(start_animation) - .add_system(keyboard_animation_control); - - app.run(); -} - -#[derive(Resource)] -struct SceneHandle { - gltf_handle: Handle, - scene_index: usize, - #[cfg(feature = "animation")] - animations: Vec>, - instance_id: Option, - is_loaded: bool, - has_light: bool, -} - -fn parse_scene(scene_path: String) -> (String, usize) { - if scene_path.contains('#') { - let gltf_and_scene = scene_path.split('#').collect::>(); - if let Some((last, path)) = gltf_and_scene.split_last() { - if let Some(index) = last - .strip_prefix("Scene") - .and_then(|index| index.parse::().ok()) - { - return (path.join("#"), index); - } - } - } - (scene_path, 0) -} - -fn setup(mut commands: Commands, asset_server: Res) { - let scene_path = std::env::args() - .nth(1) - .unwrap_or_else(|| "assets/models/FlightHelmet/FlightHelmet.gltf".to_string()); - info!("Loading {}", scene_path); - let (file_path, scene_index) = parse_scene(scene_path); - commands.insert_resource(SceneHandle { - gltf_handle: asset_server.load(file_path), - scene_index, - #[cfg(feature = "animation")] - animations: Vec::new(), - instance_id: None, - is_loaded: false, - has_light: false, - }); -} - -fn scene_load_check( - asset_server: Res, - mut scenes: ResMut>, - gltf_assets: ResMut>, - mut scene_handle: ResMut, - mut scene_spawner: ResMut, -) { - match scene_handle.instance_id { - None => { - if asset_server.get_load_state(&scene_handle.gltf_handle) == LoadState::Loaded { - let gltf = gltf_assets.get(&scene_handle.gltf_handle).unwrap(); - if gltf.scenes.len() > 1 { - info!( - "Displaying scene {} out of {}", - scene_handle.scene_index, - gltf.scenes.len() - ); - info!("You can select the scene by adding '#Scene' followed by a number to the end of the file path (e.g '#Scene1' to load the second scene)."); - } - - let gltf_scene_handle = - gltf.scenes - .get(scene_handle.scene_index) - .unwrap_or_else(|| { - panic!( - "glTF file doesn't contain scene {}!", - scene_handle.scene_index - ) - }); - let scene = scenes.get_mut(gltf_scene_handle).unwrap(); - - let mut query = scene - .world - .query::<(Option<&DirectionalLight>, Option<&PointLight>)>(); - scene_handle.has_light = - query - .iter(&scene.world) - .any(|(maybe_directional_light, maybe_point_light)| { - maybe_directional_light.is_some() || maybe_point_light.is_some() - }); - - scene_handle.instance_id = - Some(scene_spawner.spawn(gltf_scene_handle.clone_weak())); - - #[cfg(feature = "animation")] - { - scene_handle.animations = gltf.animations.clone(); - if !scene_handle.animations.is_empty() { - info!( - "Found {} animation{}", - scene_handle.animations.len(), - if scene_handle.animations.len() == 1 { - "" - } else { - "s" - } - ); - } - } - - info!("Spawning scene..."); - } - } - Some(instance_id) if !scene_handle.is_loaded => { - if scene_spawner.instance_is_ready(instance_id) { - info!("...done!"); - scene_handle.is_loaded = true; - } - } - Some(_) => {} - } -} - -#[cfg(feature = "animation")] -fn start_animation( - mut player: Query<&mut AnimationPlayer>, - mut done: Local, - scene_handle: Res, -) { - if !*done { - if let Ok(mut player) = player.get_single_mut() { - if let Some(animation) = scene_handle.animations.first() { - player.play(animation.clone_weak()).repeat(); - *done = true; - } - } - } -} - -#[cfg(feature = "animation")] -fn keyboard_animation_control( - keyboard_input: Res>, - mut animation_player: Query<&mut AnimationPlayer>, - scene_handle: Res, - mut current_animation: Local, - mut changing: Local, -) { - if scene_handle.animations.is_empty() { - return; - } - - if let Ok(mut player) = animation_player.get_single_mut() { - if keyboard_input.just_pressed(KeyCode::Space) { - if player.is_paused() { - player.resume(); - } else { - player.pause(); - } - } - - if *changing { - // change the animation the frame after return was pressed - *current_animation = (*current_animation + 1) % scene_handle.animations.len(); - player - .play(scene_handle.animations[*current_animation].clone_weak()) - .repeat(); - *changing = false; - } - - if keyboard_input.just_pressed(KeyCode::Return) { - // delay the animation change for one frame - *changing = true; - // set the current animation to its start and pause it to reset to its starting state - player.set_elapsed(0.0).pause(); - } - } -} - -fn setup_scene_after_load( - mut commands: Commands, - mut setup: Local, - mut scene_handle: ResMut, - meshes: Query<(&GlobalTransform, Option<&Aabb>), With>>, -) { - if scene_handle.is_loaded && !*setup { - *setup = true; - // Find an approximate bounding box of the scene from its meshes - if meshes.iter().any(|(_, maybe_aabb)| maybe_aabb.is_none()) { - return; - } - - let mut min = Vec3A::splat(f32::MAX); - let mut max = Vec3A::splat(f32::MIN); - for (transform, maybe_aabb) in &meshes { - let aabb = maybe_aabb.unwrap(); - // If the Aabb had not been rotated, applying the non-uniform scale would produce the - // correct bounds. However, it could very well be rotated and so we first convert to - // a Sphere, and then back to an Aabb to find the conservative min and max points. - let sphere = Sphere { - center: Vec3A::from(transform.transform_point(Vec3::from(aabb.center))), - radius: transform.radius_vec3a(aabb.half_extents), - }; - let aabb = Aabb::from(sphere); - min = min.min(aabb.min()); - max = max.max(aabb.max()); - } - - let size = (max - min).length(); - let aabb = Aabb::from_min_max(Vec3::from(min), Vec3::from(max)); - - info!("Spawning a controllable 3D perspective camera"); - let mut projection = PerspectiveProjection::default(); - projection.far = projection.far.max(size * 10.0); - commands.spawn(( - Camera3dBundle { - projection: projection.into(), - transform: Transform::from_translation( - Vec3::from(aabb.center) + size * Vec3::new(0.5, 0.25, 0.5), - ) - .looking_at(Vec3::from(aabb.center), Vec3::Y), - camera: Camera { - is_active: false, - ..default() - }, - ..default() - }, - CameraController::default(), - )); - - // Spawn a default light if the scene does not have one - if !scene_handle.has_light { - let sphere = Sphere { - center: aabb.center, - radius: aabb.half_extents.length(), - }; - let aabb = Aabb::from(sphere); - let min = aabb.min(); - let max = aabb.max(); - - info!("Spawning a directional light"); - commands.spawn(DirectionalLightBundle { - directional_light: DirectionalLight { - shadow_projection: OrthographicProjection { - left: min.x, - right: max.x, - bottom: min.y, - top: max.y, - near: min.z, - far: max.z, - ..default() - }, - shadows_enabled: false, - ..default() - }, - ..default() - }); - - scene_handle.has_light = true; - } - } -} - -const SCALE_STEP: f32 = 0.1; - -fn update_lights( - key_input: Res>, - time: Res