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);
+}