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}