Skip to main content

euv_engine/raytracing/pipeline/render/
fn.rs

1use super::*;
2
3/// Builds the world-space direction for one supersample of one pixel.
4///
5/// The camera ray is
6/// `normalize(forward + right * (ndc_x * aspect * tan(fov/2)) + up *
7/// (ndc_y * tan(fov/2)))`. The `tan(fov/2)` factor is what makes the
8/// rendered vertical field of view exactly `fov`; omitting it renders
9/// wider than the camera asks for and no longer matches either shader,
10/// which both apply it.
11///
12/// Rows are addressed top-down, matching the WGSL fragment position and the
13/// `gl_FragCoord`-flipped WebGL2 ordering, so all three backends agree row
14/// for row.
15///
16/// # Arguments
17///
18/// - `&FrameBasis` - The validated camera basis.
19/// - `f64` - The pixel's x coordinate.
20/// - `f64` - The pixel's y coordinate, counted from the top row.
21/// - `f64` - The sub-sample's offset within the pixel, in `[0, 1)`.
22/// - `f64` - The second sub-sample's offset within the pixel.
23///
24/// # Returns
25///
26/// - `Vector3D` - The unit world-space ray direction.
27pub(crate) fn pixel_direction(
28    basis: &FrameBasis,
29    x: f64,
30    y: f64,
31    offset_x: f64,
32    offset_y: f64,
33) -> Vector3D {
34    let ndc_x: f64 = ((x + offset_x) / basis.get_width() as f64) * 2.0 - 1.0;
35    let ndc_y: f64 = 1.0 - ((y + offset_y) / basis.get_height() as f64) * 2.0;
36    let tan_half: f64 = basis.get_tan_half_fov();
37    (basis.get_forward()
38        + basis
39            .get_right()
40            .scaled(ndc_x * basis.get_aspect() * tan_half)
41        + basis.get_up().scaled(ndc_y * tan_half))
42    .normalized()
43}
44
45/// Converts one linear channel to a gamma-corrected byte.
46///
47/// The clamp to `[0, 1]` happens before the curve and the result is
48/// rounded, matching both shaders' `pow(clamp(c, 0, 1), 1/2.2)`.
49///
50/// # Arguments
51///
52/// - `f64` - The linear channel, which may fall outside `[0, 1]`.
53///
54/// # Returns
55///
56/// - `u8` - The gamma-corrected byte, clamped to `[0, 255]`.
57pub fn gamma_byte(channel: f64) -> u8 {
58    let clamped: f64 = channel.clamp(0.0, 1.0);
59    let encoded: f64 = clamped.powf(1.0 / RAYTRACE_GAMMA);
60    (encoded * 255.0).round().clamp(0.0, 255.0) as u8
61}
62
63/// Renders a ray-traced RGBA frame on the CPU.
64///
65/// This is the CPU reference and the Canvas 2D backend's renderer. It does
66/// not re-implement traversal or shading: each pixel builds a [`Ray`] — a
67/// stack value, so no allocation happens per pixel, per bounce, or per
68/// frame — and hands it to [`RayTraceScene::trace_with_bounces`], which
69/// runs the scene's own bounding volume hierarchy and the real
70/// [`LightingUniforms`] shading. The two GPU backends implement the same
71/// math in shader form against the packed buffers; they are the only
72/// duplicates, and they must agree with this path rather than with each
73/// other.
74///
75/// The frame is described by a [`FrameBasis`], a stack value built by the
76/// same validator [`pack_raytrace_frame`] uses, so this path never packs a
77/// GPU buffer, never re-encodes the scene, and allocates nothing at all
78/// beyond the caller-owned `out`. A steady-state frame therefore performs
79/// exactly one heap operation: none.
80///
81/// Each pixel traces `samples_per_axis` squared supersampled rays, averages
82/// them, clamps the linear color to `[0, 1]`, applies the `1 / 2.2` gamma
83/// curve, and writes an opaque alpha. Rows are emitted top-down.
84///
85/// # Arguments
86///
87/// - `&RayTraceScene` - The scene to render.
88/// - `&Camera3D` - The camera supplying the eye, target, up, fov, and
89///   near/far range.
90/// - `&LightingUniforms` - The lights, ambient, and shading eye.
91/// - `&RayTraceSettings` - The sampling grid and bounce limit.
92/// - `u32` - The frame width in pixels.
93/// - `u32` - The frame height in pixels.
94/// - `&mut Vec<u8>` - The destination RGBA buffer, cleared and reused.
95///
96/// # Returns
97///
98/// - `Result<(), String>` - `Ok` once the frame is written, or the reason
99///   the frame cannot be rendered.
100pub fn render_raytrace_rgba(
101    scene: &RayTraceScene,
102    camera: &Camera3D,
103    lights: &LightingUniforms,
104    settings: &RayTraceSettings,
105    width: u32,
106    height: u32,
107    out: &mut Vec<u8>,
108) -> Result<(), String> {
109    settings.validate()?;
110    let basis: FrameBasis = frame_basis(camera, width, height)?;
111    let pixel_count: usize = (basis.get_width() as usize) * (basis.get_height() as usize);
112    let byte_count: usize = match pixel_count.checked_mul(4) {
113        Some(count) => count,
114        None => return Err(RAYTRACE_ERROR_DIMENSION_OVERFLOW.to_string()),
115    };
116    let samples: u32 = settings.get_samples_per_axis();
117    let sample_scale: f64 = 1.0 / samples as f64;
118    let rays: f64 = (samples * samples) as f64;
119    let bounces: u32 = settings.get_max_bounces();
120    out.clear();
121    out.resize(byte_count, 0);
122    for y in 0..basis.get_height() {
123        for x in 0..basis.get_width() {
124            let mut accumulator: Vector3D = Vector3D::zero();
125            for sy in 0..samples {
126                for sx in 0..samples {
127                    let direction: Vector3D = pixel_direction(
128                        &basis,
129                        x as f64,
130                        y as f64,
131                        (sx as f64 + 0.5) * sample_scale,
132                        (sy as f64 + 0.5) * sample_scale,
133                    );
134                    let ray: Ray = Ray::new(basis.get_eye(), direction)
135                        .with_range(basis.get_near_t(), basis.get_far_t());
136                    accumulator += scene.trace_with_bounces(ray, lights, bounces);
137                }
138            }
139            let linear: Vector3D = accumulator.scaled(1.0 / rays);
140            let offset: usize = ((y as usize) * (basis.get_width() as usize) + x as usize) * 4;
141            out[offset] = gamma_byte(linear.get_x());
142            out[offset + 1] = gamma_byte(linear.get_y());
143            out[offset + 2] = gamma_byte(linear.get_z());
144            out[offset + 3] = 255;
145        }
146    }
147    Ok(())
148}