diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index de3fe7fd..cd2b2678 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -56,7 +56,7 @@ namespace mujoco::studio { static constexpr platform::Window::Config kWindowConfig = { -#ifdef EMSCRIPTEN +#if defined(__EMSCRIPTEN__) .render_config = platform::Window::RenderConfig::kFilamentWebGL, #elif defined(USE_FILAMENT_VULKAN) .render_config = platform::Window::RenderConfig::kFilamentVulkan, @@ -195,34 +195,36 @@ void App::LoadModel(std::string data, ContentType type) { // Delete the existing mjModel and mjData. ClearModel(); - char err[1000] = ""; - if (type == ContentType::kFilepath) { - // Store the file path as the model name. Note that we use this model name - // to perform reload operations. - model_name_ = std::move(data); - if (model_name_.ends_with(".mjb")) { - model_ = mj_loadModel(model_name_.c_str(), 0); - } else if (model_name_.ends_with(".xml")) { - spec_ = mj_parseXML(model_name_.c_str(), nullptr, err, sizeof(err)); + if (!data.empty()) { + char err[1000] = ""; + if (type == ContentType::kFilepath) { + // Store the file path as the model name. Note that we use this model name + // to perform reload operations. + model_name_ = std::move(data); + if (model_name_.ends_with(".mjb")) { + model_ = mj_loadModel(model_name_.c_str(), 0); + } else if (model_name_.ends_with(".xml")) { + spec_ = mj_parseXML(model_name_.c_str(), nullptr, err, sizeof(err)); + if (spec_ && err[0] == 0) { + model_ = mj_compile(spec_, nullptr); + } + } else { + error_ = "Unknown model file type; expected .mjb or .xml."; + } + } else if (type == ContentType::kModelXml) { + model_name_ = "[xml]"; + spec_ = mj_parseXMLString(data.c_str(), nullptr, err, sizeof(err)); if (spec_ && err[0] == 0) { model_ = mj_compile(spec_, nullptr); } - } else { - error_ = "Unknown model file type; expected .mjb or .xml."; + } else if (type == ContentType::kModelMjb) { + model_name_ = "[mjb]"; + model_ = mj_loadModelBuffer(data.data(), data.size()); } - } else if (type == ContentType::kModelXml) { - model_name_ = "[xml]"; - spec_ = mj_parseXMLString(data.c_str(), nullptr, err, sizeof(err)); - if (spec_ && err[0] == 0) { - model_ = mj_compile(spec_, nullptr); - } - } else if (type == ContentType::kModelMjb) { - model_name_ = "[mjb]"; - model_ = mj_loadModelBuffer(data.data(), data.size()); - } - if (err[0]) { - error_ = err; + if (err[0]) { + error_ = err; + } } // If no mjModel was loaded, load an empty mjModel. diff --git a/src/experimental/studio/index.html b/src/experimental/studio/index.html new file mode 100644 index 00000000..8edd8b4c --- /dev/null +++ b/src/experimental/studio/index.html @@ -0,0 +1,154 @@ + + + + + MuJoCo Studio! + + +
+ + +
+ + + + + diff --git a/src/experimental/studio/wasm.cc b/src/experimental/studio/wasm.cc new file mode 100644 index 00000000..0253ca2f --- /dev/null +++ b/src/experimental/studio/wasm.cc @@ -0,0 +1,120 @@ +// Copyright 2025 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. + +// Main entry point for the Filament-based MuJoCo web app. + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "experimental/studio/app.h" + +// Global app instance. Lifetime is controlled by Init/Deinit calls which are +// triggered by Javascript. +mujoco::studio::App* g_app = nullptr; + +// Static registry of assets that are loaded in JSON before the main App is +// initialized. +class AssetRegistry { + public: + // Returns the singleton instance of the registry. + static AssetRegistry& Instance() { + static AssetRegistry instance; + return instance; + } + + // Registers asset contents with the given filename. + void RegisterAsset(std::string filename, std::string contents) { + assets_[filename] = std::move(contents); + } + + // Returns the contents of the given asset by name. + std::vector LoadAsset(std::string_view filename) { + if (auto it = assets_.find(std::string(filename)); it != assets_.end()) { + const std::byte* begin = reinterpret_cast(it->second.data()); + const std::byte* end = begin + it->second.size(); + return std::vector(begin, end); + } + return {}; + } + + private: + std::unordered_map assets_; +}; + +static std::vector LoadAsset(std::string_view filename) { + return AssetRegistry::Instance().LoadAsset(filename); +} + +// Javascript-facing function to register an asset. +void RegisterAsset(std::string filename, std::string contents) { + AssetRegistry::Instance().RegisterAsset(std::move(filename), + std::move(contents)); +} + +// Javascript-facing function to initialize the app. +void Init() { + // Note: dimensions do not matter as window will be resized to fit canvas. + const int width = 100; + const int height = 100; + const std::string ini_path = ""; + g_app = new mujoco::studio::App(width, height, ini_path, LoadAsset); + g_app->LoadModel("", mujoco::studio::App::ContentType::kModelXml); +} + +// Javascript-facing function to load a model from a MJB file. +void LoadMjb(const std::string& src) { + if (g_app) { + g_app->LoadModel(src, mujoco::studio::App::ContentType::kModelMjb); + } +} + +// Javascript-facing function to load a model from an XML file. +void LoadXml(const std::string& src) { + if (g_app) { + g_app->LoadModel(src, mujoco::studio::App::ContentType::kModelXml); + } +} + +// Javascript-facing function to render a single frame. +void RenderFrame() { + if (g_app) { + if (g_app->Update()) { + g_app->BuildGui(); + g_app->Render(); + } + } +} + +// Javascript-facing function to deinitialize the app. +void Deinit() { + delete g_app; + g_app = nullptr; +} + +EMSCRIPTEN_BINDINGS(studio_bindings) { + emscripten::function("registerAsset", &RegisterAsset); + emscripten::function("init", &Init); + emscripten::function("loadMjb", &LoadMjb); + emscripten::function("loadXml", &LoadXml); + emscripten::function("renderFrame", &RenderFrame); + emscripten::function("deinit", &Deinit); +}