
# Objective Fix https://github.com/bevyengine/bevy/issues/4530 - Make it easier to open/close/modify windows by setting them up as `Entity`s with a `Window` component. - Make multiple windows very simple to set up. (just add a `Window` component to an entity and it should open) ## Solution - Move all properties of window descriptor to ~components~ a component. - Replace `WindowId` with `Entity`. - ~Use change detection for components to update backend rather than events/commands. (The `CursorMoved`/`WindowResized`/... events are kept for user convenience.~ Check each field individually to see what we need to update, events are still kept for user convenience. --- ## Changelog - `WindowDescriptor` renamed to `Window`. - Width/height consolidated into a `WindowResolution` component. - Requesting maximization/minimization is done on the [`Window::state`] field. - `WindowId` is now `Entity`. ## Migration Guide - Replace `WindowDescriptor` with `Window`. - Change `width` and `height` fields in a `WindowResolution`, either by doing ```rust WindowResolution::new(width, height) // Explicitly // or using From<_> for tuples for convenience (1920., 1080.).into() ``` - Replace any `WindowCommand` code to just modify the `Window`'s fields directly and creating/closing windows is now by spawning/despawning an entity with a `Window` component like so: ```rust let window = commands.spawn(Window { ... }).id(); // open window commands.entity(window).despawn(); // close window ``` ## Unresolved - ~How do we tell when a window is minimized by a user?~ ~Currently using the `Resize(0, 0)` as an indicator of minimization.~ No longer attempting to tell given how finnicky this was across platforms, now the user can only request that a window be maximized/minimized. ## Future work - Move `exit_on_close` functionality out from windowing and into app(?) - https://github.com/bevyengine/bevy/issues/5621 - https://github.com/bevyengine/bevy/issues/7099 - https://github.com/bevyengine/bevy/issues/7098 Co-authored-by: Carter Anderson <mcanders1@gmail.com>
165 lines
5.6 KiB
Rust
165 lines
5.6 KiB
Rust
//! 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::{
|
|
math::Vec3A,
|
|
prelude::*,
|
|
render::primitives::{Aabb, Sphere},
|
|
window::WindowPlugin,
|
|
};
|
|
|
|
mod camera_controller_plugin;
|
|
mod scene_viewer_plugin;
|
|
|
|
use camera_controller_plugin::{CameraController, CameraControllerPlugin};
|
|
use scene_viewer_plugin::{SceneHandle, SceneViewerPlugin};
|
|
|
|
fn main() {
|
|
let mut app = App::new();
|
|
app.insert_resource(AmbientLight {
|
|
color: Color::WHITE,
|
|
brightness: 1.0 / 5.0f32,
|
|
})
|
|
.add_plugins(
|
|
DefaultPlugins
|
|
.set(WindowPlugin {
|
|
primary_window: Some(Window {
|
|
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_plugin(CameraControllerPlugin)
|
|
.add_plugin(SceneViewerPlugin)
|
|
.add_startup_system(setup)
|
|
.add_system_to_stage(CoreStage::PreUpdate, setup_scene_after_load);
|
|
|
|
app.run();
|
|
}
|
|
|
|
fn parse_scene(scene_path: String) -> (String, usize) {
|
|
if scene_path.contains('#') {
|
|
let gltf_and_scene = scene_path.split('#').collect::<Vec<_>>();
|
|
if let Some((last, path)) = gltf_and_scene.split_last() {
|
|
if let Some(index) = last
|
|
.strip_prefix("Scene")
|
|
.and_then(|index| index.parse::<usize>().ok())
|
|
{
|
|
return (path.join("#"), index);
|
|
}
|
|
}
|
|
}
|
|
(scene_path, 0)
|
|
}
|
|
|
|
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|
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::new(asset_server.load(file_path), scene_index));
|
|
}
|
|
|
|
fn setup_scene_after_load(
|
|
mut commands: Commands,
|
|
mut setup: Local<bool>,
|
|
mut scene_handle: ResMut<SceneHandle>,
|
|
meshes: Query<(&GlobalTransform, Option<&Aabb>), With<Handle<Mesh>>>,
|
|
) {
|
|
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);
|
|
|
|
let camera_controller = CameraController::default();
|
|
|
|
// Display the controls of the scene viewer
|
|
info!("{}", camera_controller);
|
|
info!("{}", *scene_handle);
|
|
|
|
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()
|
|
},
|
|
camera_controller,
|
|
));
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
}
|