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 fn base_color(&self) -> crate::math::LinearRgba {
177 self.base_color
178 }
179
180 fn emissive_color(&self) -> crate::math::LinearRgba {
181 self.emissive
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188
189 #[test]
190 fn test_standard_material_default() {
191 let material = StandardMaterial::default();
192
193 assert_eq!(material.base_color, LinearRgba::new(0.8, 0.8, 0.8, 1.0));
194 assert_eq!(material.metallic, 0.0);
195 assert_eq!(material.roughness, 0.5);
196 assert_eq!(material.emissive, LinearRgba::new(0.0, 0.0, 0.0, 1.0));
197 assert_eq!(material.alpha_mode, AlphaMode::Opaque);
198 assert_eq!(material.alpha_cutoff, 0.5);
199 assert!(!material.double_sided);
200 // assert!(material.base_color_texture.is_none());
201 // assert!(material.metallic_roughness_texture.is_none());
202 // assert!(material.normal_map.is_none());
203 // assert!(material.occlusion_map.is_none());
204 // assert!(material.emissive_texture.is_none());
205 }
206
207 #[test]
208 fn test_standard_material_custom_creation() {
209 let material = StandardMaterial {
210 base_color: LinearRgba::new(1.0, 0.0, 0.0, 1.0),
211 metallic: 1.0,
212 roughness: 0.2,
213 ..Default::default()
214 };
215
216 assert_eq!(material.base_color, LinearRgba::new(1.0, 0.0, 0.0, 1.0));
217 assert_eq!(material.metallic, 1.0);
218 assert_eq!(material.roughness, 0.2);
219 }
220
221 #[test]
222 fn test_standard_material_metallic_range() {
223 // Test common metallic values
224 let dielectric = StandardMaterial {
225 metallic: 0.0,
226 ..Default::default()
227 };
228 assert_eq!(dielectric.metallic, 0.0);
229
230 let metal = StandardMaterial {
231 metallic: 1.0,
232 ..Default::default()
233 };
234 assert_eq!(metal.metallic, 1.0);
235 }
236
237 #[test]
238 fn test_standard_material_roughness_range() {
239 // Test roughness extremes
240 let smooth = StandardMaterial {
241 roughness: 0.0,
242 ..Default::default()
243 };
244 assert_eq!(smooth.roughness, 0.0);
245
246 let rough = StandardMaterial {
247 roughness: 1.0,
248 ..Default::default()
249 };
250 assert_eq!(rough.roughness, 1.0);
251 }
252
253 #[test]
254 fn test_standard_material_alpha_modes() {
255 let opaque = StandardMaterial {
256 alpha_mode: AlphaMode::Opaque,
257 ..Default::default()
258 };
259 assert_eq!(opaque.alpha_mode, AlphaMode::Opaque);
260
261 let masked = StandardMaterial {
262 alpha_mode: AlphaMode::Mask(0.5),
263 alpha_cutoff: 0.5,
264 ..Default::default()
265 };
266 assert_eq!(masked.alpha_mode, AlphaMode::Mask(0.5));
267 assert_eq!(masked.alpha_cutoff, 0.5);
268
269 let blend = StandardMaterial {
270 alpha_mode: AlphaMode::Blend,
271 ..Default::default()
272 };
273 assert_eq!(blend.alpha_mode, AlphaMode::Blend);
274 }
275
276 #[test]
277 fn test_standard_material_double_sided() {
278 let single_sided = StandardMaterial {
279 double_sided: false,
280 ..Default::default()
281 };
282 assert!(!single_sided.double_sided);
283
284 let double_sided = StandardMaterial {
285 double_sided: true,
286 ..Default::default()
287 };
288 assert!(double_sided.double_sided);
289 }
290
291 #[test]
292 fn test_standard_material_clone() {
293 let original = StandardMaterial {
294 base_color: LinearRgba::new(0.5, 0.5, 0.5, 1.0),
295 metallic: 0.8,
296 roughness: 0.3,
297 ..Default::default()
298 };
299
300 let cloned = original.clone();
301 assert_eq!(cloned.base_color, original.base_color);
302 assert_eq!(cloned.metallic, original.metallic);
303 assert_eq!(cloned.roughness, original.roughness);
304 }
305}