Skip to main content

nightshade/ecs/material/
components.rs

1//! Material component definitions.
2
3use crate::ecs::asset_id::{MaterialId, TextureId};
4use serde::{Deserialize, Serialize};
5
6/// Component referencing a material by name in the [`super::MaterialRegistry`].
7///
8/// The `id` field is populated when the material is resolved from the registry.
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, enum2schema::Schema)]
10pub struct MaterialRef {
11    /// Name of the material in the registry.
12    pub name: String,
13    /// Resolved material identifier.
14    #[serde(skip)]
15    #[schema(skip)]
16    pub id: Option<MaterialId>,
17}
18
19impl MaterialRef {
20    /// Creates a new material reference by name.
21    pub fn new(name: impl Into<String>) -> Self {
22        Self {
23            name: name.into(),
24            id: None,
25        }
26    }
27
28    /// Creates a material reference with a pre-resolved identifier.
29    pub fn with_id(name: impl Into<String>, id: MaterialId) -> Self {
30        Self {
31            name: name.into(),
32            id: Some(id),
33        }
34    }
35}
36
37impl Default for MaterialRef {
38    fn default() -> Self {
39        Self {
40            name: "Default".to_string(),
41            id: None,
42        }
43    }
44}
45
46impl From<String> for MaterialRef {
47    fn from(name: String) -> Self {
48        Self { name, id: None }
49    }
50}
51
52/// Per-primitive material variant table from the
53/// [`KHR_materials_variants`] glTF extension. Each mapping says: when
54/// any listed variant name is active, swap the entity's material to
55/// the named one. Outside an active variant the entity uses
56/// `default_material_name`. Variant names rather than document-scoped
57/// indices are stored so multiple assets with different variant
58/// orderings can coexist.
59#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
60pub struct MaterialVariants {
61    pub default_material_name: String,
62    pub mappings: Vec<MaterialVariantMapping>,
63}
64
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66pub struct MaterialVariantMapping {
67    pub variant_names: Vec<String>,
68    pub material_name: String,
69}
70
71impl MaterialVariants {
72    /// Returns the material name to use for the supplied variant name,
73    /// or `default_material_name` if no mapping covers it.
74    pub fn material_for_variant(&self, variant_name: &str) -> &str {
75        for mapping in &self.mappings {
76            if mapping.variant_names.iter().any(|n| n == variant_name) {
77                return &mapping.material_name;
78            }
79        }
80        &self.default_material_name
81    }
82}
83
84impl From<&str> for MaterialRef {
85    fn from(name: &str) -> Self {
86        Self {
87            name: name.to_string(),
88            id: None,
89        }
90    }
91}
92
93/// Per-texture UV transform from glTF KHR_texture_transform.
94///
95/// Applied as `T(offset) * R(rotation) * S(scale)` to homogeneous UVs.
96/// When the extension is absent, the transform is identity and `uv_set`
97/// inherits from the binding's tex_coord.
98#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
99pub struct TextureTransform {
100    /// UV offset (translation).
101    pub offset: [f32; 2],
102    /// UV rotation in radians (positive = counter-clockwise about origin in glTF).
103    pub rotation: f32,
104    /// UV scale.
105    pub scale: [f32; 2],
106    /// Which TEXCOORD_n attribute to sample (0 or 1).
107    pub uv_set: u32,
108}
109
110impl Default for TextureTransform {
111    fn default() -> Self {
112        Self {
113            offset: [0.0, 0.0],
114            rotation: 0.0,
115            scale: [1.0, 1.0],
116            uv_set: 0,
117        }
118    }
119}
120
121impl TextureTransform {
122    pub const IDENTITY: Self = Self {
123        offset: [0.0, 0.0],
124        rotation: 0.0,
125        scale: [1.0, 1.0],
126        uv_set: 0,
127    };
128
129    /// Compose offset/rotation/scale as `T * R * S` and pack into the two-row
130    /// `mat3x2` form used by the shader. Returns `(row0, row1)` where
131    /// row0 = (m00, m01, m02), row1 = (m10, m11, m12).
132    ///
133    /// Per KHR_texture_transform reference renderer (matches Khronos sample
134    /// renderings of TextureTransformTest):
135    ///
136    /// | cos*sx   sin*sy  ox |
137    /// | -sin*sx  cos*sy  oy |
138    /// |       0       0   1 |
139    ///
140    /// applied as `M * (uv.x, uv.y, 1)^T`.
141    pub fn to_packed(&self) -> ([f32; 3], [f32; 3]) {
142        let cos_r = self.rotation.cos();
143        let sin_r = self.rotation.sin();
144        let m00 = cos_r * self.scale[0];
145        let m01 = sin_r * self.scale[1];
146        let m02 = self.offset[0];
147        let m10 = -sin_r * self.scale[0];
148        let m11 = cos_r * self.scale[1];
149        let m12 = self.offset[1];
150        ([m00, m01, m02], [m10, m11, m12])
151    }
152}
153
154/// How alpha values are interpreted for transparency.
155#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, Hash)]
156pub enum AlphaMode {
157    /// Fully opaque, alpha is ignored.
158    #[default]
159    Opaque,
160    /// Binary transparency using alpha cutoff threshold.
161    Mask,
162    /// Full alpha blending with background.
163    Blend,
164}
165
166/// PBR material definition following glTF 2.0 conventions.
167///
168/// Supports the metallic-roughness workflow with optional textures for each parameter.
169/// Includes glTF extensions: KHR_materials_transmission, KHR_materials_volume,
170/// KHR_materials_specular, and KHR_materials_emissive_strength.
171#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
172pub struct Material {
173    /// Base color (albedo) as RGBA. Multiplied with base_texture if present.
174    pub base_color: [f32; 4],
175    /// Emissive color multiplier as RGB.
176    pub emissive_factor: [f32; 3],
177    /// Transparency handling mode.
178    pub alpha_mode: AlphaMode,
179    /// Alpha threshold for [`AlphaMode::Mask`].
180    pub alpha_cutoff: f32,
181    /// Alpha threshold above which a fragment of an [`AlphaMode::Blend`]
182    /// material is treated as effectively opaque by the depth prepass:
183    /// fragments at or above this value write depth so transparent
184    /// fragments behind them are correctly occluded; fragments below
185    /// the threshold skip the prepass and accumulate through OIT.
186    /// Defaults to 0.99; lower it for materials with soft alpha edges
187    /// like anti-aliased text or decals where a tighter threshold
188    /// produces visible halos in OIT.
189    #[serde(default = "default_blend_opaque_alpha_threshold")]
190    pub blend_opaque_alpha_threshold: f32,
191    /// Path to base color texture.
192    pub base_texture: Option<String>,
193    #[serde(default)]
194    pub base_texture_transform: TextureTransform,
195    /// Path to emissive texture.
196    pub emissive_texture: Option<String>,
197    #[serde(default)]
198    pub emissive_texture_transform: TextureTransform,
199    /// Path to normal map texture.
200    pub normal_texture: Option<String>,
201    #[serde(default)]
202    pub normal_texture_transform: TextureTransform,
203    /// Normal map intensity multiplier.
204    #[serde(default = "default_normal_scale")]
205    pub normal_scale: f32,
206    /// Flip normal map Y (green) channel for DirectX-style maps.
207    #[serde(default)]
208    pub normal_map_flip_y: bool,
209    /// Two-component normal map (RG only, B reconstructed).
210    #[serde(default)]
211    pub normal_map_two_component: bool,
212    /// Path to metallic (B) / roughness (G) texture.
213    pub metallic_roughness_texture: Option<String>,
214    #[serde(default)]
215    pub metallic_roughness_texture_transform: TextureTransform,
216    /// Path to ambient occlusion texture (R channel).
217    pub occlusion_texture: Option<String>,
218    #[serde(default)]
219    pub occlusion_texture_transform: TextureTransform,
220    /// Occlusion effect strength (0 = none, 1 = full).
221    #[serde(default = "default_occlusion_strength")]
222    pub occlusion_strength: f32,
223    /// Surface roughness (0 = smooth/mirror, 1 = rough/diffuse).
224    pub roughness: f32,
225    /// Metallic factor (0 = dielectric, 1 = metal).
226    pub metallic: f32,
227    /// Skip lighting calculations (flat shaded).
228    pub unlit: bool,
229    /// Render both sides of faces.
230    #[serde(default)]
231    pub double_sided: bool,
232    /// Transmission factor for refractive materials (KHR_materials_transmission).
233    #[serde(default)]
234    pub transmission_factor: f32,
235    #[serde(default)]
236    pub transmission_texture: Option<String>,
237    #[serde(default)]
238    pub transmission_texture_transform: TextureTransform,
239    /// Volume thickness for transmission (KHR_materials_volume).
240    #[serde(default)]
241    pub thickness: f32,
242    #[serde(default)]
243    pub thickness_texture: Option<String>,
244    #[serde(default)]
245    pub thickness_texture_transform: TextureTransform,
246    /// Light absorption color inside the volume.
247    #[serde(default = "default_attenuation_color")]
248    pub attenuation_color: [f32; 3],
249    /// Distance at which light is attenuated to attenuation_color.
250    #[serde(default)]
251    pub attenuation_distance: f32,
252    /// Index of refraction (default 1.5 for glass).
253    #[serde(default = "default_ior")]
254    pub ior: f32,
255    /// Specular intensity override (KHR_materials_specular).
256    #[serde(default = "default_specular_factor")]
257    pub specular_factor: f32,
258    /// Specular color tint.
259    #[serde(default = "default_specular_color_factor")]
260    pub specular_color_factor: [f32; 3],
261    #[serde(default)]
262    pub specular_texture: Option<String>,
263    #[serde(default)]
264    pub specular_texture_transform: TextureTransform,
265    #[serde(default)]
266    pub specular_color_texture: Option<String>,
267    #[serde(default)]
268    pub specular_color_texture_transform: TextureTransform,
269    /// Emissive intensity multiplier (KHR_materials_emissive_strength).
270    #[serde(default = "default_emissive_strength")]
271    pub emissive_strength: f32,
272    /// Diffuse transmission factor (KHR_materials_diffuse_transmission).
273    /// Fraction of base color light transmitted as Lambertian through the surface.
274    #[serde(default)]
275    pub diffuse_transmission_factor: f32,
276    #[serde(default)]
277    pub diffuse_transmission_texture: Option<String>,
278    #[serde(default)]
279    pub diffuse_transmission_texture_transform: TextureTransform,
280    /// Color tint applied to the diffuse-transmitted light.
281    #[serde(default = "default_diffuse_transmission_color")]
282    pub diffuse_transmission_color_factor: [f32; 3],
283    #[serde(default)]
284    pub diffuse_transmission_color_texture: Option<String>,
285    #[serde(default)]
286    pub diffuse_transmission_color_texture_transform: TextureTransform,
287    /// Chromatic dispersion strength (KHR_materials_dispersion).
288    /// Splits the refraction angle per wavelength using Cauchy's approximation.
289    #[serde(default)]
290    pub dispersion: f32,
291    /// Anisotropy strength (KHR_materials_anisotropy). 0 = isotropic, 1 = maximum.
292    #[serde(default)]
293    pub anisotropy_strength: f32,
294    /// Rotation of anisotropic direction in radians around the surface normal.
295    #[serde(default)]
296    pub anisotropy_rotation: f32,
297    #[serde(default)]
298    pub anisotropy_texture: Option<String>,
299    #[serde(default)]
300    pub anisotropy_texture_transform: TextureTransform,
301    /// Iridescence strength (KHR_materials_iridescence). 0 = none, 1 = full thin-film effect.
302    #[serde(default)]
303    pub iridescence_factor: f32,
304    #[serde(default)]
305    pub iridescence_texture: Option<String>,
306    #[serde(default)]
307    pub iridescence_texture_transform: TextureTransform,
308    /// Index of refraction for the iridescent thin-film layer.
309    #[serde(default = "default_iridescence_ior")]
310    pub iridescence_ior: f32,
311    /// Minimum film thickness in nanometers.
312    #[serde(default = "default_iridescence_thickness_min")]
313    pub iridescence_thickness_minimum: f32,
314    /// Maximum film thickness in nanometers (modulated by iridescence_thickness_texture.g).
315    #[serde(default = "default_iridescence_thickness_max")]
316    pub iridescence_thickness_maximum: f32,
317    #[serde(default)]
318    pub iridescence_thickness_texture: Option<String>,
319    #[serde(default)]
320    pub iridescence_thickness_texture_transform: TextureTransform,
321    /// Sheen color (KHR_materials_sheen). Multiplied with sheen_color_texture if present.
322    #[serde(default)]
323    pub sheen_color_factor: [f32; 3],
324    #[serde(default)]
325    pub sheen_color_texture: Option<String>,
326    #[serde(default)]
327    pub sheen_color_texture_transform: TextureTransform,
328    /// Sheen roughness (0 = sharp, 1 = smooth velvet).
329    #[serde(default)]
330    pub sheen_roughness_factor: f32,
331    #[serde(default)]
332    pub sheen_roughness_texture: Option<String>,
333    #[serde(default)]
334    pub sheen_roughness_texture_transform: TextureTransform,
335    /// Clearcoat layer strength (KHR_materials_clearcoat). 0 = none, 1 = full coat.
336    #[serde(default)]
337    pub clearcoat_factor: f32,
338    #[serde(default)]
339    pub clearcoat_texture: Option<String>,
340    #[serde(default)]
341    pub clearcoat_texture_transform: TextureTransform,
342    /// Clearcoat layer roughness.
343    #[serde(default)]
344    pub clearcoat_roughness_factor: f32,
345    #[serde(default)]
346    pub clearcoat_roughness_texture: Option<String>,
347    #[serde(default)]
348    pub clearcoat_roughness_texture_transform: TextureTransform,
349    /// Optional separate normal map for the clearcoat layer.
350    #[serde(default)]
351    pub clearcoat_normal_texture: Option<String>,
352    #[serde(default)]
353    pub clearcoat_normal_texture_transform: TextureTransform,
354    #[serde(default = "default_normal_scale")]
355    pub clearcoat_normal_scale: f32,
356}
357
358/// Resolved [`TextureId`]s for every texture role on a [`Material`].
359///
360/// Mirrors the `Option<String>` fields on [`Material`] one-for-one so that
361/// hot-path code (per-frame material rebuild) can index a layer map by
362/// [`TextureId`] without re-hashing texture names. Lives in a parallel `Vec`
363/// alongside the [`crate::ecs::material::MaterialRegistry`] entries; populated
364/// by `material_registry_resolve_uploaded_textures` from the renderer drain.
365#[derive(Clone, Copy, Debug, Default)]
366pub struct MaterialTextureIds {
367    pub base: Option<TextureId>,
368    pub emissive: Option<TextureId>,
369    pub normal: Option<TextureId>,
370    pub metallic_roughness: Option<TextureId>,
371    pub occlusion: Option<TextureId>,
372    pub transmission: Option<TextureId>,
373    pub thickness: Option<TextureId>,
374    pub specular: Option<TextureId>,
375    pub specular_color: Option<TextureId>,
376    pub diffuse_transmission: Option<TextureId>,
377    pub diffuse_transmission_color: Option<TextureId>,
378    pub anisotropy: Option<TextureId>,
379    pub iridescence: Option<TextureId>,
380    pub iridescence_thickness: Option<TextureId>,
381    pub sheen_color: Option<TextureId>,
382    pub sheen_roughness: Option<TextureId>,
383    pub clearcoat: Option<TextureId>,
384    pub clearcoat_roughness: Option<TextureId>,
385    pub clearcoat_normal: Option<TextureId>,
386}
387
388fn default_blend_opaque_alpha_threshold() -> f32 {
389    0.99
390}
391
392fn default_normal_scale() -> f32 {
393    1.0
394}
395
396fn default_occlusion_strength() -> f32 {
397    1.0
398}
399
400fn default_attenuation_color() -> [f32; 3] {
401    [1.0, 1.0, 1.0]
402}
403
404fn default_ior() -> f32 {
405    1.5
406}
407
408fn default_specular_factor() -> f32 {
409    1.0
410}
411
412fn default_specular_color_factor() -> [f32; 3] {
413    [1.0, 1.0, 1.0]
414}
415
416fn default_emissive_strength() -> f32 {
417    1.0
418}
419
420fn default_diffuse_transmission_color() -> [f32; 3] {
421    [1.0, 1.0, 1.0]
422}
423
424fn default_iridescence_ior() -> f32 {
425    1.3
426}
427
428fn default_iridescence_thickness_min() -> f32 {
429    100.0
430}
431
432fn default_iridescence_thickness_max() -> f32 {
433    400.0
434}
435
436impl Material {
437    /// Returns `true` if this material requires transparency handling.
438    pub fn is_transparent(&self) -> bool {
439        matches!(self.alpha_mode, AlphaMode::Mask | AlphaMode::Blend)
440    }
441
442    /// Yields every texture name referenced by this material across every
443    /// PBR slot (base color, normal map, metallic-roughness, all glTF
444    /// extension textures). Used by the texture cache to bump and drop
445    /// reference counts without missing any slots.
446    pub fn texture_names(&self) -> impl Iterator<Item = &str> {
447        [
448            self.base_texture.as_deref(),
449            self.emissive_texture.as_deref(),
450            self.normal_texture.as_deref(),
451            self.metallic_roughness_texture.as_deref(),
452            self.occlusion_texture.as_deref(),
453            self.transmission_texture.as_deref(),
454            self.thickness_texture.as_deref(),
455            self.specular_texture.as_deref(),
456            self.specular_color_texture.as_deref(),
457            self.diffuse_transmission_texture.as_deref(),
458            self.diffuse_transmission_color_texture.as_deref(),
459            self.anisotropy_texture.as_deref(),
460            self.iridescence_texture.as_deref(),
461            self.iridescence_thickness_texture.as_deref(),
462            self.sheen_color_texture.as_deref(),
463            self.sheen_roughness_texture.as_deref(),
464            self.clearcoat_texture.as_deref(),
465            self.clearcoat_roughness_texture.as_deref(),
466            self.clearcoat_normal_texture.as_deref(),
467        ]
468        .into_iter()
469        .flatten()
470    }
471}
472
473impl Default for Material {
474    fn default() -> Self {
475        Self {
476            base_color: [0.7, 0.7, 0.7, 1.0],
477            emissive_factor: [0.0, 0.0, 0.0],
478            alpha_mode: AlphaMode::Opaque,
479            alpha_cutoff: 0.5,
480            blend_opaque_alpha_threshold: 0.99,
481            base_texture: None,
482            base_texture_transform: TextureTransform::IDENTITY,
483            emissive_texture: None,
484            emissive_texture_transform: TextureTransform::IDENTITY,
485            normal_texture: None,
486            normal_texture_transform: TextureTransform::IDENTITY,
487            normal_scale: 1.0,
488            normal_map_flip_y: false,
489            normal_map_two_component: false,
490            metallic_roughness_texture: None,
491            metallic_roughness_texture_transform: TextureTransform::IDENTITY,
492            occlusion_texture: None,
493            occlusion_texture_transform: TextureTransform::IDENTITY,
494            occlusion_strength: 1.0,
495            roughness: 0.5,
496            metallic: 0.0,
497            unlit: false,
498            double_sided: false,
499            transmission_factor: 0.0,
500            transmission_texture: None,
501            transmission_texture_transform: TextureTransform::IDENTITY,
502            thickness: 0.0,
503            thickness_texture: None,
504            thickness_texture_transform: TextureTransform::IDENTITY,
505            attenuation_color: [1.0, 1.0, 1.0],
506            attenuation_distance: 0.0,
507            ior: 1.5,
508            specular_factor: 1.0,
509            specular_color_factor: [1.0, 1.0, 1.0],
510            specular_texture: None,
511            specular_texture_transform: TextureTransform::IDENTITY,
512            specular_color_texture: None,
513            specular_color_texture_transform: TextureTransform::IDENTITY,
514            emissive_strength: 1.0,
515            diffuse_transmission_factor: 0.0,
516            diffuse_transmission_texture: None,
517            diffuse_transmission_texture_transform: TextureTransform::IDENTITY,
518            diffuse_transmission_color_factor: [1.0, 1.0, 1.0],
519            diffuse_transmission_color_texture: None,
520            diffuse_transmission_color_texture_transform: TextureTransform::IDENTITY,
521            dispersion: 0.0,
522            anisotropy_strength: 0.0,
523            anisotropy_rotation: 0.0,
524            anisotropy_texture: None,
525            anisotropy_texture_transform: TextureTransform::IDENTITY,
526            iridescence_factor: 0.0,
527            iridescence_texture: None,
528            iridescence_texture_transform: TextureTransform::IDENTITY,
529            iridescence_ior: 1.3,
530            iridescence_thickness_minimum: 100.0,
531            iridescence_thickness_maximum: 400.0,
532            iridescence_thickness_texture: None,
533            iridescence_thickness_texture_transform: TextureTransform::IDENTITY,
534            sheen_color_factor: [0.0, 0.0, 0.0],
535            sheen_color_texture: None,
536            sheen_color_texture_transform: TextureTransform::IDENTITY,
537            sheen_roughness_factor: 0.0,
538            sheen_roughness_texture: None,
539            sheen_roughness_texture_transform: TextureTransform::IDENTITY,
540            clearcoat_factor: 0.0,
541            clearcoat_texture: None,
542            clearcoat_texture_transform: TextureTransform::IDENTITY,
543            clearcoat_roughness_factor: 0.0,
544            clearcoat_roughness_texture: None,
545            clearcoat_roughness_texture_transform: TextureTransform::IDENTITY,
546            clearcoat_normal_texture: None,
547            clearcoat_normal_texture_transform: TextureTransform::IDENTITY,
548            clearcoat_normal_scale: 1.0,
549        }
550    }
551}