1465d8b6ce
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
256 lines
9.5 KiB
HTML
256 lines
9.5 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<title>MuJoCo Live</title>
|
|
</head>
|
|
<body style="margin: 0; overflow: hidden">
|
|
<div style="position: absolute; top: 3px; right: 3px; z-index: 1;">
|
|
<button id="uploadButton">Upload Model</button>
|
|
<input type="file" id="fileInput" accept=".xml,.mjb,.mjz,.zip" style="display: none;">
|
|
</div>
|
|
<canvas
|
|
class="emscripten"
|
|
id="canvas"
|
|
oncontextmenu="event.preventDefault()"
|
|
style="width: 100vw; height: 100vh; display: block"
|
|
></canvas>
|
|
<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 = {
|
|
preRun: [],
|
|
postRun: [],
|
|
locateFile: function(path) {
|
|
const baseURL = window.location.origin + window.location.pathname.substring(0, window.location.pathname.lastIndexOf("/"));
|
|
return baseURL + "/bin/" + path;
|
|
},
|
|
print: console.log,
|
|
printErr: text => {
|
|
console.error(text + "\n" + new Error().stack);
|
|
},
|
|
canvas: (() => {
|
|
const canvas = document.getElementById("canvas");
|
|
// As a default initial behavior, pop up an alert when webgl context is lost. To make your
|
|
// application robust, you may want to override this behavior before shipping!
|
|
// See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
|
|
canvas.addEventListener(
|
|
"webglcontextlost",
|
|
e => {
|
|
alert("WebGL context lost. You will need to reload the page.");
|
|
e.preventDefault();
|
|
},
|
|
false,
|
|
);
|
|
return canvas;
|
|
})(),
|
|
setStatus(text) {},
|
|
totalDependencies: 0,
|
|
monitorRunDependencies(left) {},
|
|
onRuntimeInitialized: () => {
|
|
// Define assets to prefetch. These paths are relative to the mujoco_live_wasm/ directory.
|
|
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 assetUrl = Module.locateFile(relativePath);
|
|
try {
|
|
const response = await fetch(assetUrl);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch ${assetUrl}: ${response.statusText}`);
|
|
}
|
|
const buffer = await response.arrayBuffer();
|
|
const filename = relativePath.substring(relativePath.lastIndexOf('/') + 1);
|
|
Module.registerAsset(filename, new Uint8Array(buffer));
|
|
console.log(`Registered asset: ${filename}`);
|
|
} catch (error) {
|
|
console.error(`Error prefetching asset ${assetUrl}:`, error);
|
|
throw error; // Re-throw to be caught by Promise.all
|
|
}
|
|
});
|
|
|
|
Promise.all(assetPromises)
|
|
.then(() => {
|
|
try {
|
|
Module.init();
|
|
|
|
// 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) {
|
|
console.error('Failed to initialize app.', error);
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
console.error('Failed to prefetch one or more assets.', error);
|
|
});
|
|
},
|
|
animate: async () => {
|
|
try {
|
|
await Module.renderFrame();
|
|
} catch (error) {
|
|
console.error('Update error:', error);
|
|
}
|
|
requestAnimationFrame(Module.animate);
|
|
},
|
|
};
|
|
// Ensure the canvas is resized when the window is resized.
|
|
window.addEventListener("resize", function () {
|
|
Module.canvas.style.width = window.innerWidth + "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', () => {
|
|
const uploadButton = document.getElementById('uploadButton');
|
|
const fileInput = document.getElementById('fileInput');
|
|
|
|
uploadButton.addEventListener('click', () => {
|
|
fileInput.click();
|
|
});
|
|
|
|
fileInput.addEventListener('change', (event) => {
|
|
const file = event.target.files[0];
|
|
if (!file) {
|
|
return;
|
|
}
|
|
handleFile(file);
|
|
});
|
|
|
|
// --- Drag-and-drop support ---
|
|
const dropOverlay = document.createElement('div');
|
|
dropOverlay.id = 'dropOverlay';
|
|
dropOverlay.style.cssText =
|
|
'position:fixed;top:0;left:0;width:100%;height:100%;' +
|
|
'background:rgba(0,0,0,0.5);z-index:9999;display:none;' +
|
|
'pointer-events:none;align-items:center;justify-content:center;';
|
|
dropOverlay.innerHTML =
|
|
'<div style="color:#fff;font-size:24px;font-family:sans-serif;' +
|
|
'padding:32px 48px;border:3px dashed #fff;border-radius:16px;">' +
|
|
'Drop model file here</div>';
|
|
document.body.appendChild(dropOverlay);
|
|
|
|
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 async src="bin/mujoco_live.js"></script>
|
|
</body>
|
|
</html>
|