mod accumulation;
mod bvh;
mod denoise;
pub(crate) mod environment;
mod gpu_scene;
mod pipeline;
pub mod scene_data;
mod tex_array;
mod tonemap;
use std::path::Path;
use crate::camera::Camera3d;
use crate::light::LightCollection;
use crate::scene::SceneNode3d;
use accumulation::Accumulation;
use denoise::Denoise;
use environment::Environment;
use gpu_scene::GpuScene;
use pipeline::{FrameUniforms, PathTracePipeline};
use tonemap::Tonemap;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum RayBackend {
Software,
Hardware,
}
pub struct RayTracer {
backend: RayBackend,
pipeline: PathTracePipeline,
tonemap: Tonemap,
denoise: Denoise,
gpu_scene: Option<GpuScene>,
accum: Accumulation,
sample_index: u32,
max_bounces: u32,
samples_per_frame: u32,
interactive_scale: f32,
lens_radius: f32,
focus_distance: f32,
environment: Environment,
env_rotation: f32,
env_intensity: f32,
last_env: EnvSignature,
last_camera: [f32; 16],
dirty: bool,
max_pixels: u64,
denoise_enabled: bool,
denoise_iterations: u32,
enabled: bool,
}
#[derive(Clone, Copy, PartialEq)]
struct EnvSignature {
present: bool,
rotation: f32,
intensity: f32,
skybox_gen: u64,
}
fn capped_resolution(width: u32, height: u32, max_pixels: u64) -> (u32, u32) {
let pixels = width as u64 * height as u64;
if pixels == 0 {
return (1, 1);
}
if pixels <= max_pixels {
return (width, height);
}
let scale = (max_pixels as f64 / pixels as f64).sqrt();
let rw = ((width as f64 * scale).floor() as u32).max(1);
let rh = ((height as f64 * scale).floor() as u32).max(1);
(rw, rh)
}
fn effective_denoise_iterations(max_iterations: u32, samples: u32) -> u32 {
if max_iterations == 0 {
return 0;
}
const FULL: u32 = 16;
const CONVERGED: u32 = 512;
if samples <= FULL {
return max_iterations;
}
if samples >= CONVERGED {
return 0;
}
let t = (samples as f32 / FULL as f32).log2() / (CONVERGED as f32 / FULL as f32).log2();
let iterations = (max_iterations as f32 * (1.0 - t)).ceil() as u32;
iterations.max(1)
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum RayTracerPreset {
Low,
Medium,
High,
Ultra,
}
impl RayTracer {
pub fn new() -> RayTracer {
Self::with_backend(Self::pick_backend())
}
pub fn preset(preset: RayTracerPreset) -> RayTracer {
let mut rt = Self::new();
match preset {
RayTracerPreset::Low => {
rt.interactive_scale = 0.4;
rt.samples_per_frame = 1;
rt.max_bounces = 4;
}
RayTracerPreset::Medium => {
rt.interactive_scale = 0.5;
rt.samples_per_frame = 2;
rt.max_bounces = 8;
}
RayTracerPreset::High => {
rt.interactive_scale = 0.75;
rt.samples_per_frame = 4;
rt.max_bounces = 8;
}
RayTracerPreset::Ultra => {
rt.interactive_scale = 1.0;
rt.samples_per_frame = 8;
rt.max_bounces = 12;
}
}
rt
}
pub fn with_enabled(enabled: bool) -> RayTracer {
RayTracer {
enabled,
..Self::default()
}
}
fn with_backend(backend: RayBackend) -> RayTracer {
let limits = crate::context::Context::get().device.limits();
let max_bytes = limits
.max_storage_buffer_binding_size
.min(limits.max_buffer_size);
let max_pixels = (max_bytes / 16).max(1);
RayTracer {
backend,
pipeline: PathTracePipeline::new(backend),
tonemap: Tonemap::new(),
denoise: Denoise::new(),
gpu_scene: None,
accum: Accumulation::new(1, 1),
sample_index: 0,
max_bounces: 8,
samples_per_frame: 1,
interactive_scale: 0.5,
lens_radius: 0.0,
focus_distance: 1.0,
environment: Environment::fallback(),
env_rotation: 0.0,
env_intensity: 1.0,
last_env: EnvSignature {
present: false,
rotation: f32::NAN,
intensity: f32::NAN,
skybox_gen: u64::MAX,
},
last_camera: [f32::NAN; 16],
dirty: true,
max_pixels,
denoise_enabled: false,
denoise_iterations: 5,
enabled: true,
}
}
fn pick_backend() -> RayBackend {
let features = crate::context::Context::get().device.features();
if features.contains(wgpu::Features::EXPERIMENTAL_RAY_QUERY) {
RayBackend::Hardware
} else {
RayBackend::Software
}
}
pub fn backend(&self) -> RayBackend {
self.backend
}
pub fn enabled(&self) -> bool {
self.enabled
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
pub fn set_max_bounces(&mut self, bounces: u32) {
if self.max_bounces != bounces {
self.max_bounces = bounces.max(1);
self.dirty = true;
}
}
pub fn set_denoise(&mut self, enabled: bool) {
self.denoise_enabled = enabled;
}
pub fn denoise(&self) -> bool {
self.denoise_enabled
}
pub fn set_denoise_iterations(&mut self, iterations: u32) {
self.denoise_iterations = iterations.max(1);
}
pub fn set_samples_per_frame(&mut self, samples: u32) {
let samples = samples.max(1);
if self.samples_per_frame != samples {
self.samples_per_frame = samples;
self.dirty = true;
}
}
pub fn set_interactive_scale(&mut self, scale: f32) {
self.interactive_scale = scale.clamp(0.05, 1.0);
}
pub fn set_aperture(&mut self, lens_radius: f32, focus_distance: f32) {
let lens_radius = lens_radius.max(0.0);
let focus_distance = focus_distance.max(1e-3);
if self.lens_radius != lens_radius || self.focus_distance != focus_distance {
self.lens_radius = lens_radius;
self.focus_distance = focus_distance;
self.dirty = true;
}
}
pub fn set_f_number(&mut self, f_number: f32, focus_distance: f32) {
let r = if f_number > 0.0 {
focus_distance / (2.0 * f_number)
} else {
0.0
};
self.set_aperture(r, focus_distance);
}
pub fn set_environment_from_file(&mut self, path: &Path) -> bool {
match Environment::from_file(path) {
Some(env) => {
self.environment = env;
self.dirty = true;
true
}
None => false,
}
}
pub fn set_environment_image(&mut self, image: &image::DynamicImage) {
self.environment = Environment::from_image(image);
self.dirty = true;
}
pub fn clear_environment(&mut self) {
self.environment = Environment::fallback();
self.dirty = true;
}
pub fn set_environment_orientation(&mut self, rotation_radians: f32, intensity: f32) {
self.env_rotation = rotation_radians;
self.env_intensity = intensity.max(0.0);
self.dirty = true;
}
pub fn samples_accumulated(&self) -> u32 {
self.sample_index
}
pub fn mark_dirty(&mut self) {
self.dirty = true;
}
pub fn guide_resolution(&self) -> (u32, u32) {
(self.accum.width, self.accum.height)
}
pub fn guide_albedo_buffer(&self) -> &wgpu::Buffer {
&self.accum.buffer
}
pub fn guide_normal_buffer(&self) -> &wgpu::Buffer {
&self.accum.buffer
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn render_frame(
&mut self,
scene: &SceneNode3d,
camera: &mut dyn Camera3d,
lights: &LightCollection,
background: crate::color::Color,
skybox: Option<(&Environment, f32, f32, u64)>,
encoder: &mut wgpu::CommandEncoder,
output_view: &wgpu::TextureView,
width: u32,
height: u32,
exposure: f32,
tonemap_operator: u32,
gpu: &mut crate::renderer::timings::GpuTimer,
) {
let cam = camera.transformation().to_cols_array();
let camera_moved = cam != self.last_camera;
self.last_camera = cam;
let render_layers = camera.render_layers();
let hash = scene_data::scene_hash(scene, lights, render_layers);
let scene_changed = self.gpu_scene.as_ref().is_none_or(|g| g.hash != hash);
let moving = camera_moved || scene_changed;
let scale = if moving { self.interactive_scale } else { 1.0 };
let sw = ((width as f32 * scale).round() as u32).max(1);
let sh = ((height as f32 * scale).round() as u32).max(1);
let (render_width, render_height) = capped_resolution(sw, sh, self.max_pixels);
let (env, env_rotation, env_intensity, env_present, skybox_gen) =
if self.environment.present {
(
&self.environment,
self.env_rotation,
self.env_intensity,
true,
0,
)
} else if let Some((sky_env, sky_rot, sky_int, sky_gen)) = skybox {
(sky_env, sky_rot, sky_int, true, sky_gen)
} else {
(
&self.environment,
self.env_rotation,
self.env_intensity,
false,
0,
)
};
let mut reset = camera_moved;
let env_sig = EnvSignature {
present: env_present,
rotation: env_rotation,
intensity: env_intensity,
skybox_gen,
};
if env_sig != self.last_env {
self.last_env = env_sig;
reset = true;
}
if scene_changed {
let rt_scene = scene_data::gather(scene, lights, render_layers);
self.gpu_scene = Some(GpuScene::build(&rt_scene, self.backend));
reset = true;
}
if self.accum.ensure(render_width, render_height) {
reset = true;
}
if self.dirty {
reset = true;
self.dirty = false;
}
if reset {
self.sample_index = 0;
}
let gpu_scene = self.gpu_scene.as_ref().expect("gpu scene just ensured");
let spp = self.samples_per_frame.max(1);
let uniforms = FrameUniforms {
inv_view_proj: camera.inverse_transformation().to_cols_array_2d(),
env_rotation: [env_rotation.cos(), env_rotation.sin(), env_intensity, 0.0],
cam_eye: camera.eye().to_array(),
width: render_width,
height: render_height,
sample_index: self.sample_index,
num_triangles: gpu_scene.num_triangles,
num_lights: gpu_scene.num_lights,
ambient: lights.ambient,
max_bounces: self.max_bounces,
seed: self.sample_index,
samples_per_frame: spp,
num_emitters: gpu_scene.num_emitters,
lens_radius: self.lens_radius,
focus_distance: self.focus_distance,
has_env: env_present as u32,
background: [
background.r,
background.g,
background.b,
if gpu_scene.has_translucent { 1.0 } else { 0.0 },
],
ambient_color: [
lights.ambient_color.r,
lights.ambient_color.g,
lights.ambient_color.b,
1.0,
],
fog_color: [
lights.fog.color.r,
lights.fog.color.g,
lights.fog.color.b,
lights.fog.color.a,
],
fog_params: lights.fog.params(),
flags: [gpu_scene.has_non_shadow_caster as u32, 0, 0, 0],
};
self.pipeline.write_uniforms(&uniforms);
match self.backend {
RayBackend::Software => {
self.pipeline.dispatch_compute(
encoder,
gpu_scene,
&self.accum,
env,
render_width,
render_height,
gpu,
);
}
RayBackend::Hardware => {
self.pipeline.dispatch_hardware(
encoder,
gpu_scene,
&self.accum,
env,
render_width,
render_height,
gpu,
);
}
}
let effective_iterations = if self.denoise_enabled {
effective_denoise_iterations(self.denoise_iterations, self.sample_index + spp)
} else {
0
};
let radiance = if effective_iterations > 0 {
self.denoise
.run(encoder, &self.accum, effective_iterations, gpu)
} else {
&self.accum.buffer
};
self.tonemap.draw(
encoder,
&self.accum,
radiance,
exposure,
tonemap_operator,
output_view,
width,
height,
gpu,
);
self.sample_index += spp;
}
}
impl Default for RayTracer {
fn default() -> Self {
Self::new()
}
}