khora_core/asset/materials/
standard.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Defines the standard PBR material with metallic-roughness workflow.
16
17use crate::{
18    asset::{Asset, Material},
19    math::LinearRgba,
20};
21
22use super::AlphaMode;
23
24/// A physically-based rendering (PBR) material using the metallic-roughness workflow.
25///
26/// This is the primary material type for realistic 3D objects in Khora. It implements
27/// the standard PBR metallic-roughness model, which is widely used in modern game engines
28/// and 3D content creation tools (e.g., glTF 2.0 standard).
29///
30/// # PBR Properties
31///
32/// - **Base Color**: The surface's base color (albedo). For metals, this represents
33///   the reflectance; for dielectrics, this is the diffuse color.
34/// - **Metallic**: Controls whether the surface behaves like a metal (1.0) or a
35///   dielectric/non-metal (0.0). Intermediate values create unrealistic results.
36/// - **Roughness**: Controls how smooth (0.0) or rough (1.0) the surface appears.
37///   This affects specular reflections.
38///
39/// # Texture Maps
40///
41/// Supports all common PBR texture maps:
42/// - Base color/albedo
43/// - Metallic-roughness combined (metallic in B channel, roughness in G channel)
44/// - Normal map for surface detail
45/// - Ambient occlusion for subtle shadows
46/// - Emissive for self-illuminating areas
47///
48/// # Examples
49///
50/// ```
51/// use khora_core::asset::StandardMaterial;
52/// use khora_core::math::LinearRgba;
53///
54/// // Create a rough, non-metallic surface (e.g., concrete)
55/// let concrete = StandardMaterial {
56///     base_color: LinearRgba::new(0.5, 0.5, 0.5, 1.0),
57///     metallic: 0.0,
58///     roughness: 0.9,
59///     ..Default::default()
60/// };
61///
62/// // Create a smooth, metallic surface (e.g., polished gold)
63/// let gold = StandardMaterial {
64///     base_color: LinearRgba::new(1.0, 0.766, 0.336, 1.0),
65///     metallic: 1.0,
66///     roughness: 0.2,
67///     ..Default::default()
68/// };
69/// ```
70#[derive(Clone, Debug)]
71pub struct StandardMaterial {
72    /// The base color (albedo) of the material.
73    ///
74    /// For metals, this is the reflectance color at normal incidence.
75    /// For dielectrics, this is the diffuse color.
76    pub base_color: LinearRgba,
77
78    /// Optional texture for the base color.
79    ///
80    /// If present, this texture's RGB values are multiplied with `base_color`.
81    /// The alpha channel can be used for transparency when combined with appropriate `alpha_mode`.
82    ///
83    /// **Future work**: Texture asset system integration pending.
84    // pub base_color_texture: Option<AssetHandle<TextureId>>,
85
86    /// The metallic factor (0.0 = dielectric, 1.0 = metal).
87    ///
88    /// This value should typically be either 0.0 or 1.0 for physically accurate results.
89    /// Intermediate values can be used for artistic effects but are not physically based.
90    pub metallic: f32,
91
92    /// The roughness factor (0.0 = smooth, 1.0 = rough).
93    ///
94    /// Controls the microsurface detail of the material, affecting specular reflection.
95    /// Lower values produce sharp, mirror-like reflections; higher values produce
96    /// more diffuse reflections.
97    pub roughness: f32,
98
99    /// Optional texture for metallic and roughness values.
100    ///
101    /// **glTF 2.0 convention**: Blue channel = metallic, Green channel = roughness.
102    /// If present, the texture values are multiplied with the `metallic` and `roughness` factors.
103    ///
104    /// **Future work**: Texture asset system integration pending.
105    // pub metallic_roughness_texture: Option<AssetHandle<TextureId>>,
106
107    /// Optional normal map for adding surface detail.
108    ///
109    /// Normal maps perturb the surface normal to create the illusion of fine geometric
110    /// detail without adding actual geometry. Stored in tangent space.
111    ///
112    /// **Future work**: Texture asset system integration pending.
113    // pub normal_map: Option<AssetHandle<TextureId>>,
114
115    /// Optional ambient occlusion map.
116    ///
117    /// AO maps darken areas that should receive less ambient light, such as crevices
118    /// and contact points. The red channel is typically used.
119    ///
120    /// **Future work**: Texture asset system integration pending.
121    // pub occlusion_map: Option<AssetHandle<TextureId>>,
122
123    /// The emissive color of the material.
124    ///
125    /// Allows the material to emit light. This color is added to the final shaded result
126    /// and is not affected by lighting. Useful for self-illuminating objects like screens,
127    /// neon signs, or magical effects.
128    pub emissive: LinearRgba,
129
130    /// Optional texture for emissive color.
131    ///
132    /// If present, this texture's RGB values are multiplied with `emissive`.
133    ///
134    /// **Future work**: Texture asset system integration pending.
135    // pub emissive_texture: Option<AssetHandle<TextureId>>,
136
137    /// The alpha blending mode for this material.
138    ///
139    /// Determines how transparency is handled. See [`AlphaMode`] for details.
140    pub alpha_mode: AlphaMode,
141
142    /// The alpha cutoff threshold when using `AlphaMode::Mask`.
143    ///
144    /// Fragments with alpha values below this threshold are discarded.
145    /// Typically set to 0.5. Only used when `alpha_mode` is `AlphaMode::Mask`.
146    pub alpha_cutoff: f32,
147
148    /// Whether the material should be rendered double-sided.
149    ///
150    /// If `false`, back-facing triangles are culled for better performance.
151    /// If `true`, both sides of the geometry are rendered.
152    pub double_sided: bool,
153}
154
155impl Default for StandardMaterial {
156    fn default() -> Self {
157        Self {
158            base_color: LinearRgba::new(0.8, 0.8, 0.8, 1.0), // Light gray
159            // base_color_texture: None,
160            metallic: 0.0,  // Non-metallic by default
161            roughness: 0.5, // Medium roughness
162            // metallic_roughness_texture: None,
163            // normal_map: None,
164            // occlusion_map: None,
165            emissive: LinearRgba::new(0.0, 0.0, 0.0, 1.0), // No emission
166            // emissive_texture: None,
167            alpha_mode: AlphaMode::Opaque,
168            alpha_cutoff: 0.5,
169            double_sided: false,
170        }
171    }
172}
173
174impl Asset for StandardMaterial {}
175impl Material for StandardMaterial {}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn test_standard_material_default() {
183        let material = StandardMaterial::default();
184
185        assert_eq!(material.base_color, LinearRgba::new(0.8, 0.8, 0.8, 1.0));
186        assert_eq!(material.metallic, 0.0);
187        assert_eq!(material.roughness, 0.5);
188        assert_eq!(material.emissive, LinearRgba::new(0.0, 0.0, 0.0, 1.0));
189        assert_eq!(material.alpha_mode, AlphaMode::Opaque);
190        assert_eq!(material.alpha_cutoff, 0.5);
191        assert!(!material.double_sided);
192        // assert!(material.base_color_texture.is_none());
193        // assert!(material.metallic_roughness_texture.is_none());
194        // assert!(material.normal_map.is_none());
195        // assert!(material.occlusion_map.is_none());
196        // assert!(material.emissive_texture.is_none());
197    }
198
199    #[test]
200    fn test_standard_material_custom_creation() {
201        let material = StandardMaterial {
202            base_color: LinearRgba::new(1.0, 0.0, 0.0, 1.0),
203            metallic: 1.0,
204            roughness: 0.2,
205            ..Default::default()
206        };
207
208        assert_eq!(material.base_color, LinearRgba::new(1.0, 0.0, 0.0, 1.0));
209        assert_eq!(material.metallic, 1.0);
210        assert_eq!(material.roughness, 0.2);
211    }
212
213    #[test]
214    fn test_standard_material_metallic_range() {
215        // Test common metallic values
216        let dielectric = StandardMaterial {
217            metallic: 0.0,
218            ..Default::default()
219        };
220        assert_eq!(dielectric.metallic, 0.0);
221
222        let metal = StandardMaterial {
223            metallic: 1.0,
224            ..Default::default()
225        };
226        assert_eq!(metal.metallic, 1.0);
227    }
228
229    #[test]
230    fn test_standard_material_roughness_range() {
231        // Test roughness extremes
232        let smooth = StandardMaterial {
233            roughness: 0.0,
234            ..Default::default()
235        };
236        assert_eq!(smooth.roughness, 0.0);
237
238        let rough = StandardMaterial {
239            roughness: 1.0,
240            ..Default::default()
241        };
242        assert_eq!(rough.roughness, 1.0);
243    }
244
245    #[test]
246    fn test_standard_material_alpha_modes() {
247        let opaque = StandardMaterial {
248            alpha_mode: AlphaMode::Opaque,
249            ..Default::default()
250        };
251        assert_eq!(opaque.alpha_mode, AlphaMode::Opaque);
252
253        let masked = StandardMaterial {
254            alpha_mode: AlphaMode::Mask(0.5),
255            alpha_cutoff: 0.5,
256            ..Default::default()
257        };
258        assert_eq!(masked.alpha_mode, AlphaMode::Mask(0.5));
259        assert_eq!(masked.alpha_cutoff, 0.5);
260
261        let blend = StandardMaterial {
262            alpha_mode: AlphaMode::Blend,
263            ..Default::default()
264        };
265        assert_eq!(blend.alpha_mode, AlphaMode::Blend);
266    }
267
268    #[test]
269    fn test_standard_material_double_sided() {
270        let single_sided = StandardMaterial {
271            double_sided: false,
272            ..Default::default()
273        };
274        assert!(!single_sided.double_sided);
275
276        let double_sided = StandardMaterial {
277            double_sided: true,
278            ..Default::default()
279        };
280        assert!(double_sided.double_sided);
281    }
282
283    #[test]
284    fn test_standard_material_clone() {
285        let original = StandardMaterial {
286            base_color: LinearRgba::new(0.5, 0.5, 0.5, 1.0),
287            metallic: 0.8,
288            roughness: 0.3,
289            ..Default::default()
290        };
291
292        let cloned = original.clone();
293        assert_eq!(cloned.base_color, original.base_color);
294        assert_eq!(cloned.metallic, original.metallic);
295        assert_eq!(cloned.roughness, original.roughness);
296    }
297}