Skip to main content

wde_pbr/deferred/
pbr_material.rs

1use bevy::{
2    ecs::system::{SystemParamItem, lifetimeless::SRes},
3    prelude::*
4};
5use wde_renderer::prelude::*;
6
7use crate::prelude::PbrBatchesMarker;
8
9/// Marker component to indicate that an entity's transform should be included in the [PbrSsboTransform] updates.
10/// This automatically adds the [PbrBatchesMarker] (and thus the [PbrSsboTransformMarker]) to the entity.
11#[derive(Component, Reflect, Clone)]
12#[reflect(Component)]
13#[require(PbrBatchesMarker)]
14pub struct PbrMaterial3d<T: RenderBinding + Asset>(pub Handle<T>);
15
16/// Uniform structure for PBR material data.
17///
18/// The structure sent to the GPU is the following :
19/// ```wgsl
20/// struct Material3dUniform {
21///     flags: vec4<f32>, // Flags indicating material textures (1.0 = present, 0.0 = absent) - albedo, metallic-roughness, normal, occlusion
22///     albedo: vec4<f32>, // Albedo color of the material (r, g, b)
23///     metallic: f32,    // Metallic intensity of the material
24///     roughness: f32,   // Roughness intensity of the material
25///     _padding: vec2<f32>, // Unused padding to align to 16 bytes
26/// };
27///
28/// @group(2) @binding(0) var<uniform> pbr_material: PbrMaterialUniform;    /// Material uniform buffer
29/// @group(2) @binding(1) var albedo_texture: texture_2d<f32>;              /// Albedo texture (r, g, b)
30/// @group(2) @binding(2) var albedo_sampler: sampler;                      /// Albedo texture sampler
31/// @group(2) @binding(3) var metallic_roughness_texture: texture_2d<f32>;  /// Metallic-roughness texture (b = metallic, g = roughness)
32/// @group(2) @binding(4) var metallic_roughness_sampler: sampler;          /// Metallic-roughness texture sampler
33/// @group(2) @binding(5) var normal_texture: texture_2d<f32>;              /// Normal texture (x, y, z)
34/// @group(2) @binding(6) var normal_sampler: sampler;                      /// Normal texture sampler
35/// @group(2) @binding(7) var occlusion_texture: texture_2d<f32>;           /// Occlusion texture (r)
36/// @group(2) @binding(8) var occlusion_sampler: sampler;                   /// Occlusion texture sampler
37/// ```
38#[repr(C)]
39#[derive(Default, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
40pub(crate) struct PbrMaterialUniform {
41    /// Flags indicating material textures (1.0 = present, 0.0 = absent) - albedo, metallic-roughness, normal, occlusion
42    pub flags: [f32; 4],
43    /// Albedo color of the material (r, g, b).
44    pub albedo: [f32; 4],
45    /// Metallic intensity of the material.
46    pub metallic: f32,
47    /// Roughness intensity of the material.
48    pub roughness: f32,
49    /// Unused padding to align to 16 bytes.
50    _padding: [f32; 2]
51}
52
53/// How a [PbrMaterial] is composited.
54#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Reflect)]
55pub enum PbrAlphaMode {
56    /// Written to the G-buffer and lit by the deferred lighting pass.
57    #[default]
58    Opaque,
59    /// Forward-lit and alpha-blended in the transparent pass. Does not cast shadows.
60    Blend
61}
62
63/// Describes a physically based rendering material.
64/// It contains the material properties such as albedo color, metallic and roughness intensity, as well as the textures for these properties. It also contains a uniform buffer that is automatically created and updated with the material data, which can be used in the shader to access the material properties.
65/// The [PbrMaterial3d] component is a wrapper around a handle to a [PbrMaterial] asset, which also adds the [PbrSsboTransformMarker] to the entity, indicating that its transform should be included in the [PbrSsboTransform](crate::prelude::PbrSsboTransform) updates.
66#[derive(Asset, Clone, TypePath)]
67pub struct PbrMaterial {
68    pub label: String,
69
70    /// The albedo color of the material instance (r, g, b).
71    /// The alpha channel is the opacity when `alpha_mode` is [PbrAlphaMode::Blend], unused otherwise.
72    pub albedo: (f32, f32, f32, f32),
73    /// The albedo texture of the material instance. If `None`, the material will use the albedo color.
74    pub albedo_t: Option<Handle<Texture>>,
75
76    /// The metallic intensity of the material instance.
77    pub metallic: f32,
78    /// The roughness intensity of the material instance.
79    pub roughness: f32,
80    /// The metallic-roughness texture of the material instance. If `None`, the material will use the metallic and roughness scalar intensity.
81    /// The metallic value is stored in the blue channel, and the roughness value is stored in the green channel.
82    pub metallic_roughness_t: Option<Handle<Texture>>,
83
84    /// The normal texture of the material instance.
85    /// The normal map is expected to be in tangent space.
86    /// The alpha channel is unused.
87    pub normal_t: Option<Handle<Texture>>,
88    /// The occlusion texture of the material instance.
89    /// The occlusion value is stored in the red channel.
90    pub occlusion_t: Option<Handle<Texture>>,
91
92    /// An optional stencil value to set the stencil buffer to when rendering the material.
93    pub stencil_value: Option<u32>,
94
95    /// Whether the material is drawn opaque in the G-buffer or alpha-blended in the transparent pass.
96    pub alpha_mode: PbrAlphaMode,
97
98    /// The buffer containing the material data. It is automatically created and updated if not set.
99    pub uniform_buffer: Option<Handle<Buffer>>,
100    /// Uniform bytes last written to `uniform_buffer`, so `describe` (which reruns on every retry
101    /// attempt, not just on an actual material change) doesn't recreate the buffer - and orphan a
102    /// not-yet-ready one - on every attempt.
103    pub last_uniform: Option<Vec<u8>>
104}
105impl Default for PbrMaterial {
106    fn default() -> Self {
107        PbrMaterial {
108            label: "pbr-material".to_string(),
109
110            albedo: (1.0, 1.0, 1.0, 0.0),
111            albedo_t: None,
112
113            metallic: 1.0,
114            roughness: 1.0,
115            metallic_roughness_t: None,
116
117            normal_t: None,
118            occlusion_t: None,
119
120            stencil_value: None,
121
122            alpha_mode: PbrAlphaMode::Opaque,
123
124            uniform_buffer: None,
125            last_uniform: None
126        }
127    }
128}
129impl RenderBinding for PbrMaterial {
130    type Params = (SRes<AssetServer>, SRes<PlaceholderTexture>);
131
132    fn describe(
133        &mut self,
134        (asset_server, placeholder_tex): &SystemParamItem<Self::Params>,
135        builder: &mut RenderBindingBuilder
136    ) {
137        // Create the uniform buffer
138        let uniform = PbrMaterialUniform {
139            flags: [
140                if self.albedo_t.is_some() { 1.0 } else { 0.0 },
141                if self.metallic_roughness_t.is_some() {
142                    1.0
143                } else {
144                    0.0
145                },
146                if self.normal_t.is_some() { 1.0 } else { 0.0 },
147                if self.occlusion_t.is_some() { 1.0 } else { 0.0 }
148            ],
149            albedo: [self.albedo.0, self.albedo.1, self.albedo.2, self.albedo.3],
150            metallic: self.metallic,
151            roughness: self.roughness,
152            _padding: [0.0, 0.0]
153        };
154        // `describe` reruns on every retry attempt (not just when the material asset actually
155        // changed) - recreating the buffer unconditionally used to orphan a not-yet-ready one on
156        // every attempt, so it could never converge. Only recreating it when the uniform content
157        // actually changed keeps a pending buffer stable across retries, while still refreshing a
158        // real change (e.g. `albedo_t` set after the first `describe` call used to leave
159        // `flags.x` at 0 forever).
160        let content = bytemuck::cast_slice(&[uniform]).to_vec();
161        if self.uniform_buffer.is_none() || self.last_uniform.as_deref() != Some(&content[..]) {
162            let uniform_handle = asset_server.add(Buffer {
163                label: format!("{}-uniform-buffer", self.label),
164                size: std::mem::size_of::<PbrMaterialUniform>(),
165                usage: BufferUsage::UNIFORM | BufferUsage::COPY_DST,
166                content: Some(content.clone())
167            });
168            self.uniform_buffer = Some(uniform_handle);
169            self.last_uniform = Some(content);
170        }
171
172        // Build the material
173        builder.add_buffer_from_id(Some(self.uniform_buffer.as_ref().unwrap().id()));
174        if let Some(albedo_t) = &self.albedo_t {
175            builder.add_texture_view_from_id(Some(albedo_t.id()));
176            builder.add_texture_sampler_from_id(Some(albedo_t.id()));
177        } else {
178            builder.add_texture_view_from_id(Some(placeholder_tex.0.id()));
179            builder.add_texture_sampler_from_id(Some(placeholder_tex.0.id()));
180        }
181        if let Some(metallic_roughness_t) = &self.metallic_roughness_t {
182            builder.add_texture_view_from_id(Some(metallic_roughness_t.id()));
183            builder.add_texture_sampler_from_id(Some(metallic_roughness_t.id()));
184        } else {
185            builder.add_texture_view_from_id(Some(placeholder_tex.0.id()));
186            builder.add_texture_sampler_from_id(Some(placeholder_tex.0.id()));
187        }
188        if let Some(normal_t) = &self.normal_t {
189            builder.add_texture_view_from_id(Some(normal_t.id()));
190            builder.add_texture_sampler_from_id(Some(normal_t.id()));
191        } else {
192            builder.add_texture_view_from_id(Some(placeholder_tex.0.id()));
193            builder.add_texture_sampler_from_id(Some(placeholder_tex.0.id()));
194        }
195        if let Some(occlusion_t) = &self.occlusion_t {
196            builder.add_texture_view_from_id(Some(occlusion_t.id()));
197            builder.add_texture_sampler_from_id(Some(occlusion_t.id()));
198        } else {
199            builder.add_texture_view_from_id(Some(placeholder_tex.0.id()));
200            builder.add_texture_sampler_from_id(Some(placeholder_tex.0.id()));
201        }
202    }
203
204    fn label(&self) -> &str {
205        &self.label
206    }
207}