1#[allow(dead_code)]
10#[derive(Debug, Clone, PartialEq)]
11pub struct Color4 {
12 pub r: f32,
13 pub g: f32,
14 pub b: f32,
15 pub a: f32,
16}
17
18impl Color4 {
19 #[allow(dead_code)]
21 pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
22 Self { r, g, b, a }
23 }
24
25 #[allow(dead_code)]
27 pub fn white() -> Self {
28 Self::new(1.0, 1.0, 1.0, 1.0)
29 }
30
31 #[allow(dead_code)]
33 pub fn black() -> Self {
34 Self::new(0.0, 0.0, 0.0, 1.0)
35 }
36
37 #[allow(dead_code)]
39 pub fn to_array(&self) -> [f32; 4] {
40 [self.r, self.g, self.b, self.a]
41 }
42}
43
44#[allow(dead_code)]
48#[derive(Debug, Clone)]
49pub struct PbrMaterial {
50 pub name: String,
51 pub base_color: Color4,
53 pub metallic: f32,
55 pub roughness: f32,
57 pub emissive: [f32; 3],
59 pub alpha_cutoff: Option<f32>,
61 pub double_sided: bool,
63}
64
65impl PbrMaterial {
66 #[allow(dead_code)]
68 pub fn default_skin() -> Self {
69 PbrMaterial {
70 name: "skin".to_string(),
71 base_color: Color4::new(0.85, 0.65, 0.55, 1.0),
72 metallic: 0.0,
73 roughness: 0.6,
74 emissive: [0.0, 0.0, 0.0],
75 alpha_cutoff: None,
76 double_sided: false,
77 }
78 }
79
80 #[allow(dead_code)]
82 pub fn default_cloth() -> Self {
83 PbrMaterial {
84 name: "cloth".to_string(),
85 base_color: Color4::new(0.5, 0.5, 0.5, 1.0),
86 metallic: 0.0,
87 roughness: 0.9,
88 emissive: [0.0, 0.0, 0.0],
89 alpha_cutoff: None,
90 double_sided: false,
91 }
92 }
93
94 #[allow(dead_code)]
96 pub fn default_metal() -> Self {
97 PbrMaterial {
98 name: "metal".to_string(),
99 base_color: Color4::new(0.8, 0.8, 0.8, 1.0),
100 metallic: 1.0,
101 roughness: 0.2,
102 emissive: [0.0, 0.0, 0.0],
103 alpha_cutoff: None,
104 double_sided: false,
105 }
106 }
107
108 #[allow(dead_code)]
110 pub fn default_glass() -> Self {
111 PbrMaterial {
112 name: "glass".to_string(),
113 base_color: Color4::new(0.9, 0.95, 1.0, 0.15),
114 metallic: 0.0,
115 roughness: 0.05,
116 emissive: [0.0, 0.0, 0.0],
117 alpha_cutoff: Some(0.01),
118 double_sided: true,
119 }
120 }
121}
122
123#[allow(dead_code)]
127#[derive(Debug, Clone, Default)]
128pub struct MaterialLibrary {
129 pub materials: Vec<PbrMaterial>,
130}
131
132impl MaterialLibrary {
133 #[allow(dead_code)]
135 pub fn new() -> Self {
136 Self::default()
137 }
138
139 #[allow(dead_code)]
141 pub fn add(&mut self, mat: PbrMaterial) -> usize {
142 let idx = self.materials.len();
143 self.materials.push(mat);
144 idx
145 }
146
147 #[allow(dead_code)]
149 pub fn get(&self, idx: usize) -> Option<&PbrMaterial> {
150 self.materials.get(idx)
151 }
152
153 #[allow(dead_code)]
155 pub fn by_name(&self, name: &str) -> Option<&PbrMaterial> {
156 self.materials.iter().find(|m| m.name == name)
157 }
158}
159
160#[allow(dead_code)]
164pub fn material_to_gltf_json(mat: &PbrMaterial) -> String {
165 let c = &mat.base_color;
166 let alpha_mode = if mat.alpha_cutoff.is_some() {
167 "\"MASK\""
168 } else if c.a < 1.0 {
169 "\"BLEND\""
170 } else {
171 "\"OPAQUE\""
172 };
173 let cutoff_fragment = mat
174 .alpha_cutoff
175 .map(|t| format!(", \"alphaCutoff\": {:.4}", t))
176 .unwrap_or_default();
177 format!(
178 r#"{{"name": "{name}", "pbrMetallicRoughness": {{"baseColorFactor": [{r:.4}, {g:.4}, {b:.4}, {a:.4}], "metallicFactor": {m:.4}, "roughnessFactor": {ro:.4}}}, "emissiveFactor": [{er:.4}, {eg:.4}, {eb:.4}], "alphaMode": {am}, "doubleSided": {ds}{cutoff}}}"#,
179 name = mat.name,
180 r = c.r,
181 g = c.g,
182 b = c.b,
183 a = c.a,
184 m = mat.metallic,
185 ro = mat.roughness,
186 er = mat.emissive[0],
187 eg = mat.emissive[1],
188 eb = mat.emissive[2],
189 am = alpha_mode,
190 ds = mat.double_sided,
191 cutoff = cutoff_fragment,
192 )
193}
194
195#[allow(dead_code)]
197pub fn color_to_hex(c: &Color4) -> String {
198 let r = (c.r.clamp(0.0, 1.0) * 255.0).round() as u8;
199 let g = (c.g.clamp(0.0, 1.0) * 255.0).round() as u8;
200 let b = (c.b.clamp(0.0, 1.0) * 255.0).round() as u8;
201 let a = (c.a.clamp(0.0, 1.0) * 255.0).round() as u8;
202 format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a)
203}
204
205#[allow(dead_code)]
209pub fn lerp_material(a: &PbrMaterial, b: &PbrMaterial, t: f32) -> PbrMaterial {
210 let lerp_f32 = |x: f32, y: f32| x + (y - x) * t;
211 let lerp_color = |ca: &Color4, cb: &Color4| Color4 {
212 r: lerp_f32(ca.r, cb.r),
213 g: lerp_f32(ca.g, cb.g),
214 b: lerp_f32(ca.b, cb.b),
215 a: lerp_f32(ca.a, cb.a),
216 };
217 PbrMaterial {
218 name: a.name.clone(),
219 base_color: lerp_color(&a.base_color, &b.base_color),
220 metallic: lerp_f32(a.metallic, b.metallic),
221 roughness: lerp_f32(a.roughness, b.roughness),
222 emissive: [
223 lerp_f32(a.emissive[0], b.emissive[0]),
224 lerp_f32(a.emissive[1], b.emissive[1]),
225 lerp_f32(a.emissive[2], b.emissive[2]),
226 ],
227 alpha_cutoff: a.alpha_cutoff,
228 double_sided: a.double_sided,
229 }
230}
231
232#[cfg(test)]
235mod tests {
236 use super::*;
237
238 #[test]
239 fn color4_white() {
240 let c = Color4::white();
241 assert!((c.r - 1.0).abs() < 1e-6);
242 assert!((c.g - 1.0).abs() < 1e-6);
243 assert!((c.b - 1.0).abs() < 1e-6);
244 assert!((c.a - 1.0).abs() < 1e-6);
245 }
246
247 #[test]
248 fn color4_to_array() {
249 let c = Color4::new(0.1, 0.2, 0.3, 0.4);
250 let arr = c.to_array();
251 assert_eq!(arr, [0.1, 0.2, 0.3, 0.4]);
252 }
253
254 #[test]
255 fn color_to_hex_white() {
256 assert_eq!(color_to_hex(&Color4::white()), "#FFFFFFFF");
257 }
258
259 #[test]
260 fn color_to_hex_black() {
261 assert_eq!(color_to_hex(&Color4::black()), "#000000FF");
262 }
263
264 #[test]
265 fn default_skin_low_metallic() {
266 let skin = PbrMaterial::default_skin();
267 assert!(skin.metallic < 0.1);
268 }
269
270 #[test]
271 fn default_cloth_not_metallic() {
272 let cloth = PbrMaterial::default_cloth();
273 assert!((cloth.metallic - 0.0).abs() < 1e-6);
274 }
275
276 #[test]
277 fn default_metal_metallic_one() {
278 let metal = PbrMaterial::default_metal();
279 assert!((metal.metallic - 1.0).abs() < 1e-6);
280 }
281
282 #[test]
283 fn default_glass_has_alpha() {
284 let glass = PbrMaterial::default_glass();
285 assert!(glass.base_color.a < 1.0, "glass should be transparent");
286 }
287
288 #[test]
289 fn default_glass_has_alpha_cutoff() {
290 let glass = PbrMaterial::default_glass();
291 assert!(glass.alpha_cutoff.is_some());
292 }
293
294 #[test]
295 fn material_to_gltf_json_contains_pbr_key() {
296 let mat = PbrMaterial::default_skin();
297 let json = material_to_gltf_json(&mat);
298 assert!(json.contains("pbrMetallicRoughness"));
299 }
300
301 #[test]
302 fn library_add_and_get() {
303 let mut lib = MaterialLibrary::new();
304 let idx = lib.add(PbrMaterial::default_skin());
305 assert_eq!(idx, 0);
306 assert!(lib.get(0).is_some());
307 }
308
309 #[test]
310 fn library_by_name() {
311 let mut lib = MaterialLibrary::new();
312 lib.add(PbrMaterial::default_skin());
313 assert!(lib.by_name("skin").is_some());
314 assert!(lib.by_name("nonexistent").is_none());
315 }
316
317 #[test]
318 fn library_get_out_of_bounds() {
319 let lib = MaterialLibrary::new();
320 assert!(lib.get(99).is_none());
321 }
322
323 #[test]
324 fn lerp_material_midpoint() {
325 let a = PbrMaterial::default_skin(); let b = PbrMaterial::default_metal(); let mid = lerp_material(&a, &b, 0.5);
328 assert!((mid.metallic - 0.5).abs() < 1e-5);
329 }
330
331 #[test]
332 fn lerp_material_at_t0_matches_a() {
333 let a = PbrMaterial::default_cloth();
334 let b = PbrMaterial::default_metal();
335 let result = lerp_material(&a, &b, 0.0);
336 assert!((result.metallic - a.metallic).abs() < 1e-6);
337 assert!((result.roughness - a.roughness).abs() < 1e-6);
338 }
339
340 #[test]
341 fn lerp_material_at_t1_matches_b() {
342 let a = PbrMaterial::default_cloth();
343 let b = PbrMaterial::default_metal();
344 let result = lerp_material(&a, &b, 1.0);
345 assert!((result.metallic - b.metallic).abs() < 1e-6);
346 assert!((result.roughness - b.roughness).abs() < 1e-6);
347 }
348}