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