use astrelis_core::profiling::profile_function;
use crate::{Color, GraphicsContext};
use ahash::HashMap;
use glam::{Mat4, Vec2, Vec3, Vec4};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub enum MaterialParameter {
Float(f32),
Vec2(Vec2),
Vec3(Vec3),
Vec4(Vec4),
Color(Color),
Matrix4(Mat4),
FloatArray(Vec<f32>),
Vec2Array(Vec<Vec2>),
Vec3Array(Vec<Vec3>),
Vec4Array(Vec<Vec4>),
}
impl MaterialParameter {
pub fn as_bytes(&self) -> Vec<u8> {
match self {
MaterialParameter::Float(v) => bytemuck::bytes_of(v).to_vec(),
MaterialParameter::Vec2(v) => bytemuck::bytes_of(v).to_vec(),
MaterialParameter::Vec3(v) => {
let mut bytes = Vec::with_capacity(16);
bytes.extend_from_slice(bytemuck::bytes_of(v));
bytes.extend_from_slice(&[0u8; 4]); bytes
}
MaterialParameter::Vec4(v) => bytemuck::bytes_of(v).to_vec(),
MaterialParameter::Color(c) => bytemuck::bytes_of(c).to_vec(),
MaterialParameter::Matrix4(m) => bytemuck::bytes_of(m).to_vec(),
MaterialParameter::FloatArray(arr) => bytemuck::cast_slice(arr).to_vec(),
MaterialParameter::Vec2Array(arr) => bytemuck::cast_slice(arr).to_vec(),
MaterialParameter::Vec3Array(arr) => {
let mut bytes = Vec::with_capacity(arr.len() * 16);
for v in arr {
bytes.extend_from_slice(bytemuck::bytes_of(v));
bytes.extend_from_slice(&[0u8; 4]); }
bytes
}
MaterialParameter::Vec4Array(arr) => bytemuck::cast_slice(arr).to_vec(),
}
}
pub fn size(&self) -> u64 {
match self {
MaterialParameter::Float(_) => 4,
MaterialParameter::Vec2(_) => 8,
MaterialParameter::Vec3(_) => 16, MaterialParameter::Vec4(_) => 16,
MaterialParameter::Color(_) => 16,
MaterialParameter::Matrix4(_) => 64,
MaterialParameter::FloatArray(arr) => (arr.len() * 4) as u64,
MaterialParameter::Vec2Array(arr) => (arr.len() * 8) as u64,
MaterialParameter::Vec3Array(arr) => (arr.len() * 16) as u64, MaterialParameter::Vec4Array(arr) => (arr.len() * 16) as u64,
}
}
}
#[derive(Debug, Clone)]
pub struct MaterialTexture {
pub texture: wgpu::Texture,
pub view: wgpu::TextureView,
pub sampler: Option<wgpu::Sampler>,
}
#[derive(Debug, Clone)]
pub struct PipelineState {
pub topology: wgpu::PrimitiveTopology,
pub cull_mode: Option<wgpu::Face>,
pub front_face: wgpu::FrontFace,
pub polygon_mode: wgpu::PolygonMode,
pub depth_test: bool,
pub depth_write: bool,
pub blend: Option<wgpu::BlendState>,
}
impl Default for PipelineState {
fn default() -> Self {
Self {
topology: wgpu::PrimitiveTopology::TriangleList,
cull_mode: Some(wgpu::Face::Back),
front_face: wgpu::FrontFace::Ccw,
polygon_mode: wgpu::PolygonMode::Fill,
depth_test: false,
depth_write: false,
blend: None,
}
}
}
pub struct Material {
shader: Arc<wgpu::ShaderModule>,
parameters: HashMap<String, MaterialParameter>,
textures: HashMap<String, MaterialTexture>,
pipeline_state: PipelineState,
context: Arc<GraphicsContext>,
uniform_buffer: Option<wgpu::Buffer>,
bind_group_layout: Option<wgpu::BindGroupLayout>,
bind_group: Option<wgpu::BindGroup>,
dirty: bool,
}
impl Material {
pub fn new(shader: Arc<wgpu::ShaderModule>, context: Arc<GraphicsContext>) -> Self {
Self {
shader,
parameters: HashMap::default(),
textures: HashMap::default(),
pipeline_state: PipelineState::default(),
context,
uniform_buffer: None,
bind_group_layout: None,
bind_group: None,
dirty: true,
}
}
pub fn from_source(source: &str, label: Option<&str>, context: Arc<GraphicsContext>) -> Self {
profile_function!();
let shader = context
.device()
.create_shader_module(wgpu::ShaderModuleDescriptor {
label,
source: wgpu::ShaderSource::Wgsl(source.into()),
});
Self::new(Arc::new(shader), context)
}
pub fn set_parameter(&mut self, name: impl Into<String>, value: MaterialParameter) {
self.parameters.insert(name.into(), value);
self.dirty = true;
}
pub fn get_parameter(&self, name: &str) -> Option<&MaterialParameter> {
self.parameters.get(name)
}
pub fn set_texture(&mut self, name: impl Into<String>, texture: MaterialTexture) {
self.textures.insert(name.into(), texture);
self.dirty = true;
}
pub fn get_texture(&self, name: &str) -> Option<&MaterialTexture> {
self.textures.get(name)
}
pub fn set_pipeline_state(&mut self, state: PipelineState) {
self.pipeline_state = state;
}
pub fn pipeline_state(&self) -> &PipelineState {
&self.pipeline_state
}
pub fn shader(&self) -> &wgpu::ShaderModule {
&self.shader
}
fn update_resources(&mut self) {
profile_function!();
if !self.dirty {
return;
}
let mut uniform_size = 0u64;
for param in self.parameters.values() {
uniform_size += param.size();
if !uniform_size.is_multiple_of(16) {
uniform_size += 16 - (uniform_size % 16);
}
}
if uniform_size > 0 {
let mut uniform_data = Vec::new();
for param in self.parameters.values() {
uniform_data.extend_from_slice(¶m.as_bytes());
let current_size = uniform_data.len() as u64;
if !current_size.is_multiple_of(16) {
let padding = 16 - (current_size % 16);
uniform_data.extend(vec![0u8; padding as usize]);
}
}
if let Some(buffer) = &self.uniform_buffer {
self.context.queue().write_buffer(buffer, 0, &uniform_data);
} else {
let buffer = self
.context
.device()
.create_buffer(&wgpu::BufferDescriptor {
label: Some("Material Uniform Buffer"),
size: uniform_size,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
self.context.queue().write_buffer(&buffer, 0, &uniform_data);
self.uniform_buffer = Some(buffer);
}
}
self.rebuild_bind_groups();
self.dirty = false;
}
fn rebuild_bind_groups(&mut self) {
let mut layout_entries = Vec::new();
let mut bind_entries = Vec::new();
let mut binding = 0u32;
if let Some(buf) = &self.uniform_buffer {
layout_entries.push(wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
});
bind_entries.push(wgpu::BindGroupEntry {
binding,
resource: buf.as_entire_binding(),
});
binding += 1;
}
for texture in self.textures.values() {
layout_entries.push(wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
});
bind_entries.push(wgpu::BindGroupEntry {
binding,
resource: wgpu::BindingResource::TextureView(&texture.view),
});
binding += 1;
layout_entries.push(wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
});
let sampler = if let Some(ref s) = texture.sampler {
s
} else {
unimplemented!("Default sampler not yet implemented - please provide sampler")
};
bind_entries.push(wgpu::BindGroupEntry {
binding,
resource: wgpu::BindingResource::Sampler(sampler),
});
binding += 1;
}
let layout =
self.context
.device()
.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Material Bind Group Layout"),
entries: &layout_entries,
});
let bind_group = self
.context
.device()
.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Material Bind Group"),
layout: &layout,
entries: &bind_entries,
});
self.bind_group_layout = Some(layout);
self.bind_group = Some(bind_group);
}
pub fn bind<'a>(&'a mut self, pass: &mut wgpu::RenderPass<'a>, bind_group_index: u32) {
profile_function!();
self.update_resources();
if let Some(ref bind_group) = self.bind_group {
pass.set_bind_group(bind_group_index, bind_group, &[]);
}
}
pub fn bind_group_layout(&mut self) -> &wgpu::BindGroupLayout {
if self.dirty || self.bind_group_layout.is_none() {
self.update_resources();
}
self.bind_group_layout
.as_ref()
.expect("Bind group layout should be created")
}
}
pub struct MaterialBuilder {
shader: Option<Arc<wgpu::ShaderModule>>,
parameters: HashMap<String, MaterialParameter>,
textures: HashMap<String, MaterialTexture>,
pipeline_state: PipelineState,
context: Arc<GraphicsContext>,
}
impl MaterialBuilder {
pub fn new(context: Arc<GraphicsContext>) -> Self {
Self {
shader: None,
parameters: HashMap::default(),
textures: HashMap::default(),
pipeline_state: PipelineState::default(),
context,
}
}
pub fn shader(mut self, shader: Arc<wgpu::ShaderModule>) -> Self {
self.shader = Some(shader);
self
}
pub fn shader_source(mut self, source: &str, label: Option<&str>) -> Self {
let shader = self
.context
.device()
.create_shader_module(wgpu::ShaderModuleDescriptor {
label,
source: wgpu::ShaderSource::Wgsl(source.into()),
});
self.shader = Some(Arc::new(shader));
self
}
pub fn parameter(mut self, name: impl Into<String>, value: MaterialParameter) -> Self {
self.parameters.insert(name.into(), value);
self
}
pub fn texture(mut self, name: impl Into<String>, texture: MaterialTexture) -> Self {
self.textures.insert(name.into(), texture);
self
}
pub fn pipeline_state(mut self, state: PipelineState) -> Self {
self.pipeline_state = state;
self
}
pub fn build(self) -> Material {
let shader = self.shader.expect("Shader is required");
let mut material = Material::new(shader, self.context);
material.parameters = self.parameters;
material.textures = self.textures;
material.pipeline_state = self.pipeline_state;
material.dirty = true;
material
}
}