diff --git a/bindings/pydrake/geometry_py.cc b/bindings/pydrake/geometry_py.cc index 0f9cfd5325e6..ea8b93218798 100644 --- a/bindings/pydrake/geometry_py.cc +++ b/bindings/pydrake/geometry_py.cc @@ -1283,11 +1283,14 @@ void DoScalarIndependentDefinitions(py::module m) { py_rvp::reference_internal, py::arg("diffuse"), doc.MakePhongIllustrationProperties.doc); - m.def("ReadObjToSurfaceMesh", - py::overload_cast( - &geometry::ReadObjToSurfaceMesh), + m.def( + "ReadObjToSurfaceMesh", + [](const std::string& filename, double scale) { + return geometry::ReadObjToSurfaceMesh(filename, scale); + }, py::arg("filename"), py::arg("scale") = 1.0, - doc.ReadObjToSurfaceMesh.doc_2args_filename_scale); + // N.B. We have not bound the optional "on_warning" argument. + doc.ReadObjToSurfaceMesh.doc_3args_filename_scale_on_warning); } void def_geometry(py::module m) { diff --git a/geometry/proximity/obj_to_surface_mesh.cc b/geometry/proximity/obj_to_surface_mesh.cc index cff83a1bb24c..43f09415590c 100644 --- a/geometry/proximity/obj_to_surface_mesh.cc +++ b/geometry/proximity/obj_to_surface_mesh.cc @@ -2,7 +2,9 @@ #include #include +#include #include +#include #include #include #include @@ -12,6 +14,7 @@ #include #include "drake/common/drake_assert.h" +#include "drake/common/filesystem.h" #include "drake/common/text_logging.h" #include "drake/geometry/proximity/surface_mesh.h" @@ -23,12 +26,6 @@ namespace { // TODO(DamrongGuoy): Refactor the tinyobj usage between here and // ProximityEngine. -// TODO(DamrongGuoy): Remove the guard DRAKE_DOXYGEN_CXX when we fixed -// issue#11130 "doxygen: Do not emit for `*.cc` files, also ignore -// `internal` namespace when appropriate". - -#ifndef DRAKE_DOXYGEN_CXX - /* Converts vertices of tinyobj to vertices of SurfaceMesh. @param tinyobj_vertices @@ -111,39 +108,39 @@ void TinyObjToSurfaceFaces(const tinyobj::mesh_t& mesh, } } -#endif // #ifndef DRAKE_DOXYGEN_CXX - -} // namespace - -SurfaceMesh ReadObjToSurfaceMesh(const std::string& filename, - const double scale) { - std::ifstream input_stream(filename); - if (!input_stream.is_open()) { - throw std::runtime_error("Cannot open file '" + filename +"'"); - } - return ReadObjToSurfaceMesh(&input_stream, scale); -} - -SurfaceMesh ReadObjToSurfaceMesh(std::istream* input_stream, - const double scale) { +SurfaceMesh DoReadObjToSurfaceMesh( + std::istream* input_stream, + const double scale, + const std::optional& mtl_basedir, + const std::function on_warning) { tinyobj::attrib_t attrib; // Used for vertices. std::vector shapes; // Used for triangles. std::vector materials; // Not used. std::string warn; std::string err; - // Ignore material-library file. - tinyobj::MaterialReader* readMatFn = nullptr; + std::unique_ptr readMatFn; + if (mtl_basedir) { + readMatFn = std::make_unique(*mtl_basedir); + } // triangulate non-triangle faces. bool triangulate = true; bool ret = tinyobj::LoadObj( - &attrib, &shapes, &materials, &warn, &err, input_stream, readMatFn, + &attrib, &shapes, &materials, &warn, &err, input_stream, readMatFn.get(), triangulate); if (!ret || !err.empty()) { throw std::runtime_error("Error parsing Wavefront obj file : " + err); } if (!warn.empty()) { - drake::log()->warn("Warning parsing Wavefront obj file : {}", warn); + warn = "Warning parsing Wavefront obj file : " + warn; + if (warn.back() == '\n') { + warn.pop_back(); + } + if (on_warning) { + on_warning(warn); + } else { + drake::log()->warn(warn); + } } if (shapes.size() == 0) { throw std::runtime_error("The Wavefront obj file has no faces."); @@ -169,5 +166,30 @@ SurfaceMesh ReadObjToSurfaceMesh(std::istream* input_stream, return SurfaceMesh(std::move(faces), std::move(vertices)); } +} // namespace + +SurfaceMesh ReadObjToSurfaceMesh( + const std::string& filename, + const double scale, + std::function on_warning) { + std::ifstream input_stream(filename); + if (!input_stream.is_open()) { + throw std::runtime_error("Cannot open file '" + filename +"'"); + } + const std::string mtl_basedir = + filesystem::path(filename).parent_path().string() + "/"; + return DoReadObjToSurfaceMesh(&input_stream, scale, mtl_basedir, + std::move(on_warning)); +} + +SurfaceMesh ReadObjToSurfaceMesh( + std::istream* input_stream, + const double scale, + std::function on_warning) { + DRAKE_THROW_UNLESS(input_stream != nullptr); + return DoReadObjToSurfaceMesh(input_stream, scale, + std::nullopt /* mtl_basedir */, std::move(on_warning)); +} + } // namespace geometry } // namespace drake diff --git a/geometry/proximity/obj_to_surface_mesh.h b/geometry/proximity/obj_to_surface_mesh.h index b13896a793ac..530a719242af 100644 --- a/geometry/proximity/obj_to_surface_mesh.h +++ b/geometry/proximity/obj_to_surface_mesh.h @@ -1,7 +1,9 @@ #pragma once +#include #include #include +#include #include #include "drake/geometry/proximity/surface_mesh.h" @@ -19,18 +21,26 @@ namespace geometry { A valid file name with absolute path or relative path. @param scale An optional scale to coordinates. + @param on_warning + An optional callback that will receive warning message(s) encountered + while reading the mesh. When not provided, drake::log() will be used. @throws std::runtime_error if `filename` doesn't have a valid file path, or the file has no faces. @return surface mesh */ -SurfaceMesh ReadObjToSurfaceMesh(const std::string& filename, - double scale = 1.0); +SurfaceMesh ReadObjToSurfaceMesh( + const std::string& filename, + double scale = 1.0, + std::function on_warning = {}); /** Overload of @ref ReadObjToSurfaceMesh(const std::string&, double) with the Wavefront .obj file given in std::istream. */ -SurfaceMesh ReadObjToSurfaceMesh(std::istream* input_stream, - double scale = 1.0); +SurfaceMesh ReadObjToSurfaceMesh( + std::istream* input_stream, + double scale = 1.0, + std::function on_warning = {}); + } // namespace geometry } // namespace drake diff --git a/geometry/proximity/test/obj_to_surface_mesh_test.cc b/geometry/proximity/test/obj_to_surface_mesh_test.cc index 742a8fd07a80..5b57a5ac3d9b 100644 --- a/geometry/proximity/test/obj_to_surface_mesh_test.cc +++ b/geometry/proximity/test/obj_to_surface_mesh_test.cc @@ -1,5 +1,6 @@ #include "drake/geometry/proximity/obj_to_surface_mesh.h" +#include #include #include #include @@ -159,6 +160,39 @@ GTEST_TEST(ObjToSurfaceMeshTest, ThrowExceptionForEmptyFile) { "The Wavefront obj file has no faces."); } +void FailOnWarning(std::string_view message) { + throw std::runtime_error(fmt::format("FailOnWarning: {}", message)); +} + +GTEST_TEST(ObjToSurfaceMeshTest, WarningCallback) { + // This *.obj file refers to a separate *.mtl file. In various cases below, + // this may cause warnings from the parser. + const std::string filename = + FindResourceOrThrow("drake/geometry/test/quad_cube.obj"); + + // When loaded as a stream (such that the *.mtl file is missing) with + // a defaulted callback, we will drake::log() but not throw. + { + std::ifstream input(filename); + EXPECT_NO_THROW(ReadObjToSurfaceMesh(&input, 1.0)); + } + + // When loaded as a stream (such that the *.mtl file is missing), the user- + // provided callback may choose to throw, and our test stub callback does so. + { + std::ifstream input(filename); + DRAKE_EXPECT_THROWS_MESSAGE( + ReadObjToSurfaceMesh(&input, 1.0, &FailOnWarning), + std::exception, + "FailOnWarning: Warning parsing Wavefront obj file : " + ".*CubeMaterial.*not found.*"); + } + + // When parsing using a filename, we are able to locate the *.mtl file with + // no warnings. + EXPECT_NO_THROW(ReadObjToSurfaceMesh(filename, 1.0, &FailOnWarning)); +} + GTEST_TEST(ObjToSurfaceMeshTest, ThrowExceptionFileHasNoFaces) { std::istringstream no_faces{R"( v 1.0 0.0 0.0