Skip to main content

fyrox_impl/scene/
skybox.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! Skybox is a huge box around a camera. See [`SkyBox`] docs for more info.
22
23use std::{fmt::Display, sync::LazyLock};
24
25use crate::{
26    asset::{builtin::BuiltInResource, embedded_data_source, untyped::ResourceKind},
27    core::{log::Log, reflect::prelude::*, uuid_provider, visitor::prelude::*},
28};
29use fyrox_core::color::Color;
30use fyrox_texture::{
31    CompressionOptions, Texture, TextureImportOptions, TextureKind, TextureMinificationFilter,
32    TexturePixelKind, TextureResource, TextureResourceExtension, TextureWrapMode,
33};
34use uuid::{uuid, Uuid};
35
36/// Skybox is a huge box around a camera. Each face has its own texture, when textures are
37/// properly made, there is no seams and you get good decoration which contains static
38/// skies and/or some other objects (mountains, buildings, etc.). Usually skyboxes used
39/// in outdoor scenes, however real use of it limited only by your imagination. Skybox
40/// will be drawn first, none of objects could be drawn before skybox.
41#[derive(Debug, Clone, Default, PartialEq, Reflect, Visit, Eq)]
42pub struct SkyBox {
43    /// Texture for front face.
44    #[reflect(setter = "set_front")]
45    pub(crate) front: Option<TextureResource>,
46
47    /// Texture for back face.
48    #[reflect(setter = "set_back")]
49    pub(crate) back: Option<TextureResource>,
50
51    /// Texture for left face.
52    #[reflect(setter = "set_left")]
53    pub(crate) left: Option<TextureResource>,
54
55    /// Texture for right face.
56    #[reflect(setter = "set_right")]
57    pub(crate) right: Option<TextureResource>,
58
59    /// Texture for top face.
60    #[reflect(setter = "set_top")]
61    pub(crate) top: Option<TextureResource>,
62
63    /// Texture for bottom face.
64    #[reflect(setter = "set_bottom")]
65    pub(crate) bottom: Option<TextureResource>,
66
67    /// Cubemap texture
68    #[reflect(hidden)]
69    #[visit(skip)]
70    pub(crate) cubemap: Option<TextureResource>,
71}
72
73uuid_provider!(SkyBox = "45f359f1-e26f-4ace-81df-097f63474c72");
74
75impl SkyBox {
76    /// Creates a new sky box from a single color.
77    pub fn from_single_color(color: Color) -> Self {
78        let dark_gray_texture = TextureResource::from_bytes(
79            Uuid::new_v4(),
80            TextureKind::Rectangle {
81                width: 1,
82                height: 1,
83            },
84            TexturePixelKind::RGBA8,
85            vec![color.r, color.g, color.b, color.a],
86            ResourceKind::Embedded,
87        )
88        .unwrap();
89
90        SkyBoxBuilder::from_texture(&dark_gray_texture)
91            .build()
92            .unwrap()
93    }
94
95    /// Returns cubemap texture
96    pub fn cubemap(&self) -> Option<TextureResource> {
97        self.cubemap.clone()
98    }
99
100    /// Returns cubemap texture
101    pub fn cubemap_ref(&self) -> Option<&TextureResource> {
102        self.cubemap.as_ref()
103    }
104
105    /// Validates input set of texture and checks if it possible to create a cube map from them.
106    /// There are two main conditions for successful cube map creation:
107    /// - All textures must have same width and height, and width must be equal to height.
108    /// - All textures must have same pixel kind.
109    pub fn validate(&self) -> Result<(), SkyBoxError> {
110        struct TextureInfo {
111            pixel_kind: TexturePixelKind,
112            width: u32,
113            height: u32,
114        }
115
116        let mut first_info: Option<TextureInfo> = None;
117
118        for (index, texture) in self.textures().iter().enumerate() {
119            if let Some(texture) = texture {
120                if let Some(texture) = texture.state().data() {
121                    if let TextureKind::Rectangle { width, height } = texture.kind() {
122                        if width != height {
123                            return Err(SkyBoxError::NonSquareTexture {
124                                index,
125                                width,
126                                height,
127                            });
128                        }
129
130                        if let Some(first_info) = first_info.as_mut() {
131                            if first_info.width != width
132                                || first_info.height != height
133                                || first_info.pixel_kind != texture.pixel_kind()
134                            {
135                                return Err(SkyBoxError::DifferentTexture {
136                                    expected_width: first_info.width,
137                                    expected_height: first_info.height,
138                                    expected_pixel_kind: first_info.pixel_kind,
139                                    index,
140                                    actual_width: width,
141                                    actual_height: height,
142                                    actual_pixel_kind: texture.pixel_kind(),
143                                });
144                            }
145                        } else {
146                            first_info = Some(TextureInfo {
147                                pixel_kind: texture.pixel_kind(),
148                                width,
149                                height,
150                            });
151                        }
152                    }
153                } else {
154                    return Err(SkyBoxError::TextureIsNotReady { index });
155                }
156            }
157        }
158
159        Ok(())
160    }
161
162    /// Creates a cubemap using provided faces. If some face has not been provided corresponding side will be black.
163    ///
164    /// # Important notes.
165    ///
166    /// It will fail if provided face's kind is not TextureKind::Rectangle.
167    pub fn create_cubemap(&mut self) -> Result<(), SkyBoxError> {
168        self.validate()?;
169
170        let (kind, pixel_kind, bytes_per_face) =
171            self.textures().iter().find(|face| face.is_some()).map_or(
172                (
173                    TextureKind::Rectangle {
174                        width: 1,
175                        height: 1,
176                    },
177                    TexturePixelKind::R8,
178                    1,
179                ),
180                |face| {
181                    let face = face.clone().unwrap();
182                    let data = face.data_ref();
183
184                    (data.kind(), data.pixel_kind(), data.mip_level_data(0).len())
185                },
186            );
187
188        let size = match kind {
189            TextureKind::Rectangle { width, height } => {
190                assert_eq!(width, height);
191                width
192            }
193            _ => return Err(SkyBoxError::UnsupportedTextureKind(kind)),
194        };
195
196        let mut data = Vec::<u8>::with_capacity(bytes_per_face * 6);
197        for face in self.textures().iter() {
198            if let Some(f) = face.clone() {
199                data.extend(f.data_ref().mip_level_data(0));
200            } else {
201                let black_face_data = vec![0; bytes_per_face];
202                data.extend(black_face_data);
203            }
204        }
205
206        let cubemap = TextureResource::from_bytes(
207            Uuid::new_v4(),
208            TextureKind::Cube { size },
209            pixel_kind,
210            data,
211            ResourceKind::Embedded,
212        )
213        .ok_or(SkyBoxError::UnableToBuildCubeMap)?;
214
215        let mut cubemap_ref = cubemap.data_ref();
216        cubemap_ref.set_s_wrap_mode(TextureWrapMode::ClampToEdge);
217        cubemap_ref.set_t_wrap_mode(TextureWrapMode::ClampToEdge);
218        drop(cubemap_ref);
219
220        self.cubemap = Some(cubemap);
221
222        Ok(())
223    }
224
225    /// Returns slice with all textures, where: 0 - Left, 1 - Right, 2 - Top, 3 - Bottom
226    /// 4 - Front, 5 - Back.
227    ///
228    /// # Important notes.
229    ///
230    /// These textures are **not** used for rendering! The renderer uses cube map made of these
231    /// textures. Public access for these textures is needed in case you need to read internals
232    /// of the textures.
233    pub fn textures(&self) -> [Option<TextureResource>; 6] {
234        [
235            self.left.clone(),
236            self.right.clone(),
237            self.top.clone(),
238            self.bottom.clone(),
239            self.front.clone(),
240            self.back.clone(),
241        ]
242    }
243
244    /// Set new texture for the left side of the skybox.
245    pub fn set_left(&mut self, texture: Option<TextureResource>) -> Option<TextureResource> {
246        let prev = std::mem::replace(&mut self.left, texture);
247        Log::verify(self.create_cubemap());
248        prev
249    }
250
251    /// Returns a texture that is used for left face of the cube map.
252    ///
253    /// # Important notes.
254    ///
255    /// This textures is not used for rendering! The renderer uses cube map made of face textures.
256    pub fn left(&self) -> Option<TextureResource> {
257        self.left.clone()
258    }
259
260    /// Set new texture for the right side of the skybox.
261    pub fn set_right(&mut self, texture: Option<TextureResource>) -> Option<TextureResource> {
262        let prev = std::mem::replace(&mut self.right, texture);
263        Log::verify(self.create_cubemap());
264        prev
265    }
266
267    /// Returns a texture that is used for right face of the cube map.
268    ///
269    /// # Important notes.
270    ///
271    /// This textures is not used for rendering! The renderer uses cube map made of face textures.
272    pub fn right(&self) -> Option<TextureResource> {
273        self.right.clone()
274    }
275
276    /// Set new texture for the top side of the skybox.
277    pub fn set_top(&mut self, texture: Option<TextureResource>) -> Option<TextureResource> {
278        let prev = std::mem::replace(&mut self.top, texture);
279        Log::verify(self.create_cubemap());
280        prev
281    }
282
283    /// Returns a texture that is used for top face of the cube map.
284    ///
285    /// # Important notes.
286    ///
287    /// This textures is not used for rendering! The renderer uses cube map made of face textures.
288    pub fn top(&self) -> Option<TextureResource> {
289        self.top.clone()
290    }
291
292    /// Set new texture for the bottom side of the skybox.
293    pub fn set_bottom(&mut self, texture: Option<TextureResource>) -> Option<TextureResource> {
294        let prev = std::mem::replace(&mut self.bottom, texture);
295        Log::verify(self.create_cubemap());
296        prev
297    }
298
299    /// Returns a texture that is used for bottom face of the cube map.
300    ///
301    /// # Important notes.
302    ///
303    /// This textures is not used for rendering! The renderer uses cube map made of face textures.
304    pub fn bottom(&self) -> Option<TextureResource> {
305        self.bottom.clone()
306    }
307
308    /// Set new texture for the front side of the skybox.
309    pub fn set_front(&mut self, texture: Option<TextureResource>) -> Option<TextureResource> {
310        let prev = std::mem::replace(&mut self.front, texture);
311        Log::verify(self.create_cubemap());
312        prev
313    }
314
315    /// Returns a texture that is used for front face of the cube map.
316    ///
317    /// # Important notes.
318    ///
319    /// This textures is not used for rendering! The renderer uses cube map made of face textures.
320    pub fn front(&self) -> Option<TextureResource> {
321        self.front.clone()
322    }
323
324    /// Set new texture for the back side of the skybox.
325    pub fn set_back(&mut self, texture: Option<TextureResource>) -> Option<TextureResource> {
326        let prev = std::mem::replace(&mut self.back, texture);
327        Log::verify(self.create_cubemap());
328        prev
329    }
330
331    /// Returns a texture that is used for back face of the cube map.
332    ///
333    /// # Important notes.
334    ///
335    /// This textures is not used for rendering! The renderer uses cube map made of face textures.
336    pub fn back(&self) -> Option<TextureResource> {
337        self.back.clone()
338    }
339}
340
341/// An error that may occur during skybox creation.
342#[derive(Debug)]
343pub enum SkyBoxError {
344    /// Texture kind is not TextureKind::Rectangle
345    UnsupportedTextureKind(TextureKind),
346    /// Cube map was failed to build.
347    UnableToBuildCubeMap,
348    /// Input texture is not square.
349    NonSquareTexture {
350        /// Texture index.
351        index: usize,
352        /// Width of the faulty texture.
353        width: u32,
354        /// Height of the faulty texture.
355        height: u32,
356    },
357    /// Some input texture differs in size or pixel kind.
358    DifferentTexture {
359        /// Actual width of the first valid texture in the input set.
360        expected_width: u32,
361        /// Actual height of the first valid texture in the input set.
362        expected_height: u32,
363        /// Actual pixel kind of the first valid texture in the input set.
364        expected_pixel_kind: TexturePixelKind,
365        /// Index of the faulty input texture.
366        index: usize,
367        /// Width of the faulty texture.
368        actual_width: u32,
369        /// Height of the faulty texture.
370        actual_height: u32,
371        /// Pixel kind of the faulty texture.
372        actual_pixel_kind: TexturePixelKind,
373    },
374    /// Occurs when one of the input textures is either still loading or failed to load.
375    TextureIsNotReady {
376        /// Index of the faulty input texture.
377        index: usize,
378    },
379}
380
381impl std::error::Error for SkyBoxError {}
382
383impl Display for SkyBoxError {
384    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
385        match self {
386            SkyBoxError::UnsupportedTextureKind(texture_kind) => {
387                write!(f, "Unsupported texture kind: {texture_kind:?}")
388            }
389            SkyBoxError::UnableToBuildCubeMap => f.write_str("Cube map was failed to build."),
390            SkyBoxError::NonSquareTexture {
391                index,
392                width,
393                height,
394            } => write!(
395                f,
396                "Input texture is not square. Index: {index}, width: {width}, height: {height}"
397            ),
398            SkyBoxError::DifferentTexture {
399                expected_width,
400                expected_height,
401                expected_pixel_kind,
402                index,
403                actual_width,
404                actual_height,
405                actual_pixel_kind,
406            } => write!(f, "Some input texture differs in size or pixel kind. Index: {index}. \
407            Expected width: {expected_width}, height: {expected_height}, kind: {expected_pixel_kind:?}. \
408            Actual width: {actual_width}, height: {actual_height}, kind: {actual_pixel_kind:?}."),
409            SkyBoxError::TextureIsNotReady { index } => write!(f, "Input texture is not loaded. Index: {index}"),
410        }
411    }
412}
413
414/// SkyBox builder is used to create new skybox in declarative manner.
415pub struct SkyBoxBuilder {
416    /// Texture for front face.
417    pub front: Option<TextureResource>,
418    /// Texture for back face.
419    pub back: Option<TextureResource>,
420    /// Texture for left face.
421    pub left: Option<TextureResource>,
422    /// Texture for right face.
423    pub right: Option<TextureResource>,
424    /// Texture for top face.
425    pub top: Option<TextureResource>,
426    /// Texture for bottom face.
427    pub bottom: Option<TextureResource>,
428}
429
430impl SkyBoxBuilder {
431    /// Creates a new builder, where each texture for each side of the sky box is the same.
432    pub fn from_texture(texture: &TextureResource) -> Self {
433        Self {
434            front: Some(texture.clone()),
435            back: Some(texture.clone()),
436            left: Some(texture.clone()),
437            right: Some(texture.clone()),
438            top: Some(texture.clone()),
439            bottom: Some(texture.clone()),
440        }
441    }
442
443    /// Sets desired front face of cubemap.
444    pub fn with_front(mut self, texture: TextureResource) -> Self {
445        self.front = Some(texture);
446        self
447    }
448
449    /// Sets desired back face of cubemap.
450    pub fn with_back(mut self, texture: TextureResource) -> Self {
451        self.back = Some(texture);
452        self
453    }
454
455    /// Sets desired left face of cubemap.
456    pub fn with_left(mut self, texture: TextureResource) -> Self {
457        self.left = Some(texture);
458        self
459    }
460
461    /// Sets desired right face of cubemap.
462    pub fn with_right(mut self, texture: TextureResource) -> Self {
463        self.right = Some(texture);
464        self
465    }
466
467    /// Sets desired top face of cubemap.
468    pub fn with_top(mut self, texture: TextureResource) -> Self {
469        self.top = Some(texture);
470        self
471    }
472
473    /// Sets desired front face of cubemap.
474    pub fn with_bottom(mut self, texture: TextureResource) -> Self {
475        self.bottom = Some(texture);
476        self
477    }
478
479    /// Creates a new instance of skybox.
480    pub fn build(self) -> Result<SkyBox, SkyBoxError> {
481        let mut skybox = SkyBox {
482            left: self.left,
483            right: self.right,
484            top: self.top,
485            bottom: self.bottom,
486            front: self.front,
487            back: self.back,
488            cubemap: None,
489        };
490
491        skybox.create_cubemap()?;
492
493        Ok(skybox)
494    }
495}
496
497fn load_texture(id: Uuid, data: &[u8]) -> TextureResource {
498    TextureResource::load_from_memory(
499        id,
500        ResourceKind::External,
501        data,
502        TextureImportOptions::default()
503            .with_compression(CompressionOptions::NoCompression)
504            .with_minification_filter(TextureMinificationFilter::Linear),
505    )
506    .ok()
507    .unwrap()
508}
509
510static BUILT_IN_SKYBOX_FRONT: LazyLock<BuiltInResource<Texture>> = LazyLock::new(|| {
511    BuiltInResource::new(
512        "__BUILT_IN_SKYBOX_FRONT",
513        embedded_data_source!("skybox/front.png"),
514        |data| load_texture(uuid!("f8d4519b-2947-4c83-9aa5-800a70ae918e"), data),
515    )
516});
517
518static BUILT_IN_SKYBOX_BACK: LazyLock<BuiltInResource<Texture>> = LazyLock::new(|| {
519    BuiltInResource::new(
520        "__BUILT_IN_SKYBOX_BACK",
521        embedded_data_source!("skybox/back.png"),
522        |data| load_texture(uuid!("28676705-58bd-440f-b0aa-ce42cf95be79"), data),
523    )
524});
525
526static BUILT_IN_SKYBOX_TOP: LazyLock<BuiltInResource<Texture>> = LazyLock::new(|| {
527    BuiltInResource::new(
528        "__BUILT_IN_SKYBOX_TOP",
529        embedded_data_source!("skybox/top.png"),
530        |data| load_texture(uuid!("03e38da7-53d1-48c0-87f8-2baf9869d61d"), data),
531    )
532});
533
534static BUILT_IN_SKYBOX_BOTTOM: LazyLock<BuiltInResource<Texture>> = LazyLock::new(|| {
535    BuiltInResource::new(
536        "__BUILT_IN_SKYBOX_BOTTOM",
537        embedded_data_source!("skybox/bottom.png"),
538        |data| load_texture(uuid!("01684dc1-34b2-48b3-b8c2-30a7718cb9e7"), data),
539    )
540});
541
542static BUILT_IN_SKYBOX_LEFT: LazyLock<BuiltInResource<Texture>> = LazyLock::new(|| {
543    BuiltInResource::new(
544        "__BUILT_IN_SKYBOX_LEFT",
545        embedded_data_source!("skybox/left.png"),
546        |data| load_texture(uuid!("1725b779-7633-477a-a7b0-995c079c3202"), data),
547    )
548});
549
550static BUILT_IN_SKYBOX_RIGHT: LazyLock<BuiltInResource<Texture>> = LazyLock::new(|| {
551    BuiltInResource::new(
552        "__BUILT_IN_SKYBOX_RIGHT",
553        embedded_data_source!("skybox/right.png"),
554        |data| load_texture(uuid!("5f74865a-3eae-4bff-8743-b9d1f7bb3c59"), data),
555    )
556});
557
558static BUILT_IN_SKYBOX: LazyLock<SkyBox> = LazyLock::new(SkyBoxKind::make_built_in_skybox);
559
560impl SkyBoxKind {
561    fn make_built_in_skybox() -> SkyBox {
562        let front = BUILT_IN_SKYBOX_FRONT.resource();
563        let back = BUILT_IN_SKYBOX_BACK.resource();
564        let top = BUILT_IN_SKYBOX_TOP.resource();
565        let bottom = BUILT_IN_SKYBOX_BOTTOM.resource();
566        let left = BUILT_IN_SKYBOX_LEFT.resource();
567        let right = BUILT_IN_SKYBOX_RIGHT.resource();
568
569        SkyBoxBuilder {
570            front: Some(front),
571            back: Some(back),
572            left: Some(left),
573            right: Some(right),
574            top: Some(top),
575            bottom: Some(bottom),
576        }
577        .build()
578        .unwrap()
579    }
580
581    /// Returns a references to built-in sky box.
582    pub fn built_in_skybox() -> &'static SkyBox {
583        &BUILT_IN_SKYBOX
584    }
585
586    /// Returns an array with references to the textures being used in built-in sky box. The order is:
587    /// front, back, top, bottom, left, right.
588    pub fn built_in_skybox_textures() -> [&'static BuiltInResource<Texture>; 6] {
589        [
590            &BUILT_IN_SKYBOX_FRONT,
591            &BUILT_IN_SKYBOX_BACK,
592            &BUILT_IN_SKYBOX_TOP,
593            &BUILT_IN_SKYBOX_BOTTOM,
594            &BUILT_IN_SKYBOX_LEFT,
595            &BUILT_IN_SKYBOX_RIGHT,
596        ]
597    }
598}
599
600/// A fixed set of possible sky boxes, that can be selected when building [`super::camera::Camera`] scene node.
601#[derive(Default)]
602pub enum SkyBoxKind {
603    /// Uses built-in sky box. This is default sky box.
604    #[default]
605    Builtin,
606    /// No sky box. Surroundings will be filled with back buffer clear color.
607    None,
608    /// Specific skybox. One can be built using [`SkyBoxBuilder`].
609    Specific(SkyBox),
610}