volren_core/transfer_function/
two_d.rs1#[derive(Debug, Clone, Copy, PartialEq)]
7pub struct TransferFunction2DRegion {
8 pub scalar_range: [f64; 2],
10 pub gradient_range: [f64; 2],
12 pub rgba: [f64; 4],
14}
15
16impl TransferFunction2DRegion {
17 #[must_use]
19 pub fn new(scalar_range: [f64; 2], gradient_range: [f64; 2], rgba: [f64; 4]) -> Self {
20 Self {
21 scalar_range,
22 gradient_range,
23 rgba,
24 }
25 }
26
27 #[must_use]
29 pub fn contains(&self, scalar: f64, gradient: f64) -> bool {
30 scalar >= self.scalar_range[0]
31 && scalar <= self.scalar_range[1]
32 && gradient >= self.gradient_range[0]
33 && gradient <= self.gradient_range[1]
34 }
35}
36
37#[derive(Debug, Clone, Default)]
42pub struct TransferFunction2D {
43 regions: Vec<TransferFunction2DRegion>,
44 background: [f64; 4],
45}
46
47impl TransferFunction2D {
48 #[must_use]
50 pub fn new() -> Self {
51 Self::default()
52 }
53
54 #[must_use]
56 pub fn with_background(mut self, rgba: [f64; 4]) -> Self {
57 self.background = rgba;
58 self
59 }
60
61 pub fn add_region(&mut self, region: TransferFunction2DRegion) {
63 self.regions.push(region);
64 }
65
66 pub fn remove_region(&mut self, index: usize) -> Option<TransferFunction2DRegion> {
68 if index < self.regions.len() {
69 Some(self.regions.remove(index))
70 } else {
71 None
72 }
73 }
74
75 #[must_use]
77 pub fn regions(&self) -> &[TransferFunction2DRegion] {
78 &self.regions
79 }
80
81 #[must_use]
85 pub fn evaluate(&self, scalar: f64, gradient: f64) -> [f64; 4] {
86 self.regions
87 .iter()
88 .filter(|region| region.contains(scalar, gradient))
89 .fold(self.background, |dst, region| alpha_over(dst, region.rgba))
90 }
91}
92
93fn alpha_over(dst: [f64; 4], src: [f64; 4]) -> [f64; 4] {
94 let src_a = src[3].clamp(0.0, 1.0);
95 let dst_a = dst[3].clamp(0.0, 1.0);
96 let out_a = src_a + dst_a * (1.0 - src_a);
97 if out_a <= f64::EPSILON {
98 return [0.0, 0.0, 0.0, 0.0];
99 }
100
101 let blend_channel =
102 |src_c: f64, dst_c: f64| (src_c * src_a + dst_c * dst_a * (1.0 - src_a)) / out_a;
103
104 [
105 blend_channel(src[0], dst[0]),
106 blend_channel(src[1], dst[1]),
107 blend_channel(src[2], dst[2]),
108 out_a,
109 ]
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use approx::assert_abs_diff_eq;
116
117 #[test]
118 fn empty_returns_background() {
119 let tf = TransferFunction2D::new().with_background([0.1, 0.2, 0.3, 0.4]);
120 assert_eq!(tf.evaluate(1.0, 2.0), [0.1, 0.2, 0.3, 0.4]);
121 }
122
123 #[test]
124 fn matching_region_returns_rgba() {
125 let mut tf = TransferFunction2D::new();
126 tf.add_region(TransferFunction2DRegion::new(
127 [100.0, 200.0],
128 [0.0, 1.0],
129 [1.0, 0.9, 0.8, 0.7],
130 ));
131 let rgba = tf.evaluate(150.0, 0.5);
132 assert_abs_diff_eq!(rgba[0], 1.0, epsilon = 1e-12);
133 assert_abs_diff_eq!(rgba[1], 0.9, epsilon = 1e-12);
134 assert_abs_diff_eq!(rgba[2], 0.8, epsilon = 1e-12);
135 assert_abs_diff_eq!(rgba[3], 0.7, epsilon = 1e-12);
136 }
137
138 #[test]
139 fn non_matching_region_ignored() {
140 let mut tf = TransferFunction2D::new();
141 tf.add_region(TransferFunction2DRegion::new(
142 [100.0, 200.0],
143 [2.0, 3.0],
144 [1.0, 0.0, 0.0, 1.0],
145 ));
146 assert_eq!(tf.evaluate(150.0, 0.5), [0.0, 0.0, 0.0, 0.0]);
147 }
148
149 #[test]
150 fn overlapping_regions_alpha_composite() {
151 let mut tf = TransferFunction2D::new();
152 tf.add_region(TransferFunction2DRegion::new(
153 [0.0, 10.0],
154 [0.0, 10.0],
155 [1.0, 0.0, 0.0, 0.5],
156 ));
157 tf.add_region(TransferFunction2DRegion::new(
158 [0.0, 10.0],
159 [0.0, 10.0],
160 [0.0, 0.0, 1.0, 0.5],
161 ));
162
163 let rgba = tf.evaluate(5.0, 5.0);
164 assert_abs_diff_eq!(rgba[3], 0.75, epsilon = 1e-12);
165 }
166}