1#![allow(dead_code)]
10
11use serde_json::{json, Value};
12
13pub fn khr_materials_unlit() -> Value {
22 json!({})
23}
24
25pub fn khr_materials_emissive_strength(emissive_strength: f32) -> Value {
34 json!({ "emissiveStrength": emissive_strength })
35}
36
37#[derive(Debug, Clone, PartialEq)]
43pub struct ClearcoatExt {
44 pub clearcoat_factor: f32,
46 pub clearcoat_roughness_factor: f32,
48}
49
50impl Default for ClearcoatExt {
51 fn default() -> Self {
52 Self {
53 clearcoat_factor: 0.0,
54 clearcoat_roughness_factor: 0.0,
55 }
56 }
57}
58
59pub fn khr_materials_clearcoat(params: &ClearcoatExt) -> Value {
61 json!({
62 "clearcoatFactor": params.clearcoat_factor,
63 "clearcoatRoughnessFactor": params.clearcoat_roughness_factor,
64 })
65}
66
67#[derive(Debug, Clone, PartialEq)]
73pub struct SheenExt {
74 pub sheen_color_factor: [f32; 3],
76 pub sheen_roughness_factor: f32,
78}
79
80impl Default for SheenExt {
81 fn default() -> Self {
82 Self {
83 sheen_color_factor: [0.0, 0.0, 0.0],
84 sheen_roughness_factor: 0.0,
85 }
86 }
87}
88
89pub fn khr_materials_sheen(params: &SheenExt) -> Value {
91 let [r, g, b] = params.sheen_color_factor;
92 json!({
93 "sheenColorFactor": [r, g, b],
94 "sheenRoughnessFactor": params.sheen_roughness_factor,
95 })
96}
97
98pub fn khr_materials_transmission(transmission_factor: f32) -> Value {
104 json!({ "transmissionFactor": transmission_factor })
105}
106
107#[derive(Debug, Clone, PartialEq)]
113pub struct VolumeExt {
114 pub thickness_factor: f32,
116 pub attenuation_distance: f32,
119 pub attenuation_color: [f32; 3],
122}
123
124impl Default for VolumeExt {
125 fn default() -> Self {
126 Self {
127 thickness_factor: 0.0,
128 attenuation_distance: f32::INFINITY,
129 attenuation_color: [1.0, 1.0, 1.0],
130 }
131 }
132}
133
134pub fn khr_materials_volume(params: &VolumeExt) -> Value {
141 let attn_dist = if params.attenuation_distance.is_infinite() {
142 f64::MAX
143 } else {
144 f64::from(params.attenuation_distance)
145 };
146 let [r, g, b] = params.attenuation_color;
147 json!({
148 "thicknessFactor": params.thickness_factor,
149 "attenuationDistance": attn_dist,
150 "attenuationColor": [r, g, b],
151 })
152}
153
154pub fn khr_materials_ior(ior: f32) -> Value {
162 json!({ "ior": ior })
163}
164
165#[derive(Debug, Clone, PartialEq)]
171pub struct SpecularExt {
172 pub specular_factor: f32,
174 pub specular_color_factor: [f32; 3],
176}
177
178impl Default for SpecularExt {
179 fn default() -> Self {
180 Self {
181 specular_factor: 1.0,
182 specular_color_factor: [1.0, 1.0, 1.0],
183 }
184 }
185}
186
187pub fn khr_materials_specular(params: &SpecularExt) -> Value {
189 let [r, g, b] = params.specular_color_factor;
190 json!({
191 "specularFactor": params.specular_factor,
192 "specularColorFactor": [r, g, b],
193 })
194}
195
196#[derive(Debug, Clone, PartialEq)]
202pub enum AlphaMode {
203 Opaque,
205 Mask(f32),
207 Blend,
209}
210
211impl AlphaMode {
212 fn as_str(&self) -> &'static str {
213 match self {
214 AlphaMode::Opaque => "OPAQUE",
215 AlphaMode::Mask(_) => "MASK",
216 AlphaMode::Blend => "BLEND",
217 }
218 }
219}
220
221#[derive(Debug, Clone)]
227pub struct GltfMaterialDef {
228 pub name: String,
230 pub base_color: [f32; 4],
232 pub metallic_factor: f32,
234 pub roughness_factor: f32,
236 pub emissive_factor: [f32; 3],
238 pub alpha_mode: AlphaMode,
240 pub double_sided: bool,
242 pub extensions: Vec<(String, Value)>,
244}
245
246impl Default for GltfMaterialDef {
247 fn default() -> Self {
248 Self {
249 name: "default".to_string(),
250 base_color: [0.8, 0.8, 0.8, 1.0],
251 metallic_factor: 0.0,
252 roughness_factor: 0.5,
253 emissive_factor: [0.0, 0.0, 0.0],
254 alpha_mode: AlphaMode::Opaque,
255 double_sided: false,
256 extensions: Vec::new(),
257 }
258 }
259}
260
261impl GltfMaterialDef {
262 pub fn skin() -> Self {
269 Self {
270 name: "skin".to_string(),
271 base_color: [0.94, 0.76, 0.69, 1.0],
272 metallic_factor: 0.0,
273 roughness_factor: 0.5,
274 emissive_factor: [0.0, 0.0, 0.0],
275 alpha_mode: AlphaMode::Opaque,
276 double_sided: true,
277 extensions: Vec::new(),
278 }
279 }
280
281 pub fn cloth() -> Self {
283 let sheen = khr_materials_sheen(&SheenExt {
284 sheen_color_factor: [0.8, 0.6, 0.4],
285 sheen_roughness_factor: 0.7,
286 });
287 Self {
288 name: "cloth".to_string(),
289 base_color: [0.15, 0.15, 0.20, 1.0],
290 metallic_factor: 0.0,
291 roughness_factor: 0.85,
292 emissive_factor: [0.0, 0.0, 0.0],
293 alpha_mode: AlphaMode::Opaque,
294 double_sided: false,
295 extensions: vec![("KHR_materials_sheen".to_string(), sheen)],
296 }
297 }
298
299 pub fn glass() -> Self {
302 let transmission = khr_materials_transmission(1.0);
303 let volume = khr_materials_volume(&VolumeExt {
304 thickness_factor: 0.5,
305 attenuation_distance: 5.0,
306 attenuation_color: [0.95, 0.97, 1.0],
307 });
308 let ior = khr_materials_ior(1.5);
309 Self {
310 name: "glass".to_string(),
311 base_color: [1.0, 1.0, 1.0, 0.0],
312 metallic_factor: 0.0,
313 roughness_factor: 0.05,
314 emissive_factor: [0.0, 0.0, 0.0],
315 alpha_mode: AlphaMode::Blend,
316 double_sided: true,
317 extensions: vec![
318 ("KHR_materials_transmission".to_string(), transmission),
319 ("KHR_materials_volume".to_string(), volume),
320 ("KHR_materials_ior".to_string(), ior),
321 ],
322 }
323 }
324
325 pub fn metallic() -> Self {
327 let specular = khr_materials_specular(&SpecularExt {
328 specular_factor: 1.0,
329 specular_color_factor: [0.9, 0.85, 0.8],
330 });
331 Self {
332 name: "metallic".to_string(),
333 base_color: [0.7, 0.7, 0.7, 1.0],
334 metallic_factor: 1.0,
335 roughness_factor: 0.1,
336 emissive_factor: [0.0, 0.0, 0.0],
337 alpha_mode: AlphaMode::Opaque,
338 double_sided: false,
339 extensions: vec![("KHR_materials_specular".to_string(), specular)],
340 }
341 }
342
343 pub fn with_extension(mut self, name: &str, value: Value) -> Self {
349 if let Some(pos) = self.extensions.iter().position(|(n, _)| n == name) {
351 self.extensions[pos].1 = value;
352 } else {
353 self.extensions.push((name.to_string(), value));
354 }
355 self
356 }
357
358 pub fn to_json(&self) -> Value {
364 let [r, g, b, a] = self.base_color;
365 let [er, eg, eb] = self.emissive_factor;
366
367 let mut mat = json!({
368 "name": self.name,
369 "pbrMetallicRoughness": {
370 "baseColorFactor": [r, g, b, a],
371 "metallicFactor": self.metallic_factor,
372 "roughnessFactor": self.roughness_factor,
373 },
374 "emissiveFactor": [er, eg, eb],
375 "alphaMode": self.alpha_mode.as_str(),
376 "doubleSided": self.double_sided,
377 });
378
379 if let AlphaMode::Mask(cutoff) = self.alpha_mode {
381 mat["alphaCutoff"] = json!(cutoff);
382 }
383
384 if !self.extensions.is_empty() {
386 let mut ext_obj = serde_json::Map::new();
387 for (name, val) in &self.extensions {
388 ext_obj.insert(name.clone(), val.clone());
389 }
390 mat["extensions"] = Value::Object(ext_obj);
391 }
392
393 mat
394 }
395
396 pub fn extension_names(&self) -> Vec<&str> {
398 self.extensions.iter().map(|(n, _)| n.as_str()).collect()
399 }
400}
401
402pub fn build_materials_json(materials: &[GltfMaterialDef]) -> Value {
409 let arr: Vec<Value> = materials.iter().map(|m| m.to_json()).collect();
410 Value::Array(arr)
411}
412
413pub fn validate_material_json(mat: &Value) -> Result<(), String> {
423 let pbr = mat
424 .get("pbrMetallicRoughness")
425 .ok_or_else(|| "missing 'pbrMetallicRoughness'".to_string())?;
426
427 if !pbr.is_object() {
428 return Err("'pbrMetallicRoughness' must be an object".to_string());
429 }
430
431 if let Some(mf) = pbr.get("metallicFactor") {
433 let v = mf
434 .as_f64()
435 .ok_or_else(|| "'metallicFactor' must be a number".to_string())?;
436 if !(0.0..=1.0).contains(&v) {
437 return Err(format!("'metallicFactor' out of range [0,1]: {v}"));
438 }
439 }
440
441 if let Some(rf) = pbr.get("roughnessFactor") {
443 let v = rf
444 .as_f64()
445 .ok_or_else(|| "'roughnessFactor' must be a number".to_string())?;
446 if !(0.0..=1.0).contains(&v) {
447 return Err(format!("'roughnessFactor' out of range [0,1]: {v}"));
448 }
449 }
450
451 if let Some(bcf) = pbr.get("baseColorFactor") {
453 let arr = bcf
454 .as_array()
455 .ok_or_else(|| "'baseColorFactor' must be an array".to_string())?;
456 if arr.len() != 4 {
457 return Err(format!(
458 "'baseColorFactor' must have 4 elements, got {}",
459 arr.len()
460 ));
461 }
462 }
463
464 if let Some(am) = mat.get("alphaMode") {
466 let s = am
467 .as_str()
468 .ok_or_else(|| "'alphaMode' must be a string".to_string())?;
469 if !matches!(s, "OPAQUE" | "MASK" | "BLEND") {
470 return Err(format!("unknown 'alphaMode': '{s}'"));
471 }
472 }
473
474 Ok(())
475}
476
477pub fn extract_extensions_used(gltf_json: &Value) -> Vec<String> {
486 gltf_json
487 .get("extensionsUsed")
488 .and_then(|v| v.as_array())
489 .map(|arr| {
490 arr.iter()
491 .filter_map(|v| v.as_str().map(str::to_owned))
492 .collect()
493 })
494 .unwrap_or_default()
495}
496
497#[cfg(test)]
502mod tests {
503 use super::*;
504 use std::fs;
505
506 #[test]
510 fn test_unlit_is_empty_object() {
511 let v = khr_materials_unlit();
512 assert!(v.is_object());
513 assert_eq!(v.as_object().expect("should succeed").len(), 0);
514 }
515
516 #[test]
520 fn test_emissive_strength_value() {
521 let v = khr_materials_emissive_strength(3.5);
522 assert!((v["emissiveStrength"].as_f64().expect("should succeed") - 3.5).abs() < 1e-6);
523 }
524
525 #[test]
529 fn test_clearcoat_fields() {
530 let p = ClearcoatExt {
531 clearcoat_factor: 0.8,
532 clearcoat_roughness_factor: 0.3,
533 };
534 let v = khr_materials_clearcoat(&p);
535 assert!((v["clearcoatFactor"].as_f64().expect("should succeed") - 0.8).abs() < 1e-6);
536 assert!((v["clearcoatRoughnessFactor"].as_f64().expect("should succeed") - 0.3).abs() < 1e-6);
537 }
538
539 #[test]
543 fn test_sheen_color_array_length() {
544 let p = SheenExt {
545 sheen_color_factor: [0.5, 0.2, 0.8],
546 sheen_roughness_factor: 0.6,
547 };
548 let v = khr_materials_sheen(&p);
549 assert_eq!(v["sheenColorFactor"].as_array().expect("should succeed").len(), 3);
550 }
551
552 #[test]
556 fn test_transmission_round_trip() {
557 let v = khr_materials_transmission(0.75);
558 assert!((v["transmissionFactor"].as_f64().expect("should succeed") - 0.75).abs() < 1e-6);
559 }
560
561 #[test]
565 fn test_volume_infinity_becomes_large_number() {
566 let p = VolumeExt::default(); let v = khr_materials_volume(&p);
568 let dist = v["attenuationDistance"].as_f64().expect("should succeed");
569 assert!(dist > 1e300, "expected very large number, got {dist}");
570 }
571
572 #[test]
576 fn test_ior_default() {
577 let v = khr_materials_ior(1.5);
578 assert!((v["ior"].as_f64().expect("should succeed") - 1.5).abs() < 1e-6);
579 }
580
581 #[test]
585 fn test_specular_color_components() {
586 let p = SpecularExt::default();
587 let v = khr_materials_specular(&p);
588 let arr = v["specularColorFactor"].as_array().expect("should succeed");
589 assert_eq!(arr.len(), 3);
590 assert!((arr[0].as_f64().expect("should succeed") - 1.0).abs() < 1e-6);
591 }
592
593 #[test]
597 fn test_skin_preset_to_json() {
598 let mat = GltfMaterialDef::skin();
599 let j = mat.to_json();
600 assert_eq!(j["name"].as_str().expect("should succeed"), "skin");
601 assert!(j["pbrMetallicRoughness"].is_object());
602 assert!(j["doubleSided"].as_bool().expect("should succeed"));
603 assert_eq!(j["alphaMode"].as_str().expect("should succeed"), "OPAQUE");
604 }
605
606 #[test]
610 fn test_glass_preset_has_extensions() {
611 let mat = GltfMaterialDef::glass();
612 let names = mat.extension_names();
613 assert!(names.contains(&"KHR_materials_transmission"));
614 assert!(names.contains(&"KHR_materials_volume"));
615 assert!(names.contains(&"KHR_materials_ior"));
616 let j = mat.to_json();
617 assert!(j["extensions"].is_object());
618 }
619
620 #[test]
624 fn test_alpha_mask_cutoff_in_json() {
625 let mat = GltfMaterialDef {
626 alpha_mode: AlphaMode::Mask(0.5),
627 ..Default::default()
628 };
629 let j = mat.to_json();
630 assert_eq!(j["alphaMode"].as_str().expect("should succeed"), "MASK");
631 assert!((j["alphaCutoff"].as_f64().expect("should succeed") - 0.5).abs() < 1e-6);
632 }
633
634 #[test]
638 fn test_build_materials_json_length() {
639 let mats = vec![
640 GltfMaterialDef::skin(),
641 GltfMaterialDef::cloth(),
642 GltfMaterialDef::glass(),
643 GltfMaterialDef::metallic(),
644 ];
645 let j = build_materials_json(&mats);
646 assert_eq!(j.as_array().expect("should succeed").len(), 4);
647 }
648
649 #[test]
653 fn test_validate_material_json() {
654 let good = GltfMaterialDef::skin().to_json();
655 assert!(validate_material_json(&good).is_ok());
656
657 let bad = json!({ "name": "no_pbr" });
658 assert!(validate_material_json(&bad).is_err());
659
660 let out_of_range = json!({
661 "name": "bad",
662 "pbrMetallicRoughness": {
663 "metallicFactor": 2.5
664 }
665 });
666 assert!(validate_material_json(&out_of_range).is_err());
667 }
668
669 #[test]
673 fn test_extract_extensions_used() {
674 let gltf = json!({
675 "extensionsUsed": [
676 "KHR_materials_unlit",
677 "KHR_materials_transmission"
678 ]
679 });
680 let list = extract_extensions_used(&gltf);
681 assert_eq!(list.len(), 2);
682 assert!(list.contains(&"KHR_materials_unlit".to_string()));
683 }
684
685 #[test]
689 fn test_with_extension_dedup() {
690 let mat = GltfMaterialDef::default()
691 .with_extension("KHR_materials_unlit", khr_materials_unlit())
692 .with_extension("KHR_materials_ior", khr_materials_ior(1.5))
693 .with_extension("KHR_materials_ior", khr_materials_ior(1.8));
695
696 assert_eq!(
697 mat.extensions.len(),
698 2,
699 "duplicate extension should be replaced"
700 );
701 let ior_val = mat
702 .extensions
703 .iter()
704 .find(|(n, _)| n == "KHR_materials_ior")
705 .map(|(_, v)| v["ior"].as_f64().expect("should succeed"))
706 .expect("should succeed");
707 assert!((ior_val - 1.8).abs() < 1e-6);
708 }
709
710 #[test]
714 fn test_write_materials_to_tmp() {
715 let mats = vec![GltfMaterialDef::skin(), GltfMaterialDef::glass()];
716 let j = build_materials_json(&mats);
717 let path = "/tmp/oxihuman_gltf_ext_test_materials.json";
718 let s = serde_json::to_string_pretty(&j).expect("should succeed");
719 fs::write(path, &s).expect("should succeed");
720 let raw = fs::read_to_string(path).expect("should succeed");
721 let parsed: Value = serde_json::from_str(&raw).expect("should succeed");
722 assert_eq!(parsed.as_array().expect("should succeed").len(), 2);
723 }
724
725 #[test]
729 fn test_cloth_preset_sheen_extension() {
730 let mat = GltfMaterialDef::cloth();
731 assert!(mat.extension_names().contains(&"KHR_materials_sheen"));
732 }
733
734 #[test]
738 fn test_extract_extensions_used_missing() {
739 let gltf = json!({ "asset": { "version": "2.0" } });
740 let list = extract_extensions_used(&gltf);
741 assert!(list.is_empty());
742 }
743}