1use std::collections::HashMap;
8
9#[derive(Debug, Clone)]
15pub struct Material {
16 pub name: String,
18 pub is_flat: bool,
20 pub is_blendable: bool,
22 pub ambient: f32,
24 pub diffuse: f32,
26 pub specular: f32,
28 pub shininess: f32,
30}
31
32impl Material {
33 pub fn new(name: impl Into<String>) -> Self {
35 Self {
36 name: name.into(),
37 is_flat: false,
38 is_blendable: false,
39 ambient: 0.2,
40 diffuse: 0.7,
41 specular: 0.3,
42 shininess: 32.0,
43 }
44 }
45
46 pub fn blendable(
48 name: impl Into<String>,
49 ambient: f32,
50 diffuse: f32,
51 specular: f32,
52 shininess: f32,
53 ) -> Self {
54 Self {
55 name: name.into(),
56 is_flat: false,
57 is_blendable: true,
58 ambient,
59 diffuse,
60 specular,
61 shininess,
62 }
63 }
64
65 pub fn static_mat(
67 name: impl Into<String>,
68 ambient: f32,
69 diffuse: f32,
70 specular: f32,
71 shininess: f32,
72 ) -> Self {
73 Self {
74 name: name.into(),
75 is_flat: false,
76 is_blendable: false,
77 ambient,
78 diffuse,
79 specular,
80 shininess,
81 }
82 }
83
84 pub fn flat(name: impl Into<String>) -> Self {
86 Self {
87 name: name.into(),
88 is_flat: true,
89 is_blendable: true,
90 ambient: 1.0,
91 diffuse: 0.0,
92 specular: 0.0,
93 shininess: 1.0,
94 }
95 }
96
97 #[must_use]
99 pub fn clay() -> Self {
100 Self::blendable("clay", 0.25, 0.75, 0.1, 8.0)
101 }
102
103 #[must_use]
105 pub fn wax() -> Self {
106 Self::blendable("wax", 0.2, 0.7, 0.4, 16.0)
107 }
108
109 #[must_use]
111 pub fn candy() -> Self {
112 Self::blendable("candy", 0.15, 0.6, 0.7, 64.0)
113 }
114
115 #[must_use]
117 pub fn ceramic() -> Self {
118 Self::static_mat("ceramic", 0.2, 0.65, 0.5, 32.0)
119 }
120
121 #[must_use]
123 pub fn jade() -> Self {
124 Self::static_mat("jade", 0.3, 0.6, 0.3, 24.0)
125 }
126
127 #[must_use]
129 pub fn mud() -> Self {
130 Self::static_mat("mud", 0.3, 0.7, 0.0, 1.0)
131 }
132
133 #[must_use]
135 pub fn normal() -> Self {
136 Self::static_mat("normal", 0.2, 0.7, 0.3, 32.0)
137 }
138}
139
140impl Default for Material {
141 fn default() -> Self {
142 Self::clay()
143 }
144}
145
146#[repr(C)]
148#[derive(Debug, Clone, Copy, bytemuck::Pod, bytemuck::Zeroable)]
149pub struct MaterialUniforms {
150 pub ambient: f32,
152 pub diffuse: f32,
154 pub specular: f32,
156 pub shininess: f32,
158}
159
160impl From<&Material> for MaterialUniforms {
161 fn from(mat: &Material) -> Self {
162 Self {
163 ambient: mat.ambient,
164 diffuse: mat.diffuse,
165 specular: mat.specular,
166 shininess: mat.shininess,
167 }
168 }
169}
170
171impl Default for MaterialUniforms {
172 fn default() -> Self {
173 Self {
174 ambient: 0.2,
175 diffuse: 0.7,
176 specular: 0.3,
177 shininess: 32.0,
178 }
179 }
180}
181
182pub struct MatcapTextureSet {
188 pub tex_r: wgpu::TextureView,
190 pub tex_g: wgpu::TextureView,
192 pub tex_b: wgpu::TextureView,
194 pub tex_k: wgpu::TextureView,
196 pub sampler: wgpu::Sampler,
198 pub bind_group: wgpu::BindGroup,
200}
201
202#[derive(Default)]
204pub struct MaterialRegistry {
205 materials: HashMap<String, Material>,
206 default_material: String,
207}
208
209impl MaterialRegistry {
210 #[must_use]
212 pub fn new() -> Self {
213 let mut registry = Self {
214 materials: HashMap::new(),
215 default_material: "clay".to_string(),
216 };
217 registry.register_defaults();
218 registry
219 }
220
221 fn register_defaults(&mut self) {
222 self.register(Material::clay());
224 self.register(Material::wax());
225 self.register(Material::candy());
226 self.register(Material::ceramic());
227 self.register(Material::jade());
228 self.register(Material::mud());
229 self.register(Material::normal());
230 self.register(Material::flat("flat"));
231 }
232
233 pub fn register(&mut self, material: Material) {
235 self.materials.insert(material.name.clone(), material);
236 }
237
238 #[must_use]
240 pub fn get(&self, name: &str) -> Option<&Material> {
241 self.materials.get(name)
242 }
243
244 #[must_use]
246 pub fn has(&self, name: &str) -> bool {
247 self.materials.contains_key(name)
248 }
249
250 #[must_use]
252 pub fn default_material(&self) -> &Material {
253 self.materials
254 .get(&self.default_material)
255 .unwrap_or_else(|| {
256 self.materials
257 .values()
258 .next()
259 .expect("no materials registered")
260 })
261 }
262
263 pub fn set_default(&mut self, name: &str) {
265 if self.materials.contains_key(name) {
266 self.default_material = name.to_string();
267 }
268 }
269
270 #[must_use]
273 pub fn names(&self) -> Vec<&str> {
274 const BUILTIN_ORDER: &[&str] = &[
275 "clay", "wax", "candy", "flat", "mud", "ceramic", "jade", "normal",
276 ];
277 let mut names: Vec<&str> = Vec::new();
278 for &builtin in BUILTIN_ORDER {
280 if self.materials.contains_key(builtin) {
281 names.push(builtin);
282 }
283 }
284 let mut custom: Vec<&str> = self
286 .materials
287 .keys()
288 .map(String::as_str)
289 .filter(|n| !BUILTIN_ORDER.contains(n))
290 .collect();
291 custom.sort_unstable();
292 names.extend(custom);
293 names
294 }
295
296 #[must_use]
298 pub fn len(&self) -> usize {
299 self.materials.len()
300 }
301
302 #[must_use]
304 pub fn is_empty(&self) -> bool {
305 self.materials.is_empty()
306 }
307}
308
309mod matcap_data {
313 pub const CLAY_R: &[u8] = include_bytes!("../data/matcaps/clay_r.hdr");
315 pub const CLAY_G: &[u8] = include_bytes!("../data/matcaps/clay_g.hdr");
316 pub const CLAY_B: &[u8] = include_bytes!("../data/matcaps/clay_b.hdr");
317 pub const CLAY_K: &[u8] = include_bytes!("../data/matcaps/clay_k.hdr");
318
319 pub const WAX_R: &[u8] = include_bytes!("../data/matcaps/wax_r.hdr");
321 pub const WAX_G: &[u8] = include_bytes!("../data/matcaps/wax_g.hdr");
322 pub const WAX_B: &[u8] = include_bytes!("../data/matcaps/wax_b.hdr");
323 pub const WAX_K: &[u8] = include_bytes!("../data/matcaps/wax_k.hdr");
324
325 pub const CANDY_R: &[u8] = include_bytes!("../data/matcaps/candy_r.hdr");
327 pub const CANDY_G: &[u8] = include_bytes!("../data/matcaps/candy_g.hdr");
328 pub const CANDY_B: &[u8] = include_bytes!("../data/matcaps/candy_b.hdr");
329 pub const CANDY_K: &[u8] = include_bytes!("../data/matcaps/candy_k.hdr");
330
331 pub const FLAT_R: &[u8] = include_bytes!("../data/matcaps/flat_r.hdr");
333 pub const FLAT_G: &[u8] = include_bytes!("../data/matcaps/flat_g.hdr");
334 pub const FLAT_B: &[u8] = include_bytes!("../data/matcaps/flat_b.hdr");
335 pub const FLAT_K: &[u8] = include_bytes!("../data/matcaps/flat_k.hdr");
336
337 pub const MUD: &[u8] = include_bytes!("../data/matcaps/mud.jpg");
339 pub const CERAMIC: &[u8] = include_bytes!("../data/matcaps/ceramic.jpg");
340 pub const JADE: &[u8] = include_bytes!("../data/matcaps/jade.jpg");
341 pub const NORMAL: &[u8] = include_bytes!("../data/matcaps/normal.jpg");
342}
343
344fn decode_matcap_image(data: &[u8]) -> (u32, u32, Vec<f32>) {
349 use image::GenericImageView;
350
351 let img = image::load_from_memory(data).expect("Failed to decode matcap image");
352 let (width, height) = img.dimensions();
353
354 let rgb32f = img.to_rgb32f();
356 let pixels = rgb32f.as_raw();
357
358 let mut rgba = Vec::with_capacity((width * height * 4) as usize);
360 for chunk in pixels.chunks(3) {
361 rgba.push(chunk[0]);
362 rgba.push(chunk[1]);
363 rgba.push(chunk[2]);
364 rgba.push(1.0);
365 }
366
367 (width, height, rgba)
368}
369
370pub fn decode_matcap_image_from_file(
377 path: &std::path::Path,
378) -> std::result::Result<(u32, u32, Vec<f32>), String> {
379 use image::GenericImageView;
380
381 let img =
382 image::open(path).map_err(|e| format!("failed to open '{}': {}", path.display(), e))?;
383 let (width, height) = img.dimensions();
384
385 if width == 0 || height == 0 {
386 return Err(format!("image '{}' has zero dimensions", path.display()));
387 }
388
389 let rgb32f = img.to_rgb32f();
390 let pixels = rgb32f.as_raw();
391
392 let mut rgba = Vec::with_capacity((width * height * 4) as usize);
394 for chunk in pixels.chunks(3) {
395 rgba.push(chunk[0]);
396 rgba.push(chunk[1]);
397 rgba.push(chunk[2]);
398 rgba.push(1.0);
399 }
400
401 Ok((width, height, rgba))
402}
403
404#[must_use]
406pub fn upload_matcap_texture(
407 device: &wgpu::Device,
408 queue: &wgpu::Queue,
409 label: &str,
410 width: u32,
411 height: u32,
412 rgba_data: &[f32],
413) -> wgpu::Texture {
414 let texture = device.create_texture(&wgpu::TextureDescriptor {
415 label: Some(label),
416 size: wgpu::Extent3d {
417 width,
418 height,
419 depth_or_array_layers: 1,
420 },
421 mip_level_count: 1,
422 sample_count: 1,
423 dimension: wgpu::TextureDimension::D2,
424 format: wgpu::TextureFormat::Rgba16Float,
425 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
426 view_formats: &[],
427 });
428
429 let half_data: Vec<u16> = rgba_data
431 .iter()
432 .map(|&v| half::f16::from_f32(v).to_bits())
433 .collect();
434
435 queue.write_texture(
436 wgpu::TexelCopyTextureInfo {
437 texture: &texture,
438 mip_level: 0,
439 origin: wgpu::Origin3d::ZERO,
440 aspect: wgpu::TextureAspect::All,
441 },
442 bytemuck::cast_slice(&half_data),
443 wgpu::TexelCopyBufferLayout {
444 offset: 0,
445 bytes_per_row: Some(width * 4 * 2), rows_per_image: Some(height),
447 },
448 wgpu::Extent3d {
449 width,
450 height,
451 depth_or_array_layers: 1,
452 },
453 );
454
455 texture
456}
457
458#[must_use]
460pub fn create_matcap_sampler(device: &wgpu::Device) -> wgpu::Sampler {
461 device.create_sampler(&wgpu::SamplerDescriptor {
462 label: Some("Matcap Sampler"),
463 address_mode_u: wgpu::AddressMode::ClampToEdge,
464 address_mode_v: wgpu::AddressMode::ClampToEdge,
465 address_mode_w: wgpu::AddressMode::ClampToEdge,
466 mag_filter: wgpu::FilterMode::Linear,
467 min_filter: wgpu::FilterMode::Linear,
468 mipmap_filter: wgpu::FilterMode::Linear,
469 ..Default::default()
470 })
471}
472
473#[must_use]
477pub fn init_matcap_textures(
478 device: &wgpu::Device,
479 queue: &wgpu::Queue,
480 bind_group_layout: &wgpu::BindGroupLayout,
481) -> HashMap<String, MatcapTextureSet> {
482 type BlendableMatEntry<'a> = (&'a str, &'a [u8], &'a [u8], &'a [u8], &'a [u8]);
484
485 let sampler = create_matcap_sampler(device);
486 let mut textures = HashMap::new();
487
488 let upload = |label: &str, data: &[u8]| -> wgpu::Texture {
490 let (w, h, rgba) = decode_matcap_image(data);
491 upload_matcap_texture(device, queue, label, w, h, &rgba)
492 };
493
494 let blendable_mats: &[BlendableMatEntry<'_>] = &[
496 (
497 "clay",
498 matcap_data::CLAY_R,
499 matcap_data::CLAY_G,
500 matcap_data::CLAY_B,
501 matcap_data::CLAY_K,
502 ),
503 (
504 "wax",
505 matcap_data::WAX_R,
506 matcap_data::WAX_G,
507 matcap_data::WAX_B,
508 matcap_data::WAX_K,
509 ),
510 (
511 "candy",
512 matcap_data::CANDY_R,
513 matcap_data::CANDY_G,
514 matcap_data::CANDY_B,
515 matcap_data::CANDY_K,
516 ),
517 (
518 "flat",
519 matcap_data::FLAT_R,
520 matcap_data::FLAT_G,
521 matcap_data::FLAT_B,
522 matcap_data::FLAT_K,
523 ),
524 ];
525
526 for &(name, r_data, g_data, b_data, k_data) in blendable_mats {
527 let tex_r = upload(&format!("matcap_{name}_r"), r_data);
528 let tex_g = upload(&format!("matcap_{name}_g"), g_data);
529 let tex_b = upload(&format!("matcap_{name}_b"), b_data);
530 let tex_k = upload(&format!("matcap_{name}_k"), k_data);
531
532 let view_r = tex_r.create_view(&wgpu::TextureViewDescriptor::default());
533 let view_g = tex_g.create_view(&wgpu::TextureViewDescriptor::default());
534 let view_b = tex_b.create_view(&wgpu::TextureViewDescriptor::default());
535 let view_k = tex_k.create_view(&wgpu::TextureViewDescriptor::default());
536
537 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
538 label: Some(&format!("matcap_{name}_bind_group")),
539 layout: bind_group_layout,
540 entries: &[
541 wgpu::BindGroupEntry {
542 binding: 0,
543 resource: wgpu::BindingResource::TextureView(&view_r),
544 },
545 wgpu::BindGroupEntry {
546 binding: 1,
547 resource: wgpu::BindingResource::TextureView(&view_g),
548 },
549 wgpu::BindGroupEntry {
550 binding: 2,
551 resource: wgpu::BindingResource::TextureView(&view_b),
552 },
553 wgpu::BindGroupEntry {
554 binding: 3,
555 resource: wgpu::BindingResource::TextureView(&view_k),
556 },
557 wgpu::BindGroupEntry {
558 binding: 4,
559 resource: wgpu::BindingResource::Sampler(&sampler),
560 },
561 ],
562 });
563
564 textures.insert(
565 name.to_string(),
566 MatcapTextureSet {
567 tex_r: view_r,
568 tex_g: view_g,
569 tex_b: view_b,
570 tex_k: view_k,
571 sampler: create_matcap_sampler(device), bind_group,
573 },
574 );
575 }
576
577 let static_mats: &[(&str, &[u8])] = &[
579 ("mud", matcap_data::MUD),
580 ("ceramic", matcap_data::CERAMIC),
581 ("jade", matcap_data::JADE),
582 ("normal", matcap_data::NORMAL),
583 ];
584
585 for &(name, data) in static_mats {
586 let tex = upload(&format!("matcap_{name}"), data);
587 let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
588
589 let view_r = tex.create_view(&wgpu::TextureViewDescriptor::default());
591 let view_g = tex.create_view(&wgpu::TextureViewDescriptor::default());
592 let view_b = tex.create_view(&wgpu::TextureViewDescriptor::default());
593
594 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
595 label: Some(&format!("matcap_{name}_bind_group")),
596 layout: bind_group_layout,
597 entries: &[
598 wgpu::BindGroupEntry {
599 binding: 0,
600 resource: wgpu::BindingResource::TextureView(&view),
601 },
602 wgpu::BindGroupEntry {
603 binding: 1,
604 resource: wgpu::BindingResource::TextureView(&view_r),
605 },
606 wgpu::BindGroupEntry {
607 binding: 2,
608 resource: wgpu::BindingResource::TextureView(&view_g),
609 },
610 wgpu::BindGroupEntry {
611 binding: 3,
612 resource: wgpu::BindingResource::TextureView(&view_b),
613 },
614 wgpu::BindGroupEntry {
615 binding: 4,
616 resource: wgpu::BindingResource::Sampler(&sampler),
617 },
618 ],
619 });
620
621 textures.insert(
622 name.to_string(),
623 MatcapTextureSet {
624 tex_r: tex.create_view(&wgpu::TextureViewDescriptor::default()),
625 tex_g: tex.create_view(&wgpu::TextureViewDescriptor::default()),
626 tex_b: tex.create_view(&wgpu::TextureViewDescriptor::default()),
627 tex_k: tex.create_view(&wgpu::TextureViewDescriptor::default()),
628 sampler: create_matcap_sampler(device),
629 bind_group,
630 },
631 );
632 }
633
634 textures
635}
636
637#[must_use]
639pub fn create_matcap_bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout {
640 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
641 label: Some("Matcap Bind Group Layout"),
642 entries: &[
643 wgpu::BindGroupLayoutEntry {
645 binding: 0,
646 visibility: wgpu::ShaderStages::FRAGMENT,
647 ty: wgpu::BindingType::Texture {
648 sample_type: wgpu::TextureSampleType::Float { filterable: true },
649 view_dimension: wgpu::TextureViewDimension::D2,
650 multisampled: false,
651 },
652 count: None,
653 },
654 wgpu::BindGroupLayoutEntry {
656 binding: 1,
657 visibility: wgpu::ShaderStages::FRAGMENT,
658 ty: wgpu::BindingType::Texture {
659 sample_type: wgpu::TextureSampleType::Float { filterable: true },
660 view_dimension: wgpu::TextureViewDimension::D2,
661 multisampled: false,
662 },
663 count: None,
664 },
665 wgpu::BindGroupLayoutEntry {
667 binding: 2,
668 visibility: wgpu::ShaderStages::FRAGMENT,
669 ty: wgpu::BindingType::Texture {
670 sample_type: wgpu::TextureSampleType::Float { filterable: true },
671 view_dimension: wgpu::TextureViewDimension::D2,
672 multisampled: false,
673 },
674 count: None,
675 },
676 wgpu::BindGroupLayoutEntry {
678 binding: 3,
679 visibility: wgpu::ShaderStages::FRAGMENT,
680 ty: wgpu::BindingType::Texture {
681 sample_type: wgpu::TextureSampleType::Float { filterable: true },
682 view_dimension: wgpu::TextureViewDimension::D2,
683 multisampled: false,
684 },
685 count: None,
686 },
687 wgpu::BindGroupLayoutEntry {
689 binding: 4,
690 visibility: wgpu::ShaderStages::FRAGMENT,
691 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
692 count: None,
693 },
694 ],
695 })
696}
697
698#[cfg(test)]
699mod tests {
700 use super::*;
701
702 #[test]
703 fn test_flat_material() {
704 let mat = Material::flat("test_flat");
705 assert!(mat.is_flat);
706 assert!(mat.is_blendable);
707 assert_eq!(mat.diffuse, 0.0);
708 assert_eq!(mat.specular, 0.0);
709 }
710
711 #[test]
712 fn test_material_registry() {
713 let registry = MaterialRegistry::new();
714 assert!(registry.get("clay").is_some());
715 assert!(registry.get("wax").is_some());
716 assert!(registry.get("candy").is_some());
717 assert!(registry.get("flat").is_some());
718 assert!(registry.get("nonexistent").is_none());
719 }
720
721 #[test]
722 fn test_material_uniforms() {
723 let mat = Material::candy();
724 let uniforms = MaterialUniforms::from(&mat);
725 assert_eq!(uniforms.ambient, mat.ambient);
726 assert_eq!(uniforms.specular, mat.specular);
727 }
728
729 #[test]
730 fn test_blendable_materials() {
731 assert!(Material::clay().is_blendable);
732 assert!(Material::wax().is_blendable);
733 assert!(Material::candy().is_blendable);
734 assert!(Material::flat("flat").is_blendable);
735 assert!(!Material::mud().is_blendable);
736 assert!(!Material::ceramic().is_blendable);
737 assert!(!Material::jade().is_blendable);
738 assert!(!Material::normal().is_blendable);
739 }
740
741 #[test]
742 fn test_material_registry_has() {
743 let registry = MaterialRegistry::new();
744 assert!(registry.has("clay"));
745 assert!(registry.has("wax"));
746 assert!(registry.has("normal"));
747 assert!(!registry.has("nonexistent"));
748 assert!(!registry.has("my_custom"));
749 }
750
751 #[test]
752 fn test_material_registry_names_order() {
753 let registry = MaterialRegistry::new();
754 let names = registry.names();
755 assert_eq!(
757 names,
758 vec![
759 "clay", "wax", "candy", "flat", "mud", "ceramic", "jade", "normal"
760 ]
761 );
762 }
763
764 #[test]
765 fn test_material_registry_custom() {
766 let mut registry = MaterialRegistry::new();
767 let mut custom = Material::clay();
768 custom.name = "zebra_mat".to_string();
769 registry.register(custom);
770
771 let mut custom2 = Material::clay();
772 custom2.name = "alpha_mat".to_string();
773 registry.register(custom2);
774
775 assert!(registry.has("zebra_mat"));
776 assert!(registry.has("alpha_mat"));
777
778 let names = registry.names();
779 let expected = vec![
781 "clay",
782 "wax",
783 "candy",
784 "flat",
785 "mud",
786 "ceramic",
787 "jade",
788 "normal",
789 "alpha_mat",
790 "zebra_mat",
791 ];
792 assert_eq!(names, expected);
793 }
794}