1#![allow(dead_code)]
5#![allow(non_snake_case)]
6
7use std::fs;
8use std::io::{BufRead, BufReader};
9use std::path::Path;
10
11use anyhow::Context;
12use oxihuman_mesh::MeshBuffers;
13
14pub struct UsdExportOptions {
16 pub prim_name: String,
17 pub root_name: String,
18 pub up_axis: String,
19 pub meters_per_unit: f32,
20 pub include_normals: bool,
21 pub include_uvs: bool,
22 pub include_displayColor: bool,
23}
24
25impl Default for UsdExportOptions {
26 fn default() -> Self {
27 Self {
28 prim_name: "Body".to_string(),
29 root_name: "Root".to_string(),
30 up_axis: "Y".to_string(),
31 meters_per_unit: 1.0,
32 include_normals: true,
33 include_uvs: true,
34 include_displayColor: false,
35 }
36 }
37}
38
39pub struct UsdExportStats {
41 pub vertex_count: usize,
42 pub face_count: usize,
43 pub file_size_bytes: usize,
44 pub has_normals: bool,
45 pub has_uvs: bool,
46}
47
48pub fn format_float3_array(data: &[[f32; 3]]) -> String {
52 let inner: Vec<String> = data
53 .iter()
54 .map(|v| format!("({:.6}, {:.6}, {:.6})", v[0], v[1], v[2]))
55 .collect();
56 format!("[{}]", inner.join(", "))
57}
58
59pub fn format_float2_array(data: &[[f32; 2]]) -> String {
61 let inner: Vec<String> = data
62 .iter()
63 .map(|v| format!("({:.6}, {:.6})", v[0], v[1]))
64 .collect();
65 format!("[{}]", inner.join(", "))
66}
67
68pub fn format_int_array(data: &[u32]) -> String {
70 let inner: Vec<String> = data.iter().map(|i| i.to_string()).collect();
71 format!("[{}]", inner.join(", "))
72}
73
74pub fn build_usda(mesh: &MeshBuffers, opts: &UsdExportOptions) -> String {
78 let face_count = mesh.indices.len() / 3;
79
80 let mut out = format!(
82 "#usda 1.0\n(\n defaultPrim = \"{root}\"\n upAxis = \"{up}\"\n metersPerUnit = {mpu}\n)\n\n",
83 root = opts.root_name,
84 up = opts.up_axis,
85 mpu = opts.meters_per_unit,
86 );
87
88 out.push_str(&format!(
90 "def Xform \"{root}\"\n{{\n def Mesh \"{prim}\"\n {{\n",
91 root = opts.root_name,
92 prim = opts.prim_name,
93 ));
94
95 out.push_str(&format!(
97 " float3[] points = {}\n",
98 format_float3_array(&mesh.positions)
99 ));
100
101 let counts: Vec<u32> = vec![3u32; face_count];
103 out.push_str(&format!(
104 " int[] faceVertexCounts = {}\n",
105 format_int_array(&counts)
106 ));
107
108 out.push_str(&format!(
110 " int[] faceVertexIndices = {}\n",
111 format_int_array(&mesh.indices)
112 ));
113
114 if opts.include_normals && !mesh.normals.is_empty() {
116 out.push_str(&format!(
117 " normal3f[] normals = {}\n",
118 format_float3_array(&mesh.normals)
119 ));
120 }
121
122 if opts.include_uvs && !mesh.uvs.is_empty() {
124 out.push_str(&format!(
125 " texCoord2f[] primvars:st = {}\n",
126 format_float2_array(&mesh.uvs)
127 ));
128 out.push_str(" uniform token[] primvars:st:indices = None\n");
129 }
130
131 out.push_str(" double3 xformOp:translate = (0, 0, 0)\n");
133 out.push_str(" uniform token[] xformOpOrder = [\"xformOp:translate\"]\n");
134
135 out.push_str(" }\n}\n");
137
138 out
139}
140
141pub fn export_usda(
145 mesh: &MeshBuffers,
146 path: &Path,
147 opts: &UsdExportOptions,
148) -> anyhow::Result<UsdExportStats> {
149 let content = build_usda(mesh, opts);
150 let bytes = content.as_bytes();
151
152 if let Some(parent) = path.parent() {
153 if !parent.as_os_str().is_empty() {
154 fs::create_dir_all(parent).with_context(|| format!("create dir {:?}", parent))?;
155 }
156 }
157
158 fs::write(path, bytes).with_context(|| format!("write {:?}", path))?;
159
160 Ok(UsdExportStats {
161 vertex_count: mesh.positions.len(),
162 face_count: mesh.indices.len() / 3,
163 file_size_bytes: bytes.len(),
164 has_normals: opts.include_normals && !mesh.normals.is_empty(),
165 has_uvs: opts.include_uvs && !mesh.uvs.is_empty(),
166 })
167}
168
169pub fn export_usda_scene(
171 meshes: &[(&MeshBuffers, &str)],
172 path: &Path,
173 opts: &UsdExportOptions,
174) -> anyhow::Result<()> {
175 let mut out = format!(
176 "#usda 1.0\n(\n defaultPrim = \"{root}\"\n upAxis = \"{up}\"\n metersPerUnit = {mpu}\n)\n\n",
177 root = opts.root_name,
178 up = opts.up_axis,
179 mpu = opts.meters_per_unit,
180 );
181
182 out.push_str(&format!(
183 "def Xform \"{root}\"\n{{\n",
184 root = opts.root_name
185 ));
186
187 for (mesh, name) in meshes {
188 let face_count = mesh.indices.len() / 3;
189 out.push_str(&format!(" def Mesh \"{name}\"\n {{\n"));
190
191 out.push_str(&format!(
192 " float3[] points = {}\n",
193 format_float3_array(&mesh.positions)
194 ));
195
196 let counts: Vec<u32> = vec![3u32; face_count];
197 out.push_str(&format!(
198 " int[] faceVertexCounts = {}\n",
199 format_int_array(&counts)
200 ));
201
202 out.push_str(&format!(
203 " int[] faceVertexIndices = {}\n",
204 format_int_array(&mesh.indices)
205 ));
206
207 if opts.include_normals && !mesh.normals.is_empty() {
208 out.push_str(&format!(
209 " normal3f[] normals = {}\n",
210 format_float3_array(&mesh.normals)
211 ));
212 }
213
214 if opts.include_uvs && !mesh.uvs.is_empty() {
215 out.push_str(&format!(
216 " texCoord2f[] primvars:st = {}\n",
217 format_float2_array(&mesh.uvs)
218 ));
219 out.push_str(" uniform token[] primvars:st:indices = None\n");
220 }
221
222 out.push_str(" double3 xformOp:translate = (0, 0, 0)\n");
223 out.push_str(" uniform token[] xformOpOrder = [\"xformOp:translate\"]\n");
224 out.push_str(" }\n");
225 }
226
227 out.push_str("}\n");
228
229 if let Some(parent) = path.parent() {
230 if !parent.as_os_str().is_empty() {
231 fs::create_dir_all(parent).with_context(|| format!("create dir {:?}", parent))?;
232 }
233 }
234
235 fs::write(path, out.as_bytes()).with_context(|| format!("write {:?}", path))?;
236 Ok(())
237}
238
239pub fn validate_usda(path: &Path) -> anyhow::Result<bool> {
241 let file = fs::File::open(path).with_context(|| format!("open {:?}", path))?;
242 let mut reader = BufReader::new(file);
243 let mut first_line = String::new();
244 reader
245 .read_line(&mut first_line)
246 .with_context(|| "read first line")?;
247 Ok(first_line.trim() == "#usda 1.0")
248}
249
250#[cfg(test)]
253mod tests {
254 use super::*;
255 use oxihuman_mesh::MeshBuffers;
256
257 fn two_tri_mesh() -> MeshBuffers {
259 MeshBuffers {
260 positions: vec![
261 [0.0, 0.0, 0.0],
262 [1.0, 0.0, 0.0],
263 [1.0, 1.0, 0.0],
264 [0.0, 1.0, 0.0],
265 ],
266 normals: vec![
267 [0.0, 0.0, 1.0],
268 [0.0, 0.0, 1.0],
269 [0.0, 0.0, 1.0],
270 [0.0, 0.0, 1.0],
271 ],
272 tangents: vec![[1.0, 0.0, 0.0, 1.0]; 4],
273 uvs: vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
274 indices: vec![0, 1, 2, 0, 2, 3],
275 colors: None,
276 has_suit: true,
277 }
278 }
279
280 #[test]
281 fn test_build_usda_header() {
282 let mesh = two_tri_mesh();
283 let opts = UsdExportOptions::default();
284 let usda = build_usda(&mesh, &opts);
285 assert!(usda.starts_with("#usda 1.0"), "must start with #usda 1.0");
286 assert!(usda.contains("defaultPrim = \"Root\""));
287 assert!(usda.contains("upAxis = \"Y\""));
288 assert!(usda.contains("metersPerUnit = 1"));
289 }
290
291 #[test]
292 fn test_build_usda_has_points() {
293 let mesh = two_tri_mesh();
294 let opts = UsdExportOptions::default();
295 let usda = build_usda(&mesh, &opts);
296 assert!(
297 usda.contains("float3[] points"),
298 "must contain points array"
299 );
300 assert!(usda.contains("(0.000000, 0.000000, 0.000000)"));
301 assert!(usda.contains("(1.000000, 0.000000, 0.000000)"));
302 }
303
304 #[test]
305 fn test_build_usda_has_face_counts() {
306 let mesh = two_tri_mesh();
307 let opts = UsdExportOptions::default();
308 let usda = build_usda(&mesh, &opts);
309 assert!(
310 usda.contains("int[] faceVertexCounts"),
311 "must contain faceVertexCounts"
312 );
313 assert!(usda.contains("[3, 3]"));
315 }
316
317 #[test]
318 fn test_build_usda_has_indices() {
319 let mesh = two_tri_mesh();
320 let opts = UsdExportOptions::default();
321 let usda = build_usda(&mesh, &opts);
322 assert!(
323 usda.contains("int[] faceVertexIndices"),
324 "must contain faceVertexIndices"
325 );
326 assert!(usda.contains("[0, 1, 2, 0, 2, 3]"));
327 }
328
329 #[test]
330 fn test_build_usda_with_normals() {
331 let mesh = two_tri_mesh();
332 let opts = UsdExportOptions {
333 include_normals: true,
334 ..Default::default()
335 };
336 let usda = build_usda(&mesh, &opts);
337 assert!(usda.contains("normal3f[] normals"), "must contain normals");
338 assert!(usda.contains("(0.000000, 0.000000, 1.000000)"));
339 }
340
341 #[test]
342 fn test_build_usda_with_uvs() {
343 let mesh = two_tri_mesh();
344 let opts = UsdExportOptions {
345 include_uvs: true,
346 ..Default::default()
347 };
348 let usda = build_usda(&mesh, &opts);
349 assert!(
350 usda.contains("texCoord2f[] primvars:st"),
351 "must contain UVs"
352 );
353 assert!(usda.contains("primvars:st:indices"));
354 }
355
356 #[test]
357 fn test_format_float3_array() {
358 let data: Vec<[f32; 3]> = vec![[1.0, 2.0, 3.0], [4.5, 5.5, 6.5]];
359 let result = format_float3_array(&data);
360 assert_eq!(
361 result,
362 "[(1.000000, 2.000000, 3.000000), (4.500000, 5.500000, 6.500000)]"
363 );
364 }
365
366 #[test]
367 fn test_format_float2_array() {
368 let data: Vec<[f32; 2]> = vec![[0.0, 1.0], [0.5, 0.5]];
369 let result = format_float2_array(&data);
370 assert_eq!(result, "[(0.000000, 1.000000), (0.500000, 0.500000)]");
371 }
372
373 #[test]
374 fn test_format_int_array() {
375 let data: Vec<u32> = vec![0, 1, 2, 3];
376 let result = format_int_array(&data);
377 assert_eq!(result, "[0, 1, 2, 3]");
378 }
379
380 #[test]
381 fn test_export_usda_to_file() {
382 let mesh = two_tri_mesh();
383 let opts = UsdExportOptions::default();
384 let path = std::path::PathBuf::from("/tmp/test_export.usda");
385 let stats = export_usda(&mesh, &path, &opts).expect("export_usda should succeed");
386 assert_eq!(stats.vertex_count, 4);
387 assert_eq!(stats.face_count, 2);
388 assert!(stats.file_size_bytes > 0);
389 assert!(stats.has_normals);
390 assert!(stats.has_uvs);
391 assert!(path.exists());
392 }
393
394 #[test]
395 fn test_validate_usda_valid() {
396 let mesh = two_tri_mesh();
397 let opts = UsdExportOptions::default();
398 let path = std::path::PathBuf::from("/tmp/test_validate_valid.usda");
399 export_usda(&mesh, &path, &opts).expect("export_usda should succeed");
400 let valid = validate_usda(&path).expect("validate_usda should succeed");
401 assert!(valid, "exported file should be valid USDA");
402 }
403
404 #[test]
405 fn test_validate_usda_invalid() {
406 let path = std::path::PathBuf::from("/tmp/test_validate_invalid.usda");
407 fs::write(&path, b"not a usda file\nsome content\n").expect("write temp file");
408 let valid = validate_usda(&path).expect("validate_usda should succeed");
409 assert!(!valid, "file without #usda 1.0 header should be invalid");
410 }
411
412 #[test]
413 fn test_export_usda_scene() {
414 let mesh1 = two_tri_mesh();
415 let mesh2 = two_tri_mesh();
416 let opts = UsdExportOptions::default();
417 let path = std::path::PathBuf::from("/tmp/test_scene.usda");
418 let meshes: Vec<(&MeshBuffers, &str)> = vec![(&mesh1, "Body"), (&mesh2, "Hair")];
419 export_usda_scene(&meshes, &path, &opts).expect("export_usda_scene should succeed");
420 let content = fs::read_to_string(&path).expect("read scene file");
421 assert!(content.starts_with("#usda 1.0"));
422 assert!(content.contains("def Mesh \"Body\""));
423 assert!(content.contains("def Mesh \"Hair\""));
424 assert!(path.exists());
425 }
426}