enginerenderer 0.0.1

A zero-dependency offline rendering engine in pure Rust — CPU path tracing, BVH acceleration, 16-band spectral rendering, PBR materials, animation & video export.
Documentation
use crate::core::engine::rendering::{
    effects::volumetric_effects::medium::VolumetricMedium,
    environment::procedural::ProceduralEnvironment,
    loader::content_loader::ContentLoader,
    loader::glb_loader::GlbLoader,
    loader::obj_loader::ObjLoader,
    materials::material::MaterialLibrary,
    mesh::asset::MeshAsset,
    mesh::operations::{compute_tangents, recalculate_normals, subdivide},
    mesh::vertex::geometric_density,
    raytracing::{AreaLight, Camera, DirectionalLight, Material, Scene, Sphere, Triangle, Vec3},
};

pub fn build_realistic_scene(aspect_ratio: f64) -> (Scene, Camera) {
    let polished_ceramic = Material::new(Vec3::new(0.92, 0.94, 0.98), 0.16, 0.02, 0.22, Vec3::ZERO)
        .with_layers(0.98, 0.18, Vec3::new(0.06, 0.08, 0.10))
        .with_optics(0.10, 0.06, 0.22);
    let brushed_copper = Material::new(Vec3::new(0.82, 0.45, 0.20), 0.22, 0.88, 0.72, Vec3::ZERO)
        .with_layers(0.90, 0.24, Vec3::new(0.12, 0.05, 0.02))
        .with_optics(0.03, 0.76, 0.18);
    let cobalt_lacquer = Material::new(Vec3::new(0.19, 0.33, 0.69), 0.09, 0.12, 0.54, Vec3::ZERO)
        .with_layers(0.96, 0.36, Vec3::new(0.04, 0.06, 0.12))
        .with_optics(0.14, 0.12, 0.44);
    let obsidian = Material::new(Vec3::new(0.06, 0.07, 0.10), 0.04, 0.72, 0.86, Vec3::ZERO)
        .with_layers(0.88, 0.28, Vec3::new(0.05, 0.05, 0.08))
        .with_optics(0.02, 0.64, 0.26);
    let floor = MaterialLibrary::rocky_world(Vec3::new(0.55, 0.56, 0.60));
    let emitter = MaterialLibrary::stellar_surface();
    let ocean_world = MaterialLibrary::ocean_world();
    let icy_world = MaterialLibrary::icy_world();

    let content_bundle = ContentLoader.load_showcase_bundle();
    let obj_meshes = ObjLoader.load_embedded_showcase();
    let glb_meshes = GlbLoader.load_embedded_showcase();

    let asset_spheres = asset_meshes_to_spheres(
        &content_bundle.primary_meshes,
        &content_bundle.cinematic_meshes,
        &obj_meshes,
        &glb_meshes,
    );
    let mut triangles = asset_meshes_to_triangles(
        &content_bundle.primary_meshes,
        &content_bundle.cinematic_meshes,
        &obj_meshes,
        &glb_meshes,
    );
    triangles.push(Triangle::flat(
        Vec3::new(-2.8, 0.02, 1.6),
        Vec3::new(-1.2, 0.42, 2.4),
        Vec3::new(-0.6, 0.02, 0.7),
        MaterialLibrary::rocky_world(Vec3::new(0.48, 0.44, 0.40)),
    ));

    let mut objects = vec![
        Sphere {
            center: Vec3::new(0.0, -1002.0, 0.0),
            radius: 1000.0,
            material: floor,
        },
        Sphere {
            center: Vec3::new(-2.4, 0.7, -1.2),
            radius: 1.7,
            material: cobalt_lacquer,
        },
        Sphere {
            center: Vec3::new(1.2, 0.55, 0.3),
            radius: 1.55,
            material: polished_ceramic,
        },
        Sphere {
            center: Vec3::new(3.4, 0.15, -2.4),
            radius: 1.15,
            material: brushed_copper,
        },
        Sphere {
            center: Vec3::new(-0.4, 0.2, 2.8),
            radius: 1.1,
            material: obsidian,
        },
        Sphere {
            center: Vec3::new(-4.8, 0.35, 4.6),
            radius: 0.95,
            material: ocean_world,
        },
        Sphere {
            center: Vec3::new(4.9, 0.25, 5.2),
            radius: 0.88,
            material: icy_world,
        },
        Sphere {
            center: Vec3::new(0.0, 6.5, -4.0),
            radius: 1.0,
            material: emitter,
        },
    ];
    objects.extend(asset_spheres);

    let scene = Scene {
        objects,
        triangles,
        sun: DirectionalLight {
            direction: Vec3::new(-0.7, -1.0, -0.45).normalize(),
            color: Vec3::new(1.0, 0.95, 0.9),
            intensity: 2.8,
            angular_radius: 0.04,
        },
        area_lights: vec![
            AreaLight {
                position: Vec3::new(-3.2, 3.8, 2.4),
                u: Vec3::new(1.2, 0.0, 0.0),
                v: Vec3::new(0.0, 0.0, 1.0),
                color: Vec3::new(1.0, 0.82, 0.72),
                intensity: 1.6,
            },
            AreaLight {
                position: Vec3::new(2.6, 2.9, -1.8),
                u: Vec3::new(0.0, 0.9, 0.0),
                v: Vec3::new(0.0, 0.0, 1.4),
                color: Vec3::new(0.62, 0.76, 1.0),
                intensity: 1.4,
            },
        ],
        sky_top: Vec3::new(0.06, 0.10, 0.28),
        sky_bottom: Vec3::new(0.22, 0.26, 0.36),
        exposure: 1.18,
        volume: VolumetricMedium::cinematic_nebula().with_density_multiplier(1.1),
        hdri: Some(ProceduralEnvironment::cinematic_space()),
        solar_elevation: 0.48,
    };

    let camera = Camera::look_at(
        Vec3::new(0.6, 2.0, 9.0),
        Vec3::new(0.2, 0.8, 0.2),
        Vec3::new(0.0, 1.0, 0.0),
        36.0,
        aspect_ratio,
    );

    (scene, camera)
}

