lumo/tracer/
microfacet.rs1use crate::tracer::{ onb::Onb, Color };
2use crate::{ Normal, Direction, Float, Vec2 };
3
4#[derive(Copy, Clone)]
6pub struct MicrofacetConfig {
7 pub roughness: Float,
9 pub refraction_idx: Float,
11 pub metallicity: Float,
13 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#[derive(Copy, Clone)]
40pub enum MfDistribution {
41 Ggx(MicrofacetConfig),
43 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 pub fn is_transparent(&self) -> bool {
59 self.get_config().transparent
60 }
61
62 pub fn is_specular(&self) -> bool {
64 self.is_transparent() || self.get_config().roughness < 0.01
65 }
66
67 pub fn is_delta(&self) -> bool {
69 self.get_config().roughness < 1e-2
70 }
71
72 pub fn get_rfrct_idx(&self) -> Float {
74 self.get_config().refraction_idx
75 }
76
77 pub fn get_roughness(&self) -> Float {
79 self.get_config().roughness
80 }
81
82 fn get_config(&self) -> &MicrofacetConfig {
84 match self {
85 Self::Ggx(cfg) | Self::Beckmann(cfg) => cfg,
86 }
87 }
88
89 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 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 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 pub fn g(&self, v: Direction, wi: Direction, wh: Normal, no: Normal) -> Float {
170 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 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 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 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 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 pub fn sample_normal(&self, v: Direction, rand_sq: Vec2) -> Normal {
264 match self {
265 Self::Ggx(cfg) => {
266 let roughness = cfg.roughness;
270 let v_stretch = Direction::new(
272 v.x * roughness,
273 v.y * roughness,
274 v.z
275 ).normalize();
276
277 let hemi_basis = Onb::new(v_stretch);
279
280 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 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 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}