mirage_engine/material.rs
1use crate::Color;
2
3/// The alpha a cutout material is drawn from; `forward.wgsl` drops what
4/// lands under it.
5pub(crate) const CUTOUT: f32 = 0.5;
6
7/// A surface's shading: a tint, how strongly lights affect it, and the light
8/// it adds of its own.
9///
10/// A tint alpha under `1.0` draws the surface in the transparent pass:
11/// sorted back to front, blended over what is behind it, and never written
12/// to depth.
13///
14/// Set as a slot's default, or per draw with
15/// [`Instance::material`](crate::mesh::Instance::material).
16#[derive(Clone, Copy, Debug, PartialEq)]
17pub struct Material {
18 tint: Color,
19 litness: f32,
20 emissive: Color,
21 roughness: f32,
22 metallic: f32,
23 cutout: bool,
24 additive: bool,
25}
26
27impl Material {
28 /// A flat color; lights do not affect it.
29 pub const fn color(color: Color) -> Self {
30 Self::new(color, 0.0)
31 }
32
33 /// A color fully lit by the frame's lights.
34 pub const fn lit(color: Color) -> Self {
35 Self::new(color, 1.0)
36 }
37
38 /// A color between flat and lit by `litness`, a fraction clamped to
39 /// `0.0..=1.0`.
40 pub fn shaded(color: Color, litness: f32) -> Self {
41 Self::new(color, litness.clamp(0.0, 1.0))
42 }
43
44 /// Adds light of its own to the surface; [`Color::BLACK`] by default.
45 ///
46 /// Required if you want a surface bright on its own: values past `1.0`
47 /// are what [`FrameContext::set_bloom`](crate::FrameContext::set_bloom)
48 /// spreads. In the transparent pass the tint's alpha scales everything
49 /// the surface draws, this light too — fade one or the other, not both.
50 #[must_use]
51 pub const fn emissive(mut self, color: Color) -> Self {
52 self.emissive = color;
53 self
54 }
55
56 /// The surface's roughness, a fraction clamped to `0.0..=1.0` and
57 /// `1.0` by default: the factor a `.glb` material declares, held per
58 /// draw.
59 #[must_use]
60 pub fn roughness(mut self, roughness: f32) -> Self {
61 self.roughness = roughness.clamp(0.0, 1.0);
62 self
63 }
64
65 /// The surface's metallic, a fraction clamped to `0.0..=1.0` and `0.0`
66 /// by default: the factor a `.glb` material declares, held per draw.
67 #[must_use]
68 pub fn metallic(mut self, metallic: f32) -> Self {
69 self.metallic = metallic.clamp(0.0, 1.0);
70 self
71 }
72
73 /// Drops the texels where `tint × texture` alpha lands under `0.5`, and
74 /// draws the rest as opaque.
75 ///
76 /// Required if you want a sprite drawn with no blending at its edges: a
77 /// cutout draw writes depth and is not sorted. A tint alpha under `1.0`
78 /// still draws it in the transparent pass, where the same texels are
79 /// dropped.
80 #[must_use]
81 pub const fn cutout(mut self) -> Self {
82 self.cutout = true;
83 self
84 }
85
86 /// Adds what the draw would be to what is behind it, instead of drawing
87 /// over it.
88 ///
89 /// Required if you want a draw that only ever adds light and never
90 /// darkens what it covers: it is drawn after the transparent pass, in
91 /// the order it was submitted, is never written to depth, and casts no
92 /// shadow. The tint's alpha scales everything it adds — the
93 /// [`emissive`](Material::emissive) light too: fade one or the other,
94 /// not both. The texture's alpha scales each texel the same way: an
95 /// empty texel adds nothing, and one half covered adds half of what it
96 /// holds. A tint past `1.0` scales what the texture holds past it: the
97 /// draw adds light in the shape and color of its own texture. A flat
98 /// [`emissive`](Material::emissive) adds one color over every texel
99 /// instead. A material that also set [`cutout`](Material::cutout) drops
100 /// nothing.
101 #[must_use]
102 pub const fn additive(mut self) -> Self {
103 self.additive = true;
104 self
105 }
106
107 /// The color shading multiplies the lighting by.
108 pub const fn tint(&self) -> Color {
109 self.tint
110 }
111
112 /// The fraction of lit shading applied: `0.0` flat, `1.0` fully lit.
113 pub const fn litness(&self) -> f32 {
114 self.litness
115 }
116
117 /// The light the surface adds of its own.
118 pub const fn emission(&self) -> Color {
119 self.emissive
120 }
121
122 /// The surface's roughness, a fraction.
123 pub const fn rough(&self) -> f32 {
124 self.roughness
125 }
126
127 /// The surface's metallic, a fraction.
128 pub const fn metal(&self) -> f32 {
129 self.metallic
130 }
131
132 /// The same material at `factor` of its tint alpha, which
133 /// [`Instance::faded`](crate::mesh::Instance::faded) scales a resolved
134 /// slot by.
135 pub(crate) fn faded(self, factor: f32) -> Self {
136 Self {
137 tint: self.tint.with_alpha(self.tint.alpha * factor),
138 ..self
139 }
140 }
141
142 /// Whether a draw of this material belongs in the transparent pass.
143 pub(crate) fn translucent(&self) -> bool {
144 self.tint.alpha < 1.0
145 }
146
147 /// Whether a draw of this material covers anything at all: a tint alpha
148 /// of zero leaves nothing to blend over what is drawn, and nothing for a
149 /// depth map to take.
150 pub(crate) fn covers(&self) -> bool {
151 self.tint.alpha > 0.0
152 }
153
154 /// Whether a draw of this material drops the texels its alpha leaves
155 /// out.
156 pub(crate) fn cuts(&self) -> bool {
157 self.cutout
158 }
159
160 /// Whether a draw of this material adds to what is behind it rather
161 /// than drawing over it, whatever its alpha and its cutout are set to.
162 pub(crate) fn adds(&self) -> bool {
163 self.additive
164 }
165
166 const fn new(tint: Color, litness: f32) -> Self {
167 Self {
168 tint,
169 litness,
170 emissive: Color::BLACK,
171 roughness: 1.0,
172 metallic: 0.0,
173 cutout: false,
174 additive: false,
175 }
176 }
177}
178
179impl Default for Material {
180 /// Lit white.
181 fn default() -> Self {
182 Self::lit(Color::WHITE)
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn only_the_materials_asked_to_drop_texels_do() {
192 assert!(!Material::lit(Color::WHITE).cuts());
193 assert!(Material::lit(Color::WHITE).cutout().cuts());
194 assert!(
195 Material::lit(Color::WHITE)
196 .cutout()
197 .emissive(Color::WHITE)
198 .cuts(),
199 "and keep it through the knobs that follow"
200 );
201 }
202
203 #[test]
204 fn only_the_materials_asked_to_add_what_they_draw_do() {
205 assert!(!Material::lit(Color::WHITE).adds());
206 assert!(Material::lit(Color::WHITE).additive().adds());
207 assert!(
208 Material::lit(Color::rgba(1.0, 1.0, 1.0, 0.25))
209 .additive()
210 .cutout()
211 .adds(),
212 "whatever the alpha and the cutout of the material around it"
213 );
214 }
215
216 #[test]
217 fn a_material_is_fully_rough_and_no_metal_until_it_is_asked_to_be_otherwise() {
218 let white = Material::lit(Color::WHITE);
219
220 assert_eq!(
221 (white.rough(), white.metal()),
222 (1.0, 0.0),
223 "which is the surface that reflects only its base share of the \
224 sky, blurred to one color, at every angle"
225 );
226 assert_eq!(white.roughness(0.25).rough(), 0.25);
227 assert_eq!(white.metallic(0.25).metal(), 0.25);
228 assert_eq!(
229 (white.roughness(4.0).rough(), white.metallic(4.0).metal()),
230 (1.0, 1.0),
231 "and neither reaches past the sharpest surface there is"
232 );
233 assert_eq!(
234 (white.roughness(-1.0).rough(), white.metallic(-1.0).metal()),
235 (0.0, 0.0)
236 );
237 }
238
239 #[test]
240 fn only_a_tint_alpha_under_one_draws_in_the_transparent_pass() {
241 assert!(!Material::color(Color::WHITE).translucent());
242 assert!(Material::color(Color::rgba(1.0, 1.0, 1.0, 0.999)).translucent());
243 assert!(
244 !Material::color(Color::rgba(1.0, 1.0, 1.0, f32::NAN)).translucent(),
245 "an alpha that is not a number is not under one either"
246 );
247 }
248}