Skip to main content

all_is_cubes_render/camera/
stdcam.rs

1use alloc::sync::Arc;
2use core::fmt;
3
4use all_is_cubes::character::{Character, Cursor, cursor_raycast};
5use all_is_cubes::listen;
6use all_is_cubes::math::FreeCoordinate;
7use all_is_cubes::space::Space;
8use all_is_cubes::universe::{Handle, HandleError, ReadTicket, StrongHandle, Universe};
9
10use crate::camera::{Camera, GraphicsOptions, NdcPoint2, ViewTransform, Viewport};
11
12/// A collection of values associated with each of the layers of graphics that
13/// is normally drawn (HUD on top of world, currently) by [`HeadlessRenderer`] or
14/// other renderers.
15///
16/// [`HeadlessRenderer`]: crate::HeadlessRenderer
17// Exhaustive: Changing this will probably be breaking anyway, until we make it a
18// more thorough abstraction.
19#[expect(clippy::exhaustive_structs)]
20#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
21pub struct Layers<T> {
22    /// The game world.
23    pub world: T,
24    /// The user interface, or HUD, drawn in front of the world.
25    pub ui: T,
26}
27
28impl<T> Layers<T> {
29    /// Clone the given value for each layer.
30    pub fn splat(value: T) -> Self
31    where
32        T: Clone,
33    {
34        Layers {
35            world: value.clone(),
36            ui: value,
37        }
38    }
39
40    // experimental API
41    #[cfg(feature = "raytracer")]
42    pub(crate) fn as_refs(&self) -> Layers<&T> {
43        Layers {
44            world: &self.world,
45            ui: &self.ui,
46        }
47    }
48
49    // experimental API
50    #[cfg(feature = "raytracer")]
51    pub(crate) fn map<U, F: FnMut(T) -> U>(self, mut f: F) -> Layers<U> {
52        Layers {
53            world: f(self.world),
54            ui: f(self.ui),
55        }
56    }
57
58    #[doc(hidden)] // used in related crates, but it's ugly and experimental
59    pub fn try_map_ref<U, E>(&self, mut f: impl FnMut(&T) -> Result<U, E>) -> Result<Layers<U>, E> {
60        Ok(Layers {
61            world: f(&self.world)?,
62            ui: f(&self.ui)?,
63        })
64    }
65}
66
67/// Bundle of inputs specifying the “standard” configuration of [`Camera`]s and other
68/// things to render an All is Cubes scene and user interface.
69///
70/// All of its data is provided through [`listen::DynSource`]s, and consists of:
71///
72/// * [`GraphicsOptions`].
73/// * A [`Viewport`] specifying the dimensions of image to render.
74/// * A [`Handle`] to the [`Character`] whose eyes we look through to render the “world”
75///   [`Space`].
76/// * A [`Handle`] to the UI/HUD [`Space`] overlaid on the world, if any.
77///
78/// When [`StandardCameras::update()`] is called, all of these data sources are read
79/// and used to update the [`Camera`] data. Those cameras, and copies of the input
80/// data, are then available for use while rendering.
81///
82/// Because every input is a [`listen::DynSource`], it is never necessary to call a setter.
83/// Every [`StandardCameras`] which was created with the same sources will have the same
84/// results (after `update()`).
85///
86/// Design note: The sense in which this is “standard” is that if an application wished
87/// to, for example, have multiple views into the same [`Space`], it would need to create
88/// additional [`Camera`]s (or multiple [`StandardCameras`]) and update them itself.
89#[derive(Debug)]
90pub struct StandardCameras {
91    /// Cameras are synced with this
92    graphics_options: listen::DynSource<Arc<GraphicsOptions>>,
93    graphics_options_dirty: listen::Flag,
94
95    character_source: listen::DynSource<Option<StrongHandle<Character>>>,
96    /// Tracks whether the character was replaced (not whether its view changed).
97    character_dirty: listen::Flag,
98    character: Option<StrongHandle<Character>>,
99    /// Cached and listenable version of character's space.
100    /// TODO: This should be in a `Layers` along with `ui_state`...?
101    world_space: listen::Cell<Option<Handle<Space>>>,
102
103    ui_source: listen::DynSource<Arc<UiViewState>>,
104    ui_dirty: listen::Flag,
105    ui_space: Option<Handle<Space>>,
106
107    viewport_source: listen::DynSource<Viewport>,
108    viewport_dirty: listen::Flag,
109
110    cameras: Layers<Camera>,
111}
112
113impl StandardCameras {
114    /// Most general constructor; hidden because the details needed might vary and so we
115    /// want to discourage use of this directly.
116    ///
117    /// Note that the initial state is not a correct snapshot of the data sources;
118    /// you must call [`Self::update()`] at least once.
119    #[doc(hidden)]
120    pub fn new(
121        graphics_options: listen::DynSource<Arc<GraphicsOptions>>,
122        viewport_source: listen::DynSource<Viewport>,
123        character_source: listen::DynSource<Option<StrongHandle<Character>>>,
124        ui_source: listen::DynSource<Arc<UiViewState>>,
125    ) -> Self {
126        // TODO: Add a unit test that each of these listeners works as intended.
127        // TODO: This is also an awful lot of repetitive code; we should design a pattern
128        // to not have it (some kind of "following cell")?
129        let graphics_options_dirty = listen::Flag::listening(false, &graphics_options);
130        let viewport_dirty = listen::Flag::listening(false, &viewport_source);
131
132        let initial_options: &GraphicsOptions = &graphics_options.get();
133        let initial_viewport: Viewport = viewport_source.get();
134
135        let ui_state = ui_source.get();
136
137        Self {
138            cameras: Layers {
139                ui: Camera::new((*ui_state.graphics_options).clone(), initial_viewport),
140                world: Camera::new(initial_options.clone(), initial_viewport),
141            },
142
143            graphics_options,
144            graphics_options_dirty,
145
146            character_dirty: listen::Flag::listening(true, &character_source),
147            character_source,
148            character: None, // update() will fix these up
149            world_space: listen::Cell::new(None),
150
151            ui_space: ui_state.space.clone(),
152            ui_dirty: listen::Flag::listening(true, &ui_source),
153            ui_source,
154
155            viewport_dirty,
156            viewport_source,
157        }
158    }
159
160    #[doc(hidden)]
161    pub fn from_constant_for_test(
162        graphics_options: GraphicsOptions,
163        viewport: Viewport,
164        universe: &Universe,
165    ) -> Self {
166        let mut new_self = Self::new(
167            listen::constant(Arc::new(graphics_options)),
168            listen::constant(viewport),
169            listen::constant(universe.get_default_character().map(StrongHandle::new)),
170            listen::constant(Default::default()),
171        );
172        new_self.update(Layers {
173            world: universe.read_ticket(),
174            ui: ReadTicket::stub(),
175        });
176        new_self
177    }
178
179    /// Updates camera state from data sources.
180    ///
181    /// This should be called at the beginning of each frame or as needed when the
182    /// cameras are to be used.
183    ///
184    /// Returns whether any values actually changed.
185    /// (This does not include tracking changes to space content — only which part of which spaces
186    /// are being looked at.)
187    pub fn update(&mut self, read_tickets: Layers<ReadTicket<'_>>) -> bool {
188        let mut anything_changed = false;
189
190        let options_dirty = self.graphics_options_dirty.get_and_clear();
191        if options_dirty {
192            anything_changed = true;
193            self.cameras.world.set_options((*self.graphics_options.get()).clone());
194        }
195
196        let ui_dirty = self.ui_dirty.get_and_clear();
197        if ui_dirty || options_dirty {
198            anything_changed = true;
199            let UiViewState {
200                space,
201                view_transform: ui_transform,
202                graphics_options: ui_options,
203            } = if self.graphics_options.get().show_ui {
204                (*self.ui_source.get()).clone()
205            } else {
206                UiViewState::default()
207            };
208            self.ui_space = space;
209            self.cameras.ui.set_options((*ui_options).clone());
210            self.cameras.ui.set_view_transform(ui_transform);
211        }
212
213        // Update viewports.
214        // Note: The UI does its own independent re-layout when the viewport aspect ratio
215        // changes.
216        let viewport_dirty = self.viewport_dirty.get_and_clear();
217        if viewport_dirty {
218            anything_changed = true;
219            let viewport: Viewport = self.viewport_source.get();
220            // TODO: this should be a Layers::iter_mut() or something
221            self.cameras.world.set_viewport(viewport);
222            self.cameras.ui.set_viewport(viewport);
223        }
224
225        if self.character_dirty.get_and_clear() {
226            anything_changed = true;
227            self.character = self.character_source.get();
228            if self.character.is_none() {
229                // Reset transform so it isn't a *stale* transform.
230                // TODO: set an error flag saying that nothing should be drawn
231                self.cameras.world.set_view_transform(ViewTransform::identity());
232            }
233        }
234
235        if let Some(character_handle) = &self.character {
236            match Character::view(character_handle, read_tickets.world) {
237                Ok((space_handle, view_transform, exposure)) => {
238                    if view_transform != self.cameras.world.view_transform() {
239                        anything_changed = true;
240                        self.cameras.world.set_view_transform(view_transform);
241                    }
242
243                    // TODO: listen::Cell should make this easier and cheaper
244                    if Option::as_ref(&self.world_space.get()) != Some(space_handle) {
245                        anything_changed = true;
246
247                        self.world_space.set(Some(space_handle.clone()));
248                    }
249
250                    // Update camera exposure from character.
251                    let old_actual_exposure = self.cameras.world.exposure();
252                    self.cameras.world.set_measured_exposure(exposure);
253                    anything_changed |= self.cameras.world.exposure() != old_actual_exposure;
254                }
255                Err(_) => {
256                    // TODO: set an error flag indicating failure to update
257                }
258            }
259        } else {
260            // We now have no character. Drop the previous character's space if there is one.
261            if self.world_space.get().is_some() {
262                anything_changed = true;
263                self.world_space.set(None);
264            }
265        }
266
267        anything_changed
268    }
269
270    /// Returns current graphics options as of the last [`update()`](Self::update).
271    ///
272    /// These options are to be used for the world and not the UI.
273    pub fn graphics_options(&self) -> &GraphicsOptions {
274        self.cameras.world.options()
275    }
276
277    /// Returns a clone of the source of graphics options that this [`StandardCameras`]
278    /// was created with.
279    ///
280    /// These options are to be used for the world and not the UI.
281    pub fn graphics_options_source(&self) -> listen::DynSource<Arc<GraphicsOptions>> {
282        self.graphics_options.clone()
283    }
284
285    /// Returns [`Camera`]s appropriate for drawing each graphical layer.
286    pub fn cameras(&self) -> &Layers<Camera> {
287        &self.cameras
288    }
289
290    /// Returns the character's viewpoint to draw in the world layer.
291    /// May be [`None`] if there is no current character.
292    pub fn character(&self) -> Option<&StrongHandle<Character>> {
293        self.character.as_ref()
294    }
295
296    /// Returns the space that should be drawn as the game world, using `self.cameras().world`.
297    ///
298    /// This is a [`listen::DynSource`] to make it simple to cache the Space rendering data and
299    /// follow space transitions.
300    /// It updates when [`Self::update()`] is called.
301    pub fn world_space(&self) -> listen::DynSource<Option<Handle<Space>>> {
302        self.world_space.as_source()
303    }
304
305    /// Returns the UI space, that should be drawn on top of the world using `self.cameras().ui`.
306    ///
307    /// This implements [`GraphicsOptions::show_ui`] by returning [`None`] when the option is
308    /// false.
309    ///
310    /// TODO: Make this also a [`listen::DynSource`]
311    pub fn ui_space(&self) -> Option<&Handle<Space>> {
312        self.ui_space.as_ref()
313    }
314
315    // TODO: unclear if good API; added so that we can get Source access to the graphics options,
316    // and *something* of the sort should be public, but I don't know if exposing UiViewState
317    // directly, as opposed to a source of a Camera, is right.
318    #[cfg(feature = "raytracer")] // not used otherwise
319    pub(crate) fn ui_view_source(&self) -> listen::DynSource<Arc<UiViewState>> {
320        self.ui_source.clone()
321    }
322
323    /// Returns the current viewport.
324    ///
325    /// This is always equal to the viewports of all managed [`Camera`]s,
326    /// and only updates when [`StandardCameras::update()`] is called.
327    pub fn viewport(&self) -> Viewport {
328        self.cameras.world.viewport()
329    }
330
331    /// Returns a clone of the viewport source this is following.
332    pub fn viewport_source(&self) -> listen::DynSource<Viewport> {
333        self.viewport_source.clone()
334    }
335
336    /// Perform a raycast through these cameras to find what the cursor hits.
337    ///
338    /// Make sure to call [`StandardCameras::update`] first so that the cameras are
339    /// up to date with game state.
340    pub fn project_cursor(
341        &self,
342        read_tickets: Layers<ReadTicket<'_>>,
343        ndc_pos: NdcPoint2,
344    ) -> Result<Option<Cursor>, HandleError> {
345        if let Some(ui_space_handle) = self.ui_space.as_ref() {
346            let ray = self.cameras.ui.project_ndc_into_world(ndc_pos);
347            if let res @ (Ok(Some(_)) | Err(_)) = cursor_raycast(
348                read_tickets.ui,
349                ray,
350                ui_space_handle,
351                FreeCoordinate::INFINITY,
352            ) {
353                return res;
354            }
355        }
356
357        if let Some(character_handle) = self.character.as_ref() {
358            let ray = self.cameras.world.project_ndc_into_world(ndc_pos);
359            // TODO: maximum distance should be determined by character/universe parameters
360            // instead of hardcoded
361            if let res @ (Ok(Some(_)) | Err(_)) = cursor_raycast(
362                read_tickets.world,
363                ray,
364                character_handle.read(read_tickets.world)?.space(),
365                6.0,
366            ) {
367                return res;
368            }
369        }
370
371        Ok(None)
372    }
373
374    /// Returns a [`StandardCameras`] which tracks the same data sources (graphics
375    /// options, scene sources, viewport) as `self`, but whose local state (such as
376    /// the last updated camera state) is independent.
377    ///
378    /// The local state is also not updated; you must call [`StandardCameras::update()`]
379    /// on the clone before reading anything from it.
380    #[must_use]
381    pub fn clone_unupdated(&self) -> Self {
382        Self::new(
383            self.graphics_options.clone(),
384            self.viewport_source.clone(),
385            self.character_source.clone(),
386            self.ui_source.clone(),
387        )
388    }
389}
390
391impl fmt::Pointer for StandardCameras {
392    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
393        // Print all and only the fields which point to external mutable state
394        write!(
395            f,
396            indoc::indoc! {"\
397                StandardCameras {{
398                    graphics_options: {graphics_options:p},
399                    viewport_source: {viewport_source:p},
400                    character_source: {character_source:p},
401                    ui_source: {ui_source:p},
402                }}\
403            "},
404            graphics_options = self.graphics_options,
405            viewport_source = self.viewport_source,
406            character_source = self.character_source,
407            ui_source = self.ui_source,
408        )
409    }
410}
411
412/// Specifies what to render for the UI layer in front of the world.
413///
414/// This struct contains all the information needed to know how to render the UI
415/// *specifically* (distinct from the world). It differs from [`Camera`] in that it
416/// includes the [`Space`] and excludes the viewport.
417///
418/// TODO: This struct needs a better name. And is it good for non-UI, too?
419/// Note that we may wish to revise this bundle if we start having continuously changing
420/// `view_transform`.
421#[derive(Clone, Debug, PartialEq)]
422#[expect(clippy::exhaustive_structs)]
423pub struct UiViewState {
424    /// The [`Space`] to render as the UI.
425    pub space: Option<Handle<Space>>,
426
427    /// The viewpoint to render the `space` from.
428    pub view_transform: ViewTransform,
429
430    /// The graphics options to render the `space` with.
431    //---
432    // Design note: This is an `Arc` not because it strongly needs to be,
433    // but because other parts of the system pass around `Arc`ed graphics options
434    // and we want to be efficiently compatible with them.
435    pub graphics_options: Arc<GraphicsOptions>,
436}
437
438impl Default for UiViewState {
439    /// Draws no space, with default graphics options.
440    fn default() -> Self {
441        Self {
442            space: Default::default(),
443            view_transform: ViewTransform::identity(),
444            graphics_options: Default::default(),
445        }
446    }
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452
453    #[test]
454    fn cameras_follow_character_and_world() {
455        let character_cell = listen::Cell::new(None);
456        let mut cameras = StandardCameras::new(
457            listen::constant(Arc::new(GraphicsOptions::default())),
458            listen::constant(Viewport::ARBITRARY),
459            character_cell.as_source(),
460            listen::constant(Arc::new(UiViewState::default())),
461        );
462        cameras.update(Layers::splat(ReadTicket::stub()));
463
464        let world_source = cameras.world_space();
465        let world_flag = listen::Flag::listening(false, &world_source);
466        assert_eq!(world_source.get().as_ref(), None);
467
468        // No redundant notification when world is absent
469        {
470            let changed = cameras.update(Layers::splat(ReadTicket::stub()));
471            assert_eq!((changed, world_flag.get_and_clear()), (false, false));
472        }
473
474        // Create a universe with space and character
475        let mut universe = Universe::new();
476        let space_handle = universe.insert_anonymous(Space::empty_positive(1, 1, 1));
477        let character = universe
478            .insert(
479                "character".into(),
480                Character::spawn_default(universe.read_ticket(), space_handle.clone()).unwrap(),
481            )
482            .unwrap();
483        character_cell.set(Some(StrongHandle::new(character)));
484
485        // Now the world_source should be reporting the new space
486        {
487            assert!(!world_flag.get_and_clear());
488            let changed = cameras.update(Layers {
489                world: universe.read_ticket(),
490                ui: ReadTicket::stub(),
491            });
492            assert_eq!((changed, world_flag.get_and_clear()), (true, true));
493            assert_eq!(world_source.get().as_ref(), Some(&space_handle));
494        }
495
496        // No redundant notification when world is present
497        {
498            let changed = cameras.update(Layers {
499                world: universe.read_ticket(),
500                ui: ReadTicket::stub(),
501            });
502            assert_eq!((changed, world_flag.get_and_clear()), (false, false));
503        }
504
505        // TODO: test further changes
506    }
507
508    #[test]
509    fn cameras_clone() {
510        let options_cell = listen::Cell::new(Arc::new(GraphicsOptions::default()));
511        let mut cameras = StandardCameras::new(
512            options_cell.as_source(),
513            listen::constant(Viewport::ARBITRARY),
514            listen::constant(None),
515            listen::constant(Arc::new(UiViewState::default())),
516        );
517        cameras.update(Layers::splat(ReadTicket::stub()));
518        let mut cameras2 = cameras.clone_unupdated();
519        cameras2.update(Layers::splat(ReadTicket::stub()));
520
521        let default_o = GraphicsOptions::default();
522        let mut different_o = default_o.clone();
523        different_o.debug_chunk_boxes = true;
524        options_cell.set(Arc::new(different_o.clone()));
525
526        // Each `StandardCameras` has independent updating from the same data sources.
527        assert_eq!(cameras.cameras().world.options(), &default_o);
528        assert_eq!(cameras2.cameras().world.options(), &default_o);
529        cameras.update(Layers::splat(ReadTicket::stub()));
530        assert_eq!(cameras.cameras().world.options(), &different_o);
531        assert_eq!(cameras2.cameras().world.options(), &default_o);
532        cameras2.update(Layers::splat(ReadTicket::stub()));
533        assert_eq!(cameras.cameras().world.options(), &different_o);
534        assert_eq!(cameras2.cameras().world.options(), &different_o);
535    }
536}