bones_framework/render/camera.rs
1//! Camera components.
2
3use std::collections::VecDeque;
4
5use crate::prelude::*;
6
7/// Makes an entity behave like a camera.
8///
9/// The entity must also have a [`Transform`] component for the camera to render anything.
10#[derive(Clone, Debug, HasSchema)]
11// TODO: make repr(C) when `Option`s are supported.
12// We don't have `Option` support in `bones_schema` right now.
13// Once we do, we can make this type `#[repr(C)]` instead of `#[schema(opaque)]`.
14#[repr(C)]
15pub struct Camera {
16 /// The height of the camera in in-game pixels.
17 ///
18 /// The width of the camera will be determined from the window aspect ratio.
19 pub size: CameraSize,
20 /// Whether or not the camera is enabled and rendering.
21 pub active: bool,
22 /// An optional viewport override, allowing you to specify that the camera should render to only
23 /// a portion of the window.
24 ///
25 /// This can be used, for example, for split screen functionality.
26 pub viewport: Maybe<Viewport>,
27 /// Cameras with a higher priority will be rendered on top of cameras with a lower priority.
28 pub priority: i32,
29}
30
31/// A size setting for a camera.
32#[derive(HasSchema, Debug, Clone, Copy)]
33#[repr(C, u8)]
34pub enum CameraSize {
35 /// The camera will capture exactly the `width` and `height` in world units,
36 /// stretching with the viewport.
37 Fixed {
38 /// The camera will capture exactly this width in world units.
39 width: f32,
40 /// The camera will capture exactly this height in world units.
41 height: f32,
42 },
43 /// The camera will be a fixed height with a width dependent on the aspect
44 /// ratio.
45 FixedHeight(f32),
46 /// The camera will be a fixed width with a height dependent on the aspect
47 /// ratio.
48 FixedWidth(f32),
49 /// The viewport will determine the camera size with the camera capturing
50 /// one world unit per the specified amount of viewport pixels.
51 Window {
52 /// The amount of viewport pixels that will cover one world unit. The
53 /// camera will capture one world unit per this amount of pixels.
54 pixels_per_unit: f32,
55 },
56 /// The camera will capture at least the specified height and width in world
57 /// units.
58 Min {
59 /// The camera will capture at least this many world units wide.
60 min_width: f32,
61 /// The camera will capture at least this many world units high.
62 min_height: f32,
63 },
64 /// The camera will capture at most the specified height and width in world
65 /// units.
66 Max {
67 /// The camera will capture at most this many world units wide.
68 max_width: f32,
69 /// The camera will capture at most this many world units high.
70 max_height: f32,
71 },
72}
73
74impl Default for CameraSize {
75 fn default() -> Self {
76 Self::FixedHeight(400.)
77 }
78}
79
80/// A custom viewport specification for a [`Camera`].
81#[derive(Clone, Copy, Debug, HasSchema, Default)]
82#[repr(C)]
83pub struct Viewport {
84 /// The physical position to render this viewport to within the RenderTarget of this Camera.
85 /// (0,0) corresponds to the top-left corner.
86 pub position: UVec2,
87 /// The physical size of the viewport rectangle to render to within the RenderTarget of this
88 /// Camera. The origin of the rectangle is in the top-left corner.
89 pub size: UVec2,
90 /// The minimum depth to render (on a scale from 0.0 to 1.0).
91 pub depth_min: f32,
92 /// The maximum depth to render (on a scale from 0.0 to 1.0).
93 pub depth_max: f32,
94}
95
96impl Default for Camera {
97 fn default() -> Self {
98 Self {
99 active: true,
100 viewport: Unset,
101 priority: 0,
102 size: default(),
103 }
104 }
105}
106
107/// Resource for controlling the clear color.
108#[derive(Deref, DerefMut, Clone, Copy, HasSchema, Default)]
109pub struct ClearColor(pub Color);
110
111/// Utility function that spawns the camera in a default position.
112///
113/// Camera will be spawned such that it is positioned at `0` on X and Y axis and at `1000` on the Z
114/// axis, allowing it to see sprites with a Z position from `0` to `1000` non-inclusive.
115pub fn spawn_default_camera(
116 entities: &mut Entities,
117 transforms: &mut CompMut<Transform>,
118 cameras: &mut CompMut<Camera>,
119) -> Entity {
120 let ent = entities.create();
121 cameras.insert(ent, default());
122 transforms.insert(ent, Transform::from_translation(Vec3::new(0., 0., 1000.)));
123 ent
124}
125
126/// Install the camera utilities on the given [`SystemStages`].
127pub fn plugin(session: &mut Session) {
128 session
129 .stages
130 .add_system_to_stage(CoreStage::Last, apply_shake)
131 .add_system_to_stage(CoreStage::Last, apply_trauma)
132 .add_system_to_stage(CoreStage::Last, decay_trauma);
133}
134
135/// Resource providing a noise source for [`CameraShake`] entities to use.
136#[derive(Clone, HasSchema)]
137pub struct ShakeNoise(pub noise::permutationtable::PermutationTable);
138
139impl Default for ShakeNoise {
140 fn default() -> Self {
141 Self(noise::permutationtable::PermutationTable::new(0))
142 }
143}
144
145/// Component for an entity with camera shake.
146#[derive(Clone, HasSchema, Debug, Copy)]
147#[repr(C)]
148pub struct CameraShake {
149 /// Value from 0-1 that indicates the intensity of the shake. Should usually be set with
150 /// `CameraShake::add_trauma` and not manually decayed.
151 pub trauma: f32,
152 /// The maximum offset angle in radians that the camera shake can cause.
153 pub max_angle_rad: f32,
154 /// The maximum offset position that the camera shake can cause.
155 pub max_offset: Vec2,
156 /// The the length of time in seconds for the camera trauma to decay from 1 to 0.
157 pub decay_rate: f32,
158 /// The speed that the screen is shook.
159 pub speed: f32,
160 /// The camera will always restore to this position.
161 pub center: Vec3,
162}
163
164impl Default for CameraShake {
165 fn default() -> Self {
166 Self {
167 trauma: 0.0,
168 max_angle_rad: 90.0,
169 max_offset: Vec2::splat(100.0),
170 decay_rate: 0.5,
171 speed: 1.5,
172 center: Vec3::ZERO,
173 }
174 }
175}
176
177impl CameraShake {
178 /// Create a new [`CameraShake`] component with the provided maximum offset angle (in degrees)
179 /// and position as well as the trauma decay rate in seconds.
180 pub fn new(max_angle_deg: f32, max_offset: Vec2, speed: f32, decay_rate: f32) -> Self {
181 Self {
182 max_angle_rad: max_angle_deg.to_radians(),
183 max_offset,
184 decay_rate,
185 speed,
186 ..default()
187 }
188 }
189
190 /// Create a new [`CameraShake`] component with the provided maximum offset angle (in degrees)
191 /// and position and its initial trauma set to some value (clamped between 0 and 1).
192 pub fn with_trauma(
193 trauma: f32,
194 max_angle_deg: f32,
195 max_offset: Vec2,
196 speed: f32,
197 decay_rate: f32,
198 ) -> Self {
199 let mut shake = Self::new(max_angle_deg, max_offset, speed, decay_rate);
200 shake.trauma = trauma.clamp(0.0, 1.0);
201 shake
202 }
203
204 /// Adds trauma to the camera, capping it at 1.0
205 pub fn add_trauma(&mut self, value: f32) {
206 self.trauma += value;
207 if 1.0 < self.trauma {
208 self.trauma = 1.0;
209 }
210 }
211}
212
213/// Queue that can be used to send camera trauma events.
214#[derive(Default, Clone, HasSchema)]
215pub struct CameraTraumaEvents {
216 /// The event queue.
217 pub queue: VecDeque<f32>,
218}
219
220impl CameraTraumaEvents {
221 /// Send a camera trauma event.
222 pub fn send(&mut self, trauma: f32) {
223 self.queue.push_back(trauma);
224 }
225}
226
227fn apply_trauma(
228 entities: Res<Entities>,
229 mut camera_shakes: CompMut<CameraShake>,
230 mut trauma_events: ResMutInit<CameraTraumaEvents>,
231) {
232 for (_ent, camera_shake) in entities.iter_with(&mut camera_shakes) {
233 camera_shake.add_trauma(
234 trauma_events
235 .queue
236 .iter()
237 .fold(0.0, |acc, trauma| acc + trauma),
238 );
239 }
240 trauma_events.queue.clear();
241}
242fn decay_trauma(entities: Res<Entities>, mut camera_shakes: CompMut<CameraShake>, time: Res<Time>) {
243 for (_ent, shake) in entities.iter_with(&mut camera_shakes) {
244 shake.trauma = 0.0f32.max(shake.trauma - shake.decay_rate * time.delta_seconds())
245 }
246}
247fn apply_shake(
248 entities: Res<Entities>,
249 mut transforms: CompMut<Transform>,
250 camera_shakes: Comp<CameraShake>,
251 time: Res<Time>,
252 noise: ResInit<ShakeNoise>,
253) {
254 macro_rules! offset_noise {
255 ($offset:expr, $shake_speed:expr) => {
256 perlin_noise::perlin_1d(
257 ((time.elapsed_seconds() + $offset) * $shake_speed * 0.001).into(),
258 &noise.0,
259 ) as f32
260 };
261 }
262
263 for (_ent, (shake, transform)) in entities.iter_with((&camera_shakes, &mut transforms)) {
264 (transform.rotation, transform.translation) = if shake.trauma > 0.0 {
265 let sqr_trauma = shake.trauma * shake.trauma;
266
267 let rotation = Quat::from_axis_angle(
268 Vec3::Z,
269 sqr_trauma * offset_noise!(0.0, shake.speed) * shake.max_angle_rad,
270 );
271
272 let x_offset = sqr_trauma * offset_noise!(1.0, shake.speed) * shake.max_offset.x;
273 let y_offset = sqr_trauma * offset_noise!(2.0, shake.speed) * shake.max_offset.y;
274
275 (rotation, shake.center + Vec3::new(x_offset, y_offset, 0.0))
276 } else {
277 // In future we may need to provide a rotation field on `CameraShake` should we need to
278 // rotate the camera in another context.
279 (Quat::IDENTITY, shake.center)
280 }
281 }
282}
283
284/// This module is copied from code from this commit:
285/// <https://github.com/Razaekel/noise-rs/commit/1a2b5e0880656e8d2ae1025df576d70180d7592a>.
286///
287/// We temporarily vendor the code here because the 1D perlin noise hasn't been released yet:
288/// <https://github.com/Razaekel/noise-rs/issues/306>
289///
290/// From the repo:
291///
292/// > Licensed under either of
293/// >
294/// > Apache License, Version 2.0 (LICENSE-APACHE or <http://www.apache.org/licenses/LICENSE-2.0>)
295/// > MIT license (LICENSE-MIT or <http://opensource.org/licenses/MIT>)
296/// > at your option.
297mod perlin_noise {
298 #[inline(always)]
299 pub fn perlin_1d<NH>(point: f64, hasher: &NH) -> f64
300 where
301 NH: noise::permutationtable::NoiseHasher + ?Sized,
302 {
303 // Unscaled range of linearly interpolated perlin noise should be (-sqrt(N)/2, sqrt(N)/2).
304 // Need to invert this value and multiply the unscaled result by the value to get a scaled
305 // range of (-1, 1).
306 //
307 // 1/(sqrt(N)/2), N=1 -> 1/2
308 const SCALE_FACTOR: f64 = 0.5;
309
310 #[inline(always)]
311 #[rustfmt::skip]
312 fn gradient_dot_v(perm: usize, point: f64) -> f64 {
313 let x = point;
314
315 match perm & 0b1 {
316 0 => x, // ( 1 )
317 1 => -x, // (-1 )
318 _ => unreachable!(),
319 }
320 }
321
322 let floored = point.floor();
323 let corner = floored as isize;
324 let distance = point - floored;
325
326 macro_rules! call_gradient(
327 ($x_offset:expr) => {
328 {
329 gradient_dot_v(
330 hasher.hash(&[corner + $x_offset]),
331 distance - $x_offset as f64
332 )
333 }
334 }
335 );
336
337 let g0 = call_gradient!(0);
338 let g1 = call_gradient!(1);
339
340 let u = map_quintic(distance);
341
342 let unscaled_result = linear_interpolation(u, g0, g1);
343
344 let scaled_result = unscaled_result * SCALE_FACTOR;
345
346 // At this point, we should be really damn close to the (-1, 1) range, but some float errors
347 // could have accumulated, so let's just clamp the results to (-1, 1) to cut off any
348 // outliers and return it.
349 scaled_result.clamp(-1.0, 1.0)
350 }
351 #[inline(always)]
352 fn linear_interpolation(u: f64, g0: f64, g1: f64) -> f64 {
353 let k0 = g0;
354 let k1 = g1 - g0;
355 k0 + k1 * u
356 }
357 fn map_quintic(n: f64) -> f64 {
358 let x = n.clamp(0.0, 1.0);
359
360 x * x * x * (x * (x * 6.0 - 15.0) + 10.0)
361 }
362}