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}