diff --git a/python/mujoco/__init__.py b/python/mujoco/__init__.py
index 2d8d22ad..e80dfff3 100644
--- a/python/mujoco/__init__.py
+++ b/python/mujoco/__init__.py
@@ -59,8 +59,16 @@ from mujoco._functions import *
from mujoco._render import *
from mujoco._specs import *
from mujoco._structs import *
-from mujoco.gl_context import *
-from mujoco.renderer import Renderer
+
+try:
+ from mujoco.rendering.classic.renderer import Renderer # pytype: disable=import-error
+except ImportError:
+ pass
+
+try:
+ from mujoco.rendering.classic.gl_context import * # pytype: disable=import-error
+except ImportError:
+ pass
MjStruct: TypeAlias = Union[
_specs.MjsBody,
diff --git a/python/mujoco/gl_context.py b/python/mujoco/gl_context.py
index 61b861ec..3db3cab9 100644
--- a/python/mujoco/gl_context.py
+++ b/python/mujoco/gl_context.py
@@ -12,37 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
-"""Exports GLContext for MuJoCo Python bindings."""
+"""Wrapper around mujoco.rendering.classic.gl_context for backwards compatibility."""
-import ctypes
-import ctypes.util
-import os
-import platform
-# pylint: disable=g-import-not-at-top
-_SYSTEM = platform.system()
-_MUJOCO_GL = os.environ.get('MUJOCO_GL', '').lower().strip()
-if _MUJOCO_GL not in ('disable', 'disabled', 'off', 'false', '0'):
- _VALID_MUJOCO_GL = ('enable', 'enabled', 'on', 'true', '1' , 'glfw', '')
- if _SYSTEM == 'Linux':
- _VALID_MUJOCO_GL += ('glx', 'egl', 'osmesa')
- elif _SYSTEM == 'Windows':
- _VALID_MUJOCO_GL += ('wgl',)
- elif _SYSTEM == 'Darwin':
- _VALID_MUJOCO_GL += ('cgl',)
- if _MUJOCO_GL not in _VALID_MUJOCO_GL:
- raise RuntimeError(
- f'invalid value for environment variable MUJOCO_GL: {_MUJOCO_GL}')
-
- if _SYSTEM == 'Linux' and _MUJOCO_GL == 'osmesa':
- from mujoco.osmesa import GLContext as _GLContext
- GLContext = _GLContext
- elif _SYSTEM == 'Linux' and _MUJOCO_GL == 'egl':
- from mujoco.egl import GLContext as _GLContext
- GLContext = _GLContext
- elif _SYSTEM == 'Darwin':
- from mujoco.cgl import GLContext as _GLContext
- GLContext = _GLContext
- else:
- from mujoco.glfw import GLContext as _GLContext
- GLContext = _GLContext
+try:
+ from mujoco.rendering.classic.gl_context import * # pytype: disable=import-error
+except ImportError:
+ pass
diff --git a/python/mujoco/renderer.py b/python/mujoco/renderer.py
index d21033c5..a8bb1a18 100644
--- a/python/mujoco/renderer.py
+++ b/python/mujoco/renderer.py
@@ -12,329 +12,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
-"""Defines a renderer class for the MuJoCo Python native bindings."""
-
-from typing import Optional, Union
-
-from mujoco import _enums
-from mujoco import _functions
-from mujoco import _render
-from mujoco import _structs
-from mujoco import gl_context
-import numpy as np
+"""Wrapper around mujoco.rendering.classic.renderer for backwards compatibility."""
-class Renderer:
- """Renders MuJoCo scenes."""
-
- def __init__(
- self,
- model: _structs.MjModel,
- height: int = 240,
- width: int = 320,
- max_geom: int = 10000,
- font_scale: _enums.mjtFontScale = _enums.mjtFontScale.mjFONTSCALE_150,
- ) -> None:
- """Initializes a new `Renderer`.
-
- Args:
- model: an MjModel instance.
- height: image height in pixels.
- width: image width in pixels.
- max_geom: Optional integer specifying the maximum number of geoms that can
- be rendered in the same scene. If None this will be chosen automatically
- based on the estimated maximum number of renderable geoms in the model.
- font_scale: Optional enum specifying the font scale for text.
-
- Raises:
- ValueError: If `camera_id` is outside the valid range, or if `width` or
- `height` exceed the dimensions of MuJoCo's offscreen framebuffer.
- """
- buffer_width = model.vis.global_.offwidth
- buffer_height = model.vis.global_.offheight
- if width > buffer_width:
- raise ValueError(f"""
-Image width {width} > framebuffer width {buffer_width}. Either reduce the image
-width or specify a larger offscreen framebuffer in the model XML using the
-clause:
-
-
-""".lstrip())
-
- if height > buffer_height:
- raise ValueError(f"""
-Image height {height} > framebuffer height {buffer_height}. Either reduce the
-image height or specify a larger offscreen framebuffer in the model XML using
-the clause:
-
-
-""".lstrip())
-
- self._width = width
- self._height = height
- self._model = model
-
- self._scene = _structs.MjvScene(model=model, maxgeom=max_geom)
- self._scene_option = _structs.MjvOption()
-
- self._rect = _render.MjrRect(0, 0, self._width, self._height)
-
- # Create render contexts.
- # TODO(nimrod): Figure out why pytype doesn't like gl_context.GLContext
- self._gl_context = None # type: ignore
- if gl_context.GLContext is not None:
- self._gl_context = gl_context.GLContext(width, height)
- if self._gl_context:
- self._gl_context.make_current()
- self._mjr_context = _render.MjrContext(model, font_scale.value)
- _render.mjr_setBuffer(
- _enums.mjtFramebuffer.mjFB_OFFSCREEN.value, self._mjr_context
- )
- self._mjr_context.readDepthMap = _enums.mjtDepthMap.mjDEPTH_ZEROFAR
-
- # Default render flags.
- self._depth_rendering = False
- self._segmentation_rendering = False
-
- @property
- def model(self):
- return self._model
-
- @property
- def scene(self) -> _structs.MjvScene:
- return self._scene
-
- @property
- def height(self):
- return self._height
-
- @property
- def width(self):
- return self._width
-
- def enable_depth_rendering(self):
- self._segmentation_rendering = False
- self._depth_rendering = True
-
- def disable_depth_rendering(self):
- self._depth_rendering = False
-
- def enable_segmentation_rendering(self):
- self._segmentation_rendering = True
- self._depth_rendering = False
-
- def disable_segmentation_rendering(self):
- self._segmentation_rendering = False
-
- def render(self, *, out: Optional[np.ndarray] = None) -> np.ndarray:
- """Renders the scene as a numpy array of pixel values.
-
- Args:
- out: Alternative output array in which to place the resulting pixels. It
- must have the same shape as the expected output but the type will be
- cast if necessary. The expted shape depends on the value of
- `self._depth_rendering`: when `True`, we expect `out.shape == (width,
- height)`, and `out.shape == (width, height, 3)` when `False`.
-
- Returns:
- A new numpy array holding the pixels with shape `(H, W)` or `(H, W, 3)`,
- depending on the value of `self._depth_rendering` unless
- `out is None`, in which case a reference to `out` is returned.
-
- Raises:
- RuntimeError: if this method is called after the close method.
- """
- original_flags = self._scene.flags.copy()
-
- # Using segmented rendering for depth makes the calculated depth more
- # accurate at far distances.
- if self._depth_rendering or self._segmentation_rendering:
- self._scene.flags[_enums.mjtRndFlag.mjRND_SEGMENT] = True
- self._scene.flags[_enums.mjtRndFlag.mjRND_IDCOLOR] = True
-
- if self._mjr_context is None:
- raise RuntimeError('render cannot be called after close.')
-
- if self._gl_context:
- self._gl_context.make_current()
-
- if self._depth_rendering:
- out_shape = (self._height, self._width)
- out_dtype = np.float32
- else:
- out_shape = (self._height, self._width, 3)
- out_dtype = np.uint8
-
- if out is None:
- out = np.empty(out_shape, dtype=out_dtype)
- else:
- if out.shape != out_shape:
- raise ValueError(
- f'Expected `out.shape == {out_shape}`. Got `out.shape={out.shape}`'
- ' instead. When using depth rendering, the out array should be of'
- ' shape `(width, height)` and otherwise (width, height, 3).'
- f' Got `(self.height, self.width)={(self.height, self.width)}` and'
- f' `self._depth_rendering={self._depth_rendering}`.'
- )
-
- # Render scene and read contents of RGB and depth buffers.
- _render.mjr_render(self._rect, self._scene, self._mjr_context)
- if self._depth_rendering:
- _render.mjr_readPixels(None, out, self._rect, self._mjr_context)
-
- # Get the distances to the near and far clipping planes.
- extent = self._model.stat.extent
- near = self._model.vis.map.znear * extent
- far = self._model.vis.map.zfar * extent
-
- # Calculate OpenGL perspective matrix values in float32 precision
- # so they are close to what glFrustum returns
- # https://registry.khronos.org/OpenGL-Refpages/gl2.1/xhtml/glFrustum.xml
- zfar = np.float32(far)
- znear = np.float32(near)
- c_coef = -(zfar + znear) / (zfar - znear)
- d_coef = -(np.float32(2) * zfar * znear) / (zfar - znear)
-
- # In reverse Z mode the perspective matrix is transformed by the following
- c_coef = np.float32(-0.5) * c_coef - np.float32(0.5)
- d_coef = np.float32(-0.5) * d_coef
-
- # We need 64 bits to convert Z from ndc to metric depth without noticeable
- # losses in precision
- out_64 = out.astype(np.float64)
-
- # Undo OpenGL projection
- # Note: We do not need to take action to convert from window coordinates
- # to normalized device coordinates because in reversed Z mode the mapping
- # is identity
- out_64 = d_coef / (out_64 + c_coef)
-
- # Cast result back to float32 for backwards compatibility
- # This has a small accuracy cost
- out[:] = out_64.astype(np.float32)
-
- # Reset scene flags.
- np.copyto(self._scene.flags, original_flags)
- elif self._segmentation_rendering:
- _render.mjr_readPixels(out, None, self._rect, self._mjr_context)
-
- # Convert 3-channel uint8 to 1-channel uint32.
- image3 = out.astype(np.uint32)
- segimage = (
- image3[:, :, 0] + image3[:, :, 1] * (2**8) + image3[:, :, 2] * (2**16)
- )
- # Remap segid to 2-channel (object ID, object type) pair.
- # Seg ID 0 is background -- will be remapped to (-1, -1).
- ngeoms = self._scene.ngeom
- segid2output = np.full(
- (ngeoms + 1, 2), fill_value=-1, dtype=np.int32
- ) # Seg id cannot be > ngeom + 1.
- visible_geoms = [g for g in self._scene.geoms[:ngeoms] if g.segid != -1]
- visible_segids = np.array([g.segid + 1 for g in visible_geoms], np.int32)
- visible_objid = np.array([g.objid for g in visible_geoms], np.int32)
- visible_objtype = np.array([g.objtype for g in visible_geoms], np.int32)
- segid2output[visible_segids, 0] = visible_objid
- segid2output[visible_segids, 1] = visible_objtype
- out = segid2output[segimage]
-
- # Reset scene flags.
- np.copyto(self._scene.flags, original_flags)
- else:
- _render.mjr_readPixels(out, None, self._rect, self._mjr_context)
-
- if self._gl_context:
- # If using EGL, OSMesa, or GLFW, the output image is flipped vertically.
- # No flip is needed for Filament.
- out[:] = np.flipud(out)
-
- return out
-
- def update_scene(
- self,
- data: _structs.MjData,
- camera: Union[int, str, _structs.MjvCamera] = -1,
- scene_option: Optional[_structs.MjvOption] = None,
- ):
- """Updates geometry used for rendering.
-
- Args:
- data: An instance of `MjData`.
- camera: An instance of `MjvCamera`, a string or an integer
- scene_option: A custom `MjvOption` instance to use to render the scene
- instead of the default.
-
- Raises:
- ValueError: If `camera_id` is outside the valid range, or if camera does
- not exist.
- """
- if not isinstance(camera, _structs.MjvCamera):
- camera_id = camera
- if isinstance(camera_id, str):
- camera_id = _functions.mj_name2id(
- self._model, _enums.mjtObj.mjOBJ_CAMERA.value, camera_id
- )
- if camera_id == -1:
- raise ValueError(f'The camera "{camera}" does not exist.')
- if camera_id < -1 or camera_id >= self._model.ncam:
- raise ValueError(
- f'The camera id {camera_id} is out of'
- f' range [-1, {self._model.ncam}).'
- )
-
- # Render camera.
- camera = _structs.MjvCamera()
- camera.fixedcamid = camera_id
-
- # Defaults to mjCAMERA_FREE, otherwise mjCAMERA_FIXED refers to a
- # camera explicitly defined in the model.
- if camera_id == -1:
- camera.type = _enums.mjtCamera.mjCAMERA_FREE
- _functions.mjv_defaultFreeCamera(self._model, camera)
- else:
- camera.type = _enums.mjtCamera.mjCAMERA_FIXED
-
- scene_option = scene_option or self._scene_option
- _functions.mjv_updateScene(
- self._model,
- data,
- scene_option,
- None,
- camera,
- _enums.mjtCatBit.mjCAT_ALL.value,
- self._scene,
- )
-
- def close(self) -> None:
- """Frees the resources used by the renderer.
-
- This method can be used directly:
-
- ```python
- renderer = Renderer(...)
- # Use renderer.
- renderer.close()
- ```
-
- or via a context manager:
-
- ```python
- with Renderer(...) as renderer:
- # Use renderer.
- ```
- """
- if self._gl_context:
- self._gl_context.free()
- self._gl_context = None
- if self._mjr_context:
- self._mjr_context.free()
- self._mjr_context = None
-
- def __enter__(self):
- return self
-
- def __exit__(self, exc_type, exc_value, traceback):
- del exc_type, exc_value, traceback # Unused.
- self.close()
-
- def __del__(self) -> None:
- self.close()
+try:
+ from mujoco.rendering.classic.renderer import Renderer # pytype: disable=import-error
+except ImportError:
+ pass
diff --git a/python/mujoco/rendering/classic/__init__.py b/python/mujoco/rendering/classic/__init__.py
new file mode 100644
index 00000000..a9c4bdbf
--- /dev/null
+++ b/python/mujoco/rendering/classic/__init__.py
@@ -0,0 +1,16 @@
+# Copyright 2026 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.
+# ==============================================================================
+
+# Empty.
diff --git a/python/mujoco/rendering/classic/gl_context.py b/python/mujoco/rendering/classic/gl_context.py
new file mode 100644
index 00000000..61b861ec
--- /dev/null
+++ b/python/mujoco/rendering/classic/gl_context.py
@@ -0,0 +1,48 @@
+# Copyright 2022 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.
+# ==============================================================================
+"""Exports GLContext for MuJoCo Python bindings."""
+
+import ctypes
+import ctypes.util
+import os
+import platform
+
+# pylint: disable=g-import-not-at-top
+_SYSTEM = platform.system()
+_MUJOCO_GL = os.environ.get('MUJOCO_GL', '').lower().strip()
+if _MUJOCO_GL not in ('disable', 'disabled', 'off', 'false', '0'):
+ _VALID_MUJOCO_GL = ('enable', 'enabled', 'on', 'true', '1' , 'glfw', '')
+ if _SYSTEM == 'Linux':
+ _VALID_MUJOCO_GL += ('glx', 'egl', 'osmesa')
+ elif _SYSTEM == 'Windows':
+ _VALID_MUJOCO_GL += ('wgl',)
+ elif _SYSTEM == 'Darwin':
+ _VALID_MUJOCO_GL += ('cgl',)
+ if _MUJOCO_GL not in _VALID_MUJOCO_GL:
+ raise RuntimeError(
+ f'invalid value for environment variable MUJOCO_GL: {_MUJOCO_GL}')
+
+ if _SYSTEM == 'Linux' and _MUJOCO_GL == 'osmesa':
+ from mujoco.osmesa import GLContext as _GLContext
+ GLContext = _GLContext
+ elif _SYSTEM == 'Linux' and _MUJOCO_GL == 'egl':
+ from mujoco.egl import GLContext as _GLContext
+ GLContext = _GLContext
+ elif _SYSTEM == 'Darwin':
+ from mujoco.cgl import GLContext as _GLContext
+ GLContext = _GLContext
+ else:
+ from mujoco.glfw import GLContext as _GLContext
+ GLContext = _GLContext
diff --git a/python/mujoco/rendering/classic/renderer.py b/python/mujoco/rendering/classic/renderer.py
new file mode 100644
index 00000000..e2069db8
--- /dev/null
+++ b/python/mujoco/rendering/classic/renderer.py
@@ -0,0 +1,339 @@
+# Copyright 2022 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.
+# ==============================================================================
+"""Defines a renderer class for the MuJoCo Python native bindings."""
+
+from typing import Optional, Union
+
+import mujoco
+from mujoco.rendering.classic import gl_context
+import numpy as np
+
+GLContext = gl_context.GLContext if hasattr(gl_context, 'GLContext') else None
+
+
+class Renderer:
+ """Renders MuJoCo scenes."""
+
+ def __init__(
+ self,
+ model: mujoco.MjModel,
+ height: int = 240,
+ width: int = 320,
+ max_geom: int = 10000,
+ font_scale: mujoco.mjtFontScale = mujoco.mjtFontScale.mjFONTSCALE_150,
+ ) -> None:
+ """Initializes a new `Renderer`.
+
+ Args:
+ model: an MjModel instance.
+ height: image height in pixels.
+ width: image width in pixels.
+ max_geom: Optional integer specifying the maximum number of geoms that can
+ be rendered in the same scene. If None this will be chosen automatically
+ based on the estimated maximum number of renderable geoms in the model.
+ font_scale: Optional enum specifying the font scale for text.
+
+ Raises:
+ ValueError: If `camera_id` is outside the valid range, or if `width` or
+ `height` exceed the dimensions of MuJoCo's offscreen framebuffer.
+ """
+ buffer_width = model.vis.global_.offwidth
+ buffer_height = model.vis.global_.offheight
+ if width > buffer_width:
+ raise ValueError(f"""
+Image width {width} > framebuffer width {buffer_width}. Either reduce the image
+width or specify a larger offscreen framebuffer in the model XML using the
+clause:
+
+
+""".lstrip())
+
+ if height > buffer_height:
+ raise ValueError(f"""
+Image height {height} > framebuffer height {buffer_height}. Either reduce the
+image height or specify a larger offscreen framebuffer in the model XML using
+the clause:
+
+
+""".lstrip())
+
+ self._width = width
+ self._height = height
+ self._model = model
+
+ self._scene = mujoco.MjvScene(model=model, maxgeom=max_geom)
+ self._scene_option = mujoco.MjvOption()
+
+ self._rect = mujoco.MjrRect(0, 0, self._width, self._height)
+
+ # Create render contexts.
+ # TODO(nimrod): Figure out why pytype doesn't like gl_context.GLContext
+ self._gl_context = None # type: ignore
+ if gl_context.GLContext is not None:
+ self._gl_context = gl_context.GLContext(width, height)
+ if self._gl_context:
+ self._gl_context.make_current()
+ self._mjr_context = mujoco.MjrContext(model, font_scale.value)
+ mujoco.mjr_setBuffer(
+ mujoco.mjtFramebuffer.mjFB_OFFSCREEN.value, self._mjr_context
+ )
+ self._mjr_context.readDepthMap = mujoco.mjtDepthMap.mjDEPTH_ZEROFAR
+
+ # Default render flags.
+ self._depth_rendering = False
+ self._segmentation_rendering = False
+
+ @property
+ def model(self):
+ return self._model
+
+ @property
+ def scene(self) -> mujoco.MjvScene:
+ return self._scene
+
+ @property
+ def height(self):
+ return self._height
+
+ @property
+ def width(self):
+ return self._width
+
+ def enable_depth_rendering(self):
+ self._segmentation_rendering = False
+ self._depth_rendering = True
+
+ def disable_depth_rendering(self):
+ self._depth_rendering = False
+
+ def enable_segmentation_rendering(self):
+ self._segmentation_rendering = True
+ self._depth_rendering = False
+
+ def disable_segmentation_rendering(self):
+ self._segmentation_rendering = False
+
+ def render(self, *, out: Optional[np.ndarray] = None) -> np.ndarray:
+ """Renders the scene as a numpy array of pixel values.
+
+ Args:
+ out: Alternative output array in which to place the resulting pixels. It
+ must have the same shape as the expected output but the type will be
+ cast if necessary. The expted shape depends on the value of
+ `self._depth_rendering`: when `True`, we expect `out.shape == (width,
+ height)`, and `out.shape == (width, height, 3)` when `False`.
+
+ Returns:
+ A new numpy array holding the pixels with shape `(H, W)` or `(H, W, 3)`,
+ depending on the value of `self._depth_rendering` unless
+ `out is None`, in which case a reference to `out` is returned.
+
+ Raises:
+ RuntimeError: if this method is called after the close method.
+ """
+ original_flags = self._scene.flags.copy()
+
+ # Using segmented rendering for depth makes the calculated depth more
+ # accurate at far distances.
+ if self._depth_rendering or self._segmentation_rendering:
+ self._scene.flags[mujoco.mjtRndFlag.mjRND_SEGMENT] = True
+ self._scene.flags[mujoco.mjtRndFlag.mjRND_IDCOLOR] = True
+
+ if self._mjr_context is None:
+ raise RuntimeError('render cannot be called after close.')
+
+ if self._gl_context:
+ self._gl_context.make_current()
+
+ if self._depth_rendering:
+ out_shape = (self._height, self._width)
+ out_dtype = np.float32
+ else:
+ out_shape = (self._height, self._width, 3)
+ out_dtype = np.uint8
+
+ if out is None:
+ out = np.empty(out_shape, dtype=out_dtype)
+ else:
+ if out.shape != out_shape:
+ raise ValueError(
+ f'Expected `out.shape == {out_shape}`. Got `out.shape={out.shape}`'
+ ' instead. When using depth rendering, the out array should be of'
+ ' shape `(width, height)` and otherwise (width, height, 3).'
+ f' Got `(self.height, self.width)={(self.height, self.width)}` and'
+ f' `self._depth_rendering={self._depth_rendering}`.'
+ )
+
+ # Render scene and read contents of RGB and depth buffers.
+ mujoco.mjr_render(self._rect, self._scene, self._mjr_context)
+ if self._depth_rendering:
+ mujoco.mjr_readPixels(None, out, self._rect, self._mjr_context)
+
+ # Get the distances to the near and far clipping planes.
+ extent = self._model.stat.extent
+ near = self._model.vis.map.znear * extent
+ far = self._model.vis.map.zfar * extent
+
+ # Calculate OpenGL perspective matrix values in float32 precision
+ # so they are close to what glFrustum returns
+ # https://registry.khronos.org/OpenGL-Refpages/gl2.1/xhtml/glFrustum.xml
+ zfar = np.float32(far)
+ znear = np.float32(near)
+ c_coef = -(zfar + znear) / (zfar - znear)
+ d_coef = -(np.float32(2) * zfar * znear) / (zfar - znear)
+
+ # In reverse Z mode the perspective matrix is transformed by the following
+ c_coef = np.float32(-0.5) * c_coef - np.float32(0.5)
+ d_coef = np.float32(-0.5) * d_coef
+
+ # We need 64 bits to convert Z from ndc to metric depth without noticeable
+ # losses in precision
+ out_64 = out.astype(np.float64)
+
+ # Undo OpenGL projection
+ # Note: We do not need to take action to convert from window coordinates
+ # to normalized device coordinates because in reversed Z mode the mapping
+ # is identity
+ out_64 = d_coef / (out_64 + c_coef)
+
+ # Cast result back to float32 for backwards compatibility
+ # This has a small accuracy cost
+ out[:] = out_64.astype(np.float32)
+
+ # Reset scene flags.
+ np.copyto(self._scene.flags, original_flags)
+ elif self._segmentation_rendering:
+ mujoco.mjr_readPixels(out, None, self._rect, self._mjr_context)
+
+ # Convert 3-channel uint8 to 1-channel uint32.
+ image3 = out.astype(np.uint32)
+ segimage = (
+ image3[:, :, 0] + image3[:, :, 1] * (2**8) + image3[:, :, 2] * (2**16)
+ )
+ # Remap segid to 2-channel (object ID, object type) pair.
+ # Seg ID 0 is background -- will be remapped to (-1, -1).
+ ngeoms = self._scene.ngeom
+ segid2output = np.full(
+ (ngeoms + 1, 2), fill_value=-1, dtype=np.int32
+ ) # Seg id cannot be > ngeom + 1.
+ visible_geoms = [g for g in self._scene.geoms[:ngeoms] if g.segid != -1]
+ visible_segids = np.array([g.segid + 1 for g in visible_geoms], np.int32)
+ visible_objid = np.array([g.objid for g in visible_geoms], np.int32)
+ visible_objtype = np.array([g.objtype for g in visible_geoms], np.int32)
+ segid2output[visible_segids, 0] = visible_objid
+ segid2output[visible_segids, 1] = visible_objtype
+ out = segid2output[segimage]
+
+ # Reset scene flags.
+ np.copyto(self._scene.flags, original_flags)
+ else:
+ mujoco.mjr_readPixels(out, None, self._rect, self._mjr_context)
+
+ if self._gl_context:
+ # If using EGL, OSMesa, or GLFW, the output image is flipped vertically.
+ # No flip is needed for Filament.
+ out[:] = np.flipud(out)
+
+ return out
+
+ def update_scene(
+ self,
+ data: mujoco.MjData,
+ camera: Union[int, str, mujoco.MjvCamera] = -1,
+ scene_option: Optional[mujoco.MjvOption] = None,
+ ):
+ """Updates geometry used for rendering.
+
+ Args:
+ data: An instance of `MjData`.
+ camera: An instance of `MjvCamera`, a string or an integer
+ scene_option: A custom `MjvOption` instance to use to render the scene
+ instead of the default.
+
+ Raises:
+ ValueError: If `camera_id` is outside the valid range, or if camera does
+ not exist.
+ """
+ if not isinstance(camera, mujoco.MjvCamera):
+ camera_id = camera
+ if isinstance(camera_id, str):
+ camera_id = mujoco.mj_name2id(
+ self._model, mujoco.mjtObj.mjOBJ_CAMERA.value, camera_id
+ )
+ if camera_id == -1:
+ raise ValueError(f'The camera "{camera}" does not exist.')
+ if camera_id < -1 or camera_id >= self._model.ncam:
+ raise ValueError(
+ f'The camera id {camera_id} is out of'
+ f' range [-1, {self._model.ncam}).'
+ )
+
+ # Render camera.
+ camera = mujoco.MjvCamera()
+ camera.fixedcamid = camera_id
+
+ # Defaults to mjCAMERA_FREE, otherwise mjCAMERA_FIXED refers to a
+ # camera explicitly defined in the model.
+ if camera_id == -1:
+ camera.type = mujoco.mjtCamera.mjCAMERA_FREE
+ mujoco.mjv_defaultFreeCamera(self._model, camera)
+ else:
+ camera.type = mujoco.mjtCamera.mjCAMERA_FIXED
+
+ scene_option = scene_option or self._scene_option
+ mujoco.mjv_updateScene(
+ self._model,
+ data,
+ scene_option,
+ None,
+ camera,
+ mujoco.mjtCatBit.mjCAT_ALL.value,
+ self._scene,
+ )
+
+ def close(self) -> None:
+ """Frees the resources used by the renderer.
+
+ This method can be used directly:
+
+ ```python
+ renderer = Renderer(...)
+ # Use renderer.
+ renderer.close()
+ ```
+
+ or via a context manager:
+
+ ```python
+ with Renderer(...) as renderer:
+ # Use renderer.
+ ```
+ """
+ if self._gl_context:
+ self._gl_context.free()
+ self._gl_context = None
+ if self._mjr_context:
+ self._mjr_context.free()
+ self._mjr_context = None
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ del exc_type, exc_value, traceback # Unused.
+ self.close()
+
+ def __del__(self) -> None:
+ self.close()