lumo/tracer/
microfacet.rs

1use crate::tracer::{ onb::Onb, Color };
2use crate::{ Normal, Direction, Float, Vec2 };
3
4/// Configurable parameters for a microsurface
5#[derive(Copy, Clone)]
6pub struct MicrofacetConfig {
7    /// Roughness of the surface (α) [0,1]
8    pub roughness: Float,
9    /// Refraction index of the material >= 1.0
10    pub refraction_idx: Float,
11    /// Ratio of how metallic the material is [0,1]
12    pub metallicity: Float,
13    /// Transparency of the material
14    pub transparent: bool,
15}
16
17impl MicrofacetConfig {
18    pub fn new(
19        roughness: Float,
20        refraction_idx: Float,
21        metallicity: Float,
22        transparent: bool
23    ) -> Self {
24        assert!((0.0..=1.0).contains(&roughness));
25        assert!((0.0..=1.0).contains(&metallicity));
26        assert!(refraction_idx >= 1.0);
27
28        Self {
29            roughness: roughness.max(1e-5),
30            refraction_idx,
31            metallicity,
32            transparent,
33        }
34    }
35}
36
37/// Defines a distribution of normals for a microfacet. `Float` parameter is the
38/// roughness (α) of the surface.
39#[derive(Copy, Clone)]
40pub enum MfDistribution {
41    /// Walter et al. 2007
42    Ggx(MicrofacetConfig),
43    /// Beckmann et al. 1987
44    Beckmann(MicrofacetConfig),
45}
46
47impl MfDistribution {
48    pub fn new(
49        roughness: Float,
50        refraction_idx: Float,
51        metallicity: Float,
52        transparent: bool
53    ) -> Self {
54        Self::Ggx(MicrofacetConfig::new(roughness, refraction_idx, metallicity, transparent))
55    }
56
57    /// Is the material transparent?
58    pub fn is_transparent(&self) -> bool {
59        self.get_config().transparent
60    }
61
62    /// might need tuning, send ratio that emittance is multiplied with?
63    pub fn is_specular(&self) -> bool {
64        self.is_transparent() || self.get_config().roughness < 0.01
65    }
66
67    /// Does the material have delta scattering distribution?
68    pub fn is_delta(&self) -> bool {
69        self.get_config().roughness < 1e-2
70    }
71
72    /// Gets the refraction index
73    pub fn get_rfrct_idx(&self) -> Float {
74        self.get_config().refraction_idx
75    }
76
77    /// Get roughness from config
78    pub fn get_roughness(&self) -> Float {
79        self.get_config().roughness
80    }
81
82    /// Getter, better way to do this?
83    fn get_config(&self) -> &MicrofacetConfig {
84        match self {
85            Self::Ggx(cfg) | Self::Beckmann(cfg) => cfg,
86        }
87    }
88
89    /// Disney diffuse (Burley 2012) with renormalization to conserve energy
90    /// as done in Frostbite (Lagarde et al. 2014)
91    pub fn disney_diffuse(
92        &self,
93        no_dot_v: Float,
94        no_dot_wh: Float,
95        no_dot_wi: Float
96    ) -> Float {
97        let roughness2 = self.get_config().roughness.powi(2);
98        let energy_bias = 0.5 * roughness2;
99        let fd90 = energy_bias + 2.0 * no_dot_wh.powi(2) * roughness2;
100
101        let view_scatter = 1.0 + (fd90 - 1.0) * (1.0 - no_dot_v).powi(5);
102        let light_scatter = 1.0 + (fd90 - 1.0) * (1.0 - no_dot_wi).powi(5);
103
104        let energy_factor = 1.0 + roughness2 * (1.0 / 1.51 - 1.0);
105
106        view_scatter * light_scatter * energy_factor
107    }
108
109    /// The microfacet distribution function.
110    ///
111    /// # Distributions
112    /// * Beckmann - exp(-tan^2(θ) / α^2) / (π * α^2 * cos^4(θ))
113    /// * GGX - α^2 / (π * (cos^4(θ) * (α^2 - 1.0) + 1.0)^2)
114    ///
115    /// # Arguments
116    /// * `wh` - Microsurface normal
117    /// * `no` - Macrosurface normal
118    pub fn d(&self, wh: Normal, no: Normal) -> Float {
119        match self {
120            Self::Ggx(cfg) => {
121                let cos2_theta = wh.dot(no).powi(2);
122
123                if cos2_theta < crate::EPSILON {
124                    0.0
125                } else {
126                    let roughness2 = cfg.roughness * cfg.roughness;
127
128                    roughness2
129                        / (crate::PI * (1.0 - cos2_theta * (1.0 - roughness2)).powi(2))
130                }
131            }
132            Self::Beckmann(cfg) => {
133                let cos2_theta = wh.dot(no).powi(2);
134
135                if cos2_theta < crate::EPSILON {
136                    0.0
137                } else {
138                    let roughness2 = cfg.roughness * cfg.roughness;
139                    let tan2_theta = (1.0 - cos2_theta) / cos2_theta;
140
141                    (-tan2_theta / roughness2).exp()
142                        / (crate::PI * roughness2 * cos2_theta.powi(2))
143                }
144            }
145        }
146    }
147
148    /// Fresnel term with Schlick's approximation
149    pub fn f(&self, wo: Direction, wh: Normal, color: Color) -> Color {
150        let eta = self.get_config().refraction_idx;
151        let metallicity = self.get_config().metallicity;
152
153        let f0 = (eta - 1.0) / (eta + 1.0);
154        let f0 = Color::splat(f0 * f0).lerp(color, metallicity);
155
156        let wo_dot_wh = wo.dot(wh).abs();
157        f0 + (Color::WHITE - f0) * (1.0 - wo_dot_wh).powi(5)
158    }
159
160    /// Shadow-masking term. Used to make sure that only microfacets that are
161    /// visible from `v` direction are considered. Uses the method described
162    /// in Chapter 8.4.3 of PBR due to Heitz et al. 2013.
163    ///
164    /// # Arguments
165    /// * `v` - View direction
166    /// * `wi` - Direction of ray away from the point of impact
167    /// * `wh` - Microsurface normal
168    /// * `no` - Macrosurface normal
169    pub fn g(&self, v: Direction, wi: Direction, wh: Normal, no: Normal) -> Float {
170        // signum to fix refraction
171        let chi = wh.dot(no).signum() * v.dot(wh) / v.dot(no);
172        if chi < crate::EPSILON {
173            0.0
174        } else {
175            1.0 / (1.0 + self.lambda(v, no) + self.lambda(wi, no))
176        }
177    }
178
179    pub fn g1(&self, v: Direction, wh: Normal, no: Normal) -> Float {
180        // signum to fix refraction
181        let chi = wh.dot(no).signum() * v.dot(wh) / v.dot(no);
182        if chi < crate::EPSILON {
183            0.0
184        } else {
185            1.0 / (1.0 + self.lambda(v, no))
186        }
187    }
188
189    /// Lambda function used in the definition of the shadow-masking term.
190    /// Beckmann with polynomial approximation and GGX exactly. PBR Chapter 8.4.3
191    ///
192    /// # Arguments
193    /// * `w` - Direction to consider
194    /// * `no` - Macrosurface normal
195    fn lambda(&self, w: Direction, no: Normal) -> Float {
196        match self {
197            Self::Ggx(cfg) => {
198                let cos2_theta = w.dot(no).powi(2);
199                if cos2_theta < crate::EPSILON {
200                    0.0
201                } else {
202                    let tan2_theta = (1.0 - cos2_theta) / cos2_theta;
203                    let roughness2 = cfg.roughness * cfg.roughness;
204
205                    ((1.0 + roughness2 * tan2_theta).sqrt() - 1.0) / 2.0
206                }
207            }
208            Self::Beckmann(cfg) => {
209                let cos2_theta = w.dot(no).powi(2);
210                if cos2_theta < crate::EPSILON {
211                    0.0
212                } else {
213                    let tan2_theta = ((1.0 - cos2_theta) / cos2_theta).abs();
214                    let a = 1.0 / (cfg.roughness * tan2_theta);
215
216                    if a >= 1.6 {
217                        0.0
218                    } else {
219                        (1.0 - 1.259 * a + 0.396 * a * a)
220                            / (3.535 * a + 2.181 * a * a)
221                    }
222                }
223            }
224        }
225    }
226
227    /// Probability to do importance sampling from NDF. Estimate based on
228    /// the Fresnel term.
229    pub fn probability_ndf_sample(&self, albedo: Color) -> Float {
230        let cfg = self.get_config();
231
232        let f0 = (cfg.refraction_idx - 1.0) / (cfg.refraction_idx + 1.0);
233        let f0 = f0 * f0;
234
235        (1.0 - cfg.metallicity) * f0 + cfg.metallicity * albedo.mean()
236    }
237
238    /// Probability that `wh` got sampled
239    pub fn sample_normal_pdf(
240        &self,
241        wh: Normal,
242        v: Direction,
243        no: Normal
244    ) -> Float {
245        let pdf = match self {
246            Self::Beckmann(..) => {
247                let wh_dot_no = wh.dot(no);
248                self.d(wh, no) * wh_dot_no
249            }
250            Self::Ggx(..) => {
251                let wh_dot_v = wh.dot(v);
252                let no_dot_v = no.dot(v);
253
254                self.g1(v, wh, no) * self.d(wh, no) * wh_dot_v / no_dot_v
255            }
256        };
257
258        pdf.max(0.0)
259    }
260
261    /// Sampling microfacet normals per distribution for importance sampling.
262    /// `v` in shading space.
263    pub fn sample_normal(&self, v: Direction, rand_sq: Vec2) -> Normal {
264        match self {
265            Self::Ggx(cfg) => {
266                // Heitz 2018 or
267                // https://schuttejoe.github.io/post/ggximportancesamplingpart2/
268
269                let roughness = cfg.roughness;
270                // Map the GGX ellipsoid to a hemisphere
271                let v_stretch = Direction::new(
272                    v.x * roughness,
273                    v.y * roughness,
274                    v.z
275                ).normalize();
276
277                // ONB basis of the hemisphere configuration
278                let hemi_basis = Onb::new(v_stretch);
279
280                // compute a point on the disk
281                let a = 1.0 / (1.0 + v_stretch.z);
282                let r = rand_sq.x.sqrt();
283                let phi = if rand_sq.y < a {
284                    crate::PI * rand_sq.y / a
285                } else {
286                    crate::PI + crate::PI * (rand_sq.y - a) / (1.0 - a)
287                };
288
289                let x = r * phi.cos();
290                let y = if rand_sq.y < a {
291                    r * phi.sin()
292                } else {
293                    r * phi.sin() * v_stretch.z
294                };
295
296                // compute normal in hemisphere configuration
297                let wm = Normal::new(
298                    x,
299                    y,
300                    (1.0 - x*x - y*y).max(0.0).sqrt(),
301                );
302                let wm = hemi_basis.to_world(wm);
303
304                // move back to ellipsoid
305                Normal::new(
306                    roughness * wm.x,
307                    roughness * wm.y,
308                    wm.z.max(0.0)
309                ).normalize()
310            }
311            Self::Beckmann(cfg) => {
312                let roughness2 = cfg.roughness * cfg.roughness;
313                let theta = (-roughness2 * (1.0 - rand_sq.y).ln()).sqrt().atan();
314                let phi = 2.0 * crate::PI * rand_sq.x;
315
316                Normal::new(
317                    theta.sin() * phi.cos(),
318                    theta.sin() * phi.sin(),
319                    theta.cos(),
320                )
321            }
322        }
323    }
324}