Skip to main content

oxicuda_nerf/rendering/
volume_render.rs

1//! Volume rendering equations for NeRF.
2//!
3//! Standard NeRF volume rendering:
4//! - `alpha_i = 1 - exp(-max(0, sigma_i) * delta_i)`
5//! - `T_i = exp(-sum_{j<i} sigma_j * delta_j) = prod_{j<i}(1 - alpha_j)`
6//! - `C(r) = sum_i T_i * alpha_i * c_i`
7//! - `D(r) = sum_i T_i * alpha_i * t_i`
8//! - `O(r) = sum_i T_i * alpha_i = 1 - T_N`
9
10use crate::error::{NerfError, NerfResult};
11
12// ─── RenderResult ─────────────────────────────────────────────────────────────
13
14/// Output of volume rendering for a single ray.
15#[derive(Debug, Clone, Copy, Default)]
16pub struct RenderResult {
17    /// Rendered RGB color.
18    pub rgb: [f32; 3],
19    /// Expected depth (weighted by transmittance × alpha).
20    pub depth: f32,
21    /// Accumulated opacity (0 = transparent, 1 = fully opaque).
22    pub opacity: f32,
23}
24
25// ─── Volume render (single ray) ───────────────────────────────────────────────
26
27/// Volume render a single ray.
28///
29/// - `sigma`: density values `[N]` (non-negative; negatives are clamped to 0).
30/// - `color`: RGB values `[N * 3]` in [0, 1].
31/// - `t_vals`: sample positions `[N]` along the ray.
32///
33/// Last segment uses delta = 1e10 (infinity).
34///
35/// # Errors
36///
37/// Returns `DimensionMismatch` if sizes don't match,
38/// `EmptyInput` if N == 0,
39/// `VolumeRenderError` for invalid configurations.
40pub fn volume_render(sigma: &[f32], color: &[f32], t_vals: &[f32]) -> NerfResult<RenderResult> {
41    let n = sigma.len();
42    if n == 0 {
43        return Err(NerfError::EmptyInput);
44    }
45    if color.len() != n * 3 {
46        return Err(NerfError::DimensionMismatch {
47            expected: n * 3,
48            got: color.len(),
49        });
50    }
51    if t_vals.len() != n {
52        return Err(NerfError::DimensionMismatch {
53            expected: n,
54            got: t_vals.len(),
55        });
56    }
57
58    let mut rgb = [0.0_f32; 3];
59    let mut depth = 0.0_f32;
60    let mut opacity = 0.0_f32;
61    let mut transmittance = 1.0_f32;
62
63    for i in 0..n {
64        // Delta: distance to next sample (last gets "infinity")
65        let delta = if i + 1 < n {
66            (t_vals[i + 1] - t_vals[i]).max(0.0)
67        } else {
68            1e10_f32
69        };
70
71        let sigma_i = sigma[i].max(0.0);
72        let alpha = 1.0 - (-sigma_i * delta).exp();
73        let weight = transmittance * alpha;
74
75        rgb[0] += weight * color[i * 3];
76        rgb[1] += weight * color[i * 3 + 1];
77        rgb[2] += weight * color[i * 3 + 2];
78        depth += weight * t_vals[i];
79        opacity += weight;
80
81        transmittance *= 1.0 - alpha;
82
83        // Early termination when transmittance is essentially zero
84        if transmittance < 1e-4 {
85            break;
86        }
87    }
88
89    Ok(RenderResult {
90        rgb,
91        depth,
92        opacity,
93    })
94}
95
96// ─── Volume render (batch) ────────────────────────────────────────────────────
97
98/// Volume render a batch of M rays.
99///
100/// - `sigma`: `[M * N]` densities, row-major (ray-major).
101/// - `color`: `[M * N * 3]` RGB colors.
102/// - `t_vals`: `[M * N]` sample positions (same layout as sigma).
103/// - `n_rays`: M
104/// - `n_samples`: N
105///
106/// Returns `(rgb: [M*3], depth: [M], opacity: [M])`.
107///
108/// # Errors
109///
110/// Returns `DimensionMismatch` or `EmptyInput` for inconsistent inputs.
111pub fn volume_render_batch(
112    sigma: &[f32],
113    color: &[f32],
114    t_vals: &[f32],
115    n_rays: usize,
116    n_samples: usize,
117) -> NerfResult<(Vec<f32>, Vec<f32>, Vec<f32>)> {
118    if n_rays == 0 || n_samples == 0 {
119        return Err(NerfError::EmptyInput);
120    }
121    if sigma.len() != n_rays * n_samples {
122        return Err(NerfError::DimensionMismatch {
123            expected: n_rays * n_samples,
124            got: sigma.len(),
125        });
126    }
127    if color.len() != n_rays * n_samples * 3 {
128        return Err(NerfError::DimensionMismatch {
129            expected: n_rays * n_samples * 3,
130            got: color.len(),
131        });
132    }
133    if t_vals.len() != n_rays * n_samples {
134        return Err(NerfError::DimensionMismatch {
135            expected: n_rays * n_samples,
136            got: t_vals.len(),
137        });
138    }
139
140    let mut rgb_out = vec![0.0_f32; n_rays * 3];
141    let mut depth_out = vec![0.0_f32; n_rays];
142    let mut opacity_out = vec![0.0_f32; n_rays];
143
144    for ray_idx in 0..n_rays {
145        let s_off = ray_idx * n_samples;
146        let c_off = ray_idx * n_samples * 3;
147
148        let ray_sigma = &sigma[s_off..s_off + n_samples];
149        let ray_color = &color[c_off..c_off + n_samples * 3];
150        let ray_t = &t_vals[s_off..s_off + n_samples];
151
152        let res = volume_render(ray_sigma, ray_color, ray_t)?;
153
154        rgb_out[ray_idx * 3] = res.rgb[0];
155        rgb_out[ray_idx * 3 + 1] = res.rgb[1];
156        rgb_out[ray_idx * 3 + 2] = res.rgb[2];
157        depth_out[ray_idx] = res.depth;
158        opacity_out[ray_idx] = res.opacity;
159    }
160
161    Ok((rgb_out, depth_out, opacity_out))
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn empty_scene_zero_opacity() {
170        let sigma = vec![0.0_f32; 8];
171        let color = vec![1.0_f32; 24]; // white
172        let t = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8];
173        let res = volume_render(&sigma, &color, &t).unwrap();
174        assert!(res.opacity < 1e-6, "zero density → zero opacity");
175    }
176
177    #[test]
178    fn opaque_first_sample() {
179        let mut sigma = vec![0.0_f32; 8];
180        sigma[0] = 1e6_f32; // extremely dense first sample
181        let mut color = vec![0.0_f32; 24];
182        // First sample is red
183        color[0] = 1.0;
184        let t = vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8];
185        let res = volume_render(&sigma, &color, &t).unwrap();
186        assert!(
187            res.rgb[0] > 0.99,
188            "red first sample dominant, got {}",
189            res.rgb[0]
190        );
191        assert!(res.opacity > 0.99, "high opacity, got {}", res.opacity);
192    }
193
194    #[test]
195    fn batch_render_shape() {
196        let n_rays = 4;
197        let n_samp = 8;
198        let sigma = vec![0.1_f32; n_rays * n_samp];
199        let color = vec![0.5_f32; n_rays * n_samp * 3];
200        let t: Vec<f32> = (0..n_rays * n_samp).map(|i| i as f32 * 0.1 + 0.1).collect();
201        let (rgb, depth, opacity) =
202            volume_render_batch(&sigma, &color, &t, n_rays, n_samp).unwrap();
203        assert_eq!(rgb.len(), n_rays * 3);
204        assert_eq!(depth.len(), n_rays);
205        assert_eq!(opacity.len(), n_rays);
206    }
207}