Add ability to specify a material assignment to a mesh asset.

This extends both the spec and the XML definition to allow specification of a material name on a mesh. This material acts as a fallback in the case that the referencing geom element does not specify a material of it's own.

PiperOrigin-RevId: 798232378
Change-Id: Iaf300c727732744ea1b613e64334ee505ac6e209
This commit is contained in:
Sam Haves
2025-08-22 09:10:35 -07:00
committed by Copybara-Service
parent 42d8454555
commit 1fb1810b07
13 changed files with 120 additions and 29 deletions
+14 -7
View File
@@ -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<asset-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 <asset-mesh-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:
+2
View File
@@ -105,6 +105,8 @@
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
| | | | :ref:`maxhullvert<asset-mesh-maxhullvert>` | :ref:`inertia<asset-mesh-inertia>` | :ref:`builtin<asset-mesh-builtin>` | :ref:`params<asset-mesh-params>` | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
| | | | :ref:`material<asset-mesh-material>` | | | | |
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
+------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| |_2| mesh |br| |_2| |L| | | .. table:: |
| :ref:`plugin | \* | :class: mjcf-attributes |
+3
View File
@@ -31,6 +31,9 @@ General
- Added support for shells with a curved reference configuration. See this `example
<https://github.com/google-deepmind/mujoco/blob/main/model/flex/basket.xml>`__.
- Added support for assigning a default material to a mesh asset using the :ref:`mesh/material <asset-mesh-material>`
attribute.
MJX
^^^
- Promote ``ten_length`` to the public MJX API. Add Warp support for ``mjx.tendon``.
+1
View File
@@ -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
+1
View File
@@ -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;
+7
View File
@@ -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(
+28
View File
@@ -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()
+3
View File
@@ -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_);
+17 -15
View File
@@ -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<mjCMesh*>(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<mjCMesh*>(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());
+6 -1
View File
@@ -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) {
+8 -4
View File
@@ -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<double>& 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<int> face_; // vertex indices
std::vector<int> facenormal_; // normal indices
std::vector<int> 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<int> spec_face_;
std::vector<int> spec_facenormal_;
std::vector<int> 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<int>& Face() const { return face_; }
const std::vector<int>& 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; }
+7 -2
View File
@@ -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());
}
+23
View File
@@ -1379,5 +1379,28 @@ TEST_F(MjCMeshTest, SphereSizes) {
mj_deleteModel(model);
}
TEST_F(MjCMeshTest, MeshMaterial) {
static constexpr char xml[] = R"(
<mujoco model="mesh_material">
<asset>
<mesh name="h0" builtin="sphere" params="0" material="m1"/>
<material name="m0" rgba="1 0 0 1"/>
<material name="m1" rgba="1 1 0 1"/>
</asset>
<worldbody>
<geom name="g0" type="mesh" mesh="h0"/>
<geom name="g1" type="mesh" mesh="h0" material="m0"/>
</worldbody>
</mujoco>
)";
std::array<char, 1024> 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