This course runs MuJoCo in two places, and both are the same engine.
In this browser. Every lab on these pages runs Google DeepMind’s official WebAssembly build of MuJoCo, published on npm as @mujoco/mujoco. Its version numbers track MuJoCo releases one to one, and this course loads exactly 3.14.0. The physics you see in a lab is computed by the same C code as the Python package, compiled for the browser. Only the drawing differs: the labs draw MuJoCo’s geometry with three.js, not with MuJoCo’s own OpenGL renderer.
On your machine, in Python. The companion package mjcourse holds every model, every example script, the projects and their tests. Anything that trains a network, renders datasets, runs thousands of episodes or needs MuJoCo’s own renderer belongs here.
[!established] The versions this course is pinned to MuJoCo 3.14.0 (released 22 September 2026), for both the Python package and the WebAssembly module. Every API name, default value and measured number in a lesson marked complete was checked against this release. The Python package declares
mujoco==3.14.0as an exact requirement.
Pinning is not pedantry. Between 3.10 and 3.14, MuJoCo changed the signature of mj_fullM, removed the field mjData.qM that many tutorials still use, added a discrete integrator, rewrote the box-box collider and changed several defaults (for example sleep_tolerance and bvactive). A course that floats on “the latest MuJoCo” quietly becomes wrong. When a lesson depends on a version-specific behaviour, it says so in a box like this:
[!version] Example: mj_fullM changed in 3.10.0 Before 3.10.0 the dense mass matrix was obtained with
mj_fullM(m, dst, d.qM). From 3.10.0 the signature ismj_fullM(m, d, dst), andmjData.qMitself was removed in 3.11.0. Code written for older releases fails with a type error on the first call.
MuJoCo 3.14.0 publishes wheels for CPython 3.10 to 3.15 on these platforms:
| Platform | Wheel tag | Notes |
|---|---|---|
| Linux x86-64 | manylinux_2_27_x86_64.manylinux_2_28_x86_64 |
glibc 2.27 or newer |
| Linux ARM64 | manylinux_2_27_aarch64.manylinux_2_28_aarch64 |
for example a Jetson board on a recent Ubuntu-based image |
| macOS, Apple silicon | macosx_11_0_arm64 |
macOS 11 or newer |
| Windows x86-64 | win_amd64 |
There is no 3.14.0 wheel for Intel Macs; on such a machine pip falls back to building from the source distribution, which needs a C++ toolchain and CMake. The table comes from the files PyPI lists for the release, not from memory.
Clone only the course’s code (the site repository is large) and install it into a fresh environment:
git clone --depth 1 --filter=blob:none --sparse https://github.com/s-elim/s-elim.github.io
cd s-elim.github.io && git sparse-checkout set learn/mujoco/code && cd learn/mujoco/code
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]" # mujoco==3.14.0, numpy, gymnasium, scipy, pytest
pip install -e ".[learn]" # adds PyTorch, needed from Level 13
[!recommendation] One environment per project, pinned Install the course into its own virtual environment. MuJoCo is a compiled extension and robotics stacks (robosuite, Meta-World, LIBERO, dm_control) each pin their own MuJoCo. Two benchmarks that need different MuJoCo releases cannot share an environment, and a silent version change is the commonest cause of “the same code gives different numbers on my machine”. The cost is disk space.
INPUT: nothing; reads the installed packages and the `MUJOCO_GL` environment variable
PROCESS: import MuJoCo, compile and step a tiny model, try an offscreen render
OUTPUT: a report of what works; non-zero exit only if MuJoCo is missing
```python file=examples/l0_2_check_install.py “"”Lesson 0.2: check that this machine can run every part of the course.
INPUT nothing (reads the installed packages and the MUJOCO_GL environment variable) PROCESS import mujoco, compile and step a model, try an offscreen render OUTPUT a short report; exit status 1 only if MuJoCo itself is missing or wrong
Run: python examples/l0_2_check_install.py “””
import os import platform import sys
EXPECTED = “3.14.0”
def main() -> int: print(f”python {sys.version.split()[0]} on {platform.system()} {platform.machine()}”) try: import mujoco except ImportError: print(“mujoco NOT INSTALLED: pip install mujoco==3.14.0”) return 1 import numpy as np
print(f"mujoco {mujoco.__version__} (engine reports {mujoco.mj_versionString()})")
print(f"numpy {np.__version__}")
if mujoco.__version__ != EXPECTED:
print(f"WARNING the course was verified with {EXPECTED}; numbers may differ")
xml = """<mujoco><worldbody><geom type="plane" size="1 1 .1"/>
<body pos="0 0 .5"><freejoint/><geom size=".05"/></body></worldbody></mujoco>"""
model = mujoco.MjModel.from_xml_string(xml)
data = mujoco.MjData(model)
for _ in range(500):
mujoco.mj_step(model, data)
print(f"physics ok: t = {data.time:.3f} s, ball at z = {data.qpos[2]:.4f} m, {data.ncon} contact(s)")
backend = os.environ.get("MUJOCO_GL", "(unset: platform default)")
try:
renderer = mujoco.Renderer(model, height=48, width=64)
renderer.update_scene(data)
image = renderer.render()
renderer.close()
print(f"render ok with MUJOCO_GL={backend}: image {image.shape} {image.dtype}")
except Exception as err: # noqa: BLE001 - any GL failure is reported, not fatal
print(f"render unavailable with MUJOCO_GL={backend}: {type(err).__name__}: {err}")
print(" physics, kinematics, control and learning lessons still work;")
print(" see 'Headless rendering' in Lesson 0.2 for camera lessons")
try:
import gymnasium
print(f"gym gymnasium {gymnasium.__version__}")
except ImportError:
print("gym gymnasium not installed (needed from Level 12): pip install gymnasium")
try:
import torch
print(f"torch {torch.__version__} (CUDA available: {torch.cuda.is_available()})")
except ImportError:
print("torch not installed (needed for PPO and vision policies): pip install -e .[learn]")
return 0
if name == “main”: sys.exit(main())
On a Linux server with no display and nothing configured, it prints (abridged):
```text
python 3.12.14 on Linux x86_64
mujoco 3.14.0 (engine reports 3.14.0)
physics ok: t = 1.000 s, ball at z = 0.0496 m, 1 contact(s)
render unavailable with MUJOCO_GL=(unset: platform default): FatalError: an OpenGL platform
library has not been loaded into this process, ...
and, on the same machine with MUJOCO_GL=osmesa and a Mesa build on the library path:
render ok with MUJOCO_GL=osmesa: image (48, 64, 3) uint8
Physics never needs a display. Rendering does: MuJoCo’s renderer is OpenGL, and OpenGL needs a context. The Python package chooses how to create one from the environment variable MUJOCO_GL, read once, when mujoco is first imported. Setting it inside a script after import mujoco has no effect.
MUJOCO_GL |
Platform | When to use it |
|---|---|---|
unset or glfw |
all | a desktop session with a display; also needed by the interactive viewer |
egl |
Linux | a GPU server with an NVIDIA (or other EGL-capable) driver and no display |
osmesa |
Linux | no GPU at all: software rendering on the CPU, slow but always available |
cgl |
macOS | offscreen rendering without a window |
wgl |
Windows | offscreen rendering without a window |
The valid values above are those accepted by mujoco/rendering/classic/gl_context.py in the 3.14.0 source. For EGL and OSMesa, PyOpenGL must agree: set PYOPENGL_PLATFORM to the same value.
# GPU server, no display
export MUJOCO_GL=egl PYOPENGL_PLATFORM=egl
# CPU-only server (OSMesa must be installed: apt install libosmesa6, or a user-space Mesa build)
export MUJOCO_GL=osmesa PYOPENGL_PLATFORM=osmesa
[!warning] The two errors you will meet
FatalError: an OpenGL platform library has not been loaded into this processmeans no context could be created: on a server, setMUJOCO_GLbefore Python starts.AttributeError: 'NoneType' object has no attribute 'glGetError'during import means PyOpenGL could not find the library for the platform you asked for, for exampleMUJOCO_GL=osmesawithout OSMesa installed or without its directory onLD_LIBRARY_PATH.
[!implementation] macOS and the interactive viewer On macOS,
mujoco.viewer.launch_passivemust run under themjpythonlauncher installed with the package (mjpython my_script.py), because macOS requires the main thread to do the rendering. The documentation’s warning is specific to the passive viewer; the blockingmujoco.viewer.launchowns its own loop.
| You want to | Browser labs | Python package |
|---|---|---|
| Step any course model, change parameters live, inspect state and contacts | yes | yes |
| Edit MJCF and see compile errors | yes (Playground) | yes |
| Kinematics, dynamics, controllers | yes (labs) | yes (mjcourse.kinematics, mjcourse.control) |
| RGB, depth and segmentation images | approximations drawn by three.js | yes, MuJoCo’s renderer |
| Train neural policies, generate datasets | small demonstrations only | yes |
| Thousands of parallel rollouts | no | yes (mujoco.rollout, threads; MJX or MuJoCo Warp on GPU) |
| Exact numbers quoted in lessons | identical physics | the reference |
[!implementation] Same physics, different pictures The browser module and the Python wheel are built from the same MuJoCo source at the same version, so stepping the same model from the same state gives the same numbers. In this course’s own check, a pendulum-plus-box model stepped 500 times agreed to six printed decimals in Node.js and in Python. Rendering is where they differ: MuJoCo’s WebAssembly build does not include its OpenGL renderer, so the labs draw geoms from
mjData.geom_xposandgeom_xmatwith three.js. Lighting, shadows and textures are the course’s, the geometry and every pose are MuJoCo’s.
pip builds MuJoCo from source and fails. You are on a platform or Python version without a wheel (an Intel Mac, Python 3.9, a 32-bit system). Use a supported Python, or build MuJoCo from source following its documentation.
ImportError: ... GLIBC_2.27 not found. The Linux wheels need glibc 2.27 or newer. Old cluster images (CentOS 7 era) do not have it; use a container with a newer base image.
Numbers differ from the lesson. Print mujoco.__version__. If it is not 3.14.0, that is the first suspect; MuJoCo’s changelog lists behaviour changes per release.
The browser labs show “could not be loaded”. The WebAssembly module is about 10 MB and comes from the jsDelivr CDN. A network that blocks it, or a very old browser without WebAssembly, will stop the labs; the lesson text and the Python code do not depend on them.
Run examples/l0_2_check_install.py on every machine you will use for this course (laptop, workstation, cluster node). For each, record the Python version, the MuJoCo version and which MUJOCO_GL value renders. Keep the table: it is the first thing you will need when a result does not reproduce.
Make rendering work on a machine where the check reports it unavailable, without root access. (On Linux, a user-space Mesa build plus MUJOCO_GL=osmesa, PYOPENGL_PLATFORM=osmesa and LD_LIBRARY_PATH pointing at Mesa’s lib directory is enough; this course’s own server works that way.) Measure frames per second for a 640 by 480 render of pick_place.xml, so you know the cost before you plan a dataset.
A reproducible MuJoCo experiment records, at minimum, the MuJoCo version, the Python and NumPy versions, the operating system and the GPU driver if rendering was involved. MJX and MuJoCo Warp ship as the separate packages mujoco-mjx and mujoco-warp (both at 3.14.0 on PyPI when this was written); pin them to the same release as mujoco and record them too. Level 21’s experiment template writes all of this into every run’s metadata automatically.
{"id": "0.2-check", "title": "Knowledge check", "questions": [
{"kind": "mcq", "q": "A script sets <code>os.environ['MUJOCO_GL'] = 'egl'</code> on its third line, after <code>import mujoco</code> on its first. Rendering fails on a headless GPU server. Why?",
"options": ["EGL is not supported on Linux", "MUJOCO_GL is read when mujoco is imported, so the assignment comes too late", "PYOPENGL_PLATFORM must be set instead", "The server needs a display"],
"answer": 1,
"explain": "<p>The OpenGL backend is selected at import time. Set the variable in the shell, in the job script, or before the first <code>import mujoco</code>. Setting <code>PYOPENGL_PLATFORM</code> to the same value is also needed, but it would not fix this ordering bug.</p>"},
{"kind": "mcq", "q": "Which statement about the browser labs is accurate?",
"options": ["They run a JavaScript re-implementation of MuJoCo's equations", "They run MuJoCo's C code compiled to WebAssembly, and draw it with three.js", "They replay trajectories precomputed in Python", "They use MuJoCo's OpenGL renderer through WebGL"],
"answer": 1,
"explain": "<p>The physics is MuJoCo 3.14.0 itself (the official <code>@mujoco/mujoco</code> package). Drawing is done by the course with three.js because the WebAssembly build does not include MuJoCo's renderer.</p>"},
{"kind": "open", "q": "Your lab's cluster image has MuJoCo 3.3.2 and your laptop has 3.14.0. A colleague says the difference does not matter for a pick-and-place success rate. How would you test that claim, and what result would make you accept it?",
"reference": "<p>Run the identical evaluation (same policy, same seeds, same initial states, same number of episodes) under both versions in separate environments, and compare success rates with confidence intervals. Also compare intermediate quantities that should be version-independent if the claim is true, for example contact counts and object trajectories over the first second from fixed states. Accept the claim only if the intervals overlap substantially <em>and</em> the trajectory comparison shows no systematic difference; otherwise report both numbers. The changelog between the two versions (collision rewrites, solver changes, integrator changes) tells you where to look first.</p>"}
]}
Lesson 0.3 is short and deliberately sceptical: what MuJoCo models well, what it approximates, and what it does not model at all.