From 9d75d6a507cbdebc6a2758a4b5e0039292a2a12b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Quentin=20Gallou=C3=A9dec?= <45557362+qgallouedec@users.noreply.github.com> Date: Tue, 24 Jan 2023 14:41:40 +0100 Subject: [PATCH] Fix rendering for `render_mode=human` (#56) * fix human rendering * add render_mode attr * add test * RGBA to RGB * fix python3.10 * render Move render kwargs to constructor, add renderer arg * doc * Fix render docstring * target pos default handling in render * fix defaults typo * Clarify renderer documentation * update test * fix doc * update version --- docs/_static/img/opengl.png | Bin 0 -> 23363 bytes docs/_static/img/tiny.png | Bin 0 -> 13796 bytes docs/_static/img/top_view.png | Bin 0 -> 22492 bytes docs/conf.py | 2 +- docs/index.rst | 1 + docs/usage/advanced_rendering.rst | 75 +++++++++ panda_gym/envs/core.py | 78 +++++----- panda_gym/envs/panda_tasks.py | 248 ++++++++++++++++++++++-------- panda_gym/pybullet.py | 49 ++---- panda_gym/version.txt | 2 +- test/render_test.py | 31 ++++ 11 files changed, 344 insertions(+), 142 deletions(-) create mode 100644 docs/_static/img/opengl.png create mode 100644 docs/_static/img/tiny.png create mode 100644 docs/_static/img/top_view.png create mode 100644 docs/usage/advanced_rendering.rst create mode 100644 test/render_test.py diff --git a/docs/_static/img/opengl.png b/docs/_static/img/opengl.png new file mode 100644 index 0000000000000000000000000000000000000000..d9edae9a0b4bf30ceff230b599ce0eaf9c6b86e0 GIT binary patch literal 23363 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 SFn1u14LRiGPHA=sX8s?UmS;8q literal 0 HcmV?d00001 diff --git a/docs/_static/img/tiny.png b/docs/_static/img/tiny.png new file mode 100644 index 0000000000000000000000000000000000000000..a255ef46883ce36e8f59ecbcf2ba9c4d141dc3ea GIT binary patch literal 13796 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 None: + def __init__( + self, + robot: PyBulletRobot, + task: Task, + render_width: int = 720, + render_height: int = 480, + render_target_position: Optional[np.ndarray] = None, + render_distance: float = 1.4, + render_yaw: float = 45, + render_pitch: float = -30, + render_roll: float = 0, + ) -> None: assert robot.sim == task.sim, "The robot and the task must belong to the same simulation." self.sim = robot.sim + self.render_mode = self.sim.render_mode self.metadata["render_fps"] = 1 / self.sim.dt self.robot = robot self.task = task @@ -226,6 +245,14 @@ def __init__(self, robot: PyBulletRobot, task: Task) -> None: self.compute_reward = self.task.compute_reward self._saved_goal = dict() # For state saving and restoring + self.render_width = render_width + self.render_height = render_height + self.render_target_position = render_target_position + self.render_distance = render_distance + self.render_yaw = render_yaw + self.render_pitch = render_pitch + self.render_roll = render_roll + def _get_obs(self) -> Dict[str, np.ndarray]: robot_obs = self.robot.get_obs().astype(np.float32) # robot state task_obs = self.task.get_obs().astype(np.float32) # object position, velococity, etc... @@ -291,49 +318,20 @@ def step(self, action: np.ndarray) -> Tuple[Dict[str, np.ndarray], float, bool, def close(self) -> None: self.sim.close() - def render( - self, - width: int = 720, - height: int = 480, - target_position: Optional[np.ndarray] = None, - distance: float = 1.4, - yaw: float = 45, - pitch: float = -30, - roll: float = 0, - mode: Optional[str] = None, - ) -> Optional[np.ndarray]: + def render(self) -> Optional[np.ndarray]: """Render. - If render mode is "rgb_array", return an RGB array of the scene. Else, do nothing. - - Args: - width (int, optional): Image width. Defaults to 720. - height (int, optional): Image height. Defaults to 480. - target_position (np.ndarray, optional): Camera targetting this postion, as (x, y, z). - Defaults to [0., 0., 0.]. - distance (float, optional): Distance of the camera. Defaults to 1.4. - yaw (float, optional): Yaw of the camera. Defaults to 45. - pitch (float, optional): Pitch of the camera. Defaults to -30. - roll (int, optional): Rool of the camera. Defaults to 0. - mode (str, optional): Deprecated: This argument is deprecated and will be removed in a future - version. Use the render_mode argument of the constructor instead. + If render mode is "rgb_array", return an RGB array of the scene. Else, do nothing and return None. Returns: RGB np.ndarray or None: An RGB array if mode is 'rgb_array', else None. """ - if mode is not None: - warnings.warn( - "The 'mode' argument is deprecated and will be removed in " - "a future version. Use the 'render_mode' argument of the constructor instead.", - DeprecationWarning, - ) - target_position = target_position if target_position is not None else np.zeros(3) return self.sim.render( - width=width, - height=height, - target_position=target_position, - distance=distance, - yaw=yaw, - pitch=pitch, - roll=roll, + width=self.render_width, + height=self.render_height, + target_position=self.render_target_position, + distance=self.render_distance, + yaw=self.render_yaw, + pitch=self.render_pitch, + roll=self.render_roll, ) diff --git a/panda_gym/envs/panda_tasks.py b/panda_gym/envs/panda_tasks.py index bfd45d9b..b809316b 100644 --- a/panda_gym/envs/panda_tasks.py +++ b/panda_gym/envs/panda_tasks.py @@ -1,4 +1,3 @@ -import warnings from typing import Optional import numpy as np @@ -22,8 +21,17 @@ class PandaFlipEnv(RobotTaskEnv): reward_type (str, optional): "sparse" or "dense". Defaults to "sparse". control_type (str, optional): "ee" to control end-effector position or "joints" to control joint values. Defaults to "ee". - render (bool, optional): Deprecated: This argument is deprecated and will be removed in a future - version. Use the render_mode argument instead. + renderer (str, optional): Renderer, either "Tiny" or OpenGL". Defaults to "Tiny" if render mode is "human" + and "OpenGL" if render mode is "rgb_array". Only "OpenGL" is available for human render mode. + render_width (int, optional): Image width. Defaults to 720. + render_height (int, optional): Image height. Defaults to 480. + render_target_position (np.ndarray, optional): Camera targetting this postion, as (x, y, z). + Defaults to [0., 0., 0.]. + render_distance (float, optional): Distance of the camera. Defaults to 1.4. + render_yaw (float, optional): Yaw of the camera. Defaults to 45. + render_pitch (float, optional): Pitch of the camera. Defaults to -30. + render_roll (int, optional): Rool of the camera. Defaults to 0. + """ def __init__( @@ -31,18 +39,29 @@ def __init__( render_mode: str = "rgb_array", reward_type: str = "sparse", control_type: str = "ee", - render: Optional[bool] = None, + renderer: str = "Tiny", + render_width: int = 720, + render_height: int = 480, + render_target_position: Optional[np.ndarray] = None, + render_distance: float = 1.4, + render_yaw: float = 45, + render_pitch: float = -30, + render_roll: float = 0, ) -> None: - if render is not None: - warnings.warn( - "The 'render' argument is deprecated and will be removed in " - "a future version. Use the 'render_mode' argument instead.", - DeprecationWarning, - ) - sim = PyBullet(render_mode=render_mode) + sim = PyBullet(render_mode=render_mode, renderer=renderer) robot = Panda(sim, block_gripper=False, base_position=np.array([-0.6, 0.0, 0.0]), control_type=control_type) task = Flip(sim, reward_type=reward_type) - super().__init__(robot, task) + super().__init__( + robot, + task, + render_width=render_width, + render_height=render_height, + render_target_position=render_target_position, + render_distance=render_distance, + render_yaw=render_yaw, + render_pitch=render_pitch, + render_roll=render_roll, + ) class PandaPickAndPlaceEnv(RobotTaskEnv): @@ -53,8 +72,16 @@ class PandaPickAndPlaceEnv(RobotTaskEnv): reward_type (str, optional): "sparse" or "dense". Defaults to "sparse". control_type (str, optional): "ee" to control end-effector position or "joints" to control joint values. Defaults to "ee". - render (bool, optional): Deprecated: This argument is deprecated and will be removed in a future - version. Use the render_mode argument instead. + renderer (str, optional): Renderer, either "Tiny" or OpenGL". Defaults to "Tiny" if render mode is "human" + and "OpenGL" if render mode is "rgb_array". Only "OpenGL" is available for human render mode. + render_width (int, optional): Image width. Defaults to 720. + render_height (int, optional): Image height. Defaults to 480. + render_target_position (np.ndarray, optional): Camera targetting this postion, as (x, y, z). + Defaults to [0., 0., 0.]. + render_distance (float, optional): Distance of the camera. Defaults to 1.4. + render_yaw (float, optional): Yaw of the camera. Defaults to 45. + render_pitch (float, optional): Pitch of the camera. Defaults to -30. + render_roll (int, optional): Rool of the camera. Defaults to 0. """ def __init__( @@ -62,18 +89,29 @@ def __init__( render_mode: str = "rgb_array", reward_type: str = "sparse", control_type: str = "ee", - render: Optional[bool] = None, + renderer: str = "Tiny", + render_width: int = 720, + render_height: int = 480, + render_target_position: Optional[np.ndarray] = None, + render_distance: float = 1.4, + render_yaw: float = 45, + render_pitch: float = -30, + render_roll: float = 0, ) -> None: - if render is not None: - warnings.warn( - "The 'render' argument is deprecated and will be removed in " - "a future version. Use the 'render_mode' argument instead.", - DeprecationWarning, - ) - sim = PyBullet(render_mode=render_mode) + sim = PyBullet(render_mode=render_mode, renderer=renderer) robot = Panda(sim, block_gripper=False, base_position=np.array([-0.6, 0.0, 0.0]), control_type=control_type) task = PickAndPlace(sim, reward_type=reward_type) - super().__init__(robot, task) + super().__init__( + robot, + task, + render_width=render_width, + render_height=render_height, + render_target_position=render_target_position, + render_distance=render_distance, + render_yaw=render_yaw, + render_pitch=render_pitch, + render_roll=render_roll, + ) class PandaPushEnv(RobotTaskEnv): @@ -84,8 +122,16 @@ class PandaPushEnv(RobotTaskEnv): reward_type (str, optional): "sparse" or "dense". Defaults to "sparse". control_type (str, optional): "ee" to control end-effector position or "joints" to control joint values. Defaults to "ee". - render (bool, optional): Deprecated: This argument is deprecated and will be removed in a future - version. Use the render_mode argument instead. + renderer (str, optional): Renderer, either "Tiny" or OpenGL". Defaults to "Tiny" if render mode is "human" + and "OpenGL" if render mode is "rgb_array". Only "OpenGL" is available for human render mode. + render_width (int, optional): Image width. Defaults to 720. + render_height (int, optional): Image height. Defaults to 480. + render_target_position (np.ndarray, optional): Camera targetting this postion, as (x, y, z). + Defaults to [0., 0., 0.]. + render_distance (float, optional): Distance of the camera. Defaults to 1.4. + render_yaw (float, optional): Yaw of the camera. Defaults to 45. + render_pitch (float, optional): Pitch of the camera. Defaults to -30. + render_roll (int, optional): Rool of the camera. Defaults to 0. """ def __init__( @@ -93,18 +139,29 @@ def __init__( render_mode: str = "rgb_array", reward_type: str = "sparse", control_type: str = "ee", - render: Optional[bool] = None, + renderer: str = "Tiny", + render_width: int = 720, + render_height: int = 480, + render_target_position: Optional[np.ndarray] = None, + render_distance: float = 1.4, + render_yaw: float = 45, + render_pitch: float = -30, + render_roll: float = 0, ) -> None: - if render is not None: - warnings.warn( - "The 'render' argument is deprecated and will be removed in " - "a future version. Use the 'render_mode' argument instead.", - DeprecationWarning, - ) - sim = PyBullet(render_mode=render_mode) + sim = PyBullet(render_mode=render_mode, renderer=renderer) robot = Panda(sim, block_gripper=True, base_position=np.array([-0.6, 0.0, 0.0]), control_type=control_type) task = Push(sim, reward_type=reward_type) - super().__init__(robot, task) + super().__init__( + robot, + task, + render_width=render_width, + render_height=render_height, + render_target_position=render_target_position, + render_distance=render_distance, + render_yaw=render_yaw, + render_pitch=render_pitch, + render_roll=render_roll, + ) class PandaReachEnv(RobotTaskEnv): @@ -115,8 +172,16 @@ class PandaReachEnv(RobotTaskEnv): reward_type (str, optional): "sparse" or "dense". Defaults to "sparse". control_type (str, optional): "ee" to control end-effector position or "joints" to control joint values. Defaults to "ee". - render (bool, optional): Deprecated: This argument is deprecated and will be removed in a future - version. Use the render_mode argument instead. + renderer (str, optional): Renderer, either "Tiny" or OpenGL". Defaults to "Tiny" if render mode is "human" + and "OpenGL" if render mode is "rgb_array". Only "OpenGL" is available for human render mode. + render_width (int, optional): Image width. Defaults to 720. + render_height (int, optional): Image height. Defaults to 480. + render_target_position (np.ndarray, optional): Camera targetting this postion, as (x, y, z). + Defaults to [0., 0., 0.]. + render_distance (float, optional): Distance of the camera. Defaults to 1.4. + render_yaw (float, optional): Yaw of the camera. Defaults to 45. + render_pitch (float, optional): Pitch of the camera. Defaults to -30. + render_roll (int, optional): Rool of the camera. Defaults to 0. """ def __init__( @@ -124,18 +189,29 @@ def __init__( render_mode: str = "rgb_array", reward_type: str = "sparse", control_type: str = "ee", - render: Optional[bool] = None, + renderer: str = "Tiny", + render_width: int = 720, + render_height: int = 480, + render_target_position: Optional[np.ndarray] = None, + render_distance: float = 1.4, + render_yaw: float = 45, + render_pitch: float = -30, + render_roll: float = 0, ) -> None: - if render is not None: - warnings.warn( - "The 'render' argument is deprecated and will be removed in " - "a future version. Use the 'render_mode' argument instead.", - DeprecationWarning, - ) - sim = PyBullet(render_mode=render_mode) + sim = PyBullet(render_mode=render_mode, renderer=renderer) robot = Panda(sim, block_gripper=True, base_position=np.array([-0.6, 0.0, 0.0]), control_type=control_type) task = Reach(sim, reward_type=reward_type, get_ee_position=robot.get_ee_position) - super().__init__(robot, task) + super().__init__( + robot, + task, + render_width=render_width, + render_height=render_height, + render_target_position=render_target_position, + render_distance=render_distance, + render_yaw=render_yaw, + render_pitch=render_pitch, + render_roll=render_roll, + ) class PandaSlideEnv(RobotTaskEnv): @@ -146,8 +222,16 @@ class PandaSlideEnv(RobotTaskEnv): reward_type (str, optional): "sparse" or "dense". Defaults to "sparse". control_type (str, optional): "ee" to control end-effector position or "joints" to control joint values. Defaults to "ee". - render (bool, optional): Deprecated: This argument is deprecated and will be removed in a future - version. Use the render_mode argument instead. + renderer (str, optional): Renderer, either "Tiny" or OpenGL". Defaults to "Tiny" if render mode is "human" + and "OpenGL" if render mode is "rgb_array". Only "OpenGL" is available for human render mode. + render_width (int, optional): Image width. Defaults to 720. + render_height (int, optional): Image height. Defaults to 480. + render_target_position (np.ndarray, optional): Camera targetting this postion, as (x, y, z). + Defaults to [0., 0., 0.]. + render_distance (float, optional): Distance of the camera. Defaults to 1.4. + render_yaw (float, optional): Yaw of the camera. Defaults to 45. + render_pitch (float, optional): Pitch of the camera. Defaults to -30. + render_roll (int, optional): Rool of the camera. Defaults to 0. """ def __init__( @@ -155,18 +239,29 @@ def __init__( render_mode: str = "rgb_array", reward_type: str = "sparse", control_type: str = "ee", - render: Optional[bool] = None, + renderer: str = "Tiny", + render_width: int = 720, + render_height: int = 480, + render_target_position: Optional[np.ndarray] = None, + render_distance: float = 1.4, + render_yaw: float = 45, + render_pitch: float = -30, + render_roll: float = 0, ) -> None: - if render is not None: - warnings.warn( - "The 'render' argument is deprecated and will be removed in " - "a future version. Use the 'render_mode' argument instead.", - DeprecationWarning, - ) - sim = PyBullet(render_mode=render_mode) + sim = PyBullet(render_mode=render_mode, renderer=renderer) robot = Panda(sim, block_gripper=True, base_position=np.array([-0.6, 0.0, 0.0]), control_type=control_type) task = Slide(sim, reward_type=reward_type) - super().__init__(robot, task) + super().__init__( + robot, + task, + render_width=render_width, + render_height=render_height, + render_target_position=render_target_position, + render_distance=render_distance, + render_yaw=render_yaw, + render_pitch=render_pitch, + render_roll=render_roll, + ) class PandaStackEnv(RobotTaskEnv): @@ -177,8 +272,16 @@ class PandaStackEnv(RobotTaskEnv): reward_type (str, optional): "sparse" or "dense". Defaults to "sparse". control_type (str, optional): "ee" to control end-effector position or "joints" to control joint values. Defaults to "ee". - render (bool, optional): Deprecated: This argument is deprecated and will be removed in a future - version. Use the render_mode argument instead. + renderer (str, optional): Renderer, either "Tiny" or OpenGL". Defaults to "Tiny" if render mode is "human" + and "OpenGL" if render mode is "rgb_array". Only "OpenGL" is available for human render mode. + render_width (int, optional): Image width. Defaults to 720. + render_height (int, optional): Image height. Defaults to 480. + render_target_position (np.ndarray, optional): Camera targetting this postion, as (x, y, z). + Defaults to [0., 0., 0.]. + render_distance (float, optional): Distance of the camera. Defaults to 1.4. + render_yaw (float, optional): Yaw of the camera. Defaults to 45. + render_pitch (float, optional): Pitch of the camera. Defaults to -30. + render_roll (int, optional): Rool of the camera. Defaults to 0. """ def __init__( @@ -186,15 +289,26 @@ def __init__( render_mode: str = "rgb_array", reward_type: str = "sparse", control_type: str = "ee", - render: Optional[bool] = None, + renderer: str = "Tiny", + render_width: int = 720, + render_height: int = 480, + render_target_position: Optional[np.ndarray] = None, + render_distance: float = 1.4, + render_yaw: float = 45, + render_pitch: float = -30, + render_roll: float = 0, ) -> None: - if render is not None: - warnings.warn( - "The 'render' argument is deprecated and will be removed in " - "a future version. Use the 'render_mode' argument instead.", - DeprecationWarning, - ) - sim = PyBullet(render_mode=render_mode) + sim = PyBullet(render_mode=render_mode, renderer=renderer) robot = Panda(sim, block_gripper=False, base_position=np.array([-0.6, 0.0, 0.0]), control_type=control_type) task = Stack(sim, reward_type=reward_type) - super().__init__(robot, task) + super().__init__( + robot, + task, + render_width=render_width, + render_height=render_height, + render_target_position=render_target_position, + render_distance=render_distance, + render_yaw=render_yaw, + render_pitch=render_pitch, + render_roll=render_roll, + ) diff --git a/panda_gym/pybullet.py b/panda_gym/pybullet.py index 0145f4c8..a6760cbf 100644 --- a/panda_gym/pybullet.py +++ b/panda_gym/pybullet.py @@ -1,5 +1,4 @@ import os -import warnings from contextlib import contextmanager from typing import Any, Dict, Iterator, Optional @@ -19,8 +18,8 @@ class PyBullet: n_substeps (int, optional): Number of sim substep when step() is called. Defaults to 20. background_color (np.ndarray, optional): The background color as (red, green, blue). Defaults to np.array([223, 54, 45]). - render (bool, optional): Deprecated: This argument is deprecated and will be removed in a future - version. Use the render_mode argument instead. + renderer (str, optional): Renderer, either "Tiny" or OpenGL". Defaults to "Tiny" if render mode is "human" + and "OpenGL" if render mode is "rgb_array". Only "OpenGL" is available for human render mode. """ def __init__( @@ -28,15 +27,9 @@ def __init__( render_mode: str = "rgb_array", n_substeps: int = 20, background_color: Optional[np.ndarray] = None, - render: Optional[bool] = None, + renderer: str = "Tiny", ) -> None: self.render_mode = render_mode - if render is not None: - warnings.warn( - "The 'render' argument is deprecated and will be removed in " - "a future version. Use the 'render_mode' argument instead.", - DeprecationWarning, - ) background_color = background_color if background_color is not None else np.array([223.0, 54.0, 45.0]) self.background_color = background_color.astype(np.float32) / 255 options = "--background_color_red={} --background_color_green={} --background_color_blue={}".format( @@ -45,7 +38,12 @@ def __init__( if self.render_mode == "human": self.connection_mode = p.GUI elif self.render_mode == "rgb_array": - self.connection_mode = p.DIRECT + if renderer == "OpenGL": + self.connection_mode = p.GUI + elif renderer == "Tiny": + self.connection_mode = p.DIRECT + else: + raise ValueError("The 'renderer' argument is must be in {'Tiny', 'OpenGL'}") else: raise ValueError("The 'render' argument is must be in {'rgb_array', 'human'}") self.physics_client = bc.BulletClient(connection_mode=self.connection_mode, options=options) @@ -108,11 +106,10 @@ def render( yaw: float = 45, pitch: float = -30, roll: float = 0, - mode: Optional[str] = None, ) -> Optional[np.ndarray]: """Render. - If render mode is "rgb_array", return an RGB array of the scene. Else, do nothing. + If render mode is "rgb_array", return an RGB array of the scene. Else, do nothing and return None. Args: width (int, optional): Image width. Defaults to 720. @@ -129,24 +126,8 @@ def render( Returns: RGB np.ndarray or None: An RGB array if mode is 'rgb_array', else None. """ - if mode is not None: - warnings.warn( - "The 'mode' argument is deprecated and will be removed in " - "a future version. Use the 'render_mode' argument of the constructor instead.", - DeprecationWarning, - ) - target_position = target_position if target_position is not None else np.zeros(3) if self.render_mode == "rgb_array": - if self.connection_mode == p.DIRECT: - warnings.warn( - "You have set 'render_mode' to be 'rgb_array'. This is correct if you want to render the " - "environment without an OpenGL engine. However, this option does not support transparency " - "or background rendering and may result in lower quality rendering. To improve the rendering " - "quality, we recommend that you use render_mode='human' instead. The render() method will " - "return a more qualitative rendering of the environment. " - "For example: env = gym.make('PandaReach-v3', render_mode='human').", - UserWarning, - ) + target_position = target_position if target_position is not None else np.zeros(3) view_matrix = self.physics_client.computeViewMatrixFromYawPitchRoll( cameraTargetPosition=target_position, distance=distance, @@ -158,15 +139,17 @@ def render( proj_matrix = self.physics_client.computeProjectionMatrixFOV( fov=60, aspect=float(width) / height, nearVal=0.1, farVal=100.0 ) - (_, _, px, depth, _) = self.physics_client.getCameraImage( + (_, _, rgba, _, _) = self.physics_client.getCameraImage( width=width, height=height, viewMatrix=view_matrix, projectionMatrix=proj_matrix, + shadow=True, renderer=p.ER_BULLET_HARDWARE_OPENGL, ) - - return px + # With Python3.10, pybullet return flat tuple instead of array. So we need to build create the array. + rgba = np.array(rgba, dtype=np.uint8).reshape((height, width, 4)) + return rgba[..., :3] def get_base_position(self, body: str) -> np.ndarray: """Get the position of the body. diff --git a/panda_gym/version.txt b/panda_gym/version.txt index 282895a8..b38ebbfc 100644 --- a/panda_gym/version.txt +++ b/panda_gym/version.txt @@ -1 +1 @@ -3.0.3 \ No newline at end of file +3.0.4 \ No newline at end of file diff --git a/test/render_test.py b/test/render_test.py new file mode 100644 index 00000000..05fe1660 --- /dev/null +++ b/test/render_test.py @@ -0,0 +1,31 @@ +import gymnasium as gym + +import panda_gym + + +def test_render(): + env = gym.make("PandaReach-v3", render_mode="rgb_array") + env.reset() + + for _ in range(100): + _, _, terminated, truncated, _ = env.step(env.action_space.sample()) + img = env.render() + assert img.shape == (480, 720, 3) + if terminated or truncated: + env.reset() + + env.close() + + +def test_new_render_shape(): + env = gym.make("PandaReach-v3", render_mode="rgb_array", render_height=48, render_width=84) + + env.reset() + for _ in range(100): + _, _, terminated, truncated, _ = env.step(env.action_space.sample()) + image = env.render() + assert image.shape == (48, 84, 3) + if terminated or truncated: + env.reset() + + env.close()