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
//! How a surface answers light.
//!
//! Kept apart from the mesh: the same shape can be a matte board tile or a
//! polished piece, and which it is has nothing to do with its geometry.
//!
//! ```
//! # use codecraft::material::Material;
//! let piece = Material::gloss(0.6, 48.0);
//! let tile = Material::matte();
//! assert!(piece.specular > tile.specular);
//! ```
/// Matte, or polished enough to hold a highlight and a little of the sky.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Material {
/// How bright a highlight it takes.
pub specular: f32,
/// How tight that highlight is. Small is a wide sheen, large a point.
pub shininess: f32,
/// How much of the sky it shows back, at a glancing angle.
pub reflectivity: f32,
}
impl Default for Material {
fn default() -> Self {
// A little sheen on everything: a scene of pure matte surfaces reads
// as paper whatever the lighting does.
Self {
specular: 0.12,
shininess: 24.0,
reflectivity: 0.05,
}
}
}
impl Material {
/// A polished surface: a brighter, tighter highlight, and enough of the
/// sky in it to read as a material rather than a colour.
pub fn gloss(specular: f32, shininess: f32) -> Self {
Self {
specular,
shininess,
reflectivity: (specular * 0.5).min(1.0),
}
}
/// Nothing to catch the light at all.
pub fn matte() -> Self {
Self {
specular: 0.0,
shininess: 1.0,
reflectivity: 0.0,
}
}
/// How much of the sky it shows back, for a surface that wants a
/// different amount than its polish implies.
pub fn reflectivity(mut self, reflectivity: f32) -> Self {
self.reflectivity = reflectivity;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_matte_surface_takes_no_highlight_and_shows_nothing_back() {
let matte = Material::matte();
assert_eq!(matte.specular, 0.0);
assert_eq!(matte.reflectivity, 0.0);
}
#[test]
fn polish_brings_the_sky_with_it() {
let dull = Material::gloss(0.2, 16.0);
let polished = Material::gloss(0.8, 64.0);
assert!(polished.reflectivity > dull.reflectivity);
assert!(polished.shininess > dull.shininess);
assert!(
polished.reflectivity <= 1.0,
"and never more than all of it"
);
}
}
impl Material {
/// The same surface as an OpenPBR one.
///
/// The three Blinn-Phong numbers do not describe a material -- they
/// describe how a particular shader was told to behave -- so this is a
/// mapping and not a conversion, and it exists so that a scene written
/// against [`Material::gloss`] keeps looking like itself once the shading
/// is done properly. Anything that wants a real material should author an
/// [`OpenPbrSurface`] and skip this.
///
/// - `shininess` is a Phong exponent, which is a sharpness; roughness is
/// the opposite, and `sqrt(2 / (n + 2))` is the usual bridge between
/// them.
/// - `specular` scales the lobe, which is what `specular_weight` does.
/// - `reflectivity` asked for more of the sky at a glancing angle, which
/// is Fresnel, which is the IOR. More of it means a higher index.
pub fn to_openpbr(self) -> OpenPbrSurface {
let roughness = (2.0 / (self.shininess.max(1.0) + 2.0))
.sqrt()
.clamp(0.02, 1.0);
OpenPbrSurface {
// The base colour is the instance's tint, applied in the shader:
// one material serves every piece on the board.
base_color: Color3::new(1.0, 1.0, 1.0),
specular_weight: self.specular.clamp(0.0, 1.0),
specular_roughness: roughness,
specular_ior: 1.5 + self.reflectivity.clamp(0.0, 1.0) * 0.5,
..OpenPbrSurface::default()
}
}
}
use crate::materials::openpbr::{Color3, OpenPbrSurface};
#[cfg(test)]
mod openpbr_tests {
use super::*;
#[test]
fn polish_becomes_smoothness() {
let dull = Material::gloss(0.2, 8.0).to_openpbr();
let polished = Material::gloss(0.8, 128.0).to_openpbr();
assert!(
polished.specular_roughness < dull.specular_roughness,
"a tighter highlight is a smoother surface",
);
assert!(polished.specular_weight > dull.specular_weight);
}
#[test]
fn a_matte_surface_keeps_its_specular_off() {
assert_eq!(Material::matte().to_openpbr().specular_weight, 0.0);
}
#[test]
fn roughness_never_reaches_zero() {
// A perfectly smooth specular lobe is a delta function, which an
// analytic light can never hit.
let mirror = Material::gloss(1.0, 1.0e9).to_openpbr();
assert!(mirror.specular_roughness >= 0.02);
}
}