1
2use glam::{Vec2, Vec3, Vec4};
5use std::collections::HashMap;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum TextureWrap { Repeat, MirroredRepeat, ClampToEdge, ClampToBorder }
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub enum TextureFilter { Nearest, Linear, Trilinear, Anisotropic2, Anisotropic4, Anisotropic8, Anisotropic16 }
15#[derive(Debug, Clone, Copy, PartialEq)]
16pub enum TextureColorSpace { Linear, Srgb }
17
18#[derive(Debug, Clone)]
19pub struct TextureSlot {
20 pub name: String,
21 pub texture_id: Option<u64>,
22 pub uv_channel: u8,
23 pub tiling: Vec2,
24 pub offset: Vec2,
25 pub rotation: f32,
26 pub wrap_u: TextureWrap,
27 pub wrap_v: TextureWrap,
28 pub filter: TextureFilter,
29 pub color_space: TextureColorSpace,
30 pub mip_bias: f32,
31 pub enabled: bool,
32}
33
34impl TextureSlot {
35 pub fn new(name: impl Into<String>) -> Self {
36 Self {
37 name: name.into(),
38 texture_id: None,
39 uv_channel: 0,
40 tiling: Vec2::ONE,
41 offset: Vec2::ZERO,
42 rotation: 0.0,
43 wrap_u: TextureWrap::Repeat,
44 wrap_v: TextureWrap::Repeat,
45 filter: TextureFilter::Anisotropic8,
46 color_space: TextureColorSpace::Srgb,
47 mip_bias: 0.0,
48 enabled: true,
49 }
50 }
51
52 pub fn with_texture(mut self, id: u64) -> Self {
53 self.texture_id = Some(id);
54 self
55 }
56
57 pub fn uv_matrix(&self) -> [[f32; 3]; 2] {
58 let cos = self.rotation.cos();
59 let sin = self.rotation.sin();
60 [
61 [cos * self.tiling.x, -sin * self.tiling.y, self.offset.x],
62 [sin * self.tiling.x, cos * self.tiling.y, self.offset.y],
63 ]
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq)]
72pub enum BlendMode {
73 Opaque,
74 AlphaTest,
75 Transparent,
76 Additive,
77 Multiply,
78 Screen,
79 Custom,
80}
81
82impl BlendMode {
83 pub fn label(self) -> &'static str {
84 match self {
85 BlendMode::Opaque => "Opaque",
86 BlendMode::AlphaTest => "Alpha Test",
87 BlendMode::Transparent => "Transparent",
88 BlendMode::Additive => "Additive",
89 BlendMode::Multiply => "Multiply",
90 BlendMode::Screen => "Screen",
91 BlendMode::Custom => "Custom",
92 }
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq)]
97pub enum ShadingModel {
98 Standard,
99 Unlit,
100 SubsurfaceScattering,
101 ThinTranslucent,
102 ClearCoat,
103 Hair,
104 Eye,
105 Cloth,
106 Foliage,
107 Water,
108 Custom,
109}
110
111impl ShadingModel {
112 pub fn label(self) -> &'static str {
113 match self {
114 ShadingModel::Standard => "Standard PBR",
115 ShadingModel::Unlit => "Unlit",
116 ShadingModel::SubsurfaceScattering => "Subsurface Scattering",
117 ShadingModel::ThinTranslucent => "Thin Translucent",
118 ShadingModel::ClearCoat => "Clear Coat",
119 ShadingModel::Hair => "Hair",
120 ShadingModel::Eye => "Eye",
121 ShadingModel::Cloth => "Cloth",
122 ShadingModel::Foliage => "Foliage",
123 ShadingModel::Water => "Water",
124 ShadingModel::Custom => "Custom",
125 }
126 }
127 pub fn uses_metallic(self) -> bool { matches!(self, ShadingModel::Standard | ShadingModel::ClearCoat) }
128 pub fn uses_subsurface(self) -> bool { matches!(self, ShadingModel::SubsurfaceScattering | ShadingModel::Foliage) }
129}
130
131#[derive(Debug, Clone)]
132pub struct PbrProperties {
133 pub base_color: Vec4,
134 pub metallic: f32,
135 pub roughness: f32,
136 pub specular: f32,
137 pub specular_color: Vec3,
138 pub emissive_color: Vec3,
139 pub emissive_intensity: f32,
140 pub normal_scale: f32,
141 pub occlusion_strength: f32,
142 pub alpha_cutoff: f32,
143 pub ior: f32,
144 pub transmission: f32,
145 pub thickness: f32,
146 pub subsurface_color: Vec3,
147 pub subsurface_radius: f32,
148 pub subsurface_power: f32,
149 pub sheen_color: Vec3,
150 pub sheen_roughness: f32,
151 pub clear_coat: f32,
152 pub clear_coat_roughness: f32,
153 pub anisotropy: f32,
154 pub anisotropy_rotation: f32,
155 pub horizon_fade: f32,
156 pub reflectance: f32,
157 pub cloth_sheen_color: Vec3,
158 pub cloth_sheen_roughness: f32,
159 pub cloth_normal_scale: f32,
160 pub dithered_lod_transition: bool,
161 pub receive_decals: bool,
162 pub cast_shadow: bool,
163 pub receive_shadow: bool,
164}
165
166impl Default for PbrProperties {
167 fn default() -> Self {
168 Self {
169 base_color: Vec4::new(0.8, 0.8, 0.8, 1.0),
170 metallic: 0.0,
171 roughness: 0.5,
172 specular: 0.5,
173 specular_color: Vec3::ONE,
174 emissive_color: Vec3::ZERO,
175 emissive_intensity: 1.0,
176 normal_scale: 1.0,
177 occlusion_strength: 1.0,
178 alpha_cutoff: 0.5,
179 ior: 1.5,
180 transmission: 0.0,
181 thickness: 0.0,
182 subsurface_color: Vec3::new(1.0, 0.2, 0.1),
183 subsurface_radius: 1.0,
184 subsurface_power: 12.234,
185 sheen_color: Vec3::ONE,
186 sheen_roughness: 0.3,
187 clear_coat: 0.0,
188 clear_coat_roughness: 0.0,
189 anisotropy: 0.0,
190 anisotropy_rotation: 0.0,
191 horizon_fade: 1.0,
192 reflectance: 0.5,
193 cloth_sheen_color: Vec3::new(0.83, 0.81, 0.78),
194 cloth_sheen_roughness: 0.5,
195 cloth_normal_scale: 1.0,
196 dithered_lod_transition: false,
197 receive_decals: true,
198 cast_shadow: true,
199 receive_shadow: true,
200 }
201 }
202}
203
204#[derive(Debug, Clone)]
209pub struct Material {
210 pub id: u64,
211 pub name: String,
212 pub shading_model: ShadingModel,
213 pub blend_mode: BlendMode,
214 pub properties: PbrProperties,
215 pub texture_slots: HashMap<String, TextureSlot>,
216 pub two_sided: bool,
217 pub depth_write: bool,
218 pub depth_test: bool,
219 pub stencil_mask: u8,
220 pub render_queue: i32,
221 pub shader_override: Option<String>,
222 pub tags: Vec<String>,
223 pub parent_material: Option<u64>,
224 pub instance_params: HashMap<String, Vec4>,
225 pub is_variant: bool,
226}
227
228impl Material {
229 pub fn new(id: u64, name: impl Into<String>) -> Self {
230 let mut slots = HashMap::new();
231 for slot_name in &["BaseColor", "Normal", "Roughness", "Metallic", "AO", "Emissive", "Height", "Opacity", "Subsurface", "Clearcoat"] {
232 slots.insert(slot_name.to_string(), TextureSlot::new(*slot_name));
233 }
234 Self {
235 id,
236 name: name.into(),
237 shading_model: ShadingModel::Standard,
238 blend_mode: BlendMode::Opaque,
239 properties: PbrProperties::default(),
240 texture_slots: slots,
241 two_sided: false,
242 depth_write: true,
243 depth_test: true,
244 stencil_mask: 0,
245 render_queue: 2000,
246 shader_override: None,
247 tags: Vec::new(),
248 parent_material: None,
249 instance_params: HashMap::new(),
250 is_variant: false,
251 }
252 }
253
254 pub fn new_unlit(id: u64, name: impl Into<String>, color: Vec4) -> Self {
255 let mut mat = Self::new(id, name);
256 mat.shading_model = ShadingModel::Unlit;
257 mat.properties.base_color = color;
258 mat
259 }
260
261 pub fn new_emissive(id: u64, name: impl Into<String>, emissive: Vec3, intensity: f32) -> Self {
262 let mut mat = Self::new(id, name);
263 mat.properties.emissive_color = emissive;
264 mat.properties.emissive_intensity = intensity;
265 mat
266 }
267
268 pub fn new_glass(id: u64, name: impl Into<String>) -> Self {
269 let mut mat = Self::new(id, name);
270 mat.shading_model = ShadingModel::ThinTranslucent;
271 mat.blend_mode = BlendMode::Transparent;
272 mat.properties.base_color = Vec4::new(0.9, 0.95, 1.0, 0.3);
273 mat.properties.roughness = 0.0;
274 mat.properties.metallic = 0.0;
275 mat.properties.transmission = 0.9;
276 mat.properties.ior = 1.5;
277 mat.depth_write = false;
278 mat.two_sided = true;
279 mat
280 }
281
282 pub fn set_texture(&mut self, slot: &str, id: u64) {
283 if let Some(s) = self.texture_slots.get_mut(slot) {
284 s.texture_id = Some(id);
285 }
286 }
287
288 pub fn has_texture(&self, slot: &str) -> bool {
289 self.texture_slots.get(slot).and_then(|s| s.texture_id).is_some()
290 }
291
292 pub fn requires_alpha_pass(&self) -> bool {
293 matches!(self.blend_mode, BlendMode::Transparent | BlendMode::Additive | BlendMode::Multiply | BlendMode::Screen)
294 }
295
296 pub fn generate_shader_defines(&self) -> String {
297 let mut defs = String::new();
298 let m = &self.properties;
299 defs.push_str(&format!("#define SHADING_MODEL_{:?}\n", self.shading_model));
300 defs.push_str(&format!("#define BLEND_MODE_{:?}\n", self.blend_mode));
301 if self.has_texture("BaseColor") { defs.push_str("#define HAS_ALBEDO_MAP\n"); }
302 if self.has_texture("Normal") { defs.push_str("#define HAS_NORMAL_MAP\n"); }
303 if self.has_texture("Roughness") { defs.push_str("#define HAS_ROUGHNESS_MAP\n"); }
304 if self.has_texture("Metallic") { defs.push_str("#define HAS_METALLIC_MAP\n"); }
305 if self.has_texture("AO") { defs.push_str("#define HAS_AO_MAP\n"); }
306 if self.has_texture("Emissive") { defs.push_str("#define HAS_EMISSIVE_MAP\n"); }
307 if m.emissive_intensity > 0.0 && m.emissive_color.length() > 0.01 { defs.push_str("#define HAS_EMISSIVE\n"); }
308 if m.clear_coat > 0.0 { defs.push_str("#define HAS_CLEAR_COAT\n"); }
309 if m.transmission > 0.0 { defs.push_str("#define HAS_TRANSMISSION\n"); }
310 if m.anisotropy.abs() > 0.01 { defs.push_str("#define HAS_ANISOTROPY\n"); }
311 if self.two_sided { defs.push_str("#define TWO_SIDED\n"); }
312 defs
313 }
314
315 pub fn permutation_hash(&self) -> u64 {
316 let mut h = 0u64;
317 h ^= self.shading_model as u64;
318 h ^= (self.blend_mode as u64) << 4;
319 for (i, (_, slot)) in self.texture_slots.iter().enumerate() {
320 if slot.texture_id.is_some() { h ^= 1 << (8 + i); }
321 }
322 if self.two_sided { h ^= 1 << 24; }
323 h
324 }
325}
326
327#[derive(Debug, Clone, Copy, PartialEq)]
332pub enum LayerBlendOp {
333 Normal,
334 Add,
335 Multiply,
336 Screen,
337 Overlay,
338 Lerp,
339 HeightBlend,
340 AngleBlend,
341 MaskBlend,
342}
343
344impl LayerBlendOp {
345 pub fn label(self) -> &'static str {
346 match self {
347 LayerBlendOp::Normal => "Normal",
348 LayerBlendOp::Add => "Add",
349 LayerBlendOp::Multiply => "Multiply",
350 LayerBlendOp::Screen => "Screen",
351 LayerBlendOp::Overlay => "Overlay",
352 LayerBlendOp::Lerp => "Lerp",
353 LayerBlendOp::HeightBlend => "Height Blend",
354 LayerBlendOp::AngleBlend => "Angle Blend",
355 LayerBlendOp::MaskBlend => "Mask Blend",
356 }
357 }
358}
359
360#[derive(Debug, Clone)]
361pub struct MaterialLayer {
362 pub name: String,
363 pub material_id: u64,
364 pub blend_op: LayerBlendOp,
365 pub opacity: f32,
366 pub mask_texture: Option<u64>,
367 pub mask_channel: u8, pub enabled: bool,
369 pub properties: PbrProperties,
370 pub uv_tiling: Vec2,
371 pub uv_offset: Vec2,
372 pub triplanar: bool,
373 pub triplanar_blend: f32,
374 pub world_space_mapping: bool,
375}
376
377impl MaterialLayer {
378 pub fn new(name: impl Into<String>, mat_id: u64) -> Self {
379 Self {
380 name: name.into(),
381 material_id: mat_id,
382 blend_op: LayerBlendOp::Normal,
383 opacity: 1.0,
384 mask_texture: None,
385 mask_channel: 0,
386 enabled: true,
387 properties: PbrProperties::default(),
388 uv_tiling: Vec2::ONE,
389 uv_offset: Vec2::ZERO,
390 triplanar: false,
391 triplanar_blend: 1.0,
392 world_space_mapping: false,
393 }
394 }
395}
396
397#[derive(Debug, Clone)]
398pub struct LayeredMaterial {
399 pub id: u64,
400 pub name: String,
401 pub layers: Vec<MaterialLayer>,
402 pub blend_mode: BlendMode,
403 pub two_sided: bool,
404}
405
406impl LayeredMaterial {
407 pub fn new(id: u64, name: impl Into<String>) -> Self {
408 Self { id, name: name.into(), layers: Vec::new(), blend_mode: BlendMode::Opaque, two_sided: false }
409 }
410
411 pub fn add_layer(&mut self, layer: MaterialLayer) {
412 self.layers.push(layer);
413 }
414
415 pub fn remove_layer(&mut self, idx: usize) {
416 if idx < self.layers.len() { self.layers.remove(idx); }
417 }
418
419 pub fn move_layer_up(&mut self, idx: usize) {
420 if idx > 0 { self.layers.swap(idx, idx - 1); }
421 }
422
423 pub fn move_layer_down(&mut self, idx: usize) {
424 if idx + 1 < self.layers.len() { self.layers.swap(idx, idx + 1); }
425 }
426}
427
428#[derive(Debug, Clone, Copy, PartialEq)]
433pub enum DecalProjection { Box, Sphere, Cylinder }
434#[derive(Debug, Clone, Copy, PartialEq)]
435pub enum DecalBlendTarget { All, AlbedoRoughness, NormalOcclusion, Emissive }
436
437#[derive(Debug, Clone)]
438pub struct DecalMaterial {
439 pub id: u64,
440 pub name: String,
441 pub base_material: u64,
442 pub projection: DecalProjection,
443 pub blend_target: DecalBlendTarget,
444 pub sort_order: i32,
445 pub fade_in_distance: f32,
446 pub fade_out_distance: f32,
447 pub depth_bias: f32,
448 pub draw_on_water: bool,
449 pub draw_on_static: bool,
450 pub draw_on_dynamic: bool,
451}
452
453#[derive(Debug, Clone)]
458pub struct MaterialLibrary {
459 pub materials: Vec<Material>,
460 pub layered: Vec<LayeredMaterial>,
461 pub decals: Vec<DecalMaterial>,
462 pub next_id: u64,
463 pub categories: HashMap<String, Vec<u64>>,
464}
465
466impl MaterialLibrary {
467 pub fn new() -> Self {
468 let mut lib = Self {
469 materials: Vec::new(),
470 layered: Vec::new(),
471 decals: Vec::new(),
472 next_id: 1,
473 categories: HashMap::new(),
474 };
475 lib.populate_defaults();
476 lib
477 }
478
479 fn populate_defaults(&mut self) {
480 let defaults = [
481 ("Default", ShadingModel::Standard, BlendMode::Opaque, Vec4::new(0.8,0.8,0.8,1.0), 0.0, 0.5),
482 ("Metal_Chrome", ShadingModel::Standard, BlendMode::Opaque, Vec4::new(0.8,0.8,0.85,1.0), 1.0, 0.05),
483 ("Rubber_Black", ShadingModel::Standard, BlendMode::Opaque, Vec4::new(0.02,0.02,0.02,1.0), 0.0, 0.9),
484 ("Wood_Oak", ShadingModel::Standard, BlendMode::Opaque, Vec4::new(0.5,0.35,0.2,1.0), 0.0, 0.7),
485 ("Concrete", ShadingModel::Standard, BlendMode::Opaque, Vec4::new(0.6,0.6,0.6,1.0), 0.0, 0.95),
486 ("Glass", ShadingModel::ThinTranslucent, BlendMode::Transparent, Vec4::new(0.9,0.95,1.0,0.2), 0.0, 0.02),
487 ("Emissive_Glow", ShadingModel::Standard, BlendMode::Opaque, Vec4::new(0.1,0.1,0.1,1.0), 0.0, 0.8),
488 ("Skin", ShadingModel::SubsurfaceScattering, BlendMode::Opaque, Vec4::new(0.82,0.67,0.58,1.0), 0.0, 0.6),
489 ];
490 for (name, model, blend, color, metallic, roughness) in &defaults {
491 let id = self.next_id;
492 self.next_id += 1;
493 let mut mat = Material::new(id, *name);
494 mat.shading_model = *model;
495 mat.blend_mode = *blend;
496 mat.properties.base_color = *color;
497 mat.properties.metallic = *metallic;
498 mat.properties.roughness = *roughness;
499 let category = match model {
500 ShadingModel::ThinTranslucent => "Transparent",
501 ShadingModel::SubsurfaceScattering => "Organic",
502 _ => "Standard",
503 };
504 self.categories.entry(category.to_string()).or_default().push(id);
505 self.materials.push(mat);
506 }
507 }
508
509 pub fn add_material(&mut self, mat: Material) -> u64 {
510 let id = mat.id;
511 self.materials.push(mat);
512 id
513 }
514
515 pub fn find(&self, id: u64) -> Option<&Material> {
516 self.materials.iter().find(|m| m.id == id)
517 }
518
519 pub fn find_mut(&mut self, id: u64) -> Option<&mut Material> {
520 self.materials.iter_mut().find(|m| m.id == id)
521 }
522
523 pub fn search(&self, query: &str) -> Vec<&Material> {
524 let q = query.to_lowercase();
525 self.materials.iter().filter(|m| {
526 m.name.to_lowercase().contains(&q) ||
527 m.tags.iter().any(|t| t.to_lowercase().contains(&q)) ||
528 m.shading_model.label().to_lowercase().contains(&q)
529 }).collect()
530 }
531
532 pub fn by_category(&self, cat: &str) -> Vec<&Material> {
533 let ids = self.categories.get(cat).cloned().unwrap_or_default();
534 ids.iter().filter_map(|id| self.find(*id)).collect()
535 }
536
537 pub fn material_count(&self) -> usize { self.materials.len() }
538}
539
540#[derive(Debug, Clone, Copy, PartialEq)]
545pub enum MaterialEditorTab {
546 Properties,
547 TextureSlots,
548 ShadingModel,
549 Layers,
550 Preview,
551 Code,
552 History,
553}
554
555#[derive(Debug, Clone, Copy, PartialEq)]
556pub enum PreviewShape {
557 Sphere, Cube, Plane, Cylinder, Capsule, Custom,
558}
559
560#[derive(Debug, Clone, Copy, PartialEq)]
561pub enum PreviewLighting {
562 Studio, Outdoor, Night, Custom,
563}
564
565#[derive(Debug, Clone)]
566pub struct MaterialEditorState {
567 pub library: MaterialLibrary,
568 pub selected_material: Option<u64>,
569 pub active_tab: MaterialEditorTab,
570 pub preview_shape: PreviewShape,
571 pub preview_lighting: PreviewLighting,
572 pub preview_rotate: bool,
573 pub preview_rotation: f32,
574 pub preview_zoom: f32,
575 pub show_wireframe: bool,
576 pub show_uv_grid: bool,
577 pub show_tangent_space: bool,
578 pub search_query: String,
579 pub category_filter: Option<String>,
580 pub history: Vec<Material>,
581 pub history_pos: usize,
582 pub compare_material: Option<u64>,
583}
584
585impl MaterialEditorState {
586 pub fn new() -> Self {
587 Self {
588 library: MaterialLibrary::new(),
589 selected_material: Some(1),
590 active_tab: MaterialEditorTab::Properties,
591 preview_shape: PreviewShape::Sphere,
592 preview_lighting: PreviewLighting::Studio,
593 preview_rotate: true,
594 preview_rotation: 0.0,
595 preview_zoom: 1.0,
596 show_wireframe: false,
597 show_uv_grid: false,
598 show_tangent_space: false,
599 search_query: String::new(),
600 category_filter: None,
601 history: Vec::new(),
602 history_pos: 0,
603 compare_material: None,
604 }
605 }
606
607 pub fn selected_material(&self) -> Option<&Material> {
608 self.selected_material.and_then(|id| self.library.find(id))
609 }
610
611 pub fn selected_material_mut(&mut self) -> Option<&mut Material> {
612 self.selected_material.and_then(|id| self.library.find_mut(id))
613 }
614
615 pub fn snapshot(&mut self) {
616 if let Some(mat) = self.selected_material().cloned() {
617 self.history.truncate(self.history_pos);
618 self.history.push(mat);
619 self.history_pos = self.history.len();
620 }
621 }
622
623 pub fn undo(&mut self) {
624 if self.history_pos > 1 {
625 self.history_pos -= 1;
626 let mat = self.history[self.history_pos - 1].clone();
627 let id = mat.id;
628 if let Some(m) = self.library.find_mut(id) {
629 *m = mat;
630 }
631 }
632 }
633
634 pub fn redo(&mut self) {
635 if self.history_pos < self.history.len() {
636 let mat = self.history[self.history_pos].clone();
637 let id = mat.id;
638 self.history_pos += 1;
639 if let Some(m) = self.library.find_mut(id) {
640 *m = mat;
641 }
642 }
643 }
644
645 pub fn update(&mut self, dt: f32) {
646 if self.preview_rotate {
647 self.preview_rotation += dt * 30.0;
648 self.preview_rotation %= 360.0;
649 }
650 }
651
652 pub fn search_results(&self) -> Vec<&Material> {
653 if self.search_query.is_empty() {
654 match &self.category_filter {
655 Some(cat) => self.library.by_category(cat),
656 None => self.library.materials.iter().collect(),
657 }
658 } else {
659 self.library.search(&self.search_query)
660 }
661 }
662
663 pub fn duplicate_selected(&mut self) -> Option<u64> {
664 let mat = self.selected_material().cloned()?;
665 let new_id = self.library.next_id;
666 self.library.next_id += 1;
667 let mut new_mat = mat;
668 new_mat.id = new_id;
669 new_mat.name = format!("{}_copy", new_mat.name);
670 new_mat.is_variant = false;
671 new_mat.parent_material = None;
672 self.library.add_material(new_mat);
673 Some(new_id)
674 }
675
676 pub fn create_variant(&mut self) -> Option<u64> {
677 let mat = self.selected_material().cloned()?;
678 let new_id = self.library.next_id;
679 self.library.next_id += 1;
680 let mut variant = mat;
681 variant.id = new_id;
682 variant.name = format!("{}_variant", variant.name);
683 variant.is_variant = true;
684 variant.parent_material = self.selected_material;
685 self.library.add_material(variant);
686 Some(new_id)
687 }
688}
689
690#[cfg(test)]
694mod tests {
695 use super::*;
696
697 #[test]
698 fn test_material_creation() {
699 let mat = Material::new(1, "TestMat");
700 assert!(mat.texture_slots.contains_key("BaseColor"));
701 assert!(!mat.requires_alpha_pass());
702 }
703
704 #[test]
705 fn test_glass_material() {
706 let glass = Material::new_glass(2, "Glass");
707 assert!(glass.requires_alpha_pass());
708 assert_eq!(glass.shading_model, ShadingModel::ThinTranslucent);
709 }
710
711 #[test]
712 fn test_shader_defines() {
713 let mat = Material::new(3, "Test");
714 let defs = mat.generate_shader_defines();
715 assert!(defs.contains("SHADING_MODEL"));
716 }
717
718 #[test]
719 fn test_library() {
720 let lib = MaterialLibrary::new();
721 assert!(!lib.materials.is_empty());
722 let results = lib.search("metal");
723 assert!(!results.is_empty());
724 }
725
726 #[test]
727 fn test_editor_undo_redo() {
728 let mut ed = MaterialEditorState::new();
729 ed.snapshot();
730 if let Some(mat) = ed.selected_material_mut() {
731 mat.properties.roughness = 0.99;
732 }
733 ed.undo();
734 if let Some(mat) = ed.selected_material() {
735 assert!((mat.properties.roughness - 0.5).abs() < 0.01);
736 }
737 }
738}