bevy/examples/tools/scene_viewer.rs
Carter Anderson 01aedc8431 Spawn now takes a Bundle (#6054)
# Objective

Now that we can consolidate Bundles and Components under a single insert (thanks to #2975 and #6039), almost 100% of world spawns now look like `world.spawn().insert((Some, Tuple, Here))`. Spawning an entity without any components is an extremely uncommon pattern, so it makes sense to give spawn the "first class" ergonomic api. This consolidated api should be made consistent across all spawn apis (such as World and Commands).

## Solution

All `spawn` apis (`World::spawn`, `Commands:;spawn`, `ChildBuilder::spawn`, and `WorldChildBuilder::spawn`) now accept a bundle as input:

```rust
// before:
commands
  .spawn()
  .insert((A, B, C));
world
  .spawn()
  .insert((A, B, C);

// after
commands.spawn((A, B, C));
world.spawn((A, B, C));
```

All existing instances of `spawn_bundle` have been deprecated in favor of the new `spawn` api. A new `spawn_empty` has been added, replacing the old `spawn` api.  

By allowing `world.spawn(some_bundle)` to replace `world.spawn().insert(some_bundle)`, this opened the door to removing the initial entity allocation in the "empty" archetype / table done in `spawn()` (and subsequent move to the actual archetype in `.insert(some_bundle)`).

This improves spawn performance by over 10%:
![image](https://user-images.githubusercontent.com/2694663/191627587-4ab2f949-4ccd-4231-80eb-80dd4d9ad6b9.png)

To take this measurement, I added a new `world_spawn` benchmark.

Unfortunately, optimizing `Commands::spawn` is slightly less trivial, as Commands expose the Entity id of spawned entities prior to actually spawning. Doing the optimization would (naively) require assurances that the `spawn(some_bundle)` command is applied before all other commands involving the entity (which would not necessarily be true, if memory serves). Optimizing `Commands::spawn` this way does feel possible, but it will require careful thought (and maybe some additional checks), which deserves its own PR. For now, it has the same performance characteristics of the current `Commands::spawn_bundle` on main.

**Note that 99% of this PR is simple renames and refactors. The only code that needs careful scrutiny is the new `World::spawn()` impl, which is relatively straightforward, but it has some new unsafe code (which re-uses battle tested BundlerSpawner code path).** 

---

## Changelog

- All `spawn` apis (`World::spawn`, `Commands:;spawn`, `ChildBuilder::spawn`, and `WorldChildBuilder::spawn`) now accept a bundle as input
- All instances of `spawn_bundle` have been deprecated in favor of the new `spawn` api
- World and Commands now have `spawn_empty()`, which is equivalent to the old `spawn()` behavior.  

## Migration Guide

```rust
// Old (0.8):
commands
  .spawn()
  .insert_bundle((A, B, C));
// New (0.9)
commands.spawn((A, B, C));

// Old (0.8):
commands.spawn_bundle((A, B, C));
// New (0.9)
commands.spawn((A, B, C));

// Old (0.8):
let entity = commands.spawn().id();
// New (0.9)
let entity = commands.spawn_empty().id();

// Old (0.8)
let entity = world.spawn().id();
// New (0.9)
let entity = world.spawn_empty();
```
2022-09-23 19:55:54 +00:00

537 lines
18 KiB
Rust

//! A simple glTF scene viewer made 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.
use bevy::{
asset::LoadState,
gltf::Gltf,
input::mouse::MouseMotion,
math::Vec3A,
prelude::*,
render::primitives::{Aabb, Sphere},
scene::InstanceId,
};
use std::f32::consts::PI;
#[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 cameras
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,
})
.insert_resource(AssetServerSettings {
asset_folder: std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string()),
watch_for_changes: true,
})
.insert_resource(WindowDescriptor {
title: "bevy scene viewer".to_string(),
..default()
})
.init_resource::<CameraTracker>()
.add_plugins(DefaultPlugins)
.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 {
handle: Handle<Gltf>,
#[cfg(feature = "animation")]
animations: Vec<Handle<AnimationClip>>,
instance_id: Option<InstanceId>,
is_loaded: bool,
has_light: bool,
}
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);
commands.insert_resource(SceneHandle {
handle: asset_server.load(&scene_path),
#[cfg(feature = "animation")]
animations: Vec::new(),
instance_id: None,
is_loaded: false,
has_light: false,
});
}
fn scene_load_check(
asset_server: Res<AssetServer>,
mut scenes: ResMut<Assets<Scene>>,
gltf_assets: ResMut<Assets<Gltf>>,
mut scene_handle: ResMut<SceneHandle>,
mut scene_spawner: ResMut<SceneSpawner>,
) {
match scene_handle.instance_id {
None => {
if asset_server.get_load_state(&scene_handle.handle) == LoadState::Loaded {
let gltf = gltf_assets.get(&scene_handle.handle).unwrap();
let gltf_scene_handle = gltf.scenes.first().expect("glTF file contains no scenes!");
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<bool>,
scene_handle: Res<SceneHandle>,
) {
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<Input<KeyCode>>,
mut animation_player: Query<&mut AnimationPlayer>,
scene_handle: Res<SceneHandle>,
mut current_animation: Local<usize>,
mut changing: Local<bool>,
) {
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<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.mul_vec3(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<Input<KeyCode>>,
time: Res<Time>,
mut query: Query<(&mut Transform, &mut DirectionalLight)>,
mut animate_directional_light: Local<bool>,
) {
let mut projection_adjustment = Vec3::ONE;
if key_input.just_pressed(KeyCode::Key5) {
projection_adjustment.x -= SCALE_STEP;
} else if key_input.just_pressed(KeyCode::Key6) {
projection_adjustment.x += SCALE_STEP;
} else if key_input.just_pressed(KeyCode::Key7) {
projection_adjustment.y -= SCALE_STEP;
} else if key_input.just_pressed(KeyCode::Key8) {
projection_adjustment.y += SCALE_STEP;
} else if key_input.just_pressed(KeyCode::Key9) {
projection_adjustment.z -= SCALE_STEP;
} else if key_input.just_pressed(KeyCode::Key0) {
projection_adjustment.z += SCALE_STEP;
}
for (_, mut light) in &mut query {
light.shadow_projection.left *= projection_adjustment.x;
light.shadow_projection.right *= projection_adjustment.x;
light.shadow_projection.bottom *= projection_adjustment.y;
light.shadow_projection.top *= projection_adjustment.y;
light.shadow_projection.near *= projection_adjustment.z;
light.shadow_projection.far *= projection_adjustment.z;
if key_input.just_pressed(KeyCode::U) {
light.shadows_enabled = !light.shadows_enabled;
}
}
if key_input.just_pressed(KeyCode::L) {
*animate_directional_light = !*animate_directional_light;
}
if *animate_directional_light {
for (mut transform, _) in &mut query {
transform.rotation = Quat::from_euler(
EulerRot::ZYX,
0.0,
time.seconds_since_startup() as f32 * PI / 15.0,
-PI / 4.,
);
}
}
}
#[derive(Resource, Default)]
struct CameraTracker {
active_index: Option<usize>,
cameras: Vec<Entity>,
}
impl CameraTracker {
fn track_camera(&mut self, entity: Entity) -> bool {
self.cameras.push(entity);
if self.active_index.is_none() {
self.active_index = Some(self.cameras.len() - 1);
true
} else {
false
}
}
fn active_camera(&self) -> Option<Entity> {
self.active_index.map(|i| self.cameras[i])
}
fn set_next_active(&mut self) -> Option<Entity> {
let active_index = self.active_index?;
let new_i = (active_index + 1) % self.cameras.len();
self.active_index = Some(new_i);
Some(self.cameras[new_i])
}
}
fn camera_tracker(
mut camera_tracker: ResMut<CameraTracker>,
keyboard_input: Res<Input<KeyCode>>,
mut queries: ParamSet<(
Query<(Entity, &mut Camera), (Added<Camera>, Without<CameraController>)>,
Query<(Entity, &mut Camera), (Added<Camera>, With<CameraController>)>,
Query<&mut Camera>,
)>,
) {
// track added scene camera entities first, to ensure they are preferred for the
// default active camera
for (entity, mut camera) in queries.p0().iter_mut() {
camera.is_active = camera_tracker.track_camera(entity);
}
// iterate added custom camera entities second
for (entity, mut camera) in queries.p1().iter_mut() {
camera.is_active = camera_tracker.track_camera(entity);
}
if keyboard_input.just_pressed(KeyCode::C) {
// disable currently active camera
if let Some(e) = camera_tracker.active_camera() {
if let Ok(mut camera) = queries.p2().get_mut(e) {
camera.is_active = false;
}
}
// enable next active camera
if let Some(e) = camera_tracker.set_next_active() {
if let Ok(mut camera) = queries.p2().get_mut(e) {
camera.is_active = true;
}
}
}
}
#[derive(Component)]
struct CameraController {
pub enabled: bool,
pub initialized: bool,
pub sensitivity: f32,
pub key_forward: KeyCode,
pub key_back: KeyCode,
pub key_left: KeyCode,
pub key_right: KeyCode,
pub key_up: KeyCode,
pub key_down: KeyCode,
pub key_run: KeyCode,
pub mouse_key_enable_mouse: MouseButton,
pub keyboard_key_enable_mouse: KeyCode,
pub walk_speed: f32,
pub run_speed: f32,
pub friction: f32,
pub pitch: f32,
pub yaw: f32,
pub velocity: Vec3,
}
impl Default for CameraController {
fn default() -> Self {
Self {
enabled: true,
initialized: false,
sensitivity: 0.5,
key_forward: KeyCode::W,
key_back: KeyCode::S,
key_left: KeyCode::A,
key_right: KeyCode::D,
key_up: KeyCode::E,
key_down: KeyCode::Q,
key_run: KeyCode::LShift,
mouse_key_enable_mouse: MouseButton::Left,
keyboard_key_enable_mouse: KeyCode::M,
walk_speed: 5.0,
run_speed: 15.0,
friction: 0.5,
pitch: 0.0,
yaw: 0.0,
velocity: Vec3::ZERO,
}
}
}
fn camera_controller(
time: Res<Time>,
mut mouse_events: EventReader<MouseMotion>,
mouse_button_input: Res<Input<MouseButton>>,
key_input: Res<Input<KeyCode>>,
mut move_toggled: Local<bool>,
mut query: Query<(&mut Transform, &mut CameraController), With<Camera>>,
) {
let dt = time.delta_seconds();
if let Ok((mut transform, mut options)) = query.get_single_mut() {
if !options.initialized {
let (yaw, pitch, _roll) = transform.rotation.to_euler(EulerRot::YXZ);
options.yaw = yaw;
options.pitch = pitch;
options.initialized = true;
}
if !options.enabled {
return;
}
// Handle key input
let mut axis_input = Vec3::ZERO;
if key_input.pressed(options.key_forward) {
axis_input.z += 1.0;
}
if key_input.pressed(options.key_back) {
axis_input.z -= 1.0;
}
if key_input.pressed(options.key_right) {
axis_input.x += 1.0;
}
if key_input.pressed(options.key_left) {
axis_input.x -= 1.0;
}
if key_input.pressed(options.key_up) {
axis_input.y += 1.0;
}
if key_input.pressed(options.key_down) {
axis_input.y -= 1.0;
}
if key_input.just_pressed(options.keyboard_key_enable_mouse) {
*move_toggled = !*move_toggled;
}
// Apply movement update
if axis_input != Vec3::ZERO {
let max_speed = if key_input.pressed(options.key_run) {
options.run_speed
} else {
options.walk_speed
};
options.velocity = axis_input.normalize() * max_speed;
} else {
let friction = options.friction.clamp(0.0, 1.0);
options.velocity *= 1.0 - friction;
if options.velocity.length_squared() < 1e-6 {
options.velocity = Vec3::ZERO;
}
}
let forward = transform.forward();
let right = transform.right();
transform.translation += options.velocity.x * dt * right
+ options.velocity.y * dt * Vec3::Y
+ options.velocity.z * dt * forward;
// Handle mouse input
let mut mouse_delta = Vec2::ZERO;
if mouse_button_input.pressed(options.mouse_key_enable_mouse) || *move_toggled {
for mouse_event in mouse_events.iter() {
mouse_delta += mouse_event.delta;
}
}
if mouse_delta != Vec2::ZERO {
// Apply look update
options.pitch = (options.pitch - mouse_delta.y * 0.5 * options.sensitivity * dt)
.clamp(-PI / 2., PI / 2.);
options.yaw -= mouse_delta.x * options.sensitivity * dt;
transform.rotation = Quat::from_euler(EulerRot::ZYX, 0.0, options.yaw, options.pitch);
}
}
}