From 1465d8b6cec816c525b9342f2c986203ea1454ba Mon Sep 17 00:00:00 2001 From: Saran Tunyasuvunakool Date: Thu, 23 Apr 2026 12:55:24 -0700 Subject: [PATCH] Initial implementation of MuJoCo Live. This is a combination of minor tweaks to the existing Studio WASM application, CMake cleanup to make it buildable, and integration with GitHub Actions for deployment. The new features added are drag-and-drop, model specification through `?model=` URL parameter, HTTP and HTTPS resource provider, and a user-visible loading message while the model is loading. PiperOrigin-RevId: 904594610 Change-Id: I3a73b1ca0fcd6fc9ba192469942b2dab010a9308 --- .github/workflows/build_steps.sh | 26 ++++ .github/workflows/live.yml | 51 ++++++ CMakeLists.txt | 40 ++++- cmake/MujocoOptions.cmake | 6 +- cmake/third_party_deps/filament.cmake | 12 +- sample/cmake/SampleOptions.cmake | 6 +- simulate/cmake/SimulateOptions.cmake | 6 +- src/experimental/filament/CMakeLists.txt | 40 +++-- src/experimental/filament/filament/mesh.cc | 44 ++++-- src/experimental/filament/filament/mesh.h | 9 +- src/experimental/studio/CMakeLists.txt | 173 +++++++++++++-------- src/experimental/studio/index.html | 161 ++++++++++++++++--- src/experimental/studio/wasm.cc | 115 ++++++++++++++ src/render/noop/CMakeLists.txt | 4 +- 14 files changed, 573 insertions(+), 120 deletions(-) create mode 100644 .github/workflows/live.yml diff --git a/.github/workflows/build_steps.sh b/.github/workflows/build_steps.sh index c173333f..9e5440f9 100755 --- a/.github/workflows/build_steps.sh +++ b/.github/workflows/build_steps.sh @@ -295,6 +295,32 @@ EOF } +build_mujoco_live() { + echo "Setting up Emscripten SDK..." + source emsdk/emsdk_env.sh + + echo "Building Filament tools, targeting host platform..." + cmake -S . -B build_host -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DUSE_STATIC_LIBCXX=OFF \ + -DMUJOCO_BUILD_STUDIO=ON \ + -DMUJOCO_USE_FILAMENT=ON \ + -DMUJOCO_BUILD_TESTS=OFF \ + -DMUJOCO_BUILD_EXAMPLES=OFF \ + -DMUJOCO_BUILD_SIMULATE=OFF + cmake --build build_host --target matc resgen cmgen mujoco_filament_assets -j$(nproc) + + echo "Building WASM app..." + emcmake cmake -S . -B build_wasm -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DMUJOCO_BUILD_STUDIO=ON \ + -DMUJOCO_USE_FILAMENT=ON \ + -DMUJOCO_BUILD_TESTS_WASM=OFF \ + -DMUJOCO_NATIVE_BUILD_DIR=$(pwd)/build_host + cmake --build build_wasm --target mujoco_live -j$(nproc) +} + + # Discover functions defined in this script by finding identifiers followed by # "()" and capturing the identifier as a valid function name. VALID_FUNCTIONS=() diff --git a/.github/workflows/live.yml b/.github/workflows/live.yml new file mode 100644 index 00000000..49cd5359 --- /dev/null +++ b/.github/workflows/live.yml @@ -0,0 +1,51 @@ +name: live + +on: + push: + branches: + - live + +permissions: + contents: read + pages: write + id-token: write + +jobs: + build-and-upload-artifacts: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Prepare Linux + run: bash ./.github/workflows/build_steps.sh prepare_linux + + - name: Setup Emscripten + run: bash ./.github/workflows/build_steps.sh setup_emsdk + + - name: Build MuJoCo Live + env: + CC: clang-18 + CXX: clang++-18 + run: bash ./.github/workflows/build_steps.sh build_mujoco_live + + - name: Prepare files for GitHub Pages + run: | + mkdir -p dist/bin + cp -r build_wasm/bin/* dist/bin/ + cp src/experimental/studio/index.html dist/index.html + + - name: Upload GitHub Pages artifacts + uses: actions/upload-pages-artifact@v3 + with: + path: dist + + deploy-pages: + needs: build-and-upload-artifacts + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/CMakeLists.txt b/CMakeLists.txt index a2184a1c..c6a8f634 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,12 +53,49 @@ endif() if(EMSCRIPTEN) option(MUJOCO_BUILD_TESTS_WASM "Build tests for WASM bindings" ON) + option(MUJOCO_BUILD_STUDIO "Build studio for MuJoCo (WASM)" OFF) + option(MUJOCO_USE_FILAMENT "Use filament rendering" OFF) option(MUJOCO_WASM_THREADS "Build with multi-threading support" ON) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20 -O3 -fexceptions") if(MUJOCO_WASM_THREADS) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pthread") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread") endif() + + # Filament uses the WEBGL variable (not EMSCRIPTEN) to identify web builds. + # Without this, it falls into the LINUX path and tries to compile with + # futex, X11, Vulkan, etc. + if(MUJOCO_USE_FILAMENT) + set(WEBGL ON CACHE BOOL "Filament WebGL mode" FORCE) + endif() + + # Automatically generate host tool imports for Filament cross-compilation. + # When WEBGL is true, Filament hardcodes the path to: + # ${FILAMENT}/${IMPORT_EXECUTABLES_DIR}/ImportExecutables-Release.cmake + # So we write our file with that exact name and set IMPORT_EXECUTABLES_DIR + # to point at CMAKE_BINARY_DIR (which is ../../ relative to filament-src). + set(MUJOCO_NATIVE_BUILD_DIR "${PROJECT_SOURCE_DIR}/build-host" CACHE PATH "Path to native build directory containing host tools") + find_program(MATC_EXE matc PATHS "${MUJOCO_NATIVE_BUILD_DIR}/bin" NO_DEFAULT_PATH) + find_program(RESGEN_EXE resgen PATHS "${MUJOCO_NATIVE_BUILD_DIR}/bin" NO_DEFAULT_PATH) + find_program(CMGEN_EXE cmgen PATHS "${MUJOCO_NATIVE_BUILD_DIR}/bin" NO_DEFAULT_PATH) + + if(MATC_EXE AND RESGEN_EXE AND CMGEN_EXE) + message(STATUS "Found host tools in ${MUJOCO_NATIVE_BUILD_DIR}/bin") + set(IMPORT_EXECUTABLES_FILE "${CMAKE_BINARY_DIR}/ImportExecutables-Release.cmake") + file(WRITE "${IMPORT_EXECUTABLES_FILE}" + "add_executable(matc IMPORTED)\n" + "set_property(TARGET matc PROPERTY IMPORTED_LOCATION \"${MATC_EXE}\")\n" + "add_executable(resgen IMPORTED)\n" + "set_property(TARGET resgen PROPERTY IMPORTED_LOCATION \"${RESGEN_EXE}\")\n" + "add_executable(cmgen IMPORTED)\n" + "set_property(TARGET cmgen PROPERTY IMPORTED_LOCATION \"${CMGEN_EXE}\")\n" + ) + # Filament's WEBGL path resolves: ${FILAMENT}/${IMPORT_EXECUTABLES_DIR}/ImportExecutables-Release.cmake + # FILAMENT = _deps/filament-src, so ../../ resolves to CMAKE_BINARY_DIR. + set(IMPORT_EXECUTABLES_DIR "../../" CACHE PATH "" FORCE) + else() + message(WARNING "Host tools (matc, resgen, cmgen) not found in ${MUJOCO_NATIVE_BUILD_DIR}/bin. WASM build of Studio might fail.") + endif() endif() if(APPLE AND (MUJOCO_BUILD_EXAMPLES OR MUJOCO_BUILD_SIMULATE)) @@ -125,8 +162,9 @@ if(NOT EMSCRIPTEN AND NOT MUJOCO_USE_FILAMENT_MJR_COMPAT) add_subdirectory(src/render/classic) add_subdirectory(src/ui) endif() +add_subdirectory(src/render/noop) -if(MUJOCO_USE_FILAMENT AND NOT EMSCRIPTEN) +if(MUJOCO_USE_FILAMENT) add_subdirectory(src/experimental/filament) endif() diff --git a/cmake/MujocoOptions.cmake b/cmake/MujocoOptions.cmake index a606220c..74dc340f 100644 --- a/cmake/MujocoOptions.cmake +++ b/cmake/MujocoOptions.cmake @@ -18,7 +18,11 @@ set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -set(CMAKE_C_EXTENSIONS OFF) +if(EMSCRIPTEN) + set(CMAKE_C_EXTENSIONS ON) +else() + set(CMAKE_C_EXTENSIONS OFF) +endif() set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # For LLVM tooling if(NOT CMAKE_CONFIGURATION_TYPES) diff --git a/cmake/third_party_deps/filament.cmake b/cmake/third_party_deps/filament.cmake index 007bc23b..0c94d53f 100644 --- a/cmake/third_party_deps/filament.cmake +++ b/cmake/third_party_deps/filament.cmake @@ -13,7 +13,7 @@ # limitations under the License. set(MUJOCO_DEP_VERSION_filament - a4945939de514d049baeed654efbbdd06bc5bdbf + 06793c4a80dd467025b2db1b3b7ea63bf1a865bb CACHE STRING "Tag/version of `filament` to be fetched." ) mark_as_advanced(MUJOCO_DEP_VERSION_filament) @@ -23,6 +23,15 @@ include(FindOrFetch) set(BUILD_SHARED_LIBS_OLD ${BUILD_SHARED_LIBS}) set(BUILD_SHARED_LIBS OFF) +# Filament's ShaderMinifier.cpp uses strlen without including , and +# PostProcessManager.h uses std::optional without including . +set(CMAKE_CXX_FLAGS_OLD "${CMAKE_CXX_FLAGS}") +if(MSVC) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /FI cstring /FI optional") +else() + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -include cstring -include optional") +endif() + set(FILAMENT_ENABLE_EXPERIMENTAL_GCC_SUPPORT ON) set(FILAMENT_SKIP_SDL2 ON) set(FILAMENT_USE_EXTERNAL_ABSL ON) @@ -39,3 +48,4 @@ fetchpackage( ) set(BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS_OLD}) +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS_OLD}") diff --git a/sample/cmake/SampleOptions.cmake b/sample/cmake/SampleOptions.cmake index a606220c..74dc340f 100644 --- a/sample/cmake/SampleOptions.cmake +++ b/sample/cmake/SampleOptions.cmake @@ -18,7 +18,11 @@ set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -set(CMAKE_C_EXTENSIONS OFF) +if(EMSCRIPTEN) + set(CMAKE_C_EXTENSIONS ON) +else() + set(CMAKE_C_EXTENSIONS OFF) +endif() set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # For LLVM tooling if(NOT CMAKE_CONFIGURATION_TYPES) diff --git a/simulate/cmake/SimulateOptions.cmake b/simulate/cmake/SimulateOptions.cmake index a606220c..74dc340f 100644 --- a/simulate/cmake/SimulateOptions.cmake +++ b/simulate/cmake/SimulateOptions.cmake @@ -18,7 +18,11 @@ set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) -set(CMAKE_C_EXTENSIONS OFF) +if(EMSCRIPTEN) + set(CMAKE_C_EXTENSIONS ON) +else() + set(CMAKE_C_EXTENSIONS OFF) +endif() set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # For LLVM tooling if(NOT CMAKE_CONFIGURATION_TYPES) diff --git a/src/experimental/filament/CMakeLists.txt b/src/experimental/filament/CMakeLists.txt index 59e8432f..9e69f162 100644 --- a/src/experimental/filament/CMakeLists.txt +++ b/src/experimental/filament/CMakeLists.txt @@ -129,20 +129,32 @@ foreach(MATERIAL_FILE ${MATERIAL_FILES}) set(INPUT_FILE "${ASSETS_DIR}/${MATERIAL_FILE}") set(OUTPUT_FILE "${OUTPUT_ASSETS_DIR}/${MATERIAL_NAME}.filamat") - add_custom_command( - OUTPUT ${OUTPUT_FILE} - COMMAND ${MATC_EXECUTABLE} - --platform=all - --api=vulkan - --api=opengl - --variant-filter skinning - --optimize-size - --output ${OUTPUT_FILE} - ${INPUT_FILE} - DEPENDS ${INPUT_FILE} - DEPENDS matc - COMMENT "Compiling ${MATERIAL_FILE}" - ) + if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") + set(PRECOMPILED_FILE "${MUJOCO_NATIVE_BUILD_DIR}/src/experimental/filament/assets/${MATERIAL_NAME}.filamat") + add_custom_command( + OUTPUT ${OUTPUT_FILE} + COMMAND ${CMAKE_COMMAND} -E copy + ${PRECOMPILED_FILE} + ${OUTPUT_FILE} + DEPENDS ${PRECOMPILED_FILE} + COMMENT "Copying precompiled material ${MATERIAL_NAME}.filamat" + ) + else() + add_custom_command( + OUTPUT ${OUTPUT_FILE} + COMMAND ${MATC_EXECUTABLE} + --platform=all + --api=vulkan + --api=opengl + --variant-filter skinning + --optimize-size + --output ${OUTPUT_FILE} + ${INPUT_FILE} + DEPENDS ${INPUT_FILE} + DEPENDS matc + COMMENT "Compiling ${MATERIAL_FILE}" + ) + endif() list(APPEND MUJOCO_FILAMENT_ASSET_FILES ${OUTPUT_FILE}) endforeach() diff --git a/src/experimental/filament/filament/mesh.cc b/src/experimental/filament/filament/mesh.cc index 1e958658..e38c4399 100644 --- a/src/experimental/filament/filament/mesh.cc +++ b/src/experimental/filament/filament/mesh.cc @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include #include @@ -105,7 +107,7 @@ void mjr_defaultMeshData(mjrMeshData* data) { } Mesh::Mesh(filament::Engine* engine, const mjrMeshData& data) - : engine_(engine) { + : engine_(engine), shared_state_(std::make_shared()) { type_ = data.primitive_type == mjMESH_PRIMITIVE_TYPE_TRIANGLES ? filament::RenderableManager::PrimitiveType::TRIANGLES : filament::RenderableManager::PrimitiveType::LINES; @@ -113,7 +115,7 @@ Mesh::Mesh(filament::Engine* engine, const mjrMeshData& data) // If the user has provided a release callback, then we need to ensure we // call is when filament is done with the mesh data. if (data.release_callback) { - release_callbacks_.push_back([=]() { + shared_state_->callbacks.push_back([=]() { data.release_callback(data.user_data); }); } @@ -138,11 +140,21 @@ void Mesh::BuildVertexBuffer(const mjrMeshData& data) { mju_error("mjrMeshData has no vertices."); } - // The filament BufferDescriptor callback for releasing the memory. We assume - // that ReleaseResources() can be called multiple times, so we assign this - // callback to each buffer descriptor. + // The filament BufferDescriptor callback for releasing the memory. + // We pass a heap-allocated shared_ptr to the shared state as the user data. auto callback = +[](void* buffer, size_t size, void* user) { - static_cast(user)->ReleaseResources(); + auto* state_ptr = static_cast*>(user); + auto state = *state_ptr; + delete state_ptr; + + std::lock_guard lock(state->mutex); + if (!state->called) { + for (const auto& cb : state->callbacks) { + cb(); + } + state->callbacks.clear(); + state->called = true; + } }; // Pointers to specific attributes in the mesh data, used for additional @@ -206,7 +218,8 @@ void Mesh::BuildVertexBuffer(const mjrMeshData& data) { attributes_[i] = usage; } vertex_buffer_ = vb_builder.build(*engine_); - vertex_buffer_->setBufferAt(*engine_, 0, {bytes, nbytes, callback, this}); + auto* user_data = new std::shared_ptr(shared_state_); + vertex_buffer_->setBufferAt(*engine_, 0, {bytes, nbytes, callback, user_data}); } else { // For a non-interleaved vertex buffer, we assign a separate buffer to each // attribute. @@ -238,7 +251,8 @@ void Mesh::BuildVertexBuffer(const mjrMeshData& data) { nbytes = data.nvertices * sizeof(float4); bytes = BuildOrientationsFromNormals(data.nvertices, attrib); } - vertex_buffer_->setBufferAt(*engine_, i, {bytes, nbytes, callback, this}); + auto* user_data = new std::shared_ptr(shared_state_); + vertex_buffer_->setBufferAt(*engine_, i, {bytes, nbytes, callback, user_data}); } } } @@ -259,7 +273,7 @@ void Mesh::BuildIndexBuffer(const mjrMeshData& data) { const void* indices = data.indices; if (indices == nullptr) { std::byte* sequence = new std::byte[num_bytes]; - release_callbacks_.push_back([=]() { + shared_state_->callbacks.push_back([=]() { delete[] sequence; }); @@ -286,7 +300,7 @@ void Mesh::BuildIndexBuffer(const mjrMeshData& data) { float4* Mesh::BuildOrientationsFromNormals(int nvertices, const mjrVertexAttribute& normals) { float4* orientations = new float4[nvertices]; - release_callbacks_.push_back([=]() { + shared_state_->callbacks.push_back([=]() { delete[] orientations; }); const float* normals_ptr = reinterpret_cast(normals.bytes); @@ -321,10 +335,14 @@ void Mesh::UpdateBounds(const mjrMeshData& data) { } void Mesh::ReleaseResources() { - for (const auto& callback : release_callbacks_) { - callback(); + std::lock_guard lock(shared_state_->mutex); + if (!shared_state_->called) { + for (const auto& callback : shared_state_->callbacks) { + callback(); + } + shared_state_->callbacks.clear(); + shared_state_->called = true; } - release_callbacks_.clear(); } filament::IndexBuffer* Mesh::GetFilamentIndexBuffer() const { diff --git a/src/experimental/filament/filament/mesh.h b/src/experimental/filament/filament/mesh.h index ab259bc4..ba15d8ca 100644 --- a/src/experimental/filament/filament/mesh.h +++ b/src/experimental/filament/filament/mesh.h @@ -18,6 +18,8 @@ #include #include #include +#include +#include #include #include #include @@ -180,7 +182,12 @@ class Mesh { filament::RenderableManager::PrimitiveType type_ = filament::RenderableManager::PrimitiveType::TRIANGLES; std::optional bounds_; - std::vector> release_callbacks_; + struct SharedState { + std::vector> callbacks; + std::mutex mutex; + bool called = false; + }; + std::shared_ptr shared_state_; std::array attributes_; int num_attributes_ = 0; }; diff --git a/src/experimental/studio/CMakeLists.txt b/src/experimental/studio/CMakeLists.txt index cafa6a66..6dfd90f5 100644 --- a/src/experimental/studio/CMakeLists.txt +++ b/src/experimental/studio/CMakeLists.txt @@ -14,87 +14,124 @@ cmake_minimum_required(VERSION 3.16) -set(MUJOCO_STUDIO_TARGET_NAME mujoco_studio) - -add_executable(${MUJOCO_STUDIO_TARGET_NAME}) - -target_sources(${MUJOCO_STUDIO_TARGET_NAME} - PRIVATE - app.cc - app.h - main.cc -) - -target_include_directories(${MUJOCO_STUDIO_TARGET_NAME} - PUBLIC - ${PROJECT_SOURCE_DIR}/include - ${PROJECT_SOURCE_DIR}/src -) - -if (WIN32) - target_compile_definitions(${MUJOCO_STUDIO_TARGET_NAME} - PRIVATE - -D_USE_MATH_DEFINES - ) - set_target_properties(${MUJOCO_STUDIO_TARGET_NAME} - PROPERTIES - VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}" - ) -endif() - include(third_party_deps/dear_imgui) include(third_party_deps/implot) include(third_party_deps/opensans) include(third_party_deps/font_awesome) -target_link_libraries(${MUJOCO_STUDIO_TARGET_NAME} - PRIVATE - absl::flags - absl::flags_parse - dear_imgui - implot - mujoco::mujoco - mujoco::platform -) - -# TODO: re-enable mjz support on Windows builds once DllMain issue is resolved. -if (NOT WIN32) - target_link_libraries(${MUJOCO_STUDIO_TARGET_NAME} +# Common configuration shared between mujoco_studio and mujoco_live. +function(configure_studio_target TARGET_NAME) + target_sources(${TARGET_NAME} PRIVATE - mujoco::mjz + app.cc + app.h ) -endif() -file(MAKE_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/assets) + target_include_directories(${TARGET_NAME} + PUBLIC + ${PROJECT_SOURCE_DIR}/include + ${PROJECT_SOURCE_DIR}/src + ) -add_custom_command( - TARGET ${MUJOCO_STUDIO_TARGET_NAME} - POST_BUILD - COMMAND ${CMAKE_COMMAND} - -E copy - ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/../_deps/opensans-src/fonts/ttf/OpenSans-Regular.ttf - ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/assets - COMMENT "Copying OpenSans-Regular.ttf assets to build directory" -) -add_custom_command( - TARGET ${MUJOCO_STUDIO_TARGET_NAME} - POST_BUILD - COMMAND ${CMAKE_COMMAND} - -E copy - ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/../_deps/font_awesome-src/fonts/fontawesome-webfont.ttf - ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/assets - COMMENT "Copying fontawesome-webfont.ttf to build directory" -) + if (WIN32) + target_compile_definitions(${TARGET_NAME} + PRIVATE + -D_USE_MATH_DEFINES + ) + set_target_properties(${TARGET_NAME} + PROPERTIES + VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}" + ) + endif() + + target_link_libraries(${TARGET_NAME} + PRIVATE + absl::flags + absl::flags_parse + dear_imgui + implot + mujoco::mujoco + mujoco::platform + ) + + # TODO: re-enable mjz support on Windows builds once DllMain issue is resolved. + if (NOT WIN32) + target_link_libraries(${TARGET_NAME} + PRIVATE + mujoco::mjz + ) + endif() + + file(MAKE_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/assets) -# Filament backend requires additional files to be copied into an "assets" folder. -if(MUJOCO_USE_FILAMENT) add_custom_command( - TARGET ${MUJOCO_STUDIO_TARGET_NAME} + TARGET ${TARGET_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} - -E copy_directory - ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/../src/experimental/filament/assets + -E copy + ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/../_deps/opensans-src/fonts/ttf/OpenSans-Regular.ttf ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/assets - COMMENT "Copying Filament assets to build directory" + COMMENT "Copying OpenSans-Regular.ttf assets to build directory" + ) + add_custom_command( + TARGET ${TARGET_NAME} + POST_BUILD + COMMAND ${CMAKE_COMMAND} + -E copy + ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/../_deps/font_awesome-src/fonts/fontawesome-webfont.ttf + ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/assets + COMMENT "Copying fontawesome-webfont.ttf to build directory" + ) + + # Filament backend requires additional files to be copied into an "assets" folder. + if(MUJOCO_USE_FILAMENT) + add_custom_command( + TARGET ${TARGET_NAME} + POST_BUILD + COMMAND ${CMAKE_COMMAND} + -E copy_directory + ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/../src/experimental/filament/assets + ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/assets + COMMENT "Copying Filament assets to build directory" + ) + endif() +endfunction() + +# Desktop Studio target. +if(NOT EMSCRIPTEN) + add_executable(mujoco_studio) + target_sources(mujoco_studio PRIVATE main.cc) + configure_studio_target(mujoco_studio) +endif() + +# WASM Live target. +if(EMSCRIPTEN) + add_executable(mujoco_live) + target_sources(mujoco_live PRIVATE wasm.cc) + target_compile_options(mujoco_live PRIVATE -g) + configure_studio_target(mujoco_live) + target_link_libraries(mujoco_live PRIVATE mujoco::render_noop) + + # Ensure resource decoder plugins are linked into the WASM binary. + target_link_options(mujoco_live PRIVATE + -Wl,--whole-archive $ -Wl,--no-whole-archive + ) + + # Filament's OpenGL backend requires WebGL2 / full ES3 bindings. + target_link_options(mujoco_live PRIVATE + --bind + -sUSE_WEBGL2=1 + -sFULL_ES3 + -sMIN_WEBGL_VERSION=2 + -sMAX_WEBGL_VERSION=2 + -sALLOW_MEMORY_GROWTH=1 + -sASYNCIFY=1 + -sFETCH=1 + -sGL_PREINITIALIZED_CONTEXT=1 + -sSTACK_SIZE=512mb + -sINITIAL_MEMORY=1024mb + -sASSERTIONS=1 + -fexceptions + -g ) endif() diff --git a/src/experimental/studio/index.html b/src/experimental/studio/index.html index 8eb3190f..ec40e14e 100644 --- a/src/experimental/studio/index.html +++ b/src/experimental/studio/index.html @@ -1,8 +1,8 @@ - + - MuJoCo Studio! + MuJoCo Live
@@ -16,6 +16,28 @@ style="width: 100vw; height: 100vh; display: block" > - + diff --git a/src/experimental/studio/wasm.cc b/src/experimental/studio/wasm.cc index 8e5e8768..29da5294 100644 --- a/src/experimental/studio/wasm.cc +++ b/src/experimental/studio/wasm.cc @@ -18,7 +18,11 @@ #include #include +#include +#include +#include #include +#include #include #include #include @@ -59,6 +63,84 @@ class AssetRegistry { std::unordered_map assets_; }; +// --------------------------------------------------------------------------- +// HTTP/HTTPS resource fetching via the JS fetch API (uses ASYNCIFY to yield). +// --------------------------------------------------------------------------- + +// Fetches a URL using the JS fetch API. Returns a malloc'd buffer and its size. +// The caller is responsible for freeing the buffer. Returns 0 on failure. +EM_ASYNC_JS(int, FetchUrl, + (const char* url, char** out_data, std::int32_t* out_size), { + try { + const urlStr = UTF8ToString(url); + const response = await fetch(urlStr); + if (!response.ok) { + console.error('Fetch failed: ' + response.status + ' ' + + urlStr); + return 0; + } + const buffer = await response.arrayBuffer(); + const bytes = new Uint8Array(buffer); + const ptr = _malloc(bytes.length); + HEAPU8.set(bytes, ptr); + setValue(out_data, ptr, '*'); + setValue(out_size, bytes.length, 'i32'); + return 1; + } catch (e) { + console.error('Fetch error:', e); + return 0; + } + }); + +// Cache for data fetched via HTTP/HTTPS. Stores the downloaded bytes keyed by +// the resource name (URL) so that read() can return a pointer to the data. +class FetchCache { + public: + static FetchCache& Instance() { + static FetchCache instance; + return instance; + } + + // Fetches the URL and stores the result. Returns the size (>0) on success. + int Fetch(const char* url) { + char* data = nullptr; + std::int32_t size = 0; + if (!FetchUrl(url, &data, &size)) { + return 0; + } + entries_[url] = Entry{UniquePtrWasm(data), size}; + return size; + } + + // Returns pointer and size for a previously fetched URL. + int Read(const char* url, const void** buffer) { + auto it = entries_.find(url); + if (it == entries_.end()) { + return -1; + } + *buffer = it->second.data.get(); + return it->second.size; + } + + // Frees the data for a URL. + void Close(const char* url) { entries_.erase(url); } + + private: + struct FreeDeleter { + void operator()(void* p) const { std::free(p); } + }; + template + using UniquePtrWasm = std::unique_ptr; + + struct Entry { + UniquePtrWasm data; + int size; + }; + std::unordered_map entries_; +}; + +// --------------------------------------------------------------------------- + // Javascript-facing function to register an asset. void RegisterAsset(std::string filename, std::string contents) { AssetRegistry::Instance().RegisterAsset(std::move(filename), @@ -92,6 +174,27 @@ void Init() { resource_provider.prefix = "filament"; mjp_registerResourceProvider(&resource_provider); + // Register HTTP/HTTPS resource providers so that models loaded from URLs + // can automatically fetch referenced assets (meshes, textures, etc.) over + // the network. + mjpResourceProvider http_provider; + mjp_defaultResourceProvider(&http_provider); + + http_provider.open = [](mjResource* resource) { + return FetchCache::Instance().Fetch(resource->name); + }; + http_provider.read = [](mjResource* resource, const void** buffer) { + return FetchCache::Instance().Read(resource->name, buffer); + }; + http_provider.close = [](mjResource* resource) { + FetchCache::Instance().Close(resource->name); + }; + + http_provider.prefix = "http"; + mjp_registerResourceProvider(&http_provider); + http_provider.prefix = "https"; + mjp_registerResourceProvider(&http_provider); + g_app = new mujoco::studio::App({ .width = width, .height = height, @@ -124,6 +227,17 @@ void LoadFile(const std::string& filename, const std::string& data) { g_app->LoadModelFromBuffer({ptr, ptr + data.size()}, content_type, filename); } +// Javascript-facing function to load a model from a URL. +// The URL is passed directly to LoadModelFromFile, which will use the +// registered HTTP/HTTPS resource providers to fetch the model and any +// referenced assets. +void LoadUrl(const std::string& url) { + if (!g_app) { + return; + } + g_app->LoadModelFromFile(url); +} + // Javascript-facing function to render a single frame. void RenderFrame() { if (g_app) { @@ -144,6 +258,7 @@ EMSCRIPTEN_BINDINGS(studio_bindings) { emscripten::function("registerAsset", &RegisterAsset); emscripten::function("init", &Init); emscripten::function("loadFile", &LoadFile); + emscripten::function("loadUrl", &LoadUrl); emscripten::function("renderFrame", &RenderFrame); emscripten::function("deinit", &Deinit); } diff --git a/src/render/noop/CMakeLists.txt b/src/render/noop/CMakeLists.txt index 782c4858..313ca063 100644 --- a/src/render/noop/CMakeLists.txt +++ b/src/render/noop/CMakeLists.txt @@ -12,4 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -target_sources(mujoco PRIVATE render_noop.c) +add_library(render_noop STATIC render_noop.c) +target_link_libraries(render_noop PUBLIC mujoco::mujoco) +add_library(mujoco::render_noop ALIAS render_noop)