From f0af9d19756948d5fa7c88bf31744e44b9b7b6d1 Mon Sep 17 00:00:00 2001 From: Alexis Duburcq Date: Sun, 8 Feb 2026 00:15:36 +0800 Subject: [PATCH] Disable shadow and plane reflection when using software rendering. --- genesis/engine/sensors/imu.py | 8 ++--- genesis/ext/pyrender/font.py | 56 ++++++++++++++++++++++++------ genesis/ext/pyrender/viewer.py | 46 ++++++++++++++++-------- genesis/options/sensors/options.py | 14 ++++---- genesis/vis/viewer.py | 4 --- tests/test_render.py | 31 ++++++++++------- tests/utils.py | 2 +- 7 files changed, 108 insertions(+), 53 deletions(-) diff --git a/genesis/engine/sensors/imu.py b/genesis/engine/sensors/imu.py index 9298b087cb..329ff04f03 100644 --- a/genesis/engine/sensors/imu.py +++ b/genesis/engine/sensors/imu.py @@ -271,7 +271,7 @@ def _draw_debug(self, context: "RasterizerContext", buffer_updates: dict[str, np data = self.read(env_idx) acc_vec = data.lin_acc.reshape((3,)) * self._options.debug_acc_scale gyro_vec = data.ang_vel.reshape((3,)) * self._options.debug_gyro_scale - mag_vec = data.mag.reshape((3,)) * self._options.debug_mag_scale # added mag debug + mag_vec = data.mag.reshape((3,)) * self._options.debug_mag_scale # transform from local frame to world frame offset_quat = transform_quat_by_quat(self.quat_offset, quat) @@ -286,8 +286,8 @@ def _draw_debug(self, context: "RasterizerContext", buffer_updates: dict[str, np self.debug_objects += filter( None, ( - context.draw_debug_arrow(pos=pos, vec=acc_vec, color=self._options.debug_acc_color), - context.draw_debug_arrow(pos=pos, vec=gyro_vec, color=self._options.debug_gyro_color), - context.draw_debug_arrow(pos=pos, vec=mag_vec, color=self._options.debug_mag_color), + context.draw_debug_arrow(pos=pos, vec=acc_vec, radius=0.006, color=self._options.debug_acc_color), + context.draw_debug_arrow(pos=pos, vec=gyro_vec, radius=0.0055, color=self._options.debug_gyro_color), + context.draw_debug_arrow(pos=pos, vec=mag_vec, radius=0.005, color=self._options.debug_mag_color), ), ) diff --git a/genesis/ext/pyrender/font.py b/genesis/ext/pyrender/font.py index e47a17d192..5fab19fbd5 100644 --- a/genesis/ext/pyrender/font.py +++ b/genesis/ext/pyrender/font.py @@ -79,24 +79,60 @@ def __init__(self, font_file, font_pt=40): self._character_map = {} for i in range(0, 128): - # Generate texture face = self._face - face.load_char(chr(i)) - src = np.asarray(face.glyph.bitmap.buffer, dtype=np.float32) / 255.0 - src = src.reshape((face.glyph.bitmap.rows, face.glyph.bitmap.width)) + + if "PYTEST_VERSION" in os.environ: + # Bit-exact, ugly but deterministic + face.load_char( + chr(i), + freetype.FT_LOAD_RENDER + | freetype.FT_LOAD_MONOCHROME + | freetype.FT_LOAD_NO_HINTING + | freetype.FT_LOAD_NO_AUTOHINT, + ) + + # FreeType mono bitmaps are 1 bit per pixel, packed + bitmap = face.glyph.bitmap + buf = np.asarray(bitmap.buffer, dtype=np.uint8) + bits = np.unpackbits(buf).reshape(bitmap.rows, bitmap.pitch * 8) + src = bits[:, : bitmap.width].astype(np.float32) + if src.size == 1: + src = np.zeros((0, 0), dtype=np.float32) + + sampler = Sampler( + magFilter=GL_NEAREST, + minFilter=GL_NEAREST, + wrapS=GL_CLAMP_TO_EDGE, + wrapT=GL_CLAMP_TO_EDGE, + ) + else: + # Normal, pretty rendering + face.load_char(chr(i)) + + bitmap = face.glyph.bitmap + src = np.asarray(bitmap.buffer, dtype=np.float32) / 255.0 + src = src.reshape((bitmap.rows, bitmap.width)) + + sampler = Sampler( + magFilter=GL_LINEAR, + minFilter=GL_LINEAR, + wrapS=GL_CLAMP_TO_EDGE, + wrapT=GL_CLAMP_TO_EDGE, + ) + tex = Texture( - sampler=Sampler( - magFilter=GL_LINEAR, minFilter=GL_LINEAR, wrapS=GL_CLAMP_TO_EDGE, wrapT=GL_CLAMP_TO_EDGE - ), + sampler=sampler, source=src, source_channels="R", ) + character = Character( texture=tex, - size=np.array([face.glyph.bitmap.width, face.glyph.bitmap.rows]), - bearing=np.array([face.glyph.bitmap_left, face.glyph.bitmap_top]), + size=np.array([bitmap.width, bitmap.rows], dtype=np.int32), + bearing=np.array([face.glyph.bitmap_left, face.glyph.bitmap_top], dtype=np.int32), advance=face.glyph.advance.x, ) + self._character_map[chr(i)] = character self._vbo = None @@ -248,7 +284,7 @@ def render_string(self, text, x, y, scale=1.0, align=TextAlign.BOTTOM_LEFT): glBindBuffer(GL_ARRAY_BUFFER, self._vbo) glBufferData(GL_ARRAY_BUFFER, FLOAT_SZ * 6 * 4, vertices, GL_DYNAMIC_DRAW) - # TODO MAKE THIS MORE EFFICIENT, lgBufferSubData is broken + # TODO MAKE THIS MORE EFFICIENT, glBufferSubData is broken # glBufferSubData( # GL_ARRAY_BUFFER, 0, 6 * 4 * FLOAT_SZ, # np.ascontiguousarray(vertices.flatten) diff --git a/genesis/ext/pyrender/viewer.py b/genesis/ext/pyrender/viewer.py index e3d331e199..a10ac20538 100644 --- a/genesis/ext/pyrender/viewer.py +++ b/genesis/ext/pyrender/viewer.py @@ -59,6 +59,7 @@ pyglet.options["shadow_window"] = False +pyglet.options["dpi_scaling"] = "real" MODULE_DIR = os.path.dirname(__file__) @@ -366,9 +367,7 @@ def __init__( ####################################################################### # Initialize OpenGL context and renderer ####################################################################### - self._renderer = Renderer( - self._viewport_size[0], self._viewport_size[1], context.jit, self.render_flags["point_size"] - ) + self._renderer = Renderer(*self._viewport_size, context.jit, self.render_flags["point_size"]) self._is_active = True # Starting the viewer would raise an exception if the OpenGL context is invalid for some reason. This exception @@ -654,12 +653,18 @@ def on_close(self): super().close() except Exception: pass - finally: + try: super().on_close() - try: - pyglet.app.exit() - except Exception: - pass + except Exception: + pass + try: + pyglet.app.exit() + except Exception: + pass + try: + pyglet.app.platform_event_loop.stop() + except Exception: + pass self._offscreen_semaphore.release() @@ -788,8 +793,8 @@ def on_resize(self, width: int, height: int) -> EVENT_HANDLE_STATE: self._viewport_size = (width, height) self._trackball.resize(self._viewport_size) - self._renderer.viewport_width = self._viewport_size[0] - self._renderer.viewport_height = self._viewport_size[1] + self._renderer.viewport_width = width + self._renderer.viewport_height = height self.on_draw() def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> EVENT_HANDLE_STATE: @@ -998,9 +1003,9 @@ def _render(self, camera_node=None, renderer=None, normal=False): elif self.render_flags["all_solid"]: flags |= RenderFlags.ALL_SOLID - if self.render_flags["shadows"]: + if self.render_flags["shadows"] and not self._is_software: flags |= RenderFlags.SHADOWS_ALL - if self.render_flags["plane_reflection"]: + if self.render_flags["plane_reflection"] and not self._is_software: flags |= RenderFlags.REFLECTIVE_FLOOR if self.render_flags["env_separate_rigid"]: flags |= RenderFlags.ENV_SEPARATE @@ -1149,12 +1154,16 @@ def start(self, auto_refresh=True): confs.insert(0, conf) raise + # Determine if software emulation is being used + glinfo = self.context.get_info() + renderer = glinfo.get_renderer() + self._is_software = any(e in renderer for e in ("llvmpipe", "Apple Software Renderer")) + # Run the entire rendering pipeline first without window, to make sure that all kernels are compiled self.refresh() - # At this point, we are all set to display the graphical window if requested - if not pyglet.options["headless"]: - self.set_visible(True) + # At this point, we are all set to display the graphical window + self.set_visible(True) # Run the entire rendering pipeline once again, as a final validation that everything is fine self.refresh() @@ -1202,6 +1211,13 @@ def start(self, auto_refresh=True): if not self._initialized_event.is_set(): self._initialized_event.set() + gs.logger.debug(f"Using interactive viewer OpenGL device: {renderer}") + if self._is_software: + gs.logger.info( + "Software rendering context detected. Shadows and plane reflection not supported. Beware rendering " + "will be extremely slow." + ) + if auto_refresh: while self._is_active: try: diff --git a/genesis/options/sensors/options.py b/genesis/options/sensors/options.py index 0d40449a78..562ccdaf40 100644 --- a/genesis/options/sensors/options.py +++ b/genesis/options/sensors/options.py @@ -226,15 +226,15 @@ class IMU(RigidSensorOptionsMixin, NoisySensorOptionsMixin, SensorOptions): mag_random_walk : tuple[float, float, float] The standard deviation of the bias drift for each axis of the magnetometer. debug_acc_color : float, optional - The rgba color of the debug acceleration arrow. Defaults to (0.0, 1.0, 1.0, 0.5). + The rgba color of the debug acceleration arrow. Defaults to (1.0, 0.0, 0.0, 0.6). debug_acc_scale: float, optional The scale factor for the debug acceleration arrow. Defaults to 0.01. debug_gyro_color : float, optional - The rgba color of the debug gyroscope arrow. Defaults to (1.0, 1.0, 0.0, 0.5). + The rgba color of the debug gyroscope arrow. Defaults to (0.0, 1.0, 0.0, 0.6). debug_gyro_scale: float, optional The scale factor for the debug gyroscope arrow. Defaults to 0.01. debug_mag_color : float, optional - The rgba color of the debug magnetometer arrow. Defaults to (1.0, 1.0, 0.0, 0.5). + The rgba color of the debug magnetometer arrow. Defaults to (0.0, 0.0, 1.0, 0.6). debug_mag_scale: float, optional The scale factor for the debug magnetometer arrow. Defaults to 0.01. """ @@ -261,12 +261,12 @@ class IMU(RigidSensorOptionsMixin, NoisySensorOptionsMixin, SensorOptions): mag_random_walk: MaybeTuple3FType = 0.0 magnetic_field: MaybeTuple3FType = (0.0, 0.0, 0.5) - debug_acc_color: tuple[float, float, float, float] = (0.0, 1.0, 1.0, 0.5) + debug_acc_color: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.6) debug_acc_scale: float = 0.01 - debug_gyro_color: tuple[float, float, float, float] = (1.0, 1.0, 0.0, 0.5) + debug_gyro_color: tuple[float, float, float, float] = (0.0, 1.0, 0.0, 0.6) debug_gyro_scale: float = 0.01 - debug_mag_color: tuple[float, float, float, float] = (0.0, 0.0, 1.0, 0.5) - debug_mag_scale: float = 2.0 + debug_mag_color: tuple[float, float, float, float] = (0.0, 0.0, 1.0, 0.6) + debug_mag_scale: float = 0.5 def model_post_init(self, _): self._validate_cross_axis_coupling(self.acc_cross_axis_coupling) diff --git a/genesis/vis/viewer.py b/genesis/vis/viewer.py index e5f64e7cae..4f2383b274 100644 --- a/genesis/vis/viewer.py +++ b/genesis/vis/viewer.py @@ -137,10 +137,6 @@ def build(self, scene): gs.logger.info(f"Viewer created. Resolution: ~<{self._res[0]}×{self._res[1]}>~, max_FPS: ~<{self._max_FPS}>~.") - glinfo = self._pyrender_viewer.context.get_info() - renderer = glinfo.get_renderer() - gs.logger.debug(f"Using interactive viewer OpenGL device: {renderer}") - self._is_built = True def run(self): diff --git a/tests/test_render.py b/tests/test_render.py index 7bf621d70e..6cb20d711b 100644 --- a/tests/test_render.py +++ b/tests/test_render.py @@ -1036,17 +1036,21 @@ def test_draw_debug(renderer, show_viewer): @pytest.mark.parametrize("n_envs", [0, 2]) @pytest.mark.parametrize("renderer_type", [RENDERER_TYPE.RASTERIZER]) @pytest.mark.skipif(not IS_INTERACTIVE_VIEWER_AVAILABLE, reason="Interactive viewer not supported on this platform.") -def test_sensors_draw_debug(n_envs, renderer, png_snapshot): +def test_sensors_draw_debug(n_envs, renderer_type, renderer, png_snapshot): """Test that sensor debug drawing works correctly and renders visible debug elements.""" scene = gs.Scene( viewer_options=gs.options.ViewerOptions( camera_pos=(2.0, 2.0, 2.0), camera_lookat=(0.0, 0.0, 0.2), # Force screen-independent low-quality resolution when running unit tests for consistency - res=(640, 480), + res=(480, 320), # Enable running in background thread if supported by the platform run_in_thread=(sys.platform == "linux"), ), + vis_options=gs.options.VisOptions( + # Disable shadows systematically for Rasterizer because they are forcibly disabled on CPU backend anyway + shadow=(renderer_type != RENDERER_TYPE.RASTERIZER), + ), profiling_options=gs.options.ProfilingOptions( show_FPS=False, ), @@ -1143,19 +1147,13 @@ def test_sensors_draw_debug(n_envs, renderer, png_snapshot): if renderer == "Apple Software Renderer": pytest.xfail("Tile ground colors are altered on Apple Software Renderer.") - try: - assert rgb_array_to_png_bytes(rgb_arr) == png_snapshot - except AssertionError: - # TODO: Need to investigate root cause and either fix rendering consistency - if sys.platform == "linux" and gs.use_ndarray: - pytest.xfail("Sensor debug drawing produces inconsistent results on Linux with static array mode.") - raise + assert rgb_array_to_png_bytes(rgb_arr) == png_snapshot @pytest.mark.required @pytest.mark.parametrize("renderer_type", [RENDERER_TYPE.RASTERIZER]) @pytest.mark.skipif(not IS_INTERACTIVE_VIEWER_AVAILABLE, reason="Interactive viewer not supported on this platform.") -def test_interactive_viewer_key_press(tmp_path, monkeypatch, renderer, png_snapshot): +def test_interactive_viewer_key_press(renderer_type, tmp_path, monkeypatch, renderer, png_snapshot): IMAGE_FILENAME = tmp_path / "screenshot.png" # Mock 'get_save_filename' to avoid poping up an interactive dialog @@ -1180,7 +1178,8 @@ def on_key_press(self, symbol: int, modifiers: int): # Create a scene scene = gs.Scene( viewer_options=gs.options.ViewerOptions( - # Force screen-independent low-quality resolution when running unit tests for consistency + # Force screen-independent low-quality resolution when running unit tests for consistency. + # Still, it must be large enough since rendering text involved alpha blending, which is platform-dependent. res=(640, 480), # Enable running in background thread if supported by the platform. # Note that windows is not supported because it would trigger the following exception if some previous tests @@ -1188,6 +1187,10 @@ def on_key_press(self, symbol: int, modifiers: int): # 'EventLoop.run() must be called from the same thread that imports pyglet.app'. run_in_thread=(sys.platform == "linux"), ), + vis_options=gs.options.VisOptions( + # Disable shadows systematically for Rasterizer because they are forcibly disabled on CPU backend anyway + shadow=(renderer_type != RENDERER_TYPE.RASTERIZER), + ), renderer=renderer, show_viewer=True, show_FPS=False, @@ -1242,7 +1245,11 @@ def test_camera_gimbal_lock_singularity(renderer, show_viewer): """ # Minimal scene - scene = gs.Scene(renderer=renderer, show_viewer=False, show_FPS=False) + scene = gs.Scene( + renderer=renderer, + show_viewer=show_viewer, + show_FPS=False, + ) cam = scene.add_camera(pos=(0.0, -1.5, 5.0), lookat=(0.0, 0.0, 0.0)) scene.build() diff --git a/tests/utils.py b/tests/utils.py index 50cf63c700..bc612cccbd 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -37,7 +37,7 @@ DEFAULT_BRANCH_NAME = "main" HUGGINGFACE_ASSETS_REVISION = "ca29b66018b449a37738257a3a76a78529d29bcc" -HUGGINGFACE_SNAPSHOT_REVISION = "0cf1780dd70b67dc426023cd97738037f0d834e3" +HUGGINGFACE_SNAPSHOT_REVISION = "5b9bb5f0752afa691e04a50d0f1a189cde3ecb38" MESH_EXTENSIONS = (".mtl", *MESH_FORMATS, *GLTF_FORMATS, *USD_FORMATS) IMAGE_EXTENSIONS = (".png", ".jpg")