mirror of
https://github.com/bevyengine/bevy
synced 2024-11-22 04:33:37 +00:00
Add first person view model example (#13828)
# Objective A very common way to organize a first-person view is to split it into two kinds of models: - The *view model* is the model that represents the player's body. - The *world model* is everything else. The reason for this distinction is that these two models should be rendered with different FOVs. 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? ## Solution I've added an example implementing the described setup as follows. 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. I 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. To better see the effect, the player can move the camera by dragging their mouse and change the world model's FOV with the arrow keys. The arrow up key maps to "decrease FOV" and the arrow down key maps to "increase FOV". This sounds backwards on paper, but is more intuitive when actually changing the FOV in-game since a decrease in FOV looks like a zoom-in. I intentionally do not allow changing the view model's FOV even though it would be illustrative because that would be an anti-pattern and bloat the code a bit. The example is called `first_person_view_model` and not just `first_person` because I want to highlight that this is not a simple flycam, but actually renders the player. ## Testing Default FOV: <img width="1392" alt="image" src="https://github.com/bevyengine/bevy/assets/9047632/8c2e804f-fac2-48c7-8a22-d85af999dfb2"> Decreased FOV: <img width="1392" alt="image" src="https://github.com/bevyengine/bevy/assets/9047632/1733b3e5-f583-4214-a454-3554e3cbd066"> Increased FOV: <img width="1392" alt="image" src="https://github.com/bevyengine/bevy/assets/9047632/0b0640e6-5743-46f6-a79a-7181ba9678e8"> Note that the white bar on the right represents the player's arm, which is more obvious in-game because you can move the camera around. The box on top is there to make sure that the view model is receiving shadows. I tested only on macOS. --- ## Changelog I don't think new examples go in here, do they? ## Caveat The solution used here was implemented with help by @robtfm on [Discord](https://discord.com/channels/691052431525675048/866787577687310356/1241019224491561000): > shadow maps are specific to lights, not to layers > if you want shadows from some meshes that are not visible, you could have light on layer 1+2, meshes on layer 2, camera on layer 1 (for example) > but this might change in future, it's not exactly an intended feature In other words, the example code as-is is not guaranteed to work in the future. I want to bring this up because the use-case presented here is extremely common in first-person games and important for accessibility. It would be good to have a blessed and easy way of how to achieve it. I'm also not happy about how I get the `perspective` variable in `change_fov`. Very open to suggestions :) ## Related issues - Addresses parts of #12658 - Addresses parts of #12588 --------- Co-authored-by: Pascal Hertleif <killercup@gmail.com>
This commit is contained in:
parent
16e02e1889
commit
48f70789f5
3 changed files with 266 additions and 0 deletions
11
Cargo.toml
11
Cargo.toml
|
@ -3134,6 +3134,17 @@ description = "A 2D top-down camera smoothly following player movements"
|
|||
category = "Camera"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "first_person_view_model"
|
||||
path = "examples/camera/first_person_view_model.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.first_person_view_model]
|
||||
name = "First person view model"
|
||||
description = "A first-person camera that uses a world model and a view model with different field of views (FOV)"
|
||||
category = "Camera"
|
||||
wasm = true
|
||||
|
||||
[package.metadata.example.fps_overlay]
|
||||
name = "FPS overlay"
|
||||
description = "Demonstrates FPS overlay"
|
||||
|
|
|
@ -246,6 +246,7 @@ Example | Description
|
|||
Example | Description
|
||||
--- | ---
|
||||
[2D top-down camera](../examples/camera/2d_top_down_camera.rs) | A 2D top-down camera smoothly following player movements
|
||||
[First person view model](../examples/camera/first_person_view_model.rs) | A first-person camera that uses a world model and a view model with different field of views (FOV)
|
||||
|
||||
## Dev tools
|
||||
|
||||
|
|
254
examples/camera/first_person_view_model.rs
Normal file
254
examples/camera/first_person_view_model.rs
Normal file
|
@ -0,0 +1,254 @@
|
|||
//! 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::MouseMotion;
|
||||
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<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
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<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
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(
|
||||
mut mouse_motion: EventReader<MouseMotion>,
|
||||
mut player: Query<&mut Transform, With<Player>>,
|
||||
) {
|
||||
let mut transform = player.single_mut();
|
||||
for motion in mouse_motion.read() {
|
||||
let yaw = -motion.delta.x * 0.003;
|
||||
let pitch = -motion.delta.y * 0.002;
|
||||
// Order of rotations is important, see <https://gamedev.stackexchange.com/a/136175/103059>
|
||||
transform.rotate_y(yaw);
|
||||
transform.rotate_local_x(pitch);
|
||||
}
|
||||
}
|
||||
|
||||
fn change_fov(
|
||||
input: Res<ButtonInput<KeyCode>>,
|
||||
mut world_model_projection: Query<&mut Projection, With<WorldModelCamera>>,
|
||||
) {
|
||||
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());
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue