Path: blob/main/examples/camera/first_person_view_model.rs
6592 views
//! This example showcases a 3D first-person camera.1//!2//! The setup presented here is a very common way of organizing a first-person game3//! where the player can see their own arms. We use two industry terms to differentiate4//! the kinds of models we have:5//!6//! - The *view model* is the model that represents the player's body.7//! - The *world model* is everything else.8//!9//! ## Motivation10//!11//! The reason for this distinction is that these two models should be rendered with different field of views (FOV).12//! The view model is typically designed and animated with a very specific FOV in mind, so it is13//! generally *fixed* and cannot be changed by a player. The world model, on the other hand, should14//! be able to change its FOV to accommodate the player's preferences for the following reasons:15//! - *Accessibility*: How prone is the player to motion sickness? A wider FOV can help.16//! - *Tactical preference*: Does the player want to see more of the battlefield?17//! Or have a more zoomed-in view for precision aiming?18//! - *Physical considerations*: How well does the in-game FOV match the player's real-world FOV?19//! Are they sitting in front of a monitor or playing on a TV in the living room? How big is the screen?20//!21//! ## Implementation22//!23//! The `Player` is an entity holding two cameras, one for each model. The view model camera has a fixed24//! FOV of 70 degrees, while the world model camera has a variable FOV that can be changed by the player.25//!26//! We use different `RenderLayers` to select what to render.27//!28//! - The world model camera has no explicit `RenderLayers` component, so it uses the layer 0.29//! All static objects in the scene are also on layer 0 for the same reason.30//! - The view model camera has a `RenderLayers` component with layer 1, so it only renders objects31//! explicitly assigned to layer 1. The arm of the player is one such object.32//! The order of the view model camera is additionally bumped to 1 to ensure it renders on top of the world model.33//! - The light source in the scene must illuminate both the view model and the world model, so it is34//! assigned to both layers 0 and 1.35//!36//! ## Controls37//!38//! | Key Binding | Action |39//! |:---------------------|:--------------|40//! | mouse | Look around |41//! | arrow up | Decrease FOV |42//! | arrow down | Increase FOV |4344use std::f32::consts::FRAC_PI_2;4546use bevy::{47camera::visibility::RenderLayers, color::palettes::tailwind,48input::mouse::AccumulatedMouseMotion, light::NotShadowCaster, prelude::*,49};5051fn main() {52App::new()53.add_plugins(DefaultPlugins)54.add_systems(55Startup,56(57spawn_view_model,58spawn_world_model,59spawn_lights,60spawn_text,61),62)63.add_systems(Update, (move_player, change_fov))64.run();65}6667#[derive(Debug, Component)]68struct Player;6970#[derive(Debug, Component, Deref, DerefMut)]71struct CameraSensitivity(Vec2);7273impl Default for CameraSensitivity {74fn default() -> Self {75Self(76// These factors are just arbitrary mouse sensitivity values.77// It's often nicer to have a faster horizontal sensitivity than vertical.78// We use a component for them so that we can make them user-configurable at runtime79// for accessibility reasons.80// It also allows you to inspect them in an editor if you `Reflect` the component.81Vec2::new(0.003, 0.002),82)83}84}8586#[derive(Debug, Component)]87struct WorldModelCamera;8889/// Used implicitly by all entities without a `RenderLayers` component.90/// Our world model camera and all objects other than the player are on this layer.91/// The light source belongs to both layers.92const DEFAULT_RENDER_LAYER: usize = 0;9394/// Used by the view model camera and the player's arm.95/// The light source belongs to both layers.96const VIEW_MODEL_RENDER_LAYER: usize = 1;9798fn spawn_view_model(99mut commands: Commands,100mut meshes: ResMut<Assets<Mesh>>,101mut materials: ResMut<Assets<StandardMaterial>>,102) {103let arm = meshes.add(Cuboid::new(0.1, 0.1, 0.5));104let arm_material = materials.add(Color::from(tailwind::TEAL_200));105106commands.spawn((107Player,108CameraSensitivity::default(),109Transform::from_xyz(0.0, 1.0, 0.0),110Visibility::default(),111children![112(113WorldModelCamera,114Camera3d::default(),115Projection::from(PerspectiveProjection {116fov: 90.0_f32.to_radians(),117..default()118}),119),120// Spawn view model camera.121(122Camera3d::default(),123Camera {124// Bump the order to render on top of the world model.125order: 1,126..default()127},128Projection::from(PerspectiveProjection {129fov: 70.0_f32.to_radians(),130..default()131}),132// Only render objects belonging to the view model.133RenderLayers::layer(VIEW_MODEL_RENDER_LAYER),134),135// Spawn the player's right arm.136(137Mesh3d(arm),138MeshMaterial3d(arm_material),139Transform::from_xyz(0.2, -0.1, -0.25),140// Ensure the arm is only rendered by the view model camera.141RenderLayers::layer(VIEW_MODEL_RENDER_LAYER),142// The arm is free-floating, so shadows would look weird.143NotShadowCaster,144),145],146));147}148149fn spawn_world_model(150mut commands: Commands,151mut meshes: ResMut<Assets<Mesh>>,152mut materials: ResMut<Assets<StandardMaterial>>,153) {154let floor = meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(10.0)));155let cube = meshes.add(Cuboid::new(2.0, 0.5, 1.0));156let material = materials.add(Color::WHITE);157158// The world model camera will render the floor and the cubes spawned in this system.159// Assigning no `RenderLayers` component defaults to layer 0.160161commands.spawn((Mesh3d(floor), MeshMaterial3d(material.clone())));162163commands.spawn((164Mesh3d(cube.clone()),165MeshMaterial3d(material.clone()),166Transform::from_xyz(0.0, 0.25, -3.0),167));168169commands.spawn((170Mesh3d(cube),171MeshMaterial3d(material),172Transform::from_xyz(0.75, 1.75, 0.0),173));174}175176fn spawn_lights(mut commands: Commands) {177commands.spawn((178PointLight {179color: Color::from(tailwind::ROSE_300),180shadows_enabled: true,181..default()182},183Transform::from_xyz(-2.0, 4.0, -0.75),184// The light source illuminates both the world model and the view model.185RenderLayers::from_layers(&[DEFAULT_RENDER_LAYER, VIEW_MODEL_RENDER_LAYER]),186));187}188189fn spawn_text(mut commands: Commands) {190commands191.spawn(Node {192position_type: PositionType::Absolute,193bottom: px(12),194left: px(12),195..default()196})197.with_child(Text::new(concat!(198"Move the camera with your mouse.\n",199"Press arrow up to decrease the FOV of the world model.\n",200"Press arrow down to increase the FOV of the world model."201)));202}203204fn move_player(205accumulated_mouse_motion: Res<AccumulatedMouseMotion>,206player: Single<(&mut Transform, &CameraSensitivity), With<Player>>,207) {208let (mut transform, camera_sensitivity) = player.into_inner();209210let delta = accumulated_mouse_motion.delta;211212if delta != Vec2::ZERO {213// Note that we are not multiplying by delta_time here.214// The reason is that for mouse movement, we already get the full movement that happened since the last frame.215// This means that if we multiply by delta_time, we will get a smaller rotation than intended by the user.216// This situation is reversed when reading e.g. analog input from a gamepad however, where the same rules217// as for keyboard input apply. Such an input should be multiplied by delta_time to get the intended rotation218// independent of the framerate.219let delta_yaw = -delta.x * camera_sensitivity.x;220let delta_pitch = -delta.y * camera_sensitivity.y;221222let (yaw, pitch, roll) = transform.rotation.to_euler(EulerRot::YXZ);223let yaw = yaw + delta_yaw;224225// If the pitch was ±¹⁄₂ π, the camera would look straight up or down.226// When the user wants to move the camera back to the horizon, which way should the camera face?227// The camera has no way of knowing what direction was "forward" before landing in that extreme position,228// so the direction picked will for all intents and purposes be arbitrary.229// Another issue is that for mathematical reasons, the yaw will effectively be flipped when the pitch is at the extremes.230// To not run into these issues, we clamp the pitch to a safe range.231const PITCH_LIMIT: f32 = FRAC_PI_2 - 0.01;232let pitch = (pitch + delta_pitch).clamp(-PITCH_LIMIT, PITCH_LIMIT);233234transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);235}236}237238fn change_fov(239input: Res<ButtonInput<KeyCode>>,240mut world_model_projection: Single<&mut Projection, With<WorldModelCamera>>,241) {242let Projection::Perspective(perspective) = world_model_projection.as_mut() else {243unreachable!(244"The `Projection` component was explicitly built with `Projection::Perspective`"245);246};247248if input.pressed(KeyCode::ArrowUp) {249perspective.fov -= 1.0_f32.to_radians();250perspective.fov = perspective.fov.max(20.0_f32.to_radians());251}252if input.pressed(KeyCode::ArrowDown) {253perspective.fov += 1.0_f32.to_radians();254perspective.fov = perspective.fov.min(160.0_f32.to_radians());255}256}257258259