scene_viewer: load cameras (#4425)

# Objective

glTF files can contain cameras. Currently the scene viewer example uses _a_ camera defined in the file if possible, otherwise it spawns a new one. It would be nice if instead it could load all the cameras and cycle through them, while also having a separate user-controller camera.

## Solution

- instead of just a camera that is already defined, always spawn a new separate user-controller camera
- maintain a list of loaded cameras and cycle through them (wrapping to the user-controller camera) when pressing `C`

This matches the behavious that https://github.khronos.org/glTF-Sample-Viewer-Release/ has.

## Implementation notes

- The gltf scene asset loader just spawns the cameras into the world, but does not return a mapping of camera index to bevy entity. So instead the scene_viewer example just collects all spawned cameras with a good old `query.iter().collect()`, so the order is unspecified and may change between runs.

## Demo

https://user-images.githubusercontent.com/22177966/161826637-40161482-5b3b-4df5-aae8-1d5e9b918393.mp4


using the virtual city glTF sample file: https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/VC

Co-authored-by: Jakob Hellermann <hellermann@sipgate.de>
This commit is contained in:
Jakob Hellermann 2022-04-11 22:56:06 +00:00
parent 612a5ba3a9
commit 193e8c4ada
2 changed files with 88 additions and 52 deletions

View file

@ -28,7 +28,7 @@ use bevy_window::{WindowCreated, WindowId, WindowResized, Windows};
use serde::{Deserialize, Serialize};
use wgpu::Extent3d;
#[derive(Component, Default, Debug, Reflect)]
#[derive(Component, Default, Debug, Reflect, Clone)]
#[reflect(Component)]
pub struct Camera {
pub projection_matrix: Mat4,

View file

@ -5,7 +5,7 @@ use bevy::{
math::Vec3A,
prelude::*,
render::{
camera::{Camera2d, Camera3d, CameraProjection},
camera::{ActiveCamera, Camera3d, CameraProjection},
primitives::{Aabb, Frustum, Sphere},
},
scene::InstanceId,
@ -26,6 +26,7 @@ Controls:
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
@ -51,11 +52,11 @@ Controls:
.add_startup_system(setup)
.add_system_to_stage(CoreStage::PreUpdate, scene_load_check)
.add_system_to_stage(CoreStage::PreUpdate, camera_spawn_check)
.add_system(check_camera_controller)
.add_system(update_lights)
.add_system(camera_controller.after(check_camera_controller))
.add_system(camera_controller)
.add_system(start_animation)
.add_system(keyboard_animation_control)
.add_system(keyboard_cameras_control)
.run();
}
@ -97,15 +98,6 @@ fn scene_load_check(
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<&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>)>();
@ -197,10 +189,50 @@ fn keyboard_animation_control(
}
}
fn keyboard_cameras_control(
mut cameras: Local<(bool, Vec<Entity>)>,
mut active_camera: Local<Option<usize>>, // `None` means user-controlled camera
scene_handle: Res<SceneHandle>,
keyboard_input: Res<Input<KeyCode>>,
camera_query: Query<Entity, (With<Camera3d>, Without<CameraController>)>,
user_camera_query: Query<Entity, (With<Camera3d>, With<CameraController>)>,
mut active_camera_3d: ResMut<ActiveCamera<Camera3d>>,
) {
if !scene_handle.is_loaded {
return;
}
if !cameras.0 {
cameras.1 = camera_query.iter().collect();
cameras.0 = true;
}
if keyboard_input.just_pressed(KeyCode::C) && !cameras.1.is_empty() {
*active_camera = match *active_camera {
Some(index) if index + 1 == cameras.1.len() => None,
Some(index) => Some(index + 1),
None => Some(0),
};
match *active_camera {
Some(i) => {
info!("Using camera {i}");
active_camera_3d.set(cameras.1[i]);
}
None => {
info!("Using user-controller camera");
active_camera_3d.set(user_camera_query.single());
}
}
}
}
fn camera_spawn_check(
mut commands: Commands,
mut scene_handle: ResMut<SceneHandle>,
meshes: Query<(&GlobalTransform, Option<&Aabb>), With<Handle<Mesh>>>,
cameras_3d: Query<(&GlobalTransform, &Camera), With<Camera3d>>,
mut active_camera_3d: ResMut<ActiveCamera<Camera3d>>,
) {
// 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
@ -229,33 +261,51 @@ fn camera_spawn_check(
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 {
let bundle = if let Some((transform, camera)) = cameras_3d.iter().next() {
let mut transform: Transform = (*transform).into();
let (yaw, pitch, _) = transform.rotation.to_euler(EulerRot::YXZ);
transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, 0.0);
PerspectiveCameraBundle {
camera: camera.clone(),
transform,
..PerspectiveCameraBundle::new_3d()
}
} else {
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(),
);
let camera = Camera {
near: perspective_projection.near,
far: perspective_projection.far,
..default()
},
perspective_projection,
frustum,
transform,
..PerspectiveCameraBundle::new_3d()
});
};
PerspectiveCameraBundle {
camera,
perspective_projection,
frustum,
transform,
..PerspectiveCameraBundle::new_3d()
}
};
info!("Spawning a 3D perspective camera");
let entity = commands
.spawn_bundle(bundle)
.insert(CameraController::default())
.id();
active_camera_3d.set(entity);
scene_handle.has_camera = true;
}
@ -293,20 +343,6 @@ fn camera_spawn_check(
}
}
fn check_camera_controller(
mut commands: Commands,
camera: Query<Entity, (With<Camera>, Without<CameraController>)>,
mut found_camera: Local<bool>,
) {
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(
@ -412,7 +448,7 @@ fn camera_controller(
if let Ok((mut transform, mut options)) = query.get_single_mut() {
if !options.initialized {
let (_roll, yaw, pitch) = transform.rotation.to_euler(EulerRot::ZYX);
let (yaw, pitch, _roll) = transform.rotation.to_euler(EulerRot::YXZ);
options.yaw = yaw;
options.pitch = pitch;
options.initialized = true;