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
use crate::Color;
/// The alpha a cutout material is drawn from; `forward.wgsl` drops what
/// lands under it.
pub(crate) const CUTOUT: f32 = 0.5;
/// A surface's shading: a tint, how strongly lights affect it, and the light
/// it adds of its own.
///
/// A tint alpha under `1.0` draws the surface in the transparent pass:
/// sorted back to front, blended over what is behind it, and never written
/// to depth.
///
/// Set as a slot's default, or per draw with
/// [`Instance::material`](crate::mesh::Instance::material).
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Material {
tint: Color,
litness: f32,
emissive: Color,
roughness: f32,
metallic: f32,
cutout: bool,
additive: bool,
}
impl Material {
/// A flat color; lights do not affect it.
pub const fn color(color: Color) -> Self {
Self::new(color, 0.0)
}
/// A color fully lit by the frame's lights.
pub const fn lit(color: Color) -> Self {
Self::new(color, 1.0)
}
/// A color between flat and lit by `litness`, a fraction clamped to
/// `0.0..=1.0`.
pub fn shaded(color: Color, litness: f32) -> Self {
Self::new(color, litness.clamp(0.0, 1.0))
}
/// Adds light of its own to the surface; [`Color::BLACK`] by default.
///
/// Required if you want a surface bright on its own: values past `1.0`
/// are what [`FrameContext::set_bloom`](crate::FrameContext::set_bloom)
/// spreads. In the transparent pass the tint's alpha scales everything
/// the surface draws, this light too — fade one or the other, not both.
#[must_use]
pub const fn emissive(mut self, color: Color) -> Self {
self.emissive = color;
self
}
/// The surface's roughness, a fraction clamped to `0.0..=1.0` and
/// `1.0` by default: the factor a `.glb` material declares, held per
/// draw.
#[must_use]
pub fn roughness(mut self, roughness: f32) -> Self {
self.roughness = roughness.clamp(0.0, 1.0);
self
}
/// The surface's metallic, a fraction clamped to `0.0..=1.0` and `0.0`
/// by default: the factor a `.glb` material declares, held per draw.
#[must_use]
pub fn metallic(mut self, metallic: f32) -> Self {
self.metallic = metallic.clamp(0.0, 1.0);
self
}
/// Drops the texels where `tint × texture` alpha lands under `0.5`, and
/// draws the rest as opaque.
///
/// Required if you want a sprite drawn with no blending at its edges: a
/// cutout draw writes depth and is not sorted. A tint alpha under `1.0`
/// still draws it in the transparent pass, where the same texels are
/// dropped.
#[must_use]
pub const fn cutout(mut self) -> Self {
self.cutout = true;
self
}
/// Adds what the draw would be to what is behind it, instead of drawing
/// over it.
///
/// Required if you want a draw that only ever adds light and never
/// darkens what it covers: it is drawn after the transparent pass, in
/// the order it was submitted, is never written to depth, and casts no
/// shadow. The tint's alpha scales everything it adds — the
/// [`emissive`](Material::emissive) light too: fade one or the other,
/// not both. The texture's alpha scales each texel the same way: an
/// empty texel adds nothing, and one half covered adds half of what it
/// holds. A tint past `1.0` scales what the texture holds past it: the
/// draw adds light in the shape and color of its own texture. A flat
/// [`emissive`](Material::emissive) adds one color over every texel
/// instead. A material that also set [`cutout`](Material::cutout) drops
/// nothing.
#[must_use]
pub const fn additive(mut self) -> Self {
self.additive = true;
self
}
/// The color shading multiplies the lighting by.
pub const fn tint(&self) -> Color {
self.tint
}
/// The fraction of lit shading applied: `0.0` flat, `1.0` fully lit.
pub const fn litness(&self) -> f32 {
self.litness
}
/// The light the surface adds of its own.
pub const fn emission(&self) -> Color {
self.emissive
}
/// The surface's roughness, a fraction.
pub const fn rough(&self) -> f32 {
self.roughness
}
/// The surface's metallic, a fraction.
pub const fn metal(&self) -> f32 {
self.metallic
}
/// The same material at `factor` of its tint alpha, which
/// [`Instance::faded`](crate::mesh::Instance::faded) scales a resolved
/// slot by.
pub(crate) fn faded(self, factor: f32) -> Self {
Self {
tint: self.tint.with_alpha(self.tint.alpha * factor),
..self
}
}
/// Whether a draw of this material belongs in the transparent pass.
pub(crate) fn translucent(&self) -> bool {
self.tint.alpha < 1.0
}
/// Whether a draw of this material covers anything at all: a tint alpha
/// of zero leaves nothing to blend over what is drawn, and nothing for a
/// depth map to take.
pub(crate) fn covers(&self) -> bool {
self.tint.alpha > 0.0
}
/// Whether a draw of this material drops the texels its alpha leaves
/// out.
pub(crate) fn cuts(&self) -> bool {
self.cutout
}
/// Whether a draw of this material adds to what is behind it rather
/// than drawing over it, whatever its alpha and its cutout are set to.
pub(crate) fn adds(&self) -> bool {
self.additive
}
const fn new(tint: Color, litness: f32) -> Self {
Self {
tint,
litness,
emissive: Color::BLACK,
roughness: 1.0,
metallic: 0.0,
cutout: false,
additive: false,
}
}
}
impl Default for Material {
/// Lit white.
fn default() -> Self {
Self::lit(Color::WHITE)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_the_materials_asked_to_drop_texels_do() {
assert!(!Material::lit(Color::WHITE).cuts());
assert!(Material::lit(Color::WHITE).cutout().cuts());
assert!(
Material::lit(Color::WHITE)
.cutout()
.emissive(Color::WHITE)
.cuts(),
"and keep it through the knobs that follow"
);
}
#[test]
fn only_the_materials_asked_to_add_what_they_draw_do() {
assert!(!Material::lit(Color::WHITE).adds());
assert!(Material::lit(Color::WHITE).additive().adds());
assert!(
Material::lit(Color::rgba(1.0, 1.0, 1.0, 0.25))
.additive()
.cutout()
.adds(),
"whatever the alpha and the cutout of the material around it"
);
}
#[test]
fn a_material_is_fully_rough_and_no_metal_until_it_is_asked_to_be_otherwise() {
let white = Material::lit(Color::WHITE);
assert_eq!(
(white.rough(), white.metal()),
(1.0, 0.0),
"which is the surface that reflects only its base share of the \
sky, blurred to one color, at every angle"
);
assert_eq!(white.roughness(0.25).rough(), 0.25);
assert_eq!(white.metallic(0.25).metal(), 0.25);
assert_eq!(
(white.roughness(4.0).rough(), white.metallic(4.0).metal()),
(1.0, 1.0),
"and neither reaches past the sharpest surface there is"
);
assert_eq!(
(white.roughness(-1.0).rough(), white.metallic(-1.0).metal()),
(0.0, 0.0)
);
}
#[test]
fn only_a_tint_alpha_under_one_draws_in_the_transparent_pass() {
assert!(!Material::color(Color::WHITE).translucent());
assert!(Material::color(Color::rgba(1.0, 1.0, 1.0, 0.999)).translucent());
assert!(
!Material::color(Color::rgba(1.0, 1.0, 1.0, f32::NAN)).translucent(),
"an alpha that is not a number is not under one either"
);
}
}