diff --git a/.github/workflows/build_steps.sh b/.github/workflows/build_steps.sh index d97dd120..066d6949 100755 --- a/.github/workflows/build_steps.sh +++ b/.github/workflows/build_steps.sh @@ -102,6 +102,7 @@ copy_plugins_posix() { 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/libstl_decoder.* ${TMPDIR}/mujoco_install/mujoco_plugin && cp lib/libsensor.* ${TMPDIR}/mujoco_install/mujoco_plugin && cp lib/libsdf_plugin.* ${TMPDIR}/mujoco_install/mujoco_plugin } @@ -113,6 +114,7 @@ copy_plugins_window() { 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/stl_decoder.dll ${TMPDIR}/mujoco_install/mujoco_plugin && cp bin/Release/sensor.dll ${TMPDIR}/mujoco_install/mujoco_plugin } diff --git a/CMakeLists.txt b/CMakeLists.txt index 8bef8270..e0f0653d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -111,6 +111,7 @@ if(NOT EMSCRIPTEN) add_subdirectory(plugin/sdf) endif() add_subdirectory(plugin/obj_decoder) +add_subdirectory(plugin/stl_decoder) add_subdirectory(src/engine) add_subdirectory(src/user) add_subdirectory(src/xml) diff --git a/plugin/stl_decoder/CMakeLists.txt b/plugin/stl_decoder/CMakeLists.txt new file mode 100644 index 00000000..1d383632 --- /dev/null +++ b/plugin/stl_decoder/CMakeLists.txt @@ -0,0 +1,54 @@ +# 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. + +if(EMSCRIPTEN) + add_library(stl_decoder OBJECT stl_decoder.cc) +else() + set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) + set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_LIBDIR}") + + add_library(stl_decoder SHARED stl_decoder.cc) +endif() + +target_include_directories(stl_decoder PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../.. + ${CMAKE_CURRENT_SOURCE_DIR}/../../include +) + +if(EMSCRIPTEN) + target_link_libraries(stl_decoder PRIVATE) +else() + target_link_libraries(stl_decoder PRIVATE + mujoco + ) +endif() + +target_compile_options(stl_decoder PRIVATE + ${AVX_COMPILE_OPTIONS} + ${MUJOCO_MACOS_COMPILE_OPTIONS} + ${EXTRA_COMPILE_OPTIONS} + ${MUJOCO_CXX_FLAGS} +) + +if(NOT EMSCRIPTEN) + target_link_options(stl_decoder PRIVATE + ${MUJOCO_MACOS_LINK_OPTIONS} + ${EXTRA_LINK_OPTIONS} + ) + + install( + TARGETS stl_decoder + LIBRARY DESTINATION "${CMAKE_INSTALL_BINDIR}/mujoco_plugin" + ) +endif() diff --git a/plugin/stl_decoder/stl_decoder.cc b/plugin/stl_decoder/stl_decoder.cc new file mode 100644 index 00000000..c6932d9a --- /dev/null +++ b/plugin/stl_decoder/stl_decoder.cc @@ -0,0 +1,143 @@ +// 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 +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +template +void ReadFromBuffer(T* dst, const char* src) { + std::memcpy(dst, src, sizeof(T)); +} + +struct Vec3Key { + int x, y, z; + bool operator<(const Vec3Key& o) const { + if (x != o.x) return x < o.x; + if (y != o.y) return y < o.y; + return z < o.z; + } +}; + +Vec3Key FloatToKey(const float v[3]) { + int x, y, z; + std::memcpy(&x, &v[0], sizeof(int)); + std::memcpy(&y, &v[1], sizeof(int)); + std::memcpy(&z, &v[2], sizeof(int)); + return {x, y, z}; +} + +mjSpec* Decode(mjResource* resource, const mjVFS* vfs) { + const void* bytes = nullptr; + int buffer_sz = mju_readResource(resource, &bytes); + if (buffer_sz < 0) { + mju_warning("stl_decoder: could not read STL file '%s'", resource->name); + return nullptr; + } + if (!buffer_sz) { + mju_warning("stl_decoder: STL file '%s' is empty", resource->name); + return nullptr; + } + + const char* buffer = static_cast(bytes); + + if (buffer_sz < 84) { + mju_warning("stl_decoder: invalid header in STL file '%s'", resource->name); + return nullptr; + } + + int nfaces = 0; + ReadFromBuffer(&nfaces, buffer + 80); + if (nfaces < 1 || nfaces > 200000) { + mju_warning( + "stl_decoder: number of faces should be between 1 and 200000 in STL " + "file '%s'; perhaps this is an ASCII file?", + resource->name); + return nullptr; + } + + if (nfaces * 50 != buffer_sz - 84) { + mju_warning( + "stl_decoder: STL file '%s' has wrong size; perhaps this is an ASCII " + "file?", + resource->name); + return nullptr; + } + + const char* stl = buffer + 84; + + std::vector uservert; + std::vector userface(3 * nfaces, 0); + std::map vertmap; + + for (int i = 0; i < nfaces; i++) { + for (int j = 0; j < 3; j++) { + float v[3]; + ReadFromBuffer(&v, stl + 50 * i + 12 * (j + 1)); + + if (std::fabs(v[0]) > std::pow(2, 30) || + std::fabs(v[1]) > std::pow(2, 30) || + std::fabs(v[2]) > std::pow(2, 30)) { + mju_warning( + "stl_decoder: vertex in STL file '%s' " + "exceeds maximum bounds", + resource->name); + return nullptr; + } + + Vec3Key key = FloatToKey(v); + auto [it, inserted] = vertmap.emplace(key, uservert.size() / 3); + if (inserted) { + uservert.push_back(v[0]); + uservert.push_back(v[1]); + uservert.push_back(v[2]); + } + userface[3 * i + j] = it->second; + } + } + + mjSpec* spec = mj_makeSpec(); + mjsMesh* mesh = mjs_addMesh(spec, nullptr); + + mjs_setString(mesh->file, resource->name); + mjs_setFloat(mesh->uservert, uservert.data(), uservert.size()); + mjs_setInt(mesh->userface, userface.data(), userface.size()); + + return spec; +} + +int CanDecode(const mjResource* resource) { + std::string_view name(resource->name); + return name.ends_with(".stl") || name.ends_with(".STL"); +} + +} // namespace + +mjPLUGIN_LIB_INIT { + mjpDecoder decoder; + mjp_defaultDecoder(&decoder); + decoder.content_type = "model/stl"; + decoder.extension = ".stl"; + decoder.decode = Decode; + decoder.can_decode = CanDecode; + mjp_registerDecoder(&decoder); +} diff --git a/src/user/user_mesh.cc b/src/user/user_mesh.cc index 2379f831..f3c9d19c 100644 --- a/src/user/user_mesh.cc +++ b/src/user/user_mesh.cc @@ -614,10 +614,6 @@ void mjCMesh::ProcessVertices(const std::vector& vert, bool remove_repeat -bool mjCMesh::IsSTL(std::string_view filename, std::string_view ct) { - std::string asset_type = GetAssetContentType(filename, ct); - return asset_type == "model/stl"; -} bool mjCMesh::IsMSH(std::string_view filename, std::string_view ct) { std::string asset_type = GetAssetContentType(filename, ct); @@ -626,9 +622,6 @@ bool mjCMesh::IsMSH(std::string_view filename, std::string_view ct) { -bool mjCMesh::IsSTL() const { - return content_type_ == "model/stl"; -} bool mjCMesh::IsMSH() const { return content_type_ == "model/vnd.mujoco.msh"; @@ -684,9 +677,7 @@ void mjCMesh::LoadFromResource(mjResource* resource, bool remove_repeated) { std::string asset_type = GetAssetContentType(resource->name, content_type_); content_type_ = asset_type; - if (IsSTL()) { - LoadSTL(resource); - } else if (IsMSH()) { + if (IsMSH()) { LoadMSH(resource, remove_repeated); } else { LoadFromDecoder(resource, remove_repeated); @@ -1115,77 +1106,6 @@ bool mjCMesh::LoadCachedMesh(mjCCache *cache, const mjResource* resource) { -// load STL binary mesh -void mjCMesh::LoadSTL(mjResource* resource) { - bool righthand = scale[0] * scale[1] * scale[2] > 0; - - // get file data in buffer - char* buffer = 0; - int buffer_sz = mju_readResource(resource, (const void**)&buffer); - - // still not found - if (buffer_sz < 0) { - throw mjCError(this, "could not read STL file '%s'", resource->name); - } else if (!buffer_sz) { - throw mjCError(this, "STL file '%s' is empty", resource->name); - } - - // make sure there is enough data for header - if (buffer_sz < 84) { - throw mjCError(this, "invalid header in STL file '%s'", resource->name); - } - - // get number of triangles, check bounds - int nfaces = 0; - ReadFromBuffer(&nfaces, buffer + 80); - if (nfaces < 1 || nfaces > 200000) { - throw mjCError(this, - "number of faces should be between 1 and 200000 in STL file '%s';" - " perhaps this is an ASCII file?", resource->name); - } - - // check remaining buffer size - if (nfaces*50 != buffer_sz-84) { - throw mjCError(this, - "STL file '%s' has wrong size; perhaps this is an ASCII file?", - resource->name); - } - - // assign stl data pointer - const char* stl = buffer + 84; - - // allocate face and vertex data - face_.assign(3*nfaces, 0); - std::vector vert; - - // add vertices and faces, including repeated for now - for (int i=0; i < nfaces; i++) { - for (int j=0; j < 3; j++) { - // read vertex coordinates - float v[3]; - ReadFromBuffer(&v, stl + 50*i + 12*(j + 1)); - - // check if vertex can be cast to an int safely - if (fabs(v[0]) > pow(2, 30) || fabs(v[1]) > pow(2, 30) || fabs(v[2]) > pow(2, 30)) { - throw mjCError(this, "vertex in STL file '%s' exceed maximum bounds", resource->name); - } - - // add vertex address in face; change order if scale makes it lefthanded - if (righthand || j == 0) { - face_[3*i + j] = vert.size() / 3; - } else { - face_[3*i + 3 - j] = vert.size() / 3; - } - - // add vertex data - vert.push_back(v[0]); - vert.push_back(v[1]); - vert.push_back(v[2]); - } - } - ProcessVertices(vert, true); -} - // load MSH binary mesh diff --git a/wasm/CMakeLists.txt b/wasm/CMakeLists.txt index fd43498c..b6e28b30 100644 --- a/wasm/CMakeLists.txt +++ b/wasm/CMakeLists.txt @@ -60,6 +60,6 @@ set_target_properties(mujoco_wasm PROPERTIES OUTPUT_NAME "mujoco" ) -target_link_libraries(mujoco_wasm ccd lodepng mujoco tinyxml2 qhullstatic_r obj_decoder) +target_link_libraries(mujoco_wasm ccd lodepng mujoco tinyxml2 qhullstatic_r obj_decoder stl_decoder) install(TARGETS mujoco_wasm DESTINATION ${DIVISIBLE_INSTALL_BIN_DIR}) diff --git a/wasm/tests/CMakeLists.txt b/wasm/tests/CMakeLists.txt index 11ba4c15..0a21ec31 100644 --- a/wasm/tests/CMakeLists.txt +++ b/wasm/tests/CMakeLists.txt @@ -53,6 +53,6 @@ add_executable(mujoco_wasm_benchmark ${MUJOCO_WASM_FILES}) set_target_properties(mujoco_wasm_benchmark PROPERTIES LINK_FLAGS "${EMCC_LINKER_FLAGS_STR}") -target_link_libraries(mujoco_wasm_benchmark ccd lodepng mujoco tinyxml2 qhullstatic_r obj_decoder) +target_link_libraries(mujoco_wasm_benchmark ccd lodepng mujoco tinyxml2 qhullstatic_r obj_decoder stl_decoder) install(TARGETS mujoco_wasm_benchmark DESTINATION ${DIVISIBLE_INSTALL_BIN_DIR})