1use astrelis_core::profiling::profile_function;
26
27use crate::{Color, GraphicsContext};
28use ahash::HashMap;
29use glam::{Mat4, Vec2, Vec3, Vec4};
30use std::sync::Arc;
31
32#[derive(Debug, Clone)]
34pub enum MaterialParameter {
35 Float(f32),
37 Vec2(Vec2),
39 Vec3(Vec3),
41 Vec4(Vec4),
43 Color(Color),
45 Matrix4(Mat4),
47 FloatArray(Vec<f32>),
49 Vec2Array(Vec<Vec2>),
51 Vec3Array(Vec<Vec3>),
53 Vec4Array(Vec<Vec4>),
55}
56
57impl MaterialParameter {
58 pub fn as_bytes(&self) -> Vec<u8> {
60 match self {
61 MaterialParameter::Float(v) => bytemuck::bytes_of(v).to_vec(),
62 MaterialParameter::Vec2(v) => bytemuck::bytes_of(v).to_vec(),
63 MaterialParameter::Vec3(v) => {
64 let mut bytes = Vec::with_capacity(16);
66 bytes.extend_from_slice(bytemuck::bytes_of(v));
67 bytes.extend_from_slice(&[0u8; 4]); bytes
69 }
70 MaterialParameter::Vec4(v) => bytemuck::bytes_of(v).to_vec(),
71 MaterialParameter::Color(c) => bytemuck::bytes_of(c).to_vec(),
72 MaterialParameter::Matrix4(m) => bytemuck::bytes_of(m).to_vec(),
73 MaterialParameter::FloatArray(arr) => bytemuck::cast_slice(arr).to_vec(),
74 MaterialParameter::Vec2Array(arr) => bytemuck::cast_slice(arr).to_vec(),
75 MaterialParameter::Vec3Array(arr) => {
76 let mut bytes = Vec::with_capacity(arr.len() * 16);
78 for v in arr {
79 bytes.extend_from_slice(bytemuck::bytes_of(v));
80 bytes.extend_from_slice(&[0u8; 4]); }
82 bytes
83 }
84 MaterialParameter::Vec4Array(arr) => bytemuck::cast_slice(arr).to_vec(),
85 }
86 }
87
88 pub fn size(&self) -> u64 {
90 match self {
91 MaterialParameter::Float(_) => 4,
92 MaterialParameter::Vec2(_) => 8,
93 MaterialParameter::Vec3(_) => 16, MaterialParameter::Vec4(_) => 16,
95 MaterialParameter::Color(_) => 16,
96 MaterialParameter::Matrix4(_) => 64,
97 MaterialParameter::FloatArray(arr) => (arr.len() * 4) as u64,
98 MaterialParameter::Vec2Array(arr) => (arr.len() * 8) as u64,
99 MaterialParameter::Vec3Array(arr) => (arr.len() * 16) as u64, MaterialParameter::Vec4Array(arr) => (arr.len() * 16) as u64,
101 }
102 }
103}
104
105#[derive(Debug, Clone)]
107pub struct MaterialTexture {
108 pub texture: wgpu::Texture,
110 pub view: wgpu::TextureView,
112 pub sampler: Option<wgpu::Sampler>,
114}
115
116#[derive(Debug, Clone)]
118pub struct PipelineState {
119 pub topology: wgpu::PrimitiveTopology,
121 pub cull_mode: Option<wgpu::Face>,
123 pub front_face: wgpu::FrontFace,
125 pub polygon_mode: wgpu::PolygonMode,
127 pub depth_test: bool,
129 pub depth_write: bool,
131 pub blend: Option<wgpu::BlendState>,
133}
134
135impl Default for PipelineState {
136 fn default() -> Self {
137 Self {
138 topology: wgpu::PrimitiveTopology::TriangleList,
139 cull_mode: Some(wgpu::Face::Back),
140 front_face: wgpu::FrontFace::Ccw,
141 polygon_mode: wgpu::PolygonMode::Fill,
142 depth_test: false,
143 depth_write: false,
144 blend: None,
145 }
146 }
147}
148
149pub struct Material {
151 shader: Arc<wgpu::ShaderModule>,
153 parameters: HashMap<String, MaterialParameter>,
155 textures: HashMap<String, MaterialTexture>,
157 pipeline_state: PipelineState,
159 context: Arc<GraphicsContext>,
161 uniform_buffer: Option<wgpu::Buffer>,
163 bind_group_layout: Option<wgpu::BindGroupLayout>,
165 bind_group: Option<wgpu::BindGroup>,
167 dirty: bool,
169}
170
171impl Material {
172 pub fn new(shader: Arc<wgpu::ShaderModule>, context: Arc<GraphicsContext>) -> Self {
174 Self {
175 shader,
176 parameters: HashMap::default(),
177 textures: HashMap::default(),
178 pipeline_state: PipelineState::default(),
179 context,
180 uniform_buffer: None,
181 bind_group_layout: None,
182 bind_group: None,
183 dirty: true,
184 }
185 }
186
187 pub fn from_source(
189 source: &str,
190 label: Option<&str>,
191 context: Arc<GraphicsContext>,
192 ) -> Self {
193 profile_function!();
194 let shader = context
195 .device()
196 .create_shader_module(wgpu::ShaderModuleDescriptor {
197 label,
198 source: wgpu::ShaderSource::Wgsl(source.into()),
199 });
200 Self::new(Arc::new(shader), context)
201 }
202
203 pub fn set_parameter(&mut self, name: impl Into<String>, value: MaterialParameter) {
205 self.parameters.insert(name.into(), value);
206 self.dirty = true;
207 }
208
209 pub fn get_parameter(&self, name: &str) -> Option<&MaterialParameter> {
211 self.parameters.get(name)
212 }
213
214 pub fn set_texture(&mut self, name: impl Into<String>, texture: MaterialTexture) {
216 self.textures.insert(name.into(), texture);
217 self.dirty = true;
218 }
219
220 pub fn get_texture(&self, name: &str) -> Option<&MaterialTexture> {
222 self.textures.get(name)
223 }
224
225 pub fn set_pipeline_state(&mut self, state: PipelineState) {
227 self.pipeline_state = state;
228 }
229
230 pub fn pipeline_state(&self) -> &PipelineState {
232 &self.pipeline_state
233 }
234
235 pub fn shader(&self) -> &wgpu::ShaderModule {
237 &self.shader
238 }
239
240 fn update_resources(&mut self) {
242 profile_function!();
243 if !self.dirty {
244 return;
245 }
246
247 let mut uniform_size = 0u64;
249 for param in self.parameters.values() {
250 uniform_size += param.size();
251 if !uniform_size.is_multiple_of(16) {
253 uniform_size += 16 - (uniform_size % 16);
254 }
255 }
256
257 if uniform_size > 0 {
259 let mut uniform_data = Vec::new();
260 for param in self.parameters.values() {
261 uniform_data.extend_from_slice(¶m.as_bytes());
262 let current_size = uniform_data.len() as u64;
264 if !current_size.is_multiple_of(16) {
265 let padding = 16 - (current_size % 16);
266 uniform_data.extend(vec![0u8; padding as usize]);
267 }
268 }
269
270 if let Some(buffer) = &self.uniform_buffer {
271 self.context.queue().write_buffer(buffer, 0, &uniform_data);
273 } else {
274 let buffer = self
276 .context
277 .device()
278 .create_buffer(&wgpu::BufferDescriptor {
279 label: Some("Material Uniform Buffer"),
280 size: uniform_size,
281 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
282 mapped_at_creation: false,
283 });
284 self.context.queue().write_buffer(&buffer, 0, &uniform_data);
285 self.uniform_buffer = Some(buffer);
286 }
287 }
288
289 self.rebuild_bind_groups();
291
292 self.dirty = false;
293 }
294
295 fn rebuild_bind_groups(&mut self) {
297 let mut layout_entries = Vec::new();
298 let mut bind_entries = Vec::new();
299 let mut binding = 0u32;
300
301 if self.uniform_buffer.is_some() {
303 layout_entries.push(wgpu::BindGroupLayoutEntry {
304 binding,
305 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
306 ty: wgpu::BindingType::Buffer {
307 ty: wgpu::BufferBindingType::Uniform,
308 has_dynamic_offset: false,
309 min_binding_size: None,
310 },
311 count: None,
312 });
313
314 bind_entries.push(wgpu::BindGroupEntry {
315 binding,
316 resource: self.uniform_buffer.as_ref().unwrap().as_entire_binding(),
317 });
318
319 binding += 1;
320 }
321
322 for texture in self.textures.values() {
324 layout_entries.push(wgpu::BindGroupLayoutEntry {
326 binding,
327 visibility: wgpu::ShaderStages::FRAGMENT,
328 ty: wgpu::BindingType::Texture {
329 sample_type: wgpu::TextureSampleType::Float { filterable: true },
330 view_dimension: wgpu::TextureViewDimension::D2,
331 multisampled: false,
332 },
333 count: None,
334 });
335
336 bind_entries.push(wgpu::BindGroupEntry {
337 binding,
338 resource: wgpu::BindingResource::TextureView(&texture.view),
339 });
340
341 binding += 1;
342
343 layout_entries.push(wgpu::BindGroupLayoutEntry {
345 binding,
346 visibility: wgpu::ShaderStages::FRAGMENT,
347 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
348 count: None,
349 });
350
351 let sampler = if let Some(ref s) = texture.sampler {
353 s
354 } else {
355 unimplemented!("Default sampler not yet implemented - please provide sampler")
359 };
360
361 bind_entries.push(wgpu::BindGroupEntry {
362 binding,
363 resource: wgpu::BindingResource::Sampler(sampler),
364 });
365
366 binding += 1;
367 }
368
369 let layout = self
371 .context
372 .device()
373 .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
374 label: Some("Material Bind Group Layout"),
375 entries: &layout_entries,
376 });
377
378 let bind_group = self
380 .context
381 .device()
382 .create_bind_group(&wgpu::BindGroupDescriptor {
383 label: Some("Material Bind Group"),
384 layout: &layout,
385 entries: &bind_entries,
386 });
387
388 self.bind_group_layout = Some(layout);
389 self.bind_group = Some(bind_group);
390 }
391
392 pub fn bind<'a>(&'a mut self, pass: &mut wgpu::RenderPass<'a>, bind_group_index: u32) {
401 profile_function!();
402 self.update_resources();
403
404 if let Some(ref bind_group) = self.bind_group {
405 pass.set_bind_group(bind_group_index, bind_group, &[]);
406 }
407 }
408
409 pub fn bind_group_layout(&mut self) -> &wgpu::BindGroupLayout {
413 if self.dirty || self.bind_group_layout.is_none() {
414 self.update_resources();
415 }
416 self.bind_group_layout
417 .as_ref()
418 .expect("Bind group layout should be created")
419 }
420}
421
422pub struct MaterialBuilder {
424 shader: Option<Arc<wgpu::ShaderModule>>,
425 parameters: HashMap<String, MaterialParameter>,
426 textures: HashMap<String, MaterialTexture>,
427 pipeline_state: PipelineState,
428 context: Arc<GraphicsContext>,
429}
430
431impl MaterialBuilder {
432 pub fn new(context: Arc<GraphicsContext>) -> Self {
434 Self {
435 shader: None,
436 parameters: HashMap::default(),
437 textures: HashMap::default(),
438 pipeline_state: PipelineState::default(),
439 context,
440 }
441 }
442
443 pub fn shader(mut self, shader: Arc<wgpu::ShaderModule>) -> Self {
445 self.shader = Some(shader);
446 self
447 }
448
449 pub fn shader_source(mut self, source: &str, label: Option<&str>) -> Self {
451 let shader = self
452 .context
453 .device()
454 .create_shader_module(wgpu::ShaderModuleDescriptor {
455 label,
456 source: wgpu::ShaderSource::Wgsl(source.into()),
457 });
458 self.shader = Some(Arc::new(shader));
459 self
460 }
461
462 pub fn parameter(mut self, name: impl Into<String>, value: MaterialParameter) -> Self {
464 self.parameters.insert(name.into(), value);
465 self
466 }
467
468 pub fn texture(mut self, name: impl Into<String>, texture: MaterialTexture) -> Self {
470 self.textures.insert(name.into(), texture);
471 self
472 }
473
474 pub fn pipeline_state(mut self, state: PipelineState) -> Self {
476 self.pipeline_state = state;
477 self
478 }
479
480 pub fn build(self) -> Material {
482 let shader = self.shader.expect("Shader is required");
483 let mut material = Material::new(shader, self.context);
484 material.parameters = self.parameters;
485 material.textures = self.textures;
486 material.pipeline_state = self.pipeline_state;
487 material.dirty = true;
488 material
489 }
490}