1use crate::assets::Assets;
11
12pub const ORM_PREFIX: &str = "orm:";
14
15#[derive(Clone, Debug, Default, PartialEq)]
17pub struct PbrSet {
18 pub albedo: String,
20 pub normal: String,
22 pub orm: String,
24}
25
26pub fn orm_key(ao: &str, rough: &str, metal: &str, smooth: &str) -> String {
29 if ao.is_empty() && rough.is_empty() && metal.is_empty() && smooth.is_empty() {
30 return String::new();
31 }
32 format!("{ORM_PREFIX}ao={ao}|rough={rough}|metal={metal}|smooth={smooth}")
33}
34
35pub fn parse_orm_key(key: &str) -> Option<[String; 4]> {
37 let body = key.strip_prefix(ORM_PREFIX)?;
38 let mut out: [String; 4] = Default::default();
39 for part in body.split('|') {
40 let (name, value) = part.split_once('=')?;
41 match name {
42 "ao" => out[0] = value.into(),
43 "rough" => out[1] = value.into(),
44 "metal" => out[2] = value.into(),
45 "smooth" => out[3] = value.into(),
46 _ => return None,
47 }
48 }
49 Some(out)
50}
51
52fn lower(s: &str) -> String {
53 s.to_ascii_lowercase()
54}
55
56pub fn discover(assets: &Assets, path: &str) -> PbrSet {
58 let (dir, file) = path.rsplit_once('/').map_or(("", path), |(d, f)| (d, f));
59 let stem = file.rsplit_once('.').map_or(file, |(s, _)| s);
60 let stem = [
62 "_albedotransparency",
63 "_basecolor",
64 "_albedo",
65 "_diffuse",
66 "_normal",
67 "_metallicsmoothness",
68 "_roughness",
69 "_metallic",
70 "_ambientocclusion",
71 "_ao",
72 "_height",
73 ]
74 .iter()
75 .find_map(|suffix| lower(stem).strip_suffix(suffix).map(|s| stem[..s.len()].to_string()))
76 .unwrap_or_else(|| stem.to_string());
77 let stem_l = lower(&stem);
78 let siblings: Vec<String> = assets.list().into_iter().filter(|p| p.rsplit_once('/').map_or("", |(d, _)| d) == dir).collect();
79 let find = |suffixes: &[&str]| -> String {
80 for s in suffixes {
81 let want = format!("{stem_l}{s}");
82 let hit = siblings.iter().find(|p| {
83 let f = lower(p.rsplit('/').next().unwrap_or(p));
84 f.rsplit_once('.').is_some_and(|(name, ext)| name == want && matches!(ext, "png" | "jpg" | "jpeg"))
85 });
86 if let Some(h) = hit {
87 return h.clone();
88 }
89 }
90 String::new()
91 };
92 let albedo = find(&["_albedotransparency", "_basecolor", "_albedo", "_diffuse"]);
93 let normal = find(&["_normal", "_normalgl"]);
94 let (smooth, rough, metal, ao) =
95 (find(&["_metallicsmoothness"]), find(&["_roughness"]), find(&["_metallic"]), find(&["_ambientocclusion", "_ao"]));
96 PbrSet { albedo, normal, orm: orm_key(&ao, &rough, &metal, &smooth) }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 #[test]
104 fn orm_keys_roundtrip() {
105 let key = orm_key("a b/ao.png", "r.png", "", "");
106 assert_eq!(parse_orm_key(&key), Some(["a b/ao.png".into(), "r.png".into(), "".into(), "".into()]));
107 assert_eq!(orm_key("", "", "", ""), "");
108 }
109
110 #[test]
111 fn discovers_unity_and_substance_sets() {
112 let dir = std::env::temp_dir().join(format!("kvas_pbr_{}", std::process::id()));
113 std::fs::create_dir_all(dir.join("m")).unwrap();
114 for f in [
115 "trn_Barrel.fbx",
116 "trn_Barrel_AlbedoTransparency.png",
117 "trn_Barrel_Normal.png",
118 "trn_Barrel_MetallicSmoothness.png",
119 "Brick_BaseColor.png",
120 "Brick_Roughness.png",
121 "Brick_AmbientOcclusion.png",
122 "Brick_Normal.png",
123 ] {
124 std::fs::write(dir.join("m").join(f), b"x").unwrap();
125 }
126 let assets = Assets::Dir(dir.clone());
127 let unity = discover(&assets, "m/trn_Barrel.fbx");
128 assert_eq!((unity.albedo.as_str(), unity.normal.as_str()), ("m/trn_Barrel_AlbedoTransparency.png", "m/trn_Barrel_Normal.png"));
129 assert!(unity.orm.contains("smooth=m/trn_Barrel_MetallicSmoothness.png"));
130 let sub = discover(&assets, "m/Brick_Normal.png");
131 assert_eq!(sub.albedo, "m/Brick_BaseColor.png");
132 assert!(sub.orm.contains("rough=m/Brick_Roughness.png") && sub.orm.contains("ao=m/Brick_AmbientOcclusion.png"));
133 std::fs::remove_dir_all(dir).ok();
134 }
135}