rend3_routine/pbr/
material.rs

1//! Types which make up `rend3-routine`'s material [`PbrMaterial`]
2
3use std::mem;
4
5use glam::{Mat3, Mat3A, Vec3, Vec4};
6use rend3::types::{Material, TextureHandle};
7
8use crate::{
9    common::Sorting,
10    depth::{AlphaCutoutSpec, DepthRenderableMaterial},
11};
12
13bitflags::bitflags! {
14    /// Flags which shaders use to determine properties of a material
15    #[derive(Default)]
16    pub struct MaterialFlags : u32 {
17        const ALBEDO_ACTIVE =       0b0000_0000_0000_0001;
18        const ALBEDO_BLEND =        0b0000_0000_0000_0010;
19        const ALBEDO_VERTEX_SRGB =  0b0000_0000_0000_0100;
20        const BICOMPONENT_NORMAL =  0b0000_0000_0000_1000;
21        const SWIZZLED_NORMAL =     0b0000_0000_0001_0000;
22        const YDOWN_NORMAL =        0b0000_0000_0010_0000;
23        const AOMR_COMBINED =       0b0000_0000_0100_0000;
24        const AOMR_SWIZZLED_SPLIT = 0b0000_0000_1000_0000;
25        const AOMR_SPLIT =          0b0000_0001_0000_0000;
26        const AOMR_BW_SPLIT =       0b0000_0010_0000_0000;
27        const CC_GLTF_COMBINED =    0b0000_0100_0000_0000;
28        const CC_GLTF_SPLIT =       0b0000_1000_0000_0000;
29        const CC_BW_SPLIT =         0b0001_0000_0000_0000;
30        const UNLIT =               0b0010_0000_0000_0000;
31        const NEAREST =             0b0100_0000_0000_0000;
32    }
33}
34
35/// How the albedo color should be determined.
36#[derive(Debug, Clone)]
37pub enum AlbedoComponent {
38    /// No albedo color.
39    None,
40    /// Albedo color is the vertex value.
41    Vertex {
42        /// Vertex should be converted from srgb -> linear before
43        /// multiplication.
44        srgb: bool,
45    },
46    /// Albedo color is the given value.
47    Value(Vec4),
48    /// Albedo color is the given value multiplied by the vertex color.
49    ValueVertex {
50        value: Vec4,
51        /// Vertex should be converted from srgb -> linear before
52        /// multiplication.
53        srgb: bool,
54    },
55    /// Albedo color is loaded from the given texture.
56    Texture(TextureHandle),
57    /// Albedo color is loaded from the given texture, then multiplied
58    /// by the vertex color.
59    TextureVertex {
60        texture: TextureHandle,
61        /// Vertex should be converted from srgb -> linear before
62        /// multiplication.
63        srgb: bool,
64    },
65    /// Albedo color is loaded from given texture, then multiplied
66    /// by the given value.
67    TextureValue { texture: TextureHandle, value: Vec4 },
68    /// Albedo color is loaded from the given texture, then multiplied
69    /// by the vertex color and the given value.
70    TextureVertexValue {
71        texture: TextureHandle,
72        /// Vertex should be converted from srgb -> linear before
73        /// multiplication.
74        srgb: bool,
75        value: Vec4,
76    },
77}
78
79impl Default for AlbedoComponent {
80    fn default() -> Self {
81        Self::None
82    }
83}
84
85impl AlbedoComponent {
86    pub fn to_value(&self) -> Vec4 {
87        match *self {
88            Self::Value(value) => value,
89            Self::ValueVertex { value, .. } => value,
90            Self::TextureValue { value, .. } => value,
91            _ => Vec4::splat(1.0),
92        }
93    }
94
95    pub fn to_flags(&self) -> MaterialFlags {
96        match *self {
97            Self::None => MaterialFlags::empty(),
98            Self::Value(_) | Self::Texture(_) | Self::TextureValue { .. } => MaterialFlags::ALBEDO_ACTIVE,
99            Self::Vertex { srgb: false }
100            | Self::ValueVertex { srgb: false, .. }
101            | Self::TextureVertex { srgb: false, .. }
102            | Self::TextureVertexValue { srgb: false, .. } => {
103                MaterialFlags::ALBEDO_ACTIVE | MaterialFlags::ALBEDO_BLEND
104            }
105            Self::Vertex { srgb: true }
106            | Self::ValueVertex { srgb: true, .. }
107            | Self::TextureVertex { srgb: true, .. }
108            | Self::TextureVertexValue { srgb: true, .. } => {
109                MaterialFlags::ALBEDO_ACTIVE | MaterialFlags::ALBEDO_BLEND | MaterialFlags::ALBEDO_VERTEX_SRGB
110            }
111        }
112    }
113
114    pub fn is_texture(&self) -> bool {
115        matches!(
116            *self,
117            Self::Texture(..)
118                | Self::TextureVertex { .. }
119                | Self::TextureValue { .. }
120                | Self::TextureVertexValue { .. }
121        )
122    }
123
124    pub fn to_texture(&self) -> Option<&TextureHandle> {
125        match *self {
126            Self::None | Self::Vertex { .. } | Self::Value(_) | Self::ValueVertex { .. } => None,
127            Self::Texture(ref texture)
128            | Self::TextureVertex { ref texture, .. }
129            | Self::TextureValue { ref texture, .. }
130            | Self::TextureVertexValue { ref texture, .. } => Some(texture),
131        }
132    }
133}
134
135/// Generic container for a component of a material that could either be from a
136/// texture or a fixed value.
137#[derive(Debug, Clone)]
138pub enum MaterialComponent<T> {
139    None,
140    Value(T),
141    Texture(TextureHandle),
142    TextureValue { texture: TextureHandle, value: T },
143}
144
145impl<T> Default for MaterialComponent<T> {
146    fn default() -> Self {
147        Self::None
148    }
149}
150
151impl<T: Copy> MaterialComponent<T> {
152    pub fn to_value(&self, default: T) -> T {
153        match *self {
154            Self::Value(value) | Self::TextureValue { value, .. } => value,
155            Self::None | Self::Texture(_) => default,
156        }
157    }
158
159    pub fn is_texture(&self) -> bool {
160        matches!(*self, Self::Texture(..) | Self::TextureValue { .. })
161    }
162
163    pub fn to_texture(&self) -> Option<&TextureHandle> {
164        match *self {
165            Self::None | Self::Value(_) => None,
166            Self::Texture(ref texture) | Self::TextureValue { ref texture, .. } => Some(texture),
167        }
168    }
169}
170
171/// The direction of the Y (i.e. green) value in the normal maps
172#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
173pub enum NormalTextureYDirection {
174    /// Right handed. X right, Y up. OpenGL convention.
175    Up,
176    /// Left handed. X right, Y down. DirectX convention.
177    Down,
178}
179
180impl Default for NormalTextureYDirection {
181    fn default() -> Self {
182        Self::Up
183    }
184}
185
186/// How normals should be derived
187#[derive(Debug, Clone)]
188pub enum NormalTexture {
189    /// No normal texture.
190    None,
191    /// Normal stored in RGB values.
192    Tricomponent(TextureHandle, NormalTextureYDirection),
193    /// Normal stored in RG values, third value should be reconstructed.
194    Bicomponent(TextureHandle, NormalTextureYDirection),
195    /// Normal stored in Green and Alpha values, third value should be
196    /// reconstructed. This is useful for storing in BC3 or BC7 compressed
197    /// textures.
198    BicomponentSwizzled(TextureHandle, NormalTextureYDirection),
199}
200impl Default for NormalTexture {
201    fn default() -> Self {
202        Self::None
203    }
204}
205
206impl NormalTexture {
207    pub fn to_texture(&self) -> Option<&TextureHandle> {
208        match *self {
209            Self::None => None,
210            Self::Tricomponent(ref texture, _)
211            | Self::Bicomponent(ref texture, _)
212            | Self::BicomponentSwizzled(ref texture, _) => Some(texture),
213        }
214    }
215
216    pub fn to_flags(&self) -> MaterialFlags {
217        // Start with the base component flags
218        let base = match self {
219            Self::None => MaterialFlags::empty(),
220            Self::Tricomponent(..) => MaterialFlags::empty(),
221            Self::Bicomponent(..) => MaterialFlags::BICOMPONENT_NORMAL,
222            Self::BicomponentSwizzled(..) => MaterialFlags::BICOMPONENT_NORMAL | MaterialFlags::SWIZZLED_NORMAL,
223        };
224
225        // Add the direction flags
226        match self {
227            Self::Tricomponent(_, NormalTextureYDirection::Down)
228            | Self::Bicomponent(_, NormalTextureYDirection::Down)
229            | Self::BicomponentSwizzled(_, NormalTextureYDirection::Down) => base | MaterialFlags::YDOWN_NORMAL,
230            _ => base,
231        }
232    }
233}
234
235/// How the Ambient Occlusion, Metalic, and Roughness values should be
236/// determined.
237#[derive(Debug, Clone)]
238pub enum AoMRTextures {
239    None,
240    Combined {
241        /// Texture with Ambient Occlusion in R, Roughness in G, and Metallic in
242        /// B
243        texture: Option<TextureHandle>,
244    },
245    SwizzledSplit {
246        /// Texture with Ambient Occlusion in R
247        ao_texture: Option<TextureHandle>,
248        /// Texture with Roughness in G and Metallic in B
249        mr_texture: Option<TextureHandle>,
250    },
251    Split {
252        /// Texture with Ambient Occlusion in R
253        ao_texture: Option<TextureHandle>,
254        /// Texture with Roughness in R and Metallic in G
255        mr_texture: Option<TextureHandle>,
256    },
257    BWSplit {
258        /// Texture with Ambient Occlusion in R
259        ao_texture: Option<TextureHandle>,
260        /// Texture with Metallic in R
261        m_texture: Option<TextureHandle>,
262        /// Texture with Roughness in R
263        r_texture: Option<TextureHandle>,
264    },
265}
266
267impl AoMRTextures {
268    pub fn to_roughness_texture(&self) -> Option<&TextureHandle> {
269        match *self {
270            Self::Combined {
271                texture: Some(ref texture),
272            } => Some(texture),
273            Self::SwizzledSplit {
274                mr_texture: Some(ref texture),
275                ..
276            } => Some(texture),
277            Self::Split {
278                mr_texture: Some(ref texture),
279                ..
280            } => Some(texture),
281            Self::BWSplit {
282                r_texture: Some(ref texture),
283                ..
284            } => Some(texture),
285            _ => None,
286        }
287    }
288
289    pub fn to_metallic_texture(&self) -> Option<&TextureHandle> {
290        match *self {
291            Self::Combined { .. } => None,
292            Self::SwizzledSplit { .. } => None,
293            Self::Split { .. } => None,
294            Self::BWSplit {
295                m_texture: Some(ref texture),
296                ..
297            } => Some(texture),
298            _ => None,
299        }
300    }
301
302    pub fn to_ao_texture(&self) -> Option<&TextureHandle> {
303        match *self {
304            Self::Combined { .. } => None,
305            Self::SwizzledSplit {
306                ao_texture: Some(ref texture),
307                ..
308            } => Some(texture),
309            Self::Split {
310                ao_texture: Some(ref texture),
311                ..
312            } => Some(texture),
313            Self::BWSplit {
314                ao_texture: Some(ref texture),
315                ..
316            } => Some(texture),
317            _ => None,
318        }
319    }
320
321    pub fn to_flags(&self) -> MaterialFlags {
322        match self {
323            Self::Combined { .. } => MaterialFlags::AOMR_COMBINED,
324            Self::SwizzledSplit { .. } => MaterialFlags::AOMR_SWIZZLED_SPLIT,
325            Self::Split { .. } => MaterialFlags::AOMR_SPLIT,
326            Self::BWSplit { .. } => MaterialFlags::AOMR_BW_SPLIT,
327            // Use AOMR_COMBINED so shader only checks roughness texture, then bails
328            Self::None => MaterialFlags::AOMR_COMBINED,
329        }
330    }
331}
332impl Default for AoMRTextures {
333    fn default() -> Self {
334        Self::None
335    }
336}
337
338/// How clearcoat values should be derived.
339#[derive(Debug, Clone)]
340pub enum ClearcoatTextures {
341    GltfCombined {
342        /// Texture with Clearcoat in R, and Clearcoat Roughness in G
343        texture: Option<TextureHandle>,
344    },
345    GltfSplit {
346        /// Texture with Clearcoat in R
347        clearcoat_texture: Option<TextureHandle>,
348        /// Texture with Clearcoat Roughness in G
349        clearcoat_roughness_texture: Option<TextureHandle>,
350    },
351    BWSplit {
352        /// Texture with Clearcoat in R
353        clearcoat_texture: Option<TextureHandle>,
354        /// Texture with Clearcoat Roughness in R
355        clearcoat_roughness_texture: Option<TextureHandle>,
356    },
357    None,
358}
359
360impl ClearcoatTextures {
361    pub fn to_clearcoat_texture(&self) -> Option<&TextureHandle> {
362        match *self {
363            Self::GltfCombined {
364                texture: Some(ref texture),
365            } => Some(texture),
366            Self::GltfSplit {
367                clearcoat_texture: Some(ref texture),
368                ..
369            } => Some(texture),
370            Self::BWSplit {
371                clearcoat_texture: Some(ref texture),
372                ..
373            } => Some(texture),
374            _ => None,
375        }
376    }
377
378    pub fn to_clearcoat_roughness_texture(&self) -> Option<&TextureHandle> {
379        match *self {
380            Self::GltfCombined { .. } => None,
381            Self::GltfSplit {
382                clearcoat_roughness_texture: Some(ref texture),
383                ..
384            } => Some(texture),
385            Self::BWSplit {
386                clearcoat_roughness_texture: Some(ref texture),
387                ..
388            } => Some(texture),
389            _ => None,
390        }
391    }
392
393    pub fn to_flags(&self) -> MaterialFlags {
394        match self {
395            Self::GltfCombined { .. } => MaterialFlags::CC_GLTF_COMBINED,
396            Self::GltfSplit { .. } => MaterialFlags::CC_GLTF_SPLIT,
397            Self::BWSplit { .. } => MaterialFlags::CC_BW_SPLIT,
398            // Use CC_GLTF_COMBINED so shader only checks clear coat texture, then bails
399            Self::None => MaterialFlags::CC_GLTF_COMBINED,
400        }
401    }
402}
403impl Default for ClearcoatTextures {
404    fn default() -> Self {
405        Self::None
406    }
407}
408
409/// How textures should be sampled.
410#[derive(Debug, Copy, Clone, PartialEq, Eq)]
411pub enum SampleType {
412    Nearest,
413    Linear,
414}
415impl Default for SampleType {
416    fn default() -> Self {
417        Self::Linear
418    }
419}
420
421/// The type of transparency in a material.
422#[repr(u8)]
423#[derive(Debug, Copy, Clone, PartialEq)]
424pub enum TransparencyType {
425    /// Alpha is completely ignored.
426    Opaque,
427    /// Alpha less than a specified value is discorded.
428    Cutout,
429    /// Alpha is blended.
430    Blend,
431}
432impl From<Transparency> for TransparencyType {
433    fn from(t: Transparency) -> Self {
434        match t {
435            Transparency::Opaque => Self::Opaque,
436            Transparency::Cutout { .. } => Self::Cutout,
437            Transparency::Blend => Self::Blend,
438        }
439    }
440}
441impl TransparencyType {
442    pub fn to_debug_str(self) -> &'static str {
443        match self {
444            TransparencyType::Opaque => "opaque",
445            TransparencyType::Cutout => "cutout",
446            TransparencyType::Blend => "blend",
447        }
448    }
449
450    pub fn to_sorting(self) -> Option<Sorting> {
451        match self {
452            Self::Opaque => None,
453            Self::Cutout => None,
454            Self::Blend => Some(Sorting::BackToFront),
455        }
456    }
457}
458
459#[allow(clippy::cmp_owned)] // This thinks making a temporary TransparencyType is the end of the world
460impl PartialEq<Transparency> for TransparencyType {
461    fn eq(&self, other: &Transparency) -> bool {
462        *self == Self::from(*other)
463    }
464}
465
466#[allow(clippy::cmp_owned)]
467impl PartialEq<TransparencyType> for Transparency {
468    fn eq(&self, other: &TransparencyType) -> bool {
469        TransparencyType::from(*self) == *other
470    }
471}
472
473/// How transparency should be handled in a material.
474#[derive(Debug, Copy, Clone, PartialEq)]
475pub enum Transparency {
476    /// Alpha is completely ignored.
477    Opaque,
478    /// Pixels with alpha less than `cutout` is discorded.
479    Cutout { cutout: f32 },
480    /// Alpha is blended.
481    Blend,
482}
483impl Default for Transparency {
484    fn default() -> Self {
485        Self::Opaque
486    }
487}
488
489// Consider:
490//
491// - Green screen value
492/// A set of textures and values that determine the how an object interacts with
493/// light.
494#[derive(Default)]
495pub struct PbrMaterial {
496    pub albedo: AlbedoComponent,
497    pub transparency: Transparency,
498    pub normal: NormalTexture,
499    pub aomr_textures: AoMRTextures,
500    pub ao_factor: Option<f32>,
501    pub metallic_factor: Option<f32>,
502    pub roughness_factor: Option<f32>,
503    pub clearcoat_textures: ClearcoatTextures,
504    pub clearcoat_factor: Option<f32>,
505    pub clearcoat_roughness_factor: Option<f32>,
506    pub emissive: MaterialComponent<Vec3>,
507    pub reflectance: MaterialComponent<f32>,
508    pub anisotropy: MaterialComponent<f32>,
509    pub uv_transform0: Mat3,
510    pub uv_transform1: Mat3,
511    // TODO: Determine how to make this a clearer part of the type system, esp. with the changable_struct macro.
512    pub unlit: bool,
513    pub sample_type: SampleType,
514}
515
516impl Material for PbrMaterial {
517    const TEXTURE_COUNT: u32 = 10;
518    const DATA_SIZE: u32 = mem::size_of::<ShaderMaterial>() as _;
519
520    fn object_key(&self) -> u64 {
521        TransparencyType::from(self.transparency) as u64
522    }
523
524    fn to_textures<'a>(&'a self, slice: &mut [Option<&'a TextureHandle>]) {
525        slice[0] = self.albedo.to_texture();
526        slice[1] = self.normal.to_texture();
527        slice[2] = self.aomr_textures.to_roughness_texture();
528        slice[3] = self.aomr_textures.to_metallic_texture();
529        slice[4] = self.reflectance.to_texture();
530        slice[5] = self.clearcoat_textures.to_clearcoat_texture();
531        slice[6] = self.clearcoat_textures.to_clearcoat_roughness_texture();
532        slice[7] = self.emissive.to_texture();
533        slice[8] = self.anisotropy.to_texture();
534        slice[9] = self.aomr_textures.to_ao_texture();
535    }
536
537    fn to_data(&self, slice: &mut [u8]) {
538        slice.copy_from_slice(bytemuck::bytes_of(&ShaderMaterial::from_material(self)));
539    }
540}
541
542impl DepthRenderableMaterial for PbrMaterial {
543    const ALPHA_CUTOUT: Option<AlphaCutoutSpec> = Some(AlphaCutoutSpec {
544        index: 0,
545        cutoff_offset: 152,
546        uv_transform_offset: Some(0),
547    });
548}
549
550#[test]
551fn cutout_offset() {
552    assert_eq!(bytemuck::offset_of!(ShaderMaterial, alpha_cutout), 152);
553    assert_eq!(bytemuck::offset_of!(ShaderMaterial, uv_transform0), 0);
554}
555
556#[repr(C)]
557#[derive(Debug, Default, Copy, Clone)]
558struct ShaderMaterial {
559    uv_transform0: Mat3A,
560    uv_transform1: Mat3A,
561
562    albedo: Vec4,
563    emissive: Vec3,
564    roughness: f32,
565    metallic: f32,
566    reflectance: f32,
567    clear_coat: f32,
568    clear_coat_roughness: f32,
569    anisotropy: f32,
570    ambient_occlusion: f32,
571    alpha_cutout: f32,
572
573    material_flags: MaterialFlags,
574}
575
576unsafe impl bytemuck::Zeroable for ShaderMaterial {}
577unsafe impl bytemuck::Pod for ShaderMaterial {}
578
579impl ShaderMaterial {
580    fn from_material(material: &PbrMaterial) -> Self {
581        Self {
582            uv_transform0: material.uv_transform0.into(),
583            uv_transform1: material.uv_transform1.into(),
584            albedo: material.albedo.to_value(),
585            roughness: material.roughness_factor.unwrap_or(0.0),
586            metallic: material.metallic_factor.unwrap_or(0.0),
587            reflectance: material.reflectance.to_value(0.5),
588            clear_coat: material.clearcoat_factor.unwrap_or(0.0),
589            clear_coat_roughness: material.clearcoat_roughness_factor.unwrap_or(0.0),
590            emissive: material.emissive.to_value(Vec3::ZERO),
591            anisotropy: material.anisotropy.to_value(0.0),
592            ambient_occlusion: material.ao_factor.unwrap_or(1.0),
593            alpha_cutout: match material.transparency {
594                Transparency::Cutout { cutout } => cutout,
595                _ => 0.0,
596            },
597            material_flags: {
598                let mut flags = material.albedo.to_flags();
599                flags |= material.normal.to_flags();
600                flags |= material.aomr_textures.to_flags();
601                flags |= material.clearcoat_textures.to_flags();
602                flags.set(MaterialFlags::UNLIT, material.unlit);
603                flags.set(
604                    MaterialFlags::NEAREST,
605                    match material.sample_type {
606                        SampleType::Nearest => true,
607                        SampleType::Linear => false,
608                    },
609                );
610                flags
611            },
612        }
613    }
614}