//! This example showcases a 3D first-person camera. //! //! The setup presented here is a very common way of organizing a first-person game //! where the player can see their own arms. We use two industry terms to differentiate //! the kinds of models we have: //! //! - The *view model* is the model that represents the player's body. //! - The *world model* is everything else. //! //! ## Motivation //! //! The reason for this distinction is that these two models should be rendered with different field of views (FOV). //! The view model is typically designed and animated with a very specific FOV in mind, so it is //! generally *fixed* and cannot be changed by a player. The world model, on the other hand, should //! be able to change its FOV to accommodate the player's preferences for the following reasons: //! - *Accessibility*: How prone is the player to motion sickness? A wider FOV can help. //! - *Tactical preference*: Does the player want to see more of the battlefield? //! Or have a more zoomed-in view for precision aiming? //! - *Physical considerations*: How well does the in-game FOV match the player's real-world FOV? //! Are they sitting in front of a monitor or playing on a TV in the living room? How big is the screen? //! //! ## Implementation //! //! The `Player` is an entity holding two cameras, one for each model. The view model camera has a fixed //! FOV of 70 degrees, while the world model camera has a variable FOV that can be changed by the player. //! //! We use different `RenderLayers` to select what to render. //! //! - The world model camera has no explicit `RenderLayers` component, so it uses the layer 0. //! All static objects in the scene are also on layer 0 for the same reason. //! - The view model camera has a `RenderLayers` component with layer 1, so it only renders objects //! explicitly assigned to layer 1. The arm of the player is one such object. //! The order of the view model camera is additionally bumped to 1 to ensure it renders on top of the world model. //! - The light source in the scene must illuminate both the view model and the world model, so it is //! assigned to both layers 0 and 1. //! //! ## Controls //! //! | Key Binding | Action | //! |:---------------------|:--------------| //! | mouse | Look around | //! | arrow up | Decrease FOV | //! | arrow down | Increase FOV | use bevy::color::palettes::tailwind; use bevy::input::mouse::AccumulatedMouseMotion; use bevy::pbr::NotShadowCaster; use bevy::prelude::*; use bevy::render::view::RenderLayers; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems( Startup, ( spawn_view_model, spawn_world_model, spawn_lights, spawn_text, ), ) .add_systems(Update, (move_player, change_fov)) .run(); } #[derive(Debug, Component)] struct Player; #[derive(Debug, Component)] struct WorldModelCamera; /// Used implicitly by all entities without a `RenderLayers` component. /// Our world model camera and all objects other than the player are on this layer. /// The light source belongs to both layers. const DEFAULT_RENDER_LAYER: usize = 0; /// Used by the view model camera and the player's arm. /// The light source belongs to both layers. const VIEW_MODEL_RENDER_LAYER: usize = 1; fn spawn_view_model( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, ) { let arm = meshes.add(Cuboid::new(0.1, 0.1, 0.5)); let arm_material = materials.add(Color::from(tailwind::TEAL_200)); commands .spawn(( Player, SpatialBundle { transform: Transform::from_xyz(0.0, 1.0, 0.0), ..default() }, )) .with_children(|parent| { parent.spawn(( WorldModelCamera, Camera3dBundle { projection: PerspectiveProjection { fov: 90.0_f32.to_radians(), ..default() } .into(), ..default() }, )); // Spawn view model camera. parent.spawn(( Camera3dBundle { camera: Camera { // Bump the order to render on top of the world model. order: 1, ..default() }, projection: PerspectiveProjection { fov: 70.0_f32.to_radians(), ..default() } .into(), ..default() }, // Only render objects belonging to the view model. RenderLayers::layer(VIEW_MODEL_RENDER_LAYER), )); // Spawn the player's right arm. parent.spawn(( MaterialMeshBundle { mesh: arm, material: arm_material, transform: Transform::from_xyz(0.2, -0.1, -0.25), ..default() }, // Ensure the arm is only rendered by the view model camera. RenderLayers::layer(VIEW_MODEL_RENDER_LAYER), // The arm is free-floating, so shadows would look weird. NotShadowCaster, )); }); } fn spawn_world_model( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, ) { let floor = meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(10.0))); let cube = meshes.add(Cuboid::new(2.0, 0.5, 1.0)); let material = materials.add(Color::WHITE); // The world model camera will render the floor and the cubes spawned in this system. // Assigning no `RenderLayers` component defaults to layer 0. commands.spawn(MaterialMeshBundle { mesh: floor, material: material.clone(), ..default() }); commands.spawn(MaterialMeshBundle { mesh: cube.clone(), material: material.clone(), transform: Transform::from_xyz(0.0, 0.25, -3.0), ..default() }); commands.spawn(MaterialMeshBundle { mesh: cube, material, transform: Transform::from_xyz(0.75, 1.75, 0.0), ..default() }); } fn spawn_lights(mut commands: Commands) { commands.spawn(( PointLightBundle { point_light: PointLight { color: Color::from(tailwind::ROSE_300), shadows_enabled: true, ..default() }, transform: Transform::from_xyz(-2.0, 4.0, -0.75), ..default() }, // The light source illuminates both the world model and the view model. RenderLayers::from_layers(&[DEFAULT_RENDER_LAYER, VIEW_MODEL_RENDER_LAYER]), )); } fn spawn_text(mut commands: Commands) { commands .spawn(NodeBundle { style: Style { position_type: PositionType::Absolute, bottom: Val::Px(12.0), left: Val::Px(12.0), ..default() }, ..default() }) .with_children(|parent| { parent.spawn(TextBundle::from_section( concat!( "Move the camera with your mouse.\n", "Press arrow up to decrease the FOV of the world model.\n", "Press arrow down to increase the FOV of the world model." ), TextStyle { font_size: 25.0, ..default() }, )); }); } fn move_player( accumulated_mouse_motion: Res, mut player: Query<&mut Transform, With>, ) { let mut transform = player.single_mut(); let delta = accumulated_mouse_motion.delta; if delta != Vec2::ZERO { let yaw = -delta.x * 0.003; let pitch = -delta.y * 0.002; // Order of rotations is important, see transform.rotate_y(yaw); transform.rotate_local_x(pitch); } } fn change_fov( input: Res>, mut world_model_projection: Query<&mut Projection, With>, ) { let mut projection = world_model_projection.single_mut(); let Projection::Perspective(ref mut perspective) = projection.as_mut() else { unreachable!( "The `Projection` component was explicitly built with `Projection::Perspective`" ); }; if input.pressed(KeyCode::ArrowUp) { perspective.fov -= 1.0_f32.to_radians(); perspective.fov = perspective.fov.max(20.0_f32.to_radians()); } if input.pressed(KeyCode::ArrowDown) { perspective.fov += 1.0_f32.to_radians(); perspective.fov = perspective.fov.min(160.0_f32.to_radians()); } }