Add support for loading 1D flex components from OBJ line segments.
The obj_decoder plugin is updated to parse line segments ('l') from OBJ files and represent them as degenerate triangles. The user_flexcomp compiler now supports flex dimensions of 1, using these parsed line segments to define the 1D flex elements. The compile sample's error reporting is also improved.
PiperOrigin-RevId: 928566870
Change-Id: I6920729d1b1d5aeb391401c3245ebb1d4784c190
This commit is contained in:
committed by
Copybara-Service
parent
ac7f198536
commit
7a7dc7ccad
@@ -16,6 +16,7 @@ General
|
||||
- Added :ref:`mjs_makeFlex`, a new C API function equivalent to the :ref:`flexcomp<body-flexcomp>` element for
|
||||
programmatically creating flex objects with auto-generated bodies, joints, and equality constraints. Exposed as
|
||||
``body.make_flex()`` in Python.
|
||||
- Added support for loading 1D flex components from OBJ line segments
|
||||
- Significantly improved the quality of coarse convex hulls produced by the :ref:`maxhullvert<asset-mesh-maxhullvert>`
|
||||
attribute by invoking Qhull's `Q9 <http://www.qhull.org/html/qh-optq.htm#Q9>`__ option.
|
||||
|
||||
|
||||
@@ -53,41 +53,80 @@ mjSpec* Decode(mjResource* resource, const mjVFS* vfs) {
|
||||
std::vector<int> userfacetexcoord;
|
||||
|
||||
if (!obj_reader.GetShapes().empty()) {
|
||||
const auto& obj_mesh = obj_reader.GetShapes()[0].mesh;
|
||||
const auto& obj_shape = obj_reader.GetShapes()[0];
|
||||
if (!obj_shape.mesh.indices.empty()) {
|
||||
const auto& obj_mesh = obj_shape.mesh;
|
||||
|
||||
std::vector<tinyobj::index_t> face_indices;
|
||||
for (size_t face = 0, idx = 0; idx < obj_mesh.indices.size();) {
|
||||
int nfacevert = obj_mesh.num_face_vertices[face];
|
||||
if (nfacevert < 3 || nfacevert > 4) {
|
||||
mju_warning(
|
||||
"obj_decoder: only tri or quad meshes are supported (file '%s')",
|
||||
resource->name);
|
||||
mj_deleteSpec(spec);
|
||||
return nullptr;
|
||||
size_t num_face_indices = 0;
|
||||
for (size_t face = 0; face < obj_mesh.num_face_vertices.size(); ++face) {
|
||||
int nfacevert = obj_mesh.num_face_vertices[face];
|
||||
if (nfacevert == 3) {
|
||||
num_face_indices += 3;
|
||||
} else if (nfacevert == 4) {
|
||||
num_face_indices += 6;
|
||||
}
|
||||
}
|
||||
|
||||
face_indices.push_back(obj_mesh.indices[idx]);
|
||||
face_indices.push_back(obj_mesh.indices[idx + 1]);
|
||||
face_indices.push_back(obj_mesh.indices[idx + 2]);
|
||||
|
||||
if (nfacevert == 4) {
|
||||
face_indices.push_back(obj_mesh.indices[idx]);
|
||||
face_indices.push_back(obj_mesh.indices[idx + 2]);
|
||||
face_indices.push_back(obj_mesh.indices[idx + 3]);
|
||||
}
|
||||
idx += nfacevert;
|
||||
++face;
|
||||
}
|
||||
|
||||
for (const auto& mesh_index : face_indices) {
|
||||
userface.push_back(mesh_index.vertex_index);
|
||||
|
||||
std::vector<tinyobj::index_t> face_indices;
|
||||
face_indices.reserve(num_face_indices);
|
||||
userface.reserve(num_face_indices);
|
||||
if (!usernormal.empty()) {
|
||||
userfacenormal.push_back(mesh_index.normal_index);
|
||||
userfacenormal.reserve(num_face_indices);
|
||||
}
|
||||
if (!usertexcoord.empty()) {
|
||||
userfacetexcoord.reserve(num_face_indices);
|
||||
}
|
||||
|
||||
if (!usertexcoord.empty()) {
|
||||
userfacetexcoord.push_back(mesh_index.texcoord_index);
|
||||
for (size_t face = 0, idx = 0; idx < obj_mesh.indices.size();) {
|
||||
int nfacevert = obj_mesh.num_face_vertices[face];
|
||||
if (nfacevert < 3 || nfacevert > 4) {
|
||||
mju_warning(
|
||||
"obj_decoder: only tri or quad meshes are supported (file '%s')",
|
||||
resource->name);
|
||||
mj_deleteSpec(spec);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
face_indices.push_back(obj_mesh.indices[idx]);
|
||||
face_indices.push_back(obj_mesh.indices[idx + 1]);
|
||||
face_indices.push_back(obj_mesh.indices[idx + 2]);
|
||||
|
||||
if (nfacevert == 4) {
|
||||
face_indices.push_back(obj_mesh.indices[idx]);
|
||||
face_indices.push_back(obj_mesh.indices[idx + 2]);
|
||||
face_indices.push_back(obj_mesh.indices[idx + 3]);
|
||||
}
|
||||
idx += nfacevert;
|
||||
++face;
|
||||
}
|
||||
|
||||
for (const auto& mesh_index : face_indices) {
|
||||
userface.push_back(mesh_index.vertex_index);
|
||||
|
||||
if (!usernormal.empty()) {
|
||||
userfacenormal.push_back(mesh_index.normal_index);
|
||||
}
|
||||
|
||||
if (!usertexcoord.empty()) {
|
||||
userfacetexcoord.push_back(mesh_index.texcoord_index);
|
||||
}
|
||||
}
|
||||
} else if (!obj_shape.lines.indices.empty()) {
|
||||
// encode line segments as degenerate triangles (i1, i2, i2) in userface,
|
||||
// since mjsMesh has no native edge field; decoded back by flexcomp
|
||||
const auto& obj_lines = obj_shape.lines;
|
||||
userface.reserve(3 * (obj_lines.indices.size() - obj_lines.num_line_vertices.size()));
|
||||
size_t idx = 0;
|
||||
for (size_t l = 0; l < obj_lines.num_line_vertices.size(); l++) {
|
||||
int nlinevert = obj_lines.num_line_vertices[l];
|
||||
for (int v = 0; v < nlinevert - 1; v++) {
|
||||
int i1 = obj_lines.indices[idx + v].vertex_index;
|
||||
int i2 = obj_lines.indices[idx + v + 1].vertex_index;
|
||||
userface.push_back(i1);
|
||||
userface.push_back(i2);
|
||||
userface.push_back(i2);
|
||||
}
|
||||
idx += nlinevert;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,8 +140,10 @@ mjSpec* Decode(mjResource* resource, const mjVFS* vfs) {
|
||||
mjs_setFloat(mesh->usernormal, usernormal.data(), usernormal.size());
|
||||
mjs_setFloat(mesh->usertexcoord, usertexcoord.data(), usertexcoord.size());
|
||||
mjs_setInt(mesh->userface, userface.data(), userface.size());
|
||||
mjs_setInt(mesh->userfacenormal, userfacenormal.data(), userfacenormal.size());
|
||||
mjs_setInt(mesh->userfacetexcoord, userfacetexcoord.data(), userfacetexcoord.size());
|
||||
mjs_setInt(mesh->userfacenormal, userfacenormal.data(),
|
||||
userfacenormal.size());
|
||||
mjs_setInt(mesh->userfacetexcoord, userfacetexcoord.data(),
|
||||
userfacetexcoord.size());
|
||||
|
||||
return spec;
|
||||
}
|
||||
|
||||
@@ -1334,8 +1334,8 @@ bool mjCFlexcomp::MakeMesh(mjCModel* model, mjsCompiler* compiler, char* error,
|
||||
}
|
||||
|
||||
// check dim
|
||||
if (def.spec.flex->dim < 2) {
|
||||
return comperr(error, "Flex dim must be at least 2 for mesh", error_sz);
|
||||
if (def.spec.flex->dim < 1) {
|
||||
return comperr(error, "Flex dim must be at least 1 for mesh", error_sz);
|
||||
}
|
||||
|
||||
// load resource
|
||||
@@ -1381,6 +1381,15 @@ bool mjCFlexcomp::MakeMesh(mjCModel* model, mjsCompiler* compiler, char* error,
|
||||
// copy faces or create 3D mesh
|
||||
if (def.spec.flex->dim == 2) {
|
||||
element = mesh.Face();
|
||||
} else if (def.spec.flex->dim == 1) {
|
||||
// extract edge pairs from degenerate triangles (i1, i2, i2)
|
||||
const std::vector<int>& face = mesh.Face();
|
||||
element.clear();
|
||||
element.reserve(face.size() * 2 / 3);
|
||||
for (size_t i = 0; i < face.size(); i += 3) {
|
||||
element.push_back(face[i]);
|
||||
element.push_back(face[i+1]);
|
||||
}
|
||||
} else {
|
||||
point.insert(point.begin() + 0, origin[0]);
|
||||
point.insert(point.begin() + 1, origin[1]);
|
||||
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<flexcomp name="line" type="mesh" file="line.obj" dim="1" radius="0.1"/>
|
||||
</worldbody>
|
||||
</mujoco>
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
v 0 0 0
|
||||
v 2 0 0
|
||||
v 1 0 0
|
||||
v 3 0 0
|
||||
l 1 3 2
|
||||
l 2 4
|
||||
@@ -1399,6 +1399,18 @@ TEST_F(UserFlexTest, Vert0RotationInvariant) {
|
||||
mj_deleteModel(m2);
|
||||
}
|
||||
|
||||
TEST_F(UserFlexTest, Load1DFlexFromOBJ) {
|
||||
const std::string xml_path =
|
||||
GetTestDataFilePath("user/testdata/flex_line_obj.xml");
|
||||
std::array<char, 1024> error;
|
||||
mjModel* m = mj_loadXML(xml_path.c_str(), 0, error.data(), error.size());
|
||||
ASSERT_THAT(m, NotNull()) << error.data();
|
||||
EXPECT_EQ(m->nflexvert, 4);
|
||||
EXPECT_EQ(m->nflexelem, 3);
|
||||
EXPECT_EQ(m->flex_dim[0], 1);
|
||||
mj_deleteModel(m);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mujoco
|
||||
|
||||
|
||||
Reference in New Issue
Block a user