Skip to main content

nightshade_renderer/
material.rs

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