fn refine_mesh_for_render(mesh: &MeshAsset) -> MeshAsset {
    let mut refined = mesh.clone();
    let extra_passes = if refined.descriptor.triangle_count < 256 {
        2
    } else if refined.descriptor.triangle_count < 2_048 {
        1
    } else {
        0
    };

    for _ in 0..extra_passes {
        subdivide(&mut refined);
    }

    if extra_passes > 0 {
        recalculate_normals(&mut refined);
        compute_tangents(&mut refined);
        refined.descriptor.vertex_count = refined.vertices.len();
        refined.descriptor.triangle_count = refined.indices.len() / 3;
        refined.descriptor.bounding_radius = refined
            .vertices
            .iter()
            .map(|vertex| vertex.position.length())
            .fold(mesh.descriptor.bounding_radius.max(0.001), f64::max);
    }

    refined
}

pub fn asset_meshes_to_spheres(
    primary_meshes: &[MeshAsset],
    cinematic_meshes: &[MeshAsset],
    obj_meshes: &[MeshAsset],
    glb_meshes: &[MeshAsset],
) -> Vec<Sphere> {
    primary_meshes
        .iter()
        .chain(cinematic_meshes.iter())
        .chain(obj_meshes.iter())
        .chain(glb_meshes.iter())
        .enumerate()
        .map(|(index, mesh)| {
            let centroid = mesh.centroid();
            let orbit_offset = index as f64 * 1.35;
            let fallback_material = if index % 3 == 0 {
                MaterialLibrary::metallic_moon()
            } else if index % 3 == 1 {
                MaterialLibrary::rocky_world(Vec3::new(0.62, 0.48, 0.34))
            } else {
                MaterialLibrary::icy_world()
            };
            let material = mesh.material_or(fallback_material);

            Sphere {
                center: centroid
                    + Vec3::new(
                        -6.0 + orbit_offset,
                        0.45 + index as f64 * 0.06,
                        3.5 + orbit_offset * 0.55,
                    ),
                radius: mesh.effective_radius()
                    * (0.38
                        + geometric_density(
                            &mesh.descriptor,
                            mesh.effective_radius().powi(2) * std::f64::consts::PI * 4.0,
                        )
                        .ln()
                        .max(0.2)
                            * 0.015),
                material,
            }
        })
        .collect()
}

pub fn asset_meshes_to_triangles(
    primary_meshes: &[MeshAsset],
    cinematic_meshes: &[MeshAsset],
    obj_meshes: &[MeshAsset],
    glb_meshes: &[MeshAsset],
) -> Vec<Triangle> {
    primary_meshes
        .iter()
        .chain(cinematic_meshes.iter())
        .chain(obj_meshes.iter())
        .chain(glb_meshes.iter())
        .enumerate()
        .flat_map(|(index, mesh)| {
            let refined = refine_mesh_for_render(mesh);
            let translation = Vec3::new(
                -7.5 + index as f64 * 1.8,
                0.35 + (index % 4) as f64 * 0.28,
                1.8 + index as f64 * 1.15,
            );
            let scale = (0.16 + refined.effective_radius() * 0.10).clamp(0.14, 0.58);
            let fallback_material = match index % 4 {
                0 => MaterialLibrary::metallic_moon(),
                1 => MaterialLibrary::rocky_world(Vec3::new(0.64, 0.46, 0.30)),
                2 => MaterialLibrary::icy_world(),
                _ => MaterialLibrary::ocean_world(),
            };
            refined.to_triangles(translation, scale, refined.material_or(fallback_material))
        })
        .collect()
}