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