Skip to main content

proof_engine/deferred/
materials.rs

1//! PBR material system for deferred rendering.
2//!
3//! Provides:
4//! - PBR material definitions (albedo, metallic, roughness, emission, etc.)
5//! - Material instances (per-object parameter overrides)
6//! - Material library with named materials
7//! - Material sorting keys for draw-call batching
8//! - Instanced rendering data (per-instance transform + material ID)
9//! - Material presets (metal, plastic, glass, wood, stone, etc.)
10
11use std::collections::HashMap;
12use std::fmt;
13
14use super::{clampf, lerpf, saturate, Mat4};
15
16// ---------------------------------------------------------------------------
17// PBR Material
18// ---------------------------------------------------------------------------
19
20/// A physically-based rendering material with full parameter set.
21#[derive(Debug, Clone)]
22pub struct PbrMaterial {
23    /// Human-readable name.
24    pub name: String,
25    /// Base color / albedo (linear RGB).
26    pub albedo: [f32; 3],
27    /// Alpha / opacity (0..1). 1.0 = fully opaque.
28    pub alpha: f32,
29    /// Metallic factor (0..1). 0 = dielectric, 1 = metal.
30    pub metallic: f32,
31    /// Roughness factor (0..1). 0 = mirror, 1 = fully rough.
32    pub roughness: f32,
33    /// Emission color (linear RGB, can be HDR).
34    pub emission: [f32; 3],
35    /// Emission intensity multiplier.
36    pub emission_intensity: f32,
37    /// Normal map texture index (-1 = no normal map).
38    pub normal_map_index: i32,
39    /// Normal map strength (0..1).
40    pub normal_map_strength: f32,
41    /// Index of refraction (glass ~1.5, water ~1.33, air ~1.0).
42    pub ior: f32,
43    /// Anisotropy (-1..1). 0 = isotropic. Positive = stretched along tangent.
44    pub anisotropy: f32,
45    /// Anisotropy rotation in radians.
46    pub anisotropy_rotation: f32,
47    /// Clearcoat strength (0..1). Simulates a clear lacquer layer.
48    pub clearcoat: f32,
49    /// Clearcoat roughness (0..1).
50    pub clearcoat_roughness: f32,
51    /// Subsurface scattering factor (0..1).
52    pub subsurface: f32,
53    /// Subsurface scattering color (linear RGB).
54    pub subsurface_color: [f32; 3],
55    /// Subsurface scattering radius.
56    pub subsurface_radius: f32,
57    /// Sheen strength (for fabric-like materials).
58    pub sheen: f32,
59    /// Sheen tint (0 = white sheen, 1 = tinted by albedo).
60    pub sheen_tint: f32,
61    /// Specular intensity override (0..1, default 0.5 for 4% F0).
62    pub specular: f32,
63    /// Specular tint (0 = white specular, 1 = tinted by albedo).
64    pub specular_tint: f32,
65    /// Transmission factor (0..1). 1 = fully transmissive (glass).
66    pub transmission: f32,
67    /// Absorption color for transmissive materials (linear RGB).
68    pub absorption_color: [f32; 3],
69    /// Absorption distance (how deep before color is fully absorbed).
70    pub absorption_distance: f32,
71    /// Texture handles (opaque IDs).
72    pub albedo_texture: u64,
73    pub roughness_texture: u64,
74    pub metallic_texture: u64,
75    pub normal_texture: u64,
76    pub emission_texture: u64,
77    pub ao_texture: u64,
78    /// Material ID (for G-Buffer material ID channel).
79    pub material_id: u8,
80    /// Whether this material is transparent.
81    pub is_transparent: bool,
82    /// Whether this material uses alpha testing (cutout).
83    pub alpha_test: bool,
84    /// Alpha test threshold.
85    pub alpha_threshold: f32,
86    /// Whether this material is double-sided.
87    pub double_sided: bool,
88    /// UV tiling factor.
89    pub uv_scale: [f32; 2],
90    /// UV offset.
91    pub uv_offset: [f32; 2],
92    /// Whether this material receives shadows.
93    pub receive_shadows: bool,
94    /// Whether this material casts shadows.
95    pub cast_shadows: bool,
96    /// Render priority (lower = rendered first).
97    pub priority: i32,
98}
99
100impl PbrMaterial {
101    /// Create a new default PBR material.
102    pub fn new(name: impl Into<String>) -> Self {
103        Self {
104            name: name.into(),
105            albedo: [0.8, 0.8, 0.8],
106            alpha: 1.0,
107            metallic: 0.0,
108            roughness: 0.5,
109            emission: [0.0, 0.0, 0.0],
110            emission_intensity: 1.0,
111            normal_map_index: -1,
112            normal_map_strength: 1.0,
113            ior: 1.5,
114            anisotropy: 0.0,
115            anisotropy_rotation: 0.0,
116            clearcoat: 0.0,
117            clearcoat_roughness: 0.1,
118            subsurface: 0.0,
119            subsurface_color: [1.0, 0.2, 0.1],
120            subsurface_radius: 1.0,
121            sheen: 0.0,
122            sheen_tint: 0.5,
123            specular: 0.5,
124            specular_tint: 0.0,
125            transmission: 0.0,
126            absorption_color: [1.0, 1.0, 1.0],
127            absorption_distance: 1.0,
128            albedo_texture: 0,
129            roughness_texture: 0,
130            metallic_texture: 0,
131            normal_texture: 0,
132            emission_texture: 0,
133            ao_texture: 0,
134            material_id: 0,
135            is_transparent: false,
136            alpha_test: false,
137            alpha_threshold: 0.5,
138            double_sided: false,
139            uv_scale: [1.0, 1.0],
140            uv_offset: [0.0, 0.0],
141            receive_shadows: true,
142            cast_shadows: true,
143            priority: 0,
144        }
145    }
146
147    // Builder-pattern setters
148    pub fn with_albedo(mut self, r: f32, g: f32, b: f32) -> Self {
149        self.albedo = [r, g, b];
150        self
151    }
152
153    pub fn with_metallic(mut self, m: f32) -> Self {
154        self.metallic = clampf(m, 0.0, 1.0);
155        self
156    }
157
158    pub fn with_roughness(mut self, r: f32) -> Self {
159        self.roughness = clampf(r, 0.0, 1.0);
160        self
161    }
162
163    pub fn with_emission(mut self, r: f32, g: f32, b: f32) -> Self {
164        self.emission = [r, g, b];
165        self
166    }
167
168    pub fn with_emission_intensity(mut self, i: f32) -> Self {
169        self.emission_intensity = i.max(0.0);
170        self
171    }
172
173    pub fn with_ior(mut self, ior: f32) -> Self {
174        self.ior = ior.max(1.0);
175        self
176    }
177
178    pub fn with_anisotropy(mut self, a: f32) -> Self {
179        self.anisotropy = clampf(a, -1.0, 1.0);
180        self
181    }
182
183    pub fn with_clearcoat(mut self, strength: f32, roughness: f32) -> Self {
184        self.clearcoat = clampf(strength, 0.0, 1.0);
185        self.clearcoat_roughness = clampf(roughness, 0.0, 1.0);
186        self
187    }
188
189    pub fn with_subsurface(mut self, strength: f32, color: [f32; 3]) -> Self {
190        self.subsurface = clampf(strength, 0.0, 1.0);
191        self.subsurface_color = color;
192        self
193    }
194
195    pub fn with_sheen(mut self, strength: f32, tint: f32) -> Self {
196        self.sheen = clampf(strength, 0.0, 1.0);
197        self.sheen_tint = clampf(tint, 0.0, 1.0);
198        self
199    }
200
201    pub fn with_transmission(mut self, t: f32) -> Self {
202        self.transmission = clampf(t, 0.0, 1.0);
203        if t > 0.0 {
204            self.is_transparent = true;
205        }
206        self
207    }
208
209    pub fn with_alpha(mut self, a: f32) -> Self {
210        self.alpha = clampf(a, 0.0, 1.0);
211        if a < 1.0 {
212            self.is_transparent = true;
213        }
214        self
215    }
216
217    pub fn with_alpha_test(mut self, threshold: f32) -> Self {
218        self.alpha_test = true;
219        self.alpha_threshold = clampf(threshold, 0.0, 1.0);
220        self
221    }
222
223    pub fn with_material_id(mut self, id: u8) -> Self {
224        self.material_id = id;
225        self
226    }
227
228    pub fn with_uv_transform(mut self, scale: [f32; 2], offset: [f32; 2]) -> Self {
229        self.uv_scale = scale;
230        self.uv_offset = offset;
231        self
232    }
233
234    pub fn with_double_sided(mut self, ds: bool) -> Self {
235        self.double_sided = ds;
236        self
237    }
238
239    /// Compute the Fresnel F0 (reflectance at normal incidence) for this material.
240    pub fn f0(&self) -> [f32; 3] {
241        let dielectric_f0 = ((self.ior - 1.0) / (self.ior + 1.0)).powi(2);
242        [
243            lerpf(dielectric_f0, self.albedo[0], self.metallic),
244            lerpf(dielectric_f0, self.albedo[1], self.metallic),
245            lerpf(dielectric_f0, self.albedo[2], self.metallic),
246        ]
247    }
248
249    /// Compute the diffuse color (metals have no diffuse contribution).
250    pub fn diffuse_color(&self) -> [f32; 3] {
251        let factor = 1.0 - self.metallic;
252        [
253            self.albedo[0] * factor,
254            self.albedo[1] * factor,
255            self.albedo[2] * factor,
256        ]
257    }
258
259    /// Get the total emission (color * intensity).
260    pub fn total_emission(&self) -> [f32; 3] {
261        [
262            self.emission[0] * self.emission_intensity,
263            self.emission[1] * self.emission_intensity,
264            self.emission[2] * self.emission_intensity,
265        ]
266    }
267
268    /// Compute a sort key for batching draw calls.
269    pub fn sort_key(&self) -> MaterialSortKey {
270        MaterialSortKey::from_material(self)
271    }
272
273    /// Whether this material has any textures bound.
274    pub fn has_textures(&self) -> bool {
275        self.albedo_texture != 0
276            || self.roughness_texture != 0
277            || self.metallic_texture != 0
278            || self.normal_texture != 0
279            || self.emission_texture != 0
280            || self.ao_texture != 0
281    }
282
283    /// Compute the perceptual brightness of the albedo.
284    pub fn albedo_luminance(&self) -> f32 {
285        0.2126 * self.albedo[0] + 0.7152 * self.albedo[1] + 0.0722 * self.albedo[2]
286    }
287
288    /// Generate GLSL uniform declarations for this material's properties.
289    pub fn glsl_uniforms() -> &'static str {
290        r#"uniform vec3 u_albedo;
291uniform float u_alpha;
292uniform float u_metallic;
293uniform float u_roughness;
294uniform vec3 u_emission;
295uniform float u_emission_intensity;
296uniform float u_ior;
297uniform float u_anisotropy;
298uniform float u_clearcoat;
299uniform float u_clearcoat_roughness;
300uniform float u_subsurface;
301uniform vec3 u_subsurface_color;
302uniform float u_sheen;
303uniform float u_sheen_tint;
304uniform float u_specular;
305uniform float u_specular_tint;
306uniform float u_transmission;
307uniform float u_normal_map_strength;
308uniform vec2 u_uv_scale;
309uniform vec2 u_uv_offset;
310uniform float u_material_id;
311"#
312    }
313}
314
315impl Default for PbrMaterial {
316    fn default() -> Self {
317        Self::new("default")
318    }
319}
320
321impl fmt::Display for PbrMaterial {
322    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323        write!(
324            f,
325            "PbrMaterial '{}' (albedo=[{:.2},{:.2},{:.2}], metallic={:.2}, roughness={:.2})",
326            self.name, self.albedo[0], self.albedo[1], self.albedo[2],
327            self.metallic, self.roughness
328        )
329    }
330}
331
332// ---------------------------------------------------------------------------
333// Material instance (per-object overrides)
334// ---------------------------------------------------------------------------
335
336/// Per-object overrides layered on top of a base PbrMaterial.
337/// Only set fields are applied; None means use base material value.
338#[derive(Debug, Clone, Default)]
339pub struct MaterialInstance {
340    /// Index of the base material in the library.
341    pub base_material_index: u32,
342    /// Optional albedo override.
343    pub albedo_override: Option<[f32; 3]>,
344    /// Optional alpha override.
345    pub alpha_override: Option<f32>,
346    /// Optional metallic override.
347    pub metallic_override: Option<f32>,
348    /// Optional roughness override.
349    pub roughness_override: Option<f32>,
350    /// Optional emission override.
351    pub emission_override: Option<[f32; 3]>,
352    /// Optional emission intensity override.
353    pub emission_intensity_override: Option<f32>,
354    /// Optional UV scale override.
355    pub uv_scale_override: Option<[f32; 2]>,
356    /// Optional UV offset override.
357    pub uv_offset_override: Option<[f32; 2]>,
358    /// Optional tint color (multiplied with albedo).
359    pub tint: Option<[f32; 4]>,
360    /// Optional priority override.
361    pub priority_override: Option<i32>,
362    /// Custom float parameters (shader-specific).
363    pub custom_floats: HashMap<String, f32>,
364    /// Custom vec3 parameters.
365    pub custom_vec3s: HashMap<String, [f32; 3]>,
366}
367
368impl MaterialInstance {
369    pub fn new(base_material_index: u32) -> Self {
370        Self {
371            base_material_index,
372            ..Default::default()
373        }
374    }
375
376    pub fn with_albedo(mut self, albedo: [f32; 3]) -> Self {
377        self.albedo_override = Some(albedo);
378        self
379    }
380
381    pub fn with_alpha(mut self, alpha: f32) -> Self {
382        self.alpha_override = Some(alpha);
383        self
384    }
385
386    pub fn with_metallic(mut self, m: f32) -> Self {
387        self.metallic_override = Some(m);
388        self
389    }
390
391    pub fn with_roughness(mut self, r: f32) -> Self {
392        self.roughness_override = Some(r);
393        self
394    }
395
396    pub fn with_emission(mut self, e: [f32; 3]) -> Self {
397        self.emission_override = Some(e);
398        self
399    }
400
401    pub fn with_tint(mut self, tint: [f32; 4]) -> Self {
402        self.tint = Some(tint);
403        self
404    }
405
406    pub fn set_custom_float(&mut self, name: impl Into<String>, value: f32) {
407        self.custom_floats.insert(name.into(), value);
408    }
409
410    pub fn set_custom_vec3(&mut self, name: impl Into<String>, value: [f32; 3]) {
411        self.custom_vec3s.insert(name.into(), value);
412    }
413
414    /// Resolve this instance against its base material, producing a final PbrMaterial.
415    pub fn resolve(&self, base: &PbrMaterial) -> PbrMaterial {
416        let mut mat = base.clone();
417
418        if let Some(a) = self.albedo_override {
419            mat.albedo = a;
420        }
421        if let Some(a) = self.alpha_override {
422            mat.alpha = a;
423            if a < 1.0 {
424                mat.is_transparent = true;
425            }
426        }
427        if let Some(m) = self.metallic_override {
428            mat.metallic = m;
429        }
430        if let Some(r) = self.roughness_override {
431            mat.roughness = r;
432        }
433        if let Some(e) = self.emission_override {
434            mat.emission = e;
435        }
436        if let Some(ei) = self.emission_intensity_override {
437            mat.emission_intensity = ei;
438        }
439        if let Some(uv) = self.uv_scale_override {
440            mat.uv_scale = uv;
441        }
442        if let Some(uvo) = self.uv_offset_override {
443            mat.uv_offset = uvo;
444        }
445        if let Some(p) = self.priority_override {
446            mat.priority = p;
447        }
448
449        // Apply tint
450        if let Some(tint) = self.tint {
451            mat.albedo[0] *= tint[0];
452            mat.albedo[1] *= tint[1];
453            mat.albedo[2] *= tint[2];
454            mat.alpha *= tint[3];
455        }
456
457        mat
458    }
459
460    /// Get the number of overrides set.
461    pub fn override_count(&self) -> u32 {
462        let mut count = 0u32;
463        if self.albedo_override.is_some() { count += 1; }
464        if self.alpha_override.is_some() { count += 1; }
465        if self.metallic_override.is_some() { count += 1; }
466        if self.roughness_override.is_some() { count += 1; }
467        if self.emission_override.is_some() { count += 1; }
468        if self.emission_intensity_override.is_some() { count += 1; }
469        if self.uv_scale_override.is_some() { count += 1; }
470        if self.uv_offset_override.is_some() { count += 1; }
471        if self.tint.is_some() { count += 1; }
472        if self.priority_override.is_some() { count += 1; }
473        count += self.custom_floats.len() as u32;
474        count += self.custom_vec3s.len() as u32;
475        count
476    }
477}
478
479// ---------------------------------------------------------------------------
480// Material sort key
481// ---------------------------------------------------------------------------
482
483/// A compact sorting key for materials, used to minimize GPU state changes
484/// by batching objects with similar materials together.
485///
486/// Layout (64 bits):
487/// - Bits 63..56: transparency flag (0=opaque, 1=transparent)
488/// - Bits 55..48: material ID
489/// - Bits 47..32: shader program handle (low 16 bits)
490/// - Bits 31..16: albedo texture handle (low 16 bits)
491/// - Bits 15..0:  other texture handle (low 16 bits)
492#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
493pub struct MaterialSortKey(pub u64);
494
495impl MaterialSortKey {
496    pub fn new(key: u64) -> Self {
497        Self(key)
498    }
499
500    /// Build a sort key from a PBR material.
501    pub fn from_material(mat: &PbrMaterial) -> Self {
502        let mut key: u64 = 0;
503
504        // Transparent objects sort after opaque
505        if mat.is_transparent {
506            key |= 1u64 << 63;
507        }
508
509        // Material ID
510        key |= (mat.material_id as u64) << 48;
511
512        // Albedo texture (for batching by texture)
513        key |= (mat.albedo_texture & 0xFFFF) << 16;
514
515        // Normal texture
516        key |= mat.normal_texture & 0xFFFF;
517
518        Self(key)
519    }
520
521    /// Extract the transparency flag.
522    pub fn is_transparent(&self) -> bool {
523        (self.0 >> 63) & 1 != 0
524    }
525
526    /// Extract the material ID.
527    pub fn material_id(&self) -> u8 {
528        ((self.0 >> 48) & 0xFF) as u8
529    }
530}
531
532impl Default for MaterialSortKey {
533    fn default() -> Self {
534        Self(0)
535    }
536}
537
538// ---------------------------------------------------------------------------
539// Instance data for instanced rendering
540// ---------------------------------------------------------------------------
541
542/// Per-instance data packed for GPU instanced rendering.
543/// Each instance gets a transform matrix and material parameters.
544#[derive(Debug, Clone, Copy)]
545#[repr(C)]
546pub struct InstanceData {
547    /// Model matrix (4x4, column-major, 16 floats).
548    pub model_matrix: [[f32; 4]; 4],
549    /// Packed material parameters.
550    /// [0] = albedo.r, albedo.g, albedo.b, alpha
551    /// [1] = metallic, roughness, emission_intensity, material_id (as float)
552    /// [2] = emission.r, emission.g, emission.b, <unused>
553    /// [3] = uv_scale.x, uv_scale.y, uv_offset.x, uv_offset.y
554    pub material_params: [[f32; 4]; 4],
555}
556
557impl InstanceData {
558    /// Create instance data from a transform and material.
559    pub fn new(transform: &Mat4, material: &PbrMaterial) -> Self {
560        Self {
561            model_matrix: transform.cols,
562            material_params: [
563                [material.albedo[0], material.albedo[1], material.albedo[2], material.alpha],
564                [material.metallic, material.roughness, material.emission_intensity,
565                 material.material_id as f32],
566                [material.emission[0], material.emission[1], material.emission[2], 0.0],
567                [material.uv_scale[0], material.uv_scale[1],
568                 material.uv_offset[0], material.uv_offset[1]],
569            ],
570        }
571    }
572
573    /// Create instance data with just a transform (default material params).
574    pub fn from_transform(transform: &Mat4) -> Self {
575        Self {
576            model_matrix: transform.cols,
577            material_params: [
578                [0.8, 0.8, 0.8, 1.0],
579                [0.0, 0.5, 0.0, 0.0],
580                [0.0, 0.0, 0.0, 0.0],
581                [1.0, 1.0, 0.0, 0.0],
582            ],
583        }
584    }
585
586    /// Set the albedo color.
587    pub fn set_albedo(&mut self, r: f32, g: f32, b: f32) {
588        self.material_params[0][0] = r;
589        self.material_params[0][1] = g;
590        self.material_params[0][2] = b;
591    }
592
593    /// Set the alpha.
594    pub fn set_alpha(&mut self, a: f32) {
595        self.material_params[0][3] = a;
596    }
597
598    /// Set metallic/roughness.
599    pub fn set_metallic_roughness(&mut self, metallic: f32, roughness: f32) {
600        self.material_params[1][0] = metallic;
601        self.material_params[1][1] = roughness;
602    }
603
604    /// Set the material ID.
605    pub fn set_material_id(&mut self, id: u8) {
606        self.material_params[1][3] = id as f32;
607    }
608
609    /// Set emission.
610    pub fn set_emission(&mut self, r: f32, g: f32, b: f32, intensity: f32) {
611        self.material_params[2][0] = r;
612        self.material_params[2][1] = g;
613        self.material_params[2][2] = b;
614        self.material_params[1][2] = intensity;
615    }
616
617    /// The size of this struct in bytes (for vertex attribute stride).
618    pub fn stride() -> usize {
619        std::mem::size_of::<Self>()
620    }
621
622    /// Generate GLSL vertex attribute declarations for instanced rendering.
623    pub fn glsl_instance_attributes() -> &'static str {
624        r#"// Instance attributes (occupies locations 4-11)
625layout(location = 4)  in vec4 i_model_col0;
626layout(location = 5)  in vec4 i_model_col1;
627layout(location = 6)  in vec4 i_model_col2;
628layout(location = 7)  in vec4 i_model_col3;
629layout(location = 8)  in vec4 i_mat_params0; // albedo.rgb, alpha
630layout(location = 9)  in vec4 i_mat_params1; // metallic, roughness, emission_intensity, matid
631layout(location = 10) in vec4 i_mat_params2; // emission.rgb, unused
632layout(location = 11) in vec4 i_mat_params3; // uv_scale, uv_offset
633"#
634    }
635}
636
637impl Default for InstanceData {
638    fn default() -> Self {
639        Self::from_transform(&Mat4::IDENTITY)
640    }
641}
642
643// ---------------------------------------------------------------------------
644// Instance buffer
645// ---------------------------------------------------------------------------
646
647/// A buffer of instance data for batch instanced rendering.
648#[derive(Debug)]
649pub struct InstanceBuffer {
650    /// CPU-side instance data.
651    pub data: Vec<InstanceData>,
652    /// GPU buffer handle (opaque).
653    pub gpu_handle: u64,
654    /// Capacity (number of instances the buffer can hold before realloc).
655    pub capacity: usize,
656    /// Whether the CPU data has been modified since last GPU upload.
657    pub dirty: bool,
658    /// Generation counter.
659    pub generation: u32,
660}
661
662impl InstanceBuffer {
663    pub fn new(capacity: usize) -> Self {
664        Self {
665            data: Vec::with_capacity(capacity),
666            gpu_handle: 0,
667            capacity,
668            dirty: true,
669            generation: 0,
670        }
671    }
672
673    /// Add an instance.
674    pub fn push(&mut self, instance: InstanceData) {
675        self.data.push(instance);
676        self.dirty = true;
677    }
678
679    /// Clear all instances.
680    pub fn clear(&mut self) {
681        self.data.clear();
682        self.dirty = true;
683    }
684
685    /// Number of instances.
686    pub fn len(&self) -> usize {
687        self.data.len()
688    }
689
690    /// Whether the buffer is empty.
691    pub fn is_empty(&self) -> bool {
692        self.data.is_empty()
693    }
694
695    /// Upload to GPU (simulated).
696    pub fn upload(&mut self) {
697        if !self.dirty {
698            return;
699        }
700        // In a real engine: glBufferData / glBufferSubData
701        self.dirty = false;
702        self.generation += 1;
703    }
704
705    /// Memory usage in bytes.
706    pub fn memory_bytes(&self) -> usize {
707        self.data.len() * InstanceData::stride()
708    }
709
710    /// Sort instances by material ID for better batching.
711    pub fn sort_by_material(&mut self) {
712        self.data.sort_by(|a, b| {
713            let a_id = a.material_params[1][3] as u32;
714            let b_id = b.material_params[1][3] as u32;
715            a_id.cmp(&b_id)
716        });
717        self.dirty = true;
718    }
719}
720
721impl Default for InstanceBuffer {
722    fn default() -> Self {
723        Self::new(1024)
724    }
725}
726
727// ---------------------------------------------------------------------------
728// Material library
729// ---------------------------------------------------------------------------
730
731/// A collection of named PBR materials.
732#[derive(Debug)]
733pub struct MaterialLibrary {
734    /// All materials, indexed by their position.
735    pub materials: Vec<PbrMaterial>,
736    /// Name-to-index mapping.
737    pub name_map: HashMap<String, usize>,
738    /// Next material ID to assign.
739    next_id: u8,
740}
741
742impl MaterialLibrary {
743    pub fn new() -> Self {
744        Self {
745            materials: Vec::new(),
746            name_map: HashMap::new(),
747            next_id: 0,
748        }
749    }
750
751    /// Create a library pre-populated with all preset materials.
752    pub fn with_presets() -> Self {
753        let mut lib = Self::new();
754        let presets = MaterialPresets::all();
755        for (name, mat) in presets {
756            lib.add(name, mat);
757        }
758        lib
759    }
760
761    /// Add a material to the library. Returns its index.
762    pub fn add(&mut self, name: impl Into<String>, mut material: PbrMaterial) -> usize {
763        let name = name.into();
764        material.material_id = self.next_id;
765        self.next_id = self.next_id.wrapping_add(1);
766        material.name = name.clone();
767
768        let index = self.materials.len();
769        self.materials.push(material);
770        self.name_map.insert(name, index);
771        index
772    }
773
774    /// Get a material by name.
775    pub fn get(&self, name: &str) -> Option<&PbrMaterial> {
776        self.name_map.get(name).map(|&i| &self.materials[i])
777    }
778
779    /// Get a material by name (mutable).
780    pub fn get_mut(&mut self, name: &str) -> Option<&mut PbrMaterial> {
781        if let Some(&i) = self.name_map.get(name) {
782            Some(&mut self.materials[i])
783        } else {
784            None
785        }
786    }
787
788    /// Get a material by index.
789    pub fn get_by_index(&self, index: usize) -> Option<&PbrMaterial> {
790        self.materials.get(index)
791    }
792
793    /// Get a material by index (mutable).
794    pub fn get_by_index_mut(&mut self, index: usize) -> Option<&mut PbrMaterial> {
795        self.materials.get_mut(index)
796    }
797
798    /// Find the index of a material by name.
799    pub fn index_of(&self, name: &str) -> Option<usize> {
800        self.name_map.get(name).copied()
801    }
802
803    /// Remove a material by name.
804    pub fn remove(&mut self, name: &str) -> Option<PbrMaterial> {
805        if let Some(index) = self.name_map.remove(name) {
806            // Note: this invalidates indices > index. In production,
807            // you would use a slot map or generation system.
808            Some(self.materials.remove(index))
809        } else {
810            None
811        }
812    }
813
814    /// Number of materials in the library.
815    pub fn len(&self) -> usize {
816        self.materials.len()
817    }
818
819    /// Whether the library is empty.
820    pub fn is_empty(&self) -> bool {
821        self.materials.is_empty()
822    }
823
824    /// List all material names.
825    pub fn names(&self) -> Vec<&str> {
826        self.name_map.keys().map(|s| s.as_str()).collect()
827    }
828
829    /// Resolve a material instance against this library.
830    pub fn resolve_instance(&self, instance: &MaterialInstance) -> Option<PbrMaterial> {
831        self.materials
832            .get(instance.base_material_index as usize)
833            .map(|base| instance.resolve(base))
834    }
835
836    /// Generate a description of all materials.
837    pub fn describe(&self) -> String {
838        let mut s = format!("Material Library ({} materials):\n", self.materials.len());
839        for (i, mat) in self.materials.iter().enumerate() {
840            s.push_str(&format!(
841                "  [{}] {} (id={}, metallic={:.2}, roughness={:.2})\n",
842                i, mat.name, mat.material_id, mat.metallic, mat.roughness
843            ));
844        }
845        s
846    }
847}
848
849impl Default for MaterialLibrary {
850    fn default() -> Self {
851        Self::new()
852    }
853}
854
855// ---------------------------------------------------------------------------
856// Material presets
857// ---------------------------------------------------------------------------
858
859/// Predefined material types.
860#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
861pub enum MaterialPreset {
862    Metal,
863    Plastic,
864    Glass,
865    Wood,
866    Stone,
867    Skin,
868    Fabric,
869    Water,
870    Crystal,
871    Lava,
872    Gold,
873    Silver,
874    Copper,
875    Iron,
876    Rubber,
877    Concrete,
878    Marble,
879    Ice,
880    Leather,
881    Ceramic,
882}
883
884impl MaterialPreset {
885    /// Get the PBR material for this preset.
886    pub fn material(&self) -> PbrMaterial {
887        match self {
888            Self::Metal => PbrMaterial::new("metal")
889                .with_albedo(0.7, 0.7, 0.72)
890                .with_metallic(1.0)
891                .with_roughness(0.3),
892
893            Self::Plastic => PbrMaterial::new("plastic")
894                .with_albedo(0.8, 0.2, 0.2)
895                .with_metallic(0.0)
896                .with_roughness(0.4),
897
898            Self::Glass => PbrMaterial::new("glass")
899                .with_albedo(0.95, 0.95, 0.97)
900                .with_metallic(0.0)
901                .with_roughness(0.05)
902                .with_ior(1.52)
903                .with_transmission(0.9)
904                .with_alpha(0.3),
905
906            Self::Wood => PbrMaterial::new("wood")
907                .with_albedo(0.55, 0.35, 0.18)
908                .with_metallic(0.0)
909                .with_roughness(0.7),
910
911            Self::Stone => PbrMaterial::new("stone")
912                .with_albedo(0.5, 0.48, 0.45)
913                .with_metallic(0.0)
914                .with_roughness(0.85),
915
916            Self::Skin => PbrMaterial::new("skin")
917                .with_albedo(0.85, 0.65, 0.52)
918                .with_metallic(0.0)
919                .with_roughness(0.6)
920                .with_subsurface(0.5, [1.0, 0.4, 0.25]),
921
922            Self::Fabric => PbrMaterial::new("fabric")
923                .with_albedo(0.3, 0.3, 0.6)
924                .with_metallic(0.0)
925                .with_roughness(0.9)
926                .with_sheen(0.5, 0.5),
927
928            Self::Water => PbrMaterial::new("water")
929                .with_albedo(0.02, 0.05, 0.08)
930                .with_metallic(0.0)
931                .with_roughness(0.05)
932                .with_ior(1.33)
933                .with_transmission(0.95)
934                .with_alpha(0.6),
935
936            Self::Crystal => PbrMaterial::new("crystal")
937                .with_albedo(0.8, 0.85, 0.95)
938                .with_metallic(0.0)
939                .with_roughness(0.02)
940                .with_ior(2.42) // diamond-like
941                .with_transmission(0.8)
942                .with_clearcoat(1.0, 0.01),
943
944            Self::Lava => PbrMaterial::new("lava")
945                .with_albedo(0.1, 0.02, 0.01)
946                .with_metallic(0.0)
947                .with_roughness(0.95)
948                .with_emission(3.0, 0.8, 0.1)
949                .with_emission_intensity(5.0)
950                .with_subsurface(0.3, [1.0, 0.3, 0.05]),
951
952            Self::Gold => PbrMaterial::new("gold")
953                .with_albedo(1.0, 0.766, 0.336)
954                .with_metallic(1.0)
955                .with_roughness(0.2),
956
957            Self::Silver => PbrMaterial::new("silver")
958                .with_albedo(0.972, 0.960, 0.915)
959                .with_metallic(1.0)
960                .with_roughness(0.15),
961
962            Self::Copper => PbrMaterial::new("copper")
963                .with_albedo(0.955, 0.638, 0.538)
964                .with_metallic(1.0)
965                .with_roughness(0.25),
966
967            Self::Iron => PbrMaterial::new("iron")
968                .with_albedo(0.56, 0.57, 0.58)
969                .with_metallic(1.0)
970                .with_roughness(0.4),
971
972            Self::Rubber => PbrMaterial::new("rubber")
973                .with_albedo(0.15, 0.15, 0.15)
974                .with_metallic(0.0)
975                .with_roughness(0.95),
976
977            Self::Concrete => PbrMaterial::new("concrete")
978                .with_albedo(0.55, 0.55, 0.52)
979                .with_metallic(0.0)
980                .with_roughness(0.9),
981
982            Self::Marble => PbrMaterial::new("marble")
983                .with_albedo(0.9, 0.88, 0.85)
984                .with_metallic(0.0)
985                .with_roughness(0.3)
986                .with_subsurface(0.2, [1.0, 0.95, 0.9]),
987
988            Self::Ice => PbrMaterial::new("ice")
989                .with_albedo(0.85, 0.92, 0.97)
990                .with_metallic(0.0)
991                .with_roughness(0.1)
992                .with_ior(1.31)
993                .with_transmission(0.6)
994                .with_subsurface(0.3, [0.6, 0.8, 1.0]),
995
996            Self::Leather => PbrMaterial::new("leather")
997                .with_albedo(0.35, 0.22, 0.12)
998                .with_metallic(0.0)
999                .with_roughness(0.75),
1000
1001            Self::Ceramic => PbrMaterial::new("ceramic")
1002                .with_albedo(0.9, 0.9, 0.88)
1003                .with_metallic(0.0)
1004                .with_roughness(0.25)
1005                .with_clearcoat(0.8, 0.05),
1006        }
1007    }
1008}
1009
1010impl fmt::Display for MaterialPreset {
1011    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1012        let name = match self {
1013            Self::Metal => "Metal",
1014            Self::Plastic => "Plastic",
1015            Self::Glass => "Glass",
1016            Self::Wood => "Wood",
1017            Self::Stone => "Stone",
1018            Self::Skin => "Skin",
1019            Self::Fabric => "Fabric",
1020            Self::Water => "Water",
1021            Self::Crystal => "Crystal",
1022            Self::Lava => "Lava",
1023            Self::Gold => "Gold",
1024            Self::Silver => "Silver",
1025            Self::Copper => "Copper",
1026            Self::Iron => "Iron",
1027            Self::Rubber => "Rubber",
1028            Self::Concrete => "Concrete",
1029            Self::Marble => "Marble",
1030            Self::Ice => "Ice",
1031            Self::Leather => "Leather",
1032            Self::Ceramic => "Ceramic",
1033        };
1034        write!(f, "{}", name)
1035    }
1036}
1037
1038/// Collection of all material presets.
1039pub struct MaterialPresets;
1040
1041impl MaterialPresets {
1042    /// Get all preset materials as (name, material) pairs.
1043    pub fn all() -> Vec<(String, PbrMaterial)> {
1044        let presets = [
1045            MaterialPreset::Metal,
1046            MaterialPreset::Plastic,
1047            MaterialPreset::Glass,
1048            MaterialPreset::Wood,
1049            MaterialPreset::Stone,
1050            MaterialPreset::Skin,
1051            MaterialPreset::Fabric,
1052            MaterialPreset::Water,
1053            MaterialPreset::Crystal,
1054            MaterialPreset::Lava,
1055            MaterialPreset::Gold,
1056            MaterialPreset::Silver,
1057            MaterialPreset::Copper,
1058            MaterialPreset::Iron,
1059            MaterialPreset::Rubber,
1060            MaterialPreset::Concrete,
1061            MaterialPreset::Marble,
1062            MaterialPreset::Ice,
1063            MaterialPreset::Leather,
1064            MaterialPreset::Ceramic,
1065        ];
1066        presets.iter().map(|p| (p.to_string().to_lowercase(), p.material())).collect()
1067    }
1068
1069    /// Get a single preset material by type.
1070    pub fn get(preset: MaterialPreset) -> PbrMaterial {
1071        preset.material()
1072    }
1073
1074    /// Metal preset.
1075    pub fn metal() -> PbrMaterial { MaterialPreset::Metal.material() }
1076    /// Plastic preset.
1077    pub fn plastic() -> PbrMaterial { MaterialPreset::Plastic.material() }
1078    /// Glass preset.
1079    pub fn glass() -> PbrMaterial { MaterialPreset::Glass.material() }
1080    /// Wood preset.
1081    pub fn wood() -> PbrMaterial { MaterialPreset::Wood.material() }
1082    /// Stone preset.
1083    pub fn stone() -> PbrMaterial { MaterialPreset::Stone.material() }
1084    /// Skin preset.
1085    pub fn skin() -> PbrMaterial { MaterialPreset::Skin.material() }
1086    /// Fabric preset.
1087    pub fn fabric() -> PbrMaterial { MaterialPreset::Fabric.material() }
1088    /// Water preset.
1089    pub fn water() -> PbrMaterial { MaterialPreset::Water.material() }
1090    /// Crystal preset.
1091    pub fn crystal() -> PbrMaterial { MaterialPreset::Crystal.material() }
1092    /// Lava preset.
1093    pub fn lava() -> PbrMaterial { MaterialPreset::Lava.material() }
1094
1095    /// Interpolate between two materials for smooth transitions.
1096    pub fn lerp(a: &PbrMaterial, b: &PbrMaterial, t: f32) -> PbrMaterial {
1097        let t = saturate(t);
1098        let mut result = a.clone();
1099        result.name = format!("{}_{}_blend", a.name, b.name);
1100        result.albedo = [
1101            lerpf(a.albedo[0], b.albedo[0], t),
1102            lerpf(a.albedo[1], b.albedo[1], t),
1103            lerpf(a.albedo[2], b.albedo[2], t),
1104        ];
1105        result.alpha = lerpf(a.alpha, b.alpha, t);
1106        result.metallic = lerpf(a.metallic, b.metallic, t);
1107        result.roughness = lerpf(a.roughness, b.roughness, t);
1108        result.emission = [
1109            lerpf(a.emission[0], b.emission[0], t),
1110            lerpf(a.emission[1], b.emission[1], t),
1111            lerpf(a.emission[2], b.emission[2], t),
1112        ];
1113        result.emission_intensity = lerpf(a.emission_intensity, b.emission_intensity, t);
1114        result.ior = lerpf(a.ior, b.ior, t);
1115        result.anisotropy = lerpf(a.anisotropy, b.anisotropy, t);
1116        result.clearcoat = lerpf(a.clearcoat, b.clearcoat, t);
1117        result.clearcoat_roughness = lerpf(a.clearcoat_roughness, b.clearcoat_roughness, t);
1118        result.subsurface = lerpf(a.subsurface, b.subsurface, t);
1119        result.subsurface_color = [
1120            lerpf(a.subsurface_color[0], b.subsurface_color[0], t),
1121            lerpf(a.subsurface_color[1], b.subsurface_color[1], t),
1122            lerpf(a.subsurface_color[2], b.subsurface_color[2], t),
1123        ];
1124        result.sheen = lerpf(a.sheen, b.sheen, t);
1125        result.transmission = lerpf(a.transmission, b.transmission, t);
1126        result
1127    }
1128
1129    /// Create a material with a random variation of a base preset.
1130    pub fn randomized(base: MaterialPreset, seed: u32) -> PbrMaterial {
1131        let mut mat = base.material();
1132        // Simple hash-based pseudo-random variation
1133        let hash = |s: u32, channel: u32| -> f32 {
1134            let x = s.wrapping_mul(2654435761).wrapping_add(channel.wrapping_mul(40503));
1135            let bits = (x >> 9) | 0x3F800000;
1136            let f = f32::from_bits(bits) - 1.0;
1137            f * 0.2 - 0.1 // +/- 10% variation
1138        };
1139
1140        mat.albedo[0] = clampf(mat.albedo[0] + hash(seed, 0), 0.0, 1.0);
1141        mat.albedo[1] = clampf(mat.albedo[1] + hash(seed, 1), 0.0, 1.0);
1142        mat.albedo[2] = clampf(mat.albedo[2] + hash(seed, 2), 0.0, 1.0);
1143        mat.roughness = clampf(mat.roughness + hash(seed, 3) * 0.5, 0.01, 1.0);
1144        mat.name = format!("{}_var{}", mat.name, seed);
1145        mat
1146    }
1147}
1148
1149// ---------------------------------------------------------------------------
1150// Tests
1151// ---------------------------------------------------------------------------
1152
1153#[cfg(test)]
1154mod tests {
1155    use super::*;
1156
1157    #[test]
1158    fn test_material_defaults() {
1159        let mat = PbrMaterial::default();
1160        assert_eq!(mat.metallic, 0.0);
1161        assert_eq!(mat.roughness, 0.5);
1162        assert_eq!(mat.alpha, 1.0);
1163        assert!(!mat.is_transparent);
1164    }
1165
1166    #[test]
1167    fn test_material_builder() {
1168        let mat = PbrMaterial::new("test")
1169            .with_albedo(1.0, 0.0, 0.0)
1170            .with_metallic(0.8)
1171            .with_roughness(0.2)
1172            .with_emission(0.5, 0.5, 0.0)
1173            .with_clearcoat(0.5, 0.1);
1174
1175        assert_eq!(mat.albedo, [1.0, 0.0, 0.0]);
1176        assert_eq!(mat.metallic, 0.8);
1177        assert_eq!(mat.roughness, 0.2);
1178        assert_eq!(mat.clearcoat, 0.5);
1179    }
1180
1181    #[test]
1182    fn test_material_f0() {
1183        let dielectric = PbrMaterial::new("test").with_metallic(0.0).with_ior(1.5);
1184        let f0 = dielectric.f0();
1185        assert!((f0[0] - 0.04).abs() < 0.01);
1186
1187        let metal = PbrMaterial::new("test")
1188            .with_metallic(1.0)
1189            .with_albedo(1.0, 0.766, 0.336); // gold
1190        let f0 = metal.f0();
1191        assert!((f0[0] - 1.0).abs() < 0.01);
1192    }
1193
1194    #[test]
1195    fn test_material_instance_resolve() {
1196        let base = PbrMaterial::new("base")
1197            .with_albedo(0.5, 0.5, 0.5)
1198            .with_roughness(0.5);
1199
1200        let instance = MaterialInstance::new(0)
1201            .with_albedo([1.0, 0.0, 0.0])
1202            .with_roughness(0.2);
1203
1204        let resolved = instance.resolve(&base);
1205        assert_eq!(resolved.albedo, [1.0, 0.0, 0.0]);
1206        assert_eq!(resolved.roughness, 0.2);
1207        assert_eq!(resolved.metallic, 0.0); // unchanged from base
1208    }
1209
1210    #[test]
1211    fn test_material_instance_tint() {
1212        let base = PbrMaterial::new("base").with_albedo(1.0, 1.0, 1.0);
1213        let instance = MaterialInstance::new(0)
1214            .with_tint([0.5, 0.0, 1.0, 0.8]);
1215        let resolved = instance.resolve(&base);
1216        assert!((resolved.albedo[0] - 0.5).abs() < 0.001);
1217        assert!((resolved.albedo[1] - 0.0).abs() < 0.001);
1218        assert!((resolved.albedo[2] - 1.0).abs() < 0.001);
1219        assert!((resolved.alpha - 0.8).abs() < 0.001);
1220    }
1221
1222    #[test]
1223    fn test_material_library() {
1224        let mut lib = MaterialLibrary::new();
1225        let idx = lib.add("iron", MaterialPresets::metal());
1226        assert_eq!(idx, 0);
1227        assert_eq!(lib.len(), 1);
1228
1229        let mat = lib.get("iron").unwrap();
1230        assert_eq!(mat.metallic, 1.0);
1231
1232        assert!(lib.get("nonexistent").is_none());
1233    }
1234
1235    #[test]
1236    fn test_material_library_presets() {
1237        let lib = MaterialLibrary::with_presets();
1238        assert!(lib.len() >= 10);
1239        assert!(lib.get("gold").is_some());
1240        assert!(lib.get("glass").is_some());
1241        assert!(lib.get("lava").is_some());
1242    }
1243
1244    #[test]
1245    fn test_sort_key() {
1246        let opaque = PbrMaterial::new("opaque").with_metallic(0.0);
1247        let transparent = PbrMaterial::new("transparent").with_alpha(0.5);
1248
1249        let key_opaque = opaque.sort_key();
1250        let key_transparent = transparent.sort_key();
1251
1252        assert!(!key_opaque.is_transparent());
1253        assert!(key_transparent.is_transparent());
1254        assert!(key_opaque < key_transparent);
1255    }
1256
1257    #[test]
1258    fn test_instance_data() {
1259        let mat = MaterialPreset::Gold.material();
1260        let transform = Mat4::IDENTITY;
1261        let instance = InstanceData::new(&transform, &mat);
1262
1263        assert!((instance.material_params[0][0] - 1.0).abs() < 0.01); // gold R
1264        assert!((instance.material_params[1][0] - 1.0).abs() < 0.01); // metallic
1265    }
1266
1267    #[test]
1268    fn test_instance_buffer() {
1269        let mut buf = InstanceBuffer::new(64);
1270        assert!(buf.is_empty());
1271        buf.push(InstanceData::default());
1272        buf.push(InstanceData::default());
1273        assert_eq!(buf.len(), 2);
1274        assert!(buf.dirty);
1275        buf.upload();
1276        assert!(!buf.dirty);
1277    }
1278
1279    #[test]
1280    fn test_material_lerp() {
1281        let a = MaterialPresets::metal();
1282        let b = MaterialPresets::plastic();
1283        let mid = MaterialPresets::lerp(&a, &b, 0.5);
1284        assert!((mid.metallic - 0.5).abs() < 0.01);
1285        assert!((mid.roughness - 0.35).abs() < 0.01); // (0.3 + 0.4) / 2
1286    }
1287
1288    #[test]
1289    fn test_presets_all() {
1290        let all = MaterialPresets::all();
1291        assert!(all.len() >= 10);
1292        for (name, mat) in &all {
1293            assert!(!name.is_empty());
1294            assert!(mat.roughness >= 0.0 && mat.roughness <= 1.0);
1295            assert!(mat.metallic >= 0.0 && mat.metallic <= 1.0);
1296        }
1297    }
1298
1299    #[test]
1300    fn test_randomized_preset() {
1301        let m1 = MaterialPresets::randomized(MaterialPreset::Metal, 42);
1302        let m2 = MaterialPresets::randomized(MaterialPreset::Metal, 43);
1303        // Different seeds should produce different results
1304        assert_ne!(m1.albedo[0], m2.albedo[0]);
1305    }
1306}