1use super::{
4 ColorImage, Image, ImageSize, ThreadPool, pixel, pixel::DistancePixel,
5 voxel,
6};
7use fidget_core::rng::mix;
8use nalgebra::{
9 Const, Matrix2xX, Matrix3, Matrix3xX, OMatrix, Vector2, Vector3, Vector4,
10};
11use rand::prelude::*;
12
13pub fn denoise_normals(
18 image: &voxel::Image,
19 threads: Option<&ThreadPool>,
20) -> voxel::Image {
21 let radius = 2;
22 let mut out = voxel::Image::new(image.size());
23 out.apply_effect(
24 |x, y| {
25 let depth = image[(y, x)].depth;
26 let normal = if depth > 0 {
27 denoise_pixel(image, x, y, radius)
28 } else {
29 [0.0; 3]
30 };
31 voxel::GeometryPixel { depth, normal }
32 },
33 threads,
34 );
35 out
36}
37
38pub fn apply_shading(
43 image: &voxel::Image,
44 ssao: bool,
45 threads: Option<&ThreadPool>,
46) -> ColorImage {
47 let ssao = if ssao {
48 let ssao = compute_ssao(image, threads);
49 Some(blur_ssao(&ssao, threads))
50 } else {
51 None
52 };
53
54 let size = image.size();
55 let mut out = ColorImage::new(ImageSize::new(size.width(), size.height()));
56 out.apply_effect(
57 |x, y| {
58 if image[(y, x)].depth > 0 {
59 shade_pixel(image, ssao.as_ref(), x, y)
60 } else {
61 [0u8; 3]
62 }
63 },
64 threads,
65 );
66 out
67}
68
69pub fn compute_ssao(
74 image: &voxel::Image,
75 threads: Option<&ThreadPool>,
76) -> Image<f32> {
77 let ssao_kernel = ssao_kernel(64);
78 let ssao_noise = ssao_noise(16 * 16);
79
80 let size = image.size();
81 let mut out =
82 Image::<f32>::new(ImageSize::new(size.width(), size.height()));
83 out.apply_effect(
84 |x, y| {
85 if image[(y, x)].depth > 0 {
86 compute_pixel_ssao(image, x, y, &ssao_kernel, &ssao_noise)
87 } else {
88 f32::NAN
89 }
90 },
91 threads,
92 );
93
94 out
95}
96
97pub fn blur_ssao(
99 ssao: &Image<f32>,
100 threads: Option<&ThreadPool>,
101) -> Image<f32> {
102 let mut out = Image::<f32>::new(ssao.size());
103 let blur_radius = 2;
104 out.apply_effect(
105 |x, y| {
106 if ssao[(y, x)].is_nan() {
107 f32::NAN
108 } else {
109 compute_pixel_blur(ssao, x, y, blur_radius)
110 }
111 },
112 threads,
113 );
114 out
115}
116
117fn shade_pixel(
119 image: &voxel::Image,
120 ssao: Option<&Image<f32>>,
121 x: usize,
122 y: usize,
123) -> [u8; 3] {
124 let [nx, ny, nz] = image[(y, x)].normal;
125 let n = Vector3::new(nx, ny, nz).normalize();
126
127 let p = Vector3::new(
129 2.0 * (x as f32 / image.width() as f32 - 0.5),
130 2.0 * (y as f32 / image.height() as f32 - 0.5),
131 2.0 * (image[(y, x)].depth as f32 / image.depth() as f32 - 0.5),
132 );
133
134 let lights = [
135 Vector4::new(5.0, -5.0, 10.0, 0.5),
136 Vector4::new(-5.0, 0.0, 10.0, 0.15),
137 Vector4::new(0.0, -5.0, 10.0, 0.15),
138 ];
139 let mut accum = 0.2; for light in lights {
141 let light_dir = (light.xyz() - p).normalize();
142 accum += light_dir.dot(&n).max(0.0) * light.w;
143 }
144 if let Some(ssao) = ssao {
145 accum *= ssao[(y, x)] * 0.6 + 0.4;
146 }
147
148 accum = accum.clamp(0.0, 1.0);
149
150 let c = (accum * 255.0) as u8;
151 [c, c, c]
152}
153
154fn compute_pixel_ssao(
158 image: &voxel::Image,
159 x: usize,
160 y: usize,
161 kernel: &OMatrix<f32, Const<3>, nalgebra::Dyn>,
162 noise: &OMatrix<f32, Const<2>, nalgebra::Dyn>,
163) -> f32 {
164 let pos = (y, x);
165 let voxel::GeometryPixel {
166 normal: [nx, ny, nz],
167 depth: d,
168 } = image[pos];
169
170 if d == 0 {
171 return f32::NAN;
172 }
173
174 let scale_min = image
176 .size
177 .width()
178 .min(image.size.height())
179 .min(image.size.depth()) as f32;
180 let (scale_x, scale_y, scale_z) = (
181 scale_min / image.size.width() as f32,
182 scale_min / image.size.height() as f32,
183 scale_min / image.size.depth() as f32,
184 );
185
186 let p = Vector3::new(
190 (((x as f32 + 0.5) / image.width() as f32) - 0.5) * 2.0,
191 (((y as f32 + 0.5) / image.height() as f32) - 0.5) * 2.0,
192 ((d as f32 / image.depth() as f32) - 0.5) * 2.0,
193 );
194
195 let n = Vector3::new(nx, ny, nz).normalize();
197
198 let rvec =
200 noise.column(mix(pos.0 as u32, pos.1 as u32) as usize % noise.ncols());
201 let rvec = Vector3::new(rvec.x, rvec.y, 0.0);
202
203 let tangent = (rvec - n * rvec.dot(&n)).normalize();
205 let bitangent = n.cross(&tangent);
206 let tbn = Matrix3::from_columns(&[tangent, bitangent, n]);
207
208 const RADIUS: f32 = 0.1;
209 let mut occlusion = 0.0;
210 for i in 0..kernel.ncols() {
211 let mut offset = tbn * kernel.column(i) * RADIUS;
213 offset.x *= scale_x;
214 offset.y *= scale_y;
215 offset.z *= scale_z;
216
217 let sample_pos = offset + p;
219
220 let px = ((sample_pos.x / 2.0) + 0.5) * image.width() as f32;
222 let py = ((sample_pos.y / 2.0) + 0.5) * image.height() as f32;
223
224 let actual_h = if px < image.width() as f32
226 && py < image.height() as f32
227 && px > 0.0
228 && py > 0.0
229 {
230 image[(py as usize, px as usize)].depth
231 } else {
232 0
233 };
234
235 let actual_z = ((actual_h as f32 / image.depth() as f32) - 0.5) * 2.0;
236
237 let dz = sample_pos.z - actual_z;
238 if dz < RADIUS {
239 occlusion += (sample_pos.z <= actual_z) as usize as f32;
240 } else if dz < RADIUS * 2.0 && sample_pos.z <= actual_z {
241 occlusion += ((RADIUS - (dz - RADIUS)) / RADIUS).powi(2);
242 }
243 }
244 1.0 - (occlusion / kernel.ncols() as f32)
245}
246
247fn denoise_pixel(
249 image: &voxel::Image,
250 x: usize,
251 y: usize,
252 denoise_radius: isize,
253) -> [f32; 3] {
254 let n = image[(y, x)].normal;
255 if n[2] > 0.0 {
256 return n;
257 }
258 let x = x as isize;
259 let y = y as isize;
260 [
261 (0, 0),
262 (-denoise_radius, 0),
263 (0, -denoise_radius),
264 (-denoise_radius, -denoise_radius),
265 ]
266 .into_iter()
267 .flat_map(|(xmin, ymin)| {
268 let mut sum = Vector3::zeros();
269 let mut count = 0;
270 for i in 0..=denoise_radius {
271 for j in 0..=denoise_radius {
272 let tx = x + xmin + i;
273 let ty = y + ymin + j;
274 if tx >= 0
275 && ty >= 0
276 && (tx as usize) < image.width()
277 && (ty as usize) < image.height()
278 {
279 let pos = (ty as usize, tx as usize);
280 if image[pos].depth != 0 && image[pos].normal[2] > 0.0 {
281 let n = image[pos].normal;
282 sum += Vector3::new(n[0], n[1], n[2]);
283 count += 1;
284 }
285 }
286 }
287 }
288 if count == 0 {
289 return None;
290 }
291 let mean = sum / count as f32;
292 let mut score = 0.0;
293 for i in 0..=denoise_radius {
294 for j in 0..=denoise_radius {
295 let tx = x + xmin + i;
296 let ty = y + ymin + j;
297 if tx >= 0
298 && ty >= 0
299 && (tx as usize) < image.width()
300 && (ty as usize) < image.height()
301 {
302 let pos = (ty as usize, tx as usize);
303 if image[pos].depth != 0 {
304 let n = image[pos].normal;
305 score += Vector3::new(n[0], n[1], n[2]).dot(&mean);
306 }
307 }
308 }
309 }
310 Some((score, mean.into()))
311 })
312 .max_by_key(|(score, _mean)| ordered_float::OrderedFloat(*score))
313 .unwrap_or((0.0, n))
314 .1
315}
316
317fn compute_pixel_blur(
319 ssao: &Image<f32>,
320 x: usize,
321 y: usize,
322 blur_radius: isize,
323) -> f32 {
324 let x = x as isize;
325 let y = y as isize;
326 [
327 (0, 0),
328 (-blur_radius, 0),
329 (0, -blur_radius),
330 (-blur_radius, -blur_radius),
331 ]
332 .into_iter()
333 .flat_map(|(xmin, ymin)| {
334 let mut sum = 0.0;
335 let mut count = 0;
336 for i in 0..=blur_radius {
337 for j in 0..=blur_radius {
338 let tx = x + xmin + i;
339 let ty = y + ymin + j;
340 if tx >= 0
341 && ty >= 0
342 && (tx as usize) < ssao.width()
343 && (ty as usize) < ssao.height()
344 {
345 let s = ssao[(ty as usize, tx as usize)];
346 if !s.is_nan() {
347 sum += s;
348 count += 1;
349 }
350 }
351 }
352 }
353 if count == 0 {
354 return None;
355 }
356 let mean = sum / count as f32;
357 let mut stdev = 0.0;
358 for i in 0..=blur_radius {
359 for j in 0..=blur_radius {
360 let tx = x + xmin + i;
361 let ty = y + ymin + j;
362 if tx >= 0
363 && ty >= 0
364 && (tx as usize) < ssao.width()
365 && (ty as usize) < ssao.height()
366 {
367 let s = ssao[(ty as usize, tx as usize)];
368 if !s.is_nan() {
369 stdev += (mean - s).powi(2);
370 }
371 }
372 }
373 }
374 Some((stdev / count as f32, mean))
375 })
376 .min_by_key(|(stdev, _mean)| ordered_float::OrderedFloat(*stdev))
377 .unwrap_or_else(|| (0.0, ssao[(y as usize, x as usize)]))
378 .1
379}
380
381pub fn ssao_kernel(n: usize) -> OMatrix<f32, Const<3>, nalgebra::Dyn> {
388 use rand::prelude::*;
390
391 let mut kernel = Matrix3xX::<f32>::zeros(n);
392 let mut rng = rand::rng();
393 let xy_range = rand::distr::Uniform::new_inclusive(-1.0, 1.0).unwrap();
394 let z_range = rand::distr::Uniform::new_inclusive(0.0, 1.0).unwrap();
395
396 for i in 0..n {
397 loop {
398 let row = Vector3::<f32>::new(
399 rng.sample(xy_range),
400 rng.sample(xy_range),
401 rng.sample(z_range),
402 );
403 if row.norm() < 1.0 && row.norm() > f32::EPSILON {
405 let scale =
407 ((i as f32) / (kernel.ncols() as f32 - 1.0)).powi(2) * 0.9
408 + 0.1;
409 kernel.set_column(i, &(row * scale / row.norm()));
410 break;
411 }
412 }
413 }
414 kernel
415}
416
417pub fn ssao_noise(n: usize) -> OMatrix<f32, Const<2>, nalgebra::Dyn> {
422 let mut noise = Matrix2xX::<f32>::zeros(n);
423 let mut rng = rand::rng();
424 let xy_range = rand::distr::Uniform::new_inclusive(-1.0, 1.0).unwrap();
425 for i in 0..n {
426 loop {
427 let row =
428 Vector2::<f32>::new(rng.sample(xy_range), rng.sample(xy_range));
429 if row.norm() < 1.0 && row.norm() > f32::EPSILON {
431 noise.set_column(i, &(row / row.norm()));
432 break;
433 }
434 }
435 }
436 noise
437}
438
439pub fn to_rgba_bitmap(
444 image: pixel::Image,
445 transparent: bool,
446 threads: Option<&ThreadPool>,
447) -> Image<[u8; 4]> {
448 let mut out = Image::new(image.size());
449 out.apply_effect(
450 |x, y| {
451 let p = image[(y, x)];
452 if p.inside() {
453 [255u8; 4]
454 } else if transparent {
455 [0u8; 4]
456 } else {
457 [0, 0, 0, 255]
458 }
459 },
460 threads,
461 );
462 out
463}
464
465pub fn to_debug_bitmap(
467 image: pixel::Image,
468 threads: Option<&ThreadPool>,
469) -> Image<[u8; 4]> {
470 let mut out = Image::new(image.size());
471 out.apply_effect(
472 |x, y| match image[(y, x)].unpack() {
473 DistancePixel::Value(v) => {
474 if v < 0.0 {
475 [255u8; 4]
476 } else {
477 [0, 0, 0, 255]
478 }
479 }
480 DistancePixel::Fill { depth, inside } => match (depth, inside) {
482 (0, true) => [255, 0, 0, 255],
483 (0, false) => [50, 0, 0, 255],
484 (1, true) => [0, 255, 0, 255],
485 (1, false) => [0, 50, 0, 255],
486 (2, true) => [0, 0, 255, 255],
487 (2, false) => [0, 0, 50, 255],
488 (_, true) => [255, 255, 0, 255],
489 (_, false) => [50, 50, 0, 255],
490 },
491 },
492 threads,
493 );
494 out
495}
496
497pub fn to_rgba_distance(
505 image: pixel::Image,
506 threads: Option<&ThreadPool>,
507) -> Image<[u8; 4]> {
508 let mut out = Image::new(image.size());
509 out.apply_effect(
510 |x, y| match image[(y, x)].unpack() {
511 DistancePixel::Value(f) if f.is_nan() => [255, 0, 0, 255],
512 DistancePixel::Value(f) => {
513 let r = 1.0 - 0.1f32.copysign(f);
514 let g = 1.0 - 0.4f32.copysign(f);
515 let b = 1.0 - 0.7f32.copysign(f);
516
517 let dim = 1.0 - (-4.0 * f.abs()).exp(); let bands = 0.8 + 0.2 * (140.0 * f).cos(); let smoothstep = |edge0: f32, edge1: f32, x: f32| {
521 let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
522 t * t * (3.0 - 2.0 * t)
523 };
524 let mix = |x: f32, y: f32, a: f32| x * (1.0 - a) + y * a;
525
526 let run = |v: f32| {
527 let mut v = v * dim * bands;
528 v = mix(v, 1.0, 1.0 - smoothstep(0.0, 0.015, f.abs()));
529 v = mix(v, 1.0, 1.0 - smoothstep(0.0, 0.005, f.abs()));
530 (v.clamp(0.0, 1.0) * 255.0) as u8
531 };
532
533 [run(r), run(g), run(b), 255]
534 }
535 DistancePixel::Fill { inside, .. } => {
537 if inside {
538 [184, 235, 255, 255]
539 } else {
540 [217, 144, 72, 255]
541 }
542 }
543 },
544 threads,
545 );
546 out
547}