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}