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}