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
This commit is contained in:
committed by
Copybara-Service
parent
4eba85093a
commit
1465d8b6ce
@@ -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
|
# Discover functions defined in this script by finding identifiers followed by
|
||||||
# "()" and capturing the identifier as a valid function name.
|
# "()" and capturing the identifier as a valid function name.
|
||||||
VALID_FUNCTIONS=()
|
VALID_FUNCTIONS=()
|
||||||
|
|||||||
@@ -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
|
||||||
+39
-1
@@ -53,12 +53,49 @@ endif()
|
|||||||
|
|
||||||
if(EMSCRIPTEN)
|
if(EMSCRIPTEN)
|
||||||
option(MUJOCO_BUILD_TESTS_WASM "Build tests for WASM bindings" ON)
|
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)
|
option(MUJOCO_WASM_THREADS "Build with multi-threading support" ON)
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20 -O3 -fexceptions")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20 -O3 -fexceptions")
|
||||||
if(MUJOCO_WASM_THREADS)
|
if(MUJOCO_WASM_THREADS)
|
||||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pthread")
|
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pthread")
|
||||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread")
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread")
|
||||||
endif()
|
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()
|
endif()
|
||||||
|
|
||||||
if(APPLE AND (MUJOCO_BUILD_EXAMPLES OR MUJOCO_BUILD_SIMULATE))
|
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/render/classic)
|
||||||
add_subdirectory(src/ui)
|
add_subdirectory(src/ui)
|
||||||
endif()
|
endif()
|
||||||
|
add_subdirectory(src/render/noop)
|
||||||
|
|
||||||
if(MUJOCO_USE_FILAMENT AND NOT EMSCRIPTEN)
|
if(MUJOCO_USE_FILAMENT)
|
||||||
add_subdirectory(src/experimental/filament)
|
add_subdirectory(src/experimental/filament)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
|
|||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
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
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # For LLVM tooling
|
||||||
|
|
||||||
if(NOT CMAKE_CONFIGURATION_TYPES)
|
if(NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
set(MUJOCO_DEP_VERSION_filament
|
set(MUJOCO_DEP_VERSION_filament
|
||||||
a4945939de514d049baeed654efbbdd06bc5bdbf
|
06793c4a80dd467025b2db1b3b7ea63bf1a865bb
|
||||||
CACHE STRING "Tag/version of `filament` to be fetched."
|
CACHE STRING "Tag/version of `filament` to be fetched."
|
||||||
)
|
)
|
||||||
mark_as_advanced(MUJOCO_DEP_VERSION_filament)
|
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_OLD ${BUILD_SHARED_LIBS})
|
||||||
set(BUILD_SHARED_LIBS OFF)
|
set(BUILD_SHARED_LIBS OFF)
|
||||||
|
|
||||||
|
# Filament's ShaderMinifier.cpp uses strlen without including <cstring>, and
|
||||||
|
# PostProcessManager.h uses std::optional without including <optional>.
|
||||||
|
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_ENABLE_EXPERIMENTAL_GCC_SUPPORT ON)
|
||||||
set(FILAMENT_SKIP_SDL2 ON)
|
set(FILAMENT_SKIP_SDL2 ON)
|
||||||
set(FILAMENT_USE_EXTERNAL_ABSL ON)
|
set(FILAMENT_USE_EXTERNAL_ABSL ON)
|
||||||
@@ -39,3 +48,4 @@ fetchpackage(
|
|||||||
)
|
)
|
||||||
|
|
||||||
set(BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS_OLD})
|
set(BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS_OLD})
|
||||||
|
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS_OLD}")
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
|
|||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
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
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # For LLVM tooling
|
||||||
|
|
||||||
if(NOT CMAKE_CONFIGURATION_TYPES)
|
if(NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
|
|||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
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
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # For LLVM tooling
|
||||||
|
|
||||||
if(NOT CMAKE_CONFIGURATION_TYPES)
|
if(NOT CMAKE_CONFIGURATION_TYPES)
|
||||||
|
|||||||
@@ -129,20 +129,32 @@ foreach(MATERIAL_FILE ${MATERIAL_FILES})
|
|||||||
set(INPUT_FILE "${ASSETS_DIR}/${MATERIAL_FILE}")
|
set(INPUT_FILE "${ASSETS_DIR}/${MATERIAL_FILE}")
|
||||||
set(OUTPUT_FILE "${OUTPUT_ASSETS_DIR}/${MATERIAL_NAME}.filamat")
|
set(OUTPUT_FILE "${OUTPUT_ASSETS_DIR}/${MATERIAL_NAME}.filamat")
|
||||||
|
|
||||||
add_custom_command(
|
if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
|
||||||
OUTPUT ${OUTPUT_FILE}
|
set(PRECOMPILED_FILE "${MUJOCO_NATIVE_BUILD_DIR}/src/experimental/filament/assets/${MATERIAL_NAME}.filamat")
|
||||||
COMMAND ${MATC_EXECUTABLE}
|
add_custom_command(
|
||||||
--platform=all
|
OUTPUT ${OUTPUT_FILE}
|
||||||
--api=vulkan
|
COMMAND ${CMAKE_COMMAND} -E copy
|
||||||
--api=opengl
|
${PRECOMPILED_FILE}
|
||||||
--variant-filter skinning
|
${OUTPUT_FILE}
|
||||||
--optimize-size
|
DEPENDS ${PRECOMPILED_FILE}
|
||||||
--output ${OUTPUT_FILE}
|
COMMENT "Copying precompiled material ${MATERIAL_NAME}.filamat"
|
||||||
${INPUT_FILE}
|
)
|
||||||
DEPENDS ${INPUT_FILE}
|
else()
|
||||||
DEPENDS matc
|
add_custom_command(
|
||||||
COMMENT "Compiling ${MATERIAL_FILE}"
|
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})
|
list(APPEND MUJOCO_FILAMENT_ASSET_FILES ${OUTPUT_FILE})
|
||||||
endforeach()
|
endforeach()
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
#include <span>
|
#include <span>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
@@ -105,7 +107,7 @@ void mjr_defaultMeshData(mjrMeshData* data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Mesh::Mesh(filament::Engine* engine, const mjrMeshData& data)
|
Mesh::Mesh(filament::Engine* engine, const mjrMeshData& data)
|
||||||
: engine_(engine) {
|
: engine_(engine), shared_state_(std::make_shared<SharedState>()) {
|
||||||
type_ = data.primitive_type == mjMESH_PRIMITIVE_TYPE_TRIANGLES
|
type_ = data.primitive_type == mjMESH_PRIMITIVE_TYPE_TRIANGLES
|
||||||
? filament::RenderableManager::PrimitiveType::TRIANGLES
|
? filament::RenderableManager::PrimitiveType::TRIANGLES
|
||||||
: filament::RenderableManager::PrimitiveType::LINES;
|
: 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
|
// If the user has provided a release callback, then we need to ensure we
|
||||||
// call is when filament is done with the mesh data.
|
// call is when filament is done with the mesh data.
|
||||||
if (data.release_callback) {
|
if (data.release_callback) {
|
||||||
release_callbacks_.push_back([=]() {
|
shared_state_->callbacks.push_back([=]() {
|
||||||
data.release_callback(data.user_data);
|
data.release_callback(data.user_data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -138,11 +140,21 @@ void Mesh::BuildVertexBuffer(const mjrMeshData& data) {
|
|||||||
mju_error("mjrMeshData has no vertices.");
|
mju_error("mjrMeshData has no vertices.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// The filament BufferDescriptor callback for releasing the memory. We assume
|
// The filament BufferDescriptor callback for releasing the memory.
|
||||||
// that ReleaseResources() can be called multiple times, so we assign this
|
// We pass a heap-allocated shared_ptr to the shared state as the user data.
|
||||||
// callback to each buffer descriptor.
|
|
||||||
auto callback = +[](void* buffer, size_t size, void* user) {
|
auto callback = +[](void* buffer, size_t size, void* user) {
|
||||||
static_cast<Mesh*>(user)->ReleaseResources();
|
auto* state_ptr = static_cast<std::shared_ptr<SharedState>*>(user);
|
||||||
|
auto state = *state_ptr;
|
||||||
|
delete state_ptr;
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> 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
|
// Pointers to specific attributes in the mesh data, used for additional
|
||||||
@@ -206,7 +218,8 @@ void Mesh::BuildVertexBuffer(const mjrMeshData& data) {
|
|||||||
attributes_[i] = usage;
|
attributes_[i] = usage;
|
||||||
}
|
}
|
||||||
vertex_buffer_ = vb_builder.build(*engine_);
|
vertex_buffer_ = vb_builder.build(*engine_);
|
||||||
vertex_buffer_->setBufferAt(*engine_, 0, {bytes, nbytes, callback, this});
|
auto* user_data = new std::shared_ptr<SharedState>(shared_state_);
|
||||||
|
vertex_buffer_->setBufferAt(*engine_, 0, {bytes, nbytes, callback, user_data});
|
||||||
} else {
|
} else {
|
||||||
// For a non-interleaved vertex buffer, we assign a separate buffer to each
|
// For a non-interleaved vertex buffer, we assign a separate buffer to each
|
||||||
// attribute.
|
// attribute.
|
||||||
@@ -238,7 +251,8 @@ void Mesh::BuildVertexBuffer(const mjrMeshData& data) {
|
|||||||
nbytes = data.nvertices * sizeof(float4);
|
nbytes = data.nvertices * sizeof(float4);
|
||||||
bytes = BuildOrientationsFromNormals(data.nvertices, attrib);
|
bytes = BuildOrientationsFromNormals(data.nvertices, attrib);
|
||||||
}
|
}
|
||||||
vertex_buffer_->setBufferAt(*engine_, i, {bytes, nbytes, callback, this});
|
auto* user_data = new std::shared_ptr<SharedState>(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;
|
const void* indices = data.indices;
|
||||||
if (indices == nullptr) {
|
if (indices == nullptr) {
|
||||||
std::byte* sequence = new std::byte[num_bytes];
|
std::byte* sequence = new std::byte[num_bytes];
|
||||||
release_callbacks_.push_back([=]() {
|
shared_state_->callbacks.push_back([=]() {
|
||||||
delete[] sequence;
|
delete[] sequence;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -286,7 +300,7 @@ void Mesh::BuildIndexBuffer(const mjrMeshData& data) {
|
|||||||
float4* Mesh::BuildOrientationsFromNormals(int nvertices,
|
float4* Mesh::BuildOrientationsFromNormals(int nvertices,
|
||||||
const mjrVertexAttribute& normals) {
|
const mjrVertexAttribute& normals) {
|
||||||
float4* orientations = new float4[nvertices];
|
float4* orientations = new float4[nvertices];
|
||||||
release_callbacks_.push_back([=]() {
|
shared_state_->callbacks.push_back([=]() {
|
||||||
delete[] orientations;
|
delete[] orientations;
|
||||||
});
|
});
|
||||||
const float* normals_ptr = reinterpret_cast<const float*>(normals.bytes);
|
const float* normals_ptr = reinterpret_cast<const float*>(normals.bytes);
|
||||||
@@ -321,10 +335,14 @@ void Mesh::UpdateBounds(const mjrMeshData& data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Mesh::ReleaseResources() {
|
void Mesh::ReleaseResources() {
|
||||||
for (const auto& callback : release_callbacks_) {
|
std::lock_guard<std::mutex> lock(shared_state_->mutex);
|
||||||
callback();
|
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 {
|
filament::IndexBuffer* Mesh::GetFilamentIndexBuffer() const {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
#include <array>
|
#include <array>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <span>
|
#include <span>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -180,7 +182,12 @@ class Mesh {
|
|||||||
filament::RenderableManager::PrimitiveType type_ =
|
filament::RenderableManager::PrimitiveType type_ =
|
||||||
filament::RenderableManager::PrimitiveType::TRIANGLES;
|
filament::RenderableManager::PrimitiveType::TRIANGLES;
|
||||||
std::optional<filament::Box> bounds_;
|
std::optional<filament::Box> bounds_;
|
||||||
std::vector<std::function<void()>> release_callbacks_;
|
struct SharedState {
|
||||||
|
std::vector<std::function<void()>> callbacks;
|
||||||
|
std::mutex mutex;
|
||||||
|
bool called = false;
|
||||||
|
};
|
||||||
|
std::shared_ptr<SharedState> shared_state_;
|
||||||
std::array<filament::VertexAttribute, mjMAX_VERTEX_ATTRIBUTES> attributes_;
|
std::array<filament::VertexAttribute, mjMAX_VERTEX_ATTRIBUTES> attributes_;
|
||||||
int num_attributes_ = 0;
|
int num_attributes_ = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,87 +14,124 @@
|
|||||||
|
|
||||||
cmake_minimum_required(VERSION 3.16)
|
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/dear_imgui)
|
||||||
include(third_party_deps/implot)
|
include(third_party_deps/implot)
|
||||||
include(third_party_deps/opensans)
|
include(third_party_deps/opensans)
|
||||||
include(third_party_deps/font_awesome)
|
include(third_party_deps/font_awesome)
|
||||||
|
|
||||||
target_link_libraries(${MUJOCO_STUDIO_TARGET_NAME}
|
# Common configuration shared between mujoco_studio and mujoco_live.
|
||||||
PRIVATE
|
function(configure_studio_target TARGET_NAME)
|
||||||
absl::flags
|
target_sources(${TARGET_NAME}
|
||||||
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}
|
|
||||||
PRIVATE
|
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(
|
if (WIN32)
|
||||||
TARGET ${MUJOCO_STUDIO_TARGET_NAME}
|
target_compile_definitions(${TARGET_NAME}
|
||||||
POST_BUILD
|
PRIVATE
|
||||||
COMMAND ${CMAKE_COMMAND}
|
-D_USE_MATH_DEFINES
|
||||||
-E copy
|
)
|
||||||
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/../_deps/opensans-src/fonts/ttf/OpenSans-Regular.ttf
|
set_target_properties(${TARGET_NAME}
|
||||||
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/assets
|
PROPERTIES
|
||||||
COMMENT "Copying OpenSans-Regular.ttf assets to build directory"
|
VS_DEBUGGER_WORKING_DIRECTORY "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}"
|
||||||
)
|
)
|
||||||
add_custom_command(
|
endif()
|
||||||
TARGET ${MUJOCO_STUDIO_TARGET_NAME}
|
|
||||||
POST_BUILD
|
target_link_libraries(${TARGET_NAME}
|
||||||
COMMAND ${CMAKE_COMMAND}
|
PRIVATE
|
||||||
-E copy
|
absl::flags
|
||||||
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/../_deps/font_awesome-src/fonts/fontawesome-webfont.ttf
|
absl::flags_parse
|
||||||
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/assets
|
dear_imgui
|
||||||
COMMENT "Copying fontawesome-webfont.ttf to build directory"
|
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(
|
add_custom_command(
|
||||||
TARGET ${MUJOCO_STUDIO_TARGET_NAME}
|
TARGET ${TARGET_NAME}
|
||||||
POST_BUILD
|
POST_BUILD
|
||||||
COMMAND ${CMAKE_COMMAND}
|
COMMAND ${CMAKE_COMMAND}
|
||||||
-E copy_directory
|
-E copy
|
||||||
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/../src/experimental/filament/assets
|
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/../_deps/opensans-src/fonts/ttf/OpenSans-Regular.ttf
|
||||||
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/assets
|
${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 $<TARGET_FILE:mujoco> -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()
|
endif()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>MuJoCo Studio!</title>
|
<title>MuJoCo Live</title>
|
||||||
</head>
|
</head>
|
||||||
<body style="margin: 0; overflow: hidden">
|
<body style="margin: 0; overflow: hidden">
|
||||||
<div style="position: absolute; top: 3px; right: 3px; z-index: 1;">
|
<div style="position: absolute; top: 3px; right: 3px; z-index: 1;">
|
||||||
@@ -16,6 +16,28 @@
|
|||||||
style="width: 100vw; height: 100vh; display: block"
|
style="width: 100vw; height: 100vh; display: block"
|
||||||
></canvas>
|
></canvas>
|
||||||
<script>
|
<script>
|
||||||
|
// --- Loading overlay (shown while model is compiling) ---
|
||||||
|
var loadingOverlay = document.createElement('div');
|
||||||
|
loadingOverlay.id = 'loadingOverlay';
|
||||||
|
loadingOverlay.style.cssText =
|
||||||
|
'position:fixed;top:0;left:0;width:100%;height:100%;' +
|
||||||
|
'background:rgba(0,0,0,0.6);z-index:10000;display:none;' +
|
||||||
|
'align-items:center;justify-content:center;';
|
||||||
|
loadingOverlay.innerHTML =
|
||||||
|
'<div style="color:#fff;font-size:24px;font-family:sans-serif;' +
|
||||||
|
'text-align:center;">' +
|
||||||
|
'<div style="margin-bottom:16px;">Loading model\u2026</div>' +
|
||||||
|
'<div style="font-size:14px;opacity:0.7;">This may take a moment ' +
|
||||||
|
'for large models.</div></div>';
|
||||||
|
document.body.appendChild(loadingOverlay);
|
||||||
|
|
||||||
|
function showLoading() {
|
||||||
|
loadingOverlay.style.display = 'flex';
|
||||||
|
}
|
||||||
|
function hideLoading() {
|
||||||
|
loadingOverlay.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
var Module = {
|
var Module = {
|
||||||
preRun: [],
|
preRun: [],
|
||||||
postRun: [],
|
postRun: [],
|
||||||
@@ -46,8 +68,30 @@
|
|||||||
totalDependencies: 0,
|
totalDependencies: 0,
|
||||||
monitorRunDependencies(left) {},
|
monitorRunDependencies(left) {},
|
||||||
onRuntimeInitialized: () => {
|
onRuntimeInitialized: () => {
|
||||||
// Define assets to prefetch. These paths are relative to the wasm_binary/ directory.
|
// Define assets to prefetch. These paths are relative to the mujoco_live_wasm/ directory.
|
||||||
const assetsToPrefetch = [
|
const assetsToPrefetch = [
|
||||||
|
"assets/fontawesome-webfont.ttf",
|
||||||
|
"assets/ibl.ktx",
|
||||||
|
"assets/OpenSans-Regular.ttf",
|
||||||
|
"assets/pbr.filamat",
|
||||||
|
"assets/pbr_packed.filamat",
|
||||||
|
"assets/phong_2d_fade.filamat",
|
||||||
|
"assets/phong_2d.filamat",
|
||||||
|
"assets/phong_2d_reflect.filamat",
|
||||||
|
"assets/phong_2d_uv_fade.filamat",
|
||||||
|
"assets/phong_2d_uv.filamat",
|
||||||
|
"assets/phong_2d_uv_reflect.filamat",
|
||||||
|
"assets/phong_color_fade.filamat",
|
||||||
|
"assets/phong_color.filamat",
|
||||||
|
"assets/phong_color_reflect.filamat",
|
||||||
|
"assets/phong_cube_fade.filamat",
|
||||||
|
"assets/phong_cube.filamat",
|
||||||
|
"assets/phong_cube_reflect.filamat",
|
||||||
|
"assets/unlit_decor.filamat",
|
||||||
|
"assets/unlit_depth.filamat",
|
||||||
|
"assets/unlit_line.filamat",
|
||||||
|
"assets/unlit_segmentation.filamat",
|
||||||
|
"assets/unlit_ui.filamat"
|
||||||
];
|
];
|
||||||
|
|
||||||
const assetPromises = assetsToPrefetch.map(async (relativePath) => {
|
const assetPromises = assetsToPrefetch.map(async (relativePath) => {
|
||||||
@@ -71,7 +115,31 @@
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
try {
|
try {
|
||||||
Module.init();
|
Module.init();
|
||||||
requestAnimationFrame(Module.animate);
|
|
||||||
|
// Check for a ?model= URL parameter and load from URL.
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const modelUrl = params.get('model');
|
||||||
|
if (modelUrl) {
|
||||||
|
// loadUrl uses ASYNCIFY (via EM_ASYNC_JS fetch), which
|
||||||
|
// suspends the WASM module. We must not start the animation
|
||||||
|
// loop until the load completes, otherwise renderFrame will
|
||||||
|
// hit "Cannot have multiple async operations in flight".
|
||||||
|
showLoading();
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(async () => {
|
||||||
|
try {
|
||||||
|
await Module.loadUrl(modelUrl);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load model from URL:', error);
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
requestAnimationFrame(Module.animate);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
requestAnimationFrame(Module.animate);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize app.', error);
|
console.error('Failed to initialize app.', error);
|
||||||
}
|
}
|
||||||
@@ -95,6 +163,31 @@
|
|||||||
Module.canvas.style.height = window.innerHeight + "px";
|
Module.canvas.style.height = window.innerHeight + "px";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function handleFile(file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const buffer = e.target.result;
|
||||||
|
// Show the loading overlay, then defer the blocking WASM call by two
|
||||||
|
// animation frames so the browser has a chance to paint the overlay.
|
||||||
|
showLoading();
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
try {
|
||||||
|
Module.loadFile(file.name, buffer);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load model from file:', error);
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
reader.onerror = (e) => {
|
||||||
|
console.error('Error reading file:', e);
|
||||||
|
};
|
||||||
|
reader.readAsArrayBuffer(file);
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const uploadButton = document.getElementById('uploadButton');
|
const uploadButton = document.getElementById('uploadButton');
|
||||||
const fileInput = document.getElementById('fileInput');
|
const fileInput = document.getElementById('fileInput');
|
||||||
@@ -108,23 +201,55 @@
|
|||||||
if (!file) {
|
if (!file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
handleFile(file);
|
||||||
|
});
|
||||||
|
|
||||||
const reader = new FileReader();
|
// --- Drag-and-drop support ---
|
||||||
reader.onload = (e) => {
|
const dropOverlay = document.createElement('div');
|
||||||
const buffer = e.target.result;
|
dropOverlay.id = 'dropOverlay';
|
||||||
try {
|
dropOverlay.style.cssText =
|
||||||
Module.loadFile(file.name, buffer);
|
'position:fixed;top:0;left:0;width:100%;height:100%;' +
|
||||||
} catch (error) {
|
'background:rgba(0,0,0,0.5);z-index:9999;display:none;' +
|
||||||
console.error('Failed to load model from file:', error);
|
'pointer-events:none;align-items:center;justify-content:center;';
|
||||||
}
|
dropOverlay.innerHTML =
|
||||||
};
|
'<div style="color:#fff;font-size:24px;font-family:sans-serif;' +
|
||||||
reader.onerror = (e) => {
|
'padding:32px 48px;border:3px dashed #fff;border-radius:16px;">' +
|
||||||
console.error('Error reading file:', e);
|
'Drop model file here</div>';
|
||||||
};
|
document.body.appendChild(dropOverlay);
|
||||||
reader.readAsArrayBuffer(file);
|
|
||||||
|
let dragCounter = 0;
|
||||||
|
|
||||||
|
document.addEventListener('dragenter', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dragCounter++;
|
||||||
|
if (dragCounter === 1) {
|
||||||
|
dropOverlay.style.display = 'flex';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('dragleave', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dragCounter--;
|
||||||
|
if (dragCounter === 0) {
|
||||||
|
dropOverlay.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('dragover', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('drop', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dragCounter = 0;
|
||||||
|
dropOverlay.style.display = 'none';
|
||||||
|
const files = e.dataTransfer.files;
|
||||||
|
for (let i = 0; i < files.length; i++) {
|
||||||
|
handleFile(files[i]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<script async src="/bin/wasm.js"></script>
|
<script async src="bin/mujoco_live.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -18,7 +18,11 @@
|
|||||||
#include <emscripten/bind.h>
|
#include <emscripten/bind.h>
|
||||||
#include <emscripten/val.h>
|
#include <emscripten/val.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -59,6 +63,84 @@ class AssetRegistry {
|
|||||||
std::unordered_map<std::string, std::string> assets_;
|
std::unordered_map<std::string, std::string> 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<char[]>(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 <typename T>
|
||||||
|
using UniquePtrWasm = std::unique_ptr<T, FreeDeleter>;
|
||||||
|
|
||||||
|
struct Entry {
|
||||||
|
UniquePtrWasm<char[]> data;
|
||||||
|
int size;
|
||||||
|
};
|
||||||
|
std::unordered_map<std::string, Entry> entries_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// Javascript-facing function to register an asset.
|
// Javascript-facing function to register an asset.
|
||||||
void RegisterAsset(std::string filename, std::string contents) {
|
void RegisterAsset(std::string filename, std::string contents) {
|
||||||
AssetRegistry::Instance().RegisterAsset(std::move(filename),
|
AssetRegistry::Instance().RegisterAsset(std::move(filename),
|
||||||
@@ -92,6 +174,27 @@ void Init() {
|
|||||||
resource_provider.prefix = "filament";
|
resource_provider.prefix = "filament";
|
||||||
mjp_registerResourceProvider(&resource_provider);
|
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({
|
g_app = new mujoco::studio::App({
|
||||||
.width = width,
|
.width = width,
|
||||||
.height = height,
|
.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);
|
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.
|
// Javascript-facing function to render a single frame.
|
||||||
void RenderFrame() {
|
void RenderFrame() {
|
||||||
if (g_app) {
|
if (g_app) {
|
||||||
@@ -144,6 +258,7 @@ EMSCRIPTEN_BINDINGS(studio_bindings) {
|
|||||||
emscripten::function("registerAsset", &RegisterAsset);
|
emscripten::function("registerAsset", &RegisterAsset);
|
||||||
emscripten::function("init", &Init);
|
emscripten::function("init", &Init);
|
||||||
emscripten::function("loadFile", &LoadFile);
|
emscripten::function("loadFile", &LoadFile);
|
||||||
|
emscripten::function("loadUrl", &LoadUrl);
|
||||||
emscripten::function("renderFrame", &RenderFrame);
|
emscripten::function("renderFrame", &RenderFrame);
|
||||||
emscripten::function("deinit", &Deinit);
|
emscripten::function("deinit", &Deinit);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,6 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user