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