Skip to main content

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}