khora_core/asset/materials/
unlit.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 unlit materials for the rendering system.
16
17use crate::{
18    asset::{Asset, Material},
19    math::LinearRgba,
20};
21
22use super::AlphaMode;
23
24/// A simple, unlit material.
25///
26/// This material does not react to lighting and simply renders with a solid
27/// base color, optionally modulated by a texture. It's the most basic and
28/// performant type of material, ideal for:
29///
30/// - UI elements and 2D sprites
31/// - Debug visualization
32/// - Performance-critical scenarios
33/// - Stylized games that don't use lighting
34/// - Skyboxes and distant geometry
35///
36/// # Performance
37///
38/// UnlitMaterial is the fastest material type in Khora. It requires minimal
39/// shader calculations and is suitable for scenes with thousands of objects.
40/// The RenderAgent may choose unlit rendering as a fallback strategy when
41/// performance budgets are tight.
42///
43/// # Examples
44///
45/// ```
46/// use khora_core::asset::{UnlitMaterial, AlphaMode};
47/// use khora_core::math::LinearRgba;
48///
49/// // Create a solid red unlit material
50/// let red = UnlitMaterial {
51///     base_color: LinearRgba::new(1.0, 0.0, 0.0, 1.0),
52///     ..Default::default()
53/// };
54///
55/// // Create an unlit material with alpha masking (e.g., foliage)
56/// let foliage = UnlitMaterial {
57///     base_color: LinearRgba::new(0.2, 0.8, 0.2, 1.0),
58///     alpha_mode: AlphaMode::Mask(0.5),
59///     ..Default::default()
60/// };
61/// ```
62#[derive(Clone, Debug)]
63pub struct UnlitMaterial {
64    /// The base color of the material.
65    ///
66    /// This color is directly output without any lighting calculations.
67    /// When a texture is present, the texture color is multiplied with this value.
68    pub base_color: LinearRgba,
69
70    /// Optional texture for the base color.
71    ///
72    /// If present, the texture's RGB values are multiplied with `base_color`.
73    /// The alpha channel can be used for transparency when combined with appropriate `alpha_mode`.
74    ///
75    /// **Future work**: This will be connected to the texture asset system when texture
76    /// loading is fully implemented.
77    // pub base_color_texture: Option<AssetHandle<TextureId>>,
78
79    /// The alpha blending mode for this material.
80    ///
81    /// Determines how transparency is handled. See [`AlphaMode`] for details.
82    pub alpha_mode: AlphaMode,
83
84    /// The alpha cutoff threshold when using `AlphaMode::Mask`.
85    ///
86    /// Fragments with alpha values below this threshold are discarded.
87    /// Typically set to 0.5. Only used when `alpha_mode` is `AlphaMode::Mask`.
88    pub alpha_cutoff: f32,
89}
90
91impl Default for UnlitMaterial {
92    fn default() -> Self {
93        Self {
94            base_color: LinearRgba::new(1.0, 1.0, 1.0, 1.0), // White
95            // base_color_texture: None,
96            alpha_mode: AlphaMode::Opaque,
97            alpha_cutoff: 0.5,
98        }
99    }
100}
101
102// Mark `UnlitMaterial` as a valid asset.
103impl Asset for UnlitMaterial {}
104
105// Mark `UnlitMaterial` as a valid material.
106impl Material for UnlitMaterial {
107    fn base_color(&self) -> crate::math::LinearRgba {
108        self.base_color
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_unlit_material_default() {
118        let material = UnlitMaterial::default();
119
120        assert_eq!(material.base_color, LinearRgba::new(1.0, 1.0, 1.0, 1.0));
121        assert_eq!(material.alpha_mode, AlphaMode::Opaque);
122        assert_eq!(material.alpha_cutoff, 0.5);
123        // assert!(material.base_color_texture.is_none());
124    }
125
126    #[test]
127    fn test_unlit_material_custom_color() {
128        let material = UnlitMaterial {
129            base_color: LinearRgba::new(1.0, 0.0, 0.0, 1.0),
130            ..Default::default()
131        };
132
133        assert_eq!(material.base_color, LinearRgba::new(1.0, 0.0, 0.0, 1.0));
134    }
135
136    #[test]
137    fn test_unlit_material_alpha_modes() {
138        let opaque = UnlitMaterial {
139            alpha_mode: AlphaMode::Opaque,
140            ..Default::default()
141        };
142        assert_eq!(opaque.alpha_mode, AlphaMode::Opaque);
143
144        let masked = UnlitMaterial {
145            alpha_mode: AlphaMode::Mask(0.5),
146            alpha_cutoff: 0.5,
147            ..Default::default()
148        };
149        assert_eq!(masked.alpha_mode, AlphaMode::Mask(0.5));
150        assert_eq!(masked.alpha_cutoff, 0.5);
151
152        let blend = UnlitMaterial {
153            alpha_mode: AlphaMode::Blend,
154            ..Default::default()
155        };
156        assert_eq!(blend.alpha_mode, AlphaMode::Blend);
157    }
158
159    #[test]
160    fn test_unlit_material_clone() {
161        let original = UnlitMaterial {
162            base_color: LinearRgba::new(0.5, 0.7, 1.0, 1.0),
163            alpha_mode: AlphaMode::Mask(0.3),
164            alpha_cutoff: 0.3,
165            // base_color_texture: None,
166        };
167
168        let cloned = original.clone();
169        assert_eq!(cloned.base_color, original.base_color);
170        assert_eq!(cloned.alpha_mode, original.alpha_mode);
171        assert_eq!(cloned.alpha_cutoff, original.alpha_cutoff);
172    }
173
174    #[test]
175    fn test_unlit_material_various_colors() {
176        // Test common UI colors
177        let red = UnlitMaterial {
178            base_color: LinearRgba::new(1.0, 0.0, 0.0, 1.0),
179            ..Default::default()
180        };
181        assert_eq!(red.base_color.r, 1.0);
182
183        let blue = UnlitMaterial {
184            base_color: LinearRgba::new(0.0, 0.0, 1.0, 1.0),
185            ..Default::default()
186        };
187        assert_eq!(blue.base_color.b, 1.0);
188
189        let semi_transparent = UnlitMaterial {
190            base_color: LinearRgba::new(1.0, 1.0, 1.0, 0.5),
191            alpha_mode: AlphaMode::Blend,
192            ..Default::default()
193        };
194        assert_eq!(semi_transparent.base_color.a, 0.5);
195        assert_eq!(semi_transparent.alpha_mode, AlphaMode::Blend);
196    }
197}