diff --git a/README.md b/README.md index 330b040..c331f68 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,12 @@ Read/write meshes and point clouds from some common formats. - `read_mesh(filename)` Reads a mesh from file. Returns numpy matrices `V, F`, a Nx3 real numpy array of vertices and a Mx3 integer numpy array of 0-based face indices (or Mx4 for a quad mesh, etc). - `filename` the path to read the file from. Currently supports the same file types as [geometry-central](http://geometry-central.net/surface/utilities/io/#supported-file-types). The file type is inferred automatically from the path extension. -- `write_mesh(V, F, filename)` Write a mesh to file. +- `write_mesh(V, F, filename, UV_coords=None, UV_type=None)` Write a mesh to file, optionally with UV coords. - `V` a Nx3 real numpy array of vertices - `F` a Mx3 integer numpy array of faces, with 0-based vertex indices (or Mx4 for a quad mesh, etc). - `filename` the path to write the file to. Currently supports the same file types as [geometry-central](http://geometry-central.net/surface/utilities/io/#supported-file-types). The file type is inferred automatically from the path extension. + - `UV_coords` (optional) a Ux2 numpy array of UV coords, interpreted based on UV_type. *Warning:* this function does not currently preserve shared UV indices when writing, each written coordinate is independent + - `UV_type` (optional) string, one of `'per-vertex'`, `'per-face'`, or `'per-corner'`. The size of `U` should be `N`, `M`, or `M*3/4`, respectively - `read_point_cloud(filename)` Reads a point cloud from file. Returns numpy matrix `V`, a Nx3 real numpy array of vertices. Really, this just reads a mesh file and ignores the face entries. - `filename` the path to read the file from. Currently supports the same file types as [geometry-central](http://geometry-central.net/surface/utilities/io/#supported-file-types)'s mesh reader. The file type is inferred automatically from the path extension. diff --git a/src/cpp/io.cpp b/src/cpp/io.cpp index 9fa2236..61ea105 100644 --- a/src/cpp/io.cpp +++ b/src/cpp/io.cpp @@ -57,9 +57,9 @@ std::tuple, DenseMatrix> read_mesh(std::string file return std::make_tuple(V, F); } -void write_mesh(DenseMatrix verts, DenseMatrix faces, std::string filename) { - - // Copy in to the mesh object +namespace { // anonymous helers +SimplePolygonMesh buildMesh(const DenseMatrix& verts, const DenseMatrix& faces, + const DenseMatrix& corner_UVs) { std::vector coords(verts.rows()); for (size_t i = 0; i < verts.rows(); i++) { for (size_t j = 0; j < 3; j++) { @@ -73,10 +73,78 @@ void write_mesh(DenseMatrix verts, DenseMatrix faces, std::stri polys[i][j] = faces(i, j); } } + std::vector> corner_params; + if (corner_UVs.size() > 0) { + corner_params.resize(faces.rows()); + for (size_t i = 0; i < faces.rows(); i++) { + corner_params[i].resize(faces.cols()); + for (size_t j = 0; j < faces.cols(); j++) { + size_t ind = i * faces.cols() + j; + for (size_t k = 0; k < 2; k++) { + corner_params[i][j][k] = corner_UVs(ind, k); + } + } + } + } + + return SimplePolygonMesh(polys, coords, corner_params); +} +SimplePolygonMesh buildMesh(const DenseMatrix& verts, const DenseMatrix& faces) { + DenseMatrix empty_UVs = DenseMatrix::Zero(0, 2); + return buildMesh(verts, faces, empty_UVs); +} +} // namespace - SimplePolygonMesh pmesh(polys, coords); +void write_mesh(DenseMatrix verts, DenseMatrix faces, std::string filename) { + SimplePolygonMesh pmesh = buildMesh(verts, faces); + pmesh.writeMesh(filename); +} + +void write_mesh_pervertex_uv(DenseMatrix verts, DenseMatrix faces, DenseMatrix UVs, + std::string filename) { + size_t V = verts.rows(); + size_t F = faces.rows(); + size_t D = faces.cols(); + + // expand out to per-corner UVs + DenseMatrix face_UVs = DenseMatrix::Zero(F * D, 2); + for (size_t i = 0; i < F; i++) { + for (size_t j = 0; j < D; j++) { + size_t vInd = faces(i, j); + for (size_t k = 0; k < 2; k++) { + face_UVs(i * D + j, k) = UVs(vInd, k); + } + } + } - // Call the mesh writer + SimplePolygonMesh pmesh = buildMesh(verts, faces, face_UVs); + pmesh.writeMesh(filename); +} + +void write_mesh_perface_uv(DenseMatrix verts, DenseMatrix faces, DenseMatrix UVs, + std::string filename) { + + size_t V = verts.rows(); + size_t F = faces.rows(); + size_t D = faces.cols(); + + // expand out to per-corner UVs + DenseMatrix face_UVs = DenseMatrix::Zero(F * D, 2); + for (size_t i = 0; i < F; i++) { + for (size_t j = 0; j < D; j++) { + for (size_t k = 0; k < 2; k++) { + face_UVs(i * D + j, k) = UVs(i, k); + } + } + } + + SimplePolygonMesh pmesh = buildMesh(verts, faces, face_UVs); + pmesh.writeMesh(filename); +} + +void write_mesh_percorner_uv(DenseMatrix verts, DenseMatrix faces, DenseMatrix UVs, + std::string filename) { + SimplePolygonMesh pmesh = buildMesh(verts, faces, UVs); pmesh.writeMesh(filename); } @@ -110,7 +178,11 @@ void write_point_cloud(DenseMatrix points, std::string filename) { void bind_io(py::module& m) { m.def("read_mesh", &read_mesh, "Read a mesh from file.", py::arg("filename")); + m.def("write_mesh", &write_mesh, "Write a mesh to file.", py::arg("verts"), py::arg("faces"), py::arg("filename")); + m.def("write_mesh_pervertex_uv", &write_mesh_pervertex_uv, "Write a mesh to file.", py::arg("verts"), py::arg("faces"), py::arg("UVs"), py::arg("filename")); + m.def("write_mesh_perface_uv", &write_mesh_perface_uv, "Write a mesh to file.", py::arg("verts"), py::arg("faces"), py::arg("UVs"), py::arg("filename")); + m.def("write_mesh_percorner_uv", &write_mesh_percorner_uv, "Write a mesh to file.", py::arg("verts"), py::arg("faces"), py::arg("UVs"), py::arg("filename")); m.def("read_point_cloud", &read_point_cloud, "Read a point cloud from file.", py::arg("filename")); m.def("write_point_cloud", &write_point_cloud, "Write a point cloud to file.", py::arg("points"), py::arg("filename")); diff --git a/src/potpourri3d/io.py b/src/potpourri3d/io.py index 4e253b4..4f13780 100644 --- a/src/potpourri3d/io.py +++ b/src/potpourri3d/io.py @@ -9,9 +9,40 @@ def read_mesh(filename): F = np.ascontiguousarray(F) return V, F -def write_mesh(V, F, filename): +def write_mesh(V, F, filename, UV_coords=None, UV_type=None): + # TODO generalize this to take indexed UVs + # (the underlying geometry-central writer needs to support it first) + validate_mesh(V, F, test_indices=True) - pp3db.write_mesh(V, F, filename) + + if UV_type is None: + + pp3db.write_mesh(V, F, filename) + + elif UV_type == 'per-vertex': + + if len(UV_coords.shape) != 2 or UV_coords.shape[0] != V.shape[0] or UV_coords.shape[1] != 2: + raise ValueError("UV_coords should be a 2d Vx2 numpy array") + + pp3db.write_mesh_pervertex_uv(V, F, UV_coords, filename) + + elif UV_type == 'per-face': + + if len(UV_coords.shape) != 2 or UV_coords.shape[0] != F.shape[0] or UV_coords.shape[1] != 2: + raise ValueError("UV_coords should be a 2d Fx2 numpy array") + + pp3db.write_mesh_perface_uv(V, F, UV_coords, filename) + + elif UV_type == 'per-corner': + + if len(UV_coords.shape) != 2 or UV_coords.shape[0] != F.shape[0]*F.shape[1] or UV_coords.shape[1] != 2: + raise ValueError("UV_coords should be a 2d Fx2 numpy array") + + pp3db.write_mesh_percorner_uv(V, F, UV_coords, filename) + + else: + raise ValueError(f"unrecognized value for UV_type: {UV_type}. Should be one of: [None, 'per-vertex', 'per-face', 'per-corner']") + def read_point_cloud(filename): V = pp3db.read_point_cloud(filename) diff --git a/test/potpourri3d_test.py b/test/potpourri3d_test.py index cd10da3..940f9e4 100644 --- a/test/potpourri3d_test.py +++ b/test/potpourri3d_test.py @@ -56,6 +56,17 @@ def test_write_read_mesh(self): self.assertLess(np.amax(np.abs(V-Vnew)), 1e-6) self.assertTrue((F==Fnew).all()) + + + # smoke test various UV writers + UV_vert = V[:,:2] + pp3d.write_mesh(V,F,fname,UV_coords=UV_vert, UV_type='per-vertex') + + UV_face = F[:,:2] * .3 + pp3d.write_mesh(V,F,fname,UV_coords=UV_face, UV_type='per-face') + + UV_corner = np.zeros((F.shape[0]*F.shape[1],2)) + pp3d.write_mesh(V,F,fname,UV_coords=UV_corner, UV_type='per-corner') def test_write_read_point_cloud(self):