diff --git a/Cargo.toml b/Cargo.toml index e03574f666edf..cc7d80db0629d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -507,6 +507,10 @@ path = "examples/shader/compute_shader_game_of_life.rs" name = "bevymark" path = "examples/tools/bevymark.rs" +[[example]] +name = "scene_viewer" +path = "examples/tools/scene_viewer.rs" + # Transforms [[example]] name = "global_vs_local_translation" diff --git a/examples/README.md b/examples/README.md index 9f2e01ae800e9..805f058021b53 100644 --- a/examples/README.md +++ b/examples/README.md @@ -247,6 +247,7 @@ Example | File | Description Example | File | Description --- | --- | --- `bevymark` | [`tools/bevymark.rs`](./tools/bevymark.rs) | A heavy sprite rendering workload to benchmark your system with Bevy +`scene_viewer` | [`tools/scene_viewer.rs`](./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. ## Transforms diff --git a/examples/tools/scene_viewer.rs b/examples/tools/scene_viewer.rs new file mode 100644 index 0000000000000..b0e43bd4dd7e1 --- /dev/null +++ b/examples/tools/scene_viewer.rs @@ -0,0 +1,411 @@ +use bevy::{ + asset::{AssetServerSettings, LoadState}, + input::mouse::MouseMotion, + math::Vec3A, + prelude::*, + render::{ + camera::{Camera2d, Camera3d, CameraProjection}, + primitives::{Aabb, Frustum, Sphere}, + }, + scene::InstanceId, +}; + +#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] +struct CameraControllerCheckSystem; + +fn main() { + println!( + " +Controls: + WSAD - forward/back/strafe left/right + LShift - 'run' + E - up + Q - down + L - animate light direction + U - toggle shadows + 5/6 - decrease/increase shadow projection width + 7/8 - decrease/increase shadow projection height + 9/0 - decrease/increase shadow projection near/far +" + ); + App::new() + .insert_resource(AmbientLight { + color: Color::WHITE, + brightness: 1.0 / 5.0f32, + }) + .insert_resource(AssetServerSettings { + asset_folder: std::env::var("CARGO_MANIFEST_DIR").unwrap(), + watch_for_changes: true, + }) + .insert_resource(WindowDescriptor { + title: "bevy scene viewer".to_string(), + ..default() + }) + .add_plugins(DefaultPlugins) + .add_startup_system(setup) + .add_system_to_stage(CoreStage::PreUpdate, scene_load_check) + .add_system_to_stage(CoreStage::PreUpdate, camera_spawn_check) + .add_system(camera_controller_check.label(CameraControllerCheckSystem)) + .add_system(update_lights) + .add_system(camera_controller.after(CameraControllerCheckSystem)) + .run(); +} + +struct SceneHandle { + handle: Handle, + instance_id: Option, + is_loaded: bool, + has_camera: bool, + has_light: bool, +} + +fn setup(mut commands: Commands, asset_server: Res) { + let scene_path = std::env::args().nth(1).map_or_else( + || "assets/models/FlightHelmet/FlightHelmet.gltf#Scene0".to_string(), + |s| { + if let Some(index) = s.find("#Scene") { + if index + 6 < s.len() && s[index + 6..].chars().all(char::is_numeric) { + return s; + } + return format!("{}#Scene0", &s[..index]); + } + format!("{}#Scene0", s) + }, + ); + info!("Loading {}", scene_path); + commands.insert_resource(SceneHandle { + handle: asset_server.load(&scene_path), + instance_id: None, + is_loaded: false, + has_camera: false, + has_light: false, + }); +} + +fn scene_load_check( + asset_server: Res, + mut scenes: ResMut>, + mut scene_handle: ResMut, + mut scene_spawner: ResMut, +) { + match scene_handle.instance_id { + None if asset_server.get_load_state(&scene_handle.handle) == LoadState::Loaded => { + if let Some(scene) = scenes.get_mut(&scene_handle.handle) { + let mut query = scene + .world + .query::<(Option<&Camera2d>, Option<&Camera3d>)>(); + scene_handle.has_camera = + query + .iter(&scene.world) + .any(|(maybe_camera2d, maybe_camera3d)| { + maybe_camera2d.is_some() || maybe_camera3d.is_some() + }); + 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(scene_handle.handle.clone_weak())); + 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; + } + } + _ => {} + } +} + +fn camera_spawn_check( + mut commands: Commands, + mut scene_handle: ResMut, + meshes: Query<(&GlobalTransform, Option<&Aabb>), With>>, +) { + // If the scene did not contain a camera, find an approximate bounding box of the scene from + // its meshes and spawn a camera that fits it in view + if scene_handle.is_loaded && (!scene_handle.has_camera || !scene_handle.has_light) { + 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.iter() { + 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.mul_vec3(Vec3::from(aabb.center))), + radius: (Vec3A::from(transform.scale) * aabb.half_extents).length(), + }; + 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)); + + if !scene_handle.has_camera { + let 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); + let view = transform.compute_matrix(); + let mut perspective_projection = PerspectiveProjection::default(); + perspective_projection.far = perspective_projection.far.max(size * 10.0); + let view_projection = view.inverse() * perspective_projection.get_projection_matrix(); + let frustum = Frustum::from_view_projection( + &view_projection, + &transform.translation, + &transform.back(), + perspective_projection.far(), + ); + + info!("Spawning a 3D perspective camera"); + commands.spawn_bundle(PerspectiveCameraBundle { + camera: Camera { + near: perspective_projection.near, + far: perspective_projection.far, + ..default() + }, + perspective_projection, + frustum, + transform, + ..PerspectiveCameraBundle::new_3d() + }); + + scene_handle.has_camera = true; + } + + if !scene_handle.has_light { + // The same approach as above but now for the scene + 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_bundle(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; + } + } +} + +fn camera_controller_check( + mut commands: Commands, + camera: Query, Without)>, + mut found_camera: Local, +) { + if *found_camera { + return; + } + if let Some(entity) = camera.iter().next() { + commands.entity(entity).insert(CameraController::default()); + *found_camera = true; + } +} + +const SCALE_STEP: f32 = 0.1; + +fn update_lights( + key_input: Res>, + time: Res