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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
use rand::{rngs::StdRng, Rng};
use rand_distr::{UnitCircle, UnitDisc};
use crate::color::{hex_color, Color};
/// Represents a shader material with some physical properties
#[derive(Copy, Clone)]
pub struct Material {
/// Albedo color
pub color: Color,
/// Index of refraction
pub index: f64,
/// Roughness parameter for Beckmann microfacet distribution
pub roughness: f64,
/// Metallic versus dielectric
pub metallic: f64,
/// Self-emittance of light
pub emittance: f64,
/// Transmittance (e.g., glass)
pub transparent: bool,
}
impl Default for Material {
fn default() -> Self {
Self::specular(hex_color(0xff0000), 0.5) // red
}
}
impl Material {
/// Perfect diffuse (Lambertian) material with a given color
pub fn diffuse(color: Color) -> Material {
Material {
color,
index: 1.5,
roughness: 1.0,
metallic: 0.0,
emittance: 0.0,
transparent: false,
}
}
/// Specular material with a given color and roughness
pub fn specular(color: Color, roughness: f64) -> Material {
Material {
color,
index: 1.5,
roughness,
metallic: 0.0,
emittance: 0.0,
transparent: false,
}
}
/// Clear material with a specified index of refraction and roughness (such as glass)
pub fn clear(index: f64, roughness: f64) -> Material {
Material {
color: glm::vec3(1.0, 1.0, 1.0),
index,
roughness,
metallic: 0.0,
emittance: 0.0,
transparent: true,
}
}
/// Colored transparent material
pub fn transparent(color: Color, index: f64, roughness: f64) -> Material {
Material {
color,
index,
roughness,
metallic: 0.0,
emittance: 0.0,
transparent: true,
}
}
/// Metallic material (has extra tinted specular reflections)
pub fn metallic(color: Color, roughness: f64) -> Material {
Material {
color,
index: 1.5,
roughness,
metallic: 1.0,
emittance: 0.0,
transparent: false,
}
}
/// Perfect emissive material, useful for modeling area lights
pub fn light(color: Color, emittance: f64) -> Material {
Material {
color,
index: 1.0,
roughness: 1.0,
metallic: 0.0,
emittance,
transparent: false,
}
}
}
#[allow(clippy::many_single_char_names)]
impl Material {
/// Bidirectional scattering distribution function
///
/// - `n` - surface normal vector
/// - `wo` - unit direction vector toward the viewer
/// - `wi` - unit direction vector toward the incident ray
///
/// This works for both opaque and transmissive materials, based on a Beckmann
/// microfacet distribution model, Cook-Torrance shading for the specular component,
/// and Lambertian shading for the diffuse component. Useful references:
///
/// - http://www.codinglabs.net/article_physically_based_rendering_cook_torrance.aspx
/// - https://computergraphics.stackexchange.com/q/4394
/// - https://graphics.stanford.edu/courses/cs148-10-summer/docs/2006--degreve--reflection_refraction.pdf
/// - http://www.pbr-book.org/3ed-2018/Materials/BSDFs.html
/// - https://www.cs.cornell.edu/~srm/publications/EGSR07-btdf.pdf
pub fn bsdf(&self, n: &glm::DVec3, wo: &glm::DVec3, wi: &glm::DVec3) -> Color {
let n_dot_wi = n.dot(wi);
let n_dot_wo = n.dot(wo);
let wi_outside = n_dot_wi.is_sign_positive();
let wo_outside = n_dot_wo.is_sign_positive();
if !self.transparent && (!wi_outside || !wo_outside) {
// Opaque materials do not transmit light
return glm::vec3(0.0, 0.0, 0.0);
}
if wi_outside == wo_outside {
let h = (wi + wo).normalize(); // halfway vector
let wo_dot_h = wo.dot(&h);
let n_dot_h = n.dot(&h);
let nh2 = n_dot_h.powi(2);
// d: microfacet distribution function
// D = exp(((n • h)^2 - 1) / (m^2 (n • h)^2)) / (π m^2 (n • h)^4)
let m2 = self.roughness * self.roughness;
let d = ((nh2 - 1.0) / (m2 * nh2)).exp() / (m2 * glm::pi::<f64>() * nh2 * nh2);
// f: fresnel, schlick's approximation
// F = F0 + (1 - F0)(1 - wi • h)^5
let f = if !wi_outside && (1.0 - wo_dot_h * wo_dot_h).sqrt() * self.index > 1.0 {
// Total internal reflection
glm::vec3(1.0, 1.0, 1.0)
} else {
let f0 = ((self.index - 1.0) / (self.index + 1.0)).powi(2);
let f0 = glm::lerp(&glm::vec3(f0, f0, f0), &self.color, self.metallic);
f0 + (glm::vec3(1.0, 1.0, 1.0) - f0) * (1.0 - wo_dot_h).powi(5)
};
// g: geometry function, microfacet shadowing
// G = min(1, 2(n • h)(n • wo)/(wo • h), 2(n • h)(n • wi)/(wo • h))
let g = f64::min(n_dot_wi * n_dot_h, n_dot_wo * n_dot_h);
let g = (2.0 * g) / wo_dot_h;
let g = g.min(1.0);
// BRDF: putting it all together
// Cook-Torrance = DFG / (4(n • wi)(n • wo))
// Lambert = (1 - F) * c / π
let specular = d * f * g / (4.0 * n_dot_wo * n_dot_wi);
if self.transparent {
specular
} else {
let diffuse =
(glm::vec3(1.0, 1.0, 1.0) - f).component_mul(&self.color) / glm::pi::<f64>();
specular + diffuse
}
} else {
// Ratio of refractive indices, η_i / η_o
let eta_t = if wo_outside {
self.index
} else {
1.0 / self.index
};
let h = (wi * eta_t + wo).normalize(); // halfway vector
let wi_dot_h = wi.dot(&h);
let wo_dot_h = wo.dot(&h);
let n_dot_h = n.dot(&h);
let nh2 = n_dot_h.powi(2);
// d: microfacet distribution function
// D = exp(((n • h)^2 - 1) / (m^2 (n • h)^2)) / (π m^2 (n • h)^4)
let m2 = self.roughness * self.roughness;
let d = ((nh2 - 1.0) / (m2 * nh2)).exp() / (m2 * glm::pi::<f64>() * nh2 * nh2);
// f: fresnel, schlick's approximation
// F = F0 + (1 - F0)(1 - wi • h)^5
let f0 = ((self.index - 1.0) / (self.index + 1.0)).powi(2);
let f0 = glm::lerp(&glm::vec3(f0, f0, f0), &self.color, self.metallic);
let f = f0 + (glm::vec3(1.0, 1.0, 1.0) - f0) * (1.0 - wi_dot_h.abs()).powi(5);
// g: geometry function, microfacet shadowing
// G = min(1, 2(n • h)(n • wo)/(wo • h), 2(n • h)(n • wi)/(wo • h))
let g = f64::min((n_dot_wi * n_dot_h).abs(), (n_dot_wo * n_dot_h).abs());
let g = (2.0 * g) / wo_dot_h.abs();
let g = g.min(1.0);
// BTDF: putting it all together
// Cook-Torrance = |h • wi|/|n • wi| * |h • wo|/|n • wo|
// * η_o^2 (1 - F)DG / (η_i (h • wi) + η_o (h • wo))^2
let btdf = (wi_dot_h * wo_dot_h / (n_dot_wi * n_dot_wo)).abs()
* (d * (glm::vec3(1.0, 1.0, 1.0) - f) * g / (eta_t * wi_dot_h + wo_dot_h).powi(2));
btdf.component_mul(&self.color)
}
}
/// Sample the light hemisphere, returning a tuple of (direction vector, PDF)
///
/// This implementation samples according to the Beckmann distribution
/// function D. Specifically, it uses the fact that ∫ D(h) (n • h) dω = 1,
/// which creates a probability distribution that can be sampled from using a
/// probability integral transform.
///
/// We also need to sample from the diffuse BRDF as well, independently. We
/// calculate the ratio of samples from the diffuse vs specular components by
/// estimating the average magnitude of the Fresnel term.
///
/// Reference: https://agraphicsguy.wordpress.com/2015/11/01/sampling-microfacet-brdf/
pub fn sample_f(
&self,
n: &glm::DVec3,
wo: &glm::DVec3,
rng: &mut StdRng,
) -> Option<(glm::DVec3, f64)> {
let m2 = self.roughness * self.roughness;
// Estimate specular contribution using Fresnel term
let f0 = ((self.index - 1.0) / (self.index + 1.0)).powi(2);
let f = (1.0 - self.metallic) * f0 + self.metallic * self.color.mean();
let f = glm::mix_scalar(f, 1.0, 0.2);
// Ratio of refractive indices
let eta_t = if wo.dot(n) > 0.0 {
self.index
} else {
1.0 / self.index
};
let beckmann = |rng: &mut StdRng| {
// PIT for Beckmann distribution microfacet normal
// θ = arctan √(-m^2 ln U)
let theta = (m2 * -rng.gen::<f64>().ln()).sqrt().atan();
let (sin_t, cos_t) = theta.sin_cos();
// Generate halfway vector by sampling azimuth uniformly
let [x, y]: [f64; 2] = rng.sample(UnitCircle);
let h = glm::vec3(x * sin_t, y * sin_t, cos_t);
local_to_world(n) * h
};
let beckmann_pdf = |h: &glm::DVec3| {
// p = 1 / (πm^2 cos^3 θ) * e^(-tan^2(θ) / m^2)
let cos_t = h.dot(n).abs();
let sin_t = (1.0 - cos_t * cos_t).sqrt();
(std::f64::consts::PI * m2 * cos_t.powi(3)).recip()
* (-(sin_t / cos_t).powi(2) / m2).exp()
};
let wi = if rng.gen_bool(f) {
// Specular component
let h = beckmann(rng);
-glm::reflect_vec(wo, &h)
} else if !self.transparent {
// Diffuse component (Lambertian)
// Simple cosine-sampling using Malley's method
let [x, y]: [f64; 2] = rng.sample(UnitDisc);
let z = (1.0_f64 - x * x - y * y).sqrt();
local_to_world(n) * glm::vec3(x, y, z)
} else {
// Transmitted component
let h = beckmann(rng);
let cos_to = h.dot(wo);
let wo_perp = wo - h * cos_to;
let wi_perp = -wo_perp / eta_t;
let sin2_ti = wi_perp.magnitude_squared();
if sin2_ti > 1.0 {
// This angle doesn't yield any transmittence to wo,
// due to total internal reflection
return None;
}
let cos_ti = (1.0 - sin2_ti).sqrt();
-cos_to.signum() * cos_ti * h + wi_perp
};
// Multiple importance sampling - add up total probability
let mut p = 0.0;
p += {
// Specular component
let h = (wi + wo).normalize();
let p_h = beckmann_pdf(&h);
f * p_h / (4.0 * h.dot(wo).abs())
};
p += if !self.transparent {
// Diffuse component
(1.0 - f) * wi.dot(n).max(0.0) * std::f64::consts::FRAC_1_PI
} else if wo.dot(n).is_sign_positive() != wi.dot(n).is_sign_positive() {
// Transmitted component
let h = (wi * eta_t + wo).normalize();
let p_h = beckmann_pdf(&h);
let h_dot_wo = h.dot(wo);
let h_dot_wi = h.dot(&wi);
let jacobian = h_dot_wo.abs() / (eta_t * h_dot_wi + h_dot_wo).powi(2);
(1.0 - f) * p_h * jacobian
} else {
0.0
};
Some((wi, p))
}
}
fn local_to_world(n: &glm::DVec3) -> glm::DMat3 {
let ns = if n.x.is_normal() {
glm::vec3(n.y, -n.x, 0.0).normalize()
} else {
glm::vec3(0.0, -n.z, n.y).normalize()
};
let nss = n.cross(&ns);
glm::mat3(ns.x, nss.x, n.x, ns.y, nss.y, n.y, ns.z, nss.z, n.z)
}