crystal_ball 0.3.0

A path tracing library written in Rust.
Documentation
use std::default::Default;
use std::path::Path;
use std::sync::Arc;

use gltf::camera::Projection;
use gltf::Document;

use crate::color::{Color, Texture};
use crate::math::{Mat4, Transform};
use crate::prelude::Error;
use crate::rendering::Camera;
use crate::shapes::Object;

/// The scene, consisting of a camera, objects and a background.
pub struct Scene {
    /// The scene's camera.
    ///
    /// Defaults to [`Camera::default`].
    pub camera: Camera,
    /// The scene's objects.
    ///
    /// Before rendering, the [`Vec`] will automatically be converted to a [`BVH`](crate::shapes::BVH)
    /// for increased performance.
    ///
    /// Defaults to an empty [`Vec`].
    pub objects: Vec<Object>,
    /// The scene's background color.
    ///
    /// Defaults to `(0.8, 0.8, 0.8)`.
    pub background_color: Color,
    /// The scene's base color texture.
    ///
    /// If this is None, `background_color` remains unchanged.
    /// Otherwise each pixel value is multiplied with `background_color`.
    ///
    /// Defaults to [`None`].
    pub background_texture: Option<Arc<dyn Texture>>,
    /// The material's emissive strength.
    ///
    /// The resulting color of `background_color` and `background_texture`
    /// is multiplied with `background_strength`.
    ///
    /// Defaults to `1.0`.
    pub background_strength: f64,
    /// The material's background transform.
    ///
    /// While translation and scale also have impacts,
    /// it is only really useful to use this for rotating background images.
    ///
    /// Defaults to [`Transform::default`].
    pub background_transform: Transform,
}

impl Default for Scene {
    fn default() -> Self {
        Scene {
            camera: Camera::default(),
            objects: vec![],
            background_color: Color::splat(0.8),
            background_texture: None,
            background_strength: 1.0,
            background_transform: Transform::default(),
        }
    }
}

impl Scene {
    /// Load a scene from a [glTF](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html) file.
    ///
    /// This loads the camera as well as all meshes.
    /// If there are multiple cameras, the first one will be chosen.
    /// If there is no camera, [`Camera::default`] will be used.
    ///
    /// Orthographic camera projections are currently not supported.
    /// For more information see [`Object::load_gltf`].
    ///
    /// Returns an [`Error`] if the document can not be parsed.
    pub fn load_gltf<P: AsRef<Path>>(path: P) -> Result<Scene, Error> {
        let (document, buffers, images) = gltf::import(path)?;

        let camera = Self::load_camera(&document).unwrap_or_default();

        let objects = Object::load_gltf_document(&document, &buffers, &images)?;

        Ok(Self {
            camera,
            objects,
            ..Default::default()
        })
    }

    /// Load the first camera from a glTF file.
    fn load_camera(document: &Document) -> Option<Camera> {
        let camera = document.cameras().next()?;

        if document.cameras().len() > 1 {
            eprintln!("WARNING: multiple cameras found. Using the first one");
        }

        let projection = match camera.projection() {
            Projection::Orthographic(_) => {
                eprintln!("ERROR: orthographic camera projection is not supported");
                return None;
            }
            Projection::Perspective(p) => p,
        };

        let default_camera = Camera::default();

        let fov = projection.yfov() as f64;

        let (width, height, fov) = if let Some(aspect_ratio) = projection.aspect_ratio() {
            if aspect_ratio > 1.0 {
                (
                    default_camera.width,
                    (default_camera.width as f32 / aspect_ratio) as u32,
                    fov,
                )
            } else {
                (
                    (aspect_ratio * default_camera.height as f32) as u32,
                    default_camera.height,
                    fov * aspect_ratio as f64,
                )
            }
        } else {
            (default_camera.width, default_camera.height, fov)
        };

        let camera_node = document
            .nodes()
            .find(|n| {
                if let Some(c) = n.camera() {
                    c.index() == camera.index()
                } else {
                    false
                }
            })
            .unwrap();
        let mat4 =
            Mat4::from(camera_node.transform().matrix().map(|r| r.map(f64::from))).transpose();
        let mat4_inverse = mat4.inverse();

        let transform = Transform::new(mat4, mat4_inverse);

        Some(Camera {
            transform,
            width,
            height,
            fov,
            ..Default::default()
        })
    }
}