diff --git a/pyrender/constants.py b/pyrender/constants.py index 8a5785b..de44a94 100644 --- a/pyrender/constants.py +++ b/pyrender/constants.py @@ -54,6 +54,8 @@ class RenderFlags(object): FLAT = 4096 """Render the color buffer flat, with no lighting computations.""" SEG = 8192 + """Return a float32 color buffer instead of default uint8.""" + COLOR_FLOAT32 = 16384 class TextAlign: diff --git a/pyrender/offscreen.py b/pyrender/offscreen.py index 3401429..004b309 100644 --- a/pyrender/offscreen.py +++ b/pyrender/offscreen.py @@ -106,7 +106,7 @@ def render(self, scene, flags=RenderFlags.NONE, seg_node_map=None): if flags & RenderFlags.DEPTH_ONLY: retval = depth else: - color = self._renderer.read_color_buf() + color = self._renderer.read_color_buf(flags) retval = color, depth # Make the platform not current diff --git a/pyrender/renderer.py b/pyrender/renderer.py index f212907..6371b28 100644 --- a/pyrender/renderer.py +++ b/pyrender/renderer.py @@ -218,25 +218,37 @@ def render_text(self, text, x, y, font_name='OpenSans-Regular', # Draw text font.render_string(text, x, y, scale, align) - def read_color_buf(self): + def read_color_buf(self, flags=RenderFlags.NONE): """Read and return the current viewport's color buffer. Alpha cannot be computed for an on-screen buffer. Returns ------- - color_im : (h, w, 3) uint8 + color_im : (h, w, 3) uint8 COLOR_FLOAT32 flag is not set in flags, otherwise float32 The color buffer in RGB byte format. """ # Extract color image from frame buffer width, height = self.viewport_width, self.viewport_height glBindFramebuffer(GL_READ_FRAMEBUFFER, 0) glReadBuffer(GL_FRONT) - color_buf = glReadPixels(0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE) - - # Re-format them into numpy arrays - color_im = np.frombuffer(color_buf, dtype=np.uint8) - color_im = color_im.reshape((height, width, 3)) + if flags & RenderFlags.RGBA: + glformat = GL_RGBA + npshape = (height, width, 4) + else: + glformat = GL_RGB + npshape = (height, width, 3) + if flags & RenderFlags.COLOR_FLOAT32: + gltype = GL_FLOAT + npdtype = np.float32 + else: + gltype = GL_UNSIGNED_BYTE + npdtype = np.uint8 + color_buf = glReadPixels( + 0, 0, width, height, glformat, gltype, + ) + color_im = np.frombuffer(color_buf, dtype=npdtype) + color_im = color_im.reshape(npshape) color_im = np.flip(color_im, axis=0) # Resize for macos if needed @@ -1009,7 +1021,7 @@ def _configure_forward_pass_viewport(self, flags): # If using offscreen render, bind main framebuffer if flags & RenderFlags.OFFSCREEN: - self._configure_main_framebuffer() + self._configure_main_framebuffer(flags) glBindFramebuffer(GL_DRAW_FRAMEBUFFER, self._main_fb_ms) else: glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0) @@ -1051,7 +1063,7 @@ def _delete_shadow_framebuffer(self): if self._shadow_fb is not None: glDeleteFramebuffers(1, [self._shadow_fb]) - def _configure_main_framebuffer(self): + def _configure_main_framebuffer(self, flags): # If mismatch with prior framebuffer, delete it if (self._main_fb is not None and self.viewport_width != self._main_fb_dims[0] or @@ -1065,7 +1077,7 @@ def _configure_main_framebuffer(self): glBindRenderbuffer(GL_RENDERBUFFER, self._main_cb) glRenderbufferStorage( - GL_RENDERBUFFER, GL_RGBA, + GL_RENDERBUFFER, GL_RGBA32F if flags & RenderFlags.COLOR_FLOAT32 else GL_RGBA, self.viewport_width, self.viewport_height ) @@ -1090,7 +1102,7 @@ def _configure_main_framebuffer(self): self._main_cb_ms, self._main_db_ms = glGenRenderbuffers(2) glBindRenderbuffer(GL_RENDERBUFFER, self._main_cb_ms) glRenderbufferStorageMultisample( - GL_RENDERBUFFER, 4, GL_RGBA, + GL_RENDERBUFFER, 4, GL_RGBA32F if flags & RenderFlags.COLOR_FLOAT32 else GL_RGBA, self.viewport_width, self.viewport_height ) glBindRenderbuffer(GL_RENDERBUFFER, self._main_db_ms) @@ -1172,17 +1184,22 @@ def _read_main_framebuffer(self, scene, flags): # Read color if flags & RenderFlags.RGBA: - color_buf = glReadPixels( - 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE - ) - color_im = np.frombuffer(color_buf, dtype=np.uint8) - color_im = color_im.reshape((height, width, 4)) + glformat = GL_RGBA + npshape = (height, width, 4) else: - color_buf = glReadPixels( - 0, 0, width, height, GL_RGB, GL_UNSIGNED_BYTE - ) - color_im = np.frombuffer(color_buf, dtype=np.uint8) - color_im = color_im.reshape((height, width, 3)) + glformat = GL_RGB + npshape = (height, width, 3) + if flags & RenderFlags.COLOR_FLOAT32: + gltype = GL_FLOAT + npdtype = np.float32 + else: + gltype = GL_UNSIGNED_BYTE + npdtype = np.uint8 + color_buf = glReadPixels( + 0, 0, width, height, glformat, gltype, + ) + color_im = np.frombuffer(color_buf, dtype=npdtype) + color_im = color_im.reshape(npshape) color_im = np.flip(color_im, axis=0) # Resize for macos if needed diff --git a/tests/unit/test_offscreen.py b/tests/unit/test_offscreen.py index 88983b0..c5c55f1 100644 --- a/tests/unit/test_offscreen.py +++ b/tests/unit/test_offscreen.py @@ -1,8 +1,9 @@ import numpy as np import trimesh +import pytest from pyrender import (OffscreenRenderer, PerspectiveCamera, DirectionalLight, - SpotLight, Mesh, Node, Scene) + SpotLight, Mesh, Node, Scene, RenderFlags) def test_offscreen_renderer(tmpdir): @@ -83,10 +84,22 @@ def test_offscreen_renderer(tmpdir): _ = scene.add(cam, pose=cam_pose) r = OffscreenRenderer(viewport_width=640, viewport_height=480) - color, depth = r.render(scene) + color_u8, depth = r.render(scene) + r.delete() - assert color.shape == (480, 640, 3) + assert color_u8.shape == (480, 640, 3) + assert color_u8.dtype == np.uint8 assert depth.shape == (480, 640) assert np.max(depth.data) > 0.05 assert np.count_nonzero(depth.data) > (0.2 * depth.size) + + # render in floating point + r = OffscreenRenderer(viewport_width=640, viewport_height=480) + color_f32, depth = r.render(scene, RenderFlags.COLOR_FLOAT32) r.delete() + assert color_f32.shape == (480, 640, 3) + assert color_f32.dtype == np.float32 + + delta = np.abs(color_f32*255 - color_u8.astype(np.float32)) + assert np.percentile(delta, 99.9) <= 1 + assert np.max(delta) < 3