Skip to main content

volren_core/
render_params.rs

1//! Render parameters: blend modes, shading, interpolation, clip planes.
2
3use crate::math::Aabb;
4use crate::transfer_function::{ColorTransferFunction, OpacityTransferFunction};
5use crate::window_level::WindowLevel;
6use glam::DVec4;
7
8// ── BlendMode ─────────────────────────────────────────────────────────────────
9
10/// Compositing algorithm used during GPU raycasting.
11///
12/// # VTK Equivalent
13/// `vtkGPUVolumeRayCastMapper::SetBlendMode`.
14#[derive(Debug, Clone, Copy, PartialEq, Default)]
15#[non_exhaustive]
16pub enum BlendMode {
17    /// Front-to-back alpha compositing (default for anatomical rendering).
18    #[default]
19    Composite,
20    /// Maximum intensity projection — displays the brightest voxel along each ray.
21    MaximumIntensity,
22    /// Minimum intensity projection.
23    MinimumIntensity,
24    /// Mean intensity along the ray.
25    AverageIntensity,
26    /// Additive accumulation (unweighted).
27    Additive,
28    /// Render the isosurface at a given scalar value using Phong shading.
29    Isosurface {
30        /// The scalar value defining the isosurface.
31        iso_value: f64,
32    },
33}
34
35// ── Interpolation ─────────────────────────────────────────────────────────────
36
37/// Texture sampling interpolation mode.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39#[non_exhaustive]
40pub enum Interpolation {
41    /// Nearest-neighbour (box) interpolation.
42    Nearest,
43    /// Trilinear interpolation (default, smoother).
44    #[default]
45    Linear,
46}
47
48// ── ShadingParams ─────────────────────────────────────────────────────────────
49
50/// Phong shading parameters used in composite and isosurface modes.
51///
52/// Gradient-magnitude based shading matches VTK's `vtkVolumeProperty::ShadeOn`.
53#[derive(Debug, Clone, Copy, PartialEq)]
54pub struct ShadingParams {
55    /// Ambient term (0–1).
56    pub ambient: f32,
57    /// Diffuse term (0–1).
58    pub diffuse: f32,
59    /// Specular term (0–1).
60    pub specular: f32,
61    /// Specular power (shininess, e.g. 10–100).
62    pub specular_power: f32,
63}
64
65impl Default for ShadingParams {
66    fn default() -> Self {
67        Self {
68            ambient: 0.1,
69            diffuse: 0.7,
70            specular: 0.2,
71            specular_power: 10.0,
72        }
73    }
74}
75
76// ── ClipPlane ─────────────────────────────────────────────────────────────────
77
78/// An oriented half-space clip plane in world space.
79///
80/// Points with `plane · (pos, 1)` < 0 are clipped (not rendered).
81/// Up to 6 planes are supported simultaneously by the GPU shader.
82#[derive(Debug, Clone, Copy, PartialEq)]
83pub struct ClipPlane {
84    /// Plane equation as `(nx, ny, nz, d)` where `nx·x + ny·y + nz·z + d = 0`.
85    /// `(nx, ny, nz)` should be a unit normal.
86    pub equation: DVec4,
87}
88
89impl ClipPlane {
90    /// Create a clip plane from a point on the plane and an outward normal.
91    ///
92    /// Points on the normal-facing side of the plane are **kept**; points
93    /// behind are clipped.
94    #[must_use]
95    pub fn from_point_and_normal(point: glam::DVec3, normal: glam::DVec3) -> Self {
96        let n = normal.normalize();
97        let d = -n.dot(point);
98        Self {
99            equation: DVec4::new(n.x, n.y, n.z, d),
100        }
101    }
102
103    /// Signed distance of `pos` from the plane (positive = kept side).
104    #[must_use]
105    pub fn signed_distance(&self, pos: glam::DVec3) -> f64 {
106        self.equation.x * pos.x
107            + self.equation.y * pos.y
108            + self.equation.z * pos.z
109            + self.equation.w
110    }
111}
112
113// ── VolumeRenderParams ────────────────────────────────────────────────────────
114
115/// All parameters that control how a volume is rendered.
116///
117/// Built using a fluent builder pattern. Start from [`VolumeRenderParams::builder`]
118/// or use [`VolumeRenderParams::default`].
119#[derive(Debug, Clone)]
120pub struct VolumeRenderParams {
121    /// Colour transfer function.
122    pub color_tf: ColorTransferFunction,
123    /// Opacity transfer function.
124    pub opacity_tf: OpacityTransferFunction,
125    /// Optional gradient opacity modulation.
126    pub gradient_opacity_tf: Option<OpacityTransferFunction>,
127    /// Window/level mapping (applied before TF lookup).
128    pub window_level: Option<WindowLevel>,
129    /// Compositing algorithm.
130    pub blend_mode: BlendMode,
131    /// Texture interpolation quality.
132    pub interpolation: Interpolation,
133    /// Phong shading — only used in [`BlendMode::Composite`] and [`BlendMode::Isosurface`].
134    pub shading: Option<ShadingParams>,
135    /// Raycasting step size as a fraction of the smallest voxel spacing (default 0.5).
136    pub step_size_factor: f32,
137    /// Up to 6 world-space clip planes.
138    pub clip_planes: Vec<ClipPlane>,
139    /// Optional axis-aligned cropping box in world coordinates.
140    pub cropping_bounds: Option<Aabb>,
141    /// Background colour RGBA in `[0, 1]`.
142    pub background: [f32; 4],
143}
144
145impl Default for VolumeRenderParams {
146    fn default() -> Self {
147        Self {
148            color_tf: ColorTransferFunction::greyscale(0.0, 1.0),
149            opacity_tf: OpacityTransferFunction::linear_ramp(0.0, 1.0),
150            gradient_opacity_tf: None,
151            window_level: None,
152            blend_mode: BlendMode::default(),
153            interpolation: Interpolation::default(),
154            shading: Some(ShadingParams::default()),
155            step_size_factor: 0.5,
156            clip_planes: Vec::new(),
157            cropping_bounds: None,
158            background: [0.0, 0.0, 0.0, 1.0],
159        }
160    }
161}
162
163impl VolumeRenderParams {
164    /// Start building a new parameter set from the defaults.
165    #[must_use]
166    pub fn builder() -> VolumeRenderParamsBuilder {
167        VolumeRenderParamsBuilder::default()
168    }
169}
170
171/// Fluent builder for [`VolumeRenderParams`].
172#[derive(Debug, Default)]
173pub struct VolumeRenderParamsBuilder {
174    params: VolumeRenderParams,
175}
176
177impl VolumeRenderParamsBuilder {
178    /// Set the blend mode.
179    #[must_use]
180    pub fn blend_mode(mut self, blend_mode: BlendMode) -> Self {
181        self.params.blend_mode = blend_mode;
182        self
183    }
184
185    /// Set the texture interpolation mode.
186    #[must_use]
187    pub fn interpolation(mut self, interpolation: Interpolation) -> Self {
188        self.params.interpolation = interpolation;
189        self
190    }
191
192    /// Enable Phong shading with the given parameters.
193    #[must_use]
194    pub fn shading(mut self, params: ShadingParams) -> Self {
195        self.params.shading = Some(params);
196        self
197    }
198
199    /// Disable shading.
200    #[must_use]
201    pub fn no_shading(mut self) -> Self {
202        self.params.shading = None;
203        self
204    }
205
206    /// Set the raycasting step size factor.
207    #[must_use]
208    pub fn step_size_factor(mut self, step: f32) -> Self {
209        self.params.step_size_factor = step;
210        self
211    }
212
213    /// Set the colour transfer function.
214    #[must_use]
215    pub fn color_tf(mut self, tf: ColorTransferFunction) -> Self {
216        self.params.color_tf = tf;
217        self
218    }
219
220    /// Set the opacity transfer function.
221    #[must_use]
222    pub fn opacity_tf(mut self, tf: OpacityTransferFunction) -> Self {
223        self.params.opacity_tf = tf;
224        self
225    }
226
227    /// Set the gradient-based opacity modulation transfer function.
228    #[must_use]
229    pub fn gradient_opacity_tf(mut self, tf: OpacityTransferFunction) -> Self {
230        self.params.gradient_opacity_tf = Some(tf);
231        self
232    }
233
234    /// Set the window/level mapping.
235    #[must_use]
236    pub fn window_level(mut self, wl: WindowLevel) -> Self {
237        self.params.window_level = Some(wl);
238        self
239    }
240
241    /// Set an axis-aligned cropping box in world coordinates.
242    #[must_use]
243    pub fn cropping_bounds(mut self, bounds: Aabb) -> Self {
244        self.params.cropping_bounds = Some(bounds);
245        self
246    }
247
248    /// Add a clip plane.
249    #[must_use]
250    pub fn clip_plane(mut self, plane: ClipPlane) -> Self {
251        self.params.clip_planes.push(plane);
252        self
253    }
254
255    /// Set the background colour.
256    #[must_use]
257    pub fn background(mut self, rgba: [f32; 4]) -> Self {
258        self.params.background = rgba;
259        self
260    }
261
262    /// Finalise and return the [`VolumeRenderParams`].
263    #[must_use]
264    pub fn build(self) -> VolumeRenderParams {
265        self.params
266    }
267}
268
269// ── Tests ─────────────────────────────────────────────────────────────────────
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274    use approx::assert_abs_diff_eq;
275    use glam::DVec3;
276
277    #[test]
278    fn builder_overrides_defaults() {
279        let params = VolumeRenderParams::builder()
280            .blend_mode(BlendMode::MaximumIntensity)
281            .no_shading()
282            .step_size_factor(0.25)
283            .build();
284        assert_eq!(params.blend_mode, BlendMode::MaximumIntensity);
285        assert!(params.shading.is_none());
286        assert_abs_diff_eq!(params.step_size_factor as f64, 0.25, epsilon = 1e-6);
287    }
288
289    #[test]
290    fn clip_plane_from_point_normal() {
291        let plane = ClipPlane::from_point_and_normal(DVec3::ZERO, DVec3::Y);
292        // Points above Y=0 should be positive
293        let d = plane.signed_distance(DVec3::new(0.0, 1.0, 0.0));
294        assert!(d > 0.0, "expected positive distance, got {d}");
295        // Points below Y=0 should be negative
296        let d2 = plane.signed_distance(DVec3::new(0.0, -1.0, 0.0));
297        assert!(d2 < 0.0);
298    }
299
300    #[test]
301    fn clip_plane_at_point_is_zero() {
302        let plane = ClipPlane::from_point_and_normal(DVec3::new(0.0, 5.0, 0.0), DVec3::Y);
303        let d = plane.signed_distance(DVec3::new(3.0, 5.0, 7.0));
304        assert_abs_diff_eq!(d, 0.0, epsilon = 1e-10);
305    }
306}