1use anyhow::Result;
7use oxihuman_mesh::mesh::MeshBuffers;
8use std::fmt::Write as FmtWrite;
9use std::path::Path;
10
11pub fn export_obj(mesh: &MeshBuffers, path: &Path) -> Result<()> {
14 let content = mesh_to_obj_string(mesh)?;
15 std::fs::write(path, content)?;
16 Ok(())
17}
18
19pub fn mesh_to_obj_string(mesh: &MeshBuffers) -> Result<String> {
21 let mut out = String::new();
22 writeln!(out, "# OxiHuman exported mesh")?;
23 writeln!(out, "# Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)")?;
24 writeln!(out, "# Vertices: {}", mesh.positions.len())?;
25 writeln!(out, "# Faces: {}", mesh.indices.len() / 3)?;
26 writeln!(out)?;
27
28 for p in &mesh.positions {
30 writeln!(out, "v {:.6} {:.6} {:.6}", p[0], p[1], p[2])?;
31 }
32 writeln!(out)?;
33
34 for uv in &mesh.uvs {
36 writeln!(out, "vt {:.6} {:.6}", uv[0], uv[1])?;
37 }
38 writeln!(out)?;
39
40 for n in &mesh.normals {
42 writeln!(out, "vn {:.6} {:.6} {:.6}", n[0], n[1], n[2])?;
43 }
44 writeln!(out)?;
45
46 let has_uvs = !mesh.uvs.is_empty();
48 let has_norms = !mesh.normals.is_empty();
49 for tri in mesh.indices.chunks_exact(3) {
50 let (i0, i1, i2) = (tri[0] + 1, tri[1] + 1, tri[2] + 1); if has_uvs && has_norms {
52 writeln!(out, "f {0}/{0}/{0} {1}/{1}/{1} {2}/{2}/{2}", i0, i1, i2)?;
53 } else if has_uvs {
54 writeln!(out, "f {0}/{0} {1}/{1} {2}/{2}", i0, i1, i2)?;
55 } else {
56 writeln!(out, "f {} {} {}", i0, i1, i2)?;
57 }
58 }
59
60 Ok(out)
61}
62
63#[cfg(test)]
64mod tests {
65 use super::*;
66 use oxihuman_mesh::mesh::MeshBuffers;
67 use oxihuman_morph::engine::MeshBuffers as MB;
68
69 fn triangle_mesh() -> MeshBuffers {
70 MeshBuffers::from_morph(MB {
71 positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
72 normals: vec![[0.0, 0.0, 1.0]; 3],
73 uvs: vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
74 indices: vec![0, 1, 2],
75 has_suit: false,
76 })
77 }
78
79 #[test]
80 fn obj_string_has_vertex_lines() {
81 let m = triangle_mesh();
82 let s = mesh_to_obj_string(&m).expect("should succeed");
83 assert!(s.contains("v 0.000000 0.000000 0.000000"));
84 assert!(s.contains("v 1.000000 0.000000 0.000000"));
85 let v_count = s.lines().filter(|l| l.starts_with("v ")).count();
86 assert_eq!(v_count, 3);
87 }
88
89 #[test]
90 fn obj_string_has_face_lines() {
91 let m = triangle_mesh();
92 let s = mesh_to_obj_string(&m).expect("should succeed");
93 let f_count = s.lines().filter(|l| l.starts_with("f ")).count();
94 assert_eq!(f_count, 1);
95 }
96
97 #[test]
98 fn export_obj_creates_file() {
99 let m = triangle_mesh();
100 let path = std::path::PathBuf::from("/tmp/test_oxihuman.obj");
101 export_obj(&m, &path).expect("should succeed");
102 assert!(path.exists());
103 let content = std::fs::read_to_string(&path).expect("should succeed");
104 assert!(content.contains("v "));
105 std::fs::remove_file(&path).ok();
106 }
107
108 #[test]
109 fn obj_indices_are_one_based() {
110 let m = triangle_mesh();
111 let s = mesh_to_obj_string(&m).expect("should succeed");
112 assert!(s.contains("f 1/1/1 2/2/2 3/3/3"));
114 }
115}