1use crate::{
2 app::App,
3 assets::{plugin::AssetPlugin, upload::Asset},
4 ecs::plugin::Plugin,
5 wgpu::backend::WGPUBackend,
6};
7
8#[derive(Copy, Clone, PartialEq, Eq, Hash)]
9pub enum MaterialBindingKind {
10 Texture {
11 sample_type: wgpu::TextureSampleType,
12 view_dimension: wgpu::TextureViewDimension,
13 multisampled: bool,
14 },
15 Sampler,
16 ComparisonSampler,
17 UniformBuffer {
18 visibility: wgpu::ShaderStages,
19 has_dynamic_offset: bool,
20 min_binding_size: Option<wgpu::BufferSize>,
21 },
22 StorageBufferReadOnly {
23 visibility: wgpu::ShaderStages,
24 has_dynamic_offset: bool,
25 min_binding_size: Option<wgpu::BufferSize>,
26 },
27 StorageBufferReadWrite {
28 visibility: wgpu::ShaderStages,
29 has_dynamic_offset: bool,
30 min_binding_size: Option<wgpu::BufferSize>,
31 },
32}
33
34impl MaterialBindingKind {
35 pub fn texture_2d() -> Self {
36 Self::Texture {
37 sample_type: wgpu::TextureSampleType::Float { filterable: true },
38 view_dimension: wgpu::TextureViewDimension::D2,
39 multisampled: false,
40 }
41 }
42
43 pub fn texture_2d_array() -> Self {
44 Self::Texture {
45 sample_type: wgpu::TextureSampleType::Float { filterable: true },
46 view_dimension: wgpu::TextureViewDimension::D2Array,
47 multisampled: false,
48 }
49 }
50
51 pub fn texture_cubemap() -> Self {
52 Self::Texture {
53 sample_type: wgpu::TextureSampleType::Float { filterable: true },
54 view_dimension: wgpu::TextureViewDimension::Cube,
55 multisampled: false,
56 }
57 }
58
59 pub fn uniform_buffer() -> Self {
60 Self::UniformBuffer {
61 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
62 has_dynamic_offset: false,
63 min_binding_size: None,
64 }
65 }
66
67 pub fn dynamic_uniform_buffer(element_size: u64) -> Self {
77 Self::UniformBuffer {
78 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
79 has_dynamic_offset: true,
80 min_binding_size: wgpu::BufferSize::new(element_size),
81 }
82 }
83
84 pub fn dynamic_storage_buffer(element_size: u64, read_only: bool) -> Self {
86 let visibility = wgpu::ShaderStages::VERTEX_FRAGMENT;
87 let has_dynamic_offset = true;
88 let min_binding_size = wgpu::BufferSize::new(element_size);
89 if read_only {
90 Self::StorageBufferReadOnly { visibility, has_dynamic_offset, min_binding_size }
91 } else {
92 Self::StorageBufferReadWrite { visibility, has_dynamic_offset, min_binding_size }
93 }
94 }
95
96 pub fn layout_entry(&self, binding: u32) -> wgpu::BindGroupLayoutEntry {
97 match self {
98 MaterialBindingKind::Texture { sample_type, view_dimension, multisampled } => wgpu::BindGroupLayoutEntry {
99 binding,
100 visibility: wgpu::ShaderStages::FRAGMENT,
101 ty: wgpu::BindingType::Texture {
102 sample_type: *sample_type,
103 view_dimension: *view_dimension,
104 multisampled: *multisampled,
105 },
106 count: None,
107 },
108 MaterialBindingKind::Sampler => wgpu::BindGroupLayoutEntry {
109 binding,
110 visibility: wgpu::ShaderStages::FRAGMENT,
111 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
112 count: None,
113 },
114 MaterialBindingKind::ComparisonSampler => wgpu::BindGroupLayoutEntry {
115 binding,
116 visibility: wgpu::ShaderStages::FRAGMENT,
117 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
118 count: None,
119 },
120 MaterialBindingKind::UniformBuffer { visibility, has_dynamic_offset, min_binding_size } => wgpu::BindGroupLayoutEntry {
121 binding,
122 visibility: *visibility,
123 ty: wgpu::BindingType::Buffer {
124 ty: wgpu::BufferBindingType::Uniform,
125 has_dynamic_offset: *has_dynamic_offset,
126 min_binding_size: *min_binding_size,
127 },
128 count: None,
129 },
130 MaterialBindingKind::StorageBufferReadOnly { visibility, has_dynamic_offset, min_binding_size } => wgpu::BindGroupLayoutEntry {
131 binding,
132 visibility: *visibility,
133 ty: wgpu::BindingType::Buffer {
134 ty: wgpu::BufferBindingType::Storage { read_only: true },
135 has_dynamic_offset: *has_dynamic_offset,
136 min_binding_size: *min_binding_size,
137 },
138 count: None,
139 },
140 MaterialBindingKind::StorageBufferReadWrite { visibility, has_dynamic_offset, min_binding_size } => wgpu::BindGroupLayoutEntry {
141 binding,
142 visibility: *visibility,
143 ty: wgpu::BindingType::Buffer {
144 ty: wgpu::BufferBindingType::Storage { read_only: false },
145 has_dynamic_offset: *has_dynamic_offset,
146 min_binding_size: *min_binding_size,
147 },
148 count: None,
149 },
150 }
151 }
152}
153
154#[derive(Clone)]
155pub struct MaterialBindingEntry {
156 pub name: &'static str,
157 pub binding: u32,
160 pub kind: MaterialBindingKind,
161}
162
163pub struct MaterialDescriptor<'a> {
164 pub label: Option<&'a str>,
165 pub shader_source: &'a str,
166 pub vertex_entry: Option<&'a str>,
167 pub fragment_entry: Option<&'a str>,
168 pub vertex_layouts: Vec<wgpu::VertexBufferLayout<'static>>,
169 pub entries: Vec<MaterialBindingEntry>,
170 pub cull_mode: Option<wgpu::Face>,
171 pub depth: Option<wgpu::DepthStencilState>,
172 pub targets: Vec<wgpu::ColorTargetState>,
173 pub polygon_mode: wgpu::PolygonMode,
174 pub own_group: u32,
176 pub extra_layouts: Vec<super::layout::OwnedGroupLayout>,
181}
182
183pub const DEFAULT_TARGET: [wgpu::ColorTargetState; 1] = [wgpu::ColorTargetState {
184 format: wgpu::TextureFormat::Rgba8Unorm,
185 blend: None,
186 write_mask: wgpu::ColorWrites::ALL,
187}];
188
189impl<'a> Default for MaterialDescriptor<'a> {
190 fn default() -> Self {
191 Self {
192 label: None,
193 shader_source: "",
194 vertex_entry: Some("vs_main"),
195 fragment_entry: Some("fs_main"),
196 vertex_layouts: Vec::new(),
197 entries: Vec::new(),
198 cull_mode: Some(wgpu::Face::Back),
199 depth: None,
200 targets: Vec::new(),
201 own_group: 0,
202 extra_layouts: Vec::new(),
203 polygon_mode: wgpu::PolygonMode::Fill,
204 }
205 }
206}
207
208pub fn build_bind_group_layout(
209 device: &wgpu::Device,
210 label: Option<&str>,
211 entries: &[MaterialBindingEntry],
212) -> wgpu::BindGroupLayout {
213 let layout_entries: Vec<_> = entries.iter().map(|e| e.kind.layout_entry(e.binding)).collect();
214
215 let mut seen = std::collections::HashSet::new();
216 for e in entries {
217 if !seen.insert(e.binding) {
218 panic!(
219 "binding {} assigned more than once building bind group layout{} (entry '{}')",
220 e.binding,
221 label.map(|l| format!(" '{l}'")).unwrap_or_default(),
222 e.name
223 );
224 }
225 }
226
227 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
228 label,
229 entries: &layout_entries,
230 })
231}
232
233pub fn build_material(
234 device: &wgpu::Device,
235 desc: &MaterialDescriptor,
236) -> (wgpu::RenderPipeline, wgpu::BindGroupLayout) {
237 let layout = build_bind_group_layout(device, desc.label, &desc.entries);
238
239 let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
240 label: desc.label,
241 source: wgpu::ShaderSource::Wgsl(desc.shader_source.into()),
242 });
243
244 let mut slots: Vec<super::layout::GroupLayout> = desc
245 .extra_layouts
246 .iter()
247 .map(|g| super::layout::GroupLayout { group: g.group, layout: &g.layout })
248 .collect();
249 slots.push(super::layout::GroupLayout { group: desc.own_group, layout: &layout });
250 let bind_group_layouts = super::layout::assemble_bind_group_layouts(desc.label, slots);
251
252 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
253 label: desc.label,
254 bind_group_layouts: &bind_group_layouts,
255 immediate_size: 0,
256 });
257
258 let targets: Vec<Option<wgpu::ColorTargetState>> =
259 desc.targets.iter().cloned().map(Some).collect();
260
261 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
262 label: desc.label,
263 layout: Some(&pipeline_layout),
264 vertex: wgpu::VertexState {
265 module: &module,
266 entry_point: desc.vertex_entry,
267 compilation_options: Default::default(),
268 buffers: &desc.vertex_layouts,
269 },
270 primitive: wgpu::PrimitiveState {
271 topology: wgpu::PrimitiveTopology::TriangleList,
272 strip_index_format: None,
273 front_face: wgpu::FrontFace::Ccw,
274 cull_mode: desc.cull_mode,
275 unclipped_depth: false,
276 polygon_mode: desc.polygon_mode,
277 conservative: false,
278 },
279 depth_stencil: desc.depth.clone(),
280 multisample: wgpu::MultisampleState::default(),
281 fragment: Some(wgpu::FragmentState {
282 module: &module,
283 entry_point: desc.fragment_entry,
284 compilation_options: Default::default(),
285 targets: &targets,
286 }),
287 multiview_mask: None,
288 cache: None,
289 });
290
291 (pipeline, layout)
292}
293
294pub struct GPUMaterial {
295 pub pipeline: wgpu::RenderPipeline,
296 pub layout: wgpu::BindGroupLayout,
297 pub entries: Vec<MaterialBindingEntry>,
298}
299
300impl Asset<WGPUBackend> for GPUMaterial {
301 type Source = MaterialDescriptor<'static>;
302 type Deps<'a> = ();
303
304 fn upload<'a>(source: &MaterialDescriptor, backend: &WGPUBackend, _deps: &()) -> Option<Self> {
305 let (pipeline, layout) = build_material(&backend.device, source);
306
307 Some(Self {
308 pipeline,
309 layout,
310 entries: source.entries.to_vec(),
311 })
312 }
313}
314
315#[derive(Default)]
316pub struct MaterialPlugin;
317impl MaterialPlugin {
318 pub fn new() -> Self {
319 Self
320 }
321}
322
323impl Plugin for MaterialPlugin {
324 fn build(&self, app: &mut App) {
325 app.add_plugin(AssetPlugin::<super::backend::WGPUBackend, GPUMaterial>::new());
326 }
327}