1#[cfg(feature = "noise")]
2use crate::settings::Noise;
3use crate::settings::{FilterType, Settings};
4use glam::{USizeVec2, Vec2};
5#[cfg(feature = "image")]
6use image::{ImageBuffer, Pixel};
7#[cfg(feature = "noise")]
8use libnoise::prelude::*;
9use ndarray::ArrayViewMut3;
10
11use image_ndarray::prelude::*;
12use num_traits::AsPrimitive;
13
14const RAD: f32 = 57.2957;
16const RING_NORMALIZER: f32 = 0.1;
18const OUTER_BLUR_LIMIT: f32 = 0.4;
20const RADIUS_NORMALIZER: f32 = 0.3;
22
23pub struct Renderer {
61 settings: Settings,
63 resolution: USizeVec2,
65 center: Vec2,
67 blade_degree: f32,
69 radius_px: f32,
71
72 #[cfg(feature = "noise")]
73 noise_generator: Fbm<2, Simplex<2>>,
75}
76
77impl Renderer {
78 pub fn new(settings: Settings, resolution: USizeVec2) -> Self {
80 let center = resolution.as_vec2() * 0.5;
81 let blade_degree = Self::get_blade_degree(settings.blades);
82
83 let radius_px = center * settings.radius - 1.0;
84 let radius_px = radius_px - (radius_px * settings.outer_blur.abs() * RADIUS_NORMALIZER);
85 Self {
86 settings,
87 center,
88 resolution,
89 blade_degree,
90 radius_px: radius_px.max_element(),
91 #[cfg(feature = "noise")]
92 noise_generator: Self::get_noise_generator(settings.noise),
93 }
94 }
95
96 fn get_degrees(&self, position: Vec2) -> f32 {
98 let relative_position = position - self.center;
99 let radians = f32::atan2(relative_position.y, relative_position.x);
100 radians * RAD + self.settings.angle
101 }
102
103 #[cfg(feature = "noise")]
104 fn get_noise_generator(settings: Noise) -> Fbm<2, Simplex<2>> {
108 Source::simplex(settings.seed.max(0) as u64).fbm(settings.octaves as u32, 0.013, 2.0, 0.5)
109 }
110
111 fn get_blade_radius_multiplier(&self, position: Vec2) -> f32 {
115 let degrees = self.get_degrees(position);
116 let mut blades_offset = ((degrees)
117 - (f32::floor(degrees / self.blade_degree) * self.blade_degree))
118 / self.blade_degree;
119 blades_offset -= 0.5;
120 blades_offset = blades_offset.abs();
121
122 let curvature = match self.settings.filter_type {
123 FilterType::DISC => 1.0,
124 _ => self.settings.curvature,
125 };
126
127 (blades_offset - (blades_offset * blades_offset)) * (1.0 - f32::min(curvature, 1.0)) * 2.0
128 }
129
130 fn screen(a: f32, b: f32) -> f32 {
132 a + b - (a * b)
133 }
134
135 fn get_ring_value(&self, pixel_percentage: f32) -> f32 {
137 let ring_range = RING_NORMALIZER * self.settings.ring_size;
138 let mut ring_multiplier = 1.0 - pixel_percentage;
139 ring_multiplier = if ring_multiplier < ring_range && ring_multiplier > 0.0 {
140 1.0
141 } else {
142 0.0
143 };
144 let mut inner_blur_multiplier = 0.0;
145 if self.settings.inner_blur != 0.0 && pixel_percentage < 1.0 {
146 inner_blur_multiplier = pixel_percentage / (1.0 - ring_range);
147 inner_blur_multiplier = inner_blur_multiplier.clamp(0.0, 1.0);
148 inner_blur_multiplier = (inner_blur_multiplier
149 - (1.0 - (self.settings.inner_blur * 2.0)))
150 / (1.0 - (1.0 - (self.settings.inner_blur * 2.0)));
151 inner_blur_multiplier = inner_blur_multiplier.clamp(0.0, 1.0);
152 inner_blur_multiplier = inner_blur_multiplier * inner_blur_multiplier;
153 }
154 let mut outer_blur_multiplier = 0.0;
155 if self.settings.outer_blur != 0.0 && pixel_percentage > 1.0 {
156 outer_blur_multiplier = (pixel_percentage
157 - (1.0 + (self.settings.outer_blur.abs() * OUTER_BLUR_LIMIT)))
158 / (1.0 - (1.0 + (self.settings.outer_blur.abs() * OUTER_BLUR_LIMIT)));
159 outer_blur_multiplier = outer_blur_multiplier.clamp(0.0, 1.0);
160 outer_blur_multiplier = outer_blur_multiplier * outer_blur_multiplier;
161 }
162 ring_multiplier = Self::screen(ring_multiplier, inner_blur_multiplier);
163 ring_multiplier = Self::screen(ring_multiplier, outer_blur_multiplier);
164 ring_multiplier
165 }
166
167 fn get_inner_value(pixel_percentage: f32) -> f32 {
169 if pixel_percentage < 1.0 {
170 return 1.0;
171 }
172 0.0
173 }
174
175 fn get_blade_degree(blades: i32) -> f32 {
177 if blades == 0 {
178 return 0.0;
179 }
180 360.0 / blades as f32
181 }
182
183 fn get_bokeh_value(&self, position: Vec2, _channel: usize) -> f32 {
187 let radius_multiplier = self.get_blade_radius_multiplier(position);
188 let calculated_radius = f32::max(
189 self.radius_px - ((self.radius_px / (self.settings.blades as f32)) * radius_multiplier),
190 0.0,
191 );
192 let pixel_percentage = position.distance(self.center).abs() / calculated_radius;
193 let ring = self.get_ring_value(pixel_percentage);
194 let inner = f32::max(Self::get_inner_value(pixel_percentage) - ring, 0.0);
195 Self::screen(
196 ring * self.settings.ring_color,
197 inner * self.settings.inner_color,
198 )
199 }
200
201 pub fn render_pixel(&self, position: USizeVec2, channel: usize) -> f32 {
203 let offset_multiplier = Vec2::new(
204 3.0 - f32::min(self.settings.aspect_ratio, 1.0) * 2.0,
205 f32::max(self.settings.aspect_ratio, 1.0) * 2.0 - 1.0,
206 );
207 let coordinate = position.as_vec2() * offset_multiplier;
208 let bokeh = self.get_bokeh_value(coordinate, channel);
209 if self.settings.noise.intensity == 0.0 || self.settings.noise.size == 0.0 {
210 return bokeh;
211 }
212
213 self.apply_noise(position, offset_multiplier, bokeh)
214 }
215
216 #[cfg(feature = "noise")]
217 fn apply_noise(&self, position: USizeVec2, offset_multiplier: Vec2, bokeh: f32) -> f32 {
219 let frequency = 1.0 + (1.0 / (self.settings.noise.size * 0.01));
220 let noise_sample_position =
221 (position.as_vec2() - self.center) * offset_multiplier * frequency
222 / self.resolution.as_vec2();
223
224 let mut noise = self.noise_generator.sample([
225 noise_sample_position.x as f64,
226 noise_sample_position.y as f64,
227 ]) as f32;
228 noise = noise.clamp(-1.0, 1.0);
229 noise = (noise + 1.0) * 0.5;
230 noise = noise.powf(2.2);
231 noise * bokeh * self.settings.noise.intensity.clamp(0.0, 1.0)
232 + (bokeh * (1.0 - self.settings.noise.intensity.clamp(0.0, 1.0)))
233 }
234
235 #[cfg(not(feature = "noise"))]
236 fn apply_noise(&self, _: USizeVec2, _: Vec2, bokeh: f32) -> f32 {
238 bokeh
239 }
240
241 #[cfg(feature = "image")]
242 pub fn render_to_image<P, T>(image: &mut ImageBuffer<P, Vec<T>>, settings: Settings)
244 where
245 P: Pixel<Subpixel = T> + Sync,
246 T: Clone + Copy + NormalizedFloat<T> + AsPrimitive<f32> + AsPrimitive<f64> + Default,
247 {
248 Self::render_to_array(settings, &mut image.as_ndarray_mut().view_mut());
249 }
250
251 pub fn render_to_array<T>(settings: Settings, target: &mut ArrayViewMut3<T>)
252 where
253 T: NormalizedFloat<T> + AsPrimitive<f32> + AsPrimitive<f64> + Default,
254 {
255 let resolution = USizeVec2::new(target.dim().1, target.dim().0);
256 let renderer = Self::new(settings, resolution);
257 target
258 .indexed_iter_mut()
259 .for_each(|((y, x, channel), value)| {
260 *value =
261 T::from_f32_normalized(renderer.render_pixel(USizeVec2::new(x, y), channel))
262 .unwrap_or_default()
263 });
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use crate::settings::Noise;
271 use image::{DynamicImage, Rgba32FImage};
272 use image_compare::Algorithm;
273 use rstest::rstest;
274 use std::path::PathBuf;
275
276 fn store_test_result(image: &Rgba32FImage, path: PathBuf) {
278 DynamicImage::from(image.clone())
279 .to_rgb8()
280 .save(path)
281 .unwrap();
282 }
283
284 fn get_comparison_score(a: Rgba32FImage, b: Rgba32FImage) -> f64 {
285 let a = DynamicImage::from(a.clone()).to_luma8();
286 let b = DynamicImage::from(b.clone()).to_luma8();
287
288 image_compare::gray_similarity_structure(&Algorithm::MSSIMSimple, &a, &b)
289 .unwrap()
290 .score
291 }
292
293 fn load_test_image(path: PathBuf) -> Rgba32FImage {
294 let image = image::open(path);
295 image.unwrap().to_rgba32f()
296 }
297
298 #[rstest]
299 #[case(Settings::default(), PathBuf::from("./test/images/1_expected.jpg"))]
300 #[case(
301 Settings {
302 filter_type: FilterType::BLADE,
303 angle: 195.3,
304 curvature: 0.1,
305 ..Default::default()
306 },
307 PathBuf::from("./test/images/2_expected.jpg")
308 )]
309 #[case(
310 Settings {
311 filter_type: FilterType::BLADE,
312 angle: 90.0,
313 blades: 15,
314 ..Default::default()
315 },
316 PathBuf::from("./test/images/3_expected.jpg")
317 )]
318 #[case(
319 Settings {
320 aspect_ratio: 0.5,
321 ..Default::default()
322 },
323 PathBuf::from("./test/images/4_expected.jpg")
324 )]
325 #[case(
326 Settings {
327 aspect_ratio: 2.0,
328 ..Default::default()
329 },
330 PathBuf::from("./test/images/5_expected.jpg")
331 )]
332 #[case(
333 Settings {
334 ring_color: 0.5,
335 inner_color: 0.9,
336 ring_size: 0.5,
337 ..Default::default()
338 },
339 PathBuf::from("./test/images/6_expected.jpg")
340 )]
341 #[case(
342 Settings {
343 noise: {
344 Noise {
345 size: 0.3,
346 intensity: 1.0,
347 ..Default::default()
348 }
349 },
350 ..Default::default()
351 },
352 PathBuf::from("./test/images/7_expected.jpg")
353 )]
354 #[case(
355 Settings {
356 noise: {
357 Noise {
358 intensity: 0.0,
359 ..Default::default()
360 }
361 },
362 ..Default::default()
363 },
364 PathBuf::from("./test/images/8_expected.jpg")
365 )]
366 #[case(
367 Settings {
368 noise: {
369 Noise {
370 seed: 30,
371 ..Default::default()
372 }
373 },
374 ..Default::default()
375 },
376 PathBuf::from("./test/images/9_expected.jpg")
377 )]
378
379 fn test_kernel(#[case] settings: Settings, #[case] expected: PathBuf) {
381 let expected_image = match expected.exists() {
382 true => load_test_image(expected.clone()),
383 false => Rgba32FImage::new(256, 256),
384 };
385
386 let mut result = Rgba32FImage::new(256, 256);
387 Renderer::render_to_image(&mut result, settings);
388
389 if !(expected.clone().exists()) {
390 store_test_result(&result, expected);
391 }
392
393 let score = get_comparison_score(expected_image, result);
394 println!("Test got score: {}", score);
395
396 assert!(score > 0.9); }
398}