Rollback of Move OBJ handling to a mjpDecoder.

PiperOrigin-RevId: 875705083
Change-Id: I21403d84abd56ef72de915bddc724fe3efa506e6
This commit is contained in:
Sam Haves
2026-02-26 07:03:57 -08:00
committed by Copybara-Service
parent 55d7226b06
commit 0ec836b281
12 changed files with 109 additions and 258 deletions
-2
View File
@@ -101,7 +101,6 @@ copy_plugins_posix() {
mkdir -p ${TMPDIR}/mujoco_install/mujoco_plugin &&
cp lib/libactuator.* ${TMPDIR}/mujoco_install/mujoco_plugin &&
cp lib/libelasticity.* ${TMPDIR}/mujoco_install/mujoco_plugin &&
cp lib/libobj_decoder.* ${TMPDIR}/mujoco_install/mujoco_plugin &&
cp lib/libsensor.* ${TMPDIR}/mujoco_install/mujoco_plugin &&
cp lib/libsdf_plugin.* ${TMPDIR}/mujoco_install/mujoco_plugin
}
@@ -112,7 +111,6 @@ copy_plugins_window() {
mkdir -p ${TMPDIR}/mujoco_install/mujoco_plugin &&
cp bin/Release/actuator.dll ${TMPDIR}/mujoco_install/mujoco_plugin &&
cp bin/Release/elasticity.dll ${TMPDIR}/mujoco_install/mujoco_plugin &&
cp bin/Release/obj_decoder.dll ${TMPDIR}/mujoco_install/mujoco_plugin &&
cp bin/Release/sensor.dll ${TMPDIR}/mujoco_install/mujoco_plugin
}
-1
View File
@@ -113,7 +113,6 @@ add_subdirectory(src/engine)
add_subdirectory(src/user)
add_subdirectory(src/xml)
add_subdirectory(src/thread)
add_subdirectory(plugin/obj_decoder)
if(MUJOCO_USE_FILAMENT AND NOT EMSCRIPTEN)
# Note that, by default, the "src/render" and "src/ui" code is added directly
-1
View File
@@ -38,7 +38,6 @@ function(add_mujoco_shell_test TEST_NAME TARGET_BINARY)
"CMAKE_SOURCE_DIR=${CMAKE_SOURCE_DIR}"
"TARGET_BINARY=$<TARGET_FILE:${TARGET_BINARY}>"
"TEST_TMPDIR=${TEST_TMPDIR}"
"MUJOCO_PLUGIN_DIR=$<TARGET_FILE_DIR:obj_decoder>"
)
if(WIN32)
# Define the directory containing the mujoco DLL library so that it can be added to the PATH.
-46
View File
@@ -1,46 +0,0 @@
# Copyright 2026 DeepMind Technologies Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)
set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_LIBDIR}")
add_library(obj_decoder SHARED obj_decoder.cc)
target_include_directories(obj_decoder PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../..
)
target_link_libraries(obj_decoder PRIVATE
mujoco
tinyobjloader
)
target_compile_definitions(obj_decoder PRIVATE TINYOBJLOADER_IMPLEMENTATION)
target_compile_options(obj_decoder PRIVATE
${AVX_COMPILE_OPTIONS}
${MUJOCO_MACOS_COMPILE_OPTIONS}
${EXTRA_COMPILE_OPTIONS}
${MUJOCO_CXX_FLAGS}
)
target_link_options(obj_decoder PRIVATE
${MUJOCO_MACOS_LINK_OPTIONS}
${EXTRA_LINK_OPTIONS}
)
install(
TARGETS obj_decoder
LIBRARY DESTINATION "${CMAKE_INSTALL_BINDIR}/mujoco_plugin"
)
-125
View File
@@ -1,125 +0,0 @@
// Copyright 2026 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include <cstring>
#include <string>
#include <string_view>
#include <vector>
#include <mujoco/mjplugin.h>
#include <mujoco/mjspec.h>
#include <mujoco/mujoco.h>
#include <tiny_obj_loader.h>
namespace {
mjSpec* Decode(mjResource* resource, const mjVFS* vfs) {
const void* bytes = nullptr;
int buffer_sz = mju_readResource(resource, &bytes);
if (buffer_sz < 0) {
mju_warning("obj_decoder: could not read OBJ file '%s'", resource->name);
return nullptr;
}
tinyobj::ObjReader obj_reader;
const char* buffer = static_cast<const char*>(bytes);
obj_reader.ParseFromString(std::string(buffer, buffer_sz), std::string());
if (!obj_reader.Valid()) {
mju_warning("obj_decoder: could not parse OBJ file '%s'", resource->name);
return nullptr;
}
mjSpec* spec = mj_makeSpec();
mjsMesh* mesh = mjs_addMesh(spec, nullptr);
const auto& attrib = obj_reader.GetAttrib();
std::vector<float> usernormal = attrib.normals;
std::vector<float> usertexcoord = attrib.texcoords;
std::vector<int> userface;
std::vector<int> userfacenormal;
std::vector<int> userfacetexcoord;
if (!obj_reader.GetShapes().empty()) {
const auto& obj_mesh = obj_reader.GetShapes()[0].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;
}
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);
}
}
}
for (size_t i = 0; i < usertexcoord.size() / 2; i++) {
usertexcoord[2 * i + 1] = 1 - usertexcoord[2 * i + 1];
}
mjs_setString(mesh->file, resource->name);
mjs_setFloat(mesh->uservert, attrib.vertices.data(), attrib.vertices.size());
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());
return spec;
}
int CanDecode(const mjResource* resource) {
std::string_view name(resource->name);
return name.ends_with(".obj") || name.ends_with(".OBJ");
}
} // namespace
mjPLUGIN_LIB_INIT {
mjpDecoder decoder;
mjp_defaultDecoder(&decoder);
decoder.content_type = "model/obj";
decoder.extension = ".obj";
decoder.decode = Decode;
decoder.can_decode = CanDecode;
mjp_registerDecoder(&decoder);
}
-1
View File
@@ -14,7 +14,6 @@
# ==============================================================================
"""Tests for msh2obj.py."""
from absl.testing import absltest
from etils import epath
import mujoco
+1
View File
@@ -38,3 +38,4 @@ set(MUJOCO_USER_SRCS
)
target_sources(mujoco PRIVATE ${MUJOCO_USER_SRCS})
target_compile_definitions(mujoco PRIVATE MUJOCO_TINYOBJLOADER_IMPL)
+94 -46
View File
@@ -612,7 +612,10 @@ void mjCMesh::ProcessVertices(const std::vector<float>& vert, bool remove_repeat
}
}
bool mjCMesh::IsObj(std::string_view filename, std::string_view ct) {
std::string asset_type = GetAssetContentType(filename, ct);
return asset_type == "model/obj";
}
bool mjCMesh::IsSTL(std::string_view filename, std::string_view ct) {
std::string asset_type = GetAssetContentType(filename, ct);
@@ -624,7 +627,9 @@ bool mjCMesh::IsMSH(std::string_view filename, std::string_view ct) {
return asset_type == "model/vnd.mujoco.msh";
}
bool mjCMesh::IsObj() const {
return content_type_ == "model/obj";
}
bool mjCMesh::IsSTL() const {
return content_type_ == "model/stl";
@@ -634,62 +639,28 @@ bool mjCMesh::IsMSH() const {
return content_type_ == "model/vnd.mujoco.msh";
}
// load mesh using decoder plugin
void mjCMesh::LoadFromDecoder(mjResource* resource, bool remove_repeated) {
const mjpDecoder* decoder = mjp_findDecoder(resource, content_type_.c_str());
if (!decoder) {
throw mjCError(this, "no decoder found for mesh file '%s'", resource->name);
}
mjSpec* mesh_spec = decoder->decode(resource, nullptr);
if (!mesh_spec) {
throw mjCError(this, "decoder failed for mesh file '%s'", resource->name);
}
mjsElement* elem = mjs_firstElement(mesh_spec, mjOBJ_MESH);
if (elem) {
mjsMesh* src_mesh = mjs_asMesh(elem);
if (src_mesh) {
normal_.assign(src_mesh->usernormal->begin(), src_mesh->usernormal->end());
texcoord_.assign(src_mesh->usertexcoord->begin(), src_mesh->usertexcoord->end());
face_.assign(src_mesh->userface->begin(), src_mesh->userface->end());
facenormal_.assign(src_mesh->userfacenormal->begin(), src_mesh->userfacenormal->end());
facetexcoord_.assign(src_mesh->userfacetexcoord->begin(), src_mesh->userfacetexcoord->end());
// correct winding order for left-handed coordinate systems
bool righthand = scale[0] * scale[1] * scale[2] > 0;
if (!righthand) {
for (size_t i = 0; i < face_.size(); i += 3) {
std::swap(face_[i + 1], face_[i + 2]);
}
for (size_t i = 0; i < facenormal_.size(); i += 3) {
std::swap(facenormal_[i + 1], facenormal_[i + 2]);
}
for (size_t i = 0; i < facetexcoord_.size(); i += 3) {
std::swap(facetexcoord_[i + 1], facetexcoord_[i + 2]);
}
}
std::vector<float> vert(src_mesh->uservert->begin(), src_mesh->uservert->end());
mj_deleteSpec(mesh_spec);
ProcessVertices(vert, remove_repeated);
return;
}
}
mj_deleteSpec(mesh_spec);
}
// load mesh from resource; throw error on failure
void mjCMesh::LoadFromResource(mjResource* resource, bool remove_repeated) {
// set content type from resource name
std::string asset_type = GetAssetContentType(resource->name, content_type_);
if (asset_type.empty()) {
if (!content_type_.empty()) {
throw mjCError(this, "invalid content type: '%s'", content_type_.c_str());
}
throw mjCError(this, "unknown or unsupported mesh file: '%s'", resource->name);
}
content_type_ = asset_type;
if (IsSTL()) {
LoadSTL(resource);
} else if (IsObj()) {
LoadOBJ(resource, remove_repeated);
} else if (IsMSH()) {
LoadMSH(resource, remove_repeated);
} else {
LoadFromDecoder(resource, remove_repeated);
throw mjCError(this, "unsupported mesh type: '%s'", asset_type.c_str());
}
}
@@ -1038,6 +1009,83 @@ void mjCMesh::FitGeom(mjCGeom* geom, double center[3]) {
}
// load OBJ mesh
void mjCMesh::LoadOBJ(mjResource* resource, bool remove_repeated) {
tinyobj::ObjReader objReader;
const void* bytes = nullptr;
int buffer_sz = mju_readResource(resource, &bytes);
if (buffer_sz < 0) {
throw mjCError(this, "could not read OBJ file '%s'", resource->name);
}
// TODO(etom): support .mtl files?
const char* buffer = (const char*) bytes;
objReader.ParseFromString(std::string(buffer, buffer_sz), std::string());
if (!objReader.Valid()) {
throw mjCError(this, "could not parse OBJ file '%s'", resource->name);
}
const auto& attrib = objReader.GetAttrib();
normal_ = attrib.normals;
texcoord_ = attrib.texcoords;
facenormal_.clear();
facetexcoord_.clear();
if (!objReader.GetShapes().empty()) {
const auto& mesh = objReader.GetShapes()[0].mesh;
bool righthand = scale[0] * scale[1] * scale[2] > 0;
// iterate over mesh faces
std::vector<tinyobj::index_t> face_indices;
for (int face = 0, idx = 0; idx < mesh.indices.size();) {
int nfacevert = mesh.num_face_vertices[face];
if (nfacevert < 3 || nfacevert > 4) {
throw mjCError(
this, "only tri or quad meshes are supported for OBJ (file '%s')",
resource->name);
}
face_indices.push_back(mesh.indices[idx]);
face_indices.push_back(mesh.indices[idx + (righthand == 1 ? 1 : 2)]);
face_indices.push_back(mesh.indices[idx + (righthand == 1 ? 2 : 1)]);
if (nfacevert == 4) {
face_indices.push_back(mesh.indices[idx]);
face_indices.push_back(mesh.indices[idx + (righthand == 1 ? 2 : 3)]);
face_indices.push_back(mesh.indices[idx + (righthand == 1 ? 3 : 2)]);
}
idx += nfacevert;
++face;
}
// for each vertex, store index, normal, and texcoord
for (const auto& mesh_index : face_indices) {
face_.push_back(mesh_index.vertex_index);
if (!normal_.empty()) {
facenormal_.push_back(mesh_index.normal_index);
}
if (!texcoord_.empty()) {
facetexcoord_.push_back(mesh_index.texcoord_index);
}
}
}
// flip the second texcoord
for (int i=0; i < texcoord_.size()/2; i++) {
texcoord_[2*i+1] = 1-texcoord_[2*i+1];
}
// copy vertex data
ProcessVertices(attrib.vertices, remove_repeated);
}
// load mesh from cached asset, return true on success
bool mjCMesh::LoadCachedMesh(mjCCache *cache, const mjResource* resource) {
auto process_mesh = [&](const void* data) {
@@ -1728,7 +1776,7 @@ void mjCMesh::CheckInitialMesh() const {
// check texcoord size if no face texcoord indices are given
if (!texcoord_.empty() && texcoord_.size() != 2 * nvert() &&
facetexcoord_.empty() && content_type_ != "model/obj") {
facetexcoord_.empty() && !IsObj()) {
throw mjCError(this,
"texcoord must be 2*nv if face texcoord indices are not provided in an OBJ file");
}
+7 -4
View File
@@ -22,7 +22,6 @@
#include <array>
#include <deque>
#include <functional>
#include <map>
#include <string>
#include <string_view>
#include <unordered_map>
@@ -35,6 +34,10 @@
#include <mujoco/mjtnum.h>
#include "user/user_cache.h"
#include "user/user_util.h"
#include <tiny_obj_loader.h>
using face_vertices_type =
decltype(tinyobj::mesh_t::num_face_vertices)::value_type;
// forward declarations of all mjC/X classes
class mjCError;
@@ -1238,11 +1241,11 @@ class mjCMesh: public mjCMesh_, private mjsMesh {
// load from OBJ, STL, or MSH file; throws mjCError on failure
void LoadFromResource(mjResource* resource, bool remove_repeated = false);
static bool IsObj(std::string_view filename, std::string_view ct = "");
static bool IsSTL(std::string_view filename, std::string_view ct = "");
static bool IsMSH(std::string_view filename, std::string_view ct = "");
bool IsObj() const;
bool IsSTL() const;
bool IsMSH() const;
@@ -1259,7 +1262,7 @@ class mjCMesh: public mjCMesh_, private mjsMesh {
void ProcessVertices(const std::vector<float>& vert, bool remove_repeated = false);
void LoadFromDecoder(mjResource* resource, bool remove_repeated); // load mesh using decoder plugin
void LoadOBJ(mjResource* resource, bool remove_repeated); // load mesh in wavefront OBJ format
void LoadSTL(mjResource* resource); // load mesh in STL BIN format
void LoadMSH(mjResource* resource, bool remove_repeated); // load mesh in MSH BIN format
+3 -20
View File
@@ -12,13 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
mujoco_test(
user_model_test
PROPERTIES
ENVIRONMENT
"MUJOCO_PLUGIN_DIR=$<TARGET_FILE_DIR:obj_decoder>"
ADDITIONAL_LINK_LIBRARIES absl::str_format
)
mujoco_test(user_model_test ADDITIONAL_LINK_LIBRARIES absl::str_format)
mujoco_test(user_objects_test)
@@ -29,20 +23,9 @@ mujoco_test(
"MUJOCO_PLUGIN_DIR=$<TARGET_FILE_DIR:elasticity>"
)
mujoco_test(
user_flex_test
PROPERTIES
ENVIRONMENT
"MUJOCO_PLUGIN_DIR=$<TARGET_FILE_DIR:obj_decoder>"
)
mujoco_test(user_flex_test)
mujoco_test(
user_mesh_test
PROPERTIES
ENVIRONMENT
"MUJOCO_PLUGIN_DIR=$<TARGET_FILE_DIR:obj_decoder>"
ADDITIONAL_LINK_LIBRARIES absl::str_format
)
mujoco_test(user_mesh_test ADDITIONAL_LINK_LIBRARIES absl::str_format)
mujoco_test(user_composite_test)
+3 -6
View File
@@ -71,9 +71,6 @@ static const char* const kMalformedFaceOBJPath =
static const char* const kCubeSkinPath =
"user/testdata/cube_skin.xml";
static const char* const kNoDecoderForMeshErrorMsh =
"no decoder found for mesh";
using ::testing::ElementsAre;
using ::testing::HasSubstr;
using ::testing::IsNull;
@@ -113,7 +110,7 @@ TEST_F(MjCMeshTest, UnknownMeshFormat) {
LoadModelFromString(xml.c_str(), error.data(), error.size(), &vfs);
ASSERT_THAT(model, testing::IsNull())
<< "Should fail to load a mesh named: " << name;
EXPECT_THAT(error.data(), HasSubstr(kNoDecoderForMeshErrorMsh));
EXPECT_THAT(error.data(), HasSubstr("unknown or unsupported mesh file: "));
EXPECT_THAT(error.data(), HasSubstr(name));
}
@@ -303,7 +300,7 @@ TEST_F(MjCMeshTest, LoadMSHWithContentTypeError) {
// should error with unknown file type
mjModel* model = LoadModelFromString(xml, error, error_sz, &vfs);
EXPECT_THAT(model, IsNull());
EXPECT_THAT(error, HasSubstr(kNoDecoderForMeshErrorMsh));
EXPECT_THAT(error, HasSubstr("unsupported mesh type: 'model/unknown'"));
mj_deleteVFS(&vfs);
}
@@ -330,7 +327,7 @@ TEST_F(MjCMeshTest, LoadMSHWithInvalidContentType) {
// should error with unknown file type
mjModel* model = LoadModelFromString(xml, error, error_sz, &vfs);
EXPECT_THAT(model, IsNull());
EXPECT_THAT(error, HasSubstr(kNoDecoderForMeshErrorMsh));
EXPECT_THAT(error, HasSubstr("invalid content type: 'model'"));
mj_deleteVFS(&vfs);
}
+1 -6
View File
@@ -14,12 +14,7 @@
mujoco_test(xml_api_test)
mujoco_test(
xml_native_reader_test
PROPERTIES
ENVIRONMENT
"MUJOCO_PLUGIN_DIR=$<TARGET_FILE_DIR:obj_decoder>"
)
mujoco_test(xml_native_reader_test)
mujoco_test(xml_utils_test)