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;
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
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_unlit_material_default() {
114        let material = UnlitMaterial::default();
115
116        assert_eq!(material.base_color, LinearRgba::new(1.0, 1.0, 1.0, 1.0));
117        assert_eq!(material.alpha_mode, AlphaMode::Opaque);
118        assert_eq!(material.alpha_cutoff, 0.5);
119        // assert!(material.base_color_texture.is_none());
120    }
121
122    #[test]
123    fn test_unlit_material_custom_color() {
124        let material = UnlitMaterial {
125            base_color: LinearRgba::new(1.0, 0.0, 0.0, 1.0),
126            ..Default::default()
127        };
128
129        assert_eq!(material.base_color, LinearRgba::new(1.0, 0.0, 0.0, 1.0));
130    }
131
132    #[test]
133    fn test_unlit_material_alpha_modes() {
134        let opaque = UnlitMaterial {
135            alpha_mode: AlphaMode::Opaque,
136            ..Default::default()
137        };
138        assert_eq!(opaque.alpha_mode, AlphaMode::Opaque);
139
140        let masked = UnlitMaterial {
141            alpha_mode: AlphaMode::Mask(0.5),
142            alpha_cutoff: 0.5,
143            ..Default::default()
144        };
145        assert_eq!(masked.alpha_mode, AlphaMode::Mask(0.5));
146        assert_eq!(masked.alpha_cutoff, 0.5);
147
148        let blend = UnlitMaterial {
149            alpha_mode: AlphaMode::Blend,
150            ..Default::default()
151        };
152        assert_eq!(blend.alpha_mode, AlphaMode::Blend);
153    }
154
155    #[test]
156    fn test_unlit_material_clone() {
157        let original = UnlitMaterial {
158            base_color: LinearRgba::new(0.5, 0.7, 1.0, 1.0),
159            alpha_mode: AlphaMode::Mask(0.3),
160            alpha_cutoff: 0.3,
161            // base_color_texture: None,
162        };
163
164        let cloned = original.clone();
165        assert_eq!(cloned.base_color, original.base_color);
166        assert_eq!(cloned.alpha_mode, original.alpha_mode);
167        assert_eq!(cloned.alpha_cutoff, original.alpha_cutoff);
168    }
169
170    #[test]
171    fn test_unlit_material_various_colors() {
172        // Test common UI colors
173        let red = UnlitMaterial {
174            base_color: LinearRgba::new(1.0, 0.0, 0.0, 1.0),
175            ..Default::default()
176        };
177        assert_eq!(red.base_color.r, 1.0);
178
179        let blue = UnlitMaterial {
180            base_color: LinearRgba::new(0.0, 0.0, 1.0, 1.0),
181            ..Default::default()
182        };
183        assert_eq!(blue.base_color.b, 1.0);
184
185        let semi_transparent = UnlitMaterial {
186            base_color: LinearRgba::new(1.0, 1.0, 1.0, 0.5),
187            alpha_mode: AlphaMode::Blend,
188            ..Default::default()
189        };
190        assert_eq!(semi_transparent.base_color.a, 0.5);
191        assert_eq!(semi_transparent.alpha_mode, AlphaMode::Blend);
192    }
193}