1use std::sync::OnceLock;
4
5use wgpu::{BindGroup, BindGroupLayout, Device, RenderPipeline, TextureFormat};
6
7use crate::descriptor::{MaterialDescriptor, MaterialDescriptorError, MaterialType};
8use crate::pipeline::{create_lit_pipeline, create_shader, create_unlit_pipeline};
9use crate::shader::{
10 builtin_shader_source, load_shader_source, BuiltinShader, ShaderSource, ShaderSourceError,
11};
12use crate::texture::{FallbackTexture, Texture};
13
14pub struct MaterialPipeline {
15 pub name: String,
16 pub pipeline: RenderPipeline,
17 pub material_type: MaterialType,
18 pub bind_group: Option<BindGroup>,
22}
23
24static MATERIAL_BIND_GROUP_LAYOUT: OnceLock<BindGroupLayout> = OnceLock::new();
26
27#[derive(thiserror::Error, Debug)]
28pub enum MaterialError {
29 #[error(transparent)]
30 ShaderSource(#[from] ShaderSourceError),
31 #[error(transparent)]
32 Descriptor(#[from] MaterialDescriptorError),
33 #[error("Failed to load texture '{path}': {source}")]
34 TextureLoad {
35 path: String,
36 source: crate::texture::TextureError,
37 },
38}
39
40pub fn get_material_bind_group_layout(device: &Device) -> &'static BindGroupLayout {
42 MATERIAL_BIND_GROUP_LAYOUT.get_or_init(|| {
43 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
44 label: Some("Material Bind Group Layout"),
45 entries: &[
46 wgpu::BindGroupLayoutEntry {
48 binding: 0,
49 visibility: wgpu::ShaderStages::FRAGMENT,
50 ty: wgpu::BindingType::Texture {
51 multisampled: false,
52 view_dimension: wgpu::TextureViewDimension::D2,
53 sample_type: wgpu::TextureSampleType::Float { filterable: true },
54 },
55 count: None,
56 },
57 wgpu::BindGroupLayoutEntry {
59 binding: 1,
60 visibility: wgpu::ShaderStages::FRAGMENT,
61 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
62 count: None,
63 },
64 ],
65 })
66 })
67}
68
69impl MaterialPipeline {
70 #[allow(clippy::too_many_arguments)]
71 pub fn from_builtin(
72 device: &Device,
73 queue: &wgpu::Queue,
74 format: TextureFormat,
75 camera_layout: &BindGroupLayout,
76 light_layout: &BindGroupLayout,
77 shader: BuiltinShader,
78 material_type: MaterialType,
79 name: impl Into<String>,
80 ) -> Self {
81 let shader_src = builtin_shader_source(shader);
82 let shader_module = create_shader(device, shader_src, Some("Builtin Material Shader"));
83
84 let material_layout = get_material_bind_group_layout(device);
85
86 let (pipeline, bind_group) = match material_type {
87 MaterialType::Lit => {
88 let pipeline = create_lit_pipeline(
89 device,
90 &shader_module,
91 format,
92 camera_layout,
93 material_layout,
94 light_layout,
95 );
96 let fallback = FallbackTexture::new(device, queue);
97 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
98 label: Some("Material Bind Group (Fallback)"),
99 layout: material_layout,
100 entries: &[
101 wgpu::BindGroupEntry {
102 binding: 0,
103 resource: wgpu::BindingResource::TextureView(&fallback.texture.view),
104 },
105 wgpu::BindGroupEntry {
106 binding: 1,
107 resource: wgpu::BindingResource::Sampler(&fallback.texture.sampler),
108 },
109 ],
110 });
111 (pipeline, Some(bind_group))
112 }
113 MaterialType::Unlit => {
114 let pipeline = create_unlit_pipeline(
115 device,
116 &shader_module,
117 format,
118 camera_layout,
119 material_layout,
120 );
121 let fallback = FallbackTexture::new(device, queue);
122 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
123 label: Some("Material Bind Group (Fallback)"),
124 layout: material_layout,
125 entries: &[
126 wgpu::BindGroupEntry {
127 binding: 0,
128 resource: wgpu::BindingResource::TextureView(&fallback.texture.view),
129 },
130 wgpu::BindGroupEntry {
131 binding: 1,
132 resource: wgpu::BindingResource::Sampler(&fallback.texture.sampler),
133 },
134 ],
135 });
136 (pipeline, Some(bind_group))
137 }
138 MaterialType::Basic => {
139 let pipeline =
140 crate::pipeline::create_basic_pipeline(device, &shader_module, format);
141 (pipeline, None)
142 }
143 };
144
145 Self {
146 name: name.into(),
147 pipeline,
148 material_type,
149 bind_group,
150 }
151 }
152
153 #[allow(clippy::too_many_arguments)]
154 pub fn from_source(
155 device: &Device,
156 queue: &wgpu::Queue,
157 format: TextureFormat,
158 camera_layout: &BindGroupLayout,
159 light_layout: &BindGroupLayout,
160 source: &ShaderSource,
161 material_type: MaterialType,
162 albedo_texture: Option<&Texture>,
163 name: impl Into<String>,
164 ) -> Result<Self, MaterialError> {
165 let shader_src = load_shader_source(source)?;
166 let shader_module = create_shader(device, &shader_src, Some("Custom Material Shader"));
167
168 let material_layout = get_material_bind_group_layout(device);
169
170 let (pipeline, bind_group) = match material_type {
171 MaterialType::Lit => {
172 let pipeline = create_lit_pipeline(
173 device,
174 &shader_module,
175 format,
176 camera_layout,
177 material_layout,
178 light_layout,
179 );
180 let bind_group =
181 create_material_bind_group(device, queue, material_layout, albedo_texture);
182 (pipeline, Some(bind_group))
183 }
184 MaterialType::Unlit => {
185 let pipeline = create_unlit_pipeline(
186 device,
187 &shader_module,
188 format,
189 camera_layout,
190 material_layout,
191 );
192 let bind_group =
193 create_material_bind_group(device, queue, material_layout, albedo_texture);
194 (pipeline, Some(bind_group))
195 }
196 MaterialType::Basic => {
197 let pipeline =
198 crate::pipeline::create_basic_pipeline(device, &shader_module, format);
199 (pipeline, None)
200 }
201 };
202
203 Ok(Self {
204 name: name.into(),
205 pipeline,
206 material_type,
207 bind_group,
208 })
209 }
210
211 #[allow(clippy::too_many_arguments)]
212 pub fn from_source_with_fallback(
213 device: &Device,
214 queue: &wgpu::Queue,
215 format: TextureFormat,
216 camera_layout: &BindGroupLayout,
217 light_layout: &BindGroupLayout,
218 source: &ShaderSource,
219 fallback_shader: BuiltinShader,
220 material_type: MaterialType,
221 albedo_texture: Option<&Texture>,
222 name: impl Into<String>,
223 ) -> Self {
224 let name = name.into();
225
226 match Self::from_source(
227 device,
228 queue,
229 format,
230 camera_layout,
231 light_layout,
232 source,
233 material_type,
234 albedo_texture,
235 name.clone(),
236 ) {
237 Ok(material) => material,
238 Err(err) => {
239 tracing::warn!(
240 "Material '{}' failed to load custom shader, using fallback {:?}: {}",
241 name,
242 fallback_shader,
243 err
244 );
245
246 Self::from_builtin(
247 device,
248 queue,
249 format,
250 camera_layout,
251 light_layout,
252 fallback_shader,
253 material_type,
254 name,
255 )
256 }
257 }
258 }
259
260 pub fn from_descriptor(
261 device: &Device,
262 queue: &wgpu::Queue,
263 format: TextureFormat,
264 camera_layout: &BindGroupLayout,
265 light_layout: &BindGroupLayout,
266 descriptor: &MaterialDescriptor,
267 ) -> Result<Self, MaterialError> {
268 let source = descriptor.shader_source()?;
269 let fallback = descriptor.fallback_shader()?;
270
271 let albedo_texture = if let Some(path) = &descriptor.albedo_texture {
273 Some(Texture::from_file(device, queue, path).map_err(|source| {
274 MaterialError::TextureLoad {
275 path: path.clone(),
276 source,
277 }
278 })?)
279 } else {
280 None
281 };
282
283 Ok(Self::from_source_with_fallback(
284 device,
285 queue,
286 format,
287 camera_layout,
288 light_layout,
289 &source,
290 fallback,
291 descriptor.material_type,
292 albedo_texture.as_ref(),
293 descriptor.name.clone(),
294 ))
295 }
296}
297
298fn create_material_bind_group(
300 device: &Device,
301 queue: &wgpu::Queue,
302 layout: &BindGroupLayout,
303 texture: Option<&Texture>,
304) -> BindGroup {
305 match texture {
306 Some(tex) => device.create_bind_group(&wgpu::BindGroupDescriptor {
307 label: Some("Material Bind Group"),
308 layout,
309 entries: &[
310 wgpu::BindGroupEntry {
311 binding: 0,
312 resource: wgpu::BindingResource::TextureView(&tex.view),
313 },
314 wgpu::BindGroupEntry {
315 binding: 1,
316 resource: wgpu::BindingResource::Sampler(&tex.sampler),
317 },
318 ],
319 }),
320 None => {
321 let fallback = FallbackTexture::new(device, queue);
322 device.create_bind_group(&wgpu::BindGroupDescriptor {
323 label: Some("Material Bind Group (Fallback)"),
324 layout,
325 entries: &[
326 wgpu::BindGroupEntry {
327 binding: 0,
328 resource: wgpu::BindingResource::TextureView(&fallback.texture.view),
329 },
330 wgpu::BindGroupEntry {
331 binding: 1,
332 resource: wgpu::BindingResource::Sampler(&fallback.texture.sampler),
333 },
334 ],
335 })
336 }
337 }
338}