#include "drake/multibody/parsing/detail_mesh_parser.h" #include #include #include #include #include #include #include #include #include #include "drake/common/diagnostic_policy.h" #include "drake/geometry/proximity/obj_to_surface_mesh.h" #include "drake/geometry/proximity/triangle_surface_mesh.h" #include "drake/multibody/parsing/detail_make_model_name.h" #include "drake/multibody/tree/geometry_spatial_inertia.h" namespace drake { namespace multibody { namespace internal { namespace { using drake::geometry::TriangleSurfaceMesh; using drake::internal::DiagnosticPolicy; // The parse result of attempting to read a single object from OBJ data. struct NamedMesh { std::string name; std::unique_ptr> mesh; }; // Callback function for capturing the parsed object name; required by tinyobj's // API. `storage` is provided by the caller. void CaptureName(void* storage, const char* parsed_name) { *static_cast(storage) = parsed_name; } // Attempts to parse a single mesh out of the input stream returning the // resulting triangle mesh and its name (if any is defined). If there is a // parsing failure, the return value contains a null mesh. NamedMesh DoGetObjMesh(const DiagnosticPolicy& diagnostic, std::istream* input_stream, const std::string& mtl_basedir) { std::optional> mesh = geometry::internal::DoReadObjToSurfaceMesh(input_stream, 1.0, mtl_basedir, {.diagnostic = diagnostic, .allowed_shape_count = 1}); if (!mesh.has_value()) { return {}; } // A mesh exists, let's see if it had a name. We'll rewind the input stream // and parse explicitly listening for the "o some_name" data. std::string object_name; tinyobj::callback_t callback; callback.object_cb = CaptureName; input_stream->seekg(0); const bool result = tinyobj::LoadObjWithCallback(*input_stream, callback, &object_name); // If we have a mesh, then no error should be encountered in attempting to // find the name. DRAKE_DEMAND(result); // Note: the name may still be *empty*. This simply means the obj didn't // define an object name. return {object_name, std::make_unique>(std::move(*mesh))}; } // N.B. This layer of abstraction leaves the door open for implementing // GetMeshFromString() once we can support handling SceneGraph geometry from // an in-memory source. NamedMesh GetMeshFromFile(const DiagnosticPolicy& diagnostic, const std::string filename) { std::ifstream input_stream(filename); if (!input_stream.is_open()) { diagnostic.Error(fmt::format("Cannot open file '{}'", filename)); return {}; } // Failure to provide the directory of the obj file as the base material // library directory will cause OBJs with MTL files referenced by relative // paths to spew warnings. const std::string mtl_basedir = std::filesystem::path(filename).parent_path().string() + "/"; return DoGetObjMesh(diagnostic, &input_stream, mtl_basedir); } } // namespace std::optional AddModelFromMesh( const DataSource& data_source, const std::string& model_name, const std::optional& parent_model_name, const ParsingWorkspace& workspace) { if (!data_source.IsFilename()) { throw std::runtime_error( "Parser does not yet support adding models using in-memory OBJ data. " "The OBJ data must be referenced as a file."); } DRAKE_THROW_UNLESS(!workspace.plant->is_finalized()); const std::string filename = data_source.GetAbsolutePath(); NamedMesh named_mesh = GetMeshFromFile(workspace.diagnostic, filename); if (named_mesh.mesh == nullptr) { // Reasons for there being no mesh should already have been dispatched to // the diagnostic policy. return std::nullopt; } // Resolve the naming protocol - see Parser documentation. std::string candidate_name = model_name; if (candidate_name.empty()) { candidate_name = named_mesh.name; } if (candidate_name.empty()) { candidate_name = std::filesystem::path(filename).stem(); } // Register model instance, body, and collision and visual geometries. MultibodyPlant& plant = *workspace.plant; std::string model_instance_name = MakeModelName(candidate_name, parent_model_name, workspace); const ModelInstanceIndex model_instance = plant.AddModelInstance(model_instance_name); const SpatialInertia M_BBo_B = CalcSpatialInertia(*named_mesh.mesh, 1000.0 /* kg/m3 */); const RigidBody& body = plant.AddRigidBody(candidate_name, model_instance, M_BBo_B); if (plant.geometry_source_is_registered()) { // TODO(SeanCurtis-TRI): Consider using named_mesh.name as part of the // geometry names. // TODO(SeanCurtis-TRI): It would be better to use a single geometry and // assign all three roles to it. This would require access to the SceneGraph // and some shenanigans to placate MbP. const auto X_BG = math::RigidTransformd::Identity(); const geometry::Mesh mesh(filename); plant.RegisterCollisionGeometry(body, X_BG, mesh, "collision", CoulombFriction()); // TODO(SeanCurtis-TRI): If there's a material applied to the object, use // the specified color. plant.RegisterVisualGeometry(body, X_BG, mesh, "visual"); } return model_instance; } MeshParserWrapper::MeshParserWrapper() = default; MeshParserWrapper::~MeshParserWrapper() = default; std::optional MeshParserWrapper::AddModel( const DataSource& data_source, const std::string& model_name, const std::optional& parent_model_name, const ParsingWorkspace& workspace) { return AddModelFromMesh(data_source, model_name, parent_model_name, workspace); } std::vector MeshParserWrapper::AddAllModels( const DataSource& data_source, const std::optional& parent_model_name, const ParsingWorkspace& workspace) { std::optional maybe_index = AddModelFromMesh(data_source, "", parent_model_name, workspace); if (maybe_index.has_value()) { return {*maybe_index}; } return {}; } } // namespace internal } // namespace multibody } // namespace drake