1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//! Bitmap-based "closest opposite-color pixel" bruteforce search.
//!
//! This algorithm is a commonly implemented one, but a naive and inefficient one.
//!
//! It was most notably popularized by Valve in their 2007 SIGGRAPH entry,
//! on using 2D texture-based SDFs to render text (or any vector shapes) in 3D environments,
//! ["Improved Alpha-Tested Magnification for Vector Textures and Special Effects"](https://steamcdn-a.akamaihd.net/apps/valve/2007/SIGGRAPH2007_AlphaTestedMagnification.pdf).

use crate::{Bitmap, Image2d, Unorm8};

// HACK(eddyb) only exists to allow toggling precision for testing purposes.
#[cfg(sdfer_use_f64_instead_of_f32)]
type f32 = f64;

pub fn sdf(bitmap: &Bitmap, sdf_size: usize, spread: usize) -> Image2d<Unorm8> {
    let (w, h) = (bitmap.width(), bitmap.height());

    assert!(w.is_power_of_two() && h.is_power_of_two() && sdf_size.is_power_of_two());
    let scale = w.max(h) / sdf_size;
    assert_ne!(scale, 0);

    let spread = spread * scale;

    let width = w / scale;
    let height = h / scale;
    Image2d::from_fn(width, height, |x, y| {
        let (x, y) = (x * scale + scale / 2, y * scale + scale / 2);
        let inside = bitmap.get(x, y);
        // FIXME(eddyb) this could use a spiral search, and maybe better bitmap
        // access, e.g. "which bits in a block are different than the center x,y".
        let dist = (((y.saturating_sub(spread)..=(y + spread))
            .flat_map(|y2| (x.saturating_sub(spread)..=(x + spread)).map(move |x2| (x2, y2)))
            .filter(|&(x2, y2)| x2 < w && y2 < h && bitmap.get(x2, y2) != inside)
            .map(|(x2, y2)| x2.abs_diff(x).pow(2) + y2.abs_diff(y).pow(2))
            .min()
            .unwrap_or(usize::MAX) as f32)
            .sqrt()
            / (spread as f32))
            .clamp(0.0, 1.0);
        let signed_dist = if inside { -dist } else { dist };

        // [-1, +1] -> [0, 1]
        Unorm8::encode((signed_dist + 1.0) / 2.0)
    })
}