diff --git a/doc/XMLreference.rst b/doc/XMLreference.rst index 0c30586d..0b0d1bee 100644 --- a/doc/XMLreference.rst +++ b/doc/XMLreference.rst @@ -1424,6 +1424,11 @@ The full list of processing steps applied by the compiler to each mesh is as fol The parameters used to generate a builtin mesh. The number and type of parameters and their semantic depends on the mesh type. See :ref:`mesh/builtin` for details. +.. _asset-mesh-material: + +:at:`material`: :at-val:`string, optional` + Fallback material for mesh geoms that do not specify their own material. + .. _mesh-plugin: :el-prefix:`mesh/` |-| **plugin** (?) @@ -2553,13 +2558,15 @@ helps clarify the role of bodies and geoms in MuJoCo. .. _body-geom-material: :at:`material`: :at-val:`string, optional` - If specified, this attribute applies a material to the geom. The material determines the visual properties of the - geom. The only exception is color: if the rgba attribute below is different from its internal default, it takes - precedence while the remaining material properties are still applied. Note that if the same material is referenced - from multiple geoms (as well as sites and tendons) and the user changes some of its properties at runtime, these - changes will take effect immediately for all model elements referencing the material. This is because the compiler - saves the material and its properties as a separate element in mjModel, and the elements using this material only - keep a reference to it. + If specified, this attribute applies a material to the geom. Otherwise, if unspecified and the type of the geom is + a **mesh** the compiler will apply the mesh asset :ref:`material ` if present. + + The material determines the visual properties of the geom. The only exception is color: if the rgba attribute below + is different from its internal default, it takes precedence while the remaining material properties are still + applied. Note that if the same material is referenced from multiple geoms (as well as sites and tendons) and the user + changes some of its properties at runtime, these changes will take effect immediately for all model elements + referencing the material. This is because the compiler saves the material and its properties as a separate element in + mjModel, and the elements using this material only keep a reference to it. .. _body-geom-rgba: diff --git a/doc/XMLschema.rst b/doc/XMLschema.rst index 762879b2..45504b7c 100644 --- a/doc/XMLschema.rst +++ b/doc/XMLschema.rst @@ -105,6 +105,8 @@ | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | | | | | :ref:`maxhullvert` | :ref:`inertia` | :ref:`builtin` | :ref:`params` | | | | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +| | | | :ref:`material` | | | | | +| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ | +------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | |_2| mesh |br| |_2| |L| | | .. table:: | | :ref:`plugin | \* | :class: mjcf-attributes | diff --git a/doc/changelog.rst b/doc/changelog.rst index 26541fff..1adedb24 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -31,6 +31,9 @@ General - Added support for shells with a curved reference configuration. See this `example `__. +- Added support for assigning a default material to a mesh asset using the :ref:`mesh/material ` + attribute. + MJX ^^^ - Promote ``ten_length`` to the public MJX API. Add Warp support for ``mjx.tendon``. diff --git a/doc/includes/references.h b/doc/includes/references.h index 90d939ff..3ba9c150 100644 --- a/doc/includes/references.h +++ b/doc/includes/references.h @@ -2144,6 +2144,7 @@ typedef struct mjsMesh_ { // mesh specification mjIntVec* userface; // user vertex indices mjIntVec* userfacetexcoord; // user texcoord indices mjsPlugin plugin; // sdf plugin + mjString* material; // name of material mjString* info; // message appended to compiler errors } mjsMesh; typedef struct mjsHField_ { // height field specification diff --git a/include/mujoco/mjspec.h b/include/mujoco/mjspec.h index 222c4fd0..a8aa6f4c 100644 --- a/include/mujoco/mjspec.h +++ b/include/mujoco/mjspec.h @@ -480,6 +480,7 @@ typedef struct mjsMesh_ { // mesh specification mjIntVec* userface; // user vertex indices mjIntVec* userfacetexcoord; // user texcoord indices mjsPlugin plugin; // sdf plugin + mjString* material; // name of material mjString* info; // message appended to compiler errors } mjsMesh; diff --git a/python/mujoco/introspect/structs.py b/python/mujoco/introspect/structs.py index 537b7394..98b39032 100644 --- a/python/mujoco/introspect/structs.py +++ b/python/mujoco/introspect/structs.py @@ -9353,6 +9353,13 @@ STRUCTS: Mapping[str, StructDecl] = dict([ type=ValueType(name='mjsPlugin'), doc='sdf plugin', ), + StructFieldDecl( + name='material', + type=PointerType( + inner_type=ValueType(name='mjString'), + ), + doc='name of material', + ), StructFieldDecl( name='info', type=PointerType( diff --git a/python/mujoco/specs_test.py b/python/mujoco/specs_test.py index 6f08cbfb..35472845 100644 --- a/python/mujoco/specs_test.py +++ b/python/mujoco/specs_test.py @@ -1478,6 +1478,34 @@ class SpecsTest(absltest.TestCase): self.assertEqual(mj_model.sensor_dim[0], 4) self.assertEqual(mj_model.sensor_dim[1], 1) + def test_mesh_material(self): + spec = mujoco.MjSpec() + + spec.add_material(name='red', rgba=(1, 0, 0, 1)) + spec.add_material(name='green', rgba=(0, 1, 0, 1)) + + mesh = spec.add_mesh(name='sphere') + mesh.make_sphere(subdivision=1) + mesh.material = 'red' + + geom = spec.worldbody.add_geom() + geom.type = mujoco.mjtGeom.mjGEOM_MESH + geom.meshname = 'sphere' + + geom_2 = spec.worldbody.add_geom() + geom_2.type = mujoco.mjtGeom.mjGEOM_MESH + geom_2.meshname = 'sphere' + geom_2.material = 'green' + + model = spec.compile() + + self.assertEqual(model.geom_matid[0], 0) + self.assertEqual(model.geom_matid[1], 1) + + mesh.material = 'green' + model = spec.compile() + + self.assertEqual(model.geom_matid[0], 1) if __name__ == '__main__': absltest.main() diff --git a/src/user/user_mesh.cc b/src/user/user_mesh.cc index a4f82f66..e0534dec 100644 --- a/src/user/user_mesh.cc +++ b/src/user/user_mesh.cc @@ -278,6 +278,7 @@ void mjCMesh::PointToLocal() { spec.userface = &spec_face_; spec.usertexcoord = &spec_texcoord_; spec.userfacetexcoord = &spec_facetexcoord_; + spec.material = &spec_material_; spec.plugin.plugin_name = &plugin_name; spec.plugin.name = &plugin_instance_name; spec.info = &info; @@ -317,6 +318,7 @@ void mjCMesh::CopyFromSpec() { content_type_ = spec_content_type_; normal_ = spec_normal_; face_ = spec_face_; + material_ = spec_material_; ProcessVertices(spec_vert_); texcoord_ = spec_texcoord_; facetexcoord_ = spec_facetexcoord_; @@ -476,6 +478,7 @@ void mjCMesh::CacheMesh(mjCCache* cache, const mjResource* resource) { mesh->polygon_map_ = polygon_map_; mesh->surface_ = surface_; mesh->volume_ = volume_; + mesh->material_ = material_; std::copy(boxsz_, boxsz_ + 3, mesh->boxsz_); std::copy(aamm_, aamm_ + 6, mesh->aamm_); std::copy(pos_, pos_ + 3, mesh->pos_); diff --git a/src/user/user_model.cc b/src/user/user_model.cc index 36ab30a6..27861189 100644 --- a/src/user/user_model.cc +++ b/src/user/user_model.cc @@ -1879,7 +1879,23 @@ void mjCModel::IndexAssets(bool discard) { for (int i=0; i < geoms_.size(); i++) { mjCGeom* geom = geoms_[i]; - // find material by name + + // find mesh by name + if (!geom->get_meshname().empty()) { + mjCMesh* mesh = static_cast(FindObject(mjOBJ_MESH, geom->get_meshname())); + if (mesh) { + if (!geom->visual_) { + mesh->SetNotVisual(); // reset to true by mesh->Compile() + } + geom->mesh = (discard && geom->visual_) ? nullptr : mesh; + mesh->spec.needsdf |= geom->spec.type == mjGEOM_SDF; + } else { + throw mjCError(geom, "mesh '%s' not found in geom %d", geom->get_meshname().c_str(), i); + } + } + + // find material by name, this has to happen after mesh assignment so that if + // the geom does not specify a material but the mesh does it can fall back. if (!geom->get_material().empty()) { mjCBase* material = FindObject(mjOBJ_MATERIAL, geom->get_material()); if (material) { @@ -1889,20 +1905,6 @@ void mjCModel::IndexAssets(bool discard) { } } - // find mesh by name - if (!geom->get_meshname().empty()) { - mjCBase* mesh = FindObject(mjOBJ_MESH, geom->get_meshname()); - if (mesh) { - if (!geom->visual_) { - ((mjCMesh*)mesh)->SetNotVisual(); // reset to true by mesh->Compile() - } - geom->mesh = (discard && geom->visual_) ? nullptr : (mjCMesh*)mesh; - static_cast(mesh)->spec.needsdf |= geom->spec.type == mjGEOM_SDF; - } else { - throw mjCError(geom, "mesh '%s' not found in geom %d", geom->get_meshname().c_str(), i); - } - } - // find hfield by name if (!geom->get_hfieldname().empty()) { mjCBase* hfield = FindObject(mjOBJ_HFIELD, geom->get_hfieldname()); diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index e405fa2a..674143dd 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -3365,7 +3365,12 @@ void mjCGeom::ComputeAABB(void) { mjuu_copyvec(aabb+3, size, 3); } - +const std::string& mjCGeom::get_material() const { + if (mesh && spec_material_.empty()) { + return mesh->Material(); + } + return spec_material_; +} // compiler void mjCGeom::Compile(void) { diff --git a/src/user/user_objects.h b/src/user/user_objects.h index 3d5b983b..d9896521 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -49,9 +49,9 @@ class mjCSite; class mjCCamera; class mjCLight; class mjCHField; -class mjCFlex; // defined in user_mesh.h -class mjCMesh; // defined in user_mesh.h -class mjCSkin; // defined in user_mesh.h +class mjCFlex; +class mjCMesh; +class mjCSkin; class mjCTexture; class mjCMaterial; class mjCPair; @@ -753,7 +753,7 @@ class mjCGeom : public mjCGeom_, private mjsGeom { const std::vector& get_userdata() const { return userdata_; } const std::string& get_hfieldname() const { return spec_hfieldname_; } const std::string& get_meshname() const { return spec_meshname_; } - const std::string& get_material() const { return spec_material_; } + const std::string& get_material() const; void del_material() { spec_material_.clear(); } private: @@ -1029,6 +1029,7 @@ class mjCMesh_ : public mjCBase { std::vector face_; // vertex indices std::vector facenormal_; // normal indices std::vector facetexcoord_; // texcoord indices + std::string material_; // mesh fallback material std::string spec_content_type_; std::string spec_file_; @@ -1038,6 +1039,7 @@ class mjCMesh_ : public mjCBase { std::vector spec_face_; std::vector spec_facenormal_; std::vector spec_facetexcoord_; + std::string spec_material_; // used by the compiler bool needreorient_; // needs reorientation @@ -1117,6 +1119,8 @@ class mjCMesh: public mjCMesh_, private mjsMesh { const std::vector& Face() const { return face_; } const std::vector& UserFace() const { return spec_face_; } mjtMeshInertia Inertia() const { return spec.inertia; } + const std::string& Material() const { return material_; } + // setters void SetNeedHull(bool needhull) { needhull_ = needhull; } diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index d2de7d74..f99d255c 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -230,9 +230,9 @@ const char* MJCF[nMJCF][mjXATTRNUM] = { {"asset", "*", "0"}, {"<"}, - {"mesh", "*", "16", "name", "class", "content_type", "file", "vertex", "normal", + {"mesh", "*", "17", "name", "class", "content_type", "file", "vertex", "normal", "texcoord", "face", "refpos", "refquat", "scale", "smoothnormal", - "maxhullvert", "inertia", "builtin", "params"}, + "maxhullvert", "inertia", "builtin", "params", "material"}, {"<"}, {"plugin", "*", "2", "plugin", "instance"}, {"<"}, @@ -1569,6 +1569,11 @@ void mjXReader::OneMesh(XMLElement* elem, mjsMesh* mesh, const mjVFS* vfs) { } } + std::string material; + if (ReadAttrTxt(elem, "material", material)) { + mjs_setString(mesh->material, material.c_str()); + } + // write error info mjs_setString(mesh->info, ("line " + std::to_string(elem->GetLineNum())).c_str()); } diff --git a/test/user/user_mesh_test.cc b/test/user/user_mesh_test.cc index bfefa0e3..3713bc56 100644 --- a/test/user/user_mesh_test.cc +++ b/test/user/user_mesh_test.cc @@ -1379,5 +1379,28 @@ TEST_F(MjCMeshTest, SphereSizes) { mj_deleteModel(model); } +TEST_F(MjCMeshTest, MeshMaterial) { + static constexpr char xml[] = R"( + + + + + + + + + + + + + )"; + std::array error; + mjModel* model = LoadModelFromString(xml, error.data(), error.size()); + ASSERT_THAT(model, NotNull()) << error.data(); + EXPECT_EQ(model->geom_matid[0], 1); + EXPECT_EQ(model->geom_matid[1], 0); + mj_deleteModel(model); +} + } // namespace } // namespace mujoco