1use crate::math::Aabb;
4use crate::transfer_function::{ColorTransferFunction, OpacityTransferFunction};
5use crate::window_level::WindowLevel;
6use glam::DVec4;
7
8#[derive(Debug, Clone, Copy, PartialEq, Default)]
15#[non_exhaustive]
16pub enum BlendMode {
17 #[default]
19 Composite,
20 MaximumIntensity,
22 MinimumIntensity,
24 AverageIntensity,
26 Additive,
28 Isosurface {
30 iso_value: f64,
32 },
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39#[non_exhaustive]
40pub enum Interpolation {
41 Nearest,
43 #[default]
45 Linear,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq)]
54pub struct ShadingParams {
55 pub ambient: f32,
57 pub diffuse: f32,
59 pub specular: f32,
61 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#[derive(Debug, Clone, Copy, PartialEq)]
83pub struct ClipPlane {
84 pub equation: DVec4,
87}
88
89impl ClipPlane {
90 #[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 #[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#[derive(Debug, Clone)]
120pub struct VolumeRenderParams {
121 pub color_tf: ColorTransferFunction,
123 pub opacity_tf: OpacityTransferFunction,
125 pub gradient_opacity_tf: Option<OpacityTransferFunction>,
127 pub window_level: Option<WindowLevel>,
129 pub blend_mode: BlendMode,
131 pub interpolation: Interpolation,
133 pub shading: Option<ShadingParams>,
135 pub step_size_factor: f32,
137 pub clip_planes: Vec<ClipPlane>,
139 pub cropping_bounds: Option<Aabb>,
141 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 #[must_use]
166 pub fn builder() -> VolumeRenderParamsBuilder {
167 VolumeRenderParamsBuilder::default()
168 }
169}
170
171#[derive(Debug, Default)]
173pub struct VolumeRenderParamsBuilder {
174 params: VolumeRenderParams,
175}
176
177impl VolumeRenderParamsBuilder {
178 #[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 #[must_use]
187 pub fn interpolation(mut self, interpolation: Interpolation) -> Self {
188 self.params.interpolation = interpolation;
189 self
190 }
191
192 #[must_use]
194 pub fn shading(mut self, params: ShadingParams) -> Self {
195 self.params.shading = Some(params);
196 self
197 }
198
199 #[must_use]
201 pub fn no_shading(mut self) -> Self {
202 self.params.shading = None;
203 self
204 }
205
206 #[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 #[must_use]
215 pub fn color_tf(mut self, tf: ColorTransferFunction) -> Self {
216 self.params.color_tf = tf;
217 self
218 }
219
220 #[must_use]
222 pub fn opacity_tf(mut self, tf: OpacityTransferFunction) -> Self {
223 self.params.opacity_tf = tf;
224 self
225 }
226
227 #[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 #[must_use]
236 pub fn window_level(mut self, wl: WindowLevel) -> Self {
237 self.params.window_level = Some(wl);
238 self
239 }
240
241 #[must_use]
243 pub fn cropping_bounds(mut self, bounds: Aabb) -> Self {
244 self.params.cropping_bounds = Some(bounds);
245 self
246 }
247
248 #[must_use]
250 pub fn clip_plane(mut self, plane: ClipPlane) -> Self {
251 self.params.clip_planes.push(plane);
252 self
253 }
254
255 #[must_use]
257 pub fn background(mut self, rgba: [f32; 4]) -> Self {
258 self.params.background = rgba;
259 self
260 }
261
262 #[must_use]
264 pub fn build(self) -> VolumeRenderParams {
265 self.params
266 }
267}
268
269#[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 let d = plane.signed_distance(DVec3::new(0.0, 1.0, 0.0));
294 assert!(d > 0.0, "expected positive distance, got {d}");
295 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